SpringCloud-数据流教程-全-
SpringCloud 数据流教程(全)
一、云和大数据
数字世界由大约 440 亿字节的数据组成。一个 zettabyte 是 100 万 Pb,或 10 亿 TB,或 1 万亿 GB。2019 年,谷歌处理了大约 370 万次查询,YouTube 记录了 450 万次观看视频,脸书每 60 秒注册 100 万次登录。想象一下计算机处理所有这些请求、数据接收和数据操作的能力。常识告诉我们,大型 IT 公司使用大量硬件来保存数据。需要整合大量存储以防止容量限制。
IT 公司如何应对数据过载、成本上升或技能缺口等挑战?近年来,大型 IT 公司投入巨资开发战略,使用企业数据仓库(EDW)作为中央数据系统,从不同来源报告、提取、转换和加载(ETL)流程。如今,用户和设备(恒温器、灯泡、安全摄像头、咖啡机、门铃、座椅传感器等。)摄取数据。
戴尔、英特尔和 Cloudera 等公司携手打造硬件和存储解决方案,帮助其他公司发展壮大,变得更快、更具可扩展性。
一点数据科学
当我们谈论数据科学时,一个拥有博士学位的科学家团队浮现在脑海中。他们可能挣大钱,他们不休息,因为公司依赖他们。一个数据科学家的实际教育经历是怎样的?
几年前,计算杂志披露,随着 Hadoop、Kafka、Hive、Pig、Cassandra、D3 和 Tableau 等工具的加入,Spark 和 Scala 在希望应用数据科学的公司中迅速发展。
Python 已经成为机器学习技术的主要编程语言之一,与 R、Scala 和 Java 齐名。
机器学习通常在商业、数学、计算机科学和通信领域发挥作用。数据科学家使用数据进行预测、分类、推荐、模式检测和分组、异常检测、识别、可操作的洞察、自动化流程、决策、评分和排名、细分、优化和预测。太多了!
图 1-1。
数据科学
我们需要合适的工具、平台、基础设施和软件工程知识来创新和创造。机器学习应该依赖于一种感觉舒适、易学的编程语言(比如 Python)。平台应该有合适的引擎来处理数据。基础设施应该是可靠的、安全的和冗余的。开发技术应该创造出令人敬畏的企业解决方案,不仅使公司受益,而且使全世界的用户受益。
云
在过去的十年里,许多公司已经进入了所谓的云,或者他们是云本地人,或者他们是在云计算时代;但这到底意味着什么呢?有几家公司表示,他们一直都在云中,因为他们的所有服务都在公司之外,由第三方管理,而且如果发生停机,他们的响应速度会更快。但这准确吗?还是云意味着通过互联网启用服务器、网络、存储、开发工具和应用的架构计算?
在我看来,我们可以通过公共云环境访问互联网,用户可以通过 ?? 互联网连接随时随地“接入”数据和应用。我认为云是一种新的测量服务,采用随用随付的模式,您只需为您正在使用的任何服务器、网络、存储、带宽、应用或更多服务付费——非常类似于电力或水务公司根据消耗量收费。
我还将云视为一种按需自助服务 。您可以请求这些服务中的任何一项,只需点击几下鼠标,它们就会很快被提供。
我可以将云视为一种多租户模型,其中应用、网络或服务器的单个实例由多个用户共享。这被称为共享资源池。一旦您使用完它,它将返回到池中等待另一个用户请求它。
我可以将云视为一个弹性平台,其中的资源可以根据需要快速伸缩(见图 1-2 )。
图 1-2。
云计算
云技术和基础设施
我认为今天的云技术意味着公司可以快速扩展和适应。他们可以加速创新,更有效地推动业务灵活性,满怀信心地简化运营,并降低成本以更好地与其他公司竞争。这导致公司持续增长。如今,在技术方法上更具战略性的公司在财务上做得更好,但这些公司如何看待新的云技术?
亚马逊(按需计算的先驱)、谷歌和微软等大型 IT 公司都提供云技术。这些公司获得了丰厚的报酬,为公司提供云基础架构,提供弹性、托管服务、按需计算和存储、网络等。
实施云基础架构需要存储、服务器或虚拟机。还需要托管服务、混合运营以及数据安全和管理。这些服务允许公司将他们的数据用于所有这些新的机器学习工具,并将新的人工智能算法应用于系统分析,以帮助欺诈检测,帮助决策,这是数据处理的几个不断增长的功能(见图 1-3 )。
图 1-3。
云基础设施
合适的工具
在我 20 年的经验中,我见过大公司使用工具和技术来帮助他们以正确的方式使用收集的数据,并遵循数据操作的最佳实践和标准。由于所有新的需求和服务需求增加的方式,公司雇用知道如何使用诸如 JMS、RabbitMQ、Kinesis、Kafka、NATs、ZeroMQ、ActiveMQ、Google PubSub 等工具的人。我们看到随着这些技术出现了更多的消息模式,例如事件驱动或数据驱动模式(见图 1-4 )。这些模式并不新鲜,但直到现在才受到重视。
图 1-4。
数据驱动型企业
像 Apache Hadoop 这样的技术跨集群分布大型数据集。Apache Flume 是一个简单灵活的数据流架构,也是一个收集、聚合和移动大量日志数据的服务。Apache Sqoop 是一个批处理工具,用于在 Apache Hadoop 和结构化数据存储(如关系数据库)之间传输批量数据;它解决了你需要做的一些数据争论。
新一波编程语言可以处理大量数据。这些语言包括 R、Python 和 Scala 等语言,以及一系列用于机器学习和专家系统的库和框架,如 MadLib(见图 1-5 和 1-6 )。
图 1-6。
数据流
图 1-5。
数据流
消息传递代理的新协议每天都会出现。我们应该学习所有这些新技术吗?或者我们应该雇佣具备所有这些技能的人吗?我认为我们至少应该有一种技术来处理通信。好了,我们做到了:SpringCloudStream 和编制器,SpringCloud 数据流(见图 1-7 )。
我将讨论这两种技术。如果你是 Spring 开发者,你不需要学习任何新的消息传递 APIs 你可以使用你已经知道的东西——Java 和 Spring。如果您不熟悉 Spring,在接下来的两章中,我将快速浏览一下 Spring Boot、Spring Integration 和 Spring Batch,并向您展示如何使用它们。这三项技术是 SpringCloudStream 和 SpringCloud 数据流的核心(见图 1-7 )。
接下来,您将创建您的第一个流式应用,它可以连接而不考虑消息传递代理。没错;在多个流式应用之间设置哪个代理并不重要。SpringCloudStream 有这个能力。您将开发定制流并创建一个定制绑定器,允许您对消息传递代理隐藏任何 API。
最后,我谈谈 Spring CloudStream 及其组件,以及如何创建应用、流和任务并监控它们(见图 1-7 )。
图 1-7。
数据流:SpringCloud 数据流
摘要
在这一章中,我谈到了大数据和使用提供开箱即用解决方案的云基础架构来改善服务的新方法。每家公司都需要有知名度、速度、快速进入市场的能力,以及做出反应的时间。
在这短短的一章中,我想为这本书设定背景。在接下来的章节中,我将讨论帮助您使用大数据创建企业级解决方案的技术。
二、Spring Boot
构建云原生应用的一种方法是遵循十二因素应用指南( https://12factor.net
),这些指南有助于在任何云环境中运行应用。其中一些原则,如依赖声明(第二因素)、配置(第三因素)和端口绑定(第七因素),等等,都是 Spring Boot 支持的!Spring Boot 是一个微服务和云就绪框架。
为什么是 Spring Boot 而不仅仅是 Spring?或者是另一种技术,比如 NodeJS 或 Go 语言?Spring Boot 是一项无与伦比的技术,因为它由 Java 社区中最常用的框架提供支持,并允许您轻松创建企业级应用。其他语言需要您进行大量的手动设置和编码。Spring Boot 为你提供了它。即使像 NodeJS 这样的技术有数百个库,但在我看来,它在企业级别上比不上 Spring。别误会我。我并不是说其他技术不好或者没用,但是如果你想构建一个快速、细粒度的企业应用,只有 Spring Boot 提供了最少的配置和代码。让我们看看为什么 Spring Boot 很重要,以及它如何帮助您创建云原生应用。
什么是 Spring 框架,什么是 Spring Boot?
Spring Boot 是下一代的 Spring 应用。它是一种固执己见的运行时技术,公开了创建企业级 Spring 应用的最佳实践。
Spring 框架
让我们稍微倒退一下,谈谈 Spring 框架。使用 Spring 框架,您可以创建细粒度的企业应用,但您需要知道它如何工作,最重要的是,如何配置它。配置是 Spring 框架的关键元素之一。您可以将定制实现、DB 连接和对外部服务的调用解耦,从而使 Spring Framework 更具可扩展性、更易于维护和运行。在某种程度上,你需要知道所有适用于 Spring 应用的最佳实践。让我们从一个简单的 Spring 应用开始,它演示了 Spring 框架是如何工作的。
目录应用
假设您需要创建一个 Spring 应用来保存人们的联系信息,比如姓名、电子邮件和电话号码。这是一个基本的目录应用,它在任何数据库引擎中公开了一个具有持久性的 REST API,并且可以部署在任何兼容的 J2EE 服务器中。下面是创建这样一个应用的步骤。
-
安装一个像 Maven 或者 Gradle 这样的构建工具,编译构建源代码的目录结构。如果你有 Java 背景,你应该知道你需要一个 WEB-INF 目录结构。
-
创建
web.xml
和application-context.xml
文件。web.xml
文件有org.springframework.web.servlet.DispatcherServlet
类,它充当基于 Spring 的 web 应用的前端控制器。 -
添加一个指向
application-context.xml
文件的监听器类,在这里声明所有的 Spring beans 或应用所需的任何其他配置。如果省略侦听器部分,您需要将 Spring beans 声明文件命名为与DispatcherServlet
相同的名称。 -
在
application-context.xml
文件,
中添加几个 Spring beans 部分来覆盖每个细节。如果使用 JDBC,需要添加一个数据源、init SQL 脚本和一个事务管理器。如果您正在使用 JPA,您需要添加一个 JPA 声明(一个persistence.xml
文件,您可以在其中配置您的类和主要单元)和一个实体管理器来处理会话并与事务管理器通信。 -
因为这是一个 web app,所以需要在
application-context.xml
文件中添加一些关于 HTTP 转换器的 Spring beans 部分,这些转换器公开 JSON 视图和 MVC 驱动的注释来使用@RestController
和@RequestMapping
(或者@GetMapping、@PostMapping、@DeleteMapping 等。)以及其他 Spring MVC 注释。 -
如果您使用 JPA(最简单的持久化方法),用
@EnableJpaRepositories
注释指定存储库和类的位置。 -
要运行该应用,请以 WAR 格式打包您的应用。您需要安装一个符合 J2EE 标准的应用服务器,然后测试它。
如果你是一个有经验的 Spring 开发者,你知道我在说什么。如果你是一个新手,那么你需要学习所有的语法。这不是太难,但你需要花一些时间在这上面。或者也许有另一种方法。当然有。您可以使用基于注释的配置或 JavaConfig 类来设置 Spring beans,也可以混合使用两者。最后,你需要学习一些 Spring 注释来帮助你配置这个应用。你可以在这本书的网站上查看源代码(ch02/directory-jpa
)。
让我们回顾一下这个应用的一些代码。记住,你需要创建一个 Java web 结构(见图 2-1 )。
图 2-1。
基于 Java 网络的目录结构
图 2-1 显示了一个基于 Java 网络的目录结构。可以删除index.jsp
文件,打开web.xml
文件,全部替换为清单 2-1 所示的内容。
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/application-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Listing 2-1.web.xml
清单 2-1 向您展示了如何添加一个 Spring servlet ( DispatcherServlet
,一个前端控制器模式),它是处理来自用户的任何请求的主 servlet。
接下来,让我们通过添加清单 2-2 中的内容来创建application-context.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:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven />
<tx:annotation-driven />
<jpa:repositories base-package="com.apress.spring.directory.repository" entity-manager-factory-ref="localContainerEntityManagerFactoryBean" />
<context:component-scan base-package="com.apress.spring.directory" />
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
<bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
<property name="objectMapper" ref="xmlMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="indentOutput" value="true"/>
<property name="modulesToInstall" value="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>
</bean>
<bean id="xmlMapper" parent="objectMapper">
<property name="createXmlMapper" value="yes"/>
</bean>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="mediaTypes">
<value>
json=application/json
xml=application/xml
</value>
</property>
</bean>
<bean id="localContainerEntityManagerFactoryBean"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.hbm2ddl.auto">create-drop</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
</props>
</property>
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:mem:testdb" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="localContainerEntityManagerFactoryBean" />
</bean>
<bean id="persistenceExceptionTranslationPostProcessor" class=
"org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<jdbc:embedded-database type="H2" >
<jdbc:script location="classpath:META-INF/sql/schema.sql"/>
<jdbc:script location="classpath:META-INF/sql/data.sql"/>
</jdbc:embedded-database>
</beans>
Listing 2-2.application-context.xml
清单 2-2 显示了application-context.xml
文件,在该文件中,您为 Spring 容器添加了所有必要的配置,所有的类都是在这个容器中初始化和连接的。
在回顾每一个标记和它的声明方式之前,看看你是否能猜出每一个标记是做什么的,以及为什么它是这样配置的。看看声明之间的命名和引用。
如果你是 Spring 新手,推荐你看看 Apress 出版的 Pro Spring 系列。这些书解释了配置 Spring 的这种声明形式的每个方面。
接下来,分别添加以下类:Person
、PersonRepository
和PersonController
(参见清单 2-3 、 2-4 和 2-5 )。
package com.apress.spring.directory.domain;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Person {
@Id
private String email;
private String name;
private String phone;
public Person() {
}
public Person(String email, String name, String phone) {
this.email = email;
this.name = name;
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
Listing 2-3.com.apress.spring.directory.domain.Person.java
清单 2-3 展示了使用所有 JPA (Java Persistence API)注释的Person
类,所以很容易使用,没有更多直接的 JDBC。
package com.apress.spring.directory.repository;
import com.apress.spring.directory.domain.Person;
import org.springframework.data.repository.CrudRepository;
public interface PersonRepository extends CrudRepository<Person,String> {
}
Listing 2-4.com.apress.spring.directory.repository.PersonRepository.java
清单 2-4 显示了从另一个CrudRepository
接口扩展而来的PersonRepository
接口。在这里,它使用 Spring Data 和 Spring Data JPA 的所有功能来创建一个基于实体类及其主键(在本例中是一个String
类型)的存储库模式。换句话说,不需要创建任何 CRUD 实现——让 Spring Data 和 Spring Data JPA 来处理。
package com.apress.spring.directory.controller;
import com.apress.spring.directory.domain.Person;
import com.apress.spring.directory.repository.PersonRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@Controller
public class PersonController {
private Logger log = LoggerFactory.getLogger(PersonController.class);
private PersonRepository personRepository;
public PersonController(PersonRepository personRepository) {
this.personRepository = personRepository;
}
@RequestMapping(value = "/people",
method = RequestMethod.GET,
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
@ResponseBody
public Iterable<Person> getPeople() {
log.info("Accessing all Directory people...");
return personRepository.findAll();
}
@RequestMapping(value = "/people",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
public ResponseEntity<?> create(UriComponentsBuilder uriComponentsBuilder, @RequestBody Person person) {
personRepository.save(person);
UriComponents uriComponents =
uriComponentsBuilder.path("/people/{id}").buildAndExpand(person.getEmail());
return ResponseEntity.created(uriComponents.toUri()).build();
}
@RequestMapping(value = "/people/search",
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
public ResponseEntity<?> findByEmail(@RequestParam String email) {
log.info("Looking for {}", email);
return ResponseEntity.ok(personRepository.findById(email).orElse(null));
}
@RequestMapping(value = "/people/{email:.+}",
method = RequestMethod.DELETE)
@ResponseBody
public ResponseEntity<?> deleteByEmail(@PathVariable String email) {
log.info("About to delete {}", email);
personRepository.deleteById(email);
return ResponseEntity.accepted().build();
}
}
Listing 2-5.com.apress.spring.directory.controller.PersonController.java
清单 2-5 显示了任何用户请求/响应的PersonController
类的实现。在 Spring 中实现一个 web 控制器有很多方法,比如使用@RestController
。避免在每个方法中写@ResponseBody
,或者使用类似@GetMapping, @PostMapping,
和@DeleteMapping
的专用注释来代替@RequestMapping
。
接下来,创建初始化数据库的 SQL 文件(参见清单 2-6 和 2-7 )。
CREATE TABLE person (
email VARCHAR(100) NOT NULL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
);
Listing 2-6.META-INF/sql/schema.sql
清单 2-6 是一个简单的模式,只包含一个表。
INSERT INTO person (email,name,phone) VALUES('mark@email.com','Mark','1-800-APRESS');
INSERT INTO person (email,name,phone) VALUES('steve@email.com','Steve','1-800-APRESS');
INSERT INTO person (email,name,phone) VALUES('dan@email.com','Dan','1-800-APRESS');
Listing 2-7.META-INF/sql/data.sql
清单 2-7 显示了应用启动时要插入的一些记录。接下来因为这个 app 用的是 JPA,所以需要提供一个persistence.xml
文件。还有另一个选择——您可以向application-context.xml
添加一个 bean 声明,并声明 JPA 引擎工作所需的持久性单元(参见清单 2-8 )。
<persistence xmlns:="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.2"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<!-- Define persistence unit -->
<persistence-unit name="directory">
</persistence-unit>
</persistence>
Listing 2-8.META-INF/persistence.xml
清单 2-8 显示了声明持久性单元所需的 JPA 文件。您可以在localContainerEntityManagerFactoryBean
bean 声明中将其声明为一个属性(persistenceUnitName
属性)。
接下来是最重要的文件之一。这个应用是使用 Maven 作为构建工具创建的。让我们在项目的根目录下创建一个pom.xml
文件(参见清单 2-9 )。
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.apress.spring</groupId>
<artifactId>directory-jpa</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>directory-web-project Maven Webapp</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
<servlet.version>3.1.0</servlet.version>
<spring-framework.version>5.2.2.RELEASE</spring-framework.version>
<spring-data.jpa>2.2.3.RELEASE</spring-data.jpa>
<slf4j.version>1.7.25</slf4j.version>
<logback.version>1.2.3</logback.version>
<h2>1.4.199</h2>
<jackson>2.10.1</jackson>
</properties>
<dependencies>
<!-- Spring Core/MVC/Web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${spring-data.jpa}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Other Web dependencies -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- JPA -->
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>1.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>2.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>1.3.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.9.Final</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
<groupId>org.jboss.spec.javax.transaction</groupId>
</exclusion>
<exclusion>
<artifactId>javax.activation-api</artifactId>
<groupId>javax.activation</groupId>
</exclusion>
<exclusion>
<artifactId>javax.persistence-api</artifactId>
<groupId>javax.persistence</groupId>
</exclusion>
<exclusion>
<artifactId>jaxb-api</artifactId>
<groupId>javax.xml.bind</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- JSON/XML -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>${jackson}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>directory-jpa</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Listing 2-9.pom.xml
清单 2-9 显示了 pom.xml 文件,其中声明了所有的依赖项。如果你来自 J2EE 背景,你可能会发现这很难,因为你需要找到一种与他人相处融洽的依赖关系。这可能需要一点时间。
接下来,您需要打包应用。安装 Maven (
mvn package
该命令打包应用并生成target/directory-jpa.war
文件。要运行应用,您需要一个可以运行 J2EE 应用的应用服务器;最常见的是 Tomcat。从 https://tomcat.apache.org/download-90.cgi
下载版本 9,然后解压并将directory-jpa.war
部署/复制到webapps/
Tomcat 文件夹中。要启动 Tomcat 服务器,使用bin/
文件夹中的脚本。看一下脚本。要启动 Tomcat,通常需要执行名为startup.sh
(对于 Unix 用户)或startup.bat
(对于 Windows 用户)的脚本。
您可以使用cUrl
命令行或任何其他 GUI 应用来测试您的应用,比如 Postman ( www.getpostman.com
),来执行所有的请求。例如,要查看目录中列出的所有人,请执行以下命令。
$ curl http://localhost:8080/directory-jpa/people -H "Content-Type: application/json"
您应该得到以下输出。
[ {
"email" : "mark@email.com",
"name" : "Mark",
"phone" : "1-800-APRESS"
}, {
"email" : "steve@email.com",
"name" : "Steve",
"phone" : "1-800-APRESS"
}, {
"email" : "dan@email.com",
"name" : "Dan",
"phone" : "1-800-APRESS"
} ]
正如你所看到的,这里有很多步骤,我遗漏了你需要添加到应用中的部分业务逻辑。一个训练有素的 Spring 开发人员可能要花三个小时来交付这个应用,而且他们一半以上的时间都花在了配置应用上。如何加快这一配置过程?在配置上花费太多时间会导致错误和不良行为。
Note
请记住,您可以访问本书的配套源代码。可以从 Apress ( www.apress.com
)下载。本例的文件夹在ch02/directory-jpa
中。
Spring Boot
Spring Boot 来救援了!Spring 工程师意识到 Spring 开发人员对每个 Spring 应用都遵循相同的步骤,所以他们想出了一个更好的方法来进行配置和添加最佳实践。他们创造了 Spring Boot,这是一个固执己见的引擎,它为你设定了最佳实践,让你可以专注于代码。
Spring Boot 提供了许多功能,这些功能将在后面讨论。现在,让我们回到目录应用。
Spring Boot 的目录应用
让我们从使用 Spring Initializr 创建结构开始。打开浏览器,指向 https://start.spring.io
(见图 2-2 和 2-3 )。
图 2-2。
图 2-2 是 Spring Initializr 的截图。将以下数据添加到字段中。
-
组:
com.apress.boot
-
神器:
directory
-
依赖项:Spring Web、Spring Data JPA、Rest 存储库和 H2 数据库
您可以保留其他选项的默认值。单击生成按钮。在任何可以运行 Java 应用的 IDE 中打开 ZIP 文件。我推荐 IntelliJ 社区版( www.jetbrains.com/idea/download/
)或者微软的可视代码配合适的插件运行 Java 和 Spring 的应用( https://code.visualstudio.com/download
)。
花点时间分析一下结构。调用清单 2-3 ( Person.java
)和清单 2-4 ( PersonRepository.java
)中的类以及清单 2-7 ( data.sql
)中的文件。您可以将Person
和PersonRepository
类添加到com.apress.boot.directory
文件夹/包中,将data.sql
文件添加到资源文件夹中(参见图 2-3 )。
图 2-3。
智能:Spring Boot 目录应用结构
图 2-3 显示了 Spring Boot 应用的结构和类的位置。注意这里有额外的文件,包括mvnw*
脚本和一个隐藏的.mvn
文件夹。这是 Maven 的一个瘦包装器,意味着你不需要安装 Maven。您可以从命令行运行它。
你完了!你不需要做任何其他事情。使用 IDE 或命令行运行应用。在运行应用之前,请确保停止 Tomcat。然后,您可以从终端运行该应用,方法是转到directory
文件夹并运行以下命令。
$ ./mvnw spring-boot:run
上述命令仅在 Unix 系统上运行。如果您使用 Windows 操作系统,则执行以下操作来运行您的应用。
> mvnw.bat spring-boot:run
然后你可以如下执行一个cUrl
命令。
$ curl http://localhost:8080/persons -H "Content-Type: application/json
您会得到相同的结果—一个人员列表。一个有经验的 Spring 或 Spring Boot 开发者可以在 5 分钟内创建这个应用!一个 Spring Boot 的新手通常需要 15 分钟来创建它。(看,根本没有配置)。Spring Boot 是一个自以为是的运行时,它会发现你的类路径中有什么,并基于此设置一些默认值以避免任何其他配置,如DispatcherServlet
设置、自定义HttpMessageConverters
、DB 初始化器和控制器。它使用 HATEOAS(作为应用状态引擎的超媒体)协议来生成响应,并添加了所有的 HTTP 方法实现。如果您想添加一个新人,使用cUrl
执行以下命令。
$ curl -XPOST http://localhost:8080/persons -H "Content-Type: application/json" -d '{"email":"mike@email.com","name":"Mike","phone":"1-800-APRESS"}'
Spring Boot 是如何变魔术的?如果你想更多地了解 Spring Boot,以及这种配置是如何毫不费力地完成的,我推荐阅读我的一些其他书籍,如 Pro Spring Boot 2 (Apress,2019)。
您可以使用 Spring 和 Spring Boot 的强大功能轻松创建现成的应用。
超越目录应用示例
通常,书籍以一个必做的 HelloWorld 示例或一个简单的应用(如目录应用)开始。现在,让我们通过创建一个使用来自 Twitter feed 的流处理的微服务来见证 Spring Boot 的力量。在这一节中,我给出了构建这个应用的一步一步的说明。
首先,在 https://developer.twitter.com
登录 Twitter 开发者计划。您需要获得四个密钥:消费者 API 密钥、消费者 API 秘密密钥、访问令牌密钥和访问令牌秘密密钥(参见图 2-4 )。
图 2-4。
https://developer.twitter.com/
钥匙和令牌
接下来打开一个浏览器,指向 https://start.spring.io
(见图 2-5 )。使用以下信息。
图 2-5。
https://start.spring.io
推特 app
-
组:
com.apress.boot
-
神器:
twitter
-
依赖性:无
点击生成按钮下载一个 ZIP 文件。解压缩文件并在任何 IDE 中打开项目。创建几个类,你应该有最终的应用,如图 2-6 所示。
图 2-6。
Twitter 应用
接下来,打开pom.xml
文件并添加清单 2-10 中所示的依赖项。
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-twitter</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
Listing 2-10.pom.xml spring-social-twitter dependency
接下来,创建TwitterProperties
类(参见清单 2-11 )。
package com.apress.boot.twitter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "twitter")
public class TwitterProperties {
private String consumerKey;
private String consumerSecret;
private String accessToken;
private String accessTokenSecret;
public String getConsumerKey() {
return consumerKey;
}
public void setConsumerKey(String consumerKey) {
this.consumerKey = consumerKey;
}
public String getConsumerSecret() {
return consumerSecret;
}
public void setConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getAccessTokenSecret() {
return accessTokenSecret;
}
public void setAccessTokenSecret(String accessTokenSecret) {
this.accessTokenSecret = accessTokenSecret;
}
}
Listing 2-11.com.apress.boot.twitter.TwitterProperties.java
清单 2-11 显示了保存 Twitter API 工作所需密钥的TwitterProperties
类。所有这些属性都在application.properties
文件中声明,它有twitter
前缀。
接下来,创建TwitterConfig
类(参见清单 2-12 )。
package com.apress.boot.twitter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.twitter.api.impl.TwitterTemplate;
@Configuration
@EnableConfigurationProperties(TwitterProperties.class)
public class TwitterConfig {
private TwitterProperties twitterProperties;
public TwitterConfig(TwitterProperties twitterProperties){
this.twitterProperties = twitterProperties;
}
@Bean
TwitterTemplate twitterTemplate(){
return new TwitterTemplate(twitterProperties.getConsumerKey(),
twitterProperties.getConsumerSecret(), twitterProperties.getAccessToken(), twitterProperties.getAccessTokenSecret());
}
}
Listing 2-12.com.apress.boot.twitter.TwitterContig.java
清单 2-12 显示了TwitterConfig
类,该类具有创建TwitterTemplate
实例的初始化配置。该实例处理所需的连接和密钥交换。这是一种非常简单的方法,可以登录 Twitter 并进行一些操作,比如阅读或创建推文。
接下来,创建TwitterStart
类(参见清单 2-13 )。
package com.apress.boot.twitter;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.twitter.api.*;
import java.util.Collections;
@Configuration
public class TwitterStart {
@Bean
CommandLineRunner start(Twitter twitter){
return args -> {
twitter.streamingOperations().sample(Collections.singletonList(new StreamListener() {
@Override
public void onTweet(Tweet tweet) {
tweet.getEntities().getHashTags().forEach(hashTagEntity -> {
System.out.println(String.format("#%s",hashTagEntity.getText()));
});
}
@Override
public void onDelete(StreamDeleteEvent streamDeleteEvent) { }
@Override
public void onLimit(int i) { }
@Override
public void onWarning(StreamWarningEvent streamWarningEvent) { }
}));
};
}
}
Listing 2-13.com.apress.boot.twitter.TwitterStart.java
清单 2-13 显示了TwitterStart
类。一旦 Spring Boot 完成配置和设置,这个类就执行start
方法。这里一个重要的细节是StreamListener
的用法,它监听 Twitter 上公开的每一个标签。这个程序唯一做的事情就是打印标签。
接下来,将密钥添加到application.properties
文件中(参见清单 2-14 )。
# Twitter Properties
twitter.consumerKey=YbJisGFe9Jo3lAFE30wYR08To
twitter.consumerSecret=Slm9EJYOTFunnw5YWm13Px3HH6jQGDt2NJp8N4DyjhmIv2HtZK
twitter.accessToken=64771614-9RjhlWVy5h6PKhvQbCjm7rt2BcB66ZVEJwZ7DAPCN
twitter.accessTokenSecret=oRluin2ZMgNOKHUP0JSJc6HMEjul2QC6aeSAV4DBKW8uz
Listing 2-14.application.properties
请记住,这些键来自您的 Twitter 开发人员门户应用。现在,您可以使用 Maven 包装器运行应用。
$ ./mvnw spring-boot:run
你应该看到一堆实时发送到 Twitter 的标签!
如您所见,创建一个快速、细粒度的企业应用非常容易。这就是 Spring Boot 的魅力。
Spring Boot 特色
Spring Boot 的特色如此之多,以至于需要一整本书来描述它们。但是,我可以在本节中描述其中的一些。
-
SpringApplication
类提供了一种启动 Spring 应用的便捷方式。它在主类中。 -
Spring Boot 允许您创建没有任何 XML 配置的应用。它不做代码生成。
-
Spring Boot 通过
SpringApplicationBuilder
singleton 类提供了一个流畅的构建器 API,允许你创建具有多个应用上下文的层次结构。这个特性与 Spring 框架有关。我将在接下来的章节中解释这个特性,但是如果您是 Spring 和 Spring Boot 的新手,您只需要知道您可以扩展 Spring Boot 来获得对您的应用的更多控制。 -
Spring Boot 提供了更多配置 Spring 应用事件和监听器的方法。这将在接下来的章节中解释。
-
Spring Boot 是一种固执己见的技术。该特性试图创建正确类型的应用,既可以是 web 应用(通过嵌入 Tomcat、Netty、Undertow 或 Jetty 容器),也可以是单个应用。
-
通过
org.springframework.boot.ApplicationArguments
接口,Spring Boot 允许访问任何应用参数。当您使用参数运行应用时,这是一个非常有用的特性。 -
Spring Boot 允许您在应用启动后执行代码。您唯一需要做的就是实现
CommandLineRunner
接口和run(String ...args)
方法。一个特殊的例子是在启动时初始化数据库中的一些记录,或者在应用启动前检查一些服务以查看它们是否正在运行。 -
Spring Boot 允许您通过使用
application.properties
或application.yml
文件来具体化配置。在接下来的章节中会有更多的介绍。 -
您可以添加与管理相关的功能,通常通过 JMX,在
application.properties
或application.yml
文件中启用spring.application.admin.enabled
属性。 -
Spring Boot 允许您拥有配置文件,帮助您的应用在不同的环境中运行。
-
Spring Boot 允许您非常简单地配置和使用日志记录。
-
Spring Boot 通过使用 starter poms 提供了一种配置和管理依赖项的简单方法。换句话说,如果你想创建一个 web 应用,你只需要在你的 Maven
pom.xml
或build.gradle
文件中包含spring-boot-starter-web
依赖项。 -
Spring Boot 通过使用 Spring Boot 致动器提供开箱即用的非功能性要求,该致动器具有新的测微计平台无关框架,允许您对您的应用进行仪器化。
-
Spring Boot 提供了
@Enable<feature>
注释,帮助您启用、包含、配置和使用数据库(例如,SQL 和 NoSQL)、Spring Integration 和云,以及缓存、调度、消息、批处理等等。
我将在每一章中讨论其他特性。
摘要
在这一章中,我向你展示了什么是 Spring Boot 以及你可以用它做什么。这是 Spring Cloud Stream 和 Spring Cloud Data Flow 为其组件使用的主要技术。我还向您展示了 Spring Boot 为开发人员提供的一些特性的例子。
在下一章中,我将向您展示创建 Spring CloudStream 的另一个技术基础。也不用担心所有的新术语;它们很快就会让你明白。
三、Spring Integration
如果你搜索单词 communication 的意思,你会发现它来自拉丁语 communicare,,意思是“分享”今天, communication 这个词已经变得越来越强大,不仅用于那些想要分享思想、想法和问题的人,而且也用于技术领域。企业应用需要使用消息作为常规通信协议,在组件或外部系统之间共享数据。
在这一章中,你将了解到 Spring 框架最好的项目之一——Spring Integration!它提供了一种简单的、开箱即用的方式来使用企业集成模式。
通过使用轻量级消息传递,Spring Integration 支持通过声明性适配器与外部系统集成。换句话说,您可以连接 JMS (IBM MQ 和 Tibco,以及其他代理)、AMQP、套接字或 UDP 消息,然后处理、转换并交付到新系统中,如 REST API 端点、NoSQL 持久性或 RabbitMQ 消息传递,最多只需几行代码。
集成模式
Spring Integration 支持企业集成模式 ( www.eaipatterns.com
)。本章只介绍了几个模式来帮助你理解它们是如何工作的,以及 Spring Integration 是如何促进它们的使用的,因为它是 Spring CloudStream 的一个重要部分。剧透预警:Spring CloudStream 基于 Spring Integration!
企业集成模式(EIP)符合几种模式(见图 3-1);请记住,设计模式是对反复出现的问题的解决方案。
图 3-1。
企业集成模式
您可以看到每个模式在特定业务逻辑/规则中所扮演的角色的清晰划分。最重要的部分是消息和消息传输。
信息发送
为什么信息传递如此重要?当你创建一个解决方案时,你脑海中有一个基本的场景(见图 3-2 )。这是需要发送到输入的数据,然后是处理,最后是输出到最终阶段(打印)或发送到另一个系统。
图 3-2。
输入-处理-输出
如果你考虑在你的解决方案中调用一个特定的逻辑,你是在调用一个方法(输入),传递一些基于数据的参数(数据)。你执行一些逻辑(过程),然后返回(输出)一个结果(数据)。
现在从信息传递的角度考虑一下。您通过本地进程或向远程服务器发送消息。您的消息可以被处理、增强或触发另一个事件,然后您得到一个结果,可能是您发送的同一条消息,或者是告诉您一切正常的消息,或者可能有一个错误。所有这些通过本地或远程系统的数据都需要通过通道进行传输。渠道在传递信息的方式中扮演着重要的角色。
正如你所看到的,信息在交流中起着重要的作用。
Spring Integration
通过使用在本地或远程系统之间通信的通道或其他通道来应用消息传递。它为您提供了集成消息传递组件以创建可伸缩解决方案的参考,并展示了一些现有的消息传递技术(JMS、TIBCO、SOAP、MSMQ、NET 等)。)以及如何整合它们。如果你想更多地了解 EIP,我推荐 Gregor Hohpe 和 Bobby Woolf(Addison-Wesley Professional,2003)的企业集成模式:设计、构建和部署消息传递解决方案。这本书提供了每种模式的全面解释。
在这一节中,我将向您展示 Spring Integration 如何工作以及如何使用它。通过向您展示一个小型集成应用的例子,您可以更好地了解 Spring Integration 是怎么一回事。
电影应用规范要求
让我们创建一个包含两部分的电影应用。
假设您在一个文本文件(CSV 格式)中获得了一些关于新电影的信息(标题、演员、年份),您需要将这些信息发送到一个只接受 JSON 格式的外部和远程系统。这个远程系统有一个 REST API 服务,它接受您的集合(/v1/movies
)。收到信息后,它将信息保存到数据库(MySQL)中,并向管理员用户发送电子邮件(参见图 3-3 了解大致情况)。
图 3-3。
电影集成应用
电影应用第一部分
以下是电影应用的规格/要求。
-
获取 CSV 文件格式的新电影信息
-
将 CSV 内容转换成 JSON 格式
-
需要并行执行以下操作
-
将电影信息保存到文件系统路径中(文件名需要有一个
.processed
后缀;例如action-movies.txt > action-movies.txt.processed
-
通过调用 REST API 将 JSON 格式的电影信息发送到外部服务器
-
电影应用:第二部分(外部)
以下是应用外部系统部分的规格/要求。
-
以 REST API 服务为特色:
/v1/movies
获取 JSON 格式的电影集的信息 -
将新的电影信息存储到 MySQL 中
-
存储电影信息后发送电子邮件
这个例子非常简单,但是足以向您展示 Spring Integration 是如何工作的。
创建电影应用:第一部分
让我们从创建应用的框架开始。转到 https://start.spring.io
并创建一个项目。将其导出到任何 IDE 中(参见图 3-4 )。或者,使用 STS IDE ( https://spring.io/tools
),单击文件➤新➤ Spring Starter 项目,然后按照向导进行操作。
图 3-4。
https://start.spring.io
的 Spring
图 3-4 显示了 Spring Initializr 主页。您可以添加以下值。
-
组:
com.apress.integration
-
神器:
movie
-
依赖性:Spring Integration
点击生成项目按钮。解压缩项目,并使用任何 IDE 导入它。
检查您的项目结构,确保您有清单 3-1 中所示的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>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.apress.integration</groupId>
<artifactId>movie</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>movie</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Spring Integration - File -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
<!-- Spring Integration - Http -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>
Listing 3-1.The pom.xml File for the Movie App Project
列表 3-1 是你应该有的最终pom.xml that
。您会看到将 CSV 行转换成 object/JSON 所需的spring-boot-starter-integration
JSON(Jackson-databind)依赖项、处理文件所需的spring-integration-file
依赖项以及将 JSON 发送到外部系统所需的spring-integration-http
依赖项。
制作 Spring Boot 应用的最佳特性之一是能够以不同的方式配置 Spring 上下文。您可以通过 XML beans 文件使用声明式编程,也可以通过用@Configuration
注释您的类并使用@Bean
注释声明您的 bean 来使用 JavaConfig 类。根据您的应用,您可以避免任何配置。Spring Boot 有一个自以为是的运行时,所以它试图找出你正在运行的应用的类型,通过使用它的自动配置和查看类路径,它尽最大努力创建正确的应用类型。
在接下来的小节中,我将向您展示如何使用声明性 XML 创建 Spring Integration 应用。我认为这是学习这种特殊技术的最好方法,因为它比使用 JavaConfig 类更具可读性(在我看来);此外,通过使用声明性 XML,您可以利用 IDE 的强大功能来生成集成图,以可视化企业集成模式是如何工作的。
电影应用:声明性 XML
让我们从使用 JavaConfig 类开始。首先,用清单 3-2 中的内容添加一个名为MovieConfiguration.java
的新类。
package com.apress.integration.movie;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource("META-INF/spring/movie-app-integration.xml")
public class MovieConfiguration {
}
Listing 3-2.src/main/java/com/apress/integration/movie/MovieConfiguration.java
@ ImportResource
注释导入 XML 文件。这些文件是 Spring 配置的一部分。当使用声明式 XML 时,我认为 Spring Integration 是配置它的最佳方式。别担心。我将在下一节解释如何创建 XML 文件。
通过声明性 XML 的 Spring Integration
接下来,您需要在src/main/resources/META-INF/spring/movie-app-integration.xml
中创建一个文件。您还需要创建目录结构(参见清单 3-3 )。
<?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-file="http://www.springframework.org/schema/integration/file"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- Spring Integration -->
<int-file:inbound-channel-adapter channel="input" directory="target/input" filename-pattern="*.txt">
<int:poller fixed-rate="500"/>
</int-file:inbound-channel-adapter>
<!-- Spring Integration: Direct Channel -->
<int:channel id="input"/>
<!-- Spring Integration: Service Activator -->
<int:service-activator id="movieProcessor" input-channel="input" ref="movieEndpoint" />
</beans>
Listing 3-3.src/main/resources/META-INF/spring/movie-app-integration.xml
清单 3-3 显示了第一部分的 Spring 配置文件。要使用 Spring 配置,需要在 XML 中导入几个名称空间。以下是重要的名称空间。
-
xmlns:int
-
xmlns:int-file
-
xmlns:int-http
这些名称空间包含几个标签描述。使用最多的标签之一是<int:channel />
。这个标签创建了一个通道作为生产者和消费者之间的通信点,生产者通过这个通道发送消息,消费者接收消息。使用通道的好处之一是,它确保消息到达目的地,并将生产者和消费者分离开来。与图 3-2 相同的过程如图 3-5 所示。
图 3-5。
有通道的输入-处理-输出
那么,新的表述告诉了我们什么?
-
输入和输出的符号定义了进行点对点或发布-订阅模式通信的通道。该示例使用直接通道,这是一种点对点模式。
-
流程符号定义了一个服务激活器,它是接收到的消息的处理器。通过输入通道接收消息,如果需要,服务激活器可以通过输出通道返回消息。
Spring Integration 处理基于org.springframework.messaging.Message
类的内部消息格式。该接口定义如下。
public interface Message<T> {
/**
* Return the message payload.
*/
T getPayload();
/**
* Return message headers for the message (never {@code null} but may be empty).
*/
MessageHeaders getHeaders();
}
注意Message
是一个接口,有办法通过MessageBuilder
-fluent API 创建。
MessageBuilder.withPayload("Hello World").setHeader("myheader",
"Hi").setHeader("myotherheader","there").build();
这个例子使用了两个通道。
图 3-6a 和 3-6b。
inbound-channel-adapter
通道在左边,outbound-channel-adapter
通道在右边。
-
一个是名为
inbound-channel-adapter
的特殊通道,用图 3-6a 中的符号表示。 -
第二个通道命名为
outbound-channel-adapter
,用图 3-6b 中的符号表示。
两个频道与常规频道相同;唯一的区别是这些通道可以轮询源调用。
回到例子,让我们看看每一部分。
-
<int-file:inbound-channel-adapter />
。该通道每隔几秒钟轮询(监控)一次文件系统路径。以下是它的属性。-
directory="target/input"
表示频道正在监控目录中的任何新文件。 -
filename-pattern="*.txt"
只寻找文件。txt 扩展名。 -
channel="input"
表示一旦找到文件(. txt 文件),其内容将被发送到输入通道。 -
<int:poller fixed-rate="500"/>
是一个内部标签,表示每 500 毫秒监控一次。
-
-
<int:channel id="input"/>
。这个标签创建了一个准备接收文件内容的通道。通常,id 是最匹配的通道的名称,而不考虑其用途(输入或输出)。 -
<int:service-activator/>
。这个标签是文件内容的处理器。以下是它的属性。-
input-channel="input"
监听来自输入通道的任何消息。 -
ref="movieEndpoint"
使用名为 movieEndpoint 的 bean 来处理来自输入通道的消息。
-
如果你使用的是 Spring 工具套件,简称 STS IDE(https://spring.io/tools
,可以看到这个例子的 Spring Integration 图(见图 3-7 )。您可以用 IntelliJ 做同样的事情,但是您需要使用付费版本和 Spring Integration 模式插件。
图 3-7。
STS IDE 中的 Spring Integration 图
Spring Integration 开发
接下来,让我们创建Service Activator
类,它接收文本文件的内容。用清单 3-4 中的内容创建 MovieEndpoint.java 类。
package com.apress.spring.integration.movie;
import java.io.File;
import java.io.FileInputStream;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.annotation.MessageEndpoint
;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.util.StreamUtils;
@MessageEndpoint
public class MovieEndpoint {
private static final Logger log = LoggerFactory.getLogger(MovieEndpoint.class);
@ServiceActivator
public void process(File input, @Headers Map<String,Object> headers) throws Exception {
FileInputStream in = new FileInputStream(input);
String movies = new String(StreamUtils.copyToByteArray(in));
in.close();
log.info("Received: \n" + movies);
}
}
Listing 3-4.com.apress.integration.movie.MovieEndpoint.java
清单 3-4 显示了MovieEndpoint.java
服务激活器。让我们来看看每一部分。
-
@MessageEndpoint
是一个注释,它告诉 Spring Integration 该类必须被视为任何其他输出通道访问的端点。 -
@ServiceActivator
是对一个方法的注释,告诉 Spring Integration 该方法是消息到达和处理的入口点。以下是它的参数。-
File
是java.io.File
类。<int-file:inbound-channel-adapter />
标签自动发送 java.io.File
,它填写Message
接口中的所有头。 -
@Headers
是一个具有org.springframework.messaging.MessageHeaders
类头的注释。其中两个标题是id
和timestamp
。
-
-
process
方法是从<int:channel channel="input"/>
开始监听的切入点。
接下来,您需要创建一个包含电影的小文件来测试您的第一个 Spring Integration 应用——类似于下面的代码片段(就像电影标题、演员、年份一样简单)。
The Matrix, Keanu Reeves, 1999
Memento, Guy Pearce, 2000
The Silence of the Lambs, Jodie Foster, 1991
The Prestige, Christian Bale, 2006
Disturbia, Shia LaBeouf, 2007
将文件放入target/input
(在项目的根目录下)。当您使用 Maven 时,它会自动生成target
文件夹。您需要创建输入文件夹并将文件放在那里。
现在该运行应用了。打开一个新的终端,从根目录的项目中,执行下面的命令行。
$ ./mvn spring-boot:run
您应该会看到以下输出。
INFO 5395 --- [ask-scheduler-1] o.s.i.file.FileReadingMessageSource : Created message: [GenericMessage [payload=target/input/movies-batch1.txt, headers={timestamp=1437355862115, id=4fe905ee-8829-e3f2-df42-f5a7512635cd}]]
INFO 5395 --- [ask-scheduler-1] c.a.spring.integration.MovieEndpoint : Movies Received:
The Matrix, Keanu Reeves, 1999
Memento, Guy Pearce, 2000
The Silence of the Lambs, Jodie Foster, 1991
The Prestige, Christian Bale, 2006
Disturbia, Shia LaBeouf, 2007
成功了。如果您想创建一个行为来监控目录中的文件,您需要考虑可重用性和可扩展性。这种与文件系统的集成已经存在于 Spring Integration 中。
您需要将文件(CSV)的内容转换成 JSON 格式。我们来看看清单 3-5 。
package com.apress.integration.movie;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Component
public class MovieService {
private static final Logger log = LoggerFactory.getLogger(MovieService.class);
public String format(String contents){
log.info("Formatting to Json...");
String json = "{}";
ObjectMapper mapper = new ObjectMapper();
try {
json = mapper.writeValueAsString(parse(contents));
log.info("\n" + json);
} catch (IOException e) {
e.printStackTrace();
}
return json;
}
private List<Movie> parse(String contents){
List<Movie> movies = new ArrayList<Movie>();
String[] record = null;
for(String line: contents.split(System.getProperty("line.separator"))){
record = Arrays.asList(line.split(",")).stream().map( c -> c.trim()).toArray( size -> new String[size]);
movies.add(new Movie(record[0],record[1],Integer.valueOf(record[2])));
}
return movies;
}
}
Listing 3-5.com.apress.integration.movie.MovieService.java
清单 3-5 显示了用@Component,
注释了MovieService
类,使得这个类对 Spring 容器可见,这样您就可以在您的服务激活器中使用它。让我们看看每种方法。
-
format(String contents)
获取文件的内容并使用ObjectMapper
类(来自 Jackson 库)通过调用私有的parse
方法将它(来自电影列表)转换成 JSON 格式。 -
parse(String contents)
是获取文件内容的方法。它通过剥离每一行并(用逗号)分割成实际值来进行解析。它创建一个添加到数组列表中的Movie
对象。这个数组列表就是结果。这个方法使用 Java 8 streams 符号来避免值中有任何空格。清单 3-5 引入了一个
Movie
类,那么我们来看看这个类(参见清单 3-6 )。
package com.apress.integration.movie;
public class Movie {
private String title;
private String actor;
private int year;
public Movie(){}
public Movie(String title, String actor, int year){
this.title = title;
this.actor = actor;
this.year = year;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public String toString(){
StringBuilder builder = new StringBuilder();
builder.append("Movie(title: ");
builder.append(title);
builder.append(", actor: ");
builder.append(actor);
builder.append(", year: ");
builder.append(year);
builder.append(")");
return builder.toString();
}
}
Listing 3-6.com.apress.integration.movie.Movie.java
清单 3-6 是一个基本的电影 POJO,包含这些基本字段:标题、演员和年份。让我们在服务激活器中使用这个服务(参见清单 3-7 )。
package com.apress.integration.movie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.util.StreamUtils;
import java.io.File;
import java.io.FileInputStream;
import java.util.Map;
@MessageEndpoint
public class MovieEndpoint {
private static final Logger log = LoggerFactory.getLogger(MovieEndpoint.class);
@Autowired
private MovieService service;
@ServiceActivator
public void process(File input, @Headers Map<String,Object> headers) throws Exception {
FileInputStream in = new FileInputStream(input);
String movies = service.format(new String(StreamUtils.copyToByteArray(in)));
in.close();
log.info("Movies Received: \n" + movies);
}
}
Listing 3-7.com.apress.integration.movie.MovieEndpoint.java (version 2) Using MovieService
清单 3-7 是MovieEndpoint
类的版本 2,它使用了MovieService
。它使用了允许MovieService
被注入并准备使用的@Autowired
注释。如果您使用 Maven 或 STS IDE 运行应用,您应该会看到下面的输出。
INFO 5677 --- [ask-scheduler-1] com.apress.integration.movie.MovieService : Formatting to Json...
INFO 5677 --- [ask-scheduler-1] com.apress.integration.movie.MovieService :
[{"title":"The Matrix","actor":"Keanu Reeves","year":1999},{"title":"Memento","actor":"Guy Pearce","year":2000},{"title":"The Silence of the Lambs","actor":"Jodie Foster","year":1991},{"title":"The Prestige","actor":"Christian Bale","year":2006},{"title":"Disturbia","actor":"Shia LaBeouf","year":2007}]
这个输出是使用MovieService
的结果;它以 JSON 格式生成,这是该应用的要求之一。
下一个需求是将内容发送到一个target/output/<file>.txt.processed
格式的文件中。您可以在 service activator 中创建所有的逻辑,这样就大功告成了。但是对于使用 REST API 将文件发送到远程服务的需求呢?如果你考虑一下,你可以在同一个过程方法中在Service Activator
类中做这两个实现。但这需要时间,对吗?多亏了 Spring Integration,这个逻辑已经作为标签存在了!让我们从将处理过的文件发送到扩展名为.processed
的输出目录开始。
您需要修改movie-app-integration.xml
文件,看起来像清单 3-8 。
<?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-file="http://www.springframework.org/schema/integration/file"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- Spring Integration -->
<int-file:inbound-channel-adapter channel="input" directory="target/input" filename-pattern="*.txt">
<int:poller fixed-rate="500"/>
</int-file:inbound-channel-adapter>
<!-- Spring Integration: Direct Channel -->
<int:channel id="input"/>
<!-- Spring Integration: Service Activator -->
<int:service-activator id="movieProcessor" input-channel="input" ref="movieEndpoint" output-channel="output"/>
<!-- Spring Integration: Direct Channel -->
<int:channel id="output"/>
<!-- Spring Integration: File -->
<int-file:outbound-channel-adapter channel="output" directory="target/output" filename-generator-expression="headers['name'] + '.processed'" />
</beans>
Listing 3-8.movie-app-integration.xml (version 2)
清单 3-8 是movie-app-integration.xml
文件的版本 2。让我们看看有什么新的。
-
<int:service-activator />
有一个新属性:output-channel="output".
这个属性标识消息发出的通道;在这种情况下,频道的名称是"output"
。 -
<int:channel id="output"/>
创建新频道。这是从服务激活器发送消息的通道。 -
<int-file:outbound-channel-adapter />
获取内容(JSON 格式)并在指定目录下创建文件。使用了以下属性。-
channel="output"
设置收听输入内容的频道。 -
directory="target/output"
指定文件在目录中的位置。 -
filename-generator-expression="headers['name'] + '.processed'"
通过检查邮件头并添加后缀来生成文件名。
-
您需要再次修改服务激活器,因为它现在需要返回一些东西。在这种情况下,<int-file:outbound-channel-adapter/>
标签给你一个提示。您需要发送回一个Message
实例,至少包含新文件名的头(参见清单 3-9 )。
package com.apress.integration.movie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.StreamUtils;
import java.io.File;
import java.io.FileInputStream;
import java.util.Map;
@MessageEndpoint
public class MovieEndpoint {
private static final Logger log = LoggerFactory.getLogger(MovieEndpoint.class);
@Autowired
private MovieService service;
@ServiceActivator
public Message<String> process(File input, @Headers Map<String,Object> headers) throws Exception {
FileInputStream in = new FileInputStream(input);
String movies = service.format(new String(StreamUtils.copyToByteArray(in)));
in.close();
log.info("Sending the JSON content to a file...");
return MessageBuilder.withPayload(movies).setHeader("name",input.getName()).setHeader("Content-Type","application/json").build();
}
}
Listing 3-9.com.apress.integration.movie.MovieEndpoint.java (version 3): Returning a Message Instance
清单 3-9 是MovieService
的版本 3,从服务激活器返回一个值;在本例中,是一个org.springframework.messaging.Message<String>
接口的实例。此外,它还添加了包含名称和Content-Type
的头。这最后一个头对于 HTTP 请求(稍后出现)很有用。org.springframework.messaging.support.MessageBuilder
实用程序类构建消息。这个类提供了一个非常漂亮流畅的 API。
如果有 STS IDE,可以看到修改配置 XML 文件后的图形结果,如图 3-8 所示。
图 3-8。
Spring 积分图(第 2 版)
运行应用后,您应该得到和以前一样的输出,但是如果您看一下target/output
目录,您会发现一个新的文件名,movies.txt.processed
(如果您将文件命名为:movies.txt
)。内容是 JSON 格式的。
下一个需求是将这个文件发送到外部 REST API 服务。清单 3-10 是movie-app-integration.xml
的版本 3。
<?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-file="http://www.springframework.org/schema/integration/file"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- Spring Integration -->
<int-file:inbound-channel-adapter channel="input" directory="target/input" filename-pattern="*.txt">
<int:poller fixed-rate="500"/>
</int-file:inbound-channel-adapter>
<!-- Spring Integration: Direct Channel -->
<int:channel id="input"/>
<!-- Spring Integration: Service Activator -->
<int:service-activator input-channel="input" ref="movieEndpoint" output-channel="output"/>
<!-- Spring Integration: Direct Channel -->
<int:channel id="output"/>
<!-- Spring Integration: Router -->
<int:recipient-list-router input-channel="output">
<int:recipient channel="toFile" />
<int:recipient channel="toHttp"/>
</int:recipient-list-router>
<!-- Spring Integration: Direct Channels -->
<int:channel id="toFile"/>
<int:channel id="toHttp"/>
<!-- Spring Integration: File and Http -->
<int-file:outbound-channel-adapter channel="toFile" directory="target/output" filename-generator-expression="headers['name'] + '.processed'" />
<int-http:outbound-channel-adapter channel="toHttp" url="http://localhost:8080/v1/movies" http-method="POST"/>
</beans>
Listing 3-10.movie-app-integration.xml (version 3)
清单 3-10 是movie-app-integration.xml
文件的版本 3。让我们看看有什么新的。
-
<int:recipient-list-router />
是一个暴露路由器的新标签。这个例子使用output
通道将内容发送到toFile
和toHttp
通道。 -
<int:recipient/>
声明路由器要使用的通道。 -
<int:channel/>
表示有两个新的直接通道(点对点):toFile
和toHttp
。 -
<int-http:outbound-channel-adapter/>
向远程服务发出请求的新标签——您需要指向的 REST API(/v1/movies
)。
基于清单 3-10 ,你的曲线图应该类似于图 3-9 。
图 3-9。
Spring 积分图
运行应用会导致一些错误,因为它正在寻找http://localhost:8080/v1/movies
端点,而且还没有准备好;这是集成的下一部分。正如您所看到的,尝试实现需求的最后一部分非常耗时,但是 Spring Integration 已经拥有了这些可重用的模块(EIP 实现)。
创建电影应用:第二部分(外部)
这个应用是面向创建一个 REST API,做数据库插入,并发送电子邮件。您可以使用相同的 URL 来生成项目( http://start.spring.io
)或者使用 STS IDE 来生成相同的模板。
以下是该项目的新字段值(见图 3-10 )。
图 3-10。
spring Initializr athttps://start . spring . io
-
组:
com.apress.integration
-
神器:
movie-web
-
依赖:Spring Web,Spring Integration,Java 邮件发送器,JDBC API,H2 数据库
生成项目后,将其解压缩并导入到您喜欢的 IDE 中。您应该拥有清单 3-11 中所示的pom.xml
。(如有必要,请修改以匹配清单 3-11 。)
<?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>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.apress.integration</groupId>
<artifactId>movie-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>movie-web</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Integration -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mail</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Http -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>
Listing 3-11.pom.xml
清单 3-11 是您将要使用的pom.xml
。看一看 Spring Integration 和 JDBC 部分。你需要加上spring-integration-jdbc
、spring-integration-mail
、spring-integration-http,
和h2
。前两个依赖项添加了名称空间,以便在解决方案的这一部分使用特殊的标记。
Spring Boot MVC 电影应用第二部分
让我们看看使用 Spring MVC 组件有多容易。在本节中,您将创建com.apress.integration.movieweb.MovieController.java
类并重用Movie
类(参见清单 3-6 )。MovieController
类必须具有清单 3-12 中所示的内容。
package com.apress.integration.movieweb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MovieController {
private static final Logger log = LoggerFactory.getLogger(MovieController.class);
@RequestMapping(method=RequestMethod.POST,value="/v1/movie")
public ResponseEntity<String> movie(@RequestBody Movie body){
log.info("Movie: " + body);
return new ResponseEntity<String>(HttpStatus.ACCEPTED);
}
@RequestMapping(method=RequestMethod.POST,value="/v1/movies")
public ResponseEntity<String> movies(@RequestBody Movie[] body){
for (Movie movie: body){
log.info("Movie: " + movie);
}
return new ResponseEntity<String>(HttpStatus.ACCEPTED);
}
}
Listing 3-12.com.apress.integration.movieweb.MovieController.java
清单 3-12 显示了MovieController
类。由于其注释,此类被视为 web 控制器。Spring Boot 注册这个控制器和任何暴露的 URL 映射;在这种情况下,“/v1/movie
”和“/v1/movies
”(一个是单数,一个是复数)。我们来看看细节。
-
@RestController
。该注释是 Spring Boot 的标记,因此它被注册为入口休息点。 -
@RequestMapping
。该注释声明了 REST API、接受传入请求的方法以及接受这些请求的路径。这个注释必须放在作为请求处理程序的方法中。您也可以使用@GetMapping
注释,这是一种处理 GET HTTP 请求的简单方法。 -
@RequestBody
。两个处理程序中都使用了这个注释,但是看看参数——一个是单个实例,另一个是一组Movie
实例。每次有请求时,Spring MVC 负责自动将每个 JSON 请求转换成正确的实例;在这种情况下,Movie
的实例。(Movie
类与第一部分中的相同)。 -
ResponseEntity<String>
。每个处理程序返回一个 ResponseEntity 状态;在本例中,是一个带有 HTTP 状态代码202
的字符串Accepted
。
现在,您可以通过执行以下命令在终端中运行 web 应用。
$ ./mvnw spring-boot:run
您可以做一个小测试,验证您的 REST API 正在运行。例如,您可以打开一个终端并使用cURL
命令,如下面的代码片段所示,并获得相同的输出。
$ curl -i -H "Content-Type:application/json" -X POST -d '[{"title":"The Matrix","actor":"Keanu Reeves","year":1999},{"title":"Memento","actor":"Guy Pearce","year":2000}]' http://localhost:8080/v1/movies
HTTP/1.1 202 Accepted
Server: Apache-Coyote/1.1
Content-Length: 0
您将获得 202 Accepted 显示,并且在运行 web 应用的日志中,您将看到类似于以下输出的内容。
INFO 8052 --- [.16-8080-exec-3] c.apress.integration.movieweb.MovieWebController : Movie: Movie(title: The Matrix, actor: Keanu Reeves, year: 1999)
INFO 8052 --- [.16-8080-exec-3] c.apress.integration.movieweb.MovieWebController : Movie: Movie(title: Memento, actor: Guy Pearce, year: 2000)
好了,您刚刚创建了 REST API 服务,但是您遗漏了其他需求。将电影保存到 JDBC,并发送有关新保存的电影的电子邮件。在 rest 控制器的 handler 方法中,您可以添加执行 JDBC 和发送电子邮件的逻辑,但主要目的是为了了解 Spring Integration 的强大功能。
您可以运行电影应用(来自第一部分)来看看它是如何交流的。您应该会在MovieWeb
控制台日志中看到所有打印出来的电影。
通过声明性 XML 的 Spring Integration:第二部分
接下来,让我们创建清单 3-13 中所示的 Spring bean 上下文文件。
<?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:int-http="http://www.springframework.org/schema/integration/http"
xmlns:int-mail="http://www.springframework.org/schema/integration/mail"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/integration/jdbc http://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration/mail http://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- Spring Integration -->
<!-- Spring Integration: Http -->
<int-http:inbound-channel-adapter id="movie"
supported-methods="POST" channel="input" path="/v2/movie"
request-payload-type="com.apress.integration.movieweb.Movie"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
/>
<int-http:inbound-channel-adapter id="movies"
supported-methods="POST" channel="input" path="/v2/movies"
request-payload-type="com.apress.integration.movieweb.Movie[]"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
/>
<!-- Spring Integration: Execution Channel -->
<int:channel id="input"/>
<!-- Spring Integration Service Activator. -->
<int:service-activator input-channel="input" ref="movieEndpoint" />
</beans>
Listing 3-13.src/main/resources/META-INF/spring/movie-webapp-integration.xml
清单 3-13 是要使用的 XML 配置文件。这个文件应该创建在src/main/resources/META-INF/spring
目录下。
Note
所有源代码都在 Apress 网站上(参见 www.apress.com
的源代码/下载选项卡)。可以复制/粘贴。
看看下面的名称空间。即使您现在不会使用它们,但很快就会用到。
-
xmlns:int-mail
展示了一些对电子邮件操作有用的标签。 -
xmlns:jdbc
对 JDBC 有用。
让我们回顾一下清单 3-13 ,看看有什么新内容。
-
<int-http:inbound-channel-adapter/>
。即使您已经创建了MovieController
类,这个标签也会创建相同的行为。它使用版本 2 创建了两个端点:"/v2/movie"
和"/v2/movies"
。这些属性如下。-
supported-methods="POST"
告知端点仅接受 POST 请求。 -
channel="input"
指定请求消息发送到哪里。 -
path="/v2/movie"
是请求映射路径。定义了两个:一个用于单个电影,另一个用于电影集合。 -
request-payload-type="com.apress.integration.movieweb.Movie"
类似于声明@RequestBody
的注释。Spring MVC 自动将 JSON 格式转换成Movie
对象。对于电影集合,您声明了Movie[]
数组。 -
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
显示了类似于ResponseEntity<String>
类型的响应类型。
-
-
<int:channel id="input"/>
创建一个生产和消费一个Message
类型的通道。 -
<int:service-activator input-channel="input" ref="movieEndpoint" />
公开类的名称,在该类中,您将该端点的处理程序声明为服务激活器。
您可以使用 XML 配置文件以编程方式创建相同的 REST API 端点(如MovieWebController
类),也可以不使用任何代码(声明性的)。
这个 XML 配置文件的结果图如图 3-11 所示。
图 3-11。
Spring 积分图第二部分(第 1 版)
Spring Integration 开发:第二部分
清单 3-14 是一个服务激活器组件,即MovieEndpoint
类。
package com.apress.integration.movieweb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.handler.annotation.Headers;
import java.util.Map;
@MessageEndpoint
public class MovieEndpoint {
private static final Logger log = LoggerFactory.getLogger(MovieEndpoint.class);
@ServiceActivator
public void processMovie(Movie movie, @Headers Map<String,Object> headers) throws Exception {
log.info("Movie: " + movie);
}
@ServiceActivator
public void processMovies(Movie[] movies, @Headers Map<String,Object> headers) throws Exception {
for (Movie movie: movies){
log.info("Movie: " + movie);
}
}
}
Listing 3-14.com.apress.integration.movieweb.MovieEndpoint.java
清单 3-14 显示了消息端点。该类公开了两个服务激活器处理程序。当消息到达输入通道时,Spring Integration 决定选择哪一个。
接下来,创建MovieConfiguration
类,因为您需要包含带有@ImportResource
注释的 XML 文件(参见清单 3-15 )。
package com.apress.integration.movieweb;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource("META-INF/spring/movie-webapp-integration.xml")
public class MovieConfiguration {
}
Listing 3-15.MovieWebApplication (version 2)
现在,您可以运行应用了。现在您应该有四个 REST 端点——两个由MovieController
类(/v1/movie
和/v1/movies
)处理请求,另外两个由 Spring Integration 及其<int-http:inbound-channel-adapter/>
标记(/v2/movie
和/v2/movies
)处理请求。
可以用 cURL 测试一下。
$ curl -i -H "Content-Type:application/json" -X POST -d '[{"title":"The Matrix","actor":"Keanu Reeves","year":1999},{"title":"Memento","actor":"Guy Pearce","year":2000}]' http://localhost:8080/v2/movies
HTTP/1.1 202 Accepted
Server: Apache-Coyote/1.1
Content-Length: 0
再次注意,您使用的是 API 的版本 2。
接下来,继续第二部分的需求,有必要添加一种方法来将电影存储在数据库中,然后,在这之后,发送电子邮件。一种方法是重用服务激活器处理程序,并创建一个公共函数,将电影存储在数据库中。为此,您可以使用常规的 JDBC 代码、Spring 提供的 JDBCTemplate 或 Hibernate with JPA。
还是用 Spring Integration 吧!它有一个组件可以直接插入到数据库中(参见清单 3-16 )。
<?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:int-http="http://www.springframework.org/schema/integration/http"
xmlns:int-mail="http://www.springframework.org/schema/integration/mail"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/integration/jdbc http://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration/mail http://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- Spring Integration -->
<!-- Spring Integration: Http -->
<int-http:inbound-channel-adapter id="movie"
supported-methods="POST" channel="input" path="/v2/movie"
request-payload-type="com.apress.integration.movieweb.Movie"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
/>
<int-http:inbound-channel-adapter id="movies"
supported-methods="POST" channel="input" path="/v2/movies"
request-payload-type="com.apress.integration.movieweb.Movie[]"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
/>
<!-- Spring Integration: Execution Channel -->
<int:channel id="input"/>
<!-- Spring Integration: JDBC -->
<int-jdbc:outbound-channel-adapter
query="insert into movies (title, actor, year) values ( :payload.title, :payload.actor, :payload.year)"
data-source="dataSource" channel="input" />
</beans>
Listing 3-16.movie-web-app-integration.xml version 2
清单 3-16 显示了movie-webapp-integration.xml
的版本 2。注意不再有服务激活器,这意味着你可以直接从http:inbound
标签将电影传入数据库。
<int-jdbc:outbound-channel-adapter />
允许您连接到任何 JDBC 兼容的数据库。(NoSQL 也有一个!)以下是属性。-
query
设置 SQL 语句;这个例子非常简单,但是你也可以进行更新和删除。 -
data-source
是数据源的名称,通常,它是一个 bean,包含连接到数据库所需的所有信息,如用户名、密码、URL 等等。 -
channel
是标签获取信息的地方。
-
你应该有一个如图 3-12 所示的积分图。
图 3-12。
积分图
图 3-11 与图 3-12 有何不同?您只需移除服务激活并插入 JDBC 组件。
在尝试运行 web 应用之前,必须确保声明了 SQL 驱动程序,因为其中一个依赖项是 H2。您可以添加一个不同的引擎,但是您需要确保您的数据库引擎已经启动并且正在运行。
因为您使用的是 H2,所以 Spring Boot 使用嵌入式数据库默认设置。它有一个内存数据库。如果您使用任何其他数据库引擎,请相应地修改application.properties
中的属性。
您可以将清单 3-17 添加到 application.properties 文件中,以查看数据是否被写入 H2 数据库引擎。
# H2 Web Console
spring.h2.console.enabled=true
# External DataSource - MySQL
#spring.datasource.url=jdbc:mysql://localhost/testdb
#spring.datasource.username=scdf
#spring.datasource.password=scdf
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
Listing 3-17.src/main/resources/application.properties
清单 3-17 显示了application.properties
文件的内容。我注释了一些DataSource
属性,这样您就可以看到您能够使用 MySQL 或任何其他 DB 引擎。添加spring.datasource
属性会创建dataSource
bean(您在<int-jdbc:outbound-channel-adapter/>
标签的数据源中设置的值)。请记住添加正确的数据库访问凭据,如果使用不同的数据库引擎,请更改驱动程序类。现在,只使用 H2。
接下来,您需要创建一个模式来帮助 JDBC 适配器发送数据。Spring Boot 可以自动生成表和数据库,但是你需要使用不同的引擎,比如 Hibernate/JPA。这个例子使用了 JDBC 驱动程序(没有 JPA),所以您需要提供一个包含表创建的src/main/resources/schema.sql
文件(参见清单 3-18 )。
create table IF NOT EXISTS movies(
id int not null auto_increment,
title varchar(250),
actor varchar(100),
year smallint,
primary key(id));
Listing 3-18.src/main/resources/schema.sql
清单 3-18 是一个非常基本的表,其中声明了主要字段。
现在,您可以运行应用,并使用 cURL 函数,如下一个代码片段所示。
$ curl -i -H "Content-Type:application/json" -X POST -d '{"title":"The Matrix","actor":"Keanu Reeves","year":1990}' http://localhost:8080/v2/movie
HTTP/1.1 202 Accepted
Server: Apache-Coyote/1.1
Content-Length: 0
打开浏览器,指向http://localhost:8080/h2-console/
打开 H2 控制台(参见图 3-13 )。
图 3-13。
您应该能够使用 URL jdbc:h2:mem:testdb
、用户名sa
和一个空密码进行连接。单击连接。图 3-14 显示了一个已定义的电影表,其中包括您发布的记录。
图 3-14。
H2 控制台
你注意到了吗,curl
命令是使用/v2/movie
路径执行的,并且只发布了一部电影。怎么能一个电话就把电影集贴出来插上呢?有许多方法可以实现这一点。例如,使用一个处理集合的服务激活器(就像你在清单 3-14 中看到的)通过使用另一个连接到<int-jdbc:outbout-channel-adapter/>
标签的直接通道发送每部电影。考虑一下,如果您需要停止插入,然后恢复或安排特定的日期和时间,会发生什么情况。使用 Spring Integration 有点复杂,对吗?好的方面是有办法做这种任务,但那是你的功课。别担心。给你个提示——春批!
下一个要求是一旦电影/电影存储发送电子邮件。让我们从movie-webapp-integration.xml
的最终版本开始(见清单 3-19 )。
<?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:int-http="http://www.springframework.org/schema/integration/http"
xmlns:int-mail="http://www.springframework.org/schema/integration/mail"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/integration/jdbc http://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
http://www.springframework.org/schema/integration/mail http://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- Spring Integration -->
<!-- Spring Integration: Http -->
<int-http:inbound-channel-adapter id="movie"
supported-methods="POST" channel="publisher" path="/v2/movie"
request-payload-type="com.apress.integration.movieweb.Movie"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
/>
<int-http:inbound-channel-adapter id="movies"
supported-methods="POST" channel="publisher" path="/v2/movies"
request-payload-type="com.apress.integration.movieweb.Movie[]"
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"
/>
<!-- Publish/Subscribe Channel -->
<int:publish-subscribe-channel id="publisher" />
<!-- Spring Integration: JDBC -->
<int-jdbc:outbound-channel-adapter id="jdbc"
query="insert into movies (title, actor, year) values ( :payload.title, :payload.actor, :payload.year)"
data-source="dataSource" channel="publisher" order="1" />
<!-- Spring Integration: Mail -->
<int-mail:outbound-channel-adapter channel="mail" mail-sender="mailSender" />
<int:service-activator input-channel="publisher" output-channel="mail" ref="movieMailEndpoint" order="2" />
<int:channel id="mail">
<int:dispatcher task-executor="taskExecutor"/>
</int:channel>
<!-- More definitions in the next section ... -->
</beans>
Listing 3-19.src/main/resources/META-INF/spring/movie-webapp-integration.xml (version 3)
清单 3-19 显示的是movie-webapp-integration.xml
的最终版本。什么是新的?
-
使用发布-订阅模式创建一个频道。它按照指定的顺序将消息发送给所有订阅者(这是通过订阅者添加
order
属性来完成的)。 -
<int-jdbc:outbound-channel-adapter/>
存储电影,现在是publisher
频道的订户。让我们看看属性和它们的值。-
channel="publisher"
是发布者频道的订阅者。 -
order="1"
先收到消息。
-
-
<int:service-activator/>
是一个新的服务激活器,它创建了要交付给mail
通道的MailMessage
实例。以下是它的属性。-
ref="movieMailEndpoint"
是 bean 引用类。 -
order="2"
表示服务激活器接收到存储后的消息。 -
output-channel="mail"
是发送MailMessage
实例的通道的名称。
-
-
<int-channel>
是接收MailMessage
实例的新通道(执行者通道)。这个通道通过任务执行器执行,这意味着它可以是一个异步调用。以下是它的属性。-
<int:dispatcher/>
声明使异步调用成为可能的类。 -
task-
executor="taskExecutor"
是任务执行器类的 bean 实现的名称。
-
-
<int-mail:outbound-channel-adapter/>
是一个新标签,它根据mailSender
属性中给出的参数发送电子邮件(在application.properties
文件中)。mail-sender="mailSender"
是豆子的名字。Spring 容器基于查看email.<props>
的application.properties
文件生成这个 bean。
最终版本的 Spring Integration 图如图 3-15 所示。
图 3-15。
Spring 积分图
清单 3-19 显示了您对两个 beans 的依赖:taskExecutor
和emailSender
。将以下 bean 声明添加到movie-webapp-integration.xml
(参见清单 3-20 )。
<!-- Mail Properties -->
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="${email.host}" />
<property name="port" value="${email.port}" />
<property name="username" value="${email.account.name}" />
<property name="password" value="${email.account.password}" />
<property name="javaMailProperties">
<props>
<prop key="mail.smtp.starttls.enable">true</prop>
<prop key="mail.smtp.auth">true</prop>
</props>
</property>
</bean>
<!-- Helpers -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5" />
<property name="maxPoolSize" value="10" />
<property name="queueCapacity" value="25" />
</bean>
<!-- JSON Converter -->
<bean id="jsonConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes" value="application/json" />
</bean>
Listing 3-20.src/main/resources/META-INF/spring/movie-webapp-integration.xml (extra declaration dependencies)
清单 3-20 显示了需要添加到movie-web app-integration . XML文件中的mailSender
bean 定义和taskExecutor
定义。taskExecutor
bean 定义了ThreadPoolTaskExecutor
类,作为发送电子邮件的异步处理程序,所以在服务器响应之前,您不需要这样做。
拥有连接到您选择的任何 SMTP 提供者的所有必要信息,并且它提供了一种将值公开为属性的方法。这些值在src/main/resources/application.properties
文件中(见清单 3-21 )。
# H2 Web Console
spring.h2.console.enabled=true
# External DataSource - MySQL
#spring.datasource.url=jdbc:mysql://localhost/springxd
#spring.datasource.username=springxd
#spring.datasource.password=springxd
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.datasource.initialize=true
email.account.name=myuser@mydomain.com
email.account.password=mypassword
email.host=smtp.gmail.com
email.port=587
Listing 3-21.src/main/resources/application.properties file (final version)
清单 3-21 中显示的属性使用 Gmail SMTP 服务提供商。清单 3-22 ( movie-webapp-integration.xml
)定义了服务激活器中movieWebMailEndpoint
的引用。清单 3-22 展示了这个类。
package com.apress.integration.movieweb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.mail.MailMessage;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.messaging.handler.annotation.Headers;
import java.util.Date;
import java.util.Map;
@MessageEndpoint
public class MovieMailEndpoint {
private static final Logger log = LoggerFactory.getLogger(MovieMailEndpoint.class);
@ServiceActivator
public MailMessage process(Movie movie, @Headers Map<String,Object> headers) throws Exception {
log.info("Movie: " + movie);
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo("myuser@mydomain.com");
mailMessage.setSubject("A new Movie is in Town");
mailMessage.setSentDate(new Date(0));
mailMessage.setText(movie.toString());
return mailMessage;
}
}
Listing 3-22.com.apress.spring.integration.MovieWebMailEndpoint.java
清单 3-22 显示了Message
端点和服务激活器处理器。这个特殊的处理程序返回<int-mail:outbound-channel-adapter/>
标签所需的MailMessage
实例。这是您添加电子邮件的收件人、主题和文本的地方。处理程序正在使用Movie
对象和Message
实例上的头。
您可以运行最后一个测试来查看电影是否已存储,然后将邮件放入收件箱。确保设置了正确的属性,并且不会有任何问题。请记住,本章中的所有代码都可以从 Apress 网站下载。
现在是运行这两部分的时候了。您可以使用 Maven(在单独的窗口终端中),或者您可以选择运行您最喜欢的 IDE 来查看完整的流程!
Note
如果您正在运行 STS 并且想要运行这两个项目,您需要在菜单中禁用 Live Bean 支持特性:运行➤运行配置,选择配置,并取消选中 Live Bean 支持特性。
恭喜你!您刚刚创建了一个集成两个系统的 Spring Integration 应用。最后你在里面没用多少代码。Spring Integration 完成了大部分繁重的工作。尽管这是一个简单的例子,但您知道如何集成 JMS、RabbitMQ 或 SOAP 等系统,或者如何集成脸书或 Twitter 等社交媒体平台。
摘要
本章向您介绍了 Spring Integration。您看到了 Spring Integration 组件以及它如何依赖于消息、通道和端点。您看到了一些集成模式,以及 Spring Integration 如何促进了它们的使用。
您已经通过一个例子了解了如何使用 Spring Integration。此外,您看到了用几乎零代码创建应用并将其与其他组件或本地/外部系统集成是多么容易。
为什么 Spring Integration 很重要?Spring CloudStream 基于 Spring Integration,这使得它非常可靠、可伸缩且易于扩展。如果你想了解更多关于 Spring Integration 的知识,Apress 有非常好的标题。
四、SpringBatch
在前一章中,我向您展示了使用 Spring Integration 可以做些什么,Spring Integration 是一个轻量级框架,它将外部系统和流程连接到各种数据。但是,当您需要进行批量处理时,会发生什么呢?当您需要在任务关键型环境中的某个时间执行业务操作时,会发生什么情况?如何自动化处理大量数据?如何集成外部系统来处理大型数据集?
大型数据集需要什么样的典型系统/场景?您可能需要从数据库或文件系统(如 HDFS)中读取,或者从有数百万消息排队的消息代理中读取。然后,您需要根据业务规则处理该信息,并且您需要将数据分割成必须增强的多个块。最后,您需要将数据写入另一个引擎/系统(内部或外部)。有没有一个框架可以执行这样的场景?对,春批!
SpringBatch
Spring Batch 是一个轻量级的批处理框架,允许您为大量数据创建可重复的步骤(读、写、处理)。它提供了日志/跟踪、作业处理、作业启动/停止/重启、事务管理、顺序和部分处理、大规模并行处理等机制!使用 Spring Batch 的一个主要好处是,它遵循与 Spring 框架相同的原则,为开发人员提供了一种基于 POJO(Plain Old Java Objects)创建简单业务类的方法,基础设施、批处理执行环境和批处理应用之间关注点的明确分离,核心执行的默认实现,配置、定制和扩展服务的简单方法,以及提高生产率的简单方法。所有这些使得 Spring Batch 成为批处理的理想框架。
Spring Batch 是一个经过验证的算法集合,支持非常大的数据量,通过优化逻辑和分区技术为您提供最佳性能。Spring Batch 是一个 Spring 生态系统项目,已经成为批处理的 IT 标准。
程序设计模型
在 Spring Batch 中,有命名约定。您听说过声明作业、步骤、微线程、块、读-处理-写等等,但是您需要记住一些原则。
1 Step = 1 Read-Process-Write = Tasklet
1 Job =多步骤= Step 1 > Step 2 > Step 3 …(链式)
图 4-1 是春批成语的好图。
图 4-1。
Spring 批量定型/习惯用法
批处理(读-处理-写)与任何其他输入-处理-输出或通道-处理-通道模式相同。您有包含已定义步骤的作业,并且这些步骤是连锁的。其中一个好处是,您可以停止作业,稍后再继续,或者在运行时,您可以根据表达式跳过某些作业。你认为 Spring Batch 提供的一些模式是基于 Spring Integration 的吗?是的,他们是!
SpringBatch 功能
Spring Batch 是一个基于客户用例的模型,考虑了安全性、速度、性能和可靠性。以下是一些 Spring 批处理功能。
-
启动/停止/重新启动作业
-
重试/跳过
-
说明性输入输出
-
基于组块的处理
-
分割技术
-
事务管理
-
基于网络的管理
每个 Spring 批处理应用都有一个内部数据库,记录正在执行的步骤、跳过的步骤、触发的作业、成功和失败等等。
我认为理解 Spring Batch 的最好方法是用一个简单的例子。该示例基于上一章的电影应用。稍后您将在 Spring CloudStream、批处理和云任务处理中使用它。
使用声明性 XML 的电影批处理应用
电影批处理应用从 CSV 文件中读取并将每一行作为记录插入到数据库中。同样,这是一个非常简单的例子,但足以演示您可以使用 Spring Batch 做什么。在这个应用中,您使用 XML 上下文的声明性方法。我们不仅在批处理中使用编程方法,在本章后面的 Spring Integration 中也使用编程方法。
可以从前往 https://start.spring.io
开始(见图 4-2 )。
图 4-2。
https://start.spring.io
—Spring 初始化
您可以使用以下信息来填写输入字段。
-
组:
com.apress.batch
-
神器:
movie
-
依赖项:Spring 批处理,H2 数据库
按“生成”按钮创建一个 ZIP 文件供下载。下载完成后,你可以解压并在你喜欢的 IDE 中打开它。请记住,Spring Batch 保留了任何步骤、作业或执行的记录,因此有必要包含一个 DB 引擎驱动程序;在这种情况下,非常有用的发展,H2 驱动程序。让我们回顾一下这个项目。打开pom.xml
(见清单 4-1 )。
<?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>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.apress.batch</groupId>
<artifactId>movie</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>movie</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-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>
Listing 4-1.pom.xml
清单 4-1 显示你的pom.xml
文件。(您需要删除默认的<scope/>
标签,它是 H2 数据库的运行时标签。)将 H2 数据库设置为服务器。您可以使用任何其他客户端,比如 MySQL、PostgreSQL 或任何其他 DB 引擎;但是在这种情况下,我们使用 H2 作为服务器模式。
接下来,创建Movie
模型类(参见清单 4-2 )。
package com.apress.batch.movie;
public class Movie {
private String title;
private String actor;
private int year;
public Movie(){}
public Movie(String title, String actor, int year){
this.title = title;
this.actor = actor;
this.year = year;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public String toString(){
StringBuilder builder = new StringBuilder();
builder.append("Movie(tile: ");
builder.append(title);
builder.append(", actor: ");
builder.append(actor);
builder.append(", year: ");
builder.append(year);
builder.append(")");
return builder.toString();
}
}
Listing 4-2.com.apress.batch.movie.Movie.java
清单 4-2 显示了Movie
类(与前一章没有任何变化;它是一个简单的 POJO 类)。接下来,创建MovieConfiguration
类来设置 H2 服务器并加载批处理上下文文件(参见清单 4-3 )。
package com.apress.batch.movie;
import org.h2.tools.Server;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import java.sql.SQLException;
@Configuration
@EnableBatchProcessing
@ImportResource("META-INF/spring/movie-batch-context.xml")
public class MovieConfiguration {
@Bean(initMethod = "start", destroyMethod = "stop")
public Server h2Server() throws SQLException {
return Server.createTcpServer("-tcp", "-ifNotExists","-tcpAllowOthers", "-tcpPort", "9092");
}
}
Listing 4-3.com.apress.batch.movie.MovieConfiguration.java
清单 4-3 显示了MovieConfiguration
类。您已经了解了配置类以及如何导入上下文资源;唯一新的是@EnableBatchProcessing
。该注释执行以下操作。
-
识别数据库引擎。因为您添加了 H2 作为依赖项,Spring Batch 通过
pom.xml
文件执行与 H2 或您添加到类路径中的任何其他引擎相关的所有操作。 -
初始化内部 SQL 脚本以创建
BATCH
表。-
BATCH_JOB_EXECUTION
-
BATCH_JOB_EXECUTION_CONTEXT
-
BATCH_JOB_EXECUTION_PARAMS
-
BATCH_JOB_EXECUTION_SEQ
-
BATCH_JOB_INSTANCE
-
BATCH_JOB_SEQ
-
BATCH_STEP_EXECUTION
-
BATCH_STEP_EXECUTION_CONTEXT
-
BATCH_STEP_EXECUTION_SEQ
-
-
发现和配置任何作业
-
按照定义执行作业
清单 4-3 显示了我们正在创建h2Server
的@Bean
注释。要将 H2 设置为服务器模式,需要调用createTcpServer
的一些选项。通常,当您对h2.jar
执行java -jar
命令时,这些选项被设置为参数;但在这里,我们是在应用中开始的。
让我们导入带有@ImportResource
注释的movie-batch-context.xml
文件。接下来,让我们创建一个 XML 配置文件,因为它驱动这个应用的每个细节(参见清单 4-4 )。
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:batch="http://www.springframework.org/schema/batch"
xsi:schemaLocation="
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="movie" class="com.apress.batch.movie.Movie" scope="prototype"/>
<batch:job id="movieJob">
<batch:step id="step1">
<batch:tasklet>
<batch:chunk reader="movieFieldItemReader" writer="dbItemWriter" commit-interval="2"/>
</batch:tasklet>
</batch:step>
</batch:job>
<bean id="movieFieldItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource" value="movies.txt"/>
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="names" value="title,actor,year"/>
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="movie"/>
</bean>
</property>
</bean>
</property>
</bean>
<bean id="dbItemWriter"
class="org.springframework.batch.item.database.JdbcBatchItemWriter">
<property name="dataSource" ref="dataSource"/>
<property name="sql">
<value>
<![CDATA[
insert into movies(title,actor,year)
values (:title, :actor, :year)
]]>
</value>
</property>
<property name="itemSqlParameterSourceProvider">
<bean class="org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider"/>
</property>
</bean>
</beans>
Listing 4-4.src/main/resources/META-INF/spring/movie-batch-context.xml
清单 4-4 显示了您将使用的所有必需的 beans,但是最重要的是名称空间xmlns:batch
标签。它提供了以下内容。
-
<batch:job/>
定义名为movieJob
的批处理作业。 -
<batch:step/>
定义作业中的一个步骤。 -
<batch:tasklet/>
定义步骤中的特定任务(小任务)。 -
<batch:chunk/>
定义进程(块)、读取(输入)和写入(输出)。
以下内容将被重复使用。
-
电影文本文件:标题、演员和年份(CSV 格式)。这个
movies.txt
文件应该在您的类路径中,这样您就可以将它添加到src/main/resources
目录中。 -
你需要在
src/main/resources to
中添加schema.sql
文件。
其他 bean 的定义是不言自明的,很容易理解。如果你想了解更多关于春批的知识,我推荐迈克尔·米内拉(Apress,2019)的春批权威指南。
接下来,打开application.properties
文件并添加清单 4-5 中所示的内容。
# Local DB
spring.datasource.url=jdbc:h2:~/movies_db;AUTO_SERVER=TRUE
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.initialization-mode=always
Listing 4-5.src/main/resources/application.properties
清单 4-5 显示了所需的属性;因为您将 H2 设置为服务器模式,所以 Spring Boot 不会执行内存数据库的初始化,因为它在我们声明 Spring Bean h2Server
时被覆盖了(我们使用不同的 URL: jdbc:h2:~/movies_db;AUTO_SERVER=TRUE
)。在这种情况下,H2 服务器在您的主目录中创建了一个名为movie_db
(扩展名为.lock.db
和.mv.db
)的文件。您可以指定存储数据的路径。
现在,您可以运行您的应用了。打开一个终端窗口并执行它。
$ ./mvnw clean spring-boot:run
您的输出应该类似于以下内容。
]: Job: [FlowJob: [name=movieJob]] launched with the following parameters: [{}]
]: Executing step: [step1]
]: Step: [step1] executed in 65ms
]: Job: [FlowJob: [name=movieJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 91ms
前面的输出打印了作业、步骤执行及其完成时间。您可以看到程序一直在运行,这是因为您将 H2 设置为服务器模式。您可以查看 Spring 批处理读取文件并将其内容转储到数据库中——从movies.txt
文件开始每行一行。为了验证这一点,您可以使用类似松鼠 SQL 客户端( https://sourceforge.net/projects/squirrel-sql/
)的 UI 来查看内容(参见图 4-3 )。
图 4-3。
松鼠 SQL 客户端( https://sourceforge.net/projects/squirrel-sql/
图 4-3 显示了松鼠 SQL 客户端。您需要使用相同的 URLjdbc:h2:~/movies_db;AUTO_SERVER=TRUE
来连接。
如果您不想使用外部 UI,可以通过向应用添加少量代码来验证数据是否存在。创建清单 4-6 中所示的MovieQueryInfo
类。
package com.apress.batch.movie;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class MovieQueryInfo {
@Bean
public CommandLineRunner getInfo(JdbcTemplate jdbcTemplate){
return args -> {
jdbcTemplate.query(
"select title,actor,year from movies",
(rs, rowNum) ->
new Movie(
rs.getString("title"),
rs.getString("actor"),
rs.getInt("year")
)
).forEach(System.out::println);
};
}
}
Listing 4-6.com.apress.batch.movie.MovieQueryInfo.java
清单 4-6 显示了一个配置文件,它将getInfo
bean 声明为CommandLineRunner
,这意味着一旦 Spring 容器被配置好并准备好运行,它就会运行。您可以重新运行该应用,您应该会得到以下输出。
Movie(tile: The Matrix, actor: Keanu Reeves, year: 1999)
Movie(tile: Memento, actor: Guy Pearce, year: 2000)
Movie(tile: The Silence of the Lambs, actor: Jodie Foster, year: 1991)
Movie(tile: The Prestige, actor: Christian Bale, year: 2006)
Movie(tile: Disturbia, actor: Shia LaBeouf, year: 2007)
祝贺您,您创建了您的第一个 Spring 批处理应用!
使用 JavaConfig 的电影批处理应用
让我们通过使用 JavaConfig 方法来更改我们的电影应用。您还将阅读一个列出电影的 XML 文件。想法是一样的:读取 XML 电影文件,并将每部电影保存到数据库的一行中。
让我们从创建一个新项目开始。对之前项目的唯一修改是ArtifactId
名称。让我们将其命名为movie-custom
,具有相同的依赖关系:Spring Batch 和 H2 数据库。这个项目有一个不同的包名:com.apress.batch.moviecustom
。它基于ArtifactId
值。图 4-4 显示了结构和类别。
图 4-4。
Spring 批处理项目:电影-自定义
首先,我们需要将清单 4-7 中的依赖项添加到pom.xml
文件中。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.11.1</version>
</dependency>
Listing 4-7.pom.xml New Dependencies
清单 4-7 展示了读取 XML 文件并将它们编组到Movie
对象的新依赖关系。
接下来,创建一个新的MovieServerConfiguration
类(参见清单 4-8 )。
package com.apress.batch.moviecustom;
import org.h2.tools.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.sql.SQLException;
@Configuration
public class MovieServerConfiguration {
@Bean(initMethod = "start", destroyMethod = "stop")
public Server h2Server() throws SQLException {
return Server.createTcpServer("-tcp", "-ifNotExists","-tcpAllowOthers", "-tcpPort", "9092");
}
@Bean(initMethod = "start", destroyMethod = "stop")
public Server h2WebServer() throws SQLException {
return Server.createWebServer("-web", "-ifNotExists","-webAllowOthers", "-webPort", "8092");
}
}
Listing 4-8.com.apress.batch.moviecustom.MovieServerConfiguration.java
清单 4-8 显示了MovieServerConfiguration
类。这个类声明了两个 bean——前一个应用的h2Server
bean 和新的h2WebServer
,后者将 H2 服务器设置为可从网页访问。这在开发中很方便,并且您不需要任何额外的 SQL UI。查看代码。你看这很简单。
接下来,让我们创建MovieFieldMapper
类(参见清单 4-9 )。
package com.apress.batch.moviecustom;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.validation.BindException;
public class MovieFieldSetMapper implements FieldSetMapper<Movie> {
@Override
public Movie mapFieldSet(FieldSet fieldSet) throws BindException {
return new Movie(
fieldSet.readString("title"),
fieldSet.readString("actor"),
fieldSet.readInt("year"));
}
}
Listing 4-9.com.apress.batch.moviecustom.MovieFieldMapper.java
清单 4-9 显示了MovieFieldSetMapper
类,这是清单 4-4 ( movie-batch-context.xml
)中使用的声明部分,在这里我们声明了movieFieldItemReader
和fieldSetMapper
bean。
接下来,创建MovieBatchConfiguration
类(参见清单 4-10 )。
package com.apress.batch.moviecustom;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory
;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.xml.StaxEventItemReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.oxm.xstream.XStreamMarshaller;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableBatchProcessing
public class MovieBatchConfiguration {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Autowired
public DataSource dataSource;
@Bean
public StaxEventItemReader<Movie> movieItemReader() {
XStreamMarshaller unmarshaller = new XStreamMarshaller();
Map<String, Class> aliases = new HashMap<>();
aliases.put("movie", Movie.class);
unmarshaller.setAliases(aliases);
StaxEventItemReader<Movie> reader = new StaxEventItemReader<>();
reader.setResource(new ClassPathResource("/movies.xml"));
reader.setFragmentRootElementName("movie");
reader.setUnmarshaller(unmarshaller);
return reader;
}
@Bean
public JdbcBatchItemWriter<Movie> movieItemWriter() {
JdbcBatchItemWriter<Movie> itemWriter = new JdbcBatchItemWriter<>();
itemWriter.setDataSource(dataSource);
itemWriter.setSql("INSERT INTO movies (title,actor,year) VALUES (:title, :actor, :year)");
itemWriter.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider());
itemWriter.afterPropertiesSet();
return itemWriter;
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.<Movie, Movie>chunk(10)
.reader(movieItemReader())
.writer(movieItemWriter())
.build();
}
@Bean
public Job job() {
return jobBuilderFactory.get("job")
.start(step1())
.build();
}
}
Listing 4-10.com.apress.batch.moviecustom.MovieBatchConfiguration.java
清单 4-10 显示了MovieBatchConfiguration
类,它与 XML 配置相同。我们来复习一下。
-
JobBuilderFactory
。这个类是一个流畅的 API,允许你创建一个Job
(JobInstance
)。Job
是Step
实例的容器。它可以将属于一个流程的多个步骤组合在一起;它支持步骤的可重启性。JobInstance
是一个逻辑作业运行,它创建JobExecution
,跟踪每一步发生的事情。job
bean 使用JobBuilderFactory
实例从一个简单的步骤开始,然后添加更多。下面是一个例子。@Bean public Job consumerCreditCardJob() { return this.jobBuilderFactory.get("consumerCreditCardJob") .start(consumerLoad()) .next(processLoad()) .next(applyInterest()) .end() .build(); }
下面是另一个可以添加转场的例子。
@Bean public Job creditJob () { return jobBuilderFactory.get("creditJob") .start(step1()) .on("COMPLETED").to(step2()) .from(step2()).on("COMPLETED").stopAndRestart(step3()) .from(step3()).end() .build(); }
-
StepBuilderFactory
。这个类提供了一个流畅的 API 来创建一个Step
定义。一个Step
是一个域对象,具有批处理作业的连续阶段,这意味着每个job
都由一个或多个步骤组成。该步骤将批处理定义为读取器、写入器或处理器。 -
movieItemReader
。这个 bean 进行读取(通过使用StaxEventItemReader
类)和从 XML 到Movie
对象的编组(通过使用XStreamMarshaller
)。StaxEvenItemReader
拥有读取 XML 内容并将其转换成Movie
对象的所有必要逻辑。这里删除了很多代码。 -
movieItemWriter
。这个 bean 使用JdbcBatchItemWriter
为 XML 文件中的每个条目添加一个新记录。它使用了DataSource
,在本例中,它是由 Spring Boot 使用 H2 服务器模式实例化的。
图 4-5 是Job
、JobInstance
、JobExecution
、Step,
和StepExecution
的代表。
图 4-5。
作业-作业实例-作业执行-分步执行
最后,创建Movie
类,与清单 4-2 相同。
现在,您可以运行应用了。其中一项新功能是,您可以使用 web 浏览器通过端口 8092 访问 H2 web 部分,并执行 SQL 查询来查看添加到Movies
表中的行(参见图 4-6 和 4-7 )。
图 4-7。
H2 web 控制台—电影表
图 4-6。
H2 web 控制台在 http://localhost:8092 JDBC URL:JDBC:H2:~/movies _ db;AUTO_SERVER=TRUE
图 4-5 和 4-6 显示了在MovieServerConfiguration
类中配置 H2 web 服务器的结果,这在开发中很有用。当然,这可以由 Spring Boot 来管理,但是这个例子向您展示了控制和适应 H2 服务器模式的另一种方法。
摘要
本章介绍了 Spring Batch。尽管示例很简单,但我向您展示了应用批处理是多么容易。
Spring CloudStream 基于 Spring Integration,允许您创建批处理。它非常可靠、可伸缩且易于扩展。如果你想了解更多关于春批的知识,我推荐春批权威指南 (Apress,2019)。
接下来的章节用 Spring Task 重新审视 Spring Batch,并看看 Spring Cloud Data Flow 如何使用这些技术的力量来创建允许您将批处理集成到系统中的流。
五、SpringCloud
几年来,云、原生云、无服务器和容器这几个词一直在 IT 行业制造噪音,涉及使用新的基础设施、范例和架构来管理存储、网络、内存、CPU、隔离、可扩展性和微服务等资源。什么是微服务?在我看来,它们是一种架构模式,将一个系统构建成一个具有某些特征的服务集合,例如松散耦合、独立部署、可伸缩、可测试、可维护、围绕业务能力组织、和由一个小团队拥有。
本章向您展示了微服务如何在这个新的云原生时代发挥重要作用,并讨论了 Spring Cloud 在新的架构模式中的作用。我将向您展示使用 Spring Cloud 子项目的一些好处,比如 Spring Config Server、Spring Cloud 网飞、Spring Cloud Gateway 和 Spring Cloud Stream。首先,我们来讨论一下什么是微服务,你在这种新架构下开发所面临的问题,以及 Spring Cloud 的项目如何解决这些问题。
微服务
微服务架构模式并不新鲜;在我看来,它是随着 Unix 操作系统的出现(大约在 1971 年)及其多任务和多用户能力而产生的。借助大型软件工具(使用管道(|
)(一种允许信息传递到下一个可用程序或命令的简单符号)一起工作的小程序),你可以创建更健壮的系统。
微服务在架构师、开发人员和运营团队中获得了很大的热情,但是我们为什么需要它们呢?我认为时间是这个谜题的重要组成部分。我们的应用需要快速、灵活、高度可用、可扩展并具有高性能,以适应这个新的云原生时代。
-
速度。你需要创新、实验和更快交付的能力。使用微服务和框架来帮助您实现更快的上市时间。
-
安全。通过监控、隔离、容错和自动恢复实践,以在整个开发周期中保持稳定性、可用性和持久性的方式更快地行动。开始考虑持续交付和集成。
-
可扩展。事实证明,垂直扩展(购买更多硬件)并不能很好地扩展。使用商用硬件,重用现有硬件并进行水平扩展,创建同一应用的多个实例。使用容器来帮助您扩展。
-
机动性。随时准备从任何位置支持多台设备。移动设备的数量正在增长,它们连接到互联网,不仅用于社交媒体、电子邮件和聊天,还用于监控房屋、引擎等。
让我们来谈谈云原生架构和微服务,以及创建一个可扩展、可维护、安全且健壮的小应用。它需要传递信息。它需要通信并调用一些外部服务。信息传递是关键!我在公司看到的大多数系统使用 RESTful APIs 与其他系统或程序通信,以创建装饰者、翻译者或门面。还有其他通信选项:HTTP、TCP (WebSocket、AMQP、MQTT)、反应流等等。正如你所看到的,有很多沟通的方式,但要创建微服务,应该有一个指南。谢天谢地,有 12 个因素的应用原则。尽管我在过去的章节中简单地讨论了它们,但是你有必要理解它们的含义以及如何应用它们。
十二因素应用
根据创建云原生架构的需要,Heroku(一家云 PaaS 公司)的工程师确定了许多模式,这些模式成为了十二要素应用指南。本指南通过关注声明性配置、无状态和独立于部署,解释了如何设计一个应用(一个单元);换句话说,您的应用需要快速、安全和可伸缩。
以下是十二要素应用指南的总结。
-
代码库。在 VCS 跟踪的一个代码库;许多部署。一个应用有一个单一的代码库,由版本控制系统如 Git、Subversion 或 Mercurial 跟踪。您可以在开发、测试、试运行和生产环境中进行许多部署(从相同的代码库)。
-
依赖关系。显式声明并隔离依赖关系。有时一个环境没有互联网连接(如果是私有系统),所以你需要考虑打包你的依赖项(jar、gem、共享库等)。),或者如果您有一个库的内部存储库,您可以声明像 POM、gemfiles 和 bundles 这样的清单。永远不要假设你在最终环境中拥有一切。
-
配置。在环境中存储配置。你不应该硬编码任何变化的东西。使用环境变量或配置服务器。
-
后台服务。将后台服务视为附属资源。通过 URL 或配置连接到服务。
-
构建、发布、运行。严格分离构建和运行阶段。这与 CI/CD(持续集成,持续交付)相关。
-
流程。将应用作为一个或多个无状态进程执行。进程不应该存储内部状态。不分享任何东西。任何必要的状态都应该被认为是后台服务。
-
端口绑定。通过端口进程绑定导出服务。自包含的应用通过端口绑定公开。一个应用可以成为另一个应用的服务。
-
并发。通过流程模型向外扩展。通过添加更多应用实例进行扩展。单个进程可以自由多线程化。
-
一次性。快速启动和平稳关闭,最大限度地提高稳定性。流程应该是可处理的(它们是无状态的)和容错的。
-
环境平价。让开发、试运行和生产环境尽可能相似。这是高质量的结果,并确保连续交货。
-
日志。将日志视为事件流。您的应用应该写入标准输出。日志是聚合的、按时间顺序排列的事件流。
-
管理进程。作为一次性流程运行管理任务。在平台上运行管理流程:数据库迁移、一次性脚本等等。
我的一些学生问我,你是否需要一个云基础设施来创建微服务。在我看来,你不需要有一个庞大的基础设施或云环境来创建微服务。如果你遵循十二因素原则,你就准备好参与游戏,与更大的公司竞争。当您准备迁移或切换到更好的基础架构时,您已经在那里了。
如果要打造微服务,遵循十二要素原则,一个小团队从零开始打造一切,太过分了。你必须面对几个变化。
图 5-1。
整体服务与微服务
-
文化。你需要远离人员孤岛,创建跨职能团队;他们工作得更好,致力于解决一个业务领域。考虑连续交付,分散决策,寻找团队自治。
-
组织。创建跨职能的业务能力团队。这些团队拥有自主决策权。创建跨职能的平台团队运营。
-
技术。远离构建整体应用,采用微服务架构。在有限的环境中思考。遵循领域驱动设计的一些原则和实践。开始使用容器化来获得应用的隔离性、可伸缩性和高性能,并寻找服务集成来控制分布(见图 5-1 )。
一开始,在微服务架构中工作可能很有挑战性。并且,它提出了你需要控制的新问题。当其中一个微服务关闭时会发生什么?谁可以恢复服务?这意味着您需要具备弹性。另外,如果您有多个微服务实例在运行,谁来平衡它们呢?如果其中一个实例关闭了,如何发现新的服务或者让系统知道该实例关闭了或者现在正在运行?如果您有 SSL,保存证书或机密并在多个微服务间共享它们的最佳方法是什么?微服务架构还需要处理其他问题,并且应该能够轻松处理这些问题。
在接下来的部分中,我将讨论一些 Spring Cloud 子项目如何帮助应对这些新挑战。
SpringCloud
Spring Cloud 是一套工具/框架,可以轻松、快速、安全地开发微服务架构。本节涵盖了最常见的 Spring 云服务:Spring Cloud Config、服务注册和断路器。
Spring 云配置
Spring Cloud Config 为外部配置提供了服务器和客户端支持。这是一种集中式配置,可以在分布式系统中使用。配置服务器是一个外部化的应用配置服务,它提供了一个跨环境管理应用外部属性的中心位置(见图 5-2 )。
图 5-2。
配置服务器
您可以在开发、QA、生产或管道(继续交付)的任何阶段使用 Config Server,因为您可以集中管理每个环境来访问公共外部配置,而无需重新打包或重新部署。以下是 Spring Cloud Config Server 的一些特性。
-
加密和解密属性值(对称或非对称)
-
基于 HTTP API,允许名称-值对或 YAML 配置
-
一个可嵌入的服务器,很容易在带有
@EnableConfigServer
注释的 Spring Boot 应用中设置 -
连接到任何版本控制(例如 Git ),并允许您基于 Spring 概要文件、分支或标签来配置访问
Spring 云配置服务器
让我们创建一个 Spring Cloud Config Server,并了解配置它并让客户机可以访问它需要什么。打开浏览器,进入 https://start.spring.io
的 Spring Initializr(见图 5-3 )。
图 5-3。
使用以下数据。
-
组:
com.apress.cloud
-
神器:
config-server
-
Package: com.apress.cloud.configserver
-
依赖关系:配置服务器
接下来,单击 Generate 按钮保存一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中打开该项目。我们来复习一下。打开pom.xml
文件。为了使用所有的 Spring Cloud 特性,有必要使用依赖管理来提供一个云应用中需要的所有 jar(参见清单 5-1 )。
....
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
....
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
....
Listing 5-1.pom.xml Snippet
在撰写本书时,当前版本是 Hoxton.SR7。要保持更新,请访问主 web 项目 https://spring.io/projects/spring-cloud-config
。
接下来,打开ConfigServerApplication
类,用清单 5-2 中的内容完成代码。
package com.apress.cloud.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
Listing 5-2.src/main/java/com/apress/cloud/configserver/ConfigServerApplication.java
清单 5-2 显示了ConfigServerApplication
类。尽管我总是建议将任何配置类作为一个单独的类,但这可能是个例外,因为您只需要使用@EnableConfigServer
注释。这个注释启用了配置服务器,它从配置设置中获取任何新的更改,因此有必要告诉云配置服务器从哪里获取信息。
接下来,打开您的application.properties
文件并添加以下内容(参见清单 5-3 )。
# Default port
server.port=8888
# Spring Config Server
spring.cloud.config.server.git.uri=https://github.com/<github-username>/your-repo-app-config.git
Listing 5-3.src/main/resources/application.properties
清单 5-3 显示了application.properties
,在这里您定义了配置服务器端口(通常,如果您没有指定一个特定的端口,8888 是默认的;同样,这是在客户端)和 GIT 存储库。确保在 GitHub 或任何其他 Git 服务器中创建一个 repo。
就是这样;你不需要做任何其他事情。一旦设置了 repo(现在可以为空),就可以运行配置服务器了。您将在接下来的小节中设置 Git 服务器。
云配置客户端
以下是 Spring Cloud Config 客户端的一些特性。
-
加密和解密属性值(对称和非对称)
-
使用云配置服务器用远程属性初始化 Spring 环境
-
提取任何新的更改,并将它们应用到 Spring 容器中,而无需重新启动应用。如果您需要更改日志、任何 ConfigProperties 或任何标有
@RefreshScope
注释的@Bean
、@Value
,您会受益。
让我们创建一个例子,您可以使用 Spring Cloud Config Server 和 Spring Cloud Config Client。让我们回顾一下我们在第三章中所做的,但是这一次,您将学习一种编程方法,而不是声明性的 XML。
电影应用
让我们用电影和电影网微服务重新创建电影应用。这次您将使用 Spring Integration DSL。这两个应用都使用 Spring Cloud Config Server,这意味着您需要添加 Spring Cloud Config 客户端依赖项。
让我们从创建第一个电影项目开始。打开浏览器,进入 https://start.spring.io
。使用以下信息。
-
组:
com.apress.cloud
-
神器:
movie
-
依赖项:配置客户端、Spring Integration、Spring Boot 执行器、Lombok
点击生成按钮创建一个 ZIP 文件。将其解压缩并导入到您喜欢的 IDE 中(参见图 5-4 )。
图 5-4。
spring 初始化电影微服务
Spring Boot 执行器和龙目岛是新的依赖。Spring Cloud Config Client 允许应用获取配置中的任何新更改,它可以重新创建 Spring bean(@Value
、@Bean
、@ConfigurationProperties
、日志级别),而无需重新启动。对于这种行为,您必须让应用知道它需要获取任何新的配置。要触发它,您需要 Spring Boot 执行器,因为它启用了 JMX Bean exporter refresh
中的一个端点。
Lombok 从主代码中移除样板文件,比如构造函数或 setters 和 getters。如果你想了解更多关于龙目岛的信息,请前往 https://projectlombok.org
。
让我们从添加新的依赖项开始(参见清单 5-4 )。
...
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
...
Listing 5-4.pom.xml Snippet
清单 5-4 显示了其他一些依赖关系。您正在读取一个文件,然后通过 HTTP POST 将它发送到另一个服务。
让我们创建电影类(参见清单 5-5 )。
package com.apress.cloud.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class Movie {
private String title;
private String actor;
private int year;
}
Listing 5-5.src/main/java/com/apress/cloud/movie/Movie.java
清单 5-5 展示了没有设置器、获取器或构造器的新Movie
类。Lombok 通过使用@AllArgsConstructors
和@Data
(创建 getters、setters、toString、hashCode)注释来提供这一点。
接下来,让我们创建MovieProperties
类,它保存关于读取文件的路径、文件模式、防止重复、读取路径的固定延迟以及远程服务器的信息(参见清单 5-6 )。
package com.apress.cloud.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie")
public class MovieProperties {
private String directory;
private String filePattern;
private Boolean preventDuplicates;
private Long fixedDelay;
private String remoteService = "http://localhost:8181/v1/movie";
}
Listing 5-6.src/main/java/com/apress/cloud/movie/MovieProperties.java
清单 5-6 展示了当应用启动时到达配置服务器的MovieProperties
类。它有来自 Git 服务器的值。
接下来,让我们创建MovieConverter
类,它包含电影文件的每一行。它需要读取一行并将其转换成一个Movie
实例(参见清单 5-7 )。
package com.apress.cloud.movie;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@Component
public class MovieConverter implements Converter<String,Movie> {
@Override
public Movie convert(String source) {
log.debug(source);
List<String> fields = Stream.of(source.split(",")).map(String::trim).collect(Collectors.toList());
return new Movie(fields.get(0),fields.get(1),Integer.valueOf(fields.get(2)));
}
}
Listing 5-7.src/main/java/com/apress/cloud/movie/MovieConverter.java
清单 5-7 显示了MovieConverter
,它获取字符串(来自电影 CSV 文件的一行),并返回一个Movie
实例。另外,请注意,我们在该类中将日志记录级别设置为 DEBUG。
接下来,创建使用 Spring Integration DSL(特定于领域的语言)的MovieIntegrationConfiguration
类,它公开了一个 fluent API 来配置所有的集成流。这与使用前几章的 XML 是一样的(参见清单 5-8 )。
package com.apress.cloud.movie;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.file.dsl.Files;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.integration.http.dsl.Http;
import java.io.File;
import java.net.URI;
@AllArgsConstructor
@EnableConfigurationProperties(MovieProperties.class)
@Configuration
public class MovieIntegrationConfiguration {
private MovieProperties movieProperties;
private MovieConverter movieConverter;
@Bean
public IntegrationFlow fileFlow() {
return IntegrationFlows.from(Files
.inboundAdapter(new File(this.movieProperties.getDirectory()))
.preventDuplicates(true)
.patternFilter(this.movieProperties.getFilePattern()),
e -> e.poller(Pollers.fixedDelay(this.movieProperties.getFixedDelay())))
.split(Files.splitter().markers())
.filter(p -> !(p instanceof FileSplitter.FileMarker))
.transform(Transformers.converter(this.movieConverter))
.transform(Transformers.toJson())
.handle(Http
.outboundChannelAdapter(URI.create(this.movieProperties.getRemoteService()))
.httpMethod(HttpMethod.POST))
.get();
}
}
Listing 5-8.src/main/java/com/apress/cloud/movie/MovieIntegrationConfiguration.java
清单 5-8 显示了MovieIntegrationConfiguration
类。我们来分析一下。
-
IntegrationFlow
。这个类是IntegrationFlowBuilder
类的一个工厂,它寻找关于通道、处理器、路由器、分离器和 DSL 中定义的任何其他集成模式的任何信息。因此,它创建了整个 Spring Integration 引擎。 -
File.inboundAdapter
。通常,流总是从入站通道适配器开始。它可能是一个特定的频道。在本例中,我们使用入站文件在指定的目录中查找新文件。入站适配器的一个特性是轮询器,允许您寻找新的传入MessageSource
消息。 -
split
。该方法创建一个拆分器,将有效负载设置为Iterable
、Interator
、Array
、Stream,
或一个反应式Publisher
,并接受拆分器的一个实现;在这种情况下,我们使用的是Files.splitter.markers()
,它将文件内容分成几行,并标记文件的开始和结束。 -
filter
。这个方法过滤我们发送的行。有一个过滤器很重要,因为文件内容的第一行和最后一行是一个标记(START - END
)。 -
transform
。我们使用两台变压器。第一个调用了Transformers.converter
方法,在这里您传递了movieConverter
bean。这个类具有获取文件行、获取逗号分隔值和创建一个Movie
实例的逻辑。另一个转换器将Movie
转换成 JSON 有效载荷。 -
handle
。该处理程序是服务激活器模式端点。这里我们使用了一个Http.outboundChannelAdapter
,它为外部服务准备一个请求,并以 JSON 格式发送带有正确头的Movie
。
你可以说这个 Java DSL 类似于前面章节的声明式 XML,但是你应该选择哪一个呢?这取决于你需要什么,并为你的组织、项目或你自己增加价值。从现在开始,我们将只使用 Spring Integration DSL。
接下来,准备一些文本文件(你可以使用前面章节中的文件)并把它们放在一个已知的文件夹中。运行这个 app 时,可以将这些电影文本文件中的一部分复制到/ tmp/movies
目录路径中;如果您的系统中没有该路径,您可以创建该文件夹。如果您使用的是 Windows 操作系统,请在C:\tmp\movies
中创建它。
接下来,用.yml
扩展名将application.properties
重命名为bootstrap
。按照惯例,您必须在您的客户机中使用一个bootstrap.yml
文件,这一点很重要,因为您的主配置现在驻留在 Git 存储库或您用来集中配置的任何存储中。从现在开始,我们将使用 YAML 文件,它提供了一个简单的层次结构,易于阅读。但是要注意,对于任何新的属性,都需要至少两个空格的缩进。打开文件,添加清单 5-9 中的内容。
spring:
application:
name: movies
jmx:
enabled: true
cloud:
config:
uri:
- http://localhost:8888
Listing 5-9.src/main/resources/bootstrap.yml
清单 5-9 显示了bootstrap.yml
文件(显示了 Spring Cloud Config Server 正在运行的spring.cloud.config.uri
属性)。默认情况下,如果没有指定,Spring Cloud Config Client 总是会查看该地址,所以如果您愿意,可以删除它。此外,application.yml
正在公开spring.application.name
属性,这在您有多个微服务来识别应用时非常有用。需要定位其他微服务,因此声明它很重要。另外,请注意,您正在通过spring.jmx.enabled
属性启用 JMX,这将创建所有 JMX 端点。稍后您将使用刷新端点。
最后,让我们为这个电影微服务创建配置。接下来的步骤基于使用 GitHub ( https://github.com
),但是您可以使用任何其他 Git 服务器。我假设您对 Git 命令有所了解。使用 GitHub 的一个好处是,你可以动态地创建文件并立即提交。因此,请遵循以下步骤。
图 5-5。
https://github.com
movies-config/movies . yml
-
在
https://github.com
登录你的 GitHub 账号(如果你没有,可以创建一个;它对公共库/项目是免费的)。 -
创建一个名为
movies-config
的新存储库。 -
添加/提交包含以下内容的
movies.yml
文件。(在代码页签中有一个创建新文件按钮,可以方便地添加新文件。)(见图 5-5 。)movie: directory: /tmp/movies file-pattern: "*.txt" prevent-duplicates: true fixed-delay: 5000 logging: level: com: apress: cloud: INFO
这些属性在MovieProperties
类中声明(参见清单 5-6 )。暂时不要运行电影微服务(注意,movie.directory
属性指向“/tmp/movies
”)。如果您使用的是 Windows 操作系统,请使用带有转义字符的值like C:\\tmp\\movies
。movie.file-pattern
指定查找任何以扩展名.txt
结尾的文件。稍后您将运行这个微服务。
电影网络应用
让我们来创建电影网络微服务。记住,这个微服务有一个 HTTP POST 端点,允许将 JSON 电影对象保存到数据库中。打开浏览器,在 https://start.spring.io
进入 Spring 初始化。使用以下信息。
-
组:
com.apress.cloud
-
神器:
movie-web
-
包装:
com.apress.cloud.movieweb
-
依赖项:配置客户端,Spring Integration,Spring Web,Spring Actuator,H2,MySQL,Lombok
点击生成按钮创建一个 ZIP 文件。然后解压缩并导入到你喜欢的 IDE 中(见图 5-6 )。
图 5-6。
Spring Initializr 电影-web 微服务
让我们添加新的依赖项(参见清单 5-10 )。
...
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
</dependency>
...
Listing 5-10.pom.xml Snippet
接下来,让我们创建MovieWebProperties
类(参见清单 5-11 )。
package com.apress.cloud.movieweb;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie-web")
public class MovieWebProperties {
private String path;
}
Listing 5-11.src/main/java/com/apress/cloud/movieweb/MovieWebProperties.java
清单 5-11 显示了这个微服务拥有的唯一属性,为接收 HTTP POST 请求而创建的端点路径。
接下来可以创建Movie
类,也是一样。唯一的区别是有一个新的 Lombok 注释(见清单 5-12 )。
package com.apress.cloud.movieweb;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Movie {
private String title;
private String actor;
private int year;
}
Listing 5-12.src/main/java/com/apress/cloud/movieweb/Movie.java
列表 5-12 显示了Movie
类,它使用@NoArgsConstructor annotation
参数创建默认构造函数。
接下来,让我们创建MovieWebIntegrationConfiguration
类,它包含了与使用 Java DSL 的 Spring Integration 流相关的所有内容(参见清单 5-13 )。
package com.apress.cloud.movieweb;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.MessageChannels;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.http.dsl.Http;
import org.springframework.integration.jdbc.JdbcMessageHandler;
import org.springframework.messaging.MessageHandler;
import javax.sql.DataSource;
@AllArgsConstructor
@EnableConfigurationProperties(MovieWebProperties.class)
@Configuration
public class MovieWebIntegrationConfiguration {
private MovieWebProperties movieWebProperties;
@Bean
public IntegrationFlow httpFlow() {
return IntegrationFlows.from(Http
.inboundChannelAdapter(movieWebProperties.getPath())
.requestPayloadType(Movie.class)
.requestMapping(m -> m.methods(HttpMethod.POST)))
.channel(MessageChannels.publishSubscribe("publisher"))
.get();
}
@Bean
public IntegrationFlow logFlow() {
return IntegrationFlows.from("publisher")
.log(LoggingHandler.Level.INFO,"Movie", m -> m)
.get();
}
@Bean
@ServiceActivator(inputChannel = "publisher")
public MessageHandler process(DataSource dataSource){
JdbcMessageHandler jdbcMessageHandler = new JdbcMessageHandler(dataSource,
"INSERT INTO movies (title,actor,year) VALUES (?, ?, ?)");
jdbcMessageHandler.setPreparedStatementSetter((ps, message) -> {
ps.setString(1,((Movie)message.getPayload()).getTitle());
ps.setString(2,((Movie)message.getPayload()).getActor());
ps.setInt(3,((Movie)message.getPayload()).getYear());
});
jdbcMessageHandler.setOrder(1);
return jdbcMessageHandler;
}
}
Listing 5-13.src/main/java/com/apress/cloud/movieweb/MoviewWebIntegrationConfiguration.java
清单 5-13 显示了MovieWebIntegrationConfiguration
类。我们来分析一下。
-
Http.inboudChannelAdapter
。记住,我们的流必须从一些入站适配器开始,在这种情况下,Http.inboundChannelAdapter
创建一个接受任何请求的端点。它使用 HTTP POST 方法只接受 JSON 格式的Movie
类型(默认)。 -
channel
。收到请求后,它将Movie
发送到发布/订阅通道。在这里,可以订阅任何其他集成流。 -
log
。log 方法打印消息,但这应该是发送电子邮件的流。 -
@ServiceActivator
。这是添加处理程序的另一种方式,一种用@ServiceActivator
注释标记的方法。我们使用的是JdbcMessageHandler
,它自动处理Movie
/JSON 并将其插入数据库引擎——非常简单。它返回一个MessageHandler
来关闭流程。
清单 5-13 与第三章中的声明性 XML 相同。唯一不同的是,你不是在发邮件;这里,您正在使用log()
调用。
接下来,您需要创建初始化数据库的schema.sql
文件(参见清单 5-14 )。
create table IF NOT EXISTS movies(
id int not null auto_increment,
title varchar(250),
actor varchar(100),
year smallint,
primary key(id));
Listing 5-14.src/main/resources/schema.sql
正如你所看到的,清单 5-14 是前几章中的同一个文件。接下来,将应用的扩展名改为bootstrap.yml
,并添加清单 5-15 中的内容。
server:
port: 8181
spring:
application:
name: movie-web
Listing 5-15.src/main/resources/bootstrap.yml
清单 5-15 显示了您正在使用的bootstrap.yml
,仅仅是应用的名称和server.port
。记住我们仍然可以使用spring.cloud.config.uri,
,但是如果没有设置这个属性,它会默认连接。接下来,转到 GitHub(或者你的 Git 服务器),添加包含以下内容的movie-web.yml
文件(见图 5-7 )。
图 5-7。
https://github.com
电影-配置/电影-web.yml
spring:
h2:
console:
enabled: true
movie-web:
path: /v1/movie
运行配置服务器、电影和电影 Web 微服务
现在是时候运行这些应用了。让我们按这个顺序运行微服务:(1)配置服务器,(2)电影网络,和(3)电影。
图 5-8。
http://localhost:8181/h2-console
moview-web 微服务
-
首先,运行 config-server 微服务。如果您使用的是 Linux,请打开一个终端并使用以下命令运行它。
$ ./mvnw clean spring-boot:run
If you are using Windows, run it with the following.
> mvwn.bat clean spring-boot:run
这是对运行在端口 8888 的 GitHub 帐户和存储库的
config-server
查找。 -
接下来,运行电影-网络微服务。它基于在 GitHub 存储库中创建的
movie-web.yml
配置文件,在端口 8181 上运行。您可以使用相同的命令来运行 movie-web 微服务。这个微服务使用 8888 端口的 config-server,它根据 app 的名称拉取配置;因此,在这种情况下,它查找movie-web.yml
,因为它与在spring.application.name
属性中设置的应用同名。When running the movie-web app, look at the first logs printed. You see something like the following.
Fetching config from server at : http://localhost:8888 Located environment: name=movie-web, profiles=[default], label=null, version=a55fa1d67ebcc6f4d509f71ba9619f4fc2a116c8, state=null
这意味着它成功地连接到了 config-server 微服务,并从 GitHub 服务器加载了 movie-web 配置。它有一个
/v1/movie
端点,在端口 8181 中运行,并允许您在/h2-console
端点中使用 H2 控制台。 -
接下来,运行电影微服务。在运行它之前,请确保您已经准备好了电影文本文件。(如果您从 Apress 下载源代码,请注意,我将它们添加到了项目的根目录中,因此您可以轻松地重用它们)。
运行时,它出现在连接到端口 8888 的配置服务器微服务的日志中。
-
现在,是时候发送一些电影文件了。如果您使用的文件来自该书的源代码,请将
movies-batch1.txt
复制到/tmp/movies
文件夹中。这会读取文件并将 JSON 发送到 movie-web 微服务。在 movie-web 微服务中,您可以在日志中看到电影被插入到 H2 数据库中。如果你查看http://localhost:8181/h2-console
(电影网络微服务),你应该看到电影已经被插入。JDBC 网址为jdbc:h2:mem:testdb
(默认值),用户名为sa
,密码为空(见图 5-8 )。
恭喜,您刚刚运行了 config-server 、,它根据您的应用名称查找配置,以及使用 config-server 获取配置的电影和电影网络微服务。
更改日志记录级别
如果您关注电影《微服务》及其配置,您会看到在 GitHub 存储库中日志级别被设置为 INFO。如果你想改变登录级别会发生什么?通常,您会停止应用,将日志记录级别更改为 DEBUG,然后重新运行应用。嗯,Spring Cloud Config Client 允许你在不重启应用的情况下改变日志级别——所以不要停止电影微服务。让我们看看如何在不重启的情况下应用这些更改。
图 5-9。
jconsole . jconsole
-
修改
movie.yml
文件(来自 GitHub ),并将日志级别从 INFO 改为 DEBUG。GitHub 有一个按钮(铅笔图标),可以让你在线修改文件。或者,在您的计算机中克隆 repo,并使用 Git 命令来推动这一新的变化。 -
提交更改后,打开终端并执行
jconsole
命令。这带来了允许您连接到 JMX 协议的 JConsole。从列表中选择com.apress.cloud.movie.MovieApp
(见图 5-9 )。
图 5-10。
mbean org . spring framework . boot➤端点➤刷新➤操作
-
单击不安全连接按钮。接下来,单击 mbeans 选项卡并展开 org.springframework.boot ➤端点。选择操作/刷新图标(参见图 5-10 )。
-
单击刷新按钮。看来
logging.level.com.apress.cloud
属性发生了变化。在电影微服务日志中,您可以看到关于刷新范围和 beans 的输出。这意味着日志记录级别也发生了变化。 -
将另一个文件复制到
/tmp/movies
文件夹(例如movies-batch2.txt
)。现在你看到了电影微服务日志中的调试。
恭喜你!您在没有重启应用的情况下更改了日志记录级别,但是还有更多。您可以更改记录和标记有@ConfigurationProperties
的类别以及所有标记有@RefreshScope
的@Value
或@Bean
。这告诉 Spring 容器需要在不重启应用的情况下重新创建这些 beans。
当然,这里我们使用了 JMX,但是它也允许您拥有一个 web 端点,因此您可以向/actuator/refresh
端点发送 POST。要启用 web 端点,您需要三样东西:您的pom.xml
文件中的 spring-web 和 spring-actuator 依赖项,以及用management.endpoints.web.exposure.include=*
公开的端点。然后,使用以下命令进行任何更改。
$ curl localhost:8181/actuator/refresh -XPOST -H "Content-Type: application/json"
如果 movie-web 微服务宕机会怎么样?一种解决方案是创建几个实例。如果一个坏了,我们就可以访问另一个。但是,要获得可用电影 web 实例的列表,您需要做些什么呢?如果其中一个改变了端口会怎么样?或者,如果有一个人倒下了会发生什么?你如何去下一个可用的?
这些都是新问题。您需要一些东西来记录可用的服务以及如何连接到它们。您需要一个允许您注册服务的解决方案。好消息是有一个解决方案——尤里卡服务注册中心。
SpringCloud 网飞
SpringCloud 网飞是网飞 OSS ( https://netflix.github.io/
)在 Spring Boot apps 内的集成。它支持基于 Spring Framework 生态系统为开发人员提供的相同模式的自动配置和包装类。Spring Cloud 网飞具有创建分布式应用的注释功能。它提供了服务发现、断路器、路由和客户端负载平衡等模式实现。以下是它的一些特点。
-
尤里卡服务器是服务发现模式的实现。微服务实例可以注册并被其他微服务发现。Is 还提供了一种添加多个 Eureka 服务器的方法,这些服务器可以在它们之间注册以提供必要的冗余。它提供了支持嵌入式服务发现的
@EnableEurekaServer
。 -
尤里卡客户端提供了一个可发现的模式,用于注册尤里卡服务器并提供有用的信息,比如 URL 和端口。它提供了注册微服务的
@EnableDiscoveryClient
注释。 -
断路器是一种为您的应用提供容错场景的模式。它提供了
@EnableCircuitBreaker
和@HistirixCommand
注释,它们是简单的装饰器,具有创建容错微服务的功能。 -
Eureka Ribbon 是客户端负载平衡模式的实现。它提供了通常放在
RestTemplate
实例中的@LoadBalanced
注释;这为在 Eureka Server 中注册的多个微服务创建了所需的客户端负载平衡。 -
网飞·祖尔是创建代理的路由和过滤模式的实现。
-
外部配置提供了一种直接与网飞 Archaius 通信的方式,以提供类似于 Spring Cloud Config Server 的 Spring 环境。
服务发现:Eureka 服务注册
Eureka service registry 提供了服务发现模式的实现,这是最重要的微服务架构特性之一(参见图 5-11 )。
图 5-11。
服务注册中心
当客户机向服务注册中心注册时,它提供关于其主机和端口的元数据;它还向服务注册中心发送心跳。从服务器到客户端,一切都在内存(元数据)中。
尤里卡服务器
让我们创建尤里卡服务器微服务。进入你的浏览器,打开 https://start.spring.io
。使用以下信息。
-
组:
com.apress.cloud
-
神器:
eureka-server
-
Package: com.apress.cloud.eurekaserver
-
依赖关系:尤里卡服务器
按“生成”按钮创建并下载一个 ZIP 文件。您可以将其解压缩并导入到您喜欢的 IDE 中(参见图 5-12 )。
图 5-12。
https://start.spring.io
尤里卡-服务器
查看pom.xml
文件。您只有一个依赖项,足以创建一个服务注册解决方案。接下来打开主类,EurekaServerApplication.java
。添加@EnableEurekaServer
注释(见清单 5-16 )。
package com.apress.cloud.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Listing 5-16.src/main/java/com/apress/cloud/eurekaserver/EurekaServerApplication.java
清单 5-16 显示了主类。您唯一需要让 service registry 应用做的事情就是使用@EnableEurekaServer 注释,并且您需要使用application.properties
文件来配置它。
接下来,将application.properties
扩展名改为.yml
并打开。添加清单 5-17 中的内容。
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Listing 5-17.src/main/resources/application.yml
现在,您已经准备好运行它了。您可以使用以下命令。
$ ./mvnw clean spring-boot:run
打开一个新的浏览器选项卡并转到http://localhost:8761
(参见图 5-13 )。
图 5-13。
Eureka 服务器位于 http://localhost:8761
尤里卡客户
既然您已经启动并运行了服务注册中心(Eureka Server ),那么是时候注册它了。首先,添加一个可被发现的依赖项。先说影网微服务。
服务可发现:电影 Web 微服务
在你喜欢的 IDE 中打开你的 movie-web 微服务,打开pom.xml
。添加以下依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
接下来,打开application.yml
,将其重命名为bootstrap.yml
,并添加以下属性。
eureka:
client:
service-url:
default-zone: http://localhost:8761/eureka/
您可以将它们添加到 GitHub(或任何使用 Spring Cloud Config Server 的 Git 服务器)。您可以在 Git 存储库中修改movie-web.yml
并添加属性。前面的代码告诉 Eureka 客户机在哪里可以找到 Eureka 服务器。
您可以使用以下命令运行 movie-web 微服务。
$ ./mvnw spring-boot:run
在终端中,您应该看到类似于以下输出的日志。
com.netflix.discovery.DiscoveryClient : DiscoveryClient_MOVIE-WEB/10.0.0.2:movie-web:8181 - registration status: 204
接下来,您可以在浏览器中刷新 Eureka 服务器。然后,您应该会看到列出了 movie-web 微服务(见图 5-14 )。
图 5-14。
尤里卡服务器注册的应用
在这种情况下,您希望拥有多个 movie-web 微服务实例。打开一个新的终端窗口,并执行以下命令。
$ ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=8282"
前面的命令运行另一个实例,但使用端口 8282。现在,如果您再次刷新 Eureka Server,您会看到两个实例启动并运行,并带有它们的端口(参见图 5-15 )。
图 5-15
尤里卡服务器注册的应用
如您所见,这对客户来说很容易。您只需要添加 eureka-client 依赖项并指向 Eureka Server。Spring Boot 为您运行所有的自动配置,并在注册服务器中注册。
发现服务:电影微服务
既然我们已经将 movie-web 微服务设置为可被发现,现在是 movie 微服务使用服务实例的时候了。所以,我们来修改一下电影微服务项目。在您最喜欢的 IDE 中打开项目,让我们进行修改。打开pom.xml
文件并添加以下依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
如您所见,这与您添加到 movie-web 微服务的依赖项相同。您需要连接到 Eureka 而无需注册,因为您只需要知道其他实例。所以,我们来修改一下bootstrap.yml
文件。添加以下属性。
eureka:
client:
register-with-eureka: false
service-url:
default-zone: http://localhost:8761/eureka/
register-with-eureka
属性是您唯一需要的。您可以省略service-url
,因为它默认连接到那个 URL,除非您在一个远程服务器和不同的端口上有 Eureka 服务器。
接下来,我们来开MovieIntegrationConfiguration
课。如果您用清单 5-18 中的内容修改它,这就是版本 2。
package com.apress.cloud.movie;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.file.dsl.Files;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.web.client.RestTemplate;
import java.io.File;
// Version 2 - Eureka Client
@AllArgsConstructor
@EnableConfigurationProperties(MovieProperties.class)
@Configuration
public class MovieIntegrationConfiguration {
private MovieProperties movieProperties;
private MovieConverter movieConverter;
@Bean
public IntegrationFlow fileFlow() {
return IntegrationFlows.from(Files
.inboundAdapter(new File(this.movieProperties.getDirectory()))
.preventDuplicates(true)
.patternFilter(this.movieProperties.getFilePattern()),
e -> e.poller(Pollers.fixedDelay(this.movieProperties.getFixedDelay())))
.split(Files.splitter().markers())
.filter(p -> !(p instanceof FileSplitter.FileMarker))
.transform(Transformers.converter(this.movieConverter))
.handle("movieHandler", "process")
.get();
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public MovieHandler movieHandler(EurekaClient discoveryClient){
InstanceInfo instance = discoveryClient.getNextServerFromEureka("MOVIE-WEB", false);
return new MovieHandler(restTemplate(),instance.getHomePageUrl() + "v1/movie");
}
}
Listing 5-18.src/main/java/com/apress/cloud/movie/MovieIntegrationConfiguration.java Version 2
清单 5-18 显示了MovieIntegrationConfiguration
类的版本 2。分析它以比较与清单 5-8 的差异。我们来复习一下。
-
handle
。该方法调用正在声明的MovieHandler
bean。如果您查看该类,您会看到方法进程作为第二个参数被传递。 -
RestTemplate
。这个实例对于连接到微服务很有用。 -
MovieHandler
。这个 bean 使用EurekaClient
实例来获取下一个可用的实例地址,以便restTemplate
可以发出请求。
接下来,让我们创建MovieHandler
类(参见清单 5-19 )。
package com.apress.cloud.movie;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
@Slf4j
@AllArgsConstructor
public class MovieHandler {
private RestTemplate restTemplate;
private String serviceUrl;
public void process(Movie movie){
log.debug("Processing: {}", movie);
log.debug("ServiceURL: {}", serviceUrl);
ResponseEntity<Object> response = this.restTemplate.postForEntity(serviceUrl,movie,Object.class);
if(response.getStatusCode().equals(HttpStatus.OK))
log.debug("processed");
else
log.warn("Take a look of the logs...");
}
}
Listing 5-19.src/main/java/com/apress/cloud/movie/MovieHandler.java
清单 5-19 显示了MovieHandler
类,它使用RestTemplate
并将Movie
作为 JSON 对象从 Eureka 服务器发送到服务 URL。
现在,调用getNextServerFromEureka
并不理想,因为您需要再次调用它来获得另一个可用的实例。还能更简单吗?
Ribbon:客户端负载均衡电影微服务
为了避免直接使用 Eureka Client,Spring Cloud 团队将网飞丝带项目带到了下一个层次。网飞 Ribbon 项目是一个 IPC(进程间通信)库,为他们的服务创建,允许他们使用许多功能进行客户端负载平衡,这些功能可能是硬件负载平衡器难以实现的。
Ribbon 提供服务发现集成,意味着如果你想获得所有可用的服务,你需要有 Eureka 服务器;否则,您需要创建一个服务器列表。Ribbon 是容错,这意味着它知道服务器何时启动并运行,并且它可以检测哪些服务器关闭。Ribbon 提供了可以扩展的负载平衡规则。默认情况下,它提供了一个循环规则、一个可用性过滤规则和一个加权响应时间规则。
要使用 Ribbon,您必须添加spring-cloud-starter-netflix-ribbon
依赖项,但是因为您已经添加了spring-cloud-starter-netflix-eureka-client
依赖项,所以这不是必需的。此外,Spring Cloud 团队提供了一个@LoadBalanced
注释,该注释收集服务器列表并应用(除非配置)循环规则。
让我们修改一下MovieIntegrationConfiguration
类。这是版本 3(见清单 5-20 )。
package com.apress.cloud.movie;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.file.dsl.Files;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.web.client.RestTemplate;
import java.io.File;
// Version 3 - With Load Balancer - Ribbon
@AllArgsConstructor
@EnableConfigurationProperties(MovieProperties.class)
@Configuration
public class MovieIntegrationConfiguration {
private MovieProperties movieProperties;
private MovieConverter movieConverter;
@Bean
public IntegrationFlow fileFlow() {
return IntegrationFlows.from(Files
.inboundAdapter(new File(this.movieProperties.getDirectory()))
.preventDuplicates(true)
.patternFilter(this.movieProperties.getFilePattern()),
e -> e.poller(Pollers.fixedDelay(this.movieProperties.getFixedDelay())))
.split(Files.splitter().markers())
.filter(p -> !(p instanceof FileSplitter.FileMarker))
.transform(Transformers.converter(this.movieConverter))
.handle("movieHandler", "process")
.get();
}
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public MovieHandler movieHandler(){
return new MovieHandler(restTemplate());
}
}
Listing 5-20.src/main/java/com/apress/cloud/movie/MovieIntegrationConfiguration.java - version 3
清单 5-20 显示了MovieIntegrationConfiguration
类的版本 3。主要的变化是现在@LoadBalanced
注释在RestTemplate
实例中,而MovieHandler
只需要RestTemplate
。
接下来,让我们修改MovieHandler
(版本 2)类(参见清单 5-21 )。
package com.apress.cloud.movie;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
// Version 2 - Withe Ribbon
@Slf4j
@AllArgsConstructor
public class MovieHandler {
private RestTemplate restTemplate;
public void process(Movie movie){
log.debug("Processing: {}", movie);
ResponseEntity<Object> response = this.restTemplate.postForEntity("http://movie-web/v1/movie",movie,Object.class);
if(response.getStatusCode().equals(HttpStatus.OK))
log.debug("processed");
else
log.warn("Take a look of the logs...");
}
}
Listing 5-21.src/main/java/com/apress/cloud/movie/MovieHandler.java (Version 2)
清单 5-21 显示了MovieHandler
类的版本 2。唯一的区别是它现在只使用了RestTemplate
,尽管它使用的是硬编码的 URL(这可以在 Config Server/Git Server 中设置),但它对我们的例子很有用。这解释了很多,但是让我们玩所有的服务。
一起跑步
让我们得到所有的服务。您有需要启动和运行的配置服务器、eureka 服务器、movie-web(两个实例)和电影微服务。您需要打开几个终端,或者如果您使用任何智能 IDE,您可以一起运行它们。
-
启动配置服务器和 eureka 服务器微服务。您可以在单独的终端窗口中使用以下命令运行它们。
./mvnw clean spring-boot:run
-
启动电影网络微服务实例。运行您添加了网飞尤里卡客户端作为依赖项的版本。您可以使用下面的代码运行第一个实例。
./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=8181"
Run the second one with the following in a different terminal window.
./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=8282"
等到这两个实例出现在 Eureka 服务器中。在您的浏览器中,转到
http://localhost:8761
(见图 5-15 以确保它们已注册)。 -
开始电影微服务。使用版本 3 的
MovieIntegrationConfiguration
级(见清单 5-20 )和版本 2 的MovieHandler
(见清单 5-21 )。./mvnw clean spring-boot:run
-
您可以将任何电影文本文件(来自源代码)添加到
/tmp/movies
目录中。您应该看到 movie-web 实例 1 接收了一些电影,实例 2 接收了其他电影。
恭喜你!您已经运行了几个具有云原生环境需求的微服务,包括独立部署、多实例、服务注册、服务发现和集中配置。
您是否意识到每个电影 web 实例都有自己的电影数据库?这里您需要添加一个集中式数据库,所以将所有内容保存在一个持久性存储中。
如果所有的电影网络实例都关闭或响应时间过长,会发生什么?你应该记住容错。
断路器
这是断路器模式的一种实现,可防止级联故障并提供回退行为,直到故障服务处于正常状态(见图 5-16 )。
图 5-16。
断路器
当您将断路器应用于服务时,它会监控失败的呼叫。如果这些故障达到一定的阈值(这可以通过编程来设置),断路器将打开并将呼叫重定向到指定的回退操作。这为故障服务提供了恢复时间。这种模式实现基于网飞的 Hystrix,Spring Cloud 团队通过注释实现了这一点。
Hystrix:电影微服务
如果想在电影《微服务》中有容错,就要在pom.xml
文件中加入正确的依赖关系。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
接下来,您需要将@EnableCircuitBreaker
添加到MovieIntegrationConfiguration
类中(参见下面的代码片段)。
@EnableCircuitBreaker
@AllArgsConstructor
@EnableConfigurationProperties(MovieProperties.class)
@Configuration
public class MovieIntegrationConfiguration {
private MovieProperties movieProperties;
private MovieConverter movieConverter;
@Bean
public IntegrationFlow fileFlow() {
//....
还有另一个版本的MovieHandler
类(版本 3)(参见清单 5-22 )。
package com.apress.cloud.movie;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
// Version 3 - With Circuit Breakers
@Slf4j
@AllArgsConstructor
public class MovieHandler {
private RestTemplate restTemplate;
public void process(Movie movie){
log.debug("Processing: {}", movie);
if(postMovie(movie))
log.info("PROCESSED!");
}
@HystrixCommand(fallbackMethod = "defaultProcess")
public boolean postMovie(Movie movie){
ResponseEntity<Object> response = this.restTemplate.postForEntity("http://movie-web/v1/movie",movie,Object.class);
if(response.getStatusCode().equals(HttpStatus.OK))
return true;
return false;
}
public boolean defaultProcess(Movie movie){
log.error("COULD NOT process: {}, please try later.", movie);
return false;
}
}
Listing 5-22.src/main/java/com/apress/cloud/movie/MovieHandler.java Version 3
清单 5-22 显示了第 3 版的MovieHandler
类(注意,我们使用了@HystrixCommand
注释,并使用了defaultProcess
方法作为万一出现错误时的后备方法)。如果您尝试使用的服务不可用,它将使用defaultProcess
方法,直到服务重新启动并运行。在幕后,@HystrixCommand
注释是一个拦截器,它有一个 try/catch 实现,并创建监控线程来保持对服务的 ping,因此它知道什么时候它再次可用。此外,它还创建了一个度量流,可以用图表来显示行为。要使用这些实时指标,您需要向电影微服务中的bootstrap.xml
添加以下属性。
management:
endpoints:
web:
exposure:
include:
- hystrix.stream
- health
这暴露了/actuator/hystrix.stream
,它可以基于数据呈现一个图形。在你用这些改变运行电影微服务之前,你需要添加spring-boot-starter-web
,因为需要一个启动器的配置中缺少了一些东西。因此,将spring-boot-starter-web
依赖项添加到您的pom.xml
文件中。如果您想知道是否有一个构建 UI 来查看这些指标——有。它叫做 Hystrix 仪表板。
Hystrix Dashboard 监控并提供大量关于您的服务的指标。您可以创建另一个微服务(在 https://start.spring.io
),只添加spring-boot-actuator
和spring-cloud-starter-hystrix-dashboard
依赖项,并使用@EnableHystrixDashboard
注释来启用它。运行它,然后访问/hystrix
。将仪表板指向 Hystrix 客户端应用中的单个实例的/actuator/hystrix.stream
端点(参见图 5-17 和 5-18 )。
图 5-17。
http://localhost:8000/Hystrix 上的 hystrix 仪表板
在这里,您添加由执行器提供的微服务电影端点,类似于http://localhost:8080/actuator/hystrix.stream
,并提供一个标题,例如电影。为了让它工作,你需要发送电影标题到/tmp/movies
文件夹。然后停止 movie-web 实例并重试。你应该会看到类似于图 5-18 的东西。打开断路器。
图 5-18。
Hystrix 仪表板
如果您想了解更多关于该仪表板的信息,请访问 https://spring.io/projects/spring-cloud
。
Spring Cloud project 提供其他服务,但那是另一本书的内容。现在,您拥有了创建微服务解决方案的工具。
关于反应式编程
我已经向您展示了一些 Spring Cloud 子项目,它们解决了一些 12 因素指导方针,并转向微服务架构,但是反应式编程适合哪里呢?
请记住,微服务的一个重要特性是能够与其他微服务和遗留系统进行通信。想象一下,你的微服务应用需要同时访问几个系统,而你已经有了一个客户端,它发出几个调用来聚合一切。在某些时候,这个应用变得非常健谈(网络延迟、并发、阻塞等)。),而且你也不是只有一个客户提出这种要求。你会收到数百万个请求。
反应式编程使用一种特殊的模式解决了这个问题:API 网关。
Observable<MarketExchangeRates> details = Observable.zip(
localService.getExchangeRates("usd"),
yahooFinancialService.getGlobalRates("mxn","jpy"),
googleFinancialService.getEuropeExchangeRates(),
(local, yahoo, google) -> {
MarketExchangeRates exchangeRates = new
MarketExchangeRates();
exchangeRates.setLocalMarket(local.getRates());
exchangeRates.setEurope(google.getRates({"eur","gpb"}));
exchangeRates.setGlobal(yahoo.getRate());
return exchangeRates;
}
);
使用这段代码,您可以执行几个并行任务,并避免网络跳跃、延迟、并发和阻塞等资源。还有,像雅虎这样的服务!金融服务使用配置服务,可以将自己注册到服务注册表,并在出现故障时使用默认方法(例如断路器)。
摘要
本章讨论了微服务架构以及设计云原生应用的挑战。我向您展示了 Spring Cloud 子项目如何帮助您利用 Spring Boot 的强大功能创建快速简单的云原生应用。这是一项令人敬畏的技术,它使微服务易于开发。
六、Spring CloudStream
在前面的章节中,我讨论了一些 Spring Cloud 项目,如 Spring Cloud Config Server、Eureka Server、Ribbon 和 Hystrix(断路器模式的一种实现),以强调微服务必须解决一些问题,如外部化配置、在故障中可被发现、客户端负载平衡、容错、可靠性和崩溃后自动恢复。多亏了 Spring Cloud 模块,您可以解决这些问题以及更多问题。
到目前为止,您已经看到了通过 REST 接口相互对话的微服务。在前面的章节中,您使用了RestTemplate
从一个微服务到另一个微服务进行通信。但是,如果您需要使用不同的协议或中间件代理(如 JMS、AMQP、WebSocket、STOMP 或 MQTT)进行通信,会发生什么情况呢?做任何整合都需要学习每一项技术吗?我的意思是,有很多新技术出现,促进了快速、可靠和容错。现在有没有什么解决方案或技术可以让你不用学习就可以使用中间件来集成应用或系统?对,SpringCloudStream!
在本章中,您将使用 Spring Cloud Stream Application Starters 来了解无需应用一行代码就能使用任何中间件代理是多么容易。然后当你被介绍到架构和编程模型时,你就会看到 Spring Cloud Stream 是如何工作的。创建一个与其他技术集成的定制流是非常容易的。
Spring Cloud Stream 应用启动程序
Spring Cloud Stream 应用启动器是独立的可执行程序,它们使用消息传递(例如 HTTP)、中间件(例如 RabbitMQ、Apache Kafka、Kafka Stream 或 Amazon Kinesis)或存储解决方案(例如关系型、非关系型、文件系统)相互通信或与外部系统或应用通信。当然也支持其他经纪人,包括 Google PubSub、Solace PubSub+、Azure Event Hubs、Apache RocketMQ 社区提供了所有这些。
以下是 Application Starters 的一些特性。
-
独立的可运行的 Spring Boot 应用使它们有主见、健壮和灵活。基于 Spring Boot,这些应用可以通过命令行、环境变量、YAML 文件和 Spring Cloud 配置来覆盖其默认配置(该功能已经集成在应用中)。
-
这些应用基于企业集成模式,这意味着它们基于 Spring Integration,使它们为企业做好了准备。
-
它们可以用作 Maven 或 Docker 工件,通过 Spring CloudStream 创建流管道。
-
它们提供用于任何中间件技术(任何代理)、存储解决方案(SQL、NoSQL、文件系统)的源、处理器和接收器连接器,或者简单地作为 HTTP 请求/响应。源正在从任何 so 源接收数据,并将有效载荷转发到输出通道。处理器封装了信源和信宿,因为它从输入通道接收数据,处理有效载荷,并将其发送到输出通道。接收器从输入通道获取有效载荷。
-
它们为各种网络协议提供适配器。
-
这些应用基于 Spring Web,因此它们默认打开端口 8080,但这可以通过在运行时使用环境变量、在命令行或在
application.properties
文件中设置server.port
属性来轻松覆盖。 -
它们提供用 SpEL (Spring Expression Language)或任何其他脚本语言(如 Groovy 或 Python)定制的通用处理器。
通道是向中间件消息传递或存储公开数据的主要方法。中间件(不考虑协议)和连接应用的模块被称为绑定器。RabbitMQ、Kafka、Kafka Streams 和 Amazon Kinesis 是 Spring Cloud 团队支持的主要预构建绑定器。
在我们开始一个简单的例子之前,您需要知道一些应用启动器的命名约定。你可以在 Spring 仓库中找到它们,并在 https://repo.spring.io/release/org/springframework/cloud/stream/app/
以 jar 的形式下载。命名约定为 <功能> - <类型> - <绑定>-<版本>。在下一个例子中,您使用一个源(一个 HTTP 应用)和一个接收器(一个日志应用),它们通过 RabbitMQ(我们的绑定器)连接,因此您需要找到以下名称。
-
http-source-rabbit
是一个 HTTP Spring 云应用启动器(源码),使用 RabbitMQ 作为绑定器。它打开一个 HTTP 端口来监听传入的请求,并将有效负载转发给 RabbitMQ(输出通道)。 -
log-sink-rabbit
是一个 log-sink Spring Cloud 应用启动器,它使用 RabbitMQ 作为绑定器来监听新的输入消息(输入通道)并将它们记录到控制台中。
在本章中,您将使用几个现成的 Spring 云应用启动器。
也可以使用 also Docker,命名约定相同: springcloudstream/ <功能性> - <类型> - <绑定器> : <版本> 。
大约有 70 个经过验证的应用启动器,分为源、处理器和接收器,其中一些可以连接外部系统,如 Hadoop、GemFire、TensorFlow、Python 和 Groovy 脚本;但是让我们从第一个例子开始。
HTTP Source | Log-Sink 示例
让我们从使用现成可用的应用开始。有不同的方法可以获得这些应用;一种是在 https://github.com/spring-cloud-stream-app-starters
直接去 GitHub 下载源代码并编译它们。
./mvnw clean install -PgenerateApps
在本例中,您向 HTTP 源应用发送一条 JSON 消息,该应用将其发送给 binder (RabbitMQ),该消息由 log-sink 应用使用,并将其记录到控制台中(参见图 6-1 )。
图 6-1。
SpringCloud App 首发 HTTP|log
使用优步罐
您正在使用预构建的超级罐子。让我们一步一步来。
-
确保您的系统至少安装了 Java 8。本书中的所有例子我都运行 Java 8。
-
从
https://repo.spring.io/release/org/springframework/cloud/stream/app/
下载罐子。寻找http-source-rabbit
和log-sink-rabbit
文件夹。可以用最新版本(写这本书的时候版本分别是 2.1.1.RELEASE 和 2.1.2.RELEASE)。将它们保存在工作区目录中。 -
你需要使用 RabbitMQ 作为绑定器,所以如果你已经安装了一个,你可以使用那个,或者如果你需要安装,你可以从
www.rabbitmq.com/download.html
下载。这取决于 Erlang,所以请确保您也拥有它。在本书中,我将 Docker 用于一些技术。如果需要安装 Docker,我推荐使用 Mac 或 Windows 的话从www.docker.com/products/docker-desktop
安装 Docker 桌面。如果您使用的是 Linux,请遵循https://runnable.com/docker/install-docker-on-linux
中的安装信息。To use a RabbitMQ Docker image, open a terminal window and execute the following command.
$ docker run -d --rm --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.8.3-management-alpine
使用用户名/密码
guest
/guest
进入https://localhost:15672
,确保 RabbitMQ 启动并运行。 -
Next, let’s run the HTTP app with the following command (make sure you are in the directory where the two uber-jars are).
$ java -jar http-source-rabbit-2.1.1.RELEASE.jar \ --spring.cloud.stream.bindings.output.destination=http \ --server.port=9090
前面的命令启动 HTTP 源应用,该应用监听端口 9090 中的传入数据。请注意,您正在添加一个属性:
spring.cloud.stream.bindings.output.destination=http
。这个属性告诉应用使用http
输出通道作为转发输入负载的目的地。我稍后将讨论这些属性。如果不添加destination
属性,默认情况下应用使用output
属性。 -
Open a new terminal window and run the log-sink app by using the following command.
$ java -jar log-sink-rabbit-2.1.2.RELEASE.jar \ --spring.cloud.stream.bindings.input.destination=http \ --server.port=8090
前面的命令在端口 8090 运行 log-sink 应用,其目的地是
http
(与 HTTP 源应用相同)。如果您不添加这个属性,那么它将采用默认值input
,但是您永远也不会收到消息,因为没有人将它发送到输入目的地。这就是为什么它需要与 HTTP 源相同(见图 6-1 )。 -
接下来,打开一个新的终端发布一些消息。执行以下命令。
$ curl -XPOST -H "Content-Type: application/json" -d '{"msg": "Hello App Starters"}}' http://localhost:9090 $ curl -XPOST -H "Content-Type: application/json" -d '{"msg": "This is awesome"}}' http://localhost:9090 $ curl -XPOST -H "Content-Type: application/json" -d '{"msg": "Hello Spring Cloud"}}' http://localhost:9090
您应该在 log-sink 终端中看到该消息。
恭喜你!您已经运行了您的第一个 Spring Cloud 应用启动器解决方案!尽管这只是一个简单的例子,但是您可以在日常解决方案中使用 Application Starter,比如从文件系统中读取数据,或者处理消息并将其保存到数据库中。是的,您可以使用应用启动器来实现这一点!
有许多方法可以运行应用启动器。它们基于 Spring Boot,这意味着您可以覆盖默认设置,如使用命令行参数、环境变量、application.properties
文件和 Spring Cloud Config server。当您运行 Spring Cloud Application Starters 应用时,您会在日志中发现以下输出(在开头)。
Fetching config from server at : http://localhost:8888
这意味着,如果你运行前面章节中的配置服务器,并为这些应用创建属性,那么不使用任何额外的参数,只使用java -jar
就可以更容易地运行它们。
$ cd ch05/config-server
$ ./mvnw spring-boot:run
例如,对于http-source-rabbit
app starter,可以在 GitHub 中创建具有以下属性的http-source.yml
文件。
spring:
cloud:
stream:
bindings:
output:
destination: http
server:
port: 9090
对于log-sink-rabbit
app starter,您需要在 GitHub 存储库中创建log-sink.yml
文件,内容如下。
spring:
cloud:
stream:
bindings:
input:
destination: http
server:
port: 8090
如下运行应用,不需要任何参数。
$ java -jar http-source-rabbit-2.1.1.RELEASE.jar
$ java -jar log-sink-rabbit-2.1.2.RELEASE.jar
您正在使用 Spring Cloud 及其模块的强大功能。
使用 Docker
Application Starters 用于下一个示例。(我使用 Unix 命令。如果您使用的是 Windows 操作系统,请在使用路径时查找正确的翻译。)首先,看一下图 6-2 ,看看如何创建它。您需要 Docker 映像,因为您正在添加处理器。如果msg
键包含一个Hello
值,一个过滤处理器过滤你的信息。然后,它使用 Groovy 转换处理器将值设置为大写(您创建一个 Groovy 脚本来转换msg
值)。
图 6-2。
使用 Docker HTTP |过滤器|转换|日志
图 6-2 显示了本例的解决方案。让我们一步一步来。
-
首先,确保你的 RabbitMQ 已经停止。如果您正在使用 docker,请确保您已从 Docker 进程列表中删除。在这个场景中,您需要确保 RabbitMQ 与应用启动器运行在同一个网络中。
-
使用以下命令创建一个 Docker 网络。
$ docker network create app-starters
-
Next, put the RabbitMQ Docker image in the same network. Execute the following command.
$ docker run -d --rm \ --name rabbit \ --network app-starters \ --hostname rabbit \ rabbitmq:3.8.3-management-alpine
注意,这是
app-starters
网络。在执行命令之前,我使用了\
符号来指定还有一行。 -
Next, run the HTTP source.
docker run -d \ -p 8080:8080 \ --name http \ --rm \ --network app-starters \ --env SPRING.CLOUD.STREAM.BINDINGS.OUTPUT.DESTINATION='http' \ --env SPRING.RABBITMQ.HOST='rabbit' \ springcloudstream/http-source-rabbit:2.1.1.RELEASE
请注意,这些属性被用作环境变量。这是唯一一个向我们的主机公开 8080 的容器,所以很容易发布消息。
-
Next, run the filter processor.
docker run -d \ --name filter \ --rm \ --network app-starters \ --env SPRING.CLOUD.STREAM.BINDINGS.INPUT.DESTINATION='http' \ --env SPRING.CLOUD.STREAM.BINDINGS.OUTPUT.DESTINATION='transform' \ --env FILTER.EXPRESSION="#jsonPath(payload,'$.msg').contains('Hello')" \ --env SPRING.RABBITMQ.HOST='rabbit' \ springcloudstream/filter-processor-rabbit:2.1.1.RELEASE
过滤处理器让包含单词“
Hello
”的消息通过。它使用带有内置jsonPath
对象的 SpEL 来操作 JSON 对象。我们还声明了输入和输出目的地的属性;处理器总是需要这两者。 -
Next, in your working directory, create the
transform.groovy
script with the following content.import groovy.json.JsonSlurper import groovy.json.JsonOutput def jsonSlurper = new JsonSlurper() def json = jsonSlurper.parseText(new String(payload)) json.msg = json.msg.toUpperCase() JsonOutput.toJson(json)
这段代码是 Groovy 脚本,和 Java 很像。(在我看来,没那么啰嗦。在
https://groovy-lang.org/
了解更多信息。我觉得很直白;它获取有效载荷并大写 msg 值。在 Groovy 脚本中,最后一行被认为是返回值。 -
Next, execute the Groovy Transform processor app with the following command.
docker run -d \ --name transform \ --rm \ --network app-starters \ --env SPRING.CLOUD.STREAM.BINDINGS.INPUT.DESTINATION='transform' \ --env SPRING.CLOUD.STREAM.BINDINGS.OUTPUT.DESTINATION='log' \ --env SPRING.RABBITMQ.HOST='rabbit' \ --env GROOVY-TRANSFORMER.SCRIPT=file:///mnt/transform.groovy \ --volume $PWD/:/mnt \ springcloudstream/groovy-transform-processor-rabbit:2.1.1.RELEASE
注意,您仍然在使用输入和输出目的地。这里重要的部分是您使用了
groovy-transformer.script
属性,它在/mnt
路径中寻找transform.groovy
。还要看看 Docker 的--volume
参数,你在这里使用了我们当前的路径,你在这里保存了transform.groovy
脚本;其内容安装在/mnt
文件夹中。对于 Windows 操作系统用户来说,使用完整的路径是很重要的,比如--volume C:\workspace\:/mnt
或者任何你有transform.groovy
脚本文件的地方。运行该命令后,通过执行以下命令确保脚本在/mnt
文件夹中。$ docker exec transform bash -c 'ls -lrt /mnt'
transform.groovy
应上市。 -
接下来,用下面的命令执行 log-sink。
docker run -d \ --name log \ --rm \ --network app-starters \ --env SPRING.CLOUD.STREAM.BINDINGS.INPUT.DESTINATION='log' \ --env SPRING.RABBITMQ.HOST='rabbit' \ springcloudstream/log-sink-rabbit:2.1.2.RELEASE
-
接下来,使用以下命令查看 log-sink 应用的日志。
$ docker logs -f log
-
最后,发一些消息。
```java
$ curl -XPOST -H "Content-Type: application/json" -d '{"msg": "Hello App Starters"}}' http://localhost:8080
$ curl -XPOST -H "Content-Type: application/json" -d '{"msg": "This is awesome"}}' http://localhost:8080
$ curl -XPOST -H "Content-Type: application/json" -d '{"msg": "Hello Spring Cloud"}}' http://localhost:8080
```
当消息值包含单词Hello
时,log-sink 应用记录消息被转换为大写。恭喜你!!您创建了一个使用 Docker 作为主要基础设施的解决方案。
请记住,您仍然可以使用带有相同文件(http-source.yml
、filter-processor.yml
、groovy-transformer-processor.yml
和log-sink.yml
)的 Spring Cloud Config Server,并避免所有的环境变量定义。请记住,有许多方法可以运行这些应用。
稍后,您将使用 Kubernetes 编排所有这些容器,以获得一个使用 Spring CloudStream 的可靠、可伸缩和容错的解决方案!
使用 Docker 合成
您可以运行 Docker Compose,而不是执行所有这些命令。将下面的docker-compose.yaml
文件复制到您的工作区目录中(参见清单 6-1 )。
version: '3'
services:
rabbit:
image: "rabbitmq:3.8.3-management-alpine"
container_name: rabbit
networks:
- app-starters
http:
image: "springcloudstream/http-source-rabbit:2.1.1.RELEASE"
container_name: http
environment:
- spring.cloud.stream.bindings.output.destination=http
- spring.rabbitmq.host=rabbit
ports:
- "8080:8080"
networks:
- app-starters
filter:
image: "springcloudstream/filter-processor-rabbit:2.1.1.RELEASE"
container_name: filter
environment:
- spring.cloud.stream.bindings.input.destination=http
- spring.cloud.stream.bindings.output.destination=transform
- spring.rabbitmq.host=rabbit
- filter.expression=#jsonPath(payload,'$$.msg').contains('Hello')
networks:
- app-starters
transform:
image: "springcloudstream/groovy-transform-processor-rabbit:2.1.1.RELEASE"
container_name: transform
environment:
- spring.cloud.stream.bindings.input.destination=transform
- spring.cloud.stream.bindings.output.destination=log
- spring.rabbitmq.host=rabbit
- groovy-transformer.script=file:///mnt/transform.groovy
volumes:
- "$PWD/:/mnt"
networks:
- app-starters
log:
image: "springcloudstream/log-sink-rabbit:2.1.2.RELEASE"
container_name: log
environment:
- spring.cloud.stream.bindings.input.destination=log
- spring.rabbitmq.host=rabbit
networks:
- app-starters
networks:
app-starters
:
Listing 6-1.docker-compose.yaml
清单 6-1 显示了docker-compose.yaml
文件。注意,它与 Docker 命令相同,但是请仔细查看filter.expression
属性。因为您使用 jsonPath 来查找$.msg
值,所以必须对$
进行转义,这里您使用了双$$
。
Note
您可以在 Apress 网站下载本章的所有源代码。
我说你可以用不同的活页夹。好吧,你可以选择任何你想用 Kafka 运行应用启动器的优步-JARs 或 Docker 方法。你需要使用正确的 <功能> - <类型> - <绑定>-命名约定,所以如果你想测试 HTTP | LOG,你必须下载http-source-kafka
和log-sink-kafka
。
SpringCloudStream
在本节中,您将了解更多关于 Spring Cloud Stream、它的主要概念以及在 3.x 中改变的编程模型,我将更多地讨论绑定器。在后面的章节中,您将创建一个自定义绑定器,作为微服务之间的通信通道,而无需使用特定的 API。
首先,让我们来看一个小例子,它说明了这项技术的强大之处,以及如何使用相同的代码而不考虑绑定器。
使用卡夫卡的电影 CloudStream:电影-水槽-卡夫卡
让我们创建一个简单的微服务来扮演一个接收器的角色:记录制片人的电影(见图 6-3 )。
图 6-3。
使用卡夫卡的电影 CloudStream(sink - movie-sink-kafka)
打开浏览器,指向 h ttps://start.spring.io
。添加以下元数据。
-
组:
com.apress.cloud.stream
-
神器:
movie
-
从属关系:CloudStream,阿帕奇卡夫卡流的 Spring,龙目岛
单击生成按钮下载一个 ZIP 文件。解压缩并导入到你喜欢的 IDE 中(见图 6-4 )。
图 6-4。
https://start.spring.io
-春初始值-电影 CloudStream-沉
让我们从打开pom.xml
文件开始。注意,您已经有了两个 Kafka 依赖项,但是您需要添加第三个(参见清单 6-2 )。
...
<!-- Kafka Streams : Binder -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId> spring-cloud-stream-binder-kafka</artifactId>
</dependency>
...
Listing 6-2.pom.xml Snippet
清单 6-2 显示了pom.xml
的一个片段,它添加了 Spring Cloud Stream 使用 Kafka broker 所需的依赖项。记得添加spring-cloud-stream-binder-kafka
依赖项。
接下来,创建Movie
类。你可以使用前几章的那个(见清单 6-3 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Movie {
private String title;
private String actor;
private int year;
}
Listing 6-3.src/main/java/com/apress/cloud/stream/movie/Movie.java
清单 6-3 显示了Movie
类。注意,您正在使用 Lombok 注释来避免样板文件 setters 和 getters 以及toString
覆盖。
接下来,创建MovieStream
类。这是水槽(见清单 6-4 )。
package com.apress.cloud.stream.movie;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Consumer;
@Log4j2
@Configuration
public class MovieStream {
@Bean
public Consumer<Movie> log() {
return movie -> {
log.info("Movie received: {}",movie);
};
}
}
Listing 6-4.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 6-4 显示了MovieStream
类,一个带有名为 log 的方法的接收器,它返回一个Consumer
接口。是的,这是反应式/函数式编程的一部分,我将在下一节中讨论。它是 Spring Cloud Stream 提供的编程模型之一。
接下来,让我们用清单 6-5 中的内容修改application.properties
。
# Bindings for Kafka
spring.cloud.stream.bindings.log-in-0.destination=log
Listing 6-5.src/main/resources/application.properties
清单 6-5 显示了定义与名为 log 的目的地的log-in-0
绑定的application.properties
文件。log-in-0
是卡夫卡作品的一部分。一个组中可以有几个消费者,用name-in-#
格式,
标识他们,意思是有log-in-1, log-in-2
,以此类推;这种情况下就是log-in-0
。这对于您在这里使用的编程模型也是必要的,但是不要担心;这在接下来的章节中会变得更加清晰。
Spring Cloud Stream 应用启动日志(log-sink-rabbit)是相同的代码(或多或少),因为 Spring Cloud 团队给它添加了更多的功能。现在,你可以称这个微服务为 movie-sink-kafka 应用启动器。
我们来测试一下。使用 Docker Compose 运行 Kafka。如果你熟悉这项技术,它使用 Zookeeper 来创建集群,所以我认为在这种情况下使用 Docker 更容易。使用附带源代码中的docker-compose.yml
文件(参见清单 6-6 )。
version: '3'
services:
zookeeper:
image: 'bitnami/zookeeper:latest'
container_name: zookeeper
networks:
- kafka-net
ports:
- '2181:2181'
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: 'bitnami/kafka:latest'
container_name: kafka
networks:
- kafka-net
ports:
- '9092:9092'
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
- ALLOW_PLAINTEXT_LISTENER=yes
depends_on:
- zookeeper
networks:
kafka-net:
Listing 6-6.docker-compose.yml
清单 6-6 显示了docker-compose.yml
。请注意,您使用的是动物园管理员 Bitnami 图像和卡夫卡。要启动 Kafka,请打开一个终端窗口并执行以下命令。
$ docker-compose up
在您的 IDE 中运行该应用,或者使用微服务在终端窗口中执行以下命令。
$ ./mvnw spring-boot:run
接下来,发送一些电影到movie-sink-kafka
流。打开终端窗口并执行以下命令。这个命令是交互式的,所以您可以添加 JSON 对象。
$ docker exec -it kafka /opt/bitnami/kafka/bin/kafka-console-producer.sh --broker-list 127.0.0.1:9092 --topic log
> {"title":"The Matrix","actor":"Keanu Reeves","year":1999}
> {"title":"Memento","actor":" Guy Pearce ","year":2000}
执行这些命令后,您应该会在日志中看到类似下面的内容。
Movie received: Movie(title=The Matrix, actor=Keanu Reeves, year=1999)
Movie received: Movie(title=Memento, actor= Guy Pearce , year=2000)
恭喜你,你创造了自己的 CloudStream 汇!
通过按 Ctrl+C 停止 Docker 生成器,然后通过在启动 Docker 生成器的同一个目录中执行docker-compose down
来停止 Docker Compose。
使用 RabbitMQ: movie-sink-rabbit 的电影 CloudStream
现在是时候使用 RabbitMQ 绑定器创建一个 sink 了。创作电影《沉沦之兔》需要什么?您正在修改同一个项目。让我们开始吧(见图 6-5 )。
图 6-5。
使用 rabbit MQ(sink-movie-sink-rabbit)的电影 CloudStream
打开你的pom.xml
文件,注释三个卡夫卡依赖关系、并添加spring-cloud-starter-stream-rabbit
依赖关系(见清单 6-7 )。
...
<!-- RabbitMQ Streams - Binder-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
...
Listing 6-7.pom.xml Snippet
这就是你需要做的。我们来测试一下。
首先,使用下面的命令确保 RabbitMQ 启动并运行。
$ docker run -d --rm \
--name rabbit \
-p 15672:15672 -p 5672:5672 \
rabbitmq:3.8.3-management-alpine
接下来,运行应用。
打开浏览器,指向http://localhost:15672
。用guest/guest
访问 RabbitMQ 控制台。转到队列选项卡。你发现类似log.anonymous.xxxx
的队列。点击队列(见图 6-6 )。
图 6-6。
http://localhost:15672/#/queues-log . anonymous . xxxx
点击队列后,进入发布消息部分。在属性字段中,添加带有application/json
值的content_type
,并添加 JSON 格式的电影(参见图 6-7 )。
图 6-7。
队列发布消息
点击发布消息按钮。您会在应用日志中看到如下内容。
Movie received: Movie(title=The Matrix, actor=Keanu Reeves, year=1999)
恭喜你!你已经创建了电影-水槽-兔子应用启动器!您可能想知道为什么不更改application.properties
。这就是 Spring Cloud Stream 的美妙之处,也是 Spring Boot 自动配置的方式。它知道如何处理这些属性。我稍后会讨论它们。
SpringCloudStream
Spring Cloud Stream 是一个消息驱动的框架,用于创建微服务应用。它构建于 Spring Boot 之上,这使得轻松创建企业级微服务成为可能。它使用 Spring Integration 来公开企业集成模式,并提供了一种与任何消息传递代理进行通信(通过通道)的简单方法。
Spring Cloud Stream 的一个主要特性(在我看来是关键特性)是,它通过使用 Java 服务提供者接口(SPI)为任何中间件代理提供了自以为是的配置,使得实现和集成非常容易。它使用发布-订阅、消费者组和分区等消息传递模式(见图 6-8 )。
图 6-8。
Spring 生态系统
因为 Spring Cloud Stream 基于 Spring Boot,所以您可以通过添加以下依赖项将现有的 Spring Boot 应用转换为 Spring Cloud Stream 应用。
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
我用了霍舍姆。SR7 版 SpringCloud 依赖写这本书的时候;你可以在主页面查看最新的 GA 版本( https://spring.io/projects/spring-cloud-stream
)。
您还需要添加正确的绑定依赖项,这些依赖项基于您支持的代理;SpringCloudStream 首页显示支持的经纪人。在这本书里,你要么用卡夫卡,要么用 RabbitMQ。对于 Kafka,您需要添加以下绑定器依赖项。
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
在使用 Kafka 和 Kafka 流模型编程时,前面的依赖关系会有所帮助。如果您需要 RabbitMQ 代理,您需要添加以下绑定器依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
如果您需要进行单元和集成测试,您可以添加以下依赖项。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
SpringCloudStream 特征
Spring Cloud Stream 提供了几个特性,允许您创建和扩展消息驱动的微服务应用。
图 6-9。
Spring CloudStream 应用模型
- 它提供了一个中间件中立的核心,这意味着您的微服务可以通过特定于中间件的绑定器实现与其他微服务和外部服务进行通信。您需要在代码中使用目的地(输入和输出通道)(参见图 6-9 )。
图 6-10。
消费者群体:多个活页夹
-
您可以使用任何构建工具(包括 Maven 或 Gradle)运行独立的 Spring Cloud Stream 应用,以生成可运行的 jar。
-
Spring Cloud Stream 为 Kafka 和 RabbitMQ 提供绑定器实现(与 Spring Cloud 团队一起)。社区支持其他绑定器(Amazon Kinesis、Google PubSub、Solace PubSub+、Azure Event Hubs 和 Apache RocketMQ)。如果您没有使用这些绑定器实现,您可以创建自己的绑定器。Spring Cloud Stream 提供了基于 Java 服务提供者接口的绑定器抽象,因此很容易编写自己的即插即用绑定器。稍后您将创建一个绑定器实现。
有了这个 binder 抽象,当您声明
spring.cloud.stream.bindings.[input|output].destination
属性时,就可以很容易地在运行时通过您的应用属性动态选择到外部目的地(输入和输出通道)的映射。如果你使用 Kafka,Spring Cloud Stream 会创建一个主题,如果你使用 RabbitMQ,spring Cloud Stream 会创建一个队列。因为 Spring Cloud Stream 是固执己见的,所以它会根据类路径中的内容来配置绑定器。由于这种抽象,您可以轻松地将多个绑定器添加到您的 CloudStream 式应用中。您可能需要从 RabbitMQ 消费(输入目的地)并处理和发送有效负载(输出目的地)到 Kafka 代理(见图 6-10 )。 -
它为您的 CloudStream 式应用的单元和集成测试提供了一个测试框架。它为测试目的提供了一个专门的绑定器实现。
-
它提供了一种发布-订阅通信,减少了生产者和消费者的复杂性,形成了一个更加灵活、可伸缩、解耦和容错的环境。这种发布-订阅消息传递模式并不新鲜,但是跨不同的中间件代理实现起来很简单。
-
它提供了一种创造消费群体的方式;这样,它在默认情况下实现了 worker 消息传递模式(循环方式),该模式允许您扩展您的应用,并确保一个组中的使用者一次接收一条消息。要在您的应用中创建消费者组,您需要设置
spring.cloud.stream.bindings.<binding-name>.group
属性。如果你没有指定一个组,Spring Cloud Streams 会生成一个匿名的消费者组,并为你的消费者订阅(见图 6-10 )。 -
它提供了持久性,即使用户停机,也能保持用户群订阅的持久性。当它从故障中恢复时,消息正在等待,但您需要知道,如果您不添加组,它会创建一个匿名订阅,这是不持久的。因此,作为一个好的实践,我建议在您的应用中设置一个组和一个目的地。
-
它提供了两种不同类型的消费者:异步(通常是消息驱动模式)和同步(轮询模式)。
-
它提供了分区支持,您可以在多个实例之间对数据进行分区。这个特性允许你拥有超快的实时数据处理速度,并允许你在不支持这个特性的代理中进行分区,比如 RabbitMQ 另一方面,Kafka 默认使用分区。重要的是要知道,如果您想要使用分区特性,您需要配置两端——生产者和消费者。
在接下来的几节中,您将使用其中的一些功能。
程序设计模型
让我们回顾一下编程模型,以了解如何创建 Spring Cloud Stream 应用(也称为开箱即用的应用启动器)。
图 6-11。
目的活页夹
-
消息。这种数据结构通常包含标题(有时甚至是页脚)和有效载荷,由生产者和消费者使用。
-
目的地活页夹。一些组件提供了与中间件系统的集成。这些绑定器负责连接、数据类型转换、开发人员代码执行、委托、生产者和消费者之间的消息路由等等。尽管这些绑定能够集成,但它们需要开发人员提供与绑定相关的配置。我将在后面的章节中讨论目的地绑定器以及如何实现自己的绑定器。
-
绑定。这是应用和中间件系统之间的桥梁(通向主题和队列的桥梁)。目的地绑定器创建这些绑定(参见图 6-11 )。
基于注释的绑定版本 2.x 及以下
开发者需要遵循这些简单的步骤来创建 2.x 及更低版本的 Spring Cloud Stream 应用。
-
添加必要的依赖:
spring-cloud-stream
和spring-cloud-stream-binder-*
。 -
使用
@EnableBinding
注释来指定您的绑定是源接口、处理器接口还是接收器接口。当这个注释被添加到您的类中时,Spring Cloud 连接所有必要的组件来创建您的流式应用。 -
如果您想要创建一个源,您需要声明一个输出通道并使用它来发送有效负载。如果您想要创建一个处理器,您需要声明输入和输出通道,并使用它们来消费和产生有效负载。如果您想要创建一个接收器,您需要声明一个输入通道并使用它来消费有效负载。
- 为了消费消息(输入通道),您可以使用遗留的 Spring Integration 流或
@StreamListener
注释,这使得消费消息更容易。对于发送消息(输出通道),您可以使用遗留的 Spring Integration 流或@SendTo
注释。
- 为了消费消息(输入通道),您可以使用遗留的 Spring Integration 流或
让我们看看创建 CloudStream 式应用的绑定接口。Spring Cloud Stream 提供绑定接口,在微服务之间创建简单的契约。
-
Source
defines a contract to produce messaging by providing a destination where the produced message is sent. The following code defines theSource
interface.public interface Source { String OUTPUT = "output"; @Output(Source.OUTPUT) MessageChannel output(); }
Source
接口使用功能接口。org.springframework.messaging.MessageChannel
提供了一个重载send
方法,并由@Output
注释标记。这个标记通过连接所有必要的逻辑来识别绑定的类型、目的地和行为。 -
Processor
定义了Source
和Sink
接口的封装,这提供了一种消费和产生消息的简单方法。下面的代码定义了Processor
接口。 -
Sink
defines a contract for a message consumer by providing a destination where the message is consumed. The following code defines theSink
interface.public interface Sink { String INPUT = "input"; @Input(Sink.INPUT) SubscribableChannel input(); }
Sink
接口使用了一个从MessageChannel
接口扩展而来的org.springframework.messaging.SubscribableChannel
接口,并用@Input
标注进行了标记。同样,这有助于 Spring Cloud Stream 为您的应用连接所有必要的逻辑。
public interface Processor extends Source, Sink {}
较新版本的 Spring Cloud Stream 具有向后兼容性,因此您仍然可以在 Spring Cloud Stream 3 . x 版本中使用这些接口,我们先来看一个如何使用 2.x 及更低版本的示例。2.x 及以下版本用于 Spring Web 之上。您需要小心您的本地环境,因为如果您正在开发几个应用,您必须为每个应用更改server.port
属性。Spring Cloud 团队在 3.x 版本中关闭了这个,我的建议是,如果你想使用 2.x 或以下的绑定,选择最新版本的 Spring Cloud Stream,这样 web 就关闭了,你就不需要担心这个问题了。
文件|转换|日志(兔子)示例
让我们从一个简单的流开始。我们来读一下movies.txt
(CSV 格式)把标题大写,把电影记录在控制台里。你正在使用 RabbitMQ 作为绑定器,并且你正在创建三个小应用(见图 6-12 )。
图 6-12。
文件|转换|日志:RabbitMQ 活页夹
电影-文件-源-兔子
第一个源微服务从文件系统中读取movie.txt
。打开一个指向 https://start.spring.io
的浏览器。添加以下元数据。
-
组
: com.apress.cloud.stream
-
神器:
movie-file-source-rabbit
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
Note
我们正在更改包名。
您可以单击“生成”按钮下载一个 ZIP 文件。您可以将其解压缩并导入到您喜欢的 IDE 中(参见图 6-13 )。
图 6-13。
https://start.spring.io
:Spring 初始化
因为您使用的是 RabbitMQ 绑定器,所以打开您的pom.xml
文件并添加以下依赖项。
<!-- RabbitMQ Binder -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- Spring Integration -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
您正在添加spring-integration-file
,因为您正在从文件系统中读取。接下来,创建您的Movie
模型类。您可以使用清单 6-3 中列出的内容。
接下来,创建MovieStreamProperties
类。这个类保存了从application.properties
文件中读取的两个属性(参见清单 6-8 )。
package com.apress.cloud.stream.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie.stream")
public class MovieStreamProperties {
private String directory;
private String namePattern;
}
Listing 6-8.src/main/java/com/apress/cloud/stream/movie/MovieStreamProperties.java
您需要将 CSV 文件转换成一个Movie
对象。这意味着您需要创建一个进行转换的MovieConverter
类(参见清单 6-9 )。
package com.apress.cloud.stream.movie;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
public class MovieConverter implements Converter<String,Movie> {
@Override
public Movie convert(String s) {
List<String> fields = Stream.of(s.split(",")).map(String::trim).collect(Collectors.toList());
return new Movie(fields.get(0),fields.get(1),Integer.valueOf(fields.get(2)));
}
}
Listing 6-9.src/main/java/com/apress/cloud/stream/movie/MovieConverter.java
清单 6-9 展示了实现Converter<S,T>
接口的MovieConverter
类,该接口接受一个字符串并返回一个Movie
对象。
接下来,创建MovieStream
类,它拥有创建流式应用所需的一切(参见清单 6-10 )。
package com.apress.cloud.stream.movie
;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.file.dsl.Files;
import org.springframework.integration.file.splitter.FileSplitter;
import java.io.File;
@EnableConfigurationProperties(MovieStreamProperties.class)
@AllArgsConstructor
@EnableBinding(Source.class)
public class MovieStream {
private MovieStreamProperties movieStreamProperties;
private MovieConverter movieConverter;
@Bean
public IntegrationFlow fileFlow(){
return IntegrationFlows.from(Files
.inboundAdapter(new File(this.movieStreamProperties.getDirectory()))
.preventDuplicates(true)
.patternFilter(this.movieStreamProperties.getNamePattern()),
e -> e.poller(Pollers.fixedDelay(5000L)))
.split(Files.splitter().markers())
.filter(p -> !(p instanceof FileSplitter.FileMarker))
.transform(Transformers.converter(movieConverter))
.transform(Transformers.toJson())
.channel(Source.OUTPUT)
.get();
}
}
Listing 6-10.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 6-10 显示了MovieStream
类。我们来分析一下。
-
@EnableBinding
。这个注释告诉 Spring Cloud Stream 要应用什么配置,因为您将Source.class
作为参数传递,所以它被配置为源绑定。这个标记是你创建 Spring Cloud Stream app 唯一需要的东西。剩下的就看 Spring Cloud Stream 框架了(如何连接,发送消息等。). -
IntegrationFlow
。这部分你已经从第三章知道了。请注意,您正在声明一个IntegrationFlow,
,并且您正在使用inboundAdapter
从特定目录和特定模式的文件系统中读取文件。您每 5 秒钟轮询一次,并且您不能两次读取同一个文件。此外,您正在读取文件的内容并使用MovieConverter
创建一个Movie
对象,该对象被转换成 JSON 格式并发送到输出通道。 -
channel(Source.OUTPUT)
。这部分代码属于IntegrationFlow
,这是你发送到输出通道的最后一部分(即将声明的目的地)。
这里使用了 Spring Integration,并且使用了 Spring Integration DSL(也可以使用 XML)。记住,要创建一个 Spring CloudStream(2.x 及以下版本),需要声明@EnableBinding
注释和绑定的类型,在本例中是源绑定;第二,输出通道,因为这是一个源绑定类型,必须告诉消息发送到哪里;这是通过在最后一部分的流程中添加channel(Source.OUTPUT)
来完成的。我知道我在这里是重复的,但是我想确保您理解这段代码发生了什么,以及创建流是多么容易。
最后,您需要打开application.properties
并添加清单 6-11 中的内容。
# Bindings
spring.cloud.stream.bindings.output.destination=movie
# Movie Stream
movie.stream.directory=.
movie.stream.name-pattern=movies.txt
Listing 6-11.src/main/resources/application.properties
清单 6-11 中的重要部分是spring.cloud.stream.bindings.output.destination
属性的声明。这意味着Source.OUTPUT
通道创建一个名为movie
的目的地,作为 RabbitMQ 中的主题交换。
电影-转换-处理器-兔子
现在让我们来研究下一个处理器微服务,它消耗一个Movie
对象。它将标题改为大写。打开浏览器指向 https://start.spring.io
。添加以下元数据。
-
组
: com.apress.cloud.stream
-
神器:
movie-transform-processor-rabbit
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
Note
我们正在更改包名。
您可以单击“生成”按钮下载一个 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。这与图 6-13 相同,但是确保相应地更改工件和包名。
首先,确保您添加了 RabbitMQ 绑定器(只有绑定器)。然后,创建Movie
类(与清单 6-3 中的相同)。
接下来,创建包含转换代码的MovieStream
类。记住,它用大写字母设置电影标题(见清单 6-12 )。
package com.apress.cloud.stream.movie;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.messaging.handler.annotation.SendTo;
@EnableBinding(Processor.class)
public class MovieStream {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Movie process(Movie movie){
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
}
}
Listing 6-12.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 6-12 显示了MovieStream
类。我们来分析一下注解。
-
@EnableBinding
告诉 SpringCloudStream,app 是流,是处理器类型。这意味着它需要设置输入和输出通道。 -
@StreamListener
连接到一个队列(RabbitMQ)或者一个主题(Kafka);它设置输入通道。 -
@SendTo
将有效载荷发送到输出通道。
如你所见,这非常简单;事实上,您可以使用 transform-processor-rabbit 应用启动器来代替。你可以把这个作为作业。
最后,您需要打开application.properties
并添加清单 6-13 中的内容。
# Bindings - RabbitMQ
spring.cloud.stream.bindings.input.destination=movie
spring.cloud.stream.bindings.output.destination=log
Listing 6-13.src/main/resources/application.properties
注意,您使用的是连接到movie
(目的地)队列的输入通道,它使用输出通道将有效负载发送到 RabbitMQ 中名为log
(目的地)的主题。
电影-原木-水槽-兔子
下一个 sink 微服务接收一个Movie
对象,并将其记录到控制台中。我们先打开一个指向 https://start.spring.io
的浏览器。添加以下元数据。
-
组
: com.apress.cloud.stream
-
神器:
movie-log-sink-rabbit
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
Note
我们正在更改包名。
您可以单击“生成”按钮。这将下载一个 ZIP 文件。您可以将其解压缩并导入到您最喜欢的 IDE 中(参见图 6-13 ),但是一定要相应地更改工件和包的名称。
首先,确保您添加了 RabbitMQ 绑定器(只有绑定器)。然后,您需要创建Movie
类(参见清单 6-3 )。
接下来,创建记录Movie
的MovieStream
类,它应该以大写形式显示title
(参见清单 6-14 )。
package com.apress.cloud.stream.movie;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
@Log4j2
@EnableBinding(Sink.class)
public class MovieStream {
@StreamListener(Sink.INPUT)
public void process(Movie movie){
log.info("Movie processed: {}",movie);
}
}
Listing 6-14.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 6-14 显示了MovieStream
类。我们来分析一下注解。
-
@EnableBinding
告诉 SpringCloudStream 这个 app 是一个流,而且是一个汇的类型。这意味着它需要设置输入通道。 -
@StreamListener
连接到一个队列(RabbitMQ)或者一个主题(Kafka);它设置输入通道。
最后,您需要打开application.properties
并添加清单 6-15 中的内容。
# Bindings - RabbitMQ
spring.cloud.stream.bindings.input.destination=log
Listing 6-15.src/main/resources/application.properties
注意,您正在使用连接到一个log
(目的地)队列的输入通道来获取Movie
有效载荷,标题应该是大写的。
一起运行它们
在运行应用之前,确保 RabbitMQ 运行并监听端口 5672 是很重要的。您可以像以前一样使用 Docker 运行 RabbitMQ,命令如下。
$ docker run -d --rm \
--name rabbit \
-p 15672:15672 -p 5672:5672 \
rabbitmq:3.8.3-management-alpine
接下来,用以下代码运行应用。
$ ./mvnw spring-boot:run
我建议你从movie-log-sink-rabbit
开始,然后是movie-transform-processor-rabbit,
,最后是movie-file-source-rabbit
,按这个顺序。另外,确保movies.txt
可以被应用访问到。你可以在movie-file-source-rabbit
应用的根文件夹中找到movies.txt
的副本。
您应该在movie-log-sink-rabbit
控制台或终端中看到以下输出。
Movie processed: Movie(title=THE MATRIX, actor=Keanu Reeves, year=1999)
Movie processed: Movie(title=MEMENTO, actor=Guy Pearce, year=2000)
Movie processed: Movie(title=THE SILENCE OF THE LAMBS, actor=Jodie Foster, year=1991)
Movie processed: Movie(title=THE PRESTIGE, actor=Christian Bale, year=2006)
Movie processed: Movie(title=DISTURBIA, actor=Shia LaBeouf, year=2007)
关于绑定的更多信息
正如您在示例中看到的,Spring Cloud Stream 组件基于 Spring Messaging 的MessageChannel
(出站)和SubscribableChannel
(入站)。这些接口使得 Spring Cloud Stream 更具可扩展性。您可以拥有一个类似的界面,在其中定义多个输出,也许是为了一个动态场景。
public interface MovieGenre {
String INPUT = "movie-genre";
String GENRE_SCIENCE_FICTION = "science-fiction";
String GENRE_ROMANCE = "romance";
String GENRE_HORROR = "horror";
String GENRE_DEFAULT = "default-output";
@Input(INPUT)
SubscribableChannel movie();
@Output(GENRE_HORROR)
MessageChannel horror();
@Output(GENRE_SCIENCE_FICTION)
MessageChannel scienceFiction();
@Output(GENRE_ROMANCE)
MessageChannel romance();
}
你的Stream
类可以这样写。
@Log4j2
@EnableBinding(MovieGenre.class)
public class MovieStream {
//Acts as Processor
@Bean
@ServiceActivator(inputChannel = MovieGenre.INPUT)
public AbstractMappingMessageRouter destinationRouter(@Qualifier("binderAwareChannelResolver") DestinationResolver<MessageChannel> channelResolver) {
AbstractMappingMessageRouter router = new ExpressionEvaluatingRouter(new SpelExpressionParser().parseExpression("#jsonPath(payload,'$.genre')"));
router.setDefaultOutputChannelName(MovieGenre.GENRE_DEFAULT);
router.setChannelResolver(channelResolver);
return router;
}
//Sinks
@StreamListener(MovieGenre.GENRE_SCIENCE_FICTION)
public void genreScienceFiction(Movie movie){
log.info("Science Fiction: {}",movie);
}
@StreamListener(MovieGenre.GENRE_HORROR)
public void genreHorror(Movie movie){
log.info("Horror: {}",movie);
}
//...
}
处理器正在使用@ServiceActivator
(创建处理器或接收器的另一种方式)。您可以在附带的源代码中或者在 Apress 网站上找到movie-multiple-rabbit
项目中的完整代码。您需要在 RabbitMQ 控制台的movie-genre
匿名队列中手动发送一个 JSON 对象(发布消息时不要忘记在属性字段中添加content-type:application/json
)。JSON 应该如下所示。
{
"title":"The Matrix",
"actor":"Keanu Reeves",
"year":1999,
"genre":"science-fiction"
}
此外,您可以使用多个绑定,如下所示。
@EnableBinding(value={MovieGenre.class, MovieEvents.class})
这些例子更有可能使用基于事件的消息消费,因为您定义了多个MessageChannel
并使用DestinationResolver
将消息路由到正确的目的地。尽管如此,有时你还是需要一些控制,比如控制你的消费速度。您可以创建PollableMessageSource
,如下面的代码片段所示。
public interface MovieEvent {
@Input
PollableMessageSource ratingChanged();
}
通道/绑定命名约定
当您使用默认绑定实现时,标记的通道仅通过添加@Input
或@Output
注释来获取方法名称。换句话说,如果你有如下代码。
public interface MovieGenre {
@Input
SubscribableChannel movie();
}
绑定/目的地(代理中的主题)被命名为movie
。如果您将参数值传递给@Input
批注,您可以覆盖此行为;例如,如果您有以下代码。
public interface MovieGenre {
@Input("new-movie")
SubscribableChannel movie();
}
绑定/目的地(代理中的主题)被命名为newMovie
。一个非常简单的命名约定。此外,您可以使用spring.cloud.stream.bindings
属性覆盖该行为。这是一个表达式:
spring.cloud.stream.bindings.[channel-name].destination=[new-channel-name]
如果您使用的是我们的第一个例子,@Input
(没有参数),它采用方法的名称。如果您想覆盖它,您需要以下内容。
#application.properties
spring.cloud.stream.bindings.movie.destination=newMovie
前面的命令覆盖了名称movie.
,它在代理中创建了newMovie
绑定/目的地。如果你正在使用像@Input("newMovie")
这样的注释,你想要覆盖它。你应该把下面的放在application.properties
里。
#application.properties
spring.cloud.stream.bindings.new-movie.destination=new-movie-event
这将覆盖代理中的new-movie-event
绑定/目的地的new-movie
绑定/目的地。稍后将详细介绍这些命名约定。
绑定版本 3.x
版本 3.x 的主要新增功能之一是在处理绑定时能够使用函数来生成或使用消息。您不需要添加或使用任何额外的注释,如@EnableBinding
或@StreamListener
。新的固执己见的运行时可以查看您的代码并动态创建正确的流类型(源、处理器或接收器)。
通过使用 POJOs(普通旧 Java 对象),3.x 版本提供了一种更清晰的方式来创建流式应用,并使用上下文 beans 类型,如java.util.function.Supplier<T>
(用于源)、java.util.function.Function<T,R>
(用于处理器)和java.util.function.Consumer<T>
(用于接收器)。它创建了反应式流,这意味着通过使用这种新的编程模型,您可以立即使用反应式编程。
我们来回顾一下使用 3.x 版本创建流式应用的新方法,如果你想使用最新版本的 Spring Cloud Stream(霍舍姆。SR7),你需要做到以下几点。
-
给
pom.xml
:spring-cloud-stream
、spring-cloud-stream-binder-kafka-streams,
和spring-cloud-stream-binder-kafka
添加正确的依赖关系。 -
声明您的 beans 类型:供应商、函数、消费者。
-
有了这些类型的 beans,你就可以开始用 Spring Cloud 函数编写,创建函数组合,公开你需要的函数。对于这个特性,您可以使用
spring.cloud.function.definition
属性来提供您想要使用的函数的名称。可以用类 Unix 管道定义几个函数(函数复合);同样,在 Spring Cloud Function 框架中使用这种方法在 AWS Lambda、OpenWhisk 等中创建无服务器应用。稍后您会看到更详细的内容。
接下来,使用 Kafka 作为绑定器和 version 3.x 特性创建一个源、处理器和接收器。记住它具有向后兼容性,从 2.1 版本开始,你可以使用云函数来创建流式应用。
电影-来源-卡夫卡
这个 CloudStream app 发电影,充当片源。打开浏览器指向 https://start.spring.io
。添加以下元数据。
-
组
: com.apress.cloud.stream
-
神器:
movie-source-kafka
-
包名:
com.apress.cloud.stream.movie
-
从属关系:CloudStream,阿帕奇卡夫卡流的 Spring,龙目岛
Note
我们正在更改包名。
您可以单击“生成”按钮下载一个 ZIP 文件。您可以将其解压缩并导入到您喜欢的 IDE 中(参见图 6-14 )。
图 6-14。
https://start.spring.io
:Spring 初始化
打开您的pom.xml
文件,并确保您拥有必要的依赖项。
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
接下来,创建Movie
类。我使用的新版本只增加了流派字段(见清单 6-16 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Movie {
private String title;
private String actor;
private int year;
private String genre;
}
Listing 6-16.src/main/java/com/apress/cloud/stream/movie/Movie.java
接下来,创建MovieStream
类(参见清单 6-17 )。
package com.apress.cloud.stream.movie;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Flux;
import java.util.function.Supplier;
@Configuration
public class MovieStream {
@Bean
public Supplier<Flux<Movie>> movie() {
return () -> Flux.just(
new Movie("The Matrix","Keanu Reves",1999,"science-fiction"),
new Movie("It","Bill Skarsgård",2017,"horror")
);
}
}
Listing 6-17.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 6-17 显示了MovieStream
类。注意,该类不再使用@Binding
,而是使用@Configuration
注释来帮助 Spring Cloud Stream 识别任何声明的 beans。在这个类中,您将使用发送两个Movie
对象的java.util.function.Supplier
和Flux
(来自 https://projectreactor.io
)。还要注意,用@Bean
标记这个方法很重要,这样框架就可以连接发布消息所需的所有逻辑。就这样,很简单。接下来,打开application.properties
并添加清单 6-18 中的内容。
## Kafka Binders
spring.cloud.stream.bindings.movie-out-0.destination=uppercase
Listing 6-18.sr/main/resources/application.properties
电影处理器卡夫卡
这个 CloudStream 式应用发送Movie
对象,它在标题字段上做一个大写,充当处理器。打开浏览器指向 https://start.spring.io
。添加以下元数据。
-
组
: com.apress.cloud.stream
-
神器:
movie-processor-kafka
-
包名:
com.apress.cloud.stream.movie
-
从属关系:CloudStream,阿帕奇卡夫卡流的 Spring,龙目岛
Note
我们正在更改包名。
您可以单击“生成”按钮下载一个 ZIP 文件。您可以将其解压缩并导入到您喜欢的 IDE 中。
确保您的pom.xml
文件中有 Kafka 和 Cloud Stream 依赖项。创建您的Movie
类;同清单 6-16 。接下来,创建MovieStream
类(参见清单 6-19 )。
package com.apress.cloud.stream.movie;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@Log4j2
@Configuration
public class MovieStream {
@Bean
public Function<Movie, Movie> uppercase() {
return movie -> {
log.info("Processing: {}", movie);
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
};
}
}
Listing 6-19.src/main/java/com/.apress/cloud/stream/movie/MovieStream.java
清单 6-19 显示了充当处理器的MovieStream
类,因为它使用了接收Movie,
的Function
函数接口,并返回一个修改过的Movie
对象。注意,这个类用@Configuration
标记,而uppercase
方法用@Bean
注释标记。
接下来,将清单 6-20 中的内容添加到application.properties
文件中。
## Kafka Binders
spring.cloud.stream.bindings.uppercase-in-0.destination=uppercase
spring.cloud.stream.bindings.uppercase-out-0.destination=log
Listing 6-20.sr/main/resources/application.properties
电影-水槽-卡夫卡
这个 CloudStream 式应用记录电影,充当 sink。打开浏览器指向 https://start.spring.io
。添加以下元数据。
-
组
: com.apress.cloud.stream
-
神器:
movie-sink-kafka
-
包名:
com.apress.cloud.stream.movie
-
从属关系:CloudStream,阿帕奇卡夫卡流的 Spring,龙目岛
Note
我们正在更改包名。
您可以单击“生成”按钮下载一个 ZIP 文件。您可以将其解压缩并导入到您喜欢的 IDE 中。
确保pom.xml
中有卡夫卡和 CloudStream 依赖关系。创建一个Movie
类;同清单 6-16 。接下来,创建MovieStream
类(参见清单 6-21 )。
package com.apress.cloud.stream.movie;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Consumer
;
@Log4j2
@Configuration
public class MovieStream {
@Bean
public Consumer<Movie> log(){
return movie -> {
log.info("Movie Processed: {}", movie);
};
}
}
Listing 6-21.src/main/java/com/.apress/cloud/stream/movie/MovieStream.java
清单 6-21 显示了MovieStream
类。在这种情况下,它使用记录一个Movie
对象的Consumer
接口。请记住,您使用带有@Configuration
的标记类和带有@Bean
注释的log
方法来识别它们,并完成连接所有东西和监听来自声明主题的新消息的繁重工作。
接下来,将清单 6-22 中的内容添加到application.properties
文件中。
## Kafka Binders
spring.cloud.stream.bindings.log-in-0.destination=log
Listing 6-22.sr/main/resources/application.properties
一起运行它们
是时候运行应用了。您正在使用清单 6-6 中的docker-compose.yml
文件。打开一个终端,转到docker-compose.yml
文件,运行您的环境
$ docker-compose up
然后,在每个应用的根文件夹中,用 IDE 或下面的命令运行应用。
$ ./mvnw spring-boot:run
您应该会在movie-sink-kafka
应用控制台中看到大写的电影标题。
Movie Processed: Movie(title=THE MATRIX, actor=Keanu Reves, year=1999, genre=science-fiction)
Movie Processed: Movie(title=IT, actor=Bill Skarsgård, year=2017, genre=horror)
恭喜你!您使用 3.x 版本的新功能创建了一个 Spring Cloud Stream 应用。
绑定命名约定
您是否想知道为什么在前面的例子中使用了那种格式的spring.cloud.stream.bindings
?在 3.x 版中,绑定的命名约定如下。
-
对于输入绑定:
<function-name>
+-in
+<index>
-
对于输出绑定:
<function-name>
+-out
+<index>
索引总是从 0 开始,这与具有多个输入和输出参数的函数有关。稍后你会看到这个。这就是为什么在movie-source-kafka
应用中,函数名是movie
,这意味着它创建绑定名movie-out-0
,你用大写字母覆盖它。
在movie-processor-kafka
app 中,函数名为uppercase
。它生成(因为它是一个Function
处理器)两个绑定(uppercase-in-0
和uppercase-out-0
,你分别用uppercase
和log,
覆盖它们。
最后,在movie-sink-kafka
app 中,函数名是log
,所以生成的绑定是log-in-0,
,你用log
覆盖它。为什么需要覆盖它们?因为开发人员和任何其他外部系统更容易访问业务定义的名称。
生产和消费
有不同的方法来生成和使用消息,3.x 版提供了更多的方法来实现这一点。您仍然可以使用 XML、注释或/和 Spring Integration DSL ( IntegrationFlow
)来使用 Spring Integration。对于 3.x 版的新功能,建议根据您的系统或应用需求使用函数式和反应式编程。
使用 Spring Integration,您可以使用下面的代码向输出通道生成消息。
@Bean
@InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedRate = "5000", maxMessagesPerPoll = "1"))
public MessageSource<Movie> movieMessageSource() {
return () -> new GenericMessage<>(new Movie("The Matrix","Keanu Reeves",1999));
}
前面的代码显示了每 5 秒执行一次发送一个Movie
对象的方法movieMessageSource
的@InboundChannelAdapter
。这段代码是不切实际的,但你得到的想法是发送一个消息给来源。输出通道。
Note
你可以找到这部分项目的代码:movie-file-source-rabbit
、movie-transform-processor-rabbit,
和movie-log-sink-rabbit
。
通过使用带有以下代码的源接口可以实现同样的目的。
@Bean
public ApplicationRunner movieMessage(Source source){
return args -> {
source.output().send(new GenericMessage<>(new Movie("The Matrix","Keanu Reeves",1999)));
};
}
这里你使用了source
和定义的方法output()
来发送一次Movie
对象。或者你也可以直接使用MessageChannel
。使用java.util.function.*
功能接口可以实现同样的行为。
@Bean // Every second send this movie.
public Supplier<Movie> movieSupplier() {
return () -> new Movie("The Matrix", "Keanu Reves", 1999);
}
前面的方法每秒都有一个轮询器,您可以使用@PollableBean
注释来代替@Bean
并覆盖任何默认的轮询设置。记住,对于函数接口,您需要注意spring.cloud.stream.bindings.<function-[in|out]-<index>>.destination
属性名称。
使用带有以下代码的@ServiceActivator
注释使用 Spring Integration 来消费消息。
@ServiceActivator(inputChannel = Sink.INPUT)
public void movieProcess(Movie movie){
log.info("Movie processed: {}",movie);
}
此外,您已经知道可以使用@StreamListener
注释执行相同的行为。
@StreamListener(Sink.INPUT)
public void process(Movie movie){
log.info("Movie processed: {}",movie);
}
如果你想使用java.util.function.*
功能界面。
@Bean
public Consumer<Movie> log(){
return movie -> {
log.info("Movie Processed: {}", movie);
};
}
记住,对于函数接口,您需要注意spring.cloud.stream.bindings.<function-[in|out]-<index>>.destination
属性名称。
如果您需要消费、处理,然后产生一条消息(一个处理器),并且如果您正在使用 Spring Integration,您可以使用@Transformer
注释。
@Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
public Movie transform(Movie movie){
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
}
看到您正在使用processor.INPUT
和Processor.OUTPUT
通道。使用@ServiceActivator
注释可以实现同样的行为。
@ServiceActivator(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
public Movie transformServiceActivator(Movie movie){
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
}
可以使用其他标注,比如@Splitter
、@Aggregator
、@Filter
;当然,您可以使用专用的@StreamListener
和@SendTo
注释来实现相同的行为。
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Movie process(Movie movie){
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
}
如果你想使用java.util.function.*
功能界面。
@Bean
public Function<Movie, Movie> uppercase() {
return movie -> {
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
};
}
记住,对于函数接口,您需要注意spring.cloud.stream.bindings.<function-[in|out]-<index>>.destination
属性名称。
Note
重要的是要知道你不能让@EnableBinding
和任何其他的@Bean
有java.util.function.*
接口。如果你想把他们混在一起,他们必须在不同的班级。
@StreamListener 功能
这个注释有一些有趣的特性,当业务逻辑需要基于消息内容或消息headers
的一些路由时,这些特性会有所帮助。换句话说,您可以使用条件语句来帮助您进行路由。如果要使用这个特性,需要满足两个条件。
-
它必须是单独的消息(这意味着不支持反应式 API 方法)。
-
它不能返回值。
您将使用 SpEL 来设置要计算的表达式。如果您需要添加一个带有标题的条件表达式,您可以像下面这样做。
@StreamListener(value = Sink.INPUT, condition = "headers['genre']=='science-fiction'")
public void processScienceFiction(Movie movie){
log.info("Science Fiction Movie processed: {}",movie);
}
@StreamListener(value = Sink.INPUT, condition = "headers['genre']=='drama'")
public void processDrama(Movie movie){
log.info("Drama Movie processed: {}",movie);
}
如果您需要根据有效负载的内容来计算表达式,那么您可以这样做。
@StreamListener(value = Sink.INPUT, condition = "#jsonPath(payload,'$.year') < 2000")
public void processTwoThousandAndBelow(Movie movie){
log.info("1990-2000 Movie processed: {}",movie);
}
@StreamListener(value = Sink.INPUT, condition = "#jsonPath(payload,'$.year') >= 2000")
public void processTwoThousandAndAbove(Movie movie){
log.info("2000-Present Movie processed: {}",movie);
}
更多功能
许多特性将在接下来的章节中介绍,但是当涉及到生成和使用消息时,您可以获得不同格式的消息,并且可以访问消息头和安全性。
映射方法参数
如果您想要访问有效负载,您已经知道可以使用实例对象或表示,这取决于contentType
头。此外,您还可以访问邮件的其他部分,如。
@StreamListener(Sink.INPUT)
public void process(@Payload String payload, @Header("contentType") String contentType,@Headers Map<String, Object> map){
log.info("Payload processed: {} - ContentType: {} - Headers: {}",payload,contentType,map);
}
参见@Payload
、@Header,
和@Headers
注释的用法。另外,你可以用。
@StreamListener(Sink.INPUT)
public void process(Message message){
log.info("Message processed: {} ",message);
}
反应和功能
另一个很酷的特性是,现在您可以使用反应式编程,这是您在前面的示例中看到的。您可以拥有一个反应式的Consumer
接口,该接口使用来自 project reactor ( https://projectreactor.io/
)的Flux
或Mono
数据类型。您可以对任何通量或单声道类型应用所有操作。
public Function<Flux<Movie>, Mono<Movie>> movieConsumer() {
return flux -> flux
.map(..).filter(..).then();
}
使用前面的代码,您可以控制所有的Pollable
属性,比如fixedDelay
和maxMessagePerPoll
。
spring.cloud.stream.poller.fixed-delay=5000
此外,您可以使用Supplier
而不是@Bean
,您可以使用@PollableBean
注释并使用前面的属性来配置它。
@PollableBean
public Supplier<Flux<Movie>> stringSupplier() {
return () -> Flux.just(new Movie("The Matrix","Keanu Reeves",1999));
}
有时候你需要从一个普通的 Web 微服务连接到一个响应式的微服务,或者有几个功能需要同时使用,那么,我该如何连接和使用 Spring Cloud Stream 的功能呢?Spring Cloud Stream 引入了最后一个org.springframework.cloud.stream.function.StreamBridge
类,它提供了一个函数组合,允许将几个函数作为一个函数运行。
例如,您可以有一个基本的 RestController 类,它接受对/movies 路径的传入 POST 请求。
@AllArgsConstructor
@RestController
public class MovieController {
private StreamBridge streamBridge;
@PostMapping("/movies")
@ResponseStatus(HttpStatus.ACCEPTED)
public void toMovieBinding(@RequestBody Movie movie) {
streamBridge.send("movie-out-0", movie);
}
}
如果你尝试运行这个 app,它会失败,因为它需要知道使用什么绑定来发送Movie
对象;所以,有必要添加以下属性。
spring.cloud.stream.source=movie
spring.cloud.stream.bindings.movie-out-0.destination=movie
关键属性是spring.cloud.stream.source
,这是创建绑定所必需的。使用电影值,它创建电影输出 0 (遵循绑定命名约定)。您已经熟悉的第二个属性:它在内部将movie-out-0
重命名为movie
。
如果您需要使用 Reactor API 完成相同的行为,您可以使用下面的代码。
@RestController
public class MovieController{
EmitterProcessor<Movie> processor = EmitterProcessor.create();
@PostMapping("/movies")
@ResponseStatus(HttpStatus.ACCEPTED)
public void toMovieSupplier(@RequestBody Movie movie) {
processor.onNext(movie);
}
@Configuration
class MovieSupplier {
@Bean
public Supplier<Flux<Movie>> movie() {
return () -> processor;
}
}
}
注意,该类正在使用EmitterProcessor
,toMovieSupplier
方法控制器正在调用onNext
方法;这就是为什么您需要声明@Bean
注释,并通过调用同一个EmitterProcessor
实例(在本例中是processor
对象)来使用供应商。
看看下面的代码。
@Configuration
public class MovieStream {
String GENRE_SCIENCE_FICTION = "science-fiction";
@Bean
public Function<Flux<Movie>, Flux<Movie>> onlyScienceFiction() {
return flux -> flux.filter( movie -> movie.getGenre().equals(GENRE_SCIENCE_FICTION));
}
@Bean
public Function<Flux<Movie>, Flux<Movie>> titleUpperCase() {
return flux -> flux.map( movie -> {
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
});
}
}
您对 Project Reactor 使用 Spring Cloud 函数语法,所以它不会运行,因为您只需要定义一个函数。如果需要多个,就需要创建一个函数组合。
您需要通过使用spring.cloud.function.definition
属性和使用函数方法的|
(管道)符号来声明您的组合的名称。在这个例子中,application.propertie
s 看起来如下。
spring.cloud.function.definition=onlyScienceFiction|titleUpperCase
spring.cloud.stream.bindings.onlyScienceFictiontitleUpperCase-in-0.destination=movie
spring.cloud.stream.bindings.onlyScienceFictiontitleUpperCase-out-0.destination=log
注意spring.cloud.function.definition
被设置为onlyScienceFiction|titleUpperCase
,函数方法的名称。SpringCloudStream 配置生成onlyScienceFictiontitleUpperCase-in-0
和onlyScienceFictiontitleUpperCase-out-0
绑定器;在这种情况下,它们分别被重命名为movie
和log
。
如果因为在不同的流中使用而需要两者,请使用;
(分号)而不是|
(管道)。应该是这样的。
spring.cloud.function.definition=onlyScienceFiction;titleUpperCase
前面的属性生成了onlyScienceFiction-in-0
和onlyScienceFiction-out-0
,以及titleUpperCase-in-0
和titleUpperCase-out-0
绑定。
有时您会收到不同类型的数据,需要作为复合逻辑进行处理。Spring Cloud Stream 允许您拥有多个输入和输出参数,就像下面的类。
@Configuration
public class MovieStream {
String GENRE_SCIENCE_FICTION = "science-fiction";
@Bean
public Function<Tuple2<Flux<Integer>,Flux<Movie>>, Flux<Message<Movie>>> movieTuple() {
return tuple -> {
Flux<Integer> integerFlux = tuple.getT1();
Flux<Movie> movieFlux = tuple.getT2();
return Flux.just(
MessageBuilder.withPayload(movieFlux.blockFirst()).setHeader("stars",integerFlux.map(m -> m.toString())).build());
};
}
}
在这个例子中,您接收到一个Integer
类型的Flux
和一个Movie<T>
类型的Flux<T>
,并返回一个Message<Movie>
对象。这意味着创建的绑定是movieTuple-in-0
、movieTuple-in-1,
和movieTuple-out-0
。注意绑定命名约定中的索引。如果一个元组是返回值,那么就有<method-name>-out-0
、<method-name>-out-1,
等等,这取决于元组参数。
另一个功能让你从Function
到IntegrationFlow
。您按如下方式完成此任务。
@Configuration
public class MovieStream {
@Bean
public IntegrationFlow movieFlow() {
return IntegrationFlows.from(MovieFunction.class)
.transform(Transformers.toJson())
.channel(Source.OUTPUT)
.get();
}
public interface MovieFunction extends Function<Movie, Movie> { }
}
重要的部分是引用网关模式的MovieFunction
接口的声明,这是 Spring Integration 生成的。由于方法名是movieFlow
,它创建了movieFlow.gateway-in-0
和movieFlow.gateway-out-0
绑定。
Spring Cloud Stream、Project Reactor 和 Spring Cloud Function 组成了一个不可思议的团队,使用最新的技术来创建令人惊叹的企业级应用!
Note
本节中的所有代码都在本书的配套源代码中的ch06/function-bridge
目录结构中。它使用卡夫卡式活页夹。
选择途径
路由是另一个特征。您可以将消息路由到一个特定的函数,根据有效负载内容或消息头对其进行处理。要使用这个特性,您需要将spring.cloud.stream.function.routing.enabled
值设置为等于true
,并设置spring.cloud.function.routing-expression
属性。
默认情况下,如果您正在创建处理器流,它会创建functionRouter-in-0
和functionRouter-out-0
绑定(函数/反应流的相同命名约定)。如果你正在创建一个接收器,它会创建一个functionRouter-in-0
绑定。
下面的片段是一个例子。
@Log4j2
@Configuration
public class MovieStream {
@Bean
public Function<Movie, Movie> drama() {
return movie -> {
log.info("Drama: {}",movie);
movie.setGenre(movie.getGenre().toUpperCase());
return movie;
};
}
@Bean
public Function<Movie, Movie> fiction() {
return movie -> {
log.info("Science Fiction: {}", movie);
movie.setTitle(movie.getTitle().toUpperCase());
return movie;
};
}
}
有两个功能:drama
和fiction
。注意每个都有大写的逻辑:流派字段中的drama
大写函数,标题字段中的fiction
大写函数。让我们看看application.properties
的文件。
# Rabbit Binding
spring.cloud.stream.bindings.functionRouter-in-0.destination=movie
spring.cloud.stream.bindings.functionRouter-out-0.destination=log
# Routing Function
spring.cloud.stream.function.routing.enabled=true
spring.cloud.function.routing-expression=headers['genre']
请注意,表达式评估头类型,
,并根据值调用方法的名称。这意味着Movie
对象应该有一个带有drama
或fiction
值的标题类型。另外,请注意有两个函数。您正在启用路由功能。这意味着配置创建了functionRouter-in-0
和functionRouter-out-0\. I
。在这个例子中,您将functionRouter-[in|out]-0
分别重命名为movie
和log
绑定。这很容易理解。
Note
这一节的所有代码都在本书的配套源代码中的ch06/routing
目录结构中。它使用 RabbitMQ 活页夹。
摘要
本章描述了 Spring Cloud Stream 如何工作,以及如何轻松创建流式应用。借助 Spring Integration 的强大功能和 Spring Boot 固执己见的引擎,您可以构建连接到遗留系统或任何使用绑定功能的中间件代理的企业系统,而无需学习新的 API。本章介绍了流的类型:源、处理器和接收器。您看到了通过从 Kafka 或 RabbitMQ 添加正确的依赖项来启动一个流项目是多么容易。
在下一章中,您将看到更多功能,并了解如何创建一个绑定器来插入到您的流式应用中。
你还有很多工作要做,因为这一章只是开始。
七、Spring CloudStream 绑定器
在前一章中,我向您展示了 Spring Cloud Stream 应用启动器如何作为独立的应用工作,可以轻松交付企业级解决方案。我向您展示了 Spring Cloud Stream 模型应用,以及主内核如何基于 Spring Integration 和 Spring Boot 实现简单的配置。您看到了如何使用 Spring Cloud 函数和 Spring Integration 非常容易地创建流。我还向您展示了一个特性(在我看来是最好的),叫做 binder ,它允许您的解决方案使用任何物理目的地或任何中间件消息代理。本章介绍如何创建自定义活页夹。
您将使用 NATS 服务器( https://nats.io
),一个用于构建分布式应用的代理,以提供实时流和大数据用例(见图 7-1 )。想知道为什么我选择了 NATS 而不是其他技术?以前在 Cloud Foundry 工作( www。cloudfoundry。org )项目,保持一些虚拟机存活的主要组件之一是使用 NATS,这是一个快速可靠的消息代理,易于使用。作为实验,我决定创建一个原型作为 binder 实现。我花了几个小时。是的,创建自定义活页夹非常容易。
让我们从讨论 NATS 技术背后的东西以及如何实现它开始。
图 7-1。
绑定器
绑定器使用服务提供者接口(SPI)模式,该模式允许您通过启用功能或替换组件来扩展或添加额外的功能。自从 Java 编程语言的第一次迭代以来,这种模式就一直存在,并增加了插件功能。
Spring Cloud Stream 公开了几个接口和抽象、实用类;它还提供了允许您插入外部中间件的发现策略。这些接口和类帮助您非常容易地创建绑定器。一个典型的场景是生产者和消费者使用绑定器来产生和消费消息。绑定器负责连接、重试、会话或任何允许发布者和消费者在知道如何完成的情况下使用代理的事情。它隐藏了样板代码,避免了学习特定 API 的需要。
我们先来回顾一下主界面:org.springframework.cloud.stream.binder.Binder<T,C,P>
。该接口提供输入和输出绑定目标。它给生产者和消费者都增加了属性;这些属性以类型安全的方式为所需的特定于代理的属性(如果有的话)提供支持(参见清单 7-1 )。
public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> {
Binding<T> bindConsumer(
String bindingName, String group, T inboundBindTarget, C consumerProperties);
Binding<T> bindProducer(String bindingName, T outboundBindTarget, P producerProperties);
}
Listing 7-1.org.springframework.cloud.stream.binder.Binder Interface
让我们回顾一下清单 7-1 。
-
binderConsumer
。这个方法的第一个参数是目的地名称,它在内部创建必要的通道和代理中需要的任何目的对象,比如队列、主题等等。下一个参数是消费者接受消息的组(工人风格或发布/订阅模式)。第三个参数是目的地/通道实例,消费者在其中监听/订阅新的传入消息。第四个参数是属于消息的代理(特定的)和业务属性。 -
binderProducer
。这个方法的第一个参数是目的地名称,它创建必要的通道和代理中需要的任何目的对象,比如主题、交换等等。下一个参数是生产者发送消息的目的地/通道实例。最后一个参数是包含特定于代理的属性和业务属性的任何属性。
我认为这些签名很容易理解。图 7-2 显示了一个活页夹的例子。
图 7-2。
绑定器抽象
实现绑定器
如果你想实现一个绑定,你必须遵循这些简单的规则。
-
一个类必须实现
Binder
接口。 -
一个
@Configuration
标记的类定义了一个绑定er
bean 和创建中间件代理基础设施的步骤;它可能是一个连接、会话、某些凭证等等。 -
有必要在类路径中创建一个包含一个或多个绑定器定义的
META-INF/spring.binders
文件。
正如您所看到的,实现绑定器非常简单,所以让我们开始使用 NATS 代理创建一个定制的绑定器。
暗夜之狐
创建自定义绑定器有助于开发人员加速开发;作为 binder 开发人员,您需要了解这个代理是如何工作的。
在开始实现 binder 之前,我认为有必要创建一个允许您生成和消费消息的库,以便以后可以重用它。在本章的最后,您将创建一个包含三个模块的nats-binder
项目:nats-messaging
(NATS 客户端)nats-messaging-binder
(NAT 绑定实现),以及nats-messaging-test
(NATS 绑定测试)。
下载 NATS 服务器( https://nats.io/
)并安装。本章使用了一个 NATS 码头工人的形象。您可以使用以下命令提取它。
$ docker pull nats
此命令下载一个 10 MB 的图像。
项目:nats-binder
为了使开发更容易,让我们为主pom.xml
文件及其模块创建一个目录结构。创建一个名为nats-binder
的文件夹,并将pom.xml
文件添加到清单 7-2 中。
<?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>
<groupId>com.apress.nats</groupId>
<artifactId>nats-binder</artifactId>
<version>0.0.1</version>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<modules>
<module>nats-messaging-binder</module>
<module>nats-messaging</module>
<module>nats-messaging-test</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Listing 7-2.nats-binder/pom.xml
分析pom.xml
和依赖关系。
接下来,创建模块,因为您使用的是 Spring Initializr,所以您可以在nats-binder
文件夹中解压缩 ZIP 文件。
NATS 客户端:NATs-消息
打开浏览器,指向 https://start.spring.io
。使用以下元数据。
-
组:
com.apress.nats
-
神器:
nats-messaging
-
包装:
com.apress.nats
-
从属关系:龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩到nats-binder
目录,并导入到您喜欢的 IDE 中(参见图 7-3 )。
图 7-3。
spring Initializr NAT-messaging
接下来,让我们添加使用 NATS 服务器所需的依赖项。使用开源技术的好处之一是它对社区开放。这个案例需要 NAT Java 客户端(https://nats.io/download/nats-io/nats.java/
??github.com/nats-io/nats.java
)。
打开pom.xml
并用清单 7-3 中的内容替换。
<?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>com.apress.nats</groupId>
<artifactId>nats-binder</artifactId>
<version>0.0.1</version>
<relativePath>..</relativePath>
</parent>
<packaging>jar</packaging>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nats-messaging</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>io.nats</groupId>
<artifactId>jnats</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
Listing 7-3.nats-binder/nats-messaging/pom.xml
看一下pom.xml
,注意父项目正在声明nats-binder
主项目。记住,nats-messaging
库是一个模块。复习一下,我们继续。
在撰写本文时,Java NATS 客户端版本是 2.6.6。让我们从创建NatsProperties
类开始。这个类保存了关于服务器、端口等等的所有信息(参见清单 7-4 )。
package com.apress.nats;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties("spring.nats")
public class NatsProperties {
private String host = "localhost";
private Integer port = 4222;
}
Listing 7-4.src/main/java/com/apress/nats/NatsProperties.java
清单 7-4 显示的是NatsProperties
类;如您所见,它非常简单,并且有默认值。请记住,您可以在application.properties/yml
文件、命令行或环境变量等中覆盖这些属性。
接下来,创建NatsConnection
类(参见清单 7-5 )。
package com.apress.nats;
import io.nats.client.Connection;
import io.nats.client.Nats;
import lombok.Data;
import java.io.IOException;
@Data
public class NatsConnection {
private Connection connection;
private NatsProperties natsProperties;
private NatsConnection(){}
public NatsConnection(NatsProperties natsProperties) throws IOException, InterruptedException {
this.natsProperties = natsProperties;
this.connection =
Nats.connect("nats://" + natsProperties.getHost() + ":" + natsProperties.getPort().toString());
}
}
Listing 17-5.src/main/java/com/apress/nats/NatsConnection.java
清单 7-5 显示了NatsConnection
类。这个类有 NATS Connection
实例。在这里,当使用spring.nats.*
属性时,您调用NatProperties
来使用默认值或开发人员提供的值。如您所见,Nats
类是静态的。您可以调用connect
方法,传递 schemed ( nats://
)、主机和端口,这是一种连接 NATS 服务器的非常简单的方法。
接下来,让我们创建NatsTemplate
类(参见清单 7-6 )。
package com.apress.nats;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.log4j.Log4j2;
import org.springframework.messaging.Message;
import org.springframework.util.SerializationUtils;
import java.nio.charset.StandardCharsets;
@Log4j2
@AllArgsConstructor
@Data
public class NatsTemplate {
private NatsConnection natsConnection;
public void send(String subject, String message){
assert this.natsConnection != null && subject != null && !subject.isEmpty() && message != null && !message.isEmpty();
log.debug("Sending: {}", message);
this.natsConnection.getConnection().publish(subject, message.getBytes(StandardCharsets.UTF_8));
}
public void send(String subject,Message<?> message){
assert this.natsConnection != null && subject != null && !subject.isEmpty() && message != null;
log.debug("Sending: {}", message);
this.natsConnection.getConnection().publish(subject, SerializationUtils.serialize(message));
}
}
Listing 7-6.src/main/java/com/apress/nats/NatsTemplate.java
清单 7-6 显示了NatsTemplate
类。这个类删除了所有的样板文件,并提供了处理 NATS 服务器的所有操作。这是模板设计模式的一个实现;如果您正在使用 Spring 框架,您可以找到其中的几个,包括JmsTemplate
、RabbitTemplate
、KafkaTemplate
和JdbcTemplate
。
您只声明了两个重载方法,其中您总是接收主题(类似于主题)和消息。您正在使用org.springframework.messaging.Message
界面。还要注意,您需要NatsConnection
实例。为了发布消息,您使用连接(通过getConnection()
方法调用)并调用publish
方法。在send(String subject, Message<?> message)
方法中,您使用一个 Spring 序列化实用程序将您的消息序列化为一个字节数组。NATS 协议要求消息是byte[]
类型。
接下来,让我们创建NatMessageListener
接口(参见清单 7-7 )。
package com.apress.nats;
public interface NatsMessageListener {
void onMessage(byte[] message);
}
Listing 7-7.src/main/java/com/apress/nats/NatsMessageListener.java
清单 7-7 显示了NatsMessageListener
接口,它有一个带byte[]
类型作为参数的onMessage
方法。
接下来,让我们创建至少一个实现来委托侦听器。该类在 NATS 服务器上订阅主题(同一个主题)。
创建NatsMessageListenerAdapter
类(参见清单 7-8 )。
package com.apress.nats;
import io.nats.client.Dispatcher;
import io.nats.client.Subscription;
import lombok.Data;
import lombok.extern.log4j.Log4j2;
@Log4j2
@Data
public class NatsMessageListenerAdapter {
private NatsConnection natsConnection;
private String subject;
private NatsMessageListener adapter;
private Subscription subscription;
private Dispatcher dispatcher;
public void start(){
assert natsConnection != null && natsConnection.getConnection() != null && subject != null && adapter != null;
log.debug("Creating Message Listener...");
dispatcher = this.natsConnection.getConnection().createDispatcher((msg) -> {});
subscription = dispatcher.subscribe(this.subject, (msg) -> {
adapter.onMessage(msg.getData());
});
log.debug("Subscribed to: {}",this.subject);
}
public void stop(){
assert dispatcher != null && subject != null;
log.debug("Unsubscribing from: {}", subject);
dispatcher.unsubscribe(subject,300);
}
}
Listing 7-8.src/main/java/com/apress/nats/NatsMessageListenerAdapter.java
清单 7-8 展示了实现NatsMessageListener
的NatMessageListenerAdapter
类。在继续之前分析这个类。在 Java NATS 客户端中,有两种获取消息的方式:同步和异步。您正在实现异步方式。要使用它,您需要创建一个Dispatcher
实例(基于连接)并订阅主题(与主题相同)。当您需要删除订阅时,您只需要从Dispatcher
实例中调用unsubscribe
方法。
Note
代码使用 Lombok 中的@Log4j2
注释来注入日志。通常情况下,你不会这样使用它。您需要使用 AOP 来创建您的横切关注点。
现在您已经有了生产者(NatsTemplate
)和消费者(NatsMessageListener
),让我们来创建配置。创建NatsConfiguration
类(参见清单 7-9 )。
package com.apress.nats;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@EnableConfigurationProperties(NatsProperties.class)
@Configuration
public class NatsConfiguration {
@Bean
@ConditionalOnMissingBean
public NatsConnection natsConnection(NatsProperties natsProperties) throws IOException, InterruptedException {
return new NatsConnection(natsProperties);
}
@Bean
@ConditionalOnMissingBean
public NatsTemplate natsTemplate(NatsConnection natsConnection){
return new NatsTemplate(natsConnection);
}
}
Listing 7-9.src/main/java/com/apress/nats/NatsConfiguration.java
清单 7-9 显示了NatsConfiguration
类,它创建了NatsConnection
和NatsTemplate
Spring bean。请注意,您使用的是@ConditionalOnMissingBean
,当另一个使用该库的类创建自己的具有不同实现或值的 bean 时,这很有用,因此您可以避免拥有几个相同类型的 bean。
就这样。这是连接、产生和使用消息的 nats 消息库。现在,您可以用清单 7-10 中的代码测试它。您可以创建NatsProducerConsumer
类,也可以将这段代码添加到NatsMessagingApplicationTest
类中。
package com.apress.nats;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.nio.charset.StandardCharsets;
@Log4j2
@Configuration
public class NatsProducerConsumer {
@Bean(initMethod = "start",destroyMethod = "stop")
public NatsMessageListenerAdapter natsMessageListenerAdapter(NatsConnection natsConnection){
NatsMessageListenerAdapter adapter = new NatsMessageListenerAdapter();
adapter.setNatsConnection(natsConnection);
adapter.setSubject("test");
adapter.setAdapter( message -> {
log.info("Received: {}", new String(message, StandardCharsets.UTF_8));
});
return adapter;
}
@Bean
public ApplicationRunner sendMessage(NatsTemplate natsTemplate){
return args -> {
natsTemplate.send("test","Hello There!");
};
}
}
Listing 7-10.src/main/java/com/apress/nats/NatsProducerConsumer.java
要运行此应用,您需要启动并运行 NATS 服务器。可以用下面的命令运行它(我用的是 Docker)。
$ docker run -d --rm --name nats -p 4222:4222 nats
现在,您可以在 IDE 中或通过使用以下命令行来执行该应用。
$ ./mvnw spring-boot:run
您应该在日志中看到以下内容。
NatsTemplate : Sending: Hello There!
NatsProducerConsumer : Received: Hello There!
恭喜你!您已经创建了用于下一个模块的nats-messaging
库。现在,您可以使用以下命令停止您的 NATS 服务器。
$ docker stop nats
停止你的应用。
Warning
在继续之前,注释掉所有NatProducerConsumer.java
类的代码。
NATS 绑定器实现:nats-messaging-binder
让我们从 binder 实现开始。打开浏览器,指向 https://start.spring.io
。使用以下元数据。
-
组:
com.apress.nats
-
神器:
nats-messaging-binder
-
包装:
com.apress.nats
-
从属关系:龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩到nats-binder
目录中,并导入到您喜欢的 IDE 中(参见图 7-4 )。
图 7-4。
spring Initializr NATs-messaging-binder
让我们首先打开pom.xml
文件,用清单 7-11 中的内容替换它。
<?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>com.apress.nats</groupId>
<artifactId>nats-binder</artifactId>
<version>0.0.1</version>
<relativePath>..</relativePath>
</parent>
<packaging>jar</packaging>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-binder</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nats-messaging-binder</name>
<description>Demo project for Spring Boot</description>
<properties>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Listing 7-11.nats-binder/nats-messaging-binder/pom.xml
清单 7-11 显示了nats-messaging-binder
模块的pom.xml
文件。注意,您将nats-messaging
模块声明为一个依赖项。
接下来,让我们按照步骤创建一个新的活页夹
实现绑定器接口
如果你回顾一下Binder
接口,你会发现在你实现它之前你需要几个类(见清单 7-1 )。您需要传递的参数之一是分别用于消费者和生产者方法的入站和出站绑定目标。您可以为此创建所有的逻辑,并遵循创建不同类型的通道、消息传递支持、消息转换器等的实践,但是这需要太长的时间。如果您依赖于一些抽象实现,而这些实现已经去掉了需要通过通道等完成的底层基础设施,那该怎么办呢?
您可以使用org.springframework.cloud.stream.binder.AbstractMessageChannelBinder
类,它扩展了实现org.springframework.cloud.stream.binder.Binder
接口的org.springframework.cloud.stream.binder.AbstractBinder
类。AbstractMessageChannelBinder
类提供了为通道、连接、重试逻辑、目的地创建等创建基础设施所需的所有逻辑。这是要扩展的主要类。如果你查看它的签名,你会看到清单 7-12 中的代码。
public abstract class AbstractMessageChannelBinder<C extends ConsumerProperties, P extends ProducerProperties, PP extends ProvisioningProvider<C, P>>
extends AbstractBinder<MessageChannel, C, P> implements
PollableConsumerBinder<MessageHandler, C>, ApplicationEventPublisherAware
{
// ...
}
Listing 7-12.org.springframeworl.cloud.stream.binder.AbstractMessageChannelBinder.java
清单 7-12 是AbstractMessageChannelBinder
类的一个片段,它需要ConsumerProperties
、ProducerProperties,
和ProvisioningProvider
类。让我们从创建ProvisionProvider
实现开始。创建NatsMessageBinderProvisioningProvider
类(参见清单 7-13 )。
package com.apress.nats;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.cloud.stream.provisioning.ProvisioningException;
import org.springframework.cloud.stream.provisioning.ProvisioningProvider;
public class NatsMessageBinderProvisioningProvider implements ProvisioningProvider<ConsumerProperties, ProducerProperties> {
@Override
public ProducerDestination provisionProducerDestination(String name, ProducerProperties properties) throws ProvisioningException {
return new NatsMessageBinderDestination(name);
}
@Override
public ConsumerDestination provisionConsumerDestination(String name, String group, ConsumerProperties properties) throws ProvisioningException {
return new NatsMessageBinderDestination(name);
}
}
Listing 7-13.src/main/java/com/apress/nats/NatsMessageBinderProvisioningProvider.java
清单 7-13 显示了用ConsumerProperties
和ProducerProperties
具体类作为参数实现ProvisioningProvider
的NatsMessageBinderProvisioningProvider
类。这些类帮助所有的spring.cloud.stream.bindings.[destinationName].[consumer|producer]
属性。注意,在实现中,您正在发送一个新的NatsMessageBinderDestination
类实例。所以,让我们创建它(见清单 7-14 )。
package com.apress.nats;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
@AllArgsConstructor
@Data
public class NatsMessageBinderDestination implements ProducerDestination, ConsumerDestination {
private final String destination;
@Override
public String getName() {
return this.destination.trim();
}
@Override
public String getNameForPartition(int partition) {
throw new UnsupportedOperationException("Partition not yet implemented for Nats Binder");
}
}
Listing 7-14.src/main/java/com/apress/nats/NatsMessageBinderDestination.java
清单 7-14 显示了实现了ProducerDestination
和ConsumerDestination
接口的NatsMessageBinderDestination
。ProducerDestination
接口声明getName()
和getNameForPartition
,ConsumerDestination
接口声明getName()
。这为底层通道和集成基础设施创建了目的地和所有连线。请注意,您现在没有实现分区特性。
现在您已经有了ProvisioningProvider
接口实现,您必须通过创建消费者端点和侦听器来消费来自 NATS 服务器的传入消息。这意味着你在AbstractMessageChannelBinder
中覆盖了createConsumerEndpoint
方法,这个方法需要返回MessageProducer
。让我们使用一个实现所有必要逻辑并覆盖所需方法的类。其中一个类是MessageProducerSupport
,它是生产者端点的支持类,用于创建输出通道;它有发送消息的方法。因此,让我们创建NatsMessageBinderProducer
类(参见清单 7-15 )。
package com.apress.nats;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.SerializationUtils;
import java.nio.charset.StandardCharsets;
@Log4j2
public class NatsMessageBinderProducer extends MessageProducerSupport {
private ConsumerDestination destination;
private NatsMessageListenerAdapter adapter = new NatsMessageListenerAdapter();
public NatsMessageBinderProducer(ConsumerDestination destination, NatsConnection natsConnection){
assert destination != null && natsConnection != null;
adapter.setSubject(destination.getName());
adapter.setNatsConnection(natsConnection);
adapter.setAdapter(messageListener);
}
@Override
protected void doStart() {
adapter.start();
}
@Override
protected void doStop() {
adapter.stop();
super.doStop();
}
private NatsMessageListener messageListener = message -> {
log.debug("[BINDER] Message received from NATS: {}",message);
log.debug("[BINDER] Message Type received from NATS: {}",message.getClass().getName());
this.sendMessage((Message<?>)SerializationUtils.deserialize(message));
};
}
Listing 7-15.src/main/java/com/apress/nats/NatsMessageBinderProducer.java
清单 7-15 显示了扩展MessageProducerSupport
类的NatsMessageBinderProducer
类。您唯一可以覆盖的方法是doStart()
和doStop()
。让类业务逻辑处理剩下的事情。在本课中,您需要设置连接到 NATS 服务器的侦听器。看看需要一个NatsConnection
实例的构造函数。当底层引导调用doStart()
方法时,您开始监听。当收到消息时,使用sendMessage
方法将其反序列化为Messager<?>
类型,这是一个包含头和有效负载的包装类。
现在是时候扩展AbstractMessageChannelBinder
类了(绑定er
实现)。创建NatsMessageBinder
类(参见清单 7-16 )。
package com.apress.nats;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.integration.core.MessageProducer;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@Log4j2
public class NatsMessageBinder extends AbstractMessageChannelBinder<ConsumerProperties, ProducerProperties,NatsMessageBinderProvisioningProvider> {
private NatsTemplate natsTemplate;
public NatsMessageBinder(String[] headersToEmbed, NatsMessageBinderProvisioningProvider provisioningProvider, NatsTemplate natsTemplate) {
super(headersToEmbed, provisioningProvider);
this.natsTemplate = natsTemplate;
}
@Override
protected MessageHandler createProducerMessageHandler(ProducerDestination destination, ProducerProperties producerProperties, MessageChannel errorChannel) throws Exception {
return message -> {
assert natsTemplate != null;
log.debug("[BINDER] Sending to NATS: {}",message);
natsTemplate.send(destination.getName(),message);
};
}
@Override
protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group, ConsumerProperties properties) throws Exception {
assert natsTemplate != null;
return new NatsMessageBinderProducer(destination, this.natsTemplate.getNatsConnection());
}
}
Listing 7-16.src/main/java/com/apress/nats/NatsMessageBinder.java
清单 7-16 显示了NatsMessageBinder
类,我们的主要绑定器实现。看一下构造函数,其中需要调用基类(AbstractMessageChannelBinder
)传递消息头(例如,自定义消息头或与代理相关的消息头)、ProvisioningProvider
( NatsMessageBinderProvisioningProvider
类)和发送消息的NatsTemplate
。
我们覆盖了createProducerMessageHandler
,它返回MessageHandler.
它有消息要发送到 NATS 服务器。这就是为什么使用NatsTemplate
实例来获取目的地名称和消息。另外,我们覆盖了createConsumerEndPoint
,它返回了NatMessageBinderProducer
类的一个实例。记住,这个类是从接收消息的监听器开始的。
创建@配置 Beans
现在我们已经有了绑定器实现,是时候创建配置和执行绑定器自动配置的 Spring beans 了。创建NatsMessageBinderConfiguration
类(参见清单 7-17 )。
package com.apress.nats;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@EnableConfigurationProperties(NatsProperties.class)
@Import(NatsConfiguration.class)
@Configuration
public class NatsMessageBinderConfiguration {
@Bean
public NatsMessageBinderProvisioningProvider natsMessageBinderProvisioningProvider(){
return new NatsMessageBinderProvisioningProvider();
}
@Bean
public NatsMessageBinder natsMessageBinder(NatsMessageBinderProvisioningProvider natsMessageBinderProvisioningProvider, NatsTemplate natsTemplate){
return new NatsMessageBinder(null,natsMessageBinderProvisioningProvider, natsTemplate);
}
}
Listing 7-17.src/main/java/com/apress/nats/NatsMessageBinderConfiguration.java
清单 7-17 显示了NatsMessageBinderConfiguration
类。这个类正在导入包含NatsTemplate
和NatsConnection
的NatsConfiguration
类(参见清单 7-9 )。这里我们定义了 bind er
、natsMessageBinderProvisioningProvider,
和natsMessageBinder
,Spring beans 需要它们来连接 bind 的所有东西。在natsMessageBinder
方法中,我们返回一个有几个参数的NatsMessageBinder
类的新实例。现在,您将把null
传递给头部。你可以以后再处理他们。
创建 META-INF/spring.binders
接下来,您需要将配置添加到spring.binders
文件中。在src/main/resources
路径下创建META-INF
文件夹,用清单 7-18 中的内容创建spring.binders
文件。
nats:\
com.apress.nats.NatsMessageBinderConfiguration
Listing 7-18.src/main/resources/META-INF/spring.binders
清单 7-18 显示了spring.binders
文件,这是自动配置工作所必需的。这意味着如果你添加这个模块作为一个依赖项,它将使用spring.binders
来查找每个带有@Configuration
注释类的类,并执行自动配置逻辑来设置绑定器或任何其他配置。
注意,您将这个绑定器命名为nats
,这在一个流中使用多个绑定器时非常重要,这将在后面的小节中讨论。
NATS 绑定器试验
现在您已经有了nats-messaging
和nats-messaging-binder
模块,是时候测试它了。当然,有专门的测试类,但是我想向您展示使用这个活页夹并为以后保存单元/集成测试是多么容易。
打开浏览器,指向 https://start.spring.io
。使用以下元数据。
-
组:
com.apress.nats
-
神器:
nats-messaging-test
-
包装:
com.apress.nats
点击生成按钮下载一个 ZIP 文件。将其解压缩到nats-binder
目录,并导入到您喜欢的 IDE 中(参见图 7-5 )。
图 7-5
spring Initializr NATs-消息传递-测试
打开 pom.xml 并用清单 7-19 中的内容替换它。
<?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>com.apress.nats</groupId>
<artifactId>nats-binder</artifactId>
<version>0.0.1</version>
<relativePath>..</relativePath><!-- lookup parent from repository -->
</parent>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nats-messaging-test</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-binder</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Listing 7-19.nats-messaging-test/pom.xml
列表 7-19 显示pom.xml
。注意,您只使用了nats-messaging-binder
而没有其他依赖项,因为nats-messaging-binder
提供了您需要的一切,包括nats-messaging
模块。
接下来,让我们创建发送和接收消息的流。创建NatsStream
类(参见清单 7-20 )。
package com.apress.nats;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.core.MessageSource;
import org.springframework.messaging.support.GenericMessage;
@Log4j2
@EnableBinding({Source.class, Sink.class})
public class NatsStream {
@Bean
@InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedRate = "10000", maxMessagesPerPoll = "1"))
public MessageSource<String> timerMessageSource() {
return () -> new GenericMessage<>("Hello Spring Cloud Stream");
}
@StreamListener(Sink.INPUT)
public void process(Object message){
log.info("Received and ProcessedClass: {}", message.getClass().getName());
log.info("Received and Processed: {}", message);
}
}
Listing 7-20.src/main/java/com/apress/nats/NatsStream.java
清单 7-20 显示了NatsStream
类。看一下@EnableBinding
声明。我们使用 2.x 编程风格,带有Source
和Sink
接口。要生成消息,您可以使用 Spring Integration @InboundChannelAdapter
注释适配器。这个适配器的底层实现包含一个可轮询的逻辑,它基于来自@Poller
注释的fixedRated
参数每 10 秒调用并执行一次该方法。请注意,此适配器使用输出通道发送消息;在这种情况下,一个GenericMessage<>
类型(一个字符串)。如果您感到好奇,这与用于时间源应用启动器的逻辑相同。它使用@InboundChannelAdapter
每隔 T 秒发送一条消息。
@StreamListener
注释标记了一个从输入通道接收所有输入消息的方法。
接下来,让我们帮助活页夹命名频道/目的地并连接它们。如果不这样做,输入和输出通道/目的位置会被创建,但不会被连接。
打开application.properties
并使用清单 7-21 中的内容。
# Nats Bindings
spring.cloud.stream.bindings.output.destination=movie
spring.cloud.stream.bindings.input.destination=movie
# Debugging
logging.level.org.springframework.cloud.stream.messaging=DEBUG
logging.level.com.apress.nats=DEBUG
Listing 7-21.src/main/resources/application.properties
列表 7-21 显示application.properties
。请注意,我们使用的是 2.x 编程模型,命名约定基于输入/输出通道/目的地。所以,输入和输出通道必须有相同的名称来产生(源)和消耗(接收器);在这种情况下,您调用的是通道/目的地movie
。请注意,您使用调试日志记录级别来了解正在发生的事情。
在运行测试之前,确保您已经启动并运行了 Docker NATS 服务器映像容器。如果它没有运行,您可以使用以下命令运行它。
$ docker run -d --rm --name nats -p 4222:4222 nats
现在,您可以从您的 IDE 中运行它。如果您的 IDE 是智能的,它已经知道了配置。但是如果想从命令行运行,需要在根项目中添加以下文件(nats-binder
)。
$ cp -r nats-messaging/.mvn .
$ cp nats-messaging/mvnw* .
复制编译、安装和执行测试的 Maven 包装器。接下来,在项目(nats-binder
)中执行以下内容。
$ ./mvnw clean compile install
$ ./mvnw spring-boot:run -pl nats-messaging-test
如果一切顺利,您应该每 10 秒得到以下输出。
Received and Processed: Hello Spring Cloud Stream
还要查看日志的开头,那里有来自其他类的调试信息。您应该看到创建消息监听器和订阅:电影消息。
恭喜你!您已经创建了一个 NATS 服务器活页夹。现在你可以在任何你需要使用 NATS 的地方使用它,而不用担心任何 API。
不要忘记停止你的 NAT 服务器。
Notes
你可以在本书的配套代码的ch07/nats-binder
文件夹中找到所有的源代码。
多重绑定器
制作 NATS 活页夹很有趣,对吧?现在,让我们来看看当您需要多个活页夹时,如何解决一个特定的需求。到目前为止,您要么使用 Rabbit,要么使用 Kafka,但不是一起使用,或者可能与多个 Rabbit 代理一起使用,或者一个 Rabbit 作为源,一个 Kafka 作为处理器。
在本节中,您将学习如何使用多个活页夹,尤其是 NATS 活页夹和 RabbitMQ 活页夹。您创建了三个独立的项目:movie-file-source-nats
,它向 NATS 服务器公开 JSON 电影消息,movie-filter-processor-nats-rabbit
,它监听 NATS 并向 Rabbit 发送消息,最后,movie-log-sink-rabbit
,它记录来自 RabbitMQ 的消息(参见图 7-6 )。
图 7-6。
多重绑定器
电影-文件-源-国家
这个流从一个文件中读取所有的 JSON 电影,并使用 NATS 绑定器将它们发送到下一个流。打开浏览器,指向 https://start.spring.io
。使用以下元数据。
-
组:
com.apress.cloud.stream
-
神器:
movie-file-source-nats
-
包装:
com.apress.cloud.stream.movie
-
从属关系:龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。记下包装名称(参见图 7-7 )。
图 7-7。
Spring Initializr 电影-文件-源-国家
打开您的pom.xml
文件并添加以下两个依赖项。
<!-- NATS -->
<dependency>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-binder</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring Integration -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
接下来,创建Movie
类(参见清单 7-22 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Movie {
private String title;
private String actor;
private int year;
private String genre;
}
Listing 7-22.src/main/java/com/apress/cloud/stream/movie/Movie.java
如您所见,Movie
类与前几章中的一样。接下来,创建MovieStreamProperties
类。这个类保存了关于目录(JSON 电影所在的位置)和名称模式的信息(参见清单 7-23 )。
package com.apress.cloud.stream.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie.stream")
public class MovieStreamProperties {
private String directory;
private String namePattern;
}
Listing 7-23.src/main/java/com/apress/cloud/stream/movie/MovieStreamProperties.java
如你所见,这和其他章节中的是同一个类;没什么特别的。接下来,创建MovieStream
类(参见清单 7-24 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.file.dsl.Files;
import org.springframework.integration.file.splitter.FileSplitter;
import java.io.File;
@AllArgsConstructor
@EnableConfigurationProperties(MovieStreamProperties.class)
@EnableBinding(Source.class)
public class MovieStream {
private MovieStreamProperties movieStreamProperties;
@Bean
public IntegrationFlow fileFlow(){
return IntegrationFlows.from(Files
.inboundAdapter(new File(this.movieStreamProperties.getDirectory()))
.preventDuplicates(true)
.patternFilter(this.movieStreamProperties.getNamePattern()),
e -> e.poller(Pollers.fixedDelay(5000L)))
.split(Files.splitter().markers())
.filter(p -> !(p instanceof FileSplitter.FileMarker))
.transform(Transformers.fromJson(Movie.class))
.channel(Source.OUTPUT)
.get();
}
}
Listing 7-24.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 7-24 显示了MovieStream
类。请注意,您正在使用 2.x 版本的模型编程,其中您需要使用@EnableBinding
注释并提供类型,在本例中,是一个Source
类型。接下来,打开您的application.properties
文件并添加清单 7-25 中的内容。
# Nats Bindings
# Programming Style version 2.x
spring.cloud.stream.bindings.output.destination=movie
# Movie Stream Properties
movie.stream.directory=.
movie.stream.name-pattern=movies-json.txt
# Debugging
logging.level.com.apress.nats=DEBUG
logging.level.org.springframework.cloud.stream.messaging.DirectWithAttributesChannel=DEBUG
Listing 7-25.src/main/resources/application.properties
请注意,您正在重命名输出目的地movie
。
这是这条小溪的水。我知道代码看起来有些重复,但是它可以帮助您更好地理解这个概念。如何使用反应式编程来实现这一点?你需要做一点小小的改变。首先,你可以在fileFlow
中返回Publisher<Message<Movie>>
,而不是调用get()
方法(来获得IntegrationFlow
实例),使用toReactivePublisher()
。第二,你需要创建一个供应商。请记住,您需要订阅发布者。您需要为此声明一个Supplier
方法。第三,您需要使用spring.cloud.stream.bindings.[suplier-method-name]-out-0.destination
属性。
对于这个流,我在项目的根处添加了movies-json.txt
。它包含以下内容。
{"title":"The Matrix","actor":"Keanu Reeves","year":1999,"genre":"fiction"}
{"title":"Memento","actor":"Guy Pearce","year":2000,"genre":"drama"}
{"title":"The Prestige","actor":"Christian Bale","year":2006,"genre":"drama"}
{"title":"Disturbia","actor":"Shia LaBeouf","year":2007,"genre":"drama"}
Note
您可以在 ch07/multiple 文件夹中找到所有源代码。你会发现注释掉了反应版本。
电影-过滤器-处理器-NATs-兔子
接下来,让我们创建一个根据电影的类型值过滤电影的处理器。这个流使用 NATS(用于输入)和 RabbitMQ(用于输出)绑定器。打开浏览器,指向 https://start.spring.io
。使用以下元数据。
-
组:
com.apress.cloud.stream
-
神器:
movie-filter-processor-nats-rabbit
-
包装:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。记下包名。
打开pom.xml
文件并添加以下依赖项。
<!-- NATS -->
<dependency>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-binder</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- RabbitMQ Binder -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
接下来,创建Movie
类。您可以使用与清单 7-22 中相同的代码。接下来,您需要创建MovieStream
类(参见清单 7-26 )。
package com.apress.cloud.stream.movie;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.integration.annotation.Filter;
@EnableBinding(Processor.class)
public class MovieStream {
String GENRE_DRAMA = "drama";
@Filter(inputChannel = Processor.INPUT,outputChannel = Processor.OUTPUT)
public boolean onlyDrama(Movie movie) {
return movie.getGenre().equals(GENRE_DRAMA);
}
}
Listing 7-26.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
这里您仍然使用 2.x 版本的模型,其中您需要声明@EnableBinding
及其类型,在本例中,是带有输入和输出处理器的处理器。还要注意,您使用的 Spring Integration @Filter
注释需要根据计算的表达式返回一个布尔值。这种情况下,你是在评估体裁是不是戏剧;如果是,就让它过去吧。还要注意,@Filter
注释需要两个参数:inputChannel
和outputChannel
;在这种情况下,它们是处理器类型成员。
您可以在该项目的源代码中找到反应版本。
接下来,我们把application.properties
改名为application.yaml
。添加清单 7-27 中的内容。
spring:
cloud:
stream:
bindings:
input:
binder: nats
destination: movie
output:
binder: rabbit
destination: log
Listing 7-27.src/main/resources/application.yaml
在这种情况下,您使用 YAML,因为它比属性更清晰易懂。请注意,您正在使用设置为nats
的spring.cloud.stream.binding.input.binder
(该名称来自nats-messaging-binder
模块的META-INF/spring.binders
)。你把spring.cloud.stream.binding.input.destination
设定为movie
。将output.binder
设置为rabbit
,将output.destination
设置为log
。
如果您从MovieStream
类中启用反应部分,您需要使用绑定的命名约定。您可以在application.yaml
文件中找到这段注释掉的代码。
电影-原木-水槽-兔子
接下来,是电影-原木-水槽-兔子流。对于这个流日志,您使用一个旧的样式和一个 Spring Integration XML 文件来创建日志接收器。打开浏览器,指向 https://start.spring.io
。使用以下元数据。
-
组:
com.apress.cloud.stream
-
神器:
movie-log-sink-rabbit
-
包装:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
您可以按生成按钮下载一个 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。记下包名。
打开pom.xml
文件并添加以下依赖项。
<!-- RabbitMQ Binder -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
接下来,创建Movie
类。你可以使用清单 7-22 中的代码。然后,创建MovieStream
类(参见清单 7-28 )。
package com.apress.cloud.stream.movie;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource({"/META-INF/spring/movie-log.xml"})
@EnableBinding(Sink.class)
public class MovieStream {
}
Listing 7-28.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
您正在使用带有接收器类型作为参数的@EnableBinding
。注意,您正在使用@ImportResource
注释来加载一个遗留的 XML 文件。接下来,创建META-INF/spring/movie-log.xml
文件(参见清单 7-29 )。
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
https://www.springframework.org/schema/integration/spring-integration.xsd">
<int:json-to-object-transformer
input-channel="input"
output-channel="logger"
type="com.apress.cloud.stream.movie.Movie"/>
<int:logging-channel-adapter id="logger"
logger-name="LOG"
level="INFO"
expression="payload"/>
</beans>
Listing 7-29.src/main/resources/META-INF/spring/movie-log.xml
清单 7-29 显示了遗留的 XML Spring Integration。我认为任何仍然使用 XML 方法的遗留 Spring 系统都可以很容易地现代化到 Spring Boot,因为您可以重用 XML 并利用性能优势(这个主题将在以后讨论)。
正如您所看到的,您正在使用json-to-object-transformer
组件(因为来自 RabbitMQ 的数据是一个 application/JSON 类型)只是为了转换成一个对象并让toString()
格式登录到控制台。注意,转换器将input-channel
属性设置为input
(绑定的名称),将output-channel
属性设置为logger
,?? 是logging-channel-adapter
组件的 ID。
最后打开application.properties
,添加以下内容。
# Binding RabbitMQ
spring.cloud.stream.bindings.input.destination=log
一起运行它们
您已经准备好运行所有的东西,但是要运行所有的流,您需要确保 NATS 服务器和 RabbitMQ 代理已经启动并且正在运行。我在源代码里加了一个docker-compose.yml
文件。它包含两个服务器。您可以使用 Docker Compose 来代替手动启动它们(参见清单 7-30 )。
version: '3'
services:
nats:
image: 'nats:latest'
container_name: nats
ports:
- '4222:4222'
- '8222:8222'
rabbit:
image: 'rabbitmq:3.8.3-management-alpine'
container_name: rabbit
ports:
- '15672:15672'
- '5672:5672'
Listing 7-30.docker-compose.yml
打开终端窗口并转到该文件。运行以下命令启动服务器。
$ docker-compose up
转到您的 IDE 并运行您的流,从日志和处理器开始;或者您可以使用下面众所周知的 Maven 命令来运行这个流。
$ ./mvnw spring-boot-run.
您需要最后运行movie-file-source-nats
来读取movies-json.txt
文件,这是众所周知的项目根目录。运行它之后,movie-log-sink-rabbit 应该只播放三部戏剧类型的电影。
Movie(title=Memento, actor=Guy Pearce, year=2000, genre=drama)
Movie(title=The Prestige, actor=Christian Bale, year=2006, genre=drama)
Movie(title=Disturbia, actor=Shia LaBeouf, year=2007, genre=drama)
恭喜你!你用了多个经纪人!
当你有多个相同类型的经纪人时会发生什么?换句话说,您有一个处理器正在监听位于东海岸的 RabbitMQ 服务器,您需要处理消息并将其发送到西海岸的 RabbitMQ 服务器。使用相同的原则和命名约定,在application.yaml
中使用以下配置。
spring:
cloud:
stream:
bindings:
input:
destination: movie
binder: rabbit1
output:
destination: log
binder: rabbit2
binders:
rabbit1:
type: rabbit
environment:
spring:
rabbitmq:
host: east-coast.mydomain.com
rabbit2:
type: rabbit
environment:
spring:
rabbitmq:
host: west-coast.mydomain.com
username: admin
password: {cipher}c789b2ee5bd
如你所见,添加多个相同类型的活页夹非常容易。
额外配置
尽管我在前一章讨论了配置,但是还有其他值得一提的属性。查看org.springframework.cloud.stream.config.BindingServiceProperties
类和 Javadoc 以获得关于这些属性的更多信息。
摘要
在本章中,我向您展示了如何通过以下三个步骤创建一个定制的绑定:从Binder
接口实现,添加创建绑定er,
的配置,最后,将绑定配置添加到spring.binders
文件中。当 Spring Boot 自动配置由于类路径中的发现而启动时,将使用该选项。
您创建了一个 NATS 活页夹,并在多个活页夹中使用。本章使用了不同的方法来创建流:使用 2.x 版,其中需要声明@Binding
注释及其类型(源、处理器或接收器),使用 3.x 版,其中可以使用函数式和反应式编程,或者使用旧的 Spring Integration 注释和遗留 XML 中的一些代码。
在下一章,我将讨论 Spring CloudStream,以及 Spring Cloud Stream 和 binder 技术如何融入我们的解决方案。
八、Spring CloudStream:介绍和安装
在前面的章节中,您看到了作为独立微服务运行的 Spring CloudStream 应用。您可以创建一个 CloudStream 式应用的组合,以形成一个完整、健壮、可扩展的系统。您可以通过下载最新的优步-JAR 并执行java -jar
命令来运行它们。你可以使用 Docker Compose 的 Docker 图片来运行它们。您了解了如何在 1.x 到 2.x 版本中使用注释创建自定义 CloudStream 式应用,以及在 3.x 版本中使用函数式或反应式编程创建自定义 CloudStream 式应用。
现在你有了多个 CloudStream 式应用,应该有一种技术来管理它们。您需要能够运行您的应用,在一个应用关闭时协调它们,管理复合流的版本,用触发器编排它们,添加响应特定事件的挂钩,添加安全性以安全地连接到外部系统,调度批处理作业,等等。
既然你用过 Docker Compose,Docker Swarm 可能是个不错的解决方案。是的,但是你需要一些东西来管理应用的生命周期和整个复合流。当新的参数被设置或多个部署到不同的平台时,您需要一些东西来处理版本。您需要一种方法来收集关于您的流的信息,如日志和指标,并管理您的解决方案的各个方面。说到可伸缩性、弹性和高可用性,您可以依赖 Cloud Foundry 或 Kubernetes 这样的云平台,甚至更好的是 Red Hat OpenShift。
本章讨论 Spring Cloud Data Flow,这是一种流生命周期技术,它提供了创建健壮的、基于流的微服务和批处理数据处理解决方案所需的一切。
SpringCloudStream
Spring Cloud Data Flow 是一种开源技术,它为流和批处理数据管道组成了复杂的拓扑结构。它使用前面章节中介绍的预构建微服务,并允许您开发和测试用于数据集成的微服务。它可以独立使用,也可以在任何云平台中使用,如 Cloud Foundry 或 Kubernetes,甚至更好的是,您可以使用 Red Hat OpenShift 中的所有附加功能。
特征
让我们回顾一下 Spring Cloud Data Flow 为解决复杂的流拓扑而提供的一些主要特性。
-
编程模式。您在之前的章节中已经看到了这一点,在这些章节中,您使用了 Spring Cloud Stream 框架来创建流式应用。即使您还没有看到它的运行,您也可以使用 Spring Cloud Task framework 创建或触发一个批处理解决方案,该解决方案使用 Spring Batch 定义 ETL(提取、转换、加载)作业。正如您已经知道的,有老式的 Spring Integration(通过通道)、函数式(使用 Java 8+)和反应式编程(Kafka Streams)编程模型。
-
多语种。将流式应用创建为微服务的一个好处是,您可以使用 Python、Groovy、。NET 或任何其他语言。Spring CloudStream 可以启动您的应用来连接您的流。
-
消息代理绑定器。无论使用哪种消息中间件代理,您都可以将相同的代码与可插入绑定器一起使用。Spring Cloud Stream 团队支持 RabbitMQ 和 Kafka 开箱即用,但你可以在社区中找到多个 binder 实现。基于您在前面章节中所学的内容,您可以创建一个绑定器,并将其用于 Spring CloudStream。
-
应用启动器。Spring CloudStream 使用带有 Docker 或 Maven 工件的 Spring Cloud 流式应用启动器来创建流解决方案。您可以看到,您可以轻松地注册您的定制流,并在仪表板中使用它,这样您就有了流的可视化表示。
-
安全。我还没有谈到安全性,但是 Spring Cloud Data Flow 不仅允许您保护仪表板,还允许您使用 OAuth2 或 OpenID Connect 等安全标准进行身份验证和授权来保护所有的微服务。
-
连续交货。使用 Spring CloudStream 的好处之一是在升级流时避免停机。这是通过将金丝雀或蓝绿色的部署实践应用到您的流中,并添加持续交付和持续集成工具来实现的。
-
批处理。在 Spring Cloud Data Flow 中,您可以使用详细的状态报告和重新启动失败作业的方法来管理任何批处理作业的执行,因为它可以安装在任何版本的 Cloud Foundry 和 Kubernetes 中,所以您可以在 Spring Cloud Data Flow 仪表板中调度任何批处理作业。
-
特定领域语言。Spring CloudStream 提供了一种特定于领域的语言(DSL ),使用| pipelines 直观地显示与下一个 app 的连接。
Spring CloudStream 是流拓扑/应用的编排者。它创建了强大的集成解决方案。它提供了通过使用 REST API、shell 工具或 GUI 仪表板来可视化、运行、部署、更改和管理流版本的方法。让我们从本地安装开始,这样您就可以看到 Spring CloudStream 的运行。
本地安装
您可以在本地或开发环境中运行 Spring CloudStream。本节中的说明不适用于生产环境。请记住,如果您需要生产级指令,您需要依赖一个能够带来可伸缩性、弹性、容错、高可用性、存储、监控等功能的平台,例如 Kubernetes、Cloud Foundry、Mesos 或 Yarn。
单台机器/服务器
在接下来的部分中,我将描述如何使用 RabbitMQ 和 Kafka 作为绑定器。在其他章节中,您可以重用前面章节中的 NATs 活页夹;现在,要么选择 RabbitMQ,要么选择 Kafka。值得一提的是,这里使用的服务器(Skipper 和数据流)默认使用 H2 作为持久性引擎。这个 DB 引擎是一个内存中的数据库,这意味着一旦您完成,任何创建的流、注册的作业或应用都将消失。因此,下面几节使用 MySQL 作为持久性引擎。如果您愿意,可以使用 PostgreSQL 或 Oracle 这是一个改变 Spring Data 源属性的问题。
使用 RabbitMQ 作为绑定器,使用 MySQL 作为持久性
为了使事情更简单,创建一个名为workspace-rabbit-mysql
的文件夹。
要在单台机器上启动和运行 Spring CloudStream,需要执行以下步骤。workspace-rabbit-mysql
是目录,RabbitMQ 是绑定器,MySQL 是持久性。有些步骤是可选的,但是它们为将来的测试设置了环境。
-
Create a
download.sh
script in theworkspace-rabbit-mysql
folder, with the following content.#!/bin/sh wget https://repo.spring.io/release/org/springframework/cloud/spring-cloud-dataflow-server/2.6.0/spring-cloud-dataflow-server-2.6.0.jar wget https://repo.spring.io/release/org/springframework/cloud/spring-cloud-dataflow-shell/2.6.0/spring-cloud-dataflow-shell-2.6.0.jar wget https://repo.spring.io/release/org/springframework/cloud/spring-cloud-skipper-server/2.5.0/spring-cloud-skipper-server-2.5.0.jar
在写这本书的时候,我对数据流服务器和 shell 使用了 2.6.0 版本,对 Skipper 服务器使用了 2.50 版本。使脚本可执行并执行它。注意,您下载了三个 jar、Skipper、数据流服务器和 shell。
-
在
workspace-rabbit-mysql
文件夹中,创建包含以下内容的docker-compose.yml
文件。version: '3' services: mysql: image: mysql:5.7.25 container_name: dataflow-mysql environment: MYSQL_DATABASE: dataflow MYSQL_USER: root MYSQL_ROOT_PASSWORD: rootpw ports: - "3306:3306" rabbitmq: image: rabbitmq:3.8.3-alpine container_name: dataflow-rabbitmq ports: - "5672:5672"
-
In the
workspace-rabbit-mysql
folder, create thestartup-skipper.sh
script file with the following content and make it executable.#!/bin/sh java -jar spring-cloud-skipper-server-2.5.0.jar \ --spring.datasource.url=jdbc:mysql://localhost:3306/dataflow \ --spring.datasource.username=root \ --spring.datasource.password=rootpw \ --spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
注意,您正在声明 Spring Data 源。如果不添加这些属性,Skipper 和数据流服务器将使用 H2 嵌入式引擎作为默认的持久性机制。注意,现在,我在每个属性中使用
localhost
,这意味着如果您有远程服务器,您可以更改它。 -
In the
workspace-rabbit-mysql
folder, create thestartup-dataflow.sh
script file with the following content and make it executable.#!/bin/sh java -jar spring-cloud-dataflow-server-2.6.0.jar \ --spring.datasource.url=jdbc:mysql://localhost:3306/dataflow \ --spring.datasource.username=root \ --spring.datasource.password=rootpw \ --spring.datasource.driver-class-name=org.mariadb.jdbc.Driver \ --spring.cloud.dataflow.applicationProperties.stream.spring.rabbitmq.host=localhost
注意,您正在添加
stream.spring.rabbitmq
属性。如果你不指定它们,那么它使用 Kafka 作为默认的活页夹。 -
执行
workspace-rabbit-mysql
文件夹中的docker-compose
命令。$ docker-compose up -d
-d
选项将docker-compose
进程发送到后台。请记住,您需要稍后回到这里,因为要关闭服务。 -
在另一个终端窗口中,执行以下命令。首先启动 Skipper 服务器。
$ ./startup-skipper.sh
-
在新的终端中,启动 Spring CloudStream 服务器。
$ ./startip-dataflow.sh
使用 Kafka 作为绑定器,使用 MySQL 作为持久性
如果你想使用 Kafka 作为绑定器,创建一个workspace-kafka-mysql
文件夹来保存配置。
使用workspace-kafka-mysql
目录结构、Kafka 作为绑定器、MySQL 作为持久性,在单台机器上启动和运行 Spring CloudStream 需要以下步骤。
-
重用之前的
download.sh
脚本;你可以在这里复制。如果您已经执行了它,那么将 JARs 移动到workspace-kafka-mysql
文件夹结构中。 -
在
workspace-kafka-mysql
文件夹中,创建包含以下内容的docker-compose.yml
文件。version: '3' services: mysql: image: mysql:5.7.25 container_name: dataflow-mysql environment: MYSQL_DATABASE: dataflow MYSQL_USER: root MYSQL_ROOT_PASSWORD: rootpw ports: - "3306:3306" zookeeper: image: 'bitnami/zookeeper:latest' container_name: zookeeper networks: - kafka-net ports: - '2181:2181' environment: - ALLOW_ANONYMOUS_LOGIN=yes kafka: image: 'bitnami/kafka:latest' container_name: kafka networks: - kafka-net ports: - '9092:9092' environment: - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 - ALLOW_PLAINTEXT_LISTENER=yes depends_on: - zookeeper networks: kafka-net:
-
在
workspace-kafka-mysql
文件夹中,创建startup-skipper.sh
脚本文件。它和workspace-rabbit-mysql
一样,所以你可以在这里复制它。 -
In the
workspace-kafka-mysql
folder, create thestartup-dataflow.sh
script file with the following content and make it executable.java -jar spring-cloud-dataflow-server-2.6.0.jar \ --spring.datasource.url=jdbc:mysql://localhost:3306/dataflow \ --spring.datasource.username=root \ --spring.datasource.password=rootpw \ --spring.datasource.driver-class-name=org.mariadb.jdbc.Driver \ --spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.binder.brokers=PLAINTEXT://localhost:9092 \ --spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.streams.binder.brokers=PLAINTEXT://localhost:9092 \ --spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.binder.zkNodes=localhost:2181 \ --spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.streams.binder.zkNodes=localhost:2181
请注意,您正在传递 Kafka 活页夹的所有必要信息。
-
执行
workspace-kafka-mysql
文件夹中的docker-compose
命令。$ docker-compose up -d
-
-d
选项将docker-compose
进程发送到后台。请记住,您需要稍后返回,因为要关闭服务。 -
在另一个终端窗口中,执行以下命令启动 Skipper 服务器。
$ ./startup-skipper.sh
-
在新的终端中,启动 Spring CloudStream 服务器。
$ ./startup-dataflow.sh
您是否注意到,无论您选择 Rabbit 还是 Kafka 作为绑定器,都声明了相同的持久性机制?你注意到两台服务器上的日志了吗?您是否看到正在尝试连接到端口 8888 中的 Spring Cloud 配置服务器?如果需要使用两个绑定器,如何解决 MySQL 的重复问题?
您可以使用 Spring Cloud Config 来设置常见的配置,比如 MySQL 的持久性和特定于 binder 的配置。不使用命令行或application.properties/yaml
文件,您可以使用一个集中式服务器,这样 Skipper 和数据流服务器可以在启动时拥有这些配置。
使用 Spring Boot 配置功能
选择 RabbitMQ 或 Kafka,以避免将所有属性放在命令行中。您总是可以使用 Spring Boot 配置特性将应用的属性放在当前目录或一个config/
目录中。
我将图 8-1 所示的结构用于优步罐。
图 8-1。
带有配置/应用.属性和优步-jar 的工作区
config/application.properties
有以下内容。
## DataSource
spring.datasource.url=jdbc:mysql://localhost:3306/dataflow
spring.datasource.username=root
spring.datasource.password=rootpw
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
## Binder
spring.cloud.dataflow.applicationProperties.stream.spring.rabbitmq.host=localhost
spring.cloud.dataflow.applicationProperties.stream.spring.rabbitmq.username=guest
spring.cloud.dataflow.applicationProperties.stream.spring.rabbitmq.password=guest
您可以用下面的代码运行任一 JAR。
$ java -jar spring-cloud-skipper-server-2.6.0.jar
在本例中,Spring Boot 获取config/application.properties
并使用其内容连接到 MySQL 和 Rabbit。
Spring CloudStream 仪表板
完成本地环境设置后,打开浏览器并指向http://localhost:9393/dashboard
。你的屏幕看起来应该如图 8-2 所示。
图 8-2
Spring CloudStream 仪表板:http://localhost:9393/dashboard
图 8-2 为仪表板。一切顺利。不要担心显示的选项卡或其他链接。当你创建你的第一个流流时,我会讨论它们。
注册 CloudStream 式应用启动器
一旦服务器启动并运行,就该注册 CloudStream 式应用启动器了。在主控制面板中,单击+添加应用按钮。接下来,选择批量导入申请(见图 8-3 )。
图 8-3。
添加应用:http://localhost:9393/dashboard/#/apps/add
接下来,添加 URI。最简单的方法是点击列出所有流式应用的部分。例如,如果你正在使用 RabbitMQ,点击流式应用(RabbitMQ/Maven) (见图 8-4 )。
图 8-4。
从 URI 添加批量应用:http://localhost:9393/dashboard/#/apps/add/import-from-uri
下面列出了应用的 URIs。
-
卡夫卡
-
拉比特
-
任务
接下来,单击导入应用按钮。现在你应该已经导入了所有的应用(见图 8-5 )。
图 8-5。
导入的应用:http://localhost:9393/dashboard/#/apps
Note
我在源代码中添加了一些脚本,这些脚本可以在单台机器和 Spring CloudStream 服务器上本地启动。我假设你有docker-compose
、curl
和wget
命令。
独立的服务器或代理
如果你想让一个独立的机器单独运行,你可以这样做,但是你需要用spring.cloud.skipper.client.serverUri
属性告诉数据流服务器 Skipper 服务器在哪里。在命令行中将此属性设置为参数。
$ java -jar spring-cloud-dataflow-server-2.6.0.jar \
--spring.cloud.skipper.client.serverUri=https://my-other-server:7577/api
如果是在代理后面,需要将server.use-forward-headers
设置为true
,或者用以下参数启动数据流服务器。
$ java -jar spring-cloud-dataflow-server-2.6.0.jar \
--spring.cloud.skipper.client.serverUri=https://192.51.100.1:7577/api \
--server.use-forward-headers=true
这些是您需要在代理配置中使用的路径和 URL。
securityinfo:
path: /security/**
url: http://data-flow-server:9393/security
about:
path: /about/**
url: http://data-flow-server:9393/about
apps:
path: /apps/**
url: http://data-flow-server:9393/apps
dashboard:
path: /dashboard/**
url: http://data-flow-server:9393/dashboard
audit-records:
path: /audit-records/**
url: http://data-flow-server:9393/audit-records
jobs:
path: /jobs/**
url: http://data-flow-server:9393/jobs
streams:
path: /streams/**
url: http://data-flow-server:9393/streams
tasks:
path: /tasks/**
url: http://data-flow-server:9393/tasks
tools:
path: /tools/**
url: http://data-flow-server:9393/tools
runtime:
path: /rutime/**
url: http://data-flow-server:9393/runtime
completions:
path: /completions/**
url: http://data-flow-server:9393/completions
使用 Docker 合成
Docker 创建了本地开发和测试所需的基础设施。很好用,也很有效。所以,让我们从创建两个docker-compose
文件开始。我建议创建一个保存这些文件的文件夹。一个文件使用兔子,另一个使用卡夫卡的活页夹。
用清单 8-1 中的内容创建docker-compose-rabbitmq.yml
。
version: '3'
services:
mysql:
image: mysql:5.7.25
container_name: dataflow-mysql
environment:
MYSQL_DATABASE: dataflow
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: rootpw
expose:
- 3306
rabbitmq:
image: rabbitmq:3.8.3-alpine
container_name: dataflow-rabbitmq
expose:
- '5672'
dataflow-server:
image: springcloud/spring-cloud-dataflow-server:${DATAFLOW_VERSION:?DATAFLOW_VERSION variable needs to be set!}
container_name: dataflow-server
ports:
- "9393:9393"
environment:
- spring.cloud.dataflow.applicationProperties.stream.spring.rabbitmq.host=rabbitmq
- spring.cloud.skipper.client.serverUri=http://skipper-server:7577/api
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/dataflow
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=rootpw
- SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
depends_on:
- rabbitmq
entrypoint: "./wait-for-it.sh mysql:3306 -- java -jar /maven/spring-cloud-dataflow-server.jar"
volumes:
- ${HOST_MOUNT_PATH:-.}:${DOCKER_MOUNT_PATH:-/root/scdf}
skipper-server:
image: springcloud/spring-cloud-skipper-server:${SKIPPER_VERSION:?SKIPPER_VERSION variable needs to be set!}
container_name: skipper
ports:
- "7577:7577"
- "20000-20105:20000-20105"
volumes:
- ${HOST_MOUNT_PATH:-.}:${DOCKER_MOUNT_PATH:-/root/scdf}
app-import:
image: springcloud/openjdk:2.0.0.RELEASE
container_name: dataflow-app-import
depends_on:
- dataflow-server
command: >
/bin/sh -c "
./wait-for-it.sh -t 180 dataflow-server:9393;
wget -qO- 'http://dataflow-server:9393/apps' --post-data='uri=${STREAM_APPS_URI:-https://dataflow.spring.io/rabbitmq-maven-latest&force=true}';
echo 'Stream apps imported'
wget -qO- 'http://dataflow-server:9393/apps' --post-data='uri=${TASK_APPS_URI:-https://dataflow.spring.io/task-maven-latest&force=true}';
echo 'Task apps imported'"
Listing 8-1.docker-compose-rabbitmq.yml
注意,您使用的是DATAFLOW_VERSION
和SKIPPER_VERSION
环境变量,需要对它们进行设置以获得映像版本。使用这种方法,您可以找到任何版本的可重用 Docker 合成文件。当然,你可以用同样的形式添加 Rabbit 和 MySQL 版本。每个服务都使用指向服务名称的环境变量,这很有用,因为 Docker Compose 创建了一个 DNS,使得 DevOps 人员更容易使用名称而不是 IP。
如果你查看清单 8-1 ,你会在最后看到app-import
声明。即使你还没有看到 Spring Cloud Stream CloudStream 的组件,但是数据流服务器提供了一个 API,可以让你连接到它。在这种情况下,当您使用这个 Docker 合成文件时,您将发布一个 URI 来注册应用。
接下来,您可以用清单 8-2 中的内容创建docker-compose-kafka.yml
文件。
version: '3'
services:
mysql:
image: mysql:5.7.25
container_name: dataflow-mysql
environment:
MYSQL_DATABASE: dataflow
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: rootpw
expose:
- 3306
kafka-broker:
image: confluentinc/cp-kafka:5.3.1
container_name: dataflow-kafka
expose:
- "9092"
environment:
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka-broker:9092
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_ADVERTISED_HOST_NAME=kafka-broker
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
depends_on:
- zookeeper
zookeeper:
image: confluentinc/cp-zookeeper:5.3.1
container_name: dataflow-kafka-zookeeper
expose:
- "2181"
environment:
- ZOOKEEPER_CLIENT_PORT=2181
dataflow-server:
image: springcloud/spring-cloud-dataflow-server:${DATAFLOW_VERSION:?DATAFLOW_VERSION variable needs to be set!}
container_name: dataflow-server
ports:
- "9393:9393"
environment:
- spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.binder.brokers=PLAINTEXT://kafka-broker:9092
- spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.streams.binder.brokers=PLAINTEXT://kafka-broker:9092
- spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.binder.zkNodes=zookeeper:2181
- spring.cloud.dataflow.applicationProperties.stream.spring.cloud.stream.kafka.streams.binder.zkNodes=zookeeper:2181
- spring.cloud.skipper.client.serverUri=http://skipper-server:7577/api
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/dataflow
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=rootpw
- SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
depends_on:
- kafka-broker
entrypoint: "./wait-for-it.sh mysql:3306 -- java -jar /maven/spring-cloud-dataflow-server.jar"
volumes:
- ${HOST_MOUNT_PATH:-.}:${DOCKER_MOUNT_PATH:-/root/scdf}
app-import:
image: springcloud/openjdk:2.0.0.RELEASE
container_name: dataflow-app-import
depends_on:
- dataflow-server
command: >
/bin/sh -c "
./wait-for-it.sh -t 180 dataflow-server:9393;
wget -qO- 'http://dataflow-server:9393/apps' --post-data='uri=${STREAM_APPS_URI:-https://dataflow.spring.io/kafka-maven-latest&force=true}';
echo 'Stream apps imported'
wget -qO- 'http://dataflow-server:9393/apps' --post-data='uri=${TASK_APPS_URI:-https://dataflow.spring.io/task-maven-latest&force=true}';
echo 'Task apps imported'"
skipper-server:
image: springcloud/spring-cloud-skipper-server:${SKIPPER_VERSION:?SKIPPER_VERSION variable needs to be set!}
container_name: skipper
ports:
- "7577:7577"
- "20000-20105:20000-20105"
environment:
- SPRING_CLOUD_SKIPPER_SERVER_PLATFORM_LOCAL_ACCOUNTS_DEFAULT_PORTRANGE_LOW=20000
- SPRING_CLOUD_SKIPPER_SERVER_PLATFORM_LOCAL_ACCOUNTS_DEFAULT_PORTRANGE_HIGH=20100
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/dataflow
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=rootpw
- SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
entrypoint: "./wait-for-it.sh mysql:3306 -- java -Djava.security.egd=file:/dev/./urandom -jar /spring-cloud-skipper-server.jar"
volumes:
- ${HOST_MOUNT_PATH:-.}:${DOCKER_MOUNT_PATH:-/root/scdf}
Listing 8-2.docker-compose-kafka.yml
清单 8-2 显示的是docker-compose-kafka.yml
文件;注意和 RabbitMQ 差不多,用的是 Kafka 和 Zookeeper。还要检查属性并确定您使用的是服务名。
接下来,通过对 RabbitMQ 使用以下命令来启动您的基础设施。
$ export DATAFLOW_VERSION=2.6.0
$ export SKIPPER_VERSION=2.5.0
$ docker-compose -f docker-compose-rabbitmq.yml up
或者你用下面这句话来形容卡夫卡。
$ export DATAFLOW_VERSION=2.6.0
$ export SKIPPER_VERSION=2.5.0
$ docker-compose -f docker-compose-kafka.yml up
接下来,转到您的浏览器并打开http://localhost:9393/dashboard
。你的屏幕应该和图 8-5 一样。它应该已经导入了应用。
要关闭,如果您使用 RabbitMQ,请执行以下命令。
$ export DATAFLOW_VERSION=2.6.0
$ export SKIPPER_VERSION=2.5.0
$ docker-compose -f docker-compose-rabbitmq.yml down
如果您正在使用 Kafka,请执行以下命令。
$ export DATAFLOW_VERSION=2.6.0
$ export SKIPPER_VERSION=2.5.0
$ docker-compose -f docker-compose-kafka.yml down
请记住,shutdown 命令必须在 YAML 文件所在的目录中运行。
不可思议的安装
您还没有看到 Spring CloudStream 的强大功能以及它的能力。在接下来的章节中,您将创建可以占用多台机器的流。在某种程度上,你需要扩大规模,并且有能力洞察你的信息流发生了什么。当一些应用(在流程中)开始失败时会发生什么?您应该能够创建多个实例并对它们进行监控,如果一个实例关闭了,可以重新创建它。当然,您可以手动完成,但是当您有一个包含 100 个微服务的流程时,您需要依赖一个提供弹性、高可用性、可伸缩性和可见性的平台。
在开始之前,您需要安装并运行 Kubernetes。如果你有足够的 RAM 来运行 Spring CloudStream,或者在任何公共云中(IBM Cloud、Google、Amazon 或 Microsoft Azure)都有资源,你就可以使用你的电脑。
带有 Docker 桌面的个人电脑
Mac 或 Windows 用户可以安装 Docker Desktop。它可以启用 Kubernetes,但您的计算机上至少需要 16 GB 的内存。可以从 www.docker.com/products/docker-desktop
安装。
接下来,你需要做一些修改。打开 Docker 桌面偏好设置,进入资源,修改设置。您还需要启用 Kubernetes(参见图 8-6 和 8-7 )。
-
CPUs 个
-
RAM: 8 GB
-
交换空间:1.5 GB
-
Disk image size: 30 GB
图 8-7。
桌面坞站:首选项库
图 8-6。
Docker 桌面:首选项➤资源
如果您的计算机上有资源,请添加更多的 RAM 和 CPU。
更改这些设置后,您需要重新启动 Docker Desktop。然后,你就一切就绪了。
迷你库比
如果你用的是 Linux,可以安装 Minikube ( https://kubernetes.io/docs/tasks/tools/install-minikube/
)。遵循安装说明,或使用以下内容。
-
面向 Linux 的家酿(
https://docs.brew.sh/Homebrew-on-Linux
)。安装后,在您的终端中运行brew install minikube
。 -
SnapCraft (
https://snapcraft.io/
)。在您的终端中执行sudo snap install minikube
。
如果你在 Windows 上,使用 Chocolatey ( https://chocolatey.org
)。安装完成后,在命令行或 PowerShell 中执行choco install minikube
。
如果你在 Mac OS 上,使用 Brew ( https://brew.sh/
)。安装后,在您的终端中执行brew install minikube
。
Minikube 可以在任何操作系统中使用,所以如果你喜欢在下面的章节中使用 Minikube,它应该没有任何问题;启动的时候记得给足内存和 CPU。
$ minikube start --cpus 4 --memory 8192
阅读关于可以传递哪些其他参数的文档;例如,要使用的驱动程序。
Note
如果你正在使用 Minikube,查看kubectl describe nodes
命令。当您执行minikube start --cpus
命令时,您应该有您指定的 CPUs 如果没有,可以先执行minikube stop && minikube delete
,然后再执行minikube start --cpus 4 --memory 8192
。如果你有更多的资源,比如 32 GB 的内存,你可以使用--cpus 6
和--memory 12288
。
在 Kubernetes 中安装 Spring CloudStream
既然您已经启动并运行了 Kubernetes 集群,那么是时候安装 Spring CloudStream 了。最好的方法之一是使用kubectl
命令。你也可以使用 Helm,但是你需要知道如何修改你的图表来实现它;如果你对 Helm 感兴趣,我推荐你去参观 https://dataflow.spring.io/docs/installation/kubernetes/helm/
。
最简单的方法是从 Apress 网站下载源代码。即使我包括了 YAML 的文件,你的电脑上应该已经有了。你可以在ch08/kubernetes
文件夹结构中找到一切(见图 8-8 )。
图 8-8。
来源代码:ch 08/kubrines
使用 kubectl
让我们按照分步说明来安装每个组件。我假设您的集群正在运行,并且您已经下载了配套书籍的源代码。
-
It is important to know the versions that you are going to work on. When this book was written, the following versions were available.
-
SpringCloud 数据流:
springcloud/spring-cloud-dataflow-server:2.6.0
-
SpringCloud 队长:
springcloud/spring-cloud-skipper-server:2.5.0
-
Grafana 形象:??]
使用 Kubernetes 集群的一个好处是,您可以使用它的监控工具,但是有时您需要更多的信息,所以让我们在这一节中插入 Grafana 和 Prometheus。如果您运行的是 Minikube 或 Docker Desktop Kubernetes,则至少还需要 2 到 4 GB 的额外内存。
-
-
选择经纪人。记住 Spring Cloud Stream 提供 RabbitMQ 和 Kafka 作为代理,如果你想使用多个绑定器,你可以配置你的流。(接下来的章节将带回 NATs 代理。)选择一个绑定器,Rabbit 或 Kafka,并执行以下命令。
-
拉比特
kubectl create -f rabbitmq/
在
ch08/kubernetes/
文件夹中,有两个文件:rabbitmq-deployment.yaml
和rabbitmq-svc.yaml
(见清单 8-3 和 8-4 )。
-
apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq
labels:
app: rabbitmq
spec:
replicas: 1
selector:
matchLabels:
app: rabbitmq
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- image: rabbitmq:3.8.3-alpine
name: rabbitmq
ports:
- containerPort: 5672
Listing 8-3.rabbitmq-deployment.yaml
注意,您使用的是rabbitmq:3.8.3-alpine
映像,端口是 5672。该应用被命名为 rabbitmq,这是 Kubernetes 中的一个简单机制,用于在必要时查找、编辑和删除 pod、服务或部署。
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
labels:
app: rabbitmq
spec:
ports:
- port: 5672
selector:
app: rabbitmq
Listing 8-4.rabbitmq-svc.yaml
您将服务命名为 rabbitmq,因此通过名称定位 RabbitMQ 服务器更容易。记住 Kubernetes 有一个 DNS,一个定位服务的强大工具。
-
卡夫卡
kubectl create -f kafka/
有四个文件。Kafka 依赖于 Zookeeper,所以你必须添加它的部署和服务(见清单 8-5 、 8-6 、 8-7 和 8-8 )。
apiVersion: v1
kind: Service
metadata:
name: kafka
labels:
app: kafka
component: kafka-broker
spec:
ports:
- port: 9092
name: kafka-port
targetPort: 9092
protocol: TCP
selector:
app: kafka
component: kafka-broker
Listing 8-8.kafka-svc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kafka-broker
labels:
app: kafka
component: kafka-broker
spec:
replicas: 1
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
component: kafka-broker
spec:
containers:
- name: kafka
image: wurstmeister/kafka:2.12-2.3.0
ports:
- containerPort: 9092
env:
- name: ENABLE_AUTO_EXTEND
value: "true"
- name: KAFKA_RESERVED_BROKER_MAX_ID
value: "999999999"
- name: KAFKA_AUTO_CREATE_TOPICS_ENABLE
value: "false"
- name: KAFKA_PORT
value: "9092"
- name: KAFKA_ADVERTISED_PORT
value: "9092"
- name: KAFKA_ADVERTISED_HOST_NAME
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: KAFKA_ZOOKEEPER_CONNECT
value: kafka-zk:2181
Listing 8-7.kafka-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: kafka-zk
labels:
app: kafka
component: kafka-zk
spec:
ports:
- name: client
port: 2181
protocol: TCP
- name: follower
port: 2888
protocol: TCP
- name: leader
port: 3888
protocol: TCP
selector:
app: kafka-zk
component: kafka-zk
Listing 8-6.kafla-zk-svc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kafka-zk
labels:
app: kafka
component: kafka-zk
spec:
replicas: 1
selector:
matchLabels:
app: kafka-zk
template:
metadata:
labels:
app: kafka-zk
component: kafka-zk
spec:
containers:
- name: kafka-zk
image: digitalwonderland/zookeeper
ports:
- containerPort: 2181
env:
- name: ZOOKEEPER_ID
value: "1"
- name: ZOOKEEPER_SERVER_1
value: kafka-zk
Listing 8-5.kafka-zk-deployment.yaml
记住要么选兔子,要么选卡夫卡(暂时)。下一章混合了 RabbitMQ、Kafka 和 NATs 经纪人。
-
安装 MySQL(您可以安装任何适合您的环境或业务逻辑的工具;记得更改 YAML 文件中的属性以反映新的数据库引擎)。
kubectl create -f mysql/
在这个文件夹中,有四个文件。要使用 MySQL 服务器,您必须定义存储。在这种情况下,使用默认的存储类(您可以将其修改为适合您需求的类型,如 SSD 或可以根据需要增长的弹性存储)。需要添加用户名和密码,因此必须创建一个 Kubernetes 秘密对象(参见清单 8-9 、 8-10 、 8-11 和 8-12 )。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql
labels:
app: mysql
annotations:
volume.alpha.kubernetes.io/storage-class: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
Listing 8-9.mysql-pvc.yaml
目前,您使用的是storage-class:default
和 8 GB 的存储空间。如果你愿意,你可以修改它。
apiVersion: v1
kind: Secret
metadata:
name: mysql
labels:
app: mysql
data:
mysql-root-password: eW91cnBhc3N3b3Jk
Listing 8-10.mysql-secrets.yaml
当然,您可以将密码更改为适合您的解决方案的密码。
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
labels:
app: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.7.25
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: mysql-root-password
name: mysql
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
args:
- "--ignore-db-dir=lost+found"
volumes:
- name: data
persistentVolumeClaim:
claimName: mysql
Listing 8-11.mysql-deployment.yml
前面文件的一个主要属性是指向mysql-root-password
秘密对象的secretKeyRef
。
- 如果需要监控,安装 Grafana 和 Prometheus。Spring CloudStream 使用微米应用监控 (
http://micrometer.io/
)来发送关于您的流的统计数据,例如内存和 CPU 消耗以及线程数量。-
Prometheus
kubectl create -f prometheus/prometheus-clusterroles.yaml kubectl create -f prometheus/prometheus-clusterrolebinding.yaml kubectl create -f prometheus/prometheus-serviceaccount.yaml kubectl create -f prometheus-proxy/ kubectl create -f prometheus/prometheus-configmap.yaml kubectl create -f prometheus/prometheus-deployment.yaml kubectl create -f prometheus/prometheus-service.yaml
如您所见,需要添加集群角色、绑定和服务帐户。您必须添加一个代理来向 Grafana 发送信息,然后是
ConfigMap
、部署和服务(参见清单 8-13 、 8-14 、 8-15 、 8-16 、 8-17 、 8-18 、 8-19 、 8-20 、
-
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
Listing 8-12.mysql-svc.yaml
kind: ServiceAccount
apiVersion: v1
metadata:
name: prometheus
labels:
app: prometheus
namespace: default
Listing 8-15.prometheus-serviceaccount.yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: prometheus
labels:
app: prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: default
Listing 8-14.prometheus-clusterrolebinding.yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: prometheus
labels:
app: prometheus
rules:
- apiGroups: [""]
resources:
- nodes
- nodes/proxy
- services
- endpoints
- pods
verbs: ["get", "list", "watch"]
- apiGroups:
- extensions
resources:
- ingresses
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
Listing 8-13.prometheus-clusterroles.yaml
在这里,您将服务帐户分配给默认的名称空间,但是您可以对其进行更改以适应您的环境。
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus-proxy
labels:
app: prometheus-proxy
spec:
# replicas: 3
selector:
matchLabels:
app: prometheus-proxy
template:
metadata:
labels:
app: prometheus-proxy
spec:
serviceAccountName: prometheus-proxy
containers:
- name: prometheus-proxy
image: micrometermetrics/prometheus-rsocket-proxy:latest
imagePullPolicy: Always
ports:
- name: scrape
containerPort: 8080
- name: rsocket
containerPort: 7001
resources:
limits:
cpu: 1.0
memory: 2048Mi
requests:
cpu: 0.5
memory: 1024Mi
securityContext:
fsGroup: 2000
runAsNonRoot: true
runAsUser: 1000
Listing 8-18.prometheus-proxy-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus-proxy
labels:
app: prometheus-proxy
namespace: default
Listing 8-17.prometheus-proxy-serviceaccount.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: prometheus-proxy
labels:
app: prometheus-proxy
subjects:
- kind: ServiceAccount
name: prometheus-proxy
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
Listing 8-16.prometheus-proxy-clusterrolebinding.yaml
您正在使用普罗米修斯代理的最新图像:micrometermetrics/prometheus-rsocket-proxy
。
apiVersion: v1
kind: Service
metadata:
name: prometheus-proxy
labels:
app: prometheus-proxy
spec:
selector:
app: prometheus-proxy
ports:
- name: scrape
port: 8080
targetPort: 8080
- name: rsocket
port: 7001
targetPort: 7001
type: LoadBalancer
Listing 8-19.prometheus-proxy-service.yaml
即使这里用了kubectl -f prometheus-proxy/
,也不需要在意顺序。Prometheus 代理与我们的流式应用通信并从中获取所有指标。
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus
labels:
app: prometheus
data:
prometheus.yml: |-
global:
scrape_interval: 10s
scrape_timeout: 9s
evaluation_interval: 10s
scrape_configs:
- job_name: 'proxied-applications'
metrics_path: '/metrics/connected'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- default
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: prometheus-proxy
- source_labels: [__meta_kubernetes_pod_container_port_number]
action: keep
regex: 8080
- job_name: 'proxies'
metrics_path: '/metrics/proxy'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- default
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: prometheus-proxy
- source_labels: [__meta_kubernetes_pod_container_port_number]
action: keep
regex: 8080
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name
Listing 8-20.prometheus-configmap.yaml
告知 Prometheus 从哪里获取度量信息。
apiVersion: v1
kind: Service
metadata:
name: prometheus
labels:
app: prometheus
annotations:
prometheus.io/scrape: 'true'
prometheus.io/path: /
prometheus.io/port: '9090'
spec:
selector:
app: prometheus
ports:
- port: 9090
targetPort: 9090
Listing 8-22.prometheus-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: prometheus
name: prometheus
spec:
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
serviceAccountName: prometheus
containers:
- name: prometheus
image: prom/prometheus:v2.12.0
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus/"
- "--web.enable-lifecycle"
ports:
- name: prometheus
containerPort: 9090
volumeMounts:
- name: prometheus-config-volume
mountPath: /etc/prometheus/
- name: prometheus-storage-volume
mountPath: /prometheus/
volumes:
- name: prometheus-config-volume
configMap:
name: prometheus
- name: prometheus-storage-volume
emptyDir: {}
Listing 8-21.prometheus-deployment.yaml
给出这个顺序而不是kubectl -f prometheus/
是很重要的,因为它可以在没有分配集群角色的情况下开始部署。
- 添加数据流服务器角色、绑定和服务帐户。
-
Data Flow roles, bindings, and service account
kubectl create -f server/server-roles.yaml kubectl create -f server/server-rolebinding.yaml kubectl create -f server/service-account.yaml
参见清单 8-27 、 8-28 和 8-29 。
-
apiVersion: v1
kind: Service
metadata:
name: grafana
labels:
app: grafana
spec:
selector:
app: grafana
type: LoadBalancer
ports:
- port: 3000
targetPort: 3000
Listing 8-26.grafana-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: grafana
name: grafana
spec:
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- image: springcloud/spring-cloud-dataflow-grafana-prometheus:2.5.2.RELEASE
name: grafana
env:
- name: GF_SECURITY_ADMIN_USER
valueFrom:
secretKeyRef:
name: grafana
key: admin-username
- name: GF_SECURITY_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: grafana
key: admin-password
ports:
- containerPort: 3000
resources:
limits:
cpu: 500m
memory: 2500Mi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: config
mountPath: "/etc/grafana/provisioning/datasources/datasources.yaml"
subPath: datasources.yaml
volumes:
- name: config
configMap:
name: grafana
Listing 8-25.grafana-deployment.yaml
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: grafana
labels:
app: grafana
data:
admin-username: YWRtaW4=
admin-password: cGFzc3dvcmQ=
Listing 8-24.grafana-secret.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana
labels:
app: grafana
data:
datasources.yaml: |
apiVersion: 1
datasources:
- name: ScdfPrometheus
type: prometheus
access: proxy
org_id: 1
url: http://prometheus:9090
is_default: true
version: 5
editable: true
read_only: false
Listing 8-23.grafana-configmap.yaml
-
格拉凡娜
kubectl create -f grafana/
使用 Grafana,您必须添加知道 Prometheus 的配置以及连接它的用户名和密码(参见清单 8-23 、 8-24 、 8-25 和 8-26 )。
-
Add the cloud Skipper server. It is a key component because it deploys, keeps track of the Stream version, and much more.
-
船长
- 如果您使用 Rabbit,请执行以下命令。
-
如果你使用 Kafka,执行下面的代码。
kubectl create -f skipper/skipper-config-rabbit.yaml
kubectl create -f skipper/skipper-config-kafka.yaml
记住你只需要选择一个(现在)(见清单 8-30 和 8-31 )。
-
apiVersion: v1
kind: ServiceAccount
metadata:
name: scdf-sa
Listing 8-29.service-account.yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: scdf-rb
subjects:
- kind: ServiceAccount
name: scdf-sa
roleRef:
kind: Role
name: scdf-role
apiGroup: rbac.authorization.k8s.io
Listing 8-28.server-rolebinding.yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: scdf-role
rules:
- apiGroups: [""]
resources: ["services", "pods", "replicationcontrollers", "persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "delete", "update"]
- apiGroups: [""]
resources: ["configmaps", "secrets", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["statefulsets", "deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "delete", "update", "patch"]
- apiGroups: ["extensions"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "delete", "update", "patch"]
- apiGroups: ["batch"]
resources: ["cronjobs", "jobs"]
verbs: ["create", "delete", "get", "list", "watch", "update", "patch"]
Listing 8-27.server-roles.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: skipper
labels:
app: skipper
data:
application.yaml: |-
spring:
cloud:
skipper:
server:
platform:
kubernetes:
accounts:
default:
environmentVariables: 'SPRING_RABBITMQ_HOST=${RABBITMQ_SERVICE_HOST},SPRING_RABBITMQ_PORT=${RABBITMQ_SERVICE_PORT}'
limits:
memory: 1024Mi
cpu: 500m
readinessProbeDelay: 120
livenessProbeDelay: 90
datasource:
url: jdbc:mysql://${MYSQL_SERVICE_HOST}:${MYSQL_SERVICE_PORT}/skipper
username: root
password: ${mysql-root-password}
driverClassName: org.mariadb.jdbc.Driver
testOnBorrow: true
validationQuery: "SELECT 1"
Listing 8-30.skipper-config-rabbit.yaml
请注意,您正在 ConfigMap 中添加数据源和 RabbitMQ 服务器和端口;所有这些都是环境变量。Kubernetes 为每个服务创建一个环境变量,并将它们添加到每个容器中,因此很容易知道其他服务在哪里;它通常设置 IP 和端口。
apiVersion: v1
kind: ConfigMap
metadata:
name: skipper
labels:
app: skipper
data:
application.yaml: |-
spring:
cloud:
skipper:
server:
platform:
kubernetes:
accounts:
default:
environmentVariables: 'SPRING_CLOUD_STREAM_KAFKA_BINDER_BROKERS=${KAFKA_SERVICE_HOST}:${KAFKA_SERVICE_PORT},SPRING_CLOUD_STREAM_KAFKA_BINDER_ZK_NODES=${KAFKA_ZK_SERVICE_HOST}:${KAFKA_ZK_SERVICE_PORT}'
limits:
memory: 1024Mi
cpu: 500m
readinessProbeDelay: 120
livenessProbeDelay: 90
datasource:
url: jdbc:mysql://${MYSQL_SERVICE_HOST}:${MYSQL_SERVICE_PORT}/skipper
username: root
password: ${mysql-root-password}
driverClassName: org.mariadb.jdbc.Driver
testOnBorrow: true
validationQuery: "SELECT 1"
Listing 8-31.skipper-config-kafka.yaml
选择代理后,添加部署和服务。
gkubectl create -f skipper/skipper-deployment.yaml
kubectl create -f skipper/skipper-svc.yaml
参见清单 8-32 和 8-33 。
apiVersion: v1
kind: Service
metadata:
name: skipper
labels:
app: skipper
spring-deployment-id: scdf
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 7577
selector:
app: skipper
Listing 8-33.skipper-svc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: skipper
labels:
app: skipper
spec:
selector:
matchLabels:
app: skipper
replicas: 1
template:
metadata:
labels:
app: skipper
spec:
containers:
- name: skipper
image: springcloud/spring-cloud-skipper-server:2.6.0
imagePullPolicy: Always
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /actuator/health
port: 7577
initialDelaySeconds: 45
readinessProbe:
httpGet:
path: /actuator/info
port: 7577
initialDelaySeconds: 45
resources:
limits:
cpu: 1.0
memory: 1024Mi
requests:
cpu: 0.5
memory: 640Mi
env:
- name: SPRING_CLOUD_KUBERNETES_CONFIG_NAME
value: skipper
- name: SPRING_CLOUD_KUBERNETES_SECRETS_ENABLE_API
value: 'true'
- name: SPRING_CLOUD_KUBERNETES_SECRETS_NAME
value: mysql
initContainers:
- name: init-mysql-wait
image: busybox
command: ['sh', '-c', 'until nc -w3 -z mysql 3306; do echo waiting for mysql; sleep 3; done;']
- name: init-mysql-database
image: mysql:5.6
env:
- name: MYSQL_PWD
valueFrom:
secretKeyRef:
name: mysql
key: mysql-root-password
command: ['sh', '-c', 'mysql -h mysql -u root -e "CREATE DATABASE IF NOT EXISTS skipper;"']
serviceAccountName: scdf-sa
Listing 8-32.skipper-deployment.yaml
当你的类型设置为LoadBalancer
时,Skipper 可以从外部访问,但如果你不需要它,你可以使用NodePort
来代替。使用 Minikube 或 Docker Desktop Kubernetes 时,NodePort
很有用。
-
Finally, add the Data Flow server.
kubectl create -f server/server-config.yaml kubectl create -f server/server-deployment.yaml kubectl create -f server/server-svc.yaml
注意,您需要一个配置声明(参见清单 8-34 、 8-35 和 8-36 )。
apiVersion: v1
kind: ConfigMap
metadata:
name: scdf-server
labels:
app: scdf-server
data:
application.yaml: |-
spring:
cloud:
dataflow:
applicationProperties:
stream:
management:
metrics:
export:
prometheus:
enabled: true
rsocket:
enabled: true
host: prometheus-proxy
port: 7001
task:
management:
metrics:
export:
prometheus:
enabled: true
rsocket:
enabled: true
host: prometheus-proxy
port: 7001
grafana-info:
url: 'https://grafana:3000'
task:
platform:
kubernetes:
accounts:
default:
limits:
memory: 1024Mi
datasource:
url: jdbc:mysql://${MYSQL_SERVICE_HOST}:${MYSQL_SERVICE_PORT}/mysql
username: root
password: ${mysql-root-password}
driverClassName: org.mariadb.jdbc.Driver
testOnBorrow: true
validationQuery: "SELECT 1"
Listing 8-34.server-config.yaml
注意 Prometheus 和 Grafana 部分在ConfigMap
中,但是如果您部署时没有这些监控工具,您应该删除流(整个块)、任务(整个块和prometheus-proxy
)和grafana-info
(整个块)。
kind: Service
apiVersion: v1
metadata:
name: scdf-server
labels:
app: scdf-server
spring-deployment-id: scdf
spec:
# If you are running k8s on a local dev box or using minikube, you can use type NodePort instead
type: LoadBalancer
ports:
- port: 80
targetPort: 80
name: scdf-server
selector:
app: scdf-server
Listing 8-36.server-svc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: scdf-server
labels:
app: scdf-server
spec:
selector:
matchLabels:
app: scdf-server
replicas: 1
template:
metadata:
labels:
app: scdf-server
spec:
containers:
- name: scdf-server
image: springcloud/spring-cloud-dataflow-server:2.4.2.RELEASE
imagePullPolicy: Always
volumeMounts:
- name: database
mountPath: /etc/secrets/database
readOnly: true
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /management/health
port: 80
initialDelaySeconds: 45
readinessProbe:
httpGet:
path: /management/info
port: 80
initialDelaySeconds: 45
resources:
limits:
cpu: 1.0
memory: 2048Mi
requests:
cpu: 0.5
memory: 1024Mi
env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: "metadata.namespace"
- name: SERVER_PORT
value: '80'
- name: SPRING_CLOUD_CONFIG_ENABLED
value: 'false'
- name: SPRING_CLOUD_DATAFLOW_FEATURES_ANALYTICS_ENABLED
value: 'true'
- name: SPRING_CLOUD_DATAFLOW_FEATURES_SCHEDULES_ENABLED
value: 'true'
- name: SPRING_CLOUD_KUBERNETES_SECRETS_ENABLE_API
value: 'true'
- name: SPRING_CLOUD_KUBERNETES_SECRETS_PATHS
value: /etc/secrets
- name: SPRING_CLOUD_KUBERNETES_CONFIG_NAME
value: scdf-server
- name: SPRING_CLOUD_DATAFLOW_SERVER_URI
value: 'http://${SCDF_SERVER_SERVICE_HOST}:${SCDF_SERVER_SERVICE_PORT}'
# Provide the Skipper service location
- name: SPRING_CLOUD_SKIPPER_CLIENT_SERVER_URI
value: 'http://${SKIPPER_SERVICE_HOST}:${SKIPPER_SERVICE_PORT}/api'
# Add Maven repo for metadata artifact resolution for all stream apps
- name: SPRING_APPLICATION_JSON
value: "{ \"maven\": { \"local-repository\": null, \"remote-repositories\": { \"repo1\": { \"url\": \"https://repo.spring.io/libs-snapshot\"} } } }"
initContainers:
- name: init-mysql-wait
image: busybox
command: ['sh', '-c', 'until nc -w3 -z mysql 3306; do echo waiting for mysql; sleep 3; done;']
serviceAccountName: scdf-sa
volumes:
- name: database
secret:
secretName: mysql
Listing 8-35.server-deployment.yaml
您可以将NodePort
(如果您使用 Minikube 或 Docker Desktop Kubernetes)用于本地环境,将LoadBalancer
用于生产环境。
这些是创建 Spring CloudStream 及其组件的步骤。一旦完成部署,就可以使用下面的命令进行访问。
kubectl get svc scdf-server
寻找外部 IP。如果您正在使用NodePort
,查看端口以转到您的本地 IP。如果您正在使用 Minikube,请执行以下命令。
minikube service scdf-server --url
如果你用的是 Docker Desktop Kubernetes,访问kubernetes.docker.internal
,指向 127.0.0.1。
从scdf-server
获取 IP 后,打开浏览器,使用 IP 和端口(如果你使用了NodePort
)以及/dashboard
路径。您应该会看到如图 8-2 所示的相同仪表板。
让我们开始测试您的 Spring CloudStream 服务器。
Note
我在书的源代码中添加了install.sh
和uninstall.sh
脚本。你可以在ch08/kubernetes
文件夹中找到它们。这些脚本仅适用于 Unix/Linux,您需要安装一个 jq 工具( https://stedolan.github.io/jq/
)。
用一个简单的流测试您的安装
在本节中,您将创建一个小的流。这是你能做什么的一瞥。这是非常简单的,并确保您的安装和活页夹的工作预期。
我在第一部分中使用了 Docker Compose(本地),在基础设施中使用了 Kubernetes 集群(生产),但是在这个例子中,您可以使用任何方法。
使用 Docker 合成
在本节中,让我们使用 Docker Compose 通过 RabbitMQ 和 MySQL 来设置 Spring CloudStream 基础设施。如果你想下载代码,YAML 文件在ch08/docker-compose
文件夹中。执行以下命令。
export DATAFLOW_VERSION=2.6.0
export SKIPPER_VERSION=2.5.0
docker-compose -f docker-compose-rabbitmq.yml up -d
请注意,您正在将它发送到后台。打开浏览器并转到http://localhost:9393/dashboard
。如果您使用 YAML 文件,您应该已经注册了应用。如果您没有这些应用,请按照前面的部分进行注册。记住,它们应该基于 RabbitMQ,以及 Maven 或 Docker。
接下来,单击左侧边栏中的 Streams 窗格(参见图 8-9 )。
图 8-9。
流:http://localhost:9393/dashboard/#/streams/definitions
接下来,单击 +创建流。这将打开您添加流定义的页面。在这里,您可以使用源、处理器和接收器应用来拖放和连接它们。在这种情况下,转到文本区域(显示“输入流定义”)并输入以下内容。
time | log
app 积木形式如图 8-10 所示。
图 8-10。
流定义:http://localhost:9393/dashboard/#/streams/create
接下来,单击 Create Stream 按钮,这将打开一个弹出对话框,要求输入名称和描述。在名称字段中输入简单,在描述字段中输入只是一个测试(见图 8-11 )。
图 8-11。
创建流
点击创建流按钮创建流,然后返回流部分。在那里你会看到一个流定义列表,你会看到你的“简单”流(见图 8-12 )。
图 8-12。
创建的流
接下来,单击流行末尾的播放按钮。这会将您带到“部署流定义”页面,在这里您可以向每个应用添加属性,并添加应用所需的 CPU、内存、磁盘空间和实例数量。在这种情况下,你会看到三列,一列用于全局,在这里你可以为两个应用添加通用设置(记住,time
和log
是两个不同的微服务应用,都是应用启动器的一部分),还有一列用于每个微服务——一列用于时间,一列用于日志(见图 8-13 )。
图 8-13。
部署流简单:http://localhost:9393/dashboard/#/streams/definitions/simple/deploy
在这种情况下,您没有设置任何新的属性,所以单击 Deploy stream 按钮。这将发送部署这两个应用的指令,并返回到 Streams 页面。看看你简单的流。您会看到部署状态。如果您点击列表顶部的刷新按钮,您会看到您的应用已经部署完毕(参见图 8-14 和 8-15 )。
图 8-15。
状态:已部署
图 8-14。
状态:正在部署
那么,你怎么知道这个流是否在工作呢?基于 DSL,“time | log
”意味着您正在使用一个time
源(带有一个 RabbitMQ 绑定器,time-source-rabbit
)和一个 log-sink(带有一个 RabbitMQ 绑定器,log-sink-rabbit
)。time
源每秒发送一次,日志接收器将它记录到控制台中。如果您单击状态旁边的“I”(显示详细信息)按钮,将显示流的详细信息和两个应用的当前日志(参见图 8-16 )。
图 8-16。
详细信息:http://localhost:9393/dashboard/#/streams/definitions/simple/summary
如果您滚动,您会看到日志部分。选择simple.log-v1
。您会看到日志接收器当前接收的日志和时间。您应该会看到如下所示的内容。
...
INFO [e.time.simple-1] log-sink : 05/01/20 02:20:59
INFO [e.time.simple-1] log-sink : 05/01/20 02:21:00
INFO [e.time.simple-1] log-sink : 05/01/20 02:21:01
INFO [e.time.simple-1] log-sink : 05/01/20 02:21:02
INFO [e.time.simple-1] log-sink : 05/01/20 02:21:03
INFO [e.time.simple-1] log-sink : 05/01/20 02:21:04
INFO [e.time.simple-1] log-sink : 05/01/20 02:21:05
...
请注意,应用是使用这种模式命名的:<stream-name>.<app>-<version>
。在这种情况下,你有“??”和“??”。史奇普干的。它会跟踪版本。如果您取消部署并重新部署(停止并重新启动)流,您应该看到版本增加到v2
。
你想知道你是否能看到实时日志?如果您点击运行时部分(在左窗格中),您将进入运行时应用页面,该页面列出了您的流和所有涉及的应用(参见图 8-17 )。
图 8-17。
运行时:http://localhost:9393/dashboard/#/runtime/apps
如果你点击simple.log-v1/v2
,你会看到更多的信息(见图 8-18 )。
图 8-18。
运行时详细信息
图 8-18 显示了 app 的simple.log-v1/v2
细节。注意stdout
字段中的路径。在这种情况下,它就是/tmp/1588299625225/simple.log-v2/stdout_0.log
。
打开终端并执行以下命令。
docker exec skipper tail -f /tmp/1588299625225/simple.log-v2/stdout_0.log
... log-sink : 05/01/20 02:33:21
... log-sink : 05/01/20 02:33:22
... log-sink : 05/01/20 02:33:23
... log-sink : 05/01/20 02:33:24
... log-sink : 05/01/20 02:33:25
... log-sink : 05/01/20 02:33:26
... log-sink : 05/01/20 02:33:27
... log-sink : 05/01/20 02:33:28
... log-sink : 05/01/20 02:33:29
... log-sink : 05/01/20 02:33:30
... log-sink : 05/01/20 02:33:31
... log-sink : 05/01/20 02:33:32
你看到每一秒钟的时间都被打印出来。返回到流窗格并销毁流。
恭喜你!您刚刚使用 Docker Compose 创建了您的第一个流time | log
。现在,您可以使用以下命令关闭您的基础设施。
docker-compose -f docker-compose-rabbitmq.yml down
接下来,让我们在 Kubernetes 中部署 Spring CloudStream。
使用库比涅斯
在本节中,您将使用 Kubernetes 部署相同的"time | log
"流。不使用仪表板,而是使用 Spring CloudStream 外壳。如果你有源代码,可以添加install.sh
和uninstall.sh
脚本来设置 Kubernetes 中的一切。我使用 Minikube 安装了 Spring CloudStream,但是您可以使用任何其他 Kubernetes 集群实例。
我用了ch08/kubernetes/workspace-rabbit-mysql-monitoring
。它安装了 RabbitMQ 作为绑定器,MySQL 作为持久性,Grafana 和 Prometheus 跟踪我的应用的任何指标。
转到该文件夹并执行./install.sh
脚本。该脚本执行与您在前面几节中遵循的相同的逐步说明。因为我使用了 Minikube,所以我执行了下面的代码。
minikube service scdf-server --url
http://192.168.64.6:30724
如果我打开浏览器,我需要指向http://192.168.64.6:30724/dashboard
。我发现没有应用,我需要像以前一样做同样的程序。点击 +添加应用按钮。在这种情况下,我使用 Spring CloudStream shell。
使用 Spring CloudStream 外壳
我还没有向您展示这个特殊的工具。在这一节中,您可以先睹为快 Spring CloudStream 外壳。您正在注册应用并创建同一个简单的“time | log
”流。
在独立部分中,download.sh
包含 shell,但是如果您不记得它了,创建一个workspace-shell
文件夹,并在该文件夹中执行下面的命令。
wget https://repo.spring.io/release/org/springframework/cloud/spring-cloud-dataflow-shell/2.4.2.RELEASE/spring-cloud-dataflow-shell-2.4.2.RELEASE.jar
然后,执行以下命令。
java -jar spring-cloud-dataflow-shell-2.4.2.RELEASE.jar
...
...
server-unknown:>
一个提示说,“server-unknown
”。最好的命令之一是help
。如果你输入help
并按回车键,你会看到所有可用的命令。如果您输入help dataflow config server
,您会看到所有支持连接到数据流服务器的参数。
接下来,在 shell 中执行以下内容。
server-unknown:>dataflow config server --uri http://192.168.64.6:30724
Successfully targeted http://192.168.64.6:30724
dataflow:>
现在您看到了dataflow
提示符。如果您键入app list
,您会看到以下内容。
dataflow:>app list
No registered apps.
You can register new apps with the 'app register' and 'app import' commands.
dataflow:>
这意味着你需要注册你的应用。之前,我列出了包含所有应用启动器描述的 URL。为此,你用 https://dataflow.spring.io/rabbitmq-docker-latest
。
接下来,键入app import
并通过 URL 传递uri
参数。
dataflow:>app import --uri https://dataflow.spring.io/rabbitmq-docker-latest
Successfully registered 66 applications from ...
dataflow:>
如果执行应用列表,您会看到列出了源、处理器和接收器应用。
Note
如果您需要或忘记任何参数,数据流外壳支持制表符结束。
接下来,让我们通过执行以下命令来创建流。
dataflow:>stream create --name simple --definition "time | log"
Created new stream 'simple'
dataflow:>
前面的命令创建了以“time | log
”为定义的simple
流。如果您执行stream list,
,您会看到创建的流列表及其状态。
dataflow:>stream list
...
simple | | time | log |The app or group is known to the system, but is not currently deployed.
...
dataflow>
接下来,我们用下面的来部署一下。
dataflow:>stream deploy --name simple
Deployment request has been sent for stream 'simple'
dataflow:>
您可以查看以下状态
dataflow:>stream info --name simple
...
或者用
dataflow:>stream list
它应该说“已部署”。
如何查看日志?在 Kubernetes 中做这件事很酷的一点是,它创建了带有时间和日志应用的 pods,所以你可以打开一个新的终端并执行以下操作。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
grafana-5b89747547-h5x7l 1/1 Running 0 29m
mysql-5c59b756db-rggjw 1/1 Running 0 29m
prometheus-67896dcc8-s4qls 1/1 Running 0 29m
prometheus-proxy-86f5fd556b-6m9jn 1/1 Running 0 29m
rabbitmq-5bf579759d-c5rtj 1/1 Running 0 29m
scdf-server-5888868f84-qmwt2 1/1 Running 0 28m
simple-log-v1-5f88b7d546-g7cmb 1/1 Running 0 2m8s
simple-time-v1-5d7d4bc86f-gwq7w 1/1 Running 0 2m8s
skipper-66c685ff74-vc7b5 1/1 Running 0 29m
注意 Skipper 创建了simple-log
和simple-time
;它的命名惯例是<stream-name>-<app-name>-<version>-<pod-id>
。现在,您可以查看以下日志。
$ kubectl logs -f pod/simple-log-v1-5f88b7d546-g7cmb
... log-sink : 05/01/20 03:20:13
... log-sink : 05/01/20 03:20:14
... log-sink : 05/01/20 03:20:15
... log-sink : 05/01/20 03:20:16
恭喜你!您使用数据流 shell 创建了您的流。在你摧毁溪流之前,让我们先看看格拉法纳。转到您的浏览器并打开http://192.168.64.6:30724/dashboard
。你会看到所有注册的应用。转到“流”面板。在顶部,您可以看到 Grafana 仪表板按钮。单击它转到 Grafana 登录页面。用户名是admin
,密码是password
。请注意,您已经配置了应用、流和任务仪表板。环顾四周,你会看到一些数据(见图 8-19 和 8-20 )。
图 8-20。
grafana Streams:http://192 . 168 . 64 . 6:30018/d/scdf-Streams/Streams?orgId=1&refresh=10s
图 8-19。
Grafana 应用:http://192 . 168 . 64 . 6:30018/d/scdf-applications/applications?orgId=1&refresh=10s#/apps
现在,您可以密切监控您的流和应用。
是时候关闭一切了。若要删除该流,请在数据流外壳中执行以下操作。
dataflow:>stream destroy --name simple
Destroyed stream 'simple'
dataflow:>
如果您在终端中再次执行kubectl get pods
,这个简单的 pod 就消失了。您可以使用exit
命令退出数据流 shell。
dataflow:>exit
恭喜你!您使用 Spring CloudStream 外壳来注册应用并创建和销毁您的流。更多的动作即将到来,所以不要忘记你还在使用数据流外壳。
清理
如果你想快速清理,撤销你所做的安装,用delete
替换create
,与它们被创建的顺序相反。
kubectl delete -f server/server-deployment.yaml
kubectl delete -f server/server-svc.yaml
kubectl delete -f server/server-config.yaml
kubectl delete -f skipper/skipper-svc.yaml
kubectl delete -f skipper/skipper-deployment.yaml
kubectl delete -f skipper/skipper-config-rabbit.yaml
kubectl delete -f server/service-account.yaml
kubectl delete -f server/server-rolebinding.yaml
kubectl delete -f server/server-roles.yaml
kubectl delete -f grafana/
kubectl delete -f prometheus/prometheus-service.yaml
kubectl delete -f prometheus/prometheus-deployment.yaml
kubectl delete -f prometheus/prometheus-configmap.yaml
kubectl delete -f prometheus-proxy/
kubectl delete -f prometheus/prometheus-serviceaccount.yaml
kubectl delete -f prometheus/prometheus-clusterrolebinding.yaml
kubectl delete -f prometheus/prometheus-clusterroles.yaml
kubectl delete -f mysql/
kubectl delete -f kafka/
kubectl delete -f rabbitmq/
当然,移除服务、部署和 pod 的方法更多,比如使用提供的标签;例如,您可以删除 Grafana。
kubectl delete all,cm,svc,secrets -l app=grafana
或者您可以以相反的顺序执行前面的所有命令。使用文件声明(YAML)将会成功并删除服务,但最终,这取决于您喜欢什么。
摘要
本章解释了 Spring CloudStream 的重要性,并向您展示了如何创建您的第一个流。我们将在下一章进行更详细的讨论,我将向您展示 Spring CloudStream 的内部结构,以及如何从中获得更多。我还向您展示了设置 Spring CloudStream 的不同方法。有几个支持的平台,但 Kubernetes 是云基础设施和容器编排的事实上的技术。
九、Spring CloudStream 内部
在第八章中,您简要地看了一下 Spring CloudStream。本章深入讨论了这项技术能做什么。我回顾了您可以用流和批处理做什么,并向您展示了 Spring Cloud Task 如何使用 Spring Batch 创建有限的工作负载。
让我们开始挖掘 Spring CloudStream。
Spring CloudStream 架构
Spring CloudStream 编排和编排流式应用和批处理作业。它依靠云平台来管理我们应用的可扩展性和高可用性,以及底层基础设施(虚拟机、物理机)的弹性和容错能力。您需要关注业务逻辑。
Spring Cloud Data Flow 有几个组件用于编排和编排流式应用和批处理作业。它识别应用类型,并基于其 DSL(特定于域的语言)部署它,类似于执行 shell 命令时的类 Unix 语法。应用类型包括源、处理器、接收器和任务。我将在本章的后面介绍 Spring Cloud 任务。
在服务器端,Spring CloudStream 有两个主要组件。
- 数据流服务器。这个服务器公开了一个 REST API,它是仪表板和外壳的主要入口点。数据流服务器可以在几个云平台上运行,比如 Kubernetes、Cloud Foundry、Mesos、Yarn 和你的本地机器(独立运行或者使用 Docker 或 Docker Compose)。数据流服务器使用关系数据库(MySQL、PostgreSQL、Oracle、DB2、MS SQL Server、H2 或 HSQLDB)跟踪流及其状态。你只需要在
spring.datasource.*
属性中声明正确的驱动程序。如果没有定义datasource
属性,默认情况下,使用 H2 嵌入式数据库将所有内容保存在内存中。数据流服务器负责以下工作。-
注册 jar(独立的、HTTP 或在 Maven 坐标中)和 Docker 图像等工件
-
基于 DSL 解析流或批处理作业定义(这是类似 Unix 的 shell 命令(如
time | filter | transform | log
)。您将在剩余的章节中了解更多关于 DSL 的知识。) -
使用关系数据库验证和保存流和任务应用以及批处理作业定义
-
将批处理作业定义部署到一个或多个云平台
-
查询任务和批处理作业执行历史
-
将流部署委托给 Skipper
-
将作业调度委托给云平台
-
当部署到云平台时,向流添加额外的信息,包括附加属性,例如应用的配置(端口、通道名称、使用 SpEL 的表达式、应用属性特定)、输入和输出、绑定器属性、要运行的实例、内存和 CPU 分配以及分区组
-
增加身份验证和授权的安全性(OAuth 2.0、SSO、LDAP 或添加您自己的安全提供者),通过 SSL 访问,并保护您的仪表板
-
尝试在端口 8888 连接到 Spring Cloud 配置服务器,但是您可以通过覆盖属性来使用自己的配置
-
配置 Maven 存储库和 Docker 注册表,以便轻松访问您的流和任务应用
-
默认情况下,使用端口 9393 提供广泛的 RESTful API 和 web 仪表板;它可以通过使用属性来重写
-
它可以在没有 Skipper 的情况下使用,但是您删除了升级或进行回滚的能力(进行蓝/绿部署的能力)、部署历史和一些其他功能。
-
Skipper 服务器。Skipper 服务器负责将流部署到一个或多个平台。它可以通过跟踪发布的流版本来进行升级和回滚。它使用状态机算法进行蓝绿色部署。您还可以为身份验证和授权添加安全性(OAuth 2.0、SSO 或添加您自己的安全提供者),并使用 SSL 进行安全通信。
Skipper 服务器使用关系数据库(可以使用 MySQL、PostgreSQL、Oracle、DB2、MS SQL Server、H2 和 HSQLDB)跟踪任何流及其状态和流的版本。你只需要在
spring.datasource.*
属性中声明正确的驱动程序。如果没有定义datasource
属性,默认情况下,使用 H2 嵌入式数据库将所有内容保存在内存中。它在端口 8888 上有一个到 Spring Cloud config 服务器的默认连接,但是您可以很容易地覆盖它并添加您自己的连接。
在客户端,Spring CloudStream 有几个组件。
- 仪表盘。仪表板是数据流服务器的一部分,它的服务器是端口 9393。它确实停止了对数据流服务器的调用。这是一个可视化流的 GUI(它使用了 Spring Flo 项目,
https://spring.io/projects/spring-flo
)。通过 Flo,您可以利用仪表盘上的以下功能。-
使用 DSL 或画布创建、管理和监控您的流管道
-
注册您的应用,单个或批量
-
停止、启动和销毁流和任务应用
-
使用 Grafana 和 Prometheus(如果已配置)监控您的流和应用
-
查看您的流的分析,并使用正确的图表来表示您的数据流和逻辑
-
停止和恢复作业(仪表板和 Flo 是 Spring Batch GUI 版本的下一个迭代,用于管理任务和批处理作业)
-
使用内容辅助和自动完成功能
-
调整网格布局,以便更有效地查看和配置管道
-
查看分布式部署的可视化表示
-
为部署者添加元数据,例如 CPU、实例和内存
-
您可以在 https://github.com/spring-cloud/spring-cloud-dataflow-ui
获得数据流仪表盘的源代码。
图 9-1。
Spring CloudStream 组件
-
外壳。这是另一个通过 REST API 调用与数据流服务器通信的组件。它使用 Spring Shell 项目。以下是它的一些特点。
-
选项卡完成、着色和脚本执行
-
与 bean 验证 API 集成,因此您可以通过对象执行您的逻辑
-
基于特定于域的标准动态启用命令
-
与仪表板功能相同,但使用类似命令行的界面
-
轻松扩展到使用编程模型来创建自定义命令
-
-
Java 客户端。您可以使用
spring-cloud-dataflow-rest-client
模块以编程方式创建您的流。它提供了StreamBuilder
、StreamDefinition
、Stream
、StreamApplication
和DataFlowTemplate
DSL 类,这些类提供了流畅的 API 以便于开发(参见图 9-1 )。
在深入每个组件之前,让我们先讨论一下应用类型。你已经知道,符合流的 app 有 SpringCloudStream 和 SpringCloud 任务 app,但各有重要区别。
-
长寿命应用有输入和输出,需要一个代理进行通信。一些应用有多个输入和输出,在其他情况下,它们不需要代理来进行通信。大多数时候,你会发现那些长寿的应用是使用 Spring Cloud Stream 模块创建的。
-
短期应用有一个有限的持续时间。它们开始、处理和完成。这些应用是运行的任务,可以将它们的状态保存在数据流数据库中。通常使用 Spring Cloud 任务。这些应用的扩展使用 Spring Batch。它保存状态,从故障停止的地方重新启动,等等。
您不需要使用 Spring 来创建长期或短期的应用。你几乎可以使用任何编程语言。在创建这些应用时,您可以将它们打包成一个 Spring Boot·优步-JAR,可以使用 Maven(托管在 Maven 存储库中)作为文件或 HTTP 进行访问,或者作为托管在 Docker 注册表中的 Docker 映像进行访问。
客户端工具:cURL 命令、数据流外壳、Java 客户端、仪表板
在这一节中,我将通过创建简单的示例来介绍连接到 Spring Data 流服务器的所有可用方法,在接下来的章节中,您可以使用任何工具来实现复杂的解决方案。让我们首先启动并运行 Spring CloudStream 的基础设施(数据流服务器、Skipper 服务器、MySQL、RabbitMQ 或 Kafka );您可以使用 Docker Compose 或 Kubernetes。同样,让我们定义我们的管道 DSL。
在使用客户端工具之前,请确保您有这套额外的工具来帮助您。
-
jq
(https://stedolan.github.io/jq/
)处理 JSON 对象。 -
yq
(https://mikefarah.gitbook.io/yq/
)处理 YAML 对象。 -
httpie
(https://httpie.org/
)。HTTP 客户端。该工具依赖于 Python 3.6 或更高版本。如果这些工具对你没有帮助,安装
cURL
命令。如果你用的是 Windows OS,可以从https://curl.haxx.se/windows/
下载。也可以用 Postman (www.postman.com/downloads/
)。
接下来,让我们通过本章来定义您使用的 DSL。将其称为电影流管道 DSL 或流管道(见图 9-2 )。
图 9-2。
电影流 DSL
图 9-2 以图表形式显示了流管道。请记住,每个区块都是独立于其他区块的 Spring Cloud Stream 应用。他们通过你选择的任何一个经纪人进行交流(RabbitMQ、Kafka、NATS 等)。).部署时,每个应用可以有一个或多个实例。由于 Spring CloudStream 服务器运行在云环境/云平台(Docker Compose—local 或 Kubernetes、Cloud Foundry、Mesos、Yarn)中,因此它具有可扩展性、高可用性、容错性以及对任何微服务架构的可见性(监控)。让我们回顾一下你对 Spring Cloud Stream 应用的了解。
http
。首先,创建一个http
应用来监听任何传入的请求。这是一个来源。您发送的样本数据在清单 9-1 中列出。
{
"movies": [
{
"id": "tt0133093",
"title": "The Matrix",
"actor": "Keanu Reeves",
"year": 1999,
"genre": "fiction",
"stars": 5
},
{
"id": "tt0209144",
"title": "Memento",
"actor": "Guy Pearce",
"year": 2000,
"genre": "drama",
"stars": 4
},
{
"id": "tt0482571",
"title": "The Prestige",
"actor": "Christian Bale",
"year": 2006,
"genre": "drama",
"stars": 3
},
{
"id": "tt0486822",
"title": "Disturbia",
"actor": "Shia LaBeouf",
"year": 2007,
"genre": "drama",
"stars": 3
}
]
}
Listing 9-1.Sample data
分析数据(你发送的是一个id
,和一组电影,而不是一个)。
-
splitter
。splitter
是一个消息传递模式。根据给定的表达式,可以将一条消息分割成几部分,在这种情况下,您可以一次分割一部电影。在这种情况下,splitter
应用产生四条消息。splitter
是一个处理器。 -
groovy-transform
。您在前面的章节中使用了这一点。这一次,groovy-transform
应用通过外部系统获得“真实评级”。您使用 RapidAPI (https://rapidapi.com
)网站,该网站提供了几个 API,包括 IMDB(互联网电影数据库,www.imdb.com
),并发送 ID 以获得电影的全球评级。一旦它返回 IMDB 评级,它就会通过添加rating
和ratingCount
属性来增强我们的电影信息。groovy-transform
app 是另一个处理器。 -
filter
。有两个filter
应用:一个来自splitter
应用,这意味着它接收与groovy-transform
应用相同的消息,它使用star
属性只允许超过三颗星的电影通过。另一个filter
应用只允许高评分的电影通过(> 8.0)。filter
应用是处理器应用。 -
jdbc
。一个jdbc
应用将电影信息保存在数据库中。该消息包括原始消息和由groovy-transform
应用收集的新属性。这是一件非常简单的事情。这个jdbc
应用是一个接收器。 -
log
。log
应用记录了从filter
应用中释放的内容。请注意,您可以使用不同的接收器(兔子,HDFS,蒙哥,FTP,S3 等。).一个log
app 就是一个水槽。
如何为这个例子创建流管道?Spring CloudStream 提供了一种简单的方法来公开您的应用,并使用类似 Unix 的 DSL 语法将它们连接起来。Spring Cloud Stream 应用(你可以将它们视为 Unix 命令)可以通过使用|
符号(Unix |
管道)与其他应用进行通信,并使用>
符号进行重定向。我们的例子应该很容易在流管道 DSL 中实现(参见清单 9-2 )。
movie = http | splitter | groovy-transform | jdbc
stars = :movie.splitter > filter | log
imdb-high-rating = :movie.groovy-transform > filter | log
Listing 9-2.Movie Stream Pipeline DSL Simple Form
清单 9-2 显示了电影流管道。我们来分析一下。
-
http | splitter | groovy-transform | jdbc
。想象这是一个 Unix 命令(像cat myfile | grep "hello" | awk ‘{print $2}’ ...
)。你看到一些相似之处了吗?你所说的是,使用http
应用将消息发送到splitter
应用,然后splitter
应用将多条消息(一次一条)发送到收集一些信息的groovy-transform
应用,它将消息发送到将记录保存到数据库中的jdbc
应用。|
符号用作下一个应用(Unix 管道)的连接器或信息通道。movie =
声明可以被视为 DSL 及其定义的名称(就像一个名为 movie、值为http | splitter | groovy-transform | jdbc
的变量,比如 Unix)。 -
splitter > filter | log
。也就是说,从splitter
获取消息的副本,并将其发送到filter
应用,然后发送到log
应用。>
符号是一个重定向。不用太担心。当您使用>和:
符号时,您会学到更多。stars = :move.splitter
声明意味着您正在创建一个 tap(名为stars
)。您将到达第一个定义movie =
(或者引用变量movie
,如果您想到 Unix shell 术语的话)并使用声明:movie.splitter
访问splitter
应用;这意味着您正在创建一个 wire tap (消息模式,消息的副本)并将其发送到带有>
符号的filter
应用。 -
groovy-transform > filter | log
。这部分说从groovy-transform
获取消息的副本(一旦它完成处理消息),发送到filter
应用,然后发送到log
应用。imdb-high-rating = :movie.groovy-transform
声明意味着您正在创建一个 tap(名为imdb-high-rating
),并且您正在到达第一个定义movie =
并访问groovy-transform
;这意味着你正在创建一个窃听装置,一旦groovy-transform
返回,它就获取消息的副本,并将它发送到带有>
符号的filter
应用。
=
和:
符号(还有更多)具有我稍后在讨论标签、抽头和目的地时描述的含义;现在,把它们看作是命名流的一部分的方法,以及访问应用和应用其他符号的方法,就像 Unix shell 一样。
每个应用可以有多个属性(或者参数/自变量,如果您从 Unix shell 的角度考虑的话)。要么在 DSL 中声明它们,要么拥有一个外部属性文件(本地或远程云配置服务器)。您现在在同一个 DSL 中使用它们。
现在您已经了解了 DSL,是时候测试它并观察它的运行了。让我们从命令行开始。
使用 cURL、Httpie 和 jq
Spring CloudStream 服务器公开了一个 REST API,可以通过任何 REST 客户端或任何命令行实用程序进行编程访问,比如cURL
、wget
或Httpie
。在这一节中,我将向您展示如何使用命令行实用程序创建管道 DSL。让我们一步一步来。
-
确保您有一个命令实用程序。我将使用
cURL
和Httpie
向您展示这些命令。因为 Spring CloudStream 服务器以 JSON 格式公开其数据,所以安装 jq 实用程序是值得的。 -
确保您已经运行了 Spring Cloud Data Flow 和 Spring Cloud Data Flow Skipper 服务器。您可以使用任何方法——使用 Docker Compose 的本地设置或 Kubernetes 集群。通过使用一个
LoadBalancer
类型公开 Spring CloudStream 服务器,确保您可以到达。默认情况下,它使用端口 9393 来公开 REST API 和 GUI 的路径/dashboard
;但是你必须看服务和港口(例如,kubectl get svc -o wide
)。(我用的是 Docker Compose,用 RabbitMQ 和 MySQL 做持久化。我正在测试localhost
。$ curl -s http://localhost:9393 | jq .
Use Httpie if you don’t need to use the jq tool to format the output.
$ http :9393
如果使用的是本地基础设施(比如 Docker Compose),就不需要使用服务器名;默认情况下,它会转到本地主机。使用前面的命令,您可以看到所有公开的 API,所以现在您知道该做什么了。
-
使用查看是否已经注册了应用
$ curl -s http://localhost:9393/apps | jq .
or
$ http :9393/apps
如果没有任何 app,可以进入下一步;如果有可以跳过。
-
注册应用。根据您使用的代理和基础设施的类型,选择以下选项之一。
-
使用 Maven
-
Using Docker
-
https://dataflow.spring.io/task-docker-latest
You can execute the following command (I’m using Maven and RabbitMQ).
$ curl -s -X POST \ -d "uri=https://dataflow.spring.io/rabbitmq-maven-latest" \ -d "force=true" \ localhost:9393/apps | jq .
Or you can use this next one.
$ http -f POST \ :9393/apps \ uri=https://dataflow.spring.io/rabbitmq-maven-latest \ force=true
通过执行上一步中的命令,确定应用是否已注册。
-
-
有一个
jdbc
app,这意味着所有的电影对象都被发送到这个 sink,它需要准备好接受传入的行。你需要建立数据库。访问 MySQL 数据库非常重要。如果您使用的是 Kubernetes 和一个不同于数据流或 Skipper 使用的实例,则必须将正确的服务器名称添加到属性中,并确保您可以访问它。如果您使用的是 Docker Compose(以及本书源代码中的docker-compose.yml
文件),您可以通过以下方式获得访问权限。$ docker exec -it dataflow-mysql -uroot -prootpw
You need to create the
reviews
databases and themovies
table .mysql> create database reviews; mysql> use reviews mysql> create table movies( id varchar(10) primary key, title varchar(200), actor varchar(200), year int, genre varchar(25), stars int, rating decimal(2,1), ratingcount int);
-
其中一个流式应用是
groovy-transform
。这个应用调用外部 REST API。你使用 RapidAPI (https://rapidapi.com/
)网站。请,花点时间开个新账户(免费);要使用的服务是https://rapidapi.com/apidojo/api/imdb8
。您需要为此获取 API 密钥。您使用的最后一个 URL 是https://imdb8.p.rapidapi.com/title/get-ratings?tconst=${movie.id}
,在这里您传递电影的 ID。一旦你得到了你的 API 密匙,把它写下来,因为你接下来会用到它。 -
groovy-transform
应用需要一个脚本来执行。这个脚本在您的 Git 存储库中(您可以使用任何 Git 服务器—GitHub、GitLab、BitBucket 等。).为了方便起见,它必须是公共的。创建一个新的 repo 并添加以下 Groovy 脚本;将其命名为movie-transform.groovy
(见清单 9-3 )。
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
def jsonSlurper = new JsonSlurper()
def movie = jsonSlurper.parseText(new String(payload))
def connection = new URL( "https://imdb8.p.rapidapi.com/title/get-ratings?tconst=${movie.id}")
.openConnection() as HttpURLConnection
connection.setRequestProperty( 'x-rapidapi-host', 'imdb8.p.rapidapi.com' )
connection.setRequestProperty( 'x-rapidapi-key', 'YOURKEY')
connection.setRequestProperty( 'Accept', 'application/json' )
connection.setRequestProperty( 'Content-Type', 'application/json')
if ( connection.responseCode == 200 ) {
def imdb = connection.inputStream.withCloseable { inStream ->
new JsonSlurper().parse( inStream as InputStream )
}
movie.imdb = [ "rating": imdb.rating, "ratingCount": imdb.ratingCount ]
} else {
println connection.responseCode + ": " + connection.inputStream.text
}
JsonOutput.toJson(movie)
Listing 9-3.movie-transform.groovy
在继续之前,请分析脚本。请注意,它会转到 RapidAPI URL 并获取信息。它通过添加包含rating
和ratingCount
属性的imdb
部分来增强消息。这些属性来自对外部调用的响应。另外,请注意,您需要在x-rapidapi-key
标题中添加您的密钥,并使用提到的 URL。以原始格式访问这个脚本很重要。比如在 GitHub 中,可以在 https://raw.githubusercontent.com/<your-user-id>/<your-repo>/master/movie-transform.groovy
访问。请注意这个 URL。
-
是时候创建电影管道 DSL 了。这是您使用的最终版本(参见清单 9-4 )。
movie=http --port=9001 | splitter --expression="#jsonPath(payload, '$.movies')" | groovy-transform --script="https://raw.githubusercontent.com/<user>/<repository>/master/movie-transform.groovy" | jdbc --columns="id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount" --table-name="movies" --password="rootpw" --driver-class-name="org.mariadb.jdbc.Driver" --username="root" --url="jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false" stars=:movie.splitter > filter --expression="#jsonPath(payload,'$.stars') > 3" | log imdb-high-rating=:movie.groovy-transform > filter --expression="#jsonPath(payload,'$.imdb.rating') > 8.0" | log Listing 9-4.Movie Pipeline DSL
This movie pipeline DSL is composed of three statements.
-
movie = http | splitter | groovy-transform | jdbc
。movie
是流的名称,在接下来的语句中很有用。注意,这个定义使用了像--port
这样的属性,因为它在寻找一个本地基础设施。如果您使用 Kubernetes 方法,您需要向http
应用添加一个带有LoadBalancer
类型的服务来获得一个 IP 并访问它。splitter
应用根据movies
集合将消息分割成几个movie
对象。JSON send 包含一组movies
。groovy-transform
的脚本属性指向 Git 存储库/服务器中的原始文件(确保它是一个原始文件)。最后,它转到jdbc
应用,该应用将增强的电影对象(JSON)从groovy-transform
保存到review
数据库和movies
表中。再看--columns
属性。它定义了映射 JSON 结果的表列。 -
stars = :movie.splitter > filter | log
。stars
是流的名称。你稍后会看到这个,因为你称它为龙头。:movie.splitter
说的是“在movie
流和splitter
应用之后创建一个窃听”——换句话说,一旦你做了分割,就获得一份信息的副本。你从movies
(JSON)的集合中收集一个movie
对象(JSON)的副本;然后,您将那个movie
对象(JSON)传递给filter
应用。请注意,您使用的是>
符号而不是|
,因为这是点击消息所需的语法。tap 是 wire tap 消息集成模式实现。filter
应用使用一个 JSON 表达式来获取星星,如果大于三个,它会将其传递给 log 应用。日志应用写入控制台。 -
imdb-high-rating = :movie.groovy-transform > filter | log
。imdb-high-rating
是流的名称,它在groovy-transform
应用处获得电影流的副本,并重定向到评估imdb.rating,
的filter
,如果它大于 8.0,则将其传递给log
应用。log
应用写入控制台。You execute three different commands, one per statement.
$ curl -s -X POST \ --form 'name=movie' \ --form 'definition=movie=http --port=9001 | splitter --expression="#jsonPath(payload,'\''$.movies'\'')" | groovy-transform --script=https://raw.githubusercontent.com/<user>/<repo>/master/movie-transform.groovy | jdbc --columns=id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount --table-name=movies --password=rootpw --driver-class-name=org.mariadb.jdbc.Driver --username=root --url=jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false' \ localhost:9393/streams/definitions | jq . $ curl -s -X POST \ --form 'name=stars' \ --form 'definition=stars= :movie.splitter > filter --expression="#jsonPath(payload,'\''$.stars'\'') > 3" | log' \ http://localhost:9393/streams/definitions | jq . $ curl -s -X POST \ --form 'name=imdb-high-rating' \ --form 'definition=imdb-high-rating= :movie.groovy-transform > filter --expression="#jsonPath(payload,'\''$.imdb.rating'\'') > 8.0" | log' \ http://localhost:9393/streams/definitions | jq .
在每个命令中,您都需要
name
和definition
参数。不要忘记为您的 Git 服务器使用您自己的user
和repository
。如果你复制这段文字,一定要注意单引号和双引号以及单引号之间的区别。记住,我使用的是本地 Docker Compose。这就是为什么 http 应用中的第一个 DSL 流定义有
--port=9001
,这意味着我发布了一些指向该端口的电影。如果你正在使用 Kubernetes,你不需要(你可以删除那个属性),然后确保你可以通过给它添加一个带有LoadBalancer
类型的服务或者转发端口来到达http
应用。
-
Note
在本书的配套源代码的ch09/streams
文件夹中,有一个名为curl-movie-stream-pipeline
的文件,其中包含正确的字符。您可以复制它们或执行文件。
-
通过执行以下命令来验证我们的定义是否存在。
$ curl -s localhost:9393/streams/definitions | jq .
在这个命令中,您可以看到所有的流定义:
movie
、stars
、imdb-high-rating
。 -
部署流。为此,您需要执行以下命令。
$ curl -s -X POST \ http://localhost:9393/streams/deployments/movie | jq . $ curl -s -X POST \ http://localhost:9393/streams/deployments/stars | jq . $ curl -s -X POST \ http://localhost:9393/streams/deployments/imdb-high-rating | jq .
-
Now that the movie stream is deployed, it is time to send some data.
$ curl -s -X POST \ -H "Content-Type: application/json" \ -d '{"movies":[{"id":"tt0133093","title":"The Matrix","actor":"Keanu Reeves","year":1999,"genre":"fiction","stars":5},{"id":"tt0209144","title":"Memento","actor":"Guy Pearce","year":2000,"genre":"drama","stars":4},{"id":"tt0482571","title": "The Prestige","actor":"Christian Bale","year":2006,"genre":"drama","stars":3},{"id":"tt0486822","title":"Disturbia","actor":"Shia LaBeouf","year":2007,"genre":"drama","stars":3}]}' \ http://localhost:9001 | jq .
Note that you are sending a set of movies. If you are using Docker Compose (and
docker-compose.yml
from the source code), the Skipper server is where all the apps are running. To send the data, you need to enter the following in theskipper
docker container.$ docker exec -it skipper bash $ curl -s -X POST ....
如果你用的是 Kubernetes,需要暴露
http
app 或者做一个端口转发然后做 POST。 -
看看日志。要查看是否一切正常,请获取应用的运行时信息。执行以下命令。
$ curl -s localhost:9393/runtime/apps | jq .
This command gives you all the information about the logs (for the
stars
and theimdb-high-rating streams
). You should find the name (deploymentId
) asstars.log-v1
andimdb-high-rating.log-v1
and thestdout
property. If you are using the Docker Compose (anddocker-compose.yml
from the book’s source code), then you must go to theskipper-docker
container and do a tail over the path from thestdout
property.$ docker exec skipper tail -n 500 -f /tmp/1590548505908/stars.log-v1/stdout_0.log
If you are using Kubernetes, you should see all the apps as pods, with the same naming convention; to see the logs for
start-logs-v1
, you should execute the following.$ kubectl get pods $ kubectl logs -f pod/star-logs-v1-xxxx
xxxx 是添加到 pod 的 ID。
If you have access to the MySQL database, you should see something similar to the following.
mysql> select * from movies \G *************************** 1\. row *************************** id: tt0133093 title: The Matrix actor: Keanu Reeves year: 1999 genre: fiction stars: 5 rating: 8.7 ratingcount: 1609934 *************************** 2\. row *************************** id: tt0209144 title: Memento actor: Guy Pearce year: 2000 genre: drama stars: 4 rating: 8.4 ratingcount: 1084378 *************************** 3\. row *************************** id: tt0482571 title: The Prestige actor: Christian Bale year: 2006 genre: drama stars: 3 rating: 8.5 ratingcount: 1136643 *************************** 4\. row *************************** id: tt0486822 title: Disturbia actor: Shia LaBeouf year: 2007 genre: drama stars: 3 rating: 6.8 ratingcount: 214786
恭喜你!您使用 REST API 创建了一个电影流管道 DSL。
如果您想要取消部署您的流,您可以使用下面的命令一个接一个地完成。
$ curl -s -X DELETE \
http://localhost:9393/streams/deployments/movie | jq .
$ curl -s -X DELETE \
http://localhost:9393/streams/deployments/stars | jq .
$ curl -s -X DELETE \
http://localhost:9393/streams/deployments/imdb-high-rating | jq .
或者同时使用以下内容。
$ curl -s -X DELETE \
http://localhost:9393/streams/deployments | jq .
如果您想删除流,您可以一次删除一个,如下所示。
$ curl -s -X DELETE \
http://localhost:9393/streams/definitions/movie | jq .
$ curl -s -X DELETE \
http://localhost:9393/streams/definitions/stars | jq .
$ curl -s -X DELETE \
http://localhost:9393/streams/definitions/imdb-high-rating | jq .
或者一次完成,如下所示。
$ curl -s -X DELETE \
http://localhost:9393/streams/definitions | jq .
接下来,交互式地使用 Spring CloudStream shell,更实际地创建流。
使用 Spring CloudStream 外壳
创建流有更多的选项,在本节中,您将看到 Spring CloudStream 外壳的运行。你可以把这个客户端看作一个交互式的基于文本的/终端的工具,在这里你可以做测试。首先,确保您删除了任何流 DSL 定义,并停止/重启您的 Spring CloudStream 服务器,因为您需要再次注册所有应用。
使用 Spring CloudStream 外壳有两种选择。如果您的基础设施使用本地部署,比如 Docker Compose ( ch09/docker-compose
),您可以重用 Spring CloudStream 服务器。启动基础设施后,可以按如下方式使用数据流外壳。
$ docker exec -it dataflow-server java -jar shell.jar
你的屏幕应该看起来如图 9-3 所示。
图 9-3。
Spring CloudStream 外壳
另一个选择是从 https://repo.spring.io/release/org/springframework/cloud/spring-cloud-dataflow-shell/2.6.0/spring-cloud-dataflow-shell-2.6.0.jar
下载优步罐。下载后,执行下面的命令。
$ java -jar spring-cloud-dataflow-shell-2.6.0.jar --help
该命令显示了您可以覆盖的所有参数(参见图 9-4 )。
图 9-4。
带- help 参数的 Spring CloudStream 外壳
在继续之前,检查所有参数。数据流外壳(客户端)有默认值,比如试图连接到localhost:9393
的uri
参数。
现在再次执行该命令,但不带--help
参数。
$ java -jar spring-cloud-dataflow-shell-2.6.0.jar
如果您仍在使用 Docker Compose 和 exposed 端口 9393,则连接成功,但如果您的数据流服务器运行在 Kubernetes 中,则必须为该服务器分配 IP 或域。因为连接localhost:9393
失败,所以给你显示一个server-unknown
提示。在这种情况下,您可以使用以下命令。
server-unknown:>dataflow config server https://my-scdf-server
它伸出手,连接到你的数据流服务器(见图 9-5 )。
图 9-5。
Spring CloudStream 外壳:服务器未知
如果您使用 Docker Compose,我推荐使用下面的命令来启动数据流 shell,因为它更容易将数据发送到我们的http
应用。
$ docker exec -it dataflow-server java -jar shell.jar
使用优步-JAR 需要暴露 Skipper 服务器端口 9001,这意味着您需要在docker-compose.yml
文件中添加ports
属性。
数据流 shell 提供了 60 多个带制表符补全的命令,允许您查看、列出、创建、删除、销毁以及更多关于流和任务的 Uri。Spring CloudStream shell 是一个 REST API 客户端,但是比简单的cURL
或Httpie
命令行实用程序具有更大的容量。
让我们从查看一些可以从数据流外壳执行的命令开始。我认为您在数据流 shell 中最好的朋友之一是help
命令。键入 help 并按回车键,您会看到您可以执行的 60 多个命令(参见图 9-6 )。
图 9-6。
Spring CloudStream 外壳:使用帮助命令
dataflow:>help
您也可以使用help [command [options]]
语法。例如,您使用stream
命令,这样您就可以进行输入。
dataflow:>help stream
前面的命令显示了stream
命令的可用选项列表,您可以执行:
dataflow:>help stream create
分析输出,注意它附带了创建流所需的描述。数据流 shell 附带了TAB
完成功能,因此您可以执行stream
命令并双击TAB
键来获得可用选项,如all
、info
、create
、list
、deploy
、undeploy
等等。
当您开始创建流时,您会看到每个命令都有选项,这些选项都有需要以双破折号开头的参数。举个例子,
dataflow:>stream deploy --name=movie
有时你需要用单引号(')或双引号("
)来传递值,你需要非常小心。当然,你会发现你需要躲避一些角色,但是不要担心,时间到了。我告诉你怎么做。其他规则在这里没有涉及,但是你可以在 https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#_shell_rules
阅读。
Spring Data 流外壳的另一个好处是它解析 DSL 的方式。您必须选择一种有效的方法来添加您的表达式,而不是组合它们。例如,filter
应用需要传递一个表达式参数(正如您之前看到的cURL
/ Httpie
命令),并且您必须选择以下任意一个。
filter --expression=payload>5.0
filter --expression=#jsonPath(payload,'$.imdb.rating')>5.0
filter --expression='#jsonPath(payload,''$.imdb.rating'') > 5.0'
前面的表达式使用了 SpEL (Spring Expression Language)的#jsonPath
和通用对象,比如payload
。注意空格和单引号。没有双引号。您仍然可以添加双单引号。此外,您可以用反斜杠\
来转义字符。你可以在这里得到更多的感觉, https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#_dsl_parsing_rules
和这里 https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#_spel_syntax_and_spel_literals
。
谈到属性,您可以使用外部属性文件传递所有参数。通常,该文件可以是 JAR 被执行的地方。参数不应带有任何单引号或双引号。这些属性的语法是app.<app-name>.<parameter>=<value>
。例如,
app.filter.expression=#jsonPath(payload, '$imdb.rating') > 5.0
为了更好地理解这些特性,让我们从创建相同的电影流管道 DSL 开始。
-
注册应用。在提示符下,执行以下命令。
dataflow:>app import --uri https://dataflow.spring.io/rabbitmq-maven-latest
Remember that you need the
http
,filter
,jdbc
,log
,splitter,
andgroovy-transform
apps. Also, you need to use one broker, in the preceding command I’m assuming you are using RabbitMQ. You can use Kafka as well. Once you import them, you can have them listed withdataflow:>app list
-
创建电影流。首先,让我们看看溪流
dataflow:>stream list
It should be empty. Next, let’s add the first stream. Don’t forget to change your Git server’s
user
andrepository
.dataflow:> stream create --name movie --definition "http --port=9001 | splitter --expression=#jsonPath(payload,'$.movies')| groovy-transform --script=https://raw.githubusercontent.com/<user>/<repo>/master/movie-transform.groovy | jdbc --columns=id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount --table-name=movies --password=rootpw --driver-class-name=org.mariadb.jdbc.Driver --username=root --url=jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false" dataflow:> stream create --name stars --definition ":movie.splitter > filter --expression=\"#jsonPath(payload,'$.stars')>3\" | log" dataflow:> stream create --name imdb-high-rating --definition ":movie.groovy-transform > filter --expression=\"#jsonPath(payload,'$.imdb.rating') > 8.0\" | log"
In the scaped characters, note the
\"
. You can look at the definitions with the following.dataflow:> stream list
信息中没有转义字符。
-
It’s time to deploy. Execute the following commands.
dataflow:>stream deploy --name movie dataflow:>stream deploy --name stars dataflow:>stream deploy --name imdb-high-rating
You can look see the status using the following.
dataflow:> stream info --name movie dataflow:> stream info --name stars dataflow:> stream info --name imdb-high-rating
状态应该是已部署。
-
It’s time to send some data. The Spring Data Flow shell comes with a REST API client that you can use right there in the shell. The command is
http
. You can usehelp http
to find out which parameters are important to send information. To send some data, you can execute the following command.dataflow:> http post --target http://<change-me>:9001 --data '{"movies":[{"id": "tt0133093","title": "The Matrix","actor": "Keanu Reeves", "year": 1999,"genre":"fiction","stars": 5},{"id": "tt0209144","title": "Memento","actor": "Guy Pearce","year": 2000,"genre": "drama","stars": 4},{"id": "tt0482571","title": "The Prestige","actor": "Christian Bale","year": 2006,"genre": "drama","stars": 3 },{"id": "tt0486822","title": "Disturbia","actor": "Shia LaBeouf","year": 2007,"genre": "drama","stars": 3}]}' --contentType "application/json"
如果你查看
http post
命令,有一个--target
参数。这是http
应用运行的地方;不可能是localhost
。如果你用的是 Kubernetes,那很容易,因为你要用一个LoadBalancer
类型暴露http
app;但是如果你使用的是来自书的源代码(ch09/docker-compose
)的 Docker Compose (docker-compose.yml
),你需要确保skipper
服务器是可达的;要么使用ports
并在docker-compose.yml
中暴露"9001:9001"
(在skipper
服务中),要么使用dataflow-server
中的docker shell.jar
。换句话说,您需要知道哪个 IP 被分配给了skipper
容器。To get the IP of the
skipper
container, you can run.$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' skipper
-
该检查日志了。为此,您需要使用运行时应用命令。
dataflow:>runtime apps
This command shows you the
name
,status,
and some other properties such asstdout
andstderr
. You are interested in thestdout
property that is showing the logs. If you are using Docker Compose, then you can execute the following command.$ docker exec skipper tail -n 500 -f /tmp/1590548505908/stars.log-v1/stdout_0.log
If you are using Kubernetes, it is easier; check out the pods and use the logs command.
$ kubectl get pods $ kubectl logs start-log-v1-xxxx
-
查看 MySQL 数据库并执行 SELECT 语句。
恭喜你!您使用了 Spring CloudStream 外壳。
To remove and delete everything, you can use the same
stream
command .dataflow:> stream destroy --name imdb-high-rating dataflow:> stream destroy --name stars dataflow:> stream destroy --name movie
现在,您可以使用 exit 命令退出。如果你愿意,你可以关闭你的基础设施。接下来,让我们使用仪表板再做一次。
使用仪表板
尽管您已经了解了 dashboard,但是现在是时候使用它来部署电影流管道 DSL 了。对于这一节,从头开始就好,所以请停止/重启你的基础设施;要么使用 Docker Compose,要么使用 Kubernetes 实例。如果你正在使用 Kubernetes,试着重建 MySQL 数据库,这样它就不会有应用和流的记录。我们开始吧。
-
确保 Spring CloudStream 和 Skipper 服务器已经启动并运行。如果您使用的是 Kubernetes,请确保使用 LoadBalancer 类型公开 Spring CloudStream 服务器,这样您就可以有一个物理 IP 地址进行访问。另外,不要忘记在 MySQL 实例中创建
reviews
数据库和movies
表。 -
Open your browser and go to http://
:[9393]/dashboard (see Figure 9-7). 图 9-7。
仪表盘
图 9-7 为仪表板。在左窗格中,您会发现以下内容。
-
应用。这是你注册所有可用应用的地方,包括我们自己的(你稍后会看到)。请记住,应用基于活页夹和 Maven 或 Docker 坐标。
-
运行时(您可以看到所有部署)
-
流
-
任务
-
乔布斯
-
审计记录
-
-
让我们注册应用。请记住,您可以在 Rabbit 或 Kafka 以及 Maven 或 Docker 坐标之间进行选择。稍后,您将看到如何使用我们的定制流式应用和 NATs broker,并与其他应用一起使用。
Click the Apps pane, and in the right pane, click the + Add Application(s) button/link. Then, select the Bulk import application coordinates from an HTTP URI location option. Then, you can pre-fill the URI with one of the items in the list by clicking it. Choose either Rabbit or Kafka (depending on your primary broker) and either Maven or Docker. Then you can click Import application(s). This imports the apps, and it takes you to the main page. You should have something like Figure 9-8.
图 9-8。
仪表板:导入的应用
-
In the left pane, click the Streams option. Then in the right pane, click the + Create Stream(s) button/link . This take you to the GUI part where you can drag-n-drop apps into the canvas or you can write the DSL. To make things easier, you can add the following DSL.
movie=http --port=9001 | splitter --expression="#jsonPath(payload, '$.movies')" | groovy-transform --script="https://raw.githubusercontent.com/felipeg48/scdf-scripts/master/movie-transform.groovy" | jdbc --columns="id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount" --table-name="movies" --password="rootpw" --driver-class-name="org.mariadb.jdbc.Driver" --username="root" --url="jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false" stars=:movie.splitter > filter --expression="#jsonPath(payload,'$.stars') > 3" | log imdb-high-rating=:movie.groovy-transform > filter --expression="#jsonPath(payload,'$.imdb.rating') > 8.0" | log
Note that you added enclosing double quotes per parameter. When you add this pipeline DSL, you see the graph displayed (see Figure 9-9).
图 9-9。
仪表板:电影管道 DSL
You can now play with it by removing some of the components, or in the DSL area, you can remove all the properties and have in plain sight what the DSL looks like without parameters (see Figure 9-10).
图 9-10。
仪表板:不带参数的 DSL
movie= http | splitter | groovy-transform | jdbc stars= :movie.splitter > filter | log imdb-high-rating= :movie.groovy-transform > filter | log
If you want to add parameters without adding them to the DSL area because you want to avoid any double/single quote confusion, you can select any app from the graph, and it shows you the Options and Delete links. You can select Options. A pop-up window appears, where you can add the parameter value. In the case of the
http
app, you can add port value 9001 (see Figures 9-11 and 9-12).图 9-12。
仪表板:http 应用参数
图 9-11。
仪表板:已选择 http 应用
如果您单击 Update,它会更新 DSL,您会在 DSL 区域看到
--port=9001
。 -
Add the DSL where you have all the parameters set. Click the Create Stream(s) button, which opens a pop-up where you need to add any description (this is optional) and a name that by default is the one you added already into the DSL (see Figure 9-13).
图 9-13。
仪表板:创建流
You can click Create the three streams, which takes you back to where the streams are listed. In that list, you see the streams’ definitions. At the end of every row are the status and three icons. The first icon shows the stream’s details. The second icon (the Play icon) deploys the stream, and the last icon (a caret facing down) shows you options like Show details, Deploy, Undeploy, Destroy stream. If you click the > icon next to each stream’s definition, you find a graph related to the stream (see Figures 9-14, 9-15, and 9-16).
图 9-16。
仪表板:流图
图 9-15。
仪表板:流图标
图 9-14。
仪表板流列表
-
It’s time to deploy one by one. You start first with the
movie
stream. Then you can select any of the other in any order. In the end, thestars
andimdb-high-rating
streams depend on themovie
to start first. You can click the Play icon to deploy. When you do, another pop-up appears where there are extra settings that can help with the deployment, and there are more related to the platform where the streams are deployed. Also, you have another chance to change the existing parameters if you needed them to (see Figure 9-17).图 9-17。
仪表板:流-部署属性
The parameters are separated by the following.
-
Platform
。通常,您可以选择部署哪个平台,但这是一个混合选项,可以部署到 Cloud Foundry、Kubernetes 或本地实例。 -
Generic deployer
。您可以在其中添加一些资源约束,以便为任何其他应用节省资源,如内存、CPU、磁盘和实例数量。 -
Deployment platform
。它与 Spring Cloud 和 Java VM 属性相关,比如 java-opts、debug-port、java-command(一个带有特殊标志来运行 Java 的命令)、端口范围等等。 -
Application properties
。在那里你为应用定义了所有的参数,比如http port
、jdbc table-name
等等。Note that you have the same properties for each app defined in the
movie stream
. And if you are okay with everything, click the Deploy Stream button. This takes you to the main stream list. The Status column shows that your stream is being deployed (see Figure 9-18).
图 9-18。
仪表板:流列表–状态:正在部署
You can repeat the same for the
stars
andimdb-high-rating
streams (see Figure 9-19).图 9-19。
仪表板:流列表
-
-
Once all the streams have a Deployed status, there are several ways to see information about your app. Click
movie stream
. The information icon (i) shows the details (see Figure 9-20).图 9-20。
仪表板:显示细节-摘要-电影流
If you scroll down, you get more information about every app defined in the movie stream, and if you keep going scrolling down, you can see the logs. You have a dropdown list and choose the logs you want to see. At the top of this page, you see the Summary, Graph, and History tabs. You can check them out. The graph has the entire DSL definition, including the taps (see Figure 9-21).
图 9-21。
仪表板:显示详细信息-摘要-电影流-日志
-
Another way to see more information is to get into the runtime. Click Runtime in the left pane. It shows you everything about your apps, such as process ID, port, instance name, version, and logs for
stdout
andstderr
. You can click any of the boxes (see Figures 9-22 and 9-23).图 9-23。
仪表板:运行时–流式应用
图 9-22。
仪表板:运行时–流式应用,命名约定-
- - -
是时候发一些数据了。这里您需要使用任何 REST 客户端。你可以使用 cURL 命令,或者如果你喜欢图形界面,你可以使用 Postman (
www.postman.com/downloads/
)或者失眠症(https://insomnia.rest/
)来发送数据。您需要发送之前的 JSON 数据(参见清单 9-1 )。 -
例如,您可以返回到流列表并单击
star
流的详细信息。在摘要中,向下滚动到日志并选择stars.log-v1,
,在底部,您应该会看到一些关于您刚刚发送的数据的结果。您还可以检查 MySQL 数据库,看看记录是否在那里。
恭喜你!您已经使用仪表板部署了电影流 DSL。
要删除流,请转到流列表,选择流,单击上下颠倒的插入符号,然后选择销毁流。出现提示后,如果您确定,点击销毁流定义(见图 9-24 )。
图 9-24。
仪表板:销毁流
仪表板中有更多的特性,我将在后面的任务、作业和监控中讨论。
以编程方式创建流
创建和部署流的另一种方法是通过 Spring CloudStream 模块公开的 Java DSL APIs。使用这种方法,您可以根据您的业务需求,创建一种动态的方法来集成流的生命周期。本节将向您展示如何使用这个 API 及其关键类。
Spring CloudStream API 公开了两种风格的 Java DSL APIs。
-
定义风格。这种风格允许您使用已经在前面章节中部署的 DSL 定义。例如,
Stream.builder(dataFlowOperations) .name("simple-stream") .definition("http | log") .create();
-
流畅的风格。这种风格提供了一种使用流畅的方法链(如
source.processor.sink
)来创建流式应用的方式,以开发更动态的解决方案。例如,Stream.builder(dataFlowOperations) .name("simple-stream") .source(httpSource) .sink(logSink) .create();
Java DSL API:定义风格
让我们使用 Spring CloudStream Java DSL API 创建电影流管道 DSL,从定义样式开始。
图 9-25。
Spring Initializr 电影-dsl 项目
-
Open your browser and point to
https://start.spring.io
. Complete the metadata with the following information.-
组:
com.apress.cloud.stream
-
神器:
movie-dsl
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
单击生成按钮下载 ZIP 文件。您可以在您选择的 IDE 中解压缩并导入它(参见图 9-25 )。
-
-
Open the
pom.xml
file and add the following and required dependency.<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dataflow-rest-client</artifactId> <version>2.6.0</version> </dependency>
在撰写本书时,版本是
spring-cloud-rest-client
依赖项的2.6.0
。这个模块提供了新的类,我将很快讨论这些类。 -
创建一个
MovieDslStream
枚举。它保存了关于定义及其名称的信息(参见清单 9-5 )。
package com.apress.cloud.stream.movie;
public enum MovieDslStream {
MOVIE("movie",
"http --port=9001 | splitter --expression=\"#jsonPath(payload, '$.movies')\" | " +
"groovy-transform --script=\"https://raw.githubusercontent.com/felipeg48/scdf-scripts/master/movie-transform.groovy\" | " +
"jdbc --columns=\"id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount\" " +
"--table-name=\"movies\" --password=\"rootpw\" --driver-class-name=\"org.mariadb.jdbc.Driver\" --username=\"root\" " +
"--url=\"jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false\""),
STARS("stars",":movie.splitter > filter --expression=\"#jsonPath(payload,'$.stars') > 3\" | log"),
IMDB("imdb-high-rating",":movie.groovy-transform > filter --expression=\"#jsonPath(payload,'$.imdb.rating') > 8.0\" | log");
private String name;
private String definition;
MovieDslStream(String name, String definition){
this.name = name;
this.definition = definition;
}
public String getName(){
return this.name;
}
public String getDefinition() {
return this.definition;
}
}
Listing 9-5.src/main/java/com/apress/cloud/stream/movie/MovieDslStream.java
清单 9-5 显示了MOVIE
、STARS
、IMDB
枚举类型和它们的两个值:name
( getName()
方法)和definition
( getDefinition()
方法)。
- 创建保存一个字段的
MovieDslProperties
(参见清单 9-6 )。
package com.apress.cloud.stream.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie")
public class MovieDslProperties {
private String action = "create"; // create | deploy | destroy
}
Listing 9-6.src/main/java/com/apress/cloud/stream/movie/MovieDslProperties.java
这个类包含一个可以是create
、deploy,
或destroy
的动作。它调用正确的方法来执行和创建、部署或销毁流。尽管它只是一个属性,但是您可以添加更多的行为来对一些值做出反应。
- 创建
MovieDslService
(参见清单 9-7 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import org.springframework.cloud.dataflow.rest.client.DataFlowOperations;
import org.springframework.cloud.dataflow.rest.client.dsl.DeploymentPropertiesBuilder;
import org.springframework.cloud.dataflow.rest.client.dsl.Stream;
@AllArgsConstructor
public class MovieDslService {
private DataFlowOperations dataFlowOperations;
public void create(){
java.util.stream.Stream.of(MovieDslStream.values()).forEach( c -> {
createStream(c.getName(),c.getDefinition());
});
}
public void deploy(){
java.util.stream.Stream.of(MovieDslStream.values()).forEach( c -> {
deployStream(c.getName());
});
}
public void destroy(){
java.util.stream.Stream.of(MovieDslStream.values()).forEach( c -> {
destroyStream(c.getName());
});
}
private void createStream(String name, String definition){
Stream.builder(dataFlowOperations)
.name(name)
.definition(definition)
.create();
}
private void deployStream(String name){
dataFlowOperations.streamOperations().deploy(name,
new DeploymentPropertiesBuilder().build());
}
private void destroyStream(String name){
dataFlowOperations.streamOperations().destroy(name);
}
}
Listing 9-7.src/main/java/com/apress/cloud/stream/movie/MovieDslService.java
清单 9-7 显示了定义样式。createStream
方法使用带有 fluent API 的Stream
类,在本例中,您使用 definition 方法来创建、部署或销毁。在这种情况下,您通过使用一个定义来创建 DSL。注意你有一个DataFlowOperations
接口。DataFlowTemplate
类实现了这个接口。DataFlowTemplate
类基于模板模式,与 Spring CloudStream 服务器 REST API 交互。看看其他方法,你会发现这非常简单明了。注意,create
、deploy
和destroy
方法正在迭代MovieDslStream
枚举,并使用getName()
和getDefinition()
来获取值。
-
创建
MovieDslConfiguration
类来连接所有必要的 Spring beans(参见清单 9-8 )。package com.apress.cloud.stream.movie; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.dataflow.rest.client.DataFlowOperations; import org.springframework.cloud.dataflow.rest.client.DataFlowTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.lang.reflect.Method; import java.net.URI; @EnableConfigurationProperties(MovieDslProperties.class) @Configuration public class MovieDslConfiguration { @Bean public DataFlowOperations dataFlowOperations(){ URI dataFlowUri = URI.create("http://localhost:9393"); DataFlowOperations dataFlowOperations = new DataFlowTemplate(dataFlowUri); dataFlowOperations.appRegistryOperations().importFromResource( "https://dataflow.spring.io/rabbitmq-maven-latest", true); return dataFlowOperations; } @Bean public CommandLineRunner actions(MovieDslService movieDslService, MovieDslProperties movieDslProperties){ return args -> { Method method = movieDslService.getClass() .getMethod(movieDslProperties.getAction(),null); assert method != null; method.invoke(movieDslService,null); }; } @Bean public MovieDslService movieDslService(DataFlowOperations dataFlowOperations){ return new MovieDslService(dataFlowOperations); } } Listing 9-8.src/main/java/com/apress/cloud/stream/movie/MovieDslConfiguration.java
清单 9-8 显示了您使用的配置。我们来分析一下。
-
dataflowOperations
。这个方法使用DataFlowTemplate
类在这个基础上创建了一个DataFlowOperations
接口的实例;这个类指向数据流服务器 REST API(本例中为 http://localhost:9393)。它还使用rabbitmq-maven-latest
URI 导入应用。 -
movieDslService
。这个声明创建了通过DataFlowOperations
接口的 Spring BeanmovieDslService
。 -
actions
。当 Spring 应用容器准备好执行程序时,就会执行这个方法。这个方法有movieDSL
和movieDslProperties
参数。请注意,您正在使用 Java 反射 API 来执行 MovieDslService 方法,这是一个基于给定属性的命令模式的小型实现:create、deploy 或 destroy。
-
-
Add the following content to the
application.properties
file .## Movie properties # action = create, deploy, destroy movie.action=create
运行程序时,可以使用创建、部署或销毁。
-
在运行这个项目之前,您需要确保使用 Docker Compose 或 Kubernetes 运行您的基础设施。还要确保重新创建保存电影的数据库。
-
运行您的项目。完成后,您可以在浏览器中查看
/dashboard
路径,看到应用已注册,三个电影 DSL 流已创建(如果您在浏览器中转至流部分)。 -
Change the
movie.action
property todeploy
and run the App. Your pipeline DSL has been deployed. And you can send a movie set with the following.curl -s -X POST \ -H "Content-Type: application/json" \ -d '{"movies":[{"id":"tt0133093","title":"The Matrix","actor":"Keanu Reeves","year":1999,"genre":"fiction","stars":5},{"id":"tt0209144","title":"Memento","actor":"Guy Pearce","year":2000,"genre":"drama","stars":4},{"id":"tt0482571","title": "The Prestige","actor":"Christian Bale","year":2006,"genre":"drama","stars":3},{"id":"tt0486822","title":"Disturbia","actor":"Shia LaBeouf","year":2007,"genre":"drama","stars":3}]}' \ http://localhost:9001
根据您的部署更改地址(Docker Compose 或 Kubernetes)。接下来,查看日志和数据库(参考前面的章节)。
-
运行之后,部署并检查日志和数据库。您可以将
movie.action
属性更改为destroy
,运行项目,并在仪表板中看到您的 DSL 被删除。
恭喜你!您已经使用 Java DSL 定义样式以编程方式创建、部署和销毁了电影流管道 DSL。
Java DSL API:流畅风格
本节介绍 Java DSL 流畅风格。
-
您可以使用以下元数据创建另一个项目。
-
组:
com.apress.cloud.stream
-
神器:
movie-dsl-fluent
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
-
-
单击生成按钮下载一个 ZIP 文件。解压缩并导入到您喜欢的 IDE 中。
-
你可以复制
MovieDslStream
枚举和MovieDslProerties
类,以及application.properties
文件,你可以重用它们。 -
创建
MovieDslService
类(参见清单 9-9 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import org.springframework.cloud.dataflow.rest.client.DataFlowOperations;
import org.springframework.cloud.dataflow.rest.client.dsl.DeploymentPropertiesBuilder;
import org.springframework.cloud.dataflow.rest.client.dsl.Stream;
import org.springframework.cloud.dataflow.rest.client.dsl.StreamApplication;
import org.springframework.cloud.dataflow.rest.client.dsl.StreamBuilder;
@AllArgsConstructor
public class MovieDslService {
private DataFlowOperations dataFlowOperations;
private StreamApplication httpSource;
private StreamApplication splitterProcessor;
private StreamApplication groovyTransformProcessor;
private StreamApplication jdbcSink;
public void create(){
createFluentStream(MovieDslStream.MOVIE.getName());
java.util.stream.Stream.of(MovieDslStream.values()).filter(c -> !c.getName().equals(MovieDslStream.MOVIE.getName())).forEach( c -> {
createStream(c.getName(),c.getDefinition());
});
}
public void deploy(){
java.util.stream.Stream.of(MovieDslStream.values()).forEach( c -> {
deployStream(c.getName());
});
}
public void destroy(){
java.util.stream.Stream.of(MovieDslStream.values()).forEach( c -> {
destroyStream(c.getName());
});
}
private void createFluentStream(String name){
Stream.builder(dataFlowOperations)
.name(name)
.source(httpSource)
.processor(splitterProcessor)
.processor(groovyTransformProcessor)
.sink(jdbcSink)
.create();
}
private void createStream(String name, String definition){
Stream.builder(dataFlowOperations)
.name(name)
.definition(definition)
.create();
}
private void deployStream(String name){
dataFlowOperations.streamOperations().deploy(name,new DeploymentPropertiesBuilder().build());
}
private void destroyStream(String name){
dataFlowOperations.streamOperations().destroy(name);
}
}
Listing 9-9.src/main/java/com/apress/cloud/stream/movie/MovieDslService.java
清单 9-9 显示了MovieDslService
类。这里重要的部分是createFluentStream
方法;请注意,您没有使用该定义。您可以使用 source、processor 或 sink 方法。
- 创建
MovieDslConfiguration
类(参见清单 9-10 )。
package com.apress.cloud.stream.movie;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.dataflow.core.ApplicationType;
import org.springframework.cloud.dataflow.rest.client.DataFlowOperations;
import org.springframework.cloud.dataflow.rest.client.DataFlowTemplate;
import org.springframework.cloud.dataflow.rest.client.dsl.StreamApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.net.URI;
@EnableConfigurationProperties(MovieDslProperties.class)
@Configuration
public class MovieDslConfiguration {
@Bean
public CommandLineRunner actions(MovieDslService movieDslService, MovieDslProperties movieDslProperties){
return args -> {
Method method = movieDslService.getClass()
.getMethod(movieDslProperties.getAction(),null);
assert method != null;
method.invoke(movieDslService,null);
};
}
@Bean
public MovieDslService movieDslService(DataFlowOperations dataFlowOperations,
StreamApplication httpSource,StreamApplication splitterProcessor,StreamApplication groovyTransformProcessor,
StreamApplication jdbcSink, StreamApplication logSink){
return new MovieDslService(dataFlowOperations,httpSource,splitterProcessor,groovyTransformProcessor,jdbcSink);
}
@Bean
public DataFlowOperations dataFlowOperations(){
URI dataFlowUri = URI.create("http://localhost:9393");
DataFlowOperations dataFlowOperations = new DataFlowTemplate(dataFlowUri);
dataFlowOperations.appRegistryOperations().register("http", ApplicationType.source,
"maven://org.springframework.cloud.stream.app:http-source-rabbit:2.1.4.RELEASE",
"maven://org.springframework.cloud.stream.app:http-source-rabbit:jar:metadata:2.1.4.RELEASE",
true);
dataFlowOperations.appRegistryOperations().register("splitter", ApplicationType.processor,
"maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:2.1.3.RELEASE",
"maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:jar:metadata:2.1.3.RELEASE",
true);
dataFlowOperations.appRegistryOperations().register("groovy-transform", ApplicationType.processor,
"maven://org.springframework.cloud.stream.app:groovy-transform-processor-rabbit:2.1.3.RELEASE",
"maven://org.springframework.cloud.stream.app:groovy-transform-processor-rabbit:jar:metadata:2.1.3.RELEASE",
true);
dataFlowOperations.appRegistryOperations().register("filter", ApplicationType.processor,
"maven://org.springframework.cloud.stream.app:filter-processor-rabbit:2.1.3.RELEASE",
"maven://org.springframework.cloud.stream.app:filter-processor-rabbit:jar:metadata:2.1.3.RELEASE",
true);
dataFlowOperations.appRegistryOperations().register("jdbc", ApplicationType.sink,
"maven://org.springframework.cloud.stream.app:jdbc-sink-rabbit:2.1.6.RELEASE",
"maven://org.springframework.cloud.stream.app:jdbc-sink-rabbit:jar:metadata:2.1.6.RELEASE",
true);
dataFlowOperations.appRegistryOperations().register("log", ApplicationType.sink,
"maven://org.springframework.cloud.stream.app:log-sink-rabbit:2.1.4.RELEASE",
"maven://org.springframework.cloud.stream.app:log-sink-rabbit:jar:metadata:2.1.4.RELEASE",
true);
return dataFlowOperations;
}
@Bean
public StreamApplication httpSource(){
return new StreamApplication("http")
.addProperty("port",9001);
}
@Bean
public StreamApplication splitterProcessor(){
return new StreamApplication("splitter")
.addProperty("expression","\"#jsonPath(payload,'$.movies')\"");
}
@Bean
public StreamApplication groovyTransformProcessor(){
return new StreamApplication("groovy-transform")
.addProperty("script","\"https://raw.githubusercontent.com/felipeg48/scdf-scripts/master/movie-transform.groovy\"");
}
@Bean
public StreamApplication jdbcSink(){
return new StreamApplication("jdbc")
.addProperty("columns","\"id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount\"")
.addProperty("table-name","\"movies\"")
.addProperty("username","\"root\"")
.addProperty("password","\"rootpw\"")
.addProperty("driver-class-name","\"org.mariadb.jdbc.Driver\"")
.addProperty("url","\"jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false\"");
}
}
Listing 9-10.src/main/java/com/apress/cloud/stream/movie/MovieDslConfiguration.java
清单 9-10 显示了 MovieDslConfiguration 类。花点时间分析一下,看看有什么不同。注意,在这堂课上,我用的是旧版本。您可以修复此问题并使用最新版本。你应该没事的。
在这个类中,您发现DataFlowOperations
只注册您使用的应用;这是另一种替代而不是全部。请注意,您在注册中使用了name
、ApplicationType
和maven
坐标。另外,查看创建源、处理器和接收器的StreamApplication
bean,以及定义传递给应用的参数的addProperty
方法的用法。
- 您可以运行项目并发送一些数据。
我知道这个项目可能只有一个,但我想把它和另一个分开,这样你就可以清楚地知道选择什么风格。
恭喜你!您使用流畅的风格创建了一个电影流管道 DSL。
Note
记住所有的代码都在 Apress 网站的ch09
文件夹中。
摘要
本章介绍了 Spring CloudStream 组件,并解释了如何部署流。我向您展示了创建流的不同方法,从命令行实用程序到使用 Java DSL 创建动态流。
下一章将讨论流处理,您将添加自己的 NATs 代理。您将看到 Spring Cloud Task,并了解它如何创建批处理。你可以认为下一章是这一章的延续。
十、使用 Spring CloudStream 定制流式应用
在前一章中,我向您展示了 Spring CloudStream 组件,以及它们如何协同工作来创建用于处理数据的流解决方案。我通过指向公开 REST API 的 Spring CloudStream 服务器,向您展示了如何使用简单的 cURL 命令创建流。我向您展示了数据流 shell 如何通过创建、部署和销毁流来工作。我讨论了如何通过使用 Java DSL 创建流定义来以编程方式创建动态解决方案。我说过,你可以在任何云平台上运行相同的流,比如 Kubernetes,并利用高可用性和负载平衡等特性。
本章使用自定义流式应用,并解释如何将它们插入流定义。您正在使用自定义的 NATs 活页夹。首先,我们来复习一些概念。
定制流式应用:快速回顾
流被定义为在没有交互或中断的情况下处理数据,这对于接近实时的用例非常有用,例如预测分析、信用卡欺诈检测、垃圾邮件、商业智能,如果您使用机器学习过程插入这些流,那么您就有了一个非常强大的解决方案。
Spring Cloud Stream 提供了一个解决方案,将流作为独立的应用,这些应用通常是事件驱动的,可以通过任何消息中间件连接起来。通过连接这些应用,您可以创建一个由流数据管道组成的运行时环境,它可以是线性的,也可以是非线性的,这取决于您的业务逻辑。有了 Spring Cloud Stream,你可以使用任何适合你的基础设施的中间件;你不需要学习特定的 API 来从一个应用向另一个应用发送或接收消息。你可以使用任何来自社区或者 Spring Cloud Stream 团队支持的绑定器,比如 RabbitMQ 和 Kafka。在本章中,您将使用前几章中的自定义代理。Spring CloudStream 及其组件充当了一个编排器,可以使用任何云平台基础设施创建、部署、更新和销毁复杂的流。
在本章中,您将创建一个自定义流并使用一个自定义绑定器,因此您需要记住创建自定义 Spring CloudStream 应用的一些要点。
-
您需要选择正在创建的流的类型:源、处理器、接收器或任务。(任务应用将在下一章讨论。)
-
您需要选择
spring-cloud-stream
依赖项。如果您使用的是 Spring Cloud Stream 团队支持的代理,您可以添加spring-cloud-stream-binder-rabbit
(用于 RabbitMQ)或spring-cloud-stream-binder-kafka-streams
(用于 Kafka 依赖)。 -
您可以选择不同的编程风格。您可以使用
@EnableBinding
(Source
、Processor
、Sink
、@StreamListener
(用于接收消息)、@SendTo
(用于回复/响应,通常是一个Processor
利用这一点),或者您可以进行函数式编程并使用java.util.function
Java 包中的Supplier
、Function
或Consumer
接口以及带有Mono
或Flux
接口的项目反应器。 -
你需要配置所有的流 app 输入和输出,也就是所谓的目的地;以及你需要的任何其他属性。
请记住,Spring Cloud Stream 团队创建了独立和开箱即用的流式应用,您可以使用它们来创建流数据管道解决方案。
现在,您已经回忆起了所有这些,是时候创建自定义流式应用,使用自定义 NATs 绑定器,并使用 Spring CloudStream 仪表板或数据流外壳来编排管道 DSL 了。
Spring CloudStream 中的自定义流式应用
本节中的所有内容都将 Docker Compose 技术用于本地环境,但是如果您已经在自己的 Kubernetes 集群中运行了 Spring CloudStream 组件,那么无论如何,您都可以使用它来创建、更新和部署您的流数据管道。
让我们定义最终的管道(见图 10-1 )。
图 10-1 说明了我们将要做的事情。当用 DSL 表达时,它看起来像下面这样。
图 10-1。
自定义流管道
movie-web | splitter | movie-imdb | movie-log
尽管这是一个简单的流管道,我想向您展示如何使用 GUI 或数据流外壳的自定义应用,以及如何使用自定义绑定器。如果你仔细看看 DSL, splitter app starter 结合了三个自定义流式应用。
电影网络应用:电影-来源
这个应用公开了一个 REST API,您可以在其中发送一组电影。我们先打开浏览器,进入 Spring Initializr ( https://start.spring.io
)网站。使用以下数据。
-
组:
com.apress.cloud.stream
-
神器:
movie-source
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,春网,龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩,并将项目导入到您喜欢的 IDE 中(参见图 10-2 )。
图 10-2。
Spring 初始化电影-来源
接下来,打开pom.xml
并添加以下依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<!-- WebJars -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
如果查看依赖项,首先使用 RabbitMQ 绑定器。此外,还包括一个 WebJar 来创建一个index.html
页面来发送电影,而不是任何 cURL 命令或另一个 REST API 客户端。我希望您看到命令行以外的解决方案。
接下来修改版本,去掉快照工作,所以如下。
<version>0.0.1</version>
这对部署很重要(我稍后会解释)。接下来,让我们创建模型。创建Movie
类(参见清单 10-1 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Movie {
private String id;
private String title;
private String actor;
private int year;
private String genre;
private int stars;
}
Listing 10-1.src/main/java/com/apress/cloud/stream/movie/Movie.java
您已经知道了Movie
类,所以让我们继续创建MovieRequest
类(参见清单 10-2 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MovieRequest {
String action;
Iterable<Movie> movies;
LocalDateTime created;
}
Listing 10-2.src/main/java/com/apress/cloud/stream/movie/MovieRequest.java
清单 10-2 显示了MovieRequest
类。如你所见,这很简单。通常,当您想要公开一个 API 时,作为一种最佳实践,您应该包装您的数据。当使用审计工具时,这有助于查看请求何时发生、谁做的等等。接下来,您需要一个MovieResponse
类(参见清单 10-3 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MovieResponse {
Integer code;
String message;
LocalDateTime responseTime;
}
Listing 10-3.src/main/java/com/apress/cloud/stream/movie/MovieResponse.java
清单 10-3 显示了MovieResponse
类。尽管这个类很简单,但您可以使用它来报告对您的业务逻辑有意义的特殊代码,并发送一条暴露任何问题的消息。接下来,让我们创建MovieController
类(参见清单 10-4 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Collection;
@Log4j2
@AllArgsConstructor
@RequestMapping("/v1/api")
@RestController
public class MovieController {
private StreamBridge streamBridge;
@PostMapping("/movies")
@ResponseStatus(HttpStatus.ACCEPTED)
public ResponseEntity<MovieResponse> toMovieBinding(@RequestBody MovieRequest movieRequest) {
assert movieRequest != null;
movieRequest.setCreated(LocalDateTime.now());
log.debug("Sending: {} ", movieRequest);
assert streamBridge != null;
streamBridge.send("movie-out-0", movieRequest);
return ResponseEntity
.accepted()
.body(new MovieResponse(HttpStatus.OK.value(),"Movies processed: " + ((Collection)movieRequest.getMovies()).size(), LocalDateTime.now()) );
}
}
Listing 10-4.src/main/java/com/apress/cloud/stream/movie/MovieController.java
清单 10-4 显示了MovieController
类。重要的部分是使用StreamBridge
类将数据发送到输出绑定。send
方法使用了movie-out-0
绑定。主 API 端点是/v1/api
,而MovieRequest
端点是/v1/api/movies
。
接下来,让我们创建index.html
和 JavaScript 来创建一个发送到/v1/api/movies
端点的主页(参见清单 10-5 和 10-6 )。
function getMovieRequest(){
return `{
"MovieRequest": {
"action": "create",
"movies": [
{
"id": "tt0133093",
"title": "The Matrix",
"actor": "Keanu Reeves",
"year": 1999,
"genre": "fiction",
"stars": 5
},
{
"id": "tt0209144",
"title": "Memento",
"actor": "Guy Pearce",
"year": 2000,
"genre": "drama",
"stars": 4
}
]
}
}
`;
}
$(function(){
$('#movieRequest').val(getMovieRequest());
$('#sendRequest').click(function (){
$.ajax
({
type: "POST",
url: '/v1/api/movies',
dataType: 'json',
async: false,
contentType: 'application/json',
data: $('#movieRequest').val(),
success: function (data) {
alert(data.MovieResponse.message);
}
})
});
});
Listing 10-6.src/main/resources/static/js/main.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="/webjars/jquery/3.5.1/jquery.min.js"></script>
<script src="/webjars/bootstrap/4.5.0/js/bootstrap.min.js"></script>
<link rel="stylesheet"
href="/webjars/bootstrap/4.5.0/css/bootstrap.min.css" />
<title>Title</title>
</head>
<body>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">Movie API</h1>
<p class="lead">This is a Movie API Stream App.</p>
</div>
</div>
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-6">
<h2>Movies</h2>
<p>You can send this JSON movie request, or modify it accordingly.</p>
<div class="form-group">
<textarea class="form-control" id="movieRequest" rows="15"></textarea>
</div>
<p><a class="btn btn-primary btn-lg" href="#" role="button" id="sendRequest">Send</a></p>
</div>
</div>
<hr>
</div>
<script src="js/main.js"></script>
</body>
</html>
Listing 10-5.src/main/resources/static/index.html
正如你从之前的文件中看到的,这些非常简单——没有什么复杂的,只是一个使用$.ajax
(来自 jQuery)的帖子。注意,您已经用 movies JSON 有效负载填充了文本区域。例如,这可能是一个 SPA(单页应用)。
接下来,我们打开application.properties
文件。添加清单 10-7 中的内容。
# Server
server.port=8080
# Jackson Root Properties
spring.jackson.serialization.wrap-root-value=true
spring.jackson.deserialization.unwrap-root-value=true
# Spring Cloud Stream
spring.cloud.stream.source=movie
spring.cloud.stream.bindings.movie-out-0.destination=movie
# Logging
logging.level.com.apress.cloud.stream.movie=DEBUG
Listing 10-7.src/main/resource/application.properties
清单 10-7 显示了application.properties
文件。注意,您添加了spring.jackson.*
属性来将MovieRequest
和MovieResponse
对象包装到一个 JSON 对象中。此外,要使用StreamBridge
类,您需要定义源的名称,在本例中是movie
。此外,有必要创建基于命名约定的绑定,命名约定是movie-out-0
(您在前面的章节中已经看到了)。
你可以通过连接到log-app-starter
或splitter-app-starter
来测试这个流式应用,看看它是如何工作的。
Note
在源代码中,ch10/app-starters
文件夹包含一个setup.sh
脚本,用于下载拆分器和日志应用启动器,并设置application.properties
文件来测试movie-source
项目。需要 RabbitMQ 来测试。
电影 IMDB 应用:电影处理器
这个应用接收 JSON 格式的电影。它使用电影的 ID 去第三方 API 服务( https://rapidapi.com
)。请记住,您需要登录(RapidAPI 是免费的)并使用 IMDB 服务。您正在使用免费服务( https://imdb8.p.rapidapi.com
)和/title/get-ratings
端点来获取评级。这个流式应用与您在前一章中创建的groovy-transform
脚本非常相似。这个流式应用使用 NATs 服务器和 RabbitMQ 绑定器。
打开浏览器,进入 Spring Initializr ( https://start.spring.io
)网站。使用以下数据。
-
组:
com.apress.cloud.stream
-
神器:
movie-processor
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩并将项目导入到您喜欢的 IDE 中(参见图 10-3 )。
图 10-3。
Spring Initializr 电影处理器
接下来,打开pom.xml
并添加以下依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<!-- NATs Server -->
<dependency>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-binder</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
您正在添加 NATs 绑定器依赖项和执行请求的httpclient
。(您可以使用 Spring Web 提供的RestClient
,但我想使用一种替代方法)。此外,您正在将configuration-processor
添加到您自己的属性中。
在同一个pom.xml
文件的 build/plugins 部分,添加以下插件。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-app-starter-metadata-maven-plugin</artifactId>
<version>2.0.0.RELEASE</version>
<executions>
<execution>
<id>aggregate-metadata</id>
<phase>compile</phase>
<goals>
<goal>aggregate-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
这将创建元数据 JAR,其中包含设置流所需的所有属性信息。接下来,修改版本并删除快照工作,这样它就
<version>0.0.1</version>
这对部署很重要。
接下来,您需要创建保存新信息的模型。您正在增强Movie
对象。创建Movie
和MovieImdb
类(参见清单 10-8 和 10-9 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MovieImdb {
Float rating;
Integer ratingCount;
}
Listing 10-9.src/main/java/com/apress/cloud/stream/movie/MovieImdb.java
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Movie {
private String id;
private String title;
private String actor;
private int year;
private String genre;
private int stars;
private MovieImdb imdb;
}
Listing 10-8.src/main/java/com/apress/cloud/stream/movie/Movie.java
清单 10-9 显示了MovieImdb
类。该类保存来自 IMDB 服务的信息。接下来,让我们创建MovieProperties
类,它保存关于 IMDB 服务的信息,比如主机和调用所需的一些头(参见清单 10-10 )。
package com.apress.cloud.stream.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie")
public class MovieProperties {
String apiServer;
String headerHost;
String headerKey;
}
Listing 10-10.src/main/java/com/apress/cloud/stream/movie/MovieProperties.java
接下来,让我们创建MovieStream
类(参见清单 10-11 )。
package com.apress.cloud.stream.movie;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
@Log4j2
@EnableConfigurationProperties(MovieProperties.class)
@Configuration
public class MovieStream {
private MovieProperties movieProperties;
private final CloseableHttpClient httpclient = HttpClients.createDefault();
private final HttpGet getRequest = new HttpGet();
public MovieStream(MovieProperties movieProperties) {
this.movieProperties = movieProperties;
getRequest.addHeader("Accept", "application/json");
getRequest.addHeader("x-rapidapi-host", movieProperties.getHeaderHost());
getRequest.addHeader("x-rapidapi-key", movieProperties.getHeaderKey());
getRequest.addHeader("Content-Type", "application/json");
}
@Bean
public Function<Flux<Movie>, Flux<Movie>> movieProcessor(ObjectMapper objectMapper) {
return movieFlux -> movieFlux.map(
movie -> {
try {
getRequest.setURI(new URI(movieProperties.getApiServer().replace("ID", movie.getId())));
HttpEntity entity = httpclient.execute(getRequest).getEntity();
movie.setImdb(objectMapper.readValue(EntityUtils.toString(entity, StandardCharsets.UTF_8), MovieImdb.class));
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
log.debug("About ot send: {}", movie);
return movie;
});
}
@Bean
public ObjectMapper objectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
return objectMapper;
}
}
Listing 10-11.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 10-11 显示了MovieStream
类。我们来分析一下这个类。请注意,您在movieProcessor method
中使用了Flux<Movie>
。因为它被声明为 Spring bean,所以它识别处理传入流的主要方法,并将数据发送到正确的绑定。您正在使用httpclient
实例来调用带有正确数据、电影 ID 和头(如主机和密钥)的服务。看看entity
的例子。您正在执行请求并让一个实体映射回MovieImdb
类。映射器忽略响应中缺少的属性来映射类。
接下来,让我们创建一个包含spring-configuration-metadata.json
文件的src/main/resources/META-INF
文件夹,
。这个文件有关于每个属性的有用信息,这有助于其他开发人员重用和配置这个流(参见清单 10-12 )。
{
"groups": [
{
"name": "movie",
"type": "com.apress.cloud.stream.movie.MovieProperties",
"sourceType": "com.apress.cloud.stream.movie.MovieProperties"
},
{
"name": "spring.nats",
"type": "com.apress.nats.NatsProperties",
"sourceType": "com.apress.nats.NatsProperties"
}
],
"properties": [
{
"name": "spring.nats.host",
"type": "java.lang.String",
"description": "This NATs Server host. Default to localhost.",
"sourceType": "com.apress.nats.NatsProperties",
"defaultValue": "localhost"
},
{
"name": "spring.nats.port",
"type": "java.lang.Integer",
"description": "This NATs Server port. Default to 4222.",
"sourceType": "com.apress.nats.NatsProperties",
"defaultValue": 4222
},
{
"name": "movie.api-server",
"type": "java.lang.String",
"description": "Default to: https://imdb8.p.rapidapi.com/title/get-ratings?tconst=ID. The ID will be replaced, so it's necessary",
"sourceType": "com.apress.cloud.stream.movie.MovieProperties",
"defaultValue": "https://imdb8.p.rapidapi.com/title/get-ratings?tconst=ID"
},
{
"name": "movie.header-host",
"type": "java.lang.String",
"description": "Default to: imdb8.p.rapidapi.com.",
"sourceType": "com.apress.cloud.stream.movie.MovieProperties",
"defaultValue": "imdb8.p.rapidapi.com
"
},
{
"name": "movie.header-key",
"type": "java.lang.String",
"description": "This header-key can be obtain in your https://rapidapi.com/ profile.",
"sourceType": "com.apress.cloud.stream.movie.MovieProperties"
}
],
"hints": []
}
Listing 10-12.src/main/resource/META-INF/spring-configuration-metadata.json
接下来,在同一个META-INF/
文件夹中,添加包含用@ConfigurationProperties
注释标记的类的spring-configuration-metadata-whitelist.properties
文件。这将生成元数据以获取关于流式应用的信息(参见清单 10-13 )。
configuration-properties.classes=\
com.apress.nats.NatsProperties,\
com.apress.cloud.stream.movie.MovieProperties
configuration-properties.names=movie.api-server,movie.header-host,movie.header-key,spring.nats.host,spring.nats.port
Listing 10-13.src/main/resource/META-INF/spring-configuration-metadata-whitelist.json
请注意,您正在添加我们的 NATs 属性,因为这些属性对于告知 NATs 代理在哪里是必需的。此外,您将正在使用的属性列入白名单。如果你需要类似server.port
的东西,这是必要的。对于这个示例,您不需要它们,有了类就足够了,但是为了说明这一点,您可以包含一些其他属性,这些属性已经作为整个 stream 应用的一部分包含在内。
接下来,打开application.properties
文件并添加清单 10-14 中的内容。
# Server
server.port=8082
# IMDB API
movie.api-server=https://imdb8.p.rapidapi.com/title/get-ratings?tconst=ID
movie.header-host=imdb8.p.rapidapi.com
movie.header-key=YOUR-KEY
# Binders
spring.cloud.stream.bindings.movieProcessor-in-0.binder=rabbit
spring.cloud.stream.bindings.movieProcessor-out-0.binder=nats
# Bindings - Nats - RabbitMQ
spring.cloud.stream.bindings.movieProcessor-in-0.destination=imdb
spring.cloud.stream.bindings.movieProcessor-out-0.destination=log
# Logging
logging.level.com.apress.cloud.stream.movie=DEBUG
Listing 10-14.src/main/resource/META-INF/application.properties
清单 10-14 显示了application.properties
。注意,这个文件有movie.*
属性。有些是违约。还要查看绑定器(用于传入的流;代理使用它们进行流式传输)并注意您需要如何输入输入和输出目的地。记住采用方法名(movieProcessor
)并添加-in-0
和-out-0
终止的命名约定。
电影日志应用:电影接收器
这个流式应用记录带有 IMDB 评级的增强的Movie
对象。一个非常简单的应用。打开浏览器,进入 Spring Initializr 网站( https://start.spring.io
)。使用以下数据。
-
组:
com.apress.cloud.stream
-
神器:
movie-sink
-
包名:
com.apress.cloud.stream.movie
-
依赖:CloudStream,龙目岛
点击生成按钮下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 10-4 )。
图 10-4。
Spring 初始化电影池
打开pom.xml
并添加以下依赖项。
<!-- NATs Server -->
<dependency>
<groupId>com.apress.nats</groupId>
<artifactId>nats-messaging-binder</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
这个流式应用使用 NATs 绑定器,所以这是你唯一需要的依赖。接下来,修改版本并删除快照工作,这样它就
<version>0.0.1</version>
接下来,复制/粘贴先前的Movie
和MovieImdb
类(参见清单 10-8 和 10-9 )。
接下来,创建MovieStream
类(参见清单 10-15 )。
package com.apress.cloud.stream.movie;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Flux;
import java.util.function.Consumer;
@Log4j2
@Configuration
public class MovieStream {
@Bean
public Consumer<Flux<Movie>> log() {
return movie -> movie.subscribe(actual -> log.info(actual));
}
}
Listing 10-15.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 10-15 显示了MovieStream
类。如你所见,这很简单;您只需订阅和接收Movie
对象。
接下来,打开application.properties
文件,添加清单 10-16 中的内容。
# Server
server.port=8083
# Bindings
spring.cloud.stream.bindings.log-in-0.destination=log
Listing 10-16.src/main/resources/application.properties
打包和部署流式应用
现在,你已经准备好使用 Spring CloudStream 来创建一个流管道,但是首先,你需要决定如何打包每一个流 app。最后,您可以拥有优步-JARs 并独立运行它们。但是您需要一些东西来协调管道,而不是手动完成所有事情。对于所有这些,您需要选择您希望 Spring CloudStream 服务器如何使用您的流式应用。Spring CloudStream 服务器可以通过提供 Maven 或 Docker 坐标以及本地 JAR 来使用流式应用(但这应该仅用于开发)。
您在这里使用的是 Maven 坐标,为此,您需要打包和部署您的流式应用。如果您是一名经验丰富的开发人员,并且已经知道如何将 Maven 工件打包并部署到 Maven 存储库中,那么您可以跳过这一节。
要打包您的应用,进入每个项目的根目录并执行以下命令。
./mvnw clean -DskipTests package
该命令在target/
文件夹中生成您需要的文件。如果你看一下movie-processor
项目的target/
文件夹,你会发现movie-processor-0.0.1-metadata.jar
中有spring-configuration-metadata.*
文件,当你需要关于这个应用的信息时,这些文件很有用。
接下来,有必要将这些工件部署到 Maven 存储库中。让我告诉你,创建 Maven repo 有很多解决方案,比如使用 Docker images 或 Apache Archiva、Nexus 或 JFrog。您还可以将 Git 服务器用作 Maven repo。在本例中,我使用了一个名为 Bintray ( https://bintray.com
)的开源 Maven 回购。可以免费报名(见图 10-5 )。
图 10-5。
垃圾箱
设置帐户后,您需要创建一个存储库和一个包来保存您的流式应用。我创建了scdf
存储库,包名是movie-streams
。我最终的 Maven 回购网址是 https://bintray.com/felipeg48/scdf
(见图 10-6 )。
图 10-6。
https://bintray.com/felipeg48/scdf
一旦您完成了 Maven repo 和包名的设置,您需要向pom.xml
文件添加一些凭证和依赖项,以便您可以进行部署。首先,在 https://bintray.com/profile/edit
进入你的个人资料。从左侧菜单中选择 API 键。请将其复制到安全的地方,因为您稍后会用到它(参见图 10-7 )。
图 10-7。
https://bintray.com/profile/edit
在您的主目录中查找(~/.m2
文件夹。打开或创建~/.m2/settings.xml
并添加以下内容。
<?xml version='1.0' encoding='UTF-8'?>
<settings xsi:schemaLocation='http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd'
xmlns:='http://maven.apache.org/SETTINGS/1.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
<servers>
<server>
<id>bintray-USERNAME-scdf</id>
<username>YOUR_USERNAME</username>
<password>YOUR_KEY</password>
</server>
</servers>
</settings>
<id>
标签应该与下面的配置相同。接下来打开每个项目的pom.xml
文件,添加以下内容(这是我的个人信息)。
<distributionManagement>
<repository>
<id>bintray-felipeg48-scdf</id>
<name>felipeg48-scdf</name>
<url>https://api.bintray.com/maven/felipeg48/scdf/movie-streams/;publish=1</url>
</repository>
</distributionManagement>
<id>
必须与settings.xml
中设置的相同;<name>
可以是你想要的任何东西。知道 URL 是这种格式的 API 是很重要的。
https://api.bintray.com/maven/<username>/<repository>/<package-name>/;publish=1
此外,它必须在末尾包含用户名、存储库和包名。我的情况是这样的。
https://api.bintray.com/maven/felipeg48/scdf/movie-streams/;publish=1
如果你对此有任何问题,垃圾箱的网页上有一个设置我!按钮(见图 10-6 )。点击它可以找到更多关于设置 Maven 上传的信息。
现在,您可以通过在每个项目的根目录中执行以下操作来上传应用。
./mvnw -DskipTests deploy
该命令上传所有文件。您可以通过查看您的回购/包装来验证这一点(参见图 10-8 )。
图 10-8。
https://bintray.com/<username>/scdf/movie-streams#files/com/apress/cloud/stream
你可以检查每个文件夹,看看你的应用在那里。在movie-processor
应用中,你可以看到元数据罐。现在,您可以在 Spring CloudStream 中使用它们了。要知道这个存储库是个人的,并不对社区开放,所以你需要告诉 Spring CloudStream 服务器如何找到它。您将在下一节看到这一点。
注册流式应用
现在您已经做好了一切准备,是时候在 Spring CloudStream 服务器中使用您的应用了。您必须添加 Maven repo 来告诉 Spring CloudStream 服务器在哪里可以找到您的应用。使用以下属性指向您的 Maven 存储库。
maven.remote-repositories.repo1.url=https://dl.bintray.com/felipeg48/scdf
这个属性必须在 Spring CloudStream 服务器启动之前设置。如果您正在使用 Kubernetes,您可以将该属性添加到 YAML 文件的env
部分,在那里设置容器。
...
env:
- name: maven.remote-repositories.repo1.url
- value: https://dl.bintray.com/felipeg48/scdf
...
我从 Bintray 主页的右上角复制了这个 URL(见图 10-6 )。如果使用 Docker Compose,可以在 environment 部分添加相同的环境变量。
...
environment:
- maven.remote-repositories.repo1.url=https://dl.bintray.com/felipeg48/scdf
...
如果您的公司使用私有 Maven repo,并且需要用户名和密码,请添加以下属性。
maven.remote-repositories.repo1.auth.username=<your-username>
maven.remote-repositories.repo1.auth.password=<your-password>
如果您有多个 Maven 回购,请将repo1
更改为repo2
..repoN
。
...
maven.remote-repositories.repo1.url=...
maven.remote-repositories.repo2.url=..
maven.remote-repositories.repo3.url=..
maven.remote-repositories.repo2.auth.username=...
...
现在,您可以重启/启动您的服务器。注册应用有几个选项。
-
您可以像在前面章节中一样使用 cURL 命令进行注册。
-
您可以使用 Spring CloudStream shell 来注册它们。
-
可以使用 Spring CloudStream 仪表盘。
-
您可以通过编程方式注册应用。
我将向您展示如何使用 Spring Data 流外壳或仪表板来实现这一点。首先,确保您的 Spring CloudStream 组件已经启动并运行。请记住,Spring Data 流服务器必须从指向您的 Maven repo 的 Maven repo 环境变量开始。注册应用时,了解类型(源、处理器或接收器)、Spring Data 流服务器识别的应用名称以及 Maven 坐标(如下表所示)非常重要。
maven://<groupId>:<artifactId>:<version>
如果您创建了spring-configuration-metadata.json
属性,那么您需要以下面的形式注册您的元数据工件。
maven://<group>:artifactId>:jar:metadata:<version>
使用 Spring CloudStream 外壳来注册自定义应用
如果您想使用 Spring CloudStream shell,请确保您的服务器正在运行,然后启动您的 shell。您可以通过多种方式进行连接。如果你有一个优步罐,你可以指向 Spring Data 流服务器,就是这样。
java -jar spring-cloud-dataflow-shell-VERSION.RELEASE.jar \
--dataflow.uri=http://my-server:9393 \
--dataflow.username=my_username \
--dataflow.password=my_password \
--skip-ssl-validation=true
如果你已经在了,你看到了“server-unknown
”,你可以用dataflow
关键词连接。
server-unknown:>dataflow config server --uri http://my-server:9393 --username=my_username
接下来,用下面的 shell 命令注册应用。
dataflow:>app register --name movie-imdb --type source --uri maven://com.apress.cloud.stream:movie-source:0.0.1
dataflow:>app register --name movie-imdb --type processor --uri maven://com.apress.cloud.stream:movie-processor:0.0.1 --metadata-uri maven://com.apress.cloud.stream:movie-processor:jar:metadata:0.0.1
dataflow:>app register --name movie-log --type sink --uri maven://com.apress.cloud.stream:movie-sink:0.0.1
执行这个 shell 命令后,列出如下应用。
dataflow:>app list
您应该会看到您的自定义流被列出。对于您正在创建的管道 DSL,您需要 splitter 应用,因此您可以按如下方式注册。
dataflow:>app register --name splitter --type processor --uri maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:2.1.2.RELEASE --metadata-uri maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:jar:metadata:2.1.2.RELEASE
也许你想知道是否有更好的注册方式——不需要一个接一个地添加你的应用。在上一章中,您看到了通过使用 https://dataflow.spring.io/rabbitmq-maven-latest
URI 可以使用批量选项。如果您下载该文件,您会看到以下格式的坐标。
<type>.<name>[.metadata]=maven://<groupId>:<artifactId>[:jar:metadata]:<version>
Note
请记住,您可以使用 Docker 坐标。在这种情况下,您需要从您的自定义流式应用创建一个图像,并将其推送到注册表,可以是像hub.docker.com
这样的公共注册表,也可以是您的私有注册表。坐标有点像docker://<your-docker-id>/<your-image>:<version>
。
如果您想要了解您的应用的信息,您可以执行以下命令。
dataflow:>app info --name movie-imdb --type processor
使用这个命令,您应该可以看到所有的movie.*
和spring.nats.*
属性及其定义。
使用仪表板注册自定义应用
在这一节中,我将通过一个简单的过程向您展示如何使用仪表板。在 h ttp://<your-server>[:9393]/dashboard
打开您的仪表板。转到应用选项卡,点击 +添加应用按钮。选择第三个选项,批量导入申请。在作为属性的应用字段中,复制并粘贴以下内容。
source.movie-web=maven://com.apress.cloud.stream:movie-source:0.0.1
processor.movie-imdb=maven://com.apress.cloud.stream:movie-processor:0.0.1
processor.movie-imdb.metadata=maven://com.apress.cloud.stream:movie-processor:jar:metadata:0.0.1
processor.splitter=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:2.1.2.RELEASE
processor.splitter.metadata=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:jar:metadata:2.1.2.RELEASE
sink.movie-log=maven://com.apress.cloud.stream:movie-sink:0.0.1
请注意,splitter 应用已经包含在内。分析命名约定。您可以拥有一个外部文件并将其导入(参见图 10-9 )。
图 10-9。
批量导入申请
点击导入应用按钮。您应该会看到列出的应用(参见图 10-10 )。
图 10-10。
应用
现在您已经准备好创建和部署您的流管道了。
创建和部署自定义流
您正在使用这里的仪表板创建管道 DSL,但是也欢迎您在数据流 shell 中进行同样的操作。转到您的仪表板,单击 Stream(在左侧窗格中),然后单击 + Create stream(s) 按钮。在文本区添加 DSL(参见图 10-11 )。
图 10-11。
创建一个流
请注意,您的应用列在左侧窗格中,这意味着它们可以被拖放并连接到您的解决方案中的任何其他流式应用。您可能想知道您是否正在使用某些属性。是的,您是,但是您使用了不同的方法,以便不扰乱流管道。
接下来,单击创建流按钮。将其命名为movie-stream
并点击创建流按钮。这将带您进入流列表页面(参见图 10-12 )。
图 10-12。
流
接下来,单击 Play 按钮(>),这将打开一个部署和属性页面,您可以在其中设置每个应用的功能属性。在此页面的顶部,单击“自由文本”选项卡,然后复制以下内容。
app.movie-web.server.port=8081
app.movie-web.spring.cloud.stream.bindings.output.destination=movie
app.splitter.expression=#jsonPath(payload,'$.MovieRequest.movies')
app.splitter.spring.cloud.stream.bindings.input.destination=movie
app.splitter.spring.cloud.stream.bindings.output.destination=imdb
app.movie-imdb.spring.cloud.stream.bindings.input.binder=rabbit
app.movie-imdb.spring.cloud.stream.bindings.output.binder=nats
app.movie-imdb.spring.cloud.stream.bindings.input.destination=imdb
app.movie-imdb.movie.header-key=YOUR-KEY
app.movie-imdb.spring.nats.host=nats
app.movie-imdb.spring.cloud.stream.bindings.output.destination=log
app.movie-log.spring.cloud.stream.bindings.input.destination=log
app.movie-log.spring.nats.host=nats
在继续之前,请分析属性。请注意,您正在为拆分器应用添加表达式。您正在为 NATs 服务器设置主机,并为每个应用命名目标属性。花时间在这里,直到它对你有意义。请注意,您至少需要 IMDB 服务 API 的密钥。您可以相应地进行更改(参见图 10-13 )。
图 10-13。
应用属性
接下来,您可以点击部署流按钮。如果一切顺利,您的所有应用都应该已经部署好了。你可以打开浏览器,进入你的电影网络应用。如果您使用 Kubernetes 部署它,您可以将movie-web-xxx
pod 暴露为LoadBalancer
并访问它(参见图 10-14 )。
图 10-14。
电影 web 应用
现在您已经准备好发送MovieRequest
JSON 对象了。按下 Send 按钮,您应该会收到一条消息,说明已经处理了两部电影。如果您查看 movie-log 应用流日志,您应该会看到带有评级的增强的Movie
对象。
Movie(id=tt0133093, title=The Matrix, actor=Keanu Reeves, year=1999, genre=fiction, stars=5, imdb=MovieImdb(rating=8.7, ratingCount=1620794))
Movie(id=tt0209144, title=Memento, actor=Guy Pearce, year=2000, genre=drama, stars=4, imdb=MovieImdb(rating=8.4, ratingCount=1090922))
恭喜您,您已经使用 RabbitMQ 和您的自定义 NATs 绑定器创建了自定义流式应用并部署了流管道。
Note
所有的源代码都在ch10
文件夹里。大多数子文件夹都有自述文件和脚本,便于设置。
摘要
在本章中,我向您展示了如何使用自定义流式应用和自定义绑定器来创建流数据管道。我向您展示了如何使用 Maven 坐标在 Spring CloudStream 服务器中轻松访问。此外,您还了解了如何使用数据流外壳和仪表板。
一旦你熟悉了这些场景,你会发现 Spring CloudStream 很容易使用。请记住,您可以使用 Docker Compose 的本地开发,也可以使用更健壮的解决方案,如 Kubernetes 这样的云基础设施,它可以提供高可用性和其他很酷的云功能。
下一章将介绍 Spring Cloud 任务以及使用 Spring Batch 处理和转换由流触发的大量数据。
十一、使用 Spring CloudStream 的任务和批处理应用
在前一章中,我向您展示了如何在 Spring CloudStream 中使用多个绑定器创建和部署定制流。本章继续介绍更多功能,包括创建流 DSL 应用、运行和/或触发任务和批处理等。在前面的章节中,您看到了公司如何需要运行夜间流程——处理数据并通过增强数据内容或进行转换以及应用一些过滤来修改数据的有限作业。
SpringCloud 任务初级读本
Spring Cloud Task 是一种技术,它允许你创建有限的微服务,跟踪某件事情何时开始,何时失败,何时结束;换句话说,Spring Cloud Task 会跟踪应用执行过程中发生的任何事件。
为什么在云环境中任务是必要的,因为在云环境中,您总是有流程在运行?今天,每秒钟有数百万个请求,我们希望我们的服务是可靠的和容错的。但有时我们需要做一些不应该影响应用性能的繁重工作。这就是为什么批处理应用允许我们创建可以在云中运行的细粒度应用,并通过加载和处理数据来处理繁重的工作。
Spring Cloud Task 是同类产品中第一个这样做的,并且具有跟踪正在发生的事情的优势。以下是 Spring Cloud Task 的一些特性。
-
它使用内存中的数据库来跟踪作业执行,但是您可以使用任何其他数据库引擎,包括 DB2、Oracle、MySQL、Postgres、SQL Server、HSQLDB 和 H2。默认情况下,当应用启动时,会创建一个任务存储库,通过创建模式并插入执行时发生的事件来跟踪任务,可以使用嵌入式数据库,如 H2、HSQL 或 Derby,也可以通过配置
spring.datasource.*
属性来使用任何 SQL 驱动程序。这提供了成功或不成功的作业执行的记录。Spring Cloud Task 使用具有以下属性的TaskExecution
类对这些信息进行建模。-
taskName
是任务的名称。 -
startTime
是任务开始的时间,由SmartLifecyle
调用发出。 -
endTime
是任务完成的时间,由ApplicationReadyEvent
发布。 -
executionId
是任务运行的唯一 ID。 -
exitCode
是由ExitCodeExceptionMapper
实现生成的代码。如果失败,则发出值为 1 的ApplicationFailedEvent
;否则为 0。 -
exitMessage
退出时是否有可用的信息。它设置在TaskExecutionListener
中。 -
errorMessage
是任务结束时引起的异常(从值为 1 的ApplicationFailedEvent
发出)。 -
arguments
是传入可执行应用的参数。
-
-
它基于 Spring Boot,是一种固执己见的技术,其默认值可以使用
DefaultTaskConfigurer
或SimpleTaskConfiguration
类来覆盖。Spring Cloud Task 使用一个数据源来存储所有的任务执行/事件,并且它使用一些默认的模式和命名约定,如果需要的话可以被覆盖。它生成的表以TASK_
、TASK_EXECUTION
和TASK_EXECUTION_PARAMS
为前缀,使用spring.cloud.task.table-prefix
属性可以很容易地覆盖它们。如果不想初始化任何表,可以通过将spring.cloud.task.initialize-enabled
属性设置为false
来覆盖它。 -
它包含
TaskExecutionListener
,为任务生命周期中的特定事件注册监听器。你只需要实现TaskExecutionListener
接口;以下事件会通知您的实现。-
onTaskStartup
发生在保存任何关于TaskExecution
的信息之前。 -
onTaskEnd
发生在更新TaskExecution
条目之前。 -
onTaskFailed
发生在onTaskEnd
被调用之前。或者您可以在任何您想要处理的方法上使用专门的
@BeforeTask
、@AfterTask
和@FailedTask
注释,通过接受TaskExecution
作为参数来获取信息。
-
简单任务演示
要查看 Spring Cloud 任务的运行情况,让我们创建一个简单的任务。打开你的浏览器,指向 https://start.spring.io
的春初名。使用以下元数据。
-
组:
com.apress.cloud.task
-
神器:
task-demo
-
包名:
com.apress.cloud.task
-
依赖项:任务、Lombok、H2 和 MySQL
点击生成按钮下载一个 ZIP 文件。你可以解压并导入到你喜欢的 IDE 中(见图 11-1 )。
图 11-1。
Spring Initializr 任务-演示
spring-cloud-starter-task
依赖关系在pom.xml
文件中。Spring 任务云依赖于数据库。这就是为什么你使用 H2 或 MySQL 的依赖。
接下来,创建TaskDemoConfiguration
类(参见清单 11-1 )。
package com.apress.cloud.task;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.task.configuration.EnableTask;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Log4j2
@EnableTask
@Configuration
public class TaskDemoConfiguration {
private final String MOVIES_TABLE_SQL = "CREATE TABLE IF NOT EXISTS movies(" +
" id varchar(10) primary key," +
" title varchar(200)," +
" actor varchar(200)," +
" year int," +
" genre varchar(25)," +
" stars int," +
" rating decimal(2,1)," +
" ratingcount int);";
private final String MOVIES_INSERT_SQL_1 = "insert into movies (id,title,actor,year,genre,stars,rating,ratingcount) " +
"values ('tt0133093','The Matrix','Keanu Reeves',1999,'fiction',5,8.7,1605968);";
private final String MOVIES_INSERT_SQL_2 = "insert into movies (id,title,actor,year,genre,stars,rating,ratingcount) " +
"values ('tt0209144','Memento','Guy Pearce',2000,'drama',4,8.4,1090922);";
private final String MOVIES_INSERT_SQL_ERROR = "insert into movies (year,genre,stars,rating,ratingcount) " +
"values ('tt0209144','Memento','Guy Pearce',2000,'drama',4,8.4,1090922);";
@Bean
public CommandLineRunner process(DataSource dataSource){
return args -> {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.execute(MOVIES_TABLE_SQL);
jdbcTemplate.execute(MOVIES_INSERT_SQL_1);
jdbcTemplate.execute(MOVIES_INSERT_SQL_2);
//jdbcTemplate.execute(MOVIES_INSERT_SQL_ERROR);
};
}
@Bean
public TaskDemoListener taskDemoListener(){
return new TaskDemoListener();
}
}
Listing 11-1.src/main/java/com/apress/cloud/task/TaskDemoConfiguration.java
清单 11-1 显示了TaskDemoConfiguration
类。如果您仔细观察,您使用的唯一新关键字是@EnableTas
k 注释。这个注释触发所有的任务生命周期。这是一个简单的 SQL 创建和插入。我注释掉了jdbcTemplate
的最后一行,因为我引入了一个 SQL 语法错误,您正在查看该事件。类的结尾声明了一个taskDemoListener
bean。
接下来,创建TaskDemoListener
类(参见清单 11-2 )。
package com.apress.cloud.task;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.task.listener.annotation.AfterTask;
import org.springframework.cloud.task.listener.annotation.BeforeTask;
import org.springframework.cloud.task.listener.annotation.FailedTask;
import org.springframework.cloud.task.repository.TaskExecution;
@Log4j2
public class TaskDemoListener {
@BeforeTask
public void beforeTask(TaskExecution taskExecution) {
log.debug("[@BeforeTask] - {}", taskExecution);
}
@AfterTask
public void afterTask(TaskExecution taskExecution) {
log.debug("[@AfterTask] - {}", taskExecution);
}
@FailedTask
public void failedTask(TaskExecution taskExecution, Throwable throwable) {
log.debug("[@FailedTask] - {}", taskExecution);
log.error("[@FailedTask] - {}", throwable);
}
}
Listing 11-2.src/main/java/com/apress/cloud/task/TaskDemoListener.java
清单 11-2 显示了TaskDemoListener
类。它使用了@BeforeTask
、@AfterTask,
和@FailedTask
注释。唯一要做的就是声明它并使这个类成为一个 Spring bean,其余的都是通过 Spring Cloud Task 来完成的。现在你与这些事件联系在一起了。
Note
你可以在ch11/tasks
文件夹中找到源代码。我还添加了TaskDemoListener
类来实现TaskExecutionListener
,这是监听 Spring Cloud 任务事件的替代方法。
接下来,打开您的application.properties
,添加清单 11-3 中的内容。
# Application Name
spring.application.name=task-demo
# Logging Level
logging.level.org.springframework.cloud.task=DEBUG
logging.level.com.apress.cloud.task=DEBUG
Listing 11-3.src/main/resources/application.properties
要知道你需要声明日志级别并添加spring.application.name
属性,这样你就可以在运行程序时获取信息。接下来,如果您运行它,您应该会看到下面的输出。
...
DEBUG - [main] ... : Initializing task schema for h2 database
DEBUG - [main] ... : Creating: TaskExecution{executionId=0, parentExecutionId=null, exitCode=null, taskName='task-demo', startTime=Mon Jul 13 20:05:02 EDT 2020, endTime=null, exitMessage="null", externalExecutionId="null", errorMessage="null", arguments=[]}
DEBUG - [main] ... : [@BeforeTask] - TaskExecution{executionId=1, parentExecutionId=null, exitCode=null, taskName='task-demo' ...
INFO - [main] ... : Started TaskDemoApplication in 1.095 seconds (JVM running for 1.686)
DEBUG - [main] ... : [@AfterTask] - TaskExecution{executionId=1, parentExecutionId=null, exitCode=0, taskName='task-demo', ...
DEBUG - [main] ... : Updating: TaskExecution with executionId=1 with the following {exitCode=0, endTime=Mon Jul 13 20:05:02 EDT 2020, exitMessage="null", errorMessage="null"}
...
这显示了开始时间的时间戳、完成时间以及正在监听的事件,例如任务执行前和任务执行后。它遵循云任务生命周期。您可以取消对 SQL 错误的注释。您还应该看到未能执行的任务事件。
正如你所看到的,Spring Cloud Task 非常容易应用于任何微服务,可以执行从简单工作到大量工作负载的任何事情。接下来,我们来看看如何将 Spring Cloud Task 与 Spring Cloud Stream 进行整合。
SpringCloudStream 整合
在 Spring Cloud Stream 中集成 Spring Cloud Task 的功能有多种方式。您可以使用接收器侦听任何任务事件(与 TaskDemo 项目一样),如任务前、任务后和任务失败事件。或者您可以创建一个流管道 DSL 并启动一个任务。启动一个任务需要使用一个定制的接收器,该接收器使用@EnableTaskLauncher
来运行接收器任务启动器,例如task-launcher-local
和task-launcher-dataflow
。那么,让我们回顾一下这些选项。
Spring CloudStream 中的任务事件
Spring Cloud Task 在任务处理过程中发出事件,它还可以通过使用预定义的名为task-events
的通道/目的地将事件发送到流中。你唯一需要做的就是添加活页夹,剩下的由 Spring Cloud Task 来完成(见图 11-2 )。
图 11-2。
任务事件
以下步骤演示了这一点。
-
启动并运行 RabbitMQ。你把它作为一个活页夹,可以用 Docker 运行它。
docker run -d --rm --name rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.8.3-management-alpine
-
从
https://repo.spring.io/libs-release/org/springframework/cloud/stream/app/log-sink-rabbit/
下载log-sink-rabbit
app-starter JAR。 -
用以下内容创建一个
application.properties
文件。# Server server.port=8083 # Spring Cloud Stream spring.cloud.stream.bindings.input.destination=task-events
-
运行它
java -jar log-sink-rabbit-2.1.3.RELEASE.jar
-
重新打开 TaskDemo 项目并添加以下依赖项。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> <scope>compile</scope> </dependency>
-
运行 TaskDemo 项目,并注意 log-sink 应用启动程序中的日志。
-- log-sink : {"executionId":1,"parentExecutionId":null,"exitCode":null,"taskName":"task-demo","startTime":"2020-07-15T00:40:19.168+00:00","endTime":null,"exitMessage":null,"externalExecutionId":null,"errorMessage":null,"arguments":[]} -- log-sink : {"executionId":1,"parentExecutionId":null,"exitCode":0,"taskName":"task-demo","startTime":"2020-07-15T00:40:19.168+00:00","endTime":"2020-07-15T00:40:19.289+00:00","exitMessage":null,"externalExecutionId":null,"errorMessage":null,"arguments":[]}
通过将 binder ( spring-cloud-starter-stream-rabbit
)依赖项添加到 TaskDemo 项目中的 pom.xml 文件,Spring Cloud Task 创建了task-events
目的地,并通过该通道/目的地发送所有事件。
在 Spring CloudStream 中启动任务
您可以通过在 Spring CloudStream 中注册您的任务应用或通过具有任务启动器的接收器(自定义接收器或任务启动器数据流接收器应用启动器)来启动任务。在本节中,您将学习如何注册任务并通过仪表板启动它。
图像到收存箱任务
让我们创建一个简单的任务,假设我们需要连接到一个已经有一个电影艺术 URL 的数据库。你需要下载一张图片并发送到 Dropbox 文件夹。因为你用的是 Dropbox API,去 www.dropbox.com/developers/
。如果您没有帐户,您可以免费获得一个。你需要创建一个应用(我把我的命名为 movie-art)。您可以拥有文件夹或完全访问权限类型。有一个部分需要点击生成访问令牌。这是你上传图片到你想要的文件夹的凭证。当然,你可以创建一个文件夹来存放电影图片。我创建了一个IMDB/
文件夹。
我们先打开浏览器,去 Spring Initializr。使用以下数据。
-
组:
com.apress.cloud.task
-
神器:
image-to-dropbox
-
包名:
com.apress.cloud.task
-
依赖:MySQL 驱动,JDBC API,Lombok,Task
点击生成按钮下载一个 ZIP 文件。你可以解压并导入到你喜欢的 IDE 中(见图 11-3 )。
图 11-3。
Spring Initializr 图像到投件箱
这个项目需要部署到一个 Maven 存储库中,所以您要做的和前一章一样。我用的是 JFrog Bintray ( https://bintray.com
)开源方案。打开pom.xml
文件并添加以下依赖项。
...
<!-- DropBox -->
<dependency>
<groupId>com.dropbox.core</groupId>
<artifactId>dropbox-core-sdk</artifactId>
<version>3.1.4</version>
</dependency>
<!-- Apache Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
...
<build>
<plugins>
...
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-app-starter-metadata-maven-plugin</artifactId>
<version>2.0.0.RELEASE</version>
<executions>
<execution>
<id>aggregate-metadata</id>
<phase>compile</phase>
<goals>
<goal>aggregate-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
<distributionManagement>
<repository>
<id>bintray-felipeg48-scdf</id>
<name>felipeg48-scdf</name>
<url>https://api.bintray.com/maven/felipeg48/scdf/movie-tasks/;publish=1</url>
</repository>
</distributionManagement>
...
用 MariaDB 替换mysql
依赖项,因为您正在重用来自数据流服务器的依赖项。记住切换到您自己的分发管理标签帐户,并使用版本 0.0.1 和删除快照文本。
<version>0.0.1</version>
这对于将项目部署到 Maven 存储库中非常有用。
接下来,创建ImageToDropboxProperties
类,它保存来自 Dropbox 的密钥/令牌 API(参见清单 11-4 )。(刚刚为你的 Dropbox 应用描述生成的那个)。
package com.apress.cloud.task;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "dropbox")
public class ImageToDropboxProperties {
private String apiKey = null;
private String path = "/IMDB/";
private String localTmpFolder = "/tmp/";
}
Listing 11-4src/main/java/com/apress/cloud/task/ImageToDropboxProperties.java
清单 11-4 显示了用于 Dropbox 的属性,包括你上传图片的路径,localTempFolder
,你下载图片然后上传到 Dropbox 的路径。Token 是apiKey
的值。
接下来,创建ImageToDropbox
类(参见清单 11-5 )。
package com.apress.cloud.task;
import com.dropbox.core.DbxException;
import com.dropbox.core.DbxRequestConfig;
import com.dropbox.core.v2.DbxClientV2;
import com.dropbox.core.v2.files.FileMetadata;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@Log4j2
public class ImageToDropboxUtils {
private ImageToDropboxProperties imageToDropboxProperties;
private DbxClientV2 client;
public ImageToDropboxUtils(ImageToDropboxProperties imageToDropboxProperties){
this.imageToDropboxProperties = imageToDropboxProperties;
DbxRequestConfig config = DbxRequestConfig.newBuilder("dropbox/scdf-imdb").build();
this. client = new DbxClientV2(config, this.imageToDropboxProperties.getApiKey());
}
public void fromUrlToDropBox(String fromUrl, String filename) throws DbxException, IOException {
log.debug("Attempting to download: " + fromUrl);
FileUtils.copyURLToFile(new URL(fromUrl), new File(this.imageToDropboxProperties.getLocalTmpFolder() + filename), 10000, 10000);
InputStream in = new FileInputStream(this.imageToDropboxProperties.getLocalTmpFolder() + filename);
log.debug("Attempting to Save to Dropbox in: {}", this.imageToDropboxProperties.getPath() + filename);
client.files()
.uploadBuilder(this.imageToDropboxProperties.getPath() + filename)
.uploadAndFinish(in);
log.debug("Uploaded to Dropbox");
log.debug("Removing temporal file: {}", this.imageToDropboxProperties.getLocalTmpFolder() + filename);
FileUtils.deleteQuietly(new File(this.imageToDropboxProperties.getLocalTmpFolder() + filename));
}
}
Listing 11-5.src/main/java/com/apress/cloud/task/ImageToDropboxUtils.java
清单 11-5 展示了 utils 类。分析它,注意 Apache Commons 库用于从 URL 下载图像。Dropbox 路径用于上传图像。一个临时文件夹被用来放置一个名字。您正在使用电影的 ID 作为文件名。
接下来,创建ImageToDropboxConfiguration
类(参见清单 11-6 )。
package com.apress.cloud.task;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.task.configuration.EnableTask;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Log4j2
@EnableTask
@Configuration
@EnableConfigurationProperties({ImageToDropboxProperties.class})
public class ImageToDropboxConfiguration {
@Bean
public ImageToDropboxUtils imageToDropBoxUtils(ImageToDropboxProperties imageToDropboxProperties){
return new ImageToDropboxUtils(imageToDropboxProperties);
}
private final String MOVIES_TABLE_SQL = "CREATE TABLE IF NOT EXISTS art(" +
" id varchar(10) primary key," +
" url varchar(500));";
private final String MOVIES_INSERT_SQL_1 = "insert into art (id,url) " +
"values ('tt0133093','https://m.media-amazon.cimg/MV5BNzQzOTk3OTAtNDQ0Zi00ZTVkLWI0MTEtMDllZjNkYzNjNTc4L2ltYWdlXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg');";
private final String MOVIES_INSERT_SQL_2 = "insert into art (id,url) " +
"values ('tt0209144','https://m.media-amazon.cimg/MV5BZTcyNjk1MjgtOWI3Mi00YzQwLWI5MTktMzY4ZmI2NDAyNzYzXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg');";
private final String MOVIES_QUERY_SQL = "select url from art where id=?;";
private final String MATRIX_ART_ID = "tt0133093";
private final String MEMENTO_ART_ID = "tt0209144";
@Bean
public CommandLineRunner process(DataSource dataSource, ImageToDropboxUtils imageToDropBoxUtils){
return args -> {
log.debug("Connecting to: {} ", dataSource.getConnection().getMetaData().getURL());
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.execute(MOVIES_TABLE_SQL);
jdbcTemplate.execute(MOVIES_INSERT_SQL_1);
jdbcTemplate.execute(MOVIES_INSERT_SQL_2);
String url = null;
url = jdbcTemplate.queryForObject(
MOVIES_QUERY_SQL, new Object[]{MATRIX_ART_ID}, String.class);
log.debug("URL: {}", url);
imageToDropBoxUtils.fromUrlToDropBox(url,MATRIX_ART_ID + ".jpg");
url = jdbcTemplate.queryForObject(
MOVIES_QUERY_SQL, new Object[]{MEMENTO_ART_ID}, String.class);
log.debug("URL: {}", url);
imageToDropBoxUtils.fromUrlToDropBox(url,MEMENTO_ART_ID + ".jpg");
};
};
}
Listing 11-6.src/main/java/com/apress/cloud/task/ImageToDropboxConfiguration.java
清单 11-6 显示了任务配置。@EnableTask
被使用。这是欺骗,因为我们正在创建一个表并插入一个硬编码的 URL 来模仿使用数据库来获取电影艺术的 URL。
接下来,打开您的application.properties
文件并添加清单 11-7 中的内容。
# Application Name
spring.application.name=image-to-dropbox
# DropBox
dropbox.api-key=YOUR-KEY
dropbox.path=/IMDB/
# Logging Level
logging.level.org.springframework.cloud.task=DEBUG
logging.level.com.apress.cloud.task=DEBUG
# DataSource
spring.datasource.url=jdbc:mysql://localhost:3306/movies?useSSL=false&requireSSL=false
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=rootpw
Listing 11-7.src/main/resources/application.properties
这里您使用的是 MySQL 数据库引擎,所以您需要设置数据源。还要添加您的 Dropbox 令牌和上传图像的路径。日志级别可以看到关于任务和代码的一切。
您可以在本地运行它来测试这一点,但是要确保您已经启动并运行了 MySQL 引擎。您可以通过以下命令使用 Docker。
docker run -d --rm --name mysql \
-e MYSQL_DATABASE=movies \
-e MYSQL_USER=root \
-e MYSQL_ROOT_PASSWORD=rootpw \
-p 3306:3306 \
mysql:5.7.25
您正在创建一个movies
数据库,为连接传递用户名和密码,并暴露端口 3306。现在,您可以运行应用,它应该会显示以下输出。
...
-- : Initializing task schema for mysql database
-- : Creating: TaskExecution{executionId=0, parentExecutionId=null, exitCode=null, taskName='image-to-dropbox', startTime=Sun Jul 19 21:27:44 EDT 2020, endTime=null, exitMessage="null", externalExecutionId="null", errorMessage="null", arguments=[]}
-- : Started ImageToDropboxApplication in 1.062 seconds (JVM running for 1.475)
-- : Connecting to: jdbc:mysql://localhost:3306/movies?useSSL=false&requireSSL=false
-- : URL: https://m.media-amazon.cimg/MV5BNzQzOTk3OTAtNDQ0Zi00ZTVkLWI0MTEtMDllZjNkYzNjNTc4L2ltYWdlXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg
-- : Attempting to download: https://m.media-amazon.cimg/MV5BNzQzOTk3OTAtNDQ0Zi00ZTVkLWI0MTEtMDllZjNkYzNjNTc4L2ltYWdlXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg
-- : Attempting to Save to Dropbox in: /IMDB/tt0133093.jpg
-- : Uploaded to Dropbox
-- : Removing temporal file: /tmp/tt0133093.jpg
-- : URL: https://m.media-amazon.cimg/MV5BZTcyNjk1MjgtOWI3Mi00YzQwLWI5MTktMzY4ZmI2NDAyNzYzXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg
-- : Attempting to download: https://m.media-amazon.cimg/MV5BZTcyNjk1MjgtOWI3Mi00YzQwLWI5MTktMzY4ZmI2NDAyNzYzXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg
-- : Attempting to Save to Dropbox in: /IMDB/tt0209144.jpg
-- : Uploaded to Dropbox
-- : Removing temporal file: /tmp/tt0209144.jpg
-- : Updating: TaskExecution with executionId=1 with the following {exitCode=0, endTime=Sun Jul 19 21:27:53 EDT 2020, exitMessage="null", errorMessage="null"}
...
如果你检查你的 Dropbox 文件夹,你可以看到两个电影的图像。测试之后,您需要使用以下命令将其打包并部署到 Maven/Bintray 存储库中。
./mvnw -DskipTests clean package
./mvnw -DskipTests deploy
使用仪表板
在您部署了image-to-dropbox
工件之后,您需要启动并运行 Spring CloudStream 服务器。您可以使用本地 Docker Compose 或您的 Kubernetes 安装。您需要在用于 Spring CloudStream 服务器的 MySQL 实例中创建movies
数据库。另外,您需要记住一个主要的环境属性是 Maven repo。
maven.remote-repositories.repo1.url=https://dl.bintray.com/felipeg48/scdf
作为最佳实践,我建议使用环境变量来添加您的 Dropbox 令牌。如果您正在使用 Kubernetes,那么在运行您的 pods 时,您可以添加 ConfigMap 作为环境变量。您还需要添加密钥。
dropbox.api-key=YOUR-TOKEN
准备就绪后,转到您的控制面板,像注册应用一样注册任务。您正在注册类型为task
的应用。选择批量导入应用,并在应用中添加以下坐标作为属性文本区域(参见图 11-4 )。
task.image-to-dropbox=maven://com.apress.cloud.task:image-to-dropbox:0.0.1
task.image-to-dropbox.metadata=maven://com.apress.cloud.task:image-to-dropbox:jar:metadata:0.0.1
请注意,您使用的是以下格式。
task:<app-name>[.<metadata>]=maven://<group>:<artifact>[:jar:metadata]:version
类型就是任务。
图 11-4。
从属性应用/添加/导入
点击导入应用按钮。你应该看到你的任务现在被列出来了(见图 11-5 )。
图 11-5。
应用
接下来,在左窗格中,单击任务选项卡。这会将您带到任务页面。点击 + Create task(s) 按钮,这将带您进入熟悉的流 UI。您应该会看到左侧窗格中列出了“图像到收存箱”任务。可以拖拽,也可以在文本区输入任务 app 的名称,image-to-dropbox。如果拖放,需要将任务连接到开始和结束图标(见图 11-6 )。
图 11-6。
创建任务
接下来,单击 Create Task 按钮,这将打开一个对话框,您可以在其中命名任务。请将其设置为movie-task
。然后点击创建任务按钮(见图 11-7 )。
图 11-7。
确认任务创建
创建movie-task
之后,需要启动可用任务列表。要启动任务,请单击播放按钮(>)。将向您显示您需要的任何附加属性。在这种情况下,您必须添加以下属性作为参数。
--movie.datasource.url=jdbc:mysql://mysql:3306/movies?useSSL=false&requireSSL=false
如果你忘记添加 Dropbox 令牌,你也可以在这里添加(见图 11-8 )。
图 11-8。
启动任务电影-任务
--dropbox.api-key=YOUR-TOKEN
然后点击启动任务。之后,返回任务列表,看看是否完成了。您可以刷新页面(参见图 11-9 )。
图 11-9。
任务完成
单击执行选项卡查看状态,单击任务执行 Id 查看更多信息,包括任务日志。如果你正在使用 Kubernetes,一个 pod 被启动、执行和终止(见图 11-10 和 11-11 )。
图 11-11。
任务执行详细信息执行 ID: 1
图 11-10。
执行列表
请检查您的 Dropbox 文件夹。图像应该在那里(见图 11-12 )。
图 11-12。
dropbox
恭喜你。您已经在数据流中启动了您的任务!执行任务的另一种方式是使用流。您可以创建可以启动任务的自定义流。使用app-starters-task-launch-request-common
依赖项选择一个源或处理器,并以下面的格式发送一个 JSON 有效负载。
{
"name":"<task-name>",
"deploymentProps": {"key1":"val1","key2":"val2"},
"args":["--debug", "--foo", "bar"]
}
至少,你可以只使用名称。
{"name":"foo"}
该名称与已注册为应用的任务相关。您需要将这个有效负载发送到具有@EnableTaskLauncher
注释的接收器,就这样。你不需要做任何其他事情。这将自动获取 JSON 有效负载,并按名称启动任务。另一种方法是在源或处理器中使用TaskLaunchRequest
类来包装有效载荷,这样更容易将有效载荷发送到接收器。
而且没错,有一个启动任务的 App Starter Sink:Task Launcher 数据流 Sink App Starter。另外,您可以通过订阅task-events
目的地来监听所有的任务事件。
使用数据流外壳程序启动任务
要在数据流 shell 中启动任务,您必须首先像在应用中一样注册您的任务。
dataflow:> app register --name image-to-dropbox --type task --uri maven://com.apress.cloud.task:image-to-dropbox:0.0.1 --metadata-uri maven://com.apress.cloud.task:image-to-dropbox:jar:metadata:0.0.1
dataflow:> task create movie-task --definition "image-to-dropbox"
dataflow:> task list
在您注册并创建任务之后,您可以使用下面的代码来启动它。
dataflow:> task launch movie-task --arguments "--movie.datasource.url=jdbc:mysql://mysql:3306/movies?useSSL=false&requireSSL=false"
您可以使用以下内容来查看任务执行情况。
dataflow:> task execution list
看看可以应用于这些任务的其他命令。如你所见,它们非常简单。如果您想要销毁任务,请执行以下命令。
dataflow:> task destroy --name movie-task
Note
在这个任务中,您需要创建movies
数据库。记住,Spring Cloud Task 用任务执行的相关信息来初始化表。如果您已经有了这些信息,但是想要使用这些表,您可以通过设置以下属性来禁用初始化:spring.cloud.task.initialize-enabled=false
成批处理
先说批处理。如果你仔细看看 Spring Cloud Task,你会发现你只能执行任务和接收到的事件。如果任务在执行过程中失败,就没有办法从它停止的地方重新开始;您需要修复该问题,然后再次启动该任务。对于较简单的任务来说,这可能没问题,但是如果您有数百万条记录的繁重负载,您就不想从头开始。为此,您可以将 Spring Cloud Task 的功能与 Spring Batch 结合起来。Spring Cloud Task 可以是云环境中 Spring 批处理的包装器。您可以更好地控制作业和步骤,并执行您的业务逻辑。
电影批次
让我们创建一个与前一个任务非常相似的电影批处理。在这种情况下,它将是一个更动态的应用,因为你下载电影艺术,然后上传到 Dropbox 帐户;这意味着您需要传递 Dropbox 信息以及 URL 和电影艺术 ID。
首先打开浏览器,进入 Spring Initializr。使用以下信息。
-
组:
com.apress.cloud.batch
-
神器:
movie-batch
-
包名:
com.apress.cloud.batch
-
依赖项:Spring Batch,Task,Lombok,JDBC API,MySQL
点击生成按钮下载一个 ZIP 文件。你可以解压并导入到你喜欢的 IDE 中(见图 11-13 )。
图 11-13。
Spring Initializr 电影批处理
接下来,打开pom.xml
并添加以下依赖项和部分。
...
<!-- DropBox -->
<dependency>
<groupId>com.dropbox.core</groupId>
<artifactId>dropbox-core-sdk</artifactId>
<version>3.1.4</version>
</dependency>
<!-- Apache Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
...
...
<distributionManagement>
<repository>
<id>bintray-felipeg48-scdf</id>
<name>felipeg48-scdf</name>
<url>https://api.bintray.com/maven/felipeg48/scdf/movie-tasks/;publish=1</url>
</repository>
</distributionManagement>
看到您正在用 Maria DB 连接器替换 MySQL。您正在添加一个插件来生成元数据 jar 文件和分布标记,以便部署到 Bintray Maven 存储库。记得改成自己回购。不要忘记删除快照并使用版本 0.0.1。
<version>0.0.1</version>
接下来,让我们创建DropboxUtils
类(参见清单 11-8 )。
package com.apress.cloud.batch.dropbox;
import com.dropbox.core.DbxException;
import com.dropbox.core.DbxRequestConfig;
import com.dropbox.core.v2.DbxClientV2;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@Log4j2
@Component
public class DropboxUtils {
private DbxClientV2 client = null;
private DbxRequestConfig config = DbxRequestConfig.newBuilder("dropbox/scdf-imdb").build();
public void fromUrlToDropBox(String fromUrl, String filename, String dropboxToken, String dropboxPath, String tmpFolder) throws DbxException, IOException {
log.debug("Attempting to download: {}" , fromUrl);
this. client = new DbxClientV2(config, dropboxToken);
FileUtils.copyURLToFile(new URL(fromUrl), new File(tmpFolder + filename), 10000, 10000);
InputStream in = new FileInputStream(tmpFolder + filename);
log.debug("Attempting to Save to Dropbox in: {}", dropboxPath + filename);
client.files()
.uploadBuilder(dropboxPath + filename)
.uploadAndFinish(in);
log.debug("Uploaded to Dropbox");
log.debug("Removing temporal file: {}", tmpFolder + filename);
FileUtils.deleteQuietly(new File(tmpFolder + filename));
}
}
Listing 11-8.src/main/java/com/apress/cloud/batch/dropbox/DropboxUtils.java
清单 11-8 显示了DropboxUtils
类,它与前一个任务非常相似,但是这一次,您将会得到更多的参数:Dropbox 的令牌、路径以及下载电影艺术的tmp
文件夹。
接下来,让我们为批处理创建MovieBatchConfiguration
类(参见清单 11-9 )。
package com.apress.cloud.batch;
import com.apress.cloud.batch.dropbox.DropboxUtils;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.task.configuration.EnableTask;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Log4j2
@AllArgsConstructor
@EnableTask
@EnableBatchProcessing
@Configuration
public class MovieBatchConfiguration {
private JobBuilderFactory jobBuilderFactory;
private StepBuilderFactory stepBuilderFactory;
private DropboxUtils dropboxUtils;
@Bean
@StepScope
public Tasklet movieTasklet(
@Value("#{jobParameters['url']}") String url,
@Value("#{jobParameters['imdbId']}") String imdbId,
@Value("#{jobParameters['dropbox.token']}") String token,
@Value("#{jobParameters['dropbox.path']}") String path,
@Value("#{jobParameters['dropbox.local-tmp-folder']}") String tmp) {
return (stepContribution, chunkContext) -> {
log.debug("Using Image ID: {} and URL: {}", imdbId, url);
assert url!=null && imdbId!=null;
dropboxUtils.fromUrlToDropBox(
url,
imdbId + ".jpg",
token,
path,
tmp);
return RepeatStatus.FINISHED;
};
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet(movieTasklet(null, null, null,null,null))
.build();
}
@Bean
public Job jobParametersJob() {
return jobBuilderFactory.get("jobParametersJob")
.start(step1())
.build();
}
}
Listing 11-9.src/main/java/com/apress/cloud/batch/MovieBatchConfiguration.java
列表 11-9 显示批量处理。让我们分析一下这个班级。
-
jobParametersJob
。这个方法定义了只包含一个步骤的作业。这个方法创建了JobExecution
。 -
step1
。这个方法定义了作业的步骤并创建了一个小任务。在这种情况下,它调用带有空参数的movieTasklet
方法。这没有错,你需要传递参数,但是定义是一个与微线程有关系的容器。 -
movieTasklet
。这个方法定义了微线程。它使用了@StepScope
注释。这个注释就像一个惰性实例化,这意味着在调用 tasklet 之前,它会创建一个 Spring bean。这就是为什么你可以通过@Value
,把工作参数作为一个关键来评估。这是JobParameters
类,在这个上下文中,jobParamters
实例作为一个映射可用,所以很容易访问这些键。当然,你可以看到这个方法有很多参数;因此,您可以只将JobParameters
类作为参数传递,并使用getParameters()
方法来访问所有的键。 -
@EnableTask
,@EnableBatchProcessing
。两个注释都被使用:一个用于批处理,另一个作为任务在云中运行。
接下来,打开application.properties
并添加清单 11-10 中的内容。
# Application Name
spring.application.name=movie-batch
# Logging Level
logging.level.org.springframework.cloud.task=DEBUG
logging.level.com.apress.cloud.batch=DEBUG
# DataSource
spring.datasource.url=jdbc:mysql://localhost:3306/movies?useSSL=false&requireSSL=false
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=rootpw
# Batch
spring.batch.initialize-schema=ALWAYS
Listing 11-10.src/main/resource/application.properties
您正在使用一个 MySQL 数据库,现在,它指向一个本地数据库。要运行这个示例,您需要启动并运行 MySQL 引擎。您可以使用 Docker 命令来完成。
docker run -d --rm --name mysql \
-e MYSQL_DATABASE=movies \
-e MYSQL_USER=root \
-e MYSQL_ROOT_PASSWORD=rootpw \
-p 3306:3306 \
mysql:5.7.25
接下来,使用您的 IDE 运行应用,但是记住您需要传递一些参数。运行它的另一种方法是使用命令行(一行)。
./mvnw spring-boot:run -Dspring-boot.run.arguments="url=https://bit.ly/2ZM57Kq imdbId=spring dropbox.path=/IMDB/ dropbox.local-tmp-folder=/tmp/ dropbox.token=YOUR-TOKKEN"
添加您的 Dropbox 令牌。它只是一行,参数之间有空格。你的 Dropbox 文件夹中应该有spring.jpg
图片。如果您使用相同的参数再次运行它,您会得到一个错误,这没关系,因为作业参数与作业执行相关联,以防止错误地重新运行作业。如果您想再次测试,您可以更改url
和imbdId
参数。
另一种方法是通过创建一个可执行的 JAR 来运行它。
./mvnw -DskipTests clean package
java -jar target/movie-batch-0.0.1.jar \
url=https://bit.ly/2ZM57Kq \
imdbId=spring \
dropbox.path=/IMDB/ \
dropbox.local-tmp-folder=/tmp/ \
dropbox.token=YOUR-TOKEN
这可以是多行的(记住这是针对 Unix 操作系统的;对于 Windows,它必须在一行上)。
测试完movie-batch
应用后,您需要将它部署到 Maven 存储库中。此外,本例中使用了 Bintray。转到命令行并执行以下命令。
./mvnw -DskipTests clean package
./mvnw -DskipTests deploy
现在,你已经为下一步做好了准备。
使用数据流中的流启动任务/批处理
在 Spring CloudStream 中,有许多方法可以启动一个任务。您看到了您可以使用仪表板来启动,但是如果您有一个流并且想要触发或启动一个任务/批处理过程,会发生什么呢?Spring Cloud Stream 和 Spring Cloud Task 提供了一种在流中启动任务/批处理的方式。您唯一需要做的事情是使用一个源或处理器和一个包含 registeredtask 应用名称的简单有效负载,或者您可以使用TaskLaunchRequest
类来完成相同的任务。启动器必须是一个接收器,它可以是task-launcher-dataflow
应用启动器,或者你可以创建一个使用@EnableTaskLauncher
的自定义接收器。这个注释通过使用有效负载消息和启动任务来处理其余的事情。
电影批处理流
让我们创建一个流管道 DSL,它在 Spring CloudStream 中启动movie-batch
应用。让我们回顾一下我们正在使用的东西(见图 11-14 )。
图 11-14。
流管道 DSL
图 11-14 显示了你正在构建的流 DSL。如您所见,它与前一章中的相同,因为您已经在 Maven 存储库中拥有了一些应用,所以您可以重用它们。请注意,唯一的新部分是movie-details
和task-launcher-dataflow
应用。因此,您需要创建一个电影细节项目。
电影详情
首先打开浏览器,进入 Spring Initializr。使用以下信息。
-
组:
com.apress.cloud.stream
-
神器:
movie-details
-
包名:
com.apress.cloud.stream.movie
-
依赖项:Spring Batch,Task,Lombok,JDBC API,MySQL
点击生成按钮下载一个 ZIP 文件。你可以解压并导入到你喜欢的 IDE 中(见图 11-15 )。
图 11-15。
Spring Initializr 电影细节流处理器
接下来,打开pom.xml
文件,并添加以下依赖项和部分。
..s.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
....
....
<build>
<plugins>
...
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-app-starter-metadata-maven-plugin</artifactId>
<version>2.0.0.RELEASE</version>
<executions>
<execution>
<id>aggregate-metadata</id>
<phase>compile</phase>
<goals>
<goal>aggregate-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
<distributionManagement>
<repository>
<id>bintray-felipeg48-scdf</id>
<name>felipeg48-scdf</name>
<url>https://api.bintray.com/maven/felipeg48/scdf/movie-streams/;publish=1</url>
</repository>
</distributionManagement>
不要忘记删除快照并使用版本 0.0.1。
<version>0.0.1</version>
接下来,让我们创建DropboxProperties
和MovieProperties
类。请记住,您需要传递其中的一些属性来成功启动movie-batch
应用(参见清单 11-11 和 11-12 )。
package com.apress.cloud.stream.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "movie")
public class MovieProperties {
String apiServer = "https://imdb8.p.rapidapi.com/title/get-details?tconst=ID";
String headerHost = "imdb8.p.rapidapi.com";
String headerKey = null;
String taskName = "movie-dropbox-batch";
DropboxProperties dropbox = new DropboxProperties();
}
Listing 11-12.src/main/java/com/apress/cloud/stream/movie/MovieProperties.java
package com.apress.cloud.stream.movie;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "dropbox")
public class DropboxProperties {
private String token = null;
private String path = "/IMDB/";
private String localTmpFolder = "/tmp/";
}
Listing 11-11.src/main/java/com/apress/cloud/stream/movie/DropboxProperties.java
正如您所看到的,它几乎和以前一样,但是 URL 发生了变化。您正在访问带来电影图片 URL 的/title/get-details
端点。另外,还添加了一个新字段taskName, was
,用于保存您想要启动的任务的名称。接下来,创建Movie
和MoviePayload
类(参见清单 11-13 和 11-14 )。
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class MoviePayload {
String name;
String[] args;
}
Listing 11-14.src/main/java/com/apress/cloud/stream/movie/MoviePayload.java
package com.apress.cloud.stream.movie;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Movie {
private String id;
private String title;
private String actor;
private int year;
private String genre;
private int stars;
}
Listing 11-13.src/main/java/com/apress/cloud/stream/movie/Movie.java
清单 11-14 显示了发送到task-launcher-dataflow
应用的有效负载,因此它可以在args
字段中使用正确的作业参数启动movie-batch
任务。名称是正在创建的任务。
接下来,创建MovieStream
类(参见清单 11-15 )。
package com.apress.cloud.stream.movie;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import lombok.extern.log4j.Log4j2;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.integration.annotation.Transformer;
import org.springframework.messaging.support.GenericMessage;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
@Log4j2
@EnableConfigurationProperties({MovieProperties.class, DropboxProperties.class})
@EnableBinding(Processor.class)
public class MovieStream {
private MovieProperties movieProperties;
private final CloseableHttpClient httpclient = HttpClients.createDefault();
private final HttpGet getRequest = new HttpGet();
public MovieStream(MovieProperties movieProperties) {
this.movieProperties = movieProperties;
getRequest.addHeader("Accept", "application/json");
getRequest.addHeader("x-rapidapi-host", movieProperties.getHeaderHost());
getRequest.addHeader("x-rapidapi-key", movieProperties.getHeaderKey());
getRequest.addHeader("Content-Type", "application/json");
}
@Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
public Object process(Movie movie){
try {
getRequest.setURI(new URI(movieProperties.getApiServer().replace("ID", movie.getId())));
HttpEntity entity = httpclient.execute(getRequest).getEntity();
String url = JsonPath.parse(EntityUtils.toString(entity, StandardCharsets.UTF_8)).read("$.image.url",String.class).toString();
log.debug("Movie's URL: {}", url);
ObjectMapper mapper = new ObjectMapper();
String payload = mapper.writeValueAsString(new MoviePayload(movieProperties.getTaskName(),
new String[] {
"url=" + url,
"imdbId=" + movie.getId(),
"dropbox.token=" + movieProperties.getDropbox().getToken(),
"dropbox.path=" + movieProperties.getDropbox().getPath(),
"dropbox.local-tmp-folder=" + movieProperties.getDropbox().getLocalTmpFolder()
}));
return new GenericMessage<>(payload);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
throw new RuntimeException("Can't process the Movie.");
}
}
Listing 11-15.src/main/java/com/apress/cloud/stream/movie/MovieStream.java
清单 11-15 显示了MovieStream
类。您已经了解了这种流风格,在这里您声明了@EnableBinding
并将流配置为处理器。@Transformer
注释使用输入和输出通道。Apache Commons 请求/title/details
获取电影的艺术 URL,并将其传递给MoviePayload
实例,然后您只需向 sink 发送一个带有这个简单有效负载的GenericMessage
。要启动的任务的名称及其作业参数。
接下来,打开application.properties
文件,添加清单 11-16 中的内容。
# Server
server.port=8085
# IMDB API
movie.api-server=https://imdb8.p.rapidapi.com/title/get-details?tconst=ID
movie.header-host=imdb8.p.rapidapi.com
movie.header-key=YOUR-KEY
movie.task-name=movie-dropbox-batch
movie.dropbox.token=YOUR-TOKEN
# Bindings - RabbitMQ
spring.cloud.stream.bindings.input.destination=imdb
spring.cloud.stream.bindings.output.destination=task
# Logging
logging.level.com.apress.cloud.stream.movie=DEBUG
Listing 11-16.src/main/resources/application.properties
当然,其中一些属性必须被设置,而不是最终 JAR 的一部分,比如键和任务的名称。现在您可以编译、打包和部署到您的 Maven 存储库。
./mvnw -DskipTests clean package
./mvnw -DskipTests deploy
使用仪表板
现在是时候使用仪表板来创建流管道 DSL 了。确保您的 Spring CloudStream 服务器及其组件已经启动并正在运行。您还需要运行 NATs 服务器。打开您的仪表板,转到应用注册。选择批量导入申请。在应用作为属性文本区域中使用以下属性。
source.http=maven://org.springframework.cloud.stream.app:http-source-rabbit:2.1.2.RELEASE
source.http.metadata=maven://org.springframework.cloud.stream.app:http-source-rabbit:jar:metadata:2.1.2.RELEASE
source.movie-web=maven://com.apress.cloud.stream:movie-source:0.0.1
processor.movie-imdb=maven://com.apress.cloud.stream:movie-processor:0.0.1
processor.movie-imdb.metadata=maven://com.apress.cloud.stream:movie-processor:jar:metadata:0.0.1
processor.movie-details=maven://com.apress.cloud.stream:movie-details:0.0.1
processor.movie-details.metadata=maven://com.apress.cloud.stream:movie-details:jar:metadata:0.0.1
processor.splitter=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:2.1.2.RELEASE
processor.splitter.metadata=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:jar:metadata:2.1.2.RELEASE
sink.movie-log=maven://com.apress.cloud.stream:movie-sink:0.0.1
sink.task-launcher-dataflow=maven://org.springframework.cloud.stream.app:task-launcher-dataflow-sink-rabbit:1.1.0.RELEASE
sink.task-launcher-dataflow.metadata=maven://org.springframework.cloud.stream.app:task-launcher-dataflow-sink-rabbit:jar:metadata:1.1.0.RELEASE
sink.log=maven://org.springframework.cloud.stream.app:log-sink-rabbit:2.1.3.RELEASE
sink.log.metadata=maven://org.springframework.cloud.stream.app:log-sink-rabbit:jar:metadata:2.1.3.RELEASE
task.movie-batch=maven://com.apress.cloud.batch:movie-batch:0.0.1
task.movie-batch.metadata=maven://com.apress.cloud.batch:movie-batch:jar:metadata:0.0.1
提交这些属性后,应该会列出几个应用。请注意新注册的应用(及其类型)。你包括了task-launcher-dataflow
sink 应用,它期望一个带有名称和一些参数(作业参数)的有效载荷(见图 11-16 )。
图 11-16。
应用
接下来,转到流窗格并创建流 DSL(参见图 11-17 )。
图 11-17。
创建流
movie= movie-web | splitter | movie-imdb | movie-log
to-dropbox= :movie.splitter > movie-details | task-launcher-dataflow
您可以通过单击创建流按钮来创建流。保留默认名称(movie
和to-dropbox
)(见图 11-18 和 11-19 )。
图 11-19。
流
图 11-18。
创建流
接下来,转到任务窗格并创建一个任务。应当列出movie-batch
应用(参见图 11-20 )。
图 11-20。
创建任务
单击“创建任务”按钮,这将打开一个对话框。将其设置为movie-dropbox-batch
(参见图 11-21 )。
图 11-21。
任务创建
点击创建任务按钮。现在,您已经准备好运行流 DSL 了。转到“流”面板。点击movie
流媒体播放按钮(>)。在自由文本窗格中,使用下列属性。
app.movie-web.server.port=8081
app.movie-web.spring.cloud.stream.bindings.output.destination=movie
app.splitter.expression=#jsonPath(payload,'$.MovieRequest.movies')
app.splitter.spring.cloud.stream.bindings.input.destination=movie
app.splitter.spring.cloud.stream.bindings.output.destination=imdb
app.movie-imdb.spring.cloud.stream.bindings.input.binder=rabbit
app.movie-imdb.spring.cloud.stream.bindings.output.binder=nats
app.movie-imdb.spring.cloud.stream.bindings.input.destination=imdb
app.movie-imdb.movie.header-key=YOUR-KEY
app.movie-imdb.spring.nats.host=nats
app.movie-imdb.spring.cloud.stream.bindings.output.destination=log
app.movie-log.spring.cloud.stream.bindings.input.destination=log
app.movie-log.spring.nats.host=nats
在继续之前,让我们看看这些属性。您的-KEY 需要添加到电影 IMDB Rest 服务中。注意,在这个例子中,movie-web
在端口 8081 上运行,因为我使用了 Docker Compose。但是如果你在 Kubernetes 中运行,你可以省略这个属性,因为movie-web
应用应该用一个LoadBalancer
类型公开(见图 11-22 )。
图 11-22。
自由文本属性
现在,您可以单击 Deploy Stream 按钮。接下来,在to-dropbox
流上单击 Play 按钮,并在 Freetext 窗格中使用以下属性。
app.movie-details.movie.batch-uri=maven://com.apress.cloud.batch:movie-batch:0.0.1
app.movie-details.movie.header-key=YOUR-KEY
app.movie-details.movie.task-name=movie-dropbox-batch
app.movie-details.movie.dropbox.token=YOUR-TOKEN
app.movie-details.movie.dropbox.path=/IMDB/
app.movie-details.movie.dropbox.local-tmp-folder=/tmp/
app.movie-details.spring.cloud.stream.bindings.input.destination=imdb
app.movie-details.spring.cloud.stream.bindings.output.destination=task
app.task-launcher-dataflow.spring.cloud.stream.bindings.input.destination=task
app.task-launcher-dataflow.spring.cloud.dataflow.client.server-uri=http://dataflow-server:9393
在继续之前,我们先分析一下这些属性。您需要 IMDB 密钥和 Dropbox 令牌。还要注意,task-launcher-dataflow
应用需要知道数据流服务器的位置,因为您可以在不同的平台和服务器上启动任务。您可以配置目的地(参见图 11-23 )。
图 11-23。
自由文本属性
点击部署流按钮。现在您已经准备好测试它了。打开movie-web
app,发送电影。如果任务窗格中一切正常,执行列表应该显示启动movie-batch
的时间(参见图 11-24 )。
图 11-24。
任务执行
单击执行 ID 以查看用于启动任务的日志和参数。
恭喜你!!您已经创建了一个完整的流,并启动了一个 Spring Cloud 任务/批处理应用。
Note
所有的源代码、脚本、属性和 READMEs 都在ch11/
文件夹中。
摘要
在本章中,您了解了 Spring Cloud Task 及其优势,包括使用 Spring Batch 创建 ETL(提取、转换、加载)。您了解了如何使用流来启动任务,并快速回顾了如何创建 Spring 批处理应用,以及如何在数据流中将它们作为 Spring 云任务来运行。
如果您想了解更多关于 SpringCloud 任务的信息,请访问 https://spring.io/projects/spring-cloud-task
。
十二、监控
在前一章中,我向您展示了如何创建任务应用并使用 streams 触发它们。在本章中,您将向前迈出一步,了解您的应用和基础架构中的可见性。回想一下,第一章讨论了允许您对基础设施、应用或业务逻辑的任何问题做出反应的所有工具。这就是为什么监控是交付正确应用和解决方案的重要一环。
Spring CloudStream 公开了基于微米的度量体系结构。Spring Boot 是所有 Spring Cloud 项目的主要技术,包括 Spring CloudStream。因为千分尺是一个供应商中立的度量标准,它支持各种监控系统。Spring CloudStream 使用最流行的监控系统,包括 Prometheus、Wavefront 和 InfluxDB。好的方面是你可以通过添加一个属性来选择你想要使用的。
本章涵盖了云基础设施社区中最流行的度量系统:Prometheus 和 Grafana。在接下来的部分中,我将向您展示在流和任务中公开度量标准需要做什么,并且您将看到如何从仪表板中访问它们。
千分尺
Micrometer 是 Spring Boot 2.x 应用的“一等公民”,具有指标、健康检查器和其他非功能性需求。对于任何其他度量技术,您只需要添加spring-boot-starter-actuator
依赖项和micrometer-registry-<name-of-the-metrics-collector>
依赖项,这意味着 Spring CloudStream 是在考虑度量的情况下创建的。
Micrometer 是一个维度优先的度量收集 facade,它允许您使用供应商中立的 API 为您的代码注册时间、计数器和度量解决方案。这样,您的应用可以注册与吞吐量、总时间、最大延迟、预先计算的百分位数、百分位数、直方图、SAL 边界计数等相关的时间序列。Micrometer 对较旧的系统,如 JMX 和 Ganglia,使用维度度量和分层名称。在现有的新维度监控系统中,Micrometer 可以与 Prometheus、CloudWatch、Ganglia、Graphite、InfluxDB、网飞阿特拉斯、New Relic、StatsD、Datadog、Wavefront、SignalFx、JMX、AppOptics、Azure Application Insights、Dynatrace、ElasticbSearch 和 StackDriver 配合使用。
默认情况下,Spring Boot 2 自动配置几个指标,包括以下内容。
-
JVM,报告利用率
-
各种内存和缓冲池
-
与垃圾收集相关的统计信息
-
线程利用率
-
加载/卸载的类的数量
-
-
CPU 使用情况
-
Spring MVC 和 WebFlux 请求延迟
-
rest 风格的延迟模板
-
缓存利用率
-
数据源利用率,包括 HikariCP 池指标
-
RabbitMQ 连接工厂
-
文件描述符用法
-
日志回溯:记录每个级别日志回溯的事件数
-
正常运行时间:正常运行时间标尺和代表应用绝对启动时间的固定标尺
-
Tomcat 用法
Spring Boot 2 配置了一个io.micrometer.core.instrument.MeterRegistry
组合,这样您就可以添加注册中心实现,允许您将您的指标发送到多个监控系统。通过MeterRegistryCustomizer
,您可以一次定制一整套注册中心或者单独的实现。
Spring Boot 允许您覆盖和配置一些缺省值,因此您可以创建自己的度量分布,例如禁用 JVM 报告利用率。
management.metrics.enable.jvm=false
management.metrics.distribution.percentiles-histogram.http.server.requests=true
management.metrics.distribution.sla.http.server.requests=1ms,5ms
要使用 Micrometer,您必须添加以下依赖项。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-YOUR-METRICS-TECH</artifactId>
</dependency>
将以下内容添加到您的配置中。
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(@Value("${spring.application.name}") String appName) {
return registry -> registry.config().commonTags("application", appName);
}
需要注意的是,在你的application.properties
中,需要添加你的 app 的名称。
spring.application.name=movie-web
您可以将此视为最佳实践。
运行状况检查和监控流式应用
Spring Cloud Stream 不仅为您的流式应用提供开箱即用的指标,还为绑定器提供了健康指标。请记住,流式应用之间的通信取决于您选择的绑定器,可以是 RabbitMQ 或 Kafka 等单个绑定器,也可以为同一个流式应用选择多个绑定器。当您需要这种类型的可见性(指标和健康指标)以及关于绑定器的更多信息时,您必须通过执行以下操作来告诉您的应用您正在为您的绑定使用健康指标。
-
First, add the following dependencies to your
pom.xml
.<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud.stream.app</groupId> <artifactId>app-starters-micrometer-common</artifactId> <version>2.1.5.RELEASE</version> </dependency>
如果您使用的是 WebFlux,请将
spring-boot-starter-web
替换为spring-boot-starter-webflux
。 -
接下来,您需要设置以下属性。
management.health.binders.enabled=true management.endpoints.web.exposure.include=bindings
通过这些设置,您可以可视化当前与/actuator/bindings
端点的绑定。如果您想了解单个活页夹,请访问/actuator/bindings/<binding-name>
。
您可以通过执行 POST 来停止、启动、暂停和恢复绑定。
curl -d '{"state":"STOPPED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/<binding-name>
curl -d '{"state":"STARTED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/<binding-name>
curl -d '{"state":"PAUSED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/<binding-name>
curl -d '{"state":"RESUMED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/<binding-name>
假设您创建了一个 DSL 流,如下所示。
http --server.port=9095 | filter --expression=#jsonPath(payload,'$.msg').contains('Hello') | log
假设您有运行 Prometheus 和 Grafana 的数据流服务器,您正在本地运行它,并且您将这个流命名为simple
,您可以在端口 9095 访问http
应用。可以用http://localhost:9095/actuator/bindings
和http://localhost:9095/actuator/bindings/simple.http
得到绑定(见图 12-1 )。
图 12-1。
执行器/绑定 http://localhost:9095/actuator/bindings
这意味着开箱即用的应用启动器集成了web
和actuator
依赖项,并预先配置为使用千分尺技术。
Note
在ch12/docker-compose there
中是一个README file
和用 Spring CloudStream 服务器运行 Prometheus 和 Grafana 的文件。回顾第八章以了解如何在 Kubernetes(一个更好的应用解决方案)中运行。
要在流式应用中使用或添加指标,您需要添加我之前提到的依赖项。默认情况下,当 Spring Cloud streams 发现您添加了 actuator 和 metrics 技术时,它会自动配置这些指标。如果使用普罗米修斯作为度量技术,您必须包括以下属性。
management.metrics.export.prometheus.enabled=true
management.endpoints.web.exposure.include=prometheus
spring.cloud.streamapp.security.enabled=false
例如,您还可以在 Grafana 等可视化工具中看到指标。然后,您可以添加任何业务逻辑来注册指标。如果您想了解更多关于公制的信息,请访问千分尺网站 https://micrometer.io
。
监控任务应用
要启用任务/批处理指标,您需要具有以下依赖关系。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
因为 Spring Cloud 任务/批处理应用是短命的应用,它们需要一个服务发现组件来为应用公开的任何指标配置端点。因此,您需要以下内容。
<dependency>
<groupId>io.micrometer.prometheus</groupId>
<artifactId>prometheus-rsocket-spring</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.micrometer.prometheus</groupId>
<artifactId>prometheus-rsocket-client</artifactId>
<version>1.0.0</version>
</dependency>
对于普罗米修斯,如果你想使用波前,使用以下。
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-wavefront</artifactId>
</dependency>
<dependency>
<groupId>com.wavefront</groupId>
<artifactId>wavefront-sdk-java</artifactId>
<version>2.6.0</version>
</dependency>
如果您想使用 InfluxDB,请使用下面的代码。
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-influx</artifactId>
</dependency>
现在,您已经知道如何向您的流添加指标和健康检查,让我们使用前面章节中的流管道 DSL 和这些功能来监控流式应用。
电影流管道 DSL:把它们放在一起
请确保您的 Spring CloudStream 正常运行,并且启用了 Prometheus 和 Grafana。在源代码中,如果你想在本地运行,我创建了包含所有必要的 YAML 文件的ch12/docker-compose
文件夹。
这是最终的管道 DSL 部署(见图 12-2 )。
图 12-2。
电影管道 DSL
movie=movie-web | splitter | groovy-transform | jdbc
imdb-high-rating=:movie.groovy-transform > filter | log
stars=:movie.splitter > movie-imdb | movie-log
to-dropbox= :movie.splitter > movie-details | task-launcher-dataflow
我在ch12/streams
文件夹中添加了所有的流式应用,如果你想导入它们并查看结果的话。如果您想跟进,您需要在每个流式应用上添加:spring-boot-starter-web
、spring-boot-starter-actuator, micrometer-registry-prometheus
、app-starters-micrometer-common
和prometheus-rsocket-spring
用于批处理应用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer.prometheus</groupId>
<artifactId>prometheus-rsocket-spring</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.stream.app</groupId>
<artifactId>app-starters-micrometer-common</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
对于每个应用,您必须增加版本—在本例中,增加到 0.0.2。
Note
app-starters-micrometer-common
依赖关系为你的应用提供安全性,因此有必要在movie-source
应用中配置安全性(参见清单 12-1 )。
package com.apress.cloud.stream.movie;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class MovieSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**","/v1/api/movies").permitAll();
}
}
Listing 12-1.movie-source: src/main/java/com/apress/cloud/stream/movie/MovieSecurityConfiguration.java
这仅用于测试目的。最后,您必须添加安全性。
您需要为movie-batch
应用向pom.xml
文件添加以下依赖项。
<dependency>
<groupId>io.micrometer.prometheus</groupId>
<artifactId>prometheus-rsocket-spring</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.micrometer.prometheus</groupId>
<artifactId>prometheus-rsocket-client</artifactId>
<version>1.0.0</version>
</dependency>
应该为将 jar 部署到 Maven 存储库配置好一切。您可以编译和部署每个流和批处理应用。我创建了一个脚本来打包和部署 JARs 当然,您也可以将其添加为 CI/CD 管道,甚至部署流管道 DSL。
现在,您可以编译、打包和部署您的流和批处理应用。
确保您的所有基础设施都在运行。接下来,您需要在 Spring CloudStream 服务器正在使用的 MySQL 数据库中创建Movies
数据库。对jdbc
app 和movie-batch
app 有用。
mysql -uroot -prootpw -e "create database movies"
如果您正在运行 Kubernetes,请确保您可以使用port-forward
命令访问 MySQL 数据库,或者如果您的数据库引擎是一个服务,您可以通过LoadBalancer
访问它。
接下来,打开浏览器,进入 Spring CloudStream 仪表板。在应用窗格中,单击 +添加应用按钮,并选择第三个选项批量导入应用。在文本区域添加以下应用。
source.movie-web=maven://com.apress.cloud.stream:movie-source:0.0.2
processor.filter=maven://org.springframework.cloud.stream.app:filter-processor-rabbit:2.1.3.RELEASE
processor.filter.metadata=maven://org.springframework.cloud.stream.app:filter-processor-rabbit:jar:metadata:2.1.3.RELEASE
processor.movie-imdb=maven://com.apress.cloud.stream:movie-processor:0.0.2
processor.movie-details=maven://com.apress.cloud.stream:movie-details:0.0.2
processor.splitter=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:2.1.2.RELEASE
processor.splitter.metadata=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:jar:metadata:2.1.2.RELEASE
processor.groovy-transform=maven://org.springframework.cloud.stream.app:groovy-transform-processor-rabbit:2.1.3.RELEASE
processor.groovy-transform.metadata=maven://org.springframework.cloud.stream.app:groovy-transform-processor-rabbit:jar:metadata:2.1.3.RELEASE
sink.movie-log=maven://com.apress.cloud.stream:movie-sink:0.0.2
sink.task-launcher-dataflow=maven://org.springframework.cloud.stream.app:task-launcher-dataflow-sink-rabbit:1.1.0.RELEASE
sink.task-launcher-dataflow.metadata=maven://org.springframework.cloud.stream.app:task-launcher-dataflow-sink-rabbit:jar:metadata:1.1.0.RELEASE
sink.log=maven://org.springframework.cloud.stream.app:log-sink-rabbit:2.1.3.RELEASE
sink.log.metadata=maven://org.springframework.cloud.stream.app:log-sink-rabbit:jar:metadata:2.1.3.RELEASE
sink.jdbc=maven://org.springframework.cloud.stream.app:jdbc-sink-rabbit:2.1.5.RELEASE
sink.jdbc.metadata=maven://org.springframework.cloud.stream.app:jdbc-sink-rabbit:jar:metadata:2.1.5.RELEASE
task.movie-batch=maven://com.apress.cloud.batch:movie-batch:0.0.2
花点时间分析一下这些应用。请注意,您使用的是现成的应用启动器:splitter
、filter
、jdbc
、groovy-transform
和task-launcher-dataflow
。
流管道 DSL
接下来,点击流窗格,然后点击 +创建流按钮(参见图 12-3 )。在文本区域添加以下 DSL。
图 12-3。
流式 DSL
movie=movie-web | splitter | groovy-transform | jdbc
imdb-high-rating=:movie.groovy-transform > filter | log
stars=:movie.splitter > movie-imdb | movie-log
to-dropbox= :movie.splitter > movie-details | task-launcher-dataflow
接下来,单击创建流按钮。然后点击四个流(见图 12-4 )。
图 12-4。
流
将出现 Grafana 仪表板。您可以在部署流和任务之后使用它。
任务创建
点击任务窗格,+创建任务,然后添加movie-batch
(见图 12-5 )。
图 12-5。
电影批处理任务
单击创建任务按钮。将名称字段设置为movie-dropbox-batch
。点击创建任务按钮,这里就设置好了。
流部署
接下来,让我们部署我们的流。单击每个流上的播放按钮(>),并添加以下属性。
-
movie
。通过选择自由文本窗格使用以下属性,并在文本区域添加以下内容。app.movie-web.server.port=9095 app.movie-web.spring.cloud.stream.bindings.output.destination=movie app.splitter.expression=#jsonPath(payload,'$.MovieRequest.movies') app.splitter.spring.cloud.stream.bindings.input.destination=movie app.splitter.spring.cloud.stream.bindings.output.destination=imdb app.groovy-transform.script=https://raw.githubusercontent.com/felipeg48/scdf-scripts/master/movie-transform.groovy app.groovy-transform.spring.cloud.stream.bindings.input.destination=imdb app.groovy-transform.spring.cloud.stream.bindings.output.destination=transform app.jdbc.columns=id:id,title:title,actor:actor,year:year,genre:genre,stars:stars,rating:imdb.rating,ratingcount:imdb.ratingCount app.jdbc.table-name=movies app.jdbc.password=rootpw app.jdbc.driver-class-name=org.mariadb.jdbc.Driver app.jdbc.username=root app.jdbc.url=jdbc:mysql://mysql:3306/reviews?autoReconnect=true&useSSL=false app.jdbc.spring.cloud.stream.bindings.input.destination=transform
-
imdb-high-rating
。通过选择自由文本窗格使用以下属性,并在文本区域添加以下内容。app.filter.expression="#jsonPath(payload,'$.stars') > 3" app.filter.spring.cloud.stream.bindings.input.destination=transform app.filter.spring.cloud.stream.bindings.output.destination=log app.log.spring.cloud.stream.bindings.input.destination=log
-
stars
. Use the following properties by selecting the Freetext pane. In the text-area, add the following.app.movie-imdb.spring.cloud.stream.bindings.input.binder=rabbit app.movie-imdb.spring.cloud.stream.bindings.output.binder=nats app.movie-imdb.spring.cloud.stream.bindings.input.destination=imdb app.movie-imdb.spring.cloud.stream.bindings.output.destination=movie-log app.movie-imdb.movie.header-key=YOUR-KEY app.movie-imdb.spring.nats.host=nats app.movie-log.spring.cloud.stream.bindings.input.destination=movie-log app.movie-log.spring.nats.host=nats
请注意,您需要为 IMDB 外部服务添加您的密钥。
-
to-dropbox
. Use the following properties by selecting the Freetext pane and adding the following in the text-area.app.movie-details.movie.batch-uri=maven://com.apress.cloud.batch:movie-batch:0.0.2 app.movie-details.movie.header-key=YOUR-KEY app.movie-details.movie.task-name=movie-dropbox-batch app.movie-details.movie.dropbox.token=YOUR-TOKEN app.movie-details.movie.dropbox.path=/IMDB/ app.movie-details.movie.dropbox.local-tmp-folder=/tmp/ app.movie-details.spring.cloud.stream.bindings.input.destination=imdb app.movie-details.spring.cloud.stream.bindings.output.destination=task app.task-launcher-dataflow.spring.cloud.stream.bindings.input.destination=task app.task-launcher-dataflow.spring.cloud.dataflow.client.server-uri=http://dataflow-server:9393
在这里,您需要添加您的 IMDB 密钥和 Dropbox 令牌。
打开电影 Web 应用。如果您使用的是 Docker Compose,它应该运行在端口 9095。如果您正在使用 Kubernetes,您可以使用port-forward
命令或确保它在负载平衡器中可用(参见图 12-6 和 12-7 )。
图 12-7。
展开已部署
图 12-6。
流已部署
如果您展开已部署的流,您会看到一个带有 GUID 和州的绿色小方块,这有助于在 Prometheus 和 Grafana 中识别。
接下来,单击 Grafana 仪表板按钮。在 Grafana 网页上,用管理员 / 管理员登录。如果需要,单击“跳过”按钮;否则,您需要一个新的管理员用户密码。您可以导航查看流movie
。示例如图 12-8 所示。
图 12-8。
grafana/应用/流/电影
接下来,打开您的 Web 应用。如果您使用的是本地环境,您可以通过端口 9095 访问它。点按“发送”按钮以查看所有流的工作情况。查看 Grafana 仪表板,识别每个流和任务应用(参见图 12-9 )。
图 12-9。
Grafana 控制板
恭喜你!现在,您可以通过导出您的流和任务/批处理应用的指标来可视化您的环境中正在发生的事情,并对您的流中正在发生的事情做出反应。
Note
所有的源代码、脚本、属性和 READMEs 都在ch12/
文件夹中。
摘要
在本章中,我向你展示了如何监控你的流和批处理应用。您可以添加任何新的自定义指标,并通过集成 Spring Boot 千分尺来展示它。它提供了对流程中的任何事件做出反应所需的可见性,包括业务逻辑和定位瓶颈的跟踪。请记住,您可以在部署属性中创建更多的实例,因此您永远不会失去处理控制。