Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您使用 Spring Integration 创建一个简单的应用程序,该应用程序从 RSS 源(Spring 博客)检索数据,处理数据,然后将其写入文件。本指南使用传统的 Spring Integration XML 配置。其他指南展示了如何使用 Java 配置和 DSL(带或不带 Lambda 表达式)。

你将构建什么

您将使用传统的 XML 配置来创建一个 Spring Integration 流程。

您需要什么

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,或者跳过您已经熟悉的基本设置步骤。无论哪种方式,最终您都将获得可运行的代码。

从头开始,请继续阅读 从 Spring Initializr 开始

跳过基础部分,请执行以下操作:

完成后,您可以在 gs-integration/complete 中查看代码以核对结果。

从 Spring Initializr 开始

您可以使用这个预初始化项目,然后点击生成以下载一个 ZIP 文件。该项目已配置为适配本教程中的示例。

要手动初始化项目:

  1. 访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Spring Integration

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置好的 Web 应用程序的归档文件。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此过程。

您也可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。

添加到构建文件

对于这个示例,您需要添加两个依赖项:

  • spring-integration-feed

  • spring-integration-file

以下清单展示了最终的 pom.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>integration-complete</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>integration-complete</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-feed</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-file</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

下面的清单展示了最终的 build.gradle 文件:

plugins {
    id 'org.springframework.boot' version '3.3.0'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-integration'
    implementation 'org.springframework.integration:spring-integration-feed'
    implementation 'org.springframework.integration:spring-integration-file'
    testImplementation('org.springframework.boot:spring-boot-starter-test')
    testImplementation 'org.springframework.integration:spring-integration-test'
}

test {
    useJUnitPlatform()
}

定义集成流程

在本指南的示例应用程序中,您将定义一个 Spring Integration 流,它将:

  • 从 spring.io 的 RSS 订阅源读取博客文章。
  • 将它们转换为由文章标题和文章 URL 组成的易于阅读的 String
  • 将该 String 追加到文件 (/tmp/si/SpringBlog) 的末尾。

要定义一个集成流,您可以创建一个 Spring XML 配置文件,并使用 Spring Integration 的 XML 命名空间中的一些元素。具体来说,对于所需的集成流,您需要处理来自 Spring Integration 的以下命名空间的元素:core、feed 和 file。(这就是为什么我们需要修改 Spring Initializr 提供的构建文件的原因。)

以下 XML 配置文件(来自 src/main/resources/integration/integration.xml)定义了集成流:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:int="http://www.springframework.org/schema/integration"
    xmlns:file="http://www.springframework.org/schema/integration/file"
    xmlns:feed="http://www.springframework.org/schema/integration/feed"
    xsi:schemaLocation="http://www.springframework.org/schema/integration/feed https://www.springframework.org/schema/integration/feed/spring-integration-feed.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/integration/file https://www.springframework.org/schema/integration/file/spring-integration-file.xsd
        http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd">

    <feed:inbound-channel-adapter id="news" url="https://spring.io/blog.atom" auto-startup="${auto.startup:true}">
        <int:poller fixed-rate="5000"/>
    </feed:inbound-channel-adapter>

    <int:transformer
            input-channel="news"
            expression="payload.title + ' @ ' + payload.link + '#{systemProperties['line.separator']}'"
            output-channel="file"/>

    <file:outbound-channel-adapter id="file"
            mode="APPEND"
            charset="UTF-8"
            directory="/tmp/si"
            filename-generator-expression="'${feed.file.name:SpringBlog}'"/>

</beans>

这里有三个集成元素在起作用:

  • <feed:inbound-channel-adapter>: 一个入站适配器,用于检索帖子,每次轮询获取一个。如这里配置的那样,它每五秒轮询一次。帖子被放置到名为 news 的通道中(对应于适配器的 ID)。

  • <int:transformer>: 转换 news 通道中的条目(com.rometools.rome.feed.synd.SyndEntry),提取条目标题(payload.title)和链接(payload.link),并将它们连接成一个可读的 String(并添加换行符)。然后将 String 发送到名为 file 的输出通道。

  • <file:outbound-channel-adapter>: 一个出站通道适配器,将来自其通道(名为 file)的内容写入文件。具体来说,如这里配置的那样,它将 file 通道中的任何内容追加到 /tmp/si/SpringBlog 文件中。

下图展示了这个简单的流程:

读取RSS订阅条目的流程

暂时忽略 auto-startup 属性。我们稍后在讨论测试时会重新提到它。现在请注意,它的默认值是 true,这意味着应用程序启动时会获取帖子。另外,请注意 filename-generator-expression 中的属性占位符。它表示默认值为 SpringBlog,但可以通过属性进行覆盖。

使应用程序可执行

虽然通常在更大的应用程序(甚至可能是Web应用程序)中配置Spring Integration流是很常见的,但这并不意味着它不能在一个更简单的独立应用程序中定义。接下来您要做的是:创建一个主类来启动集成流,并声明一些bean来支持集成流。您还将把应用程序构建成一个独立的可执行JAR文件。我们使用Spring Boot的@SpringBootApplication注解来创建应用程序上下文。由于本指南使用XML命名空间来定义集成流,您必须使用@ImportResource注解将其加载到应用程序上下文中。以下清单(来自src/main/java/com/example/integration/IntegrationApplication.java)展示了应用程序文件:

package com.example.integration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication
@ImportResource("/integration/integration.xml")
public class IntegrationApplication {
  public static void main(String[] args) throws Exception {
    ConfigurableApplicationContext ctx = new SpringApplication(IntegrationApplication.class).run(args);
    System.out.println("Hit Enter to terminate");
    System.in.read();
    ctx.close();
  }

}

构建可执行的 JAR

您可以使用 Gradle 或 Maven 从命令行运行该应用程序。您还可以构建一个包含所有必要依赖项、类和资源的可执行 JAR 文件并运行它。构建可执行 JAR 文件使得在整个开发生命周期中、跨不同环境等情况下,方便地打包、版本控制和部署该服务成为可能。

如果您使用 Gradle,可以通过 ./gradlew bootRun 来运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后按如下方式运行 JAR 文件:

java -jar build/libs/gs-integration-0.1.0.jar

如果您使用 Maven,可以通过 ./mvnw spring-boot:run 来运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后运行该 JAR 文件,如下所示:

java -jar target/gs-integration-0.1.0.jar

这里描述的步骤创建了一个可运行的 JAR 文件。您也可以构建一个经典的 WAR 文件

运行应用程序

现在,您可以通过运行以下命令来从 jar 文件启动应用程序:

java -jar build/libs/{project_id}-0.1.0.jar

... app starts up ...

应用程序启动后,它会连接到 RSS 订阅源并开始获取博客文章。应用程序通过您定义的集成流处理这些文章,最终将文章信息附加到 /tmp/si/SpringBlog 文件。

应用程序运行一段时间后,您应该能够查看 /tmp/si/SpringBlog 文件,以查看一些文章的数据。在基于 UNIX 的操作系统上,您还可以通过运行以下命令来 tail 文件,实时查看写入的结果:

tail -f /tmp/si/SpringBlog

您应该会看到类似以下的示例输出(尽管实际新闻内容会有所不同):

Spring Integration Java DSL 1.0 GA Released @ https://spring.io/blog/2014/11/24/spring-integration-java-dsl-1-0-ga-released
This Week in Spring - November 25th, 2014 @ https://spring.io/blog/2014/11/25/this-week-in-spring-november-25th-2014
Spring Integration Java DSL: Line by line tutorial @ https://spring.io/blog/2014/11/25/spring-integration-java-dsl-line-by-line-tutorial
Spring for Apache Hadoop 2.1.0.M2 Released @ https://spring.io/blog/2014/11/14/spring-for-apache-hadoop-2-1-0-m2-released

测试

检查 complete 项目,您将会看到一个测试用例,位于 src/test/java/com/example/integration/FlowTests.java 中:

package com.example.integration;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;

import com.rometools.rome.feed.synd.SyndEntryImpl;

@SpringBootTest({ "auto.startup=false",   // we don't want to start the real feed
          "feed.file.name=Test" })   // use a different file
public class FlowTests {

  @Autowired
  private SourcePollingChannelAdapter newsAdapter;

  @Autowired
  private MessageChannel news;

  @Test
  public void test() throws Exception {
    assertThat(this.newsAdapter.isRunning()).isFalse();
    SyndEntryImpl syndEntry = new SyndEntryImpl();
    syndEntry.setTitle("Test Title");
    syndEntry.setLink("http://characters/frodo");
    File out = new File("/tmp/si/Test");
    out.delete();
    assertThat(out.exists()).isFalse();
    this.news.send(MessageBuilder.withPayload(syndEntry).build());
    assertThat(out.exists()).isTrue();
    BufferedReader br = new BufferedReader(new FileReader(out));
    String line = br.readLine();
    assertThat(line).isEqualTo("Test Title @ http://characters/frodo");
    br.close();
    out.delete();
  }

}

该测试使用 Spring Boot 的测试支持将名为 auto.startup 的属性设置为 false。依赖网络连接进行测试通常不是一个好主意,尤其是在 CI 环境中。因此,我们阻止了 feed 适配器的启动,并将一个 SyndEntry 注入到 news 通道中,以便由流程的其余部分进行处理。测试还设置了 feed.file.name,以便测试写入到一个不同的文件中。然后,它:

  • 验证适配器是否已停止。

  • 创建一个测试 SyndEntry

  • 删除测试输出文件(如果存在)。

  • 发送消息。

  • 验证文件是否存在。

  • 读取文件并验证数据是否符合预期。

总结

恭喜!您已经开发了一个简单的应用程序,使用 Spring Integration 从 spring.io 获取博客文章,处理它们,并将它们写入文件。

另请参阅

以下指南可能也会有所帮助:

本页目录