九、Spring Security
Spring Security 是一个高度可定制的认证和访问控制框架。它是保护基于 Spring 的应用的标准。它支持多种安全标准,如 LDAP 和 OAuth2。
此外,它与其他 Spring 模块和项目集成得很好,并且可以利用基于注释的代理。此外,它与 SpEL (Spring Expression Language)配合得很好,这一点我们将在本章中介绍。
特征
Spring Security 很容易扩展,并且有许多内置特性:
-
对身份验证和授权的全面和可扩展的支持
-
防范诸如会话固定、点击劫持、跨站点请求伪造等攻击
-
Servlet API 集成
-
与 Spring Web MVC 的可选集成
-
支持 OAuth 和 OAuth2
-
支持 SAML
概观
Spring Security 的核心分为两件事:认证,它决定用户(主体)的身份,以及访问控制,它决定什么用户可以访问什么资源。
Spring Security 认证基于一个AuthenticationManager
接口,该接口有一个方法Authentication authenticate(Authentication)
。它由拥有一个或多个认证提供者的ProviderManager
实现。AuthenticationProvider
接口有两个方法,Authentication authenticate(Authentication)
和boolean supports(Class)
,如果这个AuthenticationProvider
支持指定的Authentication
对象,则返回true
。
Spring Security 访问控制(也称为授权)基于一个拥有一个或多个AccessDecisionVoter
的AccessDecisionManager
。AccessDecisionVoter<S>
的主要实现是基于角色做出访问决策的RoleVoter
。
配置
Spring Security 性可以通过典型的方式配置,XML 或 Java 配置。
-
我们声明 AuthenticationManager 应该使用内存中的用户数据库,并添加一个默认的“admin”用户。在生产中,您可能应该将用户存储在数据库中或使用其他方法。出于演示目的,我们使用带有 DefaultPasswordEncoder()的用户构建器方法创建一个用户。这也不应该在生产中进行。用户还被赋予用户和管理员角色。
-
configure(HttpSecurity http)提供了一个流畅的界面,用于使用 URL 匹配器、登录和注销页面以及其他 web 安全设置来配置访问控制。第一个方法名为 httpBasic(),支持基于标头的 HTTP 基本身份验证。后续的方法,以及()。authorizeRequests()设置授权(访问控制)设置。
-
代码
antMatchers("/courses").hasRole("USER")
只为“/courses”路径创建一个过滤器,并指定用户必须拥有用户角色才能获得访问权限。
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// from WebSecurityConfigurerAdapter
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// here you could configure a JDBC database
// auth.jdbcAuthentication().usersByUsernameQuery(...)
auth.inMemoryAuthentication()
.withUser(User.builder().withDefaultPasswordEncoder() //(1)
.username("admin").password("123")
.roles("USER", "ADMIN")
.build());
}
// from WebSecurityConfigurerAdapter
@Override
protected void configure(HttpSecurity http) throws Exception { //(2)
http.httpBasic().and()
.authorizeRequests()
.antMatchers("/courses").hasRole("USER") //(3)
.antMatchers(HttpMethod.GET, "/actuator/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll()
.and().csrf().disable();
}
}
Listing 9-1SecurityConfig.java
代替antMatchers
,你可以使用mvcMatchers
,主要区别是后者匹配 URL 的方式与 MVC @RequestMapping
完全相同,这允许更多的灵活性,比如不同的扩展名(例如.json
或.xml
)。
第一个匹配的 URL 决定了访问,因此您应该按照从最具体到最不具体的顺序排列 URL 匹配器。
密码安全性
Spring Security 中的密码是通过PasswordEncoder
接口的实现来加密的,Spring 提供了几个实现(没有解码器,因为密码编码应该是单向算法)。
如果你正在开发一个新系统,Spring 团队建议你使用BCryptPasswordEncoder
来获得更好的安全性和与其他语言的互操作性。
你可以使用PasswordEncoderFactories
1 或者通过构造函数来创建一个DelegatingPasswordEncoder
。
为了确保适当的安全性,您应该调整您的密码编码,使其在系统上的处理时间大约为一秒钟。这有助于密码更难被暴力破解。例如,
BCryptPasswordEncoder
的构造函数可以接受一个强度参数,该参数指定了要使用的对数轮次(在 4 和 31 之间),您应该测试一下,看看哪个数字在一个像样的处理器上需要大约一秒的编码时间。
访问身份验证
SecurityContext
接口可用于通过getAuthentication()
方法访问当前登录的用户,该方法获得当前认证的主体或认证请求令牌。可以从SecurityContextHolder.getContext()
静态方法中访问SecurityContext
。默认情况下,SecurityContextHolder
使用ThreadLocal
来存储当前的SecurityContext
,它为每个线程存储一个值。
Spring 将在控制器方法中注入任何 Principal 或 Authentication 类型的参数值。
注释安全性
您可以通过注释启用访问控制。根据项目的配置,可以使用几种不同的注释,包括来自javax.annotation.security package
、@Secured
、@PreAuthorize
和@PostAuthorize.
的@RolesAllowed
首先,要使用 Spring 方法安全,我们需要添加spring-security-config
依赖,例如,使用 Maven:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
如果我们想使用 Spring Boot,我们可以使用spring-boot-starter-security依赖项,它包含了 spring-security-config(更多信息参见第 15 章 )。
通过 XML 启用:
<beans ...
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<security:global-method-security
secured-annotations="enabled"
pre-post-annotations="enabled" />
Listing 9-2security.xml
使用 Java 配置:
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true, // (1)
securedEnabled = true, // (2)
jsr250Enabled = true) // (3)
public class MethodSecurityConfig
extends GlobalMethodSecurityConfiguration {
}
Listing 9-3MethodSecurityConfig.java
-
prePostEnabled
属性决定@PreAuthorize and @PostAuthorize should be enabled
是否。 -
securedEnabled
属性确定是否应该启用@Secured
注释。 -
jsr250Enabled
属性允许我们使用@RolesAllowed
、、??、、、@DenyAll
注释。
使用@Secured
使用全局方法安全性,您可以在任何 Spring bean 上的任何方法上添加@Secured
,它将被 Spring Security 拦截以执行适当的授权。
例如:
@Secured("ROLE_USER")
// method1
@Secured({"ROLE_ADMIN", "ROLE_USER"}) //either ADMIN or USER
// method2
@Secured
可以为多个角色接受一个字符串数组,在这种情况下,如果有任何角色匹配,这是允许的。它不支持 SpEL (Spring Expression Language),所以对于更复杂的访问逻辑,需要使用不同的注释。
使用预授权
使用@PreAuthorize
和@PostAuthorize
可以实现更复杂的逻辑(包括随后描述的 SpEL)来确定哪些用户可以访问。
例如,您可以如下使用@PreAuthorize
:
//imports
import org.springframework.security.access.prepost.PreAuthorize;
// code...
@PreAuthorize("hasRole('ADMIN') && hasRole('USER')")
public void deleteById(Long id) {
在这种情况下,当前认证必须同时具有管理员和用户角色才能访问deleteById
方法。
提供给@PreAuthorize
和@PostAuthorize
注释的表达式也可以引用附加变量、@PostAuthorize
的returnObject
和@PreAuthorize
中的方法参数(使用 #name 语法)。
假设主体是具有用户属性name
的User
,并且Course
具有可能与User
的属性username
相匹配的属性owner
,例如:
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Course getCourse(Long id) {
//method definition
}
@PreAuthorize("#course.owner == authentication.principal.username")
public void removeCourse(Course course) {
//method definition
}
在第一个例子中,表达式将验证返回的课程对象的所有者是否等于 Spring Security 的认证主体的用户名(当前登录的用户名User
)。如果不是这样,用户将会得到一个身份验证异常。
第二个例子在调用方法之前(本例中为removeCourse
)验证给定课程的所有者是否等于 Spring Security 的认证主体的用户名。
全局方法安全性
拼写
SpEL (Spring Expression Language)是一种基于文本的表达式语言,由 Spring 解释,通常用于简化值注入。它可以直接在@PreAuthorize
和@PostAuthorize
值内使用,并具有 Spring Security 提供的附加功能。
Spring Expression Language
SpEL 可以使用#{ 表达式 }语法在任何@Value
注释值中使用。SpEL 支持标准操作(+ - / % < > <= >= == != && || !
)以及它们的英文单词对等词(加号、减号、div、mod、lt、gt、le、ge、eq、ne、and、or、not)。它还支持 Elvis 运算符(?:)和空安全取消引用(?。).它还支持正则表达式匹配的“匹配”。
可以使用单引号(')来指定字符串值。
SpEL 支持使用 T( Type )语法引用 Java 类型。
SpEL 支持使用{key:value}语法定义映射,例如,{‘key’: 1,’ key2’: 2}。
它还支持使用 list[n]语法通过索引访问列表值;例如,list[0]将访问第一个元素。
在 Spring Security 上下文中,hasRole 函数是可用的,因此只有当当前用户拥有 ADMIN 角色时,hasRole('ADMIN ')才会返回 true。
更多信息见 https://bit.ly/2WAAWEf
。
2
@RolesAllowed 注释是@Secured 注释的 JSR-250 等效注释。
十、Spring Web 服务
Spring Web Services (Spring WS)专注于构建契约优先的 SOAP web 服务,具有灵活的 XML 映射、契约和实现之间的松散耦合以及与 Spring 的轻松集成。它的架构类似于 Spring MVC。
特征
Spring WS 具有以下特性:
-
强大的映射——可以将传入的 XML 请求分发到任何对象,这取决于消息有效负载、SOAP 操作头或 XPath 表达式。
-
XML API 支持——传入的 XML 消息可以用标准的 JAXP API 来处理,比如 DOM、SAX 和 StAX,还有 JDOM、dom4j、XOM,甚至编组技术。
-
灵活的 XML 编组 Spring Web Services 发行版中的对象/XML 映射模块支持 JAXB 1 和 2、Castor、XMLBeans、JiBX 和 XStream。
-
支持 WS-Security——WS-Security 允许您对 SOAP 消息进行签名、加密和解密,或者根据它们进行身份验证。
-
与 Spring Security 集成 Spring Web 服务的 WS-Security 实现提供了与 Spring Security 的集成。
入门指南
首先,将以下依赖项添加到 Maven pom 文件中:
<dependencies>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-core</artifactId>
<version>3.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>jdom</groupId>
<artifactId>jdom</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
或者如果使用 Gradle,添加以下内容:
implementation 'org.springframework.ws:spring-ws-core:3.0.9.RELEASE'
implementation 'org.jdom:jdom:2.0.2'
implementation 'jaxen:jaxen:1.2.0'
在 Java 配置类上使用@EnableWs
注释,使 spring-ws 能够注册默认的EndpointMappings
、EndpointAdapter
和EndpointExceptionResolver
。
您需要创建一个 web.xml 文件,如下所示:
<web-app xmlns:="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>MyCompany Web Service</display-name>
<servlet>
<servlet-name>no-boot-spring-ws</servlet-name>
<servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>spring-ws</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Listing 10-1WEB-INF/web.xml
基于 servlet 的名称,Spring 将寻找一个对应的名为<servlet_name>-servlet.xml
的 Spring XML 配置文件。在这种情况下,它将寻找一个WEB-INF/no-boot-spring-ws-servlet.xml
文件。
Spring Boot 配置
要在 Spring Boot Gradle 项目中包含 Spring-WS,请添加以下依赖项:
implementation 'org.springframework.boot:spring-boot-starter-web-services'
implementation 'org.jdom:jdom:2.0.2'
implementation 'jaxen:jaxen:1.2.0'
Spring Boot WS 启动器(spring-boot-starter-web-services)将自动执行以下操作:
-
在 servlet 容器中配置一个
MessageDispatcherServlet
-
扫描所有的
.wsdl
和.
xsd
文档,查找 WSDL 和模式定义的 beans
先合同
首先编写契约启用实际模式的更多特性(比如限制字符串值的允许值),允许将来更容易升级,并允许与非 Java 系统更好的互操作性。
有四种不同的方法来定义这样的 XML 契约:
-
文档类型定义
-
XML 模式(XSD)
-
放松 ng
-
图式【2】
对于本书,我们将使用课程领域的 XML 模式。例如(假设您想要使用名称空间,"
http://mycompany.com/schemas
"
),创建一个名为“my.xsd”的文件,并将其放在项目的“src/main/resources”目录中,内容如下:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="http://mycompany.com/schemas"
xmlns:my="http://mycompany.com/schemas">
<xs:element name="Course">
<xs:complexType>
<xs:sequence>
<xs:element ref="my:Number"/>
<xs:element ref="my:Title"/>
<xs:element ref="my:Subtitle"/>
<xs:element ref="my:Description"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Number" type="xs:integer"/>
<xs:element name="Title" type="xs:string"/>
<xs:element name="Subtitle" type="xs:string"/>
<xs:element name="Description" type="xs:string"/>
</xs:schema>
在 Spring-WS 中,不需要手工编写 WSDL。我们将在后面的部分展示如何生成 WSDL。
编写端点
在 Spring-WS 中,您将实现端点来处理传入的 XML 消息。端点通常是通过用一个或多个处理传入请求的方法用@Endpoint
注释来注释类而创建的。方法签名非常灵活:您可以包含与传入的 XML 消息相关的任何类型的参数,这将在后面解释。
首先创建一个用@Endpoint 注释的类,该类要么被组件扫描(@Endpoint 将其标记为特殊的@Component),要么直接使用 Java configuration 将其配置为 Spring Bean。然后添加一个或多个方法来处理 XML 请求的不同元素,例如:
-
因为我们使用 JDOM2,所以我们定义了要在 Xpath 定义中使用的
Namespace
。 -
我们定义了
XPathExpression
实例,稍后我们将使用这些实例来评估 XML 有效负载的各个部分。 -
我们使用@
PayloadRoot
来定义我们希望与该方法匹配的 SOAP 有效负载的名称空间和元素。在Element
参数上的@RequestPayload
注释被注入了匹配的有效载荷,然后我们可以在这个方法中处理它。
import org.jdom2.*;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
@Endpoint
public class CourseEndpoint {
private XPathExpression<Element> numberExpression;
private XPathExpression<Element> titleExpression;
private XPathExpression<Element> subtitleExpression;
private XPathExpression<Element> descriptionExpression;
@Autowired
public CourseEndpoint() throws JDOMException {
Namespace namespace = Namespace.getNamespace("my",
"http://mycompany.com/my/schemas"); //1
XPathFactory xPathFactory = XPathFactory.instance();
numberExpression = xPathFactory.compile("//my:Number", Filters.element(), null, namespace); //2
titleExpression = xPathFactory.compile("//my:Title", Filters.element(), null, namespace);
subtitleExpression = xPathFactory.compile("//my:Subtitle", Filters.element(), null, namespace);
descriptionExpression = xPathFactory.compile("//my:Description", Filters.element(), null, namespace);
}
@PayloadRoot(namespace = "http://mycompany.com/my/schemas",
localPart = "CourseRequest") //3
public void handleRequest(@RequestPayload Element courseRequest) throws Exception {
Long number = Long.parseLong(numberExpression.evaluateFirst(courseRequest).getText());
String description = descriptionExpression.evaluateFirst(courseRequest).getText();
String fullTitle = titleExpression.evaluateFirst(courseRequest).getText() + ":"
+ subtitleExpression.evaluateFirst(courseRequest).getText();
// handleCourse(number, fullTitle, description)
}
}
生成 WSDL
下面是我们如何在 XML 配置中定义 WSDL 生成:
-
首先,id 决定了 wsdl 资源的名称(courses.wsdl)。
-
portTypeName 确定 WSDL 端口类型的名称。
-
locationUri 描述了服务本身的相对位置。
-
targetNamespace 是可选的,但是在 WSDL 本身中定义了命名空间。
<sws:dynamic-wsdl id="courses"
portTypeName="CourseResource"
locationUri="/courseService/"
targetNamespace="http://mycompany.com/definitions">
<sws:xsd location="/WEB-INF/my.xsd"/>
</sws:dynamic-wsdl>
EndpointMappings 和 EndpointExceptionResolvers
默认情况下,Spring-WS(通过WsConfigurationSupport
类)注册以下端点映射:
-
PayloadRootAnnotationMethodEndpointMapping
按 0 排序,用于将请求映射到@PayloadRoot
带注释的控制器方法 -
SoapActionAnnotationMethodEndpointMapping
排序为 1,用于将请求映射到@SoapAction
带注释的控制器方法 -
AnnotationActionEndpointMapping
在 2 排序,用于将请求映射到@Action
带注释的控制器方法
它还注册了一个EndpointAdapter
、DefaultMethodEndpointAdapter
,用于处理带有注释的端点方法和以下 EndpointExceptionResolvers 的请求:
-
SoapFaultAnnotationExceptionResolver
用于处理标注有@SoapFault
的异常 -
SimpleSoapExceptionResolver
用于创建默认例外
定制的
您可以通过实现WsConfigurer
接口或者扩展WsConfigurerAdapter
基类并覆盖单个方法来定制 Spring-WS 配置,例如:
@Configuration
@EnableWs
@ComponentScan
public class CustomWsConfiguration extends WsConfigurerAdapter {
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(new MyInterceptor());
}
@Override
public void addArgumentResolvers(
List<MethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(myArgumentResolver());
}
@Bean
public MethodArgumentResolver myArgumentResolver() {
return new MyArgumentResolver();
}
}
Listing 10-2CustomWsConfiguration.java
可重写的 WsConfigurerAdapter 方法:
| `void addArgumentResolvers(``List argumentResolvers)` | 添加冲突解决程序以支持自定义终结点方法参数类型。 | | `void addInterceptors(``List interceptors)` | 为端点方法调用的预处理和后处理添加端点拦截器。 | | `void addReturnValueHandlers(``List returnValueHandlers)` | 添加处理程序以支持自定义控制器方法返回值类型。 |端点拦截器
EndpointInterceptor 接口具有为请求、响应、错误和完成后调用的方法,并且能够清除响应、修改响应、给出完全不同的响应或停止处理。
| `void afterCompletion(``MessageContext messageContext, Object endpoint, Exception ex)` | 请求和响应(或故障,如果有的话)处理完成后的回调。 | | `boolean handleFault(``MessageContext messageContext, Object endpoint)` | 处理传出响应错误。 | | `boolean handleRequest(``MessageContext messageContext, Object endpoint)` | 处理传入的请求消息。 | | `boolean handleResponse(``MessageContext messageContext, Object endpoint)` | 处理传出的响应消息。 |每个“handle”方法被作为一个链调用,返回值决定处理是否应该停止。True 表示继续处理;false 表示此时阻止处理。如果 handleRequest 方法从任何 EndpointInterceptor 返回 false,端点本身将不会被处理。
2
十一、Spring REST
REST(表述性状态转移)概述了一种使用 HTTP 方法(如 GET、POST、PUT 和 PATCH)围绕资源和元数据设计 web 服务的方法,以映射到定义良好的动作。Roy Fielding 在 2000 年加州大学欧文分校的博士论文“基于网络的软件架构的架构风格和设计”中首次定义了它。遵循这些原则的 web 服务被称为 RESTful 。
这一章主要是关于两个 Spring 项目,Spring REST Docs 和 Spring HATEOAS。 2 它建立在第七章的内容之上,所以在阅读本章之前一定要先阅读它。尽管构建 RESTful web 服务并不需要使用这些项目,但是将它们与 Spring MVC 一起使用可以让您使用 Spring 构建一个全功能的 web API。
Spring 休息文档
Spring REST Docs 3 使用 Asciidoctor 语法基于测试结合文本文档生成文档,尽管您也可以使用 Markdown。这种方法旨在生成 API 文档,类似于 Swagger,但更灵活。
Spring REST Docs 使用用 Spring MVC 的 MockMvc、Spring WebFlux 的 WebTestClient 或 REST Assured 3 编写的测试产生的片段。这种测试驱动的方法有助于保证 web 服务文档的准确性。如果代码片段不正确,生成它的测试就会失败。
入门指南
首先,将 Spring REST Docs 依赖项添加到您的项目中。如果使用 Maven ,添加以下依赖关系:
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<version>2.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
另外,添加以下 Maven 插件,它将在准备包阶段处理 asciidoctor 文本:
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
如果使用 Gradle 构建,请使用以下构建文件:
plugins {
id "org.asciidoctor.convert" version "2.4.0"
id "java"
}
ext {
snippetsDir = file('build/generated-snippets')
ver = '2.0.4.RELEASE'
}
dependencies {
asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$ver"
testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$ver"
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
REST 文档生成
为了从一个现有的基于 Spring MVC 的项目中生成 REST 文档,您需要为您想要记录的每个请求/响应编写单元或集成测试,并在测试中包含JUnitRestDocumentation
规则。
例如,使用@SpringBootTest
定义一个测试,或者在测试的设置方法中设置应用上下文,并使用@Rule
定义一个JUnitRestDocumentation
的实例:
@RunWith(SpringRunner.class)
@SpringBootTest
public class GettingStartedDocumentation {
@Rule
public final JUnitRestDocumentation restDocumentation =
new JUnitRestDocumentation();
然后设置 MockMvc 实例
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc =
MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.alwaysDo(document("{method-name}/{step}/",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())))
.build();
}
使用以下静态导入
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static
org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
.documentationConfiguration;
对于使用mockMvc
的 JUnit 测试中的每个测试方法,Spring REST Docs 现在将(在构建期间)为每个 HTTP 请求创建一个目录,该目录通过将测试名称从 CamelCase 转换为破折号分隔的名称来命名(例如,creatingACourse
变成 creating-a-course)和一个数字索引目录。例如,如果一个测试中有四个请求,那么您将拥有目录1/ 2/ 3/
和4/
。每个 HTTP 请求依次获得以下生成的代码片段:
-
curl-request.adoc
-
httpie-request.adoc
-
http-request.adoc
-
http-response.adoc
-
request-body.adoc
-
response-body.adoc
然后,您可以在src/docs/asciidoc/
目录下编写 Asciidoctor 文档,并将生成的片段包含到您的输出中,例如:
include::{snippets}/creating-a-course/1/curl-request.adoc[]
This text is included in output.
include::{snippets}/creating-a-course/1/http-response.adoc[]
这将包括您的文档输出(通常是 HTML5 输出)中的每个前面的片段。
在 Spring Boot 提供文件服务
要在基于 Spring Boot 的项目中提供 HTML5 生成的文档,请将以下内容添加到 Gradle 构建文件中:
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
Spring 的海涛
与 REST 密切相关的是作为应用状态引擎的超媒体的概念 ( HATEOAS ),5,它概述了来自 web 服务的每个响应应该如何提供描述其他端点的信息或链接,就像网站如何工作一样。spring hate OAS6有助于启用这些类型的 RESTful web 服务。
入门指南
首先,将 Spring HATEOAS 依赖项添加到您的项目中。如果使用 Spring Boot 和 Maven,请添加以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
如果将 Spring Boot 与 Gradle 一起使用,请使用以下依赖关系:
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
创建链接
HATEOAS 的关键部分是链接,它可以包含 URI 或 URI 模板,允许客户端轻松导航 REST API,并提供未来的兼容性——客户端可以使用链接,允许服务器更改链接指向的位置。
Spring HATEOAS 提供了轻松创建链接的方法,比如LinkBuilder and WebMvcLinkBuilder
。它还提供了在响应中表示链接的模型,比如EntityModel, PagedModel, CollectionModel, and RepresentationModel
。使用哪种模型取决于返回哪种类型的数据一个实体(EntityModel),数据页(PagedModel),
或其他。
让我们举一个使用WebMvcLinkBuilder
和EntityModel:
的例子
package com.apress.spring_quick.rest;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
@RestController
public class GettingStartedController {
@GetMapping("/")
public EntityModel<Customer> getCustomer() {
return EntityModel.of(new Customer("John", "Doe"))
.add(linkTo(GettingStartedController.class).withSelfRel())
.add(linkTo(GettingStartedController.class)
.slash("next").withRel("next"));
}
}
在运行时,此端点将以 JSON 的形式返回以下内容(在本地运行时):
{
"firstname":"John",
"lastname":"Doe",
"_links":{
"self":{"href":"http://localhost:8080"},
"next":{"href":"http://localhost:8080/next"}
}
}
Hypertext Application Language
超文本应用语言(HAL) 7 是一个用于定义超媒体的标准草案,比如在 JSON 或 XML 代码中到外部资源的链接。该标准最初于 2012 年 6 月提出,专门用于 JSON,此后出现了两个版本,JSON 和 XML。两个关联的 MIME 类型是媒体类型:application/hal+xml
和媒体类型:application/hal+json
。HAL 由资源和链接组成。它可以有嵌入的资源,这些资源也有链接。例如,如果一门课程有许多测试,您可能会看到如下 HAL JSON:
{
"_links": {
"self": { "href": "http://localhost:8080/courses" },
"next": { "href": "http://localhost:8080/courses?page=2" },
"my:find": {
"href": "http://localhost:8080/courses/{?name}",
"templated": true
}
},
"total": 14,
"_embedded": {}
}
测试
测试 HATEOAS 输出的方法类似于测试任何生成 XML 或 JSON 的 web 服务。
在服务生成 JSON 的常见情况下,使用一个库来导航 JSON 会很有帮助,就像 XPath 导航 XML 文档一样, JsonPath 。一个用 Java 实现 JsonPath 的库是 Jayway JsonPath 。 8 虽然你可以直接使用它,但是 Spring 用静态MockMvcResultMatchers.jsonPath
方法包装了 JsonPath 的用法,以便于使用 Hamcrest 匹配器。
要使用 JsonPath,我们只需要在 Maven pom 中包含一个依赖项:
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
或者,如果使用 Gradle,包括
testCompile 'com.jayway.jsonpath:json-path:2.4.0'
例如,参见下面的 JUnit 测试类,它使用 JsonPath 来验证_links.self
和_links.next
不为空:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;
@ExtendWith(SpringExtension.class) // JUnit 5
@SpringBootTest
public class GettingStartedDocumentation {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.build();
}
@Test
public void index() throws Exception {
this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.self", is(notNullValue())))
.andExpect(jsonPath("_links.next", is(notNullValue())));
}
}
Listing 11-1GettingStartedDocumentation.java
www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
2
https://spring.io/projects/spring-hateoas
3
https://spring.io/projects/spring-restdocs
4
5
https://en.wikipedia.org/wiki/HATEOAS
6
https://spring.io/projects/spring-hateoas
7
https://tools.ietf.org/html/draft-kelly-json-hal-00
8
https://github.com/json-path/JsonPath
十二、反应器
反应器 1 是 Spring 的 reactive streams 实现(在版本 3 及以后)。它有两种主要的发布者类型,Flux<T>
和Mono<T>
。它使用调度程序来决定在哪个线程上运行每个操作。
Spring 框架在许多方面与 Reactor 集成,使其更容易与其他 Spring 项目(如 Spring Data 和 Spring Security)一起使用。Spring WebFlux 是一个 web 框架,很像 Spring MVC,但它是围绕反应流构建的,能够在 Netty 上运行,Netty 是一个非阻塞 I/O 客户机-服务器框架。
为什么使用 Reactor?
Reactor 和 reactive streams 的目的通常是使对大量数据的操作能够以最高效、可伸缩和最快的方式分解并在许多不同的线程(多线程)上执行。虽然使用 Java 8 的并行流可以简单地实现并行处理,但是反应式流增加了大量额外的功能和定制,比如错误处理、重试、缓存和重放流、处理背压等等。
您可以将一个反应流想象成有三个轨道,数据轨道、完成轨道(不管流是否已经完成)和错误轨道。此外,每个 rails 都可以转换成另一个 rails:完整的流可以被替换,操作可以抛出异常,或者异常可以被处理并用更多的数据替换。
此外,Reactor 还增加了上下文的概念,我们将在本章的后面探讨。
入门指南
如果您有一个 Maven 版本,请将以下内容添加到 pom 文件中:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.3.7.RELEASE</version>
<scope>test</scope>
</dependency>
对于 Gradle 构建,将以下内容添加到 Gradle 构建文件的依赖项中:
implementation 'io.projectreactor:reactor-core:3.3.7.RELEASE'
testImplementation 'io.projectreactor:reactor-test:3.3.7.RELEASE'
流量
Flux<T>
是反应器反应物流的主要入口。 2 Mono<T>
就像是一个Flux<T>
除了零或一个元素。Mono<T>
和Flux<T>
都执行org.reactivestreams.Publisher<T>
。
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
像任何反应流实现一样,Reactor 使用调度器来决定每个操作运行哪个线程。
Flux.range(1, 100)
.publishOn(Schedulers.parallel())
.subscribe(v -> doSomething(v));
Reactor 中的错误处理是通过流上的方法调用来实现的。以下方法可用于通量<T>
或单声道<T>
(为简洁起见,省略通用类型):
-
onErrorResume(Function)–接受异常并返回不同的发布者作为回退流或辅助流
-
onErrorMap(函数)–接受异常并允许您修改它,或者如果您愿意,返回一个全新的异常
-
onErrorReturn(T)-提供出现错误时使用的默认值
-
dooner error(Consumer extends Throwable>)——允许您在不影响底层流的情况下处理错误
错误(抛出的异常)总是结束一个流的事件,应该由订阅者来处理。然而,很多时候,如在前面的例子中,错误是不可能的,因此不需要处理。
单声道的
为什么有一个单独的类,称为 Mono,只有一个或零个元素?可以把它想象成 Java 8 的可选类到反应流世界的翻译。
Mono 与 Flux 非常相似,只是它有如下方法
-
justOrEmpty(T)
–采用可空值并转换成单声道。如果为 null,结果与 Mono.empty()相同。 -
justOrEmpty(Optional<? extends T>)
–取一个可选,直接转换成单声道。
与 Java 的可选不同,Mono 可以处理错误和其他事情。例如,返回 Mono 的方法可能会执行以下操作:
return Mono.error(new RuntimeException("your error"))
相应的代码可以像处理 Flux 一样处理来自单声道的错误(使用 onErrorResume、onErrorMap 或 onErrorReturn)。
创建通量或单声道
您可以从固定数据(冷数据)或以编程方式从动态数据(热数据)创建通量。
以下是产生冷流的一些不同方法:
-
从值列表中创建通量。
-
从一个迭代中产生一个通量。
-
创建一个从 1 到 64 的范围。
Flux<String> flux1 = Flux.just("a", "b", "foobar"); //1
List<String> iterable = Arrays.asList("a", "b", "foobar");
Flux<String> flux2 = Flux.fromIterable(iterable); //2
Flux<Integer> numbers = Flux.range(1, 64); //3
您可以创建一个空的或只有一个元素的简单单声道,如下所示:
-
创建一个空的单声道。
-
用一个元素创建一个单声道。
-
创建一个包装 RuntimeException 的单声道。
Mono<String> noData = Mono.empty(); //1
Mono<String> data = Mono.just("foo"); //2
Mono<String> monoError = Mono.error(new RuntimeException("error")); //3
您可以使用 generate、create 或 push 方法之一以编程方式创建通量。
generate 方法有多个重载定义,但是为了简单起见,让我们把重点放在接受一个 Supplier 和一个 BiFunction 的方法上。该函数将当前状态和 SynchronousSink 作为参数,后者可用于发布流的下一个状态。例如,以下代码使用 AtomicLong 实例从 0 到 10 递增数字,并提供每个数字的平方:
-
AtomicLong 的构造器被用作提供者。
-
递增后,将数字的平方提供给接收器。
-
10 之后,调用 complete,它调用任何订阅者的 onComplete,关闭流。create 方法使用 next、error 和 complete 方法公开一个 FluxSink。这允许您任意地将数据发布到一个 Flux 上。
Flux<Long> squares = Flux.generate(
AtomicLong::new, //1
(state, sink) -> {
long i = state.getAndIncrement();
sink.next(i * i); //2
if (i == 10) sink.complete(); //3
return state;
});
例如,下面演示了如何注册一个处理消息列表的 MessageListener:
Flux<String> bridge = Flux.create(sink -> {
messageProcessor.register(
new MessageListener<String>() {
public void handle(List<String> chunks) {
for(String s : chunks) {
sink.next(s);
}
}
public void processComplete() {
sink.complete();
}
public void processError(Throwable e) {
sink.error(e);
}
});
});
如果这里处理的消息有单线程源,可以用 push 方法代替 create 。
调度程序
reactor.core.scheduler 包下的 Schedulers 类提供了许多静态方法来提供调度程序,这些调度程序确定您的代码将在哪个或哪些线程上运行。
下面是一些静态方法及其含义:
-
schedulers . immediate()–当前线程。
-
schedulers . single()–单个可重用的线程。请注意,该方法对所有调用方重用同一个线程,直到调度程序被释放。如果您想要一个针对每个调用的专用线程,请对每个调用使用 Schedulers.newSingle()。
-
schedulers . elastic()–一个弹性线程池。它根据需要创建新的工作池,并重用空闲的工作池。闲置时间过长(默认值为 60 秒)的工作池将被释放。例如,对于 I/O 阻塞工作,这是一个很好的选择。Schedulers.elastic()是一种为阻塞进程提供自己的线程的简便方法,这样它就不会占用其他资源。
-
schedulers . parallel()–一个固定的工作池。它会创建与 CPU 核心数量一样多的工作线程。
-
schedulers . from Executor(Executor)–创建一个调度程序来使用给定的执行器,允许您使用 Java 执行器的丰富知识。
例如,让我们以生成正方形为例,让它并行运行:
-
首先,我们使用 Flux.range 获取从 1 到 64 的范围,并调用 flatMap(它采用一个 lambda 表达式,将范围内的每个值转换为一个新的反应器类型,在本例中为 Mono)。
-
使用 Schedulers.newSingle(name),我们为每个值创建一个新的单线程,传递给 subscribeOn 将导致映射表达式在该单线程上执行。请记住,我们在这里描述的是单声道的执行,而不是初始流量。
-
为了以防万一,我们提供了使用 doOnError 的异常处理代码。
-
使用 doOnComplete,当整个执行完成时,我们打印出“Completed”。
-
最后,我们订阅通量(没有这一步,什么都不会发生)并将结果添加到我们的正方形列表中。
List<Integer> squares = new ArrayList<>();
Flux.range(1, 64).flatMap(v -> // 1
Mono.just(v)
.subscribeOn(Schedulers.newSingle("comp")) //2
.map(w -> w * w))
.doOnError(ex -> ex.printStackTrace()) //3
.doOnComplete(() -> System.out.println("Completed")) //4
.subscribeOn(Schedulers.immediate())
.subscribe(squares::add); //5
这里我们再次看到在反应流中,任何东西都可以变成一个流,甚至是一个值。通过为范围内的每个值创建一个 Mono,我们能够使用 Reactor 来声明我们希望每个计算使用哪种线程。在这种情况下,由于我们使用了newSingle
,所有的处理都将通过一个新的线程对 64 个值中的每一个进行并行处理。
然而,这可能不是最有效的实现,因为创建大量线程会导致大量开销。相反,我们应该使用Schedulers.parallel()
,这样就可以精确地计算出 CPU 可以处理的线程数量。这样,Reactor 会为您处理细节。
拉事件
如果您有更多的“拉”的情况(事件是通过轮询一个源创建的),您可以使用FluxSink<T>
的 onRequest 方法。例如,以下代码创建了一个 Flux,用于轮询信道中的新事件:
-
当使用给定的数目发出请求时,轮询来自通道的事件。这个“n”是请求的项目数。
-
当通量被取消时,调用通道的
cancel
方法。 -
channel.close()
方法提供给onDispose
用于完成、出错或取消调用。 -
最后,将接收器的
next
方法注册为通道的侦听器。
Flux<String> bridge = Flux.create(sink -> {
sink.onRequest(n -> channel.poll(n)) // 1
.onCancel(channel::cancel) // 2
.onDispose(channel::close); // 3
channel.register(sink::next); // 4
});
请记住,onRequest 不会无缘无故被调用多次。反应器非常精确。
它将使用某个数字(比如 32)调用 onRequest,然后直到有大量的项目被发布到 Flux 时才再次调用它(即在 sink.next 被调用 32 次之后)。
本章中使用的代码示例可以在网上找到。3
处理背压
像所有反应流的实现一样,Reactor 具有处理背压的能力。只需在通量(或其他未列出的通量)上使用以下方法之一来指定您想要使用的背压策略:
-
onBackpressureBuffer()–缓冲所有项目,直到它们可以被下游处理。
-
onBackpressureBuffer(maxSize)–最多缓冲给定计数的项目。
-
onBackpressureBuffer(maxSize,BufferOverflowStrategy)-将项目缓冲到给定的计数,并允许您指定 BufferOverflowStrategy,例如 onBackpressureBuffer(100,bufferoverflow strategy。DROP_OLDEST)。
-
onbackpressurelast()–类似于只保存最后添加的项目的缓冲区。如果下游没有跟上上游,那么只会给下游最新的元素。
-
onBackpressureError()–如果上游生成的项目多于下游请求的项目,则通过 Exceptions.failWithOverflow()中的 IllegalStateException 错误(调用下游订阅者的 onError)结束流量。
-
onBackpressureDrop()–删除超出请求范围的所有项目。
-
onBackpressureDrop(Consumer)–丢弃超出请求的任何项目,并为每个丢弃的项目调用给定的使用者。
对于这些方法中的每一种,只有当项目在流上产生的速度快于下游(订户)可以处理的速度时,该策略才适用。如果不是这种情况,例如,对于冷流,没有背压策略是必要的。
还要记住,反应器并不神奇,在考虑背压策略时应该小心。
背压是指当流中的事件/数据过多,下游无法处理时所发生的情况。打个比方,想想在一些城市的高峰时段,当交通陷入停滞时,或者当地铁列车满员时,会发生什么。反压力是一种减慢速度的反馈机制。4
语境
Reactor 附带了一个与ThreadLocal
有些类似的高级特性,但它应用于一个Flux
或一个Mono
,而不是一个线程:??。
Reactor 的Context
很像一个不可变的映射或键/值存储。它是从订阅者到订阅者透明地存储的。
上下文是特定于反应器的,不与其他反应流实现一起工作。
当设置上下文时,您不应该在流程开始时定义它。例如,不要这样做(如您所料,上下文在下游将不可用):
// this is WRONG!
Flux<Integer> flux = Flux.just(1).subscriberContext(Context.of("pid", 12));
相反,您应该将它定义到末尾,因为它会沿着链“向后”传播,例如:
Flux<Integer> flux = Flux.just(1);
Flux<String> stringFlux = flux.flatMap(i ->
Mono.subscriberContext().map(ctx -> i + " pid: " +
ctx.getOrDefault("pid", 0)));
// supply context here:
StepVerifier.create(stringFlux.subscriberContext(Context.of("pid", 123)))
.expectNext("1 pid: 123")
.verifyComplete();
前面的代码使用 StepVerifier (我们将在接下来介绍)来验证我们是否获得了预期的值。
注意我们如何在 Mono 上使用静态方法Mono.subscriberContext()
来访问上下文。
Reactor 拥有出色的在线文档。 5
测试
自动化测试总是一个好主意,如果有工具来直接测试反应流就更好了。幸运的是,Reactor 附带了一些专门用于测试的元素,这些元素被收集到我们在本章开始时包含的它们自己的工件中:reactor-test。
反应器测试的两个主要用途如下:
-
使用 StepVerifier 测试序列是否遵循给定的场景
-
生成数据,以便用 TestPublisher 测试下游操作符(包括您自己的操作符)的行为。
步骤验证器
Reactor 的 StepVerifier 可以用来验证 Reactor 发布者的行为(Flux 或 Mono)。
下面是一个利用 StepVerifier 进行 JUnit 测试的简单示例:
-
创建一个
Mono
包装一个RuntimeException
模仿一个实际的错误状态。 -
创建一个
StepVerifier
包装单声道。 -
声明一个
onError
事件是预期的,并且异常的错误消息是“error”。 -
我们称之为
verify()
结尾。如果有任何期望没有实现,这将抛出一个AssertionError
。
@Test
public void testStepVerifier_Mono_error() {
Mono<String> monoError = Mono.error(new RuntimeException("error")); //1
StepVerifier.create(monoError) //2
.expectErrorMessage("error") //3
.verify(); //4
}
接下来,我们将创建一个只有一个字符串的单声道并验证它:
-
创建一个单声道包装一个值,“foo”。
-
创建一个包装单声道的 StepVerifier。
-
用“foo”调用 Expect onNext。
-
调用 verifyComplete()的效果与 verify()相同,但也要求调用 onComplete。
@Test public void testStepVerifier_Mono_foo() {
Mono<String> foo = Mono.just("foo"); //1
StepVerifier.create(foo) //2
.expectNext("foo") //3
.verifyComplete(); //4
}
这里,我们将使用三个值测试流量,如果测试时间过长,将会超时:
-
创造一个只有三个数字的通量。
-
创建包裹焊剂步进检验器。
-
为每个预期值调用 expectNext。
-
调用 expectComplete 以期望调用 onComplete。
-
最后,必须在最后调用 verify()。这种验证变化采用持续时间超时值。在这里,是 10 秒。在发布者可能永远不会调用 onComplete 的情况下,这有助于防止测试挂起。
@Test public void testStepVerifier_Flux() {
Flux<Integer> flux = Flux.just(1, 4, 9); //1
StepVerifier.create(flux) //2
.expectNext(1) //3
.expectNext(4)
.expectNext(9)
.expectComplete() //4
.verify(Duration.ofSeconds(10)); //5
}
测试发布者
TestPublisher<T>
类提供了为测试目的提供微调数据的能力。TestPublisher<T>
是一个反应流发布者,但是可以使用 flux()或 mono()方法转换成 Flux 或 Mono。
TextPublisher 有以下方法:
-
下一个(T)和下一个(T,T…)–触发 1-n onNext 信号
-
发出(T…)-与 next 相同,也以 onComplete 信号结束
-
完成
-
error(Throwable)-以 onError 信号终止。
下面演示了如何使用TestPublisher<T>
:
-
创建 TestPublisher 实例。
-
将其转化为通量。
-
创建新列表。出于测试目的,我们将使用该列表从发布者处收集值。
-
使用 onNext 和 onError 的两个 lambda 表达式订阅发布服务器。这将把发布者发出的每个值添加到列表中。
-
最后,从 TestPublisher 发出值“foo”和“bar”。
-
断言列表的大小是预期的 2。
TestPublisher<Object> publisher = TestPublisher.create(); //1
Flux<Object> stringFlux = publisher.flux(); //2
List list = new ArrayList(); //3
stringFlux.subscribe(next -> list.add(next),
ex -> ex.printStackTrace()); //4
publisher.emit("foo", "bar"); //5
assertEquals(2, list.size()); //6
assertEquals("foo", list.get(0));
assertEquals("bar", list.get(1));
注意,在发出任何值之前,您必须订阅TestPublisher
(在前面的例子中是通过订阅stringFlux
来完成的)。
元组和 Zip
元组是两个或更多元素的强类型集合,Reactor 内置了它们。一些操作如zipWith
返回元组的反应流。
Flux 有一个实例方法zipWith(Publisher<? extends T2> source2)
,它的返回类型为Flux<Tuple2<T,T2>>
。它等待两个通量(初始通量和源 2)发射一个元素,然后将两者组合成一组 2。还有一个静态方法 Flux.zip,它被重载以接受 2 到 8 个发布者,并将它们压缩成元组。
当您想要执行返回反应结果(通量或单声道)的多个操作并组合它们时,压缩非常有用。
Mono 有两种主要的压缩方式(非静态方法,都有一个返回类型Mono<Tuple2<T,T2>>
):
-
zipWith(Mono<? extends T2> other)
–用另一个流压缩当前流,以元组 2 的形式给出每个对应元素的组合。 -
zipWhen(Function<T,Mono<? extends T2>> rightGenerator)
–将当前单声道与另一个单声道压缩,以 Tuple2 的形式给出每个对应元素的组合,但仅在第一个流的操作完成后,从而允许您使用第一个单声道的结果来生成第二个单声道。
例如,假设您有两个执行异步操作的方法 Mono getCourse(Long id)和 MonogetStudentCount(Course Course ),假设您想从课程 id 中获取学生人数,您可以执行以下操作:
Mono<Integer> getStudentCount(Long id) {
return getCourse(id)
.zipWhen(course -> getStudentCount(course))
.map(tuple2 -> tuple2.getT2());
}
这是一个简单的例子,但是您可以想象组合两个不同的实体,或者在返回之前对它们执行逻辑,或者调用另一个带有两个参数的方法,等等。
反应器附件
Project Reactor 在io.projectreactor.addons
groupId 下提供额外的功能。Reactor extra 包括额外的数学函数,不同的重试方式,包括抖动和后退,以及 TupleUtils。
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
<version>3.3.3.RELEASE</version>
</dependency>
对于 Gradle 构建,将以下内容添加到 Gradle 构建文件的依赖项中:
implementation 'io.projectreactor.addons:reactor-extra:3.3.3.RELEASE'
当您的应用在一个集成点失败时,比如调用另一个 RESTful 服务时,为了使您的整个系统可靠,您可能需要重试调用几次。但是,为了防止失败的服务过载,您应该采用回退或增加每次重试之间的时间,以及抖动,随机修改时间,以便来自许多不同实例的重试不会同时发生(相关)。例如,看一下下面的代码:
-
我们用 IOException 的异常值创建 Retry,这意味着只有在抛出异常时才会重试(这里可以提供任何异常类;例子只有 IOException)。
-
我们将指数回退定义为初始值为 100 毫秒,最大值为 60 秒。
-
我们添加了随机抖动,并将重试最大值设置为 5,这意味着它最多重试五次。
-
我们添加了 Spring ApplicationContext,并使用它在每次失败后应用回滚。
-
最后,我们在一个 Flux 实例上调用 retryWhen(retry ),对该 Flux 应用重试。
var retry = Retry.anyOf(IOException.class) \\1
.exponentialBackoff(Duration.ofMillis(100), \\2
Duration.ofSeconds(60))
.jitter(Jitter.random()) \\3
.retryMax(5)
.withApplicationContext(appContext) \\4
.doOnRetry(context ->
context.applicationContext().rollback());
return flux.retryWhen(retry); \\5
关于重试、退避和抖动的更多信息,请参见亚马逊构建者图书馆的这篇优秀的文章。 6
2
如果你熟悉的话,通量类似于 RxJava 中的可流动的或可观察的。
3
https://github.com/adamldavis/spring-quick-ref
4
https://reactivemanifesto.org/glossary#Back-Pressure
5
https://projectreactor.io/docs
6
https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
十三、Spring Integration
Spring Integration 是一个支持众所周知的企业集成模式的编程模型。
特征
Spring Integration 实现了许多常见的企业集成模式, 1 比如通道、聚合器、过滤器和转换器,并提供了许多不同消息传递实现的抽象。
图 13-1
企业集成
Spring Integration 提供了一个消息传递范例,将一个应用分成多个组件,这些组件在相互不了解的情况下进行通信。除了将细粒度组件连接在一起,Spring Integration 还提供了许多通道适配器和网关来与外部系统通信。通道适配器用于单向集成(发送或接收),网关用于请求/回复场景(入站或出站)。
支持的消息传递包括但不限于
-
REST/HTTP
-
FTP/SFTP
-
推特
-
Web 服务(SOAP)
-
TCP/UDP
-
(同 JavaMessageService)Java 消息服务
-
拉比特
-
电子邮件
SpringCloud 集成
Spring Cloud Stream 项目建立在 Spring Integration 之上,Spring Integration 被用作消息驱动微服务的引擎。这在第十八章中有所涉及。
入门指南
最简单的开始方式是使用 Spring Initializr 或 Spring Boot CLI 创建一个新项目(它们将在第十五章中介绍)。在现有项目中,添加以下依赖项:
implementation 'org.springframework.boot:spring-boot-starter-integration'
testImplementation 'org.springframework.integration:spring-integration-test'
然后还包括您的项目需要的任何其他 Spring Boot 启动器或其他库,例如:
implementation 'org.springframework.boot:spring-boot-starter-amqp'
testImplementation 'org.springframework.amqp:spring-rabbit-test'
这带来了用于 AMQP 的 Spring Boot 启动器和用于测试与 RabbitMQ 集成的 spring-rabbit-test。
然后,在 Spring Boot 应用中,将@ EnableIntegration
注释添加到您的一个配置类中,这将执行以下操作:
-
注册一些内置的 beans,比如
errorChannel
及其LoggingHandler
、taskScheduler
用于轮询器、jsonPath
SpEL-function 等等。 -
添加几个
BeanFactoryPostProcessor
实例。 -
添加几个
BeanPostProcessor
实例来增强或转换和包装特定的 beans,以便进行集成。 -
添加注释处理器来解析消息传递注释,并在应用上下文中为它们注册组件。
您还可以使用@IntegrationComponentScan
来扫描类路径,寻找特定于 Spring Integration 的注释,比如@MessagingGateway
注释。
综上所述,您的主应用类可能如下所示:
@EnableIntegration
@IntegrationComponentScan
@SpringBootApplication
public class SpringIntApplication {
public static void main(String[] args) {
SpringApplication.run(SpringIntApplication.class, args);
}
}
添加附加支持
一般来说,当您想要将 Spring Integration 与特定的技术(比如 JPA)结合使用时,您可以在 org . Spring framework . Integration groupId 下包含名为 spring-integration- X 的附加构件,例如,对于 Kafka: 2
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-kafka</artifactId>
<version>3.3.0.RELEASE</version>
</dependency>
一些可用的支持:
| 作业的装配区(JobPackArea) | 超文本传送协议 | 数据库编程 | (同 JavaMessageService)Java 消息服务 | | 邮件 | MongoDB | 卡夫卡 | 使用心得 | | 资源 | 无线电磁指示器(Radio Magnetic Indicator 的缩写) | 窝 | science for the people 为人类服务的科学 | | 跺脚 | 溪流 | 系统记录 | TCP 和 UDP (ip) | | webflux | 网络服务 | 可扩展置标语言 | XMPP |消息网关
消息网关是在现有消息传递技术上使用的抽象 Spring Integration,它允许您的代码与接口进行交互,而无需了解底层通道。当您用@ MessagingGateway
注释一个接口,用@ Gateway
注释一个或多个方法时,Spring 在运行时使用来自您包含的支持工件的底层技术用代理实现接口。
例如,对于 Kafka 消息网关:
-
定义 requestChannel 来发送数据,在本例中是一个字符串负载。
-
使用
@Header
定义一个头,在本例中,消息有效负载将被发送到 Kafka 主题。 -
定义 replyChannel,可用于从 Kafka 获取消息。注意,返回类型是 Spring 的 Message 接口,这是一个抽象,可以用于任何消息传递系统。replyTimeout 以毫秒为单位,所以这里是 10 秒。
//Use the following imports:
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.annotation.Gateway;
import org.springframework.kafka.support.KafkaHeaders;
@MessagingGateway
public interface KafkaGateway {
@Gateway(requestChannel = "toKafka.input") \\1
void sendToKafka(String payload,
@Header(KafkaHeaders.TOPIC) String topic); \\2
@Gateway(replyChannel = "fromKafka", replyTimeout = 10000) \\3
Message<?> receiveFromKafka();
}
假设一切都设置正确,Spring Integration 在运行时将 KafkaGateway 接口实现为 Spring Bean,因此可以通过以下方式调用它:
KafkaGateway kafkaGateway = context.getBean(KafkaGateway.class);
String message = "any message";
String topic = "topic";
kafkaGateway.sendToKafka(message, topic);
集成流程
创建流有两种主要方法(实现 IntegrationFlow 接口),要么使用 lambda 表达式,要么使用从IntegrationFlows
类开始的 fluent builder DSL。
在第一个实例中,我们利用了IntegrationFlow
是一个 SAM(单一抽象方法)接口的事实,因此可以提供一个带有一个参数的 lambda 表达式,Java 将知道它从返回类型实现了该接口,例如:
@Bean
public IntegrationFlow toKafka(KafkaTemplate<?, ?> kafkaTemplate) {
return flowDefinition -> flowDefinition
.handle(Kafka.outboundChannelAdapter(kafkaTemplate)
.messageKey("si.key"));
}
IntegrationFlows
类可以用来创建一个IntegrationFlow
,例如:
@Bean
public IntegrationFlow fromKafkaFlow(
ConsumerFactory<?, ?> consumerFactory) {
return IntegrationFlows
.from(Kafka.messageDrivenChannelAdapter(consumerFactory, topic))
.channel((Channels c) -> c.queue("fromKafka"))
.get();
}
静态方法IntegrationFlows.from
返回一个扩展了IntegrationFlowDefinition
的IntegrationFlowBuilder
,并有一个“get()”方法返回一个新的StandardIntegrationFlow
实例。IntegrationFlowDefinition
上的方法可以让你流畅地构建一个IntegrationFlow
,包括以下内容:
-
aggregate——abstractcorrelationmessagehandler 的特定于聚合器的实现,abstractcorrelationmessagehandler 是一个消息处理程序,它在 MessageStore 中保存相关消息的缓冲区。它负责可以批量完成的相关消息组。
-
barrier——一个消息处理程序,它挂起线程,直到具有相应相关性的消息被传递到触发器方法或超时发生。
-
bridge——一个简单的 MessageHandler 实现,它将请求消息直接传递到输出通道,而不修改它。该处理程序的主要目的是将 PollableChannel 连接到 SubscribableChannel,反之亦然。
-
通道–定义发送消息的方法。
-
claimcheck in–使用提供的 MessageStore 填充 ClaimCheckInTransformer 的 MessageTransformingHandler。
-
claimcheck out–使用提供的 MessageStore 填充 ClaimCheckOutTransformer 的 MessageTransformingHandler。
-
Control Bus–在当前 IntegrationFlow 链位置填充特定于控制总线 EI 模式的 MessageHandler 实现。
-
convert–为运行时要转换的提供的 payloadType 填充 MessageTransformingHandler 实例。
-
delay–将 DelayHandler 填充到当前的集成流位置。
-
enrich–使用提供的选项将 ContentEnricher 填充到当前集成流位置。ContentEnricher 是一个消息转换器,可以用动态或静态值增加消息的有效负载。
-
enrich headers–填充 MessageTransformingHandler,将静态配置的头值添加到消息中。
-
filter–如果消息通过给定的 MessageSelector,则 MessageFilter 仅传递到过滤器的输出通道。
-
fixedSubscriberChannel–在当前 IntegrationFlow 链位置填充 fixedSubscriberChannel 的一个实例(在 bean 实例化过程中为单个最终订户设置的专用 SubscribableChannel)。
-
Flux transform–填充一个 FluxMessageChannel 以启动对上游数据的反应式处理,将其包装到一个 Flux,通过 Flux.transform(Function)应用所提供的函数,并将结果发送到下游流中订阅的另一个 FluxMessageChannel。
-
gateway–为提供的子流或通道填充“人工”GatewayMessageHandler。
-
handle–为提供的 MessageHandler 或 MessageProcessorSpec bean 和方法名填充 ServiceActivatingHandler。
-
headerFilter–为当前的 StandardIntegrationFlow 提供 header filter。
-
log–填充当前消息通道的窃听,并使用 LoggingHandler,这是一个简单记录消息或其有效负载的 MessageHandler 实现。
-
Logan reply——该操作符只能在流的末尾使用。与“日志”方法相同。返回
IntegrationFlow
。 -
null channel–将 bean 作为终端操作符添加到该流定义中。返回
IntegrationFlow
。 -
publishSubscribeChannel——
PublishSubscribeChannel
(向每个订户发送消息的通道)BaseIntegrationFlowDefinition.channel(java.lang.String)
方法——允许使用“子流”订户功能的特定实现。 -
重新排序–填充一个重新排序 MessageHandler,它使用 MessageStore 中相关消息的缓冲区对消息进行重新排序。
-
路线——这种方法有许多不同的变体。它们填充 MethodInvokingRouter,或者如果提供了 SpEL 表达式,则填充 ExpressionEvaluatingRouter,然后确定要使用的 MessageChannel 或通道名称。
-
routeByException–可以按异常类型路由消息。
-
route torecipients–使用 RecipientListRouterSpec 中的选项填充 RecipientListRouter,RecipientListRouterSpec 在多个通道上发送消息。
-
scatterGather 根据为分散函数提供的 MessageChannel 和为收集函数提供的 AggregatorSpec,将 ScatterGatherHandler 填充到当前的集成流位置。
-
split–使用提供的 SpEL 表达式填充 MethodInvokingSplitter 以在运行时评估服务提供的方法,或者填充 ExpressionEvaluatingSplitter。分割器将消息分割成多个消息。
-
transform–为提供的 GenericTransformer 填充 MessageTransformingHandler 实例。
-
trigger–填充 ServiceActivatingHandler 实例以执行 MessageTriggerAction。
-
wireTap–将 Wire Tap EI 模式特定的 ChannelInterceptor 实现填充到 currentMessageChannel。
这绝不是详尽无遗的。这些方法中的大多数都有几个重载的变体。
Kafka 配置
然后,您可以在spring.kafka.consumer
和spring.kafka.producer
下的application.yml
中配置 Spring Integration Kafka 的特定设置,例如:
spring:
kafka:
consumer:
group-id: siTestGroup
auto-offset-reset: earliest
enable-auto-commit: false
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
batch-size: 16384
buffer-memory: 33554432
retries: 0
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
Listing 13-1application.yml
安装卡夫卡留给读者作为练习。前往
https://kafka.apache.org/quickstart
并按照指示进行操作。然后按照本章的内容设置 Spring Integration。
主题
因为我们使用的是卡夫卡,所以我们也需要首先创建主题。
由于我们使用的是带自动配置的 Spring Boot,如果我们提供 NewTopic Spring Beans,Spring Boot 的自动配置 KafkaAdmin(来自spring-integration-kafka)
)将为我们提供主题,例如:
@Bean
public NewTopic topic() {
return new NewTopic("topic", 5, (short) 1);
}
@Bean
public NewTopic newTopic() {
return new NewTopic("topic2", 5, (short) 1);
}
这将创建名为“topic”和“topic2”的两个主题,复制 1 个(意味着只存储一个副本)和 5 个分区,这意味着数据将被分成 5 个分区。
监控
默认情况下,如果存在一个千分尺meterRegistry
bean,这将是一个包括 Spring 致动器的 Spring Boot 项目的情况,Spring Integration 度量将由千分尺管理。如果您希望使用遗留的 Spring Integration 指标,可以向应用上下文添加一个DefaultMetricsFactory
(来自 Spring Integration) bean。
www.enterpriseintegrationpatterns.com/
2
https://kafka.apache.org/quickstart
十四、SpringBatch
Spring Batch 是一个为企业系统支持长时间运行的数据转换或类似的长时间运行过程的项目。它有大量的特性,其中一些我们将会谈到。
特征
Spring Batch 提供了分区和处理大量数据的特性。它还提供了在处理大量记录时必不可少的可重用功能,包括事务管理、作业处理统计、作业重启、重试和跳过、日志记录和跟踪以及资源管理。
概观
在大图中,Spring Batch 由 JobLauncher、JobRepository、Jobs、Steps、ItemReaders、ItemProcessors 和 ItemWriters 组成。
JobLauncher 使用给定的作业参数运行作业。每个作业可以有多个步骤。每个步骤通常由一个 ItemReader、ItemProcessor 和 ItemWriter 组成。使用 JobRepository 保存和加载元数据,或关于每个实体状态的信息。
这个例子
为了演示 Spring Batch,我们将使用一个示例。在本例中,我们将使用一个简单的课程定义。Spring Batch 将用于加载定义课程的 CSV 文件,转换值,并将新的课程行保存到数据库中。
建设
为了简单起见,我们将使用 Spring Boot(这将在下一章更全面地介绍)。首先,我们将使用 spring-batch 定义一个 Gradle 构建,然后我们将讨论 Maven 构建。
Gradle Build
创建一个名为build.
gradle
的文件,内容如下:
plugins {
id 'org.springframework.boot' version '2.3.0.RELEASE' //1
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-batch' //2
runtimeOnly 'org.hsqldb:hsqldb'
testImplementation('org.springframework.boot:spring-boot-starter-test')
{
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.springframework.batch:spring-batch-test' //3
}
test {
useJUnitPlatform() //4
}
-
我们为 Spring Boot 和 Spring 依赖管理应用插件,这允许我们在依赖块中删除版本。
-
这一行定义了 spring-boot-starter-batch,它引入了 Spring Batch 所需的所有 jar。在下一行,我们包含 hsqldb 1 作为数据库。
-
还有一个专门用于测试 Spring Batch 的库,spring-batch-test。
-
这一行告诉 Gradle 使用 JUnit 5 进行测试。
Maven 构建
使用以下内容创建一个名为“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.3.0.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>batch-processing</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>batch-processing</name>
<description>Demo project for Spring Boot, Batch</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>org.hsqldb</groupId>
<artifactId>hsqldb</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.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>
除了标准的 Spring Boot Maven 构建,我们还包括 hsqldb(数据库)、spring-boot-starter-batch 和 spring-batch-test。
由于 Spring Batch 通常涉及到与数据库的交互,并且默认情况下将元数据保存到数据库中,因此 Spring Batch 的启动依赖于
spring-boot-starter-jdbc
。
计划
由于 spring-boot-starter-jdbc 位于类路径中,并且我们已经包含了一个数据库(hsqldb ),所以初始化我们的数据库唯一需要做的就是在 src/main/resources/下包含一个名为 schema-all.sql 的文件。创建此文件并添加以下内容:
DROP TABLE course IF EXISTS
;
CREATE TABLE course (
course_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
title VARCHAR(200),
description VARCHAR(250)
);
课程
我们将课程实体定义为具有标题和描述的典型领域类(POJO ):
public class Course {
private String title;
private String description;
public Course() {
}
public Course(String title, String description) {
this.title = title;
this.description = description;
}
//getters and setters...
@Override
public String toString() {
return "title: " + title + ", description: " + description;
}
}
课程处理器
Spring Batch 提供了ItemProcessor<I,O>
接口(I 代表输入,O 代表输出),用于在需要以某种方式修改或处理实体时实现逻辑。
在这种情况下,我们定义了一个实现ItemProcessor<I,O>
的CourseProcessor
,它用一个空格替换任意数量的空格,并修剪任何前导或尾随空格:
-
我们声明 CourseProcessor 实现了 ItemProcessor 接口,当然,in 和 out 类型是相同的。如果它们不同,第一个声明的类型将声明要处理的参数的类型,第二个类型将是返回类型。
-
这里,我们在标题和描述中都使用 replaceAll(使用正则表达式\s+)将任何空格替换为一个空格。我们创建一个新对象,这样处理器就是幂等的——它不应该修改输入对象。
-
最后,我们从 process 方法返回新的课程实例。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;
public class CourseProcessor implements ItemProcessor<Course, Course> { //1
private static final Logger log =
LoggerFactory.getLogger(CourseProcessor.class);
@Override
public Course process(final Course course) throws Exception {
final String title = course.getTitle()
.replaceAll("\\s+", " ").trim(); //2
final String description = course.getDescription()
.replaceAll("\\s+", " ").trim();
final Course transformedCourse = new Course(title, description);
log.info("Converting (" + course + ") into (" + transformedCourse + ")");
return transformedCourse; //3
}
}
批量配置
最后,我们定义了一个@Configuration,它定义了 Spring Batch 将自动运行的步骤和作业。虽然在这种情况下我们有一个作业和一个步骤,但是也可能有多个作业和每个作业的一个或多个步骤。如果存在多个作业,您可以指定哪个或哪些作业作为属性运行(spring.batch.job.names
)。
-
@
EnableBatchProcessing
启用 Spring Batch 的自动配置,提供默认的JobRepository
、JobBuilderFactory
、StepBuilderFactory
等 Spring beans。 -
我们创建一个
FlatFileItemReader<T>
,它是 Spring Batch 提供的众多助手类之一。这里,我们定义从哪个文件中读取,并使用一个BeanWrapperFieldSetMapper<T>
,我们定义在Course
上设置哪些字段(使用 Java Bean 标准)。 -
我们创建一个
JdbcBatchItemWriter<T>
,它将记录插入到我们的数据库中。 -
使用
StepBuilderFactory
,我们创建一个步骤,该步骤将分十个过程进行处理(一次十个)。为了提高效率和性能,数据以块的形式进行处理。如果块中发生任何错误,整个块都将回滚。 -
我们使用 JobBuilderFactory 定义作业。
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.*;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
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 javax.sql.DataSource;
@Configuration
@EnableBatchProcessing //1
public class BatchConfiguration {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Bean
public FlatFileItemReader<Course> reader() { //2
return new FlatFileItemReaderBuilder<Course>()
.name("personItemReader")
.resource(new ClassPathResource("sample-data.csv"))
.delimited()
.names(new String[]{"title", "description"})
.fieldSetMapper(new BeanWrapperFieldSetMapper<Course>() {{
setTargetType(Course.class);
}})
.build();
}
@Bean
public CourseProcessor processor() {
return new CourseProcessor();
}
@Bean
public JdbcBatchItemWriter<Course> writer(DataSource dataSource) { //3
return new JdbcBatchItemWriterBuilder<Course>()
.itemSqlParameterSourceProvider(new
BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO course (title, description) VALUES" +
" (:title, :description)")
.dataSource(dataSource)
.build();
}
@Bean
public Step readAndSaveStep(JdbcBatchItemWriter<Course> writer, //4
CourseProcessor processor) {
return stepBuilderFactory.get("saveStep")
.<Course, Course>chunk(10)
.reader(reader())
.processor(processor)
.writer(writer)
.build();
}
@Bean
public Job importCourseJob(JobCompletionListener listener, Step step) {
return jobBuilderFactory.get("importCourseJob") //5
.incrementer(new RunIdIncrementer())
.listener(listener)
.flow(step)
.end()
.build();
}
}
Listing 14-1BatchConfiguration.java
对于本例,文件 sample-data.csv 可能如下所示(注意将被删除的多余空格):
Java 11, Java 11 for beginners
Java Advanced, Advanced Java course
Spring , Course for Spring Framework
JobExecutionListener
Spring Batch 发布可以使用JobExecutionListener
监听的事件。例如,下面的类JobCompletionListener
实现了afterJob
方法,并仅在作业完成时打印出一条消息:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.stereotype.Component;
@Component
public class JobCompletionListener extends JobExecutionListenerSupport {
private static final Logger log =
LoggerFactory.getLogger(JobCompletionListener.class);
@Override
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
log.info("JOB FINISHED!");
}
}
}
JobExecutionListenerSupport
类实现了JobExecutionListener
。这允许我们实现接口,并且只定义afterJob
方法。
Spring 批处理元数据
Spring Batch 可以自动存储关于每个批处理执行的元数据作为审计记录,并帮助重启或事后分析错误。
Spring 批处理元数据表与用 Java 表示它们的域对象非常匹配。例如,JobInstance、JobExecution、JobParameters 和 StepExecution 分别映射到BATCH_JOB_INSTANCE
、BATCH_JOB_EXECUTION
、BATCH_JOB_EXECUTION_PARAMS
和BATCH_STEP_EXECUTION
。执行上下文映射到BATCH_JOB_EXECUTION_CONTEXT
和BATCH_STEP_EXECUTION_CONTEXT.
使用 Spring Boot,您可以确保使用以下属性创建该模式(创建表):
spring.batch.initialize-schema=always
默认情况下,只有当您使用嵌入式数据库时,它才会创建表。同样,您甚至可以使用
spring.batch.initialize-schema=never
Spring 重试
通常,在运行批处理过程时,如果某个操作失败,您可能希望自动重试,多次尝试相同的操作。例如,可能会出现暂时的网络故障,或者数据库出现暂时的问题。这是一个普遍期望的特征;Spring 开发了Spring Retry2项目,通过 AOP 或者编程来实现这个横切特性。
要开始使用 spring-retry,首先将其包含在构建中:
| `Maven` | ```org.springframework.retry``spring-retry``1.3.0``` | | `Gradle` | `implementation 'org.springframework.retry:spring-retry:jar:1.3.0'` |然后,为了使用声明式/AOP 方法,将@EnableRetry
注释添加到您的一个 Java 配置类中(这告诉 Spring 扫描@Retryable 注释):
@Configuration
@EnableBatchProcessing
@EnableRetry
public class BatchConfiguration {
或者在命令式(编程式)方法中使用 Spring Retry,直接使用 RetryTemplate,例如:
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(RemoteAccessException.class)
.build();
template.execute(ctx -> {
// ... some code
});
在本例中,只有在抛出 RemoteAccessException 时,执行的代码才会重试三次,并且每次都会后退一秒(1000 毫秒)。
重试条款
| 最大尝试次数 | 最大重试次数。 | | 固定补偿 | 增加重试之间暂停的时间(毫秒)。 | | 指数后退 | 当有问题的系统由于过饱和而停机时,用于以指数方式增加重试之间的暂停时间(以毫秒为单位)的参数可以更好地解决问题。 | | 随机后退 | 最好包含随机性(从 0%到 200%的延迟时间)以避免重试的相关性(这样一堆节点就不会同时重试)。 |可重试注释
使用 AOP 方法,您可以用@Retryable
(在配置类上使用@EnableRetry
之后)注释 Spring 自省的任何方法(在 Spring bean 的公共方法上)。例如,让我们修改之前的 CourseProcessor,最多重试四次:
@Retryable(maxAttempts = 4, backoff =
@Backoff(random = true, delay = 100))
@Override
public Course process(final Course course) throws Exception {
// code...
return transformedCourse;
}
注意我们是如何使用@Backoff 注释设置回退的。
2