From 3ceb52f955a2c1743d82098064496d32452211eb Mon Sep 17 00:00:00 2001 From: 58148590088075383 <723423272@qq.com> Date: Mon, 7 Mar 2022 18:07:54 +0800 Subject: [PATCH] blue MT translated --- docs/spring-amqp/spring-amqp.md | 6964 ++++++++++++++ docs/spring-batch/appendix.md | 48 + docs/spring-batch/common-patterns.md | 611 ++ docs/spring-batch/domain.md | 292 + docs/spring-batch/glossary.md | 81 + docs/spring-batch/job.md | 1064 +++ docs/spring-batch/jsr-352.md | 312 + docs/spring-batch/monitoring-and-metrics.md | 66 + docs/spring-batch/processor.md | 299 + docs/spring-batch/readersAndWriters.md | 2264 +++++ docs/spring-batch/repeat.md | 158 + docs/spring-batch/retry.md | 225 + docs/spring-batch/scalability.md | 334 + docs/spring-batch/schema-appendix.md | 292 + docs/spring-batch/spring-batch-integration.md | 1091 +++ docs/spring-batch/spring-batch-intro.md | 299 + docs/spring-batch/step.md | 1869 ++++ docs/spring-batch/testing.md | 274 + docs/spring-batch/transaction-appendix.md | 230 + docs/spring-batch/whatsnew.md | 128 + docs/spring-credhub/spring-credhub.md | 518 ++ docs/spring-flo/spring-flo.md | 260 + docs/spring-hateoas/spring-hateoas.md | 2012 ++++ docs/spring-rest-docs/spring-restdocs.md | 2443 +++++ docs/spring-shell/spring-shell.md | 754 ++ .../spring-statemachine.md | 8100 +++++++++++++++++ docs/spring-vault/spring-vault.md | 2089 +++++ 27 files changed, 33077 insertions(+) create mode 100644 docs/spring-amqp/spring-amqp.md create mode 100644 docs/spring-batch/appendix.md create mode 100644 docs/spring-batch/common-patterns.md create mode 100644 docs/spring-batch/domain.md create mode 100644 docs/spring-batch/glossary.md create mode 100644 docs/spring-batch/job.md create mode 100644 docs/spring-batch/jsr-352.md create mode 100644 docs/spring-batch/monitoring-and-metrics.md create mode 100644 docs/spring-batch/processor.md create mode 100644 docs/spring-batch/readersAndWriters.md create mode 100644 docs/spring-batch/repeat.md create mode 100644 docs/spring-batch/retry.md create mode 100644 docs/spring-batch/scalability.md create mode 100644 docs/spring-batch/schema-appendix.md create mode 100644 docs/spring-batch/spring-batch-integration.md create mode 100644 docs/spring-batch/spring-batch-intro.md create mode 100644 docs/spring-batch/step.md create mode 100644 docs/spring-batch/testing.md create mode 100644 docs/spring-batch/transaction-appendix.md create mode 100644 docs/spring-batch/whatsnew.md create mode 100644 docs/spring-credhub/spring-credhub.md create mode 100644 docs/spring-flo/spring-flo.md create mode 100644 docs/spring-hateoas/spring-hateoas.md create mode 100644 docs/spring-rest-docs/spring-restdocs.md create mode 100644 docs/spring-shell/spring-shell.md create mode 100644 docs/spring-statemachine/spring-statemachine.md create mode 100644 docs/spring-vault/spring-vault.md diff --git a/docs/spring-amqp/spring-amqp.md b/docs/spring-amqp/spring-amqp.md new file mode 100644 index 0000000..249821e --- /dev/null +++ b/docs/spring-amqp/spring-amqp.md @@ -0,0 +1,6964 @@ +# Spring AMQP + +## [](#preface)1。前言 + +Spring AMQP 项目将核心 Spring 概念应用于基于 AMQP 的消息传递解决方案的开发。我们提供了一个“模板”,作为发送和接收消息的高级抽象。我们还为消息驱动的 POJO 提供支持。这些库促进了 AMQP 资源的管理,同时促进了依赖注入和声明式配置的使用。在所有这些情况下,你都可以看到与 Spring 框架中的 JMS 支持的相似之处。有关其他项目相关信息,请访问 Spring AMQP 项目[homepage](https://projects.spring.io/spring-amqp/)。 + +## [](#whats-new)2。最新更新 + +### [](#changes-in-2-4-since-2-3)2.1。2.4 自 2.3 以来的变化 + +本部分描述了版本 2.4 和版本 2.4 之间的更改。有关以前版本的更改,请参见[变更历史](#change-history)。 + +#### [](#rabbitlistener-changes)2.1.1。`@RabbitListener`变化 + +`MessageProperties`现在可用于参数匹配。有关更多信息,请参见[带注释的端点方法签名](#async-annotation-driven-enable-signature)。 + +#### [](#rabbitadmin-changes)2.1.2。`RabbitAdmin`变化 + +一个新的属性`recoverManualDeclarations`允许恢复手动声明的队列/交换/绑定。有关更多信息,请参见[恢复自动删除声明](#declarable-recovery)。 + +#### [](#remoting-support)2.1.3。远程支持 + +使用 Spring Framework 的 RMI 支持的远程支持已被弃用,并将在 3.0 中删除。有关更多信息,请参见[Spring Remoting with AMQP](#remoting)。 + +## [](#introduction)3。导言 + +参考文档的第一部分是对 Spring AMQP 和底层概念的高级概述。它包括一些代码片段,可以让你尽快启动和运行。 + +### [](#quick-tour)3.1。不耐烦人士的快速之旅 + +#### [](#introduction-2)3.1.1。导言 + +这是从 AMQP 开始的五分钟之旅。 + +先决条件:安装并运行 RabbitMQ 代理([https://www.rabbitmq.com/download.html](https://www.rabbitmq.com/download.html))。然后获取 Spring-Rabbit JAR 及其所有依赖项——这样做的最简单方法是在构建工具中声明一个依赖项。例如,对于 Maven,你可以执行类似于以下内容的操作: + +``` + + org.springframework.amqp + spring-rabbit + 2.4.2 + +``` + +对于 Gradle,你可以做以下类似的事情: + +``` +compile 'org.springframework.amqp:spring-rabbit:2.4.2' +``` + +##### [](#compatibility)兼容性 + +Spring Framework 版本的最小依赖关系是 5.2.0。 + +最小`amqp-client`Java 客户端库版本是 5.7.0。 + +##### [](#very-very-quick)非常,非常快 + +这一节提供了最快的介绍。 + +首先,添加以下`import`语句,以使本节后面的示例有效: + +``` +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +``` + +下面的示例使用普通的、命令式的 Java 来发送和接收消息: + +``` +ConnectionFactory connectionFactory = new CachingConnectionFactory(); +AmqpAdmin admin = new RabbitAdmin(connectionFactory); +admin.declareQueue(new Queue("myqueue")); +AmqpTemplate template = new RabbitTemplate(connectionFactory); +template.convertAndSend("myqueue", "foo"); +String foo = (String) template.receiveAndConvert("myqueue"); +``` + +请注意,在本机 Java Rabbit 客户机中也有一个`ConnectionFactory`。我们在前面的代码中使用了 Spring 抽象。它缓存通道(以及可选的连接)以供重用。我们依赖于代理中的默认交换(因为在 SEND 中没有指定),以及所有队列的默认绑定到默认交换的默认名称(因此,我们可以在 SEND 中使用队列名称作为路由键)。这些行为是在 AMQP 规范中定义的。 + +##### 带有 XML 配置的[](#with-xml-configuration) + +下面的示例与前面的示例相同,但将资源配置具体化为 XML: + +``` +ApplicationContext context = + new GenericXmlApplicationContext("classpath:/rabbit-context.xml"); +AmqpTemplate template = context.getBean(AmqpTemplate.class); +template.convertAndSend("myqueue", "foo"); +String foo = (String) template.receiveAndConvert("myqueue"); +``` + +``` + + + + + + + + + + + +``` + +默认情况下,``声明会自动查找类型为`Queue`、`Exchange`和`Binding`的 bean,并代表用户向代理声明它们。因此,你不需要在简单的 Java 驱动程序中显式地使用这个 Bean。有很多选项可以配置 XML 模式中组件的属性。你可以使用 XML 编辑器的自动完成功能来探索它们并查看它们的文档。 + +##### 使用 Java 配置的[](#with-java-configuration) + +下面的示例重复了与前面的示例相同的示例,但使用了在 Java 中定义的外部配置: + +``` +ApplicationContext context = + new AnnotationConfigApplicationContext(RabbitConfiguration.class); +AmqpTemplate template = context.getBean(AmqpTemplate.class); +template.convertAndSend("myqueue", "foo"); +String foo = (String) template.receiveAndConvert("myqueue"); + +........ + +@Configuration +public class RabbitConfiguration { + + @Bean + public CachingConnectionFactory connectionFactory() { + return new CachingConnectionFactory("localhost"); + } + + @Bean + public RabbitAdmin amqpAdmin() { + return new RabbitAdmin(connectionFactory()); + } + + @Bean + public RabbitTemplate rabbitTemplate() { + return new RabbitTemplate(connectionFactory()); + } + + @Bean + public Queue myQueue() { + return new Queue("myqueue"); + } +} +``` + +##### [](#with-spring-boot-auto-configuration-and-an-async-pojo-listener)具有 Spring 引导自动配置和异步 POJO 侦听器 + +Spring 引导会自动配置基础设施 bean,如下例所示: + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public ApplicationRunner runner(AmqpTemplate template) { + return args -> template.convertAndSend("myqueue", "foo"); + } + + @Bean + public Queue myQueue() { + return new Queue("myqueue"); + } + + @RabbitListener(queues = "myqueue") + public void listen(String in) { + System.out.println(in); + } + +} +``` + +## [](#reference)4。参考文献 + +参考文档的这一部分详细介绍了构成 Spring AMQP 的各个组件。[主要章节](#amqp)涵盖了开发 AMQP 应用程序的核心类。这一部分还包括关于[示例应用程序](#sample-apps)的一章。 + +### [](#amqp)4.1。使用 Spring AMQP + +本章探讨了接口和类,它们是使用 Spring AMQP 开发应用程序所必需的组件。 + +#### [](#amqp-abstractions)4.1.1。AMQP 抽象 + +Spring AMQP 由两个模块组成(每个模块由分布中的 JAR 表示):和。“ Spring-AMQP”模块包含`org.springframework.amqp.core`包。在这个包中,你可以找到代表核心 AMQP“模型”的类。我们的目的是提供不依赖于任何特定 AMQP 代理实现或客户库的通用抽象。最终用户代码可以在供应商的实现中更具可移植性,因为它可以仅针对抽象层进行开发。然后,这些抽象由特定于代理的模块实现,例如“ Spring-Rabbit”。目前只有一个 RabbitMQ 实现。然而,除了 RabbitMQ 之外,还使用 Apache QPID 在.NET 中验证了这些抽象。由于 AMQP 在协议级别上运行,原则上,你可以将 RabbitMQ 客户机与支持相同协议版本的任何代理一起使用,但是我们目前不测试任何其他代理。 + +本概述假定你已经熟悉 AMQP 规范的基础知识。如果没有,请查看[其他资源](#resources)中列出的资源。 + +##### [](#message)`Message` + +0-9-1AMQP 规范没有定义`Message`类或接口。相反,当执行`basicPublish()`之类的操作时,内容将作为字节数组参数传递,其他属性将作为单独的参数传递。 Spring AMQP 将`Message`类定义为更通用的 AMQP 域模型表示的一部分。`Message`类的目的是将主体和属性封装在一个实例中,这样 API 就可以变得更简单。下面的示例显示了`Message`类定义: + +``` +public class Message { + + private final MessageProperties messageProperties; + + private final byte[] body; + + public Message(byte[] body, MessageProperties messageProperties) { + this.body = body; + this.messageProperties = messageProperties; + } + + public byte[] getBody() { + return this.body; + } + + public MessageProperties getMessageProperties() { + return this.messageProperties; + } +} +``` + +`MessageProperties`接口定义了几个常见的属性,如“messageID”、“timestamp”、“ContentType”等。你还可以通过调用`setHeader(String key, Object value)`方法,使用用户定义的“headers”扩展这些属性。 + +| |从版本`1.5.7`、`1.6.11`、`1.7.4`和`2.0.0`开始,如果消息体是序列化的`Serializable`Java 对象,则在执行`toString()`操作(例如在日志消息中)时,它不再被反序列化(默认情况下)。
这是为了防止不安全的反序列化。
默认情况下,只有`java.util`和`java.lang`类被反序列化。
要恢复到以前的行为,你可以通过调用`Message.addAllowedListPatterns(…​)`来添加允许的类/包模式。
支持简单的``**通配符,例如`com.something.`**`, *.MyClass`。
不能反序列化的主体在日志消息中用`byte[]`表示。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#exchange)exchange + +`Exchange`接口表示一个 AMQP 交换,这是消息生成器发送到的内容。在代理的虚拟主机中,每个交换都有一个唯一的名称以及一些其他属性。下面的示例显示了`Exchange`接口: + +``` +public interface Exchange { + + String getName(); + + String getExchangeType(); + + boolean isDurable(); + + boolean isAutoDelete(); + + Map getArguments(); + +} +``` + +正如你所看到的,`Exchange`还具有由`ExchangeTypes`中定义的常量表示的“类型”。基本类型是:`direct`,`topic`,`fanout`,和`headers`。在核心包中,你可以为每种类型找到`Exchange`接口的实现。这些`Exchange`类型的行为在它们处理与队列绑定的方式方面有所不同。例如,`Direct`交换允许队列由固定的路由密钥(通常是队列的名称)绑定。`Topic`Exchange 支持使用路由模式的绑定,其中可能包括“exactly-one”和“zero-or-more”的“\*”和“#”通配符。`Fanout`Exchange 将发布到绑定到它的所有队列,而不考虑任何路由密钥。有关这些和其他交换类型的更多信息,请参见[其他资源](#resources)。 + +| |AMQP 规范还要求任何代理提供没有名称的“默认”直接交换。
所有声明的队列都绑定到默认的`Exchange`,并将其名称作为路由键。
你可以在[`AmqpTemplate`](#AMQP-template)中了解 Spring AMQP 中默认交换的使用情况。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#queue)队列 + +`Queue`类表示消息使用者从其接收消息的组件。像各种`Exchange`类一样,我们的实现旨在作为这种核心 AMQP 类型的抽象表示。下面的清单显示了`Queue`类: + +``` +public class Queue { + + private final String name; + + private volatile boolean durable; + + private volatile boolean exclusive; + + private volatile boolean autoDelete; + + private volatile Map arguments; + + /** + * The queue is durable, non-exclusive and non auto-delete. + * + * @param name the name of the queue. + */ + public Queue(String name) { + this(name, true, false, false); + } + + // Getters and Setters omitted for brevity + +} +``` + +请注意,构造函数使用队列名。根据实现的不同,管理模板可以提供生成唯一命名的队列的方法。这样的队列可以作为“回复”地址或在其他**临时的**情况下很有用。因此,自动生成队列的“独占”和“自动删除”属性都将设置为“true”。 + +| |有关使用名称空间支持声明队列的信息,包括队列参数,请参见[配置代理](#broker-configuration)中有关队列的部分。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#binding)绑定 + +考虑到生产者向交易所发送消息,而消费者从队列接收消息,将队列连接到交易所的绑定对于通过消息传递将生产者和消费者连接起来至关重要。在 Spring AMQP 中,我们定义了一个`Binding`类来表示这些连接。本节回顾了将队列绑定到交易所的基本选项。 + +你可以使用固定的路由密钥将队列绑定到`DirectExchange`,如下例所示: + +``` +new Binding(someQueue, someDirectExchange, "foo.bar"); +``` + +可以使用路由模式将队列绑定到`TopicExchange`,如下例所示: + +``` +new Binding(someQueue, someTopicExchange, "foo.*"); +``` + +可以将队列绑定到没有路由密钥的`FanoutExchange`,如下例所示: + +``` +new Binding(someQueue, someFanoutExchange); +``` + +我们还提供了`BindingBuilder`以促进“Fluent API”风格,如下例所示: + +``` +Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*"); +``` + +| |为了清楚起见,前面的示例显示了`BindingBuilder`类,但是当为“bind()”方法使用静态导入时,这种样式很好用。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------| + +就其本身而言,`Binding`类的实例仅保存有关连接的数据。换句话说,它不是一个“活跃”的组件。但是,正如你将在后面的[配置代理](#broker-configuration)中看到的那样,`AmqpAdmin`类可以使用`Binding`实例来实际触发代理上的绑定操作。此外,正如你在同一部分中看到的那样,你可以在`@Configuration`类中使用 Spring 的`@Bean`注释来定义`Binding`实例。 Bean 还有一种方便的基类,它进一步简化了用于生成与 AMQP 相关的定义的方法,并识别队列、交换和绑定,以便它们在应用程序启动时都在 AMQP 代理上声明。 + +`AmqpTemplate`也在核心包中定义。作为实际 AMQP 消息传递所涉及的主要组件之一,它在其自己的部分中进行了详细讨论(参见[`AmqpTemplate`](#AMQP-template))。 + +#### [](#connections)4.1.2。连接和资源管理 + +尽管我们在上一节中描述的 AMQP 模型是通用的,并且适用于所有实现,但是当我们进入资源管理时,细节是特定于代理实现的。因此,在本节中,我们将重点关注仅存在于我们的“ Spring-Rabbit”模块中的代码,因为在这一点上,RabbitMQ 是唯一受支持的实现。 + +管理到 RabbitMQ 代理的连接的中心组件是`ConnectionFactory`接口。`ConnectionFactory`实现的职责是提供`org.springframework.amqp.rabbit.connection.Connection`的实例,它是`com.rabbitmq.client.Connection`的包装器。 + +##### [](#choosing-factory)选择连接工厂 + +有三家连接工厂可供选择。 + +* `PooledChannelConnectionFactory` + +* `ThreadChannelConnectionFactory` + +* `CachingConnectionFactory` + +前两个是在 2.3 版本中添加的。 + +对于大多数用例,应该使用`PooledChannelConnectionFactory`。如果你希望确保严格的消息排序,而不需要使用[作用域操作](#scoped-operations),则可以使用`ThreadChannelConnectionFactory`。如果你想要使用相关的发布者确认,或者如果你希望通过其`CacheMode`打开多个连接,则应该使用`CachingConnectionFactory`。 + +这三家工厂都支持简单的发布者确认。 + +在配置`RabbitTemplate`以使用[独立连接](#separate-connection)时,现在可以从版本 2.3.2 开始,将发布连接工厂配置为不同的类型。默认情况下,发布工厂是相同的类型,主工厂上设置的任何属性也会传播到发布工厂。 + +###### [](#pooledchannelconnectionfactory)`PooledChannelConnectionFactory` + +该工厂基于 Apache 池 2 管理单个连接和两个通道池。一个池用于事务通道,另一个池用于非事务通道。池是带有默认配置的`GenericObjectPool`s;提供了一个回调来配置池;有关更多信息,请参阅 Apache 文档。 + +Apache `commons-pool2`jar 必须位于类路径上才能使用这个工厂。 + +``` +@Bean +PooledChannelConnectionFactory pcf() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(rabbitConnectionFactory); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + // configure the transactional pool + } + else { + // configure the non-transactional pool + } + }); + return pcf; +} +``` + +###### [](#threadchannelconnectionfactory)`ThreadChannelConnectionFactory` + +这个工厂管理一个连接和两个`ThreadLocal`s,一个用于事务通道,另一个用于非事务通道。这个工厂确保同一线程上的所有操作使用相同的通道(只要它保持打开状态)。这便于在不需要[作用域操作](#scoped-operations)的情况下进行严格的消息排序。为了避免内存泄漏,如果你的应用程序使用许多短期线程,你必须调用工厂的`closeThreadChannel()`来释放通道资源。从版本 2.3.7 开始,一个线程可以将其通道传输到另一个线程。有关更多信息,请参见[多线程环境中的严格消息排序](#multi-strict)。 + +###### [](#cachingconnectionfactory)`CachingConnectionFactory` + +提供的第三个实现是`CachingConnectionFactory`,默认情况下,它建立一个可以由应用程序共享的单个连接代理。共享连接是可能的,因为与 AMQP 进行消息传递的“工作单元”实际上是一个“通道”(在某些方面,这类似于 JMS 中的连接与会话之间的关系)。连接实例提供了`createChannel`方法。`CachingConnectionFactory`实现支持对这些通道的缓存,并且它根据通道是否是事务性的,为它们维护单独的缓存。在创建`CachingConnectionFactory`实例时,可以通过构造函数提供“hostname”。你还应该提供“用户名”和“密码”属性。要配置通道缓存的大小(默认为 25),可以调用`setChannelCacheSize()`方法。 + +从版本 1.3 开始,你可以将`CachingConnectionFactory`配置为缓存连接以及仅缓存通道。在这种情况下,对`createConnection()`的每次调用都会创建一个新的连接(或从缓存中检索一个空闲的连接)。关闭连接将其返回到缓存(如果未达到缓存大小)。在这种连接上创建的通道也会被缓存。在某些环境中,使用单独的连接可能是有用的,例如从 HA 集群消费,与负载均衡器结合,以连接到不同的集群成员,以及其他环境。要缓存连接,请将`cacheMode`设置为`CacheMode.CONNECTION`。 + +| |这并不限制连接的数量。
相反,它指定了允许多少空闲的打开连接。| +|---|-------------------------------------------------------------------------------------------------------------------| + +从版本 1.5.5 开始,提供了一个名为`connectionLimit`的新属性。设置此属性时,将限制允许的连接总数。设置时,如果达到限制,则使用`channelCheckoutTimeLimit`来等待连接变为空闲。如果超过了时间,则抛出一个`AmqpTimeoutException`。 + +| |当缓存模式`CONNECTION`时,不支持自动声明队列和其他
(参见[交换、队列和绑定的自动声明](#automatic-declaration))。

此外,在编写本文时,默认情况下,`amqp-client`库为每个连接创建一个固定的线程池(默认大小:`Runtime.getRuntime().availableProcessors() * 2`线程)。
当使用大量连接时,你应该考虑在`CachingConnectionFactory`上设置一个自定义的`executor`。
然后,相同的执行器可以被所有的连接使用,并且它的线程可以被共享。
执行器的线程池应该是无界的,或者为预期的使用进行适当的设置(通常,每个连接至少有一个线程)。
如果在每个连接上创建了多个通道,池的大小会影响并发性,因此变量(或简单缓存的)线程池执行器将是最合适的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +重要的是要理解,缓存大小(默认情况下)不是一个限制,而仅仅是可以缓存的通道数量。如果缓存大小为 10,那么实际上可以使用任意数量的通道。如果使用了超过 10 个通道,并且它们都返回到缓存中,则在缓存中使用 10 个。其余部分是封闭的。 + +从 1.6 版本开始,默认通道缓存大小从 1 增加到 25。在大容量、多线程的环境中,较小的缓存意味着以较高的速率创建和关闭通道。增加默认的缓存大小可以避免这种开销。你应该通过 RabbitMQ 管理 UI 监视正在使用的通道,并且如果你看到许多通道正在创建和关闭,请考虑进一步增加缓存大小。缓存仅按需增长(以满足应用程序的并发性需求),因此此更改不会影响现有的低容量应用程序。 + +从版本 1.4.2 开始,`CachingConnectionFactory`具有一个名为`channelCheckoutTimeout`的属性。当此属性大于零时,`channelCacheSize`将成为连接上可以创建的通道数量的限制。如果达到限制,则调用线程块,直到通道可用或达到超时为止,在这种情况下,将抛出`AmqpTimeoutException`。 + +| |框架内使用的通道(例如,`RabbitTemplate`)将可靠地返回到缓存。
如果你在框架外创建通道,(例如,
通过直接访问连接并调用`createChannel()`),则必须可靠地(通过关闭)返回它们,可能是在`finally`块中,以避免渠道耗尽。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何创建一个新的`connection`: + +``` +CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost"); +connectionFactory.setUsername("guest"); +connectionFactory.setPassword("guest"); + +Connection connection = connectionFactory.createConnection(); +``` + +在使用 XML 时,配置可能看起来像以下示例: + +``` + + + + + +``` + +| |还有一个`SingleConnectionFactory`实现,它仅在框架的单元测试代码中可用。
它比`CachingConnectionFactory`更简单,因为它不缓存通道,但由于缺乏性能和弹性,它并不打算用于简单测试之外的实际使用。
如果出于某种原因需要实现自己的`ConnectionFactory`,`AbstractConnectionFactory`基类可能提供了一个很好的起点。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +使用 Rabbit 名称空间可以快速方便地创建`ConnectionFactory`,如下所示: + +``` + +``` + +在大多数情况下,这种方法更可取,因为框架可以为你选择最好的默认值。创建的实例是`CachingConnectionFactory`。请记住,通道的默认缓存大小是 25。如果你希望有更多的通道是 cachedm,那么可以通过设置“channelcachesize”属性来设置一个更大的值。在 XML 中,它将如下所示: + +``` + + + + + + +``` + +此外,在名称空间中,你可以添加“channel-cache-size”属性,如下所示: + +``` + +``` + +默认的缓存模式是`CHANNEL`,但你可以将其配置为缓存连接。在下面的示例中,我们使用`connection-cache-size`: + +``` + +``` + +你可以使用命名空间提供主机和端口属性,如下所示: + +``` + +``` + +或者,如果在集群环境中运行,则可以使用 Addresses 属性,如下所示: + +``` + +``` + +有关`address-shuffle-mode`的信息,请参见[连接到集群](#cluster)。 + +下面是一个自定义线程工厂的示例,该工厂使用`rabbitmq-`为线程名称添加前缀: + +``` + + + + + +``` + +##### [](#addressresolver)addresolver + +从版本 2.1.15 开始,你现在可以使用`AddressResover`来解析连接地址。这将覆盖`addresses`和`host/port`属性的任何设置。 + +##### [](#naming-connections)命名连接 + +从版本 1.7 开始,为注入`AbstractionConnectionFactory`提供了一个`ConnectionNameStrategy`。生成的名称用于特定于应用程序的目标 RabbitMQ 连接的标识。如果 RabbitMQ 服务器支持该连接名,则会在管理 UI 中显示该连接名。这个值不一定是唯一的,也不能用作连接标识符——例如,在 HTTPAPI 请求中。这个值应该是人类可读的,并且是`connection_name`键下`ClientProperties`的一部分。你可以使用一个简单的 lambda,如下所示: + +``` +connectionFactory.setConnectionNameStrategy(connectionFactory -> "MY_CONNECTION"); +``` + +`ConnectionFactory`参数可用于通过某种逻辑来区分目标连接名。默认情况下,`AbstractConnectionFactory`的`beanName`、表示对象的十六进制字符串和内部计数器用于生成`connection_name`。``名称空间组件还提供了`connection-name-strategy`属性。 + +`SimplePropertyValueConnectionNameStrategy`的实现将连接名设置为应用程序属性。你可以将其声明为`@Bean`,并将其注入到连接工厂中,如下例所示: + +``` +@Bean +public SimplePropertyValueConnectionNameStrategy cns() { + return new SimplePropertyValueConnectionNameStrategy("spring.application.name"); +} + +@Bean +public ConnectionFactory rabbitConnectionFactory(ConnectionNameStrategy cns) { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); + ... + connectionFactory.setConnectionNameStrategy(cns); + return connectionFactory; +} +``` + +属性必须存在于应用程序上下文的`Environment`中。 + +| |当使用 Spring 引导及其自动配置的连接工厂时,只需要声明`ConnectionNameStrategy``@Bean`。
引导自动检测 Bean 并将其连接到工厂。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#blocked-connections-and-resource-constraints)阻塞的连接和资源约束 + +与[内存闹钟](https://www.rabbitmq.com/memory.html)对应的代理可能会阻止连接以进行交互。从版本 2.0 开始,`org.springframework.amqp.rabbit.connection.Connection`可以提供`com.rabbitmq.client.BlockedListener`实例,以通知连接阻塞和未阻塞事件。此外,`AbstractConnectionFactory`分别通过其内部的`BlockedListener`实现了`ConnectionBlockedEvent`和`ConnectionUnblockedEvent`。这些允许你提供应用程序逻辑,以便对代理上的问题做出适当的反应,并(例如)采取一些纠正措施。 + +| |当应用程序被配置为单个`CachingConnectionFactory`时,就像默认的 Spring 引导自动配置一样,当连接被代理阻塞时,应用程序停止工作。
并且当它被代理阻塞时,它的任何客户机停止工作。
如果我们在同一个应用程序中有生产者和消费者,当生产者阻塞连接时(因为代理上不再有资源了),而消费者无法释放它们(因为连接被阻塞了),我们最终可能会陷入死锁。,
为了缓解这个问题,我们建议再有一个单独的`CachingConnectionFactory`实例,具有相同的选项——一个用于生产者,一个用于消费者,
对于在消费者线程上执行的事务生产者,单独的`CachingConnectionFactory`是不可能的,因为它们应该重用与消费者事务关联的`Channel`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.0.2 开始,`RabbitTemplate`有一个配置选项,可以自动使用第二个连接工厂,除非正在使用事务。有关更多信息,请参见[使用单独的连接](#separate-connection)。发布者连接的`ConnectionNameStrategy`与主策略相同,将`.publisher`附加到调用方法的结果中。 + +从版本 1.7.7 开始,将提供一个`AmqpResourceNotAvailableException`,当`SimpleConnection.createChannel()`无法创建`Channel`时,将抛出该参数(例如,因为已达到`channelMax`限制,并且缓存中没有可用通道)。你可以在`RetryPolicy`中使用此异常来恢复一些后退后的操作。 + +##### [](#connection-factory)配置底层客户端连接工厂 + +`CachingConnectionFactory`使用了 Rabbit 客户机`ConnectionFactory`的实例。在`CachingConnectionFactory`上设置等效属性时,会传递许多配置属性(例如`host, port, userName, password, requestedHeartBeat, and connectionTimeout`)。要设置其他属性(例如`clientProperties`),你可以定义 Rabbit Factory 的实例,并使用`CachingConnectionFactory`的适当构造函数提供对它的引用。当使用名称空间([如前所述](#connections))时,你需要在`connection-factory`属性中提供对已配置工厂的引用。为了方便起见,提供了工厂 Bean 以协助在 Spring 应用程序上下文中配置连接工厂,如[下一节](#rabbitconnectionfactorybean-configuring-ssl)中所讨论的。 + +``` + +``` + +| |4.0.x 客户端默认支持自动恢复。
虽然与此功能兼容, Spring AMQP 有自己的恢复机制,通常不需要客户端恢复功能。
我们建议禁用`amqp-client`自动恢复,为了避免在代理可用但连接尚未恢复时获得`AutoRecoverConnectionNotCurrentlyOpenException`实例,你可能会注意到此异常,例如,当`RetryTemplate`在`RabbitTemplate`中配置了`RetryTemplate`时,
由于自动恢复连接在定时器上恢复,因此可以通过使用 Spring AMQP 的恢复机制更快地恢复连接,
从 1.7.1 版本开始, Spring AMQP 禁用`amqp-client`自动恢复,除非你显式地创建自己的 RabbitMQ 连接工厂并将其提供给`CachingConnectionFactory`。
RabbitMQ`ConnectionFactory`由`RabbitConnectionFactoryBean`创建的实例还具有默认禁用的选项。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#rabbitconnectionfactorybean-configuring-ssl)`RabbitConnectionFactoryBean`和配置 SSL + +从版本 1.4 开始,提供了一个方便的`RabbitConnectionFactoryBean`,以便通过使用依赖项注入在底层客户机连接工厂上方便地配置 SSL 属性。其他设置者将委托给底层工厂。以前,你必须以编程方式配置 SSL 选项。下面的示例展示了如何配置`RabbitConnectionFactoryBean`: + +``` + + + + + + +``` + +有关配置 SSL 的信息,请参见[RabbitMQ 文档](https://www.rabbitmq.com/ssl.html)。省略`keyStore`和`trustStore`配置以在没有证书验证的情况下通过 SSL 连接。下一个示例展示了如何提供密钥和信任存储区配置。 + +`sslPropertiesLocation`属性是一个 Spring `Resource`指向包含以下键的属性文件: + +``` +keyStore=file:/secret/keycert.p12 +trustStore=file:/secret/trustStore +keyStore.passPhrase=secret +trustStore.passPhrase=secret +``` + +`keyStore`和`truststore`是指向存储的 Spring `Resources`。通常,这个属性文件是由具有读访问权限的应用程序的操作系统保护的。 + +从 Spring AMQP 版本 1.5 开始,你可以直接在工厂 Bean 上设置这些属性。如果同时提供离散属性和`sslPropertiesLocation`,则后者中的属性将覆盖离散值。 + +| |从版本 2.0 开始,默认情况下服务器证书是经过验证的,因为它更安全。,如果你出于某种原因希望跳过此验证,请将工厂 Bean 的`skipServerCertificateValidation`属性设置为`true`,
从版本 2.1 开始,`RabbitConnectionFactoryBean`现在默认调用`enableHostnameVerification()`。
要恢复到以前的行为,请将`enableHostnameVerification`属性设置为`false`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |从版本 2.2.5 开始,工厂 Bean 默认情况下将始终使用 TLSv1.2;以前,它在某些情况下使用 v1.1,在其他情况下使用 v1.2(取决于其他属性)。如果出于某种原因需要使用 v1.1,请设置属性:。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#cluster)连接到集群 + +要连接到群集,请在`CachingConnectionFactory`上配置`addresses`属性: + +``` +@Bean +public CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory(); + ccf.setAddresses("host1:5672,host2:5672,host3:5672"); + return ccf; +} +``` + +每当建立新的连接时,底层连接工厂将尝试连接到每个主机。从版本 2.1.8 开始,通过将`addressShuffleMode`属性设置为`RANDOM`,可以使连接顺序是随机的;在创建任何新连接之前,将应用 shuffle。从版本 2.6 开始,添加了`INORDER`shuffle 模式,这意味着在创建连接后,第一个地址将被移动到末尾。如果你希望从所有节点上的所有碎片中消费,那么你可能希望在[RabbitMQ 分片插件](https://github.com/rabbitmq/rabbitmq-sharding)与`CacheMode.CONNECTION`之间使用此模式,并使用适当的并发性。 + +``` +@Bean +public CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory(); + ccf.setAddresses("host1:5672,host2:5672,host3:5672"); + ccf.setAddressShuffleMode(AddressShuffleMode.RANDOM); + return ccf; +} +``` + +##### [](#routing-connection-factory)路由连接工厂 + +从 1.3 版本开始,引入了`AbstractRoutingConnectionFactory`。这个工厂提供了一种机制来配置几个`ConnectionFactories`的映射,并在运行时通过一些`lookupKey`确定一个目标`ConnectionFactory`。通常,实现会检查线程绑定的上下文。为了方便起见, Spring AMQP 提供了`SimpleRoutingConnectionFactory`,它从`SimpleResourceHolder`获得当前线程绑定的`lookupKey`。以下示例展示了如何在 XML 和 Java 中配置`SimpleRoutingConnectionFactory`: + +``` + + + + + + + + + + +``` + +``` +public class MyService { + + @Autowired + private RabbitTemplate rabbitTemplate; + + public void service(String vHost, String payload) { + SimpleResourceHolder.bind(rabbitTemplate.getConnectionFactory(), vHost); + rabbitTemplate.convertAndSend(payload); + SimpleResourceHolder.unbind(rabbitTemplate.getConnectionFactory()); + } + +} +``` + +在使用后解除对资源的绑定是很重要的。有关更多信息,请参见[JavaDoc](https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html)的`AbstractRoutingConnectionFactory`。 + +从版本 1.4 开始,`RabbitTemplate`支持 SPEL`sendConnectionFactorySelectorExpression`和`receiveConnectionFactorySelectorExpression`属性,这些属性在每个 AMQP 协议交互操作(`send`、`sendAndReceive`、`receive`或`receiveAndReply`)上进行评估,将所提供的`lookupKey`值解析为`AbstractRoutingConnectionFactory`。你可以在表达式中使用 Bean 引用,例如`@vHostResolver.getVHost(#root)`。对于`send`操作,要发送的消息是根求值对象。对于`receive`操作,`queueName`是根求值对象。 + +该路由算法如下:如果选择器表达式是`null`或者是被求值为`null`或者所提供的`ConnectionFactory`不是`AbstractRoutingConnectionFactory`的实例,则一切都像以前一样工作,依赖于所提供的`ConnectionFactory`实现。如果评估结果不是`null`,但是对于`lookupKey`没有目标`ConnectionFactory`,并且`AbstractRoutingConnectionFactory`配置为`lenientFallback = true`,则会发生相同的情况。在`AbstractRoutingConnectionFactory`的情况下,它确实回退到基于`determineCurrentLookupKey()`的`routing`实现。但是,如果`lenientFallback = false`,则抛出一个`IllegalStateException`。 + +名称空间支持还在``组件上提供`send-connection-factory-selector-expression`和`receive-connection-factory-selector-expression`属性。 + +此外,从版本 1.4 开始,你可以在侦听器容器中配置路由连接工厂。在这种情况下,队列名称列表被用作查找键。例如,如果你使用`setQueueNames("thing1", "thing2")`配置容器,那么查找键是`[thing1,thing]"`(请注意,该键中没有空格)。 + +从 1.6.9 版本开始,你可以在侦听器容器上使用`setLookupKeyQualifier`向查找键添加限定符。例如,这样做可以监听具有相同名称但位于不同虚拟主机中的队列(在该虚拟主机中,每个虚拟主机都有一个连接工厂)。 + +例如,使用查找键限定符`thing1`和监听队列`thing2`的容器,可以用来注册目标连接工厂的查找键可以是`thing1[thing2]`。 + +| |目标(如果提供的话,也是默认的)连接工厂必须具有相同的 Publisher 确认和返回设置。
参见[发布者确认并返回](#cf-pub-conf-ret)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#queue-affinity)队列亲和性和`LocalizedQueueConnectionFactory` + +在集群中使用 HA 队列时,为了获得最佳性能,你可能希望连接到领先队列所在的物理代理。`CachingConnectionFactory`可以配置多个代理地址。这是故障转移,客户端尝试按顺序连接。`LocalizedQueueConnectionFactory`使用管理插件提供的 REST API 来确定哪个节点是队列的领头节点。然后,它创建(或从缓存检索)一个`CachingConnectionFactory`,该节点仅连接到该节点。如果连接失败,则确定新的引导节点,并由使用者连接到该节点。`LocalizedQueueConnectionFactory`配置了默认的连接工厂,以防无法确定队列的物理位置,在这种情况下,它将正常地连接到集群。 + +`LocalizedQueueConnectionFactory`是`RoutingConnectionFactory`,而`SimpleMessageListenerContainer`使用队列名称作为查找键,如上面[路由连接工厂](#routing-connection-factory)中讨论的那样。 + +| |由于这个原因(使用队列名进行查找),只有当容器被配置为侦听单个队列时,才能使用`LocalizedQueueConnectionFactory`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |必须在每个节点上启用 RabbitMQ 管理插件。| +|---|------------------------------------------------------------| + +| |这个连接工厂用于长寿命的连接,例如`SimpleMessageListenerContainer`使用的连接。
它不用于短连接,例如使用`RabbitTemplate`,因为在建立连接之前调用 REST API 的开销很大,
也用于发布操作,队列是未知的,并且消息无论如何都会发布给所有集群成员,因此查找节点的逻辑没有什么价值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的配置示例展示了如何配置工厂: + +``` +@Autowired +private ConfigurationProperties props; + +@Bean +public CachingConnectionFactory defaultConnectionFactory() { + CachingConnectionFactory cf = new CachingConnectionFactory(); + cf.setAddresses(this.props.getAddresses()); + cf.setUsername(this.props.getUsername()); + cf.setPassword(this.props.getPassword()); + cf.setVirtualHost(this.props.getVirtualHost()); + return cf; +} + +@Bean +public LocalizedQueueConnectionFactory queueAffinityCF( + @Qualifier("defaultConnectionFactory") ConnectionFactory defaultCF) { + return new LocalizedQueueConnectionFactory(defaultCF, + StringUtils.commaDelimitedListToStringArray(this.props.getAddresses()), + StringUtils.commaDelimitedListToStringArray(this.props.getAdminUris()), + StringUtils.commaDelimitedListToStringArray(this.props.getNodes()), + this.props.getVirtualHost(), this.props.getUsername(), this.props.getPassword(), + false, null); +} +``` + +注意,前三个参数是`addresses`、`adminUris`和`nodes`的数组。这些位置在于,当容器试图连接到队列时,它使用 Admin API 来确定哪个节点是队列的领导者,并以与该节点相同的阵列位置连接到该地址。 + +##### [](#cf-pub-conf-ret)发布者确认并返回 + +通过将`CachingConnectionFactory`属性`publisherConfirmType`设置为`ConfirmType.CORRELATED`,并将`publisherReturns`属性设置为“true”,可以支持确认(具有相关性)和返回的消息。 + +当设置这些选项时,由工厂创建的`Channel`实例被包装在`PublisherCallbackChannel`中,这用于促进回调。当获得这样的信道时,客户端可以将`PublisherCallbackChannel.Listener`注册为`Channel`。`PublisherCallbackChannel`实现包含将确认或返回路由到适当侦听器的逻辑。这些特性将在下面的小节中进一步解释。 + +另见`simplePublisherConfirms`in[作用域操作](#scoped-operations)。 + +| |有关更多的背景信息,请参见 RabbitMQ 团队的博客文章[介绍出版商确认](https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#connection-channel-listeners)连接和通道侦听器 + +连接工厂支持注册`ConnectionListener`和`ChannelListener`实现。这允许你接收关于连接和通道相关事件的通知。(`ConnectionListener`用于`RabbitAdmin`在建立连接时执行声明-有关更多信息,请参见[交换、队列和绑定的自动声明](#automatic-declaration))。下面的清单显示了`ConnectionListener`接口定义: + +``` +@FunctionalInterface +public interface ConnectionListener { + + void onCreate(Connection connection); + + default void onClose(Connection connection) { + } + + default void onShutDown(ShutdownSignalException signal) { + } + +} +``` + +从版本 2.0 开始,`org.springframework.amqp.rabbit.connection.Connection`对象可以提供`com.rabbitmq.client.BlockedListener`实例,以通知连接阻塞和未阻塞事件。下面的示例展示了 Channellistener 接口定义: + +``` +@FunctionalInterface +public interface ChannelListener { + + void onCreate(Channel channel, boolean transactional); + + default void onShutDown(ShutdownSignalException signal) { + } + +} +``` + +关于可能需要注册`ChannelListener`的一种情况,请参见[发布是异步的——如何检测成功和失败](#publishing-is-async)。 + +##### [](#channel-close-logging)记录通道关闭事件 + +版本 1.5 引入了一种机制,使用户能够控制日志记录级别。 + +`CachingConnectionFactory`使用默认策略记录通道闭包,如下所示: + +* 正常的通道关闭(200OK)没有记录. + +* 如果由于被动队列声明失败而关闭了通道,则将在调试级别记录该通道。 + +* 如果一个通道由于`basic.consume`由于独占消费者条件而被拒绝而被关闭,则该通道将在 Info 级别记录。 + +* 所有其他记录在错误级别。 + +要修改此行为,可以在其`closeExceptionLogger`属性中将自定义`ConditionalExceptionLogger`插入到`CachingConnectionFactory`中。 + +另见[消费者活动](#consumer-events)。 + +##### [](#runtime-cache-properties)运行时缓存属性 + +从版本 1.6 开始,`CachingConnectionFactory`现在通过`getCacheProperties()`方法提供缓存统计信息。这些统计信息可用于优化缓存,以在生产中对其进行优化。例如,可以使用高水位标记来确定是否应该增加缓存大小。如果它等于缓存大小,你可能需要考虑进一步增加。下表描述了`CacheMode.CHANNEL`属性: + +| Property |意义| +|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| ```
connectionName
``` |由`ConnectionNameStrategy`生成的连接的名称。| +| ```
channelCacheSize
``` |当前配置的允许空闲的最大通道。| +| ```
localPort
``` |连接的本地端口(如果可用)。
这可用于与 RabbitMQ 管理 UI 上的连接和通道关联。| +| ```
idleChannelsTx
``` |当前空闲(缓存)的事务通道的数量。| +| ```
idleChannelsNotTx
``` |当前空闲(缓存)的非事务通道的数量。| +| ```
idleChannelsTxHighWater
``` |已并发空闲(缓存)的事务通道的最大数量。| +|```
idleChannelsNotTxHighWater
```|已并发空闲(缓存)的非事务通道的最大数量。| + +下表描述了`CacheMode.CONNECTION`属性: + +| Property |意义| +|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ```
connectionName:
``` |由`ConnectionNameStrategy`生成的连接的名称。| +| ```
openConnections
``` |表示到代理的连接的连接对象的数量。| +| ```
channelCacheSize
``` |当前配置的允许空闲的最大通道。| +| ```
connectionCacheSize
``` |当前配置的允许空闲的最大连接数.| +| ```
idleConnections
``` |当前空闲的连接数。| +| ```
idleConnectionsHighWater
``` |并发空闲的连接的最大数量。| +| ```
idleChannelsTx:
``` |此连接当前空闲(缓存)的事务通道的数量。
你可以使用属性名称的`localPort`部分来关联 RabbitMQ 管理 UI 上的连接和通道。| +| ```
idleChannelsNotTx:
``` |此连接当前空闲(缓存)的非事务通道的数量。
属性名称的`localPort`部分可用于与 RabbitMQ 管理 UI 上的连接和通道关联。| +| ```
idleChannelsTxHighWater:
``` |并发空闲(缓存)的事务通道的最大数量。
属性名称的 localport 部分可用于与 RabbitMQ 管理 UI 上的连接和通道关联。| +|```
idleChannelsNotTxHighWater:
```|非事务通道的最大数量已同时空闲(缓存)。
你可以使用属性名称的`localPort`部分来关联 RabbitMQ 管理 UI 上的连接和通道。| + +还包括`cacheMode`属性(`CHANNEL`或`CONNECTION`)。 + +![cacheStats](images/cacheStats.png) + +图 1。JVisualVM 示例 + +##### [](#auto-recovery)RabbitMQ 自动连接/拓扑恢复 + +自 Spring AMQP 的第一个版本以来,该框架已经在代理失败的情况下提供了自己的连接和通道恢复。同样,如[配置代理](#broker-configuration)中所讨论的,`RabbitAdmin`在重新建立连接时重新声明任何基础设施 bean(队列和其他)。因此,它不依赖现在由`amqp-client`库提供的[自动恢复](https://www.rabbitmq.com/api-guide.html#recovery)。 Spring AMQP 现在使用`4.0.x`的`amqp-client`版本,该版本在默认情况下具有启用的自动恢复功能。 Spring 如果你愿意,AMQP 仍然可以使用其自己的恢复机制,在客户端中禁用它,(通过将底层`automaticRecoveryEnabled`上的`RabbitMQ connectionFactory`属性设置为`false`)。然而,该框架与启用的自动恢复完全兼容。这意味着你在代码中创建的任何消费者(可能通过`RabbitTemplate.execute()`)都可以自动恢复。 + +| |只有被定义为 bean 的元素(队列、交换、绑定)在连接失败后才会重新声明。
直接从用户代码调用`RabbitAdmin.declare*()`方法声明的元素对于框架来说是未知的,因此无法恢复。
如果你需要变量数量的声明,考虑定义类型`Declarables`的 Bean 或 bean,如[声明交换、队列和绑定的集合](#collection-declaration)中讨论的那样。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#custom-client-props)4.1.3。添加自定义客户端连接属性 + +现在,`CachingConnectionFactory`允许你访问底层连接工厂,以允许例如设置自定义客户机属性。下面的示例展示了如何做到这一点: + +``` +connectionFactory.getRabbitConnectionFactory().getClientProperties().put("thing1", "thing2"); +``` + +当查看连接时,这些属性会出现在 RabbitMQ 管理 UI 中。 + +#### [](#amqp-template)4.1.4。`AmqpTemplate` + +与 Spring 框架和相关项目提供的许多其他高级抽象一样, Spring AMQP 提供了一个发挥核心作用的“模板”。定义主要操作的接口称为`AmqpTemplate`。这些操作涵盖了发送和接收消息的一般行为。换句话说,它们不是任何实现所独有的——因此名称中的“AMQP”。另一方面,该接口的一些实现与 AMQP 协议的实现相关联。与 JMS 本身是一个接口级 API 不同,AMQP 是一个线路级协议。该协议的实现提供了自己的客户端库,因此模板接口的每个实现都依赖于特定的客户端库。目前,只有一个实现:`RabbitTemplate`。在下面的示例中,我们经常使用`AmqpTemplate`。然而,当你查看配置示例或在其中实例化模板或调用 setter 的任何代码摘录时,你可以看到实现类型(例如,`RabbitTemplate`)。 + +如前所述,`AmqpTemplate`接口定义了用于发送和接收消息的所有基本操作。我们将分别在[发送消息](#sending-messages)和[接收消息](#receiving-messages)中研究消息的发送和接收。 + +另见[异步兔子模板](#async-template)。 + +##### [](#template-retry)添加重试功能 + +从版本 1.3 开始,你现在可以将`RabbitTemplate`配置为使用`RetryTemplate`来帮助处理代理连接问题。有关完整信息,请参见[spring-retry](https://github.com/spring-projects/spring-retry)项目。下面只是一个使用指数后退策略和默认`SimpleRetryPolicy`的示例,它在向调用方抛出异常之前进行了三次尝试。 + +下面的示例使用 XML 名称空间: + +``` + + + + + + + + + + + +``` + +下面的示例使用了 Java 中的`@Configuration`注释: + +``` +@Bean +public RabbitTemplate rabbitTemplate() { + RabbitTemplate template = new RabbitTemplate(connectionFactory()); + RetryTemplate retryTemplate = new RetryTemplate(); + ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); + backOffPolicy.setInitialInterval(500); + backOffPolicy.setMultiplier(10.0); + backOffPolicy.setMaxInterval(10000); + retryTemplate.setBackOffPolicy(backOffPolicy); + template.setRetryTemplate(retryTemplate); + return template; +} +``` + +从版本 1.4 开始,除了`retryTemplate`属性外,`recoveryCallback`选项还支持`RabbitTemplate`选项。它被用作`RetryTemplate.execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback)`的第二个参数。 + +| |`RecoveryCallback`在某种程度上是有限的,因为重试上下文只包含`lastThrowable`字段。,对于更复杂的用例,
,你应该使用一个外部`RetryTemplate`,这样你就可以通过上下文的属性向`RecoveryCallback`传递额外的信息。
下面的示例展示了如何做到这一点:| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +retryTemplate.execute( + new RetryCallback() { + + @Override + public Object doWithRetry(RetryContext context) throws Exception { + context.setAttribute("message", message); + return rabbitTemplate.convertAndSend(exchange, routingKey, message); + } + + }, new RecoveryCallback() { + + @Override + public Object recover(RetryContext context) throws Exception { + Object message = context.getAttribute("message"); + Throwable t = context.getLastThrowable(); + // Do something with message + return null; + } + }); +} +``` + +在这种情况下,将**不是**将`RetryTemplate`注入到`RabbitTemplate`中。 + +##### [](#publishing-is-async)发布是异步的——如何检测成功和失败 + +发布消息是一种异步机制,默认情况下,RabbitMQ 会删除无法路由的消息。对于成功的发布,你可以收到异步确认,如[相关发布者确认并返回](#template-confirms)中所述。考虑两种失败场景: + +* 发布到交易所,但没有匹配的目标队列。 + +* 发布到一个不存在的交易所。 + +第一种情况由 Publisher Returns 覆盖,如[相关发布者确认并返回](#template-confirms)中所述。 + +对于第二种情况,消息会被删除,并且不会生成返回。底层通道已关闭,但有一个例外。默认情况下,该异常会被记录,但是你可以用`ChannelListener`注册`CachingConnectionFactory`来获得此类事件的通知。下面的示例展示了如何添加`ConnectionListener`: + +``` +this.connectionFactory.addConnectionListener(new ConnectionListener() { + + @Override + public void onCreate(Connection connection) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + ... + } + +}); +``` + +你可以检查信号的`reason`属性,以确定发生的问题。 + +要检测发送线程上的异常,可以在`RabbitTemplate`上检测`setChannelTransacted(true)`,并在`txCommit()`上检测异常。但是,**交易大大妨碍了业绩。**,因此在仅为这个用例启用事务之前,请仔细考虑这一点。 + +##### [](#template-confirms)相关发布者确认并返回 + +`RabbitTemplate`的`AmqpTemplate`实现支持 Publisher 确认和返回。 + +对于返回的消息,对于特定的消息,模板的`mandatory`属性必须设置为`true`,或者`mandatory-expression`必须计算为`true`。此功能需要一个`CachingConnectionFactory`,其`publisherReturns`属性设置为`true`(参见[发布者确认并返回](#cf-pub-conf-ret))。它通过调用`setReturnsCallback(ReturnsCallback callback)`来注册`RabbitTemplate.ReturnsCallback`,从而将返回发送到客户机。回调必须实现以下方法: + +``` +void returnedMessage(ReturnedMessage returned); +``` + +`ReturnedMessage`具有以下属性: + +* `message`-返回的消息本身 + +* `replyCode`-指示返回原因的代码 + +* `replyText`-返回的文本原因-例如`NO_ROUTE` + +* `exchange`-消息发送到的交换器 + +* `routingKey`-使用的路由密钥 + +每个`RabbitTemplate`只支持一个`ReturnsCallback`。另见[回复超时](#reply-timeout)。 + +对于 Publisher 确认(也称为 Publisher 确认),模板需要一个`CachingConnectionFactory`,其`publisherConfirm`属性设置为`ConfirmType.CORRELATED`。它通过调用`setConfirmCallback(ConfirmCallback callback)`来注册`RabbitTemplate.ConfirmCallback`,从而将确认信息发送到客户机。回调必须实现这个方法: + +``` +void confirm(CorrelationData correlationData, boolean ack, String cause); +``` + +`CorrelationData`是客户端在发送原始消息时提供的对象。对于`ack`,`ack`为真,对于`nack`,为假。对于`nack`实例,如果在生成`nack`时可用,则原因可能包含`nack`的原因。一个例子是,当向不存在的交易所发送消息时。在这种情况下,代理将关闭该通道。闭包的原因包括在`cause`中。在 1.4 版本中添加了`cause`。 + +只有一个`ConfirmCallback`被`RabbitTemplate`所支持。 + +| |当 Rabbit 模板发送操作完成时,通道将关闭。
这将阻止在连接工厂缓存已满(当缓存中有空间时,通道没有物理关闭,返回和确认正常进行)。
当缓存已满时,框架将延迟关闭最多五秒钟,以便留出时间进行确认和返回。
当使用确认时,当接收到最后一次确认时,通道关闭。
当只使用返回时,通道在整整五秒钟内保持打开。
我们通常建议将连接工厂的`channelCacheSize`设置为足够大的值,以便将发布消息的通道返回到缓存中,而不是关闭。
你可以通过使用 RabbitMQ 管理插件来监视通道的使用情况。
如果你看到通道正在快速打开和关闭,你应该考虑增加缓存大小,以减少服务器上的开销。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在版本 2.1 之前,在收到确认信息之前,已将启用用于发布确认的通道返回到缓存。
其他一些进程可以签出该通道并执行某些操作这会导致通道关闭——例如将消息发布到一个不存在的 Exchange。
这可能会导致确认丢失。
版本 2.1 并在以后不再将信道返回到缓存中,而确认是未执行的。
该`RabbitTemplate`在每次操作之后对信道执行逻辑`close()`。
通常,这意味着一次在一个信道上只有一个确认是未执行的。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |从版本 2.2 开始,回调是在连接工厂的`executor`线程之一上调用的,
这是为了避免在回调中执行兔子操作时可能出现死锁,
与以前的版本一样,回调是在`amqp-client`连接 I/O 线程上直接调用的;如果你执行一些 RPC 操作(例如打开一个新通道),这将导致死锁,因为 I/O 线程块在等待结果,但是结果需要由 I/O 线程本身来处理。
对于那些版本,有必要将工作(例如发送消息)交给回调中的另一个线程。
这不再是必需的,因为框架现在将回调调用交给执行器。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |只要返回回调在 60 秒或更短的时间内执行,在 ACK 之前接收返回消息的保证仍然保持。
确认计划在返回回调结束之后或 60 秒之后交付,以先到者为准。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.1 开始,`CorrelationData`对象有一个`ListenableFuture`,你可以使用它来获取结果,而不是在模板上使用`ConfirmCallback`。下面的示例展示了如何配置`CorrelationData`实例: + +``` +CorrelationData cd1 = new CorrelationData(); +this.templateWithConfirmsEnabled.convertAndSend("exchange", queue.getName(), "foo", cd1); +assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); +``` + +由于它是`ListenableFuture`,所以你可以在准备就绪时获得`get()`结果,或者为异步回调添加侦听器。`Confirm`对象是一个简单的 Bean,具有 2 个属性:`ack`和`reason`(对于`nack`实例)。对于代理生成的`nack`实例,原因未填充。它是为框架生成的`nack`实例填充的(例如,在`ack`实例未完成时关闭连接)。 + +此外,当同时启用确认和返回时,`CorrelationData`将填充返回的消息,只要`CorrelationData`具有唯一的`id`;默认情况下,从版本 2.3 开始总是这样。可以保证,返回的消息在使用`ack`设置 future 之前已经设置好了。 + +另请参见[作用域操作](#scoped-operations),以获得等待发布者确认的更简单机制。 + +##### [](#scoped-operations)作用域操作 + +通常,当使用模板时,一个`Channel`将从缓存中签出(或创建),用于操作,并返回到缓存中进行重用。在多线程环境中,不能保证下一个操作使用相同的通道。然而,有时你可能希望对通道的使用有更多的控制,并确保多个操作都在同一个通道上执行。 + +从版本 2.0 开始,提供了一个名为`invoke`的新方法,并提供了一个`OperationsCallback`。在回调范围内和提供的`RabbitOperations`参数上执行的任何操作都使用相同的专用`Channel`,它将在结束时关闭(不返回到缓存)。如果通道是`PublisherCallbackChannel`,则在接收到所有确认后将其返回到缓存中(参见[相关发布者确认并返回](#template-confirms))。 + +``` +@FunctionalInterface +public interface OperationsCallback { + + T doInRabbit(RabbitOperations operations); + +} +``` + +如果你希望在底层`Channel`上使用`waitForConfirms()`方法,那么你可能需要此方法的一个示例。 Spring API 以前没有公开这种方法,因为通道通常是缓存和共享的,正如前面讨论的那样。`RabbitTemplate`现在提供`waitForConfirms(long timeout)`和`waitForConfirmsOrDie(long timeout)`,将其委托给在`OperationsCallback`范围内使用的专用通道。出于显而易见的原因,这些方法不能在该范围之外使用。 + +请注意,允许你与请求进行关联确认的更高级别的抽象在其他地方提供(参见[相关发布者确认并返回](#template-confirms))。如果你只想等待代理确认交付,那么可以使用以下示例中所示的技术: + +``` +Collection messages = getMessagesToSend(); +Boolean result = this.template.invoke(t -> { + messages.forEach(m -> t.convertAndSend(ROUTE, m)); + t.waitForConfirmsOrDie(10_000); + return true; +}); +``` + +如果你希望`RabbitAdmin`操作在`OperationsCallback`范围内的同一通道上被调用,则必须使用与`RabbitTemplate`操作相同的`RabbitTemplate`构造管理。 + +| |如果模板操作已经在现有事务的范围内执行,那么前面的讨论是没有意义的,例如,当运行在一个事务侦听器容器线程上并在一个事务模板上执行操作时,
在这种情况下,该操作在该通道上执行,并在线程返回到容器时提交。
在该场景中无需使用`invoke`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +以这种方式使用确认时,实际上并不需要为将确认与请求关联而设置的许多基础设施(除非还启用了返回)。从版本 2.2 开始,连接工厂支持一个名为`publisherConfirmType`的新属性。当这被设置为`ConfirmType.SIMPLE`时,避免了基础设施的确认,并且可以更有效地进行处理。 + +此外,`RabbitTemplate`在已发送消息`MessageProperties`中设置`publisherSequenceNumber`属性。如果你希望检查(或记录或以其他方式使用)特定的确认,则可以使用重载的`invoke`方法进行检查,如下例所示: + +``` +public T invoke(OperationsCallback action, com.rabbitmq.client.ConfirmCallback acks, + com.rabbitmq.client.ConfirmCallback nacks); +``` + +| |这些`ConfirmCallback`对象(对于`ack`和`nack`实例)是 Rabbit 客户机回调,而不是模板回调。| +|---|----------------------------------------------------------------------------------------------------------------------------| + +下面的示例日志记录`ack`和`nack`实例: + +``` +Collection messages = getMessagesToSend(); +Boolean result = this.template.invoke(t -> { + messages.forEach(m -> t.convertAndSend(ROUTE, m)); + t.waitForConfirmsOrDie(10_000); + return true; +}, (tag, multiple) -> { + log.info("Ack: " + tag + ":" + multiple); +}, (tag, multiple) -> { + log.info("Nack: " + tag + ":" + multiple); +})); +``` + +| |作用域操作绑定到线程。
有关多线程环境中严格排序的讨论,请参见[多线程环境中的严格消息排序](#multi-strict)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#multi-strict)多线程环境中的严格消息排序 + +[作用域操作](#scoped-operations)中的讨论仅在同一线程上执行操作时才适用。 + +考虑以下情况: + +* `thread-1`向队列发送消息,并将工作移交给`thread-2` + +* `thread-2`将消息发送到相同的队列 + +由于 RabbitMQ 的异步特性和缓存通道的使用,不确定是否使用相同的通道,因此不能保证消息到达队列的顺序。(在大多数情况下,它们会按顺序到达,但超量交付的可能性不是零)。要解决此用例,你可以使用大小`1`的有界通道缓存(连同`channelCheckoutTimeout`)来确保消息总是发布在相同的通道上,并且将保证顺序。要做到这一点,如果你对连接工厂有其他用途,例如消费者,那么你应该为模板使用专用的连接工厂,或者将模板配置为使用嵌入在主连接工厂中的发布者连接工厂(参见[使用单独的连接](#separate-connection))。 + +用一个简单的 Spring 引导应用程序最好地说明了这一点: + +``` +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + TaskExecutor exec() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(10); + return exec; + } + + @Bean + CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory("localhost"); + CachingConnectionFactory publisherCF = (CachingConnectionFactory) ccf.getPublisherConnectionFactory(); + publisherCF.setChannelCacheSize(1); + publisherCF.setChannelCheckoutTimeout(1000L); + return ccf; + } + + @RabbitListener(queues = "queue") + void listen(String in) { + log.info(in); + } + + @Bean + Queue queue() { + return new Queue("queue"); + } + + @Bean + public ApplicationRunner runner(Service service, TaskExecutor exec) { + return args -> { + exec.execute(() -> service.mainService("test")); + }; + } + +} + +@Component +class Service { + + private static final Logger LOG = LoggerFactory.getLogger(Service.class); + + private final RabbitTemplate template; + + private final TaskExecutor exec; + + Service(RabbitTemplate template, TaskExecutor exec) { + template.setUsePublisherConnection(true); + this.template = template; + this.exec = exec; + } + + void mainService(String toSend) { + LOG.info("Publishing from main service"); + this.template.convertAndSend("queue", toSend); + this.exec.execute(() -> secondaryService(toSend.toUpperCase())); + } + + void secondaryService(String toSend) { + LOG.info("Publishing from secondary service"); + this.template.convertAndSend("queue", toSend); + } + +} +``` + +尽管发布是在两个不同的线程上执行的,但它们都将使用相同的通道,因为缓存的上限是单个通道。 + +从版本 2.3.7 开始,`ThreadChannelConnectionFactory`支持使用`prepareContextSwitch`和`switchContext`方法将一个线程的通道转移到另一个线程。第一个方法返回一个上下文,该上下文被传递给调用第二个方法的第二个线程。线程可以有一个非事务通道,也可以有一个绑定到它的事务通道(或其中一个);除非使用两个连接工厂,否则不能单独传输它们。以下是一个例子: + +``` +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + TaskExecutor exec() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(10); + return exec; + } + + @Bean + ThreadChannelConnectionFactory tccf() { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + return new ThreadChannelConnectionFactory(rabbitConnectionFactory); + } + + @RabbitListener(queues = "queue") + void listen(String in) { + log.info(in); + } + + @Bean + Queue queue() { + return new Queue("queue"); + } + + @Bean + public ApplicationRunner runner(Service service, TaskExecutor exec) { + return args -> { + exec.execute(() -> service.mainService("test")); + }; + } + +} + +@Component +class Service { + + private static final Logger LOG = LoggerFactory.getLogger(Service.class); + + private final RabbitTemplate template; + + private final TaskExecutor exec; + + private final ThreadChannelConnectionFactory connFactory; + + Service(RabbitTemplate template, TaskExecutor exec, + ThreadChannelConnectionFactory tccf) { + + this.template = template; + this.exec = exec; + this.connFactory = tccf; + } + + void mainService(String toSend) { + LOG.info("Publishing from main service"); + this.template.convertAndSend("queue", toSend); + Object context = this.connFactory.prepareSwitchContext(); + this.exec.execute(() -> secondaryService(toSend.toUpperCase(), context)); + } + + void secondaryService(String toSend, Object threadContext) { + LOG.info("Publishing from secondary service"); + this.connFactory.switchContext(threadContext); + this.template.convertAndSend("queue", toSend); + this.connFactory.closeThreadChannel(); + } + +} +``` + +| |一旦调用`prepareSwitchContext`,如果当前线程执行更多的操作,它们将在新的通道上执行。
当不再需要线程绑定通道时,关闭该通道非常重要。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#template-messaging)消息传递集成 + +从版本 1.4 开始,`RabbitMessagingTemplate`(构建在`RabbitTemplate`之上)提供了与 Spring Framework 消息传递抽象的集成——即`org.springframework.messaging.Message`。这允许你通过使用`spring-messaging``Message`抽象来发送和接收消息。这个抽象被其他 Spring 项目所使用,例如 Spring 集成和 Spring 的 STOMP 支持。涉及两个消息转换器:一个用于在 Spring-messaging`Message`和 Spring AMQP 的`Message`抽象之间进行转换,另一个用于在 Spring AMQP 的`Message`抽象和底层 RabbitMQ 客户端库所需的格式之间进行转换。默认情况下,消息有效负载由提供的`RabbitTemplate`实例的消息转换器进行转换。或者,可以将自定义`MessagingMessageConverter`与其他有效负载转换器一起注入,如下例所示: + +``` +MessagingMessageConverter amqpMessageConverter = new MessagingMessageConverter(); +amqpMessageConverter.setPayloadConverter(myPayloadConverter); +rabbitMessagingTemplate.setAmqpMessageConverter(amqpMessageConverter); +``` + +##### [](#template-user-id)验证用户 ID + +从版本 1.6 开始,模板现在支持`user-id-expression`(使用 Java 配置时`userIdExpression`)。如果发送了消息,则在计算该表达式后设置 User ID 属性(如果尚未设置)。评估的根对象是要发送的消息。 + +以下示例展示了如何使用`user-id-expression`属性: + +``` + + + +``` + +第一个例子是一个字面表达式。第二个从应用程序上下文中的连接工厂 Bean 获得`username`属性。 + +##### [](#separate-connection)使用单独的连接 + +从版本 2.0.2 开始,你可以将`usePublisherConnection`属性设置为`true`,以便在可能的情况下使用与侦听器容器使用的不同的连接。这是为了避免当生产者由于任何原因而被封锁时,消费者被封锁。为此目的,连接工厂维护第二个内部连接工厂;默认情况下,它与主工厂的类型相同,但是如果你希望使用不同的工厂类型进行发布,则可以明确地设置该类型。如果 Rabbit 模板在侦听器容器启动的事务中运行,则使用容器的通道,而不考虑此设置。 + +| |通常,你不应该在模板中使用`RabbitAdmin`,而该模板将此设置为`true`。
使用获取连接工厂的`RabbitAdmin`构造函数。
如果你使用另一个获取模板的构造函数,请确保模板的属性为`false`。
这是因为,通常,管理员用于声明侦听器容器的队列。
使用属性设置为`true`的模板将意味着在与侦听器容器使用的连接不同的连接上声明排他队列(例如`AnonymousQueue`)。,在这种情况下,
,容器不能使用队列。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#sending-messages)4.1.5。发送消息 + +在发送消息时,可以使用以下任何一种方法: + +``` +void send(Message message) throws AmqpException; + +void send(String routingKey, Message message) throws AmqpException; + +void send(String exchange, String routingKey, Message message) throws AmqpException; +``` + +我们可以从前面清单中的最后一个方法开始讨论,因为它实际上是最明确的方法。它允许在运行时提供 AMQP 交换名(以及路由密钥)。最后一个参数是负责实际创建消息实例的回调。使用此方法发送消息的示例可能如下所示:下面的示例展示了如何使用`send`方法发送消息: + +``` +amqpTemplate.send("marketData.topic", "quotes.nasdaq.THING1", + new Message("12.34".getBytes(), someProperties)); +``` + +如果你计划在大部分时间或全部时间使用该模板实例发送到相同的 Exchange,则可以在模板本身上设置`exchange`属性。在这种情况下,你可以使用前面列表中的第二种方法。下面的示例在功能上与前面的示例等价: + +``` +amqpTemplate.setExchange("marketData.topic"); +amqpTemplate.send("quotes.nasdaq.FOO", new Message("12.34".getBytes(), someProperties)); +``` + +如果在模板上同时设置了`exchange`和`routingKey`属性,则可以使用仅接受`Message`的方法。下面的示例展示了如何做到这一点: + +``` +amqpTemplate.setExchange("marketData.topic"); +amqpTemplate.setRoutingKey("quotes.nasdaq.FOO"); +amqpTemplate.send(new Message("12.34".getBytes(), someProperties)); +``` + +考虑 Exchange 和 Routing 键属性的一种更好的方法是,显式方法参数总是覆盖模板的默认值。实际上,即使你没有显式地在模板上设置这些属性,也始终存在默认值。在这两种情况下,默认值都是空的`String`,但这实际上是一个合理的默认值。就路由密钥而言,它首先并不总是必要的(例如,对于`Fanout`交换)。此外,队列可以绑定到带有空`String`的交换。对于模板的路由密钥属性,这两种情况都是依赖默认的空`String`值的合法场景。就交换名称而言,空`String`是常用的,因为 AMQP 规范将“默认交换”定义为没有名称。由于所有队列都自动绑定到默认的 Exchange(这是一个直接的 Exchange),使用它们的名称作为绑定值,因此前面列表中的第二个方法可以用于通过默认 Exchange 向任何队列发送简单的点对点消息。通过在运行时提供方法参数,可以将队列名提供为`routingKey`。下面的示例展示了如何做到这一点: + +``` +RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange +template.send("queue.helloWorld", new Message("Hello World".getBytes(), someProperties)); +``` + +或者,你可以创建一个模板,该模板可以主要用于发布,也可以专门用于发布到单个队列。下面的示例展示了如何做到这一点: + +``` +RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange +template.setRoutingKey("queue.helloWorld"); // but we'll always send to this Queue +template.send(new Message("Hello World".getBytes(), someProperties)); +``` + +##### [](#message-builder)Message Builder API + +从版本 1.3 开始,消息生成器 API 由`MessageBuilder`和`MessagePropertiesBuilder`提供。这些方法为创建消息或消息属性提供了一种方便的“fluent”方法。以下示例展示了 Fluent API 的实际应用: + +``` +Message message = MessageBuilder.withBody("foo".getBytes()) + .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) + .setMessageId("123") + .setHeader("bar", "baz") + .build(); +``` + +``` +MessageProperties props = MessagePropertiesBuilder.newInstance() + .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) + .setMessageId("123") + .setHeader("bar", "baz") + .build(); +Message message = MessageBuilder.withBody("foo".getBytes()) + .andProperties(props) + .build(); +``` + +在[`MessageProperties`](https://DOCS. Spring.io/ Spring-amqp/DOCS/latest-ga/api/org/springframework/amqp/core/messageproperties.html)上定义的每个属性都可以被设置。其他方法包括`setHeader(String key, String value)`、`removeHeader(String key)`、`removeHeaders()`和`copyProperties(MessageProperties properties)`。每个属性设置方法都有一个`set*IfAbsent()`变量。在存在默认初始值的情况下,该方法被命名为`set*IfAbsentOrDefault()`。 + +提供了五种静态方法来创建初始消息生成器: + +``` +public static MessageBuilder withBody(byte[] body) (1) + +public static MessageBuilder withClonedBody(byte[] body) (2) + +public static MessageBuilder withBody(byte[] body, int from, int to) (3) + +public static MessageBuilder fromMessage(Message message) (4) + +public static MessageBuilder fromClonedMessage(Message message) (5) +``` + +|**1**|构建器创建的消息有一个直接引用该参数的主体。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|构建器创建的消息有一个主体,它是一个新的数组,在参数中包含一个字节的副本。| +|**3**|生成器创建的消息有一个主体,它是一个新的数组,其中包含来自参数的字节范围。
有关更多详细信息,请参见[`Arrays.copyOfRange()`](https://DOCS.oracle.com/javase/7/DOCS/api/java/util/array.html)。| +|**4**|构建器创建的消息有一个直接引用到参数主体的主体。
该参数的属性被复制到一个新的`MessageProperties`对象。| +|**5**|构建器创建的消息有一个主体,它是一个新的数组,其中包含一个参数主体的副本。
该参数的属性被复制到一个新的`MessageProperties`对象。| + +提供了三个静态方法来创建`MessagePropertiesBuilder`实例: + +``` +public static MessagePropertiesBuilder newInstance() (1) + +public static MessagePropertiesBuilder fromProperties(MessageProperties properties) (2) + +public static MessagePropertiesBuilder fromClonedProperties(MessageProperties properties) (3) +``` + +|**1**|使用默认值初始化一个新的消息属性对象。| +|-----|--------------------------------------------------------------------------------------------| +|**2**|“构建器是用提供的属性对象初始化的,`build()`将返回该对象。,| +|**3**|参数的属性被复制到一个新的`MessageProperties`对象。| + +使用`RabbitTemplate`的`AmqpTemplate`实现,每个`send()`方法都有一个重载版本,该版本接受一个额外的`CorrelationData`对象。当启用了 Publisher Confirms 时,将在[`AmqpTemplate`]中描述的回调中返回此对象。这使得发送者将确认(`ack`或`nack`)与发送的消息关联起来。 + +从 1.6.7 版本开始,引入了`CorrelationAwareMessagePostProcessor`接口,允许在消息转换后修改相关数据。下面的示例展示了如何使用它: + +``` +Message postProcessMessage(Message message, Correlation correlation); +``` + +在 2.0 版本中,此接口已被弃用。该方法已被移动到`MessagePostProcessor`,并使用一个将其委托给`postProcessMessage(Message message)`的默认实现。 + +从 1.6.7 版本开始,还提供了一个名为`CorrelationDataPostProcessor`的新回调接口。这是在所有`MessagePostProcessor`实例(在`send()`方法中提供,以及在`setBeforePublishPostProcessors()`中提供)之后调用的。实现方式可以更新或替换`send()`方法(如果有的话)中提供的相关数据。`Message`和原始`CorrelationData`(如果有的话)作为参数提供。下面的示例展示了如何使用`postProcess`方法: + +``` +CorrelationData postProcess(Message message, CorrelationData correlationData); +``` + +##### [](#publisher-returns)Publisher 返回 + +当模板的`mandatory`属性是`true`时,返回的消息由[`AmqpTemplate`]中描述的回调提供。 + +从版本 1.4 开始,`RabbitTemplate`支持 spel`mandatoryExpression`属性,该属性根据每个请求消息作为根求值对象进行求值,解析为`boolean`值。 Bean 引用,例如`@myBean.isMandatory(#root)`,可以在表达式中使用。 + +在发送和接收操作中,`RabbitTemplate`还可以在内部使用发布服务器返回。有关更多信息,请参见[回复超时](#reply-timeout)。 + +##### [](#template-batching)批处理 + +版本 1.4.2 引入了`BatchingRabbitTemplate`。这是`RabbitTemplate`的一个子类,具有一个重写的`send`方法,该方法根据`BatchingStrategy`批处理消息。只有当批处理完成时,消息才会发送到 RabbitMQ。下面的清单显示了`BatchingStrategy`接口定义: + +``` +public interface BatchingStrategy { + + MessageBatch addToBatch(String exchange, String routingKey, Message message); + + Date nextRelease(); + + Collection releaseBatches(); + +} +``` + +| |批处理的数据保存在内存中。
未发送的消息可能会在系统故障时丢失。| +|---|-------------------------------------------------------------------------------------------------| + +a`SimpleBatchingStrategy`。它支持将消息发送到单个 Exchange 或 Routing Key。它具有以下特性: + +* `batchSize`:一批消息在发送之前的数量。 + +* `bufferLimit`:批处理消息的最大大小。如果超过了`batchSize`,这会抢占该参数,并导致发送部分批处理。 + +* `timeout`:当没有新的活动向批处理添加消息时,发送部分批处理的时间。 + +`SimpleBatchingStrategy`通过在每条嵌入的消息之前使用四字节的二进制长度来格式化批处理。这是通过将`springBatchFormat`消息属性设置为`lengthHeader4`来与接收系统通信的。 + +| |批处理的消息在默认情况下由侦听器容器自动去批处理(通过使用`springBatchFormat`消息头)。
拒绝批处理中的任何消息将导致整个批处理被拒绝。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +但是,有关更多信息,请参见[@RabbitListener 与批处理](#receiving-batch)。 + +#### [](#receiving-messages)4.1.6。接收消息 + +消息接收总是比发送复杂一点。有两种方法可以接收`Message`。更简单的选项是使用轮询方法调用一次轮询一个`Message`。更复杂但更常见的方法是注册一个侦听器,该侦听器异步地按需接收`Messages`。在接下来的两个子节中,我们将考虑每种方法的示例。 + +##### [](#polling-consumer)轮询消费者 + +`AmqpTemplate`本身可以用于对`Message`的接收进行民意测验。默认情况下,如果没有可用的消息,`null`将立即返回。不存在阻塞。从版本 1.5 开始,你可以设置`receiveTimeout`,以毫秒为单位,并且 ReceiveMethods 块等待消息的时间最长可达那么长。小于零的值意味着无限期地阻塞(或至少直到与代理的连接丢失为止)。版本 1.6 引入了`receive`方法的变体,这些方法允许在每次调用时传入超时。 + +| |由于接收操作为每条消息创建了一个新的`QueueingConsumer`,因此这种技术并不适合于大容量环境。
考虑使用异步消费者或对这些用例使用零的`receiveTimeout`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有四种简单的`receive`方法可用。与发送端的`Exchange`一样,有一个方法要求在模板本身上直接设置默认的队列属性,并且有一个方法在运行时接受队列参数。版本 1.6 引入了一些变体,可以在每个请求的基础上接受`receiveTimeout`来覆盖`receiveTimeout`。下面的清单显示了这四种方法的定义: + +``` +Message receive() throws AmqpException; + +Message receive(String queueName) throws AmqpException; + +Message receive(long timeoutMillis) throws AmqpException; + +Message receive(String queueName, long timeoutMillis) throws AmqpException; +``` + +与发送消息的情况一样,`AmqpTemplate`具有一些用于接收 POJO 而不是`Message`实例的方便方法,并且实现提供了一种定制`MessageConverter`的方法,用于创建返回的`Object`:下面的列表显示了这些方法: + +``` +Object receiveAndConvert() throws AmqpException; + +Object receiveAndConvert(String queueName) throws AmqpException; + +Object receiveAndConvert(long timeoutMillis) throws AmqpException; + +Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException; +``` + +从版本 2.0 开始,这些方法有一些变体,它们接受一个额外的`ParameterizedTypeReference`参数来转换复杂类型。模板必须配置为`SmartMessageConverter`。有关更多信息,请参见[从`Message`转换为`RabbitTemplate`]。 + +与`sendAndReceive`方法类似,从版本 1.3 开始,`AmqpTemplate`具有几种方便的`receiveAndReply`方法,用于同步接收、处理和回复消息。下面的清单显示了这些方法定义: + +``` + boolean receiveAndReply(ReceiveAndReplyCallback callback) + throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) + throws AmqpException; + + boolean receiveAndReply(ReceiveAndReplyCallback callback, + String replyExchange, String replyRoutingKey) throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, + String replyExchange, String replyRoutingKey) throws AmqpException; + + boolean receiveAndReply(ReceiveAndReplyCallback callback, + ReplyToAddressCallback replyToAddressCallback) throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, + ReplyToAddressCallback replyToAddressCallback) throws AmqpException; +``` + +`AmqpTemplate`实现处理`receive`和`reply`阶段。在大多数情况下,你应该只提供`ReceiveAndReplyCallback`的实现,以便为接收到的消息执行某些业务逻辑,并在需要时构建一个应答对象或消息。注意,a`ReceiveAndReplyCallback`可能返回`null`。在这种情况下,不会发送任何回复,并且`receiveAndReply`的工作原理与`receive`方法类似。这使得相同的队列可以用于混合消息,其中一些消息可能不需要回复。 + +只有当所提供的回调不是`ReceiveAndReplyMessageCallback`的实例时,才应用自动消息(请求和回复)转换,该实例提供了原始消息交换契约。 + +对于需要自定义逻辑在运行时根据接收到的消息确定`replyTo`地址并从`ReceiveAndReplyCallback`回复的情况,`ReplyToAddressCallback`是有用的。默认情况下,请求消息中的`replyTo`信息用于路由答复。 + +下面的清单展示了一个基于 POJO 的接收和回复示例: + +``` +boolean received = + this.template.receiveAndReply(ROUTE, new ReceiveAndReplyCallback() { + + public Invoice handle(Order order) { + return processOrder(order); + } + }); +if (received) { + log.info("We received an order!"); +} +``` + +##### [](#async-consumer)异步消费者 + +| |Spring AMQP 还通过使用`@RabbitListener`注释来支持带注释的侦听器端点,并提供一个开放的基础设施来以编程方式注册端点。
这是迄今为止设置异步消费者的最方便的方式。
有关更多详细信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |预取默认值过去是 1,这可能会导致有效的使用者利用率不足,
从版本 2.0 开始,默认的预取值现在是 250,这应该会使使用者在大多数常见的情况下都很忙,
因此提高了吞吐量。,尽管如此,仍然存在,预取值应该较低的场景:对于大消息,

*,特别是如果处理速度较慢(消息在客户端进程中可能会增加大量内存)

* 当需要严格的消息排序时(在这种情况下,预取值应设置为 1)

* 其他特殊情况

同样,对于低容量的消息传递和多个消费者(包括单个侦听器容器实例中的并发性),你可能希望减少预取,以便在消费者之间获得更均匀的消息分布。

关于预取的更多背景信息,请参见[消息侦听器容器配置](#containerAttributes)。`x-expires`
,请参阅这篇关于[RabbitMQ 中的消费者利用](https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/)的文章和这篇关于[排队论](https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/)的文章。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### [](#message-listener)消息侦听器 + +对于异步`Message`接收,涉及一个专用组件(而不是`AmqpTemplate`)。该组件是用于`Message`消费回调的容器。在本节的后面部分,我们将考虑容器及其属性。不过,首先我们应该看看回调,因为这是应用程序代码与消息传递系统集成的地方。回调有几个选项,从`MessageListener`接口的实现开始,下面的清单显示了这个选项: + +``` +public interface MessageListener { + void onMessage(Message message); +} +``` + +如果回调逻辑出于任何原因依赖于 AMQP 通道实例,则可以使用`ChannelAwareMessageListener`。它看起来很相似,但有一个额外的参数。下面的清单显示了`ChannelAwareMessageListener`接口定义: + +``` +public interface ChannelAwareMessageListener { + void onMessage(Message message, Channel channel) throws Exception; +} +``` + +| |在版本 2.1 中,此接口从包`o.s.amqp.rabbit.core`移动到`o.s.amqp.rabbit.listener.api`。| +|---|-----------------------------------------------------------------------------------------------------------| + +###### [](#message-listener-adapter)`MessageListenerAdapter` + +如果希望在应用程序逻辑和消息传递 API 之间保持更严格的分离,则可以依赖框架提供的适配器实现。这通常被称为“消息驱动的 POJO”支持。 + +| |版本 1.5 引入了一种更灵活的 POJO 消息传递机制,`@RabbitListener`注释。
有关更多信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在使用适配器时,你只需要提供对适配器本身应该调用的实例的引用。下面的示例展示了如何做到这一点: + +``` +MessageListenerAdapter listener = new MessageListenerAdapter(somePojo); +listener.setDefaultListenerMethod("myMethod"); +``` + +你可以对适配器进行子类,并提供`getListenerMethodName()`的实现,以根据消息动态地选择不同的方法。这个方法有两个参数,`originalMessage`和`extractedMessage`,后者是任何转换的结果。默认情况下,配置了`SimpleMessageConverter`。有关其他可用转换器的更多信息和信息,请参见[`SimpleMessageConverter`]。 + +从版本 1.4.2 开始,原始消息具有`consumerQueue`和`consumerTag`属性,可用于确定接收消息的队列。 + +从版本 1.5 开始,你可以将消费者队列或标记的映射配置为方法名,以动态地选择要调用的方法。如果映射中没有条目,我们将返回到默认的侦听器方法。默认的侦听器方法(如果未设置)是`handleMessage`。 + +从版本 2.0 开始,提供了一个方便的`FunctionalInterface`。下面的清单显示了`FunctionalInterface`的定义: + +``` +@FunctionalInterface +public interface ReplyingMessageListener { + + R handleMessage(T t); + +} +``` + +该接口通过使用 Java8Lambdas 方便了适配器的配置,如下例所示: + +``` +new MessageListenerAdapter((ReplyingMessageListener) data -> { + ... + return result; +})); +``` + +从版本 2.2 开始,`buildListenerArguments(Object)`已被弃用,取而代之的是新的`buildListenerArguments(Object, Channel, Message)`。新方法帮助 Listener 获得`@SendTo`和`Message`参数,以执行更多操作,例如在手动确认模式下调用`channel.basicReject(long, boolean)`。下面的清单展示了最基本的示例: + +``` +public class ExtendedListenerAdapter extends MessageListenerAdapter { + + @Override + protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { + return new Object[]{extractedMessage, channel, message}; + } + +} +``` + +现在,如果需要接收“通道”和“消息”,你可以将`ExtendedListenerAdapter`配置为与`MessageListenerAdapter`相同的配置。Listener 的参数应设置为`buildListenerArguments(Object, Channel, Message)`返回,如下面的 Listener 示例所示: + +``` +public void handleMessage(Object object, Channel channel, Message message) throws IOException { + ... +} +``` + +###### [](#container)容器 + +既然你已经看到了`ChannelAwareMessageListener`-listening 回调的各种选项,那么我们就可以将注意力转向容器了。基本上,容器处理“主动”职责,以便侦听器回调可以保持被动。容器是“生命周期”组件的一个示例。它提供了启动和停止的方法。在配置容器时,实质上是在 AMQP 队列和`MessageListener`实例之间建立桥梁。你必须提供对`ConnectionFactory`的引用,以及该侦听器应该从其中使用消息的队列名称或队列实例。 + +在 2.0 版本之前,有一个侦听器容器,`SimpleMessageListenerContainer`。现在有第二个容器,`DirectMessageListenerContainer`。容器和你在选择要使用的容器时可能应用的条件之间的差异在[选择容器](#choose-container)中进行了描述。 + +下面的清单显示了最基本的示例,该示例通过使用`SimpleMessageListenerContainer`来工作: + +``` +SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); +container.setConnectionFactory(rabbitConnectionFactory); +container.setQueueNames("some.queue"); +container.setMessageListener(new MessageListenerAdapter(somePojo)); +``` + +作为一个“活动”组件,最常见的方法是创建具有 Bean 定义的侦听器容器,以便它可以在后台运行。下面的示例展示了使用 XML 的一种方法: + +``` + + + +``` + +下面的清单展示了使用 XML 的另一种方法: + +``` + + + +``` + +前面的两个示例都创建了`DirectMessageListenerContainer`(请注意`type`属性——它默认为`simple`)。 + +或者,你可能更喜欢使用 Java 配置,它看起来类似于前面的代码片段: + +``` +@Configuration +public class ExampleAmqpConfiguration { + + @Bean + public SimpleMessageListenerContainer messageListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(rabbitConnectionFactory()); + container.setQueueName("some.queue"); + container.setMessageListener(exampleListener()); + return container; + } + + @Bean + public CachingConnectionFactory rabbitConnectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost"); + connectionFactory.setUsername("guest"); + connectionFactory.setPassword("guest"); + return connectionFactory; + } + + @Bean + public MessageListener exampleListener() { + return new MessageListener() { + public void onMessage(Message message) { + System.out.println("received: " + message); + } + }; + } +} +``` + +###### [](#consumer-priority)消费者优先权 + +从 RabbitMQ3.2 版本开始,代理现在支持消费者优先权(参见[使用 RabbitMQ 的消费者优先级](https://www.rabbitmq.com/blog/2013/12/16/using-consumer-priorities-with-rabbitmq/))。这可以通过在消费者上设置`x-priority`参数来实现。`SimpleMessageListenerContainer`现在支持设置消费者参数,如下例所示: + +``` +container.setConsumerArguments(Collections. + singletonMap("x-priority", Integer.valueOf(10))); +``` + +为了方便起见,名称空间在`listener`元素上提供了`priority`属性,如下例所示: + +``` + + + +``` + +从版本 1.3 开始,你可以修改容器在运行时监听的队列。见[监听器容器队列](#listener-queues)。 + +###### [](#lc-auto-delete)`auto-delete`队列 + +当容器被配置为侦听`auto-delete`队列时,队列具有`x-expires`选项,或者在代理上配置了[活着的时间](https://www.rabbitmq.com/ttl.html)策略,当容器停止时(即最后一个使用者被取消时),代理将队列删除。在版本 1.3 之前,由于缺少队列,无法重新启动容器。`RabbitAdmin`仅在连接关闭或打开时自动重新声明队列,以此类推,而在停止和启动容器时不会发生这种情况。 + +从版本 1.3 开始,容器使用[选择容器](#choose-container)在启动过程中重新声明任何丢失的队列。 + +你还可以使用条件声明(参见[有条件声明](#conditional-declaration))和`@RabbitHandler`管理来延迟队列声明,直到容器启动。下面的示例展示了如何做到这一点: + +``` + + + + + + + + + + + + + +``` + +在这种情况下,队列和交换由`containerAdmin`声明,其具有,因此在上下文初始化期间不会声明元素。同样,由于同样的原因,容器没有启动。当容器稍后被启动时,它使用其对`containerAdmin`的引用来声明元素。 + +##### [](#de-batching)批处理消息 + +批处理的消息(由生成者创建)由侦听器容器(使用`springBatchFormat`消息头)自动进行去批处理。拒绝批处理中的任何消息都会导致整个批处理被拒绝。有关批处理的更多信息,请参见[Batching](#template-batching)。 + +从版本 2.2 开始,`SimpleMessageListenerContainer`可以用于在消费者端(生产者发送离散消息的地方)创建批处理。 + +设置容器属性`consumerBatchEnabled`以启用此功能。`deBatchingEnabled`也必须为真,以便容器负责处理这两种类型的批。当`consumerBatchEnabled`为真时实现`BatchMessageListener`或`ChannelAwareBatchMessageListener`。从版本 2.2.7 开始,`SimpleMessageListenerContainer`和`DirectMessageListenerContainer`都可以将[生产者创建了批](#template-batching)删除为`List`。有关在`@RabbitListener`中使用此功能的信息,请参见`DirectMessageListenerContainer`。 + +##### [](#consumer-events)消费者活动 + +容器在侦听器(使用者)遇到某种故障时发布应用程序事件。事件`ListenerContainerConsumerFailedEvent`具有以下属性: + +* `container`:消费者遇到问题的侦听器容器。 + +* `reason`:失败的文本原因。 + +* `fatal`:表示失败是否致命的布尔值。对于非致命的异常,容器会根据`recoveryInterval`或`recoveryBackoff`(对于`SimpleMessageListenerContainer`)或`recoveryBackoff`(对于`DirectMessageListenerContainer`)尝试重新启动消费者。 + +* `throwable`:抓到的`Throwable`。 + +可以通过实现`ApplicationListener`来消耗这些事件。 + +| |当`concurrentConsumers`大于 1 时,所有使用者都会发布系统范围的事件(例如连接失败)。| +|---|-----------------------------------------------------------------------------------------------------------------------------| + +如果一个使用者因其队列被独占使用而失败,那么默认情况下,以及在发布事件时,将发布`WARN`日志。要更改此日志记录行为,请在`SimpleMessageListenerContainer`实例的`exclusiveConsumerExceptionLogger`属性中提供一个自定义`ConditionalExceptionLogger`。另见[记录通道关闭事件](#channel-close-logging)。 + +致命错误总是记录在`ERROR`级别。这是不可更改的。 + +在容器生命周期的各个阶段还发布了其他几个事件: + +* `AsyncConsumerStartedEvent`:启动消费者时。 + +* `AsyncConsumerRestartedEvent`:当使用者在失败后重新启动时-`SimpleMessageListenerContainer`仅。 + +* `AsyncConsumerTerminatedEvent`:当消费者被正常停止时。 + +* `AsyncConsumerStoppedEvent`:仅当消费者被停止时-`SimpleMessageListenerContainer`。 + +* `ConsumeOkEvent`:当从代理收到`consumeOk`时,包含队列名和`consumerTag` + +* `ListenerContainerIdleEvent`:见[检测空闲异步消费者](#idle-containers)。 + +* [检测空闲异步消费者](#idle-containers):检测到缺少队列时。 + +##### [](#consumerTags)消费者标签 + +你可以提供一种生成消费者标签的策略。默认情况下,消费者标记是由代理生成的。下面的清单显示了`ConsumerTagStrategy`接口定义: + +``` +public interface ConsumerTagStrategy { + + String createConsumerTag(String queue); + +} +``` + +队列是可用的,以便它可以(可选地)在标记中使用。 + +见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#async-annotation-driven)注释驱动的侦听器端点 + +异步接收消息的最简单方法是使用带注释的侦听器端点基础结构。简而言之,它允许你将托管 Bean 方法公开为 Rabbit 侦听器端点。下面的示例展示了如何使用`@RabbitListener`注释: + +``` +@Component +public class MyService { + + @RabbitListener(queues = "myQueue") + public void processOrder(String data) { + ... + } + +} +``` + +前面示例的思想是,每当名为`myQueue`的队列上有消息可用时,都会相应地调用`ClassMapper`方法(在这种情况下,使用消息的有效负载)。 + +通过使用`RabbitListenerContainerFactory`,带注释的端点基础设施在幕后为每个带注释的方法创建了一个消息侦听器容器。 + +在前面的示例中,`myQueue`必须已经存在并绑定到某个交换。只要应用程序上下文中存在`RabbitAdmin`,就可以自动声明和绑定队列。 + +| |可以为注释属性(`queues`等)指定属性占位符(`${some.property}`)或 spel 表达式(`#{someExpression}`)。
参见[监听多个队列](#annotation-multiple-queues)有关为什么可能使用 spel 而不是属性占位符的示例。
下面的列表显示了如何声明 Rabbit 侦听器的三个示例:| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Component +public class MyService { + + @RabbitListener(bindings = @QueueBinding( + value = @Queue(value = "myQueue", durable = "true"), + exchange = @Exchange(value = "auto.exch", ignoreDeclarationExceptions = "true"), + key = "orderRoutingKey") + ) + public void processOrder(Order order) { + ... + } + + @RabbitListener(bindings = @QueueBinding( + value = @Queue, + exchange = @Exchange(value = "auto.exch"), + key = "invoiceRoutingKey") + ) + public void processInvoice(Invoice invoice) { + ... + } + + @RabbitListener(queuesToDeclare = @Queue(name = "${my.queue}", durable = "true")) + public String handleWithSimpleDeclare(String data) { + ... + } + +} +``` + +在第一个示例中,如果需要,一个队列`myQueue`将与交换器一起自动(持久)声明,并用路由密钥绑定到交换器。在第二个示例中,声明并绑定了一个匿名(排他的、自动删除的)队列。可以提供多个`QueueBinding`条目,让侦听器监听多个队列。在第三个示例中,如果有必要,将使用默认绑定到默认的 Exchange,并使用队列名称作为路由密钥,声明一个具有从属性`my.queue`中检索到的名称的队列。 + +自 2.0 版本以来,`@Exchange`注释支持任何交换类型,包括自定义。有关更多信息,请参见[AMQP 概念](https://www.rabbitmq.com/tutorials/amqp-concepts.html)。 + +当需要更高级的配置时,可以使用普通的`@Bean`定义。 + +请注意第一个示例中的 Exchange 上的`ignoreDeclarationExceptions`。例如,这允许绑定到可能具有不同设置的现有交换机(例如`internal`)。默认情况下,现有交易所的属性必须匹配。 + +从版本 2.0 开始,你现在可以将队列绑定到具有多个路由密钥的交易所,如下例所示: + +``` +... + key = { "red", "yellow" } +... +``` + +还可以在`@QueueBinding`注释中为队列、交换和绑定指定参数,如下例所示: + +``` +@RabbitListener(bindings = @QueueBinding( + value = @Queue(value = "auto.headers", autoDelete = "true", + arguments = @Argument(name = "x-message-ttl", value = "10000", + type = "java.lang.Integer")), + exchange = @Exchange(value = "auto.headers", type = ExchangeTypes.HEADERS, autoDelete = "true"), + arguments = { + @Argument(name = "x-match", value = "all"), + @Argument(name = "thing1", value = "somevalue"), + @Argument(name = "thing2") + }) +) +public String handleWithHeadersExchange(String foo) { + ... +} +``` + +注意,对于队列,`x-message-ttl`参数被设置为 10 秒。因为参数类型不是`String`,所以我们必须指定它的类型——在本例中,`Integer`。与所有此类声明一样,如果队列已经存在,则参数必须与队列上的参数匹配。对于头交换,我们将绑定参数设置为匹配将`thing1`头设置为`somevalue`的消息,并且`thing2`头必须带有任何值。`x-match`参数意味着这两个条件都必须满足。 + +参数名称、值和类型可以是属性占位符(`${…​}`)或 SPEL 表达式(`#{…​}`)。`name`必须解析为`String`。`type`的表达式必须解析为`Class`或类的完全限定名称。`value`必须解析为可以由`DefaultConversionService`转换为类型的内容(例如前面示例中的`x-message-ttl`)。 + +如果名称解析为`null`或空的`String`,则忽略`@Argument`。 + +###### [](#meta-annotation-driven)元注解 + +有时你可能希望对多个侦听器使用相同的配置。为了减少样板配置,你可以使用元注释来创建自己的侦听器注释。下面的示例展示了如何做到这一点: + +``` +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@RabbitListener(bindings = @QueueBinding( + value = @Queue, + exchange = @Exchange(value = "metaFanout", type = ExchangeTypes.FANOUT))) +public @interface MyAnonFanoutListener { +} + +public class MetaListener { + + @MyAnonFanoutListener + public void handle1(String foo) { + ... + } + + @MyAnonFanoutListener + public void handle2(String foo) { + ... + } + +} +``` + +在前面的示例中,由`@MyAnonFanoutListener`注释创建的每个侦听器都将一个匿名的自动删除队列绑定到 FanOut Exchange,`metaFanout`。从版本 2.2.3 开始,支持`@AliasFor`,以允许在元注释注释上覆盖属性。此外,用户注释现在可以是`@Repeatable`,从而允许为一个方法创建多个容器。 + +``` +@Component +static class MetaAnnotationTestBean { + + @MyListener("queue1") + @MyListener("queue2") + public void handleIt(String body) { + } + +} + +@RabbitListener +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(MyListeners.class) +static @interface MyListener { + + @AliasFor(annotation = RabbitListener.class, attribute = "queues") + String[] value() default {}; + +} + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +static @interface MyListeners { + + MyListener[] value(); + +} +``` + +###### [](#async-annotation-driven-enable)启用侦听器端点注释 + +要启用对`@RabbitListener`注释的支持,你可以将`@EnableRabbit`添加到你的一个`@Configuration`类中。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableRabbit +public class AppConfig { + + @Bean + public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setConcurrentConsumers(3); + factory.setMaxConcurrentConsumers(10); + factory.setContainerCustomizer(container -> /* customize the container */); + return factory; + } +} +``` + +自 2.0 版本以来,`DirectMessageListenerContainerFactory`也可用。它创建`DirectMessageListenerContainer`实例。 + +| |有关帮助你在`SimpleRabbitListenerContainerFactory`和`DirectRabbitListenerContainerFactory`之间进行选择的信息,请参见[选择容器](#choose-container)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.2.2 开始,你可以提供`ContainerCustomizer`实现(如上图所示)。在创建和配置了容器之后,可以使用它来进一步配置容器;例如,你可以使用它来设置容器工厂不公开的属性。 + +默认情况下,基础结构会寻找一个名为`rabbitListenerContainerFactory`的 Bean 作为工厂用来创建消息侦听器容器的源。在这种情况下,忽略 RabbitMQ 基础设施设置,`processOrder`方法可以通过三个线程的核心轮询大小和十个线程的最大池大小来调用。 + +你可以自定义用于每个注释的侦听器容器工厂,或者可以通过实现`RabbitListenerConfigurer`接口来配置显式默认值。只有当至少有一个端点在没有特定容器工厂的情况下注册时,才需要默认设置。有关详细信息和示例,请参见[Javadoc](https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html)。 + +容器工厂提供了用于添加`MessagePostProcessor`实例的方法,这些实例是在接收消息(调用侦听器之前)和发送回复之前应用的。 + +有关回复的信息,请参见[回复管理](#async-annotation-driven-reply)。 + +从版本 2.0.6 开始,你可以向监听器容器工厂添加`RetryTemplate`和`RecoveryCallback`。在发送回复时使用它。当重试用完时,将调用`RecoveryCallback`。你可以使用`SendRetryContextAccessor`从上下文中获取信息。下面的示例展示了如何做到这一点: + +``` +factory.setRetryTemplate(retryTemplate); +factory.setReplyRecoveryCallback(ctx -> { + Message failed = SendRetryContextAccessor.getMessage(ctx); + Address replyTo = SendRetryContextAccessor.getAddress(ctx); + Throwable t = ctx.getLastThrowable(); + ... + return null; +}); +``` + +如果你更喜欢 XML 配置,那么可以使用``元素。检测到任何带有`@RabbitListener`注释的 bean。 + +对于`SimpleRabbitListenerContainer`实例,你可以使用类似于以下的 XML: + +``` + + + + + + + +``` + +对于`DirectMessageListenerContainer`实例,你可以使用类似于以下的 XML: + +``` + + + + + + +``` + +从版本 2.0 开始,`@RabbitListener`注释具有`concurrency`属性。它支持 SPEL 表达式(`@RabbitListener`)和属性占位符(`${…​}`)。其含义和允许的值取决于容器类型,如下所示: + +* 对于`DirectMessageListenerContainer`,该值必须是一个整数值,该值设置容器上的`consumersPerQueue`属性。 + +* 对于`SimpleRabbitListenerContainer`,该值可以是一个整数值,它在容器上设置`concurrentConsumers`属性,或者它可以具有以下形式,`m`,其中`m`是`concurrentConsumers`属性,`n`是`maxConcurrentConsumers`属性。 + +在这两种情况下,此设置都会覆盖工厂上的设置。以前,如果你的侦听器需要不同的并发性,则必须定义不同的容器工厂。 + +注释还允许通过`autoStartup`和`executor`(自 2.2)注释属性重写工厂`taskExecutor`属性。对每个执行器使用不同的执行器可能有助于在日志和线程转储中识别与每个侦听器关联的线程。 + +版本 2.2 还添加了`ackMode`属性,它允许你覆盖容器工厂的`ackMode`属性。 + +``` +@RabbitListener(id = "manual.acks.1", queues = "manual.acks.1", ackMode = "MANUAL") +public void manual1(String in, Channel channel, + @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException { + + ... + channel.basicAck(tag, false); +} +``` + +###### [](#async-annotation-conversion)带注释方法的消息转换 + +在调用侦听器之前,管道中有两个转换步骤。第一步使用`MessageConverter`将传入的 Spring AMQP`Message`转换为 Spring-messaging`Message`。当调用目标方法时,如果需要,消息有效负载将被转换为方法参数类型。 + +第一步的默认`MessageConverter`是一个 Spring AMQP`SimpleMessageConverter`,它处理向`String`和`java.io.Serializable`对象的转换。所有其他参数保持为`byte[]`。在下面的讨论中,我们将其称为“消息转换器”。 + +第二步的默认转换器是`GenericMessageConverter`,它委托给一个转换服务(`DefaultFormattingConversionService`的实例)。在下面的讨论中,我们将其称为“方法参数转换器”。 + +要更改消息转换器,可以将其作为一个属性添加到容器工厂 Bean。下面的示例展示了如何做到这一点: + +``` +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + ... + factory.setMessageConverter(new Jackson2JsonMessageConverter()); + ... + return factory; +} +``` + +这配置了一个 Jackson2 转换器,该转换器期望出现头信息以指导转换。 + +还可以使用`ContentTypeDelegatingMessageConverter`,它可以处理不同内容类型的转换。 + +从版本 2.3 开始,你可以通过在`Message`属性中指定 Bean 名称来覆盖工厂转换器。 + +``` +@Bean +public Jackson2JsonMessageConverter jsonConverter() { + return new Jackson2JsonMessageConverter(); +} + +@RabbitListener(..., messageConverter = "jsonConverter") +public void listen(String in) { + ... +} +``` + +这样就避免了仅仅为了更改转换器就必须声明一个不同的容器工厂。 + +在大多数情况下,不需要自定义方法参数转换器,除非你希望使用自定义`ConversionService`。 + +在 1.6 之前的版本中,转换 JSON 的类型信息必须在消息头中提供,或者需要自定义`ClassMapper`。从版本 1.6 开始,如果没有类型信息标头,则可以从目标方法参数推断类型。 + +| |这种类型推理在方法级别上仅适用于`@RabbitListener`。| +|---|-------------------------------------------------------------------------| + +有关更多信息,请参见[Jackson2JSONMessageConverter](#json-message-converter)。 + +如果你希望自定义方法参数转换器,可以按以下方式进行: + +``` +@Configuration +@EnableRabbit +public class AppConfig implements RabbitListenerConfigurer { + + ... + + @Bean + public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { + DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); + factory.setMessageConverter(new GenericMessageConverter(myConversionService())); + return factory; + } + + @Bean + public DefaultConversionService myConversionService() { + DefaultConversionService conv = new DefaultConversionService(); + conv.addConverter(mySpecialConverter()); + return conv; + } + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory()); + } + + ... + +} +``` + +| |对于多方法侦听器(参见[多方法侦听器](#annotation-method-selection)),方法选择是基于消息的有效负载**消息转换后**。
只有在方法已被选择之后才调用方法参数转换器。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### [](#custom-argument-resolver)向 @RabbitListener 添加自定义`HandlerMethodArgumentResolver` + +从版本 2.3.7 开始,你可以添加自己的`HandlerMethodArgumentResolver`并解析自定义方法参数。你所需要的只是实现`ClassMapper`并使用来自类`RabbitListenerEndpointRegistrar`的方法`setCustomMethodArgumentResolvers()`。 + +``` +@Configuration +class CustomRabbitConfig implements RabbitListenerConfigurer { + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setCustomMethodArgumentResolvers( + new HandlerMethodArgumentResolver() { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, org.springframework.messaging.Message message) { + return new CustomMethodArgument( + (String) message.getPayload(), + message.getHeaders().get("customHeader", String.class) + ); + } + + } + ); + } + +} +``` + +###### [](#async-annotation-driven-registration)程序化端点注册 + +`RabbitListenerEndpoint`提供了一个 Rabbit 端点的模型,并负责为该模型配置容器。除了通过`RabbitListener`注释检测到的端点之外,该基础结构还允许你以编程方式配置端点。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableRabbit +public class AppConfig implements RabbitListenerConfigurer { + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); + endpoint.setQueueNames("anotherQueue"); + endpoint.setMessageListener(message -> { + // processing + }); + registrar.registerEndpoint(endpoint); + } +} +``` + +在前面的示例中,我们使用了`SimpleRabbitListenerEndpoint`,它提供了要调用的实际`MessageListener`,但是你也可以构建自己的端点变体来描述自定义调用机制。 + +需要注意的是,你也可以完全跳过`@RabbitListener`的使用,并通过`RabbitListenerConfigurer`以编程方式注册端点。 + +###### [](#async-annotation-driven-enable-signature)带注释的端点方法签名 + +到目前为止,我们已经在我们的端点中注入了一个简单的`String`,但是它实际上可以有一个非常灵活的方法签名。下面的示例重写它,以使用自定义标头注入`Order`: + +``` +@Component +public class MyService { + + @RabbitListener(queues = "myQueue") + public void processOrder(Order order, @Header("order_type") String orderType) { + ... + } +} +``` + +下面的列表显示了可用于与侦听器端点中的参数进行匹配的参数: + +* RAW`org.springframework.amqp.core.Message`。 + +* 来自 RAW`Message`的`MessageProperties`。 + +* 在其上接收消息的`com.rabbitmq.client.Channel`。 + +* 从传入的 AMQP 消息转换的`${…​}`。 + +* `@Header`-带注释的方法参数,以提取特定的标头值,包括标准的 AMQP 标头。 + +* `@Headers`-带注释的参数,该参数还必须分配给`java.util.Map`,以获得对所有标题的访问权限。 + +* 转换后的有效载荷 + +不是受支持类型之一的非注释元素(即`Message`,`MessageProperties`,`Message`和`Channel`)与有效负载匹配。你可以通过使用`@Payload`对参数进行注释来使其显式。你还可以通过添加一个额外的`@Valid`来打开验证。 + +注入 Spring 的消息抽象的能力特别有用,可以从存储在特定传输消息中的所有信息中受益,而无需依赖特定传输 API。下面的示例展示了如何做到这一点: + +``` +@RabbitListener(queues = "myQueue") +public void processOrder(Message order) { ... +} +``` + +方法参数的处理由`DefaultMessageH和lerMethodFactory`提供,你可以进一步自定义该参数以支持其他方法参数。转换和验证支持也可以在那里进行定制。 + +例如,如果我们想在处理`Order`之前确保我们的`Order`是有效的,我们可以用`@Valid`注释有效负载,并配置必要的验证器,如下所示: + +``` +@Configuration +@EnableRabbit +public class AppConfig implements RabbitListenerConfigurer { + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory()); + } + + @Bean + public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { + DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); + factory.setValidator(myValidator()); + return factory; + } +} +``` + +###### [](#rabbit-validation)@rabbitlistener@payload validation + +从版本 2.3.7 开始,现在更容易添加`Validator`来验证`@RabbitListener`和`@RabbitHandler``@Payload`参数。现在,你只需将验证器添加到注册商本身即可。 + +``` +@Configuration +@EnableRabbit +public class Config implements RabbitListenerConfigurer { + ... + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setValidator(new MyValidator()); + } +} +``` + +| |当使用 Spring boot 和验证启动器时,`LocalValidatorFactoryBean`是自动配置的:| +|---|-----------------------------------------------------------------------------------------------------| + +``` +@Configuration +@EnableRabbit +public class Config implements RabbitListenerConfigurer { + @Autowired + private LocalValidatorFactoryBean validator; + ... + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setValidator(this.validator); + } +} +``` + +验证: + +``` +public static class ValidatedClass { + @Max(10) + private int bar; + public int getBar() { + return this.bar; + } + public void setBar(int bar) { + this.bar = bar; + } +} +``` + +and + +``` +@RabbitListener(id="validated", queues = "queue1", errorHandler = "validationErrorHandler", + containerFactory = "jsonListenerContainerFactory") +public void validatedListener(@Payload @Valid ValidatedClass val) { + ... +} +@Bean +public RabbitListenerErrorHandler validationErrorHandler() { + return (m, e) -> { + ... + }; +} +``` + +###### [](#annotation-multiple-queues)监听多个队列 + +当你使用`queues`属性时,你可以指定关联的容器可以侦听多个队列。你可以使用`@Header`注释使 POJO 方法可以使用接收消息的队列名称。下面的示例展示了如何做到这一点: + +``` +@Component +public class MyService { + + @RabbitListener(queues = { "queue1", "queue2" } ) + public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + ... + } + +} +``` + +从版本 1.5 开始,你可以使用属性占位符和 SPEL 来外部化队列名称。下面的示例展示了如何做到这一点: + +``` +@Component +public class MyService { + + @RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" ) + public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + ... + } + +} +``` + +在版本 1.5 之前,只能以这种方式指定单个队列。每个队列都需要一个单独的属性。 + +###### [](#async-annotation-driven-reply)回复管理 + +`MessageListenerAdapter`中现有的支持已经允许你的方法具有一个非 void 返回类型。在这种情况下,调用的结果被封装在一条消息中,发送到原始消息的`ReplyToAddress`头中指定的地址,或发送到侦听器上配置的默认地址。你可以通过使用消息抽象的`@SendTo`注释来设置默认地址。 + +假设我们的`processOrder`方法现在应该返回一个`OrderStatus`,我们可以按以下方式编写它来自动发送回复: + +``` +@RabbitListener(destination = "myQueue") +@SendTo("status") +public OrderStatus processOrder(Order order) { + // order processing + return status; +} +``` + +如果需要以与传输无关的方式设置额外的头,则可以返回`Message`,如下所示: + +``` +@RabbitListener(destination = "myQueue") +@SendTo("status") +public Message processOrder(Order order) { + // order processing + return MessageBuilder + .withPayload(status) + .setHeader("code", 1234) + .build(); +} +``` + +或者,你可以在`beforeSendReplyMessagePostProcessors`容器工厂属性中使用`MessagePostProcessor`来添加更多标题。从版本 2.2.3 开始,被调用的 Bean/方法在回复消息中是可用的,该回复消息可以在消息后置处理器中用来将信息通信回调用者: + +``` +factory.setBeforeSendReplyPostProcessors(msg -> { + msg.getMessageProperties().setHeader("calledBean", + msg.getMessageProperties().getTargetBean().getClass().getSimpleName()); + msg.getMessageProperties().setHeader("calledMethod", + msg.getMessageProperties().getTargetMethod().getName()); + return m; +}); +``` + +从版本 2.2.5 开始,你可以配置一个`ReplyPostProcessor`来修改发送之前的回复消息;在`correlationId`头设置为与请求匹配后调用它。 + +``` +@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoCustomHeader") +public String capitalizeWithHeader(String in) { + return in.toUpperCase(); +} + +@Bean +public ReplyPostProcessor echoCustomHeader() { + return (req, resp) -> { + resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); + return resp; + }; +} +``` + +将`@SendTo`值假定为一个响应`exchange`和`routingKey`对,该对遵循`exchange/routingKey`模式,其中一个部分可以省略。有效数值如下: + +* `thing1/thing2`:`replyTo`交易所和`routingKey`.`queues`交易所:`replyTo`交易所和默认(空)`routingKey`。`/thing2`或`/thing2`交易所和默认(空)交易所。`/`或空:默认`routingKey`交易所和“默认交易所。 + +同样,你可以使用`@SendTo`而不使用`value`属性。这种情况等于一个空的`!{…​}`模式。`@SendTo`仅在入站消息没有`replyToAddress`属性的情况下才使用。 + +从版本 1.5 开始,`@SendTo`值可以是 Bean 初始化 SPEL 表达式,如下例所示: + +``` +@RabbitListener(queues = "test.sendTo.spel") +@SendTo("#{spelReplyTo}") +public String capitalizeWithSendToSpel(String foo) { + return foo.toUpperCase(); +} +... +@Bean +public String spelReplyTo() { + return "test.sendTo.reply.spel"; +} +``` + +表达式必须求值为`String`,它可以是一个简单的队列名称(发送到默认的 Exchange),也可以是与前面示例之前讨论的`exchange/routingKey`形式一起。 + +| |在初始化过程中,对`#{…​}`表达式进行一次求值。| +|---|----------------------------------------------------------------| + +对于动态回复路由,消息发送方应该包括`reply_to`消息属性,或者使用备用运行时 SPEL 表达式(在下一个示例之后描述)。 + +从版本 1.6 开始,`@SendTo`可以是一个 SPEL 表达式,在运行时根据请求和回复进行求值,如下例所示: + +``` +@RabbitListener(queues = "test.sendTo.spel") +@SendTo("!{'some.reply.queue.with.' + result.queueName}") +public Bar capitalizeWithSendToSpel(Foo foo) { + return processTheFooAndReturnABar(foo); +} +``` + +spel 表达式的运行时性质用`!{…​}`分隔符表示。表达式的求值上下文`#root`对象具有三个属性: + +* `request`:`o.s.amqp.core.Message`请求对象。 + +* `source`:转换后的`o.s.messaging.Message`。 + +* `result`:方法结果。 + +上下文有一个映射属性访问器、一个标准类型转换器和一个 Bean 解析器,它允许引用上下文中的其他 bean(例如,`@someBeanName.determineReplyQ(request, result)`)。 + +总之,`#{…​}`在初始化过程中被求值一次,而`#root`对象是应用程序上下文。bean 由它们的名称引用。`!{…​}`在运行时对每个消息进行求值,根对象具有前面列出的属性。bean 的名字是引用的,前缀是`@`。 + +从版本 2.1 开始,还支持简单属性占位符(例如,`${some.reply.to}`)。对于较早的版本,可以使用以下方法,如下例所示: + +``` +@RabbitListener(queues = "foo") +@SendTo("#{environment['my.send.to']}") +public String listen(Message in) { + ... + return ... +} +``` + +###### [](#reply-content-type)回复 ContentType + +如果使用复杂的消息转换器,例如`ContentTypeDelegatingMessageConverter`,则可以通过在侦听器上设置`replyContentType`属性来控制回复的内容类型。这允许转换器为应答选择适当的委托转换器。 + +``` +@RabbitListener(queues = "q1", messageConverter = "delegating", + replyContentType = "application/json") +public Thing2 listen(Thing1 in) { + ... +} +``` + +默认情况下,为了向后兼容,转换器设置的任何内容类型属性在转换后都将被这个值覆盖。诸如`SimpleMessageConverter`之类的转换器使用回复类型而不是内容类型来确定所需的转换,并在回复消息中适当地设置内容类型。这可能不是所需的操作,可以通过将`converterWinsContentType`属性设置为`SimpleRabbitListenerEndpoint`来重写。例如,如果返回一个包含 JSON 的`String`,则`SimpleMessageConverter`将在答复中将内容类型设置为`text/plain`。下面的配置将确保正确设置内容类型,即使使用`SimpleMessageConverter`。 + +``` +@RabbitListener(queues = "q1", replyContentType = "application/json", + converterWinsContentType = "false") +public String listen(Thing in) { + ... + return someJsonString; +} +``` + +当返回类型是 Spring AMQP`Message`或 Spring 消息传递`Message`时,这些属性(`replyContentType`和`converterWinsContentType`)不适用。在第一种情况下,不涉及转换;只需设置`contentType`消息属性。在第二种情况下,使用消息头来控制行为: + +``` +@RabbitListener(queues = "q1", messageConverter = "delegating") +@SendTo("q2") +public Message listen(String in) { + ... + return MessageBuilder.withPayload(in.toUpperCase()) + .setHeader(MessageHeaders.CONTENT_TYPE, "application/xml") + .build(); +} +``` + +此内容类型将在`MessageProperties`中传递给转换器。默认情况下,为了向后兼容,转换器设置的任何内容类型属性在转换后都将被这个值覆盖。如果你希望重写该行为,还可以将`AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS`设置为`true`,并且转换器设置的任何值都将被保留。 + +###### [](#annotation-method-selection)多方法侦听器 + +从版本 1.5.0 开始,你可以在类级别上指定`@RabbitListener`注释。与新的`@RabbitHandler`注释一起,这允许单个侦听器根据传入消息的有效负载类型调用不同的方法。这一点最好用一个例子来描述: + +``` +@RabbitListener(id="multi", queues = "someQueue") +@SendTo("my.reply.queue") +public class MultiListenerBean { + + @RabbitHandler + public String thing2(Thing2 thing2) { + ... + } + + @RabbitHandler + public String cat(Cat cat) { + ... + } + + @RabbitHandler + public String hat(@Header("amqp_receivedRoutingKey") String rk, @Payload Hat hat) { + ... + } + + @RabbitHandler(isDefault = true) + public String defaultMethod(Object object) { + ... + } + +} +``` + +在这种情况下,如果转换的有效负载是`Thing2`、`Cat`或`Hat`,则会调用单独的`@RabbitHandler`方法。你应该理解,系统必须能够基于有效负载类型来识别唯一的方法。将检查类型是否可分配给没有注释或使用`@Payload`注释的单个参数。请注意,同样的方法签名也适用,如方法级别`@RabbitListener`([前面描述的](#message-listener-adapter))中所讨论的那样。 + +从版本 2.0.3 开始,可以将`@RabbitHandler`方法指定为默认方法,如果其他方法不匹配,则调用该方法。最多只能指定一种方法。 + +| |`@RabbitHandler`仅用于处理转换后的消息负载,如果希望接收未转换的 RAW`Message`对象,则必须在方法上使用`@RabbitListener`,而不是在类上。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### [](#repeatable-rabbit-listener)`@Repeatable` `@RabbitListener` + +从版本 1.6 开始,`@RabbitListener`注释被标记为`@Repeatable`。这意味着注释可以多次出现在相同的注释元素(方法或类)上。在这种情况下,将为每个注释创建一个单独的侦听器容器,每个注释都调用相同的侦听器`@Bean`。可重复的注释可以用于 Java8 或更高版本。 + +###### [](#proxy-rabbitlistener-and-generics)代理`@RabbitListener`和泛型 + +如果你的服务旨在被代理(例如,在`@Transactional`的情况下),那么当接口具有通用参数时,你应该记住一些考虑因素。考虑以下示例: + +``` +interface TxService

{ + + String handle(P payload, String header); + +} + +static class TxServiceImpl implements TxService { + + @Override + @RabbitListener(...) + public String handle(Thing thing, String rk) { + ... + } + +} +``` + +对于通用接口和特定实现,你将被迫切换到 CGLIB 目标类代理,因为接口`handle`方法的实际实现是一种桥接方法。在事务管理的情况下,通过使用一个注释选项来配置 CGLIB 的使用:`@EnableTransactionManagement(proxyTargetClass = true)`。在这种情况下,所有注释都必须在实现中的目标方法上声明,如下例所示: + +``` +static class TxServiceImpl implements TxService { + + @Override + @Transactional + @RabbitListener(...) + public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) { + ... + } + +} +``` + +###### [](#annotation-error-handling)处理异常 + +默认情况下,如果一个带注释的侦听器方法抛出一个异常,它将被抛出到容器,消息将被重新请求并重新交付、丢弃或路由到死信交换,这取决于容器和代理配置。任何东西都不会退还给寄件人。 + +从版本 2.0 开始,`@RabbitListener`注释有两个新属性:`errorHandler`和`returnExceptions`。 + +默认情况下,不会对它们进行配置。 + +你可以使用`errorHandler`来提供`RabbitListenerErrorHandler`实现的 Bean 名称。此功能接口有一种方法,如下所示: + +``` +@FunctionalInterface +public interface RabbitListenerErrorHandler { + + Object handleError(Message amqpMessage, org.springframework.messaging.Message message, + ListenerExecutionFailedException exception) throws Exception; + +} +``` + +如你所见,你可以访问从容器接收的原始消息、消息转换器产生的 Spring-messaging`Message`对象,以及侦听器抛出的异常(包装在`ListenerExecutionFailedException`中)。错误处理程序可以返回一些结果(作为回复发送),也可以抛出原始异常或新的异常(根据`returnExceptions`设置,将其抛出到容器或返回给发送方)。 + +当`true`时,`returnExceptions`属性将导致异常返回给发送方。异常包装在`RemoteInvocationResult`对象中。在发送端,有一个可用的`RemoteInvocationAwareMessageConverterAdapter`,如果将其配置为`RabbitTemplate`,则会重新抛出服务器端异常,并包装在`AmqpRemoteException`中。服务器异常的堆栈跟踪是通过合并服务器和客户端堆栈跟踪来合成的。 + +| |这种机制通常仅对默认的`SimpleMessageConverter`有效,后者使用 Java 序列化。
异常通常不是“Jackson 友好的”,并且不能序列化到 JSON。
如果使用 JSON,请考虑在抛出异常时使用`errorHandler`返回一些其他 Jackson 友好的`Error`对象。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在版本 2.1 中,此接口从包`o.s.amqp.rabbit.listener`移动到`o.s.amqp.rabbit.listener.api`。| +|---|---------------------------------------------------------------------------------------------------------------| + +从版本 2.1.7 开始,`Channel`在消息消息头中可用;这允许你在使用`AcknowledgeMode.MANUAL`时对失败的消息进行 ACK 或 NACK: + +``` +public Object handleError(Message amqpMessage, org.springframework.messaging.Message message, + ListenerExecutionFailedException exception) { + ... + message.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class) + .basicReject(message.getHeaders().get(AmqpHeaders.DELIVERY_TAG, Long.class), + true); + } +``` + +从版本 2.2.18 开始,如果抛出消息转换异常,将调用错误处理程序,在`message`参数中使用`null`。这允许应用程序向调用者发送一些结果,表明收到了格式错误的消息。以前,此类错误是由容器抛出和处理的。 + +###### [](#container-management)容器管理 + +为注释创建的容器未在应用程序上下文中注册。你可以通过在`RabbitListenerEndpointRegistry` Bean 上调用`getListenerContainers()`来获得所有容器的集合。然后可以迭代这个集合,例如,停止或启动所有容器,或者调用注册表本身上的`Lifecycle`方法,这些方法将调用每个容器上的操作。 + +你还可以通过使用其`id`,使用`getListenerContainer(String id)`,获得对单个容器的引用——例如,对于上面的代码片段创建的容器,`registry.getListenerContainer("multi")`。 + +从版本 1.5.2 开始,你可以使用`getListenerContainerIds()`获得已注册容器的`id`值。 + +从版本 1.5 开始,你现在可以将`group`分配到`RabbitListener`端点上的容器。这提供了一种机制来获取对容器子集的引用。添加一个`group`属性会导致类型`Collection`的 Bean 被注册到带有组名的上下文中。 + +##### [](#receiving-batch)@rabbitlistener with batching + +当接收到[a batch](#template-batching)的消息时,解批处理通常由容器执行,侦听器在一次调用一条消息。从版本 2.2 开始,你可以将侦听器容器工厂和侦听器配置为在一个调用中接收整个批处理,只需设置工厂的`batchListener`属性,并将方法有效负载参数设置为`List`: + +``` +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setBatchListener(true); + return factory; +} + +@RabbitListener(queues = "batch.1") +public void listen1(List in) { + ... +} + +// or + +@RabbitListener(queues = "batch.2") +public void listen2(List> in) { + ... +} +``` + +将`batchListener`属性设置为 true 会自动关闭工厂创建的容器中的`deBatchingEnabled`容器属性(除非`consumerBatchEnabled`是`true`-见下文)。实际上,将 debatching 从容器移动到侦听器适配器,并且适配器将创建传递给侦听器的列表。 + +启用批处理的工厂不能与[多方法监听器](#annotation-method-selection)一起使用。 + +也从版本 2.2 开始。当一次只接收一条批处理消息时,最后一条消息包含一个布尔头,该头设置为`true`。这个头可以通过添加`@Header(AmqpHeaders.LAST_IN_BATCH)`BooleanLastMessageProperties.islastinBatch()AmQpHeaders 来获得。batch_size` 是用每个消息片段中批处理的大小填充的。 + +此外,在`SimpleMessageListenerContainer`中添加了一个新的属性`consumerBatchEnabled`。如果这是真的,容器将创建一批消息,最多为`batchSize`;如果`receiveTimeout`在没有新消息到达的情况下经过,则传递部分批消息。如果收到了生产者创建的批处理,则将其删除并添加到消费者侧批处理中;因此,实际交付的消息数量可能超过`batchSize`,表示从代理收到的消息的数量。`deBatchingEnabled`当`consumerBatchEnabled`为真时必须为真;容器工厂将强制执行此要求。 + +``` +@Bean +public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setConsumerTagStrategy(consumerTagStrategy()); + factory.setBatchListener(true); // configures a BatchMessageListenerAdapter + factory.setBatchSize(2); + factory.setConsumerBatchEnabled(true); + return factory; +} +``` + +当使用`consumerBatchEnabled`与`@RabbitListener`时: + +``` +@RabbitListener(queues = "batch.1", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch1(List amqpMessages) { + this.amqpMessagesReceived = amqpMessages; + this.batch1Latch.countDown(); +} + +@RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch2(List> messages) { + this.messagingMessagesReceived = messages; + this.batch2Latch.countDown(); +} + +@RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch3(List strings) { + this.batch3Strings = strings; + this.batch3Latch.countDown(); +} +``` + +* 第一个是用 RAW 调用的,未转换的`org.springframework.amqp.core.Message`s 接收到的。 + +* 第二个是用`org.springframework.messaging.Message`s 调用的,带有转换的有效负载和映射的头/属性。 + +* 第三种是使用转换后的有效负载进行调用,不访问 headers/properteis。 + +还可以添加`Channel`参数,该参数在使用`MANUAL`ACK 模式时经常使用。这在第三个示例中不是很有用,因为你无法访问`delivery_tag`属性。 + +##### [](#using-container-factories)使用容器工厂 + +引入了监听器容器工厂来支持`@RabbitListener`并使用`RabbitListenerEndpointRegistry`注册容器,如[程序化端点注册](#async-annotation-driven-registration)中所讨论的那样。 + +从版本 2.1 开始,它们可以用于创建任何侦听器容器——甚至是没有侦听器的容器(例如在 Spring 集成中使用的容器)。当然,必须在容器启动之前添加一个侦听器。 + +创建这样的容器有两种方法: + +* 使用 simplerabbitlistenerendPoint + +* 创建后添加监听器 + +下面的示例展示了如何使用`SimpleRabbitListenerEndpoint`创建侦听器容器: + +``` +@Bean +public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); + endpoint.setQueueNames("queue.1"); + endpoint.setMessageListener(message -> { + ... + }); + return rabbitListenerContainerFactory.createListenerContainer(endpoint); +} +``` + +下面的示例展示了如何在创建后添加侦听器: + +``` +@Bean +public SimpleMessageListenerContainer factoryCreatedContainerNoListener( + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer(); + container.setMessageListener(message -> { + ... + }); + container.setQueueNames("test.no.listener.yet"); + return container; +} +``` + +在这两种情况下,侦听器也可以是`ChannelAwareMessageListener`,因为它现在是`MessageListener`的子接口。 + +如果你希望创建具有类似属性的多个容器,或者使用预先配置的容器工厂(例如 Spring Boot Auto Configuration 提供的容器工厂),那么这些技术是有用的。 + +| |以这种方式创建的容器是正常的`@Bean`实例,并且不在`RabbitListenerEndpointRegistry`中注册。| +|---|------------------------------------------------------------------------------------------------------------------------| + +##### [](#async-returns)异步`@RabbitListener`返回类型 + +从版本 2.1 开始,`@RabbitListener`(和`@RabbitHandler`)方法可以用异步返回类型`ListenableFuture`和`Mono`来指定,让回复被异步发送。 + +| |侦听器容器工厂必须配置`AcknowledgeMode.MANUAL`,这样使用者线程就不会对消息进行 ACK;相反,异步完成将在异步操作完成时对消息进行 ACK 或 NACK,
当异步结果以错误完成时,消息是否被重新请求取决于抛出的异常类型、容器配置和容器错误处理程序,
默认情况下,消息将被重新请求,除非容器的`defaultRequeueRejected`属性被设置为`false`(默认情况下是`true`)。
如果异步结果是用`AmqpRejectAndDontRequeueException`完成的,则消息将不会被重新请求。,
如果容器的`defaultRequeueRejected`属性是`false`,你可以通过将 Future 的异常设置为`ImmediateRequeueException`来重写它,并且消息将被重新请求,
如果侦听器方法中发生了某些异常,从而阻止了异步结果对象的创建,你必须捕获该异常并返回一个适当的返回对象,该对象将导致消息被确认或重新请求。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.2.21、2.3.13、2.4.1 开始,当检测到异步返回类型时,`AcknowledgeMode`将自动设置`MANUAL`。此外,带有致命异常的传入消息将被单独地负面确认,以前任何先前未确认的消息也被负面确认。 + +##### [](#threading)线程和异步消费者 + +异步消费者涉及许多不同的线程。 + +在`SimpleMessageListenerContainer`中配置的`TaskExecutor`中的线程用于在`RabbitMQ Client`传递新消息时调用`MessageListener`。如果未配置,则使用`SimpleAsyncTaskExecutor`。如果使用池执行程序,则需要确保池大小足以处理配置的并发性。使用`DirectMessageListenerContainer`,在`RabbitMQ Client`线程上直接调用`MessageListener`。在这种情况下,`taskExecutor`用于监视消费者的任务。 + +| |当使用默认`SimpleAsyncTaskExecutor`时,对于侦听器所调用的线程,侦听器容器`beanName`在`threadNamePrefix`中使用。
这对于日志分析很有用。
我们通常建议始终在日志附录配置中包括线程名称。
当`TaskExecutor`通过容器上的`taskExecutor`属性特别提供时,它被原封不动地使用,
建议你使用类似的技术来命名由自定义`TaskExecutor` Bean 定义创建的线程,以帮助在日志消息中进行线程标识。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在创建连接时,在`CachingConnectionFactory`中配置的`Executor`被传递到`RabbitMQ Client`中,其线程用于将新消息传递到侦听器容器。如果未对此进行配置,则客户机将使用一个内部线程池执行器,每个连接的池大小(在编写时)为`Runtime.getRuntime().availableProcessors() * 2`。 + +如果你有大量的工厂或者正在使用`CacheMode.CONNECTION`,那么你可能希望考虑使用带有足够线程的共享`ThreadPoolTaskExecutor`来满足你的工作负载。 + +| |使用`DirectMessageListenerContainer`,你需要确保连接工厂配置了一个任务执行器,该执行器具有足够的线程来支持跨使用该工厂的所有侦听器容器的所需并发性。
缺省池大小(在编写本文时)为`Runtime.getRuntime().availableProcessors() * 2`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`RabbitMQ client`使用`ThreadFactory`为低级 I/O 操作创建线程。要修改这个工厂,你需要配置底层的 RabbitMQ`ConnectionFactory`,如[配置底层客户机连接工厂](#connection-factory)中所讨论的那样。 + +##### [](#choose-container)选择容器 + +2.0 版引入了`DirectMessageListenerContainer`。在此之前,只有`SimpleMessageListenerContainer`可用。SMLC 为每个使用者使用一个内部队列和一个专用线程。如果一个容器被配置为监听多个队列,那么将使用同一个使用者线程来处理所有队列。并发性由`concurrentConsumers`和其他属性控制。当消息从 RabbitMQ 客户机到达时,客户机线程通过队列将消息传递给消费者线程。之所以需要这种架构,是因为在 RabbitMQ 客户端的早期版本中,不可能实现多个并发交付。较新版本的客户机具有修订后的线程模型,现在可以支持并发。这允许引入 DMLC,现在在 RabbitMQ 客户端线程上直接调用侦听器。因此,它的架构实际上比 SMLC“更简单”。然而,这种方法有一些局限性,并且 SMLC 的某些功能在 DMLC 中不可用。此外,并发性由`consumersPerQueue`(以及客户库的线程池)控制。`concurrentConsumers`和相关属性在此容器中不可用。 + +以下特性可用于 SMLC,但不适用于 DMLC: + +* `batchSize`:使用 SMLC,你可以将其设置为控制事务中传递的消息数量或减少 ACK 的数量,但它可能会导致失败后重复传递的数量增加。(DMLC 确实有`messagesPerAck`,你可以使用它来减少 ACK,与`batchSize`和 SMLC 相同,但它不能用于事务——每个消息都在单独的事务中传递和 ACK’d)。 + +* `consumerBatchEnabled`:在消费者中启用离散消息的批处理;有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +* `maxConcurrentConsumers`和使用者缩放间隔或触发器——DMLC 中没有自动缩放。但是,它确实允许你以编程方式更改`consumersPerQueue`属性,并对消费者进行相应的调整。 + +然而,与 SMLC 相比,DMLC 有以下好处: + +* 在运行时添加和删除队列更有效。使用 SMLC,整个使用者线程将被重新启动(所有使用者将被取消并重新创建)。使用 DMLC,不受影响的消费者不会被取消。 + +* 避免了 RabbitMQ 客户端线程和使用者线程之间的上下文切换。 + +* 线程在消费者之间共享,而不是在 SMLC 中为每个消费者拥有一个专用线程。但是,请参见[线程和异步消费者](#threading)中有关连接工厂配置的重要注释。 + +有关将哪些配置属性应用于每个容器的信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#idle-containers)检测空闲异步消费者 + +尽管效率很高,但异步用户的一个问题是检测它们何时空闲——如果一段时间内没有消息到达,用户可能希望采取一些措施。 + +从版本 1.6 开始,现在可以将侦听器容器配置为在一段时间之后没有消息传递的情况下发布`ListenerContainerIdleEvent`。当容器处于空闲状态时,每`idleEventInterval`毫秒就会发布一个事件。 + +要配置此功能,请在容器上设置`idleEventInterval`。下面的示例展示了如何在 XML 和 Java 中这样做(对于`SimpleMessageListenerContainer`和`SimpleRabbitListenerContainerFactory`): + +``` + + + +``` + +``` +@Bean +public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ... + container.setIdleEventInterval(60000L); + ... + return container; +} +``` + +``` +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setIdleEventInterval(60000L); + ... + return factory; +} +``` + +在每种情况下,当容器处于空闲状态时,每分钟都会发布一次事件。 + +###### [](#event-consumption)事件消耗 + +你可以通过实现`ApplicationListener`来捕获空闲事件——它可以是一个普通的侦听器,也可以是一个窄到只接收这个特定事件的侦听器。还可以使用 Spring Framework4.2 中介绍的`@EventListener`。 + +下面的示例将`@RabbitListener`和`@EventListener`合并为一个类。你需要理解,应用程序侦听器获取所有容器的事件,因此,如果你想根据哪个容器空闲来采取特定的操作,你可能需要检查侦听器 ID。你也可以为此目的使用`@EventListener``condition`。 + +这些事件有四个属性: + +* `source`:侦听器容器实例 + +* `id`:侦听器 ID(或容器 Bean 名称) + +* `idleTime`:事件发布时容器处于空闲状态的时间 + +* `queueNames`:容器监听的队列的名称 + +下面的示例展示了如何同时使用`@RabbitListener`和`@EventListener`注释来创建侦听器: + +``` +public class Listener { + + @RabbitListener(id="someId", queues="#{queue.name}") + public String listen(String foo) { + return foo.toUpperCase(); + } + + @EventListener(condition = "event.listenerId == 'someId'") + public void onApplicationEvent(ListenerContainerIdleEvent event) { + ... + } + +} +``` + +| |事件侦听器看到所有容器的事件。
因此,在前面的示例中,我们根据侦听器 ID 缩小接收到的事件的范围。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果希望使用 IDLE 事件停止 Lister 容器,则不应在调用侦听器的线程上调用`container.stop()`,
这样做总是会导致延迟和不必要的日志消息。,相反,
,你应该将事件传递给一个不同的线程,然后该线程可以停止容器。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#micrometer)监视侦听器性能 + +从版本 2.2 开始,如果在类路径上检测到`Micrometer`,并且在应用程序上下文中存在`MeterRegistry`,则侦听器容器将自动为侦听器创建和更新微米计`Timer`s。可以通过将容器属性`micrometerEnabled`设置为`false`来禁用计时器。 + +两个计时器被维护-一个用于对听者的成功调用,另一个用于失败调用。对于一个简单的`MessageListener`,每个配置的队列都有一对计时器。 + +计时器名为`spring.rabbitmq.listener`,并具有以下标记: + +* `listenerId`:(侦听器 ID 或容器 Bean 名称) + +* `queue`:(当`consumerBatchEnabled`为`true`时,一个简单侦听器或配置的队列名称列表的队列名称是`true`-因为批处理可能包含来自多个队列的消息) + +* `result`:`success`或`failure` + +* `exception`:`none`或`ListenerExecutionFailedException` + +可以使用`micrometerTags`容器属性添加其他标记。 + +#### [](#containers-and-broker-named-queues)4.1.7。容器和以代理命名的队列 + +虽然使用`AnonymousQueue`实例作为自动删除队列是可取的,但从版本 2.1 开始,你可以使用带有侦听器容器的代理命名队列。下面的示例展示了如何做到这一点: + +``` +@Bean +public Queue queue() { + return new Queue("", false, true, true); +} + +@Bean +public SimpleMessageListenerContainer container() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); + container.setQueues(queue()); + container.setMessageListener(m -> { + ... + }); + container.setMissingQueuesFatal(false); + return container; +} +``` + +请注意名称的空`String`。当`RabbitAdmin`声明队列时,它使用代理返回的名称更新`Queue.actualName`属性。在配置容器以使其工作时,必须使用`setQueues()`,以便容器可以在运行时访问声明的名称。仅仅设置名字是不够的。 + +| |在容器运行时,不能将以代理命名的队列添加到容器中。| +|---|----------------------------------------------------------------------------| + +| |当一个连接被重置并建立了一个新的连接时,新的队列将获得一个新的名称,
由于在重新启动的容器和重新声明的队列之间存在竞争条件,因此将容器的`missingQueuesFatal`属性设置为`false`非常重要,因为容器最初可能会尝试重新连接到旧队列。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#message-converters)4.1.8。消息转换器 + +`AmqpTemplate`还定义了用于发送和接收委托给`MessageConverter`的消息的几种方法。`MessageConverter`为每个方向提供了一个方法:一个用于转换**to**a`Message`,另一个用于转换**来自**a`Message`。请注意,当转换为`Message`时,除了对象之外,还可以提供属性。`object`参数通常对应于消息体。下面的清单显示了`MessageConverter`接口定义: + +``` +public interface MessageConverter { + + Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException; + + Object fromMessage(Message message) throws MessageConversionException; + +} +``` + +在`AmqpTemplate`上的相关`Message`-发送方法比我们前面讨论的方法更简单,因为它们不需要`Message`实例。相反,`MessageConverter`负责“创建”每个`Message`,方法是将提供的对象转换为`Message`主体的字节数组,然后添加任何提供的`MessageProperties`。下面的清单显示了各种方法的定义: + +``` +void convertAndSend(Object message) throws AmqpException; + +void convertAndSend(String routingKey, Object message) throws AmqpException; + +void convertAndSend(String exchange, String routingKey, Object message) + throws AmqpException; + +void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) + throws AmqpException; + +void convertAndSend(String routingKey, Object message, + MessagePostProcessor messagePostProcessor) throws AmqpException; + +void convertAndSend(String exchange, String routingKey, Object message, + MessagePostProcessor messagePostProcessor) throws AmqpException; +``` + +在接收端,只有两种方法:一种接受队列名称,另一种依赖于模板的“queue”属性已设置。下面的清单显示了这两种方法的定义: + +``` +Object receiveAndConvert() throws AmqpException; + +Object receiveAndConvert(String queueName) throws AmqpException; +``` + +| |在[异步消费者](#async-consumer)中提到的`MessageListenerAdapter`也使用了`MessageConverter`。| +|---|------------------------------------------------------------------------------------------------------------------| + +##### [](#simple-message-converter)`SimpleMessageConverter` + +`MessageConverter`策略的默认实现称为`SimpleMessageConverter`。这是`RabbitTemplate`的实例所使用的转换器,如果你没有显式地配置替代选项。它处理基于文本的内容、序列化的 Java 对象和字节数组。 + +###### [](#converting-from-a-message)从`Message`转换 + +如果输入`Message`的内容类型以“text”开头(例如,“text/plain”),则它还会检查 Content-Encoding 属性,以确定将`Message`Body 字节数组转换为 Java`String`时要使用的字符集。如果输入`Message`上没有设置内容编码属性,则默认情况下它使用 UTF-8 字符集。如果需要覆盖该默认设置,可以配置`SimpleMessageConverter`的实例,设置其`defaultCharset`属性,并将其注入`RabbitTemplate`实例。 + +如果输入`Message`的 Content-Type 属性值设置为“application/x-java-serialized-object”,则`SimpleMessageConverter`将尝试将字节数组反序列化为 Java 对象。虽然这对于简单的原型设计可能很有用,但我们不建议依赖 Java 序列化,因为它会导致生产者和消费者之间的紧密耦合。当然,它也排除了任何一方使用非 Java 系统的可能性。由于 AMQP 是一种线级协议,因此很不幸的是,在这样的限制下,它将失去很多这种优势。在接下来的两节中,我们将探索在不依赖 Java 序列化的情况下传递富领域对象内容的一些替代方案。 + +对于所有其他内容类型,`SimpleMessageConverter`直接以字节数组的形式返回`Message`正文内容。 + +有关重要信息,请参见[Java 反序列化](#java-deserialization)。 + +###### [](#converting-to-a-message)转换为`Message` + +当从任意 Java 对象转换为`Message`时,`SimpleMessageConverter`同样处理字节数组、字符串和可序列化实例。它将这些转换为字节(在字节数组的情况下,没有任何可转换的内容),并且相应地 SESContent-Type 属性。如果要转换的`Object`与这些类型之一不匹配,则`Message`正文为空。 + +##### [](#serializer-message-converter)`SerializerMessageConverter` + +这种转换器类似于`SimpleMessageConverter`,只是它可以配置与其它 Spring 框架`Serializer`和`Deserializer`实现的`application/x-java-serialized-object`转换。 + +有关重要信息,请参见[Java 反序列化](#java-deserialization)。 + +##### [](#json-message-converter)Jackson2jsonmessageconverter + +本节介绍使用`Jackson2JsonMessageConverter`转换到`Message`和从`Message`转换的情况。它有以下几个部分: + +* [转换为`Message`](#Jackson2jsonMessageConverter-to-Message) + +* [转换自`Message`](#Jackson2jsonmessageconverter-from-message) + +###### [](#Jackson2JsonMessageConverter-to-message)转换为`Message` + +正如上一节中提到的,通常不建议依赖 Java 序列化。JSON(JavaScript Object Notation,JavaScript Object Notation,JavaScript Object Notation)是一种比较常见的替代方法,它在不同的语言和平台上更灵活、更可移植。转换器可以在任何`RabbitTemplate`实例上配置,以覆盖其对`SimpleMessageConverter`默认值的使用。`Jackson2JsonMessageConverter`使用`com.fasterxml.jackson`2.x 库。下面的示例配置`Jackson2JsonMessageConverter`: + +``` + + + + + + + + + +``` + +如上面所示,`Jackson2JsonMessageConverter`默认使用`DefaultClassMapper`。类型信息被添加到(并从)`MessageProperties`。如果入站消息在`MessageProperties`中不包含类型信息,但你知道期望的类型,则可以使用`defaultType`属性配置静态类型,如下例所示: + +``` + + + + + + + +``` + +此外,你还可以从`*TypeId*`标头中的值提供自定义映射。下面的示例展示了如何做到这一点: + +``` +@Bean +public Jackson2JsonMessageConverter jsonMessageConverter() { + Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter(); + jsonConverter.setClassMapper(classMapper()); + return jsonConverter; +} + +@Bean +public DefaultClassMapper classMapper() { + DefaultClassMapper classMapper = new DefaultClassMapper(); + Map> idClassMapping = new HashMap<>(); + idClassMapping.put("thing1", Thing1.class); + idClassMapping.put("thing2", Thing2.class); + classMapper.setIdClassMapping(idClassMapping); + return classMapper; +} +``` + +现在,如果发送系统将头设置为`thing1`,转换器将创建一个`Thing1`对象,依此类推。有关从非 Spring 应用程序转换消息的完整讨论,请参见[Receiving JSON from Non-Spring Applications](#spring-rabbit-json)示例应用程序。 + +###### [](#Jackson2JsonMessageConverter-from-message)从`Message`转换 + +根据发送系统添加到头部的类型信息,将入站消息转换为对象。 + +在 1.6 之前的版本中,如果不存在类型信息,则转换将失败。从版本 1.6 开始,如果缺少类型信息,转换器将使用 Jackson 默认值(通常是映射)来转换 JSON。 + +此外,从版本 1.6 开始,当使用`@RabbitListener`注释(在方法上)时,推断的类型信息将添加到`MessageProperties`中。这使得转换器可以转换为目标方法的参数类型。这仅在存在一个没有注释的参数或一个带有`@Payload`注释的参数时才适用。在分析过程中忽略类型`Message`的参数。 + +| |默认情况下,推断的类型信息将覆盖由发送系统创建的入站`*TypeId*`和相关标题

这将使接收系统自动转换为不同的域对象。
这仅适用于此。如果参数类型是具体的(不是抽象的或接口的),或者它来自`java.util`包。
在所有其他情况下,使用`*TypeId*`和相关的标题。
在某些情况下,你可能希望重写缺省行为,并始终使用`*TypeId*`信息。,例如,
,假设你有一个`@RabbitListener`,它接受一个`Thing1`参数,但是消息包含一个`Thing2`,该
是`Thing1`的子类(这是具体的)。
推断出的类型将是不正确的。
来处理这种情况,将`TYPE_ID`上的`TypePrecedence`属性设置为`TYPE_ID`,而不是默认`INFERRED`的

(该属性实际上位于转换器的`DefaultJackson2JavaTypeMapper`上,但转换器
上提供了一个设置器,以方便使用。)
如果你注入一个自定义的类型映射器,你应该在映射器上设置属性。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |当从`Message`转换时,传入的`MessageProperties.getContentType()`必须是符合 JSON 的(`contentType.contains("json")`用于检查)。
从版本 2.2 开始,如果没有`contentType`属性,则假定`application/json`,或者它具有默认值`application/octet-stream`。
以恢复到以前的行为(返回一个未转换的`byte[]`),将转换器的`assumeSupportedContentType`属性设置为`false`。
如果不支持内容类型,则生成`WARN`日志消息`Could not convert incoming message with content-type […​]`,是发出的,并且`message.getBody()`按原样返回-作为`byte[]`。
因此,为了满足`Jackson2JsonMessageConverter`在消费者方面的要求,生产者必须添加`contentType`消息属性——例如,作为`application/json`或`text/x-json`,或者通过使用`Jackson2JsonMessageConverter`,它会自动设置标题。
下面的清单显示了许多转换器调用:| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@RabbitListener +public void thing1(Thing1 thing1) {...} + +@RabbitListener +public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} + +@RabbitListener +public void thing1(Thing1 thing1, String bar) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} +``` + +在前面清单中的前四种情况中,转换器尝试转换为`Thing1`类型。第五个示例是无效的,因为我们无法确定哪个参数应该接收消息负载。在第六个示例中,由于泛型类型是`WildcardType`,所以 Jackson 默认应用。 + +但是,你可以创建一个自定义转换器,并使用`targetMethod`消息属性来决定将 JSON 转换为哪种类型。 + +| |只有在方法级别声明`@RabbitListener`注释时,才能实现这种类型推断。
对于类级别`@RabbitListener`,转换后的类型用于选择调用哪个`@RabbitHandler`方法。
由于这个原因,基础设施提供了`targetObject`属性,你可以在自定义的
转换器中使用它来确定类型。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |从版本 1.6.11 开始,`Jackson2JsonMessageConverter`,因此,`DefaultJackson2JavaTypeMapper`(`DefaultClassMapper`)提供`trustedPackages`选项,以克服[序列化小工具](https://pivotal.io/security/cve-2017-4995)漏洞。
默认情况下,对于向后兼容,`Jackson2JsonMessageConverter`信任所有包——也就是说,它使用`*`作为选项。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### [](#jackson-abstract)反序列化抽象类 + +在版本 2.2.8 之前,如果`@RabbitListener`的推断类型是抽象类(包括接口),则转换器将返回到头部中查找类型信息,如果存在,则使用该信息;如果不存在,则尝试创建抽象类。当使用自定义反序列化器来处理抽象类的自定义`ObjectMapper`时,但传入的消息具有无效的类型标头,这会导致一个问题。 + +从版本 2.2.8 开始,默认情况下保留以前的行为。如果你有这样的自定义`ObjectMapper`,并且希望忽略类型头,并且总是使用推断类型进行转换,那么将`alwaysConvertToInferredType`设置为`true`。这是向后兼容性所必需的,并且可以避免尝试转换失败时的开销(使用标准`ObjectMapper`)。 + +###### [](#data-projection)使用 Spring 数据投影接口 + +从版本 2.2 开始,你可以将 JSON 转换为 Spring 数据投影接口,而不是具体的类型。这允许对数据进行非常有选择性的、低耦合的绑定,包括从 JSON 文档中的多个位置查找值。例如,以下接口可以定义为消息有效负载类型: + +``` +interface SomeSample { + + @JsonPath({ "$.username", "$.user.name" }) + String getUsername(); + +} +``` + +``` +@RabbitListener(queues = "projection") +public void projection(SomeSample in) { + String username = in.getUsername(); + ... +} +``` + +默认情况下,访问器方法将用于在接收的 JSON 文档中查找属性名称 AS 字段。`@JsonPath`表达式允许定制值查找,甚至可以定义多个 JSON 路径表达式,从多个位置查找值,直到表达式返回实际值。 + +要启用此功能,请将消息转换器上的`useProjectionForInterfaces`设置为`true`。你还必须将`spring-data:spring-data-commons`和`com.jayway.jsonpath:json-path`添加到类路径。 + +当用作`@RabbitListener`方法的参数时,接口类型将作为正常类型自动传递给转换器。 + +###### [](#json-complex)从`Message`转换为`RabbitTemplate` + +如前所述,类型信息在消息头中传递,以在从消息转换时协助转换器。这在大多数情况下都行得通。然而,当使用泛型类型时,它只能转换简单的对象和已知的“容器”对象(列表、数组和映射)。从版本 2.0 开始,`Jackson2JsonMessageConverter`实现了`SmartMessageConverter`,这使得它可以与新的`RabbitTemplate`方法一起使用,该方法接受`ParameterizedTypeReference`参数。这允许转换复杂的泛型类型,如下例所示: + +``` +Thing1> thing1 = + rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { }); +``` + +| |从版本 2.1 开始,`AbstractJsonMessageConverter`类已被删除。
它不再是`Jackson2JsonMessageConverter`的基类。
它已被`AbstractJackson2MessageConverter`取代。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#marshallingmessageconverter)`MarshallingMessageConverter` + +还有一个选择是`MarshallingMessageConverter`。它委托给 Spring OXM 库的`Marshaller`和`Unmarshaller`策略接口的实现。你可以阅读有关该库的更多信息[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html)。在配置方面,最常见的是仅提供构造函数参数,因为`Marshaller`的大多数实现也实现`Unmarshaller`。下面的示例展示了如何配置`MarshallingMessageConverter`: + +``` + + + + + + + + +``` + +##### [](#jackson2xml)`Jackson2XmlMessageConverter` + +这个类是在版本 2.1 中引入的,可以用于将消息从 XML 转换为 XML。 + +`Jackson2XmlMessageConverter`和`Jackson2JsonMessageConverter`都具有相同的基类:`AbstractJackson2MessageConverter`。 + +| |引入`AbstractJackson2MessageConverter`类来替换已删除的类:`AbstractJsonMessageConverter`。| +|---|----------------------------------------------------------------------------------------------------------------------| + +`Jackson2XmlMessageConverter`使用`com.fasterxml.jackson`2.x 库。 + +你可以用与`Jackson2JsonMessageConverter`相同的方式使用它,只是它支持 XML 而不是 JSON。下面的示例配置`Jackson2JsonMessageConverter`: + +``` + + + + + + + +``` + +有关更多信息,请参见[Jackson2JSONMessageConverter](#json-message-converter)。 + +| |从版本 2.2 开始,如果不存在`contentType`属性,或者它具有默认值`application/octet-stream`,则假定`application/xml`。
恢复到以前的行为(返回未转换的`byte[]`),将转换器的`assumeSupportedContentType`属性设置为`false`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#contenttypedelegatingmessageconverter)`ContentTypeDelegatingMessageConverter` + +这个类是在版本 1.4.2 中引入的,允许基于`MessageProperties`中的 Content type 属性将任务委托给特定的`MessageConverter`。默认情况下,如果没有`contentType`属性,或者有一个值与所有配置的转换器都不匹配,那么它将委托给`SimpleMessageConverter`。下面的示例配置`ContentTypeDelegatingMessageConverter`: + +``` + + + + + + + + +``` + +##### [](#java-deserialization)Java 反序列化 + +本节介绍如何反序列化 Java 对象。 + +| |当从不受信任的源反序列化 Java 对象时,可能存在一个漏洞,

如果你接受来自不受信任的源的消息,并且`content-type`的值为`application/x-java-serialized-object`,你应该
考虑配置哪些包和类被允许进行反序列化。
这适用于`SimpleMessageConverter`和`SerializerMessageConverter`,当它被配置为隐式或通过配置使用`DefaultDeserializer`时。
默认情况下,允许的列表为空,这意味着所有的类都是反序列化的。

你可以设置一个模式列表,例如`thing1.`**,`thing1.thing2.Cat`或`.MySafeClass`。
在找到匹配之前,将按顺序检查模式。
如果不匹配,则将不匹配,抛出一个`SecurityException`。

可以在这些转换器上使用`allowedListPatterns`属性设置模式。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#message-properties-converters)消息属性转换器 + +`MessagePropertiesConverter`策略接口用于在兔子客户端`BasicProperties`和 Spring AMQP`MessageProperties`之间进行转换。默认的实现(`DefaultMessagePropertiesConverter`)对于大多数目的来说通常是足够的,但是如果需要,你可以实现自己的实现。当大小不大于`1024`字节时,默认属性转换器将类型`BasicProperties`的元素转换为`String`实例。较大的`LongString`实例未被转换(请参见下一段)。可以使用构造函数参数重写此限制。 + +从版本 1.6 开始,长于长字符串限制(默认值:1024)的头现在由`DefaultMessagePropertiesConverter`默认设置为`LongString`实例。你可以通过`getBytes[]`、`toString()`或`getStream()`方法访问内容。 + +以前,`DefaultMessagePropertiesConverter`将这样的标题“转换”为`DataInputStream`(实际上,它只引用了`LongString`实例的`DataInputStream`)。在输出时,这个头不会被转换(除了转换为字符串——例如,通过在流上调用`toString()`,`[[email protected]](/cdn-cgi/l/email-protection)`)。 + +大型传入`LongString`头现在也可以在输出上正确地“转换”(默认情况下)。 + +提供了一个新的构造函数,使你可以将转换器配置为像以前一样工作。下面的清单显示了该方法的 Javadoc 注释和声明: + +``` +/** + * Construct an instance where LongStrings will be returned + * unconverted or as a java.io.DataInputStream when longer than this limit. + * Use this constructor with 'true' to restore pre-1.6 behavior. + * @param longStringLimit the limit. + * @param convertLongLongStrings LongString when false, + * DataInputStream when true. + * @since 1.6 + */ +public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLongLongStrings) { ... } +``` + +同样从版本 1.6 开始,在`MessageProperties`中添加了一个名为`correlationIdString`的新属性。以前,当从 RabbitMQ 客户端使用的`BasicProperties`转换到和转换时,执行一个不必要的`byte[] <→ String`转换,因为`MessageProperties.correlationId`是一个`byte[]`,但是`BasicProperties`使用一个`String`。(最终,RabbitMQ 客户机使用 UTF-8 将`String`转换为字节以放入协议消息)。 + +为了提供最大的向后兼容性,在`DefaultMessagePropertiesConverter`中添加了一个名为`correlationIdPolicy`的新属性。这需要一个`DefaultMessagePropertiesConverter.CorrelationIdPolicy`枚举参数。默认情况下,它被设置为`BYTES`,它复制了之前的行为。 + +对于入站消息: + +* `STRING`:仅映射`correlationIdString`属性 + +* `BYTES`:仅映射`correlationId`属性 + +* `BOTH`:两个属性都映射了 + +对于出站消息: + +* `STRING`:仅映射`correlationIdString`属性 + +* `BYTES`:仅映射`correlationId`属性 + +* `BOTH`:这两个属性都被考虑,`String`属性优先。 + +同样从版本 1.6 开始,入站`deliveryMode`属性不再映射到`MessageProperties.deliveryMode`。它被映射到`MessageProperties.receivedDeliveryMode`。此外,入站`userId`属性不再映射到`MessageProperties.userId`。它被映射到`MessageProperties.receivedUserId`。如果出站消息使用相同的`MessageProperties`对象,则这些更改是为了避免这些属性的意外传播。 + +从版本 2.2 开始,`DefaultMessagePropertiesConverter`使用`getName()`而不是`toString()`转换类型为`Class`的任何自定义标头;这避免了使用`toString()`表示来解析类名称的应用程序。对于滚动升级,你可能需要更改你的消费者来理解这两种格式,直到所有的生产者都升级了。 + +#### [](#post-processing)4.1.9。修改消息-压缩和更多 + +存在一些扩展点。它们允许你对消息执行一些处理,可以在消息发送到 RabbitMQ 之前,也可以在收到消息之后立即进行处理。 + +正如可以在[消息转换器](#message-converters)中看到的那样,这样的一个扩展点是在`AmqpTemplate``convertAndReceive`操作中,其中可以提供一个`MessagePostProcessor`。例如,在你的 POJO 被转换之后,`MessagePostProcessor`允许你在`Message`上设置自定义标题或属性。 + +从版本 1.4.2 开始,在`RabbitTemplate`-`setBeforePublishPostProcessors()`和`setAfterReceivePostProcessors()`中添加了额外的扩展点。第一个使后处理器能够在发送到 RabbitMQ 之前立即运行。在使用批处理时(参见[Batching](#template-batching)),这是在组装批处理之后和发送批处理之前调用的。第二个是在接收到消息后立即调用的。 + +这些扩展点用于诸如压缩的特征,并且为此目的,提供了几个`MessagePostProcessor`实现。`GZipPostProcessor`、`ZipPostProcessor`和`DeflaterPostProcessor`在发送消息之前压缩消息,并且`GUnzipPostProcessor`、`UnzipPostProcessor`和`InflaterPostProcessor`解压接收到的消息。 + +| |从版本 2.1.5 开始,`GZipPostProcessor`可以配置`copyProperties = true`选项,以复制原始消息属性,默认情况下,由于性能原因,这些属性可以重用,并使用压缩内容编码和可选的`MessageProperties.SPRING_AUTO_DECOMPRESS`报头进行修改。
如果保留对原始出站消息的引用,其属性也会发生变化。
因此,如果你的应用程序使用这些消息后处理程序保留出站消息的副本,考虑打开`copyProperties`选项。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |从版本 2.2.12 开始,你可以配置压缩后置处理器在内容编码元素之间使用的分隔符。
在版本 2.2.11 和之前,这是硬编码为`:`,现在默认设置为`, `。
解压程序将同时使用这两个分隔符。
但是,如果你使用 2.3 或更高版本发布消息,而使用 2.2.11 或更高版本,则必须将压缩器上的`encodingDelimiter`属性设置为`:`。
当你的用户升级到 2.2.11 或更高版本时,你可以恢复到`, `的默认值。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +类似地,`SimpleMessageListenerContainer`也有一个`setAfterReceivePostProcessors()`方法,允许在容器接收到消息后执行解压缩。 + +从版本 2.1.4 开始,`addBeforePublishPostProcessors()`和`addAfterReceivePostProcessors()`已添加到`RabbitTemplate`中,以允许将新的后处理程序分别附加到发布前和接收后处理程序的列表中。还提供了删除后置处理器的方法。类似地,`AbstractMessageListenerContainer`还添加了`addAfterReceivePostProcessors()`和`removeAfterReceivePostProcessor()`方法。有关更多详细信息,请参见`RabbitTemplate`和`AbstractMessageListenerContainer`的 javadoc。 + +#### [](#request-reply)4.1.10。请求/回复消息 + +`AmqpTemplate`还提供了各种`sendAndReceive`方法,这些方法接受前面描述的用于单向发送操作的相同参数选项(`exchange`,`routingKey`和`Message`)。这些方法在请求-回复场景中非常有用,因为它们在发送之前处理必要的`reply-to`属性的配置,并且可以在内部为此目的创建的独占队列中侦听应答消息。 + +在将`MessageConverter`应用于请求和答复时,也可以使用类似的请求-答复方法。这些方法被命名为`convertSendAndReceive`。有关更多详细信息,请参见`AmqpTemplate`的[javadoc](https://DOCS. Spring.io/ Spring-amqp/DOCS/latest-ga/api/org/springframework/amqp/core/amqptemplate.html)。 + +从版本 1.5.0 开始,每个`sendAndReceive`方法变体都有一个重载版本,它接受`CorrelationData`。与正确配置的连接工厂一起,这将为发送端的操作启用发行者确认的接收。有关更多信息,请参见[相关发布者确认并返回](#template-confirms)和[javadoc for`RabbitOperations`](https://DOCS. Spring.io/ Spring-amqp/DOCS/latest-ga/api/org/springframework/amqp/rabbit/core/rabbitoperations.html)。 + +从版本 2.0 开始,这些方法有一些变体(`convertSendAndReceiveAsType`),它们接受一个额外的`ParameterizedTypeReference`参数来转换复杂的返回类型。模板必须配置为`SmartMessageConverter`。有关更多信息,请参见[从`Message`转换为`RabbitTemplate`]。 + +从版本 2.1 开始,你可以使用`noLocalReplyConsumer`选项配置`RabbitTemplate`,以控制用于回复消费者的`noLocal`标志。默认情况下,这是`false`。 + +##### [](#reply-timeout)回复超时 + +默认情况下,发送和接收方法在 5 秒后超时并返回 null。你可以通过设置`replyTimeout`属性来修改此行为。从版本 1.5 开始,如果你将`mandatory`属性设置为`true`(或者对于特定的消息,`mandatory-expression`计算为`true`),如果无法将消息传递到队列,则将抛出`AmqpMessageReturnedException`。此异常具有`returnedMessage`、`replyCode`和`replyText`属性,以及用于发送的`exchange`和`routingKey`属性。 + +| |此功能使用 Publisher Returns。
你可以通过在`CachingConnectionFactory`上将`true`设置为`true`(参见[发布者确认并返回](#cf-pub-conf-ret))来启用它。
此外,你还必须没有用`ReturnCallback`注册你自己的`ReturnCallback`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.1.2 开始,添加了一个`replyTimedOut`方法,让子类被告知超时,以便它们可以清理任何保留的状态。 + +从版本 2.0.11 和 2.1.3 开始,当你使用默认的`DirectReplyToMessageListenerContainer`时,你可以通过设置模板的`replyErrorHandler`属性来添加错误处理程序。对于任何失败的交付,例如在没有相关标头的情况下收到的延迟回复和消息,都会调用此错误处理程序。传入的异常是`ListenerExecutionFailedException`,它具有`failedMessage`属性。 + +##### [](#direct-reply-to)RabbitMQ 直接回复-回复 + +| |从 3.4.0 版本开始,RabbitMQ 服务器支持[直接回复](https://www.rabbitmq.com/direct-reply-to.html)。
这消除了固定回复队列的主要原因(以避免需要创建临时队列)对于每个请求)。
以 Spring AMQP 版本 1.4.1 开始的直接回复默认情况下使用(如果服务器支持的话),而不是创建临时回复队列。
当没有`replyQueue`被提供时(或者它的名称设置为`amq.rabbitmq.reply-to`),`RabbitTemplate`自动检测是否支持直接回复,并使用它或退回到使用临时回复队列。
当使用直接回复时,不需要`reply-listener`,也不应进行配置。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +响应侦听器仍然支持命名队列(`amq.rabbitmq.reply-to`除外),允许控制响应并发等。 + +从版本 1.6 开始,如果你希望对每个回复使用一个临时的、排他的、自动删除队列,请将`useTemporaryReplyQueues`属性设置为`true`。如果设置`replyAddress`,则忽略此属性。 + +你可以通过子类化`RabbitTemplate`并重写`useDirectReplyTo()`来检查不同的条件,从而更改决定是否使用直接回复的条件。该方法仅在发送第一个请求时调用一次。 + +在版本 2.0 之前,`RabbitTemplate`为每个请求创建一个新的使用者,并在收到答复(或超时)时取消该使用者。现在,模板使用`DirectReplyToMessageListenerContainer`代替,让消费者被重用。该模板仍然负责将回复进行关联,因此不存在将延迟的回复发送给其他发件人的风险。如果要恢复到以前的行为,请将`useDirectReplyToContainer`(使用 XML 配置时`direct-reply-to-container`)属性设置为 false。 + +`AsyncRabbitTemplate`没有这样的选项。当使用直接回复时,它总是使用`DirectReplyToContainer`作为回复。 + +从版本 2.3.7 开始,模板有一个新的属性`useChannelForCorrelation`。当这是`true`时,服务器不必将相关 ID 从请求消息头复制到回复消息。相反,用于发送请求的通道用于将答复与请求关联起来。 + +##### [](#message-correlation-with-a-reply-queue)与回复队列的消息相关性 + +当使用固定的应答队列(`amq.rabbitmq.reply-to`除外)时,必须提供相关数据,以便能够将应答与请求关联起来。见[RabbitMQ 远程过程调用](https://www.rabbitmq.com/tutorials/tutorial-six-java.html)。默认情况下,标准`correlationId`属性用于保存相关数据。但是,如果希望使用自定义属性来保存相关数据,则可以在 \上设置`correlation-key`属性。显式地将属性设置为`correlationId`与省略该属性相同。对于相关数据,客户机和服务器必须使用相同的报头。 + +| |Spring AMQP 版本 1.1 对此数据使用了一个名为`spring_reply_correlation`的自定义属性。
如果你希望用当前版本恢复到此行为(也许是为了与使用 1.1 的另一个应用程序保持兼容性),则必须将该属性设置为`spring_reply_correlation`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下,模板会生成自己的相关 ID(忽略用户提供的任何值)。如果你希望使用自己的相关 ID,请将`RabbitTemplate`实例的`userCorrelationId`属性设置为`true`。 + +| |相关 ID 必须是唯一的,以避免对请求返回错误答复的可能性。| +|---|---------------------------------------------------------------------------------------------------------| + +##### [](#reply-listener)回复侦听器容器 + +当使用 3.4.0 之前的 RabbitMQ 版本时,将为每个回复使用一个新的临时队列。但是,可以在模板上配置单个回复队列,这样效率更高,还可以让你在该队列上设置参数。但是,在这种情况下,你还必须提供一个 \子元素。这个元素为应答队列提供了一个侦听器容器,模板就是侦听器。在 \上允许的所有[消息侦听器容器配置](#containerAttributes)属性在元素上都是允许的,但`connection-factory`和`message-converter`除外,它们是从模板的配置中继承而来的。 + +| |如果你运行应用程序的多个实例或使用多个`RabbitTemplate`实例,则**MUST**对每个实例使用唯一的回复队列,
RabbitMQ 无法从队列中选择消息,因此,如果它们都使用相同的队列,每个实例都会竞争回复,而不一定会收到自己的回复。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例定义了一个带有连接工厂的 Rabbit 模板: + +``` + + + +``` + +虽然容器和模板共享一个连接工厂,但它们不共享一个通道。因此,请求和响应不是在同一个事务(如果是事务的话)中执行的。 + +| |在 1.5.0 版本之前,`reply-address`属性是不可用的,
回复总是通过使用默认的 Exchange 和`reply-queue`名称作为路由密钥来路由的,
仍然是默认的,但是你现在可以指定新的`reply-address`属性。
`reply-address`中的`reply-address`可以包含一个具有`/`表单的地址,并且将回复路由到指定的交换并被路由到与路由密钥绑定的队列。
`reply-address`的优先权高于`reply-queue`。
当仅使用`reply-address`时,``必须配置为单独的``组件。
`reply-address`和`reply-queue`(或`queues`属性在``上)必须在逻辑上引用相同的队列。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在这种配置下,使用`SimpleListenerContainer`来接收回复,而`RabbitTemplate`是`MessageListener`。当使用``名称空间元素定义模板时,如前面的示例所示,解析器将模板中的容器和线定义为侦听器。 + +| |当模板不使用固定的`replyQueue`(或正在使用直接回复—参见[RabbitMQ 直接回复](#direct-reply-to))时,不需要侦听器容器。
直接`reply-to`是使用 RabbitMQ3.4.0 或更高版本时的首选机制。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你将`RabbitTemplate`定义为``,或者使用`@Configuration`类将其定义为`@Bean`,或者当你以编程方式创建模板时,你需要自己定义并连接应答侦听器容器。如果没有做到这一点,模板将永远不会收到回复,最终会超时并返回 null 作为对`sendAndReceive`方法的调用的回复。 + +从版本 1.5 开始,`RabbitTemplate`检测是否已将其配置为`MessageListener`以接收回复。如果不是,则尝试用`IllegalStateException`发送和接收带有回复地址的消息失败(因为这些回复永远不会收到)。 + +此外,如果使用了简单的`replyAddress`(队列名称),则应答侦听器容器将验证它正在侦听具有相同名称的队列。如果回复地址是 Exchange 和 Routing Key,并且写入了调试日志消息,则无法执行此检查。 + +| |在连接应答侦听器和模板时,重要的是要确保模板的`replyAddress`和容器的`queues`(或`queueNames`)属性指向相同的队列。
模板将应答地址插入到出站消息`replyTo`属性中。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的清单展示了如何手动连接 bean 的示例: + +``` + + + + + + + + + + + + + + + + +``` + +``` + @Bean + public RabbitTemplate amqpTemplate() { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); + rabbitTemplate.setMessageConverter(msgConv()); + rabbitTemplate.setReplyAddress(replyQueue().getName()); + rabbitTemplate.setReplyTimeout(60000); + rabbitTemplate.setUseDirectReplyToContainer(false); + return rabbitTemplate; + } + + @Bean + public SimpleMessageListenerContainer replyListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory()); + container.setQueues(replyQueue()); + container.setMessageListener(amqpTemplate()); + return container; + } + + @Bean + public Queue replyQueue() { + return new Queue("my.reply.queue"); + } +``` + +`RabbitTemplate`中显示了一个完整的[这个测试用例](https://github.com/spring-projects/spring-amqp/tree/main/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java)示例,该示例连接了一个固定的应答队列,以及一个处理请求并返回应答的“远程”侦听器容器。 + +| |当回复超时(`replyTimeout`)时,`sendAndReceive()`方法返回 null。| +|---|--------------------------------------------------------------------------------------| + +在版本 1.3.6 之前,对超时邮件的延迟回复只会被记录。现在,如果收到了一个延迟的回复,它将被拒绝(模板抛出一个`AmqpRejectAndDontRequeueException`)。如果回复队列被配置为将被拒绝的消息发送到死信交换,则可以检索该回复以供以后进行分析。要做到这一点,需要将一个队列与配置的死信交换绑定,其路由密钥与回复队列的名称相等。 + +有关配置死字的更多信息,请参见[RabbitMQ 死信文档](https://www.rabbitmq.com/dlx.html)。你还可以查看`FixedReplyQueueDeadLetterTests`测试用例。 + +##### [](#async-template)异步兔子模板 + +1.6 版引入了`AsyncRabbitTemplate`。它具有与[`AmqpTemplate`](#amqp-template)上的方法类似的`sendAndReceive`(和`convertSendAndReceive`)方法。然而,它们返回的不是阻塞,而是`ListenableFuture`。 + +`sendAndReceive`方法返回一个`RabbitMessageFuture`。`convertSendAndReceive`方法返回一个`RabbitConverterFuture`。 + +你可以稍后通过在 Future 上调用`get()`来同步检索结果,也可以注册一个与结果异步调用的回调。下面的清单显示了这两种方法: + +``` +@Autowired +private AsyncRabbitTemplate template; + +... + +public void doSomeWorkAndGetResultLater() { + + ... + + ListenableFuture future = this.template.convertSendAndReceive("foo"); + + // do some more work + + String reply = null; + try { + reply = future.get(); + } + catch (ExecutionException e) { + ... + } + + ... + +} + +public void doSomeWorkAndGetResultAsync() { + + ... + + RabbitConverterFuture future = this.template.convertSendAndReceive("foo"); + future.addCallback(new ListenableFutureCallback() { + + @Override + public void onSuccess(String result) { + ... + } + + @Override + public void onFailure(Throwable ex) { + ... + } + + }); + + ... + +} +``` + +如果设置了`mandatory`并且消息无法传递,则 Future 抛出一个`ExecutionException`,原因为`AmqpMessageReturnedException`,该原因封装了返回的消息和有关返回的信息。 + +如果设置了`enableConfirms`,则 Future 有一个名为`confirm`的属性,它本身是一个`ListenableFuture`,带有`true`,表示成功发布。如果确认的 future 是`false`,则`RabbitFuture`还具有一个名为`nackCause`的属性,如果可用,则该属性包含失败的原因。 + +| |如果在回复之后收到了发布者确认,则该发布者确认将被丢弃,因为该回复意味着成功发布。| +|---|-------------------------------------------------------------------------------------------------------------------| + +你可以将模板上的`receiveTimeout`属性设置为超时回复(默认设置为`30000`-30 秒)。如果发生超时,则使用`AmqpReplyTimeoutException`完成 future。 + +模板实现`SmartLifecycle`。在存在挂起的回复时停止模板,将导致挂起的`Future`实例被取消。 + +从版本 2.0 开始,异步模板现在支持[直接回复](https://www.rabbitmq.com/direct-reply-to.html),而不是配置的回复队列。要启用此功能,请使用以下构造函数之一: + +``` +public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) + +public AsyncRabbitTemplate(RabbitTemplate template) +``` + +参见[RabbitMQ 直接回复](#direct-reply-to),以使用直接回复与同步`RabbitTemplate`。 + +版本 2.0 引入了这些方法的变体(`convertSendAndReceiveAsType`),这些方法接受一个额外的`ParameterizedTypeReference`参数来转换复杂的返回类型。你必须使用`SmartMessageConverter`配置底层`RabbitTemplate`。有关更多信息,请参见[从`Message`转换为`RabbitTemplate`]。 + +##### [](#remoting) Spring 带有 AMQP 的远程控制 + +| |该功能已被弃用,将在 3.0 中删除。
它已被[处理异常](#annotation-error-handling)取代了很长一段时间,`returnExceptions`被设置为 true,并在发送端配置了`RemoteInvocationAwareMessageConverterAdapter`。
有关更多信息,请参见[处理异常](#annotation-error-handling)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 框架具有一般的远程处理能力,允许使用各种传输的[远程过程调用](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html)。 Spring-AMQP 支持类似的机制,在客户端上是`AmqpProxyFactoryBean`,在服务器上是`AmqpInvokerServiceExporter`。这为 AMQP 提供了 RPC。在客户端,使用`RabbitTemplate`作为对[earlier](#reply-listener)的描述。在服务器端,调用者(配置为`MessageListener`)接收消息,调用配置的服务,并使用入站消息的`replyTo`信息返回回复。 + +你可以将客户端工厂 Bean 注入任何 Bean(通过使用其`serviceInterface`)。然后,客户机可以调用代理上的方法,从而在 AMQP 上进行远程执行。 + +| |对于默认的`MessageConverter`实例,方法参数和返回的值必须是`Serializable`的实例。| +|---|----------------------------------------------------------------------------------------------------------------------------| + +在服务器端,`AmqpInvokerServiceExporter`同时具有`AmqpTemplate`和`MessageConverter`属性。目前,没有使用模板的`MessageConverter`。如果需要提供自定义消息转换器,则应该通过设置`messageConverter`属性来提供它。在客户端,你可以向`AmqpTemplate`添加自定义消息转换器,该消息转换器通过使用其`amqpTemplate`属性提供给`AmqpProxyFactoryBean`。 + +下面的清单显示了示例客户机和服务器配置: + +``` + + + + + + + + + + + + + + + + + + +``` + +``` + + + + + + + + + + + + + + + + + +``` + +| |`AmqpInvokerServiceExporter`只能处理格式正确的消息,例如从`AmqpProxyFactoryBean`发送的消息。
如果收到无法解释的消息,将发送序列化的`RuntimeException`作为回复。
如果消息没有`replyToAddress`属性,如果没有配置死信交换,则消息将被拒绝并永久丢失。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |默认情况下,如果无法传递请求消息,则调用线程最终超时,并抛出一个`RemoteProxyFailureException`。
默认情况下,超时为 5 秒。
你可以通过在`RabbitTemplate`上设置`replyTimeout`属性来修改持续时间,
从 1.5 版本开始,通过将`mandatory`属性设置为`true`并在连接工厂上启用返回(参见[发布者确认并返回](#cf-pub-conf-ret)),调用线程抛出一个`AmqpMessageReturnedException`。
查看[回复超时](#reply-timeout)以获取更多信息。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#broker-configuration)4.1.11。配置代理 + +AMQP 规范描述了如何使用该协议在代理上配置队列、交换和绑定。在`org.springframework.amqp.core`包中的`AmqpAdmin`接口中存在这些操作(可从 0.8 规范和更高版本移植)。该类的 RabbitMQ 实现`RabbitAdmin`位于`org.springframework.amqp.rabbit.core`包中。 + +`AmqpAdmin`接口基于使用 Spring AMQP 域抽象,如以下清单所示: + +``` +public interface AmqpAdmin { + + // Exchange Operations + + void declareExchange(Exchange exchange); + + void deleteExchange(String exchangeName); + + // Queue Operations + + Queue declareQueue(); + + String declareQueue(Queue queue); + + void deleteQueue(String queueName); + + void deleteQueue(String queueName, boolean unused, boolean empty); + + void purgeQueue(String queueName, boolean noWait); + + // Binding Operations + + void declareBinding(Binding binding); + + void removeBinding(Binding binding); + + Properties getQueueProperties(String queueName); + +} +``` + +另见[作用域操作](#scoped-operations)。 + +`getQueueProperties()`方法返回一些关于队列的有限信息(消息计数和消费者计数)。返回的属性的键在`RabbitTemplate`(`QUEUE_NAME`、`QUEUE_MESSAGE_COUNT`和`QUEUE_CONSUMER_COUNT`)中作为常量可用。[RabbitMQ REST API](#management-rest-api)在`QueueInfo`对象中提供了更多的信息。 + +no-arg`declareQueue()`方法在代理上定义了一个具有自动生成的名称的队列。这个自动生成队列的附加属性是`exclusive=true`、`autoDelete=true`和`durable=false`。 + +`declareQueue(Queue queue)`方法接受一个`Queue`对象,并返回声明的队列的名称。如果所提供的`name`的`Queue`属性是空的`String`,则代理将使用生成的名称声明队列。该名称将返回给调用者。该名称也被添加到`Queue`的`actualName`属性中。你只能通过直接调用`RabbitAdmin`以编程方式使用此功能。当在应用程序上下文中以声明方式定义队列时,管理员使用自动声明时,可以将 name 属性设置为`""`(空字符串)。然后,代理创建名称。从版本 2.1 开始,侦听器容器可以使用这种类型的队列。有关更多信息,请参见[容器和以代理命名的队列](#containers-and-broker-named-queues)。 + +这与`AnonymousQueue`相反,该框架生成唯一的(`UUID`)名称,并将`durable`设置为`false`和`exclusive`,`autoDelete`设置为`true`。带有空(或缺少)``属性的`name`总是创建`AnonymousQueue`。 + +参见[`AnonymousQueue`](#anonymous-queue)以了解为什么`AnonymousQueue`比代理生成的队列名称更受欢迎,以及如何控制名称的格式。从版本 2.1 开始,默认情况下,匿名队列的声明参数`Queue.X_QUEUE_LEADER_LOCATOR`设置为`client-local`。这确保了队列是在应用程序连接的节点上声明的。声明式队列必须具有固定的名称,因为它们可能在上下文的其他地方被引用——例如在以下示例中显示的侦听器中: + +``` + + + +``` + +见[交换、队列和绑定的自动声明](#automatic-declaration)。 + +这个接口的 RabbitMQ 实现是`RabbitAdmin`,当使用 Spring XML 进行配置时,它类似于以下示例: + +``` + + + +``` + +当`CachingConnectionFactory`缓存模式是`CHANNEL`(默认值)时,`RabbitAdmin`实现对在同一`ApplicationContext`中声明的队列、交换和绑定进行自动延迟声明。一旦向代理打开`Connection`,就会声明这些组件。有一些名称空间特性使其非常方便——例如,在 Stocks 示例应用程序中,我们有以下内容: + +``` + + + + + + + + + + + + + + + +``` + +在前面的示例中,我们使用匿名队列(实际上,在内部,只使用由框架(而不是由代理)生成的名称的队列),并通过 ID 引用它们。我们还可以使用显式名称来声明队列,这些名称也可以作为上下文中其 Bean 定义的标识符。下面的示例使用显式名称配置队列: + +``` + +``` + +| |你可以同时提供`id`和`name`属性。
这允许你引用队列(例如,
它还允许标准 Spring 特性(例如用于队列名称的属性占位符和 SPEL 表达式)。
当你使用名称作为 Bean 标识符时,这些特性是不可用的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +队列可以配置额外的参数——例如,`x-message-ttl`。当你使用名称空间支持时,它们以参数-名称/参数-值对的`Map`形式提供,这是通过使用``元素定义的。下面的示例展示了如何做到这一点: + +``` + + + + + + +``` + +默认情况下,参数被假定为字符串。对于其他类型的参数,你必须提供该类型。下面的示例展示了如何指定类型: + +``` + + + + + +``` + +当提供混合类型的参数时,你必须为每个条目元素提供类型。下面的示例展示了如何做到这一点: + +``` + + + + 100 + + + + + +``` + +在 Spring Framework3.2 及更高版本中,可以更简洁地声明这一点,如下所示: + +``` + + + + + + +``` + +在使用 Java 配置时,`Queue.X_QUEUE_LEADER_LOCATOR`类上的`setLeaderLocator()`方法支持`Queue`参数作为第一类属性。从版本 2.1 开始,匿名队列的声明默认设置为`client-local`。这确保了队列是在应用程序连接到的节点上声明的。 + +| |RabbitMQ 代理不允许声明具有不匹配参数的队列。
例如,如果`queue`已经存在一个不带`time to live`参数的`queue`队列,并且尝试使用(例如)`key="x-message-ttl" value="100"`声明它,则抛出一个异常。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下,当发生异常时,`RabbitAdmin`立即停止处理所有声明。这可能会导致下游问题,例如由于未声明另一个队列(在错误队列之后定义),侦听器容器无法初始化。 + +可以通过在`RabbitAdmin`实例上将`ignore-declaration-exceptions`属性设置为`true`来修改此行为。此选项指示`RabbitAdmin`记录异常并继续声明其他元素。当使用 Java 配置`RabbitAdmin`时,此属性称为`ignoreDeclarationExceptions`。这是一个适用于所有元素的全局设置。队列、交换和绑定具有类似的属性,仅适用于这些元素。 + +在版本 1.6 之前,此属性仅在通道上发生`IOException`时才生效,例如当前属性与期望属性之间存在不匹配时。现在,此属性对任何异常都生效,包括`TimeoutException`和其他异常。 + +此外,任何声明异常都会导致`DeclarationExceptionEvent`的发布,这是一个`ApplicationEvent`,可以由上下文中的任何`ApplicationListener`使用。该事件包含对管理、正在声明的元素和`Throwable`的引用。 + +##### [](#headers-exchange)headers exchange + +从版本 1.3 开始,你可以将`HeadersExchange`配置为在多个头上匹配。你还可以指定是否必须匹配任何或所有标题。下面的示例展示了如何做到这一点: + +``` + + + + + + + + + + + +``` + +从版本 1.6 开始,你可以使用`internal`标志(默认为`false`)配置`Exchanges`,并且通过`RabbitAdmin`在代理上正确配置这样的`Exchange`(如果在应用程序上下文中存在一个)。如果用于交换的`internal`标志是`true`,则 RabbitMQ 不允许客户机使用该交换。这对于死信交换或交换到交换绑定非常有用,在这种情况下,你不希望发布者直接使用该交换。 + +要查看如何使用 Java 来配置 AMQP 基础架构,请查看股票示例应用程序,其中有`@Configuration`类`AbstractStockRabbitConfiguration`,它依次具有`RabbitClientConfiguration`和`RabbitServerConfiguration`子类。下面的清单显示了`AbstractStockRabbitConfiguration`的代码: + +``` +@Configuration +public abstract class AbstractStockAppRabbitConfiguration { + + @Bean + public CachingConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost"); + connectionFactory.setUsername("guest"); + connectionFactory.setPassword("guest"); + return connectionFactory; + } + + @Bean + public RabbitTemplate rabbitTemplate() { + RabbitTemplate template = new RabbitTemplate(connectionFactory()); + template.setMessageConverter(jsonMessageConverter()); + configureRabbitTemplate(template); + return template; + } + + @Bean + public Jackson2JsonMessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + public TopicExchange marketDataExchange() { + return new TopicExchange("app.stock.marketdata"); + } + + // additional code omitted for brevity + +} +``` + +在股票应用程序中,通过使用以下`@Configuration`类来配置服务器: + +``` +@Configuration +public class RabbitServerConfiguration extends AbstractStockAppRabbitConfiguration { + + @Bean + public Queue stockRequestQueue() { + return new Queue("app.stock.request"); + } +} +``` + +这是`@Configuration`类的整个继承链的结束。最终结果是在应用程序启动时向代理声明`TopicExchange`和`Queue`。在服务器配置中,没有将`TopicExchange`绑定到队列,这是在客户机应用程序中完成的。但是,股票请求队列会自动绑定到 AMQP 默认交换。此行为由规范定义。 + +客户机`@Configuration`类更有趣一些。其声明如下: + +``` +@Configuration +public class RabbitClientConfiguration extends AbstractStockAppRabbitConfiguration { + + @Value("${stocks.quote.pattern}") + private String marketDataRoutingKey; + + @Bean + public Queue marketDataQueue() { + return amqpAdmin().declareQueue(); + } + + /** + * Binds to the market data exchange. + * Interested in any stock quotes + * that match its routing key. + */ + @Bean + public Binding marketDataBinding() { + return BindingBuilder.bind( + marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey); + } + + // additional code omitted for brevity + +} +``` + +客户机通过`AmqpAdmin`上的`declareQueue()`方法声明另一个队列。它使用一种路由模式将队列绑定到市场数据交换,该路由模式在属性文件中具体化。 + +##### [](#builder-api)用于队列和交换的 Builder API + +版本 1.6 引入了一个方便的 Fluent API,用于在使用 Java 配置时配置`Queue`和`Exchange`对象。下面的示例展示了如何使用它: + +``` +@Bean +public Queue queue() { + return QueueBuilder.nonDurable("foo") + .autoDelete() + .exclusive() + .withArgument("foo", "bar") + .build(); +} + +@Bean +public Exchange exchange() { + return ExchangeBuilder.directExchange("foo") + .autoDelete() + .internal() + .withArgument("foo", "bar") + .build(); +} +``` + +参见[`org.springframework.amqp.core.QueueBuilder`](https://DOCS. Spring.io/ Spring-amqp/DOCS/latest-ga/api/org/springframework/amqp/core/queuebuilder.html)和[`org.springframework.amqp.core.ExchangeBuilder`(https://DOCS. Spring.io/ Spring-amqp/DOCS/latest-ga/api/org/springframf/amqp/core/exchangebuilder.html)以获取更多信息。 + +从版本 2.0 开始,`ExchangeBuilder`现在默认情况下创建持久交换,以与单个`AbstractExchange`类上的简单构造函数保持一致。要与构建器进行非持久交换,在调用`.build()`之前使用`.durable(false)`。不再提供不带参数的`durable()`方法。 + +2.2 版引入了 Fluent API,以添加“众所周知的”交换和队列参数。 + +``` +@Bean +public Queue allArgs1() { + return QueueBuilder.nonDurable("all.args.1") + .ttl(1000) + .expires(200_000) + .maxLength(42) + .maxLengthBytes(10_000) + .overflow(Overflow.rejectPublish) + .deadLetterExchange("dlx") + .deadLetterRoutingKey("dlrk") + .maxPriority(4) + .lazy() + .leaderLocator(LeaderLocator.minLeaders) + .singleActiveConsumer() + .build(); +} + +@Bean +public DirectExchange ex() { + return ExchangeBuilder.directExchange("ex.with.alternate") + .durable(true) + .alternate("alternate") + .build(); +} +``` + +##### [](#collection-declaration)声明交换、队列和绑定的集合 + +你可以在`Declarable`对象(`Queue`,`Exchange`,和`Binding`)的集合中包装`Declarables`对象。`RabbitAdmin`在应用程序上下文中检测此类 bean(以及离散`Declarable`bean),并在每次建立连接时(最初和连接失败后)在代理上声明所包含的对象。下面的示例展示了如何做到这一点: + +``` +@Configuration +public static class Config { + + @Bean + public CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost"); + } + + @Bean + public RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + public DirectExchange e1() { + return new DirectExchange("e1", false, true); + } + + @Bean + public Queue q1() { + return new Queue("q1", false, false, true); + } + + @Bean + public Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); + } + + @Bean + public Declarables es() { + return new Declarables( + new DirectExchange("e2", false, true), + new DirectExchange("e3", false, true)); + } + + @Bean + public Declarables qs() { + return new Declarables( + new Queue("q2", false, false, true), + new Queue("q3", false, false, true)); + } + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public Declarables prototypes() { + return new Declarables(new Queue(this.prototypeQueueName, false, false, true)); + } + + @Bean + public Declarables bs() { + return new Declarables( + new Binding("q2", DestinationType.QUEUE, "e2", "k2", null), + new Binding("q3", DestinationType.QUEUE, "e3", "k3", null)); + } + + @Bean + public Declarables ds() { + return new Declarables( + new DirectExchange("e4", false, true), + new Queue("q4", false, false, true), + new Binding("q4", DestinationType.QUEUE, "e4", "k4", null)); + } + +} +``` + +| |在 2.1 之前的版本中,你可以通过定义类型为`Collection`的 bean 来声明多个`Declarable`实例,
在某些情况下,这可能会导致不良的副作用,因为管理员必须迭代所有`Collection`bean。,
现在禁用此功能,以支持`Declarables`,正如前面在这一节中所讨论的,
你可以通过将`RabbitAdmin`属性设置为`declareCollections`来恢复到以前的行为。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +版本 2.2 将`getDeclarablesByType`方法添加到`Declarables`中;这可以作为一种方便,例如,在声明侦听器容器 Bean 时使用。 + +``` +public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, + Declarables mixedDeclarables, MessageListener listener) { + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueues(mixedDeclarables.getDeclarablesByType(Queue.class).toArray(new Queue[0])); + container.setMessageListener(listener); + return container; +} +``` + +##### [](#conditional-declaration)条件声明 + +默认情况下,所有队列、交换和绑定都由应用程序上下文中的所有`RabbitAdmin`实例声明(假设它们具有`auto-startup="true"`)。 + +从版本 2.1.9 开始,`RabbitAdmin`有一个新的属性`explicitDeclarationsOnly`(默认情况下是`false`);当将其设置为`true`时,管理员将只声明显式配置为由该管理员声明的 bean。 + +| |从 1.2 版本开始,你可以有条件地声明这些元素。
当应用程序连接到多个代理并且需要指定应该用哪些代理声明特定元素时,这一点特别有用。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +表示这些元素的类实现`Declarable`,它有两个方法:`shouldDeclare()`和`getDeclaringAdmins()`。`RabbitAdmin`使用这些方法来确定特定实例是否应该实际处理其`Connection`上的声明。 + +这些属性可以作为名称空间中的属性使用,如以下示例所示: + +``` + + + + + + + + + + + + + + + + + + + +``` + +| |默认情况下,`auto-declare`属性是`true`,并且,如果`declared-by`没有提供(或者是空的),那么所有`RabbitAdmin`实例都会声明对象(只要管理员的`auto-startup`属性是`true`,默认的,并且管理员的`explicit-declarations-only`属性是假的)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +类似地,你可以使用基于 Java 的`@Configuration`来实现相同的效果。在下面的示例中,组件由`admin1`声明,但不是由`admin2`声明: + +``` +@Bean +public RabbitAdmin admin1() { + return new RabbitAdmin(cf1()); +} + +@Bean +public RabbitAdmin admin2() { + return new RabbitAdmin(cf2()); +} + +@Bean +public Queue queue() { + Queue queue = new Queue("foo"); + queue.setAdminsThatShouldDeclare(admin1()); + return queue; +} + +@Bean +public Exchange exchange() { + DirectExchange exchange = new DirectExchange("bar"); + exchange.setAdminsThatShouldDeclare(admin1()); + return exchange; +} + +@Bean +public Binding binding() { + Binding binding = new Binding("foo", DestinationType.QUEUE, exchange().getName(), "foo", null); + binding.setAdminsThatShouldDeclare(admin1()); + return binding; +} +``` + +##### [](#note-id-name)关于`id`和`name`属性的注释 + +``和``元素上的`name`属性反映了代理中实体的名称。对于队列,如果省略`name`,则创建一个匿名队列(参见[`AnonymousQueue`](#anonymous-queue))。 + +在 2.0 之前的版本中,`name`也被注册为 Bean 名称别名(类似于`name`上的``元素)。 + +这造成了两个问题: + +* 它阻止了使用相同名称的队列和交换的声明。 + +* 如果别名包含 SPEL 表达式(`#{…​}`),则该别名将不会解析。 + +从版本 2.0 开始,如果你声明其中一个元素,同时带有`id`*和*一个`name`属性,则该名称将不再声明为 Bean name 别名。如果你希望声明一个队列并使用相同的`name`进行交换,则必须提供一个`id`。 + +如果元素仅具有`name`属性,则不会发生更改。 Bean 仍然可以被`name`引用——例如,在绑定声明中。但是,如果名称包含 SPEL,则仍然不能引用它——你必须提供`id`以供引用。 + +##### [](#anonymous-queue)`AnonymousQueue` + +通常,当你需要一个唯一命名的、排他的、自动删除的队列时,我们建议你使用`AnonymousQueue`而不是代理定义的队列名称(使用`""`作为`Queue`名称,使代理生成队列名称)。 + +这是因为: + +1. 队列实际上是在建立到代理的连接时声明的。这是在豆子被创建并连接在一起之后很久的事情。使用队列的 bean 需要知道它的名称。实际上,在启动应用程序时,代理程序甚至可能不在运行。 + +2. 如果由于某种原因丢失了与代理的连接,则管理员将重新声明同名的`AnonymousQueue`。如果我们使用代理声明的队列,队列名称将会更改。 + +你可以控制`AnonymousQueue`实例使用的队列名称的格式。 + +默认情况下,队列名称的前缀是`spring.gen-`,后面跟着`UUID`的 base64 表示形式——例如:`spring.gen-MRBv9sqISkuCiPfOYfpo4g`。 + +可以在构造函数参数中提供`AnonymousQueue.NamingStrategy`实现。下面的示例展示了如何做到这一点: + +``` +@Bean +public Queue anon1() { + return new AnonymousQueue(); +} + +@Bean +public Queue anon2() { + return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy("something-")); +} + +@Bean +public Queue anon3() { + return new AnonymousQueue(AnonymousQueue.UUIDNamingStrategy.DEFAULT); +} +``` + +第一个 Bean 生成一个以`spring.gen-`为前缀的队列名称,后面跟着`UUID`的 base64 表示——例如:`spring.gen-MRBv9sqISkuCiPfOYfpo4g`。第二 Bean 生成以`something-`为前缀的队列名称,后跟`UUID`的 base64 表示。第三个 Bean 只使用 UUID(不使用 base64 转换)生成一个名称——例如,`f20c818a-006b-4416-bf91-643590fedb0e`。 + +Base64 编码使用了 RFC4648 中的“URL 和文件名安全字母表”。删除尾随填充字符(`=`)。 + +你可以提供自己的命名策略,从而可以在队列名称中包括其他信息(例如应用程序名称或客户端主机)。 + +在使用 XML 配置时,可以指定命名策略。对于实现`AnonymousQueue.NamingStrategy`的 Bean 引用,在``元素上存在`naming-strategy`属性。以下示例展示了如何以各种方式指定命名策略: + +``` + + + + + + + + + + + +``` + +第一个示例创建了`spring.gen-MRBv9sqISkuCiPfOYfpo4g`之类的名称。第二个示例使用 UUID 的字符串表示形式创建名称。第三个示例创建了`custom.gen-MRBv9sqISkuCiPfOYfpo4g`之类的名称。 + +你还可以提供自己的命名策略 Bean。 + +从版本 2.1 开始,默认情况下,匿名队列的声明参数`Queue.X_QUEUE_LEADER_LOCATOR`设置为`client-local`。这确保了队列是在应用程序连接的节点上声明的。在构造实例之后,你可以通过调用`queue.setLeaderLocator(null)`来恢复到以前的行为。 + +##### [](#declarable-recovery)恢复自动删除声明 + +通常,`RabbitAdmin`(s)只恢复在应用程序上下文中声明为 bean 的队列/交换/绑定;如果任何此类声明是自动删除的,则如果连接丢失,代理将删除它们。当重新建立连接时,管理员将重新声明这些实体。通常,通过调用`admin.declareQueue(…​)`、`admin.declareExchange(…​)`和`admin.declareBinding(…​)`创建的实体将不会被恢复。 + +从版本 2.4 开始,管理人员有一个新的属性`redeclareManualDeclarations`;如果为真,管理人员将恢复这些实体以及应用程序上下文中的 bean。 + +如果调用`deleteQueue(…​)`、`deleteExchange(…​)`或`removeBinding(…​)`,则不会执行单个声明的恢复。删除队列和交换时,将从可恢复实体中删除相关的绑定。 + +最后,调用`resetAllManualDeclarations()`将阻止恢复任何先前声明的实体。 + +#### [](#broker-events)4.1.12。代理事件监听器 + +当启用[事件交换插件 Name](https://www.rabbitmq.com/event-exchange.html)时,如果将类型`BrokerEventListener`的 Bean 添加到应用程序上下文中,则它将所选的代理事件发布为`BrokerEvent`实例,该实例可以通过正常的 Spring `ApplicationListener`或`@EventListener`方法来使用。事件由代理发布到主题交换`amq.rabbitmq.event`,每个事件类型都有不同的路由密钥。侦听器使用事件键,用于将`AnonymousQueue`绑定到交换,以便侦听器仅接收选定的事件。由于这是一个主题交换,所以可以使用通配符(以及显式地请求特定事件),如下例所示: + +``` +@Bean +public BrokerEventListener eventListener() { + return new BrokerEventListener(connectionFactory(), "user.deleted", "channel.#", "queue.#"); +} +``` + +通过使用普通 Spring 技术,可以进一步缩小单个事件侦听器中接收到的事件的范围,如下例所示: + +``` +@EventListener(condition = "event.eventType == 'queue.created'") +public void listener(BrokerEvent event) { + ... +} +``` + +#### [](#delayed-message-exchange)4.1.13。延迟的消息交换 + +版本 1.6 引入了对[延迟消息交换插件 Name](https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/)的支持 + +| |该插件目前被标记为试验性的,但已经有一年多的时间可以使用了(在撰写本文时),
如果需要对该插件进行更改,我们计划尽快添加对此类更改的支持,由于这个原因,此功能已在 RabbitMQ3.6.0 和版本 0.0.1 的插件中进行了测试。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要使用`RabbitAdmin`来声明一个 Exchange 为延迟,可以将 Exchange Bean 上的`delayed`属性设置为`true`。`RabbitAdmin`使用交换类型(`Direct`,`Fanout`,以此类推)来设置`x-delayed-type`参数,并用类型`x-delayed-message`声明交换。 + +当使用 XML 配置 Exchange bean 时,`delayed`属性(默认:`false`)也可用。下面的示例展示了如何使用它: + +``` + +``` + +要发送延迟消息,可以通过`MessageProperties`设置`x-delay`头,如下例所示: + +``` +MessageProperties properties = new MessageProperties(); +properties.setDelay(15000); +template.send(exchange, routingKey, + MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); +``` + +``` +rabbitTemplate.convertAndSend(exchange, routingKey, "foo", new MessagePostProcessor() { + + @Override + public Message postProcessMessage(Message message) throws AmqpException { + message.getMessageProperties().setDelay(15000); + return message; + } + +}); +``` + +要检查消息是否延迟,请在`MessageProperties`上使用`getReceivedDelay()`方法。它是一个单独的属性,以避免意外传播到由输入消息生成的输出消息。 + +#### [](#management-rest-api)4.1.14。RabbitMQ REST API + +启用管理插件后,RabbitMQ 服务器公开一个 REST API 来监视和配置代理。a[API 的 Java 绑定](https://github.com/rabbitmq/hop)现已提供。`com.rabbitmq.http.client.Client`是一个标准的、直接的 API,因此是阻塞的 API。它是基于[Spring Web](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web)模块及其`RestTemplate`实现的。另一方面,`com.rabbitmq.http.client.ReactorNettyClient`是一个基于[反应堆网状结构](https://projectreactor.io/docs/netty/release/reference/docs/index.html)项目的反应式、非阻塞实现。 + +跳依赖项(`com.rabbitmq:http-client`)现在也是`optional`。 + +有关更多信息,请访问他们的 Javadoc。 + +#### [](#exception-handling)4.1.15。异常处理 + +使用 RabbitMQ Java 客户机的许多操作都可以抛出检查过的异常。例如,在很多情况下`IOException`实例可能会被抛出。`RabbitTemplate`、`SimpleMessageListenerContainer`和其他 Spring AMQP 组件捕获这些异常,并将它们转换为`AmqpException`层次结构中的一个异常。这些在’org.springframework.amqp’包中定义,而`AmqpException`是层次结构的基础。 + +当侦听器抛出异常时,它被包装在`ListenerExecutionFailedException`中。通常情况下,代理会拒绝并重新请求消息。将`defaultRequeueRejected`设置为`false`会导致消息被丢弃(或路由到死信交换)。如[消息侦听器和异步情况](#async-listeners)中所讨论的,侦听器可以抛出一个`AmqpRejectAndDontRequeueException`(或`ImmediateRequeueAmqpException`)来有条件地控制此行为。 + +但是,有一类错误是侦听器无法控制行为的。当遇到无法转换的消息(例如,无效的`content_encoding`报头)时,在消息到达用户代码之前会抛出一些异常。将`defaultRequeueRejected`设置为`true`(默认)(或抛出`ImmediateRequeueAmqpException`),这样的消息将被一遍又一遍地重新传递。在版本 1.3.2 之前,用户需要编写一个自定义`ErrorHandler`,如[异常处理](#exception-handling)中所讨论的,以避免这种情况。 + +从版本 1.3.2 开始,默认的`ErrorHandler`现在是一个`ConditionalRejectingErrorHandler`,它拒绝(并且不请求)带有不可恢复错误的失败消息。具体地说,它拒绝具有以下错误的失败消息: + +* `o.s.amqp…​MessageConversionException`:可以在使用`MessageConverter`转换传入消息有效负载时抛出。 + +* `o.s.messaging…​MessageConversionException`:如果在映射到`@RabbitListener`方法时需要额外的转换,则可以由转换服务抛出。 + +* `o.s.messaging…​MethodArgumentNotValidException`:如果在侦听器中使用验证(例如,`@Valid`)并且验证失败,则可以抛出。 + +* `o.s.messaging…​MethodArgumentTypeMismatchException`:如果入站消息被转换为针对目标方法不正确的类型,则可以抛出该消息。例如,参数被声明为`Message`,但是`Message`被接收。 + +* `java.lang.NoSuchMethodException`:在版本 1.6.3 中添加。 + +* `java.lang.ClassCastException`:在版本 1.6.3 中添加。 + +你可以使用`FatalExceptionStrategy`配置此错误处理程序的实例,以便用户可以为条件消息拒绝提供自己的规则——例如,来自 Spring Retry([消息侦听器和异步情况](#async-listeners))的`BinaryExceptionClassifier`的委托实现。此外,`ListenerExecutionFailedException`现在有一个`failedMessage`属性,你可以在决策中使用它。如果`FatalExceptionStrategy.isFatal()`方法返回`true`,则错误处理程序抛出一个`AmqpRejectAndDontRequeueException`。当异常被确定为致命异常时,默认`FatalExceptionStrategy`会记录一条警告消息。 + +自版本 1.6.3 以来,将用户异常添加到致命异常列表的一种方便的方法是子类`ConditionalRejectingErrorHandler.DefaultExceptionStrategy`并覆盖`isUserCauseFatal(Throwable cause)`方法,以返回`true`的致命异常。 + +处理 DLQ 消息的一种常见模式是在这些消息上设置`time-to-live`以及附加的 DLQ 配置,以便这些消息过期并路由回主队列进行重试。这种技术的问题在于,导致致命异常的消息会永远循环。从版本 2.1 开始,`ConditionalRejectingErrorHandler`检测消息上的`x-death`头,该头将导致抛出一个致命的异常。该消息已被记录并丢弃。通过将`ConditionalRejectingErrorHandler`上的`discardFatalsWithXDeath`属性设置为`false`,可以恢复到以前的行为。 + +| |从版本 2.1.9 开始,具有这些致命异常的消息将被拒绝,并且默认情况下不会重新请求,即使容器确认模式是手动的。
这些异常通常发生在调用侦听器之前,因此侦听器没有机会对消息进行 ACK 或 NACK,因此消息仍处于未 ACKED 状态。
以恢复到先前的行为,将`ConditionalRejectingErrorHandler`上的`rejectManual`属性设置为`false`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#transactions)4.1.16。交易 + +Spring Rabbit Framework 具有对同步和异步用例中的自动事务管理的支持,其具有许多不同的语义,这些语义可以通过声明方式进行选择,这是 Spring 事务的现有用户所熟悉的。这使得许多即使不是最常见的消息传递模式也很容易实现。 + +有两种方法可以向框架发出所需的事务语义的信号。在`RabbitTemplate`和`SimpleMessageListenerContainer`中,都有一个标志`channelTransacted`,如果`true`,它告诉框架使用事务通道,并以提交或回滚(取决于结果)结束所有操作(发送或接收),并发出回滚的异常信号。另一个信号是提供具有 Spring 的`PlatformTransactionManager`实现之一的外部事务作为正在进行的操作的上下文。如果在框架发送或接收消息时已经有一个事务在进行中,并且`channelTransacted`标志是`true`,则消息事务的提交或回滚将推迟到当前事务结束时进行。如果`channelTransacted`标志是`false`,则消息传递操作不会应用事务语义(它是自动 ACKED 的)。 + +`channelTransacted`标志是一个配置时间设置。在创建 AMQP 组件时(通常是在应用程序启动时),对它进行一次声明和处理。外部事务在原则上是更动态的,因为系统在运行时响应当前线程状态。然而,在实践中,当事务以声明方式分层到应用程序上时,它通常也是一个配置设置。 + +对于使用`RabbitTemplate`的同步用例,外部事务由调用方提供,可以是声明式的,也可以是命令式的(通常的 Spring 事务模型)。下面的示例展示了一种声明式方法(通常更受欢迎,因为它是非侵入性的),其中模板已配置为`channelTransacted=true`: + +``` +@Transactional +public void doSomething() { + String incoming = rabbitTemplate.receiveAndConvert(); + // do some more database processing... + String outgoing = processInDatabaseAndExtractReply(incoming); + rabbitTemplate.convertAndSend(outgoing); +} +``` + +在前面的示例中,在标记为`@Transactional`的方法中,作为消息体接收、转换和发送`String`有效负载。如果数据库处理出现异常而失败,则传入消息将返回给代理,而传出消息将不会发送。这适用于事务性方法链中带有`RabbitTemplate`的任何操作(例如,除非直接对`Channel`进行操作以尽早提交事务)。 + +对于带有`SimpleMessageListenerContainer`的异步用例,如果需要一个外部事务,则容器在设置侦听器时必须请求它。为了表示需要外部事务,用户在配置容器时向容器提供`PlatformTransactionManager`的实现。下面的示例展示了如何做到这一点: + +``` +@Configuration +public class ExampleExternalTransactionAmqpConfiguration { + + @Bean + public SimpleMessageListenerContainer messageListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(rabbitConnectionFactory()); + container.setTransactionManager(transactionManager()); + container.setChannelTransacted(true); + container.setQueueName("some.queue"); + container.setMessageListener(exampleListener()); + return container; + } + +} +``` + +在前面的示例中,事务管理器被添加为从另一个 Bean 定义注入的依赖项(未示出),并且`channelTransacted`标志也被设置为`true`。其结果是,如果侦听器发生异常而失败,事务将被回滚,消息也将返回给代理。值得注意的是,如果事务未能提交(例如,由于数据库约束错误或连接问题),AMQP 事务也将回滚,并将消息返回给代理。这有时被称为“尽最大努力 1 阶段提交”,是可靠消息传递的一个非常强大的模式。如果在前面的示例中将`channelTransacted`标志设置为`false`(默认值),则仍将为侦听器提供外部事务,但是所有消息传递操作都将被自动 ACK,因此其效果是即使在业务操作的回滚时也提交消息传递操作。 + +##### [](#conditional-rollback)条件回滚 + +在 1.6.6 版本之前,在使用外部事务管理器(例如 JDBC)时,向容器的`transactionAttribute`添加回滚规则不会产生任何效果。异常总是回滚事务。 + +此外,当在容器的建议链中使用[交易建议](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/transaction.html#transaction-declarative)时,条件回滚不是很有用,因为所有侦听器异常都包装在`ListenerExecutionFailedException`中。 + +第一个问题已经得到纠正,规则现在得到了适当的应用。而且,现在提供了`ListenerFailedRuleBasedTransactionAttribute`。它是`RuleBasedTransactionAttribute`的一个子类,唯一的区别是它知道`ListenerExecutionFailedException`,并将这种异常的原因用于规则。这个事务属性可以直接在容器中使用,也可以通过事务通知使用。 + +下面的示例使用了这个规则: + +``` +@Bean +public AbstractMessageListenerContainer container() { + ... + container.setTransactionManager(transactionManager); + RuleBasedTransactionAttribute transactionAttribute = + new ListenerFailedRuleBasedTransactionAttribute(); + transactionAttribute.setRollbackRules(Collections.singletonList( + new NoRollbackRuleAttribute(DontRollBackException.class))); + container.setTransactionAttribute(transactionAttribute); + ... +} +``` + +##### [](#transaction-rollback)关于回滚接收消息的说明 + +AMQP 事务只适用于发送给代理的消息和 ACK。因此,当对 Spring 事务进行回滚并且已经接收到消息时, Spring AMQP 不仅必须回滚该事务,而且还必须手动拒绝该消息(有点像 nack,但这不是规范所称的那样)。对消息拒绝所采取的操作独立于事务,并且依赖于`defaultRequeueRejected`属性(默认值:`true`)。有关拒绝失败消息的更多信息,请参见[消息侦听器和异步情况](#async-listeners)。 + +有关 RabbitMQ 事务及其限制的更多信息,请参见[RabbitMQ 代理语义](https://www.rabbitmq.com/semantics.html)。 + +| |在 RabbitMQ2.7.0 之前,这样的消息(以及在通道关闭或中止时未加控制的消息)会被发送到 Rabbit Broker 上的队列的后面。
自 2.7.0 以来,被拒绝的消息会被发送到队列的前面,其方式与 JMS 回滚消息类似。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |以前,在本地事务回滚和提供`TransactionManager`时,对事务回滚的消息请求是不一致的。
在前一种情况下,应用正常的请求逻辑(`AmqpRejectAndDontRequeueException`或`defaultRequeueRejected=false`)(参见[消息侦听器和异步情况](#async-listeners))。
与事务管理器,从版本 2.0 开始,该行为是一致的,并且在这两种情况下都应用了正常的请求逻辑。
要恢复到以前的行为,你可以将容器的`alwaysRequeueWithTxManagerRollback`属性设置为`true`。
参见[消息侦听器容器配置](#containerAttributes)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#using-rabbittransactionmanager)使用`RabbitTransactionManager` + +[RabbitTransactionManager](https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html)是在外部事务中执行 Rabbit 操作并与之同步的一种替代方法。此事务管理器是[`PlatformTransactionManager`](https://DOCS. Spring.io/ Spring/DOCS/current/javadoc-api/org/springframework/transactionmanager.html)接口的实现,应该与单个 Rabbit`ConnectionFactory`一起使用。 + +| |这种策略不能提供 XA 事务——例如,为了在消息传递和数据库访问之间共享事务。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------| + +需要应用程序代码来通过`ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)`检索事务性 Rabbit 资源,而不是随后创建通道的标准`Connection.createChannel()`调用。当使用 Spring AMQP 的[RabbitTemplate](https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/core/RabbitTemplate.html)时,它将自动检测线程绑定通道并自动参与其事务。 + +使用 Java 配置,你可以通过使用以下 Bean 设置一个新的 RabbitTransActionManager: + +``` +@Bean +public RabbitTransactionManager rabbitTransactionManager() { + return new RabbitTransactionManager(connectionFactory); +} +``` + +如果你更喜欢 XML 配置,那么可以在 XML 应用程序上下文文件中声明以下内容 Bean: + +``` + + + +``` + +##### [](#tx-sync)事务同步 + +将 RabbitMQ 事务与其他事务(例如 DBMS)同步提供了“Best Effort One Phase Commit”语义。在事务同步的完成后阶段,RabbitMQ 事务可能无法提交。这是由`spring-tx`基础架构作为错误记录的,但不会向调用代码抛出异常。从版本 2.3.10 开始,你可以在事务在处理该事务的同一线程上提交后调用`ConnectionUtils.checkAfterCompletion()`。如果没有发生异常,它将简单地返回;否则它将抛出一个`AfterCompletionFailedException`,该属性将具有表示完成的同步状态的属性。 + +通过调用`ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true)`来启用此功能;这是一个全局标志,适用于所有线程。 + +#### [](#containerAttributes)4.1.17。消息侦听器容器配置 + +对于配置与事务和服务质量相关的`SimpleMessageListenerContainer`和`DirectMessageListenerContainer`有相当多的选项,其中一些选项相互交互。适用于 SMLC、DMLC 或`StreamListenerContainer`(见[使用 RabbitMQ 流插件](#stream-support))的属性由相应列中的复选标记指示。有关帮助你决定哪个容器适合你的应用程序的信息,请参见[选择容器](#choose-container)。 + +下表显示了使用名称空间配置``时的容器属性名称及其等效属性名称(在括号中)。该元素上的`type`属性可以是`simple`(默认)或`direct`,以分别指定`SMLC`或`DMLC`。名称空间不公开某些属性。这些由属性的`N/A`表示。 + +| Property
(Attribute) |说明| SMLC | DMLC | StLC | +|-----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|--------------------------------|--------------------------------| +| []()[`ackTimeout`](#ackTimeout)
(N/A) |当`messagesPerAck`被设置时,这个超时被用作发送 ACK 的替代方法。
当一个新的消息到达时,未加 ACK 的消息的计数与`messagesPerAck`进行比较,并且将自上次 ACK 的时间与该值进行比较。
如果任一条件是`true`,当没有新的消息到达并且有未加锁的消息时,这个超时是近似的,因为每个条件只检查`monitorInterval`。
在这个表中还可以看到`messagesPerAck`和`monitorInterval`。| |![tickmark](images/tickmark.png)| | +| []()[`acknowledgeMode`](#acknowledgeMode)
(acknowledge) |*`NONE`:未发送 ACK(与`channelTransacted=true`不兼容),
RabbitMQ 将此称为“自动 ACK”,因为代理假定所有消息都是 ACK 的,而不需要消费者的任何操作。

*`MANUAL`:侦听器必须通过调用`Channel.basicAck()`来确认所有消息。

*`AUTO`:容器自动确认消息,除非`MessageListener`抛出异常。
注意,`acknowledgeMode`与`channelTransacted`是互补的——如果通道被处理,代理除了 ACK 之外还需要一个提交通知。
这是默认模式。
参见`batchSize`。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`adviceChain`](#adviceChain)
(advice-chain) |将 AOP 通知的数组应用到侦听器执行。
这可以用于应用额外的横切关注点,例如在代理死亡的情况下自动重试。
注意,在 AMQP 错误之后的简单重新连接由`CachingConnectionFactory`处理,只要经纪人还活着。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`afterReceivePostProcessors`](#afterReceivePostProcessors)
(N/A) |在调用侦听器之前调用的`MessagePostProcessor`实例的数组。
POST 处理器可以实现`PriorityOrdered`或`Ordered`。
该数组与上次调用的未排序成员进行排序。
如果 POST 处理器返回`null`,则该消息将被丢弃(并在适当的情况下得到确认)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`alwaysRequeueWithTxManagerRollback`](#alwaysRequeueWithTxManagerRollback)
(N/A) |设置为`true`,以便在配置事务管理器时始终请求回滚消息。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`autoDeclare`](#autoDeclare)
(auto-declare) |当设置为`true`(默认)时,容器使用`RabbitAdmin`来重新声明所有 AMQP 对象(队列、交换、绑定),如果它在启动过程中检测到至少有一个队列丢失,可能是因为它是`auto-delete`或过期的队列,但是如果队列由于任何原因丢失,则重新声明继续进行。
要禁用此行为,将此属性设置为`false`。
注意,如果缺少所有队列,则容器将无法启动。

| |Prior to version 1.6, if there was more than one admin in the context, the container would randomly select one.
If there were no admins, it would create one internally.
In either case, this could cause unexpected results.
Starting with version 1.6, for `autoDeclare` to work, there must be exactly one `RabbitAdmin` in the context, or a reference to a specific instance must be configured on the container using the `rabbitAdmin` property.|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| |在版本 1.6 之前,如果上下文中有一个以上的管理员,容器将随机选择一个。
如果没有管理员,它将在内部创建一个。
在这两种情况下,这都可能导致意外的结果。
从版本 1.6 开始,对于`autoDeclare`来说,上下文中必须正好有一个`RabbitAdmin`,或者必须使用`rabbitAdmin`属性在容器上配置对特定实例的引用。| | | | +| []()[`autoStartup`](#autoStartup)
(auto-startup) |表示容器应该在`ApplicationContext`开始时启动(作为`SmartLifecycle`回调的一部分,回调发生在初始化所有 bean 之后),
默认为`true`,但是,如果你的代理在启动时可能不可用,你可以将其设置为`false`,并在知道代理已经准备好时,稍后手动调用`start()`。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| +| []()[`batchSize`](#batchSize)
(transaction-size)
(batch-size) |当与`acknowledgeMode`一起使用时,将其设置为`AUTO`,容器在发送 ACK 之前尝试处理多达这个数量的消息(等待每个消息到接收超时设置),
这也是提交事务通道时的情况,
如果`prefetchCount`小于`batchSize`,它被增加以匹配`batchSize`。|![tickmark](images/tickmark.png)| | | +| []()[`batchingStrategy`](#batchingStrategy)
(N/A) |删除消息时使用的策略。
默认`SimpleDebatchingStrategy`。
参见[Batching](#template-batching)和[@RabbitListener 与批处理](#receiving-batch)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`channelTransacted`](#channelTransacted)
(channel-transacted) |布尔标志表示所有消息都应该在事务中得到确认(手动或自动)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`concurrency`](#concurrency)
(N/A) |`m-n`每个侦听器的并发消费者的范围。
如果只提供`n`,`n`是固定数量的消费者。
参见[监听器并发](#listener-concurrency)。|![tickmark](images/tickmark.png)| | | +| []()[`concurrentConsumers`](#concurrentConsumers)
(concurrency) |每个侦听器最初要启动的并发消费者的数量。
参见[监听器并发](#listener-concurrency)。|![tickmark](images/tickmark.png)| | | +| []()[`connectionFactory`](#connectionFactory)
(connection-factory) |对`ConnectionFactory`的引用。
在配置 byusing XML 名称空间时,默认引用的 Bean 名称是`rabbitConnectionFactory`。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`consecutiveActiveTrigger`](#consecutiveActiveTrigger)
(min-consecutive-active) |当考虑启动一个新的使用者时,一个使用者在不发生接收超时的情况下接收的连续消息的最小数量。
也受到“batchsize”的影响。
参见[监听器并发](#listener-concurrency)。
默认值:10。|![tickmark](images/tickmark.png)| | | +| []()[`consecutiveIdleTrigger`](#consecutiveIdleTrigger)
(min-consecutive-idle) |消费者在考虑停止一个消费者之前必须经历的接收超时的最小数量。
也受到“batchsize”的影响。
参见[监听器并发](#listener-concurrency)。
默认值:10。|![tickmark](images/tickmark.png)| | | +| []()[`consumerBatchEnabled`](#consumerBatchEnabled)
(batch-enabled) |如果`MessageListener`支持它,将其设置为 true 可以对离散消息进行批处理,最多`batchSize`;如果`receiveTimeout`中没有新消息到达,则将交付部分批处理。
如果这是 false,则仅支持由生产者创建的批处理;请参见[Batching](#template-batching)。|![tickmark](images/tickmark.png)| | | +| []()[`consumerCustomizer`](#consumerCustomizer)
(N/A) |用于修改由容器创建的流消费者的`ConsumerCustomizer` Bean。| | |![tickmark](images/tickmark.png)| +| []()[`consumerStartTimeout`](#consumerStartTimeout)
(N/A) |等待消费线程启动的时间(以毫秒为单位)。
如果这段时间过了,如果配置的`taskExecutor`没有足够的线程来支持容器`concurrentConsumers`。

可能发生这种情况的一个例子是,
默认情况:60000(一分钟)。|![tickmark](images/tickmark.png)| | | +| []()[`consumerTagStrategy`](#consumerTagStrategy)
(consumer-tag-strategy) |设置[消费者策略](#consumerTags)的实现,以便为每个消费者创建一个(唯一的)标记。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`consumersPerQueue`](#consumersPerQueue)
(consumers-per-queue) |为每个配置好的队列创建的消费者的数量。
参见[监听器并发](#listener-concurrency)。| |![tickmark](images/tickmark.png)| | +| []()[`consumeDelay`](#consumeDelay)
(N/A) |当使用[RabbitMQ 分片插件](https://github.com/rabbitmq/rabbitmq-sharding)与`concurrentConsumers > 1`时,存在一种竞争条件,可以防止消费者在碎片之间的均匀分布。
使用此属性在消费者开始之间添加一个小的延迟,以避免这种竞争条件。
你应该对值进行实验,以确定适合你的环境的延迟。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`debatchingEnabled`](#debatchingEnabled)
(N/A) |当为真时,侦听器容器将删除批处理的消息,并用批处理中的每条消息调用侦听器。,
从 2.2.7 版本开始,如果侦听器是`BatchMessageListener`或`ChannelAwareBatchMessageListener`,则[生产者创建了批](#template-batching)将被取消为[生产者创建了批](#template-batching)。否则,批处理中的消息将一次显示一次。
默认为真。
参见[Batching](#template-batching)和[@RabbitListener 与批处理](#receiving-batch)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`declarationRetries`](#declarationRetries)
(declaration-retries) |被动队列声明失败时重试尝试的次数。
被动队列声明发生在使用者启动或从多个队列消费时,当初始化期间不是所有的队列都可用时。
当重试用完后不能被动地声明(出于任何原因)配置的队列时,容器行为由前面描述的“missingqueuesfatal”属性控制。
默认情况:三次重试(总共四次尝试)。|![tickmark](images/tickmark.png)| | | +| []()[`defaultRequeueRejected`](#defaultRequeueRejected)
(requeue-rejected) |确定是否应重新请求因侦听器抛出异常而被拒绝的消息。
默认:`true`。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`errorHandler`](#errorHandler)
(error-handler) |对`ErrorHandler`策略的引用,该策略用于处理在执行 MessageListener 期间可能发生的任何未捕获的异常。
默认:`ConditionalRejectingErrorHandler`|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`exclusive`](#exclusive)
(exclusive) |确定此容器中的单个使用者是否对队列具有独占访问权限。
当这是`true`时,容器的并发性必须为 1,
如果另一个使用者具有独占访问权限,则容器将尝试恢复该使用者,根据`recovery-interval`或`recovery-back-off`.
使用名称空间时,该属性将与队列名称一起出现在``元素上。
默认:`false`。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`exclusiveConsumerExceptionLogger`](#exclusiveConsumerExceptionLogger)
(N/A) |当独占使用者无法访问队列时使用的异常记录器。
默认情况下,这是在`WARN`级别上记录的。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`failedDeclarationRetryInterval`](#failedDeclarationRetryInterval)
(failed-declaration
-retry-interval) |被动队列声明重试尝试之间的间隔。
被动队列声明发生在使用者启动时,或者在从多个队列消费时,当初始化期间不是所有队列都可用时。
默认值:5000(5 秒)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`forceCloseChannel`](#forceCloseChannel)
(N/A) |如果消费者在`shutdownTimeout`内没有对关机做出响应,如果这是`true`,则通道将被关闭,从而导致任何未被 ACKed 的消息被重新请求。
自 2.0 起默认为`true`。
你可以将其设置为`false`以恢复到以前的行为。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`globalQos`](#globalQos)
(global-qos) |当为真时,`prefetchCount`将全局地应用于通道,而不是应用于通道上的每个消费者。
有关更多信息,请参见[`basicQos.global`](https://WWW.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.global)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| (group) |这仅在使用名称空间时可用。
当指定时,类型`Collection`的 Bean 被注册为该名称,并且每个
元素的
容器被添加到集合中。
例如,这允许,通过迭代集合来启动和停止容器组。
如果多个``元素具有相同的组值,集合形式中的容器
是如此指定的所有容器的集合。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`idleEventInterval`](#idleEventInterval)
(idle-event-interval) |见[检测空闲异步消费者](#idle-containers)。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`javaLangErrorHandler`](#javaLangErrorHandler)
(N/A) |当容器线程捕获`Error`时调用的`AbstractMessageListenerContainer.JavaLangErrorHandler`实现。
默认实现调用`System.exit(99)`;要恢复到以前的行为(什么都不做),请添加一个 no-op 处理程序。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`maxConcurrentConsumers`](#maxConcurrentConsumers)
(max-concurrency) |如果需要,按需启动的最大并发消费者数量。
必须大于或等于“concurrentConsumers”。
参见[监听器并发](#listener-concurrency)。|![tickmark](images/tickmark.png)| | | +| []()[`messagesPerAck`](#messagesPerAck)
(N/A) |ACK 之间要接收的消息的数量。使用此方法可以减少发送给代理的 ACK 的数量(以增加重新交付消息的可能性为代价)。通常,你应该仅在大容量的侦听器容器上设置此属性。
如果设置了此属性,并且拒绝了一条消息(抛出了异常),则确认了挂起的 ACK,并且拒绝了失败的消息。
不允许使用已处理的通道。
如果`prefetchCount`小于`messagesPerAck`,它被增加以匹配`messagesPerAck`。
默认值:ACK 每条消息。
参见`ackTimeout`在此表中。| |![tickmark](images/tickmark.png)| | +| []()[`mismatchedQueuesFatal`](#mismatchedQueuesFatal)
(mismatched-queues-fatal) |当容器启动时,如果此属性是`true`(默认值:`false`),则容器将检查上下文中声明的所有队列是否与代理上已经存在的队列兼容。
如果存在不匹配的属性(例如`auto-delete`)或参数(skuch 为`x-message-ttl`),容器(和应用程序上下文)无法以致命的异常启动。

如果在恢复过程中检测到问题(例如,在丢失连接之后),容器被停止。

在应用程序上下文中必须有一个`RabbitAdmin`(或通过使用`rabbitAdmin`属性在容器上专门配置的一个)。
否则,该属性必须是`false`。

| |If the broker is not available during initial startup, the container starts and the conditions are checked when the connection is established.|
|---|----------------------------------------------------------------------------------------------------------------------------------------------|

| |The check is done against all queues in the context, not just the queues that a particular listener is configured to use.
If you wish to limit the checks to just those queues used by a container, you should configure a separate `RabbitAdmin` for the container, and provide a reference to it using the `rabbitAdmin` property.
See [Conditional Declaration](#conditional-declaration) for more information.|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

| |Mismatched queue argument detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`.
This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds.
Applications using lazy listener beans should check the queue arguments before getting a reference to the lazy bean.|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| |如果在初始启动期间代理不可用,则容器将启动,并在建立连接时检查条件。| | | | +| |检查是针对上下文中的所有队列进行的,而不仅仅是特定侦听器被配置使用的队列,
如果你希望将检查仅限于容器使用的那些队列,那么你应该为容器配置一个单独的`RabbitAdmin`,并使用`rabbitAdmin`属性提供对它的引用。
有关更多信息,请参见[有条件声明](#conditional-declaration)。| | | | +| |在 Bean 中为`@RabbitListener`启动容器时,禁用不匹配的队列参数检测这被标记为`@Lazy`。
这是为了避免潜在的死锁,这可能会将此类容器的启动延迟长达 60 秒。
使用 lazy Listener bean 的应用程序应该在获得对 lazy 的引用之前检查队列参数 Bean。| | | | +| []()[`missingQueuesFatal`](#missingQueuesFatal)
(missing-queues-fatal) |当设置为`true`(默认)时,如果代理上没有可用的配置队列,则认为这是致命的,
这会导致应用程序上下文在启动期间无法初始化,
同样,在默认情况下,当容器运行时删除队列时,消费者进行三次重试以连接到队列(每隔五秒),并在尝试失败时停止容器。

这在以前的版本中是不可配置的。

当设置为`false`时,在进行三次重试之后,容器进入恢复模式,与其他问题一样,例如代理被关闭。
容器尝试根据`recoveryInterval`属性进行恢复。
在每次尝试恢复期间,每个使用者再次尝试四次,以五秒的间隔被动地声明队列。
此过程将无限期地继续。

你还可以使用一个属性 Bean 来为所有容器全局地设置属性,如下:

```
id="spring.amqp.global.properties">

false


```

此全局属性不应用于具有显式`missingQueuesFatal`属性集的任何容器。

默认的重试属性(五秒间隔三次重试)可以通过设置下面的属性被重写。
| |Missing queue detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`.
This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds.
Applications using lazy listener beans should check the queue(s) before getting a reference to the lazy bean.|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| |在 Bean 中为`@RabbitListener`启动容器时,将禁用丢失的队列检测这被标记为`@Lazy`。
这是为了避免潜在的死锁,这可能会将此类容器的启动延迟长达 60 秒。
使用 Lazy Listener Bean 的应用程序应该在获得对 Lazy 的引用之前检查队列 Bean。| | | | +| []()[`monitorInterval`](#monitorInterval)
(monitor-interval) |对于 DMLC,计划在此间隔时间运行一个任务,以监视使用者的状态并恢复任何失败的任务。| |![tickmark](images/tickmark.png)| | +| []()[`noLocal`](#noLocal)
(N/A) |设置为`true`,以禁用在同一通道的连接上发布的从服务器到消费者的消息传递。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`phase`](#phase)
(phase) |当`autoStartup`是`true`时,此容器应在其中开始和停止的生命周期阶段。
值越低,此容器开始的时间越早,停止的时间越晚。
默认值是`Integer.MAX_VALUE`,这意味着容器开始得越晚,停止得越快。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +|[]()[`possibleAuthenticationFailureFatal`](#possibleAuthenticationFailureFatal)
(possible-authentication-failure-fatal)|当设置为`true`(SMLC 的默认值)时,如果在连接过程中抛出了`PossibleAuthenticationFailureException`,它被认为是致命的。
这会导致应用程序上下文在启动期间无法初始化(如果容器配置为自动启动)。

自*2.0 版本*

**directmessageListenerContainer****

(默认),当设置为`false`时,每个使用者将尝试根据`monitorInterval`重新连接。

**SimpleMessageListenerContainer**

当设置为`false`时,在进行 3 次重试之后,容器将进入恢复模式,与其他问题一样,例如代理正在关闭。
容器将根据`recoveryInterval`属性尝试恢复。
在每次尝试恢复期间,每个使用者将再次尝试 4 次以启动。
此过程将无限期地继续。

你还可以使用一个属性 Bean 来全局设置所有容器的属性,如下所示:

```
id="spring.amqp.global.properties">
key="mlc.possible.authentication.failure.fatal">
false


```
此全局属性将不会应用于任何具有显式`missingQueuesFatal`属性集的容器。
默认的重试属性(5 秒间隔 3 次重试)可以在此属性之后使用属性重写。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`prefetchCount`](#prefetchCount)
(prefetch) |在每个消费者处可能未完成的未确认消息的数量。
该值越高,消息可以交付得越快,但非顺序处理的风险越高。
如果`acknowledgeMode`是`NONE`,则忽略不计。
如果有必要,要匹配`batchSize`或`messagePerAck`。
自 2.0 起默认值为 250。
你可以将其设置为 1 以恢复到以前的行为。

| |There are scenarios where the prefetch value should
be low — for example, with large messages, especially if the processing is slow (messages could add up
to a large amount of memory in the client process), and if strict message ordering is necessary
(the prefetch value should be set back to 1 in this case).
Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers.|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Also see `globalQos`. |![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| |在某些情况下,预取取值应该
较低,例如,对于较大的消息,尤其是在处理速度较慢的情况下(消息可能会将
累加到客户端进程中的大量内存中),如果需要严格的消息排序
(在这种情况下,预取值应该设置为 1),
还可以使用低容量消息传递和多个消费者(包括单个侦听器容器实例中的并发性),你可能希望减少预取,以使消息在消费者之间的分布更加均匀。| | | | +| []()[`rabbitAdmin`](#rabbitAdmin)
(admin) |当侦听器容器侦听至少一个自动删除队列时,发现该队列在启动过程中丢失,则该容器使用`RabbitAdmin`声明队列以及任何相关的绑定和交换,
如果这些元素被配置为使用条件声明(参见[有条件声明](#conditional-declaration)),容器必须使用配置来声明这些元素的管理。
在这里指定该管理。
只有在使用带有条件声明的自动删除队列时才需要。
如果你不希望在容器启动之前声明自动删除队列,在管理员上将`auto-startup`设置为`false`。
默认设置为一个`RabbitAdmin`,该命令声明所有无条件元素。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`receiveTimeout`](#receiveTimeout)
(receive-timeout) |等待每条消息的最长时间。
如果`acknowledgeMode=NONE`,这几乎没有影响——容器旋转并请求另一条消息,
对于带有`Channel`的事务性`batchSize > 1`,它的影响最大,因为它可能导致在超时到期之前不确认已经使用的消息。
当`consumerBatchEnabled`为真时,如果在批处理完成之前发生超时,则将交付部分批处理。|![tickmark](images/tickmark.png)| | | +| []()[`recoveryBackOff`](#recoveryBackOff)
(recovery-back-off) |指定`BackOff`如果使用者由于非致命原因而无法启动,则尝试启动使用者之间的间隔。
默认值为`FixedBackOff`,每五秒进行无限次重试。
与`recoveryInterval`互斥。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`recoveryInterval`](#recoveryInterval)
(recovery-interval) |如果使用者由于非致命原因而无法启动,则确定尝试启动使用者之间的时间(以毫秒为单位)。
默认值:5000。
与`recoveryBackOff`互斥。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`retryDeclarationInterval`](#retryDeclarationInterval)
(missing-queue-
retry-interval) |如果在使用者初始化期间,配置队列的一个子集可用,则使用者开始从这些队列消费。
使用者试图通过使用此间隔被动地声明丢失的队列。
当此间隔过后,将再次使用“declarnationRetries”和“faileddeclarationRetrygt Interval”。,如果仍然存在丢失的队列,
,消费者在再次尝试之前再次等待这个间隔。
此过程将无限期地继续,直到所有队列都可用。
默认值:60000(一分钟)。|![tickmark](images/tickmark.png)| | | +| []()[`shutdownTimeout`](#shutdownTimeout)
(N/A) |当容器关闭时(例如,
如果其封闭的`ApplicationContext`已关闭),它将等待飞行中的消息被处理到这个限制。
默认为 5 秒。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`startConsumerMinInterval`](#startConsumerMinInterval)
(min-start-interval) |在按需启动每个新用户之前必须经过的毫秒时间。
参见[监听器并发](#listener-concurrency)。
默认值:10000(10 秒)。|![tickmark](images/tickmark.png)| | | +| []()[`statefulRetryFatal`](#statefulRetryFatal)
WithNullMessageId
(N/A) |当使用有状态重试建议时,如果接收到缺少`messageId`属性的消息,则默认情况下认为
对消费者是致命的(它被停止)。
将此设置为`false`以丢弃(或路由到死信队列)此类消息。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`stopConsumerMinInterval`](#stopConsumerMinInterval)
(min-stop-interval) |当检测到空闲消费者时,从最后一个消费者停止到消费者停止之前必须经过的毫秒时间。
参见[监听器并发](#listener-concurrency)。
默认值:60000(一分钟)。|![tickmark](images/tickmark.png)| | | +| []()[`streamConverter`](#streamConverter)
(N/A) |将本机流消息转换为 Spring AMQP 消息的`StreamMessageConverter`。| | |![tickmark](images/tickmark.png)| +| []()[`taskExecutor`](#taskExecutor)
(task-executor) |对用于执行侦听器调用程序的 Spring `TaskExecutor`(或标准 JDK1.5+`Executor`)的引用。
默认值是`SimpleAsyncTaskExecutor`,使用内部管理线程。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | +| []()[`taskScheduler`](#taskScheduler)
(task-scheduler) |在 DMLC 中,调度程序用来在“monitorinterval”运行监视任务。| |![tickmark](images/tickmark.png)| | +| []()[`transactionManager`](#transactionManager)
(transaction-manager) |外部事务管理器用于侦听器的操作。
还补充了`channelTransacted`—如果`Channel`被处理,则其事务与外部事务同步。|![tickmark](images/tickmark.png)|![tickmark](images/tickmark.png)| | + +#### [](#listener-concurrency)4.1.18。监听器并发 + +##### [](#simplemessagelistenercontainer)SimpleMessageListenerContainer + +默认情况下,侦听器容器启动一个从队列接收消息的使用者。 + +在检查上一节中的表时,你可以看到许多控制并发性的属性和属性。最简单的是`concurrentConsumers`,它创建了并发处理消息的(固定的)消费者数量。 + +在 1.3.0 版本之前,这是唯一可用的设置,容器必须停止并重新启动才能更改设置。 + +自版本 1.3.0 以来,你现在可以动态调整`concurrentConsumers`属性。如果在容器运行时对其进行了更改,则会根据需要添加或删除消费者,以适应新的设置。 + +此外,还添加了一个名为`maxConcurrentConsumers`的新属性,并且容器根据工作负载动态地调整并发。这与四个附加属性一起工作:`consecutiveActiveTrigger`、`startConsumerMinInterval`、`consecutiveIdleTrigger`和`stopConsumerMinInterval`。在默认设置下,增加消费者的算法工作如下: + +如果`maxConcurrentConsumers`尚未到达,并且一个现有的使用者连续十个周期处于活动状态,并且自上一个使用者启动以来至少已经过了 10 秒,则启动一个新的使用者。如果使用者在`batchSize`\*`receiveTimeout`毫秒内至少接收到一条消息,则被认为是活动的。 + +在默认设置下,减少消费者的算法工作如下: + +如果有超过`concurrentConsumers`的运行并且一个消费者检测到连续十个超时(空闲)并且最后一个消费者在至少 60 秒前被停止,则消费者被停止。超时取决于`receiveTimeout`和`batchSize`属性。如果使用者在`batchSize`\*`receiveTimeout`毫秒内没有收到消息,则被认为是空闲的。因此,使用默认的超时(一秒)和`batchSize`的四个超时,在 40 秒的空闲时间(四个超时对应一个空闲检测)后考虑停止消费者。 + +| |实际上,只有当整个容器闲置一段时间时,才可以停止使用消费者。
这是因为代理在所有活动消费者之间共享其工作。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +无论配置队列的数量如何,每个使用者都使用一个通道。 + +从版本 2.0 开始,`concurrentConsumers`和`maxConcurrentConsumers`属性可以设置为`concurrency`属性——例如,`2-4`。 + +##### [](#using-directmessagelistenercontainer)使用`DirectMessageListenerContainer` + +在这个容器中,并发是基于配置的队列和`consumersPerQueue`。每个队列的每个使用者都使用一个单独的通道,并且并发性由 Rabbit 客户端库控制。默认情况下,在编写时,它使用`DEFAULT_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 2`线程池。 + +你可以配置`taskExecutor`以提供所需的最大并发性。 + +#### [](#exclusive-consumer)4.1.19。独家消费者 + +从版本 1.3 开始,你可以使用一个独占使用者来配置侦听器容器。这可以防止其他容器从队列中消费,直到当前使用者被取消为止。这样的容器的并发性必须是`1`。 + +当使用独占消费者时,其他容器尝试根据`recoveryInterval`属性从队列中消费,如果尝试失败,则记录`WARN`消息。 + +#### [](#listener-queues)4.1.20。监听器容器队列 + +版本 1.3 为处理侦听器容器中的多个队列引入了许多改进。 + +容器必须被配置为至少监听一个队列。以前也是这样,但现在可以在运行时添加和删除队列。当处理了任何预取的消息时,容器回收(取消和重新创建)消费者。对于`addQueues`、`addQueueNames`、`removeQueues`和`removeQueueNames`方法,请参见[Javadoc](https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html)。在删除队列时,必须保留至少一个队列。 + +现在,如果消费者的队列中有任何一个可用,他就会启动。以前,如果有任何队列不可用,容器就会停止。现在,只有在没有队列可用的情况下才会出现这种情况。如果不是所有的队列都是可用的,那么容器将尝试每隔 60 秒被动地声明(并消耗)丢失的队列。 + +此外,如果使用者从代理接收到取消(例如,如果队列被删除),则使用者将尝试恢复,并且恢复的使用者将继续处理来自任何其他配置队列的消息。以前,一个队列上的取消会取消整个消费者,最终,由于缺少队列,容器将停止。 + +如果希望永久删除队列,则应在删除到队列之前或之后更新容器,以避免将来尝试使用它。 + +#### [](#resilience-recovering-from-errors-and-broker-failures)4.1.21。弹性:从错误和代理失败中恢复 + +Spring AMQP 提供的一些关键(也是最流行的)高级特性与在协议错误或代理失败的情况下的恢复和自动重新连接有关。我们已经在本指南中看到了所有相关的组件,但是在这里将它们集合在一起并单独列出特性和恢复场景应该会有所帮助。 + +主要的重新连接功能由`CachingConnectionFactory`本身启用。使用`RabbitAdmin`自动声明功能通常也是有益的。此外,如果你关心保证的交付,你可能还需要在`RabbitTemplate`和`SimpleMessageListenerContainer`中使用`channelTransacted`标志,在`AcknowledgeMode.AUTO`中使用`AcknowledgeMode.AUTO`(如果你自己进行 ACK,则使用手动)标志。 + +##### [](#automatic-declaration)交换、队列和绑定的自动声明 + +`RabbitAdmin`组件可以在启动时声明交换、队列和绑定。它通过`ConnectionListener`懒洋洋地做到了这一点。因此,如果代理在启动时不存在,这并不重要。第一次使用`Connection`(例如,通过发送消息)时,侦听器将触发并应用管理功能。在侦听器中执行自动声明的另一个好处是,如果由于任何原因(例如,代理死亡,网络故障和其他原因)而丢失连接,则在重新建立连接时再次应用这些声明。 + +| |以这种方式声明的队列必须具有固定的名称——要么是显式声明的,要么是由`AnonymousQueue`实例的框架生成的。
匿名队列是不可持久的、排他的和自动删除的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |只有当`CachingConnectionFactory`缓存模式为`CHANNEL`(默认)时,才会执行自动声明。
存在此限制,因为独占和自动删除队列绑定到连接。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 2.2.2 开始,`RabbitAdmin`将检测类型为`DeclarableCustomizer`的 bean,并在实际处理声明之前应用该函数。这是有用的,例如,设置一个新的参数(属性)之前,它在框架内有第一类支持。 + +``` +@Bean +public DeclarableCustomizer customizer() { + return dec -> { + if (dec instanceof Queue && ((Queue) dec).getName().equals("my.queue")) { + dec.addArgument("some.new.queue.argument", true); + } + return dec; + }; +} +``` + +对于不提供直接访问`Declarable` Bean 定义的项目,它也很有用。 + +另见[RabbitMQ 自动连接/拓扑恢复](#auto-recovery)。 + +##### [](#retry)同步操作失败和重试选项 + +如果在使用`RabbitTemplate`(例如)时,在同步序列中丢失了与代理的连接, Spring AMQP 将抛出一个`AmqpException`(通常但并非总是`AmqpIOException`)。我们不会试图掩盖存在问题的事实,因此你必须能够捕捉并响应异常。如果你怀疑连接丢失(而且这不是你的错误),最简单的方法是再次尝试该操作。你可以手动完成此操作,也可以使用 Spring Retry 来处理重试(强制地或声明地)。 + +Spring 重试提供了两个 AOP 拦截器和很大的灵活性来指定重试的参数(尝试次数、异常类型、退避算法和其他)。 Spring AMQP 还提供了一些方便的工厂 bean,用于为 AMQP 用例以方便的形式创建 Spring 重试拦截器,其具有可用于实现自定义恢复逻辑的强类型回调接口。有关更多详细信息,请参见`StatefulRetryOperationsInterceptor`和`StatelessRetryOperationsInterceptor`的 Javadoc 和属性。如果没有事务或者在重试回调中启动了事务,则无状态重试是合适的。请注意,无状态重试比有状态重试更容易配置和分析,但是如果有一个正在进行的事务必须回滚或者肯定要回滚,那么它通常是不合适的。事务中间的断开连接应该具有与回滚相同的效果。因此,对于在堆栈更高的位置启动事务的重新连接,有状态重试通常是最佳选择。有状态重试需要一种机制来唯一地标识消息。最简单的方法是让发送方在`MessageId`消息属性中放置一个唯一的值。所提供的消息转换器提供了这样做的选项:你可以将`createMessageIds`设置为`true`。否则,可以将`MessageKeyGenerator`实现注入拦截器。密钥生成器必须为每条消息返回唯一的密钥。在版本 2.0 之前的版本中,提供了`MissingMessageIdAdvice`。它允许不带`messageId`属性的消息只重试一次(忽略重试设置)。不再提供此建议,因为与`spring-retry`版本 1.2 一起,其功能内置在拦截器和消息侦听器容器中。 + +| |对于向后兼容性,缺省情况下(在一次重试之后),带有空消息 ID 的消息被认为对使用者是致命的(使用者被停止),
以复制`MissingMessageIdAdvice`提供的功能,你可以在侦听器容器上将`statefulRetryFatalWithNullMessageId`属性设置为`false`。
通过该设置,使用者将继续运行并拒绝消息(在一次重试之后)。
它将被丢弃或路由到死信队列(如果配置了死信队列)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +从版本 1.3 开始,提供了一个 Builder API,以通过使用 Java(在`@Configuration`类中)来帮助组装这些拦截器。下面的示例展示了如何做到这一点: + +``` +@Bean +public StatefulRetryOperationsInterceptor interceptor() { + return RetryInterceptorBuilder.stateful() + .maxAttempts(5) + .backOffOptions(1000, 2.0, 10000) // initialInterval, multiplier, maxInterval + .build(); +} +``` + +只能以这种方式配置重试功能的一个子集。更高级的功能将需要将`RetryTemplate`配置为 Spring Bean。有关可用策略及其配置的完整信息,请参见[Spring Retry Javadoc](https://docs.spring.io/spring-retry/docs/api/current/)。 + +##### [](#batch-retry)使用批处理侦听器重试 + +不建议使用批处理侦听器配置重试,除非批处理是由生成器在单个记录中创建的。有关消费者和生产者创建的批的信息,请参见[批处理消息](#de-batching)。对于消费者创建的批处理,框架不知道批处理中的哪条消息导致了故障,因此不可能在重试结束后进行恢复。使用生产者创建的批处理,由于只有一条消息实际失败,因此可以恢复整个消息。应用程序可能想要通知自定义恢复程序在批处理中发生故障的位置,可能是通过设置抛出异常的索引属性。 + +批处理侦听器的重试恢复程序必须实现`MessageBatchRecoverer`。 + +##### [](#async-listeners)消息侦听器和异步情况 + +如果`MessageListener`由于业务异常而失败,则异常由消息侦听器容器处理,然后返回到侦听另一条消息。如果故障是由已删除的连接(而不是业务异常)引起的,则必须取消并重新启动为侦听器收集消息的使用者。`SimpleMessageListenerContainer`无缝地处理此问题,并留下一个日志来表示侦听器正在重新启动。事实上,它无休止地循环,试图重新启动消费者。只有当消费者表现得非常糟糕时,它才会放弃。一个副作用是,如果代理在容器启动时关闭,它会一直尝试,直到可以建立连接为止。 + +与协议错误和断开的连接相反,业务异常处理可能需要更多的考虑和一些自定义配置,尤其是在使用事务或容器 ACK 的情况下。在 2.8.x 之前,RabbitMQ 没有对死信行为的定义。因此,在默认情况下,由于业务异常而被拒绝或回滚的消息可以无休止地重新交付。要限制客户机的再交付次数,一个选择是侦听器的建议链中的`StatefulRetryOperationsInterceptor`。拦截器可以有一个实现自定义死信操作的恢复回调——任何适合你的特定环境的操作。 + +另一种选择是将容器的`defaultRequeueRejected`属性设置为`false`。这将导致丢弃所有失败的消息。当使用 RabbitMQ2.8.x 或更高版本时,这也有利于将消息传递给死信交换。 + +或者,你可以抛出`AmqpRejectAndDontRequeueException`。无论`defaultRequeueRejected`属性的设置如何,这样做都可以防止消息请求。 + +从版本 2.1 开始,引入了一个`ImmediateRequeueAmqpException`来执行完全相反的逻辑:无论`defaultRequeueRejected`属性的设置如何,消息都将被重新请求。 + +通常,这两种技术的组合被使用。你可以在建议链中使用`StatefulRetryOperationsInterceptor`,并使用`MessageRecoverer`抛出`AmqpRejectAndDontRequeueException`。当所有重试都已用尽时,将调用`MessageRecover`。`RejectAndDontRequeueRecoverer`就是这么做的。默认的`MessageRecoverer`消耗错误消息并发出`WARN`消息。 + +从版本 1.3 开始,提供了一个新的`RepublishMessageRecoverer`,允许在重试结束后发布失败的消息。 + +当回收者使用最后一个异常时,该消息将被 ACK’d,并且不会被发送到死信交换(如果有的话)。 + +| |当`RepublishMessageRecoverer`在消费者侧使用时,接收到的消息在`receivedDeliveryMode`消息属性中具有`deliveryMode`。
在这种情况下,`deliveryMode`是`null`。
这意味着在代理上具有`NON_PERSISTENT`交付模式,
从版本 2.0 开始,你可以将`RepublishMessageRecoverer`的`deliveryMode`配置为设置到消息中以重新发布,如果它是`null`。
默认情况下,它使用`MessageProperties`默认值-`MessageDeliveryMode.PERSISTENT`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何将`RepublishMessageRecoverer`设置为回收器: + +``` +@Bean +RetryOperationsInterceptor interceptor() { + return RetryInterceptorBuilder.stateless() + .maxAttempts(5) + .recoverer(new RepublishMessageRecoverer(amqpTemplate(), "something", "somethingelse")) + .build(); +} +``` + +`RepublishMessageRecoverer`在消息头中发布带有附加信息的消息,例如异常消息、堆栈跟踪、原始交换和路由密钥。可以通过创建一个子类并覆盖`additionalHeaders()`来添加额外的标题。`deliveryMode`(或任何其他属性)也可以在`additionalHeaders()`中进行更改,如下例所示: + +``` +RepublishMessageRecoverer recoverer = new RepublishMessageRecoverer(amqpTemplate, "error") { + + protected Map additionalHeaders(Message message, Throwable cause) { + message.getMessageProperties() + .setDeliveryMode(message.getMessageProperties().getReceivedDeliveryMode()); + return null; + } + +}; +``` + +从版本 2.0.5 开始,如果堆栈跟踪太大,它可能会被截断;这是因为所有的标头都必须适合于单个框架。默认情况下,如果堆栈跟踪将导致其他头的可用字节数少于 20,000(“headroom”),那么它将被截断。如果你需要更多或更少的空间来放置其他头文件,可以通过设置 recoverer 的`frameMaxHeadroom`属性来对此进行调整。从版本 2.1.13、2.2.3 开始,异常消息将包含在此计算中,并且使用以下算法将堆栈跟踪的数量最大化: + +* 如果单独的堆栈跟踪将超过限制,则异常消息头将被截断为 97 字节加上`…​`,并且堆栈跟踪也将被截断。 + +* 如果堆栈跟踪很小,消息将被截断(加上`…​`)以适应可用字节(但堆栈跟踪本身中的消息被截断为 97 字节加上`…​`)。 + +每当发生任何类型的截断时,将记录原始异常以保留完整的信息。 + +从版本 2.3.3 开始,提供了一个新的子类`RepublishMessageRecovererWithConfirms`;这支持两种类型的 Publisher 确认,并将在返回之前等待确认(或者如果未确认或消息返回,则抛出异常)。 + +如果确认类型是`CORRELATED`,则子类还将检测是否返回了消息并抛出`AmqpMessageReturnedException`;如果发布是否定的,则将抛出`AmqpNackReceivedException`。 + +如果确认类型是`SIMPLE`,则子类将调用通道上的`waitForConfirmsOrDie`方法。 + +有关确认和返回的更多信息,请参见[发布者确认并返回](#cf-pub-conf-ret)。 + +从版本 2.1 开始,将添加`ImmediateRequeueMessageRecoverer`以抛出`ImmediateRequeueAmqpException`,该命令通知侦听器容器重新请求当前失败的消息。 + +##### Spring 重试的[](#exception-classification-for-spring-retry)异常分类 + +Spring 重试在确定哪些异常可以调用重试方面具有很大的灵活性。对于所有异常,默认配置都会重试。考虑到用户异常包装在`ListenerExecutionFailedException`中,我们需要确保分类检查异常原因。默认分类器只查看顶层异常。 + +由于 Spring 重试 1.0.3,`BinaryExceptionClassifier`具有一个名为`traverseCauses`的属性(默认:`false`)。当`true`时,它遍历异常原因,直到找到匹配的原因或没有原因为止。 + +要使用此分类器进行重试,你可以使用一个`SimpleRetryPolicy`,该构造函数创建了最大尝试次数,`Exception`实例的`Map`和布尔(`traverseCauses`),并将此策略注入`RetryTemplate`。 + +#### [](#multi-rabbit)4.1.22。多个代理(或集群)支持 + +在单个应用程序与多个代理或代理集群之间进行通信时,版本 2.3 增加了更多的便利。在消费者方面,主要的好处是基础设施可以自动将自动声明的队列与适当的代理关联起来。 + +用一个例子最好地说明了这一点: + +``` +@SpringBootApplication(exclude = RabbitAutoConfiguration.class) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + CachingConnectionFactory cf1() { + return new CachingConnectionFactory("localhost"); + } + + @Bean + CachingConnectionFactory cf2() { + return new CachingConnectionFactory("otherHost"); + } + + @Bean + CachingConnectionFactory cf3() { + return new CachingConnectionFactory("thirdHost"); + } + + @Bean + SimpleRoutingConnectionFactory rcf(CachingConnectionFactory cf1, + CachingConnectionFactory cf2, CachingConnectionFactory cf3) { + + SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory(); + rcf.setDefaultTargetConnectionFactory(cf1); + rcf.setTargetConnectionFactories(Map.of("one", cf1, "two", cf2, "three", cf3)); + return rcf; + } + + @Bean("factory1-admin") + RabbitAdmin admin1(CachingConnectionFactory cf1) { + return new RabbitAdmin(cf1); + } + + @Bean("factory2-admin") + RabbitAdmin admin2(CachingConnectionFactory cf2) { + return new RabbitAdmin(cf2); + } + + @Bean("factory3-admin") + RabbitAdmin admin3(CachingConnectionFactory cf3) { + return new RabbitAdmin(cf3); + } + + @Bean + public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() { + return new RabbitListenerEndpointRegistry(); + } + + @Bean + public RabbitListenerAnnotationBeanPostProcessor postProcessor(RabbitListenerEndpointRegistry registry) { + MultiRabbitListenerAnnotationBeanPostProcessor postProcessor + = new MultiRabbitListenerAnnotationBeanPostProcessor(); + postProcessor.setEndpointRegistry(registry); + postProcessor.setContainerFactoryBeanName("defaultContainerFactory"); + return postProcessor; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory1(CachingConnectionFactory cf1) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf1); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory2(CachingConnectionFactory cf2) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf2); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory3(CachingConnectionFactory cf3) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf3); + return factory; + } + + @Bean + RabbitTemplate template(RoutingConnectionFactory rcf) { + return new RabbitTemplate(rcf); + } + + @Bean + ConnectionFactoryContextWrapper wrapper(SimpleRoutingConnectionFactory rcf) { + return new ConnectionFactoryContextWrapper(rcf); + } + +} + +@Component +class Listeners { + + @RabbitListener(queuesToDeclare = @Queue("q1"), containerFactory = "factory1") + public void listen1(String in) { + + } + + @RabbitListener(queuesToDeclare = @Queue("q2"), containerFactory = "factory2") + public void listen2(String in) { + + } + + @RabbitListener(queuesToDeclare = @Queue("q3"), containerFactory = "factory3") + public void listen3(String in) { + + } + +} +``` + +正如你所看到的,我们已经声明了 3 组基础设施(连接工厂、管理员、容器工厂)。如前所述,`@RabbitListener`可以定义使用哪个容器工厂;在这种情况下,它们还使用`queuesToDeclare`,如果队列不存在,则该队列将在代理上声明。通过使用约定`-admin`命名`RabbitAdmin`bean,基础结构能够确定哪个管理员应该声明队列。这也将与`bindings = @QueueBinding(…​)`一起工作,其中交换和绑定也将被声明。它将不能与`queues`一起工作,因为这期望队列已经存在。 + +在生产者方面,提供了一个方便的`ConnectionFactoryContextWrapper`类,以使使用`RoutingConnectionFactory`(参见[路由连接工厂](#routing-connection-factory))变得更简单。 + +正如你在上面看到的,一个`SimpleRoutingConnectionFactory` Bean 已经添加了路由密钥`one`,`two`和`three`。还有一个`RabbitTemplate`使用了那个工厂。下面是一个使用该模板和包装器路由到代理集群之一的示例。 + +``` +@Bean +public ApplicationRunner runner(RabbitTemplate template, ConnectionFactoryContextWrapper wrapper) { + return args -> { + wrapper.run("one", () -> template.convertAndSend("q1", "toCluster1")); + wrapper.run("two", () -> template.convertAndSend("q2", "toCluster2")); + wrapper.run("three", () -> template.convertAndSend("q3", "toCluster3")); + }; +} +``` + +#### [](#debugging)4.1.23。调试 + +Spring AMQP 提供了广泛的日志记录,特别是在`DEBUG`级别。 + +如果希望监视应用程序和代理之间的 AMQP 协议,则可以使用 Wireshark 之类的工具,该工具具有一个插件来解码该协议。或者,RabbitMQ Java 客户机提供了一个非常有用的类`Tracer`。当作为`main`运行时,默认情况下,它会监听端口 5673 并连接到 LocalHost 上的端口 5672。你可以运行它并更改连接工厂配置以连接到 LocalHost 上的端口 5673。它在控制台上显示已解码的协议。有关更多信息,请参见`Tracer`Javadoc。 + +### [](#stream-support)4.2。使用 RabbitMQ 流插件 + +版本 2.4 为[RabbitMQ 流插件](https://rabbitmq.com/stream.html)引入了对[RabbitMQ 流插件 Java 客户端](https://github.com/rabbitmq/rabbitmq-stream-java-client)的初始支持。 + +* `RabbitStreamTemplate` + +* `StreamListenerContainer` + +#### [](#sending-messages-2)4.2.1。发送消息 + +`RabbitStreamTemplate`提供了`RabbitTemplate`功能的一个子集。 + +例 1。RabbitStreamOperations + +``` +public interface RabbitStreamOperations extends AutoCloseable { + + ListenableFuture send(Message message); + + ListenableFuture convertAndSend(Object message); + + ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + + ListenableFuture send(com.rabbitmq.stream.Message message); + + MessageBuilder messageBuilder(); + + MessageConverter messageConverter(); + + StreamMessageConverter streamMessageConverter(); + + @Override + void close() throws AmqpException; + +} +``` + +`RabbitStreamTemplate`实现具有以下构造函数和属性: + +例 2。RabbitStreamTemplate + +``` +public RabbitStreamTemplate(Environment environment, String streamName) { +} + +public void setMessageConverter(MessageConverter messageConverter) { +} + +public void setStreamConverter(StreamMessageConverter streamConverter) { +} + +public synchronized void setProducerCustomizer(ProducerCustomizer producerCustomizer) { +} +``` + +`MessageConverter`在`convertAndSend`方法中用于将对象转换为 Spring amqp`Message`。 + +`StreamMessageConverter`用于将 Spring AMQP`Message`转换为本机流`Message`。 + +你还可以直接发送本机流`Message`s;使用`messageBuilder()`方法证明对`Producer`的消息生成器的访问。 + +`ProducerCustomizer`提供了一种机制,可以在生成生产者之前对其进行定制。 + +请参阅关于自定义[Java 客户端文档](https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/)和`Producer`的[Java 客户端文档](https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/)。 + +#### [](#receiving-messages-2)4.2.2。接收消息 + +异步消息接收由`StreamListenerContainer`提供(当使用`@RabbitListener`时,`StreamRabbitListenerContainerFactory`)。 + +侦听器容器需要`Environment`以及一个流名。 + +你可以使用经典的`MessageListener`接收 Spring AMQP`Message`s,也可以使用新的接口接收本机流`Message`s: + +``` +public interface StreamMessageListener extends MessageListener { + + void onStreamMessage(Message message, Context context); + +} +``` + +有关受支持的属性的信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +与模板类似,容器具有`ConsumerCustomizer`属性。 + +请参阅关于自定义[Java 客户端文档](https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/)和`Consumer`的[Java 客户端文档](https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/)。 + +当使用`@RabbitListener`时,配置一个`StreamRabbitListenerContainerFactory`;此时,大多数`@RabbitListener`属性(`concurrency`等)被忽略。只支持`id`、`queues`、`autoStartup`和`containerFactory`。此外,`queues`只能包含一个流名。 + +#### [](#examples)4.2.3。例子 + +``` +@Bean +RabbitStreamTemplate streamTemplate(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "test.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + return template; +} + +@Bean +RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { + return new StreamRabbitListenerContainerFactory(env); +} + +@RabbitListener(queues = "test.stream.queue1") +void listen(String in) { + ... +} + +@Bean +RabbitListenerContainerFactory nativeFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name("myConsumer") + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; +} + +@RabbitListener(id = "test", queues = "test.stream.queue2", containerFactory = "nativeFactory") +void nativeMsg(Message in, Context context) { + ... + context.storeOffset(); +} +``` + +### [](#logging)4.3。日志记录子系统 AMQP 附录 + +该框架为一些流行的日志记录子系统提供了日志附录: + +* 注销(自 Spring AMQP 版本 1.4 起) + +* log4j2(自 Spring AMQP 版本 1.6 起) + +通过使用日志记录子系统的常规机制来配置附录,可用的属性在下面的部分中指定。 + +#### [](#common-properties)4.3.1。共同属性 + +以下属性可与所有附录一起使用: + +| Property | Default |说明| +|-------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ```
exchangeName
``` | ```
logs
``` |要向其发布日志事件的交易所的名称。| +| ```
exchangeType
``` | ```
topic
``` |将日志事件发布到其中的交换类型—仅当 Appender 声明交换时才需要。
请参见`declareExchange`。| +| ```
routingKeyPattern
``` | ```
%c.%p
``` |用于生成路由密钥的记录子系统模式格式.| +| ```
applicationId
``` | ```

``` |应用程序 ID——如果模式包含`%X{applicationId}`,则将其添加到路由键中。| +| ```
senderPoolSize
``` | ```
2
``` |用于发布日志事件的线程数。| +| ```
maxSenderRetries
``` | ```
30
``` |如果代理不可用或存在其他错误,则重试发送消息的次数。
重试延迟如下:`N ^ log(N)`,其中`N`是重试编号。| +| ```
addresses
``` | ```

``` |以下列形式的以逗号分隔的代理地址列表:`host:port[,host:port]*`-覆盖`host`和`port`。| +| ```
host
``` | ```
localhost
``` |连接到哪个主机的 RabbitMQ。| +| ```
port
``` | ```
5672
``` |连接到的 RabbitMQ 端口。| +| ```
virtualHost
``` | ```
/
``` |连接到的 RabbitMQ 虚拟主机。| +| ```
username
``` | ```
guest
``` |RabbitMQ 用户连接时使用.| +| ```
password
``` | ```
guest
``` |此用户的 RabbitMQ 密码。| +| ```
useSsl
``` | ```
false
``` |是否将 SSL 用于 RabbitMQ 连接。
参见[`RabbitConnectionFactoryBean`并配置 SSL]| +| ```
verifyHostname
``` | ```
true
``` |启用 TLS 连接的服务器主机名验证。
参见[`RabbitConnectionFactoryBean`并配置 SSL]| +| ```
sslAlgorithm
``` | ```
null
``` |使用的 SSL 算法。| +| ```
sslPropertiesLocation
``` | ```
null
``` |SSL 属性文件的位置。| +| ```
keyStore
``` | ```
null
``` |密钥存储库的位置。| +| ```
keyStorePassphrase
``` | ```
null
``` |密钥库的密码。| +| ```
keyStoreType
``` | ```
JKS
``` |keystore 类型。| +| ```
trustStore
``` | ```
null
``` |信任库的位置。| +| ```
trustStorePassphrase
``` | ```
null
``` |信任存储库的密码。| +| ```
trustStoreType
``` | ```
JKS
``` |信任库类型。| +| ```
saslConfig
``` |```
null (RabbitMQ client default applies)
```|`saslConfig`-关于有效值,请参见`RabbitUtils.stringToSaslConfig`的 Javadoc。| +| ```
ContentType
``` | ```
text/plain
``` |日志消息的`content-type`属性。| +| ```
contentEncoding
``` | ```

``` |`content-encoding`日志消息的属性。| +| ```
declareExchange
``` | ```
false
``` |是否在此附录启动时声明已配置的交换。
另请参见`durable`和`autoDelete`。| +| ```
durable
``` | ```
true
``` |当`declareExchange`为`true`时,持久标志被设置为该值。| +| ```
autoDelete
``` | ```
false
``` |当`declareExchange`为`true`时,自动删除标志被设置为该值。| +| ```
charset
``` | ```
null
``` |将`String`转换为`byte[]`时使用的字符集。
默认:null(使用的是系统默认字符集)。
如果当前平台不支持字符集,我们将退回使用系统字符集。| +| ```
deliveryMode
``` | ```
PERSISTENT
``` |`PERSISTENT`或`NON_PERSISTENT`,以确定 RabbitMQ 是否应该持久化消息。| +| ```
generateId
``` | ```
false
``` |用于确定`messageId`属性是否设置为唯一值。| +|```
clientConnectionProperties
```| ```
null
``` |用于 RabbitMQ 连接的自定义客户端属性的`key:value`对的逗号分隔列表。| +| ```
addMdcAsHeaders
``` | ```
true
``` |在引入此属性之前,MDC 属性总是被添加到 RabbitMQ 消息头中。
它可能会导致大 MDC 的问题,因为 RabbitMQ 对所有头都有有限的缓冲区大小而且这个缓冲区很小。
引入这个属性是为了避免在大 MDC 的情况下出现问题。
默认情况下,这个值设置为`true`,用于向后兼容。
`false`将序列化 MDC 关闭到头中,
请注意,默认情况下,`JsonLayout`将 MDC 添加到消息中。| + +#### [](#log4j-2-appender)4.3.2。log4j2 附录 + +下面的示例展示了如何配置 log4j2Appender: + +``` + + ... + + + +``` + +| |从 1.6.10 和 1.7.3 版本开始,默认情况下,log4j2Appender 将消息发布到调用线程上的 RabbitMQ。
这是因为 log4j2 默认情况下不会创建线程安全事件。
如果代理关闭,则使用`maxSenderRetries`进行重试,重试之间没有延迟。
如果你希望恢复以前在单独的线程上发布消息的行为(`senderPoolSize`),则可以将`async`属性设置为`true`,但是,
,你还需要配置 log4j2 来使用`DefaultLogEventFactory`而不是`ReusableLogEventFactory`。
这样做的一种方法是设置系统属性`-Dlog4j2.enable.threadlocals=false`。
如果你使用异步发布与`ReusableLogEventFactory`,由于相声,事件很有可能被破坏。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#logback-appender)4.3.3。回录附录 + +下面的示例展示了如何配置一个 Logback Appender: + +``` + + + %n ]]> + + foo:5672,bar:5672 + 36 + false + myApplication + %property{applicationId}.%c.%p + true + UTF-8 + false + NON_PERSISTENT + true + false + +``` + +从版本 1.7.1 开始,logback`AmqpAppender`提供了一个`includeCallerData`选项,默认情况下是`false`。提取调用者数据可能会非常昂贵,因为日志事件必须创建一个可丢弃的数据,并对其进行检查以确定调用位置。因此,默认情况下,当事件被添加到事件队列时,不会提取与事件相关的调用方数据。通过将`includeCallerData`属性设置为`true`,可以将 Appender 配置为包括调用方数据。 + +从版本 2.0.0 开始,Logback`AmqpAppender`使用`encoder`选项支持[翻录编码器](https://logback.qos.ch/manual/encoders.html)。`encoder`和`layout`选项是互斥的。 + +#### [](#customizing-the-messages)4.3.4。自定义消息 + +默认情况下,AMQP Appenders 填充以下消息属性: + +* `deliveryMode` + +* contentType + +* `contentEncoding`,如果已配置 + +* `messageId`,如果`generateId`已配置 + +* 日志事件的`timestamp` + +* `appId`,如果配置了 ApplicationID + +此外,它们还用以下值填充标题: + +* 日志事件的`categoryName` + +* 日志事件的级别 + +* `thread`:发生日志事件的线程的名称 + +* 日志事件调用的堆栈跟踪的位置 + +* 所有 MDC 属性的副本(除非`addMdcAsHeaders`被设置为`false`) + +每个附录都可以进行子类,这样你就可以在发布之前修改消息了。下面的示例展示了如何自定义日志消息: + +``` +public class MyEnhancedAppender extends AmqpAppender { + + @Override + public Message postProcessMessageBeforeSend(Message message, Event event) { + message.getMessageProperties().setHeader("foo", "bar"); + return message; + } + +} +``` + +从 2.2.4 开始,log4j2`AmqpAppender`可以使用`@PluginBuilderFactory`进行扩展,也可以使用`AmqpAppender.Builder`进行扩展。 + +``` +@Plugin(name = "MyEnhancedAppender", category = "Core", elementType = "appender", printObject = true) +public class MyEnhancedAppender extends AmqpAppender { + + public MyEnhancedAppender(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue, String foo, String bar) { + super(name, filter, layout, ignoreExceptions, manager, eventQueue); + + @Override + public Message postProcessMessageBeforeSend(Message message, Event event) { + message.getMessageProperties().setHeader("foo", "bar"); + return message; + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + protected static class Builder extends AmqpAppender.Builder { + + @Override + protected AmqpAppender buildInstance(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue) { + return new MyEnhancedAppender(name, filter, layout, ignoreExceptions, manager, eventQueue); + } + } + +} +``` + +#### [](#customizing-the-client-properties)4.3.5。自定义客户端属性 + +你可以通过添加字符串属性或更复杂的属性来添加自定义客户机属性。 + +##### [](#simple-string-properties)简单字符串属性 + +每个 Appender 都支持向 RabbitMQ 连接添加客户端属性。 + +下面的示例展示了如何为回登添加自定义客户机属性: + +``` + + ... + thing1:thing2,cat:hat + ... + +``` + +示例 3.log4j2 + +``` + + ... + + +``` + +这些属性是用逗号分隔的`key:value`对列表。键和值不能包含逗号或冒号。 + +当查看连接时,这些属性会出现在 RabbitMQ 管理 UI 上。 + +##### [](#advanced-technique-for-logback)回传的高级技术 + +你可以对 Logback Appender 进行子类。这样做可以让你在建立连接之前修改客户机连接属性。下面的示例展示了如何做到这一点: + +``` +public class MyEnhancedAppender extends AmqpAppender { + + private String thing1; + + @Override + protected void updateConnectionClientProperties(Map clientProperties) { + clientProperties.put("thing1", this.thing1); + } + + public void setThing1(String thing1) { + this.thing1 = thing1; + } + +} +``` + +然后可以将`thing2`添加到 logback.xml。 + +对于字符串属性(如前面示例中所示的那些),可以使用前面的技术。子类允许添加更丰富的属性(例如添加`Map`或数字属性)。 + +#### [](#providing-a-custom-queue-implementation)4.3.6。提供自定义队列实现 + +`AmqpAppenders`使用`BlockingQueue`将日志事件异步发布到 RabbitMQ。默认情况下,使用`LinkedBlockingQueue`。但是,你可以提供任何类型的自定义`BlockingQueue`实现。 + +下面的示例展示了如何对注销执行此操作: + +``` +public class MyEnhancedAppender extends AmqpAppender { + + @Override + protected BlockingQueue createEventQueue() { + return new ArrayBlockingQueue(); + } + +} +``` + +log4j2Appender 支持使用[`BlockingQueueFactory`](https://logging. Apache.org/log4j/2.x/manual/appenders.html#BlockingQueueFactory),如下例所示: + +``` + + ... + + + + +``` + +### [](#sample-apps)4.4。示例应用程序 + +项目包括两个示例应用程序。第一个是一个简单的“Hello World”示例,演示了同步和异步消息接收。它为获得对基本组件的理解提供了一个很好的起点。第二个示例基于股票交易用例,以演示在现实世界的应用程序中常见的交互类型。在这一章中,我们提供了每个示例的快速演练,以便你能够关注最重要的组件。这些示例都是基于 Maven 的,因此你应该能够将它们直接导入到任何 Maven 可感知的 IDE 中(例如[SpringSource 工具套件](https://www.springsource.org/sts))。 + +#### [](#hello-world-sample)4.4.1。《Hello World》样本 + +“Hello World”示例演示了同步和异步消息接收。你可以将`spring-rabbit-helloworld`示例导入到 IDE 中,然后按照下面的讨论进行操作。 + +##### [](#hello-world-sync)同步示例 + +在`src/main/java`目录中,导航到`org.springframework.amqp.helloworld`包。打开`HelloWorldConfiguration`类,注意它包含类级的`@Configuration`注释,并注意方法级的一些`@Bean`注释。这是 Spring 基于 Java 的配置的一个示例。你可以阅读有关[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-java)的更多信息。 + +下面的清单显示了如何创建连接工厂: + +``` +@Bean +public CachingConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost"); + connectionFactory.setUsername("guest"); + connectionFactory.setPassword("guest"); + return connectionFactory; +} +``` + +配置还包含`RabbitAdmin`的实例,默认情况下,该实例查找类型为 exchange、queue 或 binding 的任何 bean,然后在代理上声明它们。实际上,在`HelloWorldConfiguration`中生成的`helloWorldQueue` Bean 是一个示例,因为它是`Queue`的一个实例。 + +下面的清单显示了`helloWorldQueue` Bean 的定义: + +``` +@Bean +public Queue helloWorldQueue() { + return new Queue(this.helloWorldQueueName); +} +``` + +回顾`rabbitTemplate` Bean 配置,可以看到它的名称`helloWorldQueue`设置为其`queue`属性(用于接收消息)和其`routingKey`属性(用于发送消息)。 + +既然我们已经研究了配置,我们就可以查看实际使用这些组件的代码了。首先,从同一个包中打开`Producer`类。它包含一个`main()`方法,其中创建了 Spring `ApplicationContext`。 + +下面的清单显示了`main`方法: + +``` +public static void main(String[] args) { + ApplicationContext context = + new AnnotationConfigApplicationContext(RabbitConfiguration.class); + AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class); + amqpTemplate.convertAndSend("Hello World"); + System.out.println("Sent: Hello World"); +} +``` + +在前面的示例中,检索`AmqpTemplate` Bean 并用于发送`Message`。由于客户端代码应该尽可能依赖于接口,所以类型是`AmqpTemplate`而不是`RabbitTemplate`。尽管在`HelloWorldConfiguration`中创建的 Bean 是`RabbitTemplate`的一个实例,但依赖于接口意味着此代码更可移植(你可以独立于代码来更改配置)。由于调用了`convertAndSend()`方法,模板将委托给它的`MessageConverter`实例。在这种情况下,它使用默认的`SimpleMessageConverter`,但是可以向`rabbitTemplate` Bean 提供不同的实现,如`HelloWorldConfiguration`中所定义的。 + +现在打开`Consumer`类。它实际上共享相同的配置基类,这意味着它共享`rabbitTemplate` Bean。这就是为什么我们将模板配置为`routingKey`(用于发送)和`queue`(用于接收)。正如我们在[`AmqpTemplate`]中描述的那样,你可以将’RoutingKey’参数传递给 send 方法,将’Queue’参数传递给 receive 方法。`Consumer`代码基本上是生产者的镜像,调用`receiveAndConvert()`而不是`convertAndSend()`。 + +下面的清单显示了`Consumer`的主要方法: + +``` +public static void main(String[] args) { + ApplicationContext context = + new AnnotationConfigApplicationContext(RabbitConfiguration.class); + AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class); + System.out.println("Received: " + amqpTemplate.receiveAndConvert()); +} +``` + +如果运行`Producer`,然后运行`Consumer`,则应该在控制台输出中看到`Received: Hello World`。 + +##### [](#hello-world-async)异步示例 + +[同步示例](#hello-world-sync)浏览了同步 Hello World 示例。这一部分描述了一个稍微更高级但功能更强大的选项。通过一些修改,Hello World 示例可以提供异步接收的示例,也称为消息驱动 POJO。实际上,有一个子包提供了以下内容:`org.springframework.amqp.samples.helloworld.async`。 + +同样,我们从发送方开始。打开`ProducerConfiguration`类,注意它创建了一个`connectionFactory`和一个`rabbitTemplate` Bean。这一次,由于配置是专门用于消息发送端的,因此我们甚至不需要任何队列定义,并且`RabbitTemplate`仅具有’RoutingKey’属性集。记住,消息是发送到交易所的,而不是直接发送到队列的。AMQP 默认交换是一种没有名称的直接交换。所有队列都绑定到该默认交换,并以它们的名称作为路由密钥。这就是为什么我们只需要在这里提供路由密钥。 + +下面的清单显示了`rabbitTemplate`的定义: + +``` +public RabbitTemplate rabbitTemplate() { + RabbitTemplate template = new RabbitTemplate(connectionFactory()); + template.setRoutingKey(this.helloWorldQueueName); + return template; +} +``` + +由于此示例演示了异步消息接收,所以生产端被设计为连续发送消息(如果它是一个类似于同步版本的每次执行消息的模型,那么它实际上就不是一个消息驱动的消费者了)。负责持续发送消息的组件被定义为`ProducerConfiguration`中的内部类。它被配置为每三秒运行一次。 + +下面的清单显示了该组件: + +``` +static class ScheduledProducer { + + @Autowired + private volatile RabbitTemplate rabbitTemplate; + + private final AtomicInteger counter = new AtomicInteger(); + + @Scheduled(fixedRate = 3000) + public void sendMessage() { + rabbitTemplate.convertAndSend("Hello World " + counter.incrementAndGet()); + } +} +``` + +你不需要了解所有的细节,因为真正的重点应该是接收端(我们接下来将讨论这一点)。但是,如果你还不熟悉 Spring 任务调度支持,则可以了解更多[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html#scheduling-annotation-support)。简而言之,`postProcessor` Bean 中的`ProducerConfiguration`使用调度程序注册任务。 + +现在我们可以转向接受方了。为了强调消息驱动的 POJO 行为,我们从对消息做出反应的组件开始。这个类被称为`HelloWorldHandler`,如以下清单所示: + +``` +public class HelloWorldHandler { + + public void handleMessage(String text) { + System.out.println("Received: " + text); + } + +} +``` + +那门课很难上。它不扩展任何基类,不实现任何接口,甚至不包含任何导入。它正被 Spring AMQP`MessageListenerAdapter`接口“适配”到`MessageListener`接口。然后可以在`SimpleMessageListenerContainer`上配置该适配器。对于这个示例,容器是在`ConsumerConfiguration`类中创建的。你可以看到 POJO 包装在适配器那里。 + +下面的清单显示了`listenerContainer`是如何定义的: + +``` +@Bean +public SimpleMessageListenerContainer listenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory()); + container.setQueueName(this.helloWorldQueueName); + container.setMessageListener(new MessageListenerAdapter(new HelloWorldHandler())); + return container; +} +``` + +`SimpleMessageListenerContainer`是一个 Spring 生命周期组件,默认情况下,它会自动启动。如果你在`Consumer`类中查找,你可以看到它的`main()`方法仅由一行引导程序组成,用于创建`ApplicationContext`。生产者的`main()`方法也是一行引导程序,因为其方法被注释为`@Scheduled`的组件也会自动启动。你可以以任何顺序启动`Producer`和`Consumer`,并且你应该每三秒钟就会看到正在发送和接收的消息。 + +#### [](#stock-trading)4.4.2。股票交易 + +股票交易示例演示了比[Hello World 样本](#hello-world-sample)更高级的消息传递场景。然而,配置是非常相似的,如果有一点更多的参与。由于我们详细介绍了 Hello World 配置,在此,我们将重点讨论使这个示例有所不同的原因。有一个服务器将市场数据(股票行情)推送到主题交换。然后,客户端可以通过绑定具有路由模式的队列来订阅市场数据提要(例如,`app.stock.quotes.nasdaq.*`)。这个演示的另一个主要功能是由客户端发起并由服务器处理的请求-回复“股票交易”交互。这涉及一个私有`replyTo`队列,该队列由客户端在订单请求消息本身内发送。 + +服务器的核心配置在`RabbitServerConfiguration`包中的`org.springframework.amqp.rabbit.stocks.config.server`类中。它扩展了`AbstractStockAppRabbitConfiguration`。这里定义了服务器和客户机的公共资源,包括市场数据主题交换(其名称为“app.stock.marketdata”)和服务器为股票交易公开的队列(其名称为“app.stock.request”)。在该公共配置文件中,你还可以看到在`Jackson2JsonMessageConverter`上配置了`RabbitTemplate`。 + +特定于服务器的配置由两个部分组成。首先,它在`RabbitTemplate`上配置市场数据交换,这样它就不需要在每次调用发送`Message`时都提供该交换名称。它在基本配置类中定义的抽象回调方法中执行此操作。下面的列表显示了该方法: + +``` +public void configureRabbitTemplate(RabbitTemplate rabbitTemplate) { + rabbitTemplate.setExchange(MARKET_DATA_EXCHANGE_NAME); +} +``` + +其次,声明股票请求队列。在这种情况下,它不需要任何显式绑定,因为它绑定到默认的无名称交换,并以自己的名称作为路由密钥。如前所述,AMQP 规范定义了这种行为。下面的清单显示了`stockRequestQueue` Bean 的定义: + +``` +@Bean +public Queue stockRequestQueue() { + return new Queue(STOCK_REQUEST_QUEUE_NAME); +} +``` + +现在你已经看到了服务器的 AMQP 资源的配置,请导航到`org.springframework.amqp.rabbit.stocks`目录下的`src/test/java`包。在这里,你可以看到实际的`Server`类,它提供了`main()`方法。它基于`server-bootstrap.xml`配置文件创建`ApplicationContext`。在这里,你可以看到发布虚拟市场数据的计划任务。这种配置依赖于 Spring 的`task`名称空间支持。引导程序配置文件还导入了其他一些文件。最有趣的是`server-messaging.xml`,它直接位于`src/main/resources`之下。在这里,你可以看到负责处理股票交易请求的`messageListenerContainer` Bean。最后,看看`serverHandler` Bean 中定义的`server-handlers.xml`(也在“SRC/main/resources”中)。 Bean 是`ServerHandler`类的一个实例,并且是消息驱动 POJO 的一个很好的示例,该 POJO 也可以发送回复消息。请注意,它本身并不耦合到框架或任何 AMQP 概念。它接受`TradeRequest`并返回`TradeResponse`。下面的清单显示了`handleMessage`方法的定义: + +``` +public TradeResponse handleMessage(TradeRequest tradeRequest) { ... +} +``` + +现在,我们已经了解了服务器最重要的配置和代码,我们可以转到客户机。最好的起点可能是`RabbitClientConfiguration`包中的`org.springframework.amqp.rabbit.stocks.config.client`。注意,它声明了两个队列,但没有提供显式的名称。下面的清单显示了这两个队列的 Bean 定义: + +``` +@Bean +public Queue marketDataQueue() { + return amqpAdmin().declareQueue(); +} + +@Bean +public Queue traderJoeQueue() { + return amqpAdmin().declareQueue(); +} +``` + +这些都是私有队列,并且会自动生成唯一的名称。客户端使用第一个生成的队列绑定到服务器公开的市场数据交换。回想一下,在 AMQP 中,消费者与队列交互,而生产者与交换器交互。队列与交易所的“绑定”是告诉代理将消息从给定的交易所传递(或路由)到队列的方法。由于市场数据交换是一个主题交换,因此绑定可以用路由模式表示。`RabbitClientConfiguration`使用`Binding`对象执行此操作,并且该对象是使用`BindingBuilder`Fluent API 生成的。下面的清单显示了`Binding`: + +``` +@Value("${stocks.quote.pattern}") +private String marketDataRoutingKey; + +@Bean +public Binding marketDataBinding() { + return BindingBuilder.bind( + marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey); +} +``` + +请注意,实际值已在一个属性文件中外部化(`client.properties``src/main/resources`),并且我们使用 Spring 的`@Value`注释来注入该值。这通常是个好主意。否则,该值将在类中进行硬编码,并且在不重新编译的情况下不可修改。在这种情况下,在更改用于绑定的路由模式时,运行客户机的多个版本要容易得多。我们现在可以试试。 + +首先运行`org.springframework.amqp.rabbit.stocks.Server`,然后运行`org.springframework.amqp.rabbit.stocks.Client`。你应该会看到`NASDAQ`股票的虚拟报价,因为在 client.properties 中与“stocks.quote.pattern”键关联的当前值是“app.stock.quotes.nasdaq.**”。现在,在保持现有`Server`和`Client`运行的同时,将该属性值更改为’app.stock.quotes.nyse.**’,并启动第二个`Client`实例。你应该看到,第一个客户端仍然接收纳斯达克报价,而第二个客户端接收纽交所报价。相反,你可以改变模式,以获得所有的股票,甚至一个单独的股票代码。 + +我们探索的最后一个功能是从客户的角度进行请求-回复交互。回想一下,我们已经看到了接受`ServerHandler`对象并返回`TradeResponse`对象的`ServerHandler`对象。在`Client`包中,`RabbitStockServiceGateway`侧的对应代码是`RabbitStockServiceGateway`。它委托给`RabbitTemplate`以发送消息。下面的清单显示了`send`方法: + +``` +public void send(TradeRequest tradeRequest) { + getRabbitTemplate().convertAndSend(tradeRequest, new MessagePostProcessor() { + public Message postProcessMessage(Message message) throws AmqpException { + message.getMessageProperties().setReplyTo(new Address(defaultReplyToQueue)); + try { + message.getMessageProperties().setCorrelationId( + UUID.randomUUID().toString().getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) { + throw new AmqpException(e); + } + return message; + } + }); +} +``` + +请注意,在发送消息之前,它设置了`replyTo`地址。它提供了由`traderJoeQueue` Bean 定义生成的队列(如前面所示)。下面的清单显示了`@Bean`类本身的`StockServiceGateway`定义: + +``` +@Bean +public StockServiceGateway stockServiceGateway() { + RabbitStockServiceGateway gateway = new RabbitStockServiceGateway(); + gateway.setRabbitTemplate(rabbitTemplate()); + gateway.setDefaultReplyToQueue(traderJoeQueue()); + return gateway; +} +``` + +如果你不再运行服务器和客户机,请立即启动它们。尝试发送格式为“100tckr”的请求。在模拟“处理”请求的短暂人为延迟之后,你应该会看到一个确认消息出现在客户机上。 + +#### [](#spring-rabbit-json)4.4.3。从非 Spring 份申请中接收 JSON + +Spring 应用程序,在发送 JSON 时,将`*TypeId*`头设置为完全限定的类名,以协助接收应用程序将 JSON 转换回 Java 对象。 + +`spring-rabbit-json`示例探索了几种从非 Spring 应用程序转换 JSON 的技术。 + +另见[Jackson2JSONMessageConverter](#json-message-converter)以及`DefaultClassMapper`的[javadoc](https://DOCS. Spring.io/ Spring-amqp/DOCS/current/api/index.html?org/springframework/amqp/support/converter/defaultclassmapper.html)。 + +### [](#testing)4.5。测试支持 + +为异步应用程序编写集成一定比测试更简单的应用程序更复杂。当`@RabbitListener`注释之类的抽象出现在图片中时,这就变得更加复杂了。问题是如何验证在发送消息后,侦听器是否如预期的那样接收到了消息。 + +框架本身有许多单元和集成测试。一些使用模拟,而另一些则使用与实时 RabbitMQ 代理的集成测试。你可以参考这些测试来获得测试场景的一些想法。 + +Spring AMQP 版本 1.6 引入了`spring-rabbit-test`JAR,它为测试这些更复杂的场景中的一些提供了支持。预计该项目将随着时间的推移而扩展,但我们需要社区反馈来为帮助测试所需的功能提供建议。请使用[JIRA](https://jira.spring.io/browse/AMQP)或[GitHub 问题](https://github.com/spring-projects/spring-amqp/issues)提供此类反馈。 + +#### [](#spring-rabbit-test)4.5.1。@SpringRabbitTest + +使用此注释将基础设施 bean 添加到 Spring test`ApplicationContext`。在使用`@SpringBootTest`时,这是不必要的,因为 Spring boot 的自动配置将添加 bean。 + +已注册的 bean 有: + +* `CachingConnectionFactory`(`autoConnectionFactory`)。如果存在`@RabbitEnabled`,则使用其连接工厂。 + +* `RabbitTemplate`(`autoRabbitTemplate`) + +* `RabbitAdmin`(`autoRabbitAdmin`) + +* `RabbitListenerContainerFactory`(`autoContainerFactory`) + +此外,还添加了与`@EnableRabbit`相关的 bean(以支持`@RabbitListener`)。 + +例 4。JUnit5 示例 + +``` +@SpringJunitConfig +@SpringRabbitTest +public class MyRabbitTests { + + @Autowired + private RabbitTemplate template; + + @Autowired + private RabbitAdmin admin; + + @Autowired + private RabbitListenerEndpointRegistry registry; + + @Test + void test() { + ... + } + + @Configuration + public static class Config { + + ... + + } + +} +``` + +使用 JUnit4,将`@SpringJunitConfig`替换为`@RunWith(SpringRunnner.class)`。 + +#### [](#mockito-answer)4.5.2。mockito`Answer`实现 + +目前有两个`Answer`实现来帮助测试。 + +第一个是`LatchCountDownAndCallRealMethodAnswer`,它提供一个`Answer`,返回`null`并对锁存器进行倒数。下面的示例展示了如何使用`LatchCountDownAndCallRealMethodAnswer`: + +``` +LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2); +doAnswer(answer) + .when(listener).foo(anyString(), anyString()); + +... + +assertThat(answer.await(10)).isTrue(); +``` + +第二,`LambdaAnswer`提供了一种可选地调用实际方法的机制,并提供了一种返回自定义结果的机会,该结果基于`InvocationOnMock`和结果(如果有的话)。 + +考虑以下 POJO: + +``` +public class Thing { + + public String thing(String thing) { + return thing.toUpperCase(); + } + +} +``` + +下面的类测试`Thing`POJO: + +``` +Thing thing = spy(new Thing()); + +doAnswer(new LambdaAnswer(true, (i, r) -> r + r)) + .when(thing).thing(anyString()); +assertEquals("THINGTHING", thing.thing("thing")); + +doAnswer(new LambdaAnswer(true, (i, r) -> r + i.getArguments()[0])) + .when(thing).thing(anyString()); +assertEquals("THINGthing", thing.thing("thing")); + +doAnswer(new LambdaAnswer(false, (i, r) -> + "" + i.getArguments()[0] + i.getArguments()[0])).when(thing).thing(anyString()); +assertEquals("thingthing", thing.thing("thing")); +``` + +从版本 2.2.3 开始,答案将捕获由 Test 下的方法引发的任何异常。使用`answer.getExceptions()`获取对它们的引用。 + +当与[`@RabbitListenerTest`和`RabbitListenerTestHarness`]结合使用时,使用`harness.getLambdaAnswerFor("listenerId", true, …​)`可以为侦听器获得正确构造的答案。 + +#### [](#test-harness)4.5.3。`@RabbitListenerTest`和`RabbitListenerTestHarness` + +将一个`@Configuration`类注释为`@RabbitListenerTest`,会导致框架将标准`RabbitListenerAnnotationBeanPostProcessor`替换为一个名为`RabbitListenerTestHarness`的子类(它还通过`@EnableRabbit`启用`@RabbitListener`检测)。 + +`RabbitListenerTestHarness`通过两种方式增强了侦听器。首先,它将侦听器封装在`Mockito Spy`中,从而实现正常的`Mockito`存根和验证操作。它还可以向侦听器添加`Advice`,从而能够访问参数、结果和引发的任何异常。你可以通过`@RabbitListenerTest`上的属性来控制其中哪些(或两个)被启用。提供后者是为了访问关于调用的较低级别的数据。它还支持在调用异步监听器之前阻塞测试线程。 + +| |`final``@RabbitListener`方法不能被监视或通知。
此外,只有具有`id`属性的侦听器才能被监视或通知。| +|---|--------------------------------------------------------------------------------------------------------------------------------------| + +举几个例子。 + +下面的示例使用 SPY: + +``` +@Configuration +@RabbitListenerTest +public class Config { + + @Bean + public Listener listener() { + return new Listener(); + } + + ... + +} + +public class Listener { + + @RabbitListener(id="foo", queues="#{queue1.name}") + public String foo(String foo) { + return foo.toUpperCase(); + } + + @RabbitListener(id="bar", queues="#{queue2.name}") + public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) { + ... + } + +} + +public class MyTests { + + @Autowired + private RabbitListenerTestHarness harness; (1) + + @Test + public void testTwoWay() throws Exception { + assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo")); + + Listener listener = this.harness.getSpy("foo"); (2) + assertNotNull(listener); + verify(listener).foo("foo"); + } + + @Test + public void testOneWay() throws Exception { + Listener listener = this.harness.getSpy("bar"); + assertNotNull(listener); + + LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 2); (3) + doAnswer(answer).when(listener).foo(anyString(), anyString()); (4) + + this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar"); + this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz"); + + assertTrue(answer.await(10)); + verify(listener).foo("bar", this.queue2.getName()); + verify(listener).foo("baz", this.queue2.getName()); + } + +} +``` + +|**1**|将线束注入到测试用例中,这样我们就可以访问间谍。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|获取对 SPY 的引用,这样我们就可以验证它是否如预期的那样被调用了。
由于这是一个发送和接收操作,因此不需要挂起测试线程,因为它已经
挂起在`RabbitTemplate`中等待回复。| +|**3**|在这种情况下,我们只使用一个发送操作,所以我们需要一个锁存器来等待对容器线程上的侦听器
的异步调用。
我们使用[Answer\](#mockito-answer)实现之一来帮助实现这一点。
重要:由于侦听器被监视的方式,使用`harness.getLatchAnswerFor()`为间谍获得正确配置的答案是很重要的。| +|**4**|将 SPY 配置为调用`Answer`。| + +下面的示例使用了捕获建议: + +``` +@Configuration +@ComponentScan +@RabbitListenerTest(spy = false, capture = true) +public class Config { + +} + +@Service +public class Listener { + + private boolean failed; + + @RabbitListener(id="foo", queues="#{queue1.name}") + public String foo(String foo) { + return foo.toUpperCase(); + } + + @RabbitListener(id="bar", queues="#{queue2.name}") + public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) { + if (!failed && foo.equals("ex")) { + failed = true; + throw new RuntimeException(foo); + } + failed = false; + } + +} + +public class MyTests { + + @Autowired + private RabbitListenerTestHarness harness; (1) + + @Test + public void testTwoWay() throws Exception { + assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo")); + + InvocationData invocationData = + this.harness.getNextInvocationDataFor("foo", 0, TimeUnit.SECONDS); (2) + assertThat(invocationData.getArguments()[0], equalTo("foo")); (3) + assertThat((String) invocationData.getResult(), equalTo("FOO")); + } + + @Test + public void testOneWay() throws Exception { + this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar"); + this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz"); + this.rabbitTemplate.convertAndSend(this.queue2.getName(), "ex"); + + InvocationData invocationData = + this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); (4) + Object[] args = invocationData.getArguments(); + assertThat((String) args[0], equalTo("bar")); + assertThat((String) args[1], equalTo(queue2.getName())); + + invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); + args = invocationData.getArguments(); + assertThat((String) args[0], equalTo("baz")); + + invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); + args = invocationData.getArguments(); + assertThat((String) args[0], equalTo("ex")); + assertEquals("ex", invocationData.getThrowable().getMessage()); (5) + } + +} +``` + +|**1**|将线束注入到测试用例中,这样我们就可以访问间谍。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用`harness.getNextInvocationDataFor()`来检索调用数据-在这种情况下,因为这是一个请求/回复
场景,因此不需要等待任何时间,因为测试线程被挂起在`RabbitTemplate`中等待
结果。| +|**3**|然后,我们可以验证该论证和结果是否如预期的那样。| +|**4**|这一次我们需要一些时间来等待数据,因为这是容器线程上的异步操作,我们需要
来挂起测试线程。| +|**5**|当侦听器抛出异常时,它在调用数据的`throwable`属性中可用。| + +| |当将自定义`Answer`s 与线束一起使用时,为了正确地进行操作,这样的答案应该是子类`ForwardsInvocation`并从线束中获得实际的侦听器(而不是间谍)(`getDelegate("myListener")`)并调用

参见所提供的[mockito`Answer`实现](#mockito-answer)源代码的示例。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#test-template)4.5.4。使用`TestRabbitTemplate` + +`TestRabbitTemplate`用于执行一些基本的集成测试,而不需要代理。在测试用例中将其添加为`@Bean`时,它会发现上下文中的所有侦听器容器,无论是声明为`@Bean`还是``还是使用`@RabbitListener`注释。它目前仅支持按队列名进行路由。模板从容器中提取消息侦听器,并直接在测试线程上调用它。返回回复的侦听器支持请求-回复消息(`sendAndReceive`methods)。 + +下面的测试用例使用了模板: + +``` +@RunWith(SpringRunner.class) +public class TestRabbitTemplateTests { + + @Autowired + private TestRabbitTemplate template; + + @Autowired + private Config config; + + @Test + public void testSimpleSends() { + this.template.convertAndSend("foo", "hello1"); + assertThat(this.config.fooIn, equalTo("foo:hello1")); + this.template.convertAndSend("bar", "hello2"); + assertThat(this.config.barIn, equalTo("bar:hello2")); + assertThat(this.config.smlc1In, equalTo("smlc1:")); + this.template.convertAndSend("foo", "hello3"); + assertThat(this.config.fooIn, equalTo("foo:hello1")); + this.template.convertAndSend("bar", "hello4"); + assertThat(this.config.barIn, equalTo("bar:hello2")); + assertThat(this.config.smlc1In, equalTo("smlc1:hello3hello4")); + + this.template.setBroadcast(true); + this.template.convertAndSend("foo", "hello5"); + assertThat(this.config.fooIn, equalTo("foo:hello1foo:hello5")); + this.template.convertAndSend("bar", "hello6"); + assertThat(this.config.barIn, equalTo("bar:hello2bar:hello6")); + assertThat(this.config.smlc1In, equalTo("smlc1:hello3hello4hello5hello6")); + } + + @Test + public void testSendAndReceive() { + assertThat(this.template.convertSendAndReceive("baz", "hello"), equalTo("baz:hello")); + } +``` + +``` + @Configuration + @EnableRabbit + public static class Config { + + public String fooIn = ""; + + public String barIn = ""; + + public String smlc1In = "smlc1:"; + + @Bean + public TestRabbitTemplate template() throws IOException { + return new TestRabbitTemplate(connectionFactory()); + } + + @Bean + public ConnectionFactory connectionFactory() throws IOException { + ConnectionFactory factory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + willReturn(connection).given(factory).createConnection(); + willReturn(channel).given(connection).createChannel(anyBoolean()); + given(channel.isOpen()).willReturn(true); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() throws IOException { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + return factory; + } + + @RabbitListener(queues = "foo") + public void foo(String in) { + this.fooIn += "foo:" + in; + } + + @RabbitListener(queues = "bar") + public void bar(String in) { + this.barIn += "bar:" + in; + } + + @RabbitListener(queues = "baz") + public String baz(String in) { + return "baz:" + in; + } + + @Bean + public SimpleMessageListenerContainer smlc1() throws IOException { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); + container.setQueueNames("foo", "bar"); + container.setMessageListener(new MessageListenerAdapter(new Object() { + + @SuppressWarnings("unused") + public void handleMessage(String in) { + smlc1In += in; + } + + })); + return container; + } + + } + +} +``` + +#### [](#junit-rules)4.5.5。JUnit4`@Rules` + +Spring AMQP1.7 版及以后版本提供了一个名为`spring-rabbit-junit`的附加 JAR。此 JAR 包含两个实用程序`@Rule`实例,用于运行 JUnit4 测试时使用。关于 JUnit5 测试,请参见[JUnit5 条件](#junit5-conditions)。 + +##### [](#using-brokerrunning)使用`BrokerRunning` + +`BrokerRunning`提供了一种机制,当代理不运行时(默认情况下,在`localhost`上),让测试成功。 + +它还具有用于初始化和清空队列以及删除队列和交换的实用程序方法。 + +下面的示例展示了它的用法: + +``` +@ClassRule +public static BrokerRunning brokerRunning = BrokerRunning.isRunningWithEmptyQueues("foo", "bar"); + +@AfterClass +public static void tearDown() { + brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well +} +``` + +有几个`isRunning…​`静态方法,例如`isBrokerAndManagementRunning()`,它验证代理是否启用了管理插件。 + +###### [](#brokerRunning-configure)配置规则 + +有时,如果没有代理,你希望测试失败,比如夜间的 CI 构建。要在运行时禁用该规则,请将一个名为`RABBITMQ_SERVER_REQUIRED`的环境变量设置为`true`。 + +你可以使用 setter 或环境变量重写代理属性,例如 hostname: + +下面的示例展示了如何使用 setter 重写属性: + +``` +@ClassRule +public static BrokerRunning brokerRunning = BrokerRunning.isRunningWithEmptyQueues("foo", "bar"); + +static { + brokerRunning.setHostName("10.0.0.1") +} + +@AfterClass +public static void tearDown() { + brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well +} +``` + +你还可以通过设置以下环境变量来覆盖属性: + +``` +public static final String BROKER_ADMIN_URI = "RABBITMQ_TEST_ADMIN_URI"; +public static final String BROKER_HOSTNAME = "RABBITMQ_TEST_HOSTNAME"; +public static final String BROKER_PORT = "RABBITMQ_TEST_PORT"; +public static final String BROKER_USER = "RABBITMQ_TEST_USER"; +public static final String BROKER_PW = "RABBITMQ_TEST_PASSWORD"; +public static final String BROKER_ADMIN_USER = "RABBITMQ_TEST_ADMIN_USER"; +public static final String BROKER_ADMIN_PW = "RABBITMQ_TEST_ADMIN_PASSWORD"; +``` + +这些环境变量覆盖了默认设置(对于 AMQP 是`localhost:5672`,对于 Management REST API 是`[localhost:15672/api/](http://localhost:15672/api/)`)。 + +更改主机名会同时影响`amqp`和`management`RESTAPI 连接(除非显式设置了管理 URI)。 + +`BrokerRunning`还提供了一个名为`static`的`static`方法,该方法允许你在包含这些变量的映射中进行传递。它们覆盖系统环境变量。如果你希望对多个测试套件中的测试使用不同的配置,这可能会很有用。重要事项:在调用创建规则实例的任何`isRunning()`静态方法之前,必须调用该方法。变量值应用于此调用后创建的所有实例。调用`clearEnvironmentVariableOverrides()`重置规则以使用默认值(包括任何实际的环境变量)。 + +在你的测试用例中,你可以在创建连接工厂时使用`brokerRunning`;`getConnectionFactory()`返回规则的 RabbitMQ`ConnectionFactory`。下面的示例展示了如何做到这一点: + +``` +@Bean +public CachingConnectionFactory rabbitConnectionFactory() { + return new CachingConnectionFactory(brokerRunning.getConnectionFactory()); +} +``` + +##### [](#using-longrunningintegrationtest)使用`LongRunningIntegrationTest` + +`LongRunningIntegrationTest`是一条禁用长时间运行测试的规则。你可能希望在开发人员系统上使用该规则,但要确保在例如 Nightly CI 构建上禁用该规则。 + +下面的示例展示了它的用法: + +``` +@Rule +public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest(); +``` + +要在运行时禁用该规则,请将一个名为`RUN_LONG_INTEGRATION_TESTS`的环境变量设置为`true`。 + +#### [](#junit5-conditions)4.5.6。JUnit5 条件 + +2.0.2 版引入了对 JUnit5 的支持。 + +##### [](#using-the-rabbitavailable-annotation)使用`@RabbitAvailable`注释 + +这个类级注释类似于在[JUnit4`@Rules`]中讨论的`BrokerRunning``@Rule`(#JUnit-rules)。它由`RabbitAvailableCondition`处理。 + +注释有三个属性: + +* `queues`:在每次测试之前声明(和清除)并在所有测试完成后删除的队列数组。 + +* `management`:如果你的测试还需要在代理上安装管理插件,则将其设置为`true`。 + +* `purgeAfterEach`:(自版本 2.2)当`true`(默认)时,`queues`将在测试之间被清除。 + +它用于检查代理是否可用,如果不可用,则跳过测试。正如[配置规则](#brokerRunning-configure)中所讨论的,如果`RABBITMQ_SERVER_REQUIRED`没有代理,则称为`true`的环境变量会导致测试快速失败。可以通过使用[配置规则](#brokerRunning-configure)中讨论的环境变量来配置条件。 + +此外,`RabbitAvailableCondition`支持参数化测试构造函数和方法的参数解析。支持两种参数类型: + +* `BrokerRunningSupport`:实例(在 2.2 之前,这是一个 JUnit4`BrokerRunning`实例) + +* `ConnectionFactory`:`BrokerRunningSupport`实例的 RabbitMQ 连接工厂 + +下面的示例展示了这两个方面: + +``` +@RabbitAvailable(queues = "rabbitAvailableTests.queue") +public class RabbitAvailableCTORInjectionTests { + + private final ConnectionFactory connectionFactory; + + public RabbitAvailableCTORInjectionTests(BrokerRunningSupport brokerRunning) { + this.connectionFactory = brokerRunning.getConnectionFactory(); + } + + @Test + public void test(ConnectionFactory cf) throws Exception { + assertSame(cf, this.connectionFactory); + Connection conn = this.connectionFactory.newConnection(); + Channel channel = conn.createChannel(); + DeclareOk declareOk = channel.queueDeclarePassive("rabbitAvailableTests.queue"); + assertEquals(0, declareOk.getConsumerCount()); + channel.close(); + conn.close(); + } + +} +``` + +前面的测试是在框架本身中进行的,并验证参数注入和条件是否正确地创建了队列。 + +一个实际的用户测试可能如下: + +``` +@RabbitAvailable(queues = "rabbitAvailableTests.queue") +public class RabbitAvailableCTORInjectionTests { + + private final CachingConnectionFactory connectionFactory; + + public RabbitAvailableCTORInjectionTests(BrokerRunningSupport brokerRunning) { + this.connectionFactory = + new CachingConnectionFactory(brokerRunning.getConnectionFactory()); + } + + @Test + public void test() throws Exception { + RabbitTemplate template = new RabbitTemplate(this.connectionFactory); + ... + } +} +``` + +在测试类中使用 Spring 注释应用程序上下文时,可以通过一个名为`RabbitAvailableCondition.getBrokerRunning()`的静态方法获得对条件的连接工厂的引用。 + +| |从版本 2.2 开始,`getBrokerRunning()`返回一个`BrokerRunningSupport`对象;以前,返回的是 JUnit4`BrokerRunnning`实例。
新类的 API 与`BrokerRunning`相同。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的测试来自该框架,并演示了它的用法: + +``` +@RabbitAvailable(queues = { + RabbitTemplateMPPIntegrationTests.QUEUE, + RabbitTemplateMPPIntegrationTests.REPLIES }) +@SpringJUnitConfig +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +public class RabbitTemplateMPPIntegrationTests { + + public static final String QUEUE = "mpp.tests"; + + public static final String REPLIES = "mpp.tests.replies"; + + @Autowired + private RabbitTemplate template; + + @Autowired + private Config config; + + @Test + public void test() { + + ... + + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + public CachingConnectionFactory cf() { + return new CachingConnectionFactory(RabbitAvailableCondition + .getBrokerRunning() + .getConnectionFactory()); + } + + @Bean + public RabbitTemplate template() { + + ... + + } + + @Bean + public SimpleRabbitListenerContainerFactory + rabbitListenerContainerFactory() { + + ... + + } + + @RabbitListener(queues = QUEUE) + public byte[] foo(byte[] in) { + return in; + } + + } + +} +``` + +##### [](#using-the-longrunning-annotation)使用`@LongRunning`注释 + +与`LongRunningIntegrationTest`JUnit4`@Rule`类似,除非将环境变量(或系统属性)设置为`true`,否则此注释将导致跳过测试。下面的示例展示了如何使用它: + +``` +@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE) +@LongRunning +public class SimpleMessageListenerContainerLongTests { + + public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue"; + +... + +} +``` + +默认情况下,变量是`RUN_LONG_INTEGRATION_TESTS`,但你可以在注释的`value`属性中指定变量名。 + +## [](#spring-integration-reference)5。 Spring 整合-参考 + +参考文档的这一部分快速介绍了 Spring 集成项目中的 AMQP 支持。 + +### [](#spring-integration-amqp)5.1。 Spring 集成 AMQP 支持 + +这一简短的章节涵盖了 Spring 集成和 Spring AMQP 项目之间的关系。 + +#### [](#spring-integration-amqp-introduction)5.1.1。导言 + +[Spring Integration](https://www.springsource.org/spring-integration)项目包括基于 Spring AMQP 项目的 AMQP 通道适配器和网关。这些适配器是在 Spring 集成项目中开发和发布的。在 Spring 集成中,“通道适配器”是单向的,而“网关”是双向的。我们提供了入站通道适配器、出站通道适配器、入站网关和出站网关。 + +由于 AMQP 适配器是 Spring 集成发行版的一部分,所以文档可以作为 Spring 集成发行版的一部分使用。我们在这里提供了主要功能的简要概述。有关更多详细信息,请参见[Spring Integration Reference Guide](https://docs.spring.io/spring-integration/reference/htmlsingle/)。 + +#### [](#inbound-channel-adapter)5.1.2。入站通道适配器 + +要从队列接收 AMQP 消息,可以配置``。下面的示例展示了如何配置入站通道适配器: + +``` + +``` + +#### [](#outbound-channel-adapter)5.1.3。出站通道适配器 + +要将 AMQP 消息发送到 Exchange,可以配置``。你可以在 Exchange 名称之外提供一个“路由密钥”。下面的示例展示了如何定义出站通道适配器: + +``` + +``` + +#### [](#inbound-gateway)5.1.4。入站网关 + +要从队列接收 AMQP 消息并响应其应答地址,可以配置``。下面的示例展示了如何定义入站网关: + +``` + +``` + +#### [](#outbound-gateway)5.1.5。出站网关 + +要将 AMQP 消息发送到 Exchange 并从远程客户端接收回响应,可以配置``。你可以在 Exchange 名称之外提供一个“路由密钥”。下面的示例展示了如何定义出站网关: + +``` + +``` + +## [](#resources)6。其他资源 + +除了这个参考文档,还有许多其他资源可以帮助你了解 AMQP。 + +### [](#further-reading)6.1。进一步阅读 + +对于那些不熟悉 AMQP 的人来说,[规格](https://www.amqp.org/resources/download)实际上是很有可读性的。当然,它是权威的信息来源, Spring AMQP 代码对于任何熟悉该规范的人都应该很容易理解。我们目前对 RabbitMQ 支持的实现基于他们的 2.8.x 版本,它正式支持 AMQP0.8 和 0.9.1。我们建议阅读 0.9.1 文档。 + +在 RabbitMQ[开始](https://www.rabbitmq.com/how.html)页面上有很多很棒的文章、演示文稿和博客。由于这是 Spring AMQP 当前唯一受支持的实现,因此我们还建议将其作为所有与代理相关的关注的一般起点。 + +## [](#change-history)附录 A:变更历史 + +这一节描述了在版本发生变化时所做的更改。 + +### [](#current-release)a.1。当前版本 + +见[What’s New](#whats-new)。 + +### [](#previous-whats-new)a.2。以前的版本 + +#### [](#changes-in-2-3-since-2-2)a.2.1。自 2.2 以来 2.3 的变化 + +本部分描述了版本 2.2 和版本 2.3 之间的更改。有关以前版本的更改,请参见[变更历史](#change-history)。 + +##### [](#connection-factory-changes)连接工厂变更 + +现在提供了两个额外的连接工厂。有关更多信息,请参见[选择连接工厂](#choosing-factory)。 + +##### [](#rabbitlistener-changes-2)`@RabbitListener`变化 + +现在可以指定回复内容类型。有关更多信息,请参见[回复 ContentType](#reply-content-type)。 + +##### [](#message-converter-changes)消息转换器更改 + +如果`ObjectMapper`配置了自定义反序列化器,则`Jackson2JMessageConverter`s 现在可以反序列化抽象类(包括接口)。有关更多信息,请参见[反序列化抽象类](#jackson-abstract)。 + +##### [](#testing-changes)测试更改 + +提供了一个新的注释`@SpringRabbitTest`,用于在不使用`SpringBootTest`时自动配置一些基础设施 bean。有关更多信息,请参见[@SpringRabbitTest](#spring-rabbit-test)。 + +##### [](#rabbittemplate-changes)RabbitTemplate 更改 + +模板的`ReturnCallback`已被重构为`ReturnsCallback`,以便在 lambda 表达式中更简单地使用。有关更多信息,请参见[相关发布者确认并返回](#template-confirms)。 + +当使用 Returns 和相关确认时,`CorrelationData`现在需要唯一的`id`属性。有关更多信息,请参见[相关发布者确认并返回](#template-confirms)。 + +当使用直接回复时,你现在可以配置模板,这样服务器就不需要返回与回复相关的数据。有关更多信息,请参见[RabbitMQ 直接回复](#direct-reply-to)。 + +##### [](#listener-container-changes)侦听器容器更改 + +现在可以使用一个新的侦听器容器属性`consumeDelay`;当使用[RabbitMQ 分片插件](https://github.com/rabbitmq/rabbitmq-sharding)时,该属性很有帮助。 + +默认的`JavaLangErrorHandler`现在调用`System.exit(99)`。要恢复到以前的行为(什么都不做),请添加一个 no-ophandler。 + +容器现在支持`globalQos`属性,以便在全局范围内为通道而不是为通道上的每个消费者应用`prefetchCount`。 + +有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#messagepostprocessor-changes)MessagePostProcessor 更改 + +压缩`MessagePostProcessor`s 现在使用逗号来分隔多个内容编码,而不是冒号。解压器可以处理这两种格式,但是,如果你使用这个版本生成的消息被 2.2.12 之前的版本使用,那么你应该将压缩器配置为使用旧的分隔符。有关更多信息,请参见[修改消息-压缩和更多](#post-processing)中的重要注释。 + +##### [](#multiple-broker-support-improvements)多个代理支持改进 + +有关更多信息,请参见[多个代理(或集群)支持](#multi-rabbit)。 + +##### [](#republishmessagerecoverer-changes)RepublishMessageRecoverer 更改 + +不提供此 recoverer 的支持发布服务器确认的新子类。有关更多信息,请参见[消息侦听器和异步情况](#async-listeners)。 + +#### [](#changes-in-2-2-since-2-1)a.2.2。自 2.1 以来 2.2 的变化 + +本部分描述了版本 2.1 和版本 2.2 之间的更改。 + +##### [](#package-changes)软件包更改 + +以下类/接口已从`org.springframework.amqp.rabbit.core.support`移至`org.springframework.amqp.rabbit.batch`: + +* `BatchingStrategy` + +* `MessageBatch` + +* `SimpleBatchingStrategy` + +此外,`ListenerExecutionFailedException`已从`org.springframework.amqp.rabbit.listener.exception`移至`org.springframework.amqp.rabbit.support`。 + +##### [](#dependency-changes)依赖项更改 + +JUnit(4)现在是一个可选的依赖项,并且将不再以传递依赖项的形式出现。 + +`spring-rabbit-junit`模块现在是**编译**模块中的`spring-rabbit-test`依赖项,以便在仅使用单个`spring-rabbit-test`时获得更好的目标应用程序开发体验,我们获得了 AMQP 组件的完整测试实用程序堆栈。 + +##### [](#breaking-api-changes)“breaking”API 变更 + +JUnit(5)`RabbitAvailableCondition.getBrokerRunning()`现在返回一个`BrokerRunningSupport`实例,而不是一个`BrokerRunning`实例,后者取决于 JUnit4。它有相同的 API,所以只需要更改任何引用的类名。有关更多信息,请参见[JUnit5 条件](#junit5-conditions)。 + +##### [](#listenercontainer-changes)ListenerContainer 更改 + +默认情况下,即使确认模式是手动的,具有致命异常的消息现在也会被拒绝,并且不会重新请求。有关更多信息,请参见[异常处理](#exception-handling)。 + +监听器的性能现在可以使用微米计`Timer`s 进行监视。有关更多信息,请参见[监视监听器性能](#micrometer)。 + +##### [](#rabbitlistener-changes-3)@RabbitListener 更改 + +现在可以在每个侦听器上配置`executor`,覆盖工厂配置,以更容易地识别与侦听器关联的线程。现在可以使用注释的`ackMode`属性覆盖容器工厂的`acknowledgeMode`属性。有关更多信息,请参见[覆盖集装箱工厂的属性](#listener-property-overrides)。 + +当使用[batching](#receiving-batch)时,`@RabbitListener`方法现在可以在一个调用中接收一批完整的消息,而不是一次获得它们。 + +当一次只接收一条批处理消息时,最后一条消息的`isLastInBatch`消息属性设置为 true。 + +此外,收到的批处理消息现在包含`amqp_batchSize`头。 + +侦听器也可以使用在`SimpleMessageListenerContainer`中创建的批处理,即使该批处理不是由生成器创建的。有关更多信息,请参见[选择容器](#choose-container)。 + +Spring 数据投影接口现在由`Jackson2JsonMessageConverter`支持。有关更多信息,请参见[Using Spring Data Projection Interfaces](#data-projection)。 + +如果不存在`Jackson2JsonMessageConverter`属性,或者它是默认的(`application/octet-string`),则`Jackson2JsonMessageConverter`现在假定内容是 JSON。有关更多信息,请参见[converting from a`Message`](#Jackson2jsonMessageConverter-from-message)。 + +类似地,如果不存在`contentType`属性,或者它是默认的(`application/octet-string`),则`Jackson2XmlMessageConverter`现在假定内容是 XML。有关更多信息,请参见[`Jackson2XmlMessageConverter`](#Jackson2xml)。 + +当`@RabbitListener`方法返回结果时, Bean 和`Method`现在在回复消息属性中可用。这允许`beforeSendReplyMessagePostProcessor`的配置,例如,在回复中设置一个头,以指示在服务器上调用了哪个方法。有关更多信息,请参见[回复管理](#async-annotation-driven-reply)。 + +现在可以配置`ReplyPostProcessor`,以便在发送回复消息之前对其进行修改。有关更多信息,请参见[回复管理](#async-annotation-driven-reply)。 + +##### [](#amqp-logging-appenders-changes)AMQP 日志附录更改 + +log4j 和 logback`AmqpAppender`s 现在支持`verifyHostname`SSL 选项。 + +现在还可以将这些附录配置为不将 MDC 条目添加为标题。引入了`addMdcAsHeaders`布尔选项来配置这样的行为。 + +Appenders 现在支持`SaslConfig`属性。 + +有关更多信息,请参见[日志记录子系统 AMQP 附录](#logging)。 + +##### [](#messagelisteneradapter-changes)MessageListenerAdapter 更改 + +`MessageListenerAdapter`现在提供了一个新的`buildListenerArguments(Object, Channel, Message)`方法来构建一个参数数组,这些参数将被传递到目标侦听器中,而一个旧的参数将被弃用。有关更多信息,请参见[`MessageListenerAdapter`]。 + +##### [](#exchangequeue-declaration-changes)交换/队列声明更改 + +用于创建`ExchangeBuilder`和`QueueBuilder`用于由`Exchange`声明的`Queue`对象的 fluent API 现在支持“众所周知”的参数。有关更多信息,请参见[用于队列和交换的 Builder API](#builder-api)。 + +`RabbitAdmin`有一个新的属性`explicitDeclarationsOnly`。有关更多信息,请参见[有条件声明](#conditional-declaration)。 + +##### [](#connection-factory-changes-2)连接工厂变更 + +`CachingConnectionFactory`有一个新的属性`shuffleAddresses`。当提供代理节点地址列表时,将在创建连接之前对列表进行调整,以便尝试连接的顺序是随机的。有关更多信息,请参见[连接到集群](#cluster)。 + +当使用 Publisher 确认和返回时,回调现在在连接工厂的`executor`上调用。如果你在回调中执行 Rabbit 操作,这就避免了`amqp-clients`库中可能出现的死锁。有关更多信息,请参见[相关发布者确认并返回](#template-confirms)。 + +此外,现在使用`ConfirmType`枚举来指定发布服务器确认类型,而不是使用两个互斥的 setter 方法。 + +当启用 SSL 时,`RabbitConnectionFactoryBean`现在默认使用 TLS1.2。有关更多信息,请参见[`RabbitConnectionFactoryBean`和配置 SSL]。 + +##### [](#new-messagepostprocessor-classes)新的 MessagePostProcessor 类 + +当消息内容编码设置为`deflate`时,分别添加了类`DeflaterPostProcessor`和`InflaterPostProcessor`以支持压缩和解压。 + +##### [](#other-changes)其他更改 + +`Declarables`对象(用于声明多个队列、交换、绑定)现在为每个类型都有一个过滤 getter。有关更多信息,请参见[声明交换、队列和绑定的集合](#collection-declaration)。 + +现在可以在`RabbitAdmin`处理其声明之前自定义每个`Declarable` Bean。有关更多信息,请参见[交换、队列和绑定的自动声明](#automatic-declaration)。 + +`singleActiveConsumer()`已被添加到`QueueBuilder`以设置`x-single-active-consumer`队列参数。有关更多信息,请参见[用于队列和交换的 Builder API](#builder-api)。 + +类型为`Class`的出站标头现在使用`getName()`而不是`toString()`进行映射。有关更多信息,请参见[消息属性转换器](#message-properties-converters)。 + +现在支持恢复失败的生产者创建的批处理。有关更多信息,请参见[使用批处理侦听器重试](#batch-retry)。 + +#### [](#changes-in-2-1-since-2-0)a.2.3。自 2.0 以来 2.1 的变化 + +##### [](#amqp-client-library)AMQP 客户库 + +Spring AMQP 现在使用由 RabbitMQ 团队提供的`amqp-client`库的 5.4.x 版本。默认情况下,此客户端配置了自动恢复功能。见[RabbitMQ 自动连接/拓扑恢复](#auto-recovery)。 + +| |从版本 4.0 开始,客户端默认支持自动恢复。
虽然与此功能兼容, Spring AMQP 有自己的恢复机制,并且客户端恢复功能通常不需要。
我们建议禁用`amqp-client`自动恢复,为了避免在代理可用但连接尚未恢复时获得`AutoRecoverConnectionNotCurrentlyOpenException`实例。
从 1.7.1 版本开始, Spring AMQP 禁用它,除非你显式地创建自己的 RabbitMQ 连接工厂并将其提供给`CachingConnectionFactory`。
RabbitMQ`ConnectionFactory`由`RabbitConnectionFactoryBean`创建的实例还具有默认禁用的选项。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#package-changes-2)软件包更改 + +某些类已转移到不同的包。大多数是内部类,不会影响用户应用程序。两个例外是`ChannelAwareMessageListener`和`RabbitListenerErrorHandler`。这些接口现在在`org.springframework.amqp.rabbit.listener.api`中。 + +##### [](#publisher-confirms-changes)Publisher 确认更改 + +当存在未完成的确认时,启用了发布者确认的通道不会返回到缓存中。有关更多信息,请参见[相关发布者确认并返回](#template-confirms)。 + +##### [](#listener-container-factory-improvements)监听器容器工厂改进 + +现在,你可以使用侦听器容器工厂来创建任何侦听器容器,而不仅仅是用于`@RabbitListener`注释或`@RabbitListenerEndpointRegistry`注释的侦听器容器。有关更多信息,请参见[使用集装箱工厂](#using-container-factories)。 + +`ChannelAwareMessageListener`现在继承自`MessageListener`。 + +##### [](#broker-event-listener)Broker 事件侦听器 + +引入`BrokerEventListener`以将选定的代理事件发布为`ApplicationEvent`实例。有关更多信息,请参见[代理事件监听器](#broker-events)。 + +##### [](#rabbitadmin-changes-2)RabbitAdmin 更改 + +`RabbitAdmin`发现类型为`Declarables`的 bean(这是`Declarable`-`Queue`,`Exchange`和`Binding`对象的容器)并在代理上声明所包含的对象。不鼓励用户使用声明`>`(和其他)的旧机制,而应该使用`Declarables`bean。默认情况下,旧机制将被禁用。有关更多信息,请参见[声明交换、队列和绑定的集合](#collection-declaration)。 + +`AnonymousQueue`实例现在声明为`x-queue-master-locator`,默认设置为`client-local`,以确保队列是在应用程序所连接的节点上创建的。有关更多信息,请参见[配置代理](#broker-configuration)。 + +##### [](#rabbittemplate-changes-2)RabbitTemplate 更改 + +现在,你可以使用`noLocalReplyConsumer`选项配置`RabbitTemplate`,以便在`sendAndReceive()`操作中控制用于回复消费者的`noLocal`标志。有关更多信息,请参见[请求/回复消息](#request-reply)。 + +`CorrelationData`对于发布者确认现在有一个`ListenableFuture`,你可以使用它来获得确认,而不是使用回调。当启用返回和确认时,相关数据(如果提供)将填充返回的消息。有关更多信息,请参见[相关发布者确认并返回](#template-confirms)。 + +现在提供了一个名为`replyTimedOut`的方法,用于通知子类答复已超时,从而允许进行任何状态清理。有关更多信息,请参见[回复超时](#reply-timeout)。 + +现在,你可以指定一个`ErrorHandler`,当发送回复时发生异常(例如,延迟回复)时,在使用`DirectReplyToMessageListenerContainer`(默认)请求/回复时要调用的`ErrorHandler`。参见`setReplyErrorHandler`上的`RabbitTemplate`。(也是从 2.0.11 开始)。 + +##### [](#message-conversion)消息转换 + +我们引入了一个新的`Jackson2XmlMessageConverter`,以支持将消息从 XML 格式转换为 XML 格式。有关更多信息,请参见[`Jackson2XmlMessageConverter`](#Jackson2xml)。 + +##### [](#management-rest-api-2)管理 REST API + +现在不赞成`RabbitManagementTemplate`,而赞成直接使用`com.rabbitmq.http.client.Client`(或`com.rabbitmq.http.client.ReactorNettyClient`)。有关更多信息,请参见[RabbitMQ REST API](#management-rest-api)。 + +##### [](#rabbitlistener-changes-4)`@RabbitListener`变化 + +现在可以将侦听器容器工厂配置为`RetryTemplate`,也可以在发送回复时使用`RecoveryCallback`。有关更多信息,请参见[启用监听器端点注释](#async-annotation-driven-enable)。 + +##### [](#async-rabbitlistener-return)异步`@RabbitListener`返回 + +`@RabbitListener`方法现在可以返回`ListenableFuture`或`Mono`。有关更多信息,请参见[异步`@RabbitListener`返回类型]。 + +##### [](#connection-factory-bean-changes)连接工厂 Bean 变更 + +默认情况下,`RabbitConnectionFactoryBean`现在调用`enableHostnameVerification()`。要恢复到以前的行为,请将`enableHostnameVerification`属性设置为`false`。 + +##### [](#connection-factory-changes-3)连接工厂变更 + +现在,`CachingConnectionFactory`无条件地禁用底层 RabbitMQ`ConnectionFactory`中的自动恢复,即使构造函数中提供了预先配置的实例。虽然已经采取措施使 Spring AMQP 与自动恢复兼容,但在仍然存在问题的情况下出现了某些情况。 Spring AMQP 自 1.0.0 以来已经有了自己的恢复机制,并且不需要使用由客户端提供的恢复。虽然仍然有可能在`CachingConnectionFactory`后启用该功能(使用`cachingConnectionFactory.getRabbitConnectionFactory()``.setAutomaticRecoveryEnabled()`),**我们强烈建议你不要这样做。**被构造。如果在直接使用客户端工厂(而不是使用 Spring AMQP 组件)时需要自动恢复连接,我们建议你使用单独的 RabbitMQ。 + +##### [](#listener-container-changes-2)侦听器容器更改 + +如果存在`x-death`报头,那么默认的`ConditionalRejectingErrorHandler`现在将完全丢弃导致致命错误的消息。有关更多信息,请参见[异常处理](#exception-handling)。 + +##### [](#immediate-requeue)立即请求 + +引入了一个新的`ImmediateRequeueAmqpException`来通知侦听器容器消息必须重新排队。要使用此功能,需要添加一个新的`ImmediateRequeueMessageRecoverer`实现。 + +有关更多信息,请参见[消息侦听器和异步情况](#async-listeners)。 + +#### [](#changes-in-2-0-since-1-7)a.2.4。自 1.7 以来 2.0 的变化 + +##### [](#using-cachingconnectionfactory)使用`CachingConnectionFactory` + +从版本 2.0.2 开始,你可以将`RabbitTemplate`配置为使用与侦听器容器使用的不同的连接。这一变化避免了当生产商因任何原因而受阻时,消费者陷入僵局。有关更多信息,请参见[使用单独的连接](#separate-connection)。 + +##### [](#amqp-client-library-2)AMQP 客户库 + +Spring AMQP 现在使用由 RabbitMQ 团队提供的`amqp-client`库的新的 5.0.x 版本。默认情况下,此客户端配置了自动恢复。见[RabbitMQ 自动连接/拓扑恢复](#auto-recovery)。 + +| |从版本 4.0 开始,客户端默认启用自动恢复。
虽然兼容此功能, Spring AMQP 有自己的恢复机制,并且客户端恢复功能一般不需要。
我们建议你禁用`amqp-client`自动恢复,为了避免在代理可用但连接尚未恢复时获得`AutoRecoverConnectionNotCurrentlyOpenException`实例。
从 1.7.1 版本开始, Spring AMQP 禁用它,除非你显式地创建自己的 RabbitMQ 连接工厂并将其提供给`CachingConnectionFactory`。
RabbitMQ`ConnectionFactory`由`RabbitConnectionFactoryBean`创建的实例还具有默认禁用的选项。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#general-changes)一般变化 + +现在`ExchangeBuilder`默认情况下构建持久交换。在`@QeueueBinding`中使用的`@Exchange`注释也默认声明持久交换。在`@RabbitListener`中使用的`@Queue`注释默认情况下声明持久队列(如果命名)和非持久队列(如果匿名)。有关更多信息,请参见[用于队列和交换的 Builder API](#builder-api)和[注释驱动的监听器端点](#async-annotation-driven)。 + +##### [](#deleted-classes)删除类 + +`UniquelyNameQueue`不再提供。使用唯一的名称创建持久的非自动删除队列是不常见的。这个类已被删除。如果你需要它的功能,请使用`new Queue(UUID.randomUUID().toString())`。 + +##### [](#new-listener-container)新建侦听器容器 + +已在现有的`SimpleMessageListenerContainer`旁边添加了`DirectMessageListenerContainer`。有关选择使用哪个容器以及如何配置它们的信息,请参见[选择容器](#choose-container)和[消息侦听器容器配置](#containerAttributes)。 + +##### [](#log4j-appender)log4j 附录 + +由于 log4j 的报废,此附录不再可用。有关可用日志附录的信息,请参见[日志记录子系统 AMQP 附录](#logging)。 + +##### [](#rabbittemplate-changes-3)`RabbitTemplate`变更 + +| |以前,如果一个非事务性事务`RabbitTemplate`运行在事务性侦听器容器线程上,那么它就参与了一个现有事务。,
这是一个严重的错误,
但是,用户可能依赖于这种行为,
从 1.6.2 版本开始,你必须在模板上设置`channelTransacted`布尔,才能让它参与容器事务。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`RabbitTemplate`现在使用`DirectReplyToMessageListenerContainer`(默认情况下),而不是为每个请求创建一个新的使用者。有关更多信息,请参见[RabbitMQ 直接回复](#direct-reply-to)。 + +`AsyncRabbitTemplate`现在支持直接回复。有关更多信息,请参见[异步兔子模板](#async-template)。 + +`RabbitTemplate`和`AsyncRabbitTemplate`现在有`receiveAndConvert`和`convertSendAndReceiveAsType`方法,它们接受一个`ParameterizedTypeReference`参数,让调用者指定将结果转换为哪个类型。这对于复杂类型或在消息头中未传递类型信息的情况特别有用。它需要一个`SmartMessageConverter`,如`Jackson2JsonMessageConverter`。有关更多信息,请参见[接收消息](#receiving-messages),[请求/回复消息](#request-reply),[异步兔子模板](#async-template),以及[从`Message`与`RabbitTemplate`转换](#json-complex)。 + +现在可以使用`RabbitTemplate`在专用通道上执行多个操作。有关更多信息,请参见[作用域操作](#scoped-operations)。 + +##### [](#listener-adapter)侦听器适配器 + +一个方便的`FunctionalInterface`可用于使用带有`MessageListenerAdapter`的 lambdas。有关更多信息,请参见[`MessageListenerAdapter`](#message-listener-adapter)。 + +##### [](#listener-container-changes-3)侦听器容器更改 + +###### [](#prefetch-default-value)预取默认值 + +预取默认值过去是 1,这可能导致有效消费者的利用率不足。默认的预取值现在是 250,这应该会使消费者在大多数常见的情况下都很忙,从而提高吞吐量。 + +| |在某些情况下,预取取值应该
较低,例如,对于较大的消息,尤其是在处理速度较慢的情况下(消息可能会将
累加到客户端进程中的大量内存中),如果需要严格的消息排序
(在这种情况下,预取值应该设置为 1),
还可以使用低容量消息传递和多个消费者(包括单个侦听器容器实例中的并发性),你可能希望减少预取,以使消息在消费者之间的分布更加均匀。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关 Prefetch 的更多背景信息,请参见这篇关于[RabbitMQ 中的消费者利用](https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/)的文章和这篇关于[排队论](https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/)的文章。 + +###### [](#message-count)消息计数 + +以前,对于容器发出的消息,`MessageProperties.getMessageCount()`返回`0`。此属性仅当你使用`basicGet`(例如,从`RabbitTemplate.receive()`方法)时才适用,并且现在对于容器消息初始化为`null`。 + +###### [](#transaction-rollback-behavior)事务回滚行为 + +无论是否配置了事务管理器,事务回滚上的消息重新队列现在都是一致的。有关更多信息,请参见[关于回滚收到的消息的说明](#transaction-rollback)。 + +###### [](#shutdown-behavior)关机行为 + +如果容器线程不响应`shutdownTimeout`内的关机,则默认情况下将强制关闭通道。有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +###### [](#after-receive-message-post-processors)接收消息后处理程序 + +如果`afterReceiveMessagePostProcessors`属性中的`MessagePostProcessor`返回`null`,则该消息将被丢弃(并在适当的情况下进行确认)。 + +##### [](#connection-factory-changes-4)连接工厂变更 + +连接和通道侦听器接口现在提供了一种机制来获取有关异常的信息。有关更多信息,请参见[连接和通道侦听器](#connection-channel-listeners)和[发布是异步的——如何检测成功和失败](#publishing-is-async)。 + +现在提供了一个新的`ConnectionNameStrategy`,用于从`AbstractConnectionFactory`填充目标 RabbitMQ 连接的特定于应用程序的标识。有关更多信息,请参见[连接和资源管理](#connections)。 + +##### [](#retry-changes)重试更改 + +不再提供`MissingMessageIdAdvice`。它的功能现在是内置的。有关更多信息,请参见[同步操作中的故障和重试选项](#retry)。 + +##### [](#anonymous-queue-naming)匿名队列命名 + +默认情况下,`AnonymousQueues`现在使用默认的`Base64UrlNamingStrategy`来命名,而不是简单的`UUID`字符串。有关更多信息,请参见[`AnonymousQueue`]。 + +##### [](#rabbitlistener-changes-5)`@RabbitListener`变更 + +现在可以在`@RabbitListener`注释中提供简单的队列声明(仅绑定到默认的交换)。有关更多信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。 + +你现在可以配置`@RabbitListener`注释,以便将任何异常返回给发送者。你还可以配置`RabbitListenerErrorHandler`来处理异常。有关更多信息,请参见[处理异常](#annotation-error-handling)。 + +使用`@QueueBinding`注释时,现在可以将队列与多个路由键绑定。另外`@QueueBinding.exchange()`现在支持自定义交换类型,并默认声明持久交换。 + +现在,你可以在注释级设置侦听器容器的`concurrency`,而不必为不同的并发设置配置不同的容器工厂。 + +现在可以在注释级设置侦听器容器的`autoStartup`属性,从而覆盖容器工厂中的默认设置。 + +你现在可以在`RabbitListener`容器工厂中设置接收之后和发送之前的`MessagePostProcessor`实例。 + +有关更多信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。 + +从版本 2.0.3 开始,类级`@RabbitHandler`上的一个`@RabbitListener`注释可以被指定为默认值。有关更多信息,请参见[多方法侦听器](#annotation-method-selection)。 + +##### [](#container-conditional-rollback)容器条件回滚 + +当使用外部事务管理器(例如 JDBC)时,当你为容器提供事务属性时,现在支持基于规则的回滚。现在,当你使用事务建议时,它也更加灵活。有关更多信息,请参见[条件回滚](#conditional-rollback)。 + +##### [](#remove-jackson-1-x-support)删除 Jackson1.x 支持 + +在以前的版本中不推荐的 Jackson`1.x`转换器和相关组件现在已被删除。你可以使用基于 Jackson2.x 的类似组件。有关更多信息,请参见[Jackson2JSONMessageConverter](#json-message-converter)。 + +##### [](#json-message-converter-2)JSON 消息转换器 + +当将入站 JSON 消息的`*TypeId*`设置为`Hashtable`时,现在默认的转换类型是`LinkedHashMap`。在此之前,它是`Hashtable`。要恢复到`Hashtable`,可以在`DefaultClassMapper`上使用`setDefaultMapType`。 + +##### [](#xml-parsers)XML 解析器 + +在解析`Queue`和`Exchange`XML 组件时,如果存在`id`属性,则解析器不再将`name`属性值注册为 Bean 别名。有关更多信息,请参见[关于`id`和`name`属性的注释]。 + +##### [](#blocked-connection)阻塞连接 + +现在可以将`com.rabbitmq.client.BlockedListener`注入`org.springframework.amqp.rabbit.connection.Connection`对象。此外,当连接被代理阻塞或解除锁定时,`ConnectionBlockedEvent`和`ConnectionUnblockedEvent`事件由`ConnectionFactory`发出。 + +有关更多信息,请参见[连接和资源管理](#connections)。 + +#### [](#changes-in-1-7-since-1-6)a.2.5。1.7 自 1.6 以来的变化 + +##### [](#amqp-client-library-3)AMQP 客户库 + +Spring AMQP 现在使用由 RabbitMQ 团队提供的`amqp-client`库的新的 4.0.x 版本。默认情况下,此客户端配置了自动恢复功能。见[RabbitMQ 自动连接/拓扑恢复](#auto-recovery)。 + +| |4.0.x 客户端默认支持自动恢复。
虽然与此功能兼容, Spring AMQP 有自己的恢复机制,并且客户端恢复功能通常不需要。
我们建议禁用`amqp-client`自动恢复,为了避免在代理可用但连接尚未恢复时获得`AutoRecoverConnectionNotCurrentlyOpenException`实例。
从 1.7.1 版本开始, Spring AMQP 禁用它,除非你显式地创建自己的 RabbitMQ 连接工厂,并将其提供给`CachingConnectionFactory`。
RabbitMQ`ConnectionFactory`由`RabbitConnectionFactoryBean`创建的实例还具有默认禁用的选项。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#log4j-2-upgrade)log4j2 升级 + +最小的 log4j2 版本(对于`AmqpAppender`)现在是`2.7`。该框架不再与以前的版本兼容。有关更多信息,请参见[日志记录子系统 AMQP 附录](#logging)。 + +##### [](#logback-appender-2)翻录附录 + +默认情况下,此附录不再捕获调用方数据(方法、行号)。你可以通过设置`includeCallerData`配置选项重新启用它。有关可用日志附录的信息,请参见[日志记录子系统 AMQP 附录](#logging)。 + +##### [](#spring-retry-upgrade) Spring 重试升级 + +重试的最低版本现在是`1.2`。该框架不再与以前的版本兼容。 + +###### [](#shutdown-behavior-2)关机行为 + +你现在可以将`forceCloseChannel`设置为`true`,这样,如果容器线程在`shutdownTimeout`内没有响应关机,则将强制关闭通道,从而导致任何未加锁的消息重新排队。有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#fasterxml-jackson-upgrade)fasterxml Jackson 升级 + +Jackson 的最低版本现在是`2.8`。该框架不再与以前的版本兼容。 + +##### [](#junit-rules-2)JUnit`@Rules` + +以前由框架内部使用的规则现在可以在一个名为`spring-rabbit-junit`的单独 JAR 中使用。有关更多信息,请参见[JUnit4`@Rules`]。 + +##### [](#container-conditional-rollback-2)容器条件回滚 + +当你使用外部事务管理器(例如 JDBC)时,当你为容器提供事务属性时,现在支持基于规则的回滚。现在,当你使用事务建议时,它也更加灵活。 + +##### [](#connection-naming-strategy)连接命名策略 + +现在提供了一个新的`ConnectionNameStrategy`,用于从`AbstractConnectionFactory`填充目标 RabbitMQ 连接的特定于应用程序的标识。有关更多信息,请参见[连接和资源管理](#connections)。 + +##### [](#listener-container-changes-4)侦听器容器更改 + +###### [](#transaction-rollback-behavior-2)事务回滚行为 + +现在,无论是否配置了事务管理器,都可以将事务回滚上的消息重新排队配置为一致。有关更多信息,请参见[关于回滚收到的消息的说明](#transaction-rollback)。 + +#### [](#earlier-releases)a.2.6。早期版本 + +有关以前版本的更改,请参见[以前的版本](#previous-whats-new)。 + +#### [](#changes-in-1-6-since-1-5)a.2.7。自 1.5 以来 1.6 的变化 + +##### [](#testing-support)测试支持 + +现在提供了一个新的测试支持库。有关更多信息,请参见[测试支持](#testing)。 + +##### [](#builder)建设者 + +现在可以使用为配置`Queue`和`Exchange`对象提供流畅 API 的构建器。有关更多信息,请参见[用于队列和交换的 Builder API](#builder-api)。 + +##### [](#namespace-changes)名称空间变更 + +###### [](#connection-factory-2)连接工厂 + +现在可以将`thread-factory`添加到连接工厂 Bean 声明中——例如,用于命名由`amqp-client`库创建的线程。有关更多信息,请参见[连接和资源管理](#connections)。 + +当你使用`CacheMode.CONNECTION`时,现在可以限制允许的连接总数。有关更多信息,请参见[连接和资源管理](#connections)。 + +###### [](#queue-definitions)队列定义 + +现在可以为匿名队列提供命名策略。有关更多信息,请参见[`AnonymousQueue`]。 + +##### [](#listener-container-changes-5)侦听器容器更改 + +###### [](#idle-message-listener-detection)空闲消息侦听器检测 + +现在可以将侦听器容器配置为在空闲时发布`ApplicationEvent`实例。有关更多信息,请参见[检测空闲异步消费者](#idle-containers)。 + +###### [](#mismatched-queue-detection)不匹配队列检测 + +默认情况下,当侦听器容器启动时,如果检测到具有不匹配属性或参数的队列,则容器会记录异常,但会继续侦听。容器现在具有一个名为`mismatchedQueuesFatal`的属性,如果在启动过程中检测到问题,该属性将阻止容器(和上下文)启动。如果稍后检测到问题,例如从连接失败中恢复后,它还会停止容器。有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +###### [](#listener-container-logging)侦听器容器日志记录 + +现在,侦听器容器将其`beanName`提供给内部`SimpleAsyncTaskExecutor`作为`threadNamePrefix`。它对日志分析很有用。 + +###### [](#default-error-handler)默认错误处理程序 + +默认的错误处理程序(`ConditionalRejectingErrorHandler`)现在认为不可恢复的`@RabbitListener`异常是致命的。有关更多信息,请参见[异常处理](#exception-handling)。 + +##### [](#autodeclare-and-rabbitadmin-instances)`AutoDeclare`和`RabbitAdmin`实例 + +有关在应用程序上下文中使用`RabbitAdmin`实例对该选项语义的一些更改,请参见[消息侦听器容器配置](#containerAttributes)(`autoDeclare`)。 + +##### [](#amqptemplate-receive-with-timeout)`AmqpTemplate`:超时接收 + +已经为`AmqpTemplate`及其`RabbitTemplate`实现引入了许多带有`timeout`的新`receive()`方法。有关更多信息,请参见[轮询消费者](#polling-consumer)。 + +##### [](#using-asyncrabbittemplate)使用`AsyncRabbitTemplate` + +引入了一个新的`AsyncRabbitTemplate`。此模板提供了许多发送和接收方法,其中返回值为`ListenableFuture`,以后可以使用它同步或异步地获得结果。有关更多信息,请参见[异步兔子模板](#async-template)。 + +##### [](#rabbittemplate-changes-4)`RabbitTemplate`变化 + +1.4.1 引入了在代理支持[直接回复](https://www.rabbitmq.com/direct-reply-to.html)时使用[直接回复](https://www.rabbitmq.com/direct-reply-to.html)的能力。这比为每个回复使用临时队列更有效。此版本允许你通过将`useTemporaryReplyQueues`属性设置为`true`来覆盖此默认行为并使用临时队列。有关更多信息,请参见[RabbitMQ 直接回复](#direct-reply-to)。 + +`RabbitTemplate`现在支持`user-id-expression`(在使用 Java 配置时`userIdExpression`)。有关更多信息,请参见[经过验证的用户 ID RabbitMQ 文档](https://www.rabbitmq.com/validated-user-id.html)和[已验证的用户 ID](#template-user-id)。 + +##### [](#message-properties)消息属性 + +###### [](#using-correlationid)使用`CorrelationId` + +消息属性`correlationId`现在可以是`String`。有关更多信息,请参见[消息属性转换器](#message-properties-converters)。 + +###### [](#long-string-headers)长字符串标题 + +以前,`DefaultMessagePropertiesConverter`将比长字符串限制(缺省 1024)更长的头转换为`DataInputStream`(实际上,它引用了`LongString`实例的`DataInputStream`)。在输出时,这个头没有被转换(除了转换为字符串——例如,通过在流上调用`toString()`,`[[email protected]](/cdn-cgi/l/email-protection)`)。 + +在此版本中,长`LongString`实例现在默认保留为`LongString`实例。你可以使用`getBytes[]`、`toString()`或`getStream()`方法访问内容。大量输入的`LongString`现在也可以在输出上正确地“转换”。 + +有关更多信息,请参见[消息属性转换器](#message-properties-converters)。 + +###### [](#inbound-delivery-mode)入站交付模式 + +`deliveryMode`属性不再映射到`MessageProperties.deliveryMode`。如果使用相同的`MessageProperties`对象发送出站消息,则此更改将避免意外传播。相反,入站`deliveryMode`标头被映射到`MessageProperties.receivedDeliveryMode`。 + +有关更多信息,请参见[消息属性转换器](#message-properties-converters)。 + +当使用带注释的端点时,标题提供在名为`AmqpHeaders.RECEIVED_DELIVERY_MODE`的标题中。 + +有关更多信息,请参见[带注释的端点方法签名](#async-annotation-driven-enable-signature)。 + +###### [](#inbound-user-id)入站用户 ID + +`user_id`属性不再映射到`MessageProperties.userId`。如果使用相同的`MessageProperties`对象发送出站消息,则此更改将避免意外传播。相反,入站`userId`头被映射到`MessageProperties.receivedUserId`。 + +有关更多信息,请参见[消息属性转换器](#message-properties-converters)。 + +当你使用带注释的端点时,标题将在名为`AmqpHeaders.RECEIVED_USER_ID`的标题中提供。 + +有关更多信息,请参见[带注释的端点方法签名](#async-annotation-driven-enable-signature)。 + +##### [](#rabbitadmin-changes-3)`RabbitAdmin`变化 + +###### [](#declaration-failures)宣告失败 + +以前,`ignoreDeclarationFailures`标志仅对通道上的`IOException`生效(例如不匹配的参数)。它现在对任何例外情况生效(例如`TimeoutException`)。此外,现在只要声明失败,就会发布`DeclarationExceptionEvent`。`RabbitAdmin`Last declaration 事件也可以作为属性`lastDeclarationExceptionEvent`使用。有关更多信息,请参见[配置代理](#broker-configuration)。 + +##### [](#rabbitlistener-changes-6)`@RabbitListener`变更 + +###### [](#multiple-containers-for-each-bean)每个容器有多个 Bean + +当你使用 Java8 或更高版本时,你现在可以将多个`@RabbitListener`注释添加到`@Bean`类或它们的方法中。当使用 Java7 或更早版本时,你可以使用`@RabbitListeners`容器注释来提供相同的功能。有关更多信息,请参见[`@Repeatable``@RabbitListener`]。 + +###### [](#sendto-spel-expressions)`@SendTo`spel 表达式 + +`@SendTo`用于路由不带`replyTo`属性的回复,现在可以根据请求/回复计算 SPEL 表达式。有关更多信息,请参见[回复管理](#async-annotation-driven-reply)。 + +###### [](#queuebinding-improvements)`@QueueBinding`改进 + +现在可以在`@QueueBinding`注释中指定队列、交换和绑定的参数。现在`@QueueBinding`支持报头交换。有关更多信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。 + +##### [](#delayed-message-exchange-2)延迟消息交换 + +Spring AMQP 现在拥有对 RabbitMQ 延迟消息交换插件的一流支持。有关更多信息,请参见[延迟的消息交换](#delayed-message-exchange)。 + +##### [](#exchange-internal-flag)交易所内部标志 + +任何`Exchange`定义现在都可以标记为`internal`,并且`RabbitAdmin`在声明交易所时将该值传递给代理。有关更多信息,请参见[配置代理](#broker-configuration)。 + +##### [](#cachingconnectionfactory-changes)`CachingConnectionFactory`变化 + +###### [](#cachingconnectionfactory-cache-statistics)`CachingConnectionFactory`缓存统计信息 + +`CachingConnectionFactory`现在在运行时和 JMX 上提供缓存属性。有关更多信息,请参见[运行时缓存属性](#runtime-cache-properties)。 + +###### [](#accessing-the-underlying-rabbitmq-connection-factory)访问底层 RabbitMQ 连接工厂 + +添加了一个新的 getter 以提供对底层工厂的访问。例如,你可以使用此 getter 添加自定义连接属性。有关更多信息,请参见[添加自定义客户端连接属性](#custom-client-props)。 + +###### [](#channel-cache)通道缓存 + +默认通道缓存大小已从 1 增加到 25。有关更多信息,请参见[连接和资源管理](#connections)。 + +此外,`SimpleMessageListenerContainer`不再调整缓存大小,使其至少与`concurrentConsumers`的数量一样大——这是多余的,因为容器使用者通道永远不会被缓存。 + +##### [](#using-rabbitconnectionfactorybean)使用`RabbitConnectionFactoryBean` + +工厂 Bean 现在公开一个属性,以便将客户机连接属性添加到由结果工厂创建的连接中。 + +##### [](#java-deserialization-2)Java 反序列化 + +现在,你可以在使用 Java 反序列化时配置一个允许类的“允许列表”。如果你接受来自不受信任的源的带有序列化 Java 对象的消息,那么你应该考虑创建一个允许的列表。有关更多信息,请参见[Java 反序列化](#java-deserialization)。 + +##### [](#json-messageconverter)JSON `MessageConverter` + +对 JSON 消息转换器的改进现在允许使用消息头中没有类型信息的消息。有关更多信息,请参见[带注释方法的消息转换](#async-annotation-conversion)和[Jackson2JSONMessageConverter](#json-message-converter)。 + +##### [](#logging-appenders)日志附录 + +###### [](#log4j-2)log4j2 + +添加了一个 log4j2Appender,现在可以将 Appenders 配置为`addresses`属性,以连接到代理群集。 + +###### [](#client-connection-properties)客户端连接属性 + +现在可以将自定义客户端连接属性添加到 RabbitMQ 连接中。 + +有关更多信息,请参见[日志记录子系统 AMQP 附录](#logging)。 + +#### [](#changes-in-1-5-since-1-4)a.2.8。1.5 自 1.4 以来的变化 + +##### [](#spring-erlang-is-no-longer-supported)`spring-erlang`不再支持 + +`spring-erlang`JAR 不再包含在分发版中。用[RabbitMQ REST API](#management-rest-api)代替。 + +##### [](#cachingconnectionfactory-changes-2)`CachingConnectionFactory`变化 + +###### [](#empty-addresses-property-in-cachingconnectionfactory)中的空地址属性`CachingConnectionFactory` + +以前,如果连接工厂配置了主机和端口,但也为`addresses`提供了一个空字符串,则会忽略主机和端口。现在,一个空的`addresses`字符串被处理为与`null`相同的字符串,并且使用了主机和端口。 + +###### [](#uri-constructor)URI 构造函数 + +`CachingConnectionFactory`有一个附加的构造函数,带有`URI`参数,用于配置代理连接。 + +###### [](#connection-reset)连接重置 + +添加了一个名为`resetConnection()`的新方法,让用户重置连接(或连接)。例如,你可以使用它在失败转移到次要代理之后重新连接到主代理。这**是吗?**会影响进程内操作。现有的`destroy()`方法做的完全相同,但是新方法的名称不那么令人生畏。 + +##### [](#properties-to-control-container-queue-declaration-behavior)控制容器队列声明行为的属性 + +当监听器容器使用者启动时,他们尝试被动地声明队列,以确保它们在代理上可用。在此之前,如果这些声明失败(例如,因为队列不存在),或者当一个 HA 队列被移动时,重试逻辑被固定为以五秒为间隔的三次重试尝试。如果队列仍然不存在,则行为由`missingQueuesFatal`属性控制(默认:`true`)。此外,对于配置为监听多个队列的容器,如果只有一个队列子集可用,那么使用者将在 60 秒的固定时间间隔内重试丢失的队列。 + +`declarationRetries`、`failedDeclarationRetryInterval`和`retryDeclarationInterval`属性现在是可配置的。有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#class-package-change)类包更改 + +将`RabbitGatewaySupport`类从`o.s.amqp.rabbit.core.support`移动到`o.s.amqp.rabbit.core`。 + +##### [](#defaultmessagepropertiesconverter-changes)`DefaultMessagePropertiesConverter`变化 + +你现在可以配置`DefaultMessagePropertiesConverter`以确定将`LongString`转换为`String`而不是转换为`DataInputStream`的最大长度。转换器有一个替代的构造函数,它将值作为一个限制。以前,这个限制是以`1024`字节进行硬编码的。(也可在 1.4.4 中获得)。 + +##### [](#rabbitlistener-improvements)`@RabbitListener`改进 + +###### [](#queuebinding-for-rabbitlistener)`@QueueBinding`表示`@RabbitListener` + +`bindings`属性已被添加到`@RabbitListener`注释中,作为与`queues`属性的互斥,以允许`queue`的规范,其`exchange`和`binding`用于代理上的`RabbitAdmin`声明。 + +###### [](#spel-in-sendto)spel in`@SendTo` + +`@RabbitListener`的默认回复地址(`@SendTo`)现在可以是 SPEL 表达式。 + +###### [](#multiple-queue-names-through-properties)通过属性获得多个队列名称 + +现在可以使用 SPEL 和属性占位符的组合来为侦听器指定多个队列。 + +有关更多信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。 + +##### [](#automatic-exchange-queue-and-binding-declaration)自动交换、队列和绑定声明 + +现在可以声明定义这些实体集合的 bean,并且`RabbitAdmin`将内容添加到它在建立连接时声明的实体列表中。有关更多信息,请参见[声明交换、队列和绑定的集合](#collection-declaration)。 + +##### [](#rabbittemplate-changes-5)`RabbitTemplate`变更 + +###### [](#reply-address-added)`reply-address`已添加 + +已将`reply-address`属性添加到``组件中,作为替代`reply-queue`。有关更多信息,请参见[请求/回复消息](#request-reply)。(也可以在 1.4.4 中作为`RabbitTemplate`上的 setter)。 + +###### [](#blocking-receive-methods)阻塞`receive`方法 + +`RabbitTemplate`现在支持`receive`和`convertAndReceive`方法中的阻塞。有关更多信息,请参见[轮询消费者](#polling-consumer)。 + +###### [](#mandatory-with-sendandreceive-methods)强制使用`sendAndReceive`方法 + +当使用`mandatory`和`convertSendAndReceive`方法设置`mandatory`标志时,如果无法传递请求消息,则调用线程将抛出`AmqpMessageReturnedException`。有关更多信息,请参见[回复超时](#reply-timeout)。 + +###### [](#improper-reply-listener-configuration)不正确的应答侦听器配置 + +当使用命名的应答队列时,框架将尝试验证应答侦听器容器的正确配置。 + +有关更多信息,请参见[回复监听器容器](#reply-listener)。 + +##### [](#rabbitmanagementtemplate-added)`RabbitManagementTemplate`已添加 + +引入了`RabbitManagementTemplate`,通过使用其[管理插件 Name](https://www.rabbitmq.com/management.html)提供的 REST API 来监视和配置 RabbitMQ 代理。有关更多信息,请参见[RabbitMQ REST API](#management-rest-api)。 + +##### [](#listener-container-bean-names-xml)侦听器容器 Bean 名称 + +| |``元素上的`id`属性已被删除。
从这个版本开始,单独使用``子元素上的`id`来命名为每个侦听器元素创建的侦听器容器 Bean。将应用

正常的 Spring Bean 名称重载。
如果稍后的``与现有的 Bean 相同的`id`进行解析,新的定义覆盖了现有的定义。
以前, Bean 名称是由`id`和``元素的``属性组成的。
当迁移到此版本时,如果你的`id`元素上有`id`属性,删除它们并在子元素``上设置`id`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +但是,为了支持作为一个组开始和停止容器,添加了一个新的`group`属性。定义此属性时,由此元素创建的容器将添加到具有此名称的 Bean,类型为`Collection`。你可以迭代这个组来启动和停止容器。 + +##### [](#class-level-rabbitlistener)class-level`@RabbitListener` + +现在可以在类级别应用`@RabbitListener`注释。与新的`@RabbitHandler`方法注释一起,这允许你基于有效负载类型选择处理程序方法。有关更多信息,请参见[多方法侦听器](#annotation-method-selection)。 + +##### [](#simplemessagelistenercontainer-backoff-support)`SimpleMessageListenerContainer`:Backoff 支持 + +现在可以为`SimpleMessageListenerContainer`启动恢复提供`BackOff`实例。有关更多信息,请参见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#channel-close-logging-2)通道关闭日志记录 + +介绍了一种控制信道关闭日志级别的机制。见[记录通道关闭事件](#channel-close-logging)。 + +##### [](#application-events)应用程序事件 + +现在,当使用者失败时,`SimpleMessageListenerContainer`将发出应用程序事件。有关更多信息,请参见[消费者活动](#consumer-events)。 + +##### [](#consumer-tag-configuration)消费者标签配置 + +以前,异步消费者的消费者标记是由代理生成的。通过这个版本,现在可以向侦听器容器提供命名策略。见[消费者标签](#consumerTags)。 + +##### [](#using-messagelisteneradapter)使用`MessageListenerAdapter` + +`MessageListenerAdapter`现在支持队列名称(或使用者标记)到方法名称的映射,以根据接收消息的队列确定调用哪个委托方法。 + +##### [](#localizedqueueconnectionfactory-added)`LocalizedQueueConnectionFactory`已添加 + +`LocalizedQueueConnectionFactory`是一个新的连接工厂,它连接到集群中实际驻留镜像队列的节点。 + +参见[Queue Affinity 和`LocalizedQueueConnectionFactory`]。 + +##### [](#anonymous-queue-naming-2)匿名队列命名 + +从版本 1.5.3 开始,你现在可以控制`AnonymousQueue`名称的生成方式。有关更多信息,请参见[`AnonymousQueue`]。 + +#### [](#changes-in-1-4-since-1-3)a.2.9。自 1.3 以来 1.4 的变化 + +##### [](#rabbitlistener-annotation)`@RabbitListener`注释 + +POJO 侦听器可以使用`@RabbitListener`进行注释,也可以使用`@EnableRabbit`或``。 Spring 此功能需要框架 4.1。有关更多信息,请参见[注释驱动的监听器端点](#async-annotation-driven)。 + +##### [](#rabbitmessagingtemplate-added)`RabbitMessagingTemplate`已添加 + +一个新的`RabbitMessagingTemplate`允许你通过使用`spring-messaging``Message`实例与 RabbitMQ 进行交互。在内部,它使用`RabbitTemplate`,你可以将其配置为常规配置。 Spring 此功能需要框架 4.1。有关更多信息,请参见[消息传递集成](#template-messaging)。 + +##### [](#listener-container-missingqueuesfatal-attribute)侦听器容器`missingQueuesFatal`属性 + +1.3.5 在`SimpleMessageListenerContainer`上引入了`missingQueuesFatal`属性。这现在在侦听器容器名称空间元素上可用。见[消息侦听器容器配置](#containerAttributes)。 + +##### [](#rabbittemplate-confirmcallback-interface)RabbitTemplate`ConfirmCallback`接口 + +这个接口上的`confirm`方法有一个额外的参数,称为`cause`。当可用时,此参数包含否定确认的原因。见[相关发布者确认并返回](#template-confirms)。 + +##### [](#rabbitconnectionfactorybean-added)`RabbitConnectionFactoryBean`已添加 + +`RabbitConnectionFactoryBean`创建由`CachingConnectionFactory`使用的底层 RabbitMQ`ConnectionFactory`。这允许使用 Spring 的依赖注入配置 SSL 选项。见[配置底层客户机连接工厂](#connection-factory)。 + +##### [](#using-cachingconnectionfactory-2)使用`CachingConnectionFactory` + +现在,`CachingConnectionFactory`允许将`connectionTimeout`设置为名称空间中的一个属性或属性。它在底层 RabbitMQ`ConnectionFactory`上设置属性。见[配置底层客户机连接工厂](#connection-factory)。 + +##### [](#log-appender)日志附录 + +已引入了注销`org.springframework.amqp.rabbit.logback.AmqpAppender`。它提供了类似于`org.springframework.amqp.rabbit.log4j.AmqpAppender`的选项。有关更多信息,请参见这些类的 Javadoc。 + +log4j`AmqpAppender`现在支持`deliveryMode`属性(`PERSISTENT`或`NON_PERSISTENT`,缺省:`PERSISTENT`)。以前,所有的 log4j 消息都是`PERSISTENT`。 + +Appender 还支持在发送前修改`Message`——例如,允许添加自定义标头。子类应该覆盖`postProcessMessageBeforeSend()`。 + +##### [](#listener-queues-2)侦听器队列 + +现在,默认情况下,侦听器容器会在启动过程中重新声明任何丢失的队列。已向``添加了一个新的`auto-declare`属性,以防止这些重新声明。参见[`auto-delete`队列]。 + +##### [](#rabbittemplate-mandatory-and-connectionfactoryselector-expressions)`RabbitTemplate`:`mandatory`和`connectionFactorySelector`表达式 + +而`mandatoryExpression`,`sendConnectionFactorySelectorExpression`和`receiveConnectionFactorySelectorExpression`spel 表达式`s properties have been added to `RabbitTemplate`. The `mandatoryexpression` is used to evaluate a `强制` boolean value against each request message when a `returncall` is in use. See [Correlated Publisher Confirms and Returns](#template-confirms). The `sendConnectionygt=“factorexpression<4214"/>factortortoryr=“actorpressionmentfactortortorypresslausion`的``现在支持解析``子元素。现在,你可以使用`key/value`属性对配置``中的``(以便在单个标头上进行匹配)或使用``子元素(允许在多个标头上进行匹配)。这些选择是相互排斥的。见[头交换](#headers-exchange)。 + +##### [](#routing-connection-factory-2)路由连接工厂 + +引入了一个新的`SimpleRoutingConnectionFactory`。它允许配置`ConnectionFactories`映射,以确定在运行时使用的目标`ConnectionFactory`。见[路由连接工厂](#routing-connection-factory)。 + +##### [](#messagebuilder-and-messagepropertiesbuilder)`MessageBuilder`和`MessagePropertiesBuilder` + +现在提供了用于构建消息或消息属性的“Fluent API”。见[Message Builder API](#message-builder)。 + +##### [](#retryinterceptorbuilder-change)`RetryInterceptorBuilder`变化 + +现在提供了用于构建侦听器容器重试拦截器的“Fluent API”。见[同步操作中的故障和重试选项](#retry)。 + +##### [](#republishmessagerecoverer-added)`RepublishMessageRecoverer`已添加 + +提供了这个新的`MessageRecoverer`,以允许在重试用完时将失败的消息发布到另一个队列(包括消息头中的堆栈跟踪信息)。见[消息侦听器和异步情况](#async-listeners)。 + +##### [](#default-error-handler-since-1-3-2)默认错误处理程序(自 1.3.2 起) + +已将默认的`ConditionalRejectingErrorHandler`添加到侦听器容器中。此错误处理程序检测到致命的消息转换问题,并指示容器拒绝该消息,以防止代理继续重新交付不可转换的消息。见[异常处理](#exception-handling)。 + +##### [](#listener-container-missingqueuesfatal-property-since-1-3-5)侦听器容器“missingqueuesfatal”属性(自 1.3.5) + +`SimpleMessageListenerContainer`现在有一个名为`missingQueuesFatal`的属性(默认值:`true`)。以前,排不上队总是致命的。见[消息侦听器容器配置](#containerAttributes)。 + +#### [](#changes-to-1-2-since-1-1)a.2.11。自 1.1 以来对 1.2 的更改 + +##### [](#rabbitmq-version)RabbitMQ 版本 + +Spring AMQP 现在默认使用 RabbitMQ3.1.x(但保留了与早期版本的兼容性)。对于 RabbitMQ3.1.x——联邦交换和`RabbitTemplate`上的`immediate`属性不再支持的特性,已经添加了某些异议。 + +##### [](#rabbit-admin-2)兔子管理员 + +`RabbitAdmin`现在提供了一个选项,可以在声明失败时让 Exchange、Queue 和 binding 声明继续。以前,所有的声明都是在失败时停止的。通过设置`ignore-declaration-exceptions`,将记录此类异常(在`WARN`级别),但会继续进行进一步的声明。这可能有用的一个例子是,当队列声明失败时,原因是一个稍微不同的`ttl`设置,该设置通常会阻止其他声明继续进行。 + +`RabbitAdmin`现在提供了一个名为`getQueueProperties()`的附加方法。你可以使用它来确定代理上是否存在队列(对于不存在的队列,返回`null`)。此外,它返回队列中当前消息的数量以及当前消费者的数量。 + +##### [](#rabbit-template)兔子模板 + +以前,当`…​sendAndReceive()`方法与固定的应答队列一起使用时,将使用两个自定义标头来进行相关数据以及保留和恢复应答队列信息。在此版本中,默认情况下使用标准消息属性(`correlationId`),尽管你可以指定要使用的自定义属性。此外,嵌套的`replyTo`信息现在被保留在模板内部,而不是使用自定义标头。 + +`immediate`属性已弃用。在使用 RabbitMQ3.0.x 或更高版本时,不能设置此属性。 + +##### [](#json-message-converters)JSON 消息转换器 + +现在提供了一个 Jackson2.x`MessageConverter`,以及使用 Jackson1.x 的现有转换器。 + +##### [](#automatic-declaration-of-queues-and-other-items)队列和其他项的自动声明 + +以前,在声明队列、交换和绑定时,你无法定义声明使用哪个连接工厂。每个`RabbitAdmin`通过使用其连接声明所有组件。 + +从这个版本开始,你现在可以将声明限制为特定的`RabbitAdmin`实例。见[有条件声明](#conditional-declaration)。 + +##### [](#amqp-remoting)AMQP Remoting + +现在提供了用于使用 Spring 远程技术的设施,使用 AMQP 作为 RPC 调用的传输。有关更多信息,请参见[Spring Remoting with AMQP](#remoting) + +##### [](#requested-heart-beats)请求心跳 + +一些用户要求在 Spring AMQP`CachingConnectionFactory`上公开底层客户机连接工厂的`requestedHeartBeats`属性。这是现在可用的。在此之前,有必要将 AMQP 客户端工厂配置为单独的 Bean,并在`CachingConnectionFactory`中提供对它的引用。 + +#### [](#changes-to-1-1-since-1-0)a.2.12。自 1.0 以来对 1.1 的更改 + +##### [](#general)一般 + +Spring-AMQP 现在是用 Gradle 构建的。 + +添加对 Publisher 确认和返回的支持。 + +添加对 HA 队列和代理故障转移的支持。 + +增加了对死信交换和死信队列的支持。 + +##### [](#amqp-log4j-appender)AMQP log4j Appender + +添加一个选项,以支持将消息 ID 添加到已记录的消息中。 + +添加一个选项,允许在将`String`转换为`byte[]`时使用`Charset`名称的规范。 + +版本 2.4.2 最后更新时间:2022 年 01 月 16 日 15:18:50UTC + +hljs.inithighlighting()if{{{i[’GoogleAnalyticsObject’]=r;i[r]=i[r]||function(){(i[r].q=i[r].q|[]).push(参数)},i[r].l=1\*new date();a=s.createelement(o),m.getelementsname(o)[0];a.asymentsname=1;a.SRC=g.node=getname;google(a.com=node=gleanalytnode,’,’’docume’,’’’document’,’ \ No newline at end of file diff --git a/docs/spring-batch/appendix.md b/docs/spring-batch/appendix.md new file mode 100644 index 0000000..8ed32ff --- /dev/null +++ b/docs/spring-batch/appendix.md @@ -0,0 +1,48 @@ +## [](#listOfReadersAndWriters)附录 A:条目阅读器和条目编写器列表 + +### [](#itemReadersAppendix)条目阅读器 + +| Item Reader |说明| +|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|AbstractItemCountingItemStreamItemReader|抽象基类,通过计算从
和`ItemReader`返回的项数,提供基本的
重启功能。| +| AggregateItemReader |一个`ItemReader`提供一个列表作为其
项,存储来自注入的`ItemReader`的对象,直到它们
准备好作为集合打包。必须将这个类
用作自定义`ItemReader`的包装器,该包装器可以标识记录
的边界。自定义读取器应该通过返回一个`AggregateItem`来标记
记录的开始和结束,它将对其`true`查询方法
和`isFooter()`进行响应。请注意,这个阅读器
不是 Spring 批
提供的阅读器库的一部分,而是作为`spring-batch-samples`中的示例给出的。| +| AmqpItemReader |给定一个 Spring `AmqpTemplate`,它提供了
同步接收方法。`receiveAndConvert()`方法
允许你接收 POJO 对象。| +| KafkaItemReader |从 Apache Kafka 主题读取消息的`ItemReader`。
可以将其配置为从同一主题的多个分区读取消息。
此阅读器在执行上下文中存储消息偏移量,以支持重新启动功能。| +| FlatFileItemReader |从平面文件读取。包括`ItemStream`和`Skippable`功能。参见[`FlatFileItemReader`]。| +| HibernateCursorItemReader |基于 HQL 查询从游标读取。参见[`Cursor-based ItemReaders`]。| +| HibernatePagingItemReader |从分页的 HQL 查询中读取| +| ItemReaderAdapter |将任何类调整为`ItemReader`接口。| +| JdbcCursorItemReader |通过 JDBC 从数据库游标读取数据。参见[`Cursor-based ItemReaders`]。| +| JdbcPagingItemReader |给定一个 SQL 语句,在行中进行分页
,这样就可以在不耗尽
内存的情况下读取大型数据集。| +| JmsItemReader |给定一个 Spring `JmsOperations`对象和一个 JMS
向其发送错误的目的地或目的地名称,提供通过注入的项
接收到的`JmsOperations#receive()`方法。| +| JpaPagingItemReader |给定一个 JPQL 语句,通过
行进行分页,这样就可以在不耗尽
内存的情况下读取大型数据集。| +| ListItemReader |提供列表中的项,在
时间提供一个列表。| +| MongoItemReader |给定一个`MongoOperations`对象和一个基于 JSON 的 MongoDB
查询,提供从`MongoOperations#find()`方法接收的项。| +| Neo4jItemReader |给定一个`Neo4jOperations`对象和一个
Cyhper 查询的组件,返回的项是 NEO4jOperations.query
方法的结果。| +| RepositoryItemReader |给定一个 Spring data`PagingAndSortingRepository`对象,
a`Sort`,以及要执行的方法的名称,返回由
Spring 数据存储库实现的项。| +| StoredProcedureItemReader |从执行数据库存储过程的
所产生的数据库游标读取数据。参见[`StoredProcedureItemReader`]| +| StaxEventItemReader |通过 stax 进行读取,参见[`StaxEventItemReader`]。| +| JsonItemReader |从 JSON 文档中读取项目。参见[`JsonItemReader`]。| + +### [](#itemWritersAppendix)条目编写者 + +| Item Writer |说明| +|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| AbstractItemStreamItemWriter |组合`ItemStream`和`ItemWriter`接口的抽象基类。| +| AmqpItemWriter |给定 Spring `AmqpTemplate`,它为同步`send`方法提供了
。`convertAndSend(Object)`方法允许你发送 POJO 对象。| +| CompositeItemWriter |在注入`List`的`ItemWriter`对象中,将一个项传递给每个
的`write`方法。| +| FlatFileItemWriter |写入平面文件。包括`ItemStream`和
可跳过的功能。参见[`FlatFileItemWriter`]。| +| GemfireItemWriter |使用`GemfireOperations`对象,可以根据 delete
标志的配置从 Gemfire 实例中写入
项。| +| HibernateItemWriter |这个条目编写器 Hibernate-会话知道
,并处理一些与事务相关的工作,而非“ Hibernate-知道”
的条目编写器不需要了解这些工作,然后将
委托给另一个条目编写器来进行实际的写作。| +| ItemWriterAdapter |将任何类调整为`ItemWriter`接口。| +| JdbcBatchItemWriter |使用来自`PreparedStatement`(如果可用)的批处理功能,并且可以
在`flush`期间采取基本步骤来定位故障。| +| JmsItemWriter |使用`JmsOperations`对象,通过`JmsOperations#convertAndSend()`方法将项
写入默认队列。| +| JpaItemWriter |这个条目编写器是 JPA EntityManager-aware 的
,并处理一些与事务相关的工作,而非“ JPA-aware”`ItemWriter`不需要了解这些工作,并且
然后将其委托给另一个编写器来进行实际的编写。| +| KafkaItemWriter |使用`KafkaTemplate`对象,通过`KafkaTemplate#sendDefault(Object, Object)`方法将项写入默认主题,并使用`Converter`来映射该项的键。
还可以配置一个 delete 标志,以将 delete 事件发送到该主题。| +| MimeMessageItemWriter |使用 Spring 的`JavaMailSender`,类型`MimeMessage`的项作为邮件发送。| +| MongoItemWriter |给定一个`MongoOperations`对象,通过`MongoOperations.save(Object)`方法写项目
。实际的写入被延迟
,直到事务提交之前的最后一个可能时刻。| +| Neo4jItemWriter |给定一个`Neo4jOperations`对象,项目将通过`save(Object)`方法持久化,或者根据`ItemWriter’s`配置通过`delete(Object)`方法删除。| +|PropertyExtractingDelegatingItemWriter|扩展`AbstractMethodInvokingDelegator`动态创建参数。参数是通过从要处理的项中的字段(通过`SpringBeanWrapper`)检索
中的值来创建的,基于注入的字段
名称数组。| +| RepositoryItemWriter |给定一个 Spring 数据`CrudRepository`的实现,
项是通过在配置中指定的方法保存的。| +| StaxEventItemWriter |使用`Marshaller`实现
将每个项转换为 XML,然后使用
stax 将其写入 XML 文件。| +|jsonfileitemwriter| 使用`JsonObjectMarshaller`实现将每个项转换为 JSON,然后将其写入 JSON 文件。 \ No newline at end of file diff --git a/docs/spring-batch/common-patterns.md b/docs/spring-batch/common-patterns.md new file mode 100644 index 0000000..7e33935 --- /dev/null +++ b/docs/spring-batch/common-patterns.md @@ -0,0 +1,611 @@ +# 常见的批处理模式 + +## [](#commonPatterns)常见的批处理模式 + +XMLJavaBoth + +一些批处理作业可以完全由 Spring 批处理中现成的组件组装而成。例如,`ItemReader`和`ItemWriter`实现可以被配置为覆盖广泛的场景。然而,在大多数情况下,必须编写自定义代码。应用程序开发人员的主要 API 入口点是`Tasklet`、`ItemReader`、`ItemWriter`和各种侦听器接口。大多数简单的批处理作业可以使用 Spring 批处理中的现成输入`ItemReader`,但是在处理和编写过程中通常存在需要开发人员实现`ItemWriter`或`ItemProcessor`的定制问题。 + +在这一章中,我们提供了几个自定义业务逻辑中常见模式的示例。这些示例主要以侦听器接口为特征。应该注意的是,如果合适的话,`ItemReader`或`ItemWriter`也可以实现侦听器接口。 + +### [](#loggingItemProcessingAndFailures)记录项目处理和失败 + +一个常见的用例是需要在一个步骤中对错误进行特殊处理,逐项处理,可能是登录到一个特殊的通道,或者将一条记录插入到数据库中。面向块的`Step`(从 Step Factory Bean 创建)允许用户实现这个用例,它使用一个简单的`ItemReadListener`表示`read`上的错误,使用一个`ItemWriteListener`表示`write`上的错误。以下代码片段演示了记录读写失败的侦听器: + +``` +public class ItemFailureLoggerListener extends ItemListenerSupport { + + private static Log logger = LogFactory.getLog("item.error"); + + public void onReadError(Exception ex) { + logger.error("Encountered error on read", e); + } + + public void onWriteError(Exception ex, List items) { + logger.error("Encountered error on write", ex); + } +} +``` + +在实现了这个侦听器之后,必须用一个步骤对其进行注册。 + +下面的示例展示了如何用 XML 中的一个步骤注册侦听器: + +XML 配置 + +``` + +... + + + + + + +``` + +下面的示例展示了如何使用 STEP Java 注册侦听器: + +Java 配置 + +``` +@Bean +public Step simpleStep() { + return this.stepBuilderFactory.get("simpleStep") + ... + .listener(new ItemFailureLoggerListener()) + .build(); +} +``` + +| |如果你的侦听器在`onError()`方法中执行任何操作,则它必须位于
将被回滚的事务中。如果需要在`onError()`方法中使用事务性
资源,例如数据库,请考虑向该方法添加声明性
事务(有关详细信息,请参见 Spring Core Reference Guide),并给其
传播属性一个值`REQUIRES_NEW`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#stoppingAJobManuallyForBusinessReasons)由于业务原因手动停止作业 + +Spring Batch 通过`JobOperator`接口提供了`stop()`方法,但这实际上是供操作员而不是应用程序程序员使用的。有时,从业务逻辑中停止作业执行更方便或更有意义。 + +最简单的方法是抛出`RuntimeException`(这种方法既不会无限期地重试,也不会被跳过)。例如,可以使用自定义异常类型,如下例所示: + +``` +public class PoisonPillItemProcessor implements ItemProcessor { + + @Override + public T process(T item) throws Exception { + if (isPoisonPill(item)) { + throw new PoisonPillException("Poison pill detected: " + item); + } + return item; + } +} +``` + +另一种停止执行步骤的简单方法是从`ItemReader`返回`null`,如以下示例所示: + +``` +public class EarlyCompletionItemReader implements ItemReader { + + private ItemReader delegate; + + public void setDelegate(ItemReader delegate) { ... } + + public T read() throws Exception { + T item = delegate.read(); + if (isEndItem(item)) { + return null; // end the step here + } + return item; + } + +} +``` + +前面的示例实际上依赖于这样一个事实,即存在`CompletionPolicy`策略的默认实现,当要处理的项是`null`时,该策略发出一个完整批处理的信号。可以实现一个更复杂的完成策略,并通过`SimpleStepFactoryBean`注入`Step`。 + +下面的示例展示了如何在 XML 中的一个步骤中注入一个完成策略: + +XML 配置 + +``` + + + + + + + +``` + +下面的示例展示了如何在 Java 中的一个步骤中注入一个完成策略: + +Java 配置 + +``` +@Bean +public Step simpleStep() { + return this.stepBuilderFactory.get("simpleStep") + .chunk(new SpecialCompletionPolicy()) + .reader(reader()) + .writer(writer()) + .build(); +} +``` + +一种替代方法是在`StepExecution`中设置一个标志,这是由`Step`实现在框架中检查项之间的处理。要实现此替代方案,我们需要访问当前的`StepExecution`,这可以通过实现`StepListener`并将其注册到`Step`来实现。下面的示例展示了一个设置标志的侦听器: + +``` +public class CustomItemWriter extends ItemListenerSupport implements StepListener { + + private StepExecution stepExecution; + + public void beforeStep(StepExecution stepExecution) { + this.stepExecution = stepExecution; + } + + public void afterRead(Object item) { + if (isPoisonPill(item)) { + stepExecution.setTerminateOnly(); + } + } + +} +``` + +设置标志时,默认的行为是抛出`JobInterruptedException`。这种行为可以通过`StepInterruptionPolicy`来控制。然而,唯一的选择是抛出或不抛出异常,因此这始终是工作的异常结束。 + +### [](#addingAFooterRecord)添加页脚记录 + +通常,当写入平面文件时,在所有处理完成后,必须在文件的末尾附加一个“页脚”记录。这可以使用由 Spring 批提供的`FlatFileFooterCallback`接口来实现。`FlatFileFooterCallback`(及其对应的`FlatFileHeaderCallback`)是`FlatFileItemWriter`的可选属性,可以添加到项编写器中。 + +下面的示例展示了如何在 XML 中使用`FlatFileHeaderCallback`和`FlatFileFooterCallback`: + +XML 配置 + +``` + + + + + + +``` + +下面的示例展示了如何在 Java 中使用`FlatFileHeaderCallback`和`FlatFileFooterCallback`: + +Java 配置 + +``` +@Bean +public FlatFileItemWriter itemWriter(Resource outputResource) { + return new FlatFileItemWriterBuilder() + .name("itemWriter") + .resource(outputResource) + .lineAggregator(lineAggregator()) + .headerCallback(headerCallback()) + .footerCallback(footerCallback()) + .build(); +} +``` + +页脚回调接口只有一个方法,在必须写入页脚时调用该方法,如以下接口定义所示: + +``` +public interface FlatFileFooterCallback { + + void writeFooter(Writer writer) throws IOException; + +} +``` + +#### [](#writingASummaryFooter)编写摘要页脚 + +涉及页脚记录的一个常见要求是在输出过程中聚合信息,并将这些信息附加到文件的末尾。这个页脚通常用作文件的摘要或提供校验和。 + +例如,如果一个批处理作业正在将`Trade`记录写入一个平面文件,并且要求将所有`Trades`的总量放入一个页脚中,那么可以使用以下`ItemWriter`实现: + +``` +public class TradeItemWriter implements ItemWriter, + FlatFileFooterCallback { + + private ItemWriter delegate; + + private BigDecimal totalAmount = BigDecimal.ZERO; + + public void write(List items) throws Exception { + BigDecimal chunkTotal = BigDecimal.ZERO; + for (Trade trade : items) { + chunkTotal = chunkTotal.add(trade.getAmount()); + } + + delegate.write(items); + + // After successfully writing all items + totalAmount = totalAmount.add(chunkTotal); + } + + public void writeFooter(Writer writer) throws IOException { + writer.write("Total Amount Processed: " + totalAmount); + } + + public void setDelegate(ItemWriter delegate) {...} +} +``` + +这个`TradeItemWriter`存储了一个`totalAmount`值,该值随着从每个`Trade`条目中写入的`amount`而增加。在处理最后一个`Trade`之后,框架调用`writeFooter`,这将`totalAmount`放入文件。请注意,`write`方法使用了一个临时变量`chunkTotal`,该变量存储了块中`Trade`数量的总和。这样做是为了确保,如果在`write`方法中发生跳过,`totalAmount`保持不变。只有在`write`方法结束时,在保证不抛出异常之后,我们才更新`totalAmount`。 + +为了调用`writeFooter`方法,`TradeItemWriter`(它实现`FlatFileFooterCallback`)必须连接到`FlatFileItemWriter`中,作为`footerCallback`。 + +下面的示例展示了如何在 XML 中连接`TradeItemWriter`: + +XML 配置 + +``` + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中连接`TradeItemWriter`: + +Java 配置 + +``` +@Bean +public TradeItemWriter tradeItemWriter() { + TradeItemWriter itemWriter = new TradeItemWriter(); + + itemWriter.setDelegate(flatFileItemWriter(null)); + + return itemWriter; +} + +@Bean +public FlatFileItemWriter flatFileItemWriter(Resource outputResource) { + return new FlatFileItemWriterBuilder() + .name("itemWriter") + .resource(outputResource) + .lineAggregator(lineAggregator()) + .footerCallback(tradeItemWriter()) + .build(); +} +``` + +到目前为止,只有当`Step`不可重启时,`TradeItemWriter`的写入方式才能正确地执行。这是因为类是有状态的(因为它存储`totalAmount`),但是`totalAmount`不会持久化到数据库中。因此,在重新启动的情况下无法检索到它。为了使这个类重新启动,`ItemStream`接口应该与`open`和`update`方法一起实现,如下面的示例所示: + +``` +public void open(ExecutionContext executionContext) { + if (executionContext.containsKey("total.amount") { + totalAmount = (BigDecimal) executionContext.get("total.amount"); + } +} + +public void update(ExecutionContext executionContext) { + executionContext.put("total.amount", totalAmount); +} +``` + +更新方法将最新版本的`totalAmount`存储到`ExecutionContext`,就在该对象持久化到数据库之前。open 方法从`ExecutionContext`中检索任何已存在的`totalAmount`,并将其用作处理的起点,从而允许`TradeItemWriter`在重新启动时在上次运行`Step`时未启动的地方进行拾取。 + +### [](#drivingQueryBasedItemReaders)基于项目阅读器的驾驶查询 + +在[关于读者和作家的章节](readersAndWriters.html)中,讨论了利用分页进行数据库输入的问题。许多数据库供应商(例如 DB2)都有非常悲观的锁定策略,如果正在读取的表也需要由在线应用程序的其他部分使用,这些策略可能会导致问题。此外,在非常大的数据集上打开游标可能会导致某些供应商的数据库出现问题。因此,许多项目更喜欢使用“驱动查询”方法来读取数据。这种方法的工作原理是对键进行迭代,而不是对需要返回的整个对象进行迭代,如下图所示: + +![驾驶查询工作](./images/drivingQueryExample.png) + +图 1。驾驶查询工作 + +正如你所看到的,前面图片中显示的示例使用了与基于游标的示例中使用的相同的“foo”表。但是,在 SQL 语句中只选择了 ID,而不是选择整行。因此,不是从`read`返回`FOO`对象,而是返回`Integer`对象。然后可以使用这个数字来查询“details”,这是一个完整的`Foo`对象,如下图所示: + +![驱动查询示例](./images/drivingQueryJob.png) + +图 2。驱动查询示例 + +应该使用`ItemProcessor`将从驱动查询中获得的键转换为完整的`Foo`对象。现有的 DAO 可以用于基于该键查询完整的对象。 + +### [](#multiLineRecords)多行记录 + +虽然平面文件的情况通常是,每个记录都被限制在单行中,但一个文件的记录可能跨越多行,并具有多种格式,这是很常见的。下面摘自一个文件,展示了这种安排的一个例子: + +``` +HEA;0013100345;2007-02-15 +NCU;Smith;Peter;;T;20014539;F +BAD;;Oak Street 31/A;;Small Town;00235;IL;US +FOT;2;2;267.34 +``` + +以“hea”开头的行和以“fot”开头的行之间的所有内容都被视为一条记录。为了正确处理这种情况,必须考虑以下几点: + +* 而不是一次读取一条记录,`ItemReader`必须将多行记录的每一行作为一个组来读取,以便它可以完整地传递给`ItemWriter`。 + +* 每一种行类型可能需要以不同的方式进行标记。 + +由于单个记录跨越多行,并且我们可能不知道有多少行,因此`ItemReader`必须小心,以始终读取整个记录。为了做到这一点,应该将自定义`ItemReader`实现为`FlatFileItemReader`的包装器。 + +下面的示例展示了如何在 XML 中实现自定义`ItemReader`: + +XML 配置 + +``` + + + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中实现自定义`ItemReader`: + +Java 配置 + +``` +@Bean +public MultiLineTradeItemReader itemReader() { + MultiLineTradeItemReader itemReader = new MultiLineTradeItemReader(); + + itemReader.setDelegate(flatFileItemReader()); + + return itemReader; +} + +@Bean +public FlatFileItemReader flatFileItemReader() { + FlatFileItemReader reader = new FlatFileItemReaderBuilder<>() + .name("flatFileItemReader") + .resource(new ClassPathResource("data/iosample/input/multiLine.txt")) + .lineTokenizer(orderFileTokenizer()) + .fieldSetMapper(orderFieldSetMapper()) + .build(); + return reader; +} +``` + +为了确保每一行都被正确地标记,这对于固定长度的输入尤其重要,`PatternMatchingCompositeLineTokenizer`可以在委托`FlatFileItemReader`上使用。有关更多详细信息,请参见[`FlatFileItemReader`中的 Readers and Writers 章节]。然后,委托读取器使用`PassThroughFieldSetMapper`将每一行的`FieldSet`传递到包装`ItemReader`。 + +下面的示例展示了如何确保每一行都正确地在 XML 中进行了标记: + +XML 内容 + +``` + + + + + + + + + + +``` + +下面的示例展示了如何确保每一行都在 Java 中被正确地标记: + +Java 内容 + +``` +@Bean +public PatternMatchingCompositeLineTokenizer orderFileTokenizer() { + PatternMatchingCompositeLineTokenizer tokenizer = + new PatternMatchingCompositeLineTokenizer(); + + Map tokenizers = new HashMap<>(4); + + tokenizers.put("HEA*", headerRecordTokenizer()); + tokenizers.put("FOT*", footerRecordTokenizer()); + tokenizers.put("NCU*", customerLineTokenizer()); + tokenizers.put("BAD*", billingAddressLineTokenizer()); + + tokenizer.setTokenizers(tokenizers); + + return tokenizer; +} +``` + +这个包装器必须能够识别记录的结尾,以便它可以在其委托上连续调用`read()`,直到达到结尾。对于读取的每一行,包装器应该构建要返回的项。一旦到达页脚,就可以将项目返回以交付给`ItemProcessor`和`ItemWriter`,如以下示例所示: + +``` +private FlatFileItemReader

delegate; + +public Trade read() throws Exception { + Trade t = null; + + for (FieldSet line = null; (line = this.delegate.read()) != null;) { + String prefix = line.readString(0); + if (prefix.equals("HEA")) { + t = new Trade(); // Record must start with header + } + else if (prefix.equals("NCU")) { + Assert.notNull(t, "No header was found."); + t.setLast(line.readString(1)); + t.setFirst(line.readString(2)); + ... + } + else if (prefix.equals("BAD")) { + Assert.notNull(t, "No header was found."); + t.setCity(line.readString(4)); + t.setState(line.readString(6)); + ... + } + else if (prefix.equals("FOT")) { + return t; // Record must end with footer + } + } + Assert.isNull(t, "No 'END' was found."); + return null; +} +``` + +### [](#executingSystemCommands)执行系统命令 + +许多批处理作业要求从批处理作业中调用外部命令。这样的进程可以由调度器单独启动,但是有关运行的公共元数据的优势将会丧失。此外,一个多步骤的工作也需要被分解成多个工作。 + +因为这种需求是如此普遍, Spring Batch 提供了用于调用系统命令的`Tasklet`实现。 + +下面的示例展示了如何调用 XML 中的外部命令: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何在 Java 中调用外部命令: + +Java 配置 + +``` +@Bean +public SystemCommandTasklet tasklet() { + SystemCommandTasklet tasklet = new SystemCommandTasklet(); + + tasklet.setCommand("echo hello"); + tasklet.setTimeout(5000); + + return tasklet; +} +``` + +### [](#handlingStepCompletionWhenNoInputIsFound)未找到输入时的处理步骤完成 + +在许多批处理场景中,在数据库或文件中找不到要处理的行并不是例外情况。将`Step`简单地视为未找到工作,并在读取 0 项的情况下完成。所有的`ItemReader`实现都是在 Spring 批处理中提供的,默认为这种方法。如果即使存在输入,也没有写出任何内容,这可能会导致一些混乱(如果文件被错误命名或出现类似问题,通常会发生这种情况)。因此,应该检查元数据本身,以确定框架需要处理多少工作。然而,如果发现没有输入被认为是例外情况怎么办?在这种情况下,最好的解决方案是通过编程方式检查元数据,以确保未处理任何项目并导致失败。因为这是一个常见的用例, Spring Batch 提供了一个具有这种功能的侦听器,如`NoWorkFoundStepExecutionListener`的类定义所示: + +``` +public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport { + + public ExitStatus afterStep(StepExecution stepExecution) { + if (stepExecution.getReadCount() == 0) { + return ExitStatus.FAILED; + } + return null; + } + +} +``` + +前面的`StepExecutionListener`在“afterstep”阶段检查`StepExecution`的`readCount`属性,以确定是否没有读取任何项。如果是这种情况,将返回一个退出代码`FAILED`,表示`Step`应该失败。否则,将返回`null`,这不会影响`Step`的状态。 + +### [](#passingDataToFutureSteps)将数据传递给未来的步骤 + +将信息从一个步骤传递到另一个步骤通常是有用的。这可以通过`ExecutionContext`来完成。问题是有两个`ExecutionContexts`:一个在`Step`水平,一个在`Job`水平。`Step``ExecutionContext`只保留到步骤的长度,而`Job``ExecutionContext`则保留到整个`Job`。另一方面,`Step``ExecutionContext`每次`Step`提交一个块时都会更新`Job``ExecutionContext`,而`Step`只在每个`Step`的末尾更新。 + +这种分离的结果是,当`Step`执行时,所有数据都必须放在`Step``ExecutionContext`中。这样做可以确保在`Step`运行时正确地存储数据。如果数据被存储到`Job``ExecutionContext`,那么在`Step`执行期间它不会被持久化。如果`Step`失败,则该数据丢失。 + +``` +public class SavingItemWriter implements ItemWriter { + private StepExecution stepExecution; + + public void write(List items) throws Exception { + // ... + + ExecutionContext stepContext = this.stepExecution.getExecutionContext(); + stepContext.put("someKey", someObject); + } + + @BeforeStep + public void saveStepExecution(StepExecution stepExecution) { + this.stepExecution = stepExecution; + } +} +``` + +要使将来`Steps`可以使用该数据,必须在步骤完成后将其“提升”到`Job``ExecutionContext`。 Spring Batch 为此提供了`ExecutionContextPromotionListener`。侦听器必须配置与必须提升的`ExecutionContext`中的数据相关的键。它还可以配置一个退出代码模式列表(`COMPLETED`是默认的)。与所有侦听器一样,它必须在`Step`上注册。 + +下面的示例展示了如何在 XML 中将一个步骤提升到`Job``ExecutionContext`: + +XML 配置 + +``` + + + + + + + + + + + + ... + + + + + + + someKey + + + +``` + +下面的示例展示了如何在 Java 中将一个步骤提升到`Job``ExecutionContext`: + +Java 配置 + +``` +@Bean +public Job job1() { + return this.jobBuilderFactory.get("job1") + .start(step1()) + .next(step1()) + .build(); +} + +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(reader()) + .writer(savingWriter()) + .listener(promotionListener()) + .build(); +} + +@Bean +public ExecutionContextPromotionListener promotionListener() { + ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener(); + + listener.setKeys(new String[] {"someKey"}); + + return listener; +} +``` + +最后,必须从`Job``ExecutionContext`中检索保存的值,如下例所示: + +``` +public class RetrievingItemWriter implements ItemWriter { + private Object someObject; + + public void write(List items) throws Exception { + // ... + } + + @BeforeStep + public void retrieveInterstepData(StepExecution stepExecution) { + JobExecution jobExecution = stepExecution.getJobExecution(); + ExecutionContext jobContext = jobExecution.getExecutionContext(); + this.someObject = jobContext.get("someKey"); + } +} +``` \ No newline at end of file diff --git a/docs/spring-batch/domain.md b/docs/spring-batch/domain.md new file mode 100644 index 0000000..78f4e91 --- /dev/null +++ b/docs/spring-batch/domain.md @@ -0,0 +1,292 @@ +# 批处理的领域语言 + +## [](#domainLanguageOfBatch)批处理的域语言 + +XMLJavaBoth + +对于任何有经验的批处理架构师来说, Spring 批处理中使用的批处理的总体概念应该是熟悉和舒适的。有“作业”和“步骤”以及开发人员提供的处理单元,分别称为`ItemReader`和`ItemWriter`。然而,由于 Spring 模式、操作、模板、回调和习惯用法,有以下机会: + +* 在坚持明确区分关注事项方面有了显著改善。 + +* 清晰地描述了作为接口提供的体系结构层和服务。 + +* 简单和默认的实现,允许快速采用和易于使用的开箱即用。 + +* 显著增强了可扩展性。 + +下图是使用了几十年的批处理引用体系结构的简化版本。它提供了组成批处理领域语言的组件的概述。这个架构框架是一个蓝图,已经通过过去几代平台(COBOL/大型机、C/UNIX 和现在的 Java/Anywhere)上几十年的实现得到了证明。JCL 和 COBOL 开发人员可能与 C、C# 和 Java 开发人员一样熟悉这些概念。 Spring 批处理提供了通常在健壮的、可维护的系统中发现的层、组件和技术服务的物理实现,这些系统被用于解决创建简单到复杂的批处理应用程序,具有用于解决非常复杂的处理需求的基础设施和扩展。 + +![图 2.1:批处理原型](./images/spring-batch-reference-model.png) + +图 1。批处理模式 + +前面的图表突出了构成 Spring 批处理的域语言的关键概念。一个作业有一个到多个步骤,每个步骤正好有一个`ItemReader`,一个`ItemProcessor`和一个`ItemWriter`。需要启动一个作业(使用`JobLauncher`),并且需要存储有关当前运行的进程的元数据(在`JobRepository`中)。 + +### [](#job)工作 + +这一部分描述了与批处理作业的概念有关的刻板印象。`Job`是封装整个批处理过程的实体。与其他 Spring 项目一样,`Job`与 XML 配置文件或基于 Java 的配置连接在一起。这种配置可以称为“作业配置”。然而,`Job`只是整个层次结构的顶部,如下图所示: + +![工作层次结构](./images/job-heirarchy.png) + +图 2。工作层次结构 + +在 Spring 批处理中,`Job`只是用于`Step`实例的容器。它将逻辑上属于一个流的多个步骤组合在一起,并允许将属性的全局配置用于所有步骤,例如可重启性。作业配置包含: + +* 工作的简单名称。 + +* `Step`实例的定义和排序。 + +* 这份工作是否可以重新启动。 + +对于那些使用 Java 配置的人, Spring Batch 以`SimpleJob`类的形式提供了作业接口的默认实现,它在`Job`之上创建了一些标准功能。当使用基于 Java 的配置时,可以使用一个构建器集合来实例化`Job`,如以下示例所示: + +``` +@Bean +public Job footballJob() { + return this.jobBuilderFactory.get("footballJob") + .start(playerLoad()) + .next(gameLoad()) + .next(playerSummarization()) + .build(); +} +``` + +对于那些使用 XML 配置的人, Spring Batch 以`SimpleJob`类的形式提供了`Job`接口的默认实现,它在`Job`之上创建了一些标准功能。然而,批处理名称空间抽象出了直接实例化它的需要。相反,可以使用``元素,如以下示例所示: + +``` + + + + + +``` + +#### [](#jobinstance)JobInstance + +a`JobInstance`指的是逻辑作业运行的概念。考虑应该在一天结束时运行一次的批处理作业,例如前面图表中的“endofday”`Job`。有一个“endofday”作业,但是`Job`的每个单独运行都必须单独跟踪。在这种情况下,每天有一个逻辑`JobInstance`。例如,有一个 1 月 1 日运行,1 月 2 日运行,以此类推。如果 1 月 1 日运行第一次失败,并在第二天再次运行,它仍然是 1 月 1 日运行。(通常,这也对应于它正在处理的数据,这意味着 1 月 1 日运行处理 1 月 1 日的数据)。因此,每个`JobInstance`都可以有多个执行(`JobExecution`在本章后面更详细地讨论),并且只有一个`JobInstance`对应于特定的`Job`和标识`JobParameters`的执行可以在给定的时间运行。 + +`JobInstance`的定义与要加载的数据完全无关。这完全取决于`ItemReader`实现来确定如何加载数据。例如,在 Endofday 场景中,数据上可能有一个列,该列指示数据所属的“生效日期”或“计划日期”。因此,1 月 1 日的运行将只加载 1 日的数据,而 1 月 2 日的运行将只使用 2 日的数据。因为这个决定很可能是一个商业决定,所以它是由`ItemReader`来决定的。然而,使用相同的`JobInstance`确定是否使用来自先前执行的’状态’(即`ExecutionContext`,这将在本章后面讨论)。使用一个新的`JobInstance`表示“从开始”,而使用一个现有的实例通常表示“从你停止的地方开始”。 + +#### [](#jobparameters)JobParameters + +在讨论了`JobInstance`以及它与约伯有何不同之后,我们自然要问的问题是:“一个`JobInstance`如何与另一个区分开来?”答案是:`JobParameters`。`JobParameters`对象持有一组用于启动批处理作业的参数。它们可以用于标识,甚至在运行过程中作为参考数据,如下图所示: + +![作业参数](./images/job-stereotypes-parameters.png) + +图 3。作业参数 + +在前面的示例中,有两个实例,一个用于 1 月 1 日,另一个用于 1 月 2 日,实际上只有一个`Job`,但它有两个`JobParameter`对象:一个以 01-01-2017 的作业参数启动,另一个以 01-02-2017 的参数启动。因此,契约可以定义为:`JobInstance`=`Job`+ 标识`JobParameters`。这允许开发人员有效地控制`JobInstance`的定义方式,因为他们控制传入的参数。 + +| |并非所有作业参数都需要有助于识别`JobInstance`。在默认情况下,他们会这么做。但是,该框架还允许使用不影响`JobInstance`的恒等式的参数提交
的`Job`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#jobexecution)jobexecution + +a`JobExecution`指的是一次尝试运行作业的技术概念。一次执行可能以失败或成功结束,但除非执行成功完成,否则对应于给定执行的`JobInstance`不被认为是完成的。以前面描述的 Endofday`Job`为例,考虑第一次运行时失败的 01-01-2017 的`JobInstance`。如果以与第一次运行(01-01-2017)相同的标识作业参数再次运行,则会创建一个新的`JobExecution`。然而,仍然只有一个`JobInstance`。 + +`Job`定义了什么是作业以及如何执行它,而`JobInstance`是一个纯粹的组织对象,用于将执行分组在一起,主要是为了启用正确的重新启动语义。但是,`JobExecution`是运行期间实际发生的事情的主要存储机制,并且包含许多必须控制和持久化的属性,如下表所示: + +| Property |定义| +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Status |一个`BatchStatus`对象,它指示执行的状态。在运行时,它是`BatchStatus#STARTED`。如果失败,则为`BatchStatus#FAILED`。如果成功完成
,则为`BatchStatus#COMPLETED`| +| startTime |a`java.util.Date`表示开始执行的当前系统时间。
如果作业尚未开始,则此字段为空。| +| endTime |a`java.util.Date`表示当前系统执行完成时的时间,
不管是否成功。如果作业尚未
完成,则该字段为空。| +| exitStatus |`ExitStatus`,表示运行的结果。它是最重要的,因为它
包含一个返回给调用方的退出代码。有关更多详细信息,请参见第 5 章。如果作业尚未完成,则
字段为空。| +| createTime |a`java.util.Date`表示当`JobExecution`是
第一次持续存在时的当前系统时间。作业可能尚未启动(因此没有启动时间),但是
它总是有一个 CreateTime,这是管理作业级别`ExecutionContexts`的框架所要求的。| +| lastUpdated |a`java.util.Date`表示上次持久化 a`JobExecution`。如果作业尚未开始,则该字段
为空。| +|executionContext |“属性袋”包含在
执行之间需要持久化的任何用户数据。| +|failureExceptions|在执行`Job`时遇到的异常列表。如果在`Job`失败期间遇到了多个异常,则这些规则是有用的
。| + +这些属性很重要,因为它们是持久的,可以用来完全确定执行的状态。例如,如果 01-01 的 Endofday 作业在晚上 9:00 执行,并在 9:30 失败,则在批处理元数据表中创建以下条目: + +|工作 \_INST\_ID| JOB\_NAME | +|-------------|-----------| +| 1 |EndOfDayJob| + +|作业 \_ 执行 \_ID|TYPE\_CD| KEY\_NAME |DATE\_VAL |IDENTIFYING| +|------------------|--------|-------------|----------|-----------| +| 1 | DATE |schedule.Date|2017-01-01| TRUE | + +|JOB\_EXEC\_ID|工作 \_INST\_ID|开始 \_ 时间| END\_TIME |STATUS| +|-------------|-------------|----------------|----------------|------| +| 1 | 1 |2017-01-01 21:00|2017-01-01 21:30|FAILED| + +| |列名可能已被缩写或删除,以求清楚和
格式。| +|---|---------------------------------------------------------------------------------------------| + +现在工作失败了,假设花了一整夜的时间才确定问题,所以“批处理窗口”现在关闭了。进一步假设窗口在晚上 9:00 开始,工作将在 01-01 再次开始,从停止的地方开始,并在 9:30 成功完成。因为现在是第二天,所以也必须运行 01-02 作业,然后在 9:31 开始,并在 10:30 以正常的一小时时间完成。没有要求一个`JobInstance`被一个接一个地启动,除非这两个作业有可能试图访问相同的数据,从而导致数据库级别的锁定问题。完全由调度程序决定何时应该运行`Job`。由于它们是分开的`JobInstances`, Spring 批处理不会试图阻止它们同时运行。(当另一个人已经在运行`JobExecutionAlreadyRunningException`时,试图运行相同的`JobInstance`,结果会抛出一个`JobExecutionAlreadyRunningException`)。现在应该在`JobInstance`和`JobParameters`两个表中都有一个额外的条目,并且在`JobExecution`表中有两个额外的条目,如下表所示: + +|工作 \_INST\_ID| JOB\_NAME | +|-------------|-----------| +| 1 |EndOfDayJob| +| 2 |EndOfDayJob| + +|JOB\_EXECUTION\_ID|TYPE\_CD| KEY\_NAME |日期 \_val|IDENTIFYING| +|------------------|--------|-------------|-------------------|-----------| +| 1 | DATE |schedule.Date|2017-01-01 00:00:00| TRUE | +| 2 | DATE |schedule.Date|2017-01-01 00:00:00| TRUE | +| 3 | DATE |schedule.Date|2017-01-02 00:00:00| TRUE | + +|JOB\_EXEC\_ID|工作 \_INST\_ID|开始 \_ 时间| END\_TIME | STATUS | +|-------------|-------------|----------------|----------------|---------| +| 1 | 1 |2017-01-01 21:00|2017-01-01 21:30| FAILED | +| 2 | 1 |2017-01-02 21:00|2017-01-02 21:30|COMPLETED| +| 3 | 2 |2017-01-02 21:31|2017-01-02 22:29|COMPLETED| + +| |列名可能已被缩写或删除,以求清楚和
格式。| +|---|---------------------------------------------------------------------------------------------| + +### [](#step)步骤 + +`Step`是一个域对象,它封装了批处理作业的一个独立的、连续的阶段。因此,每一项工作都完全由一个或多个步骤组成。a`Step`包含定义和控制实际批处理所需的所有信息。这必然是一个模糊的描述,因为任何给定的`Step`的内容都是由编写`Job`的开发人员自行决定的。a`Step`可以是简单的,也可以是复杂的,正如开发人员所希望的那样。简单的`Step`可能会将文件中的数据加载到数据库中,只需要很少或不需要代码(取决于使用的实现)。更复杂的`Step`可能具有复杂的业务规则,这些规则作为处理的一部分被应用。与`Job`一样,`Step`具有与唯一的`StepExecution`相关的个体`StepExecution`,如下图所示: + +![图 2.1:带有步骤的工作层次结构](./images/jobHeirarchyWithSteps.png) + +图 4。带有步骤的工作层次结构 + +#### [](#stepexecution)分步执行 + +a`StepExecution`表示试图执行`Step`的一次尝试。每次运行`Step`都会创建一个新的`StepExecution`,类似于`JobExecution`。但是,如果一个步骤由于它失败之前的步骤而无法执行,则不会对它执行持久化。只有当它的`Step`实际启动时,才会创建`StepExecution`。 + +`Step`执行由`StepExecution`类的对象表示。每个执行都包含对其相应步骤的引用和`JobExecution`以及与事务相关的数据,例如提交和回滚计数以及开始和结束时间。此外,每个步骤的执行都包含`ExecutionContext`,其中包含开发人员需要在批处理运行中保持的任何数据,例如重新启动所需的统计信息或状态信息。下表列出了`StepExecution`的属性: + +| Property |定义| +|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Status |指示执行状态的`BatchStatus`对象。运行时,
状态为`BatchStatus.STARTED`。如果失败,则状态为`BatchStatus.FAILED`。如果
成功完成,则状态为`BatchStatus.COMPLETED`。| +| startTime |a`java.util.Date`表示开始执行的当前系统时间。
如果该步骤尚未开始,则此字段为空。| +| endTime |a`java.util.Date`表示当前系统执行完成的时间,
不管是否成功。如果该步骤尚未
退出,则此字段为空。| +| exitStatus |表示执行结果的`ExitStatus`。这是最重要的,因为
它包含一个返回给调用者的退出代码。有关更多详细信息,请参见第 5 章。
如果作业尚未退出,则此字段为空。| +|executionContext|“属性袋”包含在
执行之间需要持久化的任何用户数据。| +| readCount |已成功读取的项目的数量。| +| writeCount |已成功写入的项目的数量。| +| commitCount |已提交用于此执行的事务的数量。| +| rollbackCount |由`Step`控制的业务事务被回滚
的次数。| +| readSkipCount |失败的次数`read`,导致项目被跳过。| +|processSkipCount|`process`失败的次数,导致项目被跳过。| +| filterCount |已被`ItemProcessor`“过滤”的项数。| +| writeSkipCount |失败的次数`write`,导致项目被跳过。| + +### [](#executioncontext)ExecutionContext + +`ExecutionContext`表示一组键/值对的集合,这些键/值对由框架持久化并控制,以便允许开发人员有一个存储持久状态的位置,该状态的作用域为`StepExecution`对象或`JobExecution`对象。对于那些熟悉 Quartz 的人来说,它与 JobDataMap 非常相似。最好的使用示例是方便重新启动。以平面文件输入为例,在处理单个行时,该框架会在提交点周期性地保存`ExecutionContext`。这样做允许`ItemReader`存储其状态,以防在运行过程中发生致命错误,甚至断电。所需要的只是将当前读取的行数放入上下文中,如下面的示例所示,框架将完成其余的工作: + +``` +executionContext.putLong(getKey(LINES_READ_COUNT), reader.getPosition()); +``` + +使用`Job`刻板印象部分中的 Endofday 示例作为示例,假设有一个步骤“loaddata”将文件加载到数据库中。在第一次运行失败之后,元数据表将如下所示: + +|JOB\_INST\_ID| JOB\_NAME | +|-------------|-----------| +| 1 |EndOfDayJob| + +|JOB\_INST\_ID|TYPE\_CD| KEY\_NAME |DATE\_VAL | +|-------------|--------|-------------|----------| +| 1 | DATE |schedule.Date|2017-01-01| + +|JOB\_EXEC\_ID|JOB\_INST\_ID|开始 \_ 时间| END\_TIME |STATUS| +|-------------|-------------|----------------|----------------|------| +| 1 | 1 |2017-01-01 21:00|2017-01-01 21:30|FAILED| + +|STEP\_EXEC\_ID|JOB\_EXEC\_ID|STEP\_NAME|开始 \_ 时间| END\_TIME |STATUS| +|--------------|-------------|----------|----------------|----------------|------| +| 1 | 1 | loadData |2017-01-01 21:00|2017-01-01 21:30|FAILED| + +|STEP\_EXEC\_ID|短 \_ 上下文| +|--------------|-------------------| +| 1 |{piece.count=40321}| + +在前一种情况下,`Step`运行了 30 分钟,处理了 40,321 个“片段”,这将表示此场景中文件中的行。这个值在框架每次提交之前进行更新,并且可以包含与`ExecutionContext`中的条目相对应的多行。在提交之前被通知需要各种`StepListener`实现中的一种(或`ItemStream`),这将在本指南的后面进行更详细的讨论。与前面的示例一样,假定`Job`在第二天重新启动。重新启动时,将从数据库重新构造上次运行的`ExecutionContext`中的值。当打开`ItemReader`时,它可以检查上下文中是否有任何存储状态,并从那里初始化自己,如以下示例所示: + +``` +if (executionContext.containsKey(getKey(LINES_READ_COUNT))) { + log.debug("Initializing for restart. Restart data is: " + executionContext); + + long lineCount = executionContext.getLong(getKey(LINES_READ_COUNT)); + + LineReader reader = getReader(); + + Object record = ""; + while (reader.getPosition() < lineCount && record != null) { + record = readLine(); + } +} +``` + +在这种情况下,在上面的代码运行之后,当前行是 40,322,允许`Step`从它停止的地方重新开始。`ExecutionContext`还可以用于需要对运行本身进行持久化的统计信息。例如,如果一个平面文件包含跨多行的处理订单,则可能需要存储已处理的订单数量(这与读取的行数有很大不同),使一封电子邮件可以在`Step`的末尾发送,并在正文中处理订单的总数。框架为开发人员处理此存储,以便使用单独的`JobInstance`正确地对其进行范围设置。很难知道是否应该使用现有的`ExecutionContext`。例如,使用上面的“Endofday”示例,当 01-01 运行再次开始第二次时,框架识别出它是相同的`JobInstance`,并且在一个单独的`Step`基础上,将`ExecutionContext`从数据库中拉出,并将它(作为`StepExecution`的一部分)交给`Step`本身。相反,对于 01-02 运行,框架认识到它是一个不同的实例,因此必须将一个空上下文交给`Step`。框架为开发人员做出了许多此类决定,以确保在正确的时间将状态赋予他们。同样重要的是要注意,在任何给定的时间,每个`StepExecution`都存在一个`ExecutionContext`。`ExecutionContext`的客户端应该小心,因为这会创建一个共享密钥区。因此,在放入值时应该小心,以确保没有数据被覆盖。然而,`Step`在上下文中绝对不存储数据,因此没有办法对框架产生不利影响。 + +同样重要的是要注意,每`ExecutionContext`至少有一个`JobExecution`,每`StepExecution`至少有一个。例如,考虑以下代码片段: + +``` +ExecutionContext ecStep = stepExecution.getExecutionContext(); +ExecutionContext ecJob = jobExecution.getExecutionContext(); +//ecStep does not equal ecJob +``` + +如注释中所指出的,`ecStep`不等于`ecJob`。它们是两个不同的`ExecutionContexts`。作用域为`Step`的一个被保存在`Step`中的每个提交点,而作用域为该作业的一个被保存在每个`Step`执行之间。 + +### [](#jobrepository)JobRepository + +`JobRepository`是上述所有刻板印象的持久性机制。它为`JobLauncher`、`Job`和`Step`实现提供增删改查操作。当`Job`首次启动时,将从存储库获得`JobExecution`,并且在执行过程中,通过将`StepExecution`和`JobExecution`实现传递到存储库来持久化它们。 + +Spring 批处理 XML 命名空间提供了对配置带有``标记的`JobRepository`实例的支持,如以下示例所示: + +``` + +``` + +当使用 Java 配置时,`@EnableBatchProcessing`注释提供了`JobRepository`作为自动配置的组件之一。 + +### [](#joblauncher)joblauncher + +`JobLauncher`表示用于启动`Job`具有给定的`JobParameters`集的`Job`的简单接口,如以下示例所示: + +``` +public interface JobLauncher { + +public JobExecution run(Job job, JobParameters jobParameters) + throws JobExecutionAlreadyRunningException, JobRestartException, + JobInstanceAlreadyCompleteException, JobParametersInvalidException; +} +``` + +期望实现从`JobRepository`获得有效的`JobExecution`并执行`Job`。 + +### [](#item-reader)条目阅读器 + +`ItemReader`是一种抽象,表示对`Step`输入的检索,每次检索一项。当`ItemReader`已经耗尽了它可以提供的项时,它通过返回`null`来表示这一点。有关`ItemReader`接口及其各种实现方式的更多详细信息,请参见[读者和作家](readersAndWriters.html#readersAndWriters)。 + +### [](#item-writer)item writer + +`ItemWriter`是一种抽象,它表示`Step`的输出,一次输出一个批处理或一大块项目。通常,`ItemWriter`不知道下一步应该接收的输入,只知道当前调用中传递的项。有关`ItemWriter`接口及其各种实现方式的更多详细信息,请参见[读者和作家](readersAndWriters.html#readersAndWriters)。 + +### [](#item-processor)项处理器 + +`ItemProcessor`是表示项目的业务处理的抽象。当`ItemReader`读取一个项,而`ItemWriter`写入它们时,`ItemProcessor`提供了一个接入点来转换或应用其他业务处理。如果在处理该项时确定该项无效,则返回`null`表示不应写出该项。有关`ItemProcessor`接口的更多详细信息,请参见[读者和作家](readersAndWriters.html#readersAndWriters)。 + +### [](#batch-namespace)批处理名称空间 + +前面列出的许多域概念需要在 Spring `ApplicationContext`中进行配置。虽然有上述接口的实现方式可以在标准 Bean 定义中使用,但提供了一个名称空间以便于配置,如以下示例所示: + +``` + + + + + + + + + + + +``` + +只要已声明批处理命名空间,就可以使用它的任何元素。有关配置作业的更多信息,请参见[配置和运行作业](job.html#configureJob)。有关配置`Step`的更多信息,请参见[配置一个步骤](step.html#configureStep)。 \ No newline at end of file diff --git a/docs/spring-batch/glossary.md b/docs/spring-batch/glossary.md new file mode 100644 index 0000000..fa5431f --- /dev/null +++ b/docs/spring-batch/glossary.md @@ -0,0 +1,81 @@ +# 词汇表 + +## [](#glossary)附录 A:术语表 + +### [](#spring-batch-glossary) Spring 批处理术语表 + +批处理 + +随着时间的推移,商业交易的积累。 + +批处理应用程序样式 + +用于将批处理指定为一种独立的应用程序风格,类似于在线、Web 或 SOA。它具有输入、验证、信息到业务模型的转换、业务处理和输出的标准元素。此外,它还需要宏观层面的监控。 + +批处理 + +在一段时间内(如一小时、一天、一周、一个月或一年)积累的大量业务交易的处理。它是一个过程或一组过程以重复和可预测的方式应用于许多数据实体或对象,不需要手动元素,也不需要单独的手动元素进行错误处理。 + +批处理窗口 + +批处理作业必须完成的时间范围。这可能会受到其他联机系统、其他需要执行的依赖作业或批处理环境特有的其他因素的限制。 + +步骤 + +主要的批处理任务或工作单元.它根据提交间隔设置和其他因素初始化业务逻辑并控制事务环境。 + +Tasklet + +由应用程序开发人员创建的用于处理某一步骤的业务逻辑的组件。 + +批处理作业类型 + +作业类型描述了针对特定类型的处理的作业应用。常见的领域是接口处理(通常是平面文件)、表单处理(用于在线 PDF 生成或打印格式)和报告处理。 + +驾驶查询 + +驾驶查询标识了一项工作要做的一组工作。然后,这份工作将这份工作分解为各个工作单元。例如,一个驱动查询可能是要识别所有具有“挂起传输”状态的金融交易,并将它们发送到合作伙伴系统。驱动查询返回一组要处理的记录 ID。然后,每个记录 ID 就成为一个工作单位。驱动查询可能涉及一个连接(如果选择的条件跨越两个或更多个表),也可能与单个表一起工作。 + +项目 + +项表示用于处理的完整数据的最小量。用最简单的术语来说,这可能是文件中的一行,数据库表中的一行,或者 XML 文件中的特定元素。 + +逻辑工作单位 + +批处理作业通过驱动查询(或其他输入源,例如文件)进行迭代,以执行作业必须完成的一组工作。所执行的工作的每一次迭代都是一个工作单位。 + +提交间隔 + +在单个事务中处理的一组 LUW。 + +划分 + +将作业拆分成多个线程,其中每个线程负责要处理的整个数据的一个子集。执行线程可以在相同的 JVM 中,也可以在支持工作负载平衡的集群环境中跨越 JVM。 + +分段表 + +在处理临时数据时保存临时数据的表。 + +可重启 + +一种可以再次执行的作业,并假定初始运行时具有相同的标识。换句话说,它具有相同的作业实例 ID。 + +可重排 + +可以重新启动并根据前一次运行的记录处理管理自己的状态的作业。一个可重新运行的步骤的例子是一个基于驱动查询的步骤。如果可以形成驱动查询,以便在重新启动作业时限制已处理的行,则可以重新运行该查询。这是由应用程序逻辑管理的。通常,在`where`语句中添加一个条件,以限制驱动查询返回的行,其逻辑类似于“and processedFlag!=true”。 + +重复 + +批处理的最基本单元之一,它通过可重复性来定义调用代码的一部分,直到完成并且没有错误为止。通常,只要有输入,批处理过程就是可重复的。 + +重试 + +使用与处理事务输出异常最常关联的重试语义简化了操作的执行。Retry 与 Repeat 略有不同,Retry 是有状态的,并以相同的输入连续调用相同的代码块,直到它成功或超出某种类型的重试限制。只有当由于环境中的某些东西得到了改进,操作的后续调用才可能成功时,它才是通常有用的。 + +恢复 + +恢复操作以这样一种方式处理异常,即重复进程能够继续。 + +斯基普 + +跳过是一种恢复策略,通常用于文件输入源,作为忽略无效验证的错误输入记录的策略。 \ No newline at end of file diff --git a/docs/spring-batch/job.md b/docs/spring-batch/job.md new file mode 100644 index 0000000..23b0424 --- /dev/null +++ b/docs/spring-batch/job.md @@ -0,0 +1,1064 @@ +# 配置和运行作业 + +## [](#configureJob)配置和运行作业 + +XMLJavaBoth + +在[领域部分](domain.html#domainLanguageOfBatch)中,使用以下图表作为指导,讨论了总体架构设计: + +![图 2.1:批处理原型](./images/spring-batch-reference-model.png) + +图 1。批处理模式 + +虽然`Job`对象看起来像是一个用于步骤的简单容器,但开发人员必须了解许多配置选项。此外,对于如何运行`Job`以及在运行期间如何存储其元数据,有许多考虑因素。本章将解释`Job`的各种配置选项和运行时关注点。 + +### [](#configuringAJob)配置作业 + +[`Job`](#configurejob)接口有多个实现方式。然而,构建者会抽象出配置上的差异。 + +``` +@Bean +public Job footballJob() { + return this.jobBuilderFactory.get("footballJob") + .start(playerLoad()) + .next(gameLoad()) + .next(playerSummarization()) + .build(); +} +``` + +一个`Job`(以及其中的任何`Step`)需要一个`JobRepository`。`JobRepository`的配置是通过[`BatchConfigurer`]来处理的。 + +上面的示例演示了由三个`Step`实例组成的`Job`实例。与作业相关的构建器还可以包含有助于并行(`Split`)、声明性流控制(`Decision`)和流定义外部化(`Flow`)的其他元素。 + +无论你使用 Java 还是 XML,[`Job`](#configurejob)接口都有多个实现。然而,名称空间抽象出了配置上的差异。它只有三个必需的依赖项:一个名称,`JobRepository`,和一个`Step`实例的列表。 + +``` + + + + + +``` + +这里的示例使用父 Bean 定义来创建步骤。有关内联声明特定步骤详细信息的更多选项,请参见[阶跃配置](step.html#configureStep)一节。XML 名称空间默认情况下引用一个 ID 为“JobRepository”的存储库,这是一个合理的默认值。但是,这可以显式地重写: + +``` + + + + + +``` + +除了步骤之外,作业配置还可以包含有助于并行(``)、声明性流控制(``)和流定义外部化(``)的其他元素。 + +#### [](#restartability)可重启性 + +执行批处理作业时的一个关键问题与`Job`重新启动时的行为有关。如果对于特定的`Job`已经存在`JobExecution`,则将`Job`的启动视为“重新启动”。理想情况下,所有的工作都应该能够在它们停止的地方启动,但是在某些情况下这是不可能的。* 完全由开发人员来确保在此场景中创建一个新的`JobInstance`。* 但是, Spring 批处理确实提供了一些帮助。如果`Job`永远不应该重新启动,而应该始终作为新的`JobInstance`的一部分运行,那么可重启属性可以设置为“false”。 + +下面的示例展示了如何在 XML 中将`restartable`字段设置为`false`: + +XML 配置 + +``` + + ... + +``` + +下面的示例展示了如何在 Java 中将`restartable`字段设置为`false`: + +Java 配置 + +``` +@Bean +public Job footballJob() { + return this.jobBuilderFactory.get("footballJob") + .preventRestart() + ... + .build(); +} +``` + +换一种说法,将 Restartable 设置为 false 意味着“this`Job`不支持重新启动”。重新启动不可重启的`Job`会导致抛出`JobRestartException`。 + +``` +Job job = new SimpleJob(); +job.setRestartable(false); + +JobParameters jobParameters = new JobParameters(); + +JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters); +jobRepository.saveOrUpdate(firstExecution); + +try { + jobRepository.createJobExecution(job, jobParameters); + fail(); +} +catch (JobRestartException e) { + // expected +} +``` + +这段 JUnit 代码展示了如何在第一次为不可重启作业创建`JobExecution`时尝试创建`JobExecution`不会导致任何问题。但是,第二次尝试将抛出`JobRestartException`。 + +#### [](#interceptingJobExecution)拦截作业执行 + +在作业的执行过程中,通知其生命周期中的各种事件可能是有用的,以便可以执行自定义代码。通过在适当的时间调用`JobListener`,`SimpleJob`允许这样做: + +``` +public interface JobExecutionListener { + + void beforeJob(JobExecution jobExecution); + + void afterJob(JobExecution jobExecution); + +} +``` + +`JobListeners`可以通过在作业上设置侦听器来添加到`SimpleJob`。 + +下面的示例展示了如何将 Listener 元素添加到 XML 作业定义中: + +XML 配置 + +``` + + + + + + + + +``` + +下面的示例展示了如何将侦听器方法添加到 Java 作业定义中: + +Java 配置 + +``` +@Bean +public Job footballJob() { + return this.jobBuilderFactory.get("footballJob") + .listener(sampleListener()) + ... + .build(); +} +``` + +应该注意的是,无论`afterJob`方法的成功或失败,都调用`Job`方法。如果需要确定成功或失败,则可以从`JobExecution`中获得如下: + +``` +public void afterJob(JobExecution jobExecution){ + if (jobExecution.getStatus() == BatchStatus.COMPLETED ) { + //job success + } + else if (jobExecution.getStatus() == BatchStatus.FAILED) { + //job failure + } +} +``` + +与此接口对应的注释是: + +* `@BeforeJob` + +* `@AfterJob` + +#### [](#inheritingFromAParentJob)继承父作业 + +如果一组作业共享相似但不相同的配置,那么定义一个“父”`Job`可能会有所帮助,具体的作业可以从该“父”中继承属性。与 Java 中的类继承类似,“child”`Job`将把它的元素和属性与父元素和属性结合在一起。 + +在下面的示例中,“basejob”是一个抽象的`Job`定义,它只定义了一个侦听器列表。`Job`“job1”是一个具体的定义,它继承了“Basejob”的侦听器列表,并将其与自己的侦听器列表合并,以生成一个`Job`,其中包含两个侦听器和一个`Step`,即“步骤 1”。 + +``` + + + + + + + + + + + + + +``` + +有关更多详细信息,请参见[从父步骤继承](step.html#inheritingFromParentStep)一节。 + +#### [](#jobparametersvalidator)JobParametersValidator + +在 XML 命名空间中声明的作业或使用`AbstractJob`的任意子类可以在运行时为作业参数声明验证器。例如,当你需要断言一个作业是以其所有的强制参数启动时,这是有用的。有一个`DefaultJobParametersValidator`可以用来约束简单的强制参数和可选参数的组合,对于更复杂的约束,你可以自己实现接口。 + +验证程序的配置通过 XML 命名空间通过作业的一个子元素得到支持,如下面的示例所示: + +``` + + + + +``` + +验证器可以指定为引用(如前面所示),也可以指定为 bean 名称空间中的嵌套 Bean 定义。 + +通过 Java Builders 支持验证器的配置,如以下示例所示: + +``` +@Bean +public Job job1() { + return this.jobBuilderFactory.get("job1") + .validator(parametersValidator()) + ... + .build(); +} +``` + +### [](#javaConfig)Java 配置 + +Spring 3 带来了通过 Java 而不是 XML 配置应用程序的能力。从 Spring Batch2.2.0 开始,可以使用相同的 Java 配置配置来配置批处理作业。基于 Java 的配置有两个组件:`@EnableBatchProcessing`注释和两个构建器。 + +`@EnableBatchProcessing`的工作原理与 Spring 家族中的其他 @enable\* 注释类似。在这种情况下,`@EnableBatchProcessing`提供了用于构建批处理作业的基本配置。在这个基本配置中,除了许多可用于自动连线的 bean 之外,还创建了`StepScope`实例: + +* `JobRepository`: Bean 名称“jobrepository” + +* `JobLauncher`: Bean 名称“joblauncher” + +* `JobRegistry`: Bean 名称“jobregistry” + +* `PlatformTransactionManager`: Bean 名称“TransactionManager” + +* `JobBuilderFactory`: Bean name“jobbuilders” + +* `StepBuilderFactory`: Bean 名称“StepBuilders” + +此配置的核心接口是`BatchConfigurer`。默认的实现提供了上面提到的 bean,并且需要在要提供的上下文中提供一个`DataSource`作为 Bean。这个数据源由 JobRepository 使用。你可以通过创建`BatchConfigurer`接口的自定义实现来定制这些 bean 中的任何一个。通常,扩展`DefaultBatchConfigurer`(如果没有找到`BatchConfigurer`,则提供该扩展)并重写所需的吸气器就足够了。然而,可能需要从头开始实现自己的功能。下面的示例展示了如何提供自定义事务管理器: + +``` +@Bean +public BatchConfigurer batchConfigurer(DataSource dataSource) { + return new DefaultBatchConfigurer(dataSource) { + @Override + public PlatformTransactionManager getTransactionManager() { + return new MyTransactionManager(); + } + }; +} +``` + +| |只有一个配置类需要`@EnableBatchProcessing`注释。一旦
对一个类进行了注释,就可以使用上面的所有内容了。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有了基本配置,用户就可以使用提供的生成器工厂来配置作业。下面的示例显示了配置了`JobBuilderFactory`和`StepBuilderFactory`的两步作业: + +``` +@Configuration +@EnableBatchProcessing +@Import(DataSourceConfiguration.class) +public class AppConfig { + + @Autowired + private JobBuilderFactory jobs; + + @Autowired + private StepBuilderFactory steps; + + @Bean + public Job job(@Qualifier("step1") Step step1, @Qualifier("step2") Step step2) { + return jobs.get("myJob").start(step1).next(step2).build(); + } + + @Bean + protected Step step1(ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + return steps.get("step1") + . chunk(10) + .reader(reader) + .processor(processor) + .writer(writer) + .build(); + } + + @Bean + protected Step step2(Tasklet tasklet) { + return steps.get("step2") + .tasklet(tasklet) + .build(); + } +} +``` + +### [](#configuringJobRepository)配置 JobRepository + +当使用`@EnableBatchProcessing`时,将为你提供一个`JobRepository`。本节讨论如何配置自己的配置。 + +如前面所述,[`JobRepository`](#configurejob)用于 Spring 批处理中各种持久化域对象的基本增删改查操作,例如`JobExecution`和`StepExecution`。它是许多主要框架特性所要求的,例如`JobLauncher`、`Job`和`Step`。 + +批处理名称空间抽象出了`JobRepository`实现及其协作者的许多实现细节。然而,仍然有一些可用的配置选项,如以下示例所示: + +XML 配置 + +``` + +``` + +除了`id`之外,上面列出的配置选项都不是必需的。如果没有设置,将使用上面显示的默认值。以上所示是为了提高认识。`max-varchar-length`默认为 2500,这是[示例模式脚本](schema-appendix.html#metaDataSchemaOverview)中的长`VARCHAR`列的长度。 + +当使用 Java 配置时,将为你提供`JobRepository`。如果提供了`DataSource`,则提供了基于 JDBC 的一个,如果没有,则提供基于`Map`的一个。但是,你可以通过`BatchConfigurer`接口的实现来定制`JobRepository`的配置。 + +Java 配置 + +``` +... +// This would reside in your BatchConfigurer implementation +@Override +protected JobRepository createJobRepository() throws Exception { + JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); + factory.setDataSource(dataSource); + factory.setTransactionManager(transactionManager); + factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE"); + factory.setTablePrefix("BATCH_"); + factory.setMaxVarCharLength(1000); + return factory.getObject(); +} +... +``` + +除了数据源和 TransactionManager 之外,上面列出的配置选项都不是必需的。如果没有设置,将使用上面显示的默认值。以上所示是为了提高认识。最大 VARCHAR 长度默认为 2500,这是`VARCHAR`中的长[示例模式脚本](schema-appendix.html#metaDataSchemaOverview)列的长度 + +#### [](#txConfigForJobRepository)JobRepository 的事务配置 + +如果使用了名称空间或提供的`FactoryBean`,则会在存储库周围自动创建事务建议。这是为了确保批处理元数据(包括在发生故障后重新启动所必需的状态)被正确地持久化。如果存储库方法不是事务性的,那么框架的行为就没有得到很好的定义。`create*`方法属性中的隔离级别是单独指定的,以确保在启动作业时,如果两个进程试图同时启动相同的作业,则只有一个进程成功。该方法的默认隔离级别是`SERIALIZABLE`,这是非常激进的。`READ_COMMITTED`同样有效。如果两个过程不太可能以这种方式碰撞,`READ_UNCOMMITTED`就可以了。然而,由于对`create*`方法的调用相当短,所以只要数据库平台支持它,`SERIALIZED`不太可能导致问题。然而,这一点可以被重写。 + +下面的示例展示了如何覆盖 XML 中的隔离级别: + +XML 配置 + +``` + +``` + +下面的示例展示了如何在 Java 中重写隔离级别: + +Java 配置 + +``` +// This would reside in your BatchConfigurer implementation +@Override +protected JobRepository createJobRepository() throws Exception { + JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); + factory.setDataSource(dataSource); + factory.setTransactionManager(transactionManager); + factory.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ"); + return factory.getObject(); +} +``` + +如果没有使用名称空间或工厂 bean,那么使用 AOP 配置存储库的事务行为也是必不可少的。 + +下面的示例展示了如何在 XML 中配置存储库的事务行为: + +XML 配置 + +``` + + + + + + + + + + +``` + +前面的片段可以按原样使用,几乎没有变化。还请记住包括适当的名称空间声明,并确保 Spring-tx 和 Spring- AOP(或整个 Spring)都在 Classpath 上。 + +下面的示例展示了如何在 Java 中配置存储库的事务行为: + +Java 配置 + +``` +@Bean +public TransactionProxyFactoryBean baseProxy() { + TransactionProxyFactoryBean transactionProxyFactoryBean = new TransactionProxyFactoryBean(); + Properties transactionAttributes = new Properties(); + transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED"); + transactionProxyFactoryBean.setTransactionAttributes(transactionAttributes); + transactionProxyFactoryBean.setTarget(jobRepository()); + transactionProxyFactoryBean.setTransactionManager(transactionManager()); + return transactionProxyFactoryBean; +} +``` + +#### [](#repositoryTablePrefix)更改表格前缀 + +`JobRepository`的另一个可修改的属性是元数据表的表前缀。默认情况下,它们都以`BATCH_`开头。`BATCH_JOB_EXECUTION`和`BATCH_STEP_EXECUTION`是两个例子。然而,有潜在的理由修改这个前缀。如果需要将模式名称前置到表名,或者如果同一模式中需要多个元数据表集合,则需要更改表前缀: + +下面的示例展示了如何更改 XML 中的表前缀: + +XML 配置 + +``` + +``` + +下面的示例展示了如何在 Java 中更改表前缀: + +Java 配置 + +``` +// This would reside in your BatchConfigurer implementation +@Override +protected JobRepository createJobRepository() throws Exception { + JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); + factory.setDataSource(dataSource); + factory.setTransactionManager(transactionManager); + factory.setTablePrefix("SYSTEM.TEST_"); + return factory.getObject(); +} +``` + +给定上述更改,对元数据表的每个查询都以`SYSTEM.TEST_`作为前缀。`BATCH_JOB_EXECUTION`被称为系统。`TEST_JOB_EXECUTION`。 + +| |只有表前缀是可配置的。表和列名不是。| +|---|--------------------------------------------------------------------------| + +#### [](#inMemoryRepository)内存存储库 + +在某些情况下,你可能不希望将域对象持久化到数据库。原因之一可能是速度;在每个提交点存储域对象需要额外的时间。另一个原因可能是,你不需要为一份特定的工作坚持现状。出于这个原因, Spring 批处理提供了作业存储库的内存`Map`版本。 + +下面的示例显示了`MapJobRepositoryFactoryBean`在 XML 中的包含: + +XML 配置 + +``` + + + +``` + +下面的示例显示了在 Java 中包含`MapJobRepositoryFactoryBean`: + +Java 配置 + +``` +// This would reside in your BatchConfigurer implementation +@Override +protected JobRepository createJobRepository() throws Exception { + MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean(); + factory.setTransactionManager(transactionManager); + return factory.getObject(); +} +``` + +请注意,内存中的存储库是不稳定的,因此不允许在 JVM 实例之间重新启动。它也不能保证具有相同参数的两个作业实例同时启动,并且不适合在多线程作业或本地分区`Step`中使用。因此,只要你需要这些特性,就可以使用存储库的数据库版本。 + +但是,它确实需要定义事务管理器,因为存储库中存在回滚语义,并且业务逻辑可能仍然是事务性的(例如 RDBMS 访问)。对于测试目的,许多人发现`ResourcelessTransactionManager`很有用。 + +| |在 V4 中,`MapJobRepositoryFactoryBean`和相关的类已被弃用,并计划在 V5 中删除
。如果希望使用内存中的作业存储库,可以使用嵌入式数据库
,比如 H2、 Apache Derby 或 HSQLDB。有几种方法可以创建嵌入式数据库并在
你的 Spring 批处理应用程序中使用它。一种方法是使用[Spring JDBC](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-embedded-database-support)中的 API:

```
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("/org/springframework/batch/core/schema-drop-h2.sql")
.addScript("/org/springframework/batch/core/schema-h2.sql")
.build();
}
```

一旦你在应用程序上下文中将嵌入式数据源定义为 Bean,如果你使用`@EnableBatchProcessing`,就应该自动选择
。否则,你可以使用
基于`JobRepositoryFactoryBean`的 JDBC 手动配置它,如[配置 JobRepository 部分](#configuringJobRepository)所示。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#nonStandardDatabaseTypesInRepository)存储库中的非标准数据库类型 + +如果你使用的数据库平台不在受支持的平台列表中,那么如果 SQL 变量足够接近,则可以使用受支持的类型之一。要做到这一点,你可以使用 RAW`JobRepositoryFactoryBean`而不是名称空间快捷方式,并使用它将数据库类型设置为最接近的匹配。 + +下面的示例展示了如何使用`JobRepositoryFactoryBean`将数据库类型设置为 XML 中最接近的匹配: + +XML 配置 + +``` + + + + +``` + +下面的示例展示了如何使用`JobRepositoryFactoryBean`将数据库类型设置为 Java 中最接近的匹配: + +Java 配置 + +``` +// This would reside in your BatchConfigurer implementation +@Override +protected JobRepository createJobRepository() throws Exception { + JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); + factory.setDataSource(dataSource); + factory.setDatabaseType("db2"); + factory.setTransactionManager(transactionManager); + return factory.getObject(); +} +``` + +(如果没有指定,`JobRepositoryFactoryBean`会尝试从`DataSource`中自动检测数据库类型,)平台之间的主要差异主要是由主键递增策略造成的,因此,通常可能还需要覆盖`incrementerFactory`(使用 Spring 框架中的一个标准实现)。 + +如果连这都不起作用,或者你没有使用 RDBMS,那么唯一的选择可能是实现`Dao`所依赖的各种`SimpleJobRepository`接口,并以正常的方式手动连接。 + +### [](#configuringJobLauncher)配置一个 joblauncher + +当使用`@EnableBatchProcessing`时,将为你提供一个`JobRegistry`。本节讨论如何配置自己的配置。 + +`JobLauncher`接口的最基本实现是`SimpleJobLauncher`。它唯一需要的依赖关系是`JobRepository`,以便获得执行。 + +下面的示例显示了 XML 中的`SimpleJobLauncher`: + +XML 配置 + +``` + + + +``` + +下面的示例显示了 Java 中的`SimpleJobLauncher`: + +Java 配置 + +``` +... +// This would reside in your BatchConfigurer implementation +@Override +protected JobLauncher createJobLauncher() throws Exception { + SimpleJobLauncher jobLauncher = new SimpleJobLauncher(); + jobLauncher.setJobRepository(jobRepository); + jobLauncher.afterPropertiesSet(); + return jobLauncher; +} +... +``` + +一旦获得了[工作执行](domain.html#domainLanguageOfBatch),它就被传递给`Job`的执行方法,最终将`JobExecution`返回给调用者,如下图所示: + +![作业启动器序列](./images/job-launcher-sequence-sync.png) + +图 2。作业启动器序列 + +这个序列很简单,从调度程序启动时效果很好。然而,在尝试从 HTTP 请求启动时会出现问题。在这种情况下,启动需要异步完成,以便`SimpleJobLauncher`立即返回其调用方。这是因为,在长时间运行的进程(如批处理)所需的时间内保持 HTTP 请求的开放状态是不好的做法。下图显示了一个示例序列: + +![异步作业启动器序列](./images/job-launcher-sequence-async.png) + +图 3。异步作业启动器序列 + +可以通过配置`TaskExecutor`将`SimpleJobLauncher`配置为允许这种情况。 + +下面的 XML 示例显示了配置为立即返回的`SimpleJobLauncher`: + +XML 配置 + +``` + + + + + + +``` + +下面的 Java 示例显示了配置为立即返回的`SimpleJobLauncher`: + +Java 配置 + +``` +@Bean +public JobLauncher jobLauncher() { + SimpleJobLauncher jobLauncher = new SimpleJobLauncher(); + jobLauncher.setJobRepository(jobRepository()); + jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor()); + jobLauncher.afterPropertiesSet(); + return jobLauncher; +} +``` + +Spring `TaskExecutor`接口的任何实现都可以用来控制如何异步执行作业。 + +### [](#runningAJob)运行作业 + +至少,启动批处理作业需要两个条件:启动`Job`和`JobLauncher`。两者都可以包含在相同的上下文中,也可以包含在不同的上下文中。例如,如果从命令行启动一个作业,将为每个作业实例化一个新的 JVM,因此每个作业都有自己的`JobLauncher`。但是,如果在`HttpRequest`范围内的 Web 容器中运行,通常会有一个`JobLauncher`,该配置用于异步作业启动,多个请求将调用以启动其作业。 + +#### [](#runningJobsFromCommandLine)从命令行运行作业 + +对于希望从 Enterprise 调度器运行作业的用户,命令行是主要的接口。这是因为大多数调度程序(Quartz 除外,除非使用 nativeJob)直接与操作系统进程一起工作,主要是通过 shell 脚本开始的。除了 shell 脚本之外,还有许多启动 Java 进程的方法,例如 Perl、Ruby,甚至是 Ant 或 Maven 之类的“构建工具”。但是,由于大多数人都熟悉 shell 脚本,因此本例将重点讨论它们。 + +##### [](#commandLineJobRunner)The CommandlineJobrunner + +因为启动作业的脚本必须启动一个 Java 虚拟机,所以需要有一个具有 main 方法的类来充当主要入口点。 Spring 批处理提供了一种实现,它仅服务于此目的:`CommandLineJobRunner`。需要注意的是,这只是引导应用程序的一种方法,但是启动 Java 进程的方法有很多,并且这个类绝不应该被视为确定的。`CommandLineJobRunner`执行四项任务: + +* 装入适当的`ApplicationContext` + +* 将命令行参数解析为`JobParameters` + +* 根据参数定位适当的作业 + +* 使用应用程序上下文中提供的`JobLauncher`来启动作业。 + +所有这些任务都是仅使用传入的参数来完成的。以下是必要的论据: + +|jobPath|将用于
的 XML 文件的位置创建一个`ApplicationContext`。此文件
应该包含运行完整
作业所需的所有内容| +|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +|jobName|要运行的作业的名称。| + +这些参数必须首先传递路径,然后传递名称。在这些参数之后的所有参数都被认为是作业参数,被转换为一个 JobParameters 对象,并且必须是“name=value”的格式。 + +下面的示例显示了作为作业参数传递给 XML 中未定义的作业的日期: + +``` +键/值对转换为标识作业参数。但是,在下面的示例中,可以通过分别使用`+`或`-`前缀来显式地指定
哪些作业参数是标识的,哪些不是标识的。

,`schedule.date`是一个标识作业参数,而`vendor.id`不是:

```
+schedule.date(date)=2007/05/05 -vendor.id=123
```
```
+schedule.date(date)=2007/05/05 -vendor.id=123
```
可以通过使用自定义`JobParametersConverter`来重写此行为。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在大多数情况下,你可能希望使用清单在 JAR 中声明主类,但为了简单起见,直接使用了该类。这个示例使用的是来自[DomainLanguageofBatch](domain.html#domainLanguageOfBatch)的相同的“Endofday”示例。第一个参数是“endofdayjob.xml”,这是 Spring 包含`Job`的应用上下文。第二个参数“Endofday”表示工作名称。最后一个参数“schedule.date=2007/05/05”被转换为一个 JobParameters 对象。 + +下面的示例显示了在 XML 中`endOfDay`的示例配置: + +``` + + + + + + +``` + +在大多数情况下,你希望使用清单在 JAR 中声明主类,但为了简单起见,直接使用了该类。这个示例使用的是来自[DomainLanguageofBatch](domain.html#domainLanguageOfBatch)的相同的“Endofday”示例。第一个参数是“IO. Spring.EndofdayJobConfiguration”,它是包含该作业的配置类的完全限定类名称。第二个参数“Endofday”表示工作名称。最后一个参数’schedule.date=2007/05/05’被转换为`JobParameters`对象。下面是 Java 配置的一个示例: + +下面的示例显示了在 Java 中`endOfDay`的示例配置: + +``` +@Configuration +@EnableBatchProcessing +public class EndOfDayJobConfiguration { + + @Autowired + private JobBuilderFactory jobBuilderFactory; + + @Autowired + private StepBuilderFactory stepBuilderFactory; + + @Bean + public Job endOfDay() { + return this.jobBuilderFactory.get("endOfDay") + .start(step1()) + .build(); + } + + @Bean + public Step step1() { + return this.stepBuilderFactory.get("step1") + .tasklet((contribution, chunkContext) -> null) + .build(); + } +} +``` + +前面的示例过于简单,因为在 Spring 批处理中运行一个批处理作业通常有更多的需求,但是它用于显示`CommandLineJobRunner`的两个主要需求:`Job`和`JobLauncher`。 + +##### [](#exitCodes)exitcodes + +当从命令行启动批处理作业时,通常使用 Enterprise 调度器。大多数调度器都相当笨拙,只能在流程级别工作。这意味着他们只知道一些操作系统进程,比如他们正在调用的 shell 脚本。在这种情况下,将工作的成功或失败反馈给调度程序的唯一方法是通过返回代码。返回代码是进程返回给调度程序的一个数字,它指示运行的结果。在最简单的情况下:0 是成功,1 是失败。然而,可能有更复杂的情况:如果作业 A 返回 4,则启动作业 B,如果它返回 5,则启动作业 C。这种类型的行为是在计划程序级别上配置的,但是重要的是, Spring 批处理框架提供了一种方法来返回用于特定批处理作业的“退出代码”的数字表示。在 Spring 批处理中,这被封装在`ExitStatus`中,这在第 5 章中有更详细的介绍。为了讨论退出代码,唯一需要知道的是`ExitStatus`具有一个退出代码属性,该属性由框架(或开发人员)设置,并作为从`JobLauncher`返回的`JobExecution`的一部分返回。`CommandLineJobRunner`使用`ExitCodeMapper`接口将这个字符串值转换为一个数字: + +``` +public interface ExitCodeMapper { + + public int intValue(String exitCode); + +} +``` + +`ExitCodeMapper`的基本契约是,给定一个字符串退出代码,将返回一个数字表示。Job Runner 使用的默认实现是`SimpleJvmExitCodeMapper`,它返回 0 表示完成,1 表示泛型错误,2 表示任何 Job Runner 错误,例如无法在提供的上下文中找到`Job`。如果需要比上述 3 个值更复杂的值,则必须提供`ExitCodeMapper`接口的自定义实现。因为`CommandLineJobRunner`是创建`ApplicationContext`的类,因此不能“连线在一起”,所以需要重写的任何值都必须是自动连线的。这意味着,如果在`BeanFactory`中找到了`ExitCodeMapper`的实现,则将在创建上下文后将其注入到运行器中。要提供你自己的`ExitCodeMapper`,需要做的就是将实现声明为根级别 Bean,并确保它是由运行器加载的`ApplicationContext`的一部分。 + +#### [](#runningJobsFromWebContainer)在 Web 容器中运行作业 + +从历史上看,离线处理(如批处理作业)是从命令行启动的,如上文所述。然而,在许多情况下,从`HttpRequest`发射是更好的选择。许多这样的用例包括报告、临时作业运行和 Web 应用程序支持。因为按定义,批处理作业是长时间运行的,所以最重要的问题是确保异步启动该作业: + +![基于 Web 容器的异步作业启动器序列](./images/launch-from-request.png) + +图 4。来自 Web 容器的异步作业启动器序列 + +在这种情况下,控制器是 Spring MVC 控制器。关于 Spring MVC 的更多信息可以在这里找到:[](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc)[https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc)。控制器使用已配置为启动[异步](#runningJobsFromWebContainer)的`Job`启动`Job`,该控制器立即返回`JobExecution`。`Job`可能仍在运行,但是,这种非阻塞行为允许控制器立即返回,这是处理`HttpRequest`时所需的。以下是一个例子: + +``` +@Controller +public class JobLauncherController { + + @Autowired + JobLauncher jobLauncher; + + @Autowired + Job job; + + @RequestMapping("/jobLauncher.html") + public void handle() throws Exception{ + jobLauncher.run(job, new JobParameters()); + } +} +``` + +### [](#advancedMetaData)高级元数据使用 + +到目前为止,`JobLauncher`和`JobRepository`接口都已经讨论过了。它们一起表示作业的简单启动,以及批处理域对象的基本操作: + +![作业存储库](./images/job-repository.png) + +图 5。作业存储库 + +a`JobLauncher`使用`JobRepository`来创建新的`JobExecution`对象并运行它们。`Job`和`Step`实现稍后将使用相同的`JobRepository`用于运行作业期间相同执行的基本更新。对于简单的场景,基本的操作就足够了,但是在具有数百个批处理任务和复杂的调度需求的大批处理环境中,需要对元数据进行更高级的访问: + +![作业存储库高级版](./images/job-repository-advanced.png) + +图 6。高级作业存储库访问 + +下面将讨论`JobExplorer`和`JobOperator`接口,它们添加了用于查询和控制元数据的附加功能。 + +#### [](#queryingRepository)查询存储库 + +在任何高级特性之前,最基本的需求是查询存储库中现有执行的能力。此功能由`JobExplorer`接口提供: + +``` +public interface JobExplorer { + + List getJobInstances(String jobName, int start, int count); + + JobExecution getJobExecution(Long executionId); + + StepExecution getStepExecution(Long jobExecutionId, Long stepExecutionId); + + JobInstance getJobInstance(Long instanceId); + + List getJobExecutions(JobInstance jobInstance); + + Set findRunningJobExecutions(String jobName); +} +``` + +从上面的方法签名中可以明显看出,`JobExplorer`是`JobRepository`的只读版本,并且,像`JobRepository`一样,可以通过使用工厂 Bean 轻松地对其进行配置: + +下面的示例展示了如何在 XML 中配置`JobExplorer`: + +XML 配置 + +``` + +``` + +下面的示例展示了如何在 Java 中配置`JobExplorer`: + +Java 配置 + +``` +... +// This would reside in your BatchConfigurer implementation +@Override +public JobExplorer getJobExplorer() throws Exception { + JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean(); + factoryBean.setDataSource(this.dataSource); + return factoryBean.getObject(); +} +... +``` + +[在本章的前面](#repositoryTablePrefix),我们注意到`JobRepository`的表前缀可以进行修改以允许不同的版本或模式。因为`JobExplorer`与相同的表一起工作,所以它也需要设置前缀的能力。 + +下面的示例展示了如何在 XML 中设置`JobExplorer`的表前缀: + +XML 配置 + +``` + +``` + +下面的示例展示了如何在 Java 中设置`JobExplorer`的表前缀: + +Java 配置 + +``` +... +// This would reside in your BatchConfigurer implementation +@Override +public JobExplorer getJobExplorer() throws Exception { + JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean(); + factoryBean.setDataSource(this.dataSource); + factoryBean.setTablePrefix("SYSTEM."); + return factoryBean.getObject(); +} +... +``` + +#### [](#jobregistry)JobRegistry + +a`JobRegistry`(及其父接口`JobLocator`)不是强制性的,但如果你想跟踪上下文中哪些作业可用,它可能会很有用。当工作在其他地方创建时(例如,在子上下文中),它对于在应用程序上下文中集中收集工作也很有用。还可以使用自定义`JobRegistry`实现来操作已注册作业的名称和其他属性。该框架只提供了一个实现,它基于从作业名称到作业实例的简单映射。 + +下面的示例展示了如何为 XML 中定义的作业包含`JobRegistry`: + +``` + +``` + +下面的示例展示了如何为 Java 中定义的作业包含`JobRegistry`: + +当使用`@EnableBatchProcessing`时,将为你提供一个`JobRegistry`。如果你想配置自己的: + +``` +... +// This is already provided via the @EnableBatchProcessing but can be customized via +// overriding the getter in the SimpleBatchConfiguration +@Override +@Bean +public JobRegistry jobRegistry() throws Exception { + return new MapJobRegistry(); +} +... +``` + +有两种方法可以自动填充`JobRegistry`:使用 Bean 后处理器和使用注册商生命周期组件。这两种机制在下面的部分中进行了描述。 + +##### [](#jobregistrybeanpostprocessor)jobregistrybeanpostprocessor + +这是一个 Bean 后处理器,它可以在创建所有作业时注册它们。 + +下面的示例展示了如何为 XML 中定义的作业包括`JobRegistryBeanPostProcessor`: + +XML 配置 + +``` + + + +``` + +下面的示例展示了如何为在 Java 中定义的作业包括`JobRegistryBeanPostProcessor`: + +Java 配置 + +``` +@Bean +public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() { + JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor(); + postProcessor.setJobRegistry(jobRegistry()); + return postProcessor; +} +``` + +虽然这不是严格必要的,但是在示例中的后处理器已经被赋予了一个 ID,以便它可以被包括在子上下文中(例如作为父 Bean 定义),并导致在那里创建的所有作业也被自动注册。 + +##### [](#automaticjobregistrar)`AutomaticJobRegistrar` + +这是一个生命周期组件,它创建子上下文,并在创建这些上下文时从这些上下文注册作业。这样做的一个好处是,虽然子上下文中的作业名称在注册表中仍然必须是全局唯一的,但它们的依赖项可能具有“自然”名称。因此,例如,你可以创建一组 XML 配置文件,每个配置文件只具有一个作业,但所有配置文件都具有具有具有相同 Bean 名称的`ItemReader`的不同定义,例如“reader”。如果将所有这些文件导入到相同的上下文中,则读写器定义将发生冲突并相互覆盖,但是使用自动注册器可以避免这种情况。这使得集成来自应用程序的独立模块的作业变得更加容易。 + +下面的示例展示了如何为 XML 中定义的作业包括`AutomaticJobRegistrar`: + +XML 配置 + +``` + + + + + + + + + + + + +``` + +下面的示例展示了如何为在 Java 中定义的作业包括`AutomaticJobRegistrar`: + +Java 配置 + +``` +@Bean +public AutomaticJobRegistrar registrar() { + + AutomaticJobRegistrar registrar = new AutomaticJobRegistrar(); + registrar.setJobLoader(jobLoader()); + registrar.setApplicationContextFactories(applicationContextFactories()); + registrar.afterPropertiesSet(); + return registrar; + +} +``` + +注册商有两个强制属性,一个是`ApplicationContextFactory`的数组(这里是从方便的工厂 Bean 创建的),另一个是`JobLoader`。`JobLoader`负责管理子上下文的生命周期,并在`JobRegistry`中注册作业。 + +`ApplicationContextFactory`负责创建子上下文,最常见的用法是使用`ClassPathXmlApplicationContextFactory`。这个工厂的一个特性是,默认情况下,它会将一些配置从父上下文复制到子上下文。因此,例如,如果它应该与父配置相同,则不必重新定义子配置中的`PropertyPlaceholderConfigurer`或 AOP 配置。 + +如果需要,`AutomaticJobRegistrar`可以与`JobRegistryBeanPostProcessor`一起使用(只要`DefaultJobLoader`也可以使用)。例如,如果在主父上下文和子位置中定义了作业,那么这可能是可取的。 + +#### [](#JobOperator)joboperator + +如前所述,`JobRepository`提供对元数据的增删改查操作,而`JobExplorer`提供对元数据的只读操作。然而,当这些操作一起用来执行常见的监视任务时,它们是最有用的,例如停止、重新启动或汇总作业,就像批处理操作符通常做的那样。 Spring 批处理通过`JobOperator`接口提供这些类型的操作: + +``` +public interface JobOperator { + + List getExecutions(long instanceId) throws NoSuchJobInstanceException; + + List getJobInstances(String jobName, int start, int count) + throws NoSuchJobException; + + Set getRunningExecutions(String jobName) throws NoSuchJobException; + + String getParameters(long executionId) throws NoSuchJobExecutionException; + + Long start(String jobName, String parameters) + throws NoSuchJobException, JobInstanceAlreadyExistsException; + + Long restart(long executionId) + throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException, + NoSuchJobException, JobRestartException; + + Long startNextInstance(String jobName) + throws NoSuchJobException, JobParametersNotFoundException, JobRestartException, + JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException; + + boolean stop(long executionId) + throws NoSuchJobExecutionException, JobExecutionNotRunningException; + + String getSummary(long executionId) throws NoSuchJobExecutionException; + + Map getStepExecutionSummaries(long executionId) + throws NoSuchJobExecutionException; + + Set getJobNames(); + +} +``` + +上面的操作表示来自许多不同接口的方法,例如`JobLauncher`、`JobRepository`、`JobExplorer`和`JobRegistry`。由于这个原因,所提供的`JobOperator`、`SimpleJobOperator`的实现具有许多依赖性。 + +下面的示例显示了 XML 中`SimpleJobOperator`的典型 Bean 定义: + +``` + + + + + + + + + + +``` + +下面的示例显示了 Java 中`SimpleJobOperator`的典型 Bean 定义: + +``` + /** + * All injected dependencies for this bean are provided by the @EnableBatchProcessing + * infrastructure out of the box. + */ + @Bean + public SimpleJobOperator jobOperator(JobExplorer jobExplorer, + JobRepository jobRepository, + JobRegistry jobRegistry) { + + SimpleJobOperator jobOperator = new SimpleJobOperator(); + + jobOperator.setJobExplorer(jobExplorer); + jobOperator.setJobRepository(jobRepository); + jobOperator.setJobRegistry(jobRegistry); + jobOperator.setJobLauncher(jobLauncher); + + return jobOperator; + } +``` + +| |如果你在作业存储库上设置了表前缀,请不要忘记在作业资源管理器上也设置它。| +|---|------------------------------------------------------------------------------------------------------| + +#### [](#JobParametersIncrementer)JobParametersIncrementer + +关于`JobOperator`的大多数方法都是不言自明的,更详细的解释可以在[接口的 Javadoc](https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/core/launch/JobOperator.html)上找到。然而,`startNextInstance`方法是值得注意的。这个方法总是会启动一个作业的新实例。如果`JobExecution`中存在严重问题,并且需要从一开始就重新开始工作,那么这将非常有用。与`JobLauncher`不同,`JobLauncher`需要一个新的`JobParameters`对象,如果参数与以前的任何一组参数不同,则该对象将触发一个新的`JobInstance`,`startNextInstance`方法将使用绑定到`JobParametersIncrementer`的`Job`来强制将`Job`转换为一个新实例: + +``` +public interface JobParametersIncrementer { + + JobParameters getNext(JobParameters parameters); + +} +``` + +`JobParametersIncrementer`的约定是,给定一个[工作参数](#jobParameters)对象,它将通过递增它可能包含的任何必要值来返回“next”JobParameters 对象。这个策略是有用的,因为框架无法知道`JobParameters`的更改是什么,使它成为“下一个”实例。例如,如果`JobParameters`中的唯一值是日期,并且应该创建下一个实例,那么该值应该增加一天吗?或者一周(例如,如果工作是每周一次的话)?对于有助于识别工作的任何数值,也可以这样说,如下所示: + +``` +public class SampleIncrementer implements JobParametersIncrementer { + + public JobParameters getNext(JobParameters parameters) { + if (parameters==null || parameters.isEmpty()) { + return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters(); + } + long id = parameters.getLong("run.id",1L) + 1; + return new JobParametersBuilder().addLong("run.id", id).toJobParameters(); + } +} +``` + +在本例中,使用带有“run.id”键的值来区分`JobInstances`。如果传入的`JobParameters`为空,则可以假定`Job`以前从未运行过,因此可以返回其初始状态。但是,如果不是,则获得旧值,将其递增 1 并返回。 + +对于 XML 中定义的作业,Incrementer 可以通过名称空间中的’Incrementer’属性与`Job`关联,如下所示: + +``` + + ... + +``` + +对于在 Java 中定义的作业,增量程序可以通过构建器中提供的`incrementer`方法与“作业”关联,如下所示: + +``` +@Bean +public Job footballJob() { + return this.jobBuilderFactory.get("footballJob") + .incrementer(sampleIncrementer()) + ... + .build(); +} +``` + +#### [](#stoppingAJob)停止工作 + +`JobOperator`最常见的用例之一是优雅地停止一项工作: + +``` +Set executions = jobOperator.getRunningExecutions("sampleJob"); +jobOperator.stop(executions.iterator().next()); +``` + +关闭不是立即的,因为无法强制立即关闭,特别是如果当前执行的是框架无法控制的开发人员代码,例如业务服务。但是,一旦将控件返回到框架中,就会将当前`StepExecution`的状态设置为`BatchStatus.STOPPED`,保存它,然后在完成之前对`JobExecution`执行相同的操作。 + +#### [](#aborting-a-job)终止作业 + +可以重新启动`FAILED`的作业执行(如果`Job`是可重启的)。状态为`ABANDONED`的作业执行将不会被框架重新启动。在步骤执行中,`ABANDONED`状态也被用于在重新启动的作业执行中将其标记为可跳过的:如果作业正在执行,并且遇到了在上一个失败的作业执行中标记`ABANDONED`的步骤,它将进入下一个步骤(由作业流定义和步骤执行退出状态决定)。 + +如果进程死了(`"kill -9"`或服务器故障),作业当然不在运行,但是`JobRepository`无法知道,因为在进程死之前没有人告诉它。你必须手动告诉它,你知道执行失败或应该被视为已中止(将其状态更改为`FAILED`或`ABANDONED`)-这是一个业务决策,没有办法使其自动化。如果状态不是可重启的,或者你知道重新启动数据是有效的,则仅将状态更改为`FAILED`。 Spring batch admin`JobService`中有一个实用程序来中止作业执行。 \ No newline at end of file diff --git a/docs/spring-batch/jsr-352.md b/docs/spring-batch/jsr-352.md new file mode 100644 index 0000000..2a4cb5f --- /dev/null +++ b/docs/spring-batch/jsr-352.md @@ -0,0 +1,312 @@ +# JSR-352 支援 + +## [](#jsr-352)JSR-352 支持 + +XMLJavaBoth + +截至 Spring,对 JSR-352 的批处理 3.0 支持已经完全实现。本节不是规范本身的替代,而是打算解释 JSR-352 特定概念如何应用于 Spring 批处理。有关 JSR-352 的其他信息可以通过 JCP 在这里找到:[](https://jcp.org/en/jsr/detail?id=352)[https://jcp.org/en/jsr/detail?id=352](https://jcp.org/en/jsr/detail?id=352) + +### [](#jsrGeneralNotes)关于 Spring 批和 JSR-352 的一般说明 + +Spring Batch 和 JSR-352 在结构上是相同的。他们俩的工作都是由台阶组成的。它们都有读取器、处理器、编写器和监听器。然而,他们之间的互动却有微妙的不同。例如, Spring 批处理中的`org.springframework.batch.core.SkipListener#onSkipInWrite(S item, Throwable t)`接收两个参数:被跳过的项和导致跳过的异常。相同方法的 JSR-352 版本(`javax.batch.api.chunk.listener.SkipWriteListener#onSkipWriteItem(List items, Exception ex)`)也接收两个参数。但是,第一个是当前块中所有项的`List`,第二个是导致跳过的`Exception`。由于这些差异,重要的是要注意,在 Spring 批处理中执行作业有两种路径:传统的 Spring 批处理作业或基于 JSR-352 的作业。虽然 Spring 批处理工件(读取器、编写器等)的使用将在使用 JSR-352 的 JSL 配置并使用`JsrJobOperator`执行的作业中进行,但它们的行为将遵循 JSR-352 的规则。还需要注意的是,针对 JSR-352 接口开发的批处理工件将不能在传统的批处理作业中工作。 + +### [](#jsrSetup)设置 + +#### [](#jsrSetupContexts)应用程序上下文 + +Spring 批处理中的所有基于 JSR-352 的作业都由两个应用程序上下文组成。父上下文,它包含与 Spring 批处理的基础结构相关的 bean,例如`JobRepository`、`PlatformTransactionManager`等,以及包含要运行的作业的配置的子上下文。父上下文是通过框架提供的`jsrBaseContext.xml`定义的。可以通过设置`JSR-352-BASE-CONTEXT`系统属性来重写此上下文。 + +| |对于属性注入之类的事情,JSR-352 处理器不会处理基本上下文,因此
不需要在此配置额外处理的组件。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#jsrSetupLaunching)启动基于 JSR-352 的作业 + +JSR-352 需要一个非常简单的路径来执行批处理作业。以下代码是执行第一批作业所需的全部内容: + +``` +JobOperator operator = BatchRuntime.getJobOperator(); +jobOperator.start("myJob", new Properties()); +``` + +虽然这对开发人员来说很方便,但问题出在细节上。 Spring 批处理引导了一些幕后的基础设施,开发人员可能想要覆盖这些基础设施。下面是第一次调用`BatchRuntime.getJobOperator()`时的引导: + +| *Bean Name* | *Default Configuration* |*笔记*| +|------------------------|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dataSource | Apache DBCP BasicDataSource with configured values. |默认情况下,HSQLDB 是引导的。| +| `transactionManager` | `org.springframework.jdbc.datasource.DataSourceTransactionManager` |引用了上面定义的数据源 Bean。| +|A Datasource initializer| |这被配置为执行通过`batch.drop.script`和`batch.schema.script`属性配置的脚本。通过
默认值,HSQLDB 的模式脚本被执行。可以通过设置`batch.data.source.init`属性来禁用此行为。| +| jobRepository | A JDBC based `SimpleJobRepository`. |此`JobRepository`使用前面提到的数据源和事务
管理器。模式的表前缀可以通过`batch.table.prefix`属性进行配置(默认为批处理 \_)。| +| jobLauncher | `org.springframework.batch.core.launch.support.SimpleJobLauncher` |用来启动工作。| +| batchJobOperator | `org.springframework.batch.core.launch.support.SimpleJobOperator` |`JsrJobOperator`对此进行了包装,以提供其大部分功能。| +| jobExplorer |`org.springframework.batch.core.explore.support.JobExplorerFactoryBean`|用于解决`JsrJobOperator`提供的查找功能。| +| jobParametersConverter | `org.springframework.batch.core.jsr.JsrJobParametersConverter` |JSR-352 具体实现`JobParametersConverter`。| +| jobRegistry | `org.springframework.batch.core.configuration.support.MapJobRegistry` |由`SimpleJobOperator`使用。| +| placeholderProperties |`org.springframework.beans.factory.config.PropertyPlaceholderConfigure`|加载属性文件`batch-${ENVIRONMENT:hsql}.properties`来配置
上面提到的属性。Environment 是一个系统属性(默认为`hsql`)
,可用于指定当前
支持的任何受支持的数据库 Spring 批处理。| + +| |对于执行基于 JSR-352 的作业,上面的 bean 都不是可选的。所有这些都可以被重写到
,根据需要提供定制的功能。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#dependencyInjection)依赖注入 + +JSR-352 在很大程度上基于 Spring 批编程模型。因此,虽然没有显式地要求正式的依赖注入实现,但是隐含了某种类型的 DI。 Spring 批处理支持用于加载 JSR-352 定义的批处理工件的所有三种方法: + +* 实现特定的加载程序: Spring 批处理是建立在 Spring 之上的,因此在 JSR-352 批处理作业中支持 Spring 依赖注入。 + +* archive loader:JSR-352 定义了一个`batch.xml`文件的存在,该文件提供了逻辑名和类名之间的映射。如果使用此文件,则必须在`/META-INF/`目录中找到该文件。 + +* 线程上下文类装入器:JSR-352 允许配置通过内联提供完全限定的类名来指定其 JSL 中的批处理工件实现。 Spring 批处理在 JSR-352 配置的作业中也支持这一点。 + +在基于 JSR-352 的批处理作业中使用 Spring 依赖注入包括使用 Spring 应用程序上下文作为 bean 来配置批处理工件。一旦定义了 bean,作业就可以引用它们,就像在`batch.xml`文件中定义的任何 Bean 一样。 + +下面的示例展示了如何在 XML 中基于 JSR-352 的批处理作业中使用 Spring 依赖注入: + +XML 配置 + +``` + + + + + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中基于 JSR-352 的批处理作业中使用 Spring 依赖注入: + +Java 配置 + +``` +@Configuration +public class BatchConfiguration { + + @Bean + public Batchlet fooBatchlet() { + FooBatchlet batchlet = new FooBatchlet(); + batchlet.setProp("bar"); + return batchlet; + } +} + + + + + + + +``` + +Spring 上下文(导入等)的组装与 JSR-352 作业一起工作,就像与任何其他基于 Spring 的应用程序一起工作一样。与基于 JSR-352 的作业的唯一不同之处在于,上下文定义的入口点将是/meta-inf/batch-jobs/中找到的作业定义。 + +要使用线程上下文类装入器方法,你所需要做的就是提供完全限定的类名作为 ref。需要注意的是,当使用此方法或`batch.xml`方法时,引用的类需要一个无参数构造函数,该构造函数将用于创建 Bean。 + +``` + + + + + + +``` + +### [](#jsrJobProperties)批处理属性 + +#### [](#jsrPropertySupport)属性支持 + +JSR-352 允许通过在 JSL 中的配置在作业、步骤和批处理工件级别定义属性。在每个级别上,按以下方式配置批处理属性: + +``` + + + + +``` + +`Properties`可以在任何批处理工件上进行配置。 + +#### [](#jsrBatchPropertyAnnotation)@batchproperty 注释 + +`Properties`在批处理工件中通过使用`@BatchProperty`和`@Inject`注释(这两个注释都是规范所要求的)注释类字段来引用。根据 JSR-352 的定义,属性的字段必须是字符串类型的。任何类型转换都要由实现开发人员来执行。 + +可以将`javax.batch.api.chunk.ItemReader`工件配置为具有上述属性块的属性块,并以这样的方式进行访问: + +``` +public class MyItemReader extends AbstractItemReader { + @Inject + @BatchProperty + private String propertyName1; + + ... +} +``` + +字段“PropertyName1”的值将是“PropertyValue1” + +#### [](#jsrPropertySubstitution)属性替换 + +属性替换是通过运算符和简单条件表达式来提供的。一般用法是`#{operator['key']}`。 + +支持的操作符: + +* `jobParameters`:访问启动/重新启动作业的作业参数值。 + +* `jobProperties`:在 JSL 的作业级别上配置的访问属性。 + +* `systemProperties`:访问命名的系统属性。 + +* `partitionPlan`:从一个分区步骤的分区计划中访问命名属性。 + +``` +#{jobParameters['unresolving.prop']}?:#{systemProperties['file.separator']} +``` + +赋值的左边是期望值,右边是默认值。在前面的示例中,结果将解析为系统属性文件的值。分隔符 #{jobparamets[’unsolving.prop’]}被假定为不可解析。如果两个表达式都不能解析,将返回一个空字符串。可以使用多个条件,这些条件由“;”分隔。 + +### [](#jsrProcessingModels)处理模型 + +JSR-352 提供了与 Spring 批处理相同的两个基本处理模型: + +* 基于项的处理-使用`javax.batch.api.chunk.ItemReader`、可选`javax.batch.api.chunk.ItemProcessor`和`javax.batch.api.chunk.ItemWriter`。 + +* 基于任务的处理-使用`javax.batch.api.Batchlet`实现。这种处理模型与当前可用的基于`org.springframework.batch.core.step.tasklet.Tasklet`的处理相同。 + +#### [](#item-based-processing)基于项目的处理 + +在此上下文中,基于项的处理是由`ItemReader`读取的项数设置的块大小。要以这种方式配置步骤,请指定`item-count`(默认值为 10),并可选择将`checkpoint-policy`配置为项(这是默认值)。 + +``` +... + + + + + + + +... +``` + +如果选择了基于项的检查点,则支持一个附加属性`time-limit`。这为必须处理指定的项数设置了一个时间限制。如果达到了超时,那么不管`item-count`配置为什么,该块都将完成,到那时已经读取了多少项。 + +#### [](#custom-checkpointing)自定义检查点 + +JSR-352 在步骤“检查点”中调用围绕提交间隔的进程。基于项目的检查点是上面提到的一种方法。然而,在许多情况下,这还不够强大。因此,规范允许通过实现`javax.batch.api.chunk.CheckpointAlgorithm`接口来实现自定义检查点算法。该功能在功能上与 Spring Batch 的自定义完成策略相同。要使用`CheckpointAlgorithm`的实现,请使用自定义`checkpoint-policy`配置你的步骤,如下所示,其中`fooCheckpointer`是指`CheckpointAlgorithm`的实现。 + +``` +... + + + + + + + + +... +``` + +### [](#jsrRunningAJob)运行作业 + +执行基于 JSR-352 的作业的入口是通过`javax.batch.operations.JobOperator`。 Spring 批处理提供了它自己实现的这个接口(`org.springframework.batch.core.jsr.launch.JsrJobOperator`)。这个实现是通过`javax.batch.runtime.BatchRuntime`加载的。启动基于 JSR-352 的批处理作业的实现如下: + +``` +JobOperator jobOperator = BatchRuntime.getJobOperator(); +long jobExecutionId = jobOperator.start("fooJob", new Properties()); +``` + +上述代码执行以下操作: + +* 引导基本`ApplicationContext`:为了提供批处理功能,框架需要一些基础结构的引导。这在每个 JVM 中发生一次。引导的组件类似于`@EnableBatchProcessing`提供的组件。可以在`JsrJobOperator`的 Javadoc 中找到具体的详细信息。 + +* 为请求的作业加载`ApplicationContext`:在上面的示例中,框架在/meta-inf/batch-jobs 中查找一个名为 foojob.xml 的文件,并加载一个上下文,该上下文是前面提到的共享上下文的子上下文。 + +* 启动作业:在上下文中定义的作业将异步执行。将返回`JobExecution’s`ID。 + +| |所有基于 JSR-352 的批处理作业都是异步执行的。| +|---|---------------------------------------------------------| + +当使用`JobOperator#start`调用`SimpleJobOperator`时, Spring 批处理确定调用是初始运行还是对先前执行的运行的重试。使用基于 JSR-352 的`JobOperator#start(String jobXMLName, Properties jobParameters)`,框架将始终创建一个新的 JobInstance(JSR-352 作业参数是不标识的)。为了重新启动作业,需要调用`JobOperator#restart(long executionId, Properties restartParameters)`。 + +### [](#jsrContexts)上下文 + +JSR-352 定义了两个上下文对象,用于与批处理工件中的作业或步骤的元数据交互:`javax.batch.runtime.context.JobContext`和`javax.batch.runtime.context.StepContext`。这两个都可以在任何步骤级别的工件(`Batchlet`,`ItemReader`等)中使用,而`JobContext`也可以用于作业级别工件(例如`JobListener`)。 + +要获得对当前作用域中`JobContext`或`StepContext`的引用,只需使用`@Inject`注释: + +``` +@Inject +JobContext jobContext; +``` + +| |@autowire for JSR-352contexts

使用 Spring 的 @autowire 不支持这些上下文的注入。| +|---|----------------------------------------------------------------------------------------------------------------------| + +在 Spring 批处理中,`JobContext`和`StepContext`分别包装其对应的执行对象(`JobExecution`和`StepExecution`)。通过`StepContext#setPersistentUserData(Serializable data)`存储的数据存储在 Spring 批中`StepExecution#executionContext`。 + +### [](#jsrStepFlow)阶跃流 + +在基于 JSR-352 的作业中,步骤流程的工作方式与 Spring 批处理中的工作方式类似。然而,这里有几个细微的区别: + +* 决策是步骤——在常规的 Spring 批作业中,决策是一种状态,它不具有独立的`StepExecution`,也不具有伴随整个步骤而来的任何权利和责任。然而,在 JSR-352 中,一个决策就像其他任何步骤一样是一个步骤,并且将表现为任何其他步骤(事务性,它得到`StepExecution`等)。这意味着,在重启过程中,它们与其他任何步骤一样受到同等对待。 + +* `next`属性和步骤转换-在常规作业中,允许在相同的步骤中同时出现这些转换。JSR-352 允许在相同的步骤中使用它们,并在计算中优先使用 Next 属性。 + +* 转换元素排序--在标准 Spring 批处理作业中,转换元素从最特定的到最不特定的进行排序,并按照该顺序进行评估。JSR-352 作业按照转换元素在 XML 中指定的顺序对其进行评估。 + +### [](#jsrScaling)缩放 JSR-352 批处理作业 + +Spring 传统的批处理作业有四种缩放方式(最后两种能够跨多个 JVM 执行): + +* 拆分-并行运行多个步骤。 + +* 多个线程-通过多个线程执行一个步骤。 + +* 分区-将数据划分为并行处理(Manager/Worker)。 + +* 远程分块-远程执行处理器逻辑块. + +JSR-352 提供了两种缩放批处理作业的选项。这两个选项都只支持一个 JVM: + +* 拆分-与 Spring 批相同 + +* 分区-概念上与 Spring 批处理相同,但实现方式略有不同。 + +#### [](#jsrPartitioning)分区 + +从概念上讲,JSR-352 中的分区与 Spring 批处理中的分区相同。元数据被提供给每个工作人员,以标识要处理的输入,工作人员在完成后将结果报告给经理。然而,也有一些重要的不同之处: + +* 分区`Batchlet`-这将在多个线程上运行配置的`Batchlet`的多个实例。每个实例都有自己的一组属性,如 JSL 或`PartitionPlan`提供的 + +* `PartitionPlan`-通过 Spring 批处理的分区,为每个分区提供了`ExecutionContext`。在 JSR-352 中,单个`javax.batch.api.partition.PartitionPlan`被提供了一个`Properties`的数组,为每个分区提供元数据。 + +* `PartitionMapper`-JSR-352 提供了生成分区元数据的两种方法。一种是通过 JSL(分区属性)。第二个是通过`javax.batch.api.partition.PartitionMapper`接口实现的。在功能上,该接口类似于 Spring Batch 提供的`org.springframework.batch.core.partition.support.Partitioner`接口,因为它提供了一种以编程方式生成用于分区的元数据的方法。 + +* `StepExecutions`-在 Spring 批处理中,分区步骤以 Manager/Worker 的形式运行。在 JSR-352 中,发生了相同的配置。然而,工人的步骤并没有得到正式的`StepExecutions`。因此,对`JsrJobOperator#getStepExecutions(long jobExecutionId)`的调用将只返回 Manager 的`StepExecution`。 + +| |子`StepExecutions`仍然存在于作业存储库中,并且通过`JobExplorer`可用
。| +|---|-------------------------------------------------------------------------------------------------------------| + +* 补偿逻辑-由于 Spring 批处理使用步骤实现了分区的 Manager/Worker 逻辑,所以如果出现问题,`StepExecutionListeners`可以用来处理补偿逻辑。然而,由于 Workers JSR-352 提供了一个其他组件的集合,因此能够在发生错误时提供补偿逻辑并动态设置退出状态。这些组成部分包括: + +| *Artifact Interface* |*说明*| +|----------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| +|`javax.batch.api.partition.PartitionCollector`|提供了一种方法,用于将信息发送回
管理器的工作步骤。每个工作线程有一个实例。| +|`javax.batch.api.partition.PartitionAnalyzer` |端点接收由`PartitionCollector`收集的信息,以及从一个完整的分区获得的结果
状态。| +| `javax.batch.api.partition.PartitionReducer` |提供为分区
步骤提供补偿逻辑的能力。| + +### [](#jsrTesting)测试 + +由于所有基于 JSR-352 的作业都是异步执行的,因此很难确定作业何时完成。为了帮助进行测试, Spring Batch 提供了`org.springframework.batch.test.JsrTestUtils`。这个实用程序类提供了启动作业、重新启动作业并等待作业完成的功能。作业完成后,将返回相关的`JobExecution`。 \ No newline at end of file diff --git a/docs/spring-batch/monitoring-and-metrics.md b/docs/spring-batch/monitoring-and-metrics.md new file mode 100644 index 0000000..07063d4 --- /dev/null +++ b/docs/spring-batch/monitoring-and-metrics.md @@ -0,0 +1,66 @@ +# 监测和量度 + +## [](#monitoring-and-metrics)监控和度量 + +自版本 4.2 以来, Spring Batch 提供了对基于[Micrometer](https://micrometer.io/)的批监视和度量的支持。本节描述了哪些度量是开箱即用的,以及如何贡献自定义度量。 + +### [](#built-in-metrics)内置度量 + +度量集合不需要任何特定的配置。框架提供的所有指标都注册在[千分尺的全球注册中心](https://micrometer.io/docs/concepts#_global_registry)的`spring.batch`前缀下。下表详细解释了所有指标: + +| *Metric Name* | *Type* | *Description* |*标签*| +|---------------------------|-----------------|---------------------------|---------------------------------| +| `spring.batch.job` | `TIMER` | Duration of job execution |`name`, `status`| +| `spring.batch.job.active` |`LONG_TASK_TIMER`| Currently active jobs |`name`| +| `spring.batch.step` | `TIMER` |Duration of step execution |`name`, `job.name`, `status`| +| `spring.batch.item.read` | `TIMER` | Duration of item reading |`job.name`, `step.name`, `status`| +|`spring.batch.item.process`| `TIMER` |Duration of item processing|`job.name`, `step.name`, `status`| +|`spring.batch.chunk.write` | `TIMER` | Duration of chunk writing |`job.name`, `step.name`, `status`| + +| |`status`标记可以是`SUCCESS`或`FAILURE`。| +|---|------------------------------------------------------| + +### [](#custom-metrics)自定义度量 + +如果你想在自定义组件中使用自己的度量,我们建议直接使用 Micrometer API。以下是如何对`Tasklet`进行计时的示例: + +``` +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; + +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +public class MyTimedTasklet implements Tasklet { + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + Timer.Sample sample = Timer.start(Metrics.globalRegistry); + String status = "success"; + try { + // do some work + } catch (Exception e) { + // handle exception + status = "failure"; + } finally { + sample.stop(Timer.builder("my.tasklet.timer") + .description("Duration of MyTimedTasklet") + .tag("status", status) + .register(Metrics.globalRegistry)); + } + return RepeatStatus.FINISHED; + } +} +``` + +### [](#disabling-metrics)禁用度量 + +度量收集是一个类似于日志记录的问题。禁用日志通常是通过配置日志记录库来完成的,对于度量标准来说也是如此。在 Spring 批处理中没有禁用千分尺的度量的功能,这应该在千分尺的一侧完成。由于 Spring 批处理将度量存储在带有`spring.batch`前缀的 Micrometer 的全局注册中心中,因此可以通过以下代码片段将 Micrometer 配置为忽略/拒绝批处理度量: + +``` +Metrics.globalRegistry.config().meterFilter(MeterFilter.denyNameStartsWith("spring.batch")) +``` + +有关更多详情,请参阅千分尺的[参考文献](http://micrometer.io/docs/concepts#_meter_filters)。 \ No newline at end of file diff --git a/docs/spring-batch/processor.md b/docs/spring-batch/processor.md new file mode 100644 index 0000000..5ca8a89 --- /dev/null +++ b/docs/spring-batch/processor.md @@ -0,0 +1,299 @@ +# 项目处理 + +## [](#itemProcessor)项处理 + +XMLJavaBoth + +[ItemReader 和 ItemWriter 接口](readersAndWriters.html#readersAndWriters)对于它们的特定任务都非常有用,但是如果你想在编写之前插入业务逻辑呢?读和写的一个选项是使用复合模式:创建一个`ItemWriter`,其中包含另一个`ItemWriter`或一个`ItemReader`,其中包含另一个`ItemReader`。下面的代码展示了一个示例: + +``` +public class CompositeItemWriter implements ItemWriter { + + ItemWriter itemWriter; + + public CompositeItemWriter(ItemWriter itemWriter) { + this.itemWriter = itemWriter; + } + + public void write(List items) throws Exception { + //Add business logic here + itemWriter.write(items); + } + + public void setDelegate(ItemWriter itemWriter){ + this.itemWriter = itemWriter; + } +} +``` + +前面的类包含另一个`ItemWriter`,它在提供了一些业务逻辑之后将其委托给它。这种模式也可以很容易地用于`ItemReader`,也许可以基于由主`ItemReader`提供的输入来获得更多的引用数据。如果你需要自己控制对`write`的调用,它也很有用。但是,如果你只想在实际写入之前“转换”传入的用于写入的项,则不需要`write`你自己。你只需修改项目即可。对于此场景, Spring Batch 提供了`ItemProcessor`接口,如下面的接口定义所示: + +``` +public interface ItemProcessor { + + O process(I item) throws Exception; +} +``` + +`ItemProcessor`很简单。给定一个对象,将其转换并返回另一个对象。所提供的对象可以是相同类型的,也可以不是相同类型的。关键在于,业务逻辑可以应用于流程中,完全由开发人员来创建该逻辑。`ItemProcessor`可以直接连接到一个步骤。例如,假设`ItemReader`提供了类型`Foo`的类,并且在写出之前需要将其转换为类型`Bar`。下面的示例显示了执行转换的`ItemProcessor`: + +``` +public class Foo {} + +public class Bar { + public Bar(Foo foo) {} +} + +public class FooProcessor implements ItemProcessor { + public Bar process(Foo foo) throws Exception { + //Perform simple transformation, convert a Foo to a Bar + return new Bar(foo); + } +} + +public class BarWriter implements ItemWriter { + public void write(List bars) throws Exception { + //write bars + } +} +``` + +在前面的示例中,有一个类`Foo`,一个类`Bar`,以及一个类`FooProcessor`,它坚持`ItemProcessor`接口。转换很简单,但是任何类型的转换都可以在这里完成。`BarWriter`写`Bar`对象,如果提供了任何其他类型,则抛出异常。类似地,如果只提供了`Foo`,则`FooProcessor`抛出异常。然后可以将`FooProcessor`注入`Step`,如下例所示: + +XML 配置 + +``` + + + + + + + +``` + +Java 配置 + +``` +@Bean +public Job ioSampleJob() { + return this.jobBuilderFactory.get("ioSampleJob") + .start(step1()) + .build(); +} + +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(fooReader()) + .processor(fooProcessor()) + .writer(barWriter()) + .build(); +} +``` + +`ItemProcessor`与`ItemReader`或`ItemWriter`之间的区别在于,`ItemProcessor`对于`Step`是可选的。 + +### [](#chainingItemProcessors)链接项目处理器 + +在许多场景中,执行单个转换是有用的,但是如果你想将多个`ItemProcessor`实现“链”在一起,该怎么办?这可以使用前面提到的复合模式来完成。为了更新前面的单个转换,例如,将`Foo`转换为`Bar`,将其转换为`Foobar`并写出,如以下示例所示: + +``` +public class Foo {} + +public class Bar { + public Bar(Foo foo) {} +} + +public class Foobar { + public Foobar(Bar bar) {} +} + +public class FooProcessor implements ItemProcessor { + public Bar process(Foo foo) throws Exception { + //Perform simple transformation, convert a Foo to a Bar + return new Bar(foo); + } +} + +public class BarProcessor implements ItemProcessor { + public Foobar process(Bar bar) throws Exception { + return new Foobar(bar); + } +} + +public class FoobarWriter implements ItemWriter{ + public void write(List items) throws Exception { + //write items + } +} +``` + +a`FooProcessor`和 a`BarProcessor`可以’链接’在一起,以得到结果`Foobar`,如以下示例所示: + +``` +CompositeItemProcessor compositeProcessor = + new CompositeItemProcessor(); +List itemProcessors = new ArrayList(); +itemProcessors.add(new FooProcessor()); +itemProcessors.add(new BarProcessor()); +compositeProcessor.setDelegates(itemProcessors); +``` + +正如前面的示例一样,复合处理器可以配置为`Step`: + +XML 配置 + +``` + + + + + + + + + + + + + + + + +``` + +Java 配置 + +``` +@Bean +public Job ioSampleJob() { + return this.jobBuilderFactory.get("ioSampleJob") + .start(step1()) + .build(); +} + +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(fooReader()) + .processor(compositeProcessor()) + .writer(foobarWriter()) + .build(); +} + +@Bean +public CompositeItemProcessor compositeProcessor() { + List delegates = new ArrayList<>(2); + delegates.add(new FooProcessor()); + delegates.add(new BarProcessor()); + + CompositeItemProcessor processor = new CompositeItemProcessor(); + + processor.setDelegates(delegates); + + return processor; +} +``` + +### [](#filteringRecords)过滤记录 + +项目处理器的一个典型用途是在将记录传递给`ItemWriter`之前过滤掉它们。过滤是一种不同于跳过的动作。跳过表示记录无效,而筛选只表示不应写入记录。 + +例如,考虑一个批处理作业,它读取包含三种不同类型记录的文件:要插入的记录、要更新的记录和要删除的记录。如果系统不支持记录删除,那么我们将不希望将任何“delete”记录发送到`ItemWriter`。但是,由于这些记录实际上并不是不良记录,我们希望过滤掉它们,而不是跳过它们。因此,`ItemWriter`将只接收“插入”和“更新”记录。 + +要过滤记录,可以从`ItemProcessor`返回`null`。该框架检测到结果是`null`,并避免将该项添加到交付给`ItemWriter`的记录列表中。像往常一样,从`ItemProcessor`抛出的异常会导致跳过。 + +### [](#validatingInput)验证输入 + +在[项目阅读器和项目编写器](readersAndWriters.html#readersAndWriters)章中,讨论了多种解析输入的方法。如果不是“格式良好”的,每个主要实现都会抛出一个异常。如果缺少数据范围,`FixedLengthTokenizer`将抛出一个异常。类似地,试图访问`RowMapper`或`FieldSetMapper`中不存在或格式与预期不同的索引,会引发异常。所有这些类型的异常都是在`read`返回之前抛出的。但是,它们没有解决返回的项目是否有效的问题。例如,如果其中一个字段是年龄,那么它显然不可能是负的。它可以正确地解析,因为它存在并且是一个数字,但是它不会导致异常。由于已经有过多的验证框架, Spring Batch 不会尝试提供另一种验证框架。相反,它提供了一个名为`Validator`的简单接口,可以由任意数量的框架实现,如以下接口定义所示: + +``` +public interface Validator { + + void validate(T value) throws ValidationException; + +} +``` + +契约是,如果对象无效,`validate`方法抛出一个异常,如果对象有效,则正常返回。 Spring 批处理提供了开箱即用的`ValidatingItemProcessor`,如以下 Bean 定义所示: + +XML 配置 + +``` + + + + + + + + + +``` + +Java 配置 + +``` +@Bean +public ValidatingItemProcessor itemProcessor() { + ValidatingItemProcessor processor = new ValidatingItemProcessor(); + + processor.setValidator(validator()); + + return processor; +} + +@Bean +public SpringValidator validator() { + SpringValidator validator = new SpringValidator(); + + validator.setValidator(new TradeValidator()); + + return validator; +} +``` + +你还可以使用`BeanValidatingItemProcessor`来验证用 Bean 验证 API(JSR-303)注释的项。例如,给定以下类型`Person`: + +``` +class Person { + + @NotEmpty + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +``` + +可以通过在应用程序上下文中声明`BeanValidatingItemProcessor` Bean 来验证项,并在面向块的步骤中将其注册为处理器: + +``` +@Bean +public BeanValidatingItemProcessor beanValidatingItemProcessor() throws Exception { + BeanValidatingItemProcessor beanValidatingItemProcessor = new BeanValidatingItemProcessor<>(); + beanValidatingItemProcessor.setFilter(true); + + return beanValidatingItemProcessor; +} +``` + +### [](#faultTolerant)容错 + +当块被回滚时,在读取过程中缓存的项可能会被重新处理。如果一个步骤被配置为容错(通常通过使用跳过或重试处理),则所使用的任何`ItemProcessor`都应该以幂等的方式实现。通常,这将包括对`ItemProcessor`的输入项不执行任何更改,并且只更新结果中的实例。 \ No newline at end of file diff --git a/docs/spring-batch/readersAndWriters.md b/docs/spring-batch/readersAndWriters.md new file mode 100644 index 0000000..4d52154 --- /dev/null +++ b/docs/spring-batch/readersAndWriters.md @@ -0,0 +1,2264 @@ +# 项目阅读器和项目编写器 + +## [](#readersAndWriters)条目阅读器和条目编写器 + +XMLJavaBoth + +所有批处理都可以用最简单的形式描述为读取大量数据,执行某种类型的计算或转换,并将结果写出来。 Spring Batch 提供了三个关键接口来帮助执行大容量读写:`ItemReader`、`ItemProcessor`和`ItemWriter`。 + +### [](#itemReader)`ItemReader` + +虽然是一个简单的概念,但`ItemReader`是从许多不同类型的输入提供数据的手段。最常见的例子包括: + +* 平面文件:平面文件项读取器从平面文件中读取数据行,该文件通常用文件中固定位置定义的数据字段或用某些特殊字符(例如逗号)分隔的数据字段来描述记录。 + +* XML:XML`ItemReaders`独立于用于解析、映射和验证对象的技术来处理 XML。输入数据允许根据 XSD 模式验证 XML 文件。 + +* 数据库:访问数据库资源以返回结果集,这些结果集可以映射到对象以进行处理。默认的 SQL`ItemReader`实现调用`RowMapper`以返回对象,如果需要重新启动,则跟踪当前行,存储基本统计信息,并提供一些事务增强,稍后将对此进行说明。 + +还有更多的可能性,但我们将重点放在本章的基本可能性上。在[Appendix A](appendix.html#listOfReadersAndWriters)中可以找到所有可用`ItemReader`实现的完整列表。 + +`ItemReader`是用于通用输入操作的基本接口,如以下接口定义所示: + +``` +public interface ItemReader { + + T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException; + +} +``` + +`read`方法定义了`ItemReader`中最基本的契约。调用它将返回一个项,如果没有更多项,则返回`null`。项目可以表示文件中的行、数据库中的行或 XML 文件中的元素。通常预期这些被映射到一个可用的域对象(例如`Trade`,`Foo`,或其他),但是在契约中没有这样做的要求。 + +预计`ItemReader`接口的实现方式仅是前向的。但是,如果底层资源是事务性的(例如 JMS 队列),那么在回滚场景中,调用`read`可能会在随后的调用中返回相同的逻辑项。还值得注意的是,缺少由`ItemReader`处理的项并不会导致抛出异常。例如,配置了返回 0 结果的查询的数据库`ItemReader`在`read`的第一次调用时返回`null`。 + +### [](#itemWriter)`ItemWriter` + +`ItemWriter`在功能上类似于`ItemReader`,但具有反向操作。资源仍然需要定位、打开和关闭,但它们的不同之处在于`ItemWriter`写出,而不是读入。在数据库或队列的情况下,这些操作可以是插入、更新或发送。输出的序列化的格式是特定于每个批处理作业的。 + +与`ItemReader`一样,`ItemWriter`是一个相当通用的接口,如下面的接口定义所示: + +``` +public interface ItemWriter { + + void write(List items) throws Exception; + +} +``` + +与`read`上的`ItemReader`一样,`write`提供了`ItemWriter`的基本契约。它尝试写出传入的项目列表,只要它是打开的。由于通常期望将项目“批处理”到一个块中,然后输出,因此接口接受一个项目列表,而不是一个项目本身。在写出列表之后,可以在从写方法返回之前执行任何必要的刷新。例如,如果对 Hibernate DAO 进行写操作,则可以对每个项进行多个 write 调用。然后,写入器可以在返回之前调用 Hibernate 会话上的`flush`。 + +### [](#itemStream)`ItemStream` + +`ItemReaders`和`ItemWriters`都很好地服务于它们各自的目的,但是它们之间有一个共同的关注点,那就是需要另一个接口。通常,作为批处理作业范围的一部分,读取器和编写器需要被打开、关闭,并且需要一种机制来保持状态。`ItemStream`接口实现了这一目的,如下例所示: + +``` +public interface ItemStream { + + void open(ExecutionContext executionContext) throws ItemStreamException; + + void update(ExecutionContext executionContext) throws ItemStreamException; + + void close() throws ItemStreamException; +} +``` + +在描述每个方法之前,我们应该提到`ExecutionContext`。如果`ItemReader`的客户端也实现`ItemStream`,则在调用`read`之前,应该调用`open`,以便打开任何资源,例如文件或获得连接。类似的限制适用于实现`ItemStream`的`ItemWriter`。正如在第 2 章中提到的,如果在`ExecutionContext`中找到了预期的数据,则可以使用它在其初始状态以外的位置启动`ItemReader`或`ItemWriter`。相反,调用`close`是为了确保在打开期间分配的任何资源都被安全地释放。调用`update`主要是为了确保当前持有的任何状态都被加载到所提供的`ExecutionContext`中。在提交之前调用此方法,以确保在提交之前将当前状态持久化到数据库中。 + +在`ItemStream`的客户端是`Step`(来自 Spring 批处理核心)的特殊情况下,将为每个分步执行创建一个`ExecutionContext`,以允许用户存储特定执行的状态,期望在再次启动相同的`JobInstance`时返回。对于那些熟悉 Quartz 的人,其语义非常类似于 Quartz`JobDataMap`。 + +### [](#delegatePatternAndRegistering)委托模式并与步骤一起注册 + +请注意,`CompositeItemWriter`是委托模式的一个示例,这在 Spring 批处理中很常见。委托本身可能实现回调接口,例如`StepListener`。如果它们确实存在,并且如果它们是作为`Job`中的`Step`的一部分与 Spring 批处理核心一起使用的,那么几乎肯定需要用`Step`手动注册它们。直接连接到`Step`的读取器、编写器或处理器如果实现`ItemStream`或`StepListener`接口,就会自动注册。但是,由于委托不为`Step`所知,因此需要将它们作为侦听器或流注入(或者在适当的情况下将两者都注入)。 + +下面的示例展示了如何将委托作为流注入到 XML 中: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + +``` + +下面的示例展示了如何将委托作为流注入到 XML 中: + +Java 配置 + +``` +@Bean +public Job ioSampleJob() { + return this.jobBuilderFactory.get("ioSampleJob") + .start(step1()) + .build(); +} + +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(fooReader()) + .processor(fooProcessor()) + .writer(compositeItemWriter()) + .stream(barWriter()) + .build(); +} + +@Bean +public CustomCompositeItemWriter compositeItemWriter() { + + CustomCompositeItemWriter writer = new CustomCompositeItemWriter(); + + writer.setDelegate(barWriter()); + + return writer; +} + +@Bean +public BarWriter barWriter() { + return new BarWriter(); +} +``` + +### [](#flatFiles)平面文件 + +交换大容量数据的最常见机制之一一直是平面文件。与 XML 不同的是,XML 有一个一致的标准来定义它是如何结构化的(XSD),任何读取平面文件的人都必须提前确切地了解文件是如何结构化的。一般来说,所有的平面文件都分为两种类型:定长和定长。分隔符文件是那些字段被分隔符(如逗号)分隔的文件。固定长度文件的字段是固定长度的。 + +#### [](#fieldSet)the`FieldSet` + +在处理 Spring 批处理中的平面文件时,无论它是用于输入还是输出,最重要的类之一是`FieldSet`。许多体系结构和库包含帮助你从文件中读取的抽象,但它们通常返回`String`或`String`对象的数组。这真的只会让你走到一半。`FieldSet`是 Spring 批处理的抽象,用于从文件资源中绑定字段。它允许开发人员以与处理数据库输入大致相同的方式处理文件输入。a`FieldSet`在概念上类似于 jdbc`ResultSet`。`FieldSet`只需要一个参数:一个`String`令牌数组。还可以选择地配置字段的名称,以便可以按照`ResultSet`之后的模式通过索引或名称访问字段,如以下示例所示: + +``` +String[] tokens = new String[]{"foo", "1", "true"}; +FieldSet fs = new DefaultFieldSet(tokens); +String name = fs.readString(0); +int value = fs.readInt(1); +boolean booleanValue = fs.readBoolean(2); +``` + +在`FieldSet`接口上还有许多选项,例如`Date`、long、`BigDecimal`,等等。`FieldSet`的最大优点是它提供了对平面文件输入的一致解析。在处理由格式异常引起的错误或进行简单的数据转换时,它可以是一致的,而不是以潜在的意外方式对每个批处理作业进行不同的解析。 + +#### [](#flatFileItemReader)`FlatFileItemReader` + +平面文件是最多包含二维(表格)数据的任何类型的文件。 Spring 批处理框架中的平面文件的读取是由一个名为`FlatFileItemReader`的类提供的,该类为平面文件的读取和解析提供了基本功能。`FlatFileItemReader`的两个最重要的必需依赖项是`Resource`和`LineMapper`。`LineMapper`接口将在下一节中进行更多的探讨。资源属性表示 Spring 核心`Resource`。说明如何创建这种类型的 bean 的文档可以在[Spring Framework, Chapter 5. Resources](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources)中找到。因此,除了展示下面的简单示例之外,本指南不涉及创建`Resource`对象的细节: + +``` +Resource resource = new FileSystemResource("resources/trades.csv"); +``` + +在复杂的批处理环境中,目录结构通常由 Enterprise 应用程序集成基础设施管理,在该基础设施中,外部接口的下拉区被建立,用于将文件从 FTP 位置移动到批处理位置,反之亦然。文件移动实用程序超出了 Spring 批处理体系结构的范围,但是批处理作业流将文件移动实用程序作为步骤包含在作业流中并不少见。批处理架构只需要知道如何定位要处理的文件。 Spring 批处理开始从该起点将数据送入管道的过程。然而,[Spring Integration](https://projects.spring.io/spring-integration/)提供了许多这类服务。 + +`FlatFileItemReader`中的其他属性允许你进一步指定如何解释数据,如下表所示: + +| Property | Type |说明| +|---------------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| comments | String[] |指定表示注释行的行前缀。| +| encoding | String |指定要使用的文本编码。默认值是`Charset.defaultCharset()`。| +| lineMapper | `LineMapper` |将表示项的`String`转换为`Object`。| +| linesToSkip | int |文件顶部要忽略的行数。| +|recordSeparatorPolicy|RecordSeparatorPolicy|用于确定行尾的位置
,并执行类似于在引号字符串中的行尾上继续的操作。| +| resource | `Resource` |可供阅读的资源。| +|skippedLinesCallback | LineCallbackHandler |传递
中要跳过的文件行的原始行内容的接口。如果`linesToSkip`被设置为 2,那么这个接口被
调用了两次。| +| strict | boolean |在严格模式下,如果输入资源不存在
,读取器将在`ExecutionContext`上抛出异常。否则,它会记录问题并继续处理。| + +##### [](#lineMapper)`LineMapper` + +与`RowMapper`一样,它接受一个低层次的构造,例如`ResultSet`并返回一个`Object`,平面文件处理需要相同的构造来将`String`行转换为`Object`,如以下接口定义所示: + +``` +public interface LineMapper { + + T mapLine(String line, int lineNumber) throws Exception; + +} +``` + +基本的约定是,给定当前行和与其相关联的行号,映射器应该返回一个结果域对象。这类似于`RowMapper`,因为每一行都与其行号关联,就像`ResultSet`中的每一行都与其行号关联一样。这允许将行号绑定到结果域对象,以进行身份比较或进行更有信息量的日志记录。然而,与`RowMapper`不同的是,`LineMapper`给出的是一条未加工的线,正如上面讨论的那样,这条线只能让你达到一半。该行必须标记为`FieldSet`,然后可以映射到对象,如本文档后面所述。 + +##### [](#lineTokenizer)`LineTokenizer` + +将一行输入转换为`FieldSet`的抽象是必要的,因为可能有许多格式的平面文件数据需要转换为`FieldSet`。在 Spring 批处理中,这个接口是`LineTokenizer`: + +``` +public interface LineTokenizer { + + FieldSet tokenize(String line); + +} +``` + +a`LineTokenizer`的契约是这样的,给定一条输入线(理论上`String`可以包含多条线),返回一个代表该线的`FieldSet`。然后可以将这个`FieldSet`传递给`FieldSetMapper`。 Spring 批处理包含以下`LineTokenizer`实现: + +* `DelimitedLineTokenizer`:用于记录中的字段用分隔符分隔的文件。最常见的分隔符是逗号,但也经常使用管道或分号。 + +* `FixedLengthTokenizer`:用于记录中的字段都是“固定宽度”的文件。必须为每个记录类型定义每个字段的宽度。 + +* `PatternMatchingCompositeLineTokenizer`:通过检查模式,确定在特定行上应该使用记号符列表中的哪一个`LineTokenizer`。 + +##### [](#fieldSetMapper)`FieldSetMapper` + +`FieldSetMapper`接口定义了一个方法`mapFieldSet`,它接受一个`FieldSet`对象并将其内容映射到一个对象。该对象可以是自定义 DTO、域对象或数组,具体取决于作业的需要。`FieldSetMapper`与`LineTokenizer`结合使用,以将资源中的一行数据转换为所需类型的对象,如以下接口定义所示: + +``` +public interface FieldSetMapper { + + T mapFieldSet(FieldSet fieldSet) throws BindException; + +} +``` + +使用的模式与`JdbcTemplate`使用的`RowMapper`相同。 + +##### [](#defaultLineMapper)`DefaultLineMapper` + +既然已经定义了在平面文件中读取的基本接口,那么显然需要三个基本步骤: + +1. 从文件中读出一行。 + +2. 将`String`行传递到`LineTokenizer#tokenize()`方法中,以检索`FieldSet`。 + +3. 将从标记化返回的`FieldSet`传递到`FieldSetMapper`,从`ItemReader#read()`方法返回结果。 + +上面描述的两个接口代表两个独立的任务:将一行转换为`FieldSet`,并将`FieldSet`映射到域对象。由于`LineTokenizer`的输入与`LineMapper`(一行)的输入匹配,并且`FieldSetMapper`的输出与`LineMapper`的输出匹配,因此提供了一个同时使用`LineTokenizer`和`FieldSetMapper`的默认实现。下面的类定义中显示的`DefaultLineMapper`表示大多数用户需要的行为: + +``` +public class DefaultLineMapper implements LineMapper<>, InitializingBean { + + private LineTokenizer tokenizer; + + private FieldSetMapper fieldSetMapper; + + public T mapLine(String line, int lineNumber) throws Exception { + return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line)); + } + + public void setLineTokenizer(LineTokenizer tokenizer) { + this.tokenizer = tokenizer; + } + + public void setFieldSetMapper(FieldSetMapper fieldSetMapper) { + this.fieldSetMapper = fieldSetMapper; + } +} +``` + +上述功能是在默认实现中提供的,而不是内置在阅读器本身中(就像框架的以前版本中所做的那样),以允许用户在控制解析过程中具有更大的灵活性,尤其是在需要访问原始行的情况下。 + +##### [](#simpleDelimitedFileReadingExample)简单分隔的文件读取示例 + +下面的示例演示了如何在实际的域场景中读取平面文件。这个特定的批处理作业从以下文件中读取足球运动员: + +``` +ID,lastName,firstName,position,birthYear,debutYear +"AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996", +"AbduRa00,Abdullah,Rabih,rb,1975,1999", +"AberWa00,Abercrombie,Walter,rb,1959,1982", +"AbraDa00,Abramowicz,Danny,wr,1945,1967", +"AdamBo00,Adams,Bob,te,1946,1969", +"AdamCh00,Adams,Charlie,wr,1979,2003" +``` + +此文件的内容映射到以下`Player`域对象: + +``` +public class Player implements Serializable { + + private String ID; + private String lastName; + private String firstName; + private String position; + private int birthYear; + private int debutYear; + + public String toString() { + return "PLAYER:ID=" + ID + ",Last Name=" + lastName + + ",First Name=" + firstName + ",Position=" + position + + ",Birth Year=" + birthYear + ",DebutYear=" + + debutYear; + } + + // setters and getters... +} +``` + +要将`FieldSet`映射到`Player`对象中,需要定义一个返回播放机的`FieldSetMapper`,如下例所示: + +``` +protected static class PlayerFieldSetMapper implements FieldSetMapper { + public Player mapFieldSet(FieldSet fieldSet) { + Player player = new Player(); + + player.setID(fieldSet.readString(0)); + player.setLastName(fieldSet.readString(1)); + player.setFirstName(fieldSet.readString(2)); + player.setPosition(fieldSet.readString(3)); + player.setBirthYear(fieldSet.readInt(4)); + player.setDebutYear(fieldSet.readInt(5)); + + return player; + } +} +``` + +然后,可以通过正确地构造`FlatFileItemReader`并调用`read`来读取文件,如以下示例所示: + +``` +FlatFileItemReader itemReader = new FlatFileItemReader<>(); +itemReader.setResource(new FileSystemResource("resources/players.csv")); +DefaultLineMapper lineMapper = new DefaultLineMapper<>(); +//DelimitedLineTokenizer defaults to comma as its delimiter +lineMapper.setLineTokenizer(new DelimitedLineTokenizer()); +lineMapper.setFieldSetMapper(new PlayerFieldSetMapper()); +itemReader.setLineMapper(lineMapper); +itemReader.open(new ExecutionContext()); +Player player = itemReader.read(); +``` + +对`read`的每次调用都会从文件中的每一行返回一个新的`Player`对象。当到达文件的末尾时,将返回`null`。 + +##### [](#mappingFieldsByName)按名称映射字段 + +还有一个额外的功能块是`DelimitedLineTokenizer`和`FixedLengthTokenizer`都允许的,它在功能上类似于 JDBC`ResultSet`。字段的名称可以被注入到这些`LineTokenizer`实现中,以增加映射函数的可读性。首先,将平面文件中所有字段的列名注入到记号生成器中,如下例所示: + +``` +tokenizer.setNames(new String[] {"ID", "lastName", "firstName", "position", "birthYear", "debutYear"}); +``` + +a`FieldSetMapper`可以如下方式使用此信息: + +``` +public class PlayerMapper implements FieldSetMapper { + public Player mapFieldSet(FieldSet fs) { + + if (fs == null) { + return null; + } + + Player player = new Player(); + player.setID(fs.readString("ID")); + player.setLastName(fs.readString("lastName")); + player.setFirstName(fs.readString("firstName")); + player.setPosition(fs.readString("position")); + player.setDebutYear(fs.readInt("debutYear")); + player.setBirthYear(fs.readInt("birthYear")); + + return player; + } +} +``` + +##### [](#beanWrapperFieldSetMapper)向域对象自动设置字段集 + +对于许多人来说,必须为`FieldSetMapper`编写特定的`RowMapper`,就像为`JdbcTemplate`编写特定的`RowMapper`一样麻烦。 Spring 批处理通过提供`FieldSetMapper`使这一点变得更容易,该批处理通过使用 JavaBean 规范将字段名称与对象上的 setter 匹配来自动映射字段。 + +再次使用 Football 示例,`BeanWrapperFieldSetMapper`配置在 XML 中看起来像以下代码片段: + +XML 配置 + +``` + + + + + +``` + +再次使用 Football 示例,`BeanWrapperFieldSetMapper`配置在 Java 中看起来像以下代码片段: + +Java 配置 + +``` +@Bean +public FieldSetMapper fieldSetMapper() { + BeanWrapperFieldSetMapper fieldSetMapper = new BeanWrapperFieldSetMapper(); + + fieldSetMapper.setPrototypeBeanName("player"); + + return fieldSetMapper; +} + +@Bean +@Scope("prototype") +public Player player() { + return new Player(); +} +``` + +对于`FieldSet`中的每个条目,映射器在`Player`对象的新实例上查找相应的 setter(由于这个原因,需要原型作用域),就像 Spring 容器查找匹配属性名的 setter 一样。映射`FieldSet`中的每个可用字段,并返回结果`Player`对象,不需要任何代码。 + +##### [](#fixedLengthFileFormats)固定长度文件格式 + +到目前为止,只对分隔的文件进行了详细的讨论。然而,它们只代表了文件阅读图片的一半。许多使用平面文件的组织使用固定长度格式。下面是固定长度文件的示例: + +``` +UK21341EAH4121131.11customer1 +UK21341EAH4221232.11customer2 +UK21341EAH4321333.11customer3 +UK21341EAH4421434.11customer4 +UK21341EAH4521535.11customer5 +``` + +虽然这看起来像是一个很大的域,但它实际上代表了 4 个不同的域: + +1. ISIN:所订购商品的唯一标识符-12 个字符长。 + +2. 数量:订购的商品数量-3 个字符长。 + +3. 价格:该商品的价格-5 个字符长. + +4. 顾客:订购该商品的顾客的 ID-9 个字符长。 + +在配置`FixedLengthLineTokenizer`时,这些长度中的每一个都必须以范围的形式提供。 + +下面的示例展示了如何在 XML 中为`FixedLengthLineTokenizer`定义范围: + +XML 配置 + +``` + + + + +``` + +因为`FixedLengthLineTokenizer`使用与前面讨论的相同的`LineTokenizer`接口,所以它返回相同的`FieldSet`,就像使用了分隔符一样。这允许在处理其输出时使用相同的方法,例如使用`BeanWrapperFieldSetMapper`。 + +| |支持前面的范围语法需要在`ApplicationContext`中配置专门的属性编辑器`RangeArrayPropertyEditor`。然而,这 Bean
是在使用批处理名称空间的`ApplicationContext`中自动声明的。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何在 Java 中为`FixedLengthLineTokenizer`定义范围: + +Java 配置 + +``` +@Bean +public FixedLengthTokenizer fixedLengthTokenizer() { + FixedLengthTokenizer tokenizer = new FixedLengthTokenizer(); + + tokenizer.setNames("ISIN", "Quantity", "Price", "Customer"); + tokenizer.setColumns(new Range(1, 12), + new Range(13, 15), + new Range(16, 20), + new Range(21, 29)); + + return tokenizer; +} +``` + +因为`FixedLengthLineTokenizer`使用与上面讨论的相同的`LineTokenizer`接口,所以它返回相同的`FieldSet`,就像使用了分隔符一样。这使得在处理其输出时可以使用相同的方法,例如使用`BeanWrapperFieldSetMapper`。 + +##### [](#prefixMatchingLineMapper)单个文件中的多个记录类型 + +到目前为止,所有的文件读取示例都为了简单起见做出了一个关键假设:文件中的所有记录都具有相同的格式。然而,情况可能并不总是如此。很常见的一种情况是,一个文件可能具有不同格式的记录,这些记录需要以不同的方式进行标记并映射到不同的对象。下面的文件摘录说明了这一点: + +``` +USER;Smith;Peter;;T;20014539;F +LINEA;1044391041ABC037.49G201XX1383.12H +LINEB;2134776319DEF422.99M005LI +``` + +在这个文件中,我们有三种类型的记录,“user”、“linea”和“lineb”。“user”行对应于`User`对象。“linea”和“lineb”都对应于`Line`对象,尽管“linea”比“lineb”有更多的信息。 + +`ItemReader`单独读取每一行,但是我们必须指定不同的`LineTokenizer`和`FieldSetMapper`对象,以便`ItemWriter`接收正确的项。`PatternMatchingCompositeLineMapper`允许配置模式到`LineTokenizers`的映射和模式到`FieldSetMappers`的映射,从而简化了这一过程。 + +下面的示例展示了如何在 XML 中为`FixedLengthLineTokenizer`定义范围: + +XML 配置 + +``` + + + + + + + + + + + + + + + +``` + +Java 配置 + +``` +@Bean +public PatternMatchingCompositeLineMapper orderFileLineMapper() { + PatternMatchingCompositeLineMapper lineMapper = + new PatternMatchingCompositeLineMapper(); + + Map tokenizers = new HashMap<>(3); + tokenizers.put("USER*", userTokenizer()); + tokenizers.put("LINEA*", lineATokenizer()); + tokenizers.put("LINEB*", lineBTokenizer()); + + lineMapper.setTokenizers(tokenizers); + + Map mappers = new HashMap<>(2); + mappers.put("USER*", userFieldSetMapper()); + mappers.put("LINE*", lineFieldSetMapper()); + + lineMapper.setFieldSetMappers(mappers); + + return lineMapper; +} +``` + +在这个示例中,“linea”和“lineb”有单独的`LineTokenizer`实例,但它们都使用相同的`FieldSetMapper`。 + +`PatternMatchingCompositeLineMapper`使用`PatternMatcher#match`方法为每一行选择正确的委托。`PatternMatcher`允许两个具有特殊含义的通配符:问号(“?”)恰好匹配一个字符,而星号(“\*”)匹配零个或更多字符。请注意,在前面的配置中,所有模式都以星号结尾,使它们有效地成为行的前缀。无论配置中的顺序如何,`PatternMatcher`始终匹配最特定的模式。因此,如果“line\*”和“linea\*”都被列为模式,那么“linea”将匹配模式“linea\*”,而“lineb”将匹配模式“line\*”。此外,单个星号(“\*”)可以通过匹配任何其他模式不匹配的任何行来作为默认设置。 + +下面的示例展示了如何匹配 XML 中任何其他模式都不匹配的行: + +XML 配置 + +``` + +``` + +下面的示例展示了如何匹配 Java 中任何其他模式都不匹配的行: + +Java 配置 + +``` +... +tokenizers.put("*", defaultLineTokenizer()); +... +``` + +还有一个`PatternMatchingCompositeLineTokenizer`可以单独用于标记化。 + +平面文件中包含的记录跨越多行也是很常见的。要处理这种情况,需要一种更复杂的策略。在`multiLineRecords`示例中可以找到这种常见模式的演示。 + +##### [](#exceptionHandlingInFlatFiles)平面文件中的异常处理 + +在许多情况下,对一行进行标记化可能会导致抛出异常。许多平面文件是不完美的,包含格式不正确的记录。许多用户在记录问题、原始行号和行号时选择跳过这些错误行。这些日志稍后可以手动检查,也可以通过另一个批处理作业进行检查。出于这个原因, Spring Batch 为处理解析异常提供了一个异常层次结构:`FlatFileParseException`和`FlatFileFormatException`。当试图读取文件时遇到任何错误时,`FlatFileParseException`将抛出`FlatFileItemReader`。`FlatFileFormatException`由`LineTokenizer`接口的实现抛出,并指示在标记时遇到的更具体的错误。 + +###### [](#incorrectTokenCountException)`IncorrectTokenCountException` + +`DelimitedLineTokenizer`和`FixedLengthLineTokenizer`都可以指定可用于创建`FieldSet`的列名。但是,如果列名的数量与对一行进行标记时发现的列数不匹配,则无法创建`FieldSet`,并抛出一个`IncorrectTokenCountException`,其中包含遇到的令牌数量和预期的数量,如以下示例所示: + +``` +tokenizer.setNames(new String[] {"A", "B", "C", "D"}); + +try { + tokenizer.tokenize("a,b,c"); +} +catch (IncorrectTokenCountException e) { + assertEquals(4, e.getExpectedCount()); + assertEquals(3, e.getActualCount()); +} +``` + +因为标记器配置了 4 个列名,但在文件中只找到了 3 个令牌,所以抛出了一个`IncorrectTokenCountException`。 + +###### [](#incorrectLineLengthException)`IncorrectLineLengthException` + +以固定长度格式格式化的文件在解析时有额外的要求,因为与分隔格式不同,每个列必须严格遵守其预定义的宽度。如果行的总长度不等于此列的最大值,则抛出一个异常,如以下示例所示: + +``` +tokenizer.setColumns(new Range[] { new Range(1, 5), + new Range(6, 10), + new Range(11, 15) }); +try { + tokenizer.tokenize("12345"); + fail("Expected IncorrectLineLengthException"); +} +catch (IncorrectLineLengthException ex) { + assertEquals(15, ex.getExpectedLength()); + assertEquals(5, ex.getActualLength()); +} +``` + +上面的记号生成器的配置范围是:1-5、6-10 和 11-15。因此,这条线的总长度是 15。但是,在前面的示例中,传入了长度为 5 的行,从而引发了`IncorrectLineLengthException`。在此抛出一个异常,而不是仅映射第一列,这样可以使行的处理更早失败,并且所包含的信息比在试图在`FieldSetMapper`中读取第 2 列时失败时所包含的信息更多。然而,在某些情况下,直线的长度并不总是恒定的。因此,可以通过“严格”属性关闭对行长的验证,如下例所示: + +``` +tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) }); +tokenizer.setStrict(false); +FieldSet tokens = tokenizer.tokenize("12345"); +assertEquals("12345", tokens.readString(0)); +assertEquals("", tokens.readString(1)); +``` + +前面的示例与前面的示例几乎相同,只是调用了`tokenizer.setStrict(false)`。这个设置告诉标记器在标记行时不要强制行长。现在正确地创建并返回了`FieldSet`。但是,对于其余的值,它只包含空标记。 + +#### [](#flatFileItemWriter)`FlatFileItemWriter` + +写入平面文件也存在从文件读入时必须克服的问题。一个步骤必须能够以事务性的方式编写分隔格式或固定长度格式。 + +##### [](#lineAggregator)`LineAggregator` + +正如`LineTokenizer`接口是获取一个项并将其转换为`String`所必需的一样,文件写入必须有一种方法,可以将多个字段聚合到一个字符串中,以便将其写入文件。在 Spring 批处理中,这是`LineAggregator`,如下面的接口定义所示: + +``` +public interface LineAggregator { + + public String aggregate(T item); + +} +``` + +`LineAggregator`是`LineTokenizer`的逻辑对立面。`LineTokenizer`接受一个`String`并返回一个`FieldSet`,而`LineAggregator`接受一个`item`并返回一个`String`。 + +###### [](#PassThroughLineAggregator)`PassThroughLineAggregator` + +`LineAggregator`接口的最基本的实现是`PassThroughLineAggregator`,它假定对象已经是一个字符串,或者它的字符串表示可以用于编写,如下面的代码所示: + +``` +public class PassThroughLineAggregator implements LineAggregator { + + public String aggregate(T item) { + return item.toString(); + } +} +``` + +如果需要直接控制创建字符串,那么前面的实现是有用的,但是`FlatFileItemWriter`的优点,例如事务和重新启动支持,是必要的。 + +##### [](#SimplifiedFileWritingExample)简化文件编写示例 + +既然`LineAggregator`接口及其最基本的实现`PassThroughLineAggregator`已经定义好了,那么编写的基本流程就可以解释了: + +1. 要写入的对象被传递给`LineAggregator`,以获得`String`。 + +2. 返回的`String`被写入配置的文件。 + +下面摘自`FlatFileItemWriter`的代码表达了这一点: + +``` +public void write(T item) throws Exception { + write(lineAggregator.aggregate(item) + LINE_SEPARATOR); +} +``` + +在 XML 中,配置的一个简单示例可能如下所示: + +XML 配置 + +``` + + + + + + +``` + +在 Java 中,配置的一个简单示例可能如下所示: + +Java 配置 + +``` +@Bean +public FlatFileItemWriter itemWriter() { + return new FlatFileItemWriterBuilder() + .name("itemWriter") + .resource(new FileSystemResource("target/test-outputs/output.txt")) + .lineAggregator(new PassThroughLineAggregator<>()) + .build(); +} +``` + +##### [](#FieldExtractor)`FieldExtractor` + +前面的示例对于对文件的写入的最基本使用可能是有用的。然而,`FlatFileItemWriter`的大多数用户都有一个需要写出的域对象,因此必须将其转换为一行。在文件阅读中,需要进行以下操作: + +1. 从文件中读出一行。 + +2. 将该行传递到`LineTokenizer#tokenize()`方法中,以便检索`FieldSet`。 + +3. 将从标记化返回的`FieldSet`传递到`FieldSetMapper`,从`ItemReader#read()`方法返回结果。 + +编写文件也有类似但相反的步骤: + +1. 把要写的东西交给作者。 + +2. 将项目上的字段转换为数组。 + +3. 将生成的数组聚合为一条线。 + +因为框架无法知道需要从对象中写出哪些字段,所以必须编写`FieldExtractor`才能完成将项转换为数组的任务,如下面的接口定义所示: + +``` +public interface FieldExtractor { + + Object[] extract(T item); + +} +``` + +`FieldExtractor`接口的实现应该从提供的对象的字段创建一个数组,然后可以在元素之间使用分隔符写出该数组,或者作为固定宽度线的一部分。 + +###### [](#PassThroughFieldExtractor)`PassThroughFieldExtractor` + +在许多情况下,需要写出集合,例如一个数组,`Collection`或`FieldSet`。从这些集合类型中的一种“提取”一个数组是非常简单的。要做到这一点,将集合转换为一个数组。因此,在此场景中应该使用`PassThroughFieldExtractor`。应该注意的是,如果传入的对象不是集合的类型,那么`PassThroughFieldExtractor`将返回一个仅包含要提取的项的数组。 + +###### [](#BeanWrapperFieldExtractor)`BeanWrapperFieldExtractor` + +与文件读取部分中描述的`BeanWrapperFieldSetMapper`一样,通常更好的方法是配置如何将域对象转换为对象数组,而不是自己编写转换。`BeanWrapperFieldExtractor`提供了这种功能,如以下示例所示: + +``` +BeanWrapperFieldExtractor extractor = new BeanWrapperFieldExtractor<>(); +extractor.setNames(new String[] { "first", "last", "born" }); + +String first = "Alan"; +String last = "Turing"; +int born = 1912; + +Name n = new Name(first, last, born); +Object[] values = extractor.extract(n); + +assertEquals(first, values[0]); +assertEquals(last, values[1]); +assertEquals(born, values[2]); +``` + +这个提取器实现只有一个必需的属性:要映射的字段的名称。正如`BeanWrapperFieldSetMapper`需要字段名称来将`FieldSet`上的字段映射到所提供对象上的 setter 一样,`BeanWrapperFieldExtractor`也需要名称来映射到 getter 以创建对象数组。值得注意的是,名称的顺序决定了数组中字段的顺序。 + +##### [](#delimitedFileWritingExample)分隔的文件编写示例 + +最基本的平面文件格式是一种所有字段都用分隔符分隔的格式。这可以使用`DelimitedLineAggregator`来完成。下面的示例写出了一个简单的域对象,该对象表示对客户帐户的信用: + +``` +public class CustomerCredit { + + private int id; + private String name; + private BigDecimal credit; + + //getters and setters removed for clarity +} +``` + +由于正在使用域对象,因此必须提供`FieldExtractor`接口的实现以及要使用的分隔符。 + +下面的示例展示了如何在 XML 中使用带有分隔符的`FieldExtractor`: + +XML 配置 + +``` + + + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中使用带有分隔符的`FieldExtractor`: + +Java 配置 + +``` +@Bean +public FlatFileItemWriter itemWriter(Resource outputResource) throws Exception { + BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); + fieldExtractor.setNames(new String[] {"name", "credit"}); + fieldExtractor.afterPropertiesSet(); + + DelimitedLineAggregator lineAggregator = new DelimitedLineAggregator<>(); + lineAggregator.setDelimiter(","); + lineAggregator.setFieldExtractor(fieldExtractor); + + return new FlatFileItemWriterBuilder() + .name("customerCreditWriter") + .resource(outputResource) + .lineAggregator(lineAggregator) + .build(); +} +``` + +在前面的示例中,本章前面描述的`BeanWrapperFieldExtractor`用于将`CustomerCredit`中的名称和信用字段转换为一个对象数组,然后在每个字段之间使用逗号写出该对象数组。 + +也可以使用`FlatFileItemWriterBuilder.DelimitedBuilder`自动创建`BeanWrapperFieldExtractor`和`DelimitedLineAggregator`,如以下示例所示: + +Java 配置 + +``` +@Bean +public FlatFileItemWriter itemWriter(Resource outputResource) throws Exception { + return new FlatFileItemWriterBuilder() + .name("customerCreditWriter") + .resource(outputResource) + .delimited() + .delimiter("|") + .names(new String[] {"name", "credit"}) + .build(); +} +``` + +##### [](#fixedWidthFileWritingExample)固定宽度文件编写示例 + +分隔符并不是唯一一种平面文件格式。许多人更喜欢为每个列使用一个设置的宽度来划分字段,这通常称为“固定宽度”。 Spring 批处理在用`FormatterLineAggregator`写文件时支持这一点。 + +使用上述相同的`CustomerCredit`域对象,可以在 XML 中进行如下配置: + +XML 配置 + +``` + + + + + + + + + + + + + +``` + +使用上面描述的相同的`CustomerCredit`域对象,可以在 Java 中进行如下配置: + +Java 配置 + +``` +@Bean +public FlatFileItemWriter itemWriter(Resource outputResource) throws Exception { + BeanWrapperFieldExtractor fieldExtractor = new BeanWrapperFieldExtractor<>(); + fieldExtractor.setNames(new String[] {"name", "credit"}); + fieldExtractor.afterPropertiesSet(); + + FormatterLineAggregator lineAggregator = new FormatterLineAggregator<>(); + lineAggregator.setFormat("%-9s%-2.0f"); + lineAggregator.setFieldExtractor(fieldExtractor); + + return new FlatFileItemWriterBuilder() + .name("customerCreditWriter") + .resource(outputResource) + .lineAggregator(lineAggregator) + .build(); +} +``` + +前面的大多数示例看起来应该很熟悉。但是,格式属性的值是新的。 + +下面的示例显示了 XML 中的格式属性: + +``` + +``` + +下面的示例显示了 Java 中的 format 属性: + +``` +... +FormatterLineAggregator lineAggregator = new FormatterLineAggregator<>(); +lineAggregator.setFormat("%-9s%-2.0f"); +... +``` + +底层实现是使用作为 Java5 的一部分添加的相同的`Formatter`构建的。Java`Formatter`基于 C 编程语言的`printf`功能。关于如何配置格式化程序的大多数详细信息可以在[Formatter](https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html)的 Javadoc 中找到。 + +也可以使用`FlatFileItemWriterBuilder.FormattedBuilder`自动创建`BeanWrapperFieldExtractor`和`FormatterLineAggregator`,如以下示例所示: + +Java 配置 + +``` +@Bean +public FlatFileItemWriter itemWriter(Resource outputResource) throws Exception { + return new FlatFileItemWriterBuilder() + .name("customerCreditWriter") + .resource(outputResource) + .formatted() + .format("%-9s%-2.0f") + .names(new String[] {"name", "credit"}) + .build(); +} +``` + +##### [](#handlingFileCreation)处理文件创建 + +`FlatFileItemReader`与文件资源的关系非常简单。当读取器被初始化时,它会打开该文件(如果它存在的话),如果它不存在,则会抛出一个异常。写文件并不是那么简单。乍一看,对于`FlatFileItemWriter`似乎应该存在类似的直接契约:如果文件已经存在,则抛出一个异常,如果不存在,则创建它并开始写入。然而,重新启动`Job`可能会导致问题。在正常的重启场景中,契约是相反的:如果文件存在,则从最后一个已知的良好位置开始向它写入,如果不存在,则抛出一个异常。但是,如果此作业的文件名总是相同,会发生什么情况?在这种情况下,如果文件存在,你可能想要删除它,除非是重新启动。由于这种可能性,`FlatFileItemWriter`包含属性`shouldDeleteIfExists`。将此属性设置为 true 将导致在打开 Writer 时删除同名的现有文件。 + +### [](#xmlReadingWriting)XML 项读取器和编写器 + +Spring Batch 提供了用于读取 XML 记录并将它们映射到 Java 对象以及将 Java 对象写为 XML 记录的事务基础设施。 + +| |流 XML 上的约束

STAX API 用于 I/O,因为其他标准的 XML 解析 API 不符合批处理
的要求(DOM 一次将整个输入加载到内存中,SAX 通过允许用户仅提供回调来控制
解析过程)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +我们需要考虑 XML 输入和输出如何在 Spring 批处理中工作。首先,有几个概念与文件读写不同,但在 Spring 批 XML 处理中很常见。使用 XML 处理,不是需要标记的记录行(`FieldSet`实例),而是假设 XML 资源是与单个记录相对应的“片段”的集合,如下图所示: + +![XML Input](./images/xmlinput.png) + +图 1。XML 输入 + +在上面的场景中,“trade”标记被定义为“root 元素”。“\”和“\”之间的所有内容都被视为一个“片段”。 Spring 批处理使用对象/XML 映射(OXM)将片段绑定到对象。然而, Spring 批处理并不绑定到任何特定的 XML 绑定技术。典型的用途是委托给[Spring OXM](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#oxm),这为最流行的 OXM 技术提供了统一的抽象。对 Spring OXM 的依赖是可选的,如果需要,可以选择实现 Spring 批处理特定接口。与 OXM 支持的技术之间的关系如下图所示: + +![OXM 绑定](./images/oxm-fragments.png) + +图 2。OXM 绑定 + +通过介绍 OXM 以及如何使用 XML 片段来表示记录,我们现在可以更仔细地研究阅读器和编写器。 + +#### [](#StaxEventItemReader)`StaxEventItemReader` + +`StaxEventItemReader`配置为处理来自 XML 输入流的记录提供了一个典型的设置。首先,考虑`StaxEventItemReader`可以处理的以下一组 XML 记录: + +``` + + + + XYZ0001 + 5 + 11.39 + Customer1 + + + XYZ0002 + 2 + 72.99 + Customer2c + + + XYZ0003 + 9 + 99.99 + Customer3 + + +``` + +为了能够处理 XML 记录,需要具备以下条件: + +* 根元素名称:构成要映射的对象的片段的根元素的名称。示例配置用“交易价值”演示了这一点。 + +* 资源:表示要读取的文件的 Spring 资源。 + +* `Unmarshaller`: Spring OXM 提供的一种解组功能,用于将 XML 片段映射到对象。 + +下面的示例展示了如何定义一个`StaxEventItemReader`,它与一个名为`trade`的根元素、一个资源`data/iosample/input/input.xml`和一个在 XML 中名为`tradeMarshaller`的解组器一起工作: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何定义一个`StaxEventItemReader`,它与一个名为`trade`的根元素、一个资源`data/iosample/input/input.xml`和一个在 Java 中名为`tradeMarshaller`的解组器一起工作: + +Java 配置 + +``` +@Bean +public StaxEventItemReader itemReader() { + return new StaxEventItemReaderBuilder() + .name("itemReader") + .resource(new FileSystemResource("org/springframework/batch/item/xml/domain/trades.xml")) + .addFragmentRootElements("trade") + .unmarshaller(tradeMarshaller()) + .build(); + +} +``` + +请注意,在本例中,我们选择使用`XStreamMarshaller`,它接受作为映射传入的别名,第一个键和值是片段的名称(即根元素)和要绑定的对象类型。然后,类似于`FieldSet`,映射到对象类型中的字段的其他元素的名称在映射中被描述为键/值对。在配置文件中,我们可以使用 Spring 配置实用程序来描述所需的别名。 + +下面的示例展示了如何用 XML 描述别名: + +XML 配置 + +``` + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中描述别名: + +Java 配置 + +``` +@Bean +public XStreamMarshaller tradeMarshaller() { + Map aliases = new HashMap<>(); + aliases.put("trade", Trade.class); + aliases.put("price", BigDecimal.class); + aliases.put("isin", String.class); + aliases.put("customer", String.class); + aliases.put("quantity", Long.class); + + XStreamMarshaller marshaller = new XStreamMarshaller(); + + marshaller.setAliases(aliases); + + return marshaller; +} +``` + +在输入时,读取器读取 XML 资源,直到它识别出一个新的片段即将开始。默认情况下,读取器匹配元素名,以识别一个新片段即将开始。阅读器从片段中创建一个独立的 XML 文档,并将该文档传递给一个反序列化器(通常是围绕 Spring OXM`Unmarshaller`的包装器),以将 XML 映射到一个 Java 对象。 + +总之,这个过程类似于下面的 Java 代码,它使用由 Spring 配置提供的注入: + +``` +StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader<>(); +Resource resource = new ByteArrayResource(xmlResource.getBytes()); + +Map aliases = new HashMap(); +aliases.put("trade","org.springframework.batch.sample.domain.trade.Trade"); +aliases.put("price","java.math.BigDecimal"); +aliases.put("customer","java.lang.String"); +aliases.put("isin","java.lang.String"); +aliases.put("quantity","java.lang.Long"); +XStreamMarshaller unmarshaller = new XStreamMarshaller(); +unmarshaller.setAliases(aliases); +xmlStaxEventItemReader.setUnmarshaller(unmarshaller); +xmlStaxEventItemReader.setResource(resource); +xmlStaxEventItemReader.setFragmentRootElementName("trade"); +xmlStaxEventItemReader.open(new ExecutionContext()); + +boolean hasNext = true; + +Trade trade = null; + +while (hasNext) { + trade = xmlStaxEventItemReader.read(); + if (trade == null) { + hasNext = false; + } + else { + System.out.println(trade); + } +} +``` + +#### [](#StaxEventItemWriter)`StaxEventItemWriter` + +输出与输入对称地工作。`StaxEventItemWriter`需要一个`Resource`、一个编组器和一个`rootTagName`。将 Java 对象传递给编组器(通常是标准的 Spring OXM 编组器),该编组器通过使用自定义事件编写器将 OXM 工具为每个片段产生的`StartDocument`和`EndDocument`事件进行过滤,从而将其写到`Resource`。 + +下面的 XML 示例使用`MarshallingEventWriterSerializer`: + +XML 配置 + +``` + + + + + + +``` + +下面的 Java 示例使用`MarshallingEventWriterSerializer`: + +Java 配置 + +``` +@Bean +public StaxEventItemWriter itemWriter(Resource outputResource) { + return new StaxEventItemWriterBuilder() + .name("tradesWriter") + .marshaller(tradeMarshaller()) + .resource(outputResource) + .rootTagName("trade") + .overwriteOutput(true) + .build(); + +} +``` + +前面的配置设置了三个必需的属性,并设置了可选的`overwriteOutput=true`attrbute,这在本章前面提到过,用于指定现有文件是否可以重写。 + +下面的 XML 示例使用了与本章前面所示的阅读示例中使用的相同的编组器: + +XML 配置 + +``` + + + + + + + + + + + +``` + +下面的 Java 示例使用了与本章前面所示的阅读示例中使用的收集器相同的收集器: + +Java 配置 + +``` +@Bean +public XStreamMarshaller customerCreditMarshaller() { + XStreamMarshaller marshaller = new XStreamMarshaller(); + + Map aliases = new HashMap<>(); + aliases.put("trade", Trade.class); + aliases.put("price", BigDecimal.class); + aliases.put("isin", String.class); + aliases.put("customer", String.class); + aliases.put("quantity", Long.class); + + marshaller.setAliases(aliases); + + return marshaller; +} +``` + +作为 Java 示例的总结,下面的代码演示了讨论的所有要点,并演示了所需属性的编程设置: + +``` +FileSystemResource resource = new FileSystemResource("data/outputFile.xml") + +Map aliases = new HashMap(); +aliases.put("trade","org.springframework.batch.sample.domain.trade.Trade"); +aliases.put("price","java.math.BigDecimal"); +aliases.put("customer","java.lang.String"); +aliases.put("isin","java.lang.String"); +aliases.put("quantity","java.lang.Long"); +Marshaller marshaller = new XStreamMarshaller(); +marshaller.setAliases(aliases); + +StaxEventItemWriter staxItemWriter = + new StaxEventItemWriterBuilder() + .name("tradesWriter") + .marshaller(marshaller) + .resource(resource) + .rootTagName("trade") + .overwriteOutput(true) + .build(); + +staxItemWriter.afterPropertiesSet(); + +ExecutionContext executionContext = new ExecutionContext(); +staxItemWriter.open(executionContext); +Trade trade = new Trade(); +trade.setPrice(11.39); +trade.setIsin("XYZ0001"); +trade.setQuantity(5L); +trade.setCustomer("Customer1"); +staxItemWriter.write(trade); +``` + +### [](#jsonReadingWriting)JSON 条目阅读器和编写器 + +Spring Batch 以以下格式提供对读取和写入 JSON 资源的支持: + +``` +[ + { + "isin": "123", + "quantity": 1, + "price": 1.2, + "customer": "foo" + }, + { + "isin": "456", + "quantity": 2, + "price": 1.4, + "customer": "bar" + } +] +``` + +假定 JSON 资源是与单个项对应的 JSON 对象数组。 Spring 批处理不绑定到任何特定的 JSON 库。 + +#### [](#JsonItemReader)`JsonItemReader` + +`JsonItemReader`将 JSON 解析和绑定委托给`org.springframework.batch.item.json.JsonObjectReader`接口的实现。该接口旨在通过使用流 API 以块形式读取 JSON 对象来实现。目前提供了两种实现方式: + +* [Jackson](https://github.com/FasterXML/jackson)通过`org.springframework.batch.item.json.JacksonJsonObjectReader` + +* [Gson](https://github.com/google/gson)通过`org.springframework.batch.item.json.GsonJsonObjectReader` + +要能够处理 JSON 记录,需要具备以下条件: + +* `Resource`:表示要读取的 JSON 文件的 Spring 资源。 + +* `JsonObjectReader`:用于解析并将 JSON 对象绑定到项的 JSON 对象阅读器 + +下面的示例展示了如何基于 Jackson 定义一个`JsonItemReader`并与前面的 JSON 资源`org/springframework/batch/item/json/trades.json`一起工作的`JsonObjectReader`: + +``` +@Bean +public JsonItemReader jsonItemReader() { + return new JsonItemReaderBuilder() + .jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class)) + .resource(new ClassPathResource("trades.json")) + .name("tradeJsonItemReader") + .build(); +} +``` + +#### [](#jsonfileitemwriter)`JsonFileItemWriter` + +`JsonFileItemWriter`将项的编组委托给`org.springframework.batch.item.json.JsonObjectMarshaller`接口。这个接口的契约是将一个对象带到一个 JSON`String`。目前提供了两种实现方式: + +* [Jackson](https://github.com/FasterXML/jackson)通过`org.springframework.batch.item.json.JacksonJsonObjectMarshaller` + +* [Gson](https://github.com/google/gson)通过`org.springframework.batch.item.json.GsonJsonObjectMarshaller` + +为了能够编写 JSON 记录,需要具备以下条件: + +* `Resource`:表示要写入的 JSON 文件的一个 Spring `Resource` + +* `JsonObjectMarshaller`:一个 JSON 对象编组器将 Marshall 对象转换为 JSON 格式 + +下面的示例展示了如何定义`JsonFileItemWriter`: + +``` +@Bean +public JsonFileItemWriter jsonFileItemWriter() { + return new JsonFileItemWriterBuilder() + .jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>()) + .resource(new ClassPathResource("trades.json")) + .name("tradeJsonFileItemWriter") + .build(); +} +``` + +### [](#multiFileInput)多文件输入 + +在一个`Step`中处理多个文件是一个常见的要求。假设所有文件都具有相同的格式,`MultiResourceItemReader`在 XML 和平面文件处理中都支持这种类型的输入。考虑目录中的以下文件: + +``` +file-1.txt file-2.txt ignored.txt +``` + +File-1.TXT 和 File-2.TXT 的格式相同,出于业务原因,应该一起处理。`MultiResourceItemReader`可以通过使用通配符在两个文件中读取。 + +下面的示例展示了如何使用 XML 中的通配符读取文件: + +XML 配置 + +``` + + + + +``` + +下面的示例展示了如何在 Java 中使用通配符读取文件: + +Java 配置 + +``` +@Bean +public MultiResourceItemReader multiResourceReader() { + return new MultiResourceItemReaderBuilder() + .delegate(flatFileItemReader()) + .resources(resources()) + .build(); +} +``` + +引用的委托是一个简单的`FlatFileItemReader`。上面的配置读取两个文件的输入,处理回滚和重新启动场景。应该注意的是,与任何`ItemReader`一样,在重新启动时添加额外的输入(在这种情况下是一个文件)可能会导致潜在的问题。建议批处理作业使用它们自己的独立目录,直到成功完成为止。 + +| |通过使用`MultiResourceItemReader#setComparator(Comparator)`对输入资源进行排序,以确保在重新启动场景中的作业运行之间保留资源排序。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#database)数据库 + +像大多数 Enterprise 应用程序样式一样,数据库是批处理的中心存储机制。然而,由于系统必须使用的数据集的巨大规模,批处理与其他应用程序样式不同。如果 SQL 语句返回 100 万行,那么结果集可能会将所有返回的结果保存在内存中,直到所有行都被读取为止。 Spring Batch 为此问题提供了两种类型的解决方案: + +* [基于游标的`ItemReader`实现] + +* [分页`ItemReader`实现] + +#### [](#cursorBasedItemReaders)基于光标的`ItemReader`实现 + +使用数据库游标通常是大多数批处理开发人员的默认方法,因为它是数据库解决关系数据“流”问题的方法。Java`ResultSet`类本质上是一种用于操作游标的面向对象机制。a`ResultSet`维护当前数据行的游标。在`ResultSet`上调用`next`将光标移动到下一行。 Spring 基于批处理游标的`ItemReader`实现在初始化时打开游标,并在每次调用`read`时将游标向前移动一行,返回可用于处理的映射对象。然后调用`close`方法,以确保释放所有资源。 Spring 核心`JdbcTemplate`通过使用回调模式来完全映射`ResultSet`中的所有行,并在将控制权返回给方法调用方之前关闭,从而绕过了这个问题。然而,在批处理中,这必须等到步骤完成。下图显示了基于游标的`ItemReader`如何工作的通用关系图。请注意,虽然示例使用 SQL(因为 SQL 是广为人知的),但任何技术都可以实现基本方法。 + +![游标示例](./images/cursorExample.png) + +图 3。游标示例 + +这个例子说明了基本模式。给定一个有三列的“foo”表:`ID`、`NAME`和`BAR`,选择 ID 大于 1 但小于 7 的所有行。这将把游标的开头(第 1 行)放在 ID2 上。该行的结果应该是一个完全映射的`Foo`对象。调用`read()`再次将光标移动到下一行,即 ID 为 3 的`Foo`。在每个`read`之后写出这些读取的结果,从而允许对对象进行垃圾收集(假设没有实例变量维护对它们的引用)。 + +##### [](#JdbcCursorItemReader)`JdbcCursorItemReader` + +`JdbcCursorItemReader`是基于光标的技术的 JDBC 实现。它可以直接与`ResultSet`一起工作,并且需要针对从`DataSource`获得的连接运行 SQL 语句。下面的数据库模式用作示例: + +``` +CREATE TABLE CUSTOMER ( + ID BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(45), + CREDIT FLOAT +); +``` + +许多人更喜欢为每一行使用域对象,因此下面的示例使用`RowMapper`接口的实现来映射`CustomerCredit`对象: + +``` +public class CustomerCreditRowMapper implements RowMapper { + + public static final String ID_COLUMN = "id"; + public static final String NAME_COLUMN = "name"; + public static final String CREDIT_COLUMN = "credit"; + + public CustomerCredit mapRow(ResultSet rs, int rowNum) throws SQLException { + CustomerCredit customerCredit = new CustomerCredit(); + + customerCredit.setId(rs.getInt(ID_COLUMN)); + customerCredit.setName(rs.getString(NAME_COLUMN)); + customerCredit.setCredit(rs.getBigDecimal(CREDIT_COLUMN)); + + return customerCredit; + } +} +``` + +因为`JdbcCursorItemReader`与`JdbcTemplate`共享关键接口,所以查看如何使用`JdbcTemplate`在此数据中读取数据的示例非常有用,以便将其与`ItemReader`进行对比。为了这个示例的目的,假设`CUSTOMER`数据库中有 1,000 行。第一个示例使用`JdbcTemplate`: + +``` +//For simplicity sake, assume a dataSource has already been obtained +JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); +List customerCredits = jdbcTemplate.query("SELECT ID, NAME, CREDIT from CUSTOMER", + new CustomerCreditRowMapper()); +``` + +在运行前面的代码片段之后,`customerCredits`列表包含 1,000 个`CustomerCredit`对象。在查询方法中,从`DataSource`获得连接,对其运行所提供的 SQL,并对`mapRow`中的每一行调用`ResultSet`方法。将其与`JdbcCursorItemReader`的方法进行对比,如下例所示: + +``` +JdbcCursorItemReader itemReader = new JdbcCursorItemReader(); +itemReader.setDataSource(dataSource); +itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER"); +itemReader.setRowMapper(new CustomerCreditRowMapper()); +int counter = 0; +ExecutionContext executionContext = new ExecutionContext(); +itemReader.open(executionContext); +Object customerCredit = new Object(); +while(customerCredit != null){ + customerCredit = itemReader.read(); + counter++; +} +itemReader.close(); +``` + +在运行前面的代码片段之后,计数器等于 1,000。如果上面的代码将返回的`customerCredit`放入一个列表中,结果将与`JdbcTemplate`示例完全相同。然而,`ItemReader`的一大优势在于,它允许项目被“流化”。`read`方法可以调用一次,该项可以由一个`ItemWriter`写出,然后可以用`read`获得下一个项。这使得项目的读写可以在“块”中完成,并定期提交,这是高性能批处理的本质。此外,很容易地将其配置为将`Step`注入到 Spring 批中。 + +下面的示例展示了如何在 XML 中将`ItemReader`插入到`Step`中: + +XML 配置 + +``` + + + + + + + +``` + +下面的示例展示了如何在 Java 中将`ItemReader`注入`Step`: + +Java 配置 + +``` +@Bean +public JdbcCursorItemReader itemReader() { + return new JdbcCursorItemReaderBuilder() + .dataSource(this.dataSource) + .name("creditReader") + .sql("select ID, NAME, CREDIT from CUSTOMER") + .rowMapper(new CustomerCreditRowMapper()) + .build(); + +} +``` + +###### [](#JdbcCursorItemReaderProperties)附加属性 + +因为在 Java 中有很多不同的打开光标的选项,所以`JdbcCursorItemReader`上有很多可以设置的属性,如下表所示: + +| ignoreWarnings |确定是否记录了 SQLwarns 或是否导致异常。
默认值是`true`(这意味着记录了警告)。| +|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| fetchSize |当`ResultSet`对象所使用的`ResultSet`对象需要更多行时,向 JDBC 驱动程序提供有关应该从数据库中获取
的行数的提示。默认情况下,不会给出任何提示。| +| maxRows |设置底层`ResultSet`在任何时候都可以
的最大行数的限制。| +| queryTimeout |将驱动程序等待`Statement`对象的秒数设置为
运行。如果超过限制,则抛出`DataAccessException`。(有关详细信息,请咨询你的驱动程序
供应商文档)。| +| verifyCursorPosition |因为由`ItemReader`持有的相同`ResultSet`被传递到
`RowMapper`,所以用户可以自己调用`ResultSet.next()`,这可能会导致阅读器的内部计数出现问题。将该值设置为`true`会导致
在`RowMapper`调用后,如果光标位置与以前不同,将引发一个异常。| +| saveState |指示是否应将读取器的状态保存在`ExecutionContext`提供的`ItemStream#update(ExecutionContext)`中。默认值为`true`。| +| driverSupportsAbsolute |指示 JDBC 驱动程序是否支持
设置`ResultSet`上的绝对行。对于支持`ResultSet.absolute()`的 JDBC 驱动程序,建议将其设置为`true`,因为这可能会提高性能,
特别是在使用大数据集时发生步骤失败时。默认值为`false`。| +|setUseSharedExtendedConnection|指示用于光标的连接
是否应由所有其他处理使用,从而共享相同的
事务。如果将其设置为`false`,然后用它自己的连接
打开光标,并且不参与启动的任何事务对于步骤处理的其余部分,
如果将此标志设置为`true`,则必须将数据源包装在`ExtendedConnectionDataSourceProxy`中,以防止连接被关闭,并在每次提交后释放
。当你将此选项设置为`true`时,用于
打开光标的语句将使用’只读’和’持有 \_ 游标 \_over\_commit’选项创建。
这允许在事务启动时保持光标打开,并在
步骤处理中执行提交。要使用此功能,你需要一个支持此功能的数据库,以及一个支持 JDBC3.0 或更高版本的 JDBC
驱动程序。默认值为`false`。| + +##### [](#HibernateCursorItemReader)`HibernateCursorItemReader` + +正如正常的 Spring 用户对是否使用 ORM 解决方案做出重要的决定,这会影响他们是否使用`JdbcTemplate`或`HibernateTemplate`, Spring 批处理用户具有相同的选项。`HibernateCursorItemReader`是 Hibernate 游标技术的实现。 Hibernate 的批量使用一直颇具争议。这在很大程度上是因为 Hibernate 最初是为了支持在线应用程序样式而开发的。然而,这并不意味着它不能用于批处理。解决这个问题的最简单的方法是使用`StatelessSession`,而不是使用标准会话。这删除了 Hibernate 使用的所有缓存和脏检查,这可能会在批处理场景中导致问题。有关无状态会话和正常 Hibernate 会话之间的差异的更多信息,请参阅你的特定 Hibernate 版本的文档。`HibernateCursorItemReader`允许你声明一个 HQL 语句,并传入一个`SessionFactory`,它将在每个调用中传回一个项,以与`JdbcCursorItemReader`相同的基本方式进行读取。下面的示例配置使用了与 JDBC 阅读器相同的“客户信用”示例: + +``` +HibernateCursorItemReader itemReader = new HibernateCursorItemReader(); +itemReader.setQueryString("from CustomerCredit"); +//For simplicity sake, assume sessionFactory already obtained. +itemReader.setSessionFactory(sessionFactory); +itemReader.setUseStatelessSession(true); +int counter = 0; +ExecutionContext executionContext = new ExecutionContext(); +itemReader.open(executionContext); +Object customerCredit = new Object(); +while(customerCredit != null){ + customerCredit = itemReader.read(); + counter++; +} +itemReader.close(); +``` + +这个配置的`ItemReader`以与`JdbcCursorItemReader`所描述的完全相同的方式返回`CustomerCredit`对象,假设 Hibernate 已经为`Customer`表正确地创建了映射文件。“useStatelession”属性默认为 true,但在此添加此属性是为了提请注意打开或关闭它的能力。还值得注意的是,可以使用`setFetchSize`属性设置底层游标的 fetch 大小。与`JdbcCursorItemReader`一样,配置也很简单。 + +下面的示例展示了如何在 XML 中注入 Hibernate `ItemReader`: + +XML 配置 + +``` + + + + +``` + +下面的示例展示了如何在 Java 中注入 Hibernate `ItemReader`: + +Java 配置 + +``` +@Bean +public HibernateCursorItemReader itemReader(SessionFactory sessionFactory) { + return new HibernateCursorItemReaderBuilder() + .name("creditReader") + .sessionFactory(sessionFactory) + .queryString("from CustomerCredit") + .build(); +} +``` + +##### [](#StoredProcedureItemReader)`StoredProcedureItemReader` + +有时需要使用存储过程来获取游标数据。`StoredProcedureItemReader`的工作原理与`JdbcCursorItemReader`类似,不同的是,它运行的是返回光标的存储过程,而不是运行查询来获取光标。存储过程可以以三种不同的方式返回光标: + +* 作为返回的`ResultSet`(由 SQL Server、Sybase、DB2、Derby 和 MySQL 使用)。 + +* 作为 ref-cursor 作为 out 参数返回(Oracle 和 PostgreSQL 使用)。 + +* 作为存储函数调用的返回值。 + +下面的 XML 示例配置使用了与前面的示例相同的“客户信用”示例: + +XML 配置 + +``` + + + + + + + +``` + +下面的 Java 示例配置使用了与前面的示例相同的“客户信用”示例: + +Java 配置 + +``` +@Bean +public StoredProcedureItemReader reader(DataSource dataSource) { + StoredProcedureItemReader reader = new StoredProcedureItemReader(); + + reader.setDataSource(dataSource); + reader.setProcedureName("sp_customer_credit"); + reader.setRowMapper(new CustomerCreditRowMapper()); + + return reader; +} +``` + +前面的示例依赖于存储过程来提供`ResultSet`作为返回的结果(前面的选项 1)。 + +如果存储过程返回了`ref-cursor`(选项 2),那么我们将需要提供输出参数的位置,即返回的`ref-cursor`。 + +下面的示例展示了如何使用第一个参数作为 XML 中的 ref-cursor: + +XML 配置 + +``` + + + + + + + + +``` + +下面的示例展示了如何使用第一个参数作为 Java 中的 ref-cursor: + +Java 配置 + +``` +@Bean +public StoredProcedureItemReader reader(DataSource dataSource) { + StoredProcedureItemReader reader = new StoredProcedureItemReader(); + + reader.setDataSource(dataSource); + reader.setProcedureName("sp_customer_credit"); + reader.setRowMapper(new CustomerCreditRowMapper()); + reader.setRefCursorPosition(1); + + return reader; +} +``` + +如果光标是从存储函数返回的(选项 3),则需要将属性“function”设置为`true`。它的默认值为`false`。 + +下面的示例在 XML 中向`true`显示了属性: + +XML 配置 + +``` + + + + + + + + +``` + +下面的示例在 Java 中向`true`显示了属性: + +Java 配置 + +``` +@Bean +public StoredProcedureItemReader reader(DataSource dataSource) { + StoredProcedureItemReader reader = new StoredProcedureItemReader(); + + reader.setDataSource(dataSource); + reader.setProcedureName("sp_customer_credit"); + reader.setRowMapper(new CustomerCreditRowMapper()); + reader.setFunction(true); + + return reader; +} +``` + +在所有这些情况下,我们需要定义一个`RowMapper`以及一个`DataSource`和实际的过程名称。 + +如果存储过程或函数接受参数,则必须使用`parameters`属性声明和设置参数。下面的示例为 Oracle 声明了三个参数。第一个参数是返回 ref-cursor 的`out`参数,第二个和第三个参数是参数中的`INTEGER`类型的值。 + +下面的示例展示了如何使用 XML 中的参数: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +下面的示例展示了如何使用 Java 中的参数: + +Java 配置 + +``` +@Bean +public StoredProcedureItemReader reader(DataSource dataSource) { + List parameters = new ArrayList<>(); + parameters.add(new SqlOutParameter("newId", OracleTypes.CURSOR)); + parameters.add(new SqlParameter("amount", Types.INTEGER); + parameters.add(new SqlParameter("custId", Types.INTEGER); + + StoredProcedureItemReader reader = new StoredProcedureItemReader(); + + reader.setDataSource(dataSource); + reader.setProcedureName("spring.cursor_func"); + reader.setParameters(parameters); + reader.setRefCursorPosition(1); + reader.setRowMapper(rowMapper()); + reader.setPreparedStatementSetter(parameterSetter()); + + return reader; +} +``` + +除了参数声明外,我们还需要指定一个`PreparedStatementSetter`实现,该实现为调用设置参数值。这与上面的`JdbcCursorItemReader`的工作原理相同。[附加属性](#JdbcCursorItemReaderProperties)中列出的所有附加属性也适用于`StoredProcedureItemReader`。 + +#### [](#pagingItemReaders)分页`ItemReader`实现 + +使用数据库游标的一种替代方法是运行多个查询,其中每个查询获取部分结果。我们把这一部分称为一个页面。每个查询必须指定起始行号和我们希望在页面中返回的行数。 + +##### [](#JdbcPagingItemReader)`JdbcPagingItemReader` + +分页`ItemReader`的一个实现是`JdbcPagingItemReader`。`JdbcPagingItemReader`需要一个`PagingQueryProvider`,负责提供用于检索构成页面的行的 SQL 查询。由于每个数据库都有自己的策略来提供分页支持,因此我们需要为每个受支持的数据库类型使用不同的`PagingQueryProvider`。还有`SqlPagingQueryProviderFactoryBean`自动检测正在使用的数据库,并确定适当的`PagingQueryProvider`实现。这简化了配置,是推荐的最佳实践。 + +`SqlPagingQueryProviderFactoryBean`要求你指定`select`子句和`from`子句。你还可以提供一个可选的`where`子句。这些子句和所需的`sortKey`用于构建 SQL 语句。 + +| |在`sortKey`上有一个唯一的键约束是很重要的,以保证
在两次执行之间不会丢失任何数据。| +|---|--------------------------------------------------------------------------------------------------------------------------| + +打开读取器后,它会以与任何其他`ItemReader`相同的基本方式,将每个调用返回一个项到`read`。当需要额外的行时,分页会在幕后进行。 + +下面的 XML 示例配置使用了与前面显示的基于游标的`ItemReaders`类似的“客户信用”示例: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + +``` + +下面的 Java 示例配置使用了与前面显示的基于游标的`ItemReaders`类似的“客户信用”示例: + +Java 配置 + +``` +@Bean +public JdbcPagingItemReader itemReader(DataSource dataSource, PagingQueryProvider queryProvider) { + Map parameterValues = new HashMap<>(); + parameterValues.put("status", "NEW"); + + return new JdbcPagingItemReaderBuilder() + .name("creditReader") + .dataSource(dataSource) + .queryProvider(queryProvider) + .parameterValues(parameterValues) + .rowMapper(customerCreditMapper()) + .pageSize(1000) + .build(); +} + +@Bean +public SqlPagingQueryProviderFactoryBean queryProvider() { + SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean(); + + provider.setSelectClause("select id, name, credit"); + provider.setFromClause("from customer"); + provider.setWhereClause("where status=:status"); + provider.setSortKey("id"); + + return provider; +} +``` + +此配置的`ItemReader`使用`RowMapper`返回`CustomerCredit`对象,该对象必须指定。“PageSize”属性确定每次运行查询时从数据库中读取的实体的数量。 + +“parametervalues”属性可用于为查询指定一个`Map`参数值。如果在`where`子句中使用命名参数,则每个条目的键应该与命名参数的名称匹配。如果使用传统的“?”占位符,那么每个条目的键应该是占位符的编号,从 1 开始。 + +##### [](#JpaPagingItemReader)`JpaPagingItemReader` + +分页`ItemReader`的另一个实现是`JpaPagingItemReader`。 JPA 不具有类似于 Hibernate 的概念,因此我们不得不使用由 JPA 规范提供的其他特征。由于 JPA 支持分页,所以当涉及到使用 JPA 进行批处理时,这是一个自然的选择。在读取每个页面之后,这些实体将被分离,持久性上下文将被清除,从而允许在页面被处理之后对这些实体进行垃圾收集。 + +`JpaPagingItemReader`允许你声明一个 JPQL 语句,并传入一个`EntityManagerFactory`。然后,它在每个调用中传回一个项,以与任何其他`ItemReader`相同的基本方式进行读取。当需要额外的实体时,寻呼就会在幕后进行。 + +下面的 XML 示例配置使用了与前面显示的 JDBC 阅读器相同的“客户信用”示例: + +XML 配置 + +``` + + + + + +``` + +下面的 Java 示例配置使用了与前面显示的 JDBC 阅读器相同的“客户信用”示例: + +Java 配置 + +``` +@Bean +public JpaPagingItemReader itemReader() { + return new JpaPagingItemReaderBuilder() + .name("creditReader") + .entityManagerFactory(entityManagerFactory()) + .queryString("select c from CustomerCredit c") + .pageSize(1000) + .build(); +} +``` + +这个配置的`ItemReader`以与上面描述的`JdbcPagingItemReader`对象完全相同的方式返回`CustomerCredit`对象,假设`CustomerCredit`对象具有正确的 JPA 注释或 ORM 映射文件。“PageSize”属性确定每个查询执行从数据库中读取的实体的数量。 + +#### [](#databaseItemWriters)数据库项目编写器 + +虽然平面文件和 XML 文件都有一个特定的`ItemWriter`实例,但在数据库世界中没有完全相同的实例。这是因为事务提供了所需的所有功能。`ItemWriter`实现对于文件来说是必要的,因为它们必须像事务一样工作,跟踪写好的项目,并在适当的时候刷新或清除。数据库不需要此功能,因为写操作已经包含在事务中了。用户可以创建自己的 DAO 来实现`ItemWriter`接口,或者使用自定义的`ItemWriter`接口,这是为通用处理问题编写的。无论哪种方式,它们的工作都应该没有任何问题。需要注意的一点是批处理输出所提供的性能和错误处理能力。当使用 Hibernate 作为`ItemWriter`时,这是最常见的,但是当使用 JDBC 批处理模式时,可能会有相同的问题。批处理数据库输出没有任何固有的缺陷,前提是我们要小心刷新,并且数据中没有错误。然而,书写时的任何错误都可能导致混淆,因为无法知道是哪个单独的项目导致了异常,或者即使是任何单独的项目是负责任的,如下图所示: + +![刷新错误](./images/errorOnFlush.png) + +图 4。刷新错误 + +如果项目在写入之前被缓冲,则在提交之前刷新缓冲区之前不会抛出任何错误。例如,假设每个块写 20 个项,第 15 个项抛出一个`DataIntegrityViolationException`。就`Step`而言,所有 20 个项都已成功写入,因为只有在实际写入它们之前,才能知道发生了错误。一旦调用`Session#flush()`,将清空缓冲区并命中异常。在这一点上,`Step`是无能为力的。事务必须回滚。通常,此异常可能会导致跳过该项(取决于跳过/重试策略),然后不会再次写入该项。但是,在批处理场景中,无法知道是哪个项导致了问题。当故障发生时,整个缓冲区正在被写入。解决此问题的唯一方法是在每个项目之后进行刷新,如下图所示: + +![写错误](./images/errorOnWrite.png) + +图 5。写错误 + +这是一个常见的用例,尤其是在使用 Hibernate 时,而`ItemWriter`的实现的简单准则是在每次调用`write()`时刷新。这样做允许可靠地跳过项, Spring 批处理在内部处理错误后对`ItemWriter`的调用的粒度。 + +### [](#reusingExistingServices)重用现有服务 + +批处理系统通常与其他应用程序样式结合使用。最常见的是在线系统,但它也可以通过移动每个应用程序样式使用的必要的大容量数据来支持集成,甚至支持厚客户机应用程序。由于这个原因,许多用户希望在其批处理作业中重用现有的 DAO 或其他服务是很常见的。 Spring 容器本身通过允许注入任何必要的类,使这一点变得相当容易。然而,可能存在现有服务需要充当`ItemReader`或`ItemWriter`的情况,要么是为了满足另一个 Spring 批处理类的依赖关系,要么是因为它确实是主要的`ItemReader`的一个步骤。为每个需要包装的服务编写一个适配器类是相当琐碎的,但是由于这是一个常见的问题, Spring Batch 提供了实现:`ItemReaderAdapter`和`ItemWriterAdapter`。这两个类都通过调用委托模式来实现标准 Spring 方法,并且设置起来相当简单。 + +下面的 XML 示例使用`ItemReaderAdapter`: + +XML 配置 + +``` + + + + + + +``` + +下面的 Java 示例使用`ItemReaderAdapter`: + +Java 配置 + +``` +@Bean +public ItemReaderAdapter itemReader() { + ItemReaderAdapter reader = new ItemReaderAdapter(); + + reader.setTargetObject(fooService()); + reader.setTargetMethod("generateFoo"); + + return reader; +} + +@Bean +public FooService fooService() { + return new FooService(); +} +``` + +需要注意的一点是,`targetMethod`的契约必须与`read`的契约相同:当耗尽时,它返回`null`。否则,它返回一个`Object`。根据`ItemWriter`的实现,任何其他方法都会阻止框架知道处理应该何时结束,从而导致无限循环或错误失败。 + +下面的 XML 示例使用`ItemWriterAdapter`: + +XML 配置 + +``` + + + + + + +``` + +下面的 Java 示例使用`ItemWriterAdapter`: + +Java 配置 + +``` +@Bean +public ItemWriterAdapter itemWriter() { + ItemWriterAdapter writer = new ItemWriterAdapter(); + + writer.setTargetObject(fooService()); + writer.setTargetMethod("processFoo"); + + return writer; +} + +@Bean +public FooService fooService() { + return new FooService(); +} +``` + +### [](#process-indicator)防止状态持久性 + +默认情况下,所有`ItemReader`和`ItemWriter`实现在提交之前将其当前状态存储在`ExecutionContext`中。然而,这可能并不总是理想的行为。例如,许多开发人员选择通过使用过程指示器使他们的数据库阅读器“可重新运行”。在输入数据中添加一个额外的列,以指示是否对其进行了处理。当读取(或写入)特定记录时,处理后的标志从`false`翻转到`true`。然后,SQL 语句可以在`where`子句中包含一个额外的语句,例如`where PROCESSED_IND = false`,从而确保在重新启动的情况下仅返回未处理的记录。在这种情况下,最好不要存储任何状态,例如当前行号,因为它在重新启动时是不相关的。由于这个原因,所有的读者和作者都包括“SaveState”财产。 + +Bean 下面的定义展示了如何防止 XML 中的状态持久性: + +XML 配置 + +``` + + + + + + + + + SELECT games.player_id, games.year_no, SUM(COMPLETES), + SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD), + SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS), + SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD) + from games, players where players.player_id = + games.player_id group by games.player_id, games.year_no + + + +``` + +Bean 下面的定义展示了如何在 Java 中防止状态持久性: + +Java 配置 + +``` +@Bean +public JdbcCursorItemReader playerSummarizationSource(DataSource dataSource) { + return new JdbcCursorItemReaderBuilder() + .dataSource(dataSource) + .rowMapper(new PlayerSummaryMapper()) + .saveState(false) + .sql("SELECT games.player_id, games.year_no, SUM(COMPLETES)," + + "SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD)," + + "SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS)," + + "SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)" + + "from games, players where players.player_id =" + + "games.player_id group by games.player_id, games.year_no") + .build(); + +} +``` + +上面配置的`ItemReader`不会在`ExecutionContext`中为其参与的任何执行创建任何条目。 + +### [](#customReadersWriters)创建自定义项目阅读器和项目编写器 + +到目前为止,本章已经讨论了 Spring 批处理中的读和写的基本契约,以及这样做的一些常见实现。然而,这些都是相当通用的,并且有许多潜在的场景可能不会被开箱即用的实现所覆盖。本节通过使用一个简单的示例,展示了如何创建自定义`ItemReader`和`ItemWriter`实现,并正确地实现它们的契约。`ItemReader`还实现了`ItemStream`,以说明如何使读取器或写入器重新启动。 + +#### [](#customReader)自定义`ItemReader`示例 + +为了这个示例的目的,我们创建了一个简单的`ItemReader`实现,该实现从提供的列表中读取数据。我们首先实现`ItemReader`的最基本契约,即`read`方法,如以下代码所示: + +``` +public class CustomItemReader implements ItemReader { + + List items; + + public CustomItemReader(List items) { + this.items = items; + } + + public T read() throws Exception, UnexpectedInputException, + NonTransientResourceException, ParseException { + + if (!items.isEmpty()) { + return items.remove(0); + } + return null; + } +} +``` + +前面的类获取一个项目列表,并一次返回一个项目,将每个项目从列表中删除。当列表为空时,它返回`null`,从而满足`ItemReader`的最基本要求,如下面的测试代码所示: + +``` +List items = new ArrayList<>(); +items.add("1"); +items.add("2"); +items.add("3"); + +ItemReader itemReader = new CustomItemReader<>(items); +assertEquals("1", itemReader.read()); +assertEquals("2", itemReader.read()); +assertEquals("3", itemReader.read()); +assertNull(itemReader.read()); +``` + +##### [](#restartableReader)使`ItemReader`可重启 + +最后的挑战是使`ItemReader`重新启动。目前,如果处理被中断并重新开始,`ItemReader`必须在开始时开始。这实际上在许多场景中都是有效的,但有时更可取的做法是,在批处理作业停止的地方重新启动它。关键的判别式通常是读者是有状态的还是无状态的。无状态的读者不需要担心重启性,但是有状态的读者必须尝试在重新启动时重建其最后已知的状态。出于这个原因,我们建议你在可能的情况下保持自定义阅读器的无状态,这样你就不必担心重启性了。 + +如果确实需要存储状态,那么应该使用`ItemStream`接口: + +``` +public class CustomItemReader implements ItemReader, ItemStream { + + List items; + int currentIndex = 0; + private static final String CURRENT_INDEX = "current.index"; + + public CustomItemReader(List items) { + this.items = items; + } + + public T read() throws Exception, UnexpectedInputException, + ParseException, NonTransientResourceException { + + if (currentIndex < items.size()) { + return items.get(currentIndex++); + } + + return null; + } + + public void open(ExecutionContext executionContext) throws ItemStreamException { + if (executionContext.containsKey(CURRENT_INDEX)) { + currentIndex = new Long(executionContext.getLong(CURRENT_INDEX)).intValue(); + } + else { + currentIndex = 0; + } + } + + public void update(ExecutionContext executionContext) throws ItemStreamException { + executionContext.putLong(CURRENT_INDEX, new Long(currentIndex).longValue()); + } + + public void close() throws ItemStreamException {} +} +``` + +在每次调用`ItemStream``update`方法时,`ItemReader`的当前索引都存储在提供的`ExecutionContext`中,其键为“current.index”。当调用`ItemStream``open`方法时,将检查`ExecutionContext`是否包含带有该键的条目。如果找到了键,则将当前索引移动到该位置。这是一个相当微不足道的例子,但它仍然符合一般合同: + +``` +ExecutionContext executionContext = new ExecutionContext(); +((ItemStream)itemReader).open(executionContext); +assertEquals("1", itemReader.read()); +((ItemStream)itemReader).update(executionContext); + +List items = new ArrayList<>(); +items.add("1"); +items.add("2"); +items.add("3"); +itemReader = new CustomItemReader<>(items); + +((ItemStream)itemReader).open(executionContext); +assertEquals("2", itemReader.read()); +``` + +大多数`ItemReaders`都有更复杂的重启逻辑。例如,`JdbcCursorItemReader`将最后处理的行的行 ID 存储在游标中。 + +还值得注意的是,`ExecutionContext`中使用的键不应该是微不足道的。这是因为相同的`ExecutionContext`用于`ItemStreams`中的所有`Step`。在大多数情况下,只需在键前加上类名就足以保证唯一性。然而,在很少的情况下,在相同的步骤中使用两个相同类型的`ItemStream`(如果需要输出两个文件,可能会发生这种情况),则需要一个更唯一的名称。由于这个原因,许多 Spring 批处理`ItemReader`和`ItemWriter`实现都有一个`setName()`属性,该属性允许重写这个键名。 + +#### [](#customWriter)自定义`ItemWriter`示例 + +实现自定义`ItemWriter`在许多方面与上面的`ItemReader`示例相似,但在足够多的方面有所不同,以保证它自己的示例。然而,添加可重启性本质上是相同的,因此在本例中不涉及它。与`ItemReader`示例一样,使用`List`是为了使示例尽可能简单: + +``` +public class CustomItemWriter implements ItemWriter { + + List output = TransactionAwareProxyFactory.createTransactionalList(); + + public void write(List items) throws Exception { + output.addAll(items); + } + + public List getOutput() { + return output; + } +} +``` + +##### [](#restartableWriter)使`ItemWriter`重新启动 + +要使`ItemWriter`可重启,我们将遵循与`ItemReader`相同的过程,添加并实现`ItemStream`接口以同步执行上下文。在这个示例中,我们可能必须计算处理的项目的数量,并将其添加为页脚记录。如果需要这样做,我们可以在`ItemWriter`中实现`ItemStream`,这样,如果流被重新打开,计数器将从执行上下文中重新构造。 + +在许多实际的情况下,自定义`ItemWriters`也会委托给另一个本身是可重启的编写器(例如,当写到文件时),或者它会写到事务资源,因此不需要重启,因为它是无状态的。当你有一个有状态的编写器时,你可能应该确保实现`ItemStream`以及`ItemWriter`。还请记住,Writer 的客户机需要知道`ItemStream`,因此你可能需要在配置中将其注册为流。 + +### [](#itemReaderAndWriterImplementations)项读取器和编写器实现 + +在本节中,我们将向你介绍在前几节中尚未讨论过的读者和作者。 + +#### [](#decorators)装饰者 + +在某些情况下,用户需要将专门的行为附加到预先存在的`ItemReader`。 Spring Batch 提供了一些开箱即用的装饰器,它们可以将额外的行为添加到你的`ItemReader`和`ItemWriter`实现中。 + +Spring 批处理包括以下装饰器: + +* [`SynchronizedItemStreamReader`] + +* [`SingleItemPeekableItemReader`] + +* [`SynchronizedItemStreamWriter`] + +* [`MultiResourceItemWriter`] + +* [`ClassifierCompositeItemWriter`] + +* [`ClassifierCompositeItemProcessor`] + +##### [](#synchronizedItemStreamReader)`SynchronizedItemStreamReader` + +当使用不是线程安全的`ItemReader`时, Spring Batch 提供`SynchronizedItemStreamReader`decorator,该 decorator 可用于使`ItemReader`线程安全。 Spring 批处理提供了一个`SynchronizedItemStreamReaderBuilder`来构造`SynchronizedItemStreamReader`的实例。 + +##### [](#singleItemPeekableItemReader)`SingleItemPeekableItemReader` + +Spring 批处理包括向`ItemReader`添加 PEEK 方法的装饰器。这种 peek 方法允许用户提前查看一项。对 Peek 的重复调用返回相同的项,这是从`read`方法返回的下一个项。 Spring 批处理提供了一个`SingleItemPeekableItemReaderBuilder`来构造`SingleItemPeekableItemReader`的实例。 + +| |SingleitemPeekableitemreader 的 Peek 方法不是线程安全的,因为它不可能
在多个线程中执行 Peek。窥视
的线程中只有一个会在下一次调用中获得要读取的项。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [](#synchronizedItemStreamWriter)`SynchronizedItemStreamWriter` + +当使用不是线程安全的`ItemWriter`时, Spring Batch 提供`SynchronizedItemStreamWriter`decorator,该 decorator 可用于使`ItemWriter`线程安全。 Spring 批处理提供了一个`SynchronizedItemStreamWriterBuilder`来构造`SynchronizedItemStreamWriter`的实例。 + +##### [](#multiResourceItemWriter)`MultiResourceItemWriter` + +当当前资源中写入的项数超过`itemCountLimitPerResource`时,`MultiResourceItemWriter`包装一个`ResourceAwareItemWriterItemStream`并创建一个新的输出资源。 Spring 批处理提供了一个`MultiResourceItemWriterBuilder`来构造`MultiResourceItemWriter`的实例。 + +##### [](#classifierCompositeItemWriter)`ClassifierCompositeItemWriter` + +`ClassifierCompositeItemWriter`调用用于每个项的`ItemWriter`实现的集合之一,该实现基于通过提供的`Classifier`实现的路由器模式。如果所有委托都是线程安全的,则实现是线程安全的。 Spring 批处理提供了一个`ClassifierCompositeItemWriterBuilder`来构造`ClassifierCompositeItemWriter`的实例。 + +##### [](#classifierCompositeItemProcessor)`ClassifierCompositeItemProcessor` + +`ClassifierCompositeItemProcessor`是一个`ItemProcessor`,它调用`ItemProcessor`实现的集合之一,该实现基于通过所提供的`Classifier`实现的路由器模式。 Spring 批处理提供了一个`ClassifierCompositeItemProcessorBuilder`来构造`ClassifierCompositeItemProcessor`的实例。 + +#### [](#messagingReadersAndWriters)消息阅读器和消息编写器 + +Spring Batch 为常用的消息传递系统提供了以下读取器和编写器: + +* [`AmqpItemReader`] + +* [`AmqpItemWriter`] + +* [`JmsItemReader`] + +* [`JmsItemWriter`] + +* [`KafkaItemReader`] + +* [`KafkaItemWriter`] + +##### [](#amqpItemReader)`AmqpItemReader` + +`AmqpItemReader`是一个`ItemReader`,它使用`AmqpTemplate`来接收或转换来自交换的消息。 Spring 批处理提供了一个`AmqpItemReaderBuilder`来构造`AmqpItemReader`的实例。 + +##### [](#amqpItemWriter)`AmqpItemWriter` + +`AmqpItemWriter`是一个`ItemWriter`,它使用`AmqpTemplate`向 AMQP 交换发送消息。如果提供的`AmqpTemplate`中未指定名称,则将消息发送到无名交换机。 Spring 批处理提供了`AmqpItemWriterBuilder`来构造`AmqpItemWriter`的实例。 + +##### [](#jmsItemReader)`JmsItemReader` + +对于使用`JmsTemplate`的 JMS,`ItemReader`是`ItemReader`。模板应该有一个默认的目标,它用于为`read()`方法提供项。 Spring 批处理提供了一个`JmsItemReaderBuilder`来构造`JmsItemReader`的实例。 + +##### [](#jmsItemWriter)`JmsItemWriter` + +对于使用`JmsTemplate`的 JMS,`ItemWriter`是`ItemWriter`。模板应该有一个默认的目的地,用于在`write(List)`中发送项。 Spring 批处理提供了一个`JmsItemWriterBuilder`来构造`JmsItemWriter`的实例。 + +##### [](#kafkaItemReader)`KafkaItemReader` + +对于 Apache Kafka 主题,`KafkaItemReader`是`ItemReader`。可以将其配置为从同一主题的多个分区中读取消息。它在执行上下文中存储消息偏移量,以支持重新启动功能。 Spring 批处理提供了一个`KafkaItemReaderBuilder`来构造`KafkaItemReader`的实例。 + +##### [](#kafkaItemWriter)`KafkaItemWriter` + +`KafkaItemWriter`是用于 Apache Kafka 的`ItemWriter`,它使用`KafkaTemplate`将事件发送到默认主题。 Spring 批处理提供了一个`KafkaItemWriterBuilder`来构造`KafkaItemWriter`的实例。 + +#### [](#databaseReaders)数据库阅读器 + +Spring Batch 提供以下数据库阅读器: + +* [`Neo4jItemReader`](#NEO4jitemreader) + +* [`MongoItemReader`] + +* [`HibernateCursorItemReader`] + +* [`HibernatePagingItemReader`] + +* [`RepositoryItemReader`] + +##### [](#Neo4jItemReader)`Neo4jItemReader` + +`Neo4jItemReader`是一个`ItemReader`,它使用分页技术从图数据库 NEO4j 中读取对象。 Spring 批处理提供了一个`Neo4jItemReaderBuilder`来构造`Neo4jItemReader`的实例。 + +##### [](#mongoItemReader)`MongoItemReader` + +`MongoItemReader`是一个`ItemReader`,它使用分页技术从 MongoDB 读取文档。 Spring 批处理提供了一个`MongoItemReaderBuilder`来构造`MongoItemReader`的实例。 + +##### [](#hibernateCursorItemReader)`HibernateCursorItemReader` + +`HibernateCursorItemReader`是用于读取在 Hibernate 之上构建的数据库记录的`ItemStreamReader`。它执行 HQL 查询,然后在初始化时,在调用`read()`方法时对结果集进行迭代,依次返回与当前行对应的对象。 Spring 批处理提供了一个`HibernateCursorItemReaderBuilder`来构造`HibernateCursorItemReader`的实例。 + +##### [](#hibernatePagingItemReader)`HibernatePagingItemReader` + +`HibernatePagingItemReader`是一个`ItemReader`,用于读取建立在 Hibernate 之上的数据库记录,并且一次只读取固定数量的项。 Spring 批处理提供了一个`HibernatePagingItemReaderBuilder`来构造`HibernatePagingItemReader`的实例。 + +##### [](#repositoryItemReader)`RepositoryItemReader` + +`RepositoryItemReader`是通过使用`PagingAndSortingRepository`读取记录的`ItemReader`。 Spring 批处理提供了一个`RepositoryItemReaderBuilder`来构造`RepositoryItemReader`的实例。 + +#### [](#databaseWriters)数据库编写者 + +Spring Batch 提供以下数据库编写器: + +* [`Neo4jItemWriter`](#NEO4jitemwriter) + +* [`MongoItemWriter`] + +* [`RepositoryItemWriter`] + +* [`HibernateItemWriter`] + +* [`JdbcBatchItemWriter`] + +* [`JpaItemWriter`] + +* [`GemfireItemWriter`] + +##### [](#neo4jItemWriter)`Neo4jItemWriter` + +`Neo4jItemWriter`是一个`ItemWriter`实现,它将写到 NEO4J 数据库。 Spring 批处理提供了一个`Neo4jItemWriterBuilder`来构造`Neo4jItemWriter`的实例。 + +##### [](#mongoItemWriter)`MongoItemWriter` + +`MongoItemWriter`是一个`ItemWriter`实现,它使用 Spring data 的`MongoOperations`的实现将数据写到 MongoDB 存储。 Spring 批处理提供了一个`MongoItemWriterBuilder`来构造`MongoItemWriter`的实例。 + +##### [](#repositoryItemWriter)`RepositoryItemWriter` + +`RepositoryItemWriter`是来自 Spring 数据的`ItemWriter`包装器。 Spring 批处理提供了一个`RepositoryItemWriterBuilder`来构造`RepositoryItemWriter`的实例。 + +##### [](#hibernateItemWriter)`HibernateItemWriter` + +`HibernateItemWriter`是一个`ItemWriter`,它使用一个 Hibernate 会话来保存或更新不是当前 Hibernate 会话的一部分的实体。 Spring 批处理提供了一个`HibernateItemWriterBuilder`来构造`HibernateItemWriter`的实例。 + +##### [](#jdbcBatchItemWriter)`JdbcBatchItemWriter` + +`JdbcBatchItemWriter`是一个`ItemWriter`,它使用`NamedParameterJdbcTemplate`中的批处理特性来为提供的所有项执行一批语句。 Spring 批处理提供了一个`JdbcBatchItemWriterBuilder`来构造`JdbcBatchItemWriter`的实例。 + +##### [](#jpaItemWriter)`JpaItemWriter` + +`JpaItemWriter`是一个`ItemWriter`,它使用 JPA `EntityManagerFactory`来合并不属于持久性上下文的任何实体。 Spring 批处理提供了一个`JpaItemWriterBuilder`来构造`JpaItemWriter`的实例。 + +##### [](#gemfireItemWriter)`GemfireItemWriter` + +`GemfireItemWriter`是一个`ItemWriter`,它使用一个`GemfireTemplate`将项目存储在 Gemfire 中,作为键/值对。 Spring 批处理提供了一个`GemfireItemWriterBuilder`来构造`GemfireItemWriter`的实例。 + +#### [](#specializedReaders)专业阅读器 + +Spring Batch 提供以下专门的阅读器: + +* [`LdifReader`] + +* [`MappingLdifReader`] + +* [`AvroItemReader`] + +##### [](#ldifReader)`LdifReader` + +`AvroItemWriter`读取来自`Resource`的 LDIF(LDAP 数据交换格式)记录,对它们进行解析,并为执行的每个`LdapAttribute`返回一个`LdapAttribute`对象。 Spring 批处理提供了一个`LdifReaderBuilder`来构造`LdifReader`的实例。 + +##### [](#mappingLdifReader)`MappingLdifReader` + +`MappingLdifReader`从`Resource`读取 LDIF(LDAP 数据交换格式)记录,解析它们,然后将每个 LDIF 记录映射到 POJO(普通的旧 Java 对象)。每个读都返回一个 POJO。 Spring 批处理提供了一个`MappingLdifReaderBuilder`来构造`MappingLdifReader`的实例。 + +##### [](#avroItemReader)`AvroItemReader` + +`AvroItemReader`从资源中读取序列化的 AVRO 数据。每个读取返回由 Java 类或 AVRO 模式指定的类型的实例。读取器可以被可选地配置为嵌入 AVRO 模式的输入或不嵌入该模式的输入。 Spring 批处理提供了一个`AvroItemReaderBuilder`来构造`AvroItemReader`的实例。 + +#### [](#specializedWriters)专业作家 + +Spring Batch 提供以下专业的写作人员: + +* [`SimpleMailMessageItemWriter`] + +* [`AvroItemWriter`] + +##### [](#simpleMailMessageItemWriter)`SimpleMailMessageItemWriter` + +`SimpleMailMessageItemWriter`是可以发送邮件的`ItemWriter`。它将消息的实际发送委托给`MailSender`的实例。 Spring 批处理提供了一个`SimpleMailMessageItemWriterBuilder`来构造`SimpleMailMessageItemWriter`的实例。 + +##### [](#avroItemWriter)`AvroItemWriter` + +`AvroItemWrite`根据给定的类型或模式将 Java 对象序列化到一个 WriteableResource。编写器可以被可选地配置为在输出中嵌入或不嵌入 AVRO 模式。 Spring 批处理提供了一个`AvroItemWriterBuilder`来构造`AvroItemWriter`的实例。 + +#### [](#specializedProcessors)专用处理器 + +Spring Batch 提供以下专门的处理器: + +* [`ScriptItemProcessor`] + +##### [](#scriptItemProcessor)`ScriptItemProcessor` + +`ScriptItemProcessor`是一个`ItemProcessor`,它将当前项目传递给提供的脚本,并且该脚本的结果将由处理器返回。 Spring 批处理提供了一个`ScriptItemProcessorBuilder`来构造`ScriptItemProcessor`的实例。 \ No newline at end of file diff --git a/docs/spring-batch/repeat.md b/docs/spring-batch/repeat.md new file mode 100644 index 0000000..02bdb64 --- /dev/null +++ b/docs/spring-batch/repeat.md @@ -0,0 +1,158 @@ +# 重复 + +## [](#repeat)重复 + +XMLJavaBoth + +### [](#repeatTemplate)repeatemplate + +批处理是关于重复的操作,或者作为简单的优化,或者作为工作的一部分。 Spring Batch 具有`RepeatOperations`接口,可以对重复进行策略规划和推广,并提供相当于迭代器框架的内容。`RepeatOperations`接口具有以下定义: + +``` +public interface RepeatOperations { + + RepeatStatus iterate(RepeatCallback callback) throws RepeatException; + +} +``` + +回调是一个接口,如以下定义所示,它允许你插入一些要重复的业务逻辑: + +``` +public interface RepeatCallback { + + RepeatStatus doInIteration(RepeatContext context) throws Exception; + +} +``` + +回调会重复执行,直到实现确定迭代应该结束为止。这些接口中的返回值是一个枚举,可以是`RepeatStatus.CONTINUABLE`或`RepeatStatus.FINISHED`。一个`RepeatStatus`枚举向重复操作的调用者传递有关是否还有更多工作要做的信息。一般来说,`RepeatOperations`的实现应该检查`RepeatStatus`,并将其用作结束迭代的决策的一部分。任何希望向调用者发出信号表示没有更多工作要做的回调都可以返回`RepeatStatus.FINISHED`。 + +`RepeatOperations`最简单的通用实现是`RepeatTemplate`,如下例所示: + +``` +RepeatTemplate template = new RepeatTemplate(); + +template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + +template.iterate(new RepeatCallback() { + + public RepeatStatus doInIteration(RepeatContext context) { + // Do stuff in batch... + return RepeatStatus.CONTINUABLE; + } + +}); +``` + +在前面的示例中,我们返回`RepeatStatus.CONTINUABLE`,以表明还有更多的工作要做。回调还可以返回`RepeatStatus.FINISHED`,向调用者发出信号,表示没有更多的工作要做。一些迭代可以由回调中所做的工作固有的考虑因素来终止。就回调而言,其他方法实际上是无限循环,并且完成决策被委托给外部策略,如前面示例中所示的情况。 + +#### [](#repeatContext)repeatcontext + +`RepeatCallback`的方法参数是`RepeatContext`。许多回调忽略了上下文。但是,如果有必要,它可以作为一个属性包来存储迭代期间的瞬态数据。在`iterate`方法返回后,上下文不再存在。 + +如果正在进行嵌套的迭代,则`RepeatContext`具有父上下文。父上下文有时用于存储需要在对`iterate`的调用之间共享的数据。例如,如果你想计算迭代中某个事件发生的次数,并在随后的调用中记住它,那么就是这种情况。 + +#### [](#repeatStatus)重复状态 + +`RepeatStatus`是 Spring 批处理用来指示处理是否已经完成的枚举。它有两个可能的`RepeatStatus`值,如下表所示: + +| *Value* |*说明*| +|-----------|--------------------------------------| +|CONTINUABLE|还有更多的工作要做。| +| FINISHED |不应再重复。| + +`RepeatStatus`值也可以通过在`RepeatStatus`中使用`and()`方法与逻辑和操作结合。这样做的效果是在可持续的标志上做一个合乎逻辑的操作。换句话说,如果任一状态是`FINISHED`,则结果是`FINISHED`。 + +### [](#completionPolicies)完工政策 + +在`RepeatTemplate`内,`iterate`方法中的循环的终止由`CompletionPolicy`确定,这也是`RepeatContext`的工厂。`RepeatTemplate`负责使用当前策略创建`RepeatContext`,并在迭代的每个阶段将其传递给`RepeatCallback`。回调完成其`doInIteration`后,`RepeatTemplate`必须调用`CompletionPolicy`,以要求它更新其状态(该状态将存储在`RepeatContext`中)。然后,它询问策略迭代是否完成。 + +Spring 批处理提供了`CompletionPolicy`的一些简单的通用实现。`SimpleCompletionPolicy`允许执行多达固定的次数(与`RepeatStatus.FINISHED`一起强制在任何时间提前完成)。 + +对于更复杂的决策,用户可能需要实现自己的完成策略。例如,一旦联机系统投入使用,一个批处理窗口就会阻止批处理作业的执行,这将需要一个自定义策略。 + +### [](#repeatExceptionHandling)异常处理 + +如果在`RepeatCallback`中抛出了异常,则`RepeatTemplate`查询`ExceptionHandler`,该查询可以决定是否重新抛出异常。 + +下面的清单显示了`ExceptionHandler`接口定义: + +``` +public interface ExceptionHandler { + + void handleException(RepeatContext context, Throwable throwable) + throws Throwable; + +} +``` + +一个常见的用例是计算给定类型的异常数量,并在达到限制时失败。为此目的, Spring 批提供了`SimpleLimitExceptionHandler`和稍微更灵活的`RethrowOnThresholdExceptionHandler`。`SimpleLimitExceptionHandler`具有一个极限属性和一个异常类型,应该将其与当前异常进行比较。所提供类型的所有子类也被计算在内。给定类型的异常将被忽略,直到达到限制,然后重新抛出它们。其他类型的异常总是被重新抛出。 + +`SimpleLimitExceptionHandler`的一个重要的可选属性是名为`useParent`的布尔标志。默认情况下它是`false`,因此该限制仅在当前的`RepeatContext`中考虑。当设置为`true`时,该限制在嵌套迭代中跨兄弟上下文(例如步骤中的一组块)保持不变。 + +### [](#repeatListeners)听众 + +通常情况下,能够接收跨多个不同迭代的交叉关注点的额外回调是有用的。为此, Spring Batch 提供了`RepeatListener`接口。`RepeatTemplate`允许用户注册`RepeatListener`实现,并且在迭代期间可用的情况下,他们将获得带有`RepeatContext`和`RepeatStatus`的回调。 + +`RepeatListener`接口具有以下定义: + +``` +public interface RepeatListener { + void before(RepeatContext context); + void after(RepeatContext context, RepeatStatus result); + void open(RepeatContext context); + void onError(RepeatContext context, Throwable e); + void close(RepeatContext context); +} +``` + +`open`和`close`回调出现在整个迭代之前和之后。`before`,`after`,和`onError`应用于单独的`RepeatCallback`调用。 + +请注意,当有多个侦听器时,它们在一个列表中,因此有一个顺序。在这种情况下,`open`和`before`的调用顺序相同,而`after`、`onError`和`close`的调用顺序相反。 + +### [](#repeatParallelProcessing)并行处理 + +`RepeatOperations`的实现不限于按顺序执行回调。一些实现能够并行地执行它们的回调,这一点非常重要。为此, Spring Batch 提供了`TaskExecutorRepeatTemplate`,它使用 Spring `TaskExecutor`策略来运行`RepeatCallback`。默认值是使用`SynchronousTaskExecutor`,其效果是在相同的线程中执行整个迭代(与正常的`RepeatTemplate`相同)。 + +### [](#declarativeIteration)声明式迭代 + +有时,你知道有一些业务处理在每次发生时都想要重复。这方面的经典示例是消息管道的优化。如果一批消息经常到达,那么处理它们比为每条消息承担单独事务的成本更有效。 Spring Batch 提供了一个 AOP 拦截器,该拦截器仅为此目的将方法调用包装在`RepeatOperations`对象中。将`RepeatOperationsInterceptor`执行所截获的方法并根据所提供的`CompletionPolicy`中的`RepeatTemplate`进行重复。 + +下面的示例展示了使用 Spring AOP 命名空间来重复对名为`processMessage`的方法的服务调用的声明性迭代(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring 用户指南): + +``` + + + + + + +``` + +下面的示例演示了如何使用 Java 配置来重复对一个名为`processMessage`的方法的服务调用(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring 用户指南): + +``` +@Bean +public MyService myService() { + ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader()); + factory.setInterfaces(MyService.class); + factory.setTarget(new MyService()); + + MyService service = (MyService) factory.getProxy(); + JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); + pointcut.setPatterns(".*processMessage.*"); + + RepeatOperationsInterceptor interceptor = new RepeatOperationsInterceptor(); + + ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor)); + + return service; +} +``` + +前面的示例在拦截器内部使用默认的`RepeatTemplate`。要更改策略、侦听器和其他详细信息,可以将`RepeatTemplate`的实例注入拦截器。 + +如果截获的方法返回`void`,那么拦截器总是返回`RepeatStatus.CONTINUABLE`(因此,如果`CompletionPolicy`没有有限的端点,则存在无限循环的危险)。否则,它将返回`RepeatStatus.CONTINUABLE`,直到截获的方法的返回值是`null`,此时它将返回`RepeatStatus.FINISHED`。因此,目标方法中的业务逻辑可以通过返回`null`或抛出一个异常来表示没有更多的工作要做,该异常是由提供的`ExceptionHandler`中的`RepeatTemplate`重新抛出的。 diff --git a/docs/spring-batch/retry.md b/docs/spring-batch/retry.md new file mode 100644 index 0000000..1e0c87c --- /dev/null +++ b/docs/spring-batch/retry.md @@ -0,0 +1,225 @@ +# 重试 + +## [](#retry)重试 + +XMLJavaBoth + +为了使处理更健壮,更不容易失败,有时自动重试失败的操作会有所帮助,以防随后的尝试可能会成功。容易发生间歇性故障的错误通常是暂时的。例如,对 Web 服务的远程调用由于网络故障或数据库更新中的`DeadlockLoserDataAccessException`而失败。 + +### [](#retryTemplate)`RetryTemplate` + +| |重试功能在 2.2.0 时从 Spring 批中退出。
它现在是一个新库[Spring Retry](https://github.com/spring-projects/spring-retry)的一部分。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要自动化重试操作 Spring,批处理有`RetryOperations`策略。以下是`RetryOperations`的接口定义: + +``` +public interface RetryOperations { + + T execute(RetryCallback retryCallback) throws E; + + T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) + throws E; + + T execute(RetryCallback retryCallback, RetryState retryState) + throws E, ExhaustedRetryException; + + T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, + RetryState retryState) throws E; + +} +``` + +Basic Callback 是一个简单的接口,允许你插入一些要重试的业务逻辑,如下面的接口定义所示: + +``` +public interface RetryCallback { + + T doWithRetry(RetryContext context) throws E; + +} +``` + +回调会运行,如果它失败(通过抛出`Exception`),则会重试它,直到它成功或实现中止为止。在`RetryOperations`接口中有许多重载的`execute`方法。当所有的重试尝试都用完时,这些方法处理用于恢复的各种用例,并处理重试状态,这使客户机和实现在调用之间存储信息(我们将在本章后面详细介绍这一点)。 + +`RetryOperations`的最简单的通用实现是`RetryTemplate`。其用途如下: + +``` +RetryTemplate template = new RetryTemplate(); + +TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); +policy.setTimeout(30000L); + +template.setRetryPolicy(policy); + +Foo result = template.execute(new RetryCallback() { + + public Foo doWithRetry(RetryContext context) { + // Do stuff that might fail, e.g. webservice operation + return result; + } + +}); +``` + +在前面的示例中,我们进行一个 Web 服务调用,并将结果返回给用户。如果该调用失败,则重试该调用,直到达到超时为止。 + +#### [](#retryContext)`RetryContext` + +`RetryCallback`的方法参数是`RetryContext`。许多回调忽略了上下文,但如果有必要,它可以作为一个属性包来存储迭代期间的数据。 + +如果同一个线程中有一个正在进行的嵌套重试,则`RetryContext`具有父上下文。父上下文有时用于存储需要在对`execute`的调用之间共享的数据。 + +#### [](#recoveryCallback)`RecoveryCallback` + +当重试用完时,`RetryOperations`可以将控制权传递给另一个回调,称为`RecoveryCallback`。要使用此功能,客户机将回调一起传递给相同的方法,如以下示例所示: + +``` +Foo foo = template.execute(new RetryCallback() { + public Foo doWithRetry(RetryContext context) { + // business logic here + }, + new RecoveryCallback() { + Foo recover(RetryContext context) throws Exception { + // recover logic here + } +}); +``` + +如果业务逻辑在模板决定中止之前没有成功,那么客户机将有机会通过恢复回调执行一些替代处理。 + +#### [](#statelessRetry)无状态重试 + +在最简单的情况下,重试只是一个 while 循环。`RetryTemplate`可以一直尝试,直到成功或失败为止。`RetryContext`包含一些状态来决定是重试还是中止,但是这个状态在堆栈上,不需要在全局的任何地方存储它,所以我们将其称为无状态重试。无状态重试和有状态重试之间的区别包含在`RetryPolicy`的实现中(`RetryTemplate`可以同时处理这两个)。在无状态的重试中,重试回调总是在它失败时所在的线程中执行。 + +#### [](#statefulRetry)有状态重试 + +在故障导致事务资源无效的情况下,有一些特殊的考虑因素。这不适用于简单的远程调用,因为(通常)没有事务性资源,但有时确实适用于数据库更新,尤其是在使用 Hibernate 时。在这种情况下,只有立即重新抛出调用故障的异常才有意义,这样事务就可以回滚,并且我们可以启动一个新的有效事务。 + +在涉及事务的情况下,无状态重试还不够好,因为重新抛出和回滚必然涉及离开`RetryOperations.execute()`方法,并且可能会丢失堆栈上的上下文。为了避免丢失它,我们必须引入一种存储策略,将其从堆栈中取出,并将其(至少)放在堆存储中。为此, Spring 批提供了一种名为`RetryContextCache`的存储策略,它可以被注入到`RetryTemplate`中。`RetryContextCache`的默认实现是在内存中,使用一个简单的`Map`。在集群环境中使用多个进程的高级用法还可以考虑使用某种类型的集群缓存来实现`RetryContextCache`(但是,即使在集群环境中,这也可能是过度使用)。 + +`RetryOperations`的部分职责是识别在新执行中(并且通常包装在新事务中)返回的失败操作。为了促进这一点, Spring Batch 提供了`RetryState`抽象。这与`RetryOperations`接口中的特殊`execute`方法一起工作。 + +识别失败操作的方法是在重试的多个调用之间识别状态。为了识别状态,用户可以提供一个`RetryState`对象,该对象负责返回标识该项的唯一密钥。标识符在`RetryContextCache`接口中用作键。 + +| |在
中实现`Object.equals()`和`Object.hashCode()`时要非常小心。最好的建议是使用业务键来标识
项。在 JMS 消息的情况下,可以使用消息 ID。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当重试用完时,还可以选择以不同的方式处理失败的项,而不是调用`RetryCallback`(现在认为很可能会失败)。就像在无状态的情况下一样,这个选项是由`RecoveryCallback`提供的,可以通过将其传递到`execute`的`RetryOperations`方法来提供。 + +是否重试的决定实际上被委托给一个常规的`RetryPolicy`,因此通常对限制和超时的关注可以被注入到那里(在本章后面描述)。 + +### [](#retryPolicies)重试策略 + +在`RetryTemplate`中,在`execute`方法中重试或失败的决定由`RetryPolicy`决定,这也是`RetryContext`的工厂。`RetryTemplate`负责使用当前策略创建`RetryContext`,并在每次尝试时将其传递给`RetryCallback`。回调失败后,`RetryTemplate`必须调用`RetryPolicy`,要求它更新其状态(存储在`RetryContext`中),然后询问策略是否可以进行另一次尝试。如果无法进行另一次尝试(例如,当达到限制或检测到超时时时),则策略还负责处理耗尽状态。简单的实现方式会抛出`RetryExhaustedException`,这会导致任何封闭事务被回滚。更复杂的实现可能会尝试采取一些恢复操作,在这种情况下,事务可以保持不变。 + +| |失败本质上要么是可重复的,要么是不可重复的。如果从业务逻辑中总是抛出相同的异常
,则重试没有好处。因此,不要重试所有
异常类型。相反,尝试只关注那些你期望
可重新尝试的异常。更积极地重试通常不会对业务逻辑造成损害,但是
这是浪费的,因为如果失败是确定性的,那么你将花费时间重试一些你事先知道是致命的
。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 批处理提供了无状态`RetryPolicy`的一些简单的通用实现,例如`SimpleRetryPolicy`和`TimeoutRetryPolicy`(在前面的示例中使用)。 + +`SimpleRetryPolicy`允许对任何已命名的异常类型列表进行重试,重试次数最多为固定次数。它还具有一个“致命”异常列表,这些异常永远不应该被重试,并且这个列表覆盖了可重试列表,以便可以使用它对重试行为进行更好的控制,如下面的示例所示: + +``` +SimpleRetryPolicy policy = new SimpleRetryPolicy(); +// Set the max retry attempts +policy.setMaxAttempts(5); +// Retry on all exceptions (this is the default) +policy.setRetryableExceptions(new Class[] {Exception.class}); +// ... but never retry IllegalStateException +policy.setFatalExceptions(new Class[] {IllegalStateException.class}); + +// Use the policy... +RetryTemplate template = new RetryTemplate(); +template.setRetryPolicy(policy); +template.execute(new RetryCallback() { + public Foo doWithRetry(RetryContext context) { + // business logic here + } +}); +``` + +还有一个更灵活的实现叫做`ExceptionClassifierRetryPolicy`,它允许用户通过`ExceptionClassifier`抽象为任意一组异常类型配置不同的重试行为。该策略的工作原理是调用分类器将异常转换为委托`RetryPolicy`。例如,通过将一种异常类型映射到另一种策略,可以在失败前重试更多次。 + +用户可能需要实现他们自己的重试策略,以做出更多定制的决策。例如,当存在已知的、特定于解决方案的异常的可重试和不可重试的分类时,自定义重试策略是有意义的。 + +### [](#backoffPolicies)退避政策 + +当在短暂的失败之后重试时,在再次尝试之前等待一下通常会有所帮助,因为通常故障是由某些只能通过等待解决的问题引起的。如果`RetryCallback`失败,`RetryTemplate`可以根据`BackoffPolicy`暂停执行。 + +下面的代码显示了`BackOffPolicy`接口的接口定义: + +``` +public interface BackoffPolicy { + + BackOffContext start(RetryContext context); + + void backOff(BackOffContext backOffContext) + throws BackOffInterruptedException; + +} +``` + +a`BackoffPolicy`可以自由地以它选择的任何方式实现退避。 Spring Batch Out of the Box 提供的策略都使用。一个常见的用例是后退,等待时间呈指数增长,以避免两次重试进入锁定步骤,两次都失败(这是从以太网学到的经验教训)。为此, Spring batch 提供了`ExponentialBackoffPolicy`。 + +### [](#retryListeners)听众 + +通常情况下,能够接收跨多个不同重试中的交叉关注点的额外回调是有用的。为此, Spring Batch 提供了`RetryListener`接口。`RetryTemplate`允许用户注册`RetryListeners`,并且在迭代期间可用的情况下,给出带有`RetryContext`和`Throwable`的回调。 + +下面的代码显示了`RetryListener`的接口定义: + +``` +public interface RetryListener { + + boolean open(RetryContext context, RetryCallback callback); + + void onError(RetryContext context, RetryCallback callback, Throwable throwable); + + void close(RetryContext context, RetryCallback callback, Throwable throwable); +} +``` + +在最简单的情况下,`open`和`close`回调出现在整个重试之前和之后,并且`onError`应用于单个`RetryCallback`调用。`close`方法也可能接收`Throwable`。如果出现错误,则是`RetryCallback`抛出的最后一个错误。 + +请注意,当有多个侦听器时,它们在一个列表中,因此有一个顺序。在这种情况下,以相同的顺序调用`open`,而以相反的顺序调用`onError`和`close`。 + +### [](#declarativeRetry)声明式重试 + +有时,你知道有些业务处理在每次发生时都想要重试。这方面的典型例子是远程服务调用。 Spring Batch 提供了 AOP 拦截器,该拦截器仅为此目的在`RetryOperations`实现中包装方法调用。根据提供的`RepeatTemplate`中的`RetryPolicy`,`RetryOperationsInterceptor`执行截获的方法并在失败时重试。 + +下面的示例显示了一个声明性重试,它使用 Spring AOP 命名空间重试对一个名为`remoteCall`的方法的服务调用(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring 用户指南): + +``` + + + + + + +``` + +下面的示例显示了一个声明性重试,它使用 Java 配置重试对一个名为`remoteCall`的方法的服务调用(有关如何配置 AOP 拦截器的更多详细信息,请参见 Spring 用户指南): + +``` +@Bean +public MyService myService() { + ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader()); + factory.setInterfaces(MyService.class); + factory.setTarget(new MyService()); + + MyService service = (MyService) factory.getProxy(); + JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); + pointcut.setPatterns(".*remoteCall.*"); + + RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor(); + + ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor)); + + return service; +} +``` + +前面的示例在拦截器内部使用默认的`RetryTemplate`。要更改策略或侦听器,可以将`RetryTemplate`的实例注入拦截器。 \ No newline at end of file diff --git a/docs/spring-batch/scalability.md b/docs/spring-batch/scalability.md new file mode 100644 index 0000000..7c5a8a9 --- /dev/null +++ b/docs/spring-batch/scalability.md @@ -0,0 +1,334 @@ +# 缩放和并行处理 + +## [](#scalability)缩放和并行处理 + +XMLJavaBoth + +许多批处理问题可以通过单线程、单流程作业来解决,因此在考虑更复杂的实现之前,正确地检查它是否满足你的需求始终是一个好主意。衡量一项实际工作的性能,看看最简单的实现是否首先满足你的需求。即使使用标准的硬件,你也可以在一分钟内读写几百兆的文件。 + +Spring 当你准备好开始用一些并行处理来实现一个作业时, Spring Batch 提供了一系列选项,这些选项在本章中进行了描述,尽管其他地方也介绍了一些特性。在高层次上,有两种并行处理模式: + +* 单过程、多线程 + +* 多进程 + +这些指标也可分为以下几类: + +* 多线程步骤(单进程) + +* 并行步骤(单一过程) + +* 步骤的远程分块(多进程) + +* 划分一个步骤(单个或多个进程) + +首先,我们回顾一下单流程选项。然后,我们回顾了多进程的选择。 + +### [](#multithreadedStep)多线程步骤 + +启动并行处理的最简单方法是在步骤配置中添加`TaskExecutor`。 + +例如,你可以添加`tasklet`的一个属性,如下所示: + +``` + + ... + +``` + +当使用 Java 配置时,可以将`TaskExecutor`添加到该步骤中,如以下示例所示: + +Java 配置 + +``` +@Bean +public TaskExecutor taskExecutor() { + return new SimpleAsyncTaskExecutor("spring_batch"); +} + +@Bean +public Step sampleStep(TaskExecutor taskExecutor) { + return this.stepBuilderFactory.get("sampleStep") + .chunk(10) + .reader(itemReader()) + .writer(itemWriter()) + .taskExecutor(taskExecutor) + .build(); +} +``` + +在此示例中,`taskExecutor`是对另一个 Bean 定义的引用,该定义实现了`TaskExecutor`接口。[`TaskExecutor`](https://DOCS. Spring.io/ Spring/DOCS/current/javadoc-api/org/springframework/core/core/task/taskexecutor.html)是一个标准的 Spring 接口,因此请参阅 Spring 用户指南以获得可用实现的详细信息。最简单的多线程`TaskExecutor`是`SimpleAsyncTaskExecutor`。 + +上述配置的结果是,`Step`通过在单独的执行线程中读取、处理和写入每个项块(每个提交间隔)来执行。请注意,这意味着要处理的项没有固定的顺序,并且块可能包含与单线程情况相比非连续的项。除了任务执行器设置的任何限制(例如它是否由线程池支持)之外,Tasklet 配置中还有一个油门限制,默认为 4。你可能需要增加这一点,以确保线程池得到充分利用。 + +例如,你可能会增加油门限制,如以下示例所示: + +``` + ... + +``` + +在使用 Java 配置时,构建器提供对油门限制的访问,如以下示例所示: + +Java 配置 + +``` +@Bean +public Step sampleStep(TaskExecutor taskExecutor) { + return this.stepBuilderFactory.get("sampleStep") + .chunk(10) + .reader(itemReader()) + .writer(itemWriter()) + .taskExecutor(taskExecutor) + .throttleLimit(20) + .build(); +} +``` + +还请注意,在你的步骤中使用的任何池资源都可能对并发性施加限制,例如`DataSource`。确保这些资源中的池至少与步骤中所需的并发线程数量一样大。 + +对于一些常见的批处理用例,使用多线程`Step`实现有一些实际的限制。`Step`中的许多参与者(例如读者和作者)是有状态的。如果状态不是由线程隔离的,那么这些组件在多线程`Step`中是不可用的。特别是, Spring 批中的大多数现成的读取器和编写器都不是为多线程使用而设计的。然而,可以使用无状态的或线程安全的读取器和编写器,并且在[Spring Batch Samples](https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples)中有一个示例(称为`parallelJob`),该示例显示了使用过程指示器(参见[防止状态持久性](readersAndWriters.html#process-indicator))来跟踪在数据库输入表中已处理的项。 + +Spring 批处理提供了`ItemWriter`和`ItemReader`的一些实现方式。通常,他们会在 Javadoc 中说明它们是否是线程安全的,或者你必须做什么来避免在并发环境中出现问题。如果 Javadoc 中没有信息,则可以检查实现,以查看是否存在任何状态。如果阅读器不是线程安全的,那么你可以使用提供的`SynchronizedItemStreamReader`来装饰它,或者在你自己的同步委托程序中使用它。你可以将调用同步到`read()`,并且只要处理和写入是块中最昂贵的部分,你的步骤仍然可以比在单线程配置中快得多地完成。 + +### [](#scalabilityParallelSteps)平行步骤 + +只要需要并行化的应用程序逻辑可以划分为不同的职责,并分配给各个步骤,那么就可以在单个流程中进行并行化。并行步骤执行很容易配置和使用。 + +例如,与`step3`并行执行`(step1,step2)`的步骤是直接的,如以下示例所示: + +``` + + + + + + + + + + + + + + +``` + +当使用 Java 配置时,与`(step1,step2)`并行执行步骤`step3`是很简单的,如以下示例所示: + +Java 配置 + +``` +@Bean +public Job job() { + return jobBuilderFactory.get("job") + .start(splitFlow()) + .next(step4()) + .build() //builds FlowJobBuilder instance + .build(); //builds Job instance +} + +@Bean +public Flow splitFlow() { + return new FlowBuilder("splitFlow") + .split(taskExecutor()) + .add(flow1(), flow2()) + .build(); +} + +@Bean +public Flow flow1() { + return new FlowBuilder("flow1") + .start(step1()) + .next(step2()) + .build(); +} + +@Bean +public Flow flow2() { + return new FlowBuilder("flow2") + .start(step3()) + .build(); +} + +@Bean +public TaskExecutor taskExecutor() { + return new SimpleAsyncTaskExecutor("spring_batch"); +} +``` + +可配置任务执行器用于指定应该使用哪个`TaskExecutor`实现来执行各个流。默认值是`SyncTaskExecutor`,但是需要一个异步`TaskExecutor`来并行运行这些步骤。请注意,该作业确保在聚合退出状态和转换之前,拆分中的每个流都已完成。 + +有关更多详细信息,请参见[拆分流](step.html#split-flows)一节。 + +### [](#remoteChunking)远程分块 + +在远程分块中,`Step`处理被分割到多个进程中,通过一些中间件相互通信。下图显示了该模式: + +![远程分块](./images/remote-chunking.png) + +图 1。远程分块 + +Manager 组件是一个单独的进程,工作人员是多个远程进程。如果 Manager 不是瓶颈,那么这种模式最有效,因此处理必须比读取项目更昂贵(在实践中通常是这种情况)。 + +Manager 是 Spring 批处理`Step`的实现,其中`ItemWriter`被一个通用版本代替,该版本知道如何将项目块作为消息发送到中间件。工人是正在使用的任何中间件的标准侦听器(例如,对于 JMS,他们将是`MessageListener`实现),他们的角色是通过`ItemWriter`或`ItemProcessor`加上`ItemWriter`接口使用标准的项块。使用这种模式的优点之一是读写器、处理器和写写器组件是现成的(与用于步骤的本地执行的组件相同)。这些项是动态划分的,工作是通过中间件共享的,因此,如果侦听器都是热心的消费者,那么负载平衡就是自动的。 + +中间件必须是持久的,保证交付,并且每条消息只有一个使用者。JMS 是显而易见的候选者,但在网格计算和共享内存产品空间中存在其他选项(例如 JavaSpace)。 + +有关更多详细信息,请参见[Spring Batch Integration - Remote Chunking](spring-batch-integration.html#remote-chunking)一节。 + +### [](#partitioning)分区 + +Spring 批处理还提供了用于分区`Step`执行并远程执行它的 SPI。在这种情况下,远程参与者是`Step`实例,这些实例可以很容易地被配置并用于本地处理。下图显示了该模式: + +![分区概述](./images/partitioning-overview.png) + +图 2。划分 + +`Job`作为`Step`实例的序列在左侧运行,其中一个`Step`实例被标记为管理器。这张图中的工人都是`Step`的相同实例,它实际上可以代替经理,从而导致`Job`的结果相同。工作人员通常是远程服务,但也可能是执行的本地线程。在此模式中,经理发送给工作人员的消息不需要是持久的,也不需要有保证的交付。 Spring `JobRepository`中的批处理元数据确保每个工作者执行一次,并且对于每个`Job`执行只执行一次。 + +Spring 批处理中的 SPI 由`Step`(称为`PartitionStep`)的特殊实现和需要为特定环境实现的两个策略接口组成。策略接口是`PartitionHandler`和`StepExecutionSplitter`,它们的作用在下面的序列图中显示: + +![分区 SPI](./images/partitioning-spi.png) + +图 3。分区 SPI + +在这种情况下,右边的`Step`是“远程”工作者,因此,潜在地,有许多对象和或进程在扮演这个角色,并且`PartitionStep`被显示为驱动执行。 + +下面的示例显示了使用 XML 配置时的`PartitionStep`配置: + +``` + + + + + +``` + +下面的示例显示了使用 Java 配置时的`PartitionStep`配置: + +Java 配置 + +``` +@Bean +public Step step1Manager() { + return stepBuilderFactory.get("step1.manager") + .partitioner("step1", partitioner()) + .step(step1()) + .gridSize(10) + .taskExecutor(taskExecutor()) + .build(); +} +``` + +与多线程步骤的`throttle-limit`属性类似,`grid-size`属性防止任务执行器被来自单个步骤的请求饱和。 + +有一个简单的示例,可以在[Spring Batch Samples](https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples/src/main/resources/jobs)的单元测试套件中进行复制和扩展(参见`partition*Job.xml`配置)。 + +Spring 批处理为被称为“Step1:Partition0”的分区创建步骤执行,以此类推。为了保持一致性,许多人更喜欢将 Manager 步骤称为“Step1:Manager”。你可以为步骤使用别名(通过指定`name`属性而不是`id`属性)。 + +#### [](#partitionHandler)分区处理程序 + +`PartitionHandler`是了解远程或网格环境结构的组件。它能够将`StepExecution`请求发送到远程`Step`实例,并以某种特定于织物的格式包装,例如 DTO。它不需要知道如何分割输入数据或如何聚合多个`Step`执行的结果。一般来说,它可能也不需要了解弹性或故障转移,因为在许多情况下,这些都是织物的功能。在任何情况下, Spring 批处理总是提供独立于织物的重启性。失败的`Job`总是可以重新启动,并且只重新执行失败的`Steps`。 + +`PartitionHandler`接口可以为各种结构类型提供专门的实现,包括简单的 RMI 远程处理、EJB 远程处理、自定义 Web 服务、JMS、Java 空间、共享内存网格(如 Terracotta 或 Coherence)和网格执行结构(如 GridGain)。 Spring 批处理不包含用于任何专有网格或远程织物的实现方式。 + +Spring 然而,批处理确实提供了`PartitionHandler`的一种有用的实现,该实现使用 Spring 中的`TaskExecutor`策略,在单独的执行线程中本地执行`Step`实例。该实现被称为`TaskExecutorPartitionHandler`。 + +`TaskExecutorPartitionHandler`是使用前面显示的 XML 名称空间进行配置的步骤的默认值。也可以显式地对其进行配置,如以下示例所示: + +``` + + + + + + + + + +``` + +`TaskExecutorPartitionHandler`可以在 Java 配置中显式地进行配置,如以下示例所示: + +Java 配置 + +``` +@Bean +public Step step1Manager() { + return stepBuilderFactory.get("step1.manager") + .partitioner("step1", partitioner()) + .partitionHandler(partitionHandler()) + .build(); +} + +@Bean +public PartitionHandler partitionHandler() { + TaskExecutorPartitionHandler retVal = new TaskExecutorPartitionHandler(); + retVal.setTaskExecutor(taskExecutor()); + retVal.setStep(step1()); + retVal.setGridSize(10); + return retVal; +} +``` + +`gridSize`属性决定要创建的独立步骤执行的数量,因此它可以与`TaskExecutor`中线程池的大小匹配。或者,可以将其设置为比可用的线程数量更大,这使得工作块更小。 + +`TaskExecutorPartitionHandler`对于 IO 密集型`Step`实例很有用,例如复制大量文件或将文件系统复制到内容管理系统中。它还可以通过提供`Step`实现来用于远程执行,该实现是远程调用的代理(例如使用 Spring remoting)。 + +#### [](#partitioner)分割者 + +`Partitioner`有一个更简单的职责:仅为新的步骤执行生成执行上下文作为输入参数(无需担心重新启动)。它只有一个方法,如下面的接口定义所示: + +``` +public interface Partitioner { + Map partition(int gridSize); +} +``` + +这个方法的返回值将每个步骤执行的唯一名称(`String`)与输入参数(`ExecutionContext`)以`ExecutionContext`的形式关联起来。这些名称稍后会在批处理元数据中显示为分区`StepExecutions`中的步骤名称。`ExecutionContext`只是一组名称-值对,因此它可能包含一系列主键、行号或输入文件的位置。然后,远程`Step`通常使用`#{…​}`占位符(在步骤作用域中的后期绑定)绑定到上下文输入,如下一节所示。 + +步骤执行的名称(由`Partitioner`返回的`Map`中的键)需要在`Job`的步骤执行中是唯一的,但没有任何其他特定的要求。要做到这一点(并使名称对用户有意义),最简单的方法是使用前缀 + 后缀命名约定,其中前缀是正在执行的步骤的名称(它本身在`Job`中是唯一的),后缀只是一个计数器。在使用该约定的框架中有一个`SimplePartitioner`。 + +可以使用一个名为`PartitionNameProvider`的可选接口来提供与分区本身分开的分区名称。如果`Partitioner`实现了这个接口,那么在重新启动时,只会查询名称。如果分区是昂贵的,这可以是一个有用的优化。由`PartitionNameProvider`提供的名称必须与`Partitioner`提供的名称匹配。 + +#### [](#bindingInputDataToSteps)将输入数据绑定到步骤 + +由`PartitionHandler`执行的步骤具有相同的配置,并且它们的输入参数在运行时从`ExecutionContext`绑定,这是非常有效的。 Spring 批处理的 StepScope 特性很容易做到这一点(在[后期绑定](step.html#late-binding)一节中更详细地介绍)。例如,如果`Partitioner`使用一个名为`fileName`的属性键创建`ExecutionContext`实例,并针对每个步骤调用指向不同的文件(或目录),则`Partitioner`输出可能类似于下表的内容: + +|*步骤执行名称(键)*|*ExecutionContext (value)*| +|---------------------------|--------------------------| +|filecopy:分区 0| fileName=/home/data/one | +|filecopy:partition1| fileName=/home/data/two | +|filecopy:partition2|fileName=/home/data/three | + +然后,可以使用与执行上下文的后期绑定将文件名绑定到一个步骤。 + +下面的示例展示了如何在 XML 中定义后期绑定: + +XML 配置 + +``` + + + +``` + +下面的示例展示了如何在 Java 中定义后期绑定: + +Java 配置 + +``` +@Bean +public MultiResourceItemReader itemReader( + @Value("#{stepExecutionContext['fileName']}/*") Resource [] resources) { + return new MultiResourceItemReaderBuilder() + .delegate(fileReader()) + .name("itemReader") + .resources(resources) + .build(); +} +``` \ No newline at end of file diff --git a/docs/spring-batch/schema-appendix.md b/docs/spring-batch/schema-appendix.md new file mode 100644 index 0000000..6beae67 --- /dev/null +++ b/docs/spring-batch/schema-appendix.md @@ -0,0 +1,292 @@ +# 元数据模式 + +## [](#metaDataSchema)附录 A:元数据模式 + +### [](#metaDataSchemaOverview)概述 + +Spring 批处理元数据表与在 Java 中表示它们的域对象非常匹配。例如,`JobInstance`,`JobExecution`,`JobParameters`,和`StepExecution`分别映射到`BATCH_JOB_INSTANCE`,`BATCH_JOB_EXECUTION`,`BATCH_JOB_EXECUTION_PARAMS`和`BATCH_STEP_EXECUTION`。`ExecutionContext`映射到`BATCH_JOB_EXECUTION_CONTEXT`和`BATCH_STEP_EXECUTION_CONTEXT`。`JobRepository`负责将每个 Java 对象保存并存储到其正确的表中。本附录详细描述了元数据表,以及在创建元数据表时做出的许多设计决策。在查看下面的各种表创建语句时,重要的是要认识到所使用的数据类型是尽可能通用的。 Spring Batch 提供了许多模式作为示例,所有这些模式都具有不同的数据类型,这是由于各个数据库供应商处理数据类型的方式有所不同。下图显示了所有 6 个表及其相互关系的 ERD 模型: + +![Spring Batch Meta-Data ERD](./images/meta-data-erd.png) + +图 1。 Spring 批处理元数据 ERD + +#### [](#exampleDDLScripts)示例 DDL 脚本 + +Spring 批处理核心 JAR 文件包含用于为许多数据库平台创建关系表的示例脚本(反过来,这些平台由作业存储库工厂 Bean 或等效的名称空间自动检测)。这些脚本可以按原样使用,也可以根据需要修改附加的索引和约束。文件名的形式为`schema-*.sql`,其中“\*”是目标数据库平台的简称。脚本在包`org.springframework.batch.core`中。 + +#### [](#migrationDDLScripts)迁移 DDL 脚本 + +Spring Batch 提供了在升级版本时需要执行的迁移 DDL 脚本。这些脚本可以在`org/springframework/batch/core/migration`下的核心 JAR 文件中找到。迁移脚本被组织到与版本号对应的文件夹中,这些版本号被引入: + +* `2.2`:如果你从`2.2`之前的版本迁移到`2.2`版本,则包含所需的脚本 + +* `4.1`:如果你从`4.1`之前的版本迁移到`4.1`版本,则包含所需的脚本 + +#### [](#metaDataVersion)版本 + +本附录中讨论的许多数据库表都包含一个版本列。这一列很重要,因为 Spring 批处理在处理数据库更新时采用了乐观的锁定策略。这意味着每次“触摸”(更新)记录时,Version 列中的值都会增加一个。当存储库返回以保存该值时,如果版本号发生了更改,它将抛出一个`OptimisticLockingFailureException`,表示在并发访问中出现了错误。这种检查是必要的,因为即使不同的批处理作业可能在不同的机器中运行,它们都使用相同的数据库表。 + +#### [](#metaDataIdentity)恒等式 + +`BATCH_JOB_INSTANCE`、`BATCH_JOB_EXECUTION`和`BATCH_STEP_EXECUTION`都包含以`_ID`结尾的列。这些字段充当各自表的主键。然而,它们不是数据库生成的密钥。相反,它们是由单独的序列生成的。这是必要的,因为在将一个域对象插入到数据库中之后,需要在实际对象上设置给定的键,以便在 Java 中对它们进行唯一标识。较新的数据库驱动程序(JDBC3.0 及以上版本)通过数据库生成的键支持此功能。然而,使用的是序列,而不是要求该功能。模式的每个变体都包含以下语句的某种形式: + +``` +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; +``` + +许多数据库供应商不支持序列。在这些情况下,使用了变通方法,例如 MySQL 的以下语句: + +``` +CREATE TABLE BATCH_STEP_EXECUTION_SEQ (ID BIGINT NOT NULL) type=InnoDB; +INSERT INTO BATCH_STEP_EXECUTION_SEQ values(0); +CREATE TABLE BATCH_JOB_EXECUTION_SEQ (ID BIGINT NOT NULL) type=InnoDB; +INSERT INTO BATCH_JOB_EXECUTION_SEQ values(0); +CREATE TABLE BATCH_JOB_SEQ (ID BIGINT NOT NULL) type=InnoDB; +INSERT INTO BATCH_JOB_SEQ values(0); +``` + +在前一种情况下,用一个表来代替每个序列。 Spring 核心类`MySQLMaxValueIncrementer`然后在这个序列中增加一列,以便提供类似的功能。 + +### [](#metaDataBatchJobInstance)`BATCH_JOB_INSTANCE` + +`BATCH_JOB_INSTANCE`表保存了与`JobInstance`相关的所有信息,并作为整个层次结构的顶部。下面的通用 DDL 语句用于创建它: + +``` +CREATE TABLE BATCH_JOB_INSTANCE ( + JOB_INSTANCE_ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_KEY VARCHAR(2500) +); +``` + +下面的列表描述了表中的每个列: + +* `JOB_INSTANCE_ID`:标识实例的唯一 ID。这也是主要的关键。可以通过调用`JobInstance`上的`getId`方法获得此列的值。 + +* `VERSION`:见[Version](#metaDataVersion)。 + +* `JOB_NAME`:从`Job`对象获得的作业的名称。因为需要它来标识实例,所以它不能是空的。 + +* `JOB_KEY`:`JobParameters`的序列化,该序列化唯一地标识同一作业的不同实例。(具有相同工作名称的`JobInstances`必须有不同的`JobParameters`,因此,不同的`JOB_KEY`值)。 + +### [](#metaDataBatchJobParams)`BATCH_JOB_EXECUTION_PARAMS` + +`BATCH_JOB_EXECUTION_PARAMS`表包含与`JobParameters`对象相关的所有信息。它包含传递给`Job`的 0 个或更多个键/值对,并用作运行作业的参数的记录。对于每个有助于生成作业标识的参数,`IDENTIFYING`标志被设置为 true。请注意,该表已被非规范化。不是为每个类型创建一个单独的表,而是有一个表,其中有一列指示类型,如下面的清单所示: + +``` +CREATE TABLE BATCH_JOB_EXECUTION_PARAMS ( + JOB_EXECUTION_ID BIGINT NOT NULL , + TYPE_CD VARCHAR(6) NOT NULL , + KEY_NAME VARCHAR(100) NOT NULL , + STRING_VAL VARCHAR(250) , + DATE_VAL DATETIME DEFAULT NULL , + LONG_VAL BIGINT , + DOUBLE_VAL DOUBLE PRECISION , + IDENTIFYING CHAR(1) NOT NULL , + constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +); +``` + +下面的列表描述了每一列: + +* `JOB_EXECUTION_ID`:来自`BATCH_JOB_EXECUTION`表的外键,该外键指示参数项所属的作业执行。请注意,每个执行都可能存在多个行(即键/值对)。 + +* type\_cd:存储的值的类型的字符串表示形式,可以是字符串、日期、长值或双值。因为类型必须是已知的,所以它不能是空的。 + +* key\_name:参数键。 + +* string\_val:参数值,如果类型是 string。 + +* date\_val:参数值,如果类型是 date。 + +* long\_val:参数值,如果类型是 long。 + +* double\_val:参数值,如果类型是 double。 + +* 标识:标志,指示参数是否有助于相关的`JobInstance`的标识。 + +请注意,此表没有主键。这是因为该框架不需要一个框架,因此不需要它。如果需要,可以添加主键,也可以添加与数据库生成的键,而不会对框架本身造成任何问题。 + +### [](#metaDataBatchJobExecution)`BATCH_JOB_EXECUTION` + +`BATCH_JOB_EXECUTION`表包含与`JobExecution`对象相关的所有信息。每次运行`Job`时,总会有一个新的`JobExecution`,并在此表中有一个新的行。下面的清单显示了`BATCH_JOB_EXECUTION`表的定义: + +``` +CREATE TABLE BATCH_JOB_EXECUTION ( + JOB_EXECUTION_ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_INSTANCE_ID BIGINT NOT NULL, + CREATE_TIME TIMESTAMP NOT NULL, + START_TIME TIMESTAMP DEFAULT NULL, + END_TIME TIMESTAMP DEFAULT NULL, + STATUS VARCHAR(10), + EXIT_CODE VARCHAR(20), + EXIT_MESSAGE VARCHAR(2500), + LAST_UPDATED TIMESTAMP, + JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL, + constraint JOB_INSTANCE_EXECUTION_FK foreign key (JOB_INSTANCE_ID) + references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID) +) ; +``` + +下面的列表描述了每一列: + +* `JOB_EXECUTION_ID`:唯一标识此执行的主键。通过调用`JobExecution`对象的`getId`方法,可以获得此列的值。 + +* `VERSION`:见[Version](#metaDataVersion)。 + +* `JOB_INSTANCE_ID`:来自`BATCH_JOB_INSTANCE`表的外键。它指示此执行所属的实例。每个实例可能有多个执行。 + +* `CREATE_TIME`:表示创建执行时间的时间戳。 + +* `START_TIME`:表示开始执行的时间的时间戳。 + +* `END_TIME`:时间戳表示执行完成的时间,无论成功还是失败。当作业当前未运行时,此列中的空值表示发生了某种类型的错误,框架在失败之前无法执行最后一次保存。 + +* `STATUS`:表示执行状态的字符串。这可能是`COMPLETED`,`STARTED`等。此列的对象表示是`BatchStatus`枚举。 + +* `EXIT_CODE`:表示执行的退出代码的字符串。在命令行作业的情况下,可以将其转换为数字。 + +* `EXIT_MESSAGE`:表示作业如何退出的更详细描述的字符串。在失败的情况下,这可能包括尽可能多的堆栈跟踪。 + +* `LAST_UPDATED`:时间戳表示此执行最后一次被持久化的时间。 + +### [](#metaDataBatchStepExecution)`BATCH_STEP_EXECUTION` + +批处理 \_step\_execution 表保存与`StepExecution`对象相关的所有信息。该表在许多方面与`BATCH_JOB_EXECUTION`表类似,并且对于每个创建的`JobExecution`,每个`Step`总是至少有一个条目。下面的清单显示了`BATCH_STEP_EXECUTION`表的定义: + +``` +CREATE TABLE BATCH_STEP_EXECUTION ( + STEP_EXECUTION_ID BIGINT PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP DEFAULT NULL, + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT , + READ_COUNT BIGINT , + FILTER_COUNT BIGINT , + WRITE_COUNT BIGINT , + READ_SKIP_COUNT BIGINT , + WRITE_SKIP_COUNT BIGINT , + PROCESS_SKIP_COUNT BIGINT , + ROLLBACK_COUNT BIGINT , + EXIT_CODE VARCHAR(20) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED TIMESTAMP, + constraint JOB_EXECUTION_STEP_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; +``` + +下面的列表描述了每个列: + +* `STEP_EXECUTION_ID`:唯一标识此执行的主键。可以通过调用`StepExecution`对象的`getId`方法获得此列的值。 + +* `VERSION`:见[Version](#metaDataVersion)。 + +* `STEP_NAME`:此执行所属的步骤的名称。 + +* `JOB_EXECUTION_ID`:来自`BATCH_JOB_EXECUTION`表的外键。它指示了`JobExecution`这个`StepExecution`所属的`JobExecution`。对于给定的`JobExecution`名称,可能只有一个`StepExecution`。 + +* `START_TIME`:表示开始执行的时间的时间戳。 + +* `END_TIME`:时间戳表示执行完成的时间,无论成功还是失败。此列中的空值(即使作业当前未运行)表示存在某种类型的错误,并且框架在失败之前无法执行最后一次保存。 + +* `STATUS`:表示执行状态的字符串。这可能是`COMPLETED`,`STARTED`等。此列的对象表示是`BatchStatus`枚举。 + +* `COMMIT_COUNT`:在执行过程中,步骤提交事务的次数。 + +* `READ_COUNT`:执行过程中读取的项数。 + +* `FILTER_COUNT`:从该执行中筛选出的项的数量。 + +* `WRITE_COUNT`:在执行过程中写入和提交的项的数量。 + +* `READ_SKIP_COUNT`:在执行过程中读时跳过的项的数量。 + +* `WRITE_SKIP_COUNT`:在执行过程中在写入时跳过的项数。 + +* `PROCESS_SKIP_COUNT`:在此执行过程中处理过程中跳过的项的数量。 + +* `ROLLBACK_COUNT`:执行过程中的回滚次数。请注意,此计数包括每次发生回滚时,包括用于重试的回滚和在跳过恢复过程中的回滚。 + +* `EXIT_CODE`:表示执行的退出代码的字符串。在命令行作业的情况下,可以将其转换为数字。 + +* `EXIT_MESSAGE`:表示作业如何退出的更详细描述的字符串。在失败的情况下,这可能包括尽可能多的堆栈跟踪。 + +* `LAST_UPDATED`:时间戳表示此执行最后一次被持久化的时间。 + +### [](#metaDataBatchJobExecutionContext)`BATCH_JOB_EXECUTION_CONTEXT` + +`BATCH_JOB_EXECUTION_CONTEXT`表包含与`Job`的`ExecutionContext`相关的所有信息。这里正好有一个`Job``ExecutionContext`per`JobExecution`,它包含特定作业执行所需的所有作业级别数据。该数据通常表示故障后必须检索的状态,因此`JobInstance`可以“从它停止的地方开始”。下面的清单显示了`BATCH_JOB_EXECUTION_CONTEXT`表的定义: + +``` +CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT ( + JOB_EXECUTION_ID BIGINT PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT CLOB, + constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; +``` + +下面的列表描述了每一列: + +* `JOB_EXECUTION_ID`:表示上下文所属的`JobExecution`的外键。与给定的执行相关联的行可能不止一个。 + +* `SHORT_CONTEXT`:`SERIALIZED_CONTEXT`的字符串版本。 + +* `SERIALIZED_CONTEXT`:整个上下文,序列化。 + +### [](#metaDataBatchStepExecutionContext)`BATCH_STEP_EXECUTION_CONTEXT` + +`BATCH_STEP_EXECUTION_CONTEXT`表包含与`Step`的`ExecutionContext`相关的所有信息。每`StepExecution`正好有一个`ExecutionContext`,它包含了为执行特定步骤而需要持久化的所有数据。该数据通常表示故障后必须检索的状态,这样`JobInstance`就可以“从它停止的地方开始”。下面的清单显示了`BATCH_STEP_EXECUTION_CONTEXT`表的定义: + +``` +CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT ( + STEP_EXECUTION_ID BIGINT PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT CLOB, + constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) + references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) +) ; +``` + +下面的列表描述了每一列: + +* `STEP_EXECUTION_ID`:表示上下文所属的`StepExecution`的外键。可能有多个行与给定的执行相关联。 + +* `SHORT_CONTEXT`:`SERIALIZED_CONTEXT`的字符串版本。 + +* `SERIALIZED_CONTEXT`:整个上下文,序列化。 + +### [](#metaDataArchiving)存档 + +由于每次运行批处理作业时,多个表中都有条目,因此通常需要为元数据表创建归档策略。这些表本身旨在显示过去发生的事情的记录,并且通常不会影响任何作业的运行,只有一些与重新启动有关的明显例外: + +* 该框架使用元数据表来确定特定的`JobInstance`是否已经运行过。如果作业已经运行,并且不能重新启动,那么将抛出一个异常。 + +* 如果`JobInstance`的条目在未成功完成的情况下被删除,则框架认为该作业是新的,而不是重新启动。 + +* 如果重新启动了一个作业,框架将使用已持久化到`ExecutionContext`的任何数据来恢复`Job’s`状态。因此,如果作业没有成功完成,从该表中删除任何条目,将阻止它们在再次运行时从正确的点开始。 + +### [](#multiByteCharacters)国际和多字节字符 + +如果你在业务处理中使用多字节字符集(例如中文或西里尔),那么这些字符可能需要在 Spring 批处理模式中持久化。许多用户发现,只需将模式更改为`VARCHAR`列的长度的两倍就足够了。其他人更喜欢将[JobRepository](job.html#configuringJobRepository)配置为`max-varchar-length`列长度的一半。一些用户还报告说,他们在模式定义中使用`NVARCHAR`代替`VARCHAR`。最佳结果取决于数据库平台和本地配置数据库服务器的方式。 + +### [](#recommendationsForIndexingMetaDataTables)建立元数据表索引的建议 + +Spring 批处理为几个常见的数据库平台的核心 JAR 文件中的元数据表提供了 DDL 示例。索引声明不包含在该 DDL 中,因为用户可能希望索引的方式有太多的变化,这取决于他们的精确平台、本地约定以及作业如何操作的业务需求。下面的内容提供了一些指示,说明 Spring Batch 提供的 DAO 实现将在`WHERE`子句中使用哪些列,以及它们可能被使用的频率,以便各个项目可以就索引做出自己的决定: + +| Default Table Name | Where Clause |频率| +|----------------------|-----------------------------------------|-------------------------------------------------------------------| +| BATCH\_JOB\_INSTANCE | JOB\_NAME = ? and JOB\_KEY = ? |每次有工作要做的时候| +|BATCH\_JOB\_EXECUTION | JOB\_INSTANCE\_ID = ? |每次重新启动作业时| +|BATCH\_STEP\_EXECUTION| VERSION = ? |在提交间隔上,a.k.a.chunk(以及
步骤的开始和结束)| +|BATCH\_STEP\_EXECUTION|STEP\_NAME = ? and JOB\_EXECUTION\_ID = ?|在每一步执行之前| \ No newline at end of file diff --git a/docs/spring-batch/spring-batch-integration.md b/docs/spring-batch/spring-batch-integration.md new file mode 100644 index 0000000..1a7d81e --- /dev/null +++ b/docs/spring-batch/spring-batch-integration.md @@ -0,0 +1,1091 @@ +# Spring 批处理集成 + +## [](#springBatchIntegration) Spring 批处理集成 + +XMLJavaBoth + +### [](#spring-batch-integration-introduction) Spring 批处理集成介绍 + +Spring 批处理的许多用户可能会遇到不在 Spring 批处理范围内的需求,但这些需求可以通过使用 Spring 集成来高效而简洁地实现。相反, Spring 集成用户可能会遇到 Spring 批处理需求,并且需要一种有效地集成这两个框架的方法。在这种情况下,出现了几种模式和用例, Spring 批处理集成解决了这些需求。 + +Spring 批处理和 Spring 集成之间的界限并不总是清晰的,但有两条建议可以提供帮助:考虑粒度,并应用公共模式。这些常见模式中的一些在本参考手册一节中进行了描述。 + +将消息传递添加到批处理过程中,可以实现操作的自动化,还可以分离关键关注事项并制定策略。例如,消息可能会触发作业执行,然后消息的发送可以通过多种方式公开。或者,当作业完成或失败时,该事件可能会触发要发送的消息,而这些消息的消费者可能有与应用程序本身无关的操作问题。消息传递也可以嵌入到作业中(例如,通过通道读取或写入用于处理的项)。远程分区和远程分块提供了在多个工作人员上分配工作负载的方法。 + +本节涵盖以下关键概念: + +* [命名空间支持](#namespace-support) + +* [通过消息启动批处理作业](#launching-batch-jobs-through-messages) + +* [提供反馈信息](#providing-feedback-with-informational-messages) + +* [异步处理器](#asynchronous-processors) + +* [外部化批处理过程执行](#externalizing-batch-process-execution) + +#### [](#namespace-support)名称空间支持 + +Spring 自批处理集成 1.3 以来,添加了专用的 XML 命名空间支持,目的是提供更简单的配置体验。为了激活命名空间,请将以下命名空间声明添加到 Spring XML 应用程序上下文文件中: + +``` + + + ... + + +``` + +用于 Spring 批处理集成的完全配置的 Spring XML 应用程序上下文文件可能如下所示: + +``` + + + ... + + +``` + +也允许将版本号附加到引用的 XSD 文件中,但是,由于无版本声明总是使用最新的模式,因此我们通常不建议将版本号附加到 XSD 名称中。添加版本号可能会在更新 Spring 批处理集成依赖项时产生问题,因为它们可能需要 XML 模式的最新版本。 + +#### [](#launching-batch-jobs-through-messages)通过消息启动批处理作业 + +当通过使用核心 Spring 批处理 API 启动批处理作业时,你基本上有两个选项: + +* 在命令行中,使用`CommandLineJobRunner` + +* 在编程上,使用`JobOperator.start()`或`JobLauncher.run()` + +例如,当通过使用 shell 脚本调用批处理作业时,你可能希望使用`CommandLineJobRunner`。或者,你可以直接使用`JobOperator`(例如,当使用 Spring 批处理作为 Web 应用程序的一部分时)。然而,更复杂的用例呢?也许你需要轮询远程 FTP 服务器来检索批处理作业的数据,或者你的应用程序必须同时支持多个不同的数据源。例如,你不仅可以从 Web 接收数据文件,还可以从 FTP 和其他来源接收数据文件。在调用 Spring 批处理之前,可能需要对输入文件进行额外的转换。 + +因此,使用 Spring 集成及其众多适配器来执行批处理作业将会强大得多。例如,你可以使用*文件入站通道适配器*来监视文件系统中的一个目录,并在输入文件到达时立即启动批处理作业。此外,你还可以创建 Spring 集成流,这些集成流使用多个不同的适配器,仅使用配置就可以轻松地从多个源同时为批处理作业摄取数据。使用 Spring 集成实现所有这些场景是很容易的,因为它允许对`JobLauncher`进行解耦、事件驱动的执行。 + +Spring 批处理集成提供了`JobLaunchingMessageHandler`类,你可以使用它来启动批处理作业。`JobLaunchingMessageHandler`的输入由 Spring 集成消息提供,该消息的有效负载类型为`JobLaunchRequest`。这个类是围绕需要启动的`Job`和启动批处理作业所必需的`JobParameters`的包装器。 + +下面的图像演示了典型的 Spring 集成消息流,以便启动批处理作业。[Enterprise 集成模式网站](https://www.enterpriseintegrationpatterns.com/toc.html)提供了消息传递图标及其描述的完整概述。 + +![启动批处理作业](./images/launch-batch-job.png) + +图 1。启动批处理作业 + +##### [](#transforming-a-file-into-a-joblaunchrequest)将文件转换为 joblaunchrequest + +``` +package io.spring.sbi; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.integration.launch.JobLaunchRequest; +import org.springframework.integration.annotation.Transformer; +import org.springframework.messaging.Message; + +import java.io.File; + +public class FileMessageToJobRequest { + private Job job; + private String fileParameterName; + + public void setFileParameterName(String fileParameterName) { + this.fileParameterName = fileParameterName; + } + + public void setJob(Job job) { + this.job = job; + } + + @Transformer + public JobLaunchRequest toRequest(Message message) { + JobParametersBuilder jobParametersBuilder = + new JobParametersBuilder(); + + jobParametersBuilder.addString(fileParameterName, + message.getPayload().getAbsolutePath()); + + return new JobLaunchRequest(job, jobParametersBuilder.toJobParameters()); + } +} +``` + +##### [](#the-jobexecution-response)the`JobExecution`响应 + +当执行批处理作业时,将返回一个`JobExecution`实例。此实例可用于确定执行的状态。如果`JobExecution`能够成功创建,则无论实际执行是否成功,它总是被返回。 + +如何返回`JobExecution`实例的确切行为取决于所提供的`TaskExecutor`。如果使用`synchronous`(单线程)`TaskExecutor`实现,则只返回`JobExecution`响应`after`作业完成。当使用`asynchronous``TaskExecutor`时,将立即返回`JobExecution`实例。然后,用户可以使用`JobExecution`的`id`实例(带有`JobExecution.getJobId()`),并使用`JobExplorer`查询`JobRepository`中的作业更新状态。有关更多信息,请参阅关于[查询存储库](job.html#queryingRepository)的 Spring 批参考文档。 + +##### [](#spring-batch-integration-configuration) Spring 批处理集成配置 + +考虑这样一种情况:需要创建一个文件`inbound-channel-adapter`来监听所提供的目录中的 CSV 文件,将它们交给转换器(`FileMessageToJobRequest`),通过*工作启动网关*启动作业,然后用`logging-channel-adapter`记录`JobExecution`的输出。 + +下面的示例展示了如何在 XML 中配置这种常见的情况: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中配置这种常见情况: + +Java 配置 + +``` +@Bean +public FileMessageToJobRequest fileMessageToJobRequest() { + FileMessageToJobRequest fileMessageToJobRequest = new FileMessageToJobRequest(); + fileMessageToJobRequest.setFileParameterName("input.file.name"); + fileMessageToJobRequest.setJob(personJob()); + return fileMessageToJobRequest; +} + +@Bean +public JobLaunchingGateway jobLaunchingGateway() { + SimpleJobLauncher simpleJobLauncher = new SimpleJobLauncher(); + simpleJobLauncher.setJobRepository(jobRepository); + simpleJobLauncher.setTaskExecutor(new SyncTaskExecutor()); + JobLaunchingGateway jobLaunchingGateway = new JobLaunchingGateway(simpleJobLauncher); + + return jobLaunchingGateway; +} + +@Bean +public IntegrationFlow integrationFlow(JobLaunchingGateway jobLaunchingGateway) { + return IntegrationFlows.from(Files.inboundAdapter(new File("/tmp/myfiles")). + filter(new SimplePatternFileListFilter("*.csv")), + c -> c.poller(Pollers.fixedRate(1000).maxMessagesPerPoll(1))). + transform(fileMessageToJobRequest()). + handle(jobLaunchingGateway). + log(LoggingHandler.Level.WARN, "headers.id + ': ' + payload"). + get(); +} +``` + +##### [](#example-itemreader-configuration)示例 itemreader 配置 + +现在我们正在轮询文件和启动作业,我们需要配置我们的 Spring 批处理`ItemReader`(例如),以使用在名为“input.file.name”的作业参数所定义的位置找到的文件,如下面的 Bean 配置所示: + +下面的 XML 示例展示了必要的配置 Bean: + +XML 配置 + +``` + + + ... + +``` + +下面的 Java 示例展示了必要的配置 Bean: + +Java 配置 + +``` +@Bean +@StepScope +public ItemReader sampleReader(@Value("#{jobParameters[input.file.name]}") String resource) { +... + FlatFileItemReader flatFileItemReader = new FlatFileItemReader(); + flatFileItemReader.setResource(new FileSystemResource(resource)); +... + return flatFileItemReader; +} +``` + +在前面的示例中,主要的关注点是注入`#{jobParameters['input.file.name']}`的值作为资源属性值,并将`ItemReader` Bean 设置为具有*步骤范围*。将 Bean 设置为具有步骤作用域利用了后期绑定支持,这允许访问`jobParameters`变量。 + +### [](#availableAttributesOfTheJobLaunchingGateway)作业启动网关的可用属性 + +作业启动网关具有以下属性,你可以设置这些属性来控制作业: + +* `id`:标识底层的 Spring Bean 定义,它是以下两种定义之一的实例: + + * `EventDrivenConsumer` + + * `PollingConsumer`(准确的实现取决于组件的输入通道是`SubscribableChannel`还是`PollableChannel`。) + +* `auto-startup`:布尔标志,指示端点在启动时应自动启动。默认值为*true*。 + +* `request-channel`:此端点的输入`MessageChannel`。 + +* `reply-channel`:`MessageChannel`将结果`JobExecution`的有效载荷发送到该负载。 + +* `reply-timeout`:允许你指定此网关在抛出异常之前等待多长时间(以毫秒为单位)以将答复消息成功发送到答复通道。此属性仅在通道可能阻塞时才应用(例如,当使用当前已满的有界队列通道时)。另外,请记住,当发送到`DirectChannel`时,调用发生在发送方的线程中。因此,发送操作的失败可能是由更下游的其他组件引起的。`reply-timeout`属性映射到底层`sendTimeout`实例的`sendTimeout`属性。如果没有指定,则属性默认为 \-1\,这意味着,默认情况下,`Gateway`无限期地等待。 + +* `job-launcher`:可选。接受自定义`JobLauncher` Bean 引用。如果没有指定适配器,则重新使用在`jobLauncher`的`id`下注册的实例。如果不存在缺省实例,则抛出一个异常。 + +* `order`:指定当此端点作为订阅服务器连接到`SubscribableChannel`时的调用顺序。 + +### [](#sub-elements)子元素 + +当`Gateway`接收来自`PollableChannel`的消息时,你必须为`Poller`提供一个全局默认值`Poller`,或者为`Job Launching Gateway`提供一个子元素。 + +下面的示例展示了如何用 XML 提供一个 Poller: + +XML 配置 + +``` + + + +``` + +下面的示例展示了如何用 Java 提供一个 Poller: + +Java 配置 + +``` +@Bean +@ServiceActivator(inputChannel = "queueChannel", poller = @Poller(fixedRate="1000")) +public JobLaunchingGateway sampleJobLaunchingGateway() { + JobLaunchingGateway jobLaunchingGateway = new JobLaunchingGateway(jobLauncher()); + jobLaunchingGateway.setOutputChannel(replyChannel()); + return jobLaunchingGateway; +} +``` + +#### [](#providing-feedback-with-informational-messages)提供反馈信息 + +Spring 由于批处理作业可以运行很长时间,因此提供进度信息通常是至关重要的。例如,如果批处理作业的某些部分或所有部分都失败了,利益相关者可能希望得到通知。 Spring 批处理为正在通过以下方式收集的此信息提供支持: + +* 活动轮询 + +* 事件驱动侦听器 + +在异步启动 Spring 批处理作业时(例如,通过使用`Job Launching Gateway`),将返回一个`JobExecution`实例。因此,通过使用`JobExplorer`从`JobRepository`检索`JobExecution`的更新实例,`JobExecution.getJobId()`可用于连续轮询状态更新。然而,这被认为是次优的,事件驱动的方法应该是首选的。 + +因此, Spring 批提供了侦听器,包括三个最常用的侦听器: + +* `StepListener` + +* `ChunkListener` + +* `JobExecutionListener` + +在下图所示的示例中, Spring 批处理作业已配置为`StepExecutionListener`。因此, Spring 集成接收并处理事件之前或之后的任何步骤。例如,接收到的`StepExecution`可以通过使用`Router`进行检查。基于该检查的结果,可以发生各种事情(例如将消息路由到邮件出站通道适配器),以便可以基于某些条件发送出电子邮件通知。 + +![处理信息消息](./images/handling-informational-messages.png) + +图 2。处理信息消息 + +下面由两部分组成的示例展示了侦听器如何配置为向`Gateway`事件发送消息到`StepExecution`,并将其输出记录到`logging-channel-adapter`。 + +首先,创建通知集成 bean。 + +下面的示例展示了如何在 XML 中创建通知集成 bean: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何在 Java 中创建通知集成 bean: + +Java 配置 + +``` +@Bean +@ServiceActivator(inputChannel = "stepExecutionsChannel") +public LoggingHandler loggingHandler() { + LoggingHandler adapter = new LoggingHandler(LoggingHandler.Level.WARN); + adapter.setLoggerName("TEST_LOGGER"); + adapter.setLogExpressionString("headers.id + ': ' + payload"); + return adapter; +} + +@MessagingGateway(name = "notificationExecutionsListener", defaultRequestChannel = "stepExecutionsChannel") +public interface NotificationExecutionListener extends StepExecutionListener {} +``` + +| |你需要将`@IntegrationComponentScan`注释添加到配置中。| +|---|---------------------------------------------------------------------------------| + +其次,修改工作以添加一个步骤级侦听器。 + +下面的示例展示了如何在 XML 中添加一个步骤级侦听器: + +XML 配置 + +``` + + + + + + + + + ... + + +``` + +下面的示例展示了如何在 Java 中添加一个步骤级侦听器: + +Java 配置 + +``` +public Job importPaymentsJob() { + return jobBuilderFactory.get("importPayments") + .start(stepBuilderFactory.get("step1") + .chunk(200) + .listener(notificationExecutionsListener()) + ... +} +``` + +#### [](#asynchronous-processors)异步处理器 + +异步处理器帮助你扩展项目的处理。在异步处理器用例中,`AsyncItemProcessor`充当调度器,为新线程上的项执行`ItemProcessor`的逻辑。项目完成后,将`Future`传递给要写入的`AsynchItemWriter`。 + +因此,你可以通过使用异步项目处理来提高性能,基本上允许你实现*fork-join *场景。`AsyncItemWriter`收集结果,并在所有结果可用时立即写回块。 + +下面的示例展示了如何在 XML 中配置`AsyncItemProcessor`: + +XML 配置 + +``` + + + + + + + + +``` + +下面的示例展示了如何在 XML 中配置`AsyncItemProcessor`: + +Java 配置 + +``` +@Bean +public AsyncItemProcessor processor(ItemProcessor itemProcessor, TaskExecutor taskExecutor) { + AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor(); + asyncItemProcessor.setTaskExecutor(taskExecutor); + asyncItemProcessor.setDelegate(itemProcessor); + return asyncItemProcessor; +} +``` + +`delegate`属性是指你的`ItemProcessor` Bean,而`taskExecutor`属性是指你选择的`TaskExecutor`。 + +下面的示例展示了如何在 XML 中配置`AsyncItemWriter`: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何在 Java 中配置`AsyncItemWriter`: + +Java 配置 + +``` +@Bean +public AsyncItemWriter writer(ItemWriter itemWriter) { + AsyncItemWriter asyncItemWriter = new AsyncItemWriter(); + asyncItemWriter.setDelegate(itemWriter); + return asyncItemWriter; +} +``` + +同样,`delegate`属性实际上是对你的`ItemWriter` Bean 的引用。 + +#### [](#externalizing-batch-process-execution)外部化批处理过程执行 + +到目前为止讨论的集成方法建议使用 Spring 集成像外壳一样包装 Spring 批处理的用例。然而, Spring 批处理也可以在内部使用 Spring 集成。 Spring 使用这种方法,批处理用户可以将项目甚至块的处理委托给外部进程。这允许你卸载复杂的处理。 Spring 批处理集成为以下方面提供了专门的支持: + +* 远程分块 + +* 远程分区 + +##### [](#remote-chunking)远程分块 + +![远程分块](./images/remote-chunking-sbi.png) + +图 3。远程分块 + +更进一步,还可以使用`ChunkMessageChannelItemWriter`(由 Spring Batch Integration 提供)将块处理外部化,它将项发送出去并收集结果。一旦发送, Spring 批处理继续读取和分组项的过程,而无需等待结果。相反,收集结果并将其集成回 Spring 批处理过程是`ChunkMessageChannelItemWriter`的责任。 + +通过 Spring 集成,你可以完全控制进程的并发性(例如,通过使用`QueueChannel`而不是`DirectChannel`)。此外,通过依赖 Spring Integration 的通道适配器(例如 JMS 和 AMQP)的丰富集合,你可以将批处理作业的块分配给外部系统进行处理。 + +带有要远程分块的步骤的作业可能具有类似于 XML 中的以下配置: + +XML 配置 + +``` + + + + + + ... + + +``` + +带有要远程分块的步骤的作业可能具有类似于 Java 中的以下配置: + +Java 配置 + +``` +public Job chunkJob() { + return jobBuilderFactory.get("personJob") + .start(stepBuilderFactory.get("step1") + .chunk(200) + .reader(itemReader()) + .writer(itemWriter()) + .build()) + .build(); + } +``` + +`ItemReader`引用指向要用于读取 Manager 上的数据的 Bean。正如上面所描述的,`ItemWriter`引用指向一个特殊的`ItemWriter`(称为`ChunkMessageChannelItemWriter`)。处理器(如果有的话)不在 Manager 配置中,因为它是在 Worker 上配置的。在实现用例时,你应该检查任何附加的组件属性,例如油门限制等。 + +以下 XML 配置提供了基本的 Manager 设置: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + + + + +``` + +下面的 Java 配置提供了一个基本的 Manager 设置: + +Java 配置 + +``` +@Bean +public org.apache.activemq.ActiveMQConnectionFactory connectionFactory() { + ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(); + factory.setBrokerURL("tcp://localhost:61616"); + return factory; +} + +/* + * Configure outbound flow (requests going to workers) + */ +@Bean +public DirectChannel requests() { + return new DirectChannel(); +} + +@Bean +public IntegrationFlow outboundFlow(ActiveMQConnectionFactory connectionFactory) { + return IntegrationFlows + .from(requests()) + .handle(Jms.outboundAdapter(connectionFactory).destination("requests")) + .get(); +} + +/* + * Configure inbound flow (replies coming from workers) + */ +@Bean +public QueueChannel replies() { + return new QueueChannel(); +} + +@Bean +public IntegrationFlow inboundFlow(ActiveMQConnectionFactory connectionFactory) { + return IntegrationFlows + .from(Jms.messageDrivenChannelAdapter(connectionFactory).destination("replies")) + .channel(replies()) + .get(); +} + +/* + * Configure the ChunkMessageChannelItemWriter + */ +@Bean +public ItemWriter itemWriter() { + MessagingTemplate messagingTemplate = new MessagingTemplate(); + messagingTemplate.setDefaultChannel(requests()); + messagingTemplate.setReceiveTimeout(2000); + ChunkMessageChannelItemWriter chunkMessageChannelItemWriter + = new ChunkMessageChannelItemWriter<>(); + chunkMessageChannelItemWriter.setMessagingOperations(messagingTemplate); + chunkMessageChannelItemWriter.setReplyChannel(replies()); + return chunkMessageChannelItemWriter; +} +``` + +前面的配置为我们提供了许多 bean。我们使用 ActiveMQ 和 Spring Integration 提供的入站/出站 JMS 适配器配置消息传递中间件。如图所示,我们的作业步骤引用的`itemWriter` Bean 使用`ChunkMessageChannelItemWriter`在配置的中间件上写块。 + +现在我们可以转到 Worker 配置,如下面的示例所示: + +下面的示例显示了 XML 中的工作配置: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +下面的示例展示了 Java 中的 worker 配置: + +Java 配置 + +``` +@Bean +public org.apache.activemq.ActiveMQConnectionFactory connectionFactory() { + ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(); + factory.setBrokerURL("tcp://localhost:61616"); + return factory; +} + +/* + * Configure inbound flow (requests coming from the manager) + */ +@Bean +public DirectChannel requests() { + return new DirectChannel(); +} + +@Bean +public IntegrationFlow inboundFlow(ActiveMQConnectionFactory connectionFactory) { + return IntegrationFlows + .from(Jms.messageDrivenChannelAdapter(connectionFactory).destination("requests")) + .channel(requests()) + .get(); +} + +/* + * Configure outbound flow (replies going to the manager) + */ +@Bean +public DirectChannel replies() { + return new DirectChannel(); +} + +@Bean +public IntegrationFlow outboundFlow(ActiveMQConnectionFactory connectionFactory) { + return IntegrationFlows + .from(replies()) + .handle(Jms.outboundAdapter(connectionFactory).destination("replies")) + .get(); +} + +/* + * Configure the ChunkProcessorChunkHandler + */ +@Bean +@ServiceActivator(inputChannel = "requests", outputChannel = "replies") +public ChunkProcessorChunkHandler chunkProcessorChunkHandler() { + ChunkProcessor chunkProcessor + = new SimpleChunkProcessor<>(itemProcessor(), itemWriter()); + ChunkProcessorChunkHandler chunkProcessorChunkHandler + = new ChunkProcessorChunkHandler<>(); + chunkProcessorChunkHandler.setChunkProcessor(chunkProcessor); + return chunkProcessorChunkHandler; +} +``` + +这些配置项中的大多数应该在 Manager 配置中看起来很熟悉。工作人员不需要访问 Spring 批`JobRepository`,也不需要访问实际的作业配置文件。主要的 Bean 兴趣是`chunkProcessorChunkHandler`。`ChunkProcessorChunkHandler`的`chunkProcessor`属性接受一个已配置的`SimpleChunkProcessor`,在该属性中,你将提供对你的`ItemWriter`(以及你的`ItemProcessor`)的引用,该引用将在 worker 从 Manager 接收块时在其上运行。 + +有关更多信息,请参见[远程分块](https://docs.spring.io/spring-batch/docs/current/reference/html/scalability.html#remoteChunking)的“可伸缩性”章节。 + +从版本 4.1 开始, Spring 批处理集成引入了`@EnableBatchIntegration`注释,该注释可用于简化远程分块设置。这个注释提供了两个可以在应用程序上下文中自动连接的 bean: + +* `RemoteChunkingManagerStepBuilderFactory`:用于配置 Manager 步骤 + +* `RemoteChunkingWorkerBuilder`:用于配置远程工作者集成流 + +这些 API 负责配置一些组件,如下图所示: + +![远程组块配置](./images/remote-chunking-config.png) + +图 4。远程组块配置 + +在 Manager 方面,`RemoteChunkingManagerStepBuilderFactory`允许你通过声明以下内容来配置 Manager: + +* 项目阅读器读取项目并将其发送给工人 + +* 将请求发送给工作人员的输出通道(“传出请求”) + +* 接收工作人员回复的输入通道(“传入回复”) + +a`ChunkMessageChannelItemWriter`和`MessagingTemplate`不需要显式配置(如果需要,仍然可以显式配置这些参数)。 + +在 worker 方面,`RemoteChunkingWorkerBuilder`允许你将 worker 配置为: + +* 监听 Manager 在输入通道上发送的请求(“传入请求”) + +* 对于配置了`ItemProcessor`和`ItemWriter`的每个请求,调用`handleChunk`的`ChunkProcessorChunkHandler`方法 + +* 将输出通道上的回复(“输出回复”)发送给 Manager + +不需要显式地配置`SimpleChunkProcessor`和`ChunkProcessorChunkHandler`(如果需要,可以显式地配置这些参数)。 + +下面的示例展示了如何使用这些 API: + +``` +@EnableBatchIntegration +@EnableBatchProcessing +public class RemoteChunkingJobConfiguration { + + @Configuration + public static class ManagerConfiguration { + + @Autowired + private RemoteChunkingManagerStepBuilderFactory managerStepBuilderFactory; + + @Bean + public TaskletStep managerStep() { + return this.managerStepBuilderFactory.get("managerStep") + .chunk(100) + .reader(itemReader()) + .outputChannel(requests()) // requests sent to workers + .inputChannel(replies()) // replies received from workers + .build(); + } + + // Middleware beans setup omitted + + } + + @Configuration + public static class WorkerConfiguration { + + @Autowired + private RemoteChunkingWorkerBuilder workerBuilder; + + @Bean + public IntegrationFlow workerFlow() { + return this.workerBuilder + .itemProcessor(itemProcessor()) + .itemWriter(itemWriter()) + .inputChannel(requests()) // requests received from the manager + .outputChannel(replies()) // replies sent to the manager + .build(); + } + + // Middleware beans setup omitted + + } + +} +``` + +你可以找到远程分块作业[here](https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples#remote-chunking-sample)的完整示例。 + +##### [](#remote-partitioning)远程分区 + +![远程分区](./images/remote-partitioning.png) + +图 5。远程分区 + +另一方面,当导致瓶颈的不是项目的处理,而是相关的 I/O 时,远程分区是有用的。使用远程分区,可以将工作分配给执行完整 Spring 批处理步骤的工作人员。因此,每个工作者都有自己的`ItemReader`、`ItemProcessor`和`ItemWriter`。为此, Spring 批处理集成提供了`MessageChannelPartitionHandler`。 + +这个`PartitionHandler`接口的实现使用`MessageChannel`实例向远程工作者发送指令并接收他们的响应。这从用于与远程工作者通信的传输(例如 JMS 和 AMQP)中提供了一个很好的抽象。 + +“可伸缩性”章节中涉及[远程分区](scalability.html#partitioning)的部分概述了配置远程分区所需的概念和组件,并展示了使用默认`TaskExecutorPartitionHandler`在单独的本地执行线程中进行分区的示例。要对多个 JVM 进行远程分区,还需要另外两个组件: + +* 一种远程的织物或网格环境 + +* 支持所需的远程架构或网格环境的`PartitionHandler`实现 + +与远程组块类似,JMS 可以用作“远程组块结构”。在这种情况下,使用`MessageChannelPartitionHandler`实例作为`PartitionHandler`实现,如前面所述。 + +下面的示例假定存在一个分区作业,并重点关注 XML 中的`MessageChannelPartitionHandler`和 JMS 配置: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +下面的示例假定存在一个分区作业,并重点关注 Java 中的`MessageChannelPartitionHandler`和 JMS 配置: + +Java 配置 + +``` +/* + * Configuration of the manager side + */ +@Bean +public PartitionHandler partitionHandler() { + MessageChannelPartitionHandler partitionHandler = new MessageChannelPartitionHandler(); + partitionHandler.setStepName("step1"); + partitionHandler.setGridSize(3); + partitionHandler.setReplyChannel(outboundReplies()); + MessagingTemplate template = new MessagingTemplate(); + template.setDefaultChannel(outboundRequests()); + template.setReceiveTimeout(100000); + partitionHandler.setMessagingOperations(template); + return partitionHandler; +} + +@Bean +public QueueChannel outboundReplies() { + return new QueueChannel(); +} + +@Bean +public DirectChannel outboundRequests() { + return new DirectChannel(); +} + +@Bean +public IntegrationFlow outboundJmsRequests() { + return IntegrationFlows.from("outboundRequests") + .handle(Jms.outboundGateway(connectionFactory()) + .requestDestination("requestsQueue")) + .get(); +} + +@Bean +@ServiceActivator(inputChannel = "inboundStaging") +public AggregatorFactoryBean partitioningMessageHandler() throws Exception { + AggregatorFactoryBean aggregatorFactoryBean = new AggregatorFactoryBean(); + aggregatorFactoryBean.setProcessorBean(partitionHandler()); + aggregatorFactoryBean.setOutputChannel(outboundReplies()); + // configure other propeties of the aggregatorFactoryBean + return aggregatorFactoryBean; +} + +@Bean +public DirectChannel inboundStaging() { + return new DirectChannel(); +} + +@Bean +public IntegrationFlow inboundJmsStaging() { + return IntegrationFlows + .from(Jms.messageDrivenChannelAdapter(connectionFactory()) + .configureListenerContainer(c -> c.subscriptionDurable(false)) + .destination("stagingQueue")) + .channel(inboundStaging()) + .get(); +} + +/* + * Configuration of the worker side + */ +@Bean +public StepExecutionRequestHandler stepExecutionRequestHandler() { + StepExecutionRequestHandler stepExecutionRequestHandler = new StepExecutionRequestHandler(); + stepExecutionRequestHandler.setJobExplorer(jobExplorer); + stepExecutionRequestHandler.setStepLocator(stepLocator()); + return stepExecutionRequestHandler; +} + +@Bean +@ServiceActivator(inputChannel = "inboundRequests", outputChannel = "outboundStaging") +public StepExecutionRequestHandler serviceActivator() throws Exception { + return stepExecutionRequestHandler(); +} + +@Bean +public DirectChannel inboundRequests() { + return new DirectChannel(); +} + +public IntegrationFlow inboundJmsRequests() { + return IntegrationFlows + .from(Jms.messageDrivenChannelAdapter(connectionFactory()) + .configureListenerContainer(c -> c.subscriptionDurable(false)) + .destination("requestsQueue")) + .channel(inboundRequests()) + .get(); +} + +@Bean +public DirectChannel outboundStaging() { + return new DirectChannel(); +} + +@Bean +public IntegrationFlow outboundJmsStaging() { + return IntegrationFlows.from("outboundStaging") + .handle(Jms.outboundGateway(connectionFactory()) + .requestDestination("stagingQueue")) + .get(); +} +``` + +还必须确保分区`handler`属性映射到`partitionHandler` Bean。 + +下面的示例将分区`handler`属性映射到 XML 中的`partitionHandler`: + +XML 配置 + +``` + + + + ... + + +``` + +下面的示例将分区`handler`属性映射到 Java 中的`partitionHandler`: + +Java 配置 + +``` + public Job personJob() { + return jobBuilderFactory.get("personJob") + .start(stepBuilderFactory.get("step1.manager") + .partitioner("step1.worker", partitioner()) + .partitionHandler(partitionHandler()) + .build()) + .build(); + } +``` + +你可以找到远程分区作业[here](https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples#remote-partitioning-sample)的完整示例。 + +可以用来简化远程分区设置的`@EnableBatchIntegration`注释。这个注释为远程分区提供了两个有用的 bean: + +* `RemotePartitioningManagerStepBuilderFactory`:用于配置 Manager 步骤 + +* `RemotePartitioningWorkerStepBuilderFactory`:用于配置工作步骤 + +这些 API 负责配置一些组件,如下图所示: + +![远程分区配置(使用作业存储库轮询)](./images/remote-partitioning-polling-config.png) + +图 6。远程分区配置(使用作业存储库轮询) + +![远程分区配置(带有回复聚合)](./images/remote-partitioning-aggregation-config.png) + +图 7。远程分区配置(带有回复聚合) + +在 Manager 方面,`RemotePartitioningManagerStepBuilderFactory`允许你通过声明以下内容来配置 Manager: + +* 用于划分数据的`Partitioner` + +* 将请求发送给工作人员的输出通道(“传出请求”) + +* 输入通道(“传入回复”)以接收来自工作人员的回复(在配置回复聚合时) + +* 轮询间隔和超时参数(在配置作业存储库轮询时) + +不需要显式配置`MessageChannelPartitionHandler`和`MessagingTemplate`(如果需要,仍然可以显式配置这些参数)。 + +在 worker 方面,`RemotePartitioningWorkerStepBuilderFactory`允许你将 worker 配置为: + +* 监听 Manager 在输入通道上发送的请求(“传入请求”) + +* 对于每个请求调用`StepExecutionRequestHandler`的`handle`方法 + +* 将输出通道上的回复(“输出回复”)发送给 Manager + +不需要显式配置`StepExecutionRequestHandler`(如果需要,可以显式配置)。 + +下面的示例展示了如何使用这些 API: + +``` +@Configuration +@EnableBatchProcessing +@EnableBatchIntegration +public class RemotePartitioningJobConfiguration { + + @Configuration + public static class ManagerConfiguration { + + @Autowired + private RemotePartitioningManagerStepBuilderFactory managerStepBuilderFactory; + + @Bean + public Step managerStep() { + return this.managerStepBuilderFactory + .get("managerStep") + .partitioner("workerStep", partitioner()) + .gridSize(10) + .outputChannel(outgoingRequestsToWorkers()) + .inputChannel(incomingRepliesFromWorkers()) + .build(); + } + + // Middleware beans setup omitted + + } + + @Configuration + public static class WorkerConfiguration { + + @Autowired + private RemotePartitioningWorkerStepBuilderFactory workerStepBuilderFactory; + + @Bean + public Step workerStep() { + return this.workerStepBuilderFactory + .get("workerStep") + .inputChannel(incomingRequestsFromManager()) + .outputChannel(outgoingRepliesToManager()) + .chunk(100) + .reader(itemReader()) + .processor(itemProcessor()) + .writer(itemWriter()) + .build(); + } + + // Middleware beans setup omitted + + } + +} +``` \ No newline at end of file diff --git a/docs/spring-batch/spring-batch-intro.md b/docs/spring-batch/spring-batch-intro.md new file mode 100644 index 0000000..f3d4eac --- /dev/null +++ b/docs/spring-batch/spring-batch-intro.md @@ -0,0 +1,299 @@ +# Spring 批量介绍 + +## [](#spring-batch-intro) Spring 批介绍 + +Enterprise 领域中的许多应用程序需要大容量处理,以在关键任务环境中执行业务操作。这些业务包括: + +* 对大量信息进行自动化、复杂的处理,在没有用户交互的情况下进行最有效的处理。这些操作通常包括基于时间的事件(例如月末计算、通知或通信)。 + +* 在非常大的数据集中重复处理的复杂业务规则的周期性应用(例如,保险利益的确定或费率调整)。 + +* 从内部和外部系统接收的信息的集成,这些信息通常需要以事务的方式进行格式化、验证和处理,并将其集成到记录系统中。批处理用于企业每天处理数十亿笔交易。 + +Spring 批处理是一种轻量级的、全面的批处理框架,旨在使开发对于 Enterprise 系统的日常操作至关重要的健壮的批处理应用程序成为可能。 Spring 批处理构建在人们所期望的 Spring 框架的特征(生产力、基于 POJO 的开发方法和普遍的易用性)的基础上,同时使开发人员在必要时更容易访问和利用更先进的 Enterprise 服务。 Spring 批处理不是一种调度框架。在商业和开源领域都有许多很好的 Enterprise 调度器(例如 Quartz、Tivoli、Control-M 等)。它的目的是与调度器一起工作,而不是取代调度器。 + +Spring 批处理提供了可重用的功能,这些功能在处理大量记录中是必不可少的,包括日志记录/跟踪、事务管理、作业处理统计、作业重新启动、跳过和资源管理。它还提供了更先进的技术服务和功能,通过优化和分区技术实现了非常大的批量和高性能的批处理作业。 Spring 批处理既可以用于简单的用例(例如将文件读入数据库或运行存储过程),也可以用于复杂的、大容量的用例(例如在数据库之间移动大容量的数据,对其进行转换,等等)。大批量批处理作业可以以高度可伸缩的方式利用框架来处理大量信息。 + +### [](#springBatchBackground)背景 + +虽然开放源码软件项目和相关社区更多地关注基于 Web 和基于微服务的架构框架,但明显缺乏对可重用架构框架的关注,以满足基于 Java 的批处理需求,尽管仍然需要在 EnterpriseIT 环境中处理此类处理。缺乏标准的、可重用的批处理体系结构导致了在客户 EnterpriseIT 功能中开发的许多一次性内部解决方案的激增。 + +SpringSource(现为 Pivotal)和埃森哲合作改变了这种状况。埃森哲在实现批处理架构方面的行业和技术经验、SpringSource 的技术经验深度以及 Spring 经过验证的编程模型,共同形成了一种自然而强大的合作关系,以创建高质量的、与市场相关的软件,旨在填补 EnterpriseJava 领域的一个重要空白。这两家公司都与许多正在通过开发基于 Spring 的批处理架构解决方案来解决类似问题的客户合作。这提供了一些有用的附加细节和现实生活中的约束,有助于确保该解决方案可以应用于客户提出的现实世界中的问题。 + +埃森哲为 Spring 批处理项目贡献了以前专有的批处理架构框架,以及用于驱动支持、增强和现有功能集的提交者资源。埃森哲的贡献是基于数十年来在过去几代平台上构建批处理架构的经验:COBOL/大型机、C++/UNIX,以及现在的 Java/Anywhere。 + +埃森哲和 SpringSource 之间的合作旨在促进软件处理方法、框架和工具的标准化,这些方法、框架和工具可以由 Enterprise 用户在创建批处理应用程序时始终如一地加以利用。希望为其 EnterpriseIT 环境提供标准的、经过验证的解决方案的公司和政府机构可以从 Spring 批处理中受益。 + +### [](#springBatchUsageScenarios)使用场景 + +一个典型的批处理程序通常是: + +* 从数据库、文件或队列中读取大量记录。 + +* 以某种方式处理数据。 + +* 以修改后的形式写回数据。 + +Spring 批处理自动化了这种基本的批处理迭代,提供了将类似的事务作为一个集合来处理的能力,通常是在没有任何用户交互的离线环境中。批处理作业是大多数 IT 项目的一部分, Spring 批处理是唯一提供健壮的、Enterprise 规模的解决方案的开源框架。 + +业务场景 + +* 定期提交批处理过程 + +* 并发批处理:作业的并行处理 + +* 分阶段的、Enterprise 的消息驱动处理 + +* 大规模并行批处理 + +* 失败后手动或计划重启 + +* 依赖步骤的顺序处理(扩展到工作流驱动的批处理) + +* 部分处理:跳过记录(例如,在回滚时) + +* 整批事务,用于小批量或现有存储过程/脚本的情况 + +技术目标 + +* 批处理开发人员使用 Spring 编程模型:专注于业务逻辑,让框架处理基础架构。 + +* 在基础结构、批处理执行环境和批处理应用程序之间明确分离关注点。 + +* 提供公共的、核心的执行服务,作为所有项目都可以实现的接口。 + +* 提供可以“开箱即用”地使用的核心执行接口的简单和默认实现。 + +* 通过在所有层中利用 Spring 框架,易于配置、自定义和扩展服务。 + +* 所有现有的核心服务都应该易于替换或扩展,而不会对基础设施层产生任何影响。 + +* 提供一个简单的部署模型,其体系结构 JAR 与应用程序完全分开,使用 Maven 构建。 + +### [](#springBatchArchitecture) Spring 批处理体系结构 + +Spring Batch 的设计考虑到了可扩展性和多样化的最终用户群体。下图显示了支持最终用户开发人员的可扩展性和易用性的分层架构。 + +![Figure 1.1: Spring Batch Layered Architecture](./images/spring-batch-layers.png) + +图 1。 Spring 批处理分层架构 + +这个分层架构突出了三个主要的高级组件:应用程序、核心和基础架构。该应用程序包含由开发人员使用 Spring 批处理编写的所有批处理作业和自定义代码。批处理核心包含启动和控制批处理作业所必需的核心运行时类。它包括`JobLauncher`、`Job`和`Step`的实现。应用程序和核心都是建立在一个共同的基础架构之上的。这个基础结构包含常见的读取器、编写器和服务(例如`RetryTemplate`),应用程序开发人员(读取器和编写器,例如`ItemReader`和`ItemWriter`)和核心框架本身(Retry,这是它自己的库)都使用它们。 + +### [](#batchArchitectureConsiderations)一般批处理原则和准则 + +在构建批处理解决方案时,应考虑以下关键原则、指南和一般考虑因素。 + +* 请记住,批处理架构通常会影响在线架构,反之亦然。在设计时,尽可能使用通用的构建模块,同时考虑到体系结构和环境。 + +* 尽可能地简化,避免在单个批处理应用程序中构建复杂的逻辑结构。 + +* 保持数据的处理和存储在物理上紧密地联系在一起(换句话说,将数据保存在发生处理的位置)。 + +* 尽量减少系统资源的使用,特别是 I/O。在内存中执行尽可能多的操作。 + +* 检查应用程序 I/O(分析 SQL 语句),以确保避免不必要的物理 I/O。特别需要寻找以下四个常见的缺陷: + + * 当数据可以读取一次并缓存或保存在工作存储器中时,为每个事务读取数据。 + + * 重读事务的数据,而该事务的数据是在同一事务中较早读取的。 + + * 导致不必要的表或索引扫描。 + + * 没有在 SQL 语句的 WHERE 子句中指定键值。 + +* 不要在一次批处理中做两次事情。例如,如果出于报告目的需要进行数据汇总,则应该(如果可能的话)在最初处理数据时增加存储总量,这样你的报告应用程序就不必重新处理相同的数据。 + +* 在批处理应用程序的开始阶段分配足够的内存,以避免在处理过程中进行耗时的重新分配。 + +* 对于数据 Integrity,总是假设最坏的情况。插入足够的检查和记录验证,以维护数据的 Integrity。 + +* 在可能的情况下,为内部验证实现校验和。例如,平面文件应该有一个预告片记录,它告诉文件中记录的总数和关键字段的汇总。 + +* 在具有实际数据量的类似于生产的环境中,尽早地计划和执行压力测试。 + +* 在大批量系统中,备份可能是具有挑战性的,特别是如果系统在 24-7 的基础上与在线并发运行。数据库备份通常在联机设计中得到很好的处理,但是文件备份也应该被认为是同样重要的。如果系统依赖于平面文件,那么文件备份过程不仅应该到位并记录在案,还应该定期进行测试。 + +### [](#batchProcessingStrategy)批处理策略 + +为了帮助设计和实现批处理系统,基本的批处理应用程序构建块和模式应该以示例结构图和代码 shell 的形式提供给设计人员和程序员。在开始设计批处理作业时,应该将业务逻辑分解为一系列步骤,这些步骤可以使用以下标准构建块来实现: + +* *转换应用程序:*对于由外部系统提供或生成的每种类型的文件,必须创建一个转换应用程序,以将提供的事务记录转换为处理所需的标准格式。这种类型的批处理应用程序可以部分或全部由转换实用程序模块组成(请参见基本批处理服务)。 + +* *验证应用程序:*验证应用程序确保所有输入/输出记录都是正确且一致的。验证通常基于文件头和预告片、校验和和验证算法以及记录级别交叉检查。 + +* *提取应用程序:*一种应用程序,它从数据库或输入文件中读取一组记录,根据预定义的规则选择记录,并将记录写入输出文件。 + +* *提取/更新应用程序:*一种应用程序,它从数据库或输入文件中读取记录,并由每个输入记录中的数据驱动对数据库或输出文件进行更改。 + +* *处理和更新应用程序:*对来自提取或验证应用程序的输入事务执行处理的应用程序。处理通常涉及读取数据库以获取处理所需的数据,可能会更新数据库并创建用于输出处理的记录。 + +* *输出/格式应用程序:*读取输入文件、根据标准格式从该记录重组数据并产生输出文件以用于打印或传输到另一个程序或系统的应用程序。 + +此外,应该为不能使用前面提到的构建块构建的业务逻辑提供一个基本的应用程序 shell。 + +除了主要的构建块之外,每个应用程序可以使用一个或多个标准实用程序步骤,例如: + +* 排序:读取输入文件并产生输出文件的程序,其中记录已根据记录中的排序键域重新排序。排序通常由标准的系统实用程序执行。 + +* 分割:一种程序,它读取单个输入文件,并根据字段的值将每条记录写入多个输出文件中的一个。分割可以由参数驱动的标准系统实用程序进行裁剪或执行。 + +* 合并:一种程序,从多个输入文件中读取记录,并用输入文件的合并数据生成一个输出文件。合并可以由参数驱动的标准系统实用程序进行裁剪或执行。 + +批处理应用程序还可以按其输入源进行分类: + +* 数据库驱动的应用程序由从数据库检索到的行或值驱动。 + +* 文件驱动的应用程序由从文件中检索到的记录或值驱动。 + +* 消息驱动的应用程序是由从消息队列中检索到的消息驱动的。 + +任何批处理系统的基础都是处理策略。影响该策略选择的因素包括:批处理系统的估计容量、与联机系统或其他批处理系统的并发性、可用的批处理窗口。(请注意,随着越来越多的企业希望启动并运行 24x7,清晰的批处理窗口正在消失)。 + +批处理的典型处理选项如下(按实现复杂性的增加顺序排列): + +* 在离线模式下的批处理窗口期间的正常处理。 + +* 并发批处理或在线处理。 + +* 同时并行处理许多不同的批处理运行或作业。 + +* 分区(在同一时间处理同一作业的多个实例)。 + +* 上述选项的组合。 + +这些选项中的一部分或全部可能由商业调度程序支持。 + +下一节将更详细地讨论这些处理选项。重要的是要注意,根据经验,批处理过程采用的提交和锁定策略取决于所执行的处理的类型,并且在线锁定策略也应该使用相同的原则。因此,在设计整体架构时,批处理架构不能仅仅是一个事后的想法。 + +锁定策略可以是仅使用普通的数据库锁,或者在体系结构中实现额外的自定义锁定服务。锁定服务将跟踪数据库锁定(例如,通过将必要的信息存储在专用的 DB-table 中),并向请求 DB 操作的应用程序授予或拒绝权限。该体系结构还可以实现重试逻辑,以避免在锁定情况下中止批处理作业。 + +**1.批处理窗口中的正常处理**对于在单独的批处理窗口中运行的简单批处理过程,其中在线用户或其他批处理过程不需要更新的数据,并发不是一个问题,并且可以在批处理运行结束时进行一次提交。 + +在大多数情况下,更稳健的方法更合适。请记住,批处理系统在复杂性和它们处理的数据量方面都有随着时间推移而增长的趋势。如果没有锁定策略,并且系统仍然依赖于一个提交点,那么修改批处理程序可能会很痛苦。因此,即使对于最简单的批处理系统,也要考虑重新启动-恢复选项的提交逻辑的需求,以及与本节后面描述的更复杂情况有关的信息。 + +**2.并发批处理或在线处理**处理可由联机用户同时更新的数据的批处理应用程序不应锁定联机用户可能需要超过几秒钟的任何数据(数据库或文件中的数据)。此外,在每几个事务结束时,都应该将更新提交给数据库。这将最小化其他进程不可用的数据部分和数据不可用的时间。 + +最小化物理锁定的另一种选择是使用乐观锁定模式或悲观锁定模式实现逻辑行级锁定。 + +* 乐观锁定假设记录争用的可能性较低。它通常意味着在每个数据库表中插入一个时间戳列,该数据库表由批处理和在线处理并发使用。当应用程序获取要处理的行时,它也会获取时间戳。当应用程序尝试更新已处理的行时,更新将使用 WHERE 子句中的原始时间戳。如果时间戳匹配,则数据和时间戳将被更新。如果时间戳不匹配,则表示另一个应用程序在获取和更新尝试之间更新了相同的行。因此,无法执行更新。 + +* 悲观锁定是任何一种锁定策略,该策略假定存在记录争用的高可能性,因此需要在检索时获得物理或逻辑锁定。一种悲观逻辑锁使用数据库表中的专用锁列。当应用程序检索要更新的行时,它会在 Lock 列中设置一个标志。有了标志后,试图从逻辑上检索同一行的其他应用程序将失败。当设置标记的应用程序更新该行时,它也会清除标记,从而使其他应用程序能够检索该行。请注意,在初始获取和标志设置之间也必须保持数据的 Integrity,例如通过使用 DB 锁(例如`SELECT FOR UPDATE`)。还需要注意的是,这种方法与物理锁定有相同的缺点,只是在用户去吃午饭而记录被锁定的情况下,构建一个超时机制来释放锁,会更容易管理。 + +这些模式不一定适合批处理,但它们可能用于并发批处理和在线处理(例如在数据库不支持行级锁定的情况下)。作为一般规则,乐观锁定更适合于在线应用程序,而悲观锁定更适合批处理应用程序。每当使用逻辑锁时,必须对所有访问由逻辑锁保护的数据实体的应用程序使用相同的方案。 + +请注意,这两种解决方案都只解决锁定单个记录的问题。通常,我们可能需要锁定逻辑上相关的一组记录。对于物理锁,你必须非常小心地管理这些锁,以避免潜在的死锁。对于逻辑锁,通常最好构建一个逻辑锁管理器,该管理器了解你想要保护的逻辑记录组,并可以确保锁是一致的和非死锁的。这个逻辑锁管理器通常使用自己的表来进行锁管理、争用报告、超时机制和其他关注事项。 + +**3.并行处理**并行处理允许多个批处理运行或作业并行运行,以最大限度地减少总的批处理时间。只要作业不共享相同的文件、DB-tables 或索引空间,这就不是问题。如果这样做了,则应该使用分区数据来实现此服务。另一种选择是通过使用控制表构建用于维护相互依赖关系的体系结构模块。控制表应该包含每个共享资源的一行,以及应用程序是否正在使用该资源。然后,批处理架构或并行作业中的应用程序将从该表中检索信息,以确定它是否可以访问所需的资源。 + +如果数据访问不是问题,则可以通过使用额外的线程来并行处理来实现并行处理。在大型机环境中,传统上使用并行作业类,以确保所有进程都有足够的 CPU 时间。无论如何,解决方案必须足够健壮,以确保所有正在运行的进程都有时间片。 + +并行处理中的其他关键问题包括负载平衡和一般系统资源(如文件、数据库缓冲池等)的可用性。还要注意,控制表本身很容易成为关键资源。 + +**4.划分**使用分区允许多个版本的大批量应用程序同时运行。这样做的目的是减少处理长批处理作业所需的时间。可以成功分区的进程是那些可以分割输入文件和/或分区主数据库表以允许应用程序在不同的数据集上运行的进程。 + +此外,被分区的进程必须被设计为仅处理其分配的数据集。分区体系结构必须与数据库设计和数据库分区策略紧密联系在一起。请注意,数据库分区并不一定意味着数据库的物理分区,尽管在大多数情况下这是可取的。下图展示了分区方法: + +![图 1.2:分区过程](./images/partitioned.png) + +图 2。分区过程 + +体系结构应该足够灵活,以允许分区数量的动态配置。应同时考虑自动配置和用户控制配置。自动配置可以基于参数,例如输入文件的大小和输入记录的数量。 + +**4.1 划分方法**选择一种分区方法必须在逐案的基础上进行。下面的列表描述了一些可能的分区方法: + +*1.固定甚至打破记录* + +这涉及将设置的输入记录分解为偶数个部分(例如,10,其中每个部分正好占整个记录集的 1/10)。然后,每个部分由批处理/提取应用程序的一个实例进行处理。 + +为了使用这种方法,需要进行预处理以分割设置的记录。这种分割的结果将是一个下界和上界的位置编号,它可以用作批处理/提取应用程序的输入,以便将其处理限制为仅限于其部分。 + +预处理可能是一个很大的开销,因为它必须计算和确定记录集的每个部分的边界。 + +*2.按键列分开* + +这涉及分解由键列(例如位置代码)设置的输入记录,并将每个键的数据分配给批处理实例。为了实现这一点,列值可以是: + +* 由分区表分配给批处理实例(将在本节后面描述)。 + +* 分配给批处理实例的值的一部分(如 0000-0999、1000-1999 等)。 + +在选项 1 中,添加新值意味着手动重新配置批处理/提取,以确保将新值添加到特定实例中。 + +在选项 2 中,这确保通过批处理作业的实例覆盖所有值。然而,一个实例处理的值的数量取决于列值的分布(在 0000-0999 范围内可能有大量的位置,而在 1000-1999 范围内可能很少)。在此选项下,数据范围的设计应该考虑分区。 + +在这两种选择下,都不能实现记录到批处理实例的最优均匀分布。没有动态配置所使用的批处理实例的数量。 + +*3.按视图划分的分手* + +这种方法基本上是在数据库级别上按键列分解。这涉及到将已有的记录分解成不同的观点。这些视图由批处理应用程序的每个实例在其处理过程中使用。分解是通过对数据进行分组来完成的。 + +有了这个选项,一个批处理应用程序的每个实例都必须被配置为命中一个特定的视图(而不是主表)。此外,在添加了新的数据值之后,必须将这组新的数据包含到视图中。没有动态配置功能,因为实例数量的变化会导致视图的变化。 + +*4.增加一个处理指示器* + +这涉及到在输入表中添加一个新列,该列充当指示器。作为预处理步骤,所有指标都被标记为未处理。在批处理应用程序的记录获取阶段,记录被读取,条件是该记录被标记为未处理,并且一旦它们被读取(使用锁定),它们就被标记为正在处理中。当该记录完成时,指示器将更新为“完成”或“错误”。许多批处理应用程序的实例可以在不进行更改的情况下启动,因为附加的列确保只处理一次记录。 + +有了这个选项,表上的 I/O 会动态增加。在更新批处理应用程序的情况下,这种影响会减少,因为无论如何都必须进行写操作。 + +*5.将表格解压缩为平面文件* + +这涉及到将表提取到一个文件中。然后可以将该文件拆分成多个段,并将其用作批处理实例的输入。 + +有了这个选项,将表提取到一个文件中并对其进行分割的额外开销可能会抵消多个分区的影响。动态配置可以通过更改文件分割脚本来实现。 + +*6.哈希列的使用* + +此方案涉及在用于检索驱动程序记录的数据库表中添加一个散列列(key/index)。这个散列有一个指示器,用于确定批处理应用程序的哪个实例处理这个特定的行。例如,如果要启动三个批处理实例,那么“A”的指示器将标记一个由实例 1 处理的行,“B”的指示器将标记一个由实例 2 处理的行,而“C”的指示器将标记一个由实例 3 处理的行。 + +然后,用于检索记录的过程将具有一个附加的`WHERE`子句,以选择由特定指示器标记的所有行。此表中的插入将涉及添加标记字段,这将默认为其中一个实例(例如“a”)。 + +一个简单的批处理应用程序将用于更新指标,例如在不同实例之间重新分配负载。当添加了足够多的新行时,可以运行这个批处理(除了批处理窗口中的任何时候),以便将新行重新分发到其他实例。 + +批处理应用程序的其他实例只需要运行前几段所述的批处理应用程序,就可以重新分配指示器,以使用新数量的实例。 + +**4.2 数据库和应用程序设计原则** + +一个支持使用键列方法在分区数据库表上运行的多分区应用程序的体系结构应该包括一个用于存储分区参数的中心分区存储库。这提供了灵活性并确保了可维护性。存储库通常由一个表组成,称为分区表。 + +存储在分区表中的信息是静态的,并且通常应该由 DBA 来维护。表应该由多分区应用程序的每个分区的一行信息组成。表中应该有用于程序 ID 代码的列、分区号(分区的逻辑 ID)、此分区的 DB 键列的低值和此分区的 DB 键列的高值。 + +在程序启动时,程序`id`和分区号应该从体系结构(特别是从控制处理任务小程序)传递给应用程序。如果使用键列方法,则使用这些变量来读取分区表,以确定应用程序要处理的数据范围。此外,在整个处理过程中必须使用分区号,以便: + +* 添加到输出文件/数据库更新,以便合并进程正常工作。 + +* 将正常处理报告给批处理日志,并将任何错误报告给架构错误处理程序。 + +**4.3 最大限度地减少死锁** + +当应用程序并行运行或被分区时,数据库资源和死锁中可能会发生争用。作为数据库设计的一部分,数据库设计团队尽可能地消除潜在的争用情况是至关重要的。 + +此外,开发人员必须确保数据库索引表的设计考虑到死锁预防和性能。 + +死锁或热点经常出现在管理表或体系结构表中,例如日志表、控制表和锁表。这些问题的影响也应考虑在内。现实的压力测试对于识别架构中可能的瓶颈至关重要。 + +为了最大程度地减少冲突对数据的影响,体系结构应该提供服务,例如在附加到数据库或遇到死锁时提供等待和重试间隔。这意味着内置一种机制来对特定的数据库返回代码做出反应,而不是立即发出错误,而是等待预定的时间并重新尝试数据库操作。 + +**4.4 参数传递和验证** + +分区架构应该对应用程序开发人员相对透明。体系结构应该执行与以分区模式运行应用程序相关的所有任务,包括: + +* 在应用程序启动之前检索分区参数. + +* 在应用程序启动之前验证分区参数。 + +* 在启动时将参数传递给应用程序。 + +验证应包括检查,以确保: + +* 应用程序有足够的分区来覆盖整个数据范围。 + +* 分区之间没有空隙。 + +如果数据库是分区的,则可能需要进行一些额外的验证,以确保单个分区不会跨越数据库分区。 + +此外,体系结构应该考虑到分区的合并。关键问题包括: + +* 在进入下一个作业步骤之前,必须完成所有的分区吗? + +* 如果其中一个分区中止,会发生什么情况? \ No newline at end of file diff --git a/docs/spring-batch/step.md b/docs/spring-batch/step.md new file mode 100644 index 0000000..d401c3d --- /dev/null +++ b/docs/spring-batch/step.md @@ -0,0 +1,1869 @@ +# 配置一个步骤 + +## [](#configureStep)配置`Step` + +XMLJavaBoth + +正如[领域章节](domain.html#domainLanguageOfBatch)中所讨论的,`Step`是一个域对象,它封装了批处理作业的一个独立的、连续的阶段,并包含定义和控制实际批处理所需的所有信息。这必然是一个模糊的描述,因为任何给定的`Step`的内容都是由编写`Job`的开发人员自行决定的。a`Step`可以是简单的,也可以是复杂的,正如开发人员所希望的那样。一个简单的`Step`可能会将文件中的数据加载到数据库中,只需要很少或不需要代码(取决于使用的实现)。更复杂的`Step`可能具有复杂的业务规则,作为处理的一部分,如下图所示: + +![Step](./images/step.png) + +图 1。步骤 + +### [](#chunkOrientedProcessing)面向块的处理 + +Spring 批处理在其最常见的实现中使用了一种“面向块”的处理风格。面向块的处理指的是一次读取一个数据,并创建在事务边界内写出的“块”。一旦读取的项数等于提交间隔,`ItemWriter`就会写出整个块,然后提交事务。下图显示了这个过程: + +![面向块的处理](./images/chunk-oriented-processing.png) + +图 2。面向块的处理 + +下面的伪代码以简化的形式显示了相同的概念: + +``` +List items = new Arraylist(); +for(int i = 0; i < commitInterval; i++){ + Object item = itemReader.read(); + if (item != null) { + items.add(item); + } +} +itemWriter.write(items); +``` + +面向块的步骤还可以配置一个可选的`ItemProcessor`来处理项,然后将它们传递给`ItemWriter`。下图显示了在步骤中注册`ItemProcessor`时的过程: + +![基于项目处理器的面向块处理](./images/chunk-oriented-processing-with-item-processor.png) + +图 3。基于项目处理器的面向块处理 + +下面的伪代码展示了如何以简化的形式实现这一点: + +``` +List items = new Arraylist(); +for(int i = 0; i < commitInterval; i++){ + Object item = itemReader.read(); + if (item != null) { + items.add(item); + } +} + +List processedItems = new Arraylist(); +for(Object item: items){ + Object processedItem = itemProcessor.process(item); + if (processedItem != null) { + processedItems.add(processedItem); + } +} + +itemWriter.write(processedItems); +``` + +有关项处理器及其用例的更多详细信息,请参阅[项目处理](processor.html#itemProcessor)部分。 + +#### [](#configuringAStep)配置`Step` + +尽管`Step`所需依赖项的列表相对较短,但它是一个非常复杂的类,可能包含许多协作者。 + +为了简化配置,可以使用 Spring 批 XML 命名空间,如以下示例所示: + +XML 配置 + +``` + + + + + + + +``` + +在使用 Java 配置时,可以使用 Spring 批处理构建器,如以下示例所示: + +Java 配置 + +``` +/** + * Note the JobRepository is typically autowired in and not needed to be explicitly + * configured + */ +@Bean +public Job sampleJob(JobRepository jobRepository, Step sampleStep) { + return this.jobBuilderFactory.get("sampleJob") + .repository(jobRepository) + .start(sampleStep) + .build(); +} + +/** + * Note the TransactionManager is typically autowired in and not needed to be explicitly + * configured + */ +@Bean +public Step sampleStep(PlatformTransactionManager transactionManager) { + return this.stepBuilderFactory.get("sampleStep") + .transactionManager(transactionManager) + .chunk(10) + .reader(itemReader()) + .writer(itemWriter()) + .build(); +} +``` + +上面的配置包括创建面向项的步骤所需的唯一依赖项: + +* `reader`:提供处理项的`ItemReader`。 + +* `writer`:处理由`ItemReader`提供的项的`ItemWriter`。 + +* `transaction-manager`: Spring 的`PlatformTransactionManager`,在处理过程中开始并提交事务。 + +* `transactionManager`: Spring 的`PlatformTransactionManager`,在处理过程中开始并提交事务。 + +* `job-repository`:`JobRepository`的特定于 XML 的名称,该名称在处理过程中(就在提交之前)定期存储`StepExecution`和`ExecutionContext`。对于内联``(在``中定义的),它是``元素上的一个属性。对于独立的``,它被定义为 \的属性。 + +* `repository`:`JobRepository`的特定于 Java 的名称,该名称在处理过程中(就在提交之前)定期存储`StepExecution`和`ExecutionContext`。 + +* `commit-interval`:在提交事务之前要处理的项数的 XML 特定名称。 + +* `chunk`:依赖项的特定于 Java 的名称,该名称指示这是一个基于项的步骤,以及在提交事务之前要处理的项数。 + +需要注意的是,`job-repository`默认为`jobRepository`,`transaction-manager`默认为`transactionManager`。而且,`ItemProcessor`是可选的,因为该项可以直接从阅读器传递给编写器。 + +需要注意的是,`repository`默认为`jobRepository`,`transactionManager`默认为`transactionManager`(都是通过`@EnableBatchProcessing`中的基础设施提供的)。而且,`ItemProcessor`是可选的,因为该项可以直接从阅读器传递给编写器。 + +#### [](#InheritingFromParentStep)从父节点继承`Step` + +如果一组`Steps`共享类似的配置,那么定义一个“父”`Step`可能是有帮助的,具体的`Steps`可以从中继承属性。与 Java 中的类继承类似,“child”`Step`将其元素和属性与父元素和属性结合在一起。子程序还重写父程序的任何`Steps`。 + +在下面的示例中,`Step`,“concreteStep1”,继承自“parentstep”。它用“itemreader”、“itemprocessor”、“itemwriter”、`startLimit=5`和`allowStartIfComplete=true`实例化。此外,`commitInterval`是“5”,因为它被“concreteStep1”`Step`覆盖,如以下示例所示: + +``` + + + + + + + + + + + +``` + +在 Job 元素中的 Step 上仍然需要`id`属性。这有两个原因: + +* 在持久化`StepExecution`时,使用`id`作为步骤名。如果在作业中的多个步骤中引用了相同的独立步骤,则会发生错误。 + +* 在创建工作流时,如本章后面所述,`next`属性应该指代工作流中的步骤,而不是独立的步骤。 + +##### [](#abstractStep)摘要`Step` + +有时,可能需要定义不是完整的`Step`配置的父`Step`。例如,如果`reader`、`writer`和`tasklet`属性在`Step`配置中被保留,则初始化失败。如果必须在没有这些属性的情况下定义父属性,那么应该使用`abstract`属性。`abstract``Step`只是扩展,不是实例化。 + +在下面的示例中,如果不声明`Step``abstractParentStep`为抽象,则不会对其进行实例化。`Step`、“ConcreteStep2”有“itemreader”、“itemwriter”和 commit-interval=10。 + +``` + + + + + + + + + + + +``` + +##### [](#mergingListsOnStep)合并列表 + +`Steps`上的一些可配置元素是列表,例如``元素。如果父元素和子元素`Steps`都声明一个``元素,那么子元素的列表将覆盖父元素的列表。为了允许子元素向父元素定义的列表中添加额外的侦听器,每个 List 元素都具有`merge`属性。如果元素指定`merge="true"`,那么子元素的列表将与父元素的列表合并,而不是覆盖它。 + +在下面的示例中,使用两个侦听器创建`Step`“concreteStep3”:`listenerOne`和`listenerTwo`: + +``` + + + + + + + + + + + + + + +``` + +#### [](#commitInterval)提交间隔 + +如前所述,一个步骤读入并写出项,并使用提供的`PlatformTransactionManager`定期提交。如果`commit-interval`为 1,则在写入每个单独的项后提交。在许多情况下,这是不理想的,因为开始和提交事务是昂贵的。理想情况下,最好是在每个事务中处理尽可能多的项,这完全取决于所处理的数据类型以及与该步骤交互的资源。因此,可以配置在提交中处理的项数。 + +下面的示例显示了一个`step`,其`tasklet`的`commit-interval`值为 10,因为它将在 XML 中定义: + +XML 配置 + +``` + + + + + + + +``` + +下面的示例显示了一个`step`,其`tasklet`的值`commit-interval`为 10,这将在 Java 中定义: + +Java 配置 + +``` +@Bean +public Job sampleJob() { + return this.jobBuilderFactory.get("sampleJob") + .start(step1()) + .build(); +} + +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(itemReader()) + .writer(itemWriter()) + .build(); +} +``` + +在前面的示例中,在每个事务中处理 10 个项目。在处理的开始,事务就开始了。此外,每次在`read`上调用`ItemReader`时,计数器都会递增。当它达到 10 时,聚合项的列表被传递给`ItemWriter`,事务被提交。 + +#### [](#stepRestart)配置用于重新启动的`Step` + +在“[配置和运行作业](job.html#configureJob)”小节中,讨论了重新启动`Job`。重启对步骤有很多影响,因此,可能需要一些特定的配置。 + +##### [](#startLimit)设置启动限制 + +在许多情况下,你可能希望控制`Step`可以启动的次数。例如,可能需要对特定的`Step`进行配置,使其仅运行一次,因为它会使一些必须手动修复的资源失效,然后才能再次运行。这是在步骤级别上可配置的,因为不同的步骤可能有不同的需求。可以只执行一次的`Step`可以作为同一`Job`的一部分存在,也可以作为可以无限运行的`Step`的一部分存在。 + +下面的代码片段展示了一个 XML 中的 Start Limit 配置示例: + +XML 配置 + +``` + + + + + +``` + +下面的代码片段展示了一个 Java 中的 Start Limit 配置示例: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(itemReader()) + .writer(itemWriter()) + .startLimit(1) + .build(); +} +``` + +前面示例中所示的步骤只能运行一次。试图再次运行它将导致抛出`StartLimitExceededException`。请注意,start-limit 的默认值是`Integer.MAX_VALUE`。 + +##### [](#allowStartIfComplete)重新启动已完成的`Step` + +在可重启作业的情况下,无论第一次是否成功,都可能有一个或多个应该始终运行的步骤。例如,验证步骤或`Step`在处理前清理资源。在对重新启动的作业进行正常处理期间,跳过状态为“已完成”的任何步骤,这意味着该步骤已成功完成。将`allow-start-if-complete`设置为“true”会重写此项,以便该步骤始终运行。 + +下面的代码片段展示了如何在 XML 中定义一个可重启作业: + +XML 配置 + +``` + + + + + +``` + +下面的代码片段展示了如何在 Java 中定义一个可重启作业: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(itemReader()) + .writer(itemWriter()) + .allowStartIfComplete(true) + .build(); +} +``` + +##### [](#stepRestartExample)`Step`重新启动配置示例 + +下面的 XML 示例展示了如何将作业配置为具有可以重新启动的步骤: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + +``` + +下面的 Java 示例展示了如何将作业配置为具有可以重新启动的步骤: + +Java 配置 + +``` +@Bean +public Job footballJob() { + return this.jobBuilderFactory.get("footballJob") + .start(playerLoad()) + .next(gameLoad()) + .next(playerSummarization()) + .build(); +} + +@Bean +public Step playerLoad() { + return this.stepBuilderFactory.get("playerLoad") + .chunk(10) + .reader(playerFileItemReader()) + .writer(playerWriter()) + .build(); +} + +@Bean +public Step gameLoad() { + return this.stepBuilderFactory.get("gameLoad") + .allowStartIfComplete(true) + .chunk(10) + .reader(gameFileItemReader()) + .writer(gameWriter()) + .build(); +} + +@Bean +public Step playerSummarization() { + return this.stepBuilderFactory.get("playerSummarization") + .startLimit(2) + .chunk(10) + .reader(playerSummarizationSource()) + .writer(summaryWriter()) + .build(); +} +``` + +前面的示例配置用于加载有关足球比赛的信息并对其进行总结的作业。它包含三个步骤:`playerLoad`、`gameLoad`和`playerSummarization`。`playerLoad`步骤从平面文件加载玩家信息,而`gameLoad`步骤对游戏也是如此。最后一步,`playerSummarization`,然后根据提供的游戏总结每个玩家的统计数据。假设`playerLoad`加载的文件必须只加载一次,但是`gameLoad`可以加载特定目录中的任何游戏,并在它们成功加载到数据库后将其删除。因此,`playerLoad`步骤不包含额外的配置。它可以启动任意次数,如果完成,则跳过。但是,每次都需要运行`gameLoad`步骤,以防自上次运行以来添加了额外的文件。它有“允许-启动-如果-完成”设置为“真”,以便始终被启动。(假设游戏加载到的数据库表上有一个进程指示器,以确保新的游戏可以通过摘要步骤正确地找到)。摘要步骤是作业中最重要的一步,它的起始限制为 2。这是有用的,因为如果该步骤持续失败,新的退出代码将返回给控制作业执行的操作符,并且在手动干预发生之前,它不能再次启动。 + +| |此作业为该文档提供了一个示例,它与示例项目中的`footballJob`不同。| +|---|--------------------------------------------------------------------------------------------------------------------| + +本节的其余部分描述了`footballJob`示例的三次运行中的每一次运行的情况。 + +运行 1: + +1. `playerLoad`运行并成功完成,将 400 名玩家添加到“玩家”表中。 + +2. `gameLoad`运行和处理 11 个游戏数据文件,并将其内容加载到“游戏”表中。 + +3. `playerSummarization`开始处理,5 分钟后失败。 + +运行 2: + +1. `playerLoad`不运行,因为它已经成功地完成了,并且`allow-start-if-complete`是’false’(默认值)。 + +2. `gameLoad`再次运行并处理另外 2 个文件,并将其内容加载到“Games”表中(进程指示器指示它们尚未被处理)。 + +3. `playerSummarization`开始处理所有剩余的游戏数据(使用进程指示器进行过滤),并在 30 分钟后再次失败。 + +运行 3: + +1. `playerLoad`不运行,因为它已经成功地完成了,并且`allow-start-if-complete`是’false’(默认值)。 + +2. `gameLoad`再次运行并处理另外 2 个文件,并将其内容加载到“Games”表中(进程指示器指示它们尚未被处理)。 + +3. 由于这是`playerSummarization`的第三次执行,因此`playerSummarization`未启动并立即终止作业,并且其限制仅为 2。要么必须提高限制,要么必须执行`Job`作为新的`JobInstance`。 + +#### [](#configuringSkip)配置跳过逻辑 + +在许多情况下,在处理过程中遇到的错误不会导致`Step`失败,而是应该跳过。这通常是一个必须由了解数据本身及其含义的人做出的决定。例如,财务数据可能不会被跳过,因为它会导致资金转移,而这需要完全准确。另一方面,加载供应商列表可能会允许跳过。如果某个供应商由于格式化不正确或缺少必要的信息而未加载,那么很可能就不存在问题。通常,这些不良记录也会被记录下来,稍后在讨论听众时会对此进行讨论。 + +下面的 XML 示例展示了使用跳过限制的示例: + +XML 配置 + +``` + + + + + + + + + +``` + +下面的 Java 示例展示了一个使用跳过限制的示例: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(flatFileItemReader()) + .writer(itemWriter()) + .faultTolerant() + .skipLimit(10) + .skip(FlatFileParseException.class) + .build(); +} +``` + +在前面的示例中,使用了`FlatFileItemReader`。如果在任何时候抛出一个`FlatFileParseException`,则跳过该项并将其计入总跳过限制 10。声明的异常(及其子类)可能会在块处理的任何阶段(读、进程、写)抛出,但是在步骤执行中,读、进程和写的跳过是单独的计数,但是该限制适用于所有跳过。一旦达到跳过限制,发现的下一个异常将导致该步骤失败。换句话说,第 11 跳会触发异常,而不是第 10 跳会触发异常。 + +上述示例的一个问题是,除了`FlatFileParseException`之外的任何其他异常都会导致`Job`失败。在某些情况下,这可能是正确的行为。然而,在其他情况下,可能更容易确定哪些异常应该导致失败,并跳过其他所有情况。 + +下面的 XML 示例展示了一个排除特定异常的示例: + +XML 配置 + +``` + + + + + + + + + + +``` + +下面的 Java 示例展示了一个排除特定异常的示例: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(flatFileItemReader()) + .writer(itemWriter()) + .faultTolerant() + .skipLimit(10) + .skip(Exception.class) + .noSkip(FileNotFoundException.class) + .build(); +} +``` + +通过将`java.lang.Exception`标识为可跳过的异常类,配置指示所有`Exceptions`都是可跳过的。但是,通过“排除”`java.io.FileNotFoundException`,该配置将可跳过的异常类的列表细化为所有`Exceptions`*除了*`FileNotFoundException`。任何被排除的异常类如果遇到都是致命的(也就是说,它们不会被跳过)。 + +对于遇到的任何异常,可跳跃性由类层次结构中最近的超类决定。任何未分类的例外情况都被视为“致命的”。 + +``和``元素的顺序并不重要。 + +`skip`和`noSkip`方法调用的顺序并不重要。 + +#### [](#retryLogic)配置重试逻辑 + +在大多数情况下,你希望异常导致跳过或`Step`失败。然而,并非所有的例外都是确定性的。如果在读取时遇到`FlatFileParseException`,则总是为该记录抛出该记录。重置`ItemReader`不会有帮助。但是,对于其他异常,例如`DeadlockLoserDataAccessException`,它表示当前进程试图更新另一个进程持有锁定的记录。等待并再次尝试可能会取得成功。 + +在 XML 中,重试应该配置如下: + +``` + + + + + + + + + +``` + +在 Java 中,重试应该配置如下: + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(itemReader()) + .writer(itemWriter()) + .faultTolerant() + .retryLimit(3) + .retry(DeadlockLoserDataAccessException.class) + .build(); +} +``` + +`Step`允许对单个项目的重试次数进行限制,并提供“可重试”的异常列表。有关重试工作原理的更多详细信息,请参见[retry](retry.html#retry)。 + +#### [](#controllingRollback)控制回滚 + +默认情况下,不管是重试还是跳过,从`ItemWriter`抛出的任何异常都会导致由`Step`控制的事务回滚。如果按照前面描述的方式配置了 Skip,则从`ItemReader`抛出的异常不会导致回滚。但是,在许多情况下,从`ItemWriter`抛出的异常不应该导致回滚,因为没有发生任何使事务无效的操作。出于这个原因,`Step`可以配置一个不应导致回滚的异常列表。 + +在 XML 中,你可以按以下方式控制回滚: + +XML 配置 + +``` + + + + + + + + +``` + +在 Java 中,你可以按以下方式控制回滚: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(itemReader()) + .writer(itemWriter()) + .faultTolerant() + .noRollback(ValidationException.class) + .build(); +} +``` + +##### [](#transactionalReaders)事务读取器 + +`ItemReader`的基本契约是,它只是远期的。该步骤缓冲读写器的输入,以便在回滚的情况下,不需要从读写器重新读取项目。然而,在某些情况下,读取器是建立在事务性资源之上的,例如 JMS 队列。在这种情况下,由于队列与回滚的事务绑定在一起,因此从队列中拉出的消息将被放回。出于这个原因,可以将该步骤配置为不缓冲项。 + +下面的示例展示了如何创建不使用 XML 缓冲项的读取器: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何创建不在 Java 中缓冲项的读取器: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(itemReader()) + .writer(itemWriter()) + .readerIsTransactionalQueue() + .build(); +} +``` + +#### [](#transactionAttributes)事务属性 + +事务属性可用于控制`isolation`、`propagation`和`timeout`设置。有关设置事务属性的更多信息,请参见[Spring core documentation](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction)。 + +以下示例在 XML 中设置`isolation`、`propagation`和`timeout`事务属性: + +XML 配置 + +``` + + + + + + +``` + +下面的示例在 Java 中设置`isolation`、`propagation`和`timeout`事务属性: + +Java 配置 + +``` +@Bean +public Step step1() { + DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); + attribute.setPropagationBehavior(Propagation.REQUIRED.value()); + attribute.setIsolationLevel(Isolation.DEFAULT.value()); + attribute.setTimeout(30); + + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(itemReader()) + .writer(itemWriter()) + .transactionAttribute(attribute) + .build(); +} +``` + +#### [](#registeringItemStreams)用`Step`注册`ItemStream` + +该步骤必须在其生命周期中的必要点处理`ItemStream`回调(有关`ItemStream`接口的更多信息,请参见[ItemStream](readersAndWriters.html#itemStream))。如果一个步骤失败并且可能需要重新启动,这是至关重要的,因为`ItemStream`接口是该步骤获取所需的关于两次执行之间的持久状态的信息的地方。 + +如果`ItemReader`、`ItemProcessor`或`ItemWriter`本身实现了`ItemStream`接口,那么这些接口将被自动注册。任何其他流都需要单独注册。在将委托等间接依赖注入到 Reader 和 Writer 中时,通常会出现这种情况。可以通过“流”元素在`step`上注册流。 + +下面的示例显示了如何在 XML 中的`step`上注册`stream`: + +XML 配置 + +``` + + + + + + + + + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中的`step`上注册`stream`: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(itemReader()) + .writer(compositeItemWriter()) + .stream(fileItemWriter1()) + .stream(fileItemWriter2()) + .build(); +} + +/** + * In Spring Batch 4, the CompositeItemWriter implements ItemStream so this isn't + * necessary, but used for an example. + */ +@Bean +public CompositeItemWriter compositeItemWriter() { + List writers = new ArrayList<>(2); + writers.add(fileItemWriter1()); + writers.add(fileItemWriter2()); + + CompositeItemWriter itemWriter = new CompositeItemWriter(); + + itemWriter.setDelegates(writers); + + return itemWriter; +} +``` + +在上面的示例中,`CompositeItemWriter`不是`ItemStream`,但它的两个委托都是。因此,为了使框架能够正确地处理这两个委托编写器,必须将这两个委托编写器显式地注册为流。`ItemReader`不需要显式地注册为流,因为它是`Step`的直接属性。该步骤现在可以重新启动,并且在发生故障时,Reader 和 Writer 的状态被正确地持久化。 + +#### [](#interceptingStepExecution)拦截`Step`执行 + +就像`Job`一样,在执行`Step`的过程中有许多事件,其中用户可能需要执行某些功能。例如,为了写出到需要页脚的平面文件,需要在`ItemWriter`已完成时通知`Step`,以便可以写出页脚。这可以通过使用许多`Step`范围的侦听器中的一个来实现。 + +实现`StepListener`扩展之一的任何类(但不包括接口本身,因为它是空的)都可以通过`listeners`元素应用到一个步骤。`listeners`元素在步骤、任务 let 或块声明中是有效的。建议你在其函数应用的级别上声明侦听器,或者,如果它是多功能的(例如`StepExecutionListener`和`ItemReadListener`),则在其应用的最细粒度级别上声明它。 + +下面的示例展示了一个应用于 XML 块级别的侦听器: + +XML 配置 + +``` + + + + + + + + +``` + +下面的示例展示了在 Java 中应用于块级别的侦听器: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(10) + .reader(reader()) + .writer(writer()) + .listener(chunkListener()) + .build(); +} +``` + +一个`ItemReader`、`ItemWriter`或`ItemProcessor`本身实现了`StepListener`接口之一的`Step`如果使用名称空间``元素或`*StepFactoryBean`工厂之一,则自动在`Step`中注册。这仅适用于直接注入`Step`的组件。如果侦听器嵌套在另一个组件中,则需要显式地对其进行注册(如前面在[用`Step`注册`ItemStream`](#registeringitemStreams)中所述)。 + +除了`StepListener`接口外,还提供了注释来解决相同的问题。普通的旧 Java 对象可以具有带有这些注释的方法,然后将这些方法转换为相应的`StepListener`类型。对组块组件的定制实现进行注释也是常见的,例如`ItemReader`或`ItemWriter`或`Tasklet`。XML 解析器分析``元素的注释,并在构建器中使用`listener`方法注册注释,因此你所需要做的就是使用 XML 名称空间或构建器通过一个步骤注册侦听器。 + +##### [](#stepExecutionListener)`StepExecutionListener` + +`StepExecutionListener`表示用于`Step`执行的最通用的侦听器。它允许在`Step`开始之前和结束之后发出通知,无论是正常结束还是失败,如下例所示: + +``` +public interface StepExecutionListener extends StepListener { + + void beforeStep(StepExecution stepExecution); + + ExitStatus afterStep(StepExecution stepExecution); + +} +``` + +`ExitStatus`是`afterStep`的返回类型,以便使侦听器有机会修改在完成`Step`时返回的退出代码。 + +与此接口对应的注释是: + +* `@BeforeStep` + +* `@AfterStep` + +##### [](#chunkListener)`ChunkListener` + +块被定义为在事务范围内处理的项。在每个提交间隔时间提交一个事务,提交一个“块”。a`ChunkListener`可用于在块开始处理之前或在块成功完成之后执行逻辑,如以下接口定义所示: + +``` +public interface ChunkListener extends StepListener { + + void beforeChunk(ChunkContext context); + void afterChunk(ChunkContext context); + void afterChunkError(ChunkContext context); + +} +``` + +在事务启动后但在`ItemReader`上调用读之前调用 BeforeChunk 方法。相反,`afterChunk`是在提交了块之后调用的(如果有回滚,则根本不调用)。 + +与此接口对应的注释是: + +* `@BeforeChunk` + +* `@AfterChunk` + +* `@AfterChunkError` + +当没有块声明时,可以应用`ChunkListener`。`TaskletStep`负责调用`ChunkListener`,因此它也适用于非面向项目的任务小程序(在任务小程序之前和之后调用它)。 + +##### [](#itemReadListener)`ItemReadListener` + +在前面讨论跳过逻辑时,提到了记录跳过的记录可能是有益的,这样可以在以后处理它们。在读取错误的情况下,可以使用`ItemReaderListener`完成此操作,如下面的接口定义所示: + +``` +public interface ItemReadListener extends StepListener { + + void beforeRead(); + void afterRead(T item); + void onReadError(Exception ex); + +} +``` + +在每次调用之前调用`beforeRead`方法来读取`ItemReader`。在每次成功调用 read 之后,都会调用`afterRead`方法,并传递被读取的项。如果在读取时出现错误,则调用`onReadError`方法。提供了所遇到的异常,以便可以对其进行记录。 + +与此接口对应的注释是: + +* `@BeforeRead` + +* `@AfterRead` + +* `@OnReadError` + +##### [](#itemProcessListener)`ItemProcessListener` + +就像`ItemReadListener`一样,可以“监听”项目的处理,如以下接口定义所示: + +``` +public interface ItemProcessListener extends StepListener { + + void beforeProcess(T item); + void afterProcess(T item, S result); + void onProcessError(T item, Exception e); + +} +``` + +在`ItemProcessor`上,在`process`之前调用`beforeProcess`方法,并将其交给要处理的项。在成功处理该项后,将调用`afterProcess`方法。如果在处理过程中出现错误,则调用`onProcessError`方法。提供了遇到的异常和试图处理的项,以便可以对它们进行记录。 + +与此接口对应的注释是: + +* `@BeforeProcess` + +* `@AfterProcess` + +* `@OnProcessError` + +##### [](#itemWriteListener)`ItemWriteListener` + +可以使用`ItemWriteListener`“监听”项目的写入,如以下接口定义所示: + +``` +public interface ItemWriteListener extends StepListener { + + void beforeWrite(List items); + void afterWrite(List items); + void onWriteError(Exception exception, List items); + +} +``` + +在`ItemWriter`上的`write`之前调用`beforeWrite`方法,并将所写的项列表交给该方法。在成功地写入项目之后,将调用`afterWrite`方法。如果写入时出现错误,则调用`onWriteError`方法。提供了遇到的异常和试图写入的项,以便可以对它们进行记录。 + +与此接口对应的注释是: + +* `@BeforeWrite` + +* `@AfterWrite` + +* `@OnWriteError` + +##### [](#skipListener)`SkipListener` + +`ItemReadListener`、`ItemProcessListener`和`ItemWriteListener`都提供了通知错误的机制,但没有一个通知你记录实际上已被跳过。例如,`onWriteError`即使一个项目被重试并成功,也会被调用。出于这个原因,有一个单独的接口用于跟踪跳过的项目,如以下接口定义所示: + +``` +public interface SkipListener extends StepListener { + + void onSkipInRead(Throwable t); + void onSkipInProcess(T item, Throwable t); + void onSkipInWrite(S item, Throwable t); + +} +``` + +`onSkipInRead`是在读取时跳过项时调用的。需要注意的是,回滚可能会导致同一项被多次注册为跳过一次。`onSkipInWrite`是在写入时跳过一项时调用的。因为该项已被成功读取(而不是跳过),所以还将该项本身作为参数提供给它。 + +与此接口对应的注释是: + +* `@OnSkipInRead` + +* `@OnSkipInWrite` + +* `@OnSkipInProcess` + +###### [](#skipListenersAndTransactions)跳过侦听器和事务 + +`SkipListener`最常见的用例之一是注销一个跳过的项,这样就可以使用另一个批处理过程甚至人工过程来评估和修复导致跳过的问题。因为在许多情况下原始事务可能会被回滚, Spring Batch 提供了两个保证: + +1. 每个项目只调用一次适当的 Skip 方法(取决于错误发生的时间)。 + +2. 总是在事务提交之前调用`SkipListener`。这是为了确保侦听器调用的任何事务资源不会因`ItemWriter`中的故障而回滚。 + +### [](#taskletStep)`TaskletStep` + +[面向块的处理](#chunkOrientedProcessing)并不是在`Step`中进行处理的唯一方法。如果`Step`必须包含一个简单的存储过程调用怎么办?你可以将调用实现为`ItemReader`,并在过程完成后返回 null。然而,这样做有点不自然,因为需要有一个 no-op`ItemWriter`。 Spring Batch 为此场景提供了`TaskletStep`。 + +`Tasklet`是一个简单的接口,它有一个方法`execute`,它被`TaskletStep`反复调用,直到它返回`RepeatStatus.FINISHED`或抛出异常来表示失败。对`Tasklet`的每个调用都包装在一个事务中。`Tasklet`实现器可以调用一个存储过程、一个脚本或一个简单的 SQL 更新语句。 + +要在 XML 中创建`TaskletStep`,``元素的’ref’属性应该引用定义`Tasklet`对象的 Bean。在``中不应该使用``元素。下面的示例展示了一个简单的任务: + +``` + + + +``` + +要在 Java 中创建`TaskletStep`,传递给构建器的`tasklet`方法的 Bean 应该实现`Tasklet`接口。在构建`TaskletStep`时,不应调用`chunk`。下面的示例展示了一个简单的任务: + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .tasklet(myTasklet()) + .build(); +} +``` + +| |`TaskletStep`如果实现`StepListener`接口,则自动将
任务集注册为`StepListener`。| +|---|-----------------------------------------------------------------------------------------------------------------------| + +#### [](#taskletAdapter)`TaskletAdapter` + +与`ItemReader`和`ItemWriter`接口的其他适配器一样,`Tasklet`接口包含一个允许自适应到任何预先存在的类的实现:`TaskletAdapter`。这可能有用的一个例子是现有的 DAO,该 DAO 用于更新一组记录上的标志。`TaskletAdapter`可以用来调用这个类,而不必为`Tasklet`接口编写适配器。 + +下面的示例展示了如何在 XML 中定义`TaskletAdapter`: + +XML 配置 + +``` + + + + + + +``` + +下面的示例展示了如何在 Java 中定义`TaskletAdapter`: + +Java 配置 + +``` +@Bean +public MethodInvokingTaskletAdapter myTasklet() { + MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter(); + + adapter.setTargetObject(fooDao()); + adapter.setTargetMethod("updateFoo"); + + return adapter; +} +``` + +#### [](#exampleTaskletImplementation)示例`Tasklet`实现 + +许多批处理作业包含一些步骤,这些步骤必须在主处理开始之前完成,以便设置各种资源,或者在处理完成之后清理这些资源。如果作业中的文件很多,那么在成功地将某些文件上传到另一个位置后,通常需要在本地删除这些文件。下面的示例(取自[Spring Batch samples project](https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples))是一个带有这样的职责的`Tasklet`实现: + +``` +public class FileDeletingTasklet implements Tasklet, InitializingBean { + + private Resource directory; + + public RepeatStatus execute(StepContribution contribution, + ChunkContext chunkContext) throws Exception { + File dir = directory.getFile(); + Assert.state(dir.isDirectory()); + + File[] files = dir.listFiles(); + for (int i = 0; i < files.length; i++) { + boolean deleted = files[i].delete(); + if (!deleted) { + throw new UnexpectedJobExecutionException("Could not delete file " + + files[i].getPath()); + } + } + return RepeatStatus.FINISHED; + } + + public void setDirectoryResource(Resource directory) { + this.directory = directory; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(directory, "directory must be set"); + } +} +``` + +前面的`tasklet`实现将删除给定目录中的所有文件。需要注意的是,`execute`方法只被调用一次。剩下的就是引用来自`step`的`tasklet`。 + +下面的示例展示了如何在 XML 中引用来自`step`的`tasklet`: + +XML 配置 + +``` + + + + + + + + + + + + + +``` + +下面的示例展示了如何在 Java 中引用来自`step`的`tasklet`: + +Java 配置 + +``` +@Bean +public Job taskletJob() { + return this.jobBuilderFactory.get("taskletJob") + .start(deleteFilesInDir()) + .build(); +} + +@Bean +public Step deleteFilesInDir() { + return this.stepBuilderFactory.get("deleteFilesInDir") + .tasklet(fileDeletingTasklet()) + .build(); +} + +@Bean +public FileDeletingTasklet fileDeletingTasklet() { + FileDeletingTasklet tasklet = new FileDeletingTasklet(); + + tasklet.setDirectoryResource(new FileSystemResource("target/test-outputs/test-dir")); + + return tasklet; +} +``` + +### [](#controllingStepFlow)控制阶跃流 + +在拥有一份工作的过程中,有了将步骤组合在一起的能力,就需要能够控制工作如何从一个步骤“流动”到另一个步骤。a`Step`失败并不一定意味着`Job`应该失败。此外,可能有不止一种类型的“成功”来决定下一步应该执行哪个`Step`。根据`Steps`组的配置方式,某些步骤甚至可能根本不会被处理。 + +#### [](#SequentialFlow)序贯流 + +最简单的流程场景是所有步骤都按顺序执行的作业,如下图所示: + +![顺序流动](./images/sequential-flow.png) + +图 4。顺序流动 + +这可以通过使用`step`中的“next”来实现。 + +下面的示例展示了如何在 XML 中使用`next`属性: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何在 Java 中使用`next()`方法: + +Java 配置 + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(stepA()) + .next(stepB()) + .next(stepC()) + .build(); +} +``` + +在上面的场景中,“步骤 A”首先运行,因为它是列出的第一个`Step`。如果“步骤 A”正常完成,那么“步骤 B”运行,依此类推。但是,如果“步骤 A”失败,则整个`Job`失败,并且“步骤 B”不执行。 + +| |对于 Spring 批处理 XML 命名空间,配置中列出的第一步是 *always*`Job`运行的第一步。其他步骤元素的顺序并不是
重要的,但是第一步必须始终首先出现在 XML 中。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#conditionalFlow)条件流 + +在上面的例子中,只有两种可能性: + +1. `step`成功,应该执行下一个`step`。 + +2. `step`失败,因此,`job`应该失败。 + +在许多情况下,这可能就足够了。但是,如果`step`的失败应该触发不同的`step`,而不是导致失败,那么在这种情况下该怎么办?下图显示了这样的流程: + +![条件流](./images/conditional-flow.png) + +图 5。条件流 + +为了处理更复杂的场景, Spring 批 XML 命名空间允许在 Step 元素中定义转换元素。一个这样的转换是`next`元素。与`next`属性类似,`next`元素告诉`Job`下一个执行的是`Step`。然而,与属性不同的是,在给定的`Step`上允许任意数量的`next`元素,并且在失败的情况下没有默认行为。这意味着,如果使用了转换元素,则必须显式地定义`Step`转换的所有行为。还请注意,单个步骤不能同时具有`next`属性和`transition`元素。 + +`next`元素指定要匹配的模式和接下来要执行的步骤,如以下示例所示: + +XML 配置 + +``` + + + + + + + + +``` + +Java API 提供了一组流畅的方法,允许你指定流程以及当步骤失败时要做什么。下面的示例显示了如何指定一个步骤(`stepA`),然后继续执行两个不同步骤中的任何一个(`stepB`和`stepC`),这取决于`stepA`是否成功: + +Java 配置 + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(stepA()) + .on("*").to(stepB()) + .from(stepA()).on("FAILED").to(stepC()) + .end() + .build(); +} +``` + +当使用 XML 配置时,转换元素的`on`属性使用一个简单的模式匹配方案来匹配执行`ExitStatus`所产生的`ExitStatus`。 + +当使用 Java 配置时,`on()`方法使用一个简单的模式匹配方案来匹配执行`ExitStatus`所产生的`ExitStatus`。 + +模式中只允许使用两个特殊字符: + +* “\*”匹配零个或多个字符 + +* “?”正好与一个字符匹配。 + +例如,“C\*t”匹配“cat”和“count”,而“C?t”匹配“cat”,但不匹配“count”。 + +虽然对`Step`上的转换元素的数量没有限制,但是如果`Step`执行导致元素不覆盖的`ExitStatus`,那么框架将抛出一个异常,而`Job`将失败。该框架自动命令从最特定到最不特定的转换。这意味着,即使在上面的示例中将顺序交换为“stepa”,`ExitStatus`的“failed”仍将转到“stepc”。 + +##### [](#batchStatusVsExitStatus)批处理状态与退出状态 + +在为条件流配置`Job`时,重要的是要理解`BatchStatus`和`ExitStatus`之间的区别。`BatchStatus`是一个枚举,它是`JobExecution`和`StepExecution`的属性,框架使用它来记录`Job`或`Step`的状态。它可以是以下值之一:`COMPLETED`,`STARTING`,`STARTED`,`STOPPING`,`STOPPED`,`FAILED`,`ABANDONED`,或`UNKNOWN`。其中大多数是不言自明的:`COMPLETED`是当一个步骤或作业成功完成时设置的状态,`FAILED`是当它失败时设置的状态,依此类推。 + +使用 XML 配置时,下面的示例包含“next”元素: + +``` + +``` + +使用 Java 配置时,下面的示例包含“on”元素: + +``` +... +.from(stepA()).on("FAILED").to(stepB()) +... +``` + +乍一看,“on”似乎引用了它所属的`Step`的`BatchStatus`。然而,它实际上引用了`ExitStatus`的`Step`。顾名思义,`ExitStatus`表示一个`Step`在完成执行后的状态。 + +更具体地说,当使用 XML 配置时,前面的 XML 配置示例中显示的“next”元素引用`ExitStatus`的退出代码。 + +当使用 Java 配置时,前面的 Java 配置示例中显示的“on()”方法引用`ExitStatus`的退出代码。 + +在英语中,它写着:“如果退出代码是`FAILED`,则转到 STEPB。”默认情况下,对于`Step`,退出代码始终与`BatchStatus`相同,这就是上面的条目有效的原因。但是,如果退出代码需要不同,该怎么办?一个很好的例子来自于 Samples 项目中的 Skip Sample 作业: + +下面的示例展示了如何使用 XML 中的不同退出代码: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何使用 Java 中的不同退出代码: + +Java 配置 + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()).on("FAILED").end() + .from(step1()).on("COMPLETED WITH SKIPS").to(errorPrint1()) + .from(step1()).on("*").to(step2()) + .end() + .build(); +} +``` + +`step1`有三种可能性: + +1. `Step`失败,在这种情况下,作业应该失败。 + +2. `Step`成功完成。 + +3. `Step`已成功完成,但退出代码为“与跳过一起完成”。在这种情况下,应该运行一个不同的步骤来处理错误。 + +前面的配置可以正常工作。但是,需要根据跳过记录的执行情况更改退出代码,如以下示例所示: + +``` +public class SkipCheckingListener extends StepExecutionListenerSupport { + public ExitStatus afterStep(StepExecution stepExecution) { + String exitCode = stepExecution.getExitStatus().getExitCode(); + if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && + stepExecution.getSkipCount() > 0) { + return new ExitStatus("COMPLETED WITH SKIPS"); + } + else { + return null; + } + } +} +``` + +上面的代码是`StepExecutionListener`,该代码首先检查以确保`Step`成功,然后检查`StepExecution`上的跳过计数是否高于 +0. 如果这两个条件都满足,则返回一个新的`ExitStatus`,其退出代码为`COMPLETED WITH SKIPS`。 + +#### [](#configuringForStop)配置停止 + +在讨论了[batchstatus 和 exitstatus](#batchStatusVsExitStatus)之后,人们可能想知道如何确定`BatchStatus`和`ExitStatus`的`Job`。虽然这些状态是由执行的代码为`Step`确定的,但`Job`的状态是基于配置确定的。 + +到目前为止,讨论的所有作业配置都至少有一个没有转换的最终`Step`。 + +在下面的 XML 示例中,在`step`执行之后,`Job`结束: + +``` + +``` + +在下面的 Java 示例中,在`step`执行之后,`Job`结束: + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()) + .build(); +} +``` + +如果没有为`Step`定义转换,则`Job`的状态定义如下: + +* 如果`Step`以`ExitStatus`结尾失败,则`BatchStatus`和`ExitStatus`中的`Job`都是`FAILED`。 + +* 否则,`BatchStatus`和`ExitStatus`的`Job`都是`COMPLETED`。 + +虽然这种终止批处理作业的方法对于某些批处理作业(例如简单的连续步骤作业)来说已经足够了,但可能需要自定义的作业停止场景。为此, Spring Batch 提供了三个转换元素来停止`Job`(除了我们前面讨论的[`next`元素](#NextElement))。这些停止元素中的每一个都以特定的`BatchStatus`停止`Job`。重要的是要注意,停止转换元件对`BatchStatus`中的任何`Steps`的`ExitStatus`或`ExitStatus`都没有影响。这些元素只影响`Job`的最终状态。例如,对于作业中的每一步,都可能具有`FAILED`的状态,但是对于作业,则可能具有`COMPLETED`的状态。 + +##### [](#endElement)以一步结尾 + +配置步骤结束指示`Job`以`BatchStatus`的`COMPLETED`停止。已经完成了 status`COMPLETED`的`Job`不能重新启动(框架抛出一个`JobInstanceAlreadyCompleteException`)。 + +在使用 XML 配置时,此任务使用“end”元素。`end`元素还允许一个可选的’exit-code’属性,该属性可用于自定义`Job`的`ExitStatus`。如果没有给出“exit-code”属性,则`ExitStatus`默认为`COMPLETED`,以匹配`BatchStatus`。 + +当使用 Java 配置时,此任务使用“end”方法。`end`方法还允许一个可选的’exitstatus’参数,该参数可用于自定义`Job`中的`ExitStatus`。如果不提供“exitstatus”值,则`ExitStatus`默认为`COMPLETED`,以匹配`BatchStatus`。 + +考虑以下场景:如果`step2`失败,则`Job`停止,`BatchStatus`的`COMPLETED`和`ExitStatus`的`COMPLETED`和`step3`不运行。否则,执行移动到`step3`。请注意,如果`step2`失败,则`Job`不可重启(因为状态是`COMPLETED`)。 + +下面的示例以 XML 形式展示了该场景: + +``` + + + + + + + + +``` + +下面的示例展示了 Java 中的场景: + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()) + .next(step2()) + .on("FAILED").end() + .from(step2()).on("*").to(step3()) + .end() + .build(); +} +``` + +##### [](#failElement)失败的步骤 + +配置在给定点失败的步骤指示`Job`以`BatchStatus`的`FAILED`停止。与 END 不同的是,`Job`的失败并不会阻止`Job`被重新启动。 + +在使用 XML 配置时,“fail”元素还允许一个可选的“exit-code”属性,该属性可用于自定义`Job`中的`ExitStatus`。如果没有给出“exit-code”属性,则`ExitStatus`默认为`FAILED`,以匹配`BatchStatus`。 + +考虑下面的场景,如果`step2`失败,则`Job`停止,`BatchStatus`的`FAILED`和`ExitStatus`的`EARLY TERMINATION`和`step3`不执行。否则,执行移动到`step3`。此外,如果`step2`失败,并且`Job`被重新启动,那么在`step2`上再次开始执行。 + +下面的示例以 XML 形式展示了该场景: + +XML 配置 + +``` + + + + + + + + +``` + +下面的示例展示了 Java 中的场景: + +Java 配置 + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()) + .next(step2()).on("FAILED").fail() + .from(step2()).on("*").to(step3()) + .end() + .build(); +} +``` + +##### [](#stopElement)在给定的步骤停止作业 + +将作业配置为在特定的步骤停止,将指示`Job`使用`BatchStatus`的`STOPPED`停止作业。停止`Job`可以在处理中提供临时中断,以便操作员可以在重新启动`Job`之前采取一些操作。 + +在使用 XML 配置时,“stop”元素需要一个“restart”属性,该属性指定了重新启动作业时执行应该在哪里进行的步骤。 + +当使用 Java 配置时,`stopAndRestart`方法需要一个“restart”属性,该属性指定作业重新启动时执行应该在哪里进行的步骤。 + +考虑以下场景:如果`step1`以`COMPLETE`结束,那么作业将停止。一旦重新启动,执行就开始于`step2`。 + +以下清单以 XML 形式展示了该场景: + +``` + + + + + +``` + +下面的示例展示了 Java 中的场景: + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()).on("COMPLETED").stopAndRestart(step2()) + .end() + .build(); +} +``` + +#### [](#programmaticFlowDecisions)程序化流程决策 + +在某些情况下,可能需要比`ExitStatus`更多的信息来决定下一步执行哪个步骤。在这种情况下,可以使用`JobExecutionDecider`来辅助决策,如以下示例所示: + +``` +public class MyDecider implements JobExecutionDecider { + public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) { + String status; + if (someCondition()) { + status = "FAILED"; + } + else { + status = "COMPLETED"; + } + return new FlowExecutionStatus(status); + } +} +``` + +在下面的示例作业配置中,`decision`指定了要使用的决策器以及所有转换: + +XML 配置 + +``` + + + + + + + + + + + + + +``` + +在下面的示例中,在使用 Java 配置时,实现`JobExecutionDecider`的 Bean 被直接传递到`next`调用。 + +Java 配置 + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()) + .next(decider()).on("FAILED").to(step2()) + .from(decider()).on("COMPLETED").to(step3()) + .end() + .build(); +} +``` + +#### [](#split-flows)拆分流 + +到目前为止描述的每个场景都涉及一个`Job`,它以线性方式一次执行一个步骤。 Spring 除了这种典型的样式之外,批处理还允许使用并行的流来配置作业。 + +XML 命名空间允许你使用“split”元素。正如下面的示例所示,“split”元素包含一个或多个“flow”元素,可以在其中定义整个单独的流。“拆分”元素还可以包含前面讨论过的任何转换元素,例如“next”属性或“next”、“end”或“fail”元素。 + +``` + + + + + + + + + + +``` + +基于 Java 的配置允许你通过提供的构建器配置分割。正如下面的示例所示,“split”元素包含一个或多个“flow”元素,可以在其中定义整个单独的流。“拆分”元素还可以包含前面讨论过的任何转换元素,例如“next”属性或“next”、“end”或“fail”元素。 + +``` +@Bean +public Flow flow1() { + return new FlowBuilder("flow1") + .start(step1()) + .next(step2()) + .build(); +} + +@Bean +public Flow flow2() { + return new FlowBuilder("flow2") + .start(step3()) + .build(); +} + +@Bean +public Job job(Flow flow1, Flow flow2) { + return this.jobBuilderFactory.get("job") + .start(flow1) + .split(new SimpleAsyncTaskExecutor()) + .add(flow2) + .next(step4()) + .end() + .build(); +} +``` + +#### [](#external-flows)外部化作业之间的流定义和依赖关系 + +作业中的部分流可以作为单独的 Bean 定义外部化,然后重新使用。有两种方法可以做到这一点。第一种方法是简单地将流声明为对别处定义的流的引用。 + +下面的示例展示了如何将流声明为对 XML 中其他地方定义的流的引用: + +XML 配置 + +``` + + + + + + + + + +``` + +下面的示例展示了如何将流声明为对 Java 中其他地方定义的流的引用: + +Java 配置 + +``` +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(flow1()) + .next(step3()) + .end() + .build(); +} + +@Bean +public Flow flow1() { + return new FlowBuilder("flow1") + .start(step1()) + .next(step2()) + .build(); +} +``` + +如前面的示例所示,定义外部流的效果是将外部流中的步骤插入到作业中,就好像这些步骤是内联声明的一样。通过这种方式,许多作业可以引用相同的模板流,并将这样的模板组合成不同的逻辑流。这也是分离单个流的集成测试的一种好方法。 + +外部化流程的另一种形式是使用`JobStep`。a`JobStep`类似于 a`FlowStep`,但实际上是为指定的流程中的步骤创建并启动一个单独的作业执行。 + +下面的示例是 XML 中`JobStep`的示例: + +XML 配置 + +``` + + + + + + +... + + + + +``` + +下面的示例显示了 Java 中`JobStep`的示例: + +Java 配置 + +``` +@Bean +public Job jobStepJob() { + return this.jobBuilderFactory.get("jobStepJob") + .start(jobStepJobStep1(null)) + .build(); +} + +@Bean +public Step jobStepJobStep1(JobLauncher jobLauncher) { + return this.stepBuilderFactory.get("jobStepJobStep1") + .job(job()) + .launcher(jobLauncher) + .parametersExtractor(jobParametersExtractor()) + .build(); +} + +@Bean +public Job job() { + return this.jobBuilderFactory.get("job") + .start(step1()) + .build(); +} + +@Bean +public DefaultJobParametersExtractor jobParametersExtractor() { + DefaultJobParametersExtractor extractor = new DefaultJobParametersExtractor(); + + extractor.setKeys(new String[]{"input.file"}); + + return extractor; +} +``` + +作业参数提取器是一种策略,它确定如何将`Step`的`ExecutionContext`转换为正在运行的`JobParameters`的`JobParameters`。当你希望有一些更细粒度的选项来监视和报告作业和步骤时,`JobStep`非常有用。使用`JobStep`通常也是对这个问题的一个很好的回答:“我如何在工作之间创建依赖关系?”这是一种很好的方法,可以将一个大型系统分解成更小的模块,并控制工作流程。 + +### [](#late-binding)`Job`和`Step`属性的后期绑定 + +前面显示的 XML 和平面文件示例都使用 Spring `Resource`抽象来获取文件。这是因为`Resource`有一个`getFile`方法,它返回一个`java.io.File`。XML 和平面文件资源都可以使用标准的 Spring 构造进行配置: + +下面的示例展示了 XML 中的后期绑定: + +XML 配置 + +``` + + + +``` + +下面的示例展示了 Java 中的后期绑定: + +Java 配置 + +``` +@Bean +public FlatFileItemReader flatFileItemReader() { + FlatFileItemReader reader = new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource("file://outputs/file.txt")) + ... +} +``` + +前面的`Resource`从指定的文件系统位置加载文件。请注意,绝对位置必须以双斜杠(`//`)开始。在大多数 Spring 应用程序中,这种解决方案已经足够好了,因为这些资源的名称在编译时是已知的。然而,在批处理场景中,可能需要在运行时确定文件名作为作业的参数。这可以通过使用“-D”参数读取系统属性来解决。 + +下面的示例展示了如何从 XML 中的属性读取文件名: + +XML 配置 + +``` + + + +``` + +下面展示了如何从 Java 中的属性读取文件名: + +Java 配置 + +``` +@Bean +public FlatFileItemReader flatFileItemReader(@Value("${input.file.name}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +要使这个解决方案起作用,所需要的只是一个系统参数(例如`-Dinput.file.name="file://outputs/file.txt"`)。 + +| |虽然在这里可以使用`PropertyPlaceholderConfigurer`,但是如果系统属性始终设置,则不需要
,因为 Spring `ResourceEditor`中的
已经对系统属性进行了筛选和占位符替换。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通常,在批处理设置中,最好是在作业的`JobParameters`中参数化文件名,而不是通过系统属性,并以这种方式访问它们。为了实现这一点, Spring 批处理允许各种`Job`和`Step`属性的后期绑定。 + +下面的示例展示了如何用 XML 参数化一个文件名: + +XML 配置 + +``` + + + +``` + +下面的示例展示了如何在 Java 中参数化一个文件名: + +Java 配置 + +``` +@StepScope +@Bean +public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['input.file.name']}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +`JobExecution`和`StepExecution`级别`ExecutionContext`都可以以相同的方式访问。 + +下面的示例展示了如何访问 XML 中的`ExecutionContext`: + +XML 配置 + +``` + + + +``` + +XML 配置 + +``` + + + +``` + +下面的示例展示了如何在 Java 中访问`ExecutionContext`: + +Java 配置 + +``` +@StepScope +@Bean +public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.file.name']}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +Java 配置 + +``` +@StepScope +@Bean +public FlatFileItemReader flatFileItemReader(@Value("#{stepExecutionContext['input.file.name']}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +| |任何使用 late-binding 的 Bean 都必须用 scope=“step”声明。有关更多信息,请参见[Step Scope](#step-scope)。应该注意的是
a`Step` Bean 不应该是步骤作用域。如果在
定义的步骤中需要进行后期绑定,则该步骤的组件(即 tasklet、Item Reader/Writer 等)
是应该被限定范围的组件。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果你正在使用 Spring 3.0(或更高版本),则步骤作用域 bean 中的表达式使用
Spring 表达式语言,这是一种功能强大的通用语言,具有许多有趣的
特性。为了提供向后兼容性,如果 Spring 批检测到
Spring 的旧版本的存在,则它使用一种功能不那么强大的原生表达式语言和具有略有不同的解析规则的
。主要的区别在于,在
上面的示例中的 MAP 键不需要引用 Spring 2.5,但是在 Spring 3.0 中的引用是强制性的
。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#step-scope)步骤作用域 + +前面显示的所有延迟绑定示例都在 Bean 定义中声明了“步骤”的范围。 + +下面的示例展示了在 XML 中绑定到 STEP 作用域的示例: + +XML 配置 + +``` + + + +``` + +下面的示例展示了在 Java 中绑定到 STEP 作用域的示例: + +Java 配置 + +``` +@StepScope +@Bean +public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input.file.name]}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +使用 late binding 需要使用`Step`的作用域,因为在`Step`开始之前,实际上不能实例化 Bean,以允许找到属性。因为默认情况下它不是 Spring 容器的一部分,所以必须通过使用`batch`名称空间,或者通过显式地为`StepScope`包含一个 Bean 定义,或者通过使用`@EnableBatchProcessing`注释,显式地添加作用域。只使用其中一种方法。下面的示例使用`batch`名称空间: + +``` + + +... + +``` + +下面的示例明确地包括 Bean 定义: + +``` + +``` + +#### [](#job-scope)工作范围 + +在 Spring 批 3.0 中引入的`Job`作用域在配置中类似于`Step`作用域,但它是`Job`上下文的作用域,因此每个运行的作业只有一个这样的 Bean 实例。此外,还支持使用`#{..}`占位符从`JobContext`访问的引用的后期绑定。使用此特性, Bean 可以从作业或作业执行上下文和作业参数中提取属性。 + +下面的示例展示了在 XML 中绑定到作业范围的示例: + +XML 配置 + +``` + + + +``` + +XML 配置 + +``` + + + +``` + +下面的示例展示了在 Java 中绑定到作业范围的示例: + +Java 配置 + +``` +@JobScope +@Bean +public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input]}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +Java 配置 + +``` +@JobScope +@Bean +public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.name']}") String name) { + return new FlatFileItemReaderBuilder() + .name("flatFileItemReader") + .resource(new FileSystemResource(name)) + ... +} +``` + +因为默认情况下它不是 Spring 容器的一部分,所以必须通过使用`batch`命名空间,通过显式地为 JobScope 包括一个 Bean 定义,或者使用`@EnableBatchProcessing`注释(但不是所有的),显式地添加范围。下面的示例使用`batch`名称空间: + +``` + + + +... + +``` + +下面的示例包括显式定义`JobScope`的 Bean: + +``` + +``` + +| |在多线程
或分区步骤中使用作业范围的 bean 有一些实际的限制。 Spring 批处理不控制在这些
用例中产生的线程,因此不可能正确地设置它们以使用这样的 bean。因此,
不建议在多线程或分区步骤中使用作业范围的 bean。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| \ No newline at end of file diff --git a/docs/spring-batch/testing.md b/docs/spring-batch/testing.md new file mode 100644 index 0000000..9c36c42 --- /dev/null +++ b/docs/spring-batch/testing.md @@ -0,0 +1,274 @@ +# 单元测试 + +## [](#testing)单元测试 + +XMLJavaBoth + +与其他应用程序样式一样,对作为批处理作业的一部分编写的任何代码进行单元测试是非常重要的。 Spring 核心文档非常详细地介绍了如何使用 Spring 进行单元和集成测试,因此在此不再赘述。然而,重要的是要考虑如何“端到端”地测试批处理作业,这就是本章所涵盖的内容。 Spring-batch-test 项目包括促进这种端到端测试方法的类。 + +### [](#creatingUnitTestClass)创建单元测试类 + +为了让单元测试运行批处理作业,框架必须加载作业的应用上下文。使用两个注释来触发此行为: + +* `@RunWith(SpringJUnit4ClassRunner.class)`:表示类应该使用 Spring 的 JUnit 工具 + +* `@ContextConfiguration(…​)`:指示使用哪些资源配置`ApplicationContext`。 + +从 V4.1 开始,还可以使用`@SpringBatchTest`注释在测试上下文中注入 Spring 批测试实用程序,如`JobLauncherTestUtils`和`JobRepositoryTestUtils`。 + +| |需要注意的是,`JobLauncherTestUtils`需要`Job` Bean,`JobRepositoryTestUtils`需要`DataSource` Bean。由于`@SpringBatchTest`在测试
上下文中注册了一个`JobLauncherTestUtils`和一个`JobRepositoryTestUtils`,因此预计测试上下文包含一个用于`Job`和`DataSource`的单独的 AutoWire 候选项
(要么是一个单独的 Bean 定义,要么是
注释为`org.springframework.context.annotation.Primary`)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的 Java 示例显示了正在使用的注释: + +使用 Java 配置 + +``` +@SpringBatchTest +@RunWith(SpringRunner.class) +@ContextConfiguration(classes=SkipSampleConfiguration.class) +public class SkipSampleFunctionalTests { ... } +``` + +下面的 XML 示例显示了正在使用的注释: + +使用 XML 配置 + +``` +@SpringBatchTest +@RunWith(SpringRunner.class) +@ContextConfiguration(locations = { "/simple-job-launcher-context.xml", + "/jobs/skipSampleJob.xml" }) +public class SkipSampleFunctionalTests { ... } +``` + +### [](#endToEndTesting)批处理作业的端到端测试 + +“端到端”测试可以定义为从开始到结束测试批处理作业的完整运行。这允许测试设置测试条件、执行作业并验证最终结果。 + +考虑一个从数据库读取并写入平面文件的批处理作业的示例。测试方法从使用测试数据建立数据库开始。它清除 Customer 表,然后插入 10 个新记录。然后,测试使用`launchJob()`方法启动`Job`。`launchJob()`方法由`JobLauncherTestUtils`类提供。`JobLauncherTestUtils`类还提供了`launchJob(JobParameters)`方法,该方法允许测试给出特定的参数。`launchJob()`方法返回`JobExecution`对象,该对象对于断言有关`Job`运行的特定信息非常有用。在下面的情况下,测试验证`Job`以状态“完成”结束。 + +以下清单以 XML 形式展示了该示例: + +基于 XML 的配置 + +``` +@SpringBatchTest +@RunWith(SpringRunner.class) +@ContextConfiguration(locations = { "/simple-job-launcher-context.xml", + "/jobs/skipSampleJob.xml" }) +public class SkipSampleFunctionalTests { + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + private SimpleJdbcTemplate simpleJdbcTemplate; + + @Autowired + public void setDataSource(DataSource dataSource) { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + } + + @Test + public void testJob() throws Exception { + simpleJdbcTemplate.update("delete from CUSTOMER"); + for (int i = 1; i <= 10; i++) { + simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)", + i, "customer" + i); + } + + JobExecution jobExecution = jobLauncherTestUtils.launchJob(); + + Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode()); + } +} +``` + +下面的清单展示了 Java 中的示例: + +基于 Java 的配置 + +``` +@SpringBatchTest +@RunWith(SpringRunner.class) +@ContextConfiguration(classes=SkipSampleConfiguration.class) +public class SkipSampleFunctionalTests { + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + private SimpleJdbcTemplate simpleJdbcTemplate; + + @Autowired + public void setDataSource(DataSource dataSource) { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + } + + @Test + public void testJob() throws Exception { + simpleJdbcTemplate.update("delete from CUSTOMER"); + for (int i = 1; i <= 10; i++) { + simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)", + i, "customer" + i); + } + + JobExecution jobExecution = jobLauncherTestUtils.launchJob(); + + Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode()); + } +} +``` + +### [](#testingIndividualSteps)测试单个步骤 + +对于复杂的批处理作业,端到端测试方法中的测试用例可能变得难以管理。如果是这些情况,那么让测试用例自行测试单个步骤可能会更有用。`AbstractJobTests`类包含一个名为`launchStep`的方法,该方法使用一个步骤名并仅运行特定的`Step`。这种方法允许更有针对性的测试,让测试只为该步骤设置数据,并直接验证其结果。下面的示例展示了如何使用`launchStep`方法按名称加载`Step`: + +``` +JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep"); +``` + +### [](#testing-step-scoped-components)测试步骤范围内的组件 + +通常,在运行时为你的步骤配置的组件使用步骤作用域和后期绑定从步骤或作业执行中注入上下文。这些作为独立组件进行测试是很棘手的,除非你有一种方法来设置上下文,就好像它们是在一个步骤执行中一样。这是 Spring 批处理中两个组件的目标:`StepScopeTestExecutionListener`和`StepScopeTestUtils`。 + +侦听器是在类级别声明的,它的工作是为每个测试方法创建一个步骤执行上下文,如下面的示例所示: + +``` +@ContextConfiguration +@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, + StepScopeTestExecutionListener.class }) +@RunWith(SpringRunner.class) +public class StepScopeTestExecutionListenerIntegrationTests { + + // This component is defined step-scoped, so it cannot be injected unless + // a step is active... + @Autowired + private ItemReader reader; + + public StepExecution getStepExecution() { + StepExecution execution = MetaDataInstanceFactory.createStepExecution(); + execution.getExecutionContext().putString("input.data", "foo,bar,spam"); + return execution; + } + + @Test + public void testReader() { + // The reader is initialized and bound to the input data + assertNotNull(reader.read()); + } + +} +``` + +有两个`TestExecutionListeners`。一种是常规 Spring 测试框架,它处理从配置的应用程序上下文注入的依赖项,以注入读取器。另一个是 Spring 批`StepScopeTestExecutionListener`。它的工作方式是在`StepExecution`的测试用例中寻找工厂方法,并将其用作测试方法的上下文,就好像该执行在运行时在`Step`中是活动的一样。通过其签名来检测工厂方法(它必须返回`StepExecution`)。如果没有提供工厂方法,则创建一个默认的`StepExecution`。 + +从 V4.1 开始,如果测试类被注释为`@SpringBatchTest`,则将`StepScopeTestExecutionListener`和`JobScopeTestExecutionListener`作为测试执行侦听器导入。前面的测试示例可以配置如下: + +``` +@SpringBatchTest +@RunWith(SpringRunner.class) +@ContextConfiguration +public class StepScopeTestExecutionListenerIntegrationTests { + + // This component is defined step-scoped, so it cannot be injected unless + // a step is active... + @Autowired + private ItemReader reader; + + public StepExecution getStepExecution() { + StepExecution execution = MetaDataInstanceFactory.createStepExecution(); + execution.getExecutionContext().putString("input.data", "foo,bar,spam"); + return execution; + } + + @Test + public void testReader() { + // The reader is initialized and bound to the input data + assertNotNull(reader.read()); + } + +} +``` + +如果你希望将步骤作用域的持续时间作为测试方法的执行时间,那么侦听器方法是很方便的。对于更灵活但更具侵入性的方法,可以使用`StepScopeTestUtils`。下面的示例计算上一个示例中所示的阅读器中可用的项数: + +``` +int count = StepScopeTestUtils.doInStepScope(stepExecution, + new Callable() { + public Integer call() throws Exception { + + int count = 0; + + while (reader.read() != null) { + count++; + } + return count; + } +}); +``` + +### [](#validatingOutputFiles)验证输出文件 + +当批处理作业写到数据库时,很容易查询数据库以验证输出是否如预期的那样。然而,如果批处理作业写入文件,那么验证输出也同样重要。 Spring Batch 提供了一个名为的类,以便于对输出文件进行验证。名为`assertFileEquals`的方法接受两个`File`对象(或两个`Resource`对象),并逐行断言这两个文件具有相同的内容。因此,可以创建一个具有预期输出的文件,并将其与实际结果进行比较,如下例所示: + +``` +private static final String EXPECTED_FILE = "src/main/resources/data/input.txt"; +private static final String OUTPUT_FILE = "target/test-outputs/output.txt"; + +AssertFile.assertFileEquals(new FileSystemResource(EXPECTED_FILE), + new FileSystemResource(OUTPUT_FILE)); +``` + +### [](#mockingDomainObjects)模拟域对象 + +在为 Spring 批处理组件编写单元和集成测试时遇到的另一个常见问题是如何模拟域对象。一个很好的例子是`StepExecutionListener`,如以下代码片段所示: + +``` +public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport { + + public ExitStatus afterStep(StepExecution stepExecution) { + if (stepExecution.getReadCount() == 0) { + return ExitStatus.FAILED; + } + return null; + } +} +``` + +前面的侦听器示例是由框架提供的,它检查`StepExecution`是否有空读计数,因此表示没有完成任何工作。虽然这个示例相当简单,但它用于说明在试图对实现需要 Spring 批处理域对象的接口的测试类进行单元测试时可能遇到的问题类型。在前面的示例中,考虑下面的监听器单元测试: + +``` +private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener(); + +@Test +public void noWork() { + StepExecution stepExecution = new StepExecution("NoProcessingStep", + new JobExecution(new JobInstance(1L, new JobParameters(), + "NoProcessingJob"))); + + stepExecution.setExitStatus(ExitStatus.COMPLETED); + stepExecution.setReadCount(0); + + ExitStatus exitStatus = tested.afterStep(stepExecution); + assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode()); +} +``` + +因为 Spring 批处理域模型遵循良好的面向对象原则,所以`StepExecution`需要一个`JobExecution`,这需要一个`JobInstance`和`JobParameters`,以创建一个有效的`StepExecution`。虽然这在固态域模型中很好,但它确实使为单元测试创建存根对象变得非常详细。为了解决这个问题, Spring 批测试模块包括一个用于创建域对象的工厂:`MetaDataInstanceFactory`。给定这个工厂,单元测试可以更新得更简洁,如下例所示: + +``` +private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener(); + +@Test +public void testAfterStep() { + StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(); + + stepExecution.setExitStatus(ExitStatus.COMPLETED); + stepExecution.setReadCount(0); + + ExitStatus exitStatus = tested.afterStep(stepExecution); + assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode()); +} +``` + +用于创建简单`StepExecution`的前面的方法只是工厂中可用的一种方便的方法。完整的方法列表可以在其[Javadoc](https://docs.spring.io/spring-batch/apidocs/org/springframework/batch/test/MetaDataInstanceFactory.html)中找到。 \ No newline at end of file diff --git a/docs/spring-batch/transaction-appendix.md b/docs/spring-batch/transaction-appendix.md new file mode 100644 index 0000000..8b76ead --- /dev/null +++ b/docs/spring-batch/transaction-appendix.md @@ -0,0 +1,230 @@ +# 批处理和交易 + +## [](#transactions)附录 A:批处理和事务 + +### [](#transactionsNoRetry)不需要重试的简单批处理 + +考虑以下简单的嵌套批处理示例,该批处理不需要重试。它展示了批处理的一个常见场景:一个输入源被处理到耗尽,并且我们在处理的“块”结束时定期提交。 + +``` +1 | REPEAT(until=exhausted) { +| +2 | TX { +3 | REPEAT(size=5) { +3.1 | input; +3.2 | output; +| } +| } +| +| } +``` + +输入操作(3.1)可以是基于消息的接收(例如来自 JMS),也可以是基于文件的读取,但是要恢复并继续处理并有可能完成整个工作,它必须是事务性的。这同样适用于 3.2 的运算。它必须是事务性的或幂等的。 + +如果`REPEAT`(3)处的块由于 3.2 处的数据库异常而失败,那么`TX`(2)必须回滚整个块。 + +### [](#transactionStatelessRetry)简单无状态重试 + +对于非事务性的操作,例如对 Web 服务或其他远程资源的调用,使用重试也很有用,如下面的示例所示: + +``` +0 | TX { +1 | input; +1.1 | output; +2 | RETRY { +2.1 | remote access; +| } +| } +``` + +这实际上是重试中最有用的应用程序之一,因为与数据库更新相比,远程调用更有可能失败并可重试。只要远程访问(2.1)最终成功,事务`TX`(0)就提交。如果远程访问(2.1)最终失败,那么事务`TX`(0)将保证回滚。 + +### [](#repeatRetry)典型的重复重试模式 + +最典型的批处理模式是向块的内部块添加重试,如以下示例所示: + +``` +1 | REPEAT(until=exhausted, exception=not critical) { +| +2 | TX { +3 | REPEAT(size=5) { +| +4 | RETRY(stateful, exception=deadlock loser) { +4.1 | input; +5 | } PROCESS { +5.1 | output; +6 | } SKIP and RECOVER { +| notify; +| } +| +| } +| } +| +| } +``` + +内部`RETRY`(4)块被标记为“有状态”。关于有状态重试的描述,请参见[典型的用例](#transactionsNoRetry)。这意味着,如果重试`PROCESS`(5)块失败,则`RETRY`(4)的行为如下: + +1. 抛出一个异常,在块级别回滚事务`TX`(2),并允许将项重新呈现到输入队列中。 + +2. 当项目重新出现时,它可能会根据现有的重试策略被重试,再次执行`PROCESS`(5)。第二次和随后的尝试可能会再次失败,并重新抛出异常。 + +3. 最终,该项目将在最后一次出现。重试策略不允许另一次尝试,因此`PROCESS`(5)永远不会执行。在这种情况下,我们遵循`RECOVER`(6)路径,有效地“跳过”已接收和正在处理的项。 + +请注意,上面的计划中用于`RETRY`(4)的符号显式地显示了输入步骤(4.1)是重试的一部分。它还清楚地表明,有两种可供选择的处理路径:正常情况(用`PROCESS`(5)表示),以及恢复路径(在单独的块中用`RECOVER`(6)表示)。这两条可供选择的道路是完全不同的。在正常情况下只有一次。 + +在特殊情况下(例如特殊的`TransactionValidException`类型),重试策略可能能够确定`RECOVER`(6)路径可以在`PROCESS`(5)刚刚失败之后的最后一次尝试中使用,而不是等待项目被重新呈现。这不是默认的行为,因为它需要详细了解`PROCESS`(5)块内部发生了什么,而这通常是不可用的。例如,如果输出包括在失败之前的写访问,那么应该重新抛出异常,以确保事务 Integrity。 + +外部`REPEAT`(1)中的完成策略对于上述计划的成功至关重要。如果输出(5.1)失败,它可能会抛出一个异常(如所描述的,它通常会抛出),在这种情况下,事务`TX`(2)失败,并且异常可能会通过外部批处理`REPEAT`(1)向上传播。我们不希望整个批处理停止,因为如果我们再次尝试,`RETRY`(4)仍然可能成功,因此我们将`exception=not critical`添加到外部`REPEAT`(1)。 + +但是,请注意,如果`TX`(2)失败并且我们*做*再试一次,根据外部完成策略,在内部`REPEAT`(3)中下一个处理的项并不能保证就是刚刚失败的项。它可能是,但它取决于输入的实现(4.1)。因此,输出(5.1)可能在新项或旧项上再次失败。批处理的客户机不应假定每次`RETRY`(4)尝试处理的项与上次失败的尝试处理的项相同。例如,如果`REPEAT`(1)的终止策略是在 10 次尝试后失败,则它在连续 10 次尝试后失败,但不一定在同一项上失败。这与总体重试策略是一致的。内部`RETRY`(4)了解每个项目的历史,并可以决定是否对它进行另一次尝试。 + +### [](#asyncChunkProcessing)异步块处理 + +通过将外部批配置为使用`AsyncTaskExecutor`,可以同时执行[典型例子](#repeatRetry)中的内部批或块。外部批处理在完成之前等待所有的块完成。下面的示例展示了异步块处理: + +``` +1 | REPEAT(until=exhausted, concurrent, exception=not critical) { +| +2 | TX { +3 | REPEAT(size=5) { +| +4 | RETRY(stateful, exception=deadlock loser) { +4.1 | input; +5 | } PROCESS { +| output; +6 | } RECOVER { +| recover; +| } +| +| } +| } +| +| } +``` + +### [](#asyncItemProcessing)异步项处理 + +在[典型例子](#repeatRetry)中,以块为单位的单个项目原则上也可以同时处理。在这种情况下,事务边界必须移动到单个项的级别,以便每个事务都在单个线程上,如以下示例所示: + +``` +1 | REPEAT(until=exhausted, exception=not critical) { +| +2 | REPEAT(size=5, concurrent) { +| +3 | TX { +4 | RETRY(stateful, exception=deadlock loser) { +4.1 | input; +5 | } PROCESS { +| output; +6 | } RECOVER { +| recover; +| } +| } +| +| } +| +| } +``` + +这个计划牺牲了优化的好处,这也是简单计划的好处,因为它将所有事务资源合并在一起。只有当处理(5)的成本远高于事务管理(3)的成本时,它才是有用的。 + +### [](#transactionPropagation)批处理和事务传播之间的交互 + +批处理重试和事务管理之间的耦合比我们理想的更紧密。特别是,无状态重试不能用于使用不支持嵌套传播的事务管理器重试数据库操作。 + +下面的示例使用重试而不重复: + +``` +1 | TX { +| +1.1 | input; +2.2 | database access; +2 | RETRY { +3 | TX { +3.1 | database access; +| } +| } +| +| } +``` + +同样,出于同样的原因,内部事务`TX`(3)可以导致外部事务`TX`(1)失败,即使`RETRY`(2)最终成功。 + +不幸的是,相同的效果会从重试块渗透到周围的重复批处理(如果有的话),如下面的示例所示: + +``` +1 | TX { +| +2 | REPEAT(size=5) { +2.1 | input; +2.2 | database access; +3 | RETRY { +4 | TX { +4.1 | database access; +| } +| } +| } +| +| } +``` + +现在,如果 TX(3)回滚,它可能会污染 TX(1)处的整个批次,并迫使它在最后回滚。 + +那么非默认传播呢? + +* 在前面的示例中,如果两个事务最终都成功,`PROPAGATION_REQUIRES_NEW`at`TX`(3)可以防止外部`TX`(1)被污染。但是如果`TX`(3)提交并且`TX`(1)回滚,那么`TX`(3)保持提交,因此我们违反了`TX`(1)的交易契约。如果`TX`(3)回滚,`TX`(1)不一定(但在实践中可能会这样做,因为重试会抛出一个回滚异常)。 + +* `PROPAGATION_NESTED`at`TX`(3)在重试情况下(对于具有跳过的批处理),按照我们的要求工作:`TX`(3)可以提交,但随后由外部事务回滚,`TX`(1)。如果`TX`(3)回滚,则`TX`(1)在实践中回滚。此选项仅在某些平台上可用,不包括 Hibernate 或 JTA,但它是唯一一个始终有效的选项。 + +因此,如果重试块包含任何数据库访问,`NESTED`模式是最好的。 + +### [](#specialTransactionOrthogonal)特殊情况:使用正交资源的事务 + +对于没有嵌套数据库事务的简单情况,默认传播总是 OK 的。考虑以下示例,其中`SESSION`和`TX`不是全局`XA`资源,因此它们的资源是正交的: + +``` +0 | SESSION { +1 | input; +2 | RETRY { +3 | TX { +3.1 | database access; +| } +| } +| } +``` + +这里有一个事务消息`SESSION`(0),但是它不参与`PlatformTransactionManager`的其他事务,因此当`TX`(3)开始时它不会传播。在`RETRY`(2)块之外没有数据库访问权限。如果`TX`(3)失败,然后在重试时最终成功,`SESSION`(0)可以提交(独立于`TX`块)。这类似于普通的“尽最大努力-一阶段-提交”场景。当`RETRY`(2)成功而`SESSION`(0)无法提交(例如,因为消息系统不可用)时,可能发生的最坏情况是重复消息。 + +### [](#statelessRetryCannotRecover)无状态重试无法恢复 + +在上面的典型示例中,无状态重试和有状态重试之间的区别很重要。它实际上最终是一个事务性约束,它强制了这种区别,并且这种约束也使区别存在的原因变得很明显。 + +我们首先观察到,除非我们在事务中包装项目处理,否则无法跳过失败的项目并成功提交块的其余部分。因此,我们将典型的批处理执行计划简化如下: + +``` +0 | REPEAT(until=exhausted) { +| +1 | TX { +2 | REPEAT(size=5) { +| +3 | RETRY(stateless) { +4 | TX { +4.1 | input; +4.2 | database access; +| } +5 | } RECOVER { +5.1 | skip; +| } +| +| } +| } +| +| } +``` + +前面的示例显示了一个带有`RECOVER`(5)路径的无状态`RETRY`(3),该路径在最后一次尝试失败后启动。`stateless`标签意味着可以重复该块,而不会将任何异常重新抛出到某个限制。这仅在事务`TX`(4)具有传播嵌套时才有效。 + +如果内部`TX`(4)具有默认的传播属性并回滚,则会污染外部`TX`(1)。事务管理器假定内部事务已经损坏了事务资源,因此不能再次使用它。 + +对嵌套传播的支持非常少,因此我们选择在 Spring 批处理的当前版本中不支持使用无状态重试的恢复。通过使用上面的典型模式,总是可以实现相同的效果(以重复更多处理为代价)。 \ No newline at end of file diff --git a/docs/spring-batch/whatsnew.md b/docs/spring-batch/whatsnew.md new file mode 100644 index 0000000..9885616 --- /dev/null +++ b/docs/spring-batch/whatsnew.md @@ -0,0 +1,128 @@ +# 最新更新在 Spring 批 4.3 中 + +## 在 Spring 批 4.3 中[](#whatsNew)最新更新 + +这个版本附带了许多新特性、性能改进、依赖更新和 API 修改。这一节描述了最重要的变化。有关更改的完整列表,请参阅[发行说明](https://github.com/spring-projects/spring-batch/releases/tag/4.3.0)。 + +### [](#newFeatures)新功能 + +#### [](#new-synchronized-itemstreamwriter)新建同步 ItemStreamWriter + +与`SynchronizedItemStreamReader`类似,该版本引入了`SynchronizedItemStreamWriter`。这个特性在多线程的步骤中很有用,在这些步骤中,并发线程需要同步,以避免覆盖彼此的写操作。 + +#### [](#new-jpaqueryprovider-for-named-queries)用于命名查询的新 JPaqueryProvider + +这个版本在`JpaNativeQueryProvider`旁边引入了一个新的`JpaNamedQueryProvider`,以便在使用`JpaPagingItemReader`时简化 JPA 命名查询的配置: + +``` +JpaPagingItemReader reader = new JpaPagingItemReaderBuilder() + .name("fooReader") + .queryProvider(new JpaNamedQueryProvider("allFoos", Foo.class)) + // set other properties on the reader + .build(); +``` + +#### [](#new-jpacursoritemreader-implementation)新的 jpacursoritemreader 实现 + +JPA 2.2 增加了将结果作为游标而不是只进行分页的能力。该版本引入了一种新的 JPA 项读取器,该读取器使用此功能以类似于`JdbcCursorItemReader`和`HibernateCursorItemReader`的基于光标的方式流式传输结果。 + +#### [](#new-jobparametersincrementer-implementation)新 JobParametersIncrementer 实现 + +与`RunIdIncrementer`类似,这个版本添加了一个新的`JobParametersIncrementer`,它基于 Spring 框架中的`DataFieldMaxValueIncrementer`。 + +#### [](#graalvm-support)graalvm 支持 + +这个版本增加了在 GraalVM 上运行 Spring 批处理应用程序的初始支持。该支持仍处于实验阶段,并将在未来的版本中进行改进。 + +#### [](#java-records-support)Java 记录支持 + +这个版本增加了在面向块的步骤中使用 Java 记录作为项的支持。新添加的`RecordFieldSetMapper`支持从平面文件到 Java 记录的数据映射,如以下示例所示: + +``` +@Bean +public FlatFileItemReader itemReader() { + return new FlatFileItemReaderBuilder() + .name("personReader") + .resource(new FileSystemResource("persons.csv")) + .delimited() + .names("id", "name") + .fieldSetMapper(new RecordFieldSetMapper<>(Person.class)) + .build(); +} +``` + +在这个示例中,`Person`类型是一个 Java 记录,定义如下: + +``` +public record Person(int id, String name) { } +``` + +`FlatFileItemReader`使用新的`RecordFieldSetMapper`将来自`persons.csv`文件的数据映射到类型`Person`的记录。 + +### [](#performanceImprovements)性能改进 + +#### [](#use-bulk-writes-in-repositoryitemwriter)在 RepositorYitemWriter 中使用批量写操作 + +直到版本 4.2,为了在`RepositoryItemWriter`中使用`CrudRepository#saveAll`,需要扩展 writer 并覆盖`write(List)`。 + +在此版本中,`RepositoryItemWriter`已更新为默认使用`CrudRepository#saveAll`。 + +#### [](#use-bulk-writes-in-mongoitemwriter)在 MongoitemWriter 中使用批量写操作 + +`MongoItemWriter`在 for 循环中使用`MongoOperations#save()`将项保存到数据库中。在此版本中,此 Writer 已更新为使用`org.springframework.data.mongodb.core.BulkOperations`。 + +#### [](#job-startrestart-time-improvement)作业启动/重启时间改进 + +`JobRepository#getStepExecutionCount()`的实现用于在内存中加载所有作业执行和步骤执行,以在框架端完成计数。在这个版本中,实现被更改为使用 SQL Count 查询对数据库执行一个单独的调用,以便计算执行的步骤。 + +### [](#dependencyUpdates)依赖项更新 + +此版本将依赖 Spring 项目更新为以下版本: + +* Spring 框架 5.3 + +* Spring 数据 2020.0 + +* Spring 集成 5.4 + +* Spring AMQP2.3 + +* Spring 为 Apache 卡夫卡 2.6 + +* 千分尺 1.5 + +### [](#deprecation)异议 + +#### [](#apiDeprecation)API 反对 + +以下是在此版本中已被弃用的 API 列表: + +* `org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean` + +* `org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean` + +* `org.springframework.batch.core.repository.dao.MapJobInstanceDao` + +* `org.springframework.batch.core.repository.dao.MapJobExecutionDao` + +* `org.springframework.batch.core.repository.dao.MapStepExecutionDao` + +* `org.springframework.batch.core.repository.dao.MapExecutionContextDao` + +* `org.springframework.batch.item.data.AbstractNeo4jItemReader` + +* `org.springframework.batch.item.file.transform.Alignment` + +* `org.springframework.batch.item.xml.StaxUtils` + +* `org.springframework.batch.core.launch.support.ScheduledJobParametersFactory` + +* `org.springframework.batch.item.file.MultiResourceItemReader#getCurrentResource()` + +* `org.springframework.batch.core.JobExecution#stop()` + +建议的替换可以在每个不推荐的 API 的 Javadoc 中找到。 + +#### [](#sqlfireDeprecation)SQLFire 支持弃用 + +自 2014 年 11 月 1 日起,SQLfire 一直位于[EOL](https://www.vmware.com/latam/products/pivotal-sqlfire.html)。这个版本取消了使用 SQLFire 作为作业存储库的支持,并计划在 5.0 版本中删除它。 \ No newline at end of file diff --git a/docs/spring-credhub/spring-credhub.md b/docs/spring-credhub/spring-credhub.md new file mode 100644 index 0000000..452bb8c --- /dev/null +++ b/docs/spring-credhub/spring-credhub.md @@ -0,0 +1,518 @@ +# Spring Credhub + +Spring Credhub 提供了客户端支持,用于从运行在[Cloud Foundry](https://www.cloudfoundry.org/)平台中的[CredHub](https://docs.cloudfoundry.org/credhub/)服务器中存储、检索和删除凭据。 + +Credhub 提供[HTTP API](https://docs.cloudfoundry.org/api/credhub/)来安全地存储、生成、检索和删除各种类型的凭据。 Spring Credhub 为 Credhub API 提供了一个 Java 绑定,使得将 Spring 应用程序与 Credhub 集成起来变得很容易。 + +## [](#getting-started)[1.开始](#getting-started) + +Spring CredHub 支持 CredHub Server1.x 和 2.x 版本。该库旨在提供对 Credhub API 的完整覆盖--对所有凭据类型的所有操作。 + +Spring Credhub 已经进行了优化,以便与 Spring 引导应用程序一起工作。要在 Spring 引导应用程序中包含 Spring CredHub,请向项目构建文件中添加一些依赖项。 + +### [](#maven-dependencies)[1.1. Maven Dependencies](#maven-dependencies) + +将 Spring credhub 启动器添加到构建文件的`dependencies`部分: + +``` + + + org.springframework.credhub + spring-credhub-starter + 2.2.0 + + +``` + +要在 Spring Credhub 中启用反应性支持,请将以下[Spring WebFlux](https://docs.spring.io/spring-framework/docs/5.3.13/reference/html/web-reactive.html#spring-webflux)依赖项添加到构建文件中: + +``` + + + org.springframework.boot + spring-boot-starter-webflux + 5.3.13 + + +``` + +要对 Credhub 使用 OAuth2 身份验证,请将以下[Spring Security](https://spring.io/projects/spring-security)依赖项添加到构建文件中: + +``` + + + org.springframework.security + spring-security-config + 5.5.3 + + + org.springframework.security + spring-security-oauth2-client + 5.5.3 + + +``` + +### [](#gradle-dependencies)[1.2. Gradle Dependencies](#gradle-dependencies) + +将 Spring credhub 启动器添加到构建文件的`dependencies`部分: + +``` + dependencies { + compile('org.springframework.credhub:spring-credhub-starter:2.2.0') + } +``` + +要在 Spring Credhub 中启用反应性支持,请在构建文件中添加以下[Spring WebFlux](https://docs.spring.io/spring-framework/docs/5.3.13/reference/html/web-reactive.html#spring-webflux)依赖项: + +``` + dependencies { + compile("org.springframework.boot:spring-boot-starter-webflux:5.3.13") + } +``` + +要对 Credhub 使用 OAuth2 身份验证,请将以下[Spring Security](https://spring.io/projects/spring-security)依赖项添加到构建文件中: + +``` + dependencies { + compile("org.springframework.security:spring-security-config:5.5.3") + compile("org.springframework.security:spring-security-oauth2-client:5.5.3") + } +``` + +## [](#boot-configuration)[2. Spring Boot Configuration](#boot-configuration) + +当使用 Spring Credhub Starter 依赖项时, Spring Credhub 可以配置为[Spring Boot application properties](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files)。有了适当的配置属性, Spring Credhub 将自动配置到 Credhub 服务器的连接。 + +### [](#mutual-tls-authentication)[2.1.双向 TLS 认证](#mutual-tls-authentication) + +在 Cloud Foundry 上运行的应用程序可以使用共同的 TLS 对部署在同一平台上的 CredHub 服务器进行身份验证。当不提供其他身份验证凭据时,Mutual TLS 是默认的身份验证方案。要对 Credhub 服务器使用共同的 TLS 身份验证,只需提供 Credhub 服务器的 URL 作为应用程序属性: + +``` +spring: + credhub: + url: [CredHub server URL] +``` + +有关双向 TLS 身份验证的更多信息,请参见[CredHub 文档](https://docs.cloudfoundry.org/api/credhub/version/main/#_mutual_tls)。 + +在 Cloud Foundry 上运行的应用程序可以使用内部地址`[https://credhub.service.cf.internal:8844](https://credhub.service.cf.internal:8844)`与部署到同一平台的 CredHub 服务器通信。 + +### [](#oauth2-authentication)[2.2.OAuth2 身份验证](#oauth2-authentication) + +OAuth2 可用于通过对任何 Credhub 服务器的 UAA 进行身份验证。 Spring Credhub 支持使用以下 Spring Credhub 和 Spring 安全配置进行身份验证的客户端凭据授予令牌: + +``` +spring: + credhub: + url: [CredHub server URL] + oauth2: + registration-id: credhub-client + security: + oauth2: + client: + registration: + credhub-client: + provider: uaa + client-id: [OAuth2 client ID] + client-secret: [OAuth2 client secret] + authorization-grant-type: client_credentials + provider: + uaa: + token-uri: [UAA token server endpoint] +``` + +在`spring.credhub.oauth2.registration-id`中提供的 ID 必须引用在`spring.security.oauth2.client.registration`下配置的客户端。有关 Spring Boot OAuth2 客户端配置的更多信息,请参见[Spring Boot documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-security-oauth2)。 + +Spring 安全客户端注册中指定的 OAuth2 客户端必须具有像`credhub.read`或`credhub.write`这样的 Credhub 作用域,才能执行大多数操作。有关使用 UAA 进行 OAuth2 身份验证的更多信息,请参见[CredHub 文档](https://docs.cloudfoundry.org/api/credhub/version/main/#_uaa_oauth2)。 + +#### [](#auto-configuration-of-spring-security-oauth2)[2.2.1. Auto-configuration of Spring Security OAuth2](#auto-configuration-of-spring-security-oauth2) + +当`spring.credhub.oauth2`属性被设置并且 Spring 安全性在应用程序 Classpath 上时, Spring Credhub 将自动配置 OAuth2 身份验证所需的 Spring 安全性 bean。 Spring 应用程序可以提供所需的安全性 OAuth2bean,以便在必要时覆盖自动配置。 + +##### [](#servlet-and-non-reactive-applications)[Servlet and Non-reactive Applications](#servlet-and-non-reactive-applications) + +Spring Credhub 需要以下类型的 bean,由 Spring Security 提供,以便使用 OAuth2 进行身份验证。 + +| Required Bean Type |自动配置类型| +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|[`ClientRegistrationRepository`](https://docs.spring.io/spring-security/site/docs/5.5.3/api/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.html)|[`InMemoryClientRegistrationRepository`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oAuth2/client/registration/inmemoryclientregistrationrepository.html)| +|[`OAuth2AuthorizedClientRepository`](https://docs.spring.io/spring-security/site/docs/5.5.3/api/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.html) |[`AuthenticatedPrincipalOAuth2AuthorizedClientRepository`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oAuth2/client/web/authenticatedprincipalouth2authorizedclientrepository.html)| +| [`OAuth2AuthorizedClientManager`](https://docs.spring.io/spring-security/site/docs/5.5.3/api/org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.html) |[`DefaultOAuth2AuthorizedClientManager`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oAuth2/client/web\defaultoth2authorizedclientmanager.html)| + +自动配置的`DefaultOAuth2AuthorizedClientManager`假设应用程序在 Servlet 容器中运行,并且具有活动的`HttpServletRequest`。应用程序可能需要提供`OAuth2AuthorizedClientManager` Bean 的替代实现,例如[`AuthorizedClientServiceOAuth2AuthorizedClientManager`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/outh2/client/authorizedclientserviceouth2authorizedclientmanager.html),以处理以外的请求,如以下示例所示: + +``` +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.credhub; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +@Configuration +public class CredHubSecurityConfiguration { + + @Bean + public AuthorizedClientServiceOAuth2AuthorizedClientManager reactiveClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService authorizedClientService) { + AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + clientManager.setAuthorizedClientProvider(new ClientCredentialsOAuth2AuthorizedClientProvider()); + return clientManager; + } + +} +``` + +有关更多信息和配置其他 bean 的示例,请参见[Spring Security documentation](https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#oauth2login-override-boot-autoconfig)。 + +##### [](#reactive-applications)[反应性应用](#reactive-applications) + +Spring Credhub 需要以下类型的 bean,由 Spring Security 提供,以便使用 OAuth2 进行身份验证。 + +| Required Bean Type |自动配置类型| +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`ReactiveClientRegistrationRepository`](https://docs.spring.io/spring-security/site/docs/5.5.3/api/org/springframework/security/oauth2/client/registration/ReactiveClientRegistrationRepository.html) |[`InMemoryReactiveClientRegistrationRepository`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oAuth2/client/registration/inmemoryreactiveClientRegistrationRepository.html)| +|[`ServerOAuth2AuthorizedClientRepository`](https://docs.spring.io/spring-security/site/docs/5.5.3/api/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizedClientRepository.html)|[`UnAuthenticatedServerOAuth2AuthorizedClientRepository`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oAuth2/client/web/server/unauthenticatedserveroauth2authorizedclientrepository.html)| +| [`ReactiveOAuth2AuthorizedClientManager`](https://docs.spring.io/spring-security/site/docs/5.5.3/api/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientManager.html) |[`DefaultReactiveOAuth2AuthorizedClientManager`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oAuth2/client/web/defaultreactiveoAuth2authorizedclientmanager.html)| + +自动配置的`DefaultReactiveOAuth2AuthorizedClientManager`需要活动的`ServerHttpRequest`上下文。应用程序可能需要提供`ReactiveOAuth2AuthorizedClientManager` Bean 的替代实现,例如[`AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.5.3/api/org/springframework/security/oauth2/client/authorizedclientservicerereactiveoauth2authorizedclientmanager.html),以处理以外的请求,如以下示例所示: + +``` +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.credhub; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; + +@Configuration +public class CredHubReactiveSecurityConfiguration { + + @Bean + public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager reactiveClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager clientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + clientManager.setAuthorizedClientProvider(new ClientCredentialsReactiveOAuth2AuthorizedClientProvider()); + return clientManager; + } + +} +``` + +有关配置其他 bean 的更多信息和示例,请参见[Spring Security documentation](https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#oauth2login-override-boot-autoconfig)。 + +## [](#operations)[3.CredHubOperations 简介](#operations) + +接口`org.springframework.credhub.core.CredHubOperations`和实现`org.springframework.credhub.core.CredHubTemplate`是 Spring Credhub 中的中心类。`CredHubOperations`提供了对模拟完整 Credhub API 的附加操作接口的访问: + +``` +/** + * Get the operations for saving, retrieving, and deleting credentials. + */ +CredHubCredentialOperations credentials(); + +/** + * Get the operations for adding, retrieving, and deleting credential permissions. + */ +CredHubPermissionOperations permissions(); + +/** + * Get the operations for adding, retrieving, and deleting credential permissions. + */ +CredHubPermissionV2Operations permissionsV2(); + +/** + * Get the operations for retrieving, regenerating, and updating certificates. + */ +CredHubCertificateOperations certificates(); + +/** + * Get the operations for interpolating service binding credentials. + */ +CredHubInterpolationOperations interpolation(); + +/** + * Get the operations for retrieving CredHub server information. + */ +CredHubInfoOperations info(); +``` + +### [](#mapping-to-credhub-api)[3.1.映射到 credhub API](#mapping-to-credhub-api) + +`Operations`接口的每个方法都直接映射到 credhub HTTP API 的一个端点。下表显示了 credhub API 和相应的 Spring credhub`Operations`接口之间的映射。 + +| [CredHub Credentials API](https://docs.cloudfoundry.org/api/credhub/version/main/#_credentials_endpoint) |[CredHubCredentialOperations](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/credential/CredHubCredentialOperations.html)| +|------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|[CredHub Permissions API](https://docs.cloudfoundry.org/api/credhub/version/main/#_permissions_v1_deprecated) (v1)|[安全操作](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/permission/CredHubPermissionOperations.html)| +| [CredHub Permissions API](https://docs.cloudfoundry.org/api/credhub/version/main/#_permissions_v2_endpoint) (v2) |[CredHubperMissionv2 操作](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/permissionV2/CredHubPermissionV2Operations.html)| +| [CredHub Certificates API](https://docs.cloudfoundry.org/api/credhub/version/main/#_certificates_endpoint) |[CredHubCertificateOperations](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/certificate/CredHubCertificateOperations.html)| +| [CredHub Interpolation API](https://docs.cloudfoundry.org/api/credhub/version/main/#_interpolation_endpoint) |[CredHubInterpolationOperations](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/interpolation/CredHubInterpolationOperations.html)| +| [CredHub Information API](https://docs.cloudfoundry.org/api/credhub/version/main/#_info_endpoint) |[CredhubinfoOperations](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/info/CredHubInfoOperations.html)| + +### [](#credhuboperations-auto-configuration)[3.2.CredHubOperations 自动配置](#credhuboperations-auto-configuration) + +当应用程序属性被正确配置时,使用 Spring 引导自动配置来创建`CredHubOperations` Spring Bean。 Bean 应用程序类可以自动连接此实例,以与 Credhub 服务器进行交互。 + +``` +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.credhub; + +import org.springframework.credhub.core.CredHubOperations; +import org.springframework.credhub.support.CredentialDetails; +import org.springframework.credhub.support.SimpleCredentialName; +import org.springframework.credhub.support.password.PasswordCredential; +import org.springframework.credhub.support.password.PasswordParameters; +import org.springframework.credhub.support.password.PasswordParametersRequest; +import org.springframework.stereotype.Component; + +@Component +public class CredHubService { + + private final CredHubOperations credHubOperations; + + private final SimpleCredentialName credentialName; + + public CredHubService(CredHubOperations credHubOperations) { + this.credHubOperations = credHubOperations; + + this.credentialName = new SimpleCredentialName("example", "password"); + } + + public String generatePassword() { + PasswordParameters parameters = PasswordParameters.builder().length(12).excludeLower(false).excludeUpper(false) + .excludeNumber(false).includeSpecial(true).build(); + + CredentialDetails password = this.credHubOperations.credentials() + .generate(PasswordParametersRequest.builder().name(this.credentialName).parameters(parameters).build()); + + return password.getValue().getPassword(); + } + + public String getPassword() { + CredentialDetails password = this.credHubOperations.credentials() + .getByName(this.credentialName, PasswordCredential.class); + + return password.getValue().getPassword(); + } + +} +``` + +## [](#reactive-operations)[4.重新激活的硬件操作介绍](#reactive-operations) + +接口`org.springframework.credhub.core.ReactiveCredHubOperations`和实现`org.springframework.credhub.core.ReactiveCredHubTemplate`是 Spring Credhub Reactive Support 中的中心类。`ReactiveCredHubOperations`提供了对其他操作接口的访问,这些操作接口为完整的 Credhub API 建模: + +``` +/** + * Get the operations for saving, retrieving, and deleting credentials. + */ +ReactiveCredHubCredentialOperations credentials(); + +/** + * Get the operations for adding, retrieving, and deleting credential permissions. + */ +ReactiveCredHubPermissionOperations permissions(); + +/** + * Get the operations for adding, retrieving, and deleting credential permissions. + */ +ReactiveCredHubPermissionV2Operations permissionsV2(); + +/** + * Get the operations for retrieving, regenerating, and updating certificates. + */ +ReactiveCredHubCertificateOperations certificates(); + +/** + * Get the operations for interpolating service binding credentials. + */ +ReactiveCredHubInterpolationOperations interpolation(); + +/** + * Get the operations for retrieving CredHub server information. + */ +ReactiveCredHubInfoOperations info(); +``` + +### [](#mapping-to-credhub-api-2)[4.1.映射到 credhub API](#mapping-to-credhub-api-2) + +`Reactive…​Operations`接口的每个方法都直接映射到 credhub HTTP API 的一个端点。下表显示了 credhub API 与相应的 Spring credhub`Reactive…​Operations`接口之间的映射。 + +| [CredHub Credentials API](https://docs.cloudfoundry.org/api/credhub/version/main/#_credentials_endpoint) |[重新激活了牙科手术](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/credential/ReactiveCredHubCredentialOperations.html)| +|------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|[CredHub Permissions API](https://docs.cloudfoundry.org/api/credhub/version/main/#_permissions_v1_deprecated) (v1)|[重新激活了运行操作](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/permission/ReactiveCredHubPermissionOperations.html)| +| [CredHub Permissions API](https://docs.cloudfoundry.org/api/credhub/version/main/#_permissions_v2_endpoint) (v2) |[重新激活了 HubperMissionV2 操作](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/permissionV2/ReactiveCredHubPermissionV2Operations.html)| +| [CredHub Certificates API](https://docs.cloudfoundry.org/api/credhub/version/main/#_certificates_endpoint) |[重新激活已有证书的操作](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/certificate/ReactiveCredHubCertificateOperations.html)| +| [CredHub Interpolation API](https://docs.cloudfoundry.org/api/credhub/version/main/#_interpolation_endpoint) |[重新激活和插值操作](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/interpolation/ReactiveCredHubInterpolationOperations.html)| +| [CredHub Information API](https://docs.cloudfoundry.org/api/credhub/version/main/#_info_endpoint) |[重新激活 HubinfoOperations](https://docs.spring.io/spring-credhub/docs/2.2.0/api/index.html?org/springframework/credhub/core/info/ReactiveCredHubInfoOperations.html)| + +### [](#reactivecredhuboperations-auto-configuration)[4.2.重新激活和操作自动配置](#reactivecredhuboperations-auto-configuration) + +当应用程序属性被正确配置并且 Spring WebFlux 库在 Classpath 上时,使用 Spring 引导自动配置来创建`ReactiveCredHubOperations` Spring Bean。 Bean 应用程序类可以自动连接此实例以与 Credhub 服务器交互。 + +``` +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.credhub; + +import reactor.core.publisher.Mono; + +import org.springframework.credhub.core.ReactiveCredHubOperations; +import org.springframework.credhub.support.SimpleCredentialName; +import org.springframework.credhub.support.password.PasswordCredential; +import org.springframework.credhub.support.password.PasswordParameters; +import org.springframework.credhub.support.password.PasswordParametersRequest; +import org.springframework.stereotype.Component; + +@Component +public class ReactiveCredHubService { + + private final ReactiveCredHubOperations credHubOperations; + + private final SimpleCredentialName credentialName; + + public ReactiveCredHubService(ReactiveCredHubOperations credHubOperations) { + this.credHubOperations = credHubOperations; + + this.credentialName = new SimpleCredentialName("example", "password"); + } + + public Mono generatePassword() { + PasswordParameters parameters = PasswordParameters.builder().length(12).excludeLower(false).excludeUpper(false) + .excludeNumber(false).includeSpecial(true).build(); + + return this.credHubOperations.credentials() + .generate(PasswordParametersRequest.builder().name(this.credentialName).parameters(parameters).build(), + PasswordCredential.class) + .map((password) -> password.getValue().getPassword()); + } + + public Mono getPassword() { + return this.credHubOperations.credentials().getByName(this.credentialName, PasswordCredential.class) + .map((password) -> password.getValue().getPassword()); + } + +} +``` + +## [](#http-clients)[5.HTTP 客户端支持](#http-clients) + +Spring Credhub`CredHubOperations`支持多个 HTTP 客户库与 Credhub API 通信。支持以下库: + +* Java 的内置`HttpURLConnection`(默认) + +* [Apache HttpComponents](https://hc.apache.org/) + +* [OkHttp 3](https://square.github.io/okhttp/) + +* [Netty](https://netty.io/) + +Classpath 选择特定的客户端库需要在应用程序上可用的适当的依赖关系。 Classpath 将按照上面列出的顺序检查每个客户库的应用程序。 + +Spring Credhub`ReactiveCredHubOperations`只支持 Netty HTTP 客户端库。 + +### [](#apache-httpcomponents)[5.1. Apache HttpComponents](#apache-httpcomponents) + +要使用 Apache HttpComponents 与 CredHub 通信,请向应用程序添加以下依赖项: + +``` + + org.apache.httpcomponents + httpclient + +``` + +| |Apache 可以通过日志配置启用 HttpClient 的[电汇测井](https://hc.apache.org/httpcomponents-client-4.5.x/logging.html)。确保不会意外地启用有线日志,因为日志可能会以纯文本形式暴露应用程序和 Credhub 之间的流量(包括令牌和秘密)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#okhttp-3)[5.2.OKHTTP3](#okhttp-3) + +要使用 OKHTTP3 与 Credhub 通信,请向应用程序添加以下依赖项: + +``` + + com.squareup.okhttp3 + okhttp + +``` + +### [](#netty)[5.3. Netty](#netty) + +要使用 Netty 与 Credhub 通信,请向应用程序添加以下依赖项: + +``` + + io.netty + netty-all + +``` \ No newline at end of file diff --git a/docs/spring-flo/spring-flo.md b/docs/spring-flo/spring-flo.md new file mode 100644 index 0000000..dd14fe8 --- /dev/null +++ b/docs/spring-flo/spring-flo.md @@ -0,0 +1,260 @@ +# 欢迎来到 Spring FLO 维基! Spring FLO 是用于图表编辑器的一组[Angular JS](https://angularjs.org/)指令,该编辑器能够以图形方式表示 DSL 并同步该 DSL 的图形和文本表示。图形表示是用[Joint JS](http://jointjs.com/)图形对象完成的,文本表示可以是普通的 HTML 元素(例如` + +``` + +上面的 HTML 转换为一个页面,其中包含用于按钮(布局和显示/隐藏调色板)的工具栏,用于 DSL 的文本区和用于 DSL 的图形表示的 FLO 编辑器。 + +## 所有的扩展点: + +### 元模型服务该服务支持使用 FLO 的域来指定在图中连接在一起的元素的种类,以及图应该如何转换为文本表示形式。[样例元模型服务在这里](https://github.com/spring-projects/spring-flo/blob/master/samples/spring-flo-sample/src/main/resources/static/js/metamodel-service.js)。#####texttograph(FLO,定义)基于来自`definition`对象的 DSL 的文本表示设置`flo`对象的图形内容。将文本转换为相应的联合 JS 图内容。将通过`flo`对象函数(如`flo.createLink()`和`flo.createNode()`)填充图形,并使用`flo.clearGraph`###GraphtoText(FLO,(定义)将当前从`flo`对象获得的图形转换为文本表示,然后在`definition`对象上设置文本表示(作为`text`属性)。#####load()返回一个 promise,该 promise 解析为`metamodel`对象。`metamodel`对象布局是元素`group`名称的映射到属于这个`group`的元素的映射。属于`group`的元素的映射是元素的`name`和元素的[元数据对象](https://github.com/spring-projects/spring-flo/wiki#element-metadata)之间的映射 ##### 刷新()_(可选)_刷新元模型并返回一个 promise,该 promise 被解析为与[load()](#load)相同的结果。刷新也应该触发事件到`metamodel`更改侦听器。#####EncodeTextToDSL_(可选)_将 DSL 元素属性值的文本编码为 DSL 所需的格式。示例是将多行文本转换为 DSL 格式所需的单行文本。用于以人类可读的格式显示属性值。#####decodeTextFroMDSL从 DSL 格式中解码 DSL 元素属性值文本。例如,将单行文本转换为多行文本,即替换转义换行符。用于为用户通过 UI 输入的 DSL 元素设置属性值。##### 订阅_(可选)_将一个侦听器添加到`metamodel`事件。(参见[元模型侦听器](#metamodel-listener))##### 取消订阅(监听器)_(可选)_删除`metamodel`事件监听器。(参见[元模型侦听器](#metamodel-listener))####isvalidPropertyValue(元素、键、值)_(可选)_检查是否允许为指定元素上的键指定该值。例如:如果键取整数,则不允许使用字母字符。 + +### Render Service 服务负责基于元数据(来自[元模型服务](#metamodel-service))的图形元素的可视化表示。此服务是**可选的**。[示例呈现服务在这里](https://github.com/spring-projects/spring-flo/blob/master/samples/spring-flo-sample/src/main/resources/static/js/render-service.js).#####CreateNode(元数据,属性)_(可选)_创建联合 JS 图节点模型对象的实例(`joint.dia.Element`)。可能影响节点模型对象类型的参数是元素的[metadata](#element-metadata)和属性映射(如果有传入的话)。####CreateLink(源、目标、元数据、属性)_(可选)_创建一个联合 JS Graph 链接模型对象的实例(`joint.dia.Link`)。可能影响链接模型对象类型的参数是元素的[metadata](#element-metadata)、属性映射(如果有传入)、源元素和目标元素 ######createhandle_(可选)_创建联合 JS 图节点模型对象的实例(`joint.dia.Element`)。句柄的一个示例是显示在与父形状交互的旁边的形状,该形状导致在父形状上进行一些编辑操作。可能影响句柄模型对象类型的参数是`kind`类型的`string`(用户定义,i。e.`delete`,`resize`等。)和句柄的`parent`元素。只有在实现了 Editor Service`createHandles()`函数的情况下,框架才调用该函数。#####CreateDecoration_(可选)_创建一个联合 JS 图节点模型对象的实例(`joint.dia.Element`)。装饰的一个例子是显示在父形状上的验证标记。可能影响装饰模型对象种类的参数是`kind`类型的`string`和装饰的`parent`元素。注意,`kind`参数来自框架(与`createHandle`函数不同)。只有在实现了 Editor Service`validateNode()`函数的情况下,框架才调用该函数。(目前装饰只是验证错误标记)。#####initializeNewNode_(可选)_对新创建的图`node`执行任何附加的初始化,当`node`已经添加到联合 JS 图中并在画布上呈现时,E。g.元素的 SVG DOM 结构是可用的。`context`参数是一个对象,其`paper`和`graph`属性适用于`node`。当 SVG DOM 附加到页面 DOM 时,在节点上执行任何类型的初始化都是有用的。例如:将字符串标签放入形状中,在形状上使用角度指示,添加 DOM 侦听器等。#####initializenewlink_(可选)_在`link`已经添加到联合 JS 图并在画布上呈现时,执行新创建的图`link`的任何附加初始化。g.元素的 SVG DOM 结构是可用的。`context`参数是一个对象,其`paper`和`graph`属性适用于`link`。当链接的 SVG DOM 附加到页面 DOM 时,在链接上执行任何类型的初始化都是有用的。例如:在形状上使用角度指令,添加 DOM 侦听器等。#####initializenewhandle(句柄,上下文)_(可选)_在`handle`已添加到联合 JS 图并在画布上呈现时,对新创建的图`handle`执行任何附加的初始化。g.元素的 SVG DOM 结构是可用的。`context`参数是一个对象,其`paper`和`graph`属性适用于`handle`。当一个句柄形状的 SVG DOM 被追加到页面 DOM 时,对它执行任何类型的初始化都是有用的。例如:将字符串标签放入形状中,在形状上使用角度指示,添加 DOM 侦听器等。#####initializenewdecoration(装饰,上下文)_(可选)_在`decoration`已添加到联合 JS 图并在画布上呈现时,对新创建的图`decoration`执行任何附加的初始化。g.元素的 SVG DOM 结构是可用的。`context`参数是一个对象,其`paper`和`graph`属性适用于`decoration`。当装饰形状的 SVG DOM 附加到页面 DOM 时,对它执行任何类型的初始化都是有用的。例如:将字符串标签放入形状中,在形状上使用角度指示,添加 DOM 侦听器等。#####getnodeview()_(可选)_返回`joint.dia.ElementView`的实例。它也可以是`function(element)`形式的函数,该函数接受一个元素模型,并应该返回一个负责将该模型呈现到屏幕上的对象。在正常情况下,这个功能不需要实现,由框架创建的联合 JS 视图对象应该足够了。如果不同的节点需要不同的联合 JS 视图,或者视图有一些特殊的呈现(I。e.嵌入的 HTML 元素)。参见[联合 JS 文件选项](http://jointjs.com/api#joint.dia.Paper:options)###getlinkview()_(可选)_返回联合 js 的实例`joint.dia.LinkView`。默认值为`joint.dia.LinkView`。它也可以是`function(link)`窗体的函数,它接受一个链接模型,并且应该返回一个负责将该模型呈现到屏幕上的对象。在正常情况下,这个功能不需要实现,由框架创建的联合 JS 视图对象应该足够了。如果不同的链接需要不同的联合 JS 视图,或者视图有一些特殊的呈现(I。e.模式应用于行-`joint.shapes.flo.PatternLinkView`)。参见[联合 JS 文件选项](http://jointjs.com/api#joint.dia.Paper:options)####layout_(可选)_负责布局可以从传入`paper`参数(`paper.model`)中派生出的联合 JS 图。#####HandleLinkEvent(paper,event,link)_(可选)_负责处理`event`上发生的、属于在联合 JS 中传递的`link`对象的`paper`。`event`参数是一个`string`,具有可能值:`'add'`、`'remove'`或联合 JS 本地链接更改事件,例如`'change:source'`、`'change:target'`等。参见[联合 JS Link 活动](http://jointjs.com/api#joint.dia.Link:events)###issemanticproperty_(可选)_对于`string`属性路径`propertyPath`在`element`上如果图形需要执行一些基于`propertyPath`值变化的可视化更新(对于`props`下的属性不需要`true`在`element`上)。可视更新由[RefreshVisuals()](#refreshVisuals)执行。属性路径`propertyPath`是相对于联合 JS 元素`attrs`属性 ####refreshVisuals(元素,propertypath,paper)_(可选)_执行一些图形的可视化更新,或者更有可能,传入的`element`基于`propertyPath`指定的变更属性显示在联合 js 上`paper`<####getlinkanchorpoint_(可选)_此函数允许你自定义链接的锚点是什么。函数必须返回一个点(带有`x`和`y`属性),链接在这个点上锚定到元素。该函数获取链接视图、元素视图、链接应该粘贴的`port`(SVG 元素)和一个参考点(链接另一侧的最近顶点或锚点)。 + +### 编辑器服务负责为 FLO 编辑器提供丰富的编辑功能的服务,例如围绕选定形状的句柄、自定义的拖放行为、实时和静态验证。此服务是**可选的**。[示例编辑服务在这里](https://github.com/spring-projects/spring-flo/blob/master/samples/spring-flo-sample/src/main/resources/static/js/editor-service.js)###createHandles(FLO,createHandle,已选中)_(可选)_当节点被选中并且可以显示句柄时调用。句柄通常是`selected`编辑器交互中`flo`联合 JS 节点周围的小形状,用于修改`selected`节点上的属性,即调整大小或删除句柄。调用`createHandle(selected, kind, clickHandlerFunction, coordinate)`函数创建一个句柄。`kind`参数是一种`string`类型的句柄,`clickHandlerFunction`是在单击句柄时执行的,`coordinate`是放置句柄形状的位置。注意,如果实现了这个函数,那么 Render Service`createHandle(...)`函数也必须实现。当需要时,框架将自动删除句柄,因此在客户端无需担心这一点。####ValidatePort_(可选)_如果用户单击端口,请决定是否创建链接。`portView`是表示端口的 DOM 元素,`view`是端口的父联合 JS 视图对象,在联合 JS`paper`中显示 #####ValidateLink(FLO、CellViews、Ports、CellViewt、Port、End,linkview)_(可选)_决定是否允许或不允许源视图/端口(`cellViewS`/`portS`)和目标视图/端口(`cellViewT`/`portT`)之间的连接。`end`要么是`'source'`要么是`'target'`,它告诉我们拖动链接的哪一端。这对于定义例如从元素 A 的端口输出开始的链接是否可以指向 Elmement B 的端口引脚很有用。#####CalculateDragDescriptor(FLO,DraggedView,TargetUnderMouse,坐标,context)_(可选)_在`coordinate`上拖动一个节点`draggedView`时调用。还有`flo`Object 参数和`context`Object,它目前仅具有`boolean`属性`palette`来表示是否在调色板或画布上进行拖放。函数应该返回一个[拖动描述符对象](#drag-descriptor)。#####HandlenodeDropping(FLO,dragDescriptor)_(可选)_在拖动的节点被拖放时执行必要的图形操作。`dragDescriptor`[拖动描述符](#drag-descriptor)应该具有强制信息,说明正在拖动的内容和正在丢弃的位置。`flo`对象参数将有助于对图进行必要的修改 #####showdragfeedback(FLO,dragdescriptor)_(可选)_当将一个节点拖过某个图元素(节点或链接)时,任何自定义的视觉反馈都可以由该函数绘制。`dragDescriptor`参数有一个[拖动描述符对象](#drag-descriptor),其中包含有关正在进行拖动的完整信息,并且`flo`对象将有助于使用联合 JS#####HideDragFeedback 绘制反馈(FLO,dragDescriptor)_(可选)_删除由[showdragfeedback()](#show-drag-feedback)绘制的任何自定义视觉反馈。具有相同的参数。#####ValidateNode(FLO,节点)_(可选)_返回一个`javascript`数组的`string`错误消息,这些错误消息是在`node`编辑器 ####predelete(FLO,删除)_(可选)_之前调用的指定删除`deletedElement`允许在此之前进行额外的 tidyup。例如:删除与将要删除的元素相关的任何依赖的联合 JS 图元素。#####Interactive_(可选)_如果设置为`false`,则禁用与元素和链接的交互。如果它是一个函数,那么它将在单元格视图的作用下被调用,并调用它在(`'pointerdown'`,`'pointermove'`,...)中求值的方法的名称。如果这样的函数返回的值是假的,则将禁用该操作的交互。对于链接,交互对象的一些特殊属性对于禁用默认行为非常有用。这些性质是:`vertexAdd`,`vertexMove`,`vertexRemove`和`arrowheadMove`。通过将这些属性中的任何一个设置为 false,你可以禁用链接上的相关默认操作。#####AllowLinkVertexEdit_(可选)_如果设置为`false`链接顶点(或弯曲点)的创建或编辑(例如移动)在编辑器中是不允许的。 + +## 数据结构引用:###FLO 这个对象是由`flo-editor`指令控制器创建的,它包含各种特定于编辑器的属性和函数。#####ScheduleUpdateGraphRepresentation()基于文本 DSL 表示,调度图 DSL 表示的异步更新。####UpdateGraphRepresentation()基于文本 DSL 表示异步更新图形 DSL 表示。将返回一个承诺,当更新完成时,该承诺将得到解决。####UpdateTextRepresentation()基于图形 DSL 表示异步更新文本 DSL 表示(`definition`对象)。将返回一个承诺,该承诺在更新完成时得到解决。####PerformLayout()在画布上排列图形的节点和链接。####ClearGraph()清除了所有节点和链接的画布。与此同步还会导致文本 DSL 表示被清除。#####getgraph()返回对画布内容的`joint.dia.Graph`对象实例的引用(图形模型,请参见[联合 JS 图形 API ](http://jointjs.com/api#joint.dia.Graph)####getpaper()返回对画布的 joint.dia.paper 对象实例的引用(图形视图对象,参见[联合 JS 纸 API ](http://jointjs.com/api#joint.dia.Paper)####enablesyncing 基于传递的`boolean`参数`enable`启用或禁用文本和图形 DSL 表示同步机制。当文本 DSL 表示 UI 折叠时非常有用。#####getSelection()返回画布上当前选择的图形模型元素(节点或链接)####ZoomPROCENT 角度 getter/setter 函数,用于画布上的缩放值。如果提供了整数`number`参数,则设置 zoom% 值。如果参数丢失(getter 模式)#####GridSize 角度 getter/setter 函数,则返回整数 % 值,用于画布网格大小(以像素为单位)。如果提供了整数`number`参数`gridSize`,则设置网格宽度值。如果参数丢失(getter 模式),则返回当前网格大小值.请注意,将网格宽度设置为`1`会关闭网格。无效的`gridSize`值被忽略 ######getminzoom()返回整数`number`缩放百分比的最小允许值。用于设置缩放控件的适当范围。画布上的缩放控件所需的(如果设置为显示的话).默认情况下,该值等于`5`(5%)。#####getmaxzoom()返回整数`number`最大允许缩放百分比的值。用于设置缩放控件的适当范围。画布上的缩放控件所需的(如果设置为显示的话).默认情况下,该值等于`400`(400%)。#####getZoomStep()返回整数`number`缩放 % 递增/递减步骤。画布上的缩放控件所需的(如果设置为显示的话).默认情况下,该值等于`5`(5% 的递增/递减值)。####FitTopAge()将整个图形适合 Canvas 的视口(即无需滚动来查找 Canvas 上的内容)。为画布“只读”属性调整缩放级别和滚动位置 ####readonlycanvas 角度 getter/setter 函数。只读画布不允许任何用户编辑画布上任何形状的交互。根据传入的`newValue`参数设置只读属性,结果画布立即切换只读状态的行为。如果参数丢失(getter 模式),则返回当前的“只读”状态值。#####CreateNode(元数据、属性、位置)基于图节点`metadata`对象(参见[元素元数据](#element-metadata))、`properties`键-值对映射和画布上的位置(对象带有`x`和`y`属性)创建并返回新创建的联合 JS 图节点(对象)。新的节点还被添加到 FLO 画布联合 JS,因此也被添加到联合 JS,并在此函数返回结果之前立即出现在画布上。####CREATELINK(源、目标、元数据、属性);基于图形链接`source`和`target`之间创建并返回新创建的 JS 联合图形链接(实例`joint.dia.Link`)(类型为`joint.dia.Element`的节点)`metadata`对象(参见[元素元数据](#element-metadata)),`properties`键值对映射。新的链接还被添加到 FLO 画布联合 JS,因此也被添加到联合 JS,并在此函数返回结果之前立即出现在画布上。 + +### 定义此对象保存与 DSL 的文本表示相关的数据。通常,对于 DSL 文本,这个对象至少应该具有`text`类型的`string`属性,但是它也可以具有其他属性,这些属性可以由客户机的元模型服务图-文本转换函数添加。 + +### 元模型侦听器通常通过 HTTP 请求异步加载元模型对象。如果服务缓存了元数据,那么注册侦听器可能会很有用。如果元模型发生了变化,FLO 编辑器调色板将自动重建自身。 +```javascript +{ + metadataError: function(data) { + /* Error loading metadata has occurred */ + }, + metadataRefresh: function() { + /* Metadata is about to be refreshed */ + }, + metadataChanged: function(data) { + /* New metadata is available */ + } +} +``` +### 拖动描述符 API 客户机可以自由地向此对象添加额外的属性(即可能有助于绘制视觉反馈) +```javascript +{ + context: context, /* String 'palette' or 'canvas' */ + source: { + cell: draggedNode, /* Joint JS graph node being dragged */ + selector: selector, /* Optional. Joint JS CSS class selector for the subelement of the dragged node*/, + port: portType /* Optional. Involved port DOM element type attribute value == port Joint JS markup 'type' property */ + }, + target: { + cell: targetNode, /* Joint JS graph node target under mouse element */ + selector: selector, /* Optional. Joint JS CSS class selector for the element under mouse within the targetNode */ + port: portType /* Optional. Sub-element under mouse is a port. Port DOM element type attribute value == port Joint JS markup 'type' property */ + }, +}; +``` + +### 联合 JS 图节点标记 +```javascript +model: /* Joint JS model object for a module shape */ + ... + attributes: + ... + angle: 0, /* Joint JS property - rotation angle */ + + id: "02be8001-ea1e-4f30-a94e-9503da5964b5" /* Joint JS property - element model UUID + + position: /* Joint JS property - coordinates of the shape's bounding rectangle */ + x: 119 + y: 46 + + size: /* Joint JS property - size of the shape's bounding rectangle */ + height: 40 + width: 120 + + type: "sinspctr.IntNode" /* Flo property - internal, type (node, link, handle, decoration, etc) */ + + z: 1 /* Joint JS property - z-index of the shape + + ports: /* Joint JS property - internal, ports available on the shape */ + input: + id: "input" + output: + id: "output" + tap: + id: "tap" + + attrs: /* Joint JS property - user defined rendering constructs and semantic properties */ + + . /*\ */ + .border /* \ */ + .box /* \ */ + .input-port /* \ */ + .label1 /* \___User defined rendering constructs implied by the markup */ + .label2 /* / */ + .output-port /* / */ + .shape /* / */ + .stream-label /* / */ + .tap-port /*/ */ + + metadata: /* Flo property. Node metadata supplied by Metamodel Service */ + + props: /* Flo property. Semantic properties of the element. Name <-> value pair map */ + dir: "/Users/x/tmp" + file: "temp.tmp" + debug: true + + ... + ... +... +``` + +### 元模型服务提供的元素元数据图形化元素元数据 +```javascript +metadata: { + + get: function(), /* function taking property key string as a parameter */ + /* Returns promise that resolves to the metadata object of the property */ + /* See snippet below showing the format of a property metadata */ + + group: "source", /* Category/Group of an element. Translates into palette groups of elements */ + + name: "file", /* Name or Type of an element (should be unique within its group) */ + + metadata: { /* Additional metadata for the element */ + titleProperty: 'props/title', /* Property to be displayed at the top of all properties in properties Div */ + noEditableProps: false, /* If true then element doesn't have properties to edit and properties Div is not shown */ + allow-additional-properties: true, /* Allows user to create new properties for element in the properties Div */ + } + +} +``` +元素的属性元数据应该如下所示 +```javascript + properties: { + info: { + defaultValue: null, + description: "General information about the file", + id: "info", + name: "info", + shortDescription: "File Info" + }, + + language: { + defaultValue: "English" + description: "Language of the file contents", + id: "language", + name: "language", + shortDescription: "Text Language" + }, + ... +``` diff --git a/docs/spring-hateoas/spring-hateoas.md b/docs/spring-hateoas/spring-hateoas.md new file mode 100644 index 0000000..6005d4e --- /dev/null +++ b/docs/spring-hateoas/spring-hateoas.md @@ -0,0 +1,2012 @@ +# Spring Hateoas-参考文献 + +## [](#preface)1。前言 + +### [](#migrate-to-1.0)1.1。迁移到 Spring Hateoas1.0 + +对于 1.0,我们利用这个机会重新评估了我们为 0.x 分支所做的一些设计和包结构选择。对此有大量的反馈,主要版本的提升似乎是重构这些内容的最自然的地方。 + +#### [](#migrate-to-1.0.changes)1.1.1。变化 + +包结构中最大的变化是引入了超媒体类型注册 API,以支持 Spring Hateoas 中的其他媒体类型。这导致客户机和服务器 API(分别命名的包)以及包`mediatype`中的媒体类型实现的明确分离。 + +使你的代码库升级到新 API 的最简单的方法是使用[迁移脚本](#migrate-to-1.0.script)。在我们开始讨论这一问题之前,我们先来简单了解一下这些变化。 + +##### [](#migrate-to-1.0.changes.representation-models)表示模型 + +`ResourceSupport`/`Resource`/`Resources`/`PagedResources`这组类从来没有真正感觉到合适的名称。毕竟,这些类型实际上并不表示资源,而是表示模型,这些模型可以用超媒体信息和启示来丰富。以下是新名字与旧名字的对应方式: + +* `ResourceSupport`现在是`RepresentationModel` + +* `Resource`现在是`EntityModel` + +* `Resources`现在是`CollectionModel` + +* `PagedResources`现在是`PagedModel` + +因此,`ResourceAssembler`已更名为`RepresentationModelAssembler`,其方法`toResource(…)`和`toResources(…)`已分别更名为`toModel(…)`和`toCollectionModel(…)`。此外,名称更改已反映在`TypeReferences`中包含的类中。 + +* `RepresentationModel.getLinks()`现在公开了一个`Links`实例(在`List`上),因为它公开了额外的 API 来使用各种策略连接和合并不同的`Links`实例。它还被转换为自绑定泛型类型,以允许向实例添加链接的方法返回实例本身。 + +* `LinkDiscoverer`API 已被移动到`client`包中。 + +* `LinkBuilder`和`EntityLinks`API 已被移动到`server`包中。 + +* `ControllerLinkBuilder`已被移动到`server.mvc`中,并且不推荐将其替换为`WebMvcLinkBuilder`。 + +* `RelProvider`已重命名为`LinkRelationProvider`,并返回`LinkRelation`实例,而不是`String`s。 + +* `VndError`已被移动到`mediatype.vnderror`包中。 + +#### [](#migrate-to-1.0.script)1.1.2。迁移脚本 + +你可以在应用程序根目录中找到[a script](https://github.com/spring-projects/spring-hateoas/tree/master/etc)以运行,该根目录将更新所有导入语句和静态方法引用,使其指向在我们的源代码存储库中移动的 Spring Hateoas 类型。只需下载它,从你的项目根目录运行它。默认情况下,它将检查所有 Java 源文件,并用新的 Hateoas 类型引用替换旧的 Spring Hateoas 类型引用。 + +例 1。迁移脚本的示例应用程序 + +``` +$ ./migrate-to-1.0.sh + +Migrating Spring HATEOAS references to 1.0 for files : *.java + +Adapting ./src/main/java/… +… + +Done! +``` + +请注意,该脚本不一定能够完全修复所有更改,但它应该包含最重要的重构。 + +现在验证对你最喜欢的 Git 客户机中的文件所做的更改,并根据需要提交。如果你发现方法或类型引用未被删除,请打开票据在出问题追踪器。 + +#### [](#migration.1-0-M3-to-1-0-RC1)1.1.3。从 1.0m3 迁移到 1.0rc1 + +* `Link.andAffordance(…)`可提供的细节已移至`Affordances`。要手动构建`Affordance`实例,现在使用`Affordances.of(link).afford(…)`。还请注意从`Affordances`公开的新`AffordanceBuilder`类型,以便流畅地使用。详见[启示](#server.affordances)。 + +* `AffordanceModelFactory.getAffordanceModel(…)`现在接收`InputPayloadMetadata`和`PayloadMetadata`实例,而不是`ResolvableType`s,以允许非基于类型的实现。定制的媒体类型实现必须相应地进行调整。 + +* HAL 窗体现在不呈现属性属性,如果它们的值符合规范中定义的默认值。也就是说,如果以前`required`显式地设置为`false`,那么我们现在省略`required`的条目。我们现在也只强制那些使用`PATCH`作为 HTTP 方法的模板不需要它们。 + +## [](#fundamentals)2。基本原理 + +本节介绍了 Spring Hateoas 的基础知识及其基本的领域抽象。 + +### [](#fundamentals.links)2.1。链接 + +超媒体的基本思想是用超媒体元素来丰富资源的表示。最简单的形式是链接。它们指示客户机可以导航到特定的资源。相关资源的语义是在所谓的链接关系中定义的。你可能已经在 HTML 文件的头中看到了这一点: + +例 2。HTML 文档中的链接 + +``` + +``` + +如你所见,该链接指向一个资源`theme.css`,并指示它是一个样式表。链接通常带有额外的信息,例如指向的资源将返回的媒体类型。然而,链接的基本组成部分是它的引用和关系。 + +Spring Hateoas 允许你通过其不可变的`Link`值类型处理链接。它的构造函数接受超文本引用和链接关系,后者默认为 IANA 链接关系`self`。在[链接关系](#fundamentals.link-relations)中阅读有关后者的更多信息。 + +例 3。使用链接 + +``` +Link link = Link.of("/something"); +assertThat(link.getHref()).isEqualTo("/something"); +assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF); + +link = Link.of("/something", "my-rel"); +assertThat(link.getHref()).isEqualTo("/something"); +assertThat(link.getRel()).isEqualTo(LinkRelation.of("my-rel")); +``` + +`Link`公开了[RFC-8288](https://tools.ietf.org/html/rfc8288)中定义的其他属性。你可以通过在`Link`实例上调用相应的 wither 方法来设置它们。 + +在[ Building links in Spring MVC](#server.link-builder.webmvc)和[Building links in Spring WebFlux](#server.link-builder.webflux)中找到有关如何创建指向 Spring MVC 和 Spring WebFlux 控制器的链接的更多信息。 + +### [](#fundamentals.uri-templates)2.2。URI 模板 + +对于 Spring Hateoas`Link`,超文本引用不仅可以是一个 URI,而且根据[RFC-6570](https://tools.ietf.org/html/rfc6570)也可以是一个 URI 模板。URI 模板包含所谓的模板变量,并允许扩展这些参数。这允许客户机将参数化模板转换为 URI,而无需了解最终 URI 的结构,只需了解变量的名称即可。 + +例 4。使用带有模板化 URI 的链接 + +``` +Link link = Link.of("/{segment}/something{?parameter}"); +assertThat(link.isTemplated()).isTrue(); (1) +assertThat(link.getVariableNames()).contains("segment", "parameter"); (2) + +Map values = new HashMap<>(); +values.put("segment", "path"); +values.put("parameter", 42); + +assertThat(link.expand(values).getHref()) (3) + .isEqualTo("/path/something?parameter=42"); +``` + +|**1**|`Link`实例表示它是模板化的,即它包含一个 URI 模板。| +|-----|---------------------------------------------------------------------------------| +|**2**|它公开了模板中包含的参数。| +|**3**|它允许参数的扩展。| + +URI 模板可以手动构建,然后添加模板变量。 + +例 5。使用 URI 模板 + +``` +UriTemplate template = UriTemplate.of("/{segment}/something") + .with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM); + +assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}"); +``` + +### [](#fundamentals.link-relations)2.3。链接关系 + +要表示目标资源与当前资源的关系,可以使用所谓的链接关系。 Spring Hateoas 提供了一个`LinkRelation`类型来轻松地创建基于`String`的实例。 + +#### [](#fundamentals.link-relations.iana)2.3.1。IANA 链接关系 + +因特网分配号码管理局包含一组[预定义的链接关系](https://www.iana.org/assignments/link-relations/link-relations.xhtml)。它们可以通过`IanaLinkRelations`引用。 + +例 6。使用 IANA 链接关系 + +``` +Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT); + +assertThat(link.getRel()).isEqualTo(LinkRelation.of("next")); +assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue(); +``` + +### [](#fundamentals.representation-models)2.4。[]()表示模型 + +Spring 为了方便地创建富含超媒体的表示,Hateoas 提供了一组在其根上带有`RepresentationModel`的类。它基本上是一个`Link`s 集合的容器,并且有方便的方法将它们添加到模型中。模型稍后可以呈现为各种媒体类型格式,这些格式将定义超媒体元素在表示中的外观。有关此的更多信息,请查看[媒体类型](#mediatypes)。 + +例 7。`RepresentationModel`类层次结构 + +diagram classes + +使用`RepresentationModel`的默认方法是创建它的一个子类,以包含表示应该包含的所有属性,创建该类的实例,填充属性并用链接丰富它。 + +例 8。样本表示模型类型 + +``` +class PersonModel extends RepresentationModel { + + String firstname, lastname; +} +``` + +要让`RepresentationModel.add(…)`返回自身的实例,需要使用泛型自类型。模型类型现在可以这样使用: + +例 9。使用 Person 表示模型 + +``` +PersonModel model = new PersonModel(); +model.firstname = "Dave"; +model.lastname = "Matthews"; +model.add(Link.of("https://myhost/people/42")); +``` + +如果你从 Spring MVC 或 WebFlux 控制器返回了这样的实例,并且客户机发送了一个`Accept`头,将其设置为`application/hal+json`,则响应将如下所示: + +例 10。为人员表示模型生成的 HAL 表示 + +``` +{ + "_links" : { + "self" : { + "href" : "https://myhost/people/42" + } + }, + "firstname" : "Dave", + "lastname" : "Matthews" +} +``` + +#### [](#fundamentals.entity-model)2.4.1。项目资源表示模型 + +对于由单一对象或概念支持的资源,存在`EntityModel`类型的便利。与为每个概念创建自定义模型类型不同,你可以重用一个已经存在的类型,并将它的实例封装到`EntityModel`中。 + +例 11。使用`EntityModel`包装现有对象 + +``` +Person person = new Person("Dave", "Matthews"); +EntityModel model = EntityModel.of(person); +``` + +#### 2.4.2.集合资源表示模型 + +对于概念上是集合的资源,可以使用`CollectionModel`。它的元素可以是简单对象,也可以是`RepresentationModel`实例。 + +例 12。使用`CollectionModel`包装现有对象的集合 + +``` +Collection people = Collections.singleton(new Person("Dave", "Matthews")); +CollectionModel model = CollectionModel.of(people); +``` + +## [](#server)3。服务器端支持 + +### [](#server.link-builder.webmvc)3.1。[]()[]()在 Spring MVC 中构建链接 + +现在我们已经有了域词汇表,但主要的挑战仍然是:如何创建实际的 URI,并以一种不那么脆弱的方式包装到`Link`实例中。现在,我们将不得不到处复制 URI 字符串。这样做是脆弱和不可维护的。 + +假设你的 Spring MVC 控制器实现如下: + +``` +@Controller +class PersonController { + + @GetMapping("/people") + HttpEntity showAll() { … } + + @GetMapping(value = "/{person}", method = RequestMethod.GET) + HttpEntity show(@PathVariable Long person) { … } +} +``` + +我们在这里看到了两个惯例。第一个是通过`@GetMapping`控制器方法的注释公开的集合资源,该集合的各个元素作为直接子资源公开。集合资源可以在一个简单的 URI(如刚才所示)或更复杂的 URI(如`/people/{id}/addresses`)上公开。假设你想链接到所有人的集合资源。按照上面的方法会导致两个问题: + +* 要创建一个绝对 URI,你需要查找协议、主机名、端口、 Servlet 基和其他值。这很麻烦,并且需要难看的手工字符串连接代码。 + +* 你可能不希望在基础 URI 的顶部连接`/people`,因为这样你将不得不在多个位置维护信息。如果你更改了映射,那么你必须更改指向它的所有客户机。 + +Spring Hateoas 现在提供了一个`WebMvcLinkBuilder`,它允许你通过指向控制器类来创建链接。下面的示例展示了如何做到这一点: + +``` +import static org.sfw.hateoas.server.mvc.WebMvcLinkBuilder.*; + +Link link = linkTo(PersonController.class).withRel("people"); + +assertThat(link.getRel()).isEqualTo(LinkRelation.of("people")); +assertThat(link.getHref()).endsWith("/people"); +``` + +`WebMvcLinkBuilder`在引擎盖下使用 Spring 的`ServletUriComponentsBuilder`从当前请求中获得基本的 URI 信息。假设你的应用程序运行在`[localhost:8080/your-app](http://localhost:8080/your-app)`,那么这正是你正在构建附加部分的 URI。构建器现在检查给定的控制器类的根映射,并以`[localhost:8080/your-app/people](http://localhost:8080/your-app/people)`结束。你还可以构建更多的嵌套链接。下面的示例展示了如何做到这一点: + +``` +Person person = new Person(1L, "Dave", "Matthews"); +// /person / 1 +Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel(); +assertThat(link.getRel(), is(IanaLinkRelation.SELF.value())); +assertThat(link.getHref(), endsWith("/people/1")); +``` + +构建器还允许创建 URI 实例来构建(例如,响应标头值): + +``` +HttpHeaders headers = new HttpHeaders(); +headers.setLocation(linkTo(PersonController.class).slash(person).toUri()); + +return new ResponseEntity(headers, HttpStatus.CREATED); +``` + +#### [](#fundamentals.obtaining-links.builder.methods)3.1.1。建立指向方法的链接 + +你甚至可以构建指向方法的链接,或者创建虚拟控制器方法调用。第一种方法是将`Method`实例交给`WebMvcLinkBuilder`。下面的示例展示了如何做到这一点: + +``` +Method method = PersonController.class.getMethod("show", Long.class); +Link link = linkTo(method, 2L).withSelfRel(); + +assertThat(link.getHref()).endsWith("/people/2")); +``` + +这仍然有点令人不满意,因为我们必须首先获得一个`Method`实例,该实例抛出一个异常,并且通常非常麻烦。至少我们不会重复映射。一种更好的方法是在控制器代理上对目标方法进行虚拟方法调用,我们可以使用`methodOn(…)`助手来创建该方法。下面的示例展示了如何做到这一点: + +``` +Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel(); + +assertThat(link.getHref()).endsWith("/people/2"); +``` + +`methodOn(…)`创建控制器类的代理,该代理记录方法调用,并在为方法的返回类型创建的代理中公开它。这允许对我们想要获得的映射的方法进行 Fluent 表达式。然而,在使用这种技术可以获得的方法上有一些限制: + +* 返回类型必须能够代理,因为我们需要在其上公开方法调用。 + +* 传递到方法中的参数通常被忽略(通过`@PathVariable`引用的参数除外,因为它们构成了 URI)。 + +### [](#server.link-builder.webflux)3.2。在 Spring WebFlux 中构建链接 + +TODO + +### [](#server.affordances)3.3。启示 + +> > > > +> 环境所能提供的就是它所提供的……它所提供的或提供的,不管是好是坏。词典中有动词“to afford”,但名词“affordance”没有。这是我编的。 +> > > > + +——James J.Gibson + +> 视觉感知的生态学方法(第 126 页) + +基于 REST 的资源不仅提供数据,还提供控制。形成灵活服务的最后一个要素是关于如何使用各种控件的详细说明**启示**。 Spring 由于提供与链接相关联,Hateoas 提供了一个 API,以便根据需要将尽可能多的相关方法附加到链接上。正如你可以通过指向 Spring MVC 控制器方法来创建链接一样(有关详细信息,请参见[ Building links in Spring MVC](#server.link-builder.webmvc)),你… + +下面的代码展示了如何使用**自我**链接并关联另外两个启示: + +例 13。连接到`GET /employees/{id}`的启示 + +``` +@GetMapping("/employees/{id}") +public EntityModel findOne(@PathVariable Integer id) { + + Class controllerClass = EmployeeController.class; + + // Start the affordance with the "self" link, i.e. this method. + Link findOneLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel(); (1) + + // Return the affordance + a link back to the entire collection resource. + return EntityModel.of(EMPLOYEES.get(id), // + findOneLink // + .andAffordance(afford(methodOn(controllerClass).updateEmployee(null, id))) (2) + .andAffordance(afford(methodOn(controllerClass).partiallyUpdateEmployee(null, id)))); (3) +} +``` + +|**1**|创建**自我**链接。| +|-----|--------------------------------------------------------------------| +|**2**|将`updateEmployee`方法与`self`链接关联起来。| +|**3**|将`partiallyUpdateEmployee`方法与`self`链接关联起来。| + +使用`.andAffordance(afford(…​))`,你可以使用控制器的方法将`PUT`和`PATCH`操作连接到`GET`操作。假设上面的相关方法**提供**看起来是这样的: + +例 14。响应`updateEmpoyee`的`PUT /employees/{id}`方法 + +``` +@PutMapping("/employees/{id}") +public ResponseEntity updateEmployee( // + @RequestBody EntityModel employee, @PathVariable Integer id) +``` + +例 15。响应`PATCH /employees/{id}`的`partiallyUpdateEmployee`方法 + +``` +@PatchMapping("/employees/{id}") +public ResponseEntity partiallyUpdateEmployee( // + @RequestBody EntityModel employee, @PathVariable Integer id) +``` + +指向那些使用`afford(…)`方法的方法将导致 Spring Hateoas 分析请求主体和响应类型并捕获元数据,以允许不同的媒体类型实现使用该信息将其转换为输入和输出的描述。 + +#### [](#server.affordances.api)3.3.1。人工构建可供参考的功能 + +虽然注册链接的主要方法是提供支持,但可能有必要手动构建其中的一些。这可以通过使用`Affordances`API 来实现: + +例 16。使用`Affordances`API 手动注册启示 + +``` +var methodInvocation = methodOn(EmployeeController.class).all(); + +var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) (1) + + .afford(HttpMethod.POST) (2) + .withInputAndOutput(Employee.class) // + .withName("createEmployee") // + + .andAfford(HttpMethod.GET) (3) + .withOutput(Employee.class) // + .addParameters(// + QueryParameter.optional("name"), // + QueryParameter.optional("role")) // + .withName("search") // + + .toLink(); +``` + +|**1**|首先,从一个`Link`实例创建`Affordances`的实例,创建用于描述提供的上下文。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|每一个启示都是从它应该支持的 HTTP 方法开始的。然后,我们将一种类型注册为有效负载描述,并显式地给出它的名称。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这有效地创建了与指向`EmployeeController.newEmployee(…)`的指针相同的启示。| +|**3**|构建下一个启示是为了反映指向`EmployeeController.search(…)`的指针所发生的情况。这里我们将`Employee`定义为创建并显式注册`QueryParameter`s 的响应的模型。| + +启示由特定于媒体类型的启示模型支持,该模型将一般的启示元数据转换为特定的表示形式。请务必检查[媒体类型](#mediatypes)部分中有关启示的部分,以找到有关如何控制该元数据的公开的更多详细信息。 + +### [](#server.link-builder.forwarded-headers)3.4。转发头处理 + +[RFC-7239 转发头](https://tools.ietf.org/html/rfc7239)通常用于应用程序位于代理、负载均衡器或云中。实际接收 Web 请求的节点是基础设施的一部分,*向前*是对应用程序的请求。 + +你的应用程序可能运行在`localhost:8080`上,但是对于外部世界来说,你应该运行在`reallycoolsite.com`上(以及 Web 的标准端口 80 上)。通过使代理包括额外的头(许多人已经这样做了), Spring Hateoas 可以在使用 Spring 框架功能来获得原始请求的基本 URI 时正确地生成链接。 + +| |任何可以根据外部输入改变根 URI 的内容都必须得到适当的保护,
这就是为什么,默认情况下,转发头处理是**已禁用**。
你必须使其能够运行。
如果你要部署到云或控制代理和负载均衡器的配置中,那么你肯定想要使用此功能。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要启用转发头处理,你需要在应用程序中注册 Spring MVC 的`ForwardedHeaderFilter`(details[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#filters-forwarded-headers))或 Spring WebFlux 的`ForwardedHeaderTransformer`(details[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-forwarded-headers))。在 Spring 引导应用程序中,这些组件可以简单地声明为 Spring bean,如[here](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-embedded-container-servlets-filters-listeners-beans)所述。 + +例 17。注册`ForwardedHeaderFilter` + +``` +@Bean +ForwardedHeaderFilter forwardedHeaderFilter() { + return new ForwardedHeaderFilter(); +} +``` + +这将创建一个 Servlet 过滤器,用于处理所有`X-Forwarded-…`标题。它将在 Servlet 处理程序中正确地注册它。 + +对于 Spring WebFlux 应用程序,对应的反应性是`ForwardedHeaderTransformer`: + +例 18。注册`ForwardedHeaderTransformer` + +``` +@Bean +ForwardedHeaderTransformer forwardedHeaderTransformer() { + return new ForwardedHeaderTransformer(); +} +``` + +这将创建一个函数,用于转换反应性 Web 请求,处理`X-Forwarded-…`头。它将在 WebFlux 中正确地注册它。 + +有了上面所示的配置,通过`X-Forwarded-…`头的请求将会看到那些反映在生成的链接中的请求: + +例 19。使用`X-Forwarded-…`头的请求 + +``` +curl -v localhost:8080/employees \ + -H 'X-Forwarded-Proto: https' \ + -H 'X-Forwarded-Host: example.com' \ + -H 'X-Forwarded-Port: 9001' +``` + +例 20。相应的响应与所生成的链接一起来考虑那些报头 + +``` +{ + "_embedded": { + "employees": [ + { + "id": 1, + "name": "Bilbo Baggins", + "role": "burglar", + "_links": { + "self": { + "href": "https://example.com:9001/employees/1" + }, + "employees": { + "href": "https://example.com:9001/employees" + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://example.com:9001/employees" + }, + "root": { + "href": "https://example.com:9001" + } + } +} +``` + +### [](#server.entity-links)3.5。[]()使用 EntityLinks 接口 + +| |`EntityLinks`及其各种实现目前还没有为 Spring WebFlux 应用程序提供开箱即用的。
`EntityLinks`SPI 中定义的契约最初是针对 Spring Web MVC 的,并不考虑反应器类型。
开发支持反应式编程的类似契约仍在进行中。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +到目前为止,我们已经通过指向 Web 框架实现(即 Spring MVC 控制器)创建了链接,并检查了映射。在许多情况下,这些类本质上是由模型类支持的读写表示。 + +现在,`EntityLinks`接口公开了一个 API,以便根据模型类型查找`Link`或`LinkBuilder`。这些方法本质上返回指向集合资源(例如`/people`)或项资源(例如`/people/1`)的链接。下面的示例展示了如何使用`EntityLinks`: + +``` +EntityLinks links = …; +LinkBuilder builder = links.linkFor(Customer.class); +Link link = links.linkToItemResource(Customer.class, 1L); +``` + +通过在 Spring MVC 配置中激活`@EnableHypermediaSupport`,通过依赖注入可以使用`EntityLinks`。这将导致`EntityLinks`的各种默认实现被注册。最基本的是`ControllerEntityLinks`,它检查 SpringMVC 控制器类。如果你想注册你自己的`EntityLinks`的实现,请查看[本节](#server.entity-links.spi)。 + +#### [](#server.entity-links.controller)3.5.1。基于 Spring MVC 控制器的实体链接 + +激活实体链接功能将检查当前`ApplicationContext`中可用的所有 Spring MVC 控制器的`@ExposesResourceFor(…)`注释。注释公开了控制器管理的模型类型。除此之外,我们假定你遵循以下 URI 映射设置和约定: + +* 类型级别`@ExposesResourceFor(…)`,声明控制器公开用于哪个实体类型的集合和项资源。 + +* 表示集合资源的类级基映射。 + +* 一种附加的方法级映射,它扩展了映射以追加一个标识符作为附加的路径段。 + +下面的示例显示了一个`EntityLinks`功能的控制器的实现: + +``` +@Controller +@ExposesResourceFor(Order.class) (1) +@RequestMapping("/orders") (2) +class OrderController { + + @GetMapping (3) + ResponseEntity orders(…) { … } + + @GetMapping("{id}") (4) + ResponseEntity order(@PathVariable("id") … ) { … } +} +``` + +|**1**|控制器表示它正在公开实体`Order`的集合和项资源。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|它的集合资源在`/orders`下公开| +|**3**|该集合资源可以处理`GET`请求。在方便的时候为其他 HTTP 方法添加更多的方法。| +|**4**|一种额外的控制器方法来处理从属资源,该从属资源采用一个路径变量来公开一个项资源,即单个`Order`。| + +有了这一点,当你在 Spring MVC 配置中启用`EntityLinks``@EnableHypermediaSupport`时,你可以创建到控制器的链接,如下所示: + +``` +@Controller +class PaymentController { + + private final EntityLinks entityLinks; + + PaymentController(EntityLinks entityLinks) { (1) + this.entityLinks = entityLinks; + } + + @PutMapping(…) + ResponseEntity payment(@PathVariable Long orderId) { + + Link link = entityLinks.linkToItemResource(Order.class, orderId); (2) + … + } +} +``` + +|**1**|在配置中插入由`@EnableHypermediaSupport`提供的`EntityLinks`。| +|-----|----------------------------------------------------------------------------------------| +|**2**|使用 API 通过使用实体类型而不是控制器类来构建链接。| + +如你所见,你可以引用管理`Order`实例的资源,而不必显式地引用`OrderController`实例。 + +#### [](#server.entity-links.api)3.5.2。详细介绍 EntityLinks API + +从根本上说,`EntityLinks`允许将`LinkBuilder`s 和`Link`实例构建为实体类型的集合和项资源。以`linkFor…`开头的方法将产生`LinkBuilder`实例,供你扩展和增加额外的路径段、参数等。方法以`linkTo`为起点,生产制备充分的`Link`实例。 + +虽然对于集合资源来说,提供一个实体类型就足够了,但是指向项资源的链接将需要提供一个标识符。这通常看起来是这样的: + +例 21。获取到项目资源的链接 + +``` +entityLinks.linkToItemResource(order, order.getId()); +``` + +如果你发现自己重复这些方法调用,则可以将标识符提取步骤拉出到一个可重用的`Function`中,以便在不同的调用中重用: + +``` +Function idExtractor = Order::getId; (1) + +entityLinks.linkToItemResource(order, idExtractor); (2) +``` + +|**1**|标识符的提取是外部化的,因此它可以保存在一个域或常数中。| +|-----|----------------------------------------------------------------------------------------| +|**2**|使用抽取器进行链接查找。| + +##### [](#server.entity-links.api.typed)typedentylinks + +由于控制器实现通常是围绕实体类型进行分组的,因此你经常会发现自己在整个控制器类中使用相同的提取器函数(有关详细信息,请参见[详细介绍 EntityLinks API](#server.entity-links.api))。我们可以通过获得一个`TypedEntityLinks`实例来集中标识符提取逻辑,从而一次提供提取器,这样实际的查找就完全不必再处理提取了。 + +例 22。使用 typedentitylinks + +``` +class OrderController { + + private final TypedEntityLinks links; + + OrderController(EntityLinks entityLinks) { (1) + this.links = entityLinks.forType(Order::getId); (2) + } + + @GetMapping + ResponseEntity someMethod(…) { + + Order order = … // lookup order + + Link link = links.linkToItemResource(order); (3) + } +} +``` + +|**1**|注入一个`EntityLinks`实例。| +|-----|------------------------------------------------------------------------------------------------| +|**2**|表示你将使用特定的标识符提取器函数查找`Order`实例。| +|**3**|基于唯一的`Order`实例查找项目资源链接。| + +#### [](#server.entity-links.spi)3.5.3。作为 SPI 的 EntityLink + +由`@EnableHypermediaSupport`创建的`EntityLinks`实例的类型为`DelegatingEntityLinks`,它将在`ApplicationContext`中以 bean 的形式获取所有其他`EntityLinks`实现。它被注册为 primary Bean,因此当你通常注入`EntityLinks`时,它始终是唯一的注入候选。`ControllerEntityLinks`是将包含在设置中的默认实现,但是用户可以自由地实现和注册自己的实现。使那些可用于`EntityLinks`实例可用于注入是将你的实现注册为 Spring Bean 的问题。 + +例 23。声明自定义 EntityLinks 实现 + +``` +@Configuration +class CustomEntityLinksConfiguration { + + @Bean + MyEntityLinks myEntityLinks(…) { + return new MyEntityLinks(…); + } +} +``` + +这种机制的可扩展性的一个例子是 Spring data rest 的[`RepositoryEntityLinks`](https://github.com/ Spring-projects/ Spring-data-rest/blob/3a0cba94a2cc8739375ECF 24086da2f7c3bbf038/ Spring-data-rest-webmvc/mvc/main/main/java/org/repositorframework/data/data/repositforemframework/rest/spramework/sprint/springmvc/springmvc/support/support/sup 同时,它甚至还公开了其他类型资源的其他查找方法。如果你想要利用这些,只需显式地注入`RepositoryEntityLinks`。 + +### [](#server.representation-model-assembler)3.6。[]()表示模型汇编器 + +由于必须在多个地方使用从实体到表示模型的映射,因此创建一个专门的类来负责这样做是有意义的。转换包含非常自定义的步骤,但也包含一些样板步骤: + +1. 模型类的实例化 + +2. 添加一个链接,其`rel`的`self`指向呈现的资源。 + +Spring Hateoas 现在提供了一个`RepresentationModelAssemblerSupport`基类,它有助于减少你需要编写的代码量。下面的示例展示了如何使用它: + +``` +class PersonModelAssembler extends RepresentationModelAssemblerSupport { + + public PersonModelAssembler() { + super(PersonController.class, PersonModel.class); + } + + @Override + public PersonModel toModel(Person person) { + + PersonModel resource = createResource(person); + // … do further mapping + return resource; + } +} +``` + +| |`createResource(…​)`是你编写的代码,用于实例化给定`PersonModel`对象的`Person`对象。它应该只关注于设置属性,而不是填充`Links`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +正如我们在前面的示例中所做的那样,将类设置为你提供了以下好处: + +* 有几个`createModelWithId(…)`方法可以让你创建资源的实例,并在其中添加一个`Link`,rel 为`self`。该链接的 href 由配置的控制器的请求映射加上实体的 ID(例如,`/people/1`)确定。 + +* 资源类型通过反射进行实例化,并需要一个 no-arg 构造函数。如果你想使用专用的构造函数或避免反射性能开销,可以覆盖`instantiateModel(…)`。 + +然后可以使用汇编程序来组装`RepresentationModel`或`CollectionModel`。下面的示例创建了`CollectionModel`的`PersonModel`实例: + +``` +Person person = new Person(…); +Iterable people = Collections.singletonList(person); + +PersonModelAssembler assembler = new PersonModelAssembler(); +PersonModel model = assembler.toModel(person); +CollectionModel model = assembler.toCollectionModel(people); +``` + +### [](#server.processors)3.7。表示模型处理器 + +有时,你需要在超媒体表示[assembled](#server.representation-model-assembler)之后对其进行调整。 + +一个完美的例子是,你有一个处理订单履行的控制器,但你需要添加与付款相关的链接。 + +想象一下,让你的订购系统产生这种类型的超媒体: + +``` +{ + "orderId" : "42", + "state" : "AWAITING_PAYMENT", + "_links" : { + "self" : { + "href" : "http://localhost/orders/999" + } + } +} +``` + +你希望添加一个链接,以便客户端可以进行付款,但不想将有关`PaymentController`的详细信息混入`OrderController`中。 + +你可以这样写`RepresentationModelProcessor`,而不是污染你的订购系统的细节: + +``` +public class PaymentProcessor implements RepresentationModelProcessor> { (1) + + @Override + public EntityModel process(EntityModel model) { + + model.add( (2) + Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) // + .expand(model.getContent().getOrderId())); + + return model; (3) + } +} +``` + +|**1**|此处理器将只应用于`EntityModel`对象。| +|-----|-------------------------------------------------------------------------------| +|**2**|通过添加一个无条件链接来操作现有的`EntityModel`对象。| +|**3**|返回`EntityModel`,以便将其序列化为所请求的媒体类型。| + +在应用程序中注册处理器: + +``` +@Configuration +public class PaymentProcessingApp { + + @Bean + PaymentProcessor paymentProcessor() { + return new PaymentProcessor(); + } +} +``` + +现在,当你发出`Order`的超媒体重发消息时,客户机将接收到以下内容: + +``` +{ + "orderId" : "42", + "state" : "AWAITING_PAYMENT", + "_links" : { + "self" : { + "href" : "http://localhost/orders/999" + }, + "payments" : { (1) + "href" : "/payments/42" (2) + } + } +} +``` + +|**1**|你可以看到插入的`LinkRelation.of("payments")`作为此链接的关系。| +|-----|-----------------------------------------------------------------------------| +|**2**|URI 由处理器提供。| + +这个例子很简单,但你可以很容易地: + +* 使用`WebMvcLinkBuilder`或`WebFluxLinkBuilder`构建到`PaymentController`的动态链接。 + +* 插入有条件地添加由状态驱动的其他链接(例如`cancel`,`amend`)所需的任何服务。 + +* 利用诸如 Spring Security 之类的交叉服务,根据当前用户的上下文添加、删除或修改链接。 + +同样,在这个示例中,`PaymentProcessor`改变了提供的`EntityModel`。你还可以用另一个对象替换它。请注意,API 要求返回类型等于输入类型。 + +### [](#server.rel-provider)3.8。[]()使用`LinkRelationProvider`API + +在构建链接时,通常需要确定要为该链接使用的关系类型。在大多数情况下,关系类型与(域)类型直接关联。我们封装了详细的算法来查找`LinkRelationProvider`API 背后的关系类型,该 API 允许你确定单个资源和集合资源的关系类型。查找关系类型的算法如下: + +1. 如果类型是用`@Relation`注释的,那么我们使用在注释中配置的值。 + +2. 如果不是,那么对于集合`rel`,我们默认使用未大写的简单类名加上附加的`List`。 + +3. 如果[EVO 折弯机](https://github.com/atteo/evo-inflector)JAR 在 Classpath 中,则使用由多元化算法提供的单个资源`rel`的复数形式。 + +4. `@Controller`注释带`@ExposesResourceFor`的 classes(详见[使用 EntityLinks 接口](#server.entity-links))透明地查找注释带中所配置类型的关系类型,这样就可以使用`LinkRelationProvider.getItemResourceRelFor(MyController.class)`并获取所公开的域类型的关系类型。 + +当你使用`@EnableHypermediaSupport`时,`LinkRelationProvider`会自动暴露为 Spring Bean。你可以通过实现接口并将它们依次以 Spring bean 的形式公开来插入自定义提供程序。 + +## [](#mediatypes)4。媒体类型 + +### [](#mediatypes.hal)4.1。HAL-超文本应用程序语言 + +[JSON 超文本应用程序语言](https://tools.ietf.org/html/draft-kelly-json-hal-08)或 HAL 是最简单和最广泛采用的超媒体媒体类型之一,当不讨论特定的 Web 堆栈时采用。 + +这是 Spring Hateoas 采用的第一种基于规范的媒体类型。 + +#### [](#mediatypes.hal.models)4.1.1。HAL 表示模型的构建 + +从 Spring Hateoas1.1 开始,我们提供了一个专用的`HalModelBuilder`,它允许通过一个 HAL 的惯用 API 创建`RepresentationModel`实例。这些是它的基本假设: + +1. HAL 表示可以由任意对象(实体)支持,该对象构建表示中包含的域字段。 + +2. 可以通过各种嵌入式文档来丰富表示,这些文档可以是任意对象,也可以是 HAL 表示本身(即包含嵌套的嵌入式和链接)。 + +3. 某些 HAL 特定的模式(例如预览)可以直接用于 API 中,这样设置表示的代码读起来就像你按照这些习惯用法描述 HAL 表示一样。 + +下面是一个使用的 API 示例: + +``` +// An order +var order = new Order(…); (1) + +// The customer who placed the order +var customer = customer.findById(order.getCustomerId()); + +var customerLink = Link.of("/orders/{id}/customer") (2) + .expand(order.getId()) + .withRel("customer"); + +var additional = … + +var model = HalModelBuilder.halModelOf(order) + .preview(new CustomerSummary(customer)) (3) + .forLink(customerLink) (4) + .embed(additional) (5) + .link(Link.of(…, IanaLinkRelations.SELF)); + .build(); +``` + +|**1**|我们设置了一些域类型。在本例中,与下订单的客户有关系的订单。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|我们准备了一个指向将公开客户详细信息的资源的链接。| +|**3**|我们开始通过提供应该在`_embeddable`子句中呈现的有效负载来构建预览。| +|**4**|通过提供目标链接,我们得出了预览的结论。它透明地被添加到`_links`对象中,并且它的链接关系被用作上一步中提供的对象的键。| +|**5**|可以添加其他对象以显示在`_embedded`下。
列出这些对象的键来自对象关系设置。它们可以通过`@Relation`或专用的`LinkRelationProvider`进行定制(有关详细信息,请参见[使用`LinkRelationProvider`API](#server.rel-provider))。| + +``` +{ + "_links" : { + "self" : { "href" : "…" }, (1) + "customer" : { "href" : "/orders/4711/customer" } (2) + }, + "_embedded" : { + "customer" : { … }, (3) + "additional" : { … } (4) + } +} +``` + +|**1**|显式提供的`self`链接。| +|-----|--------------------------------------------------------------------------| +|**2**|通过`….preview(…).forLink(…)`透明地添加的`customer`链接。| +|**3**|提供的预览对象。| +|**4**|通过显式`….embed(…)`添加的其他元素。| + +在 HAL 中`_embedded`也用于表示顶级集合。它们通常按对象类型派生的链接关系分组。即订单列表在 HAL 中如下所示: + +``` +{ + "_embedded" : { + "orders : [ + … (1) + ] + } +} +``` + +|**1**|个人订单文件放在这里。| +|-----|-----------------------------------| + +创建这样的表示非常简单: + +``` +Collection orders = …; + +HalModelBuilder.emptyHalDocument() + .embed(orders); +``` + +也就是说,如果订单是空的,就无法推导出在`_embedded`中出现的链接关系,因此如果集合是空的,文档将保持为空。 + +如果你希望显式地通信一个空集合,则可以将一个类型传递到`….embed(…)`方法的重载中,并接受`Collection`。如果交给方法的集合是空的,这将导致呈现一个字段,其链接关系是从给定类型派生的。 + +``` +HalModelBuilder.emptyHalModel() + .embed(Collections.emptyList(), Order.class); + // or + .embed(Collections.emptyList(), LinkRelation.of("orders")); +``` + +将创建以下更明确的表示。 + +``` +{ + "_embedded" : { + "orders" : [] + } +} +``` + +#### [](#mediatypes.hal.configuration)4.1.2。配置链接呈现 + +在 HAL 中,`_links`条目是一个 JSON 对象。属性名是[链接关系](#fundamentals.link-relations),每个值都是[链接对象或链接对象数组](https://tools.ietf.org/html/draft-kelly-json-hal-07#section-4.1.1)。 + +对于具有两个或多个链接的给定的链接关系,规范在表示上是明确的: + +例 24。具有与一个关系相关联的两个链接的 HAL 文档 + +``` +{ + "_links": { + "item": [ + { "href": "https://myhost/cart/42" }, + { "href": "https://myhost/inventory/12" } + ] + }, + "customer": "Dave Matthews" +} +``` + +但是,如果给定的关系只有一个链接,则规范是不明确的。你可以将其呈现为单个对象,也可以呈现为单个项目数组。 + +默认情况下, Spring Hateoas 使用最简洁的方法,并呈现这样的单链接关系: + +例 25。以单个链接呈现为对象的 HAL 文档 + +``` +{ + "_links": { + "item": { "href": "https://myhost/inventory/12" } + }, + "customer": "Dave Matthews" +} +``` + +一些用户在使用 HAL 时不喜欢在数组和对象之间切换。他们更喜欢这种类型的渲染: + +例 26。以数组形式呈现单链路的 HAL + +``` +{ + "_links": { + "item": [{ "href": "https://myhost/inventory/12" }] + }, + "customer": "Dave Matthews" +} +``` + +如果你希望自定义此策略,那么你所要做的就是在应用程序配置中注入`HalConfiguration` Bean。有多种选择。 + +例 27。全局 HAL 单链路呈现策略 + +``` +@Bean +public HalConfiguration globalPolicy() { + return new HalConfiguration() // + .withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1) +} +``` + +|**1**|通过将所有单链接关系呈现为数组,从而覆盖 Hateoas 的默认设置。| +|-----|-----------------------------------------------------------------------------------| + +如果你只希望覆盖某些特定的链接关系,那么可以创建一个`HalConfiguration` Bean,如下所示: + +例 28。基于链路关系的 HAL 单链路呈现策略 + +``` +@Bean +public HalConfiguration linkRelationBasedPolicy() { + return new HalConfiguration() // + .withRenderSingleLinksFor( // + IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1) + .withRenderSingleLinksFor( // + LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2) +} +``` + +|**1**|总是将`item`链接关系呈现为数组。| +|-----|----------------------------------------------------------------------| +|**2**|当只有一个链接时,将`prev`链接关系作为对象呈现。| + +如果这两种方法都不符合你的需求,那么你可以使用 Ant 样式的路径模式: + +例 29。基于模式的 HAL 单链路呈现策略 + +``` +@Bean +public HalConfiguration patternBasedPolicy() { + return new HalConfiguration() // + .withRenderSingleLinksFor( // + "http*", RenderSingleLinks.AS_ARRAY); (1) +} +``` + +|**1**|将所有以`http`开头的链接关系呈现为一个数组。| +|-----|-------------------------------------------------------------| + +| |基于模式的方法使用 Spring 的`AntPathMatcher`。| +|---|----------------------------------------------------------| + +所有这些`HalConfiguration`威瑟斯可以组合成一个全面的政策。一定要广泛地测试你的 API,以避免出现意外。 + +#### [](#mediatypes.hal.i18n)4.1.3。链接标题国际化 + +HAL 为其链接对象定义了`title`属性。这些标题可以通过使用 Spring 的资源包抽象和名为`rest-messages`的资源包来填充,以便客户可以直接在其 UIS 中使用它们。这个包将被自动设置,并在 HAL 链接序列化期间使用。 + +要为链接定义标题,请使用下面的键模板`_links.$relationName.title`: + +例 30。样本`rest-messages.properties` + +``` +_links.cancel.title=Cancel order +_links.payment.title=Proceed to checkout +``` + +这将产生以下 HAL 代表: + +例 31。定义了链接标题的 HAL 示例文档 + +``` +{ + "_links" : { + "cancel" : { + "href" : "…" + "title" : "Cancel order" + }, + "payment" : { + "href" : "…" + "title" : "Proceed to checkout" + } + } +} +``` + +#### [](#mediatypes.hal.curie-provider)4.1.4。[]()使用`CurieProvider`API + +[网络链接 RFC](https://tools.ietf.org/html/rfc8288#section-2.1)描述了注册和扩展链接关系类型。已注册的 REL 是用[IANA 链接关系类型注册表](https://www.iana.org/assignments/link-relations/link-relations.xhtml)注册的众所周知的字符串。扩展`rel`URI 可以被不希望注册关系类型的应用程序使用。每个 URI 都是唯一标识关系类型的 URI。`rel`URI 可以序列化为紧凑的 URI 或[Curie](https://www.w3.org/TR/curie)。例如,一个`ex:persons`的居里表示链接关系类型`[example.com/rels/persons](https://example.com/rels/persons)`,如果`ex`被定义为`[example.com/rels/{rel}](https://example.com/rels/{rel})`。如果使用 curies,则基本 URI 必须存在于响应范围中。 + +由默认`RelProvider`创建的`rel`值是扩展关系类型,因此必须是 URI,这可能会导致大量的开销。`CurieProvider`API 负责这一点:它允许你将一个基本 URI 定义为一个 URI 模板和一个代表该基本 URI 的前缀。如果存在`CurieProvider`,则`RelProvider`前置所有带有居里前缀的`rel`值。此外,`curies`链接被自动地添加到 HAL 资源中。 + +以下配置定义了一个默认的居里提供程序: + +``` +@Configuration +@EnableWebMvc +@EnableHypermediaSupport(type= {HypermediaType.HAL}) +public class Config { + + @Bean + public CurieProvider curieProvider() { + return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}")); + } +} +``` + +注意,现在`ex:`前缀自动出现在所有未注册到 IANA 的 REL 值之前,如`ex:orders`。客户机可以使用`curies`链接将居里分解为完整形式。下面的示例展示了如何做到这一点: + +``` +{ + "_links": { + "self": { + "href": "https://myhost/person/1" + }, + "curies": { + "name": "ex", + "href": "https://example.com/rels/{rel}", + "templated": true + }, + "ex:orders": { + "href": "https://myhost/person/1/orders" + } + }, + "firstname": "Dave", + "lastname": "Matthews" +} +``` + +由于`CurieProvider`API 的目的是允许自动创建居里,因此每个应用程序范围只能定义一个`CurieProvider` Bean。 + +### [](#mediatypes.hal-forms)4.2。HAL-表格 + +[HAL-FORMS](https://rwcbook.github.io/hal-forms/)旨在向[HAL 媒体类型](#mediatypes.hal)添加运行时表单支持。 + +> > > > +> HAL-形式“看起来像 HAL。”然而,重要的是要记住,HAL 形式与 HAL 是不一样的——两者 +> 不应被认为在任何方面都是可以互换的。 +> > > > + +——Mike Amundsen + +> HAL-表格规格 + +要启用此媒体类型,请在代码中加入以下配置: + +例 32。HAL-支持表单的应用程序 + +``` +@Configuration +@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) +public class HalFormsApplication { + +} +``` + +每当客户机提供带有`application/prs.hal-forms+json`的`Accept`头时,你可以预期这样的情况: + +例 33。HAL-表格样本文件 + +``` +{ + "firstName" : "Frodo", + "lastName" : "Baggins", + "role" : "ring bearer", + "_links" : { + "self" : { + "href" : "http://localhost:8080/employees/1" + } + }, + "_templates" : { + "default" : { + "method" : "put", + "contentType" : "", + "properties" : [ { + "name" : "firstName", + "required" : true + }, { + "name" : "lastName", + "required" : true + }, { + "name" : "role", + "required" : true + } ] + }, + "partiallyUpdateEmployee" : { + "method" : "patch", + "contentType" : "", + "properties" : [ { + "name" : "firstName", + "required" : false + }, { + "name" : "lastName", + "required" : false + }, { + "name" : "role", + "required" : false + } ] + } + } +} +``` + +查看[HAL-表格规格](https://rwcbook.github.io/hal-forms/)以了解**\_ 模板**属性的详细信息。阅读有关[Affordances API](#server.affordances)的信息,以使用这些额外的元数据来增强控制器。 + +至于单项(`EntityModel`)和聚合根集合(`CollectionModel`), Spring Hateoas 将它们以相同的方式呈现为[HAL 文件](#mediatypes.hal)。 + +#### [](#mediatypes.hal-forms.metadata)4.2.1。定义 HAL-表单元数据 + +HAL-表单允许描述每个表单字段的标准。 Spring Hateoas 允许通过为输入和输出类型塑造模型类型并在其上使用注释来定制这些类型。 + +|Attribute |说明| +|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`readOnly`|如果属性没有 setter 方法,则设置为`true`。如果存在这种情况,则在访问器或字段上显式地使用 Jackson 的`@JsonProperty(Access.READ_ONLY)`。默认情况下不呈现,因此默认为`false`。| +| `regex` |可以通过在字段或类型上使用 JSR-303 的`@Pattern`注释来定制。在后一种情况下,该模式将用于声明为该特定类型的每个属性。默认情况下不呈现。| +|`required`|可以通过使用 JSR-303 的`@NotNull`进行自定义。默认情况下不呈现,因此默认为`false`。使用`PATCH`作为方法的模板将自动将所有属性设置为不需要的。| + +对于无法手动注释的类型,可以通过应用程序上下文中的`HalFormsConfiguration` Bean 注册自定义模式。 + +``` +@Configuration +class CustomConfiguration { + + @Bean + HalFormsConfiguration halFormsConfiguration() { + + HalFormsConfiguration configuration = new HalFormsConfiguration(); + configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}"); + } +} +``` + +此设置将使类型`CreditCardNumber`的表示模型属性的 HAL-Forms 模板属性声明一个`regex`字段,其值`[0-9]{16}`。 + +#### [](#mediatypes.hal-forms.i18n)4.2.2。表单属性的国际化 + +HAL 窗体包含用于人工解释的属性,例如模板的标题或属性提示。这些可以使用 Spring 的资源包支持和由 Spring Hateoas 默认配置的`rest-messages`资源包来定义和国际化。 + +##### 模板标题 + +要定义模板标题,请使用以下模式:`_templates.$affordanceName.title`。注意,在 HAL 表单中,模板的名称是`default`,如果它是唯一的一个。这意味着你通常必须使用 Affordance 描述的本地或完全限定输入类型名称来限定密钥。 + +例 34。定义 HAL-表单模板标题 + +``` +_templates.default.title=Some title (1) +_templates.putEmployee.title=Create employee (2) +Employee._templates.default.title=Create employee (3) +com.acme.Employee._templates.default.title=Create employee (4) +``` + +|**1**|以`default`为键的标题的全局定义。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|标题的全局定义,使用实际的 Affordance 名称作为键。除非在创建 Affordance 时显式定义,否则默认为`$httpMethod + $simpleInputTypeName`。| +|**3**|将本地定义的标题应用于所有名为`Employee`的类型。| +|**4**|使用完全限定类型名的标题定义。| + +| |与默认的密钥相比,使用实际的 Affaundance 名称的密钥享有优先权。| +|---|-------------------------------------------------------------------------------| + +##### 属性提示 + +还可以通过由 Spring Hateoas 自动配置的`rest-messages`资源包来解析属性提示。这些键可以全局定义、局部定义或完全限定,并且需要将`._prompt`连接到实际的属性键: + +例 35。为`email`属性定义提示 + +``` +firstName._prompt=Firstname (1) +Employee.firstName._prompt=Firstname (2) +com.acme.Employee.firstName._prompt=Firstname (3) +``` + +|**1**|所有名为`firstName`的属性都将呈现“firstname”,这与它们所声明的类型无关。| +|-----|------------------------------------------------------------------------------------------------------------| +|**2**|在名为`Employee`的类型中,`firstName`属性将被提示为“firstname”。| +|**3**|`com.acme.Employee`的`firstName`属性将得到分配的“firstname”的提示。| + +同时定义了模板标题和属性提示的样例文档将如下所示: + +例 36。带有国际化模板标题和属性提示的示例 HAL 表单文档 + +``` +{ + …, + "_templates" : { + "default" : { + "title" : "Create employee", + "method" : "put", + "contentType" : "", + "properties" : [ { + "name" : "firstName", + "prompt" : "Firstname", + "required" : true + }, { + "name" : "lastName", + "prompt" : "Lastname", + "required" : true + }, { + "name" : "role", + "prompt" : "Role", + "required" : true + } ] + } + } +} +``` + +### [](#mediatypes.http-problem)4.3。HTTP 问题详细信息 + +[HTTP API 的问题细节](https://tools.ietf.org/html/rfc7807)是一种媒体类型,用于在 HTTP 响应中包含机器可读的错误详细信息,以避免需要为 HTTP API 定义新的错误响应格式。 + +HTTP Problem Details 定义了一组 JSON 属性,这些属性携带额外的信息来向 HTTP 客户机描述错误详细信息。在[RFC 文档](https://tools.ietf.org/html/rfc7807#section-3.1)的相关部分中找到有关这些属性的更多详细信息。 + +你可以通过在 Spring MVC 控制器中使用`Problem`媒体类型域类型来创建这样的 JSON 响应: + +使用 Spring Hateoas’`Problem`类型报告问题细节 + +``` +@RestController +class PaymentController { + + @PutMapping + ResponseEntity issuePayment(@RequestBody PaymentRequest request) { + + PaymentResult result = payments.issuePayment(request.orderId, request.amount); + + if (result.isSuccess()) { + return ResponseEntity.ok(result); + } + + String title = messages.getMessage("payment.out-of-credit"); + String detail = messages.getMessage("payment.out-of-credit.details", // + new Object[] { result.getBalance(), result.getCost() }); + + Problem problem = Problem.create() (1) + .withType(OUT_OF_CREDIT_URI) // + .withTitle(title) (2) + .withDetail(detail) // + .withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) // + .withProperties(map -> { (3) + map.put("balance", result.getBalance()); + map.put("accounts", Arrays.asList( // + ACCOUNTS.expand(result.getSourceAccountId()), // + ACCOUNTS.expand(result.getTargetAccountId()) // + )); + }); + + return ResponseEntity.status(HttpStatus.FORBIDDEN) // + .body(problem); + } +} +``` + +|**1**|首先使用公开的工厂方法创建`Problem`的实例。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|你可以使用 Spring 的国际化特性为媒体类型定义的默认属性定义值,例如 URI 类型、标题和详细信息(见上文)。| +|**3**|可以通过`Map`或显式对象添加自定义属性(见下文)。| + +要为自定义属性使用专用对象,请声明一个类型,创建并填充它的一个实例,然后通过`….withProperties(…)`或通过`Problem.create(…)`在实例创建时将其传递到`Problem`实例中。 + +使用专用类型捕获扩展的问题属性 + +``` +class AccountDetails { + int balance; + List accounts; +} + +problem.withProperties(result.getDetails()); + +// or + +Problem.create(result.getDetails()); +``` + +这将导致这样的反应: + +一个示例 HTTP 问题详细信息响应 + +``` +{ + "type": "https://example.com/probs/out-of-credit", + "title": "You do not have enough credit.", + "detail": "Your current balance is 30, but that costs 50.", + "instance": "/account/12345/msgs/abc", + "balance": 30, + "accounts": ["/account/12345", + "/account/67890"] +} +``` + +### [](#mediatypes.collection-json)4.4。Collection+JSON + +[Collection+JSON](http://amundsen.com/media-types/collection/format/)是在 IANA 批准的媒体类型`application/vnd.collection+json`中注册的 JSON 规范。 + +> > > > +> [Collection+JSON](http://amundsen.com/media-types/collection/)是一种基于 JSON 的读/写超媒体类型,旨在支持 +> 简单集合的管理和查询。 +> > > > + +——Mike Amundsen + +> 集合 +JSON 规范 + +Collection+JSON 提供了一种统一的方式来表示单个项目资源以及集合。要启用此媒体类型,请在代码中加入以下配置: + +例 37。支持 Collection+JSON 的应用程序 + +``` +@Configuration +@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON) +public class CollectionJsonApplication { + +} +``` + +此配置将使你的应用程序响应具有`Accept`头`application/vnd.collection+json`的请求,如下所示。 + +下面的规范示例显示了一个单独的项目: + +例 38。集合 +JSON 单项示例 + +``` +{ + "collection": { + "version": "1.0", + "href": "https://example.org/friends/", (1) + "links": [ (2) + { + "rel": "feed", + "href": "https://example.org/friends/rss" + }, + { + "rel": "queries", + "href": "https://example.org/friends/?queries" + }, + { + "rel": "template", + "href": "https://example.org/friends/?template" + } + ], + "items": [ (3) + { + "href": "https://example.org/friends/jdoe", + "data": [ (4) + { + "name": "fullname", + "value": "J. Doe", + "prompt": "Full Name" + }, + { + "name": "email", + "value": "[email protected]", + "prompt": "Email" + } + ], + "links": [ (5) + { + "rel": "blog", + "href": "https://examples.org/blogs/jdoe", + "prompt": "Blog" + }, + { + "rel": "avatar", + "href": "https://examples.org/images/jdoe", + "prompt": "Avatar", + "render": "image" + } + ] + } + ] + } +} +``` + +|**1**|`self`链接存储在文档的`href`属性中。| +|-----|---------------------------------------------------------------------------------------------------------------| +|**2**|文档顶部的`links`部分包含集合级别的链接(减去`self`链接)。| +|**3**|`items`部分包含了一组数据。由于这是一个单项文档,所以它只有一个条目。| +|**4**|`data`部分包含实际内容。它是由财产组成的。| +|**5**|项目的个体`links`。| + +| |先前的碎片是从规格中取出的。当 Spring Hateoas 呈现`EntityModel`时,它将:

* 将`self`链接放入文档的`href`属性中和项级`href`属性。

* 将模型的其余链接放入顶层`links`以及项级`links`中。

* 从`EntityModel`中提取属性并将它们转换为…| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在呈现资源集合时,文档几乎是相同的,只是`items`JSON 数组中会有多个条目,每个条目对应一个条目。 + +Spring Hateoas 更具体地说将: + +* 将整个集合的`self`链接放入顶层`href`属性。 + +* 将`CollectionModel`链接(减去`self`)放入顶层`links`。 + +* 每个条目级别`href`将包含来自`CollectionModel.content`集合的每个条目的相应`self`链接。 + +* 每个条目级别`links`将包含来自`CollectionModel.content`的每个条目的所有其他链接。 + +### [](#mediatypes.uber)4.5。UBER-交换表示的统一基础 + +[UBER](https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia.html)是一种实验性的 JSON 规范 + +> > > > +> UBER 的文档格式是一种最小的读/写超媒体类型,旨在支持简单的状态传输和 ad-hoc +> 基于超媒体的转换。 +> > > > + +——Mike Amundsen + +> UBER 规范 + +UBER 提供了一种统一的方式来表示单个项目资源以及集合。要启用此媒体类型,请在代码中加入以下配置: + +例 39。启用 UBER+JSON 的应用程序 + +``` +@Configuration +@EnableHypermediaSupport(type = HypermediaType.UBER) +public class UberApplication { + +} +``` + +此配置将使你的应用程序使用`Accept`头`application/vnd.amundsen-uber+json`响应请求,如下所示: + +例 40。UBER 样本文件 + +``` +{ + "uber" : { + "version" : "1.0", + "data" : [ { + "rel" : [ "self" ], + "url" : "/employees/1" + }, { + "name" : "employee", + "data" : [ { + "name" : "role", + "value" : "ring bearer" + }, { + "name" : "name", + "value" : "Frodo" + } ] + } ] + } +} +``` + +这种媒体类型和规范本身都还在开发中。如果你在使用它时遇到问题,请随意[开一张票](https://github.com/spring-projects/spring-hateoas/issues)。 + +| |**UBER 媒体类型**与乘车共享公司**UBERTechnologies Inc.。**没有任何关联。| +|---|-----------------------------------------------------------------------------------------------------------| + +### [](#mediatypes.alps)4.6。ALP-应用程序级配置文件语义 + +[ALPS](https://tools.ietf.org/html/draft-amundsen-richardson-foster-alps-01)是一种媒体类型,用于提供有关另一资源的基于配置文件的元数据。 + +> > > > +> 一份阿尔卑斯山的文档可以作为一个配置文件来使用 +> 用应用程序解释文档的应用程序语义- +> 不可知的媒体类型(如 HTML、HAL、Collection+JSON、Siren、 +> 等)。这增加了整个配置文件文档的可重用性。 +> 媒体类型。 +> > > > + +——Mike Amundsen + +> 阿尔卑斯山规范 + +阿尔卑斯山不需要特殊的激活。相反,你“构建”一个`Alps`记录,并从 Spring MVC 或 Spring WebFlux Web 方法返回它,如下所示: + +例 41。创建`Alps`记录 + +``` +@GetMapping(value = "/profile", produces = ALPS_JSON_VALUE) +Alps profile() { + + return Alps.alps() // + .doc(doc() // + .href("https://example.org/samples/full/doc.html") // + .value("value goes here") // + .format(Format.TEXT) // + .build()) // + .descriptor(getExposedProperties(Employee.class).stream() // + .map(property -> Descriptor.builder() // + .id("class field [" + property.getName() + "]") // + .name(property.getName()) // + .type(Type.SEMANTIC) // + .ext(Ext.builder() // + .id("ext [" + property.getName() + "]") // + .href("https://example.org/samples/ext/" + property.getName()) // + .value("value goes here") // + .build()) // + .rt("rt for [" + property.getName() + "]") // + .descriptor(Collections.singletonList(Descriptor.builder().id("embedded").build())) // + .build()) // + .collect(Collectors.toList())) + .build(); +} +``` + +* 这个示例利用`PropertyUtils.getExposedProperties()`来提取有关域对象属性的元数据。 + +这个片段插入了测试数据。它产生了这样的 JSON: + +例 42。阿尔卑斯山 JSON + +``` +{ + "version": "1.0", + "doc": { + "format": "TEXT", + "href": "https://example.org/samples/full/doc.html", + "value": "value goes here" + }, + "descriptor": [ + { + "id": "class field [name]", + "name": "name", + "type": "SEMANTIC", + "descriptor": [ + { + "id": "embedded" + } + ], + "ext": { + "id": "ext [name]", + "href": "https://example.org/samples/ext/name", + "value": "value goes here" + }, + "rt": "rt for [name]" + }, + { + "id": "class field [role]", + "name": "role", + "type": "SEMANTIC", + "descriptor": [ + { + "id": "embedded" + } + ], + "ext": { + "id": "ext [role]", + "href": "https://example.org/samples/ext/role", + "value": "value goes here" + }, + "rt": "rt for [role]" + } + ] +} +``` + +如果你愿意的话,你可以手工编写它们,而不是“自动”地将每个字段链接到域对象的字段。也可以使用 Spring 框架的消息包和`MessageSource`接口。这使你能够将这些值委托给特定于区域的消息包,甚至使元数据国际化。 + +### [](#mediatypes.community)4.7。基于社区的媒体类型 + +由于[创建自己的媒体类型的能力](#mediatypes.custom),有几个社区领导的努力,以建立额外的媒体类型。 + +#### [](#mediatypes.community.json:api)4.7.1。JSON:API + +* [规格](https://jsonapi.org) + +* 媒体类型名称:`application/vnd.api+json` + +* 最新版本 + + * [参考文献](https://toedter.github.io/spring-hateoas-jsonapi/release/reference/index.html) + + * [API 文档](https://toedter.github.io/spring-hateoas-jsonapi/release/api/index.html) + +* 当前快照 + + * [参考文献](https://toedter.github.io/spring-hateoas-jsonapi/snapshot/reference/index.html) + + * [API 文档](https://toedter.github.io/spring-hateoas-jsonapi/snapshot/api/index.html) + +* [Source](https://github.com/toedter/spring-hateoas-jsonapi) + +* 项目负责人:[Kai Toedter](https://github.com/toedter) + +Maven 坐标 + +``` + + com.toedter + spring-hateoas-jsonapi + {see project page for current version} + +``` + +Gradle 坐标 + +``` +implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}' +``` + +如果你想要发布快照,请访问项目页面了解更多详细信息。 + +#### [](#mediatypes.community.siren)4.7.2。警报器 + +* [规格](https://github.com/kevinswiber/siren) + +* 媒体类型名称:`application/vnd.siren+json` + +* [参考文献](https://spring-hateoas-siren.ingogriebsch.de) + +* [javadocs](https://spring-hateoas-siren.ingogriebsch.de/apidocs) + +* [Source](https://github.com/ingogriebsch/spring-hateoas-siren) + +* 项目负责人:[Ingo Griebsch](https://github.com/ingogriebsch) + +Maven 坐标 + +``` + + de.ingogriebsch.hateoas + spring-hateoas-siren + {see project page for current version} + compile + +``` + +Gradle 坐标 + +``` +implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}' +``` + +### [](#mediatypes.custom)4.8。注册自定义媒体类型 + +Spring Hateoas 允许你通过 SPI 集成自定义媒体类型。这种实现的基本要素是: + +1. 某种形式的 Jackson`ObjectMapper`定制。在最简单的情况下,这是一个 Jackson`Module`实现。 + +2. 一个`LinkDiscoverer`实现,使客户端支持能够检测表示中的链接。 + +3. 一小部分基础设施配置,它将允许 Spring Hateoas 找到自定义实现并获取它。 + +#### [](#mediatypes.custom.configuration)4.8.1。自定义媒体类型配置 + +Spring Hateoas 通过扫描应用程序上下文以查找`HypermediaMappingInformation`接口的任何实现来获取自定义的媒体类型实现。每个媒体类型都必须实现这个接口,以便: + +* 应用于[`WebClient`](#client.web-client),[`WebTestClient`](#client.web-test-client),或[`RestTemplate`](#client.rest-template)实例。 + +* 支持从 Spring Web MVC 和 Spring WebFlux 控制器中提供该媒体类型的服务。 + +定义自己的媒体类型可能看起来很简单: + +``` +@Configuration +public class MyMediaTypeConfiguration implements HypermediaMappingInformation { + + @Override + public List getMediaTypes() { + return MediaType.parse("application/vnd-acme-media-type") (1) + } + + @Override + public Module getJacksonModule() { + return new Jackson2MyMediaTypeModule(); (2) + } + + @Bean + MyLinkDiscoverer myLinkDiscoverer() { + return new MyLinkDiscoverer(); (3) + } +} +``` + +|**1**|配置类返回它所支持的媒体类型。这适用于服务器端和客户端场景。| +|-----|-----------------------------------------------------------------------------------------------------------------------| +|**2**|它重写`getJacksonModule()`以提供自定义序列化器来创建特定于媒体类型的表示。| +|**3**|它还声明了用于进一步客户端支持的自定义`LinkDiscoverer`实现。| + +Jackson 模块通常声明`Serializer`和`Deserializer`实现用于表示模型类型`RepresentationModel`、`EntityModel`、`CollectionModel`和`PagedModel`。如果需要对 Jackson`ObjectMapper`进行进一步的自定义(如自定义`HandlerInstantiator`),则可以替代地覆盖`configureObjectMapper(…)`。 + +| |以前的参考文档版本已经提到实现`MediaTypeConfigurationProvider`接口并将其注册为`spring.factories`。
这是不必要的。
此 SPI 仅用于 Spring Hateoas 提供的开箱即用媒体类型。
仅实现`HypermediaMappingInformation`接口将它注册为 Spring Bean 是所有需要的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#mediatypes.custom.recommendation)4.8.2。建议 + +实现媒体类型表示的首选方法是提供与预期格式匹配的类型层次结构,并且可以按原样由 Jackson 序列化。在`Serializer`和`Deserializer`为`RepresentationModel`注册的实现中,将实例转换为特定于媒体类型的模型类型,然后为这些类型查找 Jackson 序列化器。 + +默认情况下支持的媒体类型使用与第三方实现相同的配置机制。因此,值得研究[the`mediatype`包](https://github.com/ Spring-projects/ Spring-hateoas/tree/master/SRC/main/java/org/springframework/hateoas/mediatype)中的实现。请注意,内置的媒体类型实现保持其配置类包的私有,因为它们是通过`@EnableHypermediaSupport`激活的。定制实现可能应该将这些公开,以确保用户可以从他们的应用程序包中导入这些配置类。 + +## [](#configuration)5。配置 + +本节描述如何配置 Spring Hateoas。 + +### [](#configuration.at-enable)5.1。使用`@EnableHypermediaSupport` + +要让`RepresentationModel`子类型根据各种超媒体表示类型的规范来呈现,可以通过`@EnableHypermediaSupport`激活对特定超媒体表示格式的支持。注释以`HypermediaType`枚举作为参数。目前,我们支持[HAL](https://tools.ietf.org/html/draft-kelly-json-hal)以及默认呈现。使用注释将触发以下操作: + +* 它注册了必要的 Jackson 模块,以超媒体特定的格式呈现`EntityModel`和`CollectionModel`。 + +* 如果 JsonPath 位于 Classpath 上,它会自动注册一个`LinkDiscoverer`实例,以便通过它们的`rel`在普通的 JSON 表示中查找链接(参见[使用`LinkDiscoverer`实例](#client.link-discoverer))。 + +* 默认情况下,它启用[实体链接](#fundamentals.obtaining-links.entity-links)并自动获取`EntityLinks`实现,并将它们捆绑到`DelegatingEntityLinks`实例中,你可以自动连接这些实例。 + +* 它会自动拾取`RelProvider`中的所有`ApplicationContext`实现,并将它们捆绑到可以自动连接的`DelegatingRelProvider`中。它在 Spring MVC 控制器以及域类型上注册要考虑`@Relation`的提供者。如果[EVO 折弯机](https://github.com/atteo/evo-inflector)在 Classpath 上,则集合`rel`值是通过使用在库中实现的多元化算法派生的(参见[[[spis.rel-provider]])。 + +#### [](#configuration.at-enable.stacks)5.1.1。显式地启用对专用 Web 堆栈的支持 + +默认情况下,`@EnableHypermediaSupport`将反射地检测你正在使用的 Web 应用程序堆栈,并将其钩入为这些组件注册的 Spring 组件,以支持超媒体表示。然而,在某些情况下,你只需要明确地激活对特定堆栈的支持。例如,如果你的 Spring 基于 WebMVC 的应用程序使用 WebFlux’`WebClient`发出请求,而其中一个不应该与超媒体元素一起工作,那么你可以通过在配置中显式声明 WebMVC 来限制所启用的功能: + +例 43。显式地激活对特定 Web 堆栈的超媒体支持 + +``` +@EnableHypermediaSupport(…, stacks = WebStack.WEBMVC) +class MyHypermediaConfiguration { … } +``` + +## [](#client)6。客户端支持 + +本节描述 Spring Hateoas 对客户的支持。 + +### [](#client.traverson)6.1。特拉弗森 + +Spring Hateoas 为客户端服务遍历提供了一个 API。它的灵感来自[Traverson JavaScript 函式库](https://blog.codecentric.de/en/2013/11/traverson/)。下面的示例展示了如何使用它: + +``` +Map parameters = new HashMap<>(); +parameters.put("user", 27); + +Traverson traverson = new Traverson(URI.create("http://localhost:8080/api/"), MediaTypes.HAL_JSON); +String name = traverson + .follow("movies", "movie", "actor").withTemplateParameters(parameters) + .toObject("$.name"); +``` + +通过将`Traverson`实例指向 REST 服务器并将要设置为`Accept`头的媒体类型进行配置,可以设置`Traverson`实例。然后,你可以定义你想要发现和遵循的关系名称。关系名称可以是简单的名称,也可以是 JSONPath 表达式(以`$`开头)。 + +然后,示例将一个参数映射传递到`Traverson`实例中。这些参数用于扩展在遍历期间发现的 URI(这些 URI 是模板化的)。通过访问最终遍历的表示,得出了遍历的结论。在前面的示例中,我们对一个 JSONPath 表达式进行求值,以访问参与者的名称。 + +前面的示例是最简单的遍历版本,其中`rel`值是字符串,并且在每一跳应用相同的模板参数。 + +在每个级别上都有更多的自定义模板参数的选项。下面的示例展示了这些选项。 + +``` +ParameterizedTypeReference> resourceParameterizedTypeReference = new ParameterizedTypeReference>() {}; + +EntityModel itemResource = traverson.// + follow(rel("items").withParameter("projection", "noImages")).// + follow("$._embedded.items[0]._links.self.href").// + toObject(resourceParameterizedTypeReference); +``` + +静态`rel(…​)`函数是定义单个`Hop`的方便方法。使用`.withParameter(key, value)`可以简单地指定 URI 模板变量。 + +| |`.withParameter()`返回一个新的可链接的`Hop`对象。你可以将任意多的`.withParameter`串在一起。结果是一个单独的`Hop`定义。
下面的示例展示了这样做的一种方法:| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +ParameterizedTypeReference> resourceParameterizedTypeReference = new ParameterizedTypeReference>() {}; + +Map params = Collections.singletonMap("projection", "noImages"); + +EntityModel itemResource = traverson.// + follow(rel("items").withParameters(params)).// + follow("$._embedded.items[0]._links.self.href").// + toObject(resourceParameterizedTypeReference); +``` + +还可以使用`.withParameters(Map)`加载整个`Map`参数。 + +| |`follow()`是可链接的,这意味着你可以将多个跳串在一起,如前面的示例所示。你可以放置多个基于字符串的`rel`值(`follow("items", "item")`),也可以放置一个具有特定参数的单跳。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 6.1.1.`EntityModel`vs.`CollectionModel` + +到目前为止展示的示例演示了如何避开 Java 的类型擦除,并将单个 JSON 格式的资源转换为`EntityModel`对象。但是,如果你得到了一个类似`\_embedded`HAL 集合的集合,该怎么办?你只需做一个小的调整就可以做到这一点,如下例所示: + +``` +CollectionModelType collectionModelType = + TypeReferences.CollectionModelType() {}; + +CollectionModel itemResource = traverson.// + follow(rel("items")).// + toObject(collectionModelType); +``` + +它不是获取单个资源,而是将集合反序列化为`CollectionModel`。 + +### [](#client.link-discoverer)6.2。使用`LinkDiscoverer`实例 + +在使用启用超媒体的表示时,一个常见的任务是在其中找到具有特定关系类型的链接。 Spring Hateoas 提供了基于的接口的实现方式,用于呈现或开箱即用的默认表示或 HAL。当使用`@EnableHypermediaSupport`时,我们会自动将支持配置的超媒体类型的实例公开为 Spring Bean。 + +或者,你可以按照以下方式设置和使用一个实例: + +``` +String content = "{'_links' : { 'foo' : { 'href' : '/foo/bar' }}}"; +LinkDiscoverer discoverer = new HalLinkDiscoverer(); +Link link = discoverer.findLinkWithRel("foo", content); + +assertThat(link.getRel(), is("foo")); +assertThat(link.getHref(), is("/foo/bar")); +``` + +### [](#client.web-client)6.3。配置 WebClient 实例 + +如果你需要配置一个`WebClient`来说超媒体,这很容易。获取`HypermediaWebClientConfigurer`,如下所示: + +例 44。自己配置`WebClient` + +``` +@Bean +WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1) + return configurer.registerHypermediaTypes(WebClient.builder()); (2) +} +``` + +|**1**|在你的`@Configuration`类中,获取`HypermediaWebClientConfigurer` Bean Spring 仇恨寄存器的副本。| +|-----|--------------------------------------------------------------------------------------------------------------------| +|**2**|创建`WebClient.Builder`后,使用配置器注册超媒体类型。| + +| |什么`HypermediaWebClientConfigurer`它用`WebClient.Builder`注册所有正确的编码器和解码器。要使用它,
你需要将构建器插入到应用程序的某个地方,并运行`build()`方法来生成`WebClient`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你正在使用 Spring boot,还有另一种方法:`WebClientCustomizer`。 + +例 45。让 Spring 引导配置事物 + +``` +@Bean (4) +WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1) + return webClientBuilder -> { (2) + configurer.registerHypermediaTypes(webClientBuilder); (3) + }; +} +``` + +|**1**|在创建 Spring Bean 时,请求 Spring Hateoas 的 Bean 的副本。| +|-----|----------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用 Java8lambda 表达式来定义`WebClientCustomizer`。| +|**3**|在函数调用内部,应用`registerHypermediaTypes`方法。| +|**4**|将整个事情作为 Spring Bean 返回,以便 Spring 引导可以捡起它并将其应用到其自动配置的`WebClient.Builder` Bean。| + +在此阶段,每当你需要一个具体的`WebClient`时,只需将`WebClient.Builder`注入到你的代码中,并使用`build()`。`WebClient`实例将能够使用超媒体进行交互。 + +### [](#client.web-test-client)6.4。配置`WebTestClient`实例 + +在使用启用超媒体的表示时,一个常见的任务是使用`WebTestClient`运行各种测试。 + +要在测试用例中配置`WebTestClient`的实例,请查看以下示例: + +例 46。在使用 Spring hateoas 时配置`WebTestClient` + +``` +@Test // #1225 +void webTestClientShouldSupportHypermediaDeserialization() { + + // Configure an application context programmatically. + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(HalConfig.class); (1) + context.refresh(); + + // Create an instance of a controller for testing + WebFluxEmployeeController controller = context.getBean(WebFluxEmployeeController.class); + controller.reset(); + + // Extract the WebTestClientConfigurer from the app context. + HypermediaWebTestClientConfigurer configurer = context.getBean(HypermediaWebTestClientConfigurer.class); + + // Create a WebTestClient by binding to the controller and applying the hypermedia configurer. + WebTestClient client = WebTestClient.bindToApplicationContext(context).build().mutateWith(configurer); (2) + + // Exercise the controller. + client.get().uri("http://localhost/employees").accept(HAL_JSON) // + .exchange() // + .expectStatus().isOk() // + .expectBody(new TypeReferences.CollectionModelType>() {}) (3) + .consumeWith(result -> { + CollectionModel> model = result.getResponseBody(); (4) + + // Assert against the hypermedia model. + assertThat(model.getRequiredLink(IanaLinkRelations.SELF)).isEqualTo(Link.of("http://localhost/employees")); + assertThat(model.getContent()).hasSize(2); + }); +} +``` + +|**1**|注册使用`@EnableHypermediaSupport`来启用 HAL 支持的配置类。| +|-----|----------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用`HypermediaWebTestClientConfigurer`应用超媒体支持。| +|**3**|使用 Spring Hateoas 的`TypeReferences.CollectionModelType`助手请求`CollectionModel>`的响应。| +|**4**|在得到 Spring Hateoas 格式的“身体”后,断言它!| + +| |`WebTestClient`是一种不可变值类型,因此你无法在适当的位置更改它。`HypermediaWebClientConfigurer`返回一个变异的
变量,然后必须捕获该变量才能使用它。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你正在使用 Spring 引导,还有其他选项,例如: + +例 47。在使用 Spring 引导时配置`WebTestClient` + +``` +@SpringBootTest +@AutoConfigureWebTestClient (1) +class WebClientBasedTests { + + @Test + void exampleTest(@Autowired WebTestClient.Builder builder, @Autowired HypermediaWebTestClientConfigurer configurer) { (2) + client = builder.apply(configurer).build(); (3) + + client.get().uri("/") // + .exchange() // + .expectBody(new TypeReferences.EntityModelType() {}) (4) + .consumeWith(result -> { + // assert against this EntityModel! + }); + } +} +``` + +|**1**|这是 Spring boot 的测试注释,它将为这个测试类配置`WebTestClient.Builder`。| +|-----|-------------------------------------------------------------------------------------------------------------------| +|**2**|将 AutoWire Spring boot 的`WebTestClient.Builder`转换为`builder`和 Spring Hateoas 的配置器作为方法参数。| +|**3**|使用`HypermediaWebTestClientConfigurer`注册对超媒体的支持。| +|**4**|使用`TypeReferences`返回要`EntityModel`的信号。| + +同样,你可以使用与前面的示例类似的断言。 + +还有许多其他方法来设计测试用例。`WebTestClient`可以绑定到控制器、函数和 URL。这一节并不是要展示这一切。相反,这为你提供了一些可以开始使用的示例。重要的是,通过应用`HypermediaWebTestClientConfigurer`,可以修改`WebTestClient`的任何实例来处理超媒体。 + +### [](#client.rest-template)6.5。配置 RESTTemplate 实例 + +如果你想创建自己的`RestTemplate`副本,并将其配置为说超媒体语言,则可以使用`HypermediaRestTemplateConfigurer`: + +例 48。配置`RestTemplate`自己 + +``` +/** + * Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}. + */ +@Bean +RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { (1) + return configurer.registerHypermediaTypes(new RestTemplate()); (2) +} +``` + +|**1**|在你的`@Configuration`类中,获取一份`HypermediaRestTemplateConfigurer` Bean Spring 仇恨寄存器的副本。| +|-----|-----------------------------------------------------------------------------------------------------------------------| +|**2**|在创建`RestTemplate`之后,使用配置器应用超媒体类型。| + +你可以自由地将此模式应用于所需的`RestTemplate`的任何实例,无论是创建已注册的 Bean,还是在你定义的服务中。 + +如果你正在使用 Spring boot,还有另一种方法。 + +通常, Spring 引导已经偏离了在应用程序上下文中注册`RestTemplate` Bean 的概念。 + +* 当与不同的服务交谈时,你通常需要不同的凭据。 + +* 当`RestTemplate`使用底层连接池时,会遇到其他问题。 + +* 用户通常需要不同的实例,而不是单个实例 Bean。 + +为了对此进行补偿, Spring Boot 提供了`RestTemplateBuilder`。这个自动配置的 Bean 允许你定义用于生成`RestTemplate`实例的各种 bean。你请求一个`RestTemplateBuilder` Bean,调用它的`build()`方法,然后应用最终设置(例如凭据和其他详细信息)。 + +要注册基于超媒体的消息转换器,请在代码中添加以下内容: + +例 49。让 Spring 引导配置事物 + +``` +@Bean (4) +RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1) + return restTemplate -> { (2) + configurer.registerHypermediaTypes(restTemplate); (3) + }; +} +``` + +|**1**|在创建 Spring Bean 时,请求 Spring Hateoas 的`HypermediaRestTemplateConfigurer` Bean 的副本。| +|-----|-------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用 Java8lambda 表达式来定义`RestTemplateCustomizer`。| +|**3**|在函数调用内部,应用`registerHypermediaTypes`方法。| +|**4**|将整个事情作为 Spring Bean 返回,以便 Spring 引导可以将其拾起并将其应用到其自动配置的`RestTemplateBuilder`。| + +在此阶段,每当你需要一个具体的`RestTemplate`时,只需将`RestTemplateBuilder`注入到你的代码中,并使用`build()`。`RestTemplate`实例将能够使用超媒体进行交互。 \ No newline at end of file diff --git a/docs/spring-rest-docs/spring-restdocs.md b/docs/spring-rest-docs/spring-restdocs.md new file mode 100644 index 0000000..e4243a2 --- /dev/null +++ b/docs/spring-rest-docs/spring-restdocs.md @@ -0,0 +1,2443 @@ +# Spring 休息 DOCS + +将手写文档与通过 Spring MVC 测试、WebTestClient 或 Rest Assured 生成的自动生成的片段结合在一起来实现文档服务。 + +## [导言](#introduction) + +Spring REST DOCS 的目的是帮助你为 RESTful 服务生成准确且可读的文档。 + +编写高质量的文档是困难的。减轻这一困难的一种方法是使用非常适合这项工作的工具。为此, Spring REST DOCS 默认使用[ASCIIDoctor](https://asciidoctor.org)。ASCIIDoctor 处理纯文本并生成 HTML,并根据你的需要进行样式和布局。如果你愿意,还可以将 Spring REST DOCS 配置为使用 Markdown。 + +Spring REST DOCS 使用由用 Spring MVC 的、 Spring WebFlux 的[](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.0.x/ Spring-Framework-Reference/Testing.html#WebTestClient)或编写的测试产生的代码片段。这种测试驱动的方法有助于保证服务文档的准确性。如果代码片段不正确,则生成它的测试失败。 + +记录一个 RESTful 服务主要是描述它的资源。每个资源描述的两个关键部分是它使用的 HTTP 请求的详细信息和它产生的 HTTP 响应。 Spring REST DOCS 允许你使用这些资源以及 HTTP 请求和响应,从而保护你的文档不受服务实现的内部细节的影响。这种分离可以帮助你记录服务的 API,而不是它的实现。它还可以帮助你改进实现,而无需重新编写文档。 + +## [开始](#getting-started) + +本节描述了如何开始使用 Spring REST DOCS。 + +### [示例应用程序](#getting-started-sample-applications) + +如果你想直接进入,可以使用一些示例应用程序: + +| 样本 |Build system|说明| +|------------------------------------------------------------------------------------------------------------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|[Spring Data REST](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-notes-spring-data-rest)| Maven |演示如何为使用[Spring Data REST](https://projects.spring.io/spring-data-rest/)实现的服务创建入门指南和 API 指南。| +| [Spring HATEOAS](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-notes-spring-hateoas) | Gradle |演示如何为使用[Spring HATEOAS](https://projects.spring.io/spring-hateoas/)实现的服务创建入门指南和 API 指南。| + +| 样本 |Build system| Description | +|---------------------------------------------------------------------------------------------------------------|------------|-----------------------------------------------------------------------------| +|[WebTestClient](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/web-test-client)| Gradle |Demonstrates the use of Spring REST docs with Spring WebFlux’s WebTestClient.| + +| 样本 |Build system| 说明 | +|-----------------------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------| +|[放心吧](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-assured)| Gradle |Demonstrates the use of Spring REST Docs with [放心吧](http://rest-assured.io).| + +| Sample |Build system| Description | +|--------------------------------------------------------------------------------------------------------|------------|---------------------------------------------------------------------------------------------------| +|[Slate](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-notes-slate)| Gradle |Demonstrates the use of Spring REST Docs with Markdown and[Slate](https://github.com/tripit/slate).| +|[TestNG](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/testng)| Gradle | Demonstrates the use of Spring REST Docs with [TestNG](http://testng.org). | +|[JUnit 5](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/junit5)| Gradle | Demonstrates the use of Spring REST Docs with [JUnit 5](https://junit.org/junit5/). | + +### [所需经费](#getting-started-requirements) + +Spring REST DOCS 具有以下最低要求: + +* Java8 + +* Spring 框架 5(5.0.2 或更高版本) + +此外,`spring-restdocs-restassured`模块需要 REST SUARD3.0。 + +### [构建配置](#getting-started-build-configuration) + +使用 Spring REST DOCS 的第一步是配置项目的构建。[Spring HATEOAS](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-notes-spring-hateoas)和[Spring Data REST](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-notes-spring-data-rest)样本分别包含一个`build.gradle`和`pom.xml`,你可能希望将其用作参考。配置的关键部分在以下清单中进行了描述: + +Maven + +``` + (1) + org.springframework.restdocs + spring-restdocs-mockmvc + {project-version} + test + + + + + (2) + org.asciidoctor + asciidoctor-maven-plugin + 1.5.8 + + + generate-docs + prepare-package (3) + + process-asciidoc + + + html + book + + + + + (4) + org.springframework.restdocs + spring-restdocs-asciidoctor + {project-version} + + + + + +``` + +|**1**|在`test`范围中添加对`spring-restdocs-mockmvc`的依赖关系。
如果你想使用`WebTestClient`或 Rest Assured 而不是 MockMVC,请分别在`spring-restdocs-webtestclient`或`spring-restdocs-restassured`上添加依赖关系。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|添加 ASCIIDoctor 插件。| +|**3**|使用`prepare-package`允许文档[包含在包中](#getting-started-build-configuration-packaging-the-documentation)。| +|**4**|添加`spring-restdocs-asciidoctor`作为 ASCIIDoctor 插件的依赖项。
这将自动配置在`snippets`文件中使用的`snippets`属性,以指向`target/generated-snippets`。
它还将允许你使用`operation`块宏。| + +Gradle + +``` +plugins { (1) + id "org.asciidoctor.jvm.convert" version "3.3.2" +} + +configurations { + asciidoctorExt (2) +} + +dependencies { + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' (3) + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' (4) +} + +ext { (5) + snippetsDir = file('build/generated-snippets') +} + +test { (6) + outputs.dir snippetsDir +} + +asciidoctor { (7) + inputs.dir snippetsDir (8) + configurations 'asciidoctorExt' (9) + dependsOn test (10) +} +``` + +|**1** |应用 ASCIIDoctor 插件。| +|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2** |为扩展 ASCIIDoctor 的依赖项声明`asciidoctorExt`配置。| +|**3** |在`asciidoctorExt`配置中添加对`spring-restdocs-asciidoctor`的依赖关系。
这将自动配置在你的`.adoc`文件中使用的`snippets`属性,以指向`build/generated-snippets`。
它还将允许你使用`operation`块宏。| +|**4** |在`testImplementation`配置中添加对`spring-restdocs-mockmvc`的依赖关系。
如果你想使用`WebTestClient`或 Rest Assured 而不是 MockMVC,请分别在`spring-restdocs-webtestclient`或`spring-restdocs-restassured`上添加依赖关系。| +|**5** |配置一个属性来定义生成的片段的输出位置。| +|**6** |配置`test`任务,将 snippets 目录添加为输出。| +|**7** |配置`asciidoctor`任务。| +|**8** |将 snippets 目录配置为输入。| +|**9** |为扩展配置`asciidoctorExt`配置的使用。| +|**10**|使任务依赖于测试任务,以便在创建文档之前运行测试。| + +#### [打包文档](#getting-started-build-configuration-packaging-the-documentation) + +你可能希望将生成的文档打包到项目的 JAR 文件中——例如,在 Spring 启动时将其[用作静态内容](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-static-content)。要做到这一点,请配置项目的构建,以便: + +1. 文档是在构建 JAR 之前生成的。 + +2. 生成的文档包含在 JAR 中 + +下面的列表显示了如何在 Maven 和 Gradle 中实现这一点: + +Maven + +``` + (1) + org.asciidoctor + asciidoctor-maven-plugin + + + (2) + maven-resources-plugin + 2.7 + + + copy-resources + prepare-package + + copy-resources + + (3) + + ${project.build.outputDirectory}/static/docs + + + + + ${project.build.directory}/generated-docs + + + + + + + +``` + +|**1**|ASCIIDoctor 插件的现有声明。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|资源插件必须在 ASCIIDoctor 插件之后声明,因为它们绑定到相同的阶段(`prepare-package`),并且资源插件必须在 ASCIIDoctor 插件之后运行,以确保在复制文档之前生成文档。| +|**3**|将生成的文档复制到构建输出的`static/docs`目录中,然后将其包含在 JAR 文件中。| + +Gradle + +``` +bootJar { + dependsOn asciidoctor (1) + from ("${asciidoctor.outputDir}/html5") { (2) + into 'static/docs' + } +} +``` + +|**1**|确保在构建 JAR 之前已经生成了文档。| +|-----|-------------------------------------------------------------------------| +|**2**|将生成的文档复制到 JAR 的`static/docs`目录中。| + +### [生成文档片段](#getting-started-documentation-snippets) + +Spring REST DOCS 使用 Spring MVC 的, Spring WebFlux 的[](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.0.x/ Spring-Framework-Reference/Testing.html#WebTestClient),或向你正在记录的服务发出请求。然后,它为请求和结果响应生成文档片段。 + +#### [设置你的测试](#getting-started-documentation-snippets-setup) + +你如何设置测试取决于你所使用的测试框架。 Spring REST DOCS 为 JUnit4 和 JUnit5 提供一流的支持。也支持其他框架,例如 TestNG,尽管需要稍多的设置。 + +##### [设置你的 JUnit4 测试](#getting-started-documentation-snippets-setup-junit) + +当使用 JUnit4 时,生成文档片段的第一步是声明一个`public``JUnitRestDocumentation`字段,该字段被注释为 JUnit`@Rule`。下面的示例展示了如何做到这一点: + +``` +@Rule +public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +``` + +默认情况下,`JUnitRestDocumentation`规则会根据项目的构建工具自动配置一个输出目录: + +|Build tool|输出目录| +|----------|---------------------------| +| Maven |`target/generated-snippets`| +| Gradle |`build/generated-snippets`| + +你可以通过在创建`JUnitRestDocumentation`实例时提供一个输出目录来覆盖默认值。下面的示例展示了如何做到这一点: + +``` +@Rule +public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom"); +``` + +接下来,你必须提供一个`@Before`方法来配置 MockMVC、WebTestClient 或 Rest Assured。下面的例子说明了如何做到这一点: + +MockMVC + +``` +private MockMvc mockMvc; + +@Autowired +private WebApplicationContext context; + +@Before +public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) (1) + .build(); +} +``` + +|**1**|通过使用`MockMVCRestDocumentationConfigurer`配置`MockMVC`实例。
可以从`documentationConfiguration()`上的静态`documentationConfiguration()`方法获得该类的实例。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +private WebTestClient webTestClient; + +@Autowired +private ApplicationContext context; + +@Before +public void setUp() { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(this.restDocumentation)) (1) + .build(); +} +``` + +|**1**|将`WebTestClient`实例配置为将`WebTestclientRestDocumentationConfigurer`添加为`ExchangeFilterFunction`。
可以从`org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`上的静态`documentationConfiguration()`方法获得该类的实例。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +放心吧 + +``` +private RequestSpecification spec; + +@Before +public void setUp() { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) (1) + .build(); +} +``` + +|**1**|通过将`RestAssuredRestDocumentationConfigurer`添加为`Filter`来配置 Rest Assured。
你可以在`RestAssuredRestDocumentation`包中从`RestAssuredRestDocumentation`上的静态`documentationConfiguration()`方法获得该类的实例。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +配置器应用合理的默认值,还提供了用于定制配置的 API。有关更多信息,请参见[配置部分](#configuration)。 + +##### [设置你的 JUnit5 测试](#getting-started-documentation-snippets-setup-junit-5) + +当使用 JUnit5 时,生成文档片段的第一步是将`RestDocumentationExtension`应用到测试类。下面的示例展示了如何做到这一点: + +``` +@ExtendWith(RestDocumentationExtension.class) +public class JUnit5ExampleTests { +``` + +在测试典型的 Spring 应用程序时,还应该应用`SpringExtension`: + +``` +@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) +public class JUnit5ExampleTests { +``` + +根据项目的构建工具,`RestDocumentationExtension`会自动配置一个输出目录: + +|Build tool|输出目录| +|----------|---------------------------| +| Maven |`target/generated-snippets`| +| Gradle |`build/generated-snippets`| + +如果你使用的是 JUnit5.1,那么你可以通过在你的测试类中将扩展注册为字段并在创建它时提供一个输出目录来覆盖默认的扩展。下面的示例展示了如何做到这一点: + +``` +public class JUnit5ExampleTests { + + @RegisterExtension + final RestDocumentationExtension restDocumentation = new RestDocumentationExtension ("custom"); + +} +``` + +接下来,你必须提供一个`@BeforeEach`方法来配置 MockMVC、WebTestClient 或 放心吧。下面的列表展示了如何做到这一点: + +MockMVC + +``` +private MockMvc mockMvc; + +@BeforeEach +public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) (1) + .build(); +} +``` + +|**1**|`MockMVC`实例是通过使用`MockMVCRestDocumentationConfigurer`来配置的。
你可以在`org.springframework.restdocs.mockmvc.MockMVCRestDocumentation`上从静态`documentationConfiguration()`方法获得该类的实例。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +private WebTestClient webTestClient; + +@BeforeEach +public void setUp(ApplicationContext applicationContext, RestDocumentationContextProvider restDocumentation) { + this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext).configureClient() + .filter(documentationConfiguration(restDocumentation)) (1) + .build(); +} +``` + +|**1**|将`WebTestClient`实例配置为将`WebTestClientRestDocumentationConfigurer`添加为`ExchangeFilterFunction`。
你可以从`org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`上的静态`documentationConfiguration()`方法获得该类的实例。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +放心吧 + +``` +private RequestSpecification spec; + +@BeforeEach +public void setUp(RestDocumentationContextProvider restDocumentation) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) (1) + .build(); +} +``` + +|**1**|通过将`RestAssuredRestDocumentationConfigurer`添加为`Filter`来配置 Rest Assured。
你可以在`RestAssuredRestDocumentation`包中从`RestAssuredRestDocumentation`上的静态`documentationConfiguration()`方法获得该类的实例。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +配置器应用合理的默认值,还提供了用于定制配置的 API。有关更多信息,请参见[配置部分](#configuration)。 + +##### [在不使用 JUnit 的情况下设置测试](#getting-started-documentation-snippets-setup-manual) + +不使用 JUnit 时的配置在很大程度上与使用 JUnit 时的配置相似。这一节描述了主要的区别。[测试样本](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/testng)也说明了这种方法。 + +第一个区别是,你应该使用`ManualRestDocumentation`来代替`JUnitRestDocumentation`。另外,你不需要`@Rule`注释。下面的示例展示了如何使用`ManualRestDocumentation`: + +``` +private ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); +``` + +其次,在每次测试之前,你必须调用`ManualRestDocumentation.beforeTest(Class, String)`。你可以作为配置 MockMVC、WebTestClient 或 REST ASSURED 的方法的一部分来执行此操作。下面的例子说明了如何做到这一点: + +MockMVC + +``` +private MockMvc mockMvc; + +@Autowired +private WebApplicationContext context; + +@BeforeMethod +public void setUp(Method method) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); +} +``` + +WebTestClient + +``` +private WebTestClient webTestClient; + +@Autowired +private ApplicationContext context; + +@BeforeMethod +public void setUp(Method method) { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(this.restDocumentation)) (1) + .build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); +} +``` + +放心吧 + +``` +private RequestSpecification spec; + +@BeforeMethod +public void setUp(Method method) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)).build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); +} +``` + +最后,你必须在每次测试之后调用`ManualRestDocumentation.afterTest`。下面的示例展示了如何使用 TestNG 来实现这一点: + +``` +@AfterMethod +public void tearDown() { + this.restDocumentation.afterTest(); +} +``` + +#### [Invoking the RESTful Service](#getting-started-documentation-snippets-invoking-the-service) + +现在你已经配置了测试框架,你可以使用它来调用 RESTful 服务并记录请求和响应。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) (1) + .andExpect(status().isOk()) (2) + .andDo(document("index")); (3) +``` + +|**1**|调用服务的根(`/`)并指示需要一个`application/json`响应。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|断言服务产生了预期的响应。| +|**3**|记录对服务的调用,将片段写入名为`index`的目录(位于配置的输出目录下面)。
片段由`RestDocumentationResultHandler`编写。
你可以从`org.springframework.restdocs.mockmvc.MockMVCRestDocumentation`上的静态`document`方法获得该类的实例。| + +WebTestClient + +``` +this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON) (1) + .exchange().expectStatus().isOk() (2) + .expectBody().consumeWith(document("index")); (3) +``` + +|**1**|调用服务的根(`/`)并指示需要一个`application/json`响应。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|断言服务产生了预期的响应。| +|**3**|记录对服务的调用,将片段写入名为`index`的目录(位于配置的输出目录下面)。
片段由`ExchangeResult`的`Consumer`所写。
你可以从`org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`上的静态`document`方法获得这样的使用者。| + +放心吧 + +``` +RestAssured.given(this.spec) (1) + .accept("application/json") (2) + .filter(document("index")) (3) + .when().get("/") (4) + .then().assertThat().statusCode(is(200)); (5) +``` + +|**1**|应用在`@Before`方法中初始化的规范。| +|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|表示需要`application/json`响应。| +|**3**|记录下对该服务的调用,将片段写入名为`index`的目录(位于配置的输出目录下面)。
片段由`RestDocumentationFilter`编写。
你可以在`org.springframework.restdocs.restassured3`包中从`RestAssuredRestDocumentation`上的静态`document`方法获得该类的实例。| +|**4**|调用服务的根(`/`)。| +|**5**|断言服务产生了预期的响应。| + +默认情况下,编写了六个片段: + +* `/index/curl-request.adoc` + +* `/index/http-request.adoc` + +* `/index/http-response.adoc` + +* `/index/httpie-request.adoc` + +* `/index/request-body.adoc` + +* `/index/response-body.adoc` + +参见[记录你的 API](#documenting-your-api),以获取有关这些片段和可由 Spring REST DOCS 产生的其他片段的更多信息。 + +### [使用片段](#getting-started-using-the-snippets) + +在使用生成的代码片段之前,你必须创建一个`.adoc`源文件。只要该文件有`.adoc`后缀,你就可以为该文件命名任何你喜欢的名称。生成的 HTML 文件具有相同的名称,但带有`.html`后缀。源文件和生成的 HTML 文件的默认位置取决于你是使用 Maven 还是 Gradle: + +|Build tool| Source files |生成的文件| +|----------|--------------------------|------------------------------| +| Maven |`src/main/asciidoc/*.adoc`|`target/generated-docs/*.html`| +| Gradle |`src/docs/asciidoc/*.adoc`|`build/asciidoc/html5/*.html`| + +然后,可以使用[包括宏](https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files)将生成的片段包括在手动创建的 ASCIIDoc 文件中(在本节前面描述)。可以使用`snippets`属性,该属性由`spring-restdocs-asciidoctor`中配置的[构建配置](#getting-started-build-configuration)自动设置,以引用片段输出目录。下面的示例展示了如何做到这一点: + +``` +include::{snippets}/index/curl-request.adoc[] +``` + +## [记录你的 API](#documenting-your-api) + +本节提供了有关使用 Spring REST DOCS 来记录 API 的更多详细信息。 + +### [Hypermedia](#documenting-your-api-hypermedia) + +Spring REST DOCS 提供了对[基于超媒体的](https://en.wikipedia.org/wiki/HATEOAS)API 中的链接进行文档化的支持。以下示例展示了如何使用它: + +MockMVC + +``` +this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("index", links((1) + linkWithRel("alpha").description("Link to the alpha resource"), (2) + linkWithRel("bravo").description("Link to the bravo resource")))); (3) +``` + +|**1**|配置 Spring REST DOCS 以生成描述响应链接的片段。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`links`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望一个`rel`是`alpha`的链接。
在`linkWithRel`上使用静态`linkWithRel`方法。| +|**3**|期望一个链接的`rel`是`bravo`。| + +WebTestClient + +``` +this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk() + .expectBody().consumeWith(document("index", links((1) + linkWithRel("alpha").description("Link to the alpha resource"), (2) + linkWithRel("bravo").description("Link to the bravo resource")))); (3) +``` + +|**1**|配置 Spring REST DOCS 以生成描述响应链接的片段。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`links`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望一个其`rel`是`alpha`的链接。
在`linkWithRel`上使用静态`linkWithRel`方法。| +|**3**|期望一个链接的`rel`是`bravo`。| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json").filter(document("index", links((1) + linkWithRel("alpha").description("Link to the alpha resource"), (2) + linkWithRel("bravo").description("Link to the bravo resource")))) (3) + .get("/").then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述响应链接的片段。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`links`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望一个`rel`是`alpha`的链接。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`linkWithRel`方法。| +|**3**|期望一个链接的`rel`是`bravo`。| + +结果是一个名为`links.adoc`的片段,其中包含一个描述资源链接的表。 + +| |如果响应中的链接具有`title`,则可以从其描述符中省略该描述,并使用`title`。
如果省略该描述且该链接没有`title`,则会发生故障。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在记录链接时,如果在响应中发现了未记录的链接,则测试将失败。类似地,如果在响应中未找到文档化的链接,并且该链接未标记为可选的,则测试也会失败。 + +如果不想记录链接,可以将其标记为“忽略”。这样做可以防止它出现在生成的代码片段中,同时避免上述的故障。 + +你还可以在放松模式中记录链接,在这种模式中,任何未记录的链接都不会导致测试失败。要做到这一点,请在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用`relaxedLinks`方法。当你只想关注链接的一个子集时,记录特定的场景时,这可能会很有用。 + +#### [超媒体链接格式](#documenting-your-api-hypermedia-link-formats) + +默认情况下可以理解两种链接格式: + +* Atom:链接应该在一个名为`links`的数组中。当响应的内容类型与`application/json`兼容时,默认情况下使用此选项。 + +* HAL:链接预期在一个名为`_links`的映射中。当响应的内容类型与`application/hal+json`兼容时,默认情况下使用此选项。 + +如果使用 Atom 格式或 HAL 格式的链接但具有不同的内容类型,则可以提供一个内置的`LinkExtractor`实现到`links`。下面的例子说明了如何做到这一点: + +MockMVC + +``` +.andDo(document("index", links(halLinks(), (1) + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); +``` + +|**1**|指示链接为 HAL 格式。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`halLinks`方法。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +.consumeWith(document("index", links(halLinks(), (1) + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); +``` + +|**1**|指示链接为 HAL 格式。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`halLinks`方法。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------| + +放心吧 + +``` +.filter(document("index", links(halLinks(), (1) + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) +``` + +|**1**|指示链接为 HAL 格式。
在`org.springframework.restdocs.hypermedia.HypermediaDocumentation`上使用静态`halLinks`方法。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你的 API 以 Atom 或 HAL 以外的格式表示其链接,则可以提供你自己的`LinkExtractor`接口的实现,以从响应中提取链接。 + +#### [忽略公共链接](#documenting-your-api-hypermedia-ignoring-common-links) + +在使用 HAL 时,你可能希望在概述部分对它们进行一次文档记录,然后在 API 文档的其余部分中忽略它们,而不是对每个响应都通用的链接进行文档记录,例如`self`和`curies`。为此,你可以构建[支持重用代码片段](#documenting-your-api-reusing-snippets),将链接描述符添加到预先配置为忽略某些链接的片段中。下面的示例展示了如何做到这一点: + +``` +public static LinksSnippet links(LinkDescriptor... descriptors) { + return HypermediaDocumentation.links(linkWithRel("self").ignored().optional(), linkWithRel("curies").ignored()) + .and(descriptors); +} +``` + +### [请求和响应有效载荷](#documenting-your-api-request-response-payloads) + +除了特定于超媒体的支持[前面描述的](#documenting-your-api-hypermedia)外,还提供了对请求和响应有效负载的一般文档的支持。 + +默认情况下, Spring REST DOCS 自动生成用于请求的主体和响应的主体的片段。这些片段分别命名为`request-body.adoc`和`response-body.adoc`。 + +#### [请求和响应字段](#documenting-your-api-request-response-payloads-fields) + +为了提供请求或响应有效负载的更详细的文档,提供了对有效负载字段进行文档记录的支持。 + +考虑以下有效载荷: + +``` +{ + "contact": { + "name": "Jane Doe", + "email": "[email protected]" + } +} +``` + +你可以将上一个示例的字段记录如下: + +MockMVC + +``` +this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("index", responseFields((1) + fieldWithPath("contact.email").description("The user's email address"), (2) + fieldWithPath("contact.name").description("The user's name")))); (3) +``` + +|**1**|配置 Spring REST DOCS 以生成描述响应负载中的字段的片段。
要记录请求,可以使用`requestFields`。
都是`org.springframework.restdocs.payload.PayloadDocumentation`上的静态方法。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望具有路径`contact.email`的字段。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`fieldWithPath`方法。| +|**3**|期望具有路径`contact.name`的字段。| + +WebTestClient + +``` +this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("user", + responseFields((1) + fieldWithPath("contact.email").description("The user's email address"), (2) + fieldWithPath("contact.name").description("The user's name")))); (3) +``` + +|**1**|配置 Spring REST DOCS 以生成描述响应负载中的字段的片段。
要记录请求,可以使用`requestFields`。
都是`org.springframework.restdocs.payload.PayloadDocumentation`上的静态方法。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望具有路径`contact.email`的字段。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`fieldWithPath`方法。| +|**3**|期望具有路径`contact.name`的字段。| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json").filter(document("user", responseFields((1) + fieldWithPath("contact.name").description("The user's name"), (2) + fieldWithPath("contact.email").description("The user's email address")))) (3) + .when().get("/user/5").then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述响应有效负载中的字段的片段。
要记录请求,可以使用`requestFields`。
都是`org.springframework.restdocs.payload.PayloadDocumentation`上的静态方法。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望具有路径`contact.email`的字段。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`fieldWithPath`方法。| +|**3**|期望具有路径`contact.name`的字段。| + +结果是一个包含描述字段的表的片段。对于请求,此段名为`request-fields.adoc`。对于响应,此段名为`response-fields.adoc`。 + +在记录字段时,如果在有效负载中发现了未记录的字段,则测试将失败。类似地,如果在有效负载中未找到已记录的字段,并且该字段未标记为可选字段,则测试也会失败。 + +如果你不想提供所有字段的详细文档,那么可以对有效负载的整个小节进行文档记录。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("index", responseFields((1) + subsectionWithPath("contact").description("The user's contact details")))); (1) +``` + +|**1**|用路径`contact`记录该小节。`contact.email`和`contact.name`现在也被视为已被记录。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`subsectionWithPath`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("user", + responseFields( + subsectionWithPath("contact").description("The user's contact details")))); (1) +``` + +|**1**|用路径`contact`记录该小节。`contact.email`和`contact.name`现在也被视为已被记录。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`subsectionWithPath`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json") + .filter(document("user", + responseFields(subsectionWithPath("contact").description("The user's contact details")))) (1) + .when().get("/user/5").then().assertThat().statusCode(is(200)); +``` + +|**1**|用路径`contact`记录该小节。`contact.email`和`contact.name`现在也被视为已被记录。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`subsectionWithPath`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`subsectionWithPath`对于提供有效载荷的特定部分的高级概述很有用。然后,你可以为一个小节生成单独的、更详细的文档。见[记录请求或响应有效负载的一个分节](#documenting-your-api-request-response-payloads-subsections)。 + +如果你根本不想记录某个字段或小节,可以将其标记为“忽略”。这可以防止它出现在生成的代码片段中,同时避免前面描述的故障。 + +你还可以在放松模式下记录字段,在这种模式下,任何未记录的字段都不会导致测试失败。为此,在`org.springframework.restdocs.payload.PayloadDocumentation`上使用`relaxedRequestFields`和`relaxedResponseFields`方法。在记录一个特定的场景时,如果你希望只关注有效负载的一个子集,这可能会很有用。 + +| |默认情况下, Spring REST DOCS 假定你正在记录的负载是 JSON。
如果你想记录 XML 负载,请求或响应的内容类型必须与`application/xml`兼容。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [JSON 有效载荷中的字段](#documenting-your-api-request-response-payloads-fields-json) + +本节介绍如何使用 JSON 有效负载中的字段。 + +###### [JSON 字段路径](#documenting-your-api-request-response-payloads-fields-json-field-paths) + +JSON 字段路径使用点表示法或括号表示法。点记号使用’.’分隔路径中的每个键(例如,`a.b`)。括号表示法将每个键包装在方括号和单引号中(例如,`['a']['b']`)。在这两种情况下,`[]`都用于标识一个数组。点表示法更简洁,但是使用括号表示法可以在键名中使用`.`(例如,`['a.b']`)。这两个不同的符号可以在相同的路径中使用(例如,`a['b']`)。 + +考虑以下 JSON 有效负载: + +``` +{ + "a":{ + "b":[ + { + "c":"one" + }, + { + "c":"two" + }, + { + "d":"three" + } + ], + "e.dot" : "four" + } +} +``` + +在前面的 JSON 有效负载中,以下路径都存在: + +| Path |价值| +|----------------|-----------------------------------------------| +| `a` |包含`b`的对象| +| `a.b` |包含三个对象的数组| +| `['a']['b']` |包含三个对象的数组| +| `a['b']` |包含三个对象的数组| +| `['a'].b` |包含三个对象的数组| +| `a.b[]` |包含三个对象的数组| +| `a.b[].c` |包含字符串`one`和`two`的数组| +| `a.b[].d` |字符串`three`| +| `a['e.dot']` |字符串`four`| +|`['a']['e.dot']`|字符串`four`| + +你还可以记录在根目录下使用数组的有效负载。路径`[]`表示整个数组。然后,你可以使用括号或点表示法来标识数组条目中的字段。例如,`[].id`对应于以下数组中每个对象的`id`字段: + +``` +[ + { + "id":1 + }, + { + "id":2 + } +] +``` + +可以使用`*`作为通配符来匹配具有不同名称的字段。例如,`users.*.role`可用于记录以下 JSON 中每个用户的角色: + +``` +{ + "users":{ + "ab12cd34":{ + "role": "Administrator" + }, + "12ab34cd":{ + "role": "Guest" + } + } +} +``` + +###### [JSON 字段类型](#documenting-your-api-request-response-payloads-fields-json-field-types) + +Spring 在记录字段时,REST DOCS 试图通过检查有效负载来确定其类型。支持七种不同的类型: + +| Type | Description | +|---------|---------------------------------------------------------------------------------| +| `array` |字段的每一次出现的值都是一个数组。| +|`boolean`|字段的每个出现的值都是布尔(`true`或`false`)。| +|`object` |字段的每一次出现的值都是一个对象。| +|`number` |字段的每一次出现的值都是一个数字。| +| `null` |字段的每个出现的值是`null`。| +|`string` |字段的每一次出现的值都是一个字符串。| +|`varies` |该场在有效载荷中多次出现,具有各种不同的类型。| + +还可以使用`FieldDescriptor`上的`type(Object)`方法显式地设置类型。文档中使用了提供的`Object`方法的`toString`的结果。通常,使用`JsonFieldType`枚举的值之一。下面的例子说明了如何做到这一点: + +MockMVC + +``` +.andDo(document("index", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) (1) + .description("The user's email address")))); +``` + +|**1**|将字段的类型设置为`String`。| +|-----|---------------------------------| + +WebTestClient + +``` +.consumeWith(document("user", + responseFields( + fieldWithPath("contact.email") + .type(JsonFieldType.STRING) (1) + .description("The user's email address")))); +``` + +|**1**|将字段的类型设置为`String`。| +|-----|---------------------------------| + +放心吧 + +``` +.filter(document("user", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) (1) + .description("The user's email address")))) +``` + +|**1**|将字段的类型设置为`String`。| +|-----|---------------------------------| + +##### [XML 有效负载](#documenting-your-api-request-response-payloads-fields-xml) + +本节介绍如何使用 XML 有效负载。 + +###### [XML 字段路径](#documenting-your-api-request-response-payloads-fields-xml-field-paths) + +使用 XPath 描述 XML 字段路径。`/`用于下降到子节点。 + +###### [XML 字段类型](#documenting-your-api-request-response-payloads-fields-xml-field-types) + +在记录 XML 有效负载时,必须使用`FieldDescriptor`上的`type(Object)`方法为字段提供类型。文档中使用了所提供类型的`toString`方法的结果。 + +##### [重用字段描述符](#documenting-your-api-request-response-payloads-fields-reusing-field-descriptors) + +除了对[重用片段](#documenting-your-api-reusing-snippets)的一般支持外,请求和响应片段还允许使用路径前缀配置其他描述符。这使得请求或响应有效负载的重复部分的描述符可以创建一次,然后重用。 + +考虑返回一本书的端点: + +``` +{ + "title": "Pride and Prejudice", + "author": "Jane Austen" +} +``` + +`title`和`author`的路径分别是`title`和`author`。 + +现在考虑一个返回一组图书的端点: + +``` +[{ + "title": "Pride and Prejudice", + "author": "Jane Austen" +}, +{ + "title": "To Kill a Mockingbird", + "author": "Harper Lee" +}] +``` + +`title`和`author`的路径分别是`[].title`和`[].author`。单本书和书数组之间的唯一区别是,字段的路径现在有一个`[].`前缀。 + +你可以创建描述符来记录一本书,如下所示: + +``` +FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; +``` + +然后,你可以使用它们来记录一本书,如下所示: + +MockMVC + +``` +this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("book", responseFields(book))); (1) +``` + +|**1**|通过使用现有描述符,文档`title`和`author`| +|-----|-----------------------------------------------------------| + +WebTestClient + +``` +this.webTestClient.get().uri("/books/1").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("book", + responseFields(book))); (1) +``` + +|**1**|通过使用现有描述符,文档`title`和`author`| +|-----|-----------------------------------------------------------| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json").filter(document("book", responseFields(book))) (1) + .when().get("/books/1").then().assertThat().statusCode(is(200)); +``` + +|**1**|通过使用现有描述符,文档`title`和`author`| +|-----|-----------------------------------------------------------| + +你还可以使用描述符记录一组图书,如下所示: + +MockMVC + +``` +this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("book", responseFields(fieldWithPath("[]").description("An array of books")) (1) + .andWithPrefix("[].", book))); (2) +``` + +|**1**|记录该数组。| +|-----|-----------------------------------------------------------------------------------------| +|**2**|文档`[].title`和`[].author`使用带`[].`前缀的现有描述符| + +WebTestClient + +``` +this.webTestClient.get().uri("/books").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("books", + responseFields( + fieldWithPath("[]") + .description("An array of books")) (1) + .andWithPrefix("[].", book))); (2) +``` + +|**1**|记录该数组。| +|-----|-----------------------------------------------------------------------------------------| +|**2**|文档`[].title`和`[].author`使用带`[].`前缀的现有描述符| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json") + .filter(document("books", responseFields(fieldWithPath("[]").description("An array of books")) (1) + .andWithPrefix("[].", book))) (2) + .when().get("/books").then().assertThat().statusCode(is(200)); +``` + +|**1**|记录该数组。| +|-----|-----------------------------------------------------------------------------------------| +|**2**|文档`[].title`和`[].author`使用带`[].`前缀的现有描述符| + +#### [记录请求或响应有效负载的一个分节](#documenting-your-api-request-response-payloads-subsections) + +如果有效载荷很大或结构复杂,那么记录有效载荷的各个部分可能会很有用。REST DOCS 允许你通过提取有效负载的一个小节,然后将其记录下来来实现这一点。 + +##### [记录请求或响应机构的一个部分](#documenting-your-api-request-response-payloads-subsections-body) + +考虑以下 JSON 响应主体: + +``` +{ + "weather": { + "wind": { + "speed": 15.3, + "direction": 287.0 + }, + "temperature": { + "high": 21.2, + "low": 14.8 + } + } +} +``` + +你可以生成一个片段来记录`temperature`对象,如下所示: + +MockMVC + +``` +this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("location", responseBody(beneathPath("weather.temperature")))); (1) +``` + +|**1**|在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`responseBody`和`beneathPath`方法。
为请求体产生一个片段。
可以使用`requestBody`代替`responseBody`。| +|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("temperature", + responseBody(beneathPath("weather.temperature")))); (1) +``` + +|**1**|在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`responseBody`和`beneathPath`方法。
为请求主体生成一个片段。可以使用`requestBody`代替`responseBody`。| +|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseBody(beneathPath("weather.temperature")))) (1) + .when().get("/locations/1").then().assertThat().statusCode(is(200)); +``` + +|**1**|生成包含响应体的一个分节的片段。
在`responseBody`和`beneathPath`方法上使用静态`org.springframework.restdocs.payload.PayloadDocumentation`。
为请求体生成一个片段,可以使用`requestBody`代替`responseBody`。| +|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +结果是一个包含以下内容的片段: + +``` +{ + "temperature": { + "high": 21.2, + "low": 14.8 + } +} +``` + +为了使代码片段的名称不同,还包含了该小节的标识符。默认情况下,这个标识符是`beneath-${path}`。例如,前面的代码会产生一个名为`response-body-beneath-weather.temperature.adoc`的代码片段。你可以使用`withSubsectionId(String)`方法自定义标识符,如下所示: + +``` +responseBody(beneathPath("weather.temperature").withSubsectionId("temp")); +``` + +结果是一个名为`request-body-temp.adoc`的片段。 + +##### [记录请求或响应的一个分节的字段](#documenting-your-api-request-response-payloads-subsections-fields) + +除了记录请求或响应主体的一个小节外,你还可以记录特定小节中的字段。你可以生成一个片段,该片段记录`temperature`对象(`high`和`low`)的字段,如下所示: + +MockMVC + +``` +this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("location", responseFields(beneathPath("weather.temperature"), (1) + fieldWithPath("high").description("The forecast high in degrees celcius"), (2) + fieldWithPath("low").description("The forecast low in degrees celcius")))); +``` + +|**1**|在路径`weather.temperature`下的响应有效负载的小节中生成一个描述字段的片段。
在`beneathPath`上使用静态`beneathPath`方法。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录`high`和`low`字段。| + +WebTestClient + +``` +this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("temperature", + responseFields(beneathPath("weather.temperature"), (1) + fieldWithPath("high").description("The forecast high in degrees celcius"), (2) + fieldWithPath("low").description("The forecast low in degrees celcius")))); +``` + +|**1**|在路径`weather.temperature`下的响应有效负载的小节中生成一个描述字段的片段。
在`beneathPath`上使用静态`beneathPath`方法。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录`high`和`low`字段。| + +放心吧 + +``` +RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseFields(beneathPath("weather.temperature"), (1) + fieldWithPath("high").description("The forecast high in degrees celcius"), (2) + fieldWithPath("low").description("The forecast low in degrees celcius")))) + .when().get("/locations/1").then().assertThat().statusCode(is(200)); +``` + +|**1**|在路径`weather.temperature`下的响应有效负载的小节中生成一个描述字段的片段。
在`beneathPath`上使用静态`beneathPath`方法。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录`high`和`low`字段。| + +结果是一个片段,其中包含一个表,该表描述`high`和`low`的`weather.temperature`字段。为了使代码片段的名称不同,还包含了该小节的标识符。默认情况下,这个标识符是`beneath-${path}`。例如,前面的代码会产生一个名为`response-fields-beneath-weather.temperature.adoc`的代码片段。 + +### [请求参数](#documenting-your-api-request-parameters) + +你可以使用`requestParameters`记录请求的参数。你可以在`GET`请求的查询字符串中包含请求参数。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc.perform(get("/users?page=2&per_page=100")) (1) + .andExpect(status().isOk()).andDo(document("users", requestParameters((2) + parameterWithName("page").description("The page to retrieve"), (3) + parameterWithName("per_page").description("Entries per page") (4) + ))); +``` + +|**1**|在查询字符串中,使用两个参数`GET`和`per_page`执行`GET`请求。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求参数的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`requestParameters`方法。| +|**3**|记录`page`参数。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`parameterWithName`方法。| +|**4**|记录`per_page`参数。| + +WebTestClient + +``` +this.webTestClient.get().uri("/users?page=2&per_page=100") (1) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("users", requestParameters((2) + parameterWithName("page").description("The page to retrieve"), (3) + parameterWithName("per_page").description("Entries per page") (4) + ))); +``` + +|**1**|在查询字符串中,使用两个参数`GET`和`per_page`执行`GET`请求。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求参数的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`requestParameters`方法。| +|**3**|记录`page`参数。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`parameterWithName`方法。| +|**4**|记录`per_page`参数。| + +放心吧 + +``` +RestAssured.given(this.spec).filter(document("users", requestParameters((1) + parameterWithName("page").description("The page to retrieve"), (2) + parameterWithName("per_page").description("Entries per page")))) (3) + .when().get("/users?page=2&per_page=100") (4) + .then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述请求参数的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`requestParameters`方法。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录`page`参数。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`parameterWithName`方法。| +|**3**|记录`per_page`参数。| +|**4**|在查询字符串中,使用两个参数`GET`和`per_page`执行`GET`请求。| + +还可以将请求参数作为表单数据包含在 POST 请求的主体中。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc.perform(post("/users").param("username", "Tester")) (1) + .andExpect(status().isCreated()).andDo(document("create-user", + requestParameters(parameterWithName("username").description("The user's username")))); +``` + +|**1**|使用单个参数`POST`执行`username`请求。| +|-----|-------------------------------------------------------------| + +WebTestClient + +``` +MultiValueMap formData = new LinkedMultiValueMap<>(); +formData.add("username", "Tester"); +this.webTestClient.post().uri("/users").body(BodyInserters.fromFormData(formData)) (1) + .exchange().expectStatus().isCreated().expectBody() + .consumeWith(document("create-user", requestParameters( + parameterWithName("username").description("The user's username") +))); +``` + +|**1**|使用单个参数执行`POST`请求,`username`。| +|-----|-------------------------------------------------------------| + +放心吧 + +``` +RestAssured.given(this.spec) + .filter(document("create-user", + requestParameters(parameterWithName("username").description("The user's username")))) + .formParam("username", "Tester") (1) + .when().post("/users") (2) + .then().assertThat().statusCode(is(200)); +``` + +|**1**|配置`username`参数。| +|-----|-----------------------------------| +|**2**|执行`POST`请求。| + +在所有情况下,结果都是一个名为`request-parameters.adoc`的片段,其中包含一个表,该表描述了资源所支持的参数。 + +在记录请求参数时,如果在请求中使用了未记录的请求参数,则测试将失败。类似地,如果在请求中找不到已记录的请求参数,并且该请求参数未标记为可选的,则测试也会失败。 + +如果不想记录请求参数,可以将其标记为“忽略”。这可以防止它出现在生成的代码片段中,同时避免上述的故障。 + +你还可以在放松模式中记录请求参数,在这种模式中,任何未记录的参数都不会导致测试失败。要做到这一点,请在`org.springframework.restdocs.request.RequestDocumentation`上使用`relaxedRequestParameters`方法。在记录一个特定的场景时,如果你只想关注请求参数的一个子集,这可能会很有用。 + +### [路径参数](#documenting-your-api-path-parameters) + +你可以使用`pathParameters`记录请求的路径参数。下面的例子说明了如何做到这一点: + +MockMvc + +``` +this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) (1) + .andExpect(status().isOk()).andDo(document("locations", pathParameters((2) + parameterWithName("latitude").description("The location's latitude"), (3) + parameterWithName("longitude").description("The location's longitude") (4) + ))); +``` + +|**1**|使用两个路径参数`GET`和`longitude`执行`GET`请求。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求的路径参数的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`pathParameters`方法。| +|**3**|记录名为`latitude`的参数。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`parameterWithName`方法。| +|**4**|记录名为`longitude`的参数。| + +WebTestClient + +``` +this.webTestClient.get().uri("/locations/{latitude}/{longitude}", 51.5072, 0.1275) (1) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("locations", + pathParameters((2) + parameterWithName("latitude").description("The location's latitude"), (3) + parameterWithName("longitude").description("The location's longitude")))); (4) +``` + +|**1**|使用两个路径参数执行`GET`请求,`latitude`和`longitude`。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求的路径参数的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`pathParameters`方法。| +|**3**|记录名为`latitude`的参数。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`parameterWithName`方法。| +|**4**|记录名为`longitude`的参数。| + +放心吧 + +``` +RestAssured.given(this.spec).filter(document("locations", pathParameters((1) + parameterWithName("latitude").description("The location's latitude"), (2) + parameterWithName("longitude").description("The location's longitude")))) (3) + .when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) (4) + .then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述请求的路径参数的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`pathParameters`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录名为`latitude`的参数。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`parameterWithName`方法。| +|**3**|记录名为`longitude`的参数。| +|**4**|使用两个路径参数`GET`和`longitude`执行`GET`请求。| + +结果是一个名为`path-parameters.adoc`的片段,其中包含一个表,该表描述了资源所支持的路径参数。 + +| |如果使用 MockMVC,要使路径参数对文档可用,你必须使用`RestDocumentationRequestBuilders`而不是`MockMvcRequestBuilders`上的一个方法来构建请求。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在记录路径参数时,如果在请求中使用了未记录的路径参数,则测试将失败。类似地,如果在请求中找不到文档化的路径参数,并且路径参数未标记为可选的,则测试也会失败。 + +你还可以在放松模式中记录路径参数,在这种模式中,任何未记录的参数都不会导致测试失败。要做到这一点,请在`org.springframework.restdocs.request.RequestDocumentation`上使用`relaxedPathParameters`方法。在记录一个特定的场景时,如果你只想关注路径参数的一个子集,这可能会很有用。 + +如果不想记录路径参数,可以将其标记为“忽略”。这样做可以防止它出现在生成的代码片段中,同时避免前面描述的故障。 + +### [请求零件](#documenting-your-api-request-parts) + +你可以使用`requestParts`来记录多部分请求的各个部分。下面的示例展示了如何做到这一点: + +MockMvc + +``` +this.mockMvc.perform(multipart("/upload").file("file", "example".getBytes())) (1) + .andExpect(status().isOk()).andDo(document("upload", requestParts((2) + partWithName("file").description("The file to upload")) (3) + )); +``` + +|**1**|使用一个名为`file`的部件执行`POST`请求。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求部分的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`requestParts`方法。| +|**3**|记录名为`file`的部分。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`partWithName`方法。| + +WebTestClient + +``` +MultiValueMap multipartData = new LinkedMultiValueMap<>(); +multipartData.add("file", "example".getBytes()); +this.webTestClient.post().uri("/upload").body(BodyInserters.fromMultipartData(multipartData)) (1) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("upload", requestParts((2) + partWithName("file").description("The file to upload")) (3) +)); +``` + +|**1**|使用一个名为`file`的部件执行`POST`请求。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求部分的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`requestParts`方法。| +|**3**|记录名为`file`的部分。
在`partWithName`上使用静态`partWithName`方法。| + +放心吧 + +``` +RestAssured.given(this.spec).filter(document("users", requestParts((1) + partWithName("file").description("The file to upload")))) (2) + .multiPart("file", "example") (3) + .when().post("/upload") (4) + .then().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述请求部分的片段。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`requestParts`方法。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录名为`file`的部分。
在`org.springframework.restdocs.request.RequestDocumentation`上使用静态`partWithName`方法。| +|**3**|用名为`file`的部分配置请求。| +|**4**|执行`POST`请求到`/upload`。| + +结果是一个名为`request-parts.adoc`的片段,其中包含一个表,该表描述了资源支持的请求部分。 + +在记录请求部分时,如果在请求中使用了未记录的部分,则测试将失败。类似地,如果在请求中找不到已记录的部分,并且该部分未标记为可选的,则测试也会失败。 + +你还可以在放松模式中记录请求部分,在这种模式中,任何未记录的部分都不会导致测试失败。要做到这一点,请在`org.springframework.restdocs.request.RequestDocumentation`上使用`relaxedRequestParts`方法。在记录一个特定的场景时,如果你只想关注请求部分的一个子集,这可能会很有用。 + +如果不想记录请求部分,可以将其标记为“忽略”。这可以防止它出现在生成的代码片段中,同时避免前面描述的故障。 + +### [请求部分有效载荷](#documenting-your-api-request-parts-payloads) + +你可以用与[请求的有效载荷](#documenting-your-api-request-response-payloads)几乎相同的方式记录请求部分的有效负载,并支持记录请求部分的主体及其字段。 + +#### [记录请求部分的主体](#documenting-your-api-request-parts-payloads-body) + +你可以生成一个包含请求部分主体的片段,如下所示: + +MockMvc + +``` +MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); +MockMultipartFile metadata = new MockMultipartFile("metadata", "", "application/json", + "{ \"version\": \"1.0\"}".getBytes()); + +this.mockMvc.perform(multipart("/images").file(image).file(metadata).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("image-upload", requestPartBody("metadata"))); (1) +``` + +|**1**|配置 Spring REST DOCS 以生成包含名为`metadata`的请求部分的主体的片段。
在`requestPartBody`上使用静态`requestPartBody`方法。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +MultiValueMap multipartData = new LinkedMultiValueMap<>(); +Resource imageResource = new ByteArrayResource("<>".getBytes()) { + + @Override + public String getFilename() { + return "image.png"; + } + +}; +multipartData.add("image", imageResource); +multipartData.add("metadata", Collections.singletonMap("version", "1.0")); + +this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData)) + .accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk().expectBody() + .consumeWith(document("image-upload", + requestPartBody("metadata"))); (1) +``` + +|**1**|配置 Spring REST DOCS 以生成包含名为`metadata`的请求部分的主体的片段。
在`PayloadDocumentation`上使用静态`requestPartBody`方法。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +放心吧 + +``` +Map metadata = new HashMap<>(); +metadata.put("version", "1.0"); +RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartBody("metadata"))) (1) + .when().multiPart("image", new File("image.png"), "image/png").multiPart("metadata", metadata) + .post("images").then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成包含名为`metadata`的请求部分的主体的片段。
在`PayloadDocumentation`上使用静态`requestPartBody`方法。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +结果是一个名为`request-part-${part-name}-body.adoc`的片段,其中包含了该零件的主体。例如,记录一个名为`metadata`的部分会产生一个名为`request-part-metadata-body.adoc`的片段。 + +#### [记录请求部分的字段](#documenting-your-api-request-parts-payloads-fields) + +你可以用与请求或响应的字段相同的方式记录请求部分的字段,如下所示: + +MockMvc + +``` +MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); +MockMultipartFile metadata = new MockMultipartFile("metadata", "", "application/json", + "{ \"version\": \"1.0\"}".getBytes()); + +this.mockMvc.perform(multipart("/images").file(image).file(metadata).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("image-upload", requestPartFields("metadata", (1) + fieldWithPath("version").description("The version of the image")))); (2) +``` + +|**1**|配置 Spring REST DOCS 以生成描述在名为`metadata`的请求部分的有效负载中的字段的片段。
在`requestPartFields`上使用静态`requestPartFields`方法。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望具有路径`version`的字段。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`fieldWithPath`方法。| + +WebTestClient + +``` +MultiValueMap multipartData = new LinkedMultiValueMap<>(); +Resource imageResource = new ByteArrayResource("<>".getBytes()) { + + @Override + public String getFilename() { + return "image.png"; + } + +}; +multipartData.add("image", imageResource); +multipartData.add("metadata", Collections.singletonMap("version", "1.0")); +this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData)) + .accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk().expectBody() + .consumeWith(document("image-upload", + requestPartFields("metadata", (1) + fieldWithPath("version").description("The version of the image")))); (2) +``` + +|**1**|配置 Spring REST DOCS 以生成描述在名为`metadata`的请求部分的有效负载中的字段的片段。
在`requestPartFields`上使用静态`requestPartFields`方法。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望具有路径`version`的字段。
在`fieldWithPath`上使用静态`fieldWithPath`方法。| + +放心吧 + +``` +Map metadata = new HashMap<>(); +metadata.put("version", "1.0"); +RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartFields("metadata", (1) + fieldWithPath("version").description("The version of the image")))) (2) + .when().multiPart("image", new File("image.png"), "image/png").multiPart("metadata", metadata) + .post("images").then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述在名为`metadata`的请求部分的有效负载中的字段的片段。
在`requestPartFields`上使用静态`requestPartFields`方法。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|期望具有路径`version`的字段。
在`org.springframework.restdocs.payload.PayloadDocumentation`上使用静态`fieldWithPath`方法。| + +其结果是一个包含描述该部分字段的表的片段。这个片段被命名为`request-part-${part-name}-fields.adoc`。例如,记录一个名为`metadata`的部分会产生一个名为`request-part-metadata-fields.adoc`的片段。 + +在记录字段时,如果在零件的有效负载中发现了未记录的字段,则测试将失败。类似地,如果在零件的有效载荷中没有找到已记录的字段,并且该字段未标记为可选字段,则测试也会失败。对于具有层次结构的有效负载,记录一个字段就足以使其所有后代也被视为已被记录。 + +如果不想记录某个字段,可以将其标记为“忽略”。这样做可以防止它出现在生成的代码片段中,同时避免上述的故障。 + +你还可以在放松模式下记录字段,在这种模式下,任何未记录的字段都不会导致测试失败。要做到这一点,请在`org.springframework.restdocs.payload.PayloadDocumentation`上使用`relaxedRequestPartFields`方法。在记录特定场景时,如果你只想关注部件有效负载的一个子集,这可能会很有用。 + +有关描述字段、记录使用 XML 的有效负载的更多信息,请参见[关于记录请求和响应有效载荷的部分](#documenting-your-api-request-response-payloads)。 + +### [HTTP 头](#documenting-your-api-http-headers) + +你可以通过分别使用`requestHeaders`和`responseHeaders`来记录请求或响应中的头。下面的例子说明了如何做到这一点: + +MockMvc + +``` +this.mockMvc.perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) (1) + .andExpect(status().isOk()).andDo(document("headers", requestHeaders((2) + headerWithName("Authorization").description("Basic auth credentials")), (3) + responseHeaders((4) + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))); +``` + +|**1**|使用使用基本身份验证的`Authorization`报头执行`GET`请求。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求头的片段。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`requestHeaders`方法。| +|**3**|记录`Authorization`标头。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`headerWithName`方法。| +|**4**|生成一个描述响应头的片段。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`responseHeaders`方法。| + +WebTestClient + +``` +this.webTestClient + .get().uri("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=") (1) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("headers", + requestHeaders((2) + headerWithName("Authorization").description("Basic auth credentials")), (3) + responseHeaders((4) + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))); +``` + +|**1**|使用使用基本身份验证的`Authorization`报头执行`GET`请求。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|配置 Spring REST DOCS 以生成描述请求头的片段。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`requestHeaders`方法。| +|**3**|记录`Authorization`标头。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`headerWithName`方法。| +|**4**|生成一个描述响应头的片段。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`responseHeaders`方法。| + +REST Assured + +``` +RestAssured.given(this.spec).filter(document("headers", requestHeaders((1) + headerWithName("Authorization").description("Basic auth credentials")), (2) + responseHeaders((3) + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))) + .header("Authorization", "Basic dXNlcjpzZWNyZXQ=") (4) + .when().get("/people").then().assertThat().statusCode(is(200)); +``` + +|**1**|配置 Spring REST DOCS 以生成描述请求头的片段。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`requestHeaders`方法。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|记录`Authorization`头文件。
在 `org.springframework.restdocs.headers.headerdocumentation 上使用静态`headerWithName`方法。| +|**3**|生成一个描述响应头的片段。
在`org.springframework.restdocs.headers.HeaderDocumentation`上使用静态`responseHeaders`方法。| +|**4**|用使用基本身份验证的`Authorization`头配置请求。| + +结果是一个名为`request-headers.adoc`的片段和一个名为`response-headers.adoc`的片段。每个表都包含一个描述标题的表。 + +在记录 HTTP 头文件时,如果在请求或响应中找不到已记录的头文件,则测试将失败。 + +### [重用片段](#documenting-your-api-reusing-snippets) + +对于正在文档中的 API 来说,具有一些在其多个资源中通用的特性是很常见的。为了避免在记录此类资源时出现重复,你可以重用配置有公共元素的`Snippet`。 + +首先,创建描述公共元素的`Snippet`。下面的示例展示了如何做到这一点: + +``` +protected final LinksSnippet pagingLinks = links( + linkWithRel("first").optional().description("The first page of results"), + linkWithRel("last").optional().description("The last page of results"), + linkWithRel("next").optional().description("The next page of results"), + linkWithRel("prev").optional().description("The previous page of results")); +``` + +其次,使用这个片段并添加更多特定于资源的描述符。下面的例子说明了如何做到这一点: + +MockMvc + +``` +this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("example", this.pagingLinks.and((1) + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); +``` + +|**1**|重用`pagingLinks``Snippet`,调用`and`以添加特定于被记录的资源的描述符。| +|-----|-------------------------------------------------------------------------------------------------------------------------------| + +WebTestClient + +``` +this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk().expectBody() + .consumeWith(document("example", this.pagingLinks.and((1) + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); +``` + +|**1**|重用`pagingLinks``Snippet`,调用`and`以添加特定于被记录的资源的描述符。| +|-----|-------------------------------------------------------------------------------------------------------------------------------| + +REST Assured + +``` +RestAssured.given(this.spec).accept("application/json").filter(document("example", this.pagingLinks.and((1) + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))).get("/").then().assertThat() + .statusCode(is(200)); +``` + +|**1**|重用`pagingLinks``Snippet`,调用`and`以添加特定于被记录的资源的描述符。| +|-----|-------------------------------------------------------------------------------------------------------------------------------| + +该示例的结果是,带有`rel`值的`first`、`last`、`next`、`previous`、`alpha`和`bravo`的链接都是有文档记录的。 + +### [记录约束](#documenting-your-api-constraints) + +Spring REST DOCS 提供了许多类,这些类可以帮助你记录约束。你可以使用`ConstraintDescriptions`的实例来访问类的约束的描述。下面的示例展示了如何做到这一点: + +``` +public void example() { + ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); (1) + List descriptions = userConstraints.descriptionsForProperty("name"); (2) +} + +static class UserInput { + + @NotNull + @Size(min = 1) + String name; + + @NotNull + @Size(min = 8) + String password; + +} +``` + +|**1**|为`UserInput`类创建`ConstraintDescriptions`实例。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|获取`name`属性约束的描述。
此列表包含两个描述:一个用于`NotNull`约束,另一个用于`Size`约束。| + +Spring Hateoas 示例中的[`ApiDocumentation`](https://github.com/ Spring-projects/ Spring-restdocs/tree/v2.0.6.release/samples/rest-notes- Spring-hateoas/SRC/test/java/com/example/notes/apidocumentation.java)类显示了这一功能。 + +#### [寻找约束](#documenting-your-api-constraints-finding) + +默认情况下,通过使用 Bean 验证`Validator`来找到约束。目前,只支持属性约束。你可以通过使用自定义的`ValidatorConstraintResolver`实例创建`ConstraintDescriptions`来定制`Validator`所使用的`Validator`。要完全控制约束解析,可以使用自己的`ConstraintResolver`实现。 + +#### [描述约束](#documenting-your-api-constraints-describing) + +对于 Bean Validation2.0 的所有约束,都提供了缺省描述: + +* `AssertFalse` + +* `AssertTrue` + +* `DecimalMax` + +* `DecimalMin` + +* `Digits` + +* `Email` + +* `Future` + +* `FutureOrPresent` + +* `Max` + +* `Min` + +* `Negative` + +* `NegativeOrZero` + +* `NotBlank` + +* `NotEmpty` + +* `NotNull` + +* `Null` + +* `Past` + +* `PastOrPresent` + +* `Pattern` + +* `Positive` + +* `PositiveOrZero` + +* `Size` + +Hibernate Validator 还提供了以下约束的默认描述: + +* `CodePointLength` + +* `CreditCardNumber` + +* `Currency` + +* `EAN` + +* `Email` + +* `Length` + +* `LuhnCheck` + +* `Mod10Check` + +* `Mod11Check` + +* `NotBlank` + +* `NotEmpty` + +* `Currency` + +* `Range` + +* `SafeHtml` + +* `URL` + +要重写缺省描述或提供新的描述,你可以创建一个基名为`org.springframework.restdocs.constraints.ConstraintDescriptions`的资源包。 Spring 基于 Hateoas 的样本包含[这样的资源包的一个例子](https://github.com/spring-projects/spring-restdocs/tree/v2.0.6.RELEASE/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties)。 + +资源包中的每个键都是约束的完全限定名加上`.description`。例如,标准`@NotNull`约束的键是`javax.validation.constraints.NotNull.description`。 + +你可以在约束的描述中使用引用约束属性的属性占位符。例如,`@Min`约束的默认描述`Must be at least ${value}`是指约束的`value`属性。 + +要获得约束描述解析的更多控制,可以使用自定义`ConstraintDescriptions`创建`ResourceBundleConstraintDescriptionResolver`。要获得完全的控制,你可以使用自定义的`ConstraintDescriptions`实现来创建`ConstraintDescriptionResolver`。 + +#### [在生成的代码片段中使用约束描述](#_using_constraint_descriptions_in_generated_snippets) + +一旦有了约束的描述,你就可以在生成的代码片段中随意使用它们。例如,你可能希望将约束描述作为字段描述的一部分。或者,你可以在请求字段片段中以[额外信息](#documenting-your-api-customizing-including-extra-information)的形式包含约束。 Spring 基于 Hateoas 的示例中的[`ApiDocumentation`](https://github.com/ Spring-projects/ Spring-restdocs/tree/v2.0.6.release/samples/rest-notes- Spring-hateoas/SRC/test/java/com/example/notes/apidocumentation.java)类说明了后一种方法。 + +### [默认片段](#documenting-your-api-default-snippets) + +当你记录请求和响应时,会自动生成许多代码片段。 + +| Snippet |说明| +|---------------------|---------------------------------------------------------------------------------------------------------------------| +| `curl-request.adoc` |包含[`curl`](https://curl.haxx.se)命令,该命令与文档中的`MockMvc`调用等价。| +|`httpie-request.adoc`|包含[`HTTPie`](https://httpie.org)命令,该命令相当于正在记录的`MockMvc`调用。| +| `http-request.adoc` |包含与正在记录的`MockMVC`调用等价的 HTTP 请求。| +|`http-response.adoc` |包含返回的 HTTP 响应。| +| `request-body.adoc` |包含已发送的请求的主体。| +|`response-body.adoc` |包含返回的响应的主体。| + +你可以配置默认情况下产生的代码段。有关更多信息,请参见[配置部分](#configuration)。 + +### [使用参数化输出目录](#documentating-your-api-parameterized-output-directories) + +当使用 MockMVC、Rest Assured 或`WebTestClient`时,你可以参数化`document`使用的输出目录。使用`WebTestClient`参数化输出需要 Spring Framework5.3.5 或更高版本。 + +支持以下参数: + +| Parameter |说明| +|--------------|---------------------------------------------------------------| +| {methodName} |测试方法的未修改名称。| +|{method-name} |测试方法的名称,使用 kebab-case 格式化。| +|{method\_name}|测试方法的名称,使用 snake\_case 格式化。| +| {ClassName} |测试类的未修改的简单名称。| +| {class-name} |测试类的简单名称,使用 kebab-case 格式化。| +|{class\_name} |测试类的简单名称,使用 Snake\_case 格式化。| +| {step} |当前测试中对服务的调用次数。| + +例如,`document("{class-name}/{method-name}")`在一个名为`creatingANote`的测试方法中,在测试类`GettingStartedDocumentation`上将片段写入一个名为`getting-started-documentation/creating-a-note`的目录中。 + +参数化的输出目录与`@Before`方法结合使用时特别有用。它允许在设置方法中配置文档一次,然后在类中的每个测试中重用。下面的例子说明了如何做到这一点: + +MockMVC + +``` +@Before +public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).alwaysDo(document("{method-name}/{step}/")) + .build(); +} +``` + +放心吧 + +``` +@Before +public void setUp() { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(document("{method-name}/{step}")).build(); +} +``` + +WebTestClient + +``` +@Before +public void setUp() { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(this.restDocumentation)) + .entityExchangeResultConsumer(document("{method-name}/{step}")).build(); +} +``` + +有了这种配置,对你正在测试的服务的每次调用都会产生[默认片段](#documenting-your-api-default-snippets),而不需要进行任何进一步的配置。看看每个示例应用程序中的`GettingStartedDocumentation`类,就可以看到该功能的实际应用。 + +### [自定义输出](#documenting-your-api-customizing) + +本节描述如何自定义 Spring REST DOCS 的输出。 + +#### [自定义生成的代码片段](#documenting-your-api-customizing-snippets) + +Spring REST DOCS 使用模板来产生所生成的片段。是为 Spring REST DOCS 能够产生的每个片段提供的。要定制片段的内容,你可以提供自己的模板。 + +模板是从 Classpath 从`org.springframework.restdocs.templates`子包中加载的。子包的名称由所使用的模板格式的 ID 确定。默认的模板格式 ASCIIDoctor 的 ID 为`asciidoctor`,因此从`org.springframework.restdocs.templates.asciidoctor`加载片段。每个模板都以其生成的代码片段命名。例如,要覆盖`curl-request.adoc`片段的模板,请在`src/test/resources/org/springframework/restdocs/templates/asciidoctor`中创建一个名为`curl-request.snippet`的模板。 + +#### [包括额外的信息](#documenting-your-api-customizing-including-extra-information) + +有两种方法可以提供额外的信息以包含在生成的代码片段中: + +* 使用描述符上的`attributes`方法向其添加一个或多个属性。 + +* 在调用`curlRequest`、`httpRequest`、`httpResponse`时传入一些属性,以此类推。这样的属性与代码片段作为一个整体相关联。 + +在模板呈现过程中,任何附加属性都是可用的。再加上自定义的代码片段模板,这使得在生成的代码片段中包含额外的信息成为可能。 + +一个具体的例子是,在记录请求字段时,添加了一个约束列和一个标题。第一步是为你记录的每个字段提供`constraints`属性,并提供`title`属性。下面的例子说明了如何做到这一点: + +MockMVC + +``` +.andDo(document("create-user", requestFields(attributes(key("title").value("Fields for user creation")), (1) + fieldWithPath("name").description("The user's name") + .attributes(key("constraints").value("Must not be null. Must not be empty")), (2) + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints").value("Must be a valid email address"))))); (3) +``` + +|**1**|为请求字段片段配置`title`属性。| +|-----|---------------------------------------------------------------| +|**2**|为`name`字段设置`constraints`属性。| +|**3**|为`email`字段设置`constraints`属性。| + +WebTestClient + +``` +.consumeWith(document("create-user", + requestFields( + attributes(key("title").value("Fields for user creation")), (1) + fieldWithPath("name") + .description("The user's name") + .attributes(key("constraints").value("Must not be null. Must not be empty")), (2) + fieldWithPath("email") + .description("The user's email address") + .attributes(key("constraints").value("Must be a valid email address"))))); (3) +``` + +|**1**|为请求字段片段配置`title`属性。| +|-----|---------------------------------------------------------------| +|**2**|为`name`字段设置`constraints`属性。| +|**3**|为`email`字段设置`constraints`属性。| + +放心吧 + +``` +.filter(document("create-user", + requestFields(attributes(key("title").value("Fields for user creation")), (1) + fieldWithPath("name").description("The user's name") + .attributes(key("constraints").value("Must not be null. Must not be empty")), (2) + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints").value("Must be a valid email address"))))) (3) +``` + +|**1**|为请求字段片段配置`title`属性。| +|-----|---------------------------------------------------------------| +|**2**|为`name`字段设置`constraints`属性。| +|**3**|为`email`字段设置`constraints`属性。| + +第二步是提供一个名为`request-fields.snippet`的自定义模板,该模板在生成的代码片段的表中包含有关字段约束的信息,并添加一个标题。下面的示例展示了如何做到这一点: + +``` +.{{title}} (1) +|=== +|Path|Type|Description|Constraints (2) + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} +|{{constraints}} (3) + +{{/fields}} +|=== +``` + +|**1**|将标题添加到表格中。| +|-----|--------------------------------------------------------------------------| +|**2**|添加一个名为“约束”的新列。| +|**3**|在表的每一行中包含描述符的`constraints`属性。| + +## [定制请求和响应](#customizing-requests-and-responses) + +在某些情况下,你可能不希望将请求记录为与发送的请求完全一致,或者不希望将响应记录为与接收的请求完全一致。 Spring REST DOCS 提供了许多可用于在请求或响应被文档化之前对其进行修改的预处理器。 + +通过调用`document`和`OperationRequestPreprocessor`或`OperationResponsePreprocessor`来配置预处理。可以通过在`Preprocessors`上使用静态`preprocessRequest`和`preprocessResponse`方法获得实例。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc.perform(get("/")).andExpect(status().isOk()) + .andDo(document("index", preprocessRequest(removeHeaders("Foo")), (1) + preprocessResponse(prettyPrint()))); (2) +``` + +|**1**|应用一个请求预处理程序,删除名为`Foo`的标头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +WebTestClient + +``` +this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody() + .consumeWith(document("index", + preprocessRequest(removeHeaders("Foo")), (1) + preprocessResponse(prettyPrint()))); (2) +``` + +|**1**|应用一个请求预处理程序来删除名为`Foo`的头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +放心吧 + +``` +RestAssured.given(this.spec).filter(document("index", preprocessRequest(removeHeaders("Foo")), (1) + preprocessResponse(prettyPrint()))) (2) + .when().get("/").then().assertThat().statusCode(is(200)); +``` + +|**1**|应用一个请求预处理程序来删除名为`Foo`的标头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +或者,你可能希望将相同的预处理器应用于每个测试。可以通过在`@Before`方法中使用`RestDocumentationConfigurer`API 来配置预处理器。例如,要从所有请求中删除`Foo`标头并漂亮地打印所有响应,你可以执行以下操作之一(取决于你的测试环境): + +MockMVC + +``` +private MockMvc mockMvc; + +@Before +public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) (1) + .withResponseDefaults(prettyPrint())) (2) + .build(); +} +``` + +|**1**|应用一个请求预处理程序,删除名为`Foo`的头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +WebTestClient + +``` +private WebTestClient webTestClient; + +@Before +public void setup() { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .filter(documentationConfiguration(this.restDocumentation) + .operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) (1) + .withResponseDefaults(prettyPrint())) (2) + .build(); +} +``` + +|**1**|应用一个请求预处理程序来删除名为`Foo`的头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +放心吧 + +``` +private RequestSpecification spec; + +@Before +public void setup() { + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) (1) + .withResponseDefaults(prettyPrint())) (2) + .build(); +} +``` + +|**1**|应用一个请求预处理程序,删除名为`Foo`的头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +然后,在每个测试中,你可以执行特定于该测试的任何配置。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc.perform(get("/")).andExpect(status().isOk()) + .andDo(document("index", links(linkWithRel("self").description("Canonical self link")))); +``` + +WebTestClient + +``` +this.webTestClient.get().uri("/").exchange().expectStatus().isOk() + .expectBody().consumeWith(document("index", + links(linkWithRel("self").description("Canonical self link")))); +``` + +放心吧 + +``` +RestAssured.given(this.spec) + .filter(document("index", links(linkWithRel("self").description("Canonical self link")))).when() + .get("/").then().assertThat().statusCode(is(200)); +``` + +通过`Preprocessors`上的静态方法,可以获得各种内置的预处理器,包括上面所示的那些。有关更多详细信息,请参见[below](#customizing-requests-and-responses-preprocessors)。 + +### [预处理器](#customizing-requests-and-responses-preprocessors) + +#### [漂亮的印刷](#customizing-requests-and-responses-preprocessors-pretty-print) + +`prettyPrint`on`Preprocessors`格式化请求或响应的内容,以使其更易于阅读。 + +#### [掩蔽链接](#customizing-requests-and-responses-preprocessors-mask-links) + +如果你正在记录一个基于超媒体的 API,你可能希望鼓励客户机通过使用链接而不是通过使用硬编码的 URI 来导航该 API。这样做的一种方法是限制在文档中使用 URI。`maskLinks`on`Preprocessors`将响应中任何链接的`href`替换为`…​`。如果你愿意,你还可以指定一个不同的替代品。 + +#### [删除页眉](#customizing-requests-and-responses-preprocessors-remove-headers) + +`removeHeaders`on`Preprocessors`从请求或响应中删除任何标题,其中名称等于任何给定的标题名称。 + +`removeMatchingHeaders`on`Preprocessors`删除请求或响应中名称与给定正则表达式模式匹配的任何标题。 + +#### [替换模式](#customizing-requests-and-responses-preprocessors-replace-patterns) + +`replacePattern`on`Preprocessors`提供了用于替换请求或响应中的内容的通用机制。任何与正则表达式匹配的事件都将被替换。 + +#### [修改请求参数](#customizing-requests-and-responses-preprocessors-modify-request-parameters) + +你可以在`Preprocessors`上使用`modifyParameters`来添加、设置和删除请求参数。 + +#### [修改 URI](#customizing-requests-and-responses-preprocessors-modify-uris) + +| |如果使用 MockMVC 或未绑定到服务器的 WebTestClient,则应通过[更改配置](#configuration-uris)自定义 URI。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以在`Preprocessors`上使用`modifyUris`来修改请求或响应中的任何 URI。当使用绑定到服务器的 Rest Assured 或 WebTestClient 时,这允许你在测试服务的本地实例时自定义文档中出现的 URI。 + +#### [编写自己的预处理器](#customizing-requests-and-responses-preprocessors-writing-your-own) + +如果一个内置的预处理器不能满足你的需求,你可以通过实现`OperationPreprocessor`接口来编写自己的预处理器。然后,你可以以与任何内置预处理器完全相同的方式使用你的定制预处理器。 + +如果只想修改请求或响应的内容(主体),可以考虑实现`ContentModifier`接口,并将其与内置的`ContentModifyingOperationPreprocessor`一起使用。 + +## [配置](#configuration) + +本节介绍如何配置 Spring REST DOCS。 + +### [记录的 URI](#configuration-uris) + +本节介绍如何配置有文档的 URI。 + +#### [MockMVC URI 定制](#configuration-uris-mockmvc) + +在使用 MockMVC 时, Spring REST DOCS 记录的 URI 的默认配置如下: + +|Setting|默认值| +|-------|-----------| +|Scheme |`http`| +| Host |`localhost`| +| Port |`8080`| + +此配置由`MockMVCRestDocumentationConfigurer`应用。你可以使用其 API 更改一个或多个默认值以满足你的需求。下面的示例展示了如何做到这一点: + +``` +this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).uris().withScheme("https") + .withHost("example.com").withPort(443)) + .build(); +``` + +| |如果将端口设置为配置方案的默认值(HTTP 的端口为 80,HTTPS 的端口为 443),则在生成的代码段中的任何 URI 中都将省略该端口。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |要配置请求的上下文路径,请在`MockHttpServletRequestBuilder`上使用`contextPath`方法。| +|---|-------------------------------------------------------------------------------------------------------| + +#### [REST SEART URI 定制](#configuration-uris-rest-assured) + +Rest Assured 通过发出实际的 HTTP 请求来测试服务。因此,在对服务执行操作之后,但在对其进行文档记录之前,必须对 URI 进行自定义。a[Rest Assured-Specific 预处理器](#customizing-requests-and-responses-preprocessors-modify-uris)用于此目的。 + +#### [WebTestClient URI 自定义](#configuration-uris-webtestclient) + +当使用 WebTestClient 时, Spring REST DOCS 记录的 URI 的默认基础是`[http://localhost:8080](http://localhost:8080)`。你可以通过使用`WebTestClient.Builder`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.0.x/javadoc-api/org/springframework/test/web/reactive/server/webtestclient.builder.html#baseurl-java.lang.string-)上的[<`baseUrl(String)`方法来定制这个基础。下面的示例展示了如何做到这一点: + +``` +@Before +public void setUp() { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .baseUrl("https://api.example.com") (1) + .filter(documentationConfiguration(this.restDocumentation)).build(); +} +``` + +|**1**|将已记录的 URI 的基础配置为`[https://api.example.com](https://api.example.com)`。| +|-----|-------------------------------------------------------------------------------------------------| + +### [片段编码](#configuration-snippet-encoding) + +默认的代码片段编码是`UTF-8`。你可以使用`RestDocumentationConfigurer`API 更改默认的代码片段编码。例如,以下示例使用`ISO-8859-1`: + +MockMVC + +``` +this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).snippets().withEncoding("ISO-8859-1")) + .build(); +``` + +WebTestClient + +``` +this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(this.restDocumentation) + .snippets().withEncoding("ISO-8859-1")) + .build(); +``` + +放心吧 + +``` +this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation).snippets().withEncoding("ISO-8859-1")) + .build(); +``` + +| |Spring REST DOCS 将请求或响应的内容转换为`String`时,如果`Content-Type`标头中指定的`charset`是可用的。
如果没有它,使用了 JVM 的默认`Charset`。
通过使用`file.encoding`系统属性,可以配置 JVM 的默认`Charset`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [片断模板格式](#configuration-snippet-template-format) + +默认的代码片段模板格式是 ASCIIDoctor。Markdown 还支持开箱即用。你可以使用`RestDocumentationConfigurer`API 更改默认格式。下面的例子说明了如何做到这一点: + +MockMVC + +``` +this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).snippets() + .withTemplateFormat(TemplateFormats.markdown())) + .build(); +``` + +WebTestClient + +``` +this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(this.restDocumentation) + .snippets().withTemplateFormat(TemplateFormats.markdown())) + .build(); +``` + +放心吧 + +``` +this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation).snippets() + .withTemplateFormat(TemplateFormats.markdown())).build(); +``` + +### [默认片段](#configuration-default-snippets) + +默认情况下会产生六个片段: + +* `curl-request` + +* `http-request` + +* `http-response` + +* `httpie-request` + +* `request-body` + +* `response-body` + +在安装过程中,可以使用`RestDocumentationConfigurer`API 更改默认的代码段配置。以下示例默认情况下仅生成`curl-request`片段: + +MockMvc + +``` +this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).snippets().withDefaults(curlRequest())) + .build(); +``` + +WebTestClient + +``` +this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient().filter( + documentationConfiguration(this.restDocumentation) + .snippets().withDefaults(curlRequest())) + .build(); +``` + +放心吧 + +``` +this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation).snippets().withDefaults(curlRequest())) + .build(); +``` + +### [默认操作预处理器](#configuration-default-preprocessors) + +在安装过程中,可以使用`RestDocumentationConfigurer`API 配置默认的请求和响应预处理器。以下示例从所有请求中删除`Foo`标题,并打印所有响应: + +MockMvc + +``` +this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) (1) + .withResponseDefaults(prettyPrint())) (2) + .build(); +``` + +|**1**|应用一个请求预处理程序来删除名为`Foo`的标头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +WebTestClient + +``` +this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .filter(documentationConfiguration(this.restDocumentation) + .operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) (1) + .withResponseDefaults(prettyPrint())) (2) + .build(); +``` + +|**1**|应用一个请求预处理程序,删除名为`Foo`的标头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +放心吧 + +``` +this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) (1) + .withResponseDefaults(prettyPrint())) (2) + .build(); +``` + +|**1**|应用一个请求预处理程序,删除名为`Foo`的标头。| +|-----|-----------------------------------------------------------------| +|**2**|应用一个响应预处理器来打印它的内容。| + +## [与 ASCIIDoctor 合作](#working-with-asciidoctor) + +本节描述了与 Spring REST DOCS 特别相关的与 ASCIIDoctor 合作的方面。 + +| |ASCIIDoc 是文档格式。
ASCIIDoctor 是从 ASCIIDoc 文件(以`.adoc`结尾)生成内容(通常是 HTML)的工具。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------| + +### [Resources](#working-with-asciidoctor-resources) + +* [语法快速引用](https://asciidoctor.org/docs/asciidoc-syntax-quick-reference) + +* [用户手册](https://asciidoctor.org/docs/user-manual) + +### [包括片段](#working-with-asciidoctor-including-snippets) + +本节介绍如何包含 ASCIIDoc 片段。 + +#### [包括一个操作的多个片段](#working-with-asciidoctor-including-snippets-operation) + +你可以使用名为`operation`的宏导入为特定操作生成的全部或部分片段。它可以通过在项目的[构建配置](#getting-started-build-configuration)中包含`spring-restdocs-asciidoctor`来实现。 + +宏的目标是操作的名称。在最简单的形式中,你可以使用宏来包含一个操作的所有片段,如下面的示例所示: + +``` +operation::index[] +``` + +可以使用的操作宏还支持`snippets`属性。`snippets`属性来选择应该包含的代码片段。该属性的值是一个以逗号分隔的列表。列表中的每个条目都应该是要包含的片段文件的名称(减去`.adoc`后缀)。例如,只能包含 curl、HTTP 请求和 HTTP 响应片段,如下例所示: + +``` +operation::index[snippets='curl-request,http-request,http-response'] +``` + +前面的例子相当于下面的例子: + +``` +[[example_curl_request]] +== Curl request + +include::{snippets}/index/curl-request.adoc[] + +[[example_http_request]] +== HTTP request + +include::{snippets}/index/http-request.adoc[] + +[[example_http_response]] +== HTTP response + +include::{snippets}/index/http-response.adoc[] +``` + +##### [章节标题](#working-with-asciidoctor-including-snippets-operation-titles) + +对于使用`operation`宏包含的每个片段,都会创建一个带有标题的部分。为以下内建片段提供了默认标题: + +|片断| Title | +|-----------------|---------------| +|`curl-request`| Curl Request | +|`http-request`| HTTP request | +|`http-response`| HTTP response | +|`httpie-request`|HTTPie request | +|`links`| Links | +|`request-body`| Request body | +|`request-fields`|Request fields | +|`response-body`| Response body | +|`response-fields`|Response fields| + +对于上表中未列出的片段,通过用空格替换`-`字符并将第一个字母大写来生成默认标题。例如,一个名为`custom-snippet``will be`的片段的标题是“自定义片段”。 + +你可以使用文档属性自定义默认标题。属性的名称应该是`operation-{snippet}-title`。例如,要将`curl-request`片段的标题自定义为“示例请求”,你可以使用以下属性: + +``` +:operation-curl-request-title: Example request +``` + +#### [包括单个片段](#working-with-asciidoctor-including-snippets-individual) + +[包括宏](https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files)用于在文档中包含单个片段。可以使用`snippets`属性(由[构建配置](#getting-started-build-configuration)中配置的`spring-restdocs-asciidoctor`自动设置)来引用 snippets 输出目录。下面的示例展示了如何做到这一点: + +``` +include::{snippets}/index/curl-request.adoc[] +``` + +### [自定义表格](#working-with-asciidoctor-customizing-tables) + +许多片段在其默认配置中包含一个表。表的外观可以自定义,可以在包含代码片段时提供一些附加配置,也可以使用自定义代码片段模板。 + +#### [格式化列](#working-with-asciidoctor-customizing-tables-formatting-columns) + +ASCIIDoctor 对[格式化表格的列](https://asciidoctor.org/docs/user-manual/#cols-format)提供了丰富的支持。如下例所示,你可以使用`cols`属性指定表中列的宽度: + +``` +[cols="1,3"] (1) +include::{snippets}/index/links.adoc[] +``` + +|**1**|表格的宽度分为两列,第二列的宽度是第一列的三倍。| +|-----|-----------------------------------------------------------------------------------------------------------------| + +#### [配置标题](#working-with-asciidoctor-customizing-tables-title) + +你可以使用以`.`为前缀的行来指定表格的标题。下面的示例展示了如何做到这一点: + +``` +.Links (1) +include::{snippets}/index/links.adoc[] +``` + +|**1**|表格的标题将是`Links`。| +|-----|----------------------------------| + +#### [避免表格式问题](#working-with-asciidoctor-customizing-tables-formatting-problems) + +ASCIIDoctor 使用`|`字符对表格中的单元格进行划界。如果你希望`|`出现在单元格的内容中,这可能会导致问题。你可以通过使用反斜杠转义`|`来避免这个问题——换句话说,使用`\|`而不是`|`。 + +所有默认的 ASCIIDoctor 片段模板都使用名为`tableCellContent`的小胡子 lamba 自动执行这个转义。如果你编写自己的自定义模板,你可能想要使用这个 lamba。下面的示例展示了如何在包含`description`属性的值的单元格中转义`|`字符: + +``` +| {{#tableCellContent}}{{description}}{{/tableCellContent}} +``` + +### [进一步阅读](#working-with-asciidoctor-further-reading) + +有关自定义表的更多信息,请参见[ASCIIDoctor 用户手册中的表格部分](https://asciidoctor.org/docs/user-manual/#tables)。 + +## [与 Markdown 合作](#working-with-markdown) + +本节描述了与 Spring REST DOCS 特别相关的使用 Markdown 的方面。 + +### [局限性](#working-with-markdown-limitations) + +Markdown 最初是为人们为网络写作而设计的,因此,它并不像 ASCIIDoctor 那样适合编写文档。通常,这些限制可以通过使用另一个构建在 Markdown 之上的工具来克服。 + +Markdown 没有对表格的官方支持。 Spring REST DOCS 的默认 Markdown 片段模板使用[Markdown Extra 的表格格式](https://michelf.ca/projects/php-markdown/extra/#table)。 + +### [包括片段](#working-with-markdown-including-snippets) + +Markdown 没有内置支持将一个 Markdown 文件包含在另一个文件中。要将生成的 Markdown 片段包含在文档中,你应该使用支持此功能的附加工具。一个特别适合于记录 API 的示例是[Slate](https://github.com/tripit/slate)。 + +## [贡献](#contributing) + +Spring REST DOCS 旨在使你能够轻松地为你的 RESTful 服务生成高质量的文档。然而,没有你们的贡献,我们就无法实现这一目标。 + +### [Questions](#contributing-questions) + +通过使用`spring-restdocs`标记,可以在[堆栈溢出](https://stackoverflow.com)上询问有关 Spring REST DOCS 的问题。同样,我们鼓励你通过回答问题来帮助你的同伴 Spring 休息 DOCS 用户。 + +### [Bugs](#contributing-bugs) + +如果你认为你发现了一个 bug,请花点时间搜索。如果没有其他人报告了该问题,请[打开新的一期](https://github.com/spring-projects/spring-restdocs/issues/new)详细描述该问题,并在理想情况下包含一个复制该问题的测试。 + +### [增强功能](#contributing-enhancements) + +如果你希望对 Spring REST DOCS 进行增强,那么最受欢迎的是拉请求。源代码位于[GitHub](https://github.com/spring-projects/spring-restdocs)上。你可能希望搜索[现有问题](https://github.com/spring-projects/spring-restdocs/issues?q=is%3Aissue)和[拉请求](https://github.com/spring-projects/spring-restdocs/pulls?q=is%3Apr),以查看是否已经提出了增强。你可能还希望[打开新的一期](https://github.com/spring-projects/spring-restdocs/issues/new)在开始工作之前讨论可能的增强。 \ No newline at end of file diff --git a/docs/spring-shell/spring-shell.md b/docs/spring-shell/spring-shell.md new file mode 100644 index 0000000..7f1a4cf --- /dev/null +++ b/docs/spring-shell/spring-shell.md @@ -0,0 +1,754 @@ +# Spring shell 参考文档 + +## 贝壳是什么? + +并不是所有的应用程序都需要一个花哨的 Web 用户界面!有时,使用交互式终端与应用程序进行交互是完成工作的最合适方法。 + +Spring Shell 允许人们轻松地创建这样一个可运行的应用程序,其中用户将输入文本命令,这些命令将被执行,直到程序终止。 Spring Shell 项目提供了创建这样一个 REPL(读取、求值、打印循环)的基础设施,允许开发人员使用熟悉的 Spring 编程模型,专注于命令的实现。 + +高级功能,如解析、制表符完成、输出的彩色化、漂亮的 ASCII-Art 表格显示、输入转换和验证都是免费的,开发者只需专注于核心命令逻辑即可。 + +## 使用 Spring shell + +### 开始 + +为了了解 Spring Shell 所提供的功能,让我们编写一个简单的 Shell 应用程序,它有一个简单的命令,可以将两个数字相加。 + +#### 让我们编写一个简单的启动应用程序 + +从版本 2 开始, Spring Shell 已经从头开始重写,并考虑到了各种增强功能,其中之一是轻松地与 Spring Boot 集成,尽管这不是一个很强的需求。为了本教程的目的,让我们创建一个简单的引导应用程序,例如使用[start.spring.io](https://start.spring.io)。这个最小的应用程序仅依赖于`spring-boot-starter`并配置`spring-boot-maven-plugin`,生成一个可执行的 über-jar: + +``` +... + + + org.springframework.boot + spring-boot-starter + + ... +``` + +#### 在 Spring shell 上添加依赖项 + +使用 Spring shell 的最简单方法是依赖于`spring-shell-starter`工件。这是使用 shell 所需的所有功能,并在引导时很好地发挥作用,只根据需要配置必要的 bean: + +``` +... + + org.springframework.shell + spring-shell-starter + 2.0.1.RELEASE + +... +``` + +| |考虑到 Spring shell 将通过存在此依赖关系来启动并启动 REPL,
你将需要在整个教程中构建跳过测试(`-DskipTests`),或者删除由[start.spring.io](https://start.spring.io)生成的示例集成测试
。如果不这样做,集成测试将创建
Spring `ApplicationContext`,并且根据你的构建工具,它将停留在 eval 循环中,或者与 NPE 一起崩溃。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 你的第一个命令 + +是时候添加我们的第一个命令了。创建一个新的类(随意命名),并用`@ShellComponent`(`@Component`的变体,用于限制扫描候选命令的类集)对其进行注释。 + +然后,创建一个`add`方法,该方法接受两个 INTS(`a`和`b`)并返回它们的和。用`@ShellMethod`对其进行注释,并在注释中提供对该命令的描述(这是唯一需要的信息): + +``` +package com.example.demo; + +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellComponent; + +@ShellComponent +public class MyCommands { + + @ShellMethod("Add two integers together.") + public int add(int a, int b) { + return a + b; + } +} +``` + +#### 让我们载它一程吧! + +构建应用程序并运行生成的 JAR,就像这样; + +``` +./mvnw clean install -DskipTests +[...] + +java -jar target/demo-0.0.1-SNAPSHOT.jar +``` + +下面的屏幕会欢迎你(横幅来自 Spring boot,可以自定义[as usual](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-banner)): + +``` + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v1.5.6.RELEASE) + +shell:> +``` + +下面是一个黄色的`shell:>`提示符,它邀请你键入命令。输入`add 1 2`然后进入并欣赏魔术! + +``` +shell:>add 1 2 +3 +``` + +尝试玩 shell(提示:有一个`help`命令),完成后,输入`exit`Enter。 + +本文的其余部分深入研究了整个 Spring shell 编程模型。 + +### 编写自己的命令 + +Spring shell 决定将方法转换为实际的 shell 命令的方式是完全可插入的(参见[Extending Spring Shell](#extending-spring-shell)),但是在 Spring shell2.x 中,推荐的编写命令的方式是使用本节中描述的新 API(即所谓的*标准*API)。 + +使用*标准*API,bean 上的方法将被转换为可执行命令,前提是 + +* Bean 类带有`@ShellComponent`注释。这用于限制被考虑的 bean 集。 + +* 该方法带有`@ShellMethod`注释。 + +| |`@ShellComponent`是一种原型注释本身,用`@Component`进行元注释。这样,除了可以使用
的过滤机制外,还可以使用*声明*bean(* 例如 * 使用`@ComponentScan`)。

所创建的 Bean 的名称可以使用注释的`value`属性进行自定义。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 这一切都是关于文档的! + +`@ShellMethod`注释唯一需要的属性是其`value`属性,它应该用来写一个简短的一句话,描述命令的功能。这一点很重要,这样用户就可以在不必离开 shell 的情况下获得关于命令的一致帮助(参见[集成文档`help`命令](#help-command))。 + +| |对你的命令的描述应该简短,只需一两句话。为了更好的一致性,
建议它以大写字母开头,以点结尾。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 自定义命令名 + +默认情况下,不需要为你的命令指定*钥匙*(即 * 应该在 shell 中调用它的单词)。方法的名称将被用作命令键,将 camelcase 名称转换为 dashed、gnu 样式的名称(即`sayHello()`将变为`say-hello`)。 + +但是,可以使用注释的`key`属性显式设置命令键,如下所示: + +``` + @ShellMethod(value = "Add numbers.", key = "sum") + public int add(int a, int b) { + return a + b; + } +``` + +| |`key`属性接受多个值。
如果你为一个方法设置多个键,那么将使用这些不同的别名注册该命令。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |命令键可以包含几乎任何字符,包括空格。但是,在使用名称时,
要记住,一致性通常是用户喜欢的(例如,* 避免将虚线名称与间隔名称混合,*等等。*)| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 调用你的命令 + +#### 按名称*VS。*位置参数 + +如上所述,用`@ShellMethod`装饰方法是创建命令的唯一要求。这样做时,用户可以通过两种可能的方式设置所有方法参数的值: + +* 使用参数键(* 例如 *`--arg value`)。这种方法被称为“按名称”参数。 + +* 或者在没有键的情况下,只需按照方法签名中出现的相同顺序设置参数值(“位置”参数)。 + +这两种方法可以混合和匹配,命名参数总是优先考虑(因为它们不太容易出现歧义)。因此,给出以下命令 + +``` + @ShellMethod("Display stuff.") + public String echo(int a, int b, int c) { + return String.format("You said a=%d, b=%d, c=%d", a, b, c); + } +``` + +然后,下面的调用都是等价的,如输出所示: + +``` +shell:>echo 1 2 3 (1) +You said a=1, b=2, c=3 + +shell:>echo --a 1 --b 2 --c 3 (2) +You said a=1, b=2, c=3 + +shell:>echo --b 2 --c 3 --a 1 (3) +You said a=1, b=2, c=3 + +shell:>echo --a 1 2 3 (4) +You said a=1, b=2, c=3 + +shell:>echo 1 --c 3 2 (5) +You said a=1, b=2, c=3 +``` + +|**1**|这使用了位置参数| +|-----|----------------------------------------------------------------| +|**2**|这是一个完整的副名称参数的示例| +|**3**|可以根据需要对副名称参数进行重新排序。| +|**4**|你可以混合使用这两种方法。| +|**5**|非副名称参数按其出现的顺序进行解析。| + +##### 自定义命名参数键 + +如上所示,派生命名参数的键的默认策略是使用方法签名的 Java 名称,并在其前加上两个破折号(`--`)。这可以通过两种方式进行定制: + +1. 要更改整个方法的默认前缀,请使用`prefix()`注释的`@ShellMethod`属性 + +2. 要以每参数方式重写*整体*键,请使用`@ShellOption`注释对参数进行注释。 + +看看下面的例子: + +``` + @ShellMethod(value = "Display stuff.", prefix="-") + public String echo(int a, int b, @ShellOption("--third") int c) { + return String.format("You said a=%d, b=%d, c=%d", a, b, c); + } +``` + +对于这样的设置,可能的参数键是`-a`、`-b`和`--third`。 + +| |可以为单个参数指定多个键。如果是这样,这些将以相互排斥的方式
指定相同的参数(因此只能使用其中的一个)。例如,下面是内置[
](#help-command)命令的签名:

```
@ShellMethod("Describe a command.")
public String help(@ShellOption({"-C", "--command"}) String command) {
...
}
```| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 可选参数和默认值 + +Spring Shell 提供了给出参数默认值的能力,这将允许用户省略那些参数: + +``` + @ShellMethod("Say hello.") + public String greet(@ShellOption(defaultValue="World") String who) { + return "Hello " + who; + } +``` + +现在,`greet`命令仍然可以作为`greet Mother`(或`greet --who Mother`)调用,但也可以执行以下操作: + +``` +shell:>greet +Hello World +``` + +#### 参数有序度 + +到目前为止,一直假定每个参数映射到用户输入的单个单词。但是,当参数值应该是*多值*时,可能会出现这种情况。这是由`@ShellOption`注释的`arity()`属性驱动的。只需为参数类型使用集合或数组,并指定需要多少个值: + +``` + @ShellMethod("Add Numbers.") + public float add(@ShellOption(arity=3) float[] numbers) { + return numbers[0] + numbers[1] + numbers[2]; + } +``` + +然后可以使用以下任何语法调用该命令: + +``` +shell:>add 1 2 3.3 +6.3 +shell:>add --numbers 1 2 3.3 +6.3 +``` + +| |当使用*副名*参数方法时,应该重复使用**不是**键。以下是**不是**的工作:

```
shell:>add --numbers 1 --numbers 2 --numbers 3.3
```| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 无限性 + +TO BE IMPLEMENTED + +##### 布尔参数的特殊处理 + +当涉及到参数的有用性时,有一种参数在默认情况下会受到特殊的处理,这在命令行实用程序中通常是这样的。布尔参数(即`boolean`以及`java.lang.Boolean`)的行为就像默认情况下它们的`arity()`的`0`一样,允许用户使用“标志”方法设置它们的值。请看以下内容: + +``` + @ShellMethod("Terminate the system.") + public String shutdown(boolean force) { + return "You said " + force; + } +``` + +这允许以下调用: + +``` +shell:>shutdown +You said false +shell:>shutdown --force +You said true +``` + +| |这种特殊处理与[默认值](#optional-parameters-default-values)规范配合得很好。虽然布尔参数的默认
是将其默认值设置为`false`,但是你可以另外指定(* 即 *`@ShellOption(defaultValue="true")`),并且行为将被反转(也就是说,不指定参数
将导致值为`true`,并且指定该标志将导致值`false`)| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |具有这种隐式`arity()=0`的行为可以防止用户指定一个值(* 例如 *`shutdown --force true`)。
如果你希望允许这种行为(并且放弃该标志方法),则使用注释强制执行`1`的项:

```
@ShellMethod("Terminate the system.")
public String shutdown(@ShellOption(arity=1, defaultValue="false") boolean force) {
return "You said " + force;
}
```| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 报价处理 + +Spring Shell 接受用户输入并在*文字*中对其进行标记,在空格字符上进行分割。如果用户希望提供一个包含空格的参数值,则需要引用该值。支持单引号(`'`)和双引号(`"`),这些引号不会成为值的一部分: + +``` + @ShellMethod("Prints what has been entered.") + public String echo(String what) { + return "You said " + what; + } +``` + +``` +shell:>echo Hello +You said Hello +shell:>echo 'Hello' +You said Hello +shell:>echo 'Hello World' +You said Hello World +shell:>echo "Hello World" +You said Hello World +``` + +支持单引号和双引号,允许用户轻松地将一种类型的引号嵌入到一个值中: + +``` +shell:>echo "I'm here!" +You said I'm here! +shell:>echo 'He said "Hi!"' +You said He said "Hi!" +``` + +如果用户需要嵌入用于引用整个参数的相同类型的引用,则转义序列使用反斜杠(`\`)字符: + +``` +shell:>echo 'I\'m here!' +You said I'm here! +shell:>echo "He said \"Hi!\"" +You said He said "Hi!" +shell:>echo I\'m here! +You said I'm here! +``` + +在不使用附加引号的情况下,也可以转义空格字符,例如: + +``` +shell:>echo This\ is\ a\ single\ value +You said This is a single value +``` + +#### 与壳相互作用 + +Spring shell 项目构建在[JLine](https://github.com/jline/jline3)库的基础上,因此带来了许多不错的交互特性,其中一些特性将在本节中详细介绍。 + +首先也是最重要的是, Spring shell 几乎在所有可能的地方都支持选项卡补全。因此,如果有一个`echo`命令,并且用户按 E,C,Tab,那么`echo`就会出现。如果有几个以`ec`开头的命令,那么将提示用户进行选择(使用 tab orshift+tab 进行导航,并输入以进行选择)。 + +但完成并不止于命令键。如果应用程序开发人员注册了适当的 bean(参见[提供 TAB 完成建议](#providing-tab-completion)),它也适用于参数键(`--arg`)甚至参数值。 + +Spring Shell 应用程序的另一个不错的功能是支持行延续。如果一个命令及其参数太长,且不能很好地在屏幕上显示,那么用户可以对其进行分块,并用反斜杠(`\`)结束一行,然后在下一行按 Enter 并继续。在提交了整个命令后,这将被解析为用户在换行时输入了单个空格。 + +``` +shell:>register module --type source --name foo \ (1) +> --uri file:///tmp/bar +Successfully registered module 'source:foo' +``` + +|**1**|命令在下一行继续| +|-----|------------------------------| + +如果用户打开了一个引号(参见[报价处理](#quotes-handling))并在仍处于引号中时单击 Enter,则行延续也会自动触发: + +``` +shell:>echo "Hello (1) +dquote> World" +You said Hello World +``` + +|**1**|用户按下回车键| +|-----|-----------------------| + +最后, Spring Shell 应用程序受益于许多键盘快捷方式,你在使用常规的 OS Shell 时可能已经熟悉这些快捷方式,这些快捷方式是从 Emacs 借来的。值得注意的快捷方式包括 Ctrl+R 执行反向搜索,Ctrl+A 和 Ctrl+E RESP 地移动到行的开始和结束,或 Esc f 和 Desc B 一次移动一个单词。 + +##### 提供 TAB 完成建议 + +TBD + +### 验证命令参数 + +Spring Shell 与[Bean Validation API](http://beanvalidation.org/)集成,以支持对命令参数的自动和自记录约束。 + +在命令参数上发现的注释以及在方法级别上的注释将得到尊重,并在执行命令之前触发验证。给出以下命令: + +``` + @ShellMethod("Change password.") + public String changePassword(@Size(min = 8, max = 40) String password) { + return "Password successfully set to " + password; + } +``` + +你将免费获得这种行为: + +``` +shell:>change-password hello +The following constraints were not met: + --password string : size must be between 8 and 40 (You passed 'hello') +``` + +| |应用于所有的命令实现

重要的是要注意, Bean 验证适用于所有的命令实现,不管
它们是通过使用适配器使用“标准”API 还是任何其他 API(参见[支持其他 API](#support-for-shell-1-and-jcommander))| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 动态命令可用性 + +有时,由于应用程序的内部状态,注册的命令可能没有意义。例如,可能有一个`download`命令,但它仅在用户在远程服务器上使用`connect`时才起作用。现在,如果用户尝试使用`download`命令,那么 shell 应该很好地解释*是吗?*命令的存在,但是它当时是不可用的。 Spring Shell 允许开发人员这样做,甚至提供对命令不可用的原因的简短解释。 + +命令有三种可能的方式来指示可用性。它们都利用了返回`Availability`实例的 no-arg 方法。让我们从一个简单的例子开始: + +``` +@ShellComponent +public class MyCommands { + + private boolean connected; + + @ShellMethod("Connect to the server.") + public void connect(String user, String password) { + [...] + connected = true; + } + + @ShellMethod("Download the nuclear codes.") + public void download() { + [...] + } + + public Availability downloadAvailability() { + return connected + ? Availability.available() + : Availability.unavailable("you are not connected"); + } +} +``` + +在这里你可以看到`connect`方法用于连接到服务器(详细信息从中省略),在完成时通过`connected`布尔命令来改变命令的状态。在用户连接之前,`download`命令将被标记为*不可用*,这要感谢存在一个名为`download`命令方法的方法,该方法的名称后缀为`Availability`。该方法返回`Availability`的实例,该实例是用两个工厂方法中的一个方法构造的。如果命令不可用,则必须提供解释。现在,如果用户试图在未连接的情况下调用该命令,将会发生以下情况: + +``` +shell:>download +Command 'download' exists but is not currently available because you are not connected. +Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace. +``` + +集成帮助还利用了有关当前不可用命令的信息。请参阅[使用`help`命令的集成文档]。 + +| |如果在“because…”

后面附加了“because”,那么当命令不可用时提供的原因应该很好地理解为“because”
,最好不要以大写字母开头,也不要加上最后一个点。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果由于某种原因,在命令方法的名称之后命名可用性方法不适合你,那么可以使用`@ShellMethodAvailability`提供一个显式名称,如下所示: + +``` + @ShellMethod("Download the nuclear codes.") + @ShellMethodAvailability("availabilityCheck") (1) + public void download() { + [...] + } + + public Availability availabilityCheck() { (1) + return connected + ? Availability.available() + : Availability.unavailable("you are not connected"); + } +``` + +|**1**|名字必须匹配。| +|-----|-----------------------| + +最后,通常的情况是,同一个类中的几个命令共享相同的内部状态,因此它们应该都是可用的或不可用的。 Spring shell 不需要在所有命令方法上粘贴`@ShellMethodAvailability`,而是允许用户在可用性方法上放置`@ShellMethodAvailabilty`注释,并指定它控制的命令的名称: + +``` + @ShellMethod("Download the nuclear codes.") + public void download() { + [...] + } + + @ShellMethod("Disconnect from the server.") + public void disconnect() { + [...] + } + + @ShellMethodAvailability({"download", "disconnect"}) + public Availability availabilityCheck() { + return connected + ? Availability.available() + : Availability.unavailable("you are not connected"); + } +``` + +| |`@ShellMethodAvailability.value()`属性的默认值是`"*"`,这是一个特殊的
通配符,它匹配所有命令名。因此,只需一个可用性方法,就可以轻松地打开或关闭单个类的
的所有命令。下面是一个例子:

```
@ShellComponent
public class Toggles {
@ShellMethodAvailability
public Availability availabilityOnWeekdays() {
return Calendar.getInstance().get(DAY_OF_WEEK) == SUNDAY
? Availability.available()
: Availability.unavailable("today is not Sunday");
}

@ShellMethod
public void foo() {}

@ShellMethod
public void bar() {}
}
```| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring 对于如何编写命令和如何组织类,shell 并没有施加太多的约束。
但是将相关的命令放在同一个类中通常是很好的实践,可用性指标
可以从中受益。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 组织命令 + +当你的 shell 开始提供大量功能时,你可能会使用大量命令,这可能会使你的用户感到困惑。输入`help`,他们会看到一个令人生畏的命令列表,按字母顺序排列,这可能并不总是有意义的。 + +为了缓解这种情况, Spring Shell 提供了将命令组合在一起的能力,并提供了合理的默认值。然后,相关的命令将在相同的*集团*(* 例如 *`User Management Commands`)中结束,并一起显示在帮助屏幕和其他位置中。 + +默认情况下,命令将根据它们实现的类进行分组,将 camel case 类名称转换为单独的单词(因此`URLRelatedCommands`变为`URL Related Commands`)。这是一个非常合理的默认设置,因为相关的命令通常已经在类中了,因为它们需要使用相同的协作对象。 + +但是,如果此行为不适合你,则可以以下方式重写命令的组,按优先级顺序排列: + +* 在`@ShellMethod`注释中指定`group()` + +* 在命令定义的类上放置`@ShellCommandGroup`。这将为该类中定义的所有命令应用该组(除非如上所述被重写) + +* 在包上放置`@ShellCommandGroup`(*via*`package-info.java`)命令。这将适用于包中定义的所有命令(除非如上所述在方法或类级别重写) + +下面是一个简短的例子: + +``` +public class UserCommands { + @ShellMethod(value = "This command ends up in the 'User Commands' group") + public void foo() {} + + @ShellMethod(value = "This command ends up in the 'Other Commands' group", + group = "Other Commands") + public void bar() {} +} + +... + +@ShellCommandGroup("Other Commands") +public class SomeCommands { + @ShellMethod(value = "This one is in 'Other Commands'") + public void wizz() {} + + @ShellMethod(value = "And this one is 'Yet Another Group'", + group = "Yet Another Group") + public void last() {} +} +``` + +### 内置命令 + +任何使用`spring-shell-starter`工件(或者更准确地说,使用`spring-shell-standard-commands`依赖关系)构建的应用程序都带有一组内置命令。这些命令可以单独重写或禁用(参见[覆盖或禁用内置命令](#overriding-or-disabling-built-in-commands)),但是如果它们不是,本节将描述它们的行为。 + +#### 使用`help`命令集成文档 + +运行一个 shell 应用程序通常意味着用户处于一个图形受限的环境中。虽然,在手机时代,我们总是连接在一起,但访问网络浏览器或任何其他丰富的 UI 应用程序(如 PDF 查看器)可能并不总是可能的。这就是为什么 shell 命令被正确地自我记录是很重要的,这就是`help`命令出现的地方。 + +输入`help`+Enter 将列出 shell 中所有已知的命令(包括[不可用](#dynamic-command-availability)命令),并对它们所做的工作进行简短描述: + +``` +shell:>help +AVAILABLE COMMANDS + add: Add numbers together. + * authenticate: Authenticate with the system. + * blow-up: Blow Everything up. + clear: Clear the shell screen. + connect: Connect to the system + disconnect: Disconnect from the system. + exit, quit: Exit the shell. + help: Display help about available commands. + register module: Register a new module. + script: Read and execute commands from a file. + stacktrace: Display the full stacktrace of the last error. + +Commands marked with (*) are currently unavailable. +Type `help ` to learn more. +``` + +键入`help `将显示有关命令的更详细信息,包括可用参数、它们的类型以及它们是否是强制的,*等等。* + +下面是应用于自身的`help`命令: + +``` +shell:>help help + +NAME + help - Display help about available commands. + +SYNOPSYS + help [[-C] string] + +OPTIONS + -C or --command string + The command to obtain help for. [Optional, default = ] +``` + +#### 清除屏幕 + +`clear`命令执行你预期的操作并清除屏幕,重置左上角的提示符。 + +#### 脱壳而出 + +`quit`命令(也别名为`exit`)只需请求 shell 退出,从而优雅地关闭 Spring 应用程序上下文。如果不重写,Jline`History` Bean 将把所有执行的命令的历史记录写入磁盘,以便它们在下次启动时再次可用(参见[与壳相互作用](#interacting-with-the-shell))。 + +#### 显示有关错误的详细信息 + +当命令代码中出现异常时,shell 会捕捉到异常,并显示一条简单的一行消息,以避免过多的信息溢出用户。但在某些情况下,了解到底发生了什么是很重要的(特别是如果异常有一个嵌套的原因)。 + +为此目的, Spring Shell 会记住上次发生的异常,用户以后可以使用`stacktrace`命令在控制台上打印所有血腥的细节。 + +#### 运行一批命令 + +`script`命令接受一个本地文件作为参数,并将重播在该文件中找到的命令,一次一个。 + +从文件中读取的行为与交互式 shell 中的行为完全相同,因此以`//`开头的行将被视为注释并被忽略,而以`\`结尾的行将触发行延续。 + +### 自定义 shell + +#### 覆盖或禁用内置命令 + +[内置命令](#built-in-commands)提供了 Spring shell 以实现许多(如果不是所有的话)shell 应用程序所需的日常任务。如果你对他们的行为方式不满意,你可以禁用或重写他们,就像在本节中解释的那样。 + +| |禁用所有内置命令

如果你根本不需要内置命令,那么有一种简单的方法可以“禁用”它们:只是不要包含它们!
要么在`spring-shell-standard-commands`上使用 Maven 排除,要么,如果你有选择地包括 Spring shell 依赖项,
不要将那个带进来!
<213"/>r=“r=”206"/>| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 禁用特定命令 + +要禁用单个内置命令,只需在应用程序`Environment`中将`spring.shell.command..enabled`属性设置为`false`。一种简单的方法是将额外的参数传递到你的`main()`入口点中的引导应用程序: + +``` + public static void main(String[] args) throws Exception { + String[] disabledCommands = {"--spring.shell.command.help.enabled=false"}; (1) + String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands); + SpringApplication.run(MyApp.class, fullArgs); + } +``` + +|**1**|这将禁用集成的`help`命令| +|-----|-------------------------------------------| + +##### 覆盖特定命令 + +如果你不想禁用命令,而是提供自己的实现,那么你可以选择 + +* 禁用上面说明的命令,并以相同的名称注册你的实现。 + +* 让你的实现类实现`.Command`接口。作为示例,下面介绍如何覆盖`clear`命令: + + ``` + public class MyClear implements Clear.Command { + + @ShellMethod("Clear the screen, only better.") + public void clear() { + // ... + } + } + ``` + +| |请考虑贡献你的更改

如果你认为标准命令的实现对社区可能很有价值,
请考虑在[github.com/spring-projects/spring-shell](https://github.com/spring-projects/spring-shell)处打开一个拉-请求。

或者,在你自己进行任何更改之前,你可以打开该项目的一个问题。欢迎反馈
!| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 结果解决者 + +#### PromptProvider + +每次调用命令后,shell 都会等待用户的新输入,并以黄色显示*提示*: + +``` +shell:> +``` + +可以通过注册类型`PromptProvider`的 Bean 来定制此行为。 Bean 这样的内部状态可以使用来决定向用户显示什么(它可以例如对[应用程序事件](https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#context-functionality-events-annotation)做出反应),并且可以使用 Jline 的`AttributedCharSequence`来显示花哨的 ANSI 文本。 + +下面是一个虚构的例子: + +``` +@Component +public class CustomPromptProvider implements PromptProvider { + + private ConnectionDetails connection; + + @Override + public AttributedString getPrompt() { + if (connection != null) { + return new AttributedString(connection.getHost() + ":>", + AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); + } + else { + return new AttributedString("server-unknown:>", + AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } + } + + @EventListener + public void handle(ConnectionUpdatedEvent event) { + this.connection = event.getConnectionDetails(); + } +} +``` + +#### 自定义命令行选项行为 + +Spring Shell 自带两个默认的 Spring boot`ApplicationRunners`: + +* `InteractiveShellApplicationRunner`引导 shell REPL。它设置了 Jline 基础架构,并最终调用`Shell.run()` + +* `ScriptShellApplicationRunner`查找以`@`开头的程序参数,假设这些参数是本地文件名,并尝试运行这些文件中包含的命令(具有与[脚本命令](#script-command)相同的语义),然后退出进程(通过有效禁用`InteractiveShellApplicationRunner`,见下文)。 + +如果此行为不适合你,只需提供一个(或多个)`ApplicationRunner`类型的 Bean 并可选地禁用标准类型的 Bean。你会想从`ScriptShellApplicationRunner`中获得灵感: + +``` +@Order(InteractiveShellApplicationRunner.PRECEDENCE - 100) // Runs before InteractiveShellApplicationRunner +public class ScriptShellApplicationRunner implements ApplicationRunner { + + @Override + public void run(ApplicationArguments args) throws Exception { + List scriptsToRun = args.getNonOptionArgs().stream() + .filter(s -> s.startsWith("@")) + .map(s -> new File(s.substring(1))) + .collect(Collectors.toList()); + + boolean batchEnabled = environment.getProperty(SPRING_SHELL_SCRIPT_ENABLED, boolean.class, true); + + if (!scriptsToRun.isEmpty() && batchEnabled) { + InteractiveShellApplicationRunner.disable(environment); + for (File file : scriptsToRun) { + try (Reader reader = new FileReader(file); + FileInputProvider inputProvider = new FileInputProvider(reader, parser)) { + shell.run(inputProvider); + } + } + } + } + +... +``` + +#### 自定义参数转换 + +从文本输入到实际方法参数的转换使用标准的 Spring [conversion](https://docs.spring.io/spring/docs/4.3.11.RELEASE/spring-framework-reference/htmlsingle/#core-convert)机制。 Spring Shell 安装一个新的`DefaultConversionService`(启用了内置转换器)并向其寄存器它在应用程序上下文中找到的类型`Converter`、`GenericConverter`或`ConverterFactory`的任何 Bean。 + +这意味着,对类型`Foo`的自定义对象进行自定义转换真的很容易:只需在上下文中安装`Converter` Bean。 + +``` +@ShellComponent +class ConversionCommands { + + @ShellMethod("Shows conversion using Spring converter") + public String conversionExample(DomainObject object) { + return object.getClass(); + } + +} + +class DomainObject { + private final String value; + + DomainObject(String value) { + this.value = value; + } + + public String toString() { + return value; + } +} + +@Component +class CustomDomainConverter implements Converter { + + @Override + public DomainObject convert(String source) { + return new DomainObject(source); + } +} +``` + +| |注意你的字符串表示

就像上面的例子一样,如果你能让
你的`toString()`实现返回创建对象实例时所用的东西的反方向,这可能是个好主意。这是因为当值
验证失败时, Spring shell 打印

```
The following constraints were not met:
--arg : (You passed '')
```

查看[验证命令参数](#validating-command-arguments)获取更多信息。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果你想进一步自定义`ConversionService`,你可以

* 在你的代码中插入默认的并以某种方式对其进行操作

* 将其完全覆盖到你自己的(自定义转换器将需要手动注册)。
Spring shell 使用的 ConversionService 需要[qualified](https://docs.spring.io/spring/docs/4.3.12.RELEASE/spring-framework-reference/htmlsingle/#beans-autowired-annotation-qualifiers)as`"spring-shell"`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| \ No newline at end of file diff --git a/docs/spring-statemachine/spring-statemachine.md b/docs/spring-statemachine/spring-statemachine.md new file mode 100644 index 0000000..5f13e4f --- /dev/null +++ b/docs/spring-statemachine/spring-statemachine.md @@ -0,0 +1,8100 @@ +# Spring StateMachine 文档 + +## [](#preface)序言 + +状态机的概念很可能比这个引用文档的任何读者都要古老,而且肯定比 Java 语言本身还要古老。对有限自动机的描述可以追溯到 1943 年,当时 Warren McCulloch 先生和 Walter Pitts 先生写了一篇关于它的论文。后来,George H.Mealy 在 1955 年提出了一个状态机概念(称为“Mealy Machine”)。一年后的 1956 年,Edward F.Moore 发表了另一篇论文,他在论文中描述了所谓的“摩尔机器”。如果你曾经读过任何有关状态机的东西,那么 Mealy 和 Moore 这两个名字应该是在某个时候出现的。 + +本参考文档包含以下部分: + +[导言](#introduction)包含此参考文档的介绍。 + +[Using Spring Statemachine](#statemachine)描述了 Spring statemachine 的用法。 + +[状态机示例](#statemachine-examples)包含更详细的状态机示例。 + +[FAQ](#statemachine-faq)包含常见问题。 + +[Appendices](#appendices)包含有关已用材料和状态机的通用信息。 + +# [](#introduction)引言 + +Spring StateMachine 是一种允许应用程序开发人员在 Spring 应用程序中使用传统状态机概念的框架。SSM 提供以下功能: + +* 易于使用的平面(一级)状态机,用于简单的用例。 + +* 分层状态机结构,以简化复杂的状态配置. + +* 状态机区域提供甚至更复杂的状态配置。 + +* 触发器、转换、保护和动作的使用。 + +* 类型安全配置适配器。 + +* 状态机事件监听器。 + +* Spring 将 bean 与状态机相关联的 IOC 集成。 + +在继续之前,我们建议先查看一下附录[Glossary](#glossary)和[状态机速成课程](#crashcourse),以了解状态机是什么。文档的其余部分希望你熟悉状态机的概念。 + +## [](#background)背景 + +状态机是强大的,因为它们的行为总是被保证是一致的,并且由于操作规则是在机器启动时用石头写成的,因此相对容易地进行调试。其思想是,你的应用程序现在处于并且可能存在于有限数量的状态中。然后会发生一些事情,将你的应用程序从一个州带到另一个州。状态机由触发器驱动,触发器基于事件或计时器。 + +在应用程序之外设计高级逻辑,然后以各种不同的方式与状态机交互,这要容易得多。你可以通过发送事件、监听状态机所做的工作或请求当前状态来与状态机交互。 + +传统上,当开发人员意识到代码库开始看起来像一盘满是意大利面条的盘子时,状态机就会被添加到现有的项目中。意大利面条代码看起来像是一个永无止境的,if,else 和 break 子句的层次结构,当事情开始看起来太复杂时,编译器可能应该要求开发人员回家。 + +## [](#usage-scenarios)使用场景 + +当出现以下情况时,项目是使用状态机的一个很好的候选者: + +* 你可以将应用程序或其结构的一部分表示为状态。 + +* 你希望将复杂的逻辑分割成更小的可管理的任务。 + +* 应用程序已经遇到了并发性问题,例如,某些事情是异步发生的。 + +你已经在尝试实现一个状态机,当你: + +* 使用布尔标志或枚举来建模情况。 + +* 具有仅对应用程序生命周期的某些部分具有意义的变量。 + +* 在一个 if-else 结构(或者更糟糕的是,多个这样的结构)中循环,检查是否设置了特定的标志或枚举,然后在你的标志和枚举的某些组合存在或不存在时,针对该做什么做出进一步的例外。 + +# [](#statemachine-getting-started)入门 + +如果你刚刚开始使用 Spring Statemachine,这是适合你的一节!这里,我们回答基本的“`what?`”、“`how?`”和“`why?`”问题。我们从温和地介绍 Spring 静态机械开始。然后,我们构建了我们的第一个 Spring Statemachine 应用程序,并讨论了一些核心原则。 + +## [](#system-requirement)系统需求 + +Spring StateMachine3.0.1 是用 JDK8(所有工件都具有 JDK7 兼容性)和 Spring Framework5.3.8 构建和测试的。在其核心系统中,它不需要 Spring 框架之外的任何其他依赖关系。 + +其他可选部分(例如[使用分布状态](#sm-distributed))对 ZooKeeper 具有依赖性,而[状态机示例](#statemachine-examples)则对`spring-shell`和`spring-boot`具有依赖性,这会将其他依赖性拉出框架本身之外。此外,可选的安全和数据访问功能具有对 Spring 安全和 Spring 数据模块的依赖性。 + +## [](#modules)模块 + +下表描述了 Spring Statemachine 可用的模块。 + +| Module |说明| +|------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| `spring-statemachine-core` |Spring 机械的核心系统。| +|`spring-statemachine-recipes-common`|不需要依赖于核心
框架之外的常见菜谱。| +| `spring-statemachine-kryo` |Spring statemachine 的`Kryo`序列化器。| +| `spring-statemachine-data-common` |用于`Spring Data`的公共支持模块。| +| `spring-statemachine-data-jpa` |`Spring Data JPA`的支持模块。| +| `spring-statemachine-data-redis` |`Spring Data Redis`的支持模块。| +| `spring-statemachine-data-mongodb` |`Spring Data MongoDB`的支持模块。| +| `spring-statemachine-zookeeper` |分布式状态机的 ZooKeeper 集成。| +| `spring-statemachine-test` |支持状态机测试的模块.| +| `spring-statemachine-cluster` |支持 Spring 云集群的模块。
注意, Spring 云集群已被 Spring 集成所取代。| +| `spring-statemachine-uml` |使用 Eclipse Papyrus 进行 UI UML 建模的支持模块。| +|`spring-statemachine-autoconfigure` |支持 Spring 启动的模块。| +| `spring-statemachine-bom` |材料清单 POM。| +| `spring-statemachine-starter` |Spring 引导启动器。| + +## [](#using-gradle)使用 Gradle + +下面的清单显示了一个典型的`build.gradle`文件,该文件是通过在[https://start.spring.io](https://start.spring.io)处选择各种设置来创建的: + +``` +buildscript { + ext { + springBootVersion = '2.4.8' + } + repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +group = 'com.example' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +ext { + springStatemachineVersion = '3.0.1' +} + +dependencies { + compile('org.springframework.statemachine:spring-statemachine-starter') + testCompile('org.springframework.boot:spring-boot-starter-test') +} + +dependencyManagement { + imports { + mavenBom "org.springframework.statemachine:spring-statemachine-bom:${springStatemachineVersion}" + } +} +``` + +| |用要使用的版本替换`0.0.1-SNAPSHOT`。| +|---|--------------------------------------------------------| + +对于普通的项目结构,你可以使用以下命令构建该项目: + +``` +# ./gradlew clean build +``` + +预期的 Spring 引导打包的 FAT JAR 将是`build/libs/demo-0.0.1-SNAPSHOT.jar`。 + +| |对于
产品开发,不需要`libs-milestone`和`libs-snapshot`存储库。| +|---|----------------------------------------------------------------------------------------------------| + +## [](#using-maven)使用 Maven + +下面的示例显示了一个典型的`pom.xml`文件,该文件是通过在[https://start.spring.io](https://start.spring.io)处选择各种选项创建的: + +``` + + + 4.0.0 + + com.example + demo + 0.0.1-SNAPSHOT + jar + + gs-statemachine + Demo project for Spring Statemachine + + + org.springframework.boot + spring-boot-starter-parent + 2.4.8 + + + + + UTF-8 + UTF-8 + 1.8 + 3.0.1 + + + + + org.springframework.statemachine + spring-statemachine-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.statemachine + spring-statemachine-bom + ${spring-statemachine.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + +``` + +| |用要使用的版本替换`0.0.1-SNAPSHOT`。| +|---|--------------------------------------------------------| + +对于普通的项目结构,你可以使用以下命令构建该项目: + +``` +# mvn clean package +``` + +预期的 Spring 引导打包的 fat-jar 将是`target/demo-0.0.1-SNAPSHOT.jar`。 + +| |对于
产品开发,不需要`libs-milestone`和`libs-snapshot`存储库。| +|---|-----------------------------------------------------------------------------------------------------| + +## [](#developing-your-first-spring-statemachine-application)开发你的第一个 Spring Statemachine 应用程序 + +你可以从创建一个简单的 Spring boot`Application`类开始,该类实现`CommandLineRunner`。下面的示例展示了如何做到这一点: + +``` +@SpringBootApplication +public class Application implements CommandLineRunner { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +然后需要添加状态和事件,如下例所示: + +``` +public enum States { + SI, S1, S2 +} + +public enum Events { + E1, E2 +} +``` + +然后需要添加状态机配置,如下例所示: + +``` +@Configuration +@EnableStateMachine +public class StateMachineConfig + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .autoStartup(true) + .listener(listener()); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.SI) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.SI).target(States.S1).event(Events.E1) + .and() + .withExternal() + .source(States.S1).target(States.S2).event(Events.E2); + } + + @Bean + public StateMachineListener listener() { + return new StateMachineListenerAdapter() { + @Override + public void stateChanged(State from, State to) { + System.out.println("State change to " + to.getId()); + } + }; + } +} +``` + +然后需要实现`CommandLineRunner`和 autowire`StateMachine`。下面的示例展示了如何做到这一点: + +``` +@Autowired +private StateMachine stateMachine; + +@Override +public void run(String... args) throws Exception { + stateMachine.sendEvent(Events.E1); + stateMachine.sendEvent(Events.E2); +} +``` + +根据你是使用`Gradle`还是`Maven`构建应用程序,你可以分别使用`java -jar build/libs/gs-statemachine-0.1.0.jar`或`java -jar target/gs-statemachine-0.1.0.jar`来运行它。 + +这个命令的结果应该是正常的引导输出。但是,你还应该找到以下几行: + +``` +State change to SI +State change to S1 +State change to S2 +``` + +这些行表示你构造的机器正在从一种状态移动到另一种状态,正如它应该的那样。 + +# [](#whatsnew)最新更新 + +## [](#in-1-1)in1.1 + +Spring StateMachine1.1 专注于安全性和与 Web 应用程序的更好的互操作性。它包括以下内容: + +* 增加了对 Spring 安全性的全面支持。见[状态机安全](#sm-security)。 + +* 与“@withstatemachine”的上下文集成已大大增强。见[上下文整合](#sm-context)。 + +* `StateContext`现在是一级公民,允许你与状态机进行交互。参见[使用`StateContext`]。 + +* 围绕持久性的特性已经通过对 Redis 的内置支持得到了增强。见[使用 Redis](#sm-persist-redis)。 + +* 一个新的特性有助于持久化操作。参见[使用`StateMachinePersister`]。 + +* 配置模型类现在在一个公共 API 中。 + +* 基于计时器的事件的新功能。 + +* 新`Junction`伪态。见[连接状态](#statemachine-config-states-junction)。 + +* 新的出口点和入口点是假状态。见[出境点和入境点状态](#statemachine-config-states-exitentry)。 + +* 配置模型验证器。 + +* 新样品。见[Security](#statemachine-examples-security)和[活动服务](#statemachine-examples-eventservice)。 + +* 使用 Eclipse Papyrus 的 UI 建模支持。见[Eclipse 建模支持](#sm-papyrus)。 + +## [](#in-1-2)in1.2 + +Spring StateMachine1.2 侧重于通用增强、更好的 UML 支持以及与外部配置存储库的集成。它包括以下内容: + +* 支持 UML 子机。见[使用子机引用](#sm-papyrus-submachineref)。 + +* 将机器配置保留在外部存储库中的新的存储库抽象。见[存储库支持](#sm-repository)。 + +* 对国家行动的新支持。见[国家行动](#state-actions)。 + +* 新的转换错误动作概念。见[转换动作错误处理](#statemachine-config-transition-actions-errorhandling)。 + +* 新的动作错误概念。见[状态动作错误处理](#statemachine-config-state-actions-errorhandling)。 + +* Spring 启动支持的初步工作。见[Spring Boot Support](#sm-boot)。 + +* 支持跟踪和监视。见[监视状态机](#sm-monitoring)。 + +### [](#in-1-2-8)in1.2.8 + +Spring Statemachine1.2.8 所包含的功能比在点释放中通常看不到的多一点,但是这些变化不值得 Spring Statemachine1.3 的叉子。它包括以下内容: + +* JPA 实体类已经更改了表名。见[JPA](#sm-repository-config-jpa)。 + +* 一个新的样本。见[数据持续存在](#statemachine-examples-datapersist)。 + +* 用于持久性的新实体类。见[存储库持久性](#sm-repository-persistence)。 + +* 过渡冲突政策。见[配置公共设置](#statemachine-config-commonsettings) + +## [](#in-2-0)in2.0 + +Spring StateMachine2.0 关注于 Spring Boot2.x 支持。 + +### [](#in-2-0-0)in2.0.0 + +Spring Statemachine2.0.0 包括以下内容: + +* 监视和跟踪的格式已经更改。见[监测和追踪](#sm-boot-monitoring)。 + +* `spring-statemachine-boot`模块已重命名为`spring-statemachine-autoconfigure`。 + +## [](#in-3-0)in3.0 + +Spring Statemachine3.0.0 侧重于添加反应性支持。从`2.x`移动到`3.x`会引入一些突破性的变化,在[反应堆迁移指南](#appendix-reactormigrationguide)中有详细说明。 + +使用`3.0.x`,我们已经不推荐所有阻塞方法,这些方法将在未来的版本中的某个时刻被删除。 + +| |请仔细阅读附录[反应堆迁移指南](#appendix-reactormigrationguide),因为它将引导你完成向
迁移的过程,对于内部不处理的情况。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在这一点上,大多数文档已经被更改为展示反应性接口,而我们仍然保留一些注释,以供仍在使用旧的阻塞方法的用户使用。 + +# [](#statemachine)使用 Spring 安定 + +参考文档的这一部分解释了 Spring StateMachine 向任何基于 Spring 的应用程序提供的核心功能。 + +它包括以下主题: + +* [机器构型](#sm-config)描述了通用配置支持。 + +* [状态机 ID](#sm-machineid)描述了机器 ID 的使用。 + +* [国营机器工厂](#sm-factories)描述了通用状态机工厂支持。 + +* [使用延迟事件](#sm-deferevents)描述了延迟事件支持。 + +* [使用作用域](#sm-scopes)描述了范围支持。 + +* [使用动作](#sm-actions)描述了对操作的支持。 + +* [使用防护装置](#sm-guards)描述了防护支撑。 + +* [使用扩展状态](#sm-extendedstate)描述了扩展的状态支持。 + +* [使用`StateContext`]描述状态上下文支持。 + +* [触发转换](#sm-triggers)描述了触发器的使用。 + +* [监听状态机事件](#sm-listeners)描述了状态机侦听器的使用。 + +* [上下文整合](#sm-context)描述了通用的 Spring 应用程序上下文支持。 + +* [使用`StateMachineAccessor`]描述了对状态机内部访问器的支持。 + +* [使用`StateMachineInterceptor`]描述了状态机错误处理支持。 + +* [状态机安全](#sm-security)描述了状态机的安全支持。 + +* [状态机错误处理](#sm-error-handling)描述了状态机拦截器的支持。 + +* [状态机服务](#sm-service)描述状态机服务支持。 + +* [保持状态机](#sm-persist)描述状态机持久支持。 + +* [Spring Boot Support](#sm-boot)描述了 Spring 引导支持。 + +* [监视状态机](#sm-monitoring)描述了监视和转换支持。 + +* [使用分布状态](#sm-distributed)描述分布式状态机支持。 + +* [测试支持](#sm-test)描述了状态机测试支持。 + +* [Eclipse 建模支持](#sm-papyrus)描述了状态机 UML 建模支持。 + +* [存储库支持](#sm-repository)描述状态机存储库配置支持。 + +## [](#sm-config)机械构型 + +使用状态机时的常见任务之一是设计其运行时配置。本章重点讨论如何配置 Spring StateMachine,以及如何利用 Spring 的轻量级 IoC 容器来简化应用程序内部,使其更易于管理。 + +| |本节中的配置示例没有完成功能。也就是说,
总是需要同时定义状态和转换。
否则,状态机配置将是格式错误的。我们有
,只是通过将其他需要的部分
留出来,使代码片段不那么冗长。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#statemachine-config-annotations)使用`enable`注释 + +我们使用两个熟悉的 Spring *使能者*注释来简化配置:`@EnableStateMachine`和`@EnableStateMachineFactory`。当将这些注释放置在`@Configuration`类中时,将启用状态机所需的一些基本功能。 + +当需要配置来创建`StateMachine`实例时,可以使用`@EnableStateMachine`。通常,`@Configuration`类扩展了适配器(`EnumStateMachineConfigurerAdapter`或`StateMachineConfigurerAdapter`),它允许你覆盖配置回调方法。我们会自动检测你是否使用这些适配器类,并相应地修改运行时配置逻辑。 + +当需要配置来创建`StateMachineFactory`实例时,可以使用`@EnableStateMachineFactory`。 + +| |下面几节将展示这些方法的使用示例。| +|---|----------------------------------------------------| + +### [](#statemachine-config-states)配置状态 + +在本指南的后面,我们将介绍更复杂的配置示例,但我们首先从简单的内容开始。对于大多数简单的状态机,你可以使用`EnumStateMachineConfigurerAdapter`并定义可能的状态并选择初始和可选的结束状态。 + +``` +@Configuration +@EnableStateMachine +public class Config1Enums + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + +} +``` + +你还可以使用`StateMachineConfigurerAdapter`将字符串而不是枚举作为状态和事件,如下一个示例所示。大多数配置示例使用枚举,但通常来说,你可以交换字符串和枚举。 + +``` +@Configuration +@EnableStateMachine +public class Config1Strings + extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S1") + .end("SF") + .states(new HashSet(Arrays.asList("S1","S2","S3","S4"))); + } + +} +``` + +| |使用枚举带来了一组更安全的状态和事件类型,但
限制了编译时间的可能组合。字符串没有这个
限制,允许你使用更动态的方式来构建状态
机器配置,但不允许相同级别的安全。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#configuring-hierarchical-states)配置层次结构状态 + +可以通过使用多个`withStates()`调用来定义分层状态,其中可以使用`parent()`来指示这些特定状态是某些其他状态的子状态。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableStateMachine +public class Config2 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .state(States.S1) + .and() + .withStates() + .parent(States.S1) + .initial(States.S2) + .state(States.S2); + } + +} +``` + +### [](#configuring-regions)配置区域 + +没有特殊的配置方法来将一组状态标记为正交状态的一部分。简单地说,当同一个层次状态机有多个状态集,每个状态集都有一个初始状态时,就会创建正交状态。因为单个状态机只能有一个初始状态,所以多个初始状态意味着一个特定的状态必须有多个独立的区域。下面的示例展示了如何定义区域: + +``` +@Configuration +@EnableStateMachine +public class Config10 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .state(States2.S2) + .and() + .withStates() + .parent(States2.S2) + .initial(States2.S2I) + .state(States2.S21) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S2) + .initial(States2.S3I) + .state(States2.S31) + .end(States2.S3F); + } + +} +``` + +当持久化具有区域的机器时,或者通常依赖于任何功能来重置机器时,你可能需要为一个区域提供一个专用的 ID。默认情况下,此 ID 是生成的 UUID。如下例所示,`StateConfigurer`有一个名为`region(String id)`的方法,它允许你为区域设置 ID: + +``` +@Configuration +@EnableStateMachine +public class Config10RegionId + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .state(States2.S2) + .and() + .withStates() + .parent(States2.S2) + .region("R1") + .initial(States2.S2I) + .state(States2.S21) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S2) + .region("R2") + .initial(States2.S3I) + .state(States2.S31) + .end(States2.S3F); + } + +} +``` + +### [](#configuring-transitions)配置转换 + +我们支持三种不同类型的转换:`external`、`internal`和`local`。转换由信号(发送到状态机的事件)或计时器触发。下面的示例展示了如何定义所有三种类型的转换: + +``` +@Configuration +@EnableStateMachine +public class Config3 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1).target(States.S2) + .event(Events.E1) + .and() + .withInternal() + .source(States.S2) + .event(Events.E2) + .and() + .withLocal() + .source(States.S2).target(States.S3) + .event(Events.E3); + } + +} +``` + +### [](#configuring-guards)配置守卫 + +你可以使用保护来保护状态转换。你可以使用`Guard`接口来执行计算,其中方法可以访问`StateContext`。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableStateMachine +public class Config4 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1).target(States.S2) + .event(Events.E1) + .guard(guard()) + .and() + .withExternal() + .source(States.S2).target(States.S3) + .event(Events.E2) + .guardExpression("true"); + + } + + @Bean + public Guard guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return true; + } + }; + } + +} +``` + +在前面的示例中,我们使用了两种不同类型的保护配置。首先,我们创建了一个简单的`Guard`作为 Bean,并将其附加到状态`S1`和`S2`之间的转换。 + +其次,我们使用 SPEL 表达式作为保护,要求表达式必须返回`BOOLEAN`值。在幕后,这个基于表达式的保护是`SpelExpressionGuard`。我们将其附加到状态`S2`和`S3`之间的转换。两个后卫的估值总是`true`。 + +### [](#statemachine-config-actions)配置操作 + +你可以定义要用转换和状态执行的操作。动作总是作为源自触发器的转换的结果运行的。下面的示例展示了如何定义一个动作: + +``` +@Configuration +@EnableStateMachine +public class Config51 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1) + .target(States.S2) + .event(Events.E1) + .action(action()); + } + + @Bean + public Action action() { + return new Action() { + + @Override + public void execute(StateContext context) { + // do something + } + }; + } + +} +``` + +在前面的示例中,单个`Action`被定义为名为`action`的 Bean,并与从`S1`到`S2`的转换相关联。下面的示例展示了如何多次使用一个动作: + +``` +@Configuration +@EnableStateMachine +public class Config52 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1, action()) + .state(States.S1, action(), null) + .state(States.S2, null, action()) + .state(States.S2, action()) + .state(States.S3, action(), action()); + } + + @Bean + public Action action() { + return new Action() { + + @Override + public void execute(StateContext context) { + // do something + } + }; + } + +} +``` + +| |通常,你不会为不同的
阶段定义相同的`Action`实例,但是我们在这里这样做是为了避免在代码
片段中产生太多噪声。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------| + +在前面的示例中,单个`Action`由名为`action`的 Bean 定义并与状态相关联的`S1`、`S2`和`S3`。我们需要弄清楚这是怎么回事: + +* 我们为初始状态定义了一个动作,`S1`。 + +* 我们为 state`S1`定义了一个进入动作,并将退出动作保留为空。 + +* 我们为 state`S2`定义了一个退出操作,并将该进入操作保留为空。 + +* 我们定义了状态`S2`的单个状态动作。 + +* 我们定义了状态`S3`的进入和退出操作。 + +* 注意,状态`S1`与`initial()`和`state()`函数一起使用两次。只有当你想要用初始状态定义进入或退出操作时,你才需要这样做。 + +| |用`initial()`函数定义动作,只在启动状态机或子状态时运行特定的
动作。此操作
是仅运行一次的初始化操作。如果状态机返回
并在初始和非初始状态之间向前转换,则将运行定义有
的`state()`的操作。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#state-actions)状态动作 + +与进入和退出操作相比,运行状态操作是不同的,因为执行发生在输入状态之后,如果在特定操作完成之前发生了状态退出,则可以取消执行。 + +通过订阅反应堆的默认并行调度器,使用正常的无功流执行状态操作。这意味着,无论你在操作中做什么,都需要能够捕获`InterruptedException`,或者更一般地,定期检查`Thread`是否被中断。 + +下面的示例展示了使用默认`IMMEDIATE_CANCEL`的典型配置,当运行中的任务的状态完成时,该配置将立即取消该任务: + +``` +@Configuration +@EnableStateMachine +static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.IMMEDIATE_CANCEL); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2", context -> {}) + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3") + .event("E2"); + } +} +``` + +你可以将策略设置为`TIMEOUT_CANCEL`,并为每台机器设置一个全局超时。这将更改状态行为,以便在请求取消之前等待动作完成。下面的示例展示了如何做到这一点: + +``` +@Override +public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL) + .stateDoActionPolicyTimeout(10, TimeUnit.SECONDS); +} +``` + +如果`Event`直接将机器带入一种状态,以便特定操作可以使用事件头,则还可以使用专用事件头来设置特定的超时(在`millis`中定义)。为此,你可以使用保留的标头值`StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT`。下面的示例展示了如何做到这一点: + +``` +@Autowired +StateMachine stateMachine; + +void sendEventUsingTimeout() { + stateMachine + .sendEvent(Mono.just(MessageBuilder + .withPayload("E1") + .setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 5000) + .build())) + .subscribe(); + +} +``` + +#### [](#statemachine-config-transition-actions-errorhandling)转换动作错误处理 + +你总是可以手动捕获异常。但是,对于为转换定义的操作,你可以定义一个错误操作,如果出现异常,则调用该操作。然后,从传递给该操作的`StateContext`中可以获得异常。下面的示例展示了如何创建处理异常的状态: + +``` +@Configuration +@EnableStateMachine +public class Config53 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1) + .target(States.S2) + .event(Events.E1) + .action(action(), errorAction()); + } + + @Bean + public Action action() { + return new Action() { + + @Override + public void execute(StateContext context) { + throw new RuntimeException("MyError"); + } + }; + } + + @Bean + public Action errorAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + // RuntimeException("MyError") added to context + Exception exception = context.getException(); + exception.getMessage(); + } + }; + } + +} +``` + +如果需要,你可以手动为每个动作创建类似的逻辑。下面的示例展示了如何做到这一点: + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1) + .target(States.S2) + .event(Events.E1) + .action(Actions.errorCallingAction(action(), errorAction())); +} +``` + +#### [](#statemachine-config-state-actions-errorhandling)状态动作错误处理 + +与处理状态转换中的错误的逻辑类似的逻辑也可用于状态的进入和状态的退出。 + +对于这些情况,`StateConfigurer`具有称为`stateEntry`、`stateDo`和`stateExit`的方法。这些方法定义了一个`error`动作和一个正常(非错误)`action`动作。下面的示例展示了如何使用这三种方法: + +``` +@Configuration +@EnableStateMachine +public class Config55 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .stateEntry(States.S2, action(), errorAction()) + .stateDo(States.S2, action(), errorAction()) + .stateExit(States.S2, action(), errorAction()) + .state(States.S3); + } + + @Bean + public Action action() { + return new Action() { + + @Override + public void execute(StateContext context) { + throw new RuntimeException("MyError"); + } + }; + } + + @Bean + public Action errorAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + // RuntimeException("MyError") added to context + Exception exception = context.getException(); + exception.getMessage(); + } + }; + } +} +``` + +### [](#configuring-pseudo-states)配置伪状态 + +*伪态*配置通常是通过配置状态和转换来完成的。伪状态作为状态自动添加到状态机中。 + +#### [](#initial-state)初始状态 + +可以使用`initial()`方法将特定状态标记为初始状态。例如,这个初始操作对于初始化扩展状态变量是很好的。下面的示例展示了如何使用`initial()`方法: + +``` +@Configuration +@EnableStateMachine +public class Config11 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1, initialAction()) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + + @Bean + public Action initialAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + // do something initially + } + }; + } + +} +``` + +#### [](#terminate-state)终止状态 + +可以使用`end()`方法将特定状态标记为结束状态。对于每个子机器或区域,你最多可以执行一次。下面的示例展示了如何使用`end()`方法: + +``` +@Configuration +@EnableStateMachine +public class Config1Enums + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + +} +``` + +#### [](#state-history)国家历史 + +你可以为每个单独的状态机定义一次状态历史。你需要选择其状态标识符并设置`History.SHALLOW`或`History.DEEP`。下面的示例使用`History.SHALLOW`: + +``` +@Configuration +@EnableStateMachine +public class Config12 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States3.S1) + .state(States3.S2) + .and() + .withStates() + .parent(States3.S2) + .initial(States3.S2I) + .state(States3.S21) + .state(States3.S22) + .history(States3.SH, History.SHALLOW); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withHistory() + .source(States3.SH) + .target(States3.S22); + } + +} +``` + +此外,正如前面的示例所示,你可以在同一台机器中定义从历史状态到状态顶点的缺省转换。例如,如果从未输入过机器,那么这种转换将作为默认情况发生——因此,没有可用的历史记录。如果未定义缺省状态转换,则完成对区域的正常输入。如果机器的历史记录是最终状态,也可以使用此默认转换。 + +#### [](#choice-state)选择状态 + +需要在状态和转换中定义选择才能正常工作。可以使用`choice()`方法将特定状态标记为选择状态。当为此选择配置转换时,此状态需要匹配源状态。 + +你可以通过使用`withChoice()`来配置转换,其中你定义了源状态和`first/then/last`结构,这等同于正常的`if/elseif/else`。使用`first`和`then`,你可以指定一个保护,就像使用带有`if/elseif`子句的条件一样。 + +转换需要能够存在,因此你必须确保使用`last`。否则,该配置是不成型的。下面的示例展示了如何定义选择状态: + +``` +@Configuration +@EnableStateMachine +public class Config13 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.SI) + .choice(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withChoice() + .source(States.S1) + .first(States.S2, s2Guard()) + .then(States.S3, s3Guard()) + .last(States.S4); + } + + @Bean + public Guard s2Guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return false; + } + }; + } + + @Bean + public Guard s3Guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return true; + } + }; + } + +} +``` + +操作可以与选择伪状态的传入和传出转换一起运行。正如下面的示例所示,定义了一个虚拟 lambda 操作,该操作将导致进入选择状态,并且为一个传出转换(其中还定义了一个错误操作)定义了一个类似的虚拟 lambda 操作: + +``` +@Configuration +@EnableStateMachine +public class Config23 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.SI) + .choice(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.SI) + .action(c -> { + // action with SI-S1 + }) + .target(States.S1) + .and() + .withChoice() + .source(States.S1) + .first(States.S2, c -> { + return true; + }) + .last(States.S3, c -> { + // action with S1-S3 + }, c -> { + // error callback for action S1-S3 + }); + } +} +``` + +| |具有相同 API 格式的连接意味着动作可以被定义
类似。| +|---|---------------------------------------------------------------------------| + +#### [](#statemachine-config-states-junction)结态 + +你需要在状态和转换两方面定义一个连接,以使其正常工作。可以使用`junction()`方法将特定状态标记为选择状态。当为此选择配置转换时,此状态需要与源状态匹配。 + +你可以通过使用`withJunction()`来配置转换,其中你定义了源状态和`first/then/last`结构(相当于正常的`if/elseif/else`)。使用`first`和`then`,你可以指定一个保护,就像使用带有`if/elseif`子句的条件一样。 + +转换需要能够存在,因此你必须确保使用`last`。否则,该配置是不成型的。下面的示例使用了一个结点: + +``` +@Configuration +@EnableStateMachine +public class Config20 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.SI) + .junction(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withJunction() + .source(States.S1) + .first(States.S2, s2Guard()) + .then(States.S3, s3Guard()) + .last(States.S4); + } + + @Bean + public Guard s2Guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return false; + } + }; + } + + @Bean + public Guard s3Guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return true; + } + }; + } + +} +``` + +| |选择和连接之间的区别纯粹是学术性的,因为两者都是
用`first/then/last`结构实现的。然而,在理论上,基于 UML 建模的
,`choice`只允许一个传入转换,而`junction`允许多个传入转换。在代码级别上,
功能几乎相同。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#fork-state)fork state + +你必须在状态和转换中定义一个 fork,才能使其正常工作。可以使用`fork()`方法将特定状态标记为选择状态。当为此 fork 配置转换时,此状态需要匹配源状态。 + +目标状态需要是一个区域中的超级状态或直接状态。使用超级状态作为目标,所有区域都会进入初始状态。以单个国家为目标可以更好地控制进入地区。下面的示例使用分叉: + +``` +@Configuration +@EnableStateMachine +public class Config14 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .fork(States2.S2) + .state(States2.S3) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S2I) + .state(States2.S21) + .state(States2.S22) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S3I) + .state(States2.S31) + .state(States2.S32) + .end(States2.S3F); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withFork() + .source(States2.S2) + .target(States2.S22) + .target(States2.S32); + } + +} +``` + +#### [](#join-state)加入状态 + +你必须在状态和转换中定义一个连接,才能使其正常工作。你可以使用`join()`方法将附节状态标记为选择状态。在转换配置中,此状态不需要匹配源状态或目标状态。 + +你可以选择一个目标状态,当所有源状态都已加入时,转换将在其中进行。如果使用状态托管区域作为源,则区域的结束状态被用作连接。否则,你可以从一个地区中选择任何一个州。以下 Exmaple 使用连接: + +``` +@Configuration +@EnableStateMachine +public class Config15 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .state(States2.S3) + .join(States2.S4) + .state(States2.S5) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S2I) + .state(States2.S21) + .state(States2.S22) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S3I) + .state(States2.S31) + .state(States2.S32) + .end(States2.S3F); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withJoin() + .source(States2.S2F) + .source(States2.S3F) + .target(States2.S4) + .and() + .withExternal() + .source(States2.S4) + .target(States2.S5); + } +} +``` + +你还可以让多个转换起源于一个连接状态。在这种情况下,我们建议你使用保护并定义你的保护,以便在任何给定的时间只有一个保护的值`TRUE`。否则,过渡行为是不可预测的。下面的示例显示了这一点,在该示例中,保护程序检查扩展状态是否具有变量: + +``` +@Configuration +@EnableStateMachine +public class Config22 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .state(States2.S3) + .join(States2.S4) + .state(States2.S5) + .end(States2.SF) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S2I) + .state(States2.S21) + .state(States2.S22) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S3I) + .state(States2.S31) + .state(States2.S32) + .end(States2.S3F); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withJoin() + .source(States2.S2F) + .source(States2.S3F) + .target(States2.S4) + .and() + .withExternal() + .source(States2.S4) + .target(States2.S5) + .guardExpression("!extendedState.variables.isEmpty()") + .and() + .withExternal() + .source(States2.S4) + .target(States2.SF) + .guardExpression("extendedState.variables.isEmpty()"); + } +} +``` + +#### [](#statemachine-config-states-exitentry)出入点状态 + +你可以使用出入点和进入点来执行更多的控制出入点和进入点。下面的示例使用`withEntry`和`withExit`方法来定义入口点: + +``` +@Configuration +@EnableStateMachine +static class Config21 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .state("S3") + .and() + .withStates() + .parent("S2") + .initial("S21") + .entry("S2ENTRY") + .exit("S2EXIT") + .state("S22"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S1").target("S2") + .event("E1") + .and() + .withExternal() + .source("S1").target("S2ENTRY") + .event("ENTRY") + .and() + .withExternal() + .source("S22").target("S2EXIT") + .event("EXIT") + .and() + .withEntry() + .source("S2ENTRY").target("S22") + .and() + .withExit() + .source("S2EXIT").target("S3"); + } +} +``` + +如前面所示,你需要将特定状态标记为`exit`和`entry`状态。然后创建一个正常的转换到这些状态,并指定`withExit()`和`withEntry()`,这些状态分别在其中退出和进入。 + +### [](#statemachine-config-commonsettings)配置公共设置 + +可以使用`ConfigurationConfigurer`设置公共状态机配置的一部分。有了它,你可以为状态机设置`BeanFactory`和自动启动标志。它还允许你注册`StateMachineListener`实例,配置转换冲突策略和区域执行策略。下面的示例展示了如何使用`ConfigurationConfigurer`: + +``` +@Configuration +@EnableStateMachine +public class Config17 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .autoStartup(true) + .machineId("myMachineId") + .beanFactory(new StaticListableBeanFactory()) + .listener(new StateMachineListenerAdapter()) + .transitionConflictPolicy(TransitionConflictPolicy.CHILD) + .regionExecutionPolicy(RegionExecutionPolicy.PARALLEL); + } +} +``` + +默认情况下,状态机`autoStartup`标志是禁用的,因为所有处理子状态的实例都由状态机本身控制,不能自动启动。另外,机器是否应该自动启动,由用户自己决定要安全得多。此标志仅控制顶级状态机的自动启动。 + +在配置类中设置`machineId`只是为了方便你想要或需要在配置类中设置`machineId`。 + +注册`StateMachineListener`实例在一定程度上也是为了方便,但如果你希望在状态机生命周期期间捕获回调,例如获得状态机的启动和停止事件的通知,则需要注册。请注意,如果启用`autoStartup`,则无法侦听状态机的启动事件,除非你在配置阶段注册了侦听器。 + +当可以选择多个转换路径时,可以使用`transitionConflictPolicy`。一个常见的用例是,当机器包含从子状态和父状态引出的匿名转换,并且你想要定义一个策略来选择其中一个。这是机器实例中的全局设置,默认设置为`CHILD`。 + +你可以使用`withDistributed()`来配置`DistributedStateMachine`。它允许你设置`StateMachineEnsemble`,它(如果存在的话)自动用`DistributedStateMachine`包装任何创建的`StateMachine`,并启用分布式模式。下面的示例展示了如何使用它: + +``` +@Configuration +@EnableStateMachine +public class Config18 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withDistributed() + .ensemble(stateMachineEnsemble()); + } + + @Bean + public StateMachineEnsemble stateMachineEnsemble() + throws Exception { + // naturally not null but should return ensemble instance + return null; + } +} +``` + +有关分布状态的更多信息,请参见[使用分布状态](#sm-distributed)。 + +`StateMachineModelVerifier`接口在内部用于对状态机的结构进行一些明智的检查。它的目的是尽早快速失败,而不是让常见的配置错误进入状态机。默认情况下,将自动启用验证器,并使用`DefaultStateMachineModelVerifier`实现。 + +使用`withVerifier()`,如果需要,可以禁用验证器或设置自定义验证器。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableStateMachine +public class Config19 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withVerifier() + .enabled(true) + .verifier(verifier()); + } + + @Bean + public StateMachineModelVerifier verifier() { + return new StateMachineModelVerifier() { + + @Override + public void verify(StateMachineModel model) { + // throw exception indicating malformed model + } + }; + } +} +``` + +有关配置模型的更多信息,请参见[statemachine 配置模型](#devdocs-configmodel)。 + +| |`withSecurity`、`withMonitoring`和`withPersistence`配置方法
分别在[状态机安全](#sm-security)、[监视状态机](#sm-monitoring)和[using`StateMachineRuntimePersister`](#sm-persistue-statemachinerunitemepersister)中有记载。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#configuring-model)配置模型 + +`StateMachineModelFactory`是一个钩子,它允许你在不使用手动配置的情况下配置一个 Statemachine 模型。本质上,它是一种集成到配置模型中的第三方集成。你可以使用`StateMachineModelConfigurer`将`StateMachineModelFactory`连接到配置模型中。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableStateMachine +public static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new CustomStateMachineModelFactory(); + } +} +``` + +Follwoing 示例使用`CustomStateMachineModelFactory`来定义两个状态(`S1`和`S2`)和这些状态之间的事件(`E1`): + +``` +public static class CustomStateMachineModelFactory implements StateMachineModelFactory { + + @Override + public StateMachineModel build() { + ConfigurationData configurationData = new ConfigurationData<>(); + Collection> stateData = new ArrayList<>(); + stateData.add(new StateData("S1", true)); + stateData.add(new StateData("S2")); + StatesData statesData = new StatesData<>(stateData); + Collection> transitionData = new ArrayList<>(); + transitionData.add(new TransitionData("S1", "S2", "E1")); + TransitionsData transitionsData = new TransitionsData<>(transitionData); + StateMachineModel stateMachineModel = new DefaultStateMachineModel(configurationData, + statesData, transitionsData); + return stateMachineModel; + } + + @Override + public StateMachineModel build(String machineId) { + return build(); + } +} +``` + +| |定义一个定制模型通常不是人们想要的,
尽管这是可能的。然而,它是允许
外部访问此配置模型的核心概念。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以在[Eclipse 建模支持](#sm-papyrus)中找到使用此模型工厂集成的示例。你可以在[开发人员文档](#devdocs)中找到有关自定义模型集成的更多通用信息。 + +### [](#statemachine-config-thingstoremember)要记住的事情 + +在从配置中定义操作、保护或任何其他引用时,需要记住 Spring Framework 是如何与 bean 一起工作的。在下一个示例中,我们定义了一个正常配置,其状态`S1`和`S2`之间有四个转换。所有转换都由`guard1`或`guard2`保护。你必须确保`guard1`是作为真实 Bean 创建的,因为它是用`@Bean`注释的,而`guard2`不是。 + +这意味着事件`E3`将得到`guard2`条件为`TRUE`,而`E4`将得到`guard2`条件为`FALSE`,因为这些条件来自对那些函数的普通方法调用。 + +然而,因为`guard1`被定义为`@Bean`,所以它由 Spring 框架代理。因此,对其方法的额外调用只会导致该实例的一个实例化。Event`E1`将首先获得带有条件`TRUE`的代理实例,而 Event`E2`将获得带有`TRUE`条件的相同实例,当方法调用被定义为`FALSE`时。这不是 Spring 特定于状态机的行为。相反,它是 Spring Framework 如何与 bean 一起工作的。下面的示例展示了这种安排的工作原理: + +``` +@Configuration +@EnableStateMachine +public class Config1 + extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S1").target("S2").event("E1").guard(guard1(true)) + .and() + .withExternal() + .source("S1").target("S2").event("E2").guard(guard1(false)) + .and() + .withExternal() + .source("S1").target("S2").event("E3").guard(guard2(true)) + .and() + .withExternal() + .source("S1").target("S2").event("E4").guard(guard2(false)); + } + + @Bean + public Guard guard1(final boolean value) { + return new Guard() { + @Override + public boolean evaluate(StateContext context) { + return value; + } + }; + } + + public Guard guard2(final boolean value) { + return new Guard() { + @Override + public boolean evaluate(StateContext context) { + return value; + } + }; + } +} +``` + +## [](#sm-machineid)状态机 ID + +在方法中,各种类和接口使用`machineId`作为变量或参数。本节将更仔细地了解`machineId`与正常的机器操作和实例化之间的关系。 + +在运行时期间,`machineId`实际上没有任何大的操作作用,除了区分机器之间的区别——例如,在跟踪日志或进行更深入的调试时。如果没有一种简单的方法来识别这些实例,那么拥有大量不同的机器实例很快就会让开发人员迷失在翻译过程中。因此,我们添加了设置`machineId`的选项。 + +### [](#using-enablestatemachine)使用`@EnableStateMachine` + +在 Java 配置中将`machineId`设置为`mymachine`,然后公开日志的该值。同样的`machineId`也可以从`StateMachine.getId()`方法获得。下面的示例使用`machineId`方法: + +``` +@Override +public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .machineId("mymachine"); +} +``` + +下面的日志输出示例显示了`mymachine`ID: + +``` +11:23:54,509 INFO main support.LifecycleObjectSupport [main] - +started S2 S1 / S1 / uuid=8fe53d34-8c85-49fd-a6ba-773da15fcaf1 / id=mymachine +``` + +| |手动构建器(参见[通过构建器的状态机](#state-machine-via-builder))使用相同的配置
接口,这意味着行为是等效的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#using-enablestatemachinefactory)使用`@EnableStateMachineFactory` + +如果使用`StateMachineFactory`并使用该 ID 请求一台新机器,则可以看到相同的`machineId`正在被配置,如下例所示: + +``` +StateMachineFactory factory = context.getBean(StateMachineFactory.class); +StateMachine machine = factory.getStateMachine("mymachine"); +``` + +### [](#using-statemachinemodelfactory)使用`StateMachineModelFactory` + +在幕后,所有机器配置首先被转换为`StateMachineModel`,这样`StateMachineFactory`就不需要知道配置的起源,因为机器可以从 Java 配置、UML 或存储库构建。如果你想发疯,也可以使用自定义`StateMachineModel`,这是定义配置的最低级别。 + +所有这些都与`machineId`有什么关系?`StateMachineModelFactory`还具有具有以下签名的方法:`StateMachineModel build(String machineId)`其`StateMachineModelFactory`实现可以选择使用。 + +`RepositoryStateMachineModelFactory`(参见[存储库支持](#sm-repository))使用`machineId`来支持持久存储中的不同配置 +通过 Spring 数据存储库接口进行存储。例如,`StateRepository`和`TransitionRepository`都有一个方法(`list +FindbyMachineID`), to build different states and transitions by a `MachineID`. With`RepositorystateMachineModelFactory`, if `MachineID` 被用作空或空,它默认为存储库配置(在备份-持久性模型中),而没有已知的机器 ID。 + +| |目前,`UmlStateMachineModelFactory`不区分
不同的机器 ID,因为 UML 源总是来自相同的
文件。这种情况可能会在未来的版本中发生变化。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#sm-factories)状态机工厂 + +在某些情况下,需要动态地创建状态机,而不是在编译时定义静态配置。例如,如果有一些定制组件使用它们自己的状态机,并且这些组件是动态创建的,那么就不可能在应用程序启动期间构建静态状态机。在内部,状态机总是通过工厂接口构建的。这就给了你一个以编程方式使用此功能的选项。状态机工厂的配置与本文档中各种示例中所示的配置完全相同,其中状态机配置是硬编码的。 + +### [](#factory-through-an-adapter)通过适配器出厂 + +实际上,通过使用`@EnableStateMachine`创建状态机是通过工厂工作的,所以`@EnableStateMachineFactory`只是通过其接口公开了该工厂。下面的示例使用`@EnableStateMachineFactory`: + +``` +@Configuration +@EnableStateMachineFactory +public class Config6 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + +} +``` + +既然你已经使用`@EnableStateMachineFactory`创建了一个工厂,而不是一个状态机 Bean,那么你可以插入它并使用它来请求新的状态机。下面的示例展示了如何做到这一点: + +``` +public class Bean3 { + + @Autowired + StateMachineFactory factory; + + void method() { + StateMachine stateMachine = factory.getStateMachine(); + stateMachine.startReactively().subscribe(); + } +} +``` + +#### [](#adapter-factory-limitations)适配器出厂限制 + +Factory 当前的限制是,它与状态机关联的所有操作和保护都共享同一个实例。这意味着,根据你的操作和保护,你需要专门处理由不同状态机调用相同 Bean 的情况。这一限制将在未来的版本中得到解决。 + +### [](#state-machine-via-builder)通过构建器的状态机 + +使用适配器(如上面所示)有其通过 Spring `@Configuration`类和应用程序上下文工作的要求所施加的限制。虽然这是一个配置状态机的非常清晰的模型,但它限制了编译时的配置,而这并不总是用户想要做的。如果需要构建更多的动态状态机,那么可以使用一个简单的构建器模式来构建类似的实例。通过使用字符串作为状态和事件,你可以使用此 Builder 模式在 Spring 应用程序上下文之外构建完全动态的状态机。下面的示例展示了如何做到这一点: + +``` +StateMachine buildMachine1() throws Exception { + Builder builder = StateMachineBuilder.builder(); + builder.configureStates() + .withStates() + .initial("S1") + .end("SF") + .states(new HashSet(Arrays.asList("S1","S2","S3","S4"))); + return builder.build(); +} +``` + +构建器在幕后使用与`@Configuration`模型用于适配器类相同的配置接口。同样的模型也适用于通过构建器的方法来配置转换、状态和公共配置。这意味着,无论使用普通的`EnumStateMachineConfigurerAdapter`还是`StateMachineConfigurerAdapter`,都可以通过构建器动态地使用。 + +| |目前,`builder.configureStates()`、`builder.configureTransitions()`、
和`builder.configureConfiguration()`接口方法不能被
链接在一起,这意味着生成器方法需要单独调用。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例使用构建器设置了许多选项: + +``` +StateMachine buildMachine2() throws Exception { + Builder builder = StateMachineBuilder.builder(); + builder.configureConfiguration() + .withConfiguration() + .autoStartup(false) + .beanFactory(null) + .listener(null); + return builder.build(); +} +``` + +你需要了解何时需要将公共配置与从构建器实例化的机器一起使用。你可以使用从`withConfiguration()`返回的配置器来设置`autoStart`和`BeanFactory`。你也可以使用一个来注册`StateMachineListener`。如果通过使用`@Bean`将从构建器返回的`StateMachine`实例注册为 Bean,则`BeanFactory`将自动附加。如果在 Spring 应用程序上下文之外使用实例,则必须使用这些方法来设置所需的设施。 + +## [](#sm-deferevents)使用延迟事件 + +当发送事件时,它可能会触发`EventTrigger`,如果状态机处于成功计算触发器的状态,那么这可能会导致转换发生。通常情况下,这可能会导致一种情况,即一个事件不被接受,并被放弃。但是,你可能希望将此事件推迟到状态机进入另一种状态。在这种情况下,你可以接受该事件。换句话说,一项活动是在一个不方便的时间到来的。 + +Spring Statemachine 提供了一种机制,用于将事件延迟到以后的处理中。每个州都可以有一个延迟事件的列表。如果当前状态的“延迟事件”列表中的事件发生,则该事件将被保存(延迟)以备将来处理,直到输入一个未在其“延迟事件”列表中列出该事件的状态。当输入这样的状态时,状态机会自动召回所有已保存的不再延迟的事件,然后消耗或丢弃这些事件。超状态有可能在由子状态延迟的事件上定义转换。遵循相同的层次状态机概念,子态优先于超态,事件被推迟,并且超态的转换不运行。对于正交区域,其中一个正交区域延迟一个事件,而另一个接受该事件,该接受具有优先权,并且该事件被消耗而不是延迟。 + +事件延迟最明显的用例是,当一个事件导致转换到特定状态,然后状态机返回到其原始状态时,第二个事件将导致相同的转换。下面的示例展示了这种情况: + +``` +@Configuration +@EnableStateMachine +static class Config5 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("READY") + .state("DEPLOYPREPARE", "DEPLOY") + .state("DEPLOYEXECUTE", "DEPLOY"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("READY").target("DEPLOYPREPARE") + .event("DEPLOY") + .and() + .withExternal() + .source("DEPLOYPREPARE").target("DEPLOYEXECUTE") + .and() + .withExternal() + .source("DEPLOYEXECUTE").target("READY"); + } +} +``` + +在前面的示例中,状态机的状态为`READY`,这表示机器已准备好处理将其带到`DEPLOY`状态的事件,而实际部署将在该状态中进行。运行部署操作后,机器将返回到`READY`状态。如果机器使用同步执行器,以`READY`状态发送多个事件不会造成任何麻烦,因为事件发送会在事件调用之间阻塞。但是,如果执行器使用线程,其他事件可能会丢失,因为机器不再处于可以处理事件的状态。因此,推迟这些事件中的一些可以让机器保留它们。下面的示例展示了如何配置这样的安排: + +``` +@Configuration +@EnableStateMachine +static class Config6 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("READY") + .state("DEPLOY", "DEPLOY") + .state("DONE") + .and() + .withStates() + .parent("DEPLOY") + .initial("DEPLOYPREPARE") + .state("DEPLOYPREPARE", "DONE") + .state("DEPLOYEXECUTE"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("READY").target("DEPLOY") + .event("DEPLOY") + .and() + .withExternal() + .source("DEPLOYPREPARE").target("DEPLOYEXECUTE") + .and() + .withExternal() + .source("DEPLOYEXECUTE").target("READY") + .and() + .withExternal() + .source("READY").target("DONE") + .event("DONE") + .and() + .withExternal() + .source("DEPLOY").target("DONE") + .event("DONE"); + } +} +``` + +在前面的示例中,状态机使用嵌套状态而不是平坦状态模型,因此`DEPLOY`事件可以在子状态中直接延迟。它还显示了在子状态中推迟`DONE`事件的概念,如果发送`DONE`事件时状态机恰好处于`DEPLOYPREPARE`状态,则该状态将覆盖`DEPLOY`和`DONE`状态之间的匿名转换。在`DEPLOYEXECUTE`状态下,当`DONE`事件没有延迟时,此事件将在超级状态下处理。 + +## [](#sm-scopes)使用作用域 + +状态机中对作用域的支持非常有限,但是你可以通过使用普通 Spring `@Scope`注释来启用`session`作用域,方法有以下两种: + +* 如果状态机是通过使用构建器手动构建的,并以`@Bean`的形式返回到上下文中。 + +* 通过配置适配器。 + +这两个参数都需要`@Scope`才能存在,将`scopeName`设置为`session`,将`proxyMode`设置为`ScopedProxyMode.TARGET_CLASS`。以下示例展示了这两种用例: + +``` +@Configuration +public class Config3 { + + @Bean + @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) + StateMachine stateMachine() throws Exception { + Builder builder = StateMachineBuilder.builder(); + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true); + builder.configureStates() + .withStates() + .initial("S1") + .state("S2"); + builder.configureTransitions() + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + StateMachine stateMachine = builder.build(); + return stateMachine; + } + +} +``` + +``` +@Configuration +@EnableStateMachine +@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) +public static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + } + +} +``` + +提示:有关如何使用会话范围定义,请参见[Scope](#statemachine-examples-scope)。 + +一旦将状态机的作用域设为`session`,则将其自动连线到`@Controller`中,每会话提供一个新的状态机实例。当`HttpSession`无效时,每个状态机都会被销毁。下面的示例展示了如何在控制器中使用状态机: + +``` +@Controller +public class StateMachineController { + + @Autowired + StateMachine stateMachine; + + @RequestMapping(path="/state", method=RequestMethod.POST) + public HttpEntity setState(@RequestParam("event") String event) { + stateMachine + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return new ResponseEntity(HttpStatus.ACCEPTED); + } + + @RequestMapping(path="/state", method=RequestMethod.GET) + @ResponseBody + public String getState() { + return stateMachine.getState().getId(); + } +} +``` + +| |在`session`范围内使用状态机需要仔细的计划,
主要是因为它是一个相对较重的组件。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +| |Spring StateMachine POM 与 Spring MVC类没有依赖关系,你将需要使用会话范围来处理这些类。但是,如果你正在使用 Web 应用程序,那么你已经直接从 Spring MVC 或 Spring Boot 中提取了这些依赖项。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#sm-actions)使用动作 + +动作是你可以用来与状态机交互和协作的最有用的组件之一。你可以在状态机及其状态生命周期的不同位置运行操作——例如,进入或退出状态或在转换期间。下面的示例展示了如何在状态机中使用操作: + +``` +@Override +public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.SI) + .state(States.S1, action1(), action2()) + .state(States.S2, action1(), action2()) + .state(States.S3, action1(), action3()); +} +``` + +在前面的示例中,`action1`和`action2`bean 分别附加到`entry`和`exit`状态。下面的示例定义了这些操作(以及`action3`): + +``` +@Bean +public Action action1() { + return new Action() { + + @Override + public void execute(StateContext context) { + } + }; +} + +@Bean +public BaseAction action2() { + return new BaseAction(); +} + +@Bean +public SpelAction action3() { + ExpressionParser parser = new SpelExpressionParser(); + return new SpelAction( + parser.parseExpression( + "stateMachine.sendEvent(T(org.springframework.statemachine.docs.Events).E1)")); +} + +public class BaseAction implements Action { + + @Override + public void execute(StateContext context) { + } +} + +public class SpelAction extends SpelExpressionAction { + + public SpelAction(Expression expression) { + super(expression); + } +} +``` + +你可以直接将`Action`实现为匿名函数,或者创建自己的实现,并将适当的实现定义为 Bean。 + +在前面的示例中,`action3`使用 SPEL 表达式将`Events.E1`事件发送到状态机。 + +| |`StateContext`在[使用`StateContext`]中进行了描述。| +|---|------------------------------------------------------------------------| + +### [](#spel-expressions-with-actions)带动作的 spel 表达式 + +你也可以使用 SPEL 表达式作为完整`Action`实现的替换。 + +### [](#sm-actions-reactive)反应动作 + +正常的`Action`接口是一种简单的函数方法,它取`StateContext`并返回*无效*。在你阻塞方法本身之前,这里没有任何阻塞,这是一个问题,因为 Framework 无法知道它内部到底发生了什么。 + +``` +public interface Action { + void execute(StateContext context); +} +``` + +为了克服这个问题,我们在内部更改了`Action`处理,以处理普通 Java 的`Function`,并返回`StateContext`。通过这种方式,我们可以调用 Action,并以一种反应式的方式完全执行 Action,仅在订阅时执行,并以一种非阻塞的方式等待完成。 + +``` +public interface ReactiveAction extends Function, Mono> { +} +``` + +| |在内部,旧的`Action`接口用一个可运行的反应器 Mono 包装,因为它
共享相同的返回类型。我们无法控制你用那种方法做什么!| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#sm-guards)使用保护 + +如[要记住的事情](#statemachine-config-thingstoremember)中所示,`guard1`和`guard2`bean 分别附加到进入和退出状态。下面的示例还对事件使用了保护: + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.SI).target(States.S1) + .event(Events.E1) + .guard(guard1()) + .and() + .withExternal() + .source(States.S1).target(States.S2) + .event(Events.E1) + .guard(guard2()) + .and() + .withExternal() + .source(States.S2).target(States.S3) + .event(Events.E2) + .guardExpression("extendedState.variables.get('myvar')"); +} +``` + +你可以直接将`Guard`实现为匿名函数,或者创建自己的实现,并将适当的实现定义为 Bean。在前面的示例中,`guardExpression`检查名为`myvar`的扩展状态变量是否计算为`TRUE`。下面的示例实现了一些示例保护: + +``` +@Bean +public Guard guard1() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return true; + } + }; +} + +@Bean +public BaseGuard guard2() { + return new BaseGuard(); +} + +public class BaseGuard implements Guard { + + @Override + public boolean evaluate(StateContext context) { + return false; + } +} +``` + +| |`StateContext`在[使用`StateContext`]节中进行了描述。| +|---|--------------------------------------------------------------------------------| + +### [](#spel-expressions-with-guards)带守卫的 spel 表达式 + +你也可以使用 SPEL 表达式作为完全保护实现的替代。唯一的要求是表达式需要返回一个`Boolean`值来满足`Guard`实现。这可以用一个`guardExpression()`函数来演示,该函数将一个表达式作为参数。 + +### [](#sm-guards-reactive)反应防护 + +正常的`Guard`接口是一种简单的函数方法,它取`StateContext`并返回*布尔值*。在你阻塞方法本身之前,这里没有任何阻塞,这是一个问题,因为 Framework 无法知道它内部到底发生了什么。 + +``` +public interface Guard { + boolean evaluate(StateContext context); +} +``` + +为了克服这个问题,我们在内部更改了`Guard`处理,以处理普通 Java 的`Function`,并返回`StateContext`。通过这种方式,我们可以调用 Guard,并以一种反应式的方式完全评估它,仅当它被订阅时,并且以一种非阻塞的方式等待完成,并具有一个返回值。 + +``` +public interface ReactiveGuard extends Function, Mono> { +} +``` + +| |内部旧的`Guard`接口是用反应堆单声道函数包装的。我们没有
控制你在那个方法中做什么!| +|---|----------------------------------------------------------------------------------------------------------------------------| + +## [](#sm-extendedstate)使用扩展状态 + +假设你需要创建一个状态机,该状态机跟踪用户在键盘上按下一个键的次数,然后在按键被按下 1000 次时终止。一个可能但非常幼稚的解决方案是为每 1000 次按键创建一个新的状态。你可能会突然有一个天文数字的状态,这自然是不太实际的。 + +这就是扩展的状态变量通过不需要添加更多的状态来驱动状态机更改而获得帮助的地方。相反,你可以在转换期间执行一个简单的变量更改。 + +`StateMachine`有一个名为`getExtendedState()`的方法。它返回一个名为`ExtendedState`的接口,该接口允许访问扩展的状态变量。你可以通过状态机直接访问这些变量,或者在操作或转换的回调期间通过`StateContext`访问这些变量。下面的示例展示了如何做到这一点: + +``` +public Action myVariableAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + context.getExtendedState() + .getVariables().put("mykey", "myvalue"); + } + }; +} +``` + +如果需要获得扩展状态变量更改的通知,则有两个选项:使用`StateMachineListener`或侦听`extendedStateChanged(key, value)`回调。下面的示例使用`extendedStateChanged`方法: + +``` +public class ExtendedStateVariableListener + extends StateMachineListenerAdapter { + + @Override + public void extendedStateChanged(Object key, Object value) { + // do something with changed variable + } +} +``` + +或者,你可以为`OnExtendedStateChanged`实现 Spring 应用程序上下文侦听器。正如[监听状态机事件](#sm-listeners)中提到的,你还可以侦听所有`StateMachineEvent`事件。下面的示例使用`onApplicationEvent`侦听状态更改: + +``` +public class ExtendedStateVariableEventListener + implements ApplicationListener { + + @Override + public void onApplicationEvent(OnExtendedStateChanged event) { + // do something with changed variable + } +} +``` + +## [](#sm-statecontext)使用`StateContext` + +[`StateContext`](https://DOCS. Spring.io/ Spring-StateMachine/DOCS/3.0.1/api/org/SpringFramework/StateMachine/StateContext.html)是使用状态机时最重要的对象之一,因为它被传递到各种方法和回调中,以给出状态机的当前状态以及它可能的走向。你可以将其视为当前状态机级的快照,当`StateContext`被恢复时。 + +| |在 Spring StateMachine1.0.x 中,`StateContext`的用法相对幼稚,
它是如何被用来作为简单的“pojo”传递信息的。
从 Spring StateMachine1.1.x 开始,通过使其成为状态机中的第一类公民,它的作用得到了极大的改进。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以使用`StateContext`访问以下内容: + +* 当前的`Message`或`Event`(或它们的`MessageHeaders`,如果已知的话)。 + +* 状态机的`Extended State`。 + +* `StateMachine`本身。 + +* 可能的状态机错误。 + +* 到当前`Transition`,如果适用的话。 + +* 状态机的源状态。 + +* 状态机的目标状态。 + +* 当前的`Stage`,如[Stages](#sm-statecontext-stage)中所述。 + +`StateContext`被传递到各种组件中,例如`Action`和`Guard`。 + +### [](#sm-statecontext-stage)阶段 + +[`Stage`](https://DOCS. Spring.io/ Spring-stateMachine/DOCS/3.0.1/api/org/springframework/stateMachine/stateContext.stage.html)是一个`stage`状态机当前正在与用户交互的表示。当前可用的阶段是`EVENT_NOT_ACCEPTED`,`EXTENDED_STATE_CHANGED`,`STATE_CHANGED`,`STATE_ENTRY`,`STATE_EXIT`,`STATEMACHINE_ERROR`,`STATEMACHINE_START`,`STATEMACHINE_STOP`,`TRANSITION`,和`TRANSITION_END`。这些状态可能看起来很熟悉,因为它们与你可以与侦听器交互的方式相匹配(如[监听状态机事件](#sm-listeners)中所述)。 + +## [](#sm-triggers)触发转换 + +通过使用由触发器触发的转换来驱动状态机。当前支持的触发器是`EventTrigger`和`TimerTrigger`。 + +### [](#using-eventtrigger)使用`EventTrigger` + +`EventTrigger`是最有用的触发器,因为它允许你通过向状态机发送事件来直接与其交互。这些事件也被称为信号。你可以在配置期间将状态与转换关联,从而将触发器添加到转换中。下面的示例展示了如何做到这一点: + +``` +@Autowired +StateMachine stateMachine; + +void signalMachine() { + stateMachine + .sendEvent(Mono.just(MessageBuilder + .withPayload("E1").build())) + .subscribe(); + + Message message = MessageBuilder + .withPayload("E2") + .setHeader("foo", "bar") + .build(); + stateMachine.sendEvent(Mono.just(message)).subscribe(); +} +``` + +无论你发送一个事件还是多个事件,结果总是一个结果序列。这是因为在存在多个区域的情况下,结果将从这些区域的多台机器返回。这是用方法`sendEventCollect`表示的,该方法给出了结果列表。方法本身只是一个收集`Flux`as 列表的语法糖类。如果只有一个区域,则此列表包含一个结果。 + +``` +Message message1 = MessageBuilder + .withPayload("E1") + .build(); + +Mono>> results = + stateMachine.sendEventCollect(Mono.just(message1)); + +results.subscribe(); +``` + +| |在订阅了返回的 Flux 之前,什么都不会发生。有关它的更多信息,请参见[Statemachineeversult](#sm-triggers-statemachineeventresult)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------| + +前面的示例通过构造`Mono`包装`Message`并订阅返回的`Flux`结果来发送事件。`Message`让我们向事件添加任意的额外信息,然后当(例如)实现操作时,事件对`StateContext`可见。 + +| |消息头通常被传递,直到机器运行到
特定事件的完成。例如,如果一个事件正在导致
转换为具有匿名转换为
状态的`A`状态,则原始事件可用于处于`B`状态的动作或保护。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +也可以发送`Flux`的消息,而不是只发送一个带有`Mono`的消息。 + +``` +Message message1 = MessageBuilder + .withPayload("E1") + .build(); +Message message2 = MessageBuilder + .withPayload("E2") + .build(); + +Flux> results = + stateMachine.sendEvents(Flux.just(message1, message2)); + +results.subscribe(); +``` + +#### [](#sm-triggers-statemachineeventresult)statemachineeventresult + +`StateMachineEventResult`包含有关事件发送结果的更详细信息。由此,你可以得到一个`Region`,它处理了一个事件,`Message`本身以及一个实际的`ResultType`。从`ResultType`中,你可以查看消息是否被接受、拒绝或推迟。一般来说,当下标完成时,事件被传递到机器中。 + +### [](#using-timertrigger)使用`TimerTrigger` + +`TimerTrigger`当需要在没有任何用户交互的情况下自动触发某些内容时,是很有用的。`Trigger`通过在配置期间将计时器与转换关联,将其添加到转换中。 + +目前,有两种类型的支持定时器,一种是连续地触发定时器,另一种是在进入源状态后触发定时器。下面的示例展示了如何使用触发器: + +``` +@Configuration +@EnableStateMachine +public class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S1").target("S2").event("E1") + .and() + .withExternal() + .source("S1").target("S3").event("E2") + .and() + .withInternal() + .source("S2") + .action(timerAction()) + .timer(1000) + .and() + .withInternal() + .source("S3") + .action(timerAction()) + .timerOnce(1000); + } + + @Bean + public TimerAction timerAction() { + return new TimerAction(); + } +} + +public class TimerAction implements Action { + + @Override + public void execute(StateContext context) { + // do something in every 1 sec + } +} +``` + +前面的示例有三个状态:`S1`、`S2`和`S3`。我们有一个正常的外部转换,分别是从`S1`到`S2`和从`S1`到`S3`的事件`E1`和`E2`。使用`TimerTrigger`的有趣部分是当我们定义源状态`S2`和`S3`的内部转换时。 + +对于这两个转换,我们调用`Action` Bean(`timerAction`),其中源状态`S2`使用`timer`,`S3`使用`timerOnce`。给出的值以毫秒为单位(`1000`毫秒,在两种情况下都是一秒)。 + +一旦状态机接收到事件`E1`,它就会执行从`S1`到`S2`的转换,计时器就会启动。当状态是`S2`时,`TimerTrigger`运行并导致与该状态相关的转换——在这种情况下,定义了`timerAction`的内部转换。 + +一旦状态机接收到`E2`,它就会执行从`S1`到`S3`的转换,计时器就会启动。此计时器仅在输入状态后执行一次(在计时器中定义的延迟之后)。 + +| |在幕后,计时器是可能导致
转换发生的简单触发器。使用`timer()`定义转换保持
触发,并且仅当源状态处于活动状态时才会导致转换。
使用`timerOnce()`的转换有一点不同,因为它
仅在实际进入源状态时的延迟后才触发。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果你希望在输入状态时发生一次延迟
之后发生某些事情,请使用`timerOnce()`。| +|---|-------------------------------------------------------------------------------------------------------| + +## [](#sm-listeners)监听状态机事件 + +在某些用例中,你希望了解状态机正在发生什么,对某些事情做出反应,或者获取日志详细信息以用于调试目的。 Spring StateMachine 提供了用于添加侦听器的接口。然后,这些侦听器给出一个选项,在发生各种状态更改、操作等时获得回调。 + +你基本上有两种选择:监听 Spring 应用程序上下文事件或直接将监听器附加到状态机。这两者基本上提供了相同的信息。一个生成事件作为事件类,另一个通过侦听器接口生成回调。这两点都有优点和缺点,我们将在后面进行讨论。 + +### [](#application-context-events)应用程序上下文事件 + +应用程序上下文事件类是`OnTransitionStartEvent`,`OnTransitionEvent`,`OnTransitionEndEvent`,`OnStateExitEvent`,`OnStateEntryEvent`,`OnStateChangedEvent`,`OnStateMachineStart`,`OnStateMachineStop`,以及扩展基本事件类的其他类,`StateMachineEvent`。这些可以与 Spring `ApplicationListener`一起使用。 + +`StateMachine`通过`StateMachineEventPublisher`发送上下文事件。如果用`@EnableStateMachine`注释了`@Configuration`类,则会自动创建默认实现。下面的示例从在`@Configuration`类中定义的 Bean 中获取`StateMachineApplicationEventListener`: + +``` +public class StateMachineApplicationEventListener + implements ApplicationListener { + + @Override + public void onApplicationEvent(StateMachineEvent event) { + } +} + +@Configuration +public class ListenerConfig { + + @Bean + public StateMachineApplicationEventListener contextListener() { + return new StateMachineApplicationEventListener(); + } +} +``` + +上下文事件也可以通过使用`@EnableStateMachine`自动启用,`StateMachine`用于构建机器并注册为 Bean,如下例所示: + +``` +@Configuration +@EnableStateMachine +public class ManualBuilderConfig { + + @Bean + public StateMachine stateMachine() throws Exception { + + Builder builder = StateMachineBuilder.builder(); + builder.configureStates() + .withStates() + .initial("S1") + .state("S2"); + builder.configureTransitions() + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + return builder.build(); + } +} +``` + +### [](#using-statemachinelistener)使用`StateMachineListener` + +通过使用`StateMachineListener`,你可以扩展它并实现所有回调方法,或者使用`StateMachineListenerAdapter`类,它包含存根方法实现,并选择要覆盖的那些。下面的示例使用后一种方法: + +``` +public class StateMachineEventListener + extends StateMachineListenerAdapter { + + @Override + public void stateChanged(State from, State to) { + } + + @Override + public void stateEntered(State state) { + } + + @Override + public void stateExited(State state) { + } + + @Override + public void transition(Transition transition) { + } + + @Override + public void transitionStarted(Transition transition) { + } + + @Override + public void transitionEnded(Transition transition) { + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + } + + @Override + public void stateMachineStopped(StateMachine stateMachine) { + } + + @Override + public void eventNotAccepted(Message event) { + } + + @Override + public void extendedStateChanged(Object key, Object value) { + } + + @Override + public void stateMachineError(StateMachine stateMachine, Exception exception) { + } + + @Override + public void stateContext(StateContext stateContext) { + } +} +``` + +在前面的示例中,我们创建了自己的 Listener 类(`StateMachineEventListener`),它扩展了`StateMachineListenerAdapter`。 + +`stateContext`侦听器方法允许访问不同阶段上的各种`StateContext`更改。你可以在[using`StateContext`]中找到有关它的更多信息。 + +一旦定义了自己的侦听器,就可以使用`addStateListener`方法将其注册到状态机中。是在 Spring 配置中连接它,还是在应用程序生命周期的任何时候手动连接它,这是一个风格问题。下面的示例展示了如何附加监听器: + +``` +public class Config7 { + + @Autowired + StateMachine stateMachine; + + @Bean + public StateMachineEventListener stateMachineEventListener() { + StateMachineEventListener listener = new StateMachineEventListener(); + stateMachine.addStateListener(listener); + return listener; + } + +} +``` + +### [](#limitations-and-problems)限制和问题 + +Spring 应用程序上下文不是那里最快的事件总线,因此我们建议对状态机发送的事件的速率给予一些考虑。为了获得更好的性能,使用`StateMachineListener`接口可能会更好。出于这个特定的原因,你可以使用带有`@EnableStateMachine`和`@EnableStateMachineFactory`的`contextEvents`标志来禁用 Spring 应用程序上下文事件,如上一节所示。下面的示例展示了如何禁用 Spring 应用程序上下文事件: + +``` +@Configuration +@EnableStateMachine(contextEvents = false) +public class Config8 + extends EnumStateMachineConfigurerAdapter { +} + +@Configuration +@EnableStateMachineFactory(contextEvents = false) +public class Config9 + extends EnumStateMachineConfigurerAdapter { +} +``` + +## [](#sm-context)上下文集成 + +通过监听状态机的事件或使用带有状态和转换的操作来与状态机进行交互是有点限制的。这种方法有时会过于有限和冗长,无法与状态机所使用的应用程序创建交互。对于这个特定的用例,我们进行了 Spring 风格的上下文集成,可以轻松地将状态机功能插入到 bean 中。 + +已对可用的注释进行了协调,以允许访问与[监听状态机事件](#sm-listeners)相同的状态机执行点。 + +你可以使用`@WithStateMachine`注释将状态机与现有的 Bean 关联起来。然后,你可以开始向 Bean 的方法添加受支持的注释。下面的示例展示了如何做到这一点: + +``` +@WithStateMachine +public class Bean1 { + + @OnTransition + public void anyTransition() { + } +} +``` + +你还可以使用注释`name`字段从应用程序上下文中附加任何其他状态机。下面的示例展示了如何做到这一点: + +``` +@WithStateMachine(name = "myMachineBeanName") +public class Bean2 { + + @OnTransition + public void anyTransition() { + } +} +``` + +有时,使用`machine id`会更方便,你可以将其设置为更好地识别多个实例。此 ID 映射到`StateMachine`接口中的`getId()`方法。下面的示例展示了如何使用它: + +``` +@WithStateMachine(id = "myMachineId") +public class Bean16 { + + @OnTransition + public void anyTransition() { + } +} +``` + +你也可以使用`@WithStateMachine`作为元注释,如前面的示例所示。在这种情况下,你可以用`WithMyBean`注释你的 Bean。下面的示例展示了如何做到这一点: + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@WithStateMachine(name = "myMachineBeanName") +public @interface WithMyBean { +} +``` + +| |这些方法的返回类型并不重要,并且有效地
被丢弃。| +|---|----------------------------------------------------------------------------------| + +### [](#enabling-integration)启用集成 + +你可以通过使用`@EnableWithStateMachine`注释启用`@WithStateMachine`的所有特性,该注释将所需的配置导入 Spring 应用程序上下文。`@EnableStateMachine`和`@EnableStateMachineFactory`都已经使用此注释进行了注释,因此没有必要再次添加它。但是,如果一台机器是在没有配置适配器的情况下构建和配置的,则必须使用`@EnableWithStateMachine`才能使用`@WithStateMachine`的这些功能。下面的示例展示了如何做到这一点: + +``` +public static StateMachine buildMachine(BeanFactory beanFactory) throws Exception { + Builder builder = StateMachineBuilder.builder(); + + builder.configureConfiguration() + .withConfiguration() + .machineId("myMachineId") + .beanFactory(beanFactory); + + builder.configureStates() + .withStates() + .initial("S1") + .state("S2"); + + builder.configureTransitions() + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + + return builder.build(); +} + +@WithStateMachine(id = "myMachineId") +static class Bean17 { + + @OnStateChanged + public void onStateChanged() { + } +} +``` + +| |如果机器不是作为 Bean 创建的,则需要为机器设置`BeanFactory`,如 prededing 示例中所示。否则,TGE 机器
不知道调用`@WithStateMachine`方法的处理程序。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#method-parameters)方法参数 + +每个注释都支持完全相同的一组可能的方法参数,但是运行时行为是不同的,这取决于注释本身和调用注释方法的阶段。要更好地理解上下文是如何工作的,请参见[使用`StateContext`]。 + +| |有关方法参数之间的差异,请参阅本文后面的章节,其中描述了
个别注释。| +|---|--------------------------------------------------------------------------------------------------------------------------------| + +实际上,所有带注释的方法都是通过使用 Spring SPEL 表达式来调用的,这些表达式是在处理过程中动态构建的。为了实现这一点,这些表达式需要有一个根对象(它们对其进行求值)。这个根对象是`StateContext`。我们还在内部进行了一些调整,这样就可以直接访问`StateContext`方法,而无需通过上下文句柄。 + +最简单的方法参数是`StateContext`本身。下面的示例展示了如何使用它: + +``` +@WithStateMachine +public class Bean3 { + + @OnTransition + public void anyTransition(StateContext stateContext) { + } +} +``` + +你可以访问`StateContext`内容的其余部分。参数的数量和顺序并不重要。下面的示例展示了如何访问`StateContext`内容的各个部分: + +``` +@WithStateMachine +public class Bean4 { + + @OnTransition + public void anyTransition( + @EventHeaders Map headers, + @EventHeader("myheader1") Object myheader1, + @EventHeader(name = "myheader2", required = false) String myheader2, + ExtendedState extendedState, + StateMachine stateMachine, + Message message, + Exception e) { + } +} +``` + +| |你可以使用`@EventHeader`,而不是使用所有带有`@EventHeaders`的事件头,它可以绑定到一个单独的头。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +### [](#state-machine-transition-annotations)转换注释 + +转换的注释是`@OnTransition`、`@OnTransitionStart`和`@OnTransitionEnd`。 + +这些注释的行为完全相同。为了说明它们是如何工作的,我们展示了`@OnTransition`是如何使用的。在这个注释中,你可以使用`source`和`target`属性来限定转换。如果`source`和`target`为空,则任何转换都是匹配的。下面的示例展示了如何使用`@OnTransition`注释(请记住`@OnTransitionStart`和`@OnTransitionEnd`的工作方式相同): + +``` +@WithStateMachine +public class Bean5 { + + @OnTransition(source = "S1", target = "S2") + public void fromS1ToS2() { + } + + @OnTransition + public void anyTransition() { + } +} +``` + +默认情况下,由于 Java 语言的限制,你无法使用`@OnTransition`注释来创建状态和事件枚举。出于这个原因,你需要使用字符串表示。 + +此外,你可以通过向方法添加所需的参数来访问`Event Headers`和`ExtendedState`。然后使用这些参数自动调用该方法。下面的示例展示了如何做到这一点: + +``` +@WithStateMachine +public class Bean6 { + + @StatesOnTransition(source = States.S1, target = States.S2) + public void fromS1ToS2(@EventHeaders Map headers, ExtendedState extendedState) { + } +} +``` + +但是,如果你想要一个类型安全的注释,你可以创建一个新的注释,并使用`@OnTransition`作为元注释。这种用户级别的注释可以对实际的状态和事件枚举进行引用,框架尝试以相同的方式匹配这些状态和事件。下面的示例展示了如何做到这一点: + +``` +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@OnTransition +public @interface StatesOnTransition { + + States[] source() default {}; + + States[] target() default {}; +} +``` + +在前面的示例中,我们以类型安全的方式创建了`@StatesOnTransition`注释,该注释定义了`source`和`target`。下面的示例在 Bean 中使用了该注释: + +``` +@WithStateMachine +public class Bean7 { + + @StatesOnTransition(source = States.S1, target = States.S2) + public void fromS1ToS2() { + } +} +``` + +### [](#state-annotations)状态注释 + +下面的状态注释是可用的:`@OnStateChanged`,`@OnStateEntry`,和`@OnStateExit`。下面的示例展示了如何使用`OnStateChanged`注释(其他两种方法的工作方式相同): + +``` +@WithStateMachine +public class Bean8 { + + @OnStateChanged + public void anyStateChange() { + } +} +``` + +与[过渡注释](#state-machine-transition-annotations)一样,你可以定义目标状态和源状态。下面的示例展示了如何做到这一点: + +``` +@WithStateMachine +public class Bean9 { + + @OnStateChanged(source = "S1", target = "S2") + public void stateChangeFromS1toS2() { + } +} +``` + +为了类型安全,需要使用`@OnStateChanged`作为元注释来为枚举创建新的注释。下面的例子说明了如何做到这一点: + +``` +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@OnStateChanged +public @interface StatesOnStates { + + States[] source() default {}; + + States[] target() default {}; +} +``` + +``` +@WithStateMachine +public class Bean10 { + + @StatesOnStates(source = States.S1, target = States.S2) + public void fromS1ToS2() { + } +} +``` + +状态进入和状态退出的方法的行为方式相同,如下例所示: + +``` +@WithStateMachine +public class Bean11 { + + @OnStateEntry + public void anyStateEntry() { + } + + @OnStateExit + public void anyStateExit() { + } +} +``` + +### [](#event-annotation)事件注释 + +有一个与事件相关的注释。它被命名为`@OnEventNotAccepted`。如果指定`event`属性,则可以侦听未被接受的特定事件。如果你没有指定一个事件,你可以列出未被接受的任何事件。下面的示例展示了使用`@OnEventNotAccepted`注释的两种方式: + +``` +@WithStateMachine +public class Bean12 { + + @OnEventNotAccepted + public void anyEventNotAccepted() { + } + + @OnEventNotAccepted(event = "E1") + public void e1EventNotAccepted() { + } +} +``` + +### [](#state-machine-annotations)状态机注释 + +以下注释可用于状态机:`@OnStateMachineStart`、`@OnStateMachineStop`和`@OnStateMachineError`。 + +在状态机的启动和停止过程中,会调用生命周期方法。下面的示例展示了如何使用`@OnStateMachineStart`和`@OnStateMachineStop`来侦听这些事件: + +``` +@WithStateMachine +public class Bean13 { + + @OnStateMachineStart + public void onStateMachineStart() { + } + + @OnStateMachineStop + public void onStateMachineStop() { + } +} +``` + +如果状态机出现异常错误,则调用`@OnStateMachineStop`注释。下面的示例展示了如何使用它: + +``` +@WithStateMachine +public class Bean14 { + + @OnStateMachineError + public void onStateMachineError() { + } +} +``` + +### [](#extended-state-annotation)扩展状态注释 + +有一个扩展的状态相关注释。它被命名为[Persist](#statemachine-examples-persist)。你还可以只针对特定的`key`更改监听更改。下面的示例展示了如何使用`@OnExtendedStateChanged`,包括带`key`属性和不带`key`属性: + +``` +@WithStateMachine +public class Bean15 { + + @OnExtendedStateChanged + public void anyStateChange() { + } + + @OnExtendedStateChanged(key = "key1") + public void key1Changed() { + } +} +``` + +## [](#sm-accessor)使用`StateMachineAccessor` + +`StateMachine`是与状态机通信的主要接口。有时,你可能需要对状态机及其嵌套的机器和区域的内部结构进行更动态和程序化的访问。对于这些用例,`StateMachine`公开了一个名为`StateMachineAccessor`的功能接口,该接口提供了一个接口来访问单个`StateMachine`和`Region`实例。 + +`StateMachineFunction`是一个简单的函数接口,它允许你将`StateMachineAccess`接口应用到状态机。在 JDK7 中,这些代码创建的代码是一个很小的详细代码。然而,对于 JDK8Lambdas,DOCE 相对来说并不详细。 + +`doWithAllRegions`方法允许访问状态机中的所有`Region`实例。下面的示例展示了如何使用它: + +``` +stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.setRelay(stateMachine)); + +stateMachine.getStateMachineAccessor() + .doWithAllRegions(access -> access.setRelay(stateMachine)); +``` + +`doWithRegion`方法允许访问状态机中的单个`Region`实例。下面的示例展示了如何使用它: + +``` +stateMachine.getStateMachineAccessor().doWithRegion(function -> function.setRelay(stateMachine)); + +stateMachine.getStateMachineAccessor() + .doWithRegion(access -> access.setRelay(stateMachine)); +``` + +`withAllRegions`方法允许访问状态机中的所有`Region`实例。下面的示例展示了如何使用它: + +``` +for (StateMachineAccess access : stateMachine.getStateMachineAccessor().withAllRegions()) { + access.setRelay(stateMachine); +} + +stateMachine.getStateMachineAccessor().withAllRegions() + .stream().forEach(access -> access.setRelay(stateMachine)); +``` + +`withRegion`方法允许访问状态机中的单个`Region`实例。下面的示例展示了如何使用它: + +``` +stateMachine.getStateMachineAccessor() + .withRegion().setRelay(stateMachine); +``` + +## [](#sm-interceptor)使用`StateMachineInterceptor` + +你可以使用`StateMachineInterceptor`接口,而不是使用`StateMachineListener`接口。一个概念上的区别是,你可以使用拦截器来拦截和停止当前状态更改或更改其转换逻辑。你可以使用一个名为`StateMachineInterceptorAdapter`的适配器类来覆盖缺省的 no-op 方法,而不是实现一个完整的接口。 + +| |一个配方([Persist](#statemachine-recipes-persist))和一个样本
([Persist](#statemachine-examples-persist))与使用
拦截器有关。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以通过`StateMachineAccessor`注册拦截器。拦截器的概念是一个相对较深的内部特性,因此不会直接通过`StateMachine`接口公开。 + +下面的示例展示了如何添加`StateMachineInterceptor`并覆盖选定的方法: + +``` +stateMachine.getStateMachineAccessor() + .withRegion().addStateMachineInterceptor(new StateMachineInterceptor() { + + @Override + public Message preEvent(Message message, StateMachine stateMachine) { + return message; + } + + @Override + public StateContext preTransition(StateContext stateContext) { + return stateContext; + } + + @Override + public void preStateChange(State state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + } + + @Override + public StateContext postTransition(StateContext stateContext) { + return stateContext; + } + + @Override + public void postStateChange(State state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + } + + @Override + public Exception stateMachineError(StateMachine stateMachine, + Exception exception) { + return exception; + } + }); +``` + +| |有关前面示例中显示的错误处理的更多信息,请参见[状态机错误处理](#sm-error-handling)。| +|---|--------------------------------------------------------------------------------------------------------------------| + +## [](#sm-security)状态机安全 + +安全功能是建立在[Spring Security](https://projects.spring.io/spring-security)的功能之上的。当需要保护状态机执行的一部分以及与状态机的交互时,安全性功能非常方便。 + +| |我们希望你对 Spring 安全性相当熟悉,这意味着
我们不会详细介绍整个安全框架是如何工作的。对于
此信息,你应该阅读 Spring 安全参考文档
(可用[here](https://spring.io/projects/spring-security#learn))。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +安全防御的第一个层次自然是保护事件,这些事件真正驱动了状态机中将要发生的事情。然后,你可以为转换和操作定义更细粒度的安全设置。这类似于让员工进入一栋建筑,然后进入建筑内的特定房间,甚至可以打开和关闭特定房间的灯。如果你信任你的用户,那么事件安全性可能就是你所需要的。如果没有,则需要应用更详细的安全性。 + +你可以在[理解安全](#sm-security-details)中找到更详细的信息。 + +| |有关完整的示例,请参见[Security](#statemachine-examples-security)示例。| +|---|-----------------------------------------------------------------------------------| + +### [](#configuring-security)配置安全性 + +用于安全性的所有通用配置都在`SecurityConfigurer`中完成,它是从`StateMachineConfigurationConfigurer`中获得的。默认情况下,安全性是禁用的,即使存在 Spring 安全性类。下面的示例展示了如何启用安全性: + +``` +@Configuration +@EnableStateMachine +static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true) + .transitionAccessDecisionManager(null) + .eventAccessDecisionManager(null); + } +} +``` + +如果绝对需要,可以为事件和转换自定义`AccessDecisionManager`。如果不定义决策管理器或将其设置为`null`,则默认管理器将在内部创建。 + +### [](#securing-events)确保事件安全 + +事件安全性在全局级别上由`RoleVoter`定义。下面的示例展示了如何启用事件安全性: + +``` +@Configuration +@EnableStateMachine +static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true) + .event("true") + .event("ROLE_ANONYMOUS", ComparisonType.ANY); + } +} +``` + +在前面的配置示例中,我们使用`true`的表达式,该表达式的求值总是`TRUE`。在实际应用程序中,使用始终计算为`TRUE`的表达式是没有意义的,但是它显示了表达式需要返回`TRUE`或`FALSE`的一点。我们还定义了`ROLE_ANONYMOUS`的一个属性和`ComparisonType`的一个`ANY`。有关使用属性和表达式的更多信息,请参见[使用安全属性和表达式](#sm-security-attributes-expressions)。 + +### [](#securing-transitions)确保过渡 + +你可以在全局范围内定义转换安全性,如下例所示。 + +``` +@Configuration +@EnableStateMachine +static class Config6 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true) + .transition("true") + .transition("ROLE_ANONYMOUS", ComparisonType.ANY); + } +} +``` + +如果安全性是在转换本身中定义的,那么它将覆盖任何全局设置的安全性。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableStateMachine +static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A") + .secured("ROLE_ANONYMOUS", ComparisonType.ANY) + .secured("hasTarget('S1')"); + } +} +``` + +有关使用属性和表达式的更多信息,请参见[使用安全属性和表达式](#sm-security-attributes-expressions)。 + +### [](#securing-actions)确保操作安全 + +对于状态机中的动作没有专门的安全定义,但是你可以使用 Spring Security 中的全局方法安全性来保护动作。这要求将`Action`定义为 proxied`@Bean`,并将其`execute`方法注释为`@Secured`。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableStateMachine +static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .action(securedAction()) + .event("A"); + } + + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + @Bean + public Action securedAction() { + return new Action() { + + @Secured("ROLE_ANONYMOUS") + @Override + public void execute(StateContext context) { + } + }; + } + +} +``` + +Spring 安全性需要启用全局方法安全性。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableGlobalMethodSecurity(securedEnabled = true) +public static class Config5 extends WebSecurityConfigurerAdapter { + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER"); + } +} +``` + +有关更多详细信息,请参见 Spring 安全参考指南(可用[here](https://spring.io/projects/spring-security#learn))。 + +### [](#sm-security-attributes-expressions)使用安全属性和表达式 + +通常,你可以通过两种方式定义安全属性:通过使用安全属性和通过使用安全表达式。属性更容易使用,但在功能方面相对有限。表达式提供了更多的功能,但使用起来有点困难。 + +#### [](#generic-attribute-usage)泛型属性用法 + +默认情况下,用于事件和转换的`AccessDecisionManager`实例都使用`RoleVoter`,这意味着你可以使用 Spring Security 中的角色属性。 + +对于属性,我们有三种不同的比较类型:`ANY`、`ALL`和`MAJORITY`。这些比较类型映射到默认访问决策管理器(分别为`AffirmativeBased`、`UnanimousBased`和`ConsensusBased`)。如果你定义了一个自定义`S2`,那么比较类型将被有效地丢弃,因为它仅用于创建默认管理器。 + +#### [](#generic-expression-usage)通用表达式用法 + +安全表达式必须返回`TRUE`或`FALSE`。 + +表达式根对象的基类是`SecurityExpressionRoot`。它提供了一些常见的表达式,这些表达式在转换和事件安全性方面都可用。下表描述了最常用的内置表达式: + +| Expression |说明| +|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `hasRole([role])` |如果当前主体具有指定的角色,则返回`true`。通过
默认设置,如果提供的角色不以`ROLE_`开头,则添加
。你可以通过修改`DefaultWebSecurityExpressionHandler`上的`ANY`来自定义此项。| +| `hasAnyRole([role1,role2])` |返回`true`如果当前主体有任何提供的
角色(以逗号分隔的字符串列表给出)。默认情况下,如果每个
提供的角色不以`ROLE_`开头,则添加该角色。你可以通过修改`DefaultWebSecurityExpressionHandler`上的`defaultRolePrefix`来自定义此
。| +| `hasAuthority([authority])` |如果当前主体具有指定的权限,则返回`true`。| +| `hasAnyAuthority([authority1,authority2])` |返回`true`,如果当前主体有任何提供的
角色(以逗号分隔的字符串列表给出)。| +| `principal` |允许直接访问表示
当前用户的主体对象。| +| `authentication` |允许直接访问当前`Authentication`对象从
中获得的
。| +| `permitAll` |总是计算为`true`。| +| `denyAll` |总是计算为`false`。| +| `isAnonymous()` |如果当前主体是匿名用户,则返回`true`。| +| `isRememberMe()` |如果当前主体是 rememe-me 用户,则返回`true`。| +| `isAuthenticated()` |如果用户不是匿名的,则返回`true`。| +| `isFullyAuthenticated()` |如果用户不是匿名者或 Remember-Me 用户,则返回`ROLE_`。| +| `hasPermission(Object target, Object permission)` |如果用户能够访问
给定权限所提供的目标——例如,`hasPermission(domainObject, 'read')`,则返回`hasPermission(domainObject, 'read')`。| +|`hasPermission(Object targetId, String targetType, Object
permission)`|如果用户能够访问
给定权限所提供的目标——例如,`hasPermission(1,
'com.example.domain.Message', 'read')`,则返回。| + +#### [](#event-attributes)事件属性 + +你可以使用`EVENT_`的前缀来匹配事件 ID。例如,匹配事件`A`将匹配属性`EVENT_A`。 + +#### [](#event-expressions)事件表达式 + +事件的表达式根对象的基类是`EventSecurityExpressionRoot`。它提供对`Message`对象的访问,该对象通过事件传递。`EventSecurityExpressionRoot`只有一种方法,下表对此进行了描述: + +| Expression |说明| +|------------------------|------------------------------------------------| +|`hasEvent(Object event)`|如果事件与给定事件匹配,则返回`true`。| + +#### [](#transition-attributes)转换属性 + +当匹配转换源和目标时,可以分别使用`TRANSITION_SOURCE_`和`TRANSITION_TARGET_`前缀。 + +#### [](#transition-expressions)转换表达式 + +用于转换的表达式根对象的基类是`DefaultWebSecurityExpressionHandler`。它提供了对`Transition`对象的访问,该对象被传递以进行转换更改。`TransitionSecurityExpressionRoot`有两个方法,下表对此进行了描述: + +| Expression |说明| +|--------------------------|-------------------------------------------------------------| +|`hasSource(Object source)`|如果转换源匹配给定源,则返回`true`。| +|`hasTarget(Object target)`|如果转换目标与给定目标匹配,则返回`true`。| + +### [](#sm-security-details)理解安全 + +本节提供了有关状态机中安全性如何工作的更详细信息。你可能真的不需要知道,但最好是透明的,而不是隐藏幕后发生的所有神奇的事情。 + +| |只有当 Spring statemachine 运行在一个有围墙的
花园中时,安全性才有意义,该花园中的用户没有直接访问应用程序的权限,因此可以
修改 Spring 安全性的`SecurityContext`在本地线程中保持。
如果用户控制了 JVM,那么实际上根本就没有安全性
。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +安全性的集成点是使用[`StateMachineInterceptor`](#sm-interceptor)创建的,如果启用了安全性,则会自动将其添加到状态机中。特定的类是`StateMachineSecurityInterceptor`,它拦截事件和转换。然后,该拦截器将查询 Spring Security 的`AccessDecisionManager`,以确定是否可以发送事件或是否可以执行转换。实际上,如果带有`AccessDecisionManager`的决定或投票导致异常,则该事件或转换将被拒绝。 + +由于 Spring Security 中`AccessDecisionManager`的工作方式,我们需要每个安全对象都有一个实例。这就是为什么对于事件和过渡有不同的管理人员的原因之一。在这种情况下,事件和转换是我们保护的不同的类对象。 + +默认情况下,对于事件,投票人(`EventExpressionVoter`,`EventVoter`,和`RoleVoter`)被添加到`AccessDecisionManager`中。 + +默认情况下,对于转换,投票人(`TransitionExpressionVoter`,`TransitionVoter`,和`RoleVoter`)被添加到
中。 + +## [](#sm-error-handling)状态机错误处理 + +如果状态机在状态转换逻辑期间检测到内部错误,则可能抛出异常。在内部处理此异常之前,你有机会进行拦截。 + +通常,你可以使用`StateMachineInterceptor`来拦截错误,下面的清单展示了一个例子: + +``` +StateMachine stateMachine; + +void addInterceptor() { + stateMachine.getStateMachineAccessor() + .doWithRegion(function -> + function.addStateMachineInterceptor(new StateMachineInterceptorAdapter() { + @Override + public Exception stateMachineError(StateMachine stateMachine, + Exception exception) { + return exception; + } + }) + ); + +} +``` + +当检测到错误时,将执行正常事件通知机制。这允许你使用`StateMachineListener`或 Spring 应用程序上下文事件侦听器。有关这些的更多信息,请参见[监听状态机事件](#sm-listeners)。 + +话虽如此,下面的示例展示了一个简单的侦听器: + +``` +public class ErrorStateMachineListener + extends StateMachineListenerAdapter { + + @Override + public void stateMachineError(StateMachine stateMachine, Exception exception) { + // do something with error + } +} +``` + +下面的示例显示了一个通用的`ApplicationListener`检查`StateMachineEvent`: + +``` +public class GenericApplicationEventListener + implements ApplicationListener { + + @Override + public void onApplicationEvent(StateMachineEvent event) { + if (event instanceof OnStateMachineError) { + // do something with error + } + } +} +``` + +你还可以直接定义`ApplicationListener`来只识别`StateMachineEvent`实例,如下例所示: + +``` +public class ErrorApplicationEventListener + implements ApplicationListener { + + @Override + public void onApplicationEvent(OnStateMachineError event) { + // do something with error + } +} +``` + +| |为转换定义的操作也有自己的错误处理
逻辑。见`StateMachineEvent`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有了反应性 API,就有可能从*Statemachineeversult*返回*行动*执行错误。具有简单的机器,其动作中的错误转换为状态`S1`。 + +``` +@Configuration +@EnableStateMachine +static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("SI") + .stateEntry("S1", (context) -> { + throw new RuntimeException("example error"); + }); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("SI") + .target("S1") + .event("E1"); + } +} +``` + +下面的测试概念显示了如何从*Statemachineeversult*消耗可能的错误。 + +``` +@Autowired +private StateMachine machine; + +@Test +public void testActionEntryErrorWithEvent() throws Exception { + StepVerifier.create(machine.startReactively()).verifyComplete(); + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("SI"); + + StepVerifier.create(machine.sendEvent(Mono.just(MessageBuilder.withPayload("E1").build()))) + .consumeNextWith(result -> { + StepVerifier.create(result.complete()).consumeErrorWith(e -> { + assertThat(e).isInstanceOf(StateMachineException.class).hasMessageContaining("example error"); + }).verify(); + }) + .verifyComplete(); + + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("S1"); +} +``` + +| |进入/退出操作中的错误不会阻止转换发生。| +|---|------------------------------------------------------------------| + +## [](#sm-service)状态机服务 + +StateMachine 服务是更高级别的实现,旨在提供更多的用户级功能,以简化正常的运行时操作。目前,只存在一个服务接口(`StateMachineEvent`)。 + +### [](#sm-service-statemachineservice)使用`StateMachineService` + +`StateMachineService`是一种接口,用于处理运行中的机器,并具有“获取”和“释放”机器的简单方法。它有一个默认的实现,名为`DefaultStateMachineService`。 + +## `ANY`持久化状态机 + +传统上,状态机的一个实例是在一个运行中的程序中使用的。你可以通过使用动态构建器和工厂来实现更动态的行为,这允许按需进行状态机实例化。构建状态机的实例是一项相对繁重的操作。因此,如果你需要(例如)通过使用状态机来处理数据库中的任意状态更改,那么你需要找到一种更好、更快的方法来实现这一点。 + +持久化特性允许你将状态机的状态保存到外部存储库中,然后根据序列化状态重置状态机。例如,如果你有一个保存订单的数据库表,那么如果每次更改都需要构建一个新的实例,那么用状态机更新订单状态将是非常昂贵的。持久化特性允许你重置状态机状态,而无需实例化新的状态机实例。 + +| |有一个配方(参见[Persist](#statemachine-recipes-persist))和一个样本
(参见[Persist](#statemachine-examples-persist))提供了有关
持久性状态的更多信息。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +虽然可以通过使用`StateMachineListener`构建自定义持久性特性,但它存在一个概念问题。当监听器通知状态更改时,状态更改已经发生。如果侦听器中的自定义持久方法无法更新外部存储库中的序列化状态,则状态机中的状态和外部存储库中的状态将处于不一致状态。 + +在状态机中的状态更改期间,你可以使用状态机拦截器尝试将序列化状态保存到外部存储中。如果此拦截器回调失败,你可以停止状态更改尝试,然后可以手动处理此错误,而不是以不一致的状态结束。有关如何使用拦截器,请参见[using`StateMachineInterceptor`]。 + +### [](#sm-persist-statemachinecontext)使用`StateMachineContext` + +使用普通的 Java 序列化不能持久保存`StateMachine`,因为对象图太丰富,并且包含太多对其他 Spring 上下文类的依赖关系。`StateMachineContext`是状态机的运行时表示形式,你可以使用它将现有机器恢复到由特定的`StateMachineContext`对象表示的状态。 + +`StateMachineContext`包含两种不同的方式来包含子上下文的信息。当机器包含正交区域时,通常使用这些方法。首先,上下文可以有一个子上下文列表,如果它们存在,则可以按原样使用。其次,你可以包括一个引用列表,如果未设置原始上下文子目录,则会使用这些引用。这些子引用实际上是在多个并行区域独立运行的机器中持久化的唯一方法。 + +| |[多数据持久化](#statemachine-examples-datajpamultipersist)示例显示了
如何持久化并行区域。| +|---|---------------------------------------------------------------------------------------------------------------------------| + +### [](#sm-persist-statemachinepersister)使用`StateMachinePersister` + +构建`StateMachineContext`然后从它恢复状态机,如果是手动完成的,这一直是有点“黑魔法”。`StateMachinePersister`接口旨在通过提供`persist`和`restore`方法来简化这些操作。这个接口的默认实现是`DefaultStateMachinePersister`。 + +我们可以通过跟踪测试中的片段来演示如何使用`StateMachinePersister`。我们首先为状态机创建两个类似的配置(`machine1`和`machine2`)。请注意,我们可以用其他方式为这个演示构建不同的机器,但这种方式适用于这种情况。下面的示例配置了两个状态机: + +``` +@Configuration +@EnableStateMachine(name = "machine1") +static class Config1 extends Config { +} + +@Configuration +@EnableStateMachine(name = "machine2") +static class Config2 extends Config { +} + +static class Config extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + } +} +``` + +当我们使用`StateMachinePersist`对象时,我们可以创建一个内存中的实现。 + +| |这个内存中的示例仅用于演示目的。对于真正的
应用程序,你应该使用真正的持久存储实现。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------| + +下面的清单展示了如何使用内存中的示例: + +``` +static class InMemoryStateMachinePersist implements StateMachinePersist { + + private final HashMap> contexts = new HashMap<>(); + + @Override + public void write(StateMachineContext context, String contextObj) throws Exception { + contexts.put(contextObj, context); + } + + @Override + public StateMachineContext read(String contextObj) throws Exception { + return contexts.get(contextObj); + } +} +``` + +在实例化了这两台不同的机器之后,我们可以通过事件`E1`将`S2`转换为状态`S2`。然后我们可以将其持久化并恢复`machine2`。下面的示例展示了如何做到这一点: + +``` +InMemoryStateMachinePersist stateMachinePersist = new InMemoryStateMachinePersist(); +StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + +StateMachine stateMachine1 = context.getBean("machine1", StateMachine.class); +StateMachine stateMachine2 = context.getBean("machine2", StateMachine.class); +stateMachine1.startReactively().block(); + +stateMachine1 + .sendEvent(Mono.just(MessageBuilder + .withPayload("E1").build())) + .blockLast(); +assertThat(stateMachine1.getState().getIds()).containsExactly("S2"); + +persister.persist(stateMachine1, "myid"); +persister.restore(stateMachine2, "myid"); +assertThat(stateMachine2.getState().getIds()).containsExactly("S2"); +``` + +### [](#sm-persist-redis)使用 redis + +`RepositoryStateMachinePersist`(实现`StateMachinePersist`)为将状态机持久化到 Redis 提供了支持。具体的实现是`RedisStateMachineContextRepository`,它使用`kryo`序列化将`StateMachineContext`持久化到`Redis`。 + +对于`StateMachinePersister`,我们有一个与 Redis 相关的`RedisStateMachinePersister`实现,它接受一个`StateMachinePersist`的实例,并使用`String`作为其上下文对象。 + +| |有关详细用法,请参见[活动服务](#statemachine-examples-eventservice)示例。| +|---|---------------------------------------------------------------------------------------| + +`RedisStateMachineContextRepository`需要一个`RedisConnectionFactory`才能使其工作。我们建议对它使用`JedisConnectionFactory`,如前面的示例所示。 + +### [](#sm-persist-statemachineruntimepersister)使用`StateMachineRuntimePersister` + +`StateMachineRuntimePersister`是对`StateMachinePersist`的一个简单扩展,它添加了一个接口级方法来获得与其关联的`StateMachineInterceptor`。然后,需要此拦截器在状态更改期间持久化机器,而无需停止和启动机器。 + +目前,已有针对受支持的 Spring 数据存储库的该接口的实现。这些实现方式是`JpaPersistingStateMachineInterceptor`、`MongoDbPersistingStateMachineInterceptor`和`RedisPersistingStateMachineInterceptor`。 + +| |有关详细用法,请参见[数据持续存在](#statemachine-examples-datapersist)示例。| +|---|-------------------------------------------------------------------------------------| + +## [](#sm-boot) Spring 引导支持 + +自动配置模块(`spring-statemachine-autoconfigure`)包含与 Spring 引导集成的所有逻辑,该引导为自动配置和执行器提供了功能。你所需要的只是将这个 Spring StateMachine 库作为引导应用程序的一部分。 + +### [](#sm-boot-monitoring)监视和跟踪 + +`BootStateMachineMonitor`将自动创建并与状态机关联。`BootStateMachineMonitor`是一个自定义`StateMachineMonitor`实现,它与 Spring boot 的`MeterRegistry`和通过自定义`StateMachineTraceRepository`的端点集成。你可以通过将`spring.statemachine.monitor.enabled`键设置为`StateMachineTraceRepository`来禁用此自动配置。[Monitoring](#statemachine-examples-monitoring)示例展示了如何使用这种自动配置。 + +### [](#repository-config)存储库配置 + +如果从 Classpath 中找到了所需的类,则 Spring 数据存储库和实体类扫描将自动为`StateMachineTraceRepository`自动配置。 + +当前支持的配置是`JPA`、`Redis`和`StateMachineTraceRepository`。可以分别使用`spring.statemachine.data.jpa.repositories.enabled`、`spring.statemachine.data.redis.repositories.enabled`和`spring.statemachine.data.mongo.repositories.enabled`属性禁用存储库自动配置。 + +## [](#sm-monitoring)监视状态机 + +你可以使用`StateMachineMonitor`来获取更多有关转换和执行操作所需的持续时间的信息。下面的清单展示了这个接口是如何实现的。 + +``` +public class TestStateMachineMonitor extends AbstractStateMachineMonitor { + + @Override + public void transition(StateMachine stateMachine, Transition transition, + long duration) { + } + + @Override + public void action(StateMachine stateMachine, + Function, Mono> action, long duration) { + } +} +``` + +有了`StateMachineMonitor`实现后,就可以通过配置将其添加到状态机中,如下例所示: + +``` +@Configuration +@EnableStateMachine +public class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withMonitoring() + .monitor(stateMachineMonitor()); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + } + + @Bean + public StateMachineMonitor stateMachineMonitor() { + return new TestStateMachineMonitor(); + } +} +``` + +| |有关详细用法,请参见[Monitoring](#statemachine-examples-monitoring)示例。| +|---|----------------------------------------------------------------------------------| + +## [](#sm-distributed)使用分布状态 + +分布式状态可能是 Spring 状态机中最复杂的概念之一。什么是分布式状态?单个状态机中的状态很容易理解,但是,当需要通过状态机引入共享的分布式状态时,事情就变得有点复杂了。 + +| |分布式状态功能仍然是预览功能,并不
,但在此特定版本中被认为是稳定的。我们预计这一
功能将在其首次正式发布时成熟。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关通用配置支持的信息,请参见[配置公共设置](#statemachine-config-commonsettings)。有关实际使用示例,请参见[Zookeeper](#statemachine-examples-zookeeper)示例。 + +分布式状态机是通过`DistributedStateMachine`类实现的,该类封装了`StateMachine`的实际实例。`DistributedStateMachine`拦截与`StateMachine`实例的通信,并通过`StateMachineEnsemble`接口处理分布式状态抽象。根据实际的实现,还可以使用`StateMachinePersist`接口序列化`StateMachineContext`,其中包含足够的信息来重置`StateMachine`。 + +虽然分布式状态机是通过抽象实现的,但目前只存在一个实现。它是以 ZooKeeper 为基础的。 + +下面的示例展示了如何配置基于 ZooKeeper 的分布式状态机 `: + +``` +@Configuration +@EnableStateMachine +public class Config + extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withDistributed() + .ensemble(stateMachineEnsemble()) + .and() + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + // config states + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + // config transitions + } + + @Bean + public StateMachineEnsemble stateMachineEnsemble() + throws Exception { + return new ZookeeperStateMachineEnsemble(curatorClient(), "/zkpath"); + } + + @Bean + public CuratorFramework curatorClient() + throws Exception { + CuratorFramework client = CuratorFrameworkFactory + .builder() + .defaultData(new byte[0]) + .connectString("localhost:2181").build(); + client.start(); + return client; + } + +} +``` + +你可以找到基于 Zookeeker 的分布式状态机[在附录中](#appendices-zookeeper)的当前技术文档。 + +### `StateMachine`使用`ZookeeperStateMachineEnsemble` + +`ZookeeperStateMachineEnsemble`本身需要两个强制设置,一个`curatorClient`的实例和一个`basePath`的实例。客户端是`CuratorFramework`,路径是`Zookeeper`实例中树的根。 + +可选地,你可以设置`cleanState`,它默认设置为`TRUE`,如果集成中没有成员,则清除现有数据。如果希望在应用程序重新启动期间保持分布式状态,可以将其设置为`FALSE`。 + +可选地,你可以将`logSize`(默认为`32`)的大小设置为状态更改的 keep 历史记录。这个设置的值必须是 2 的幂。`curatorClient`通常是一个很好的默认值。如果某个特定状态机的滞后时间超过了日志的大小,那么它就会进入错误状态,并与集合断开连接,这表明它已经失去了历史记录,也失去了完全重建同步状态的能力。 + +## [](#sm-test)测试支持 + +我们还添加了一组实用程序类,以方便对状态机实例的测试。这些在框架中使用,但对最终用户也非常有用。 + +`StateMachineTestPlanBuilder`构建了一个`StateMachineTestPlan`,它有一个方法(称为`test()`)。该方法运行一个计划。`StateMachineTestPlanBuilder`包含一个 Fluent Builder API,可以让你为计划添加步骤。在这些步骤中,你可以发送事件并检查各种条件,例如状态更改、转换和扩展的状态变量。 + +下面的示例使用`StateMachineBuilder`构建状态机: + +``` +private StateMachine buildMachine() throws Exception { + StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); + + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true); + + builder.configureStates() + .withStates() + .initial("SI") + .state("S1"); + + builder.configureTransitions() + .withExternal() + .source("SI").target("S1") + .event("E1") + .action(c -> { + c.getExtendedState().getVariables().put("key1", "value1"); + }); + + return builder.build(); +} +``` + +在下面的测试计划中,我们有两个步骤。首先,我们检查初始状态(`SI`)是否已设置。其次,我们发送一个事件(`E1`),并期望发生一个状态更改,并期望机器以`S1`的状态结束。下面的清单显示了测试计划: + +``` +StateMachine machine = buildMachine(); +StateMachineTestPlan plan = + StateMachineTestPlanBuilder.builder() + .defaultAwaitTime(2) + .stateMachine(machine) + .step() + .expectStates("SI") + .and() + .step() + .sendEvent("E1") + .expectStateChanged(1) + .expectStates("S1") + .expectVariable("key1") + .expectVariable("key1", "value1") + .expectVariableWith(hasKey("key1")) + .expectVariableWith(hasValue("value1")) + .expectVariableWith(hasEntry("key1", "value1")) + .expectVariableWith(not(hasKey("key2"))) + .and() + .build(); +plan.test(); +``` + +这些实用程序还在一个框架中用于测试分布式状态机的特性。请注意,你可以在一个计划中添加多台机器。如果你添加了多台机器,Yuo 还可以选择发送特定机器、随机机器或所有机器的事件。 + +前面的测试示例使用了以下 Hamcrest 导入: + +``` +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.collection.IsMapContaining.hasKey; +import static org.hamcrest.collection.IsMapContaining.hasValue; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.collection.IsMapContaining.hasEntry; +``` + +| |预期结果的所有可能选项都记录在 Javadoc 中[`StateMachineTestPlanStepBuilder`](https://DOCS. Spring.io/ Spring-statemachine/DOCS/3.0.1/api/org/springframework/statemachine/test/statemachinetplanbuilder.statemachinetestplanstepbuilder.html)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#sm-papyrus)Eclipse 建模支持 + +Eclipse Papyrus 框架支持使用 UI 建模定义状态机配置。 + +在 Eclipse 向导中,你可以使用 UML 图语言创建一个新的 Papyrus 模型。在这个示例中,它被命名为`simple-machine`。然后,你可以从各种类型的图中进行选择,并且你必须选择`StateMachine Diagram`。 + +我们希望创建一个具有两个状态(`S1`和`S2`)的机器,其中`S1`是初始状态。然后,我们需要创建事件`DefaultStateMachinePersister`来执行从`S1`到`S2`的转换。在纸莎草纸中,机器看起来就像是下面的例子: + +simple machine + +在幕后,一个原始的 UML 文件将看起来像以下示例: + +``` + + + + + + + + + + + + + + + + +``` + +| |在打开已定义为 UML 的现有模型时,你有三个
文件:`.di`、`.notation`和`.uml`。如果模型不是在你的
Eclipse 的会话中创建的,则它不了解如何打开实际状态
图表。这是 Papyrus 插件中的一个已知问题,并且有一个简单的
解决方法。在 Papyrus 透视图中,你可以看到
你的模型的模型资源管理器。双击 Diagram Statemachine Diagram,其中
指示 Eclipse 在其正确的 Papyrus
建模插件中打开此特定模型。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#using-umlstatemachinemodelfactory)使用`UmlStateMachineModelFactory` + +在项目中安装好 UML 文件之后,你可以使用`StateMachineModelConfigurer`将其导入到配置中,其中`StateMachineModelFactory`与模型相关联。`UmlStateMachineModelFactory`是一个特殊的工厂,它知道如何处理 Eclipse Papyrus\_ 生成的 UML 结构。源 UML 文件既可以作为 Spring `Resource`给出,也可以作为普通的位置字符串给出。下面的示例展示了如何创建`UmlStateMachineModelFactory`的实例: + +``` +@Configuration +@EnableStateMachine +public static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new UmlStateMachineModelFactory("classpath:org/springframework/statemachine/uml/docs/simple-machine.uml"); + } +} +``` + +像往常一样, Spring Statemachine 与守卫和动作一起工作,而守卫和动作被定义为 bean。这些都需要通过 UML 的内部建模结构来连接到 UML 中。下面的小节将展示如何在 UML 定义中定义定制的 Bean 引用。请注意,也可以手动注册特定的方法,而无需将这些方法定义为 bean。 + +如果`UmlStateMachineModelFactory`被创建为 Bean,则其`ResourceLoader`将自动连接以查找已注册的动作和保护。你还可以手动定义`StateMachineComponentResolver`,然后用它来查找这些组件。工厂还有*注册动作*和 *register守卫* 方法,你可以使用它们来注册这些组件。有关此的更多信息,请参见[using`StateMachineComponentResolver`]。 + +当涉及到 Spring StateMachine 本身这样的实现时,UML 模型是相对松散的。 Spring StateMachine 将如何实现大量的特性和功能留给实际的实现。下面的部分将介绍 Spring StateMachine 如何基于 Eclipse Papyrus 插件实现 UML 模型。 + +#### [](#sm-papyrus-statemachinecomponentresolver)使用`StateMachineComponentResolver` + +下一个示例显示如何用`UmlStateMachineModelFactory`定义`StateMachineComponentResolver`,该函数分别注册`my行动`和`myGuard`函数。请注意,这些组件不是作为 bean 创建的。下面的清单展示了这个示例: + +``` +@Configuration +@EnableStateMachine +public static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + UmlStateMachineModelFactory factory = new UmlStateMachineModelFactory( + "classpath:org/springframework/statemachine/uml/docs/simple-machine.uml"); + factory.setStateMachineComponentResolver(stateMachineComponentResolver()); + return factory; + } + + @Bean + public StateMachineComponentResolver stateMachineComponentResolver() { + DefaultStateMachineComponentResolver resolver = new DefaultStateMachineComponentResolver<>(); + resolver.registerAction("myAction", myAction()); + resolver.registerGuard("myGuard", myGuard()); + return resolver; + } + + public Action myAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + } + }; + } + + public Guard myGuard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return false; + } + }; + } +} +``` + +### [](#creating-a-model)创建一个模型 + +我们首先创建一个空的状态机模型,如下图所示: + +papyrus gs 1 + +你可以从创建一个新模型并为其命名开始,如下图所示: + +papyrus gs 2 + +然后,你需要选择静态机械图,如下所示: + +![莎草纸 GS3](images/papyrus-gs-3.png) + +你最终得到的是一个空的状态机。 + +在前面的图片中,你应该创建了一个名为`model`的示例。你应该已经结束了三个文件:`model.di`、`model.notation`和`model.uml`。然后,你可以在任何其他 Eclipse 实例中使用这些文件。此外,还可以将`model.uml`导入 Spring Statemachine。 + +### [](#defining-states)定义状态 + +状态标识符来自图中的组件名称。你的机器中必须有一个初始状态,你可以通过添加一个根元素,然后绘制到你自己的初始状态的转换,如下图所示: + +![莎草纸 GS4](images/papyrus-gs-4.png) + +在前面的图像中,我们添加了一个根元素和一个初始状态(![莎草纸 GS4](images/papyrus-gs-4.png))。然后我们在这两者之间画了一个转换,以表明`S1`是一个初始状态。 + +![莎草纸 GS5](images/papyrus-gs-5.png) + +在前面的图像中,我们添加了第二个状态(`S2`),并添加了 S1 和 S2 之间的转换(表明我们有两个状态)。 + +### [](#defining-events)定义事件 + +要将事件与转换关联起来,需要创建一个信号(在本例中为`E1`)。要这样做,请选择 rootelement new child signal。下图显示了结果: + +![莎草纸 GS6](images/papyrus-gs-6.png) + +然后,你需要用新的信号`E1`来控制一个信号事件。为此,请选择 rootelement New Child SignalEvent。下图显示了结果: + +![莎草纸 GS7](images/papyrus-gs-7.png) + +既然已经定义了`SignalEvent`,就可以使用它将触发器与转换关联起来。更多信息请参见[定义转换](#sm-papyrus-transitions)。 + +#### [](#deferring-an-event)推迟一项活动 + +你可以将事件推迟到更合适的时间来处理它们。在 UML 中,这是从一个状态本身完成的。选择任何状态,在![莎草纸 GS10](images/papyrus-gs-10.png)下创建一个新的触发器,并选择与要延迟的信号匹配的 SignalEvent。 + +### [](#sm-papyrus-transitions)定义转换 + +你可以通过在源状态和目标状态之间画一条转换线来创建转换。在前面的图像中,我们有状态`S1`和`S2`以及两者之间的匿名转换。我们希望将事件`E1`与该转换关联起来。我们选择一个转换,创建一个新的触发器,并为此定义 SignalEvente1,如下图所示: + +![莎草纸 GS8](images/papyrus-gs-8.png) + +这给出了类似于下图所示的安排: + +![莎草纸 GS9](images/papyrus-gs-9.png) + +| |如果在转换中省略了 SignalEvent,那么它将变成
匿名转换。| +|---|---------------------------------------------------------------------------------| + +### [](#defining-timers)定时器 + +转换也可以基于定时事件发生。 Spring Statemachine 支持两种类型的计时器,一种是在背景上连续触发的计时器,另一种是在进入状态时延迟触发一次的计时器。 + +若要向 Model Explorer 添加一个新的 TimeEvent 子程序,请在将表达式定义为 literalInteger 时进行修改。它的值(以毫秒为单位)成为计时器。离开是相对错误的,以使计时器连续射击。 + +![莎草纸 GS10](images/papyrus-gs-10.png) + +要定义一个基于时间的事件,在进入状态时触发,该过程与前面描述的完全相同,但 Leave 相对设置为 true。下图显示了结果: + +![莎草纸 GS11](images/papyrus-gs-11.png) + +然后,用户可以选择这些定时事件中的一个,而不是用于特定转换的信号事件。 + +### [](#sm-papyrus-choice)定义一个选择 + +选择是通过将一个传入的转换绘制到一个选择状态,并将多个传出的转换从它绘制到目标状态来定义的。我们`StateConfigurer`中的配置模型允许你定义一个 if/elseif/else 结构。然而,对于 UML,我们需要使用单独的保护来进行传出转换。 + +你必须确保为转换定义的保护不会重叠,因此,无论发生什么情况,在任何给定的时间都只有一个保护的值为 true。这为选择分支评估提供了精确且可预测的结果。我们还建议在没有保护的情况下保留一次转换,以便保证至少有一个转换路径。下图显示了用三个分支进行选择的结果: + +![莎草纸 GS16](images/papyrus-gs-16.png) + +| |Junction 的工作原理类似,只是它允许多次传入
转换。因此,其行为与选择相比纯粹是
学术性的。选择传出转换的实际逻辑完全相同。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#defining-a-junction)定义交点 + +见[定义一个选择](#sm-papyrus-choice)。 + +### [](#defining-entry-and-exit-points)定义进场点和出场点 + +你可以使用 EntryPoint 和 ExitPoint 来创建带有子状态的状态的受控入口和出口。在下面的状态图中,事件`E1`和`E2`通过进入和退出状态`S2`而具有正常状态行为,其中正常状态行为是通过进入初始状态`S21`而发生的。 + +使用事件`E3`将机器带到`ENTRY`入口点,然后在任何时候都不激活初始状态`S22`。类似地,带有事件`StateMachineBuilder`的 exitPoint 控制特定的 exit into state`S4`,而来自`S2`的正常 exit 行为将使机器进入状态`S3`。在状态`S22`时,你可以从事件`E4`和`E2`中进行选择,以使机器分别进入状态`S3`或`S4`。下图显示了结果: + +![莎草纸 GS17](images/papyrus-gs-17.png) + +| |如果状态被定义为一个子机引用,并且需要使用入口和出口点,
你必须在外部定义一个 ConnectionPointReference,其入口和出口引用设置为指向一个子机引用内的正确的入口或出口点
。只有在此之后,才有可能实现
目标转换,从而正确地将
子机器引用从外部链接到内部。使用 ConnectionPointReference,你可能需要
才能从 Properties Advanced UML
Enter/Exit 中找到这些设置。UML 规范允许你定义多个条目和出口。但是,
使用状态机时,只允许使用一个状态机。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#defining-history-states)定义历史状态 + +在研究历史国家时,有三个不同的概念在起作用。UML 定义了一段深刻的历史和一段浅薄的历史。当历史状态未知时,默认的历史状态开始起作用。以下各节对此作了说明。 + +#### [](#shallow-history)浅历史 + +在下面的图像中,选择了浅历史,并定义了一个过渡到它: + +![莎草纸 GS18](images/papyrus-gs-18.png) + +#### [](#deep-history)深历史 + +深度历史被用来表示具有其他深度嵌套状态的状态,从而为保存整个嵌套状态结构提供了机会。下图显示了一个使用了深层历史的定义: + +![Papyrus GS19](images/papyrus-gs-19.png) + +#### [](#default-history)默认历史记录 + +如果在某个历史记录上的转换在达到其最终状态之前没有被输入,则该历史记录上的转换就会终止,那么可以使用默认的历史记录机制强制转换到特定子状态。要做到这一点,你必须定义一个转换到这个默认状态。这是从`SH`到`S22`的转换。 + +在下面的图像中,如果状态`S22`从未被激活,则输入状态`S2`,因为其历史从未被记录。如果状态`S2`一直处于活动状态,则选择`S20`或`S21`。 + +![莎草纸 GS20](images/papyrus-gs-20.png) + +### [](#defining-forks-and-joins)定义 fork 和 join + +在纸莎草纸中,fork 和 join 都表示为 bar。如下一张图片所示,需要绘制一个从`FORK`到`S2`状态的输出转换,以具有正交区域。`JOIN`则是相反的,在这种情况下,从传入的转换中收集连接的状态。 + +![Papyrus GS21](images/papyrus-gs-21.png) + +### [](#defining-actions)定义动作 + +你可以通过使用一个行为来处理 SWTATE 进入和退出操作。有关此的更多信息,请参见[Defining a Bean Reference](#sm-papyrus-beanref)。 + +#### [](#using-an-initial-action)使用初始操作 + +在 UML 中定义了一个初始动作(如[配置动作](#statemachine-config-actions)中所示),在转换过程中添加一个动作,该动作将从初始状态标记引导到实际状态。然后在状态机启动时运行该操作。 + +### [](#defining-guards)定义守卫 + +你可以通过首先添加一个约束,然后将其规范定义为 opaqueExpression 来定义一个 Guard,其工作方式与[Defining a Bean Reference](#sm-papyrus-beanref)相同。 + +### [](#sm-papyrus-beanref)定义 Bean 引用 + +当需要在任何 UML 效果、操作或保护中进行 Bean 引用时,可以使用`FunctionBehavior`或`OpaqueBehavior`进行引用,其中定义的语言需要是`bean`,并且语言体 msut 具有 Bean 引用 ID。 + +### [](#sm-papyrus-spelref)定义 spel 引用 + +当你需要在任何 UML 效果、操作或保护中使用 spel 表达式而不是 Bean 引用时,你可以通过使用`FunctionBehavior`或`OpaqueBehavior`来实现,其中定义的语言需要是`spel`,并且语言主体必须是 spel 表达式。 + +### [](#sm-papyrus-submachineref)使用子机器引用 + +通常,当你使用子状态时,你会将这些状态绘制到状态图中。图表可能会变得太复杂、太大而无法遵循,因此我们也支持将子状态定义为状态机引用。 + +要创建子机器引用,你必须首先创建一个新的图,并给它起一个名称(例如,substatemachine 图)。下图显示了要使用的菜单选择: + +![莎草纸 GS12](images/papyrus-gs-12.png) + +给这张新的图表你所需要的图案。下面的图片显示了一个简单的设计作为示例: + +![莎草纸 GS13](images/papyrus-gs-13.png) + +从要链接的状态(在本例中,M 状态`S2`)中,单击`Submachine`字段并选择链接的机器(在我们的示例中,`SubStateMachine`)。 + +![莎草纸 GS14](images/papyrus-gs-14.png) + +最后,在下面的图像中,可以看到状态`S2`作为子状态链接到`SubStateMachine`。 + +![莎草纸 GS15](images/papyrus-gs-15.png) + +### [](#sm-papyrus-import)使用机器导入 + +在 UML 文件可以引用其他模型的情况下,也可以使用导入功能。 + +![莎草纸 GS22](images/papyrus-gs-22.png) + +在`UmlStateMachineModelFactory`中,可以使用额外的资源或位置来定义引用的模型文件。 + +``` +@Configuration +@EnableStateMachine +public static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new UmlStateMachineModelFactory( + "classpath:org/springframework/statemachine/uml/import-main/import-main.uml", + new String[] { "classpath:org/springframework/statemachine/uml/import-sub/import-sub.uml" }); + } +} +``` + +| |UML 模型中的文件之间的链接需要是相对的
,否则,当模型文件从
Classpath 复制到临时目录时,事情就会中断,这样 Eclipse 解析类就可以
读取这些文件。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#sm-repository)存储库支持 + +本节包含与在 Spring statemachine 中使用“ Spring data repositories”有关的文档。 + +### [](#sm-repository-config)存储库配置 + +你可以将机器配置保留在外部存储中,可以根据需要从该存储中加载它,而不是通过使用 Java 配置或基于 UML 的配置来创建静态配置。这种集成通过 Spring 数据存储库抽象工作。 + +我们创建了一个特殊的`StateMachineModelFactory`实现,称为`RepositoryStateMachineModelFactory`。它可以使用基本存储库接口(`StateRepository`,`TransitionRepository`,`ActionRepository`和`GuardRepository`)和基本实体接口(`RepositoryState`,`RepositoryTransition`,`RepositoryAction`,和`RepositoryGuard`)。 + +由于实体和存储库在 Spring 数据中的工作方式,从用户的角度来看,读访问可以像在`RepositoryStateMachineModelFactory`中所做的那样完全抽象。不需要知道存储库所使用的实际映射实体类。写入存储库总是依赖于使用真正的存储库特定的实体类。从机器配置的角度来看,我们不需要知道这些,这意味着我们不需要知道实际实现是 JPA、Redis 还是 Spring 数据支持的任何其他方式。当你手动尝试将新的状态或转换写入支持的存储库时,使用实际的与存储库相关的实体类将起作用。 + +| |`RepositoryState`和`RepositoryTransition`的实体类有一个`machineId`字段,该字段由你支配,可以用于
不同配置之间的区分——例如,如果机器是通过
构建的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +实际的实现在后面的小节中有详细的说明。下面的图像是存储库配置的 UML 等效状态图。 + +![SM 存储库 SimpleMachine](images/sm-repository-simplemachine.png) + +图 1。SimpleMachine + +![SM 存储库 SimpleSubmachine](images/sm-repository-simplesubmachine.png) + +图 2。SimpleSubmachine + +![SM 存储库展示了机器](images/sm-repository-showcasemachine.png) + +图 3。ShowcaseMachine + +#### [](#sm-repository-config-jpa) JPA + +JPA 的实际存储库实现是`JpaStateRepository`、`JpaTransitionRepository`、`JpaActionRepository`和`JpaGuardRepository`,它们分别由实体类`JpaRepositoryState`、`JpaRepositoryTransition`、`JpaRepositoryAction`和`JpaRepositoryGuard`支持。 + +| |不幸的是,版本“1.2.8”必须对 JPA 的实体
模型中使用的表名进行更改。以前,生成的表名
总是有`JPA_REPOSITORY_`的前缀,派生自实体类
名称。由于这导致了数据库对数据库对象长度施加
限制的问题,因此所有实体类都有
专门的定义来强制表名。例如,`JPA_REPOSITORY_STATE`现在是“state”——以其他
ntity 类为例。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +JPA 手动更新状态和转换的通用方法在下面的示例中显示(与[SimpleMachine](#image-sm-repository-simplemachine)中所示的机器等效): + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +void addConfig() { + JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true); + JpaRepositoryState stateS2 = new JpaRepositoryState("S2"); + JpaRepositoryState stateS3 = new JpaRepositoryState("S3"); + + stateRepository.save(stateS1); + stateRepository.save(stateS2); + stateRepository.save(stateS3); + + JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "E1"); + JpaRepositoryTransition transitionS2ToS3 = new JpaRepositoryTransition(stateS2, stateS3, "E2"); + + transitionRepository.save(transitionS1ToS2); + transitionRepository.save(transitionS2ToS3); +} +``` + +下面的示例也相当于[SimpleSubmachine](#image-sm-repository-simplesubmachine)中所示的机器。 + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +void addConfig() { + JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true); + JpaRepositoryState stateS2 = new JpaRepositoryState("S2"); + JpaRepositoryState stateS3 = new JpaRepositoryState("S3"); + + JpaRepositoryState stateS21 = new JpaRepositoryState("S21", true); + stateS21.setParentState(stateS2); + JpaRepositoryState stateS22 = new JpaRepositoryState("S22"); + stateS22.setParentState(stateS2); + + stateRepository.save(stateS1); + stateRepository.save(stateS2); + stateRepository.save(stateS3); + stateRepository.save(stateS21); + stateRepository.save(stateS22); + + JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "E1"); + JpaRepositoryTransition transitionS2ToS3 = new JpaRepositoryTransition(stateS21, stateS22, "E2"); + JpaRepositoryTransition transitionS21ToS22 = new JpaRepositoryTransition(stateS2, stateS3, "E3"); + + transitionRepository.save(transitionS1ToS2); + transitionRepository.save(transitionS2ToS3); + transitionRepository.save(transitionS21ToS22); +} +``` + +首先,你必须访问所有存储库。下面的示例展示了如何做到这一点: + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +@Autowired +ActionRepository actionRepository; + +@Autowired +GuardRepository guardRepository; +``` + +其次,你要创造行动和守卫。下面的示例展示了如何做到这一点: + +``` +JpaRepositoryGuard foo0Guard = new JpaRepositoryGuard(); +foo0Guard.setName("foo0Guard"); + +JpaRepositoryGuard foo1Guard = new JpaRepositoryGuard(); +foo1Guard.setName("foo1Guard"); + +JpaRepositoryAction fooAction = new JpaRepositoryAction(); +fooAction.setName("fooAction"); + +guardRepository.save(foo0Guard); +guardRepository.save(foo1Guard); +actionRepository.save(fooAction); +``` + +第三,你必须创造国家。下面的示例展示了如何做到这一点: + +``` +JpaRepositoryState stateS0 = new JpaRepositoryState("S0", true); +stateS0.setInitialAction(fooAction); +JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true); +stateS1.setParentState(stateS0); +JpaRepositoryState stateS11 = new JpaRepositoryState("S11", true); +stateS11.setParentState(stateS1); +JpaRepositoryState stateS12 = new JpaRepositoryState("S12"); +stateS12.setParentState(stateS1); +JpaRepositoryState stateS2 = new JpaRepositoryState("S2"); +stateS2.setParentState(stateS0); +JpaRepositoryState stateS21 = new JpaRepositoryState("S21", true); +stateS21.setParentState(stateS2); +JpaRepositoryState stateS211 = new JpaRepositoryState("S211", true); +stateS211.setParentState(stateS21); +JpaRepositoryState stateS212 = new JpaRepositoryState("S212"); +stateS212.setParentState(stateS21); + +stateRepository.save(stateS0); +stateRepository.save(stateS1); +stateRepository.save(stateS11); +stateRepository.save(stateS12); +stateRepository.save(stateS2); +stateRepository.save(stateS21); +stateRepository.save(stateS211); +stateRepository.save(stateS212); +``` + +第四,也是最后一点,你必须创建转换。下面的示例展示了如何做到这一点: + +``` +JpaRepositoryTransition transitionS1ToS1 = new JpaRepositoryTransition(stateS1, stateS1, "A"); +transitionS1ToS1.setGuard(foo1Guard); + +JpaRepositoryTransition transitionS1ToS11 = new JpaRepositoryTransition(stateS1, stateS11, "B"); +JpaRepositoryTransition transitionS21ToS211 = new JpaRepositoryTransition(stateS21, stateS211, "B"); +JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "C"); +JpaRepositoryTransition transitionS1ToS0 = new JpaRepositoryTransition(stateS1, stateS0, "D"); +JpaRepositoryTransition transitionS211ToS21 = new JpaRepositoryTransition(stateS211, stateS21, "D"); +JpaRepositoryTransition transitionS0ToS211 = new JpaRepositoryTransition(stateS0, stateS211, "E"); +JpaRepositoryTransition transitionS1ToS211 = new JpaRepositoryTransition(stateS1, stateS211, "F"); +JpaRepositoryTransition transitionS2ToS21 = new JpaRepositoryTransition(stateS2, stateS21, "F"); +JpaRepositoryTransition transitionS11ToS211 = new JpaRepositoryTransition(stateS11, stateS211, "G"); + +JpaRepositoryTransition transitionS0 = new JpaRepositoryTransition(stateS0, stateS0, "H"); +transitionS0.setKind(TransitionKind.INTERNAL); +transitionS0.setGuard(foo0Guard); +transitionS0.setActions(new HashSet<>(Arrays.asList(fooAction))); + +JpaRepositoryTransition transitionS1 = new JpaRepositoryTransition(stateS1, stateS1, "H"); +transitionS1.setKind(TransitionKind.INTERNAL); + +JpaRepositoryTransition transitionS2 = new JpaRepositoryTransition(stateS2, stateS2, "H"); +transitionS2.setKind(TransitionKind.INTERNAL); +transitionS2.setGuard(foo1Guard); +transitionS2.setActions(new HashSet<>(Arrays.asList(fooAction))); + +JpaRepositoryTransition transitionS11ToS12 = new JpaRepositoryTransition(stateS11, stateS12, "I"); +JpaRepositoryTransition transitionS12ToS212 = new JpaRepositoryTransition(stateS12, stateS212, "I"); +JpaRepositoryTransition transitionS211ToS12 = new JpaRepositoryTransition(stateS211, stateS12, "I"); + +JpaRepositoryTransition transitionS11 = new JpaRepositoryTransition(stateS11, stateS11, "J"); +JpaRepositoryTransition transitionS2ToS1 = new JpaRepositoryTransition(stateS2, stateS1, "K"); + +transitionRepository.save(transitionS1ToS1); +transitionRepository.save(transitionS1ToS11); +transitionRepository.save(transitionS21ToS211); +transitionRepository.save(transitionS1ToS2); +transitionRepository.save(transitionS1ToS0); +transitionRepository.save(transitionS211ToS21); +transitionRepository.save(transitionS0ToS211); +transitionRepository.save(transitionS1ToS211); +transitionRepository.save(transitionS2ToS21); +transitionRepository.save(transitionS11ToS211); +transitionRepository.save(transitionS0); +transitionRepository.save(transitionS1); +transitionRepository.save(transitionS2); +transitionRepository.save(transitionS11ToS12); +transitionRepository.save(transitionS12ToS212); +transitionRepository.save(transitionS211ToS12); +transitionRepository.save(transitionS11); +transitionRepository.save(transitionS2ToS1); +``` + +你可以找到一个完整的示例[here](#statemachine-examples-datajpa)。这个示例还展示了如何从具有实体类定义的现有 JSON 文件中预填充存储库。 + +#### [](#sm-repository-config-redis)Redis + +Redis 实例的实际存储库实现是`RedisStateRepository`、`RedisTransitionRepository`、`RedisActionRepository`和`RedisGuardRepository`,它们分别由实体类`RedisRepositoryState`、`RedisRepositoryTransition`、`RedisRepositoryAction`和`RedisRepositoryGuard`支持。 + +下一个示例展示了手动更新 REDI 状态和转换的通用方法。这相当于[SimpleMachine](#image-sm-repository-simplemachine)中所示的机器。 + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +void addConfig() { + RedisRepositoryState stateS1 = new RedisRepositoryState("S1", true); + RedisRepositoryState stateS2 = new RedisRepositoryState("S2"); + RedisRepositoryState stateS3 = new RedisRepositoryState("S3"); + + stateRepository.save(stateS1); + stateRepository.save(stateS2); + stateRepository.save(stateS3); + + RedisRepositoryTransition transitionS1ToS2 = new RedisRepositoryTransition(stateS1, stateS2, "E1"); + RedisRepositoryTransition transitionS2ToS3 = new RedisRepositoryTransition(stateS2, stateS3, "E2"); + + transitionRepository.save(transitionS1ToS2); + transitionRepository.save(transitionS2ToS3); +} +``` + +下面的示例相当于[SimpleSubmachine](#image-sm-repository-simplesubmachine)中所示的机器: + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +void addConfig() { + RedisRepositoryState stateS1 = new RedisRepositoryState("S1", true); + RedisRepositoryState stateS2 = new RedisRepositoryState("S2"); + RedisRepositoryState stateS3 = new RedisRepositoryState("S3"); + + stateRepository.save(stateS1); + stateRepository.save(stateS2); + stateRepository.save(stateS3); + + RedisRepositoryTransition transitionS1ToS2 = new RedisRepositoryTransition(stateS1, stateS2, "E1"); + RedisRepositoryTransition transitionS2ToS3 = new RedisRepositoryTransition(stateS2, stateS3, "E2"); + + transitionRepository.save(transitionS1ToS2); + transitionRepository.save(transitionS2ToS3); +} +``` + +#### [](#sm-repository-config-mongodb)MongoDB + +MongoDB 实例的实际存储库实现是`MongoDbStateRepository`、`MongoDbTransitionRepository`、`MongoDbActionRepository`和`MongoDbGuardRepository`,它们分别由实体类`MongoDbRepositoryState`、`MongoDbRepositoryTransition`、`MongoDbRepositoryAction`和`MongoDbRepositoryGuard`支持。 + +下一个示例展示了手动更新 MongoDB 的状态和转换的通用方法。这相当于[SimpleMachine](#image-sm-repository-simplemachine)中所示的机器。 + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +void addConfig() { + MongoDbRepositoryState stateS1 = new MongoDbRepositoryState("S1", true); + MongoDbRepositoryState stateS2 = new MongoDbRepositoryState("S2"); + MongoDbRepositoryState stateS3 = new MongoDbRepositoryState("S3"); + + stateRepository.save(stateS1); + stateRepository.save(stateS2); + stateRepository.save(stateS3); + + MongoDbRepositoryTransition transitionS1ToS2 = new MongoDbRepositoryTransition(stateS1, stateS2, "E1"); + MongoDbRepositoryTransition transitionS2ToS3 = new MongoDbRepositoryTransition(stateS2, stateS3, "E2"); + + transitionRepository.save(transitionS1ToS2); + transitionRepository.save(transitionS2ToS3); +} +``` + +下面的示例与[SimpleSubmachine](#image-sm-repository-simplesubmachine)中所示的机器等价。 + +``` +@Autowired +StateRepository stateRepository; + +@Autowired +TransitionRepository transitionRepository; + +void addConfig() { + MongoDbRepositoryState stateS1 = new MongoDbRepositoryState("S1", true); + MongoDbRepositoryState stateS2 = new MongoDbRepositoryState("S2"); + MongoDbRepositoryState stateS3 = new MongoDbRepositoryState("S3"); + + MongoDbRepositoryState stateS21 = new MongoDbRepositoryState("S21", true); + stateS21.setParentState(stateS2); + MongoDbRepositoryState stateS22 = new MongoDbRepositoryState("S22"); + stateS22.setParentState(stateS2); + + stateRepository.save(stateS1); + stateRepository.save(stateS2); + stateRepository.save(stateS3); + stateRepository.save(stateS21); + stateRepository.save(stateS22); + + MongoDbRepositoryTransition transitionS1ToS2 = new MongoDbRepositoryTransition(stateS1, stateS2, "E1"); + MongoDbRepositoryTransition transitionS2ToS3 = new MongoDbRepositoryTransition(stateS21, stateS22, "E2"); + MongoDbRepositoryTransition transitionS21ToS22 = new MongoDbRepositoryTransition(stateS2, stateS3, "E3"); + + transitionRepository.save(transitionS1ToS2); + transitionRepository.save(transitionS2ToS3); + transitionRepository.save(transitionS21ToS22); +} +``` + +### [](#sm-repository-persistence)存储库持久性 + +除了存储机器配置(如[存储库配置](#sm-repository-config)所示),在外部存储库中,还可以将机器持久化到存储库中。 + +`StateMachineRepository`接口是与机器持久性交互的中心接入点,并由实体类`RepositoryStateMachine`支持。 + +#### [](#sm-repository-persistence-jpa) JPA + +JPA 的实际存储库实现是`JpaStateMachineRepository`,它由实体类`JpaRepositoryStateMachine`支持。 + +下面的示例展示了在 JPA 中持久化机器的通用方法: + +``` +@Autowired +StateMachineRepository stateMachineRepository; + +void persist() { + + JpaRepositoryStateMachine machine = new JpaRepositoryStateMachine(); + machine.setMachineId("machine"); + machine.setState("S1"); + // raw byte[] representation of a context + machine.setStateMachineContext(new byte[] { 0 }); + + stateMachineRepository.save(machine); +} +``` + +#### [](#sm-repository-persistence-redis)Redis + +Redis 的实际存储库实现是`RedisStateMachineRepository`,它由实体类`RedisRepositoryStateMachine`支持。 + +下面的示例展示了为 Redis 持久化机器的通用方法: + +``` +@Autowired +StateMachineRepository stateMachineRepository; + +void persist() { + + RedisRepositoryStateMachine machine = new RedisRepositoryStateMachine(); + machine.setMachineId("machine"); + machine.setState("S1"); + // raw byte[] representation of a context + machine.setStateMachineContext(new byte[] { 0 }); + + stateMachineRepository.save(machine); +} +``` + +#### [](#sm-repository-persistence-mongodb)MongoDB + +MongoDB 的实际存储库实现是`MongoDbStateMachineRepository`,它由实体类`MongoDbRepositoryStateMachine`支持。 + +下面的示例展示了为 MongoDB 持久化机器的通用方法: + +``` +@Autowired +StateMachineRepository stateMachineRepository; + +void persist() { + + MongoDbRepositoryStateMachine machine = new MongoDbRepositoryStateMachine(); + machine.setMachineId("machine"); + machine.setState("S1"); + // raw byte[] representation of a context + machine.setStateMachineContext(new byte[] { 0 }); + + stateMachineRepository.save(machine); +} +``` + +# [](#statemachine-recipes)食谱 + +本章包含现有内置状态机配方的文档。 + +Spring 机械是一个基础框架。也就是说,除了 Spring 框架之外,它没有更高级别的功能或许多依赖关系。因此,正确使用状态机可能很困难。为了提供帮助,我们创建了一组解决常见用例的配方模块。 + +食谱到底是什么?状态机配方是一个解决常见用例的模块。从本质上讲,状态机配方既是一个示例,也是我们试图使你易于重用和扩展的一个示例。 + +| |食谱是对 Spring
Statemachine 项目进行外部贡献的一种很好的方式。如果你还没有准备好为
框架核心本身做出贡献,那么一个自定义的通用配方是与其他用户共享
功能的一种很好的方法。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#statemachine-recipes-persist)坚持 + +持久化配方是一个简单的实用程序,它允许你使用单个状态机实例来持久化和更新存储库中任意项的状态。 + +这个配方的主要类别是`PersistStateMachineHandler`,它做了三个假设: + +* 一个`StateMachine`的实例需要与`PersistStateMachineHandler`一起使用。请注意,状态和事件必须是`String`的类型。 + +* `PersistStateChangeListener`需要向处理程序注册才能对持久请求做出反应。 + +* `handleEventWithState`方法用于编排状态更改。 + +你可以在[Persist](#statemachine-examples-persist)处找到一个示例,该示例展示了如何使用此食谱。 + +## [](#statemachine-recipes-tasks)任务 + +任务配方是运行使用状态机的`Runnable`实例的 DAG(有向丙烯酸图)的概念。这个配方是根据[Tasks](#statemachine-examples-tasks)样本中介绍的想法开发的。 + +下一张图片展示了状态机的一般概念。在此状态图中,`TASKS`下的所有内容都显示了单个任务如何执行的通用概念。因为这个配方允许你注册一个任务的深层次 dag(这意味着实际状态图将是一个深嵌套子状态和区域的集合),所以我们没有必要更精确。 + +例如,如果只有两个已注册的任务,当`TASK_id`被替换为`TASK_1`和`TASK_2`(假设已注册的任务 ID 为`1`和`2`)时,下面的状态图将是正确的。 + +statechart9 + +执行`Runnable`可能会导致错误。特别是如果涉及复杂的 DAG 任务,你希望有一种方法来处理任务执行错误,然后有一种方法来继续执行,而不执行已经成功执行的任务。另外,如果可以自动处理一些执行错误,那将是很好的。作为最后的后备措施,如果无法自动处理错误,则将状态机置入用户可以手动处理错误的状态。 + +`TasksHandler`包含用于配置处理程序实例的构建器方法,并遵循一个简单的构建器模式。你可以使用这个构建器来注册`Runnable`任务和`TasksListener`实例并定义`StateMachinePersist`钩子。 + +现在,我们可以使用一个运行简单睡眠的简单`Runnable`,如下例所示: + +``` +private Runnable sleepRunnable() { + return new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + } + }; +} +``` + +| |前面的示例是本章所有示例的基础。| +|---|--------------------------------------------------------------------------| + +要执行多个`sleepRunnable`任务,你可以从`TasksHandler`注册任务并执行`runTasks()`方法,如下例所示: + +``` +TasksHandler handler = TasksHandler.builder() + .task("1", sleepRunnable()) + .task("2", sleepRunnable()) + .task("3", sleepRunnable()) + .build(); + +handler.runTasks(); +``` + +要侦听任务执行中发生的情况,你可以用`TasksHandler`注册`TasksListener`的实例。如果你不想实现完整的接口,则此食谱提供一个适配器`TasksListenerAdapter`。侦听器提供了各种钩子来侦听任务执行事件。下面的示例显示了`MyTasksListener`类的定义: + +``` +private class MyTasksListener extends TasksListenerAdapter { + + @Override + public void onTasksStarted() { + } + + @Override + public void onTasksContinue() { + } + + @Override + public void onTaskPreExecute(Object id) { + } + + @Override + public void onTaskPostExecute(Object id) { + } + + @Override + public void onTaskFailed(Object id, Exception exception) { + } + + @Override + public void onTaskSuccess(Object id) { + } + + @Override + public void onTasksSuccess() { + } + + @Override + public void onTasksError() { + } + + @Override + public void onTasksAutomaticFix(TasksHandler handler, StateContext context) { + } +} +``` + +你可以使用构建器注册侦听器,也可以直接使用`TasksHandler`注册侦听器,如下例所示: + +``` +MyTasksListener listener1 = new MyTasksListener(); +MyTasksListener listener2 = new MyTasksListener(); + +TasksHandler handler = TasksHandler.builder() + .task("1", sleepRunnable()) + .task("2", sleepRunnable()) + .task("3", sleepRunnable()) + .listener(listener1) + .build(); + +handler.addTasksListener(listener2); +handler.removeTasksListener(listener2); + +handler.runTasks(); +``` + +每个任务都需要具有唯一的标识符,并且(可选地)可以将任务定义为子任务。实际上,这会产生大量的任务。下面的示例展示了如何创建一个由任务组成的深度嵌套 DAG: + +``` +TasksHandler handler = TasksHandler.builder() + .task("1", sleepRunnable()) + .task("1", "12", sleepRunnable()) + .task("1", "13", sleepRunnable()) + .task("2", sleepRunnable()) + .task("2", "22", sleepRunnable()) + .task("2", "23", sleepRunnable()) + .task("3", sleepRunnable()) + .task("3", "32", sleepRunnable()) + .task("3", "33", sleepRunnable()) + .build(); + +handler.runTasks(); +``` + +当发生错误并且运行这些任务的状态机进入`ERROR`状态时,你可以调用`fixCurrentProblems`处理程序方法来重置保留在状态机的扩展状态变量中的任务的当前状态。然后,你可以使用`continueFromError`处理程序方法来指示状态机从`ERROR`状态转换回`READY`状态,在那里你可以再次运行任务。下面的示例展示了如何做到这一点: + +``` +TasksHandler handler = TasksHandler.builder() + .task("1", sleepRunnable()) + .task("2", sleepRunnable()) + .task("3", sleepRunnable()) + .build(); + + handler.runTasks(); + handler.fixCurrentProblems(); + handler.continueFromError(); +``` + +# [](#statemachine-examples)状态机示例 + +引用文档的这一部分解释了状态机以及示例代码和 UML 状态图的使用。在表示状态图、 Spring StateMachine 配置和应用程序使用状态机所做的工作之间的关系时,我们使用了一些快捷方式。对于完整的示例,你应该研究样例存储库。 + +在正常的构建周期中,样本直接从主源分布构建。本章包括以下示例: + +[Turnstile](#statemachine-examples-turnstile) + +[旋转门反应式](#statemachine-examples-turnstilereactive) + +[Showcase](#statemachine-examples-showcase) + +[CD Player](#statemachine-examples-cdplayer) + +[Tasks](#statemachine-examples-tasks) + +[Washer](#statemachine-examples-washer) + +[Persist](#statemachine-examples-persist) + +[Zookeeper](#statemachine-examples-zookeeper) + +[Web](#statemachine-examples-web) + +[Scope](#statemachine-examples-scope) + +[Security](#statemachine-examples-security) + +[活动服务](#statemachine-examples-eventservice) + +[Deploy](#statemachine-examples-deploy) + +[订单运输](#statemachine-examples-ordershipping) + +[JPA 配置](#statemachine-examples-datajpa) + +[数据持续存在](#statemachine-examples-datapersist) + +[Data JPA Persist](#statemachine-examples-datajpapersist) + +[多数据持久化](#statemachine-examples-datajpamultipersist) + +[Monitoring](#statemachine-examples-monitoring) + +下面的清单展示了如何构建示例: + +``` +./gradlew clean build -x test +``` + +每个示例都位于`spring-statemachine-samples`下的自己的目录中。这些示例基于 Spring boot 和 Spring shell,你可以在每个示例项目的`build/libs`目录下找到常用的引导 fat jar。 + +| |我们在本节中引用的 JAR 的文件名是在构建这个文档的
过程中填充的,这意味着,如果你从
Master 构建示例,那么你的文件带有`BUILD-SNAPSHOT`后缀。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#statemachine-examples-turnstile)转门 + +旋转门是一种简单的设备,如果付款,它就可以让你访问。这是一个很容易用状态机建模的概念。在最简单的形式中,只有两种状态:`LOCKED`和`UNLOCKED`。两个事件,`COIN`和`PUSH`可能发生,这取决于是否有人付款或试图通过旋转栅门。下图显示了状态机: + +statechart1 + +下面的清单显示了定义可能状态的枚举: + +国家 + +``` +public enum States { + LOCKED, UNLOCKED +} +``` + +下面的清单显示了定义事件的枚举: + +事件 + +``` +public enum Events { + COIN, PUSH +} +``` + +下面的清单显示了配置状态机的代码: + +Configuration + +``` +@Configuration +@EnableStateMachine +static class StateMachineConfig + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.LOCKED) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.LOCKED) + .target(States.UNLOCKED) + .event(Events.COIN) + .and() + .withExternal() + .source(States.UNLOCKED) + .target(States.LOCKED) + .event(Events.PUSH); + } + +} +``` + +通过运行`turnstile`样例,你可以看到这个样例状态机如何与事件交互。下面的清单显示了如何执行此操作,并显示了该命令的输出: + +``` +$ java -jar spring-statemachine-samples-turnstile-3.0.1.jar + +sm>sm print ++----------------------------------------------------------------+ +| SM | ++----------------------------------------------------------------+ +| | +| +----------------+ +----------------+ | +| *-->| LOCKED | | UNLOCKED | | +| +----------------+ +----------------+ | +| +---| entry/ | | entry/ |---+ | +| | | exit/ | | exit/ | | | +| | | | | | | | +| PUSH| | |---COIN-->| | |COIN | +| | | | | | | | +| | | | | | | | +| | | |<--PUSH---| | | | +| +-->| | | |<--+ | +| | | | | | +| +----------------+ +----------------+ | +| | ++----------------------------------------------------------------+ + +sm>sm start +State changed to LOCKED +State machine started + +sm>sm event COIN +State changed to UNLOCKED +Event COIN send + +sm>sm event PUSH +State changed to LOCKED +Event PUSH send +``` + +## [](#statemachine-examples-turnstilereactive)转门反应式 + +旋转门反应是对[Turnstile](#statemachine-examples-turnstile)样本的增强,使用相同的*机械*概念,并添加一个与*机械*反应界面进行反应通信的反应 Web 层。 + +`StateMachineController`是一个简单的`@RestController`,其中我们自动连接`StateMachine`。 + +``` +@Autowired +private StateMachine stateMachine; +``` + +我们创建第一个映射来返回机器状态。由于状态不是从机器反作用地产生的,因此我们可以*推迟*它,这样当订阅返回的`Mono`时,就会请求实际的状态。 + +``` +@GetMapping("/state") +public Mono state() { + return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId())); +} +``` + +要将单个事件或多个事件发送到机器,我们可以在传入和传出层中使用`Flux`。`EventResult`这里仅针对此示例,并简单地包装`ResultType`和事件。 + +``` +@PostMapping("/events") +public Flux events(@RequestBody Flux eventData) { + return eventData + .filter(ed -> ed.getEvent() != null) + .map(ed -> MessageBuilder.withPayload(ed.getEvent()).build()) + .flatMap(m -> stateMachine.sendEvent(Mono.just(m))) + .map(EventResult::new); +} +``` + +你可以使用以下命令来运行示例: + +``` +$ java -jar spring-statemachine-samples-turnstilereactive-3.0.1.jar +``` + +获得状态的示例: + +``` +GET http://localhost:8080/state +``` + +然后会回应: + +``` +"LOCKED" +``` + +发送事件的示例: + +``` +POST http://localhost:8080/events +content-type: application/json + +{ + "event": "COIN" +} +``` + +然后会回应: + +``` +[ + { + "event": "COIN", + "resultType": "ACCEPTED" + } +] +``` + +你可以发布多个事件: + +``` +POST http://localhost:8080/events +content-type: application/json + +[ + { + "event": "COIN" + }, + { + "event": "PUSH" + } +] +``` + +然后,Response 包含两个事件的结果: + +``` +[ + { + "event": "COIN", + "resultType": "ACCEPTED" + }, + { + "event": "PUSH", + "resultType": "ACCEPTED" + } +] +``` + +## [](#statemachine-examples-showcase)展示 + +Showcase 是一个复杂的状态机,它显示了所有可能的转换拓扑,最多可达四个状态嵌套级别。下图显示了状态机: + +statechart2 + +下面的清单显示了定义可能状态的枚举: + +国家 + +``` +public enum States { + S0, S1, S11, S12, S2, S21, S211, S212 +} +``` + +下面的清单显示了定义事件的枚举: + +事件 + +``` +public enum Events { + A, B, C, D, E, F, G, H, I +} +``` + +下面的清单显示了配置状态机的代码: + +配置状态 + +``` +@Override +public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S0, fooAction()) + .state(States.S0) + .and() + .withStates() + .parent(States.S0) + .initial(States.S1) + .state(States.S1) + .and() + .withStates() + .parent(States.S1) + .initial(States.S11) + .state(States.S11) + .state(States.S12) + .and() + .withStates() + .parent(States.S0) + .state(States.S2) + .and() + .withStates() + .parent(States.S2) + .initial(States.S21) + .state(States.S21) + .and() + .withStates() + .parent(States.S21) + .initial(States.S211) + .state(States.S211) + .state(States.S212); +} +``` + +下面的清单显示了配置状态机转换的代码: + +配置-转换 + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1).target(States.S1).event(Events.A) + .guard(foo1Guard()) + .and() + .withExternal() + .source(States.S1).target(States.S11).event(Events.B) + .and() + .withExternal() + .source(States.S21).target(States.S211).event(Events.B) + .and() + .withExternal() + .source(States.S1).target(States.S2).event(Events.C) + .and() + .withExternal() + .source(States.S2).target(States.S1).event(Events.C) + .and() + .withExternal() + .source(States.S1).target(States.S0).event(Events.D) + .and() + .withExternal() + .source(States.S211).target(States.S21).event(Events.D) + .and() + .withExternal() + .source(States.S0).target(States.S211).event(Events.E) + .and() + .withExternal() + .source(States.S1).target(States.S211).event(Events.F) + .and() + .withExternal() + .source(States.S2).target(States.S11).event(Events.F) + .and() + .withExternal() + .source(States.S11).target(States.S211).event(Events.G) + .and() + .withExternal() + .source(States.S211).target(States.S0).event(Events.G) + .and() + .withInternal() + .source(States.S0).event(Events.H) + .guard(foo0Guard()) + .action(fooAction()) + .and() + .withInternal() + .source(States.S2).event(Events.H) + .guard(foo1Guard()) + .action(fooAction()) + .and() + .withInternal() + .source(States.S1).event(Events.H) + .and() + .withExternal() + .source(States.S11).target(States.S12).event(Events.I) + .and() + .withExternal() + .source(States.S211).target(States.S212).event(Events.I) + .and() + .withExternal() + .source(States.S12).target(States.S212).event(Events.I); + +} +``` + +下面的清单显示了配置状态机操作和保护的代码: + +配置-动作和保护 + +``` +@Bean +public FooGuard foo0Guard() { + return new FooGuard(0); +} + +@Bean +public FooGuard foo1Guard() { + return new FooGuard(1); +} + +@Bean +public FooAction fooAction() { + return new FooAction(); +} +``` + +下面的列表显示了如何定义单个操作: + +Action + +``` +private static class FooAction implements Action { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + Integer foo = context.getExtendedState().get("foo", Integer.class); + if (foo == null) { + log.info("Init foo to 0"); + variables.put("foo", 0); + } else if (foo == 0) { + log.info("Switch foo to 1"); + variables.put("foo", 1); + } else if (foo == 1) { + log.info("Switch foo to 0"); + variables.put("foo", 0); + } + } +} +``` + +下面的清单显示了如何定义单个保护: + +Guard + +``` +private static class FooGuard implements Guard { + + private final int match; + + public FooGuard(int match) { + this.match = match; + } + + @Override + public boolean evaluate(StateContext context) { + Object foo = context.getExtendedState().getVariables().get("foo"); + return !(foo == null || !foo.equals(match)); + } +} +``` + +下面的清单显示了此状态机运行时产生的输出,以及向其发送的各种事件: + +``` +sm>sm start +Init foo to 0 +Entry state S0 +Entry state S1 +Entry state S11 +State machine started + +sm>sm event A +Event A send + +sm>sm event C +Exit state S11 +Exit state S1 +Entry state S2 +Entry state S21 +Entry state S211 +Event C send + +sm>sm event H +Switch foo to 1 +Internal transition source=S0 +Event H send + +sm>sm event C +Exit state S211 +Exit state S21 +Exit state S2 +Entry state S1 +Entry state S11 +Event C send + +sm>sm event A +Exit state S11 +Exit state S1 +Entry state S1 +Entry state S11 +Event A send +``` + +在前面的输出中,我们可以看到: + +* 启动状态机,通过超状态(`S11`)和(`S0`)将状态机带到其初始状态(`S11`)。此外,扩展状态变量`foo`初始化为`0`。 + +* 我们尝试用事件`A`在状态`S1`中执行自转换,但没有发生任何事情,因为该转换由变量`foo`保护为`1`。 + +* 我们发送事件`C`,它将我们带到另一个状态机,在那里输入初始状态(`S211`)及其超状态。在这里,我们可以使用事件`H`,它执行一个简单的内部转换来翻转`foo`变量。然后我们使用事件`C`返回。 + +* 事件`A`再次发送,现在`S1`执行自转换,因为该保护计算为`true`。 + +下面的示例提供了对层次结构状态及其事件处理如何工作的更详细的了解: + +``` +sm>sm variables +No variables + +sm>sm start +Init foo to 0 +Entry state S0 +Entry state S1 +Entry state S11 +State machine started + +sm>sm variables +foo=0 + +sm>sm event H +Internal transition source=S1 +Event H send + +sm>sm variables +foo=0 + +sm>sm event C +Exit state S11 +Exit state S1 +Entry state S2 +Entry state S21 +Entry state S211 +Event C send + +sm>sm variables +foo=0 + +sm>sm event H +Switch foo to 1 +Internal transition source=S0 +Event H send + +sm>sm variables +foo=1 + +sm>sm event H +Switch foo to 0 +Internal transition source=S2 +Event H send + +sm>sm variables +foo=0 +``` + +在前面的示例中: + +* 我们在不同的阶段打印扩展的状态变量。 + +* 通过 event`H`,我们最终运行了一个内部转换,该转换是用它的源状态记录的。 + +* 注意事件`H`在不同的状态下是如何处理的(`S0`,`S1`,和`S2`)。这是一个很好的例子,说明层次结构状态及其事件处理是如何工作的。如果由于保护条件,State`S2`无法处理事件`H`,则下一步检查其父事件。这保证了,当机器处于`S2`状态时,`foo`标志始终是翻转的。然而,在`S1`状态下,事件`H`总是与其虚拟转换匹配,而没有保护或操作,因此它永远不会发生。 + +## [](#statemachine-examples-cdplayer)CD 播放机 + +CD 播放机是一个示例,它类似于许多人在现实世界中使用的一个用例。CD 播放机本身是一个非常简单的实体,允许用户打开一副牌,插入或更改一个磁盘,然后通过按下各种按钮来驱动播放机的功能(`eject`,`play`,`stop`,`pause`,`rewind`,以及`backward`)。 + +我们当中有多少人真正考虑过,要制造出能与硬件交互的代码来驱动 CD 播放机,需要做些什么。是的,球员的概念很简单,但是,如果你看看幕后,事情实际上变得有点复杂。 + +你可能已经注意到,如果你的甲板是打开的,而你按下播放,甲板关闭和一首歌曲开始播放(如果 CD 被插入)。从某种意义上说,当套牌打开时,你首先需要关闭它,然后尝试开始播放(如果实际插入了 CD,则再次尝试)。希望你现在已经意识到,一个简单的 CD 播放机是如此简单。当然,你可以用一个简单的类来包装所有这些内容,这个类有几个布尔变量,可能还有几个嵌套的 if-else 子句。这样就可以了,但是如果你需要让所有这些行为变得更加复杂,那又如何呢?你真的想继续添加更多的标志和 if-else 子句吗? + +下图显示了我们的简单 CD 播放机的状态机: + +statechart3 + +本节的其余部分将介绍这个示例及其状态机是如何设计的,以及这两个状态机是如何相互交互的。以下三个配置部分在`EnumStateMachineConfigurerAdapter`中使用。 + +``` +@Override +public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.IDLE) + .state(States.IDLE) + .and() + .withStates() + .parent(States.IDLE) + .initial(States.CLOSED) + .state(States.CLOSED, closedEntryAction(), null) + .state(States.OPEN) + .and() + .withStates() + .state(States.BUSY) + .and() + .withStates() + .parent(States.BUSY) + .initial(States.PLAYING) + .state(States.PLAYING) + .state(States.PAUSED); + +} +``` + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.CLOSED).target(States.OPEN).event(Events.EJECT) + .and() + .withExternal() + .source(States.OPEN).target(States.CLOSED).event(Events.EJECT) + .and() + .withExternal() + .source(States.OPEN).target(States.CLOSED).event(Events.PLAY) + .and() + .withExternal() + .source(States.PLAYING).target(States.PAUSED).event(Events.PAUSE) + .and() + .withInternal() + .source(States.PLAYING) + .action(playingAction()) + .timer(1000) + .and() + .withInternal() + .source(States.PLAYING).event(Events.BACK) + .action(trackAction()) + .and() + .withInternal() + .source(States.PLAYING).event(Events.FORWARD) + .action(trackAction()) + .and() + .withExternal() + .source(States.PAUSED).target(States.PLAYING).event(Events.PAUSE) + .and() + .withExternal() + .source(States.BUSY).target(States.IDLE).event(Events.STOP) + .and() + .withExternal() + .source(States.IDLE).target(States.BUSY).event(Events.PLAY) + .action(playAction()) + .guard(playGuard()) + .and() + .withInternal() + .source(States.OPEN).event(Events.LOAD).action(loadAction()); +} +``` + +``` +@Bean +public ClosedEntryAction closedEntryAction() { + return new ClosedEntryAction(); +} + +@Bean +public LoadAction loadAction() { + return new LoadAction(); +} + +@Bean +public TrackAction trackAction() { + return new TrackAction(); +} + +@Bean +public PlayAction playAction() { + return new PlayAction(); +} + +@Bean +public PlayingAction playingAction() { + return new PlayingAction(); +} + +@Bean +public PlayGuard playGuard() { + return new PlayGuard(); +} +``` + +在前面的配置中: + +* 我们使用`EnumStateMachineConfigurerAdapter`来配置状态和转换。 + +* `CLOSED`和`OPEN`状态被定义为`IDLE`的子状态,`PLAYING`和`PAUSED`状态被定义为`BUSY`的子状态。 + +* 在`CLOSED`状态下,我们添加了一个名为`closedEntryAction`的 Bean 条目操作。 + +* 在转换过程中,我们主要将事件映射到预期的状态转换,例如`EJECT`关闭和打开一副牌,`PLAY`,`STOP`和`PAUSE`进行它们的自然转换。对于其他转换,我们进行了以下操作: + + * 对于 Source State`PLAYING`,我们添加了一个定时器触发器,它可以自动跟踪播放音轨中的经过时间,并具有决定何时切换到下一音轨的功能。 + + * 对于`PLAY`事件,如果源状态是`IDLE`,而目标状态是`BUSY`,我们定义了一个名为`playAction`的操作和一个名为`playGuard`的保护。 + + * 对于`LOAD`事件和`OPEN`状态,我们定义了一个名为`loadAction`的动作的内部转换,该动作跟踪插入带有扩展状态变量的磁盘。 + + * `PLAYING`状态定义了三个内部转换。一种是由一个计时器触发的,该计时器运行一个名为`playingAction`的操作,该操作会更新扩展的状态变量。另外两个转换使用带有不同事件的`trackAction`(分别为`BACK`和`FORWARD`)来处理用户想要在轨道中返回或前进时的情况。 + +这台机器只有六种状态,这些状态由以下枚举定义: + +``` +public enum States { + // super state of PLAYING and PAUSED + BUSY, + PLAYING, + PAUSED, + // super state of CLOSED and OPEN + IDLE, + CLOSED, + OPEN +} +``` + +事件表示用户可以按下的按钮,以及用户是否将光盘加载到播放机中。下面的枚举定义了事件: + +``` +public enum Events { + PLAY, STOP, PAUSE, EJECT, LOAD, FORWARD, BACK +} +``` + +`cdPlayer`和`library`bean 用于驱动应用程序。下面的清单显示了这两个 bean 的定义: + +``` +@Bean +public CdPlayer cdPlayer() { + return new CdPlayer(); +} + +@Bean +public Library library() { + return Library.buildSampleLibrary(); +} +``` + +我们将扩展的状态变量键定义为简单的枚举,如下所示: + +``` +public enum Variables { + CD, TRACK, ELAPSEDTIME +} + +public enum Headers { + TRACKSHIFT +} +``` + +我们希望使这个示例类型是安全的,因此我们定义了自己的注释(`@国家OnTransition`),它有一个强制的元注释(`@OnTransition`)。下面的清单定义了`@国家OnTransition`注释: + +``` +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@OnTransition +public @interface StatesOnTransition { + + States[] source() default {}; + + States[] target() default {}; + +} +``` + +`ClosedEntryAction`是`CLOSED`状态的一个条目操作,如果存在磁盘,则向状态机发送`PLAY`事件。下面的列表定义了`ClosedEntryAction`: + +``` +public static class ClosedEntryAction implements Action { + + @Override + public void execute(StateContext context) { + if (context.getTransition() != null + && context.getEvent() == Events.PLAY + && context.getTransition().getTarget().getId() == States.CLOSED + && context.getExtendedState().getVariables().get(Variables.CD) != null) { + context.getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.PLAY).build())) + .subscribe(); + } + } +} +``` + +`LoadAction`如果事件头包含有关要加载的磁盘的信息,则更新扩展状态变量。下面的列表定义了`LoadAction`: + +``` +public static class LoadAction implements Action { + + @Override + public void execute(StateContext context) { + Object cd = context.getMessageHeader(Variables.CD); + context.getExtendedState().getVariables().put(Variables.CD, cd); + } +} +``` + +`PlayAction`重置玩家的运行时间,该时间作为扩展状态变量保留。下面的列表定义了`PlayAction`: + +``` +public static class PlayAction implements Action { + + @Override + public void execute(StateContext context) { + context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l); + context.getExtendedState().getVariables().put(Variables.TRACK, 0); + } +} +``` + +`PlayGuard`如果`CD`扩展状态变量不表示磁盘已加载,则用`PLAY`事件保护从`IDLE`到`BUSY`的转换。下面的列表定义了`PlayGuard`: + +``` +public static class PlayGuard implements Guard { + + @Override + public boolean evaluate(StateContext context) { + ExtendedState extendedState = context.getExtendedState(); + return extendedState.getVariables().get(Variables.CD) != null; + } +} +``` + +`PlayingAction`更新了一个名为`ELAPSEDTIME`的扩展状态变量,玩家可以使用该变量读取和更新其 LCD 状态显示。`PlayingAction`还可以在用户返回或前进轨道时处理轨道移动。下面的示例定义了`PlayingAction`: + +``` +public static class PlayingAction implements Action { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + Object elapsed = variables.get(Variables.ELAPSEDTIME); + Object cd = variables.get(Variables.CD); + Object track = variables.get(Variables.TRACK); + if (elapsed instanceof Long) { + long e = ((Long)elapsed) + 1000l; + if (e > ((Cd) cd).getTracks()[((Integer) track)].getLength()*1000) { + context.getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.FORWARD) + .setHeader(Headers.TRACKSHIFT.toString(), 1).build())) + .subscribe(); + } else { + variables.put(Variables.ELAPSEDTIME, e); + } + } + } +} +``` + +`TrackAction`当用户在音轨中返回或前进时,处理音轨转换操作。如果一个音轨是光盘上的最后一个,则停止播放,并将`STOP`事件发送到状态机。下面的示例定义了`TrackAction`: + +``` +public static class TrackAction implements Action { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + Object trackshift = context.getMessageHeader(Headers.TRACKSHIFT.toString()); + Object track = variables.get(Variables.TRACK); + Object cd = variables.get(Variables.CD); + if (trackshift instanceof Integer && track instanceof Integer && cd instanceof Cd) { + int next = ((Integer)track) + ((Integer)trackshift); + if (next >= 0 && ((Cd)cd).getTracks().length > next) { + variables.put(Variables.ELAPSEDTIME, 0l); + variables.put(Variables.TRACK, next); + } else if (((Cd)cd).getTracks().length <= next) { + context.getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.STOP).build())) + .subscribe(); + } + } + } +} +``` + +状态机的另一个重要方面是它们有自己的职责(主要围绕处理状态),并且所有应用程序级别的逻辑都应该保持在外部。这意味着应用程序需要一种与状态机交互的方式。另外,请注意,我们用`CdPlayer`注释了`@WithStateMachine`,它指示状态机从你的 POJO 中查找方法,然后通过各种转换调用这些方法。下面的示例展示了它如何更新其 LCD 状态显示: + +``` +@OnTransition(target = "BUSY") +public void busy(ExtendedState extendedState) { + Object cd = extendedState.getVariables().get(Variables.CD); + if (cd != null) { + cdStatus = ((Cd)cd).getName(); + } +} +``` + +在前面的示例中,当转换发生在目标状态`BUSY`时,我们使用`@OnTransition`注释来钩住回调。 + +下面的清单显示了我们的状态机如何处理播放器是否关闭: + +``` +@StatesOnTransition(target = {States.CLOSED, States.IDLE}) +public void closed(ExtendedState extendedState) { + Object cd = extendedState.getVariables().get(Variables.CD); + if (cd != null) { + cdStatus = ((Cd)cd).getName(); + } else { + cdStatus = "No CD"; + } + trackStatus = ""; +} +``` + +`@OnTransition`(我们在前面的示例中使用了它)只能用于与枚举匹配的字符串。`@StatesOnTransition`允许你创建自己的使用实际枚举的类型安全注释。 + +下面的示例展示了这个状态机的实际工作方式。 + +``` +sm>sm start +Entry state IDLE +Entry state CLOSED +State machine started + +sm>cd lcd +No CD + +sm>cd library +0: Greatest Hits + 0: Bohemian Rhapsody 05:56 + 1: Another One Bites the Dust 03:36 +1: Greatest Hits II + 0: A Kind of Magic 04:22 + 1: Under Pressure 04:08 + +sm>cd eject +Exit state CLOSED +Entry state OPEN + +sm>cd load 0 +Loading cd Greatest Hits + +sm>cd play +Exit state OPEN +Entry state CLOSED +Exit state CLOSED +Exit state IDLE +Entry state BUSY +Entry state PLAYING + +sm>cd lcd +Greatest Hits Bohemian Rhapsody 00:03 + +sm>cd forward + +sm>cd lcd +Greatest Hits Another One Bites the Dust 00:04 + +sm>cd stop +Exit state PLAYING +Exit state BUSY +Entry state IDLE +Entry state CLOSED + +sm>cd lcd +Greatest Hits +``` + +在前一次运行中: + +* 启动状态机,这将使机器被初始化。 + +* CD 播放机的液晶屏状态是打印的。 + +* 光盘库已经印好了。 + +* CD 播放机的甲板打开了。 + +* 索引为 0 的 CD 被加载到一个甲板中。 + +* 由于插入了一张光盘,播放会导致卡片组关闭并立即播放。 + +* 我们打印液晶状态,并要求下一个轨道. + +* 我们不玩了。 + +## [](#statemachine-examples-tasks)任务 + +Tasks 示例演示了区域内的并行任务处理,并添加了错误处理来自动或手动修复任务问题,然后继续回到可以再次运行任务的状态。下图显示了任务状态机: + +statechart5 + +在此状态机中的高级级别上: + +* 我们总是尝试进入`READY`状态,这样我们就可以使用 Run 事件来执行任务。 + +* 由三个独立的区域组成的 TKHE状态被置于和状态的中间,这将导致这些区域进入它们的初始状态,并被它们的结束状态连接起来。 + +* 从`JOIN`状态,我们自动进入`CHOICE`状态,该状态检查扩展状态变量中是否存在错误标志。任务可以设置这些标志,这样做使`CHOICE`状态能够进入`ERROR`状态,在该状态中,可以自动或手动处理错误。 + +* `ERROR`中的`AUTOMATIC`状态可以尝试自动修复错误,如果成功,则返回`READY`。如果错误是无法自动处理的,则需要用户干预,并通过`FALLBACK`事件将机器放入`MANUAL`状态。 + +下面的清单显示了定义可能状态的枚举: + +States + +``` +public enum States { + READY, + FORK, JOIN, CHOICE, + TASKS, T1, T1E, T2, T2E, T3, T3E, + ERROR, AUTOMATIC, MANUAL +} +``` + +下面的清单显示了定义事件的枚举: + +事件 + +``` +public enum Events { + RUN, FALLBACK, CONTINUE, FIX; +} +``` + +下面的列表配置了可能的状态: + +配置状态 + +``` +@Override +public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.READY) + .fork(States.FORK) + .state(States.TASKS) + .join(States.JOIN) + .choice(States.CHOICE) + .state(States.ERROR) + .and() + .withStates() + .parent(States.TASKS) + .initial(States.T1) + .end(States.T1E) + .and() + .withStates() + .parent(States.TASKS) + .initial(States.T2) + .end(States.T2E) + .and() + .withStates() + .parent(States.TASKS) + .initial(States.T3) + .end(States.T3E) + .and() + .withStates() + .parent(States.ERROR) + .initial(States.AUTOMATIC) + .state(States.AUTOMATIC, automaticAction(), null) + .state(States.MANUAL); +} +``` + +以下清单配置了可能的转换: + +配置-转换 + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.READY).target(States.FORK) + .event(Events.RUN) + .and() + .withFork() + .source(States.FORK).target(States.TASKS) + .and() + .withExternal() + .source(States.T1).target(States.T1E) + .and() + .withExternal() + .source(States.T2).target(States.T2E) + .and() + .withExternal() + .source(States.T3).target(States.T3E) + .and() + .withJoin() + .source(States.TASKS).target(States.JOIN) + .and() + .withExternal() + .source(States.JOIN).target(States.CHOICE) + .and() + .withChoice() + .source(States.CHOICE) + .first(States.ERROR, tasksChoiceGuard()) + .last(States.READY) + .and() + .withExternal() + .source(States.ERROR).target(States.READY) + .event(Events.CONTINUE) + .and() + .withExternal() + .source(States.AUTOMATIC).target(States.MANUAL) + .event(Events.FALLBACK) + .and() + .withInternal() + .source(States.MANUAL) + .action(fixAction()) + .event(Events.FIX); +} +``` + +下面的保护发送一个选择项进入`ERROR`状态,如果发生错误,则需要返回`TRUE`。此保护检查所有扩展状态变量(`T1`,`T2`,和`T3`)是否`TRUE`。 + +``` +@Bean +public Guard tasksChoiceGuard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + return !(ObjectUtils.nullSafeEquals(variables.get("T1"), true) + && ObjectUtils.nullSafeEquals(variables.get("T2"), true) + && ObjectUtils.nullSafeEquals(variables.get("T3"), true)); + } + }; +} +``` + +下面的操作将事件发送到状态机,以请求下一步,即返回或继续返回到 Ready。 + +``` +@Bean +public Action automaticAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + if (ObjectUtils.nullSafeEquals(variables.get("T1"), true) + && ObjectUtils.nullSafeEquals(variables.get("T2"), true) + && ObjectUtils.nullSafeEquals(variables.get("T3"), true)) { + context.getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.CONTINUE).build())) + .subscribe(); + } else { + context.getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.FALLBACK).build())) + .subscribe(); + } + } + }; +} + +@Bean +public Action fixAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + variables.put("T1", true); + variables.put("T2", true); + variables.put("T3", true); + context.getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.CONTINUE).build())) + .subscribe(); + } + }; +} +``` + +缺省区域执行是同步的,这意味着区域将按顺序进行处理。在这个示例中,我们只是希望所有任务区域都能得到并行处理。这可以通过定义`RegionExecutionPolicy`来实现: + +``` +@Override +public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .regionExecutionPolicy(RegionExecutionPolicy.PARALLEL); +} +``` + +下面的示例展示了这个状态机的实际工作方式: + +``` +sm>sm start +State machine started +Entry state READY + +sm>tasks run +Exit state READY +Entry state TASKS +run task on T2 +run task on T1 +run task on T3 +run task on T2 done +run task on T1 done +run task on T3 done +Entry state T2 +Entry state T1 +Entry state T3 +Exit state T2 +Exit state T1 +Exit state T3 +Entry state T3E +Entry state T1E +Entry state T2E +Exit state TASKS +Entry state READY +``` + +在前面的清单中,我们可以看到任务运行了多次。在下一个列表中,我们将介绍错误: + +``` +sm>tasks list +Tasks {T1=true, T3=true, T2=true} + +sm>tasks fail T1 + +sm>tasks list +Tasks {T1=false, T3=true, T2=true} + +sm>tasks run +Entry state TASKS +run task on T1 +run task on T3 +run task on T2 +run task on T1 done +run task on T3 done +run task on T2 done +Entry state T1 +Entry state T3 +Entry state T2 +Entry state T1E +Entry state T2E +Entry state T3E +Exit state TASKS +Entry state JOIN +Exit state JOIN +Entry state ERROR +Entry state AUTOMATIC +Exit state AUTOMATIC +Exit state ERROR +Entry state READY +``` + +在前面的列表中,如果我们模拟 Task T1 的失败,它将自动修复。在下一个列表中,我们将介绍更多错误: + +``` +sm>tasks list +Tasks {T1=true, T3=true, T2=true} + +sm>tasks fail T2 + +sm>tasks run +Entry state TASKS +run task on T2 +run task on T1 +run task on T3 +run task on T2 done +run task on T1 done +run task on T3 done +Entry state T2 +Entry state T1 +Entry state T3 +Entry state T1E +Entry state T2E +Entry state T3E +Exit state TASKS +Entry state JOIN +Exit state JOIN +Entry state ERROR +Entry state AUTOMATIC +Exit state AUTOMATIC +Entry state MANUAL + +sm>tasks fix +Exit state MANUAL +Exit state ERROR +Entry state READY +``` + +在 precding 示例中,如果我们模拟任务`T2`或`T3`中的任一项失败,状态机将进入`MANUAL`状态,在此状态下,需要手动解决问题,然后才能返回`READY`状态。 + +## [](#statemachine-examples-washer)洗衣机 + +洗衣机示例演示了如何使用历史状态来恢复模拟断电情况下的运行状态配置。 + +任何使用过洗衣机的人都知道,如果你以某种方式暂停程序,它就会在不暂停的情况下从相同的状态继续运行。你可以通过使用历史伪状态在状态机中实现这种行为。下图显示了我们的洗衣机状态机: + +statechart6 + +下面的清单显示了定义可能状态的枚举: + +States + +``` +public enum States { + RUNNING, HISTORY, END, + WASHING, RINSING, DRYING, + POWEROFF +} +``` + +下面的清单显示了定义事件的枚举: + +事件 + +``` +public enum Events { + RINSE, DRY, STOP, + RESTOREPOWER, CUTPOWER +} +``` + +下面的列表配置了可能的状态: + +配置状态 + +``` +@Override +public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.RUNNING) + .state(States.POWEROFF) + .end(States.END) + .and() + .withStates() + .parent(States.RUNNING) + .initial(States.WASHING) + .state(States.RINSING) + .state(States.DRYING) + .history(States.HISTORY, History.SHALLOW); +} +``` + +以下清单配置了可能的转换: + +配置-转换 + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.WASHING).target(States.RINSING) + .event(Events.RINSE) + .and() + .withExternal() + .source(States.RINSING).target(States.DRYING) + .event(Events.DRY) + .and() + .withExternal() + .source(States.RUNNING).target(States.POWEROFF) + .event(Events.CUTPOWER) + .and() + .withExternal() + .source(States.POWEROFF).target(States.HISTORY) + .event(Events.RESTOREPOWER) + .and() + .withExternal() + .source(States.RUNNING).target(States.END) + .event(Events.STOP); +} +``` + +下面的示例展示了这个状态机的实际工作方式: + +``` +sm>sm start +Entry state RUNNING +Entry state WASHING +State machine started + +sm>sm event RINSE +Exit state WASHING +Entry state RINSING +Event RINSE send + +sm>sm event DRY +Exit state RINSING +Entry state DRYING +Event DRY send + +sm>sm event CUTPOWER +Exit state DRYING +Exit state RUNNING +Entry state POWEROFF +Event CUTPOWER send + +sm>sm event RESTOREPOWER +Exit state POWEROFF +Entry state RUNNING +Entry state WASHING +Entry state DRYING +Event RESTOREPOWER send +``` + +在前一次运行中: + +* 启动状态机,这将使机器被初始化。 + +* 状态机进入冲洗状态。 + +* 状态机进入干燥状态。 + +* 状态机切断电源,进入断电状态。 + +* 状态从历史状态恢复,这将使状态机恢复到其先前已知的状态。 + +## [](#statemachine-examples-persist)坚持 + +持久化是一个示例,它使用[Persist](#statemachine-recipes-persist)配方来演示如何通过状态机控制数据库条目更新逻辑。 + +下图显示了状态机逻辑和配置: + +statechart10 + +下面的清单显示了状态机配置: + +Statemachine 配置 + +``` +@Configuration +@EnableStateMachine +static class StateMachineConfig + extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("PLACED") + .state("PROCESSING") + .state("SENT") + .state("DELIVERED"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("PLACED").target("PROCESSING") + .event("PROCESS") + .and() + .withExternal() + .source("PROCESSING").target("SENT") + .event("SEND") + .and() + .withExternal() + .source("SENT").target("DELIVERED") + .event("DELIVER"); + } + +} +``` + +以下配置创建`PersistStateMachineHandler`: + +处理程序配置 + +``` +@Configuration +static class PersistHandlerConfig { + + @Autowired + private StateMachine stateMachine; + + @Bean + public Persist persist() { + return new Persist(persistStateMachineHandler()); + } + + @Bean + public PersistStateMachineHandler persistStateMachineHandler() { + return new PersistStateMachineHandler(stateMachine); + } + +} +``` + +下面的清单显示了与此示例一起使用的`Order`类: + +订单类 + +``` +public static class Order { + int id; + String state; + + public Order(int id, String state) { + this.id = id; + this.state = state; + } + + @Override + public String toString() { + return "Order [id=" + id + ", state=" + state + "]"; + } + +} +``` + +下面的示例显示了状态机的输出: + +``` +sm>persist db +Order [id=1, state=PLACED] +Order [id=2, state=PROCESSING] +Order [id=3, state=SENT] +Order [id=4, state=DELIVERED] + +sm>persist process 1 +Exit state PLACED +Entry state PROCESSING + +sm>persist db +Order [id=2, state=PROCESSING] +Order [id=3, state=SENT] +Order [id=4, state=DELIVERED] +Order [id=1, state=PROCESSING] + +sm>persist deliver 3 +Exit state SENT +Entry state DELIVERED + +sm>persist db +Order [id=2, state=PROCESSING] +Order [id=4, state=DELIVERED] +Order [id=1, state=PROCESSING] +Order [id=3, state=DELIVERED] +``` + +在前面的运行中,状态机: + +* 列出了现有嵌入式数据库中的行,该数据库已经填充了示例数据。 + +* 请求将命令`1`更新为`PROCESSING`状态。 + +* 再次列出数据库条目,并查看状态已从`PLACED`更改为`PROCESSING`。 + +* 更新命令`3`将其状态从`SENT`更新为`DELIVERED`。 + +| |你可能想知道数据库在哪里,因为在示例代码中不存在它的
符号。这个示例是基于 Spring boot 和,
因为必要的类在 Classpath 中,所以嵌入式的`HSQL`实例
是自动创建的。,

Spring boot 甚至创建了`JdbcTemplate`的实例,你可以将其
自动连接,就像我们在`Persist.java`中所做的那样,如下面的清单所示:

```
@Autowired
private JdbcTemplate jdbcTemplate;
```| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +接下来,我们需要处理状态更改。下面的清单展示了我们是如何做到这一点的: + +``` +public void change(int order, String event) { + Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?", + new RowMapper() { + public Order mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Order(rs.getInt("id"), rs.getString("state")); + } + }, new Object[] { order }); + handler.handleEventWithStateReactively(MessageBuilder + .withPayload(event).setHeader("order", order).build(), o.state) + .subscribe(); +} +``` + +最后,我们使用`PersistStateChangeListener`来更新数据库,如下面的清单所示: + +``` +private class LocalPersistStateChangeListener implements PersistStateChangeListener { + + @Override + public void onPersist(State state, Message message, + Transition transition, StateMachine stateMachine) { + if (message != null && message.getHeaders().containsKey("order")) { + Integer order = message.getHeaders().get("order", Integer.class); + jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order); + } + } +} +``` + +## [](#statemachine-examples-zookeeper)动物园管理员 + +ZooKeeper 是来自[Turnstile](#statemachine-examples-turnstile)示例的分布式版本。 + +| |此示例需要一个外部`Zookeeper`实例,该实例可从`localhost`访问,并具有默认端口和设置。| +|---|----------------------------------------------------------------------------------------------------------------------------| + +此示例的配置几乎与`turnstile`示例相同。我们只为配置`StateMachineEnsemble`的分布式状态机添加配置,如下面的清单所示: + +``` +@Override +public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withDistributed() + .ensemble(stateMachineEnsemble()); +} +``` + +实际的`StateMachineEnsemble`需要与`CuratorFramework`客户端一起创建为 Bean,如下例所示: + +``` +@Bean +public StateMachineEnsemble stateMachineEnsemble() throws Exception { + return new ZookeeperStateMachineEnsemble(curatorClient(), "/foo"); +} + +@Bean +public CuratorFramework curatorClient() throws Exception { + CuratorFramework client = CuratorFrameworkFactory.builder().defaultData(new byte[0]) + .retryPolicy(new ExponentialBackoffRetry(1000, 3)) + .connectString("localhost:2181").build(); + client.start(); + return client; +} +``` + +对于下一个示例,我们需要创建两个不同的 shell 实例。我们需要创建一个实例,看看会发生什么,然后创建第二个实例。下面的命令启动 shell 实例(请记住现在只启动一个实例): + +``` +@n1:~# java -jar spring-statemachine-samples-zookeeper-3.0.1.jar +``` + +启动状态机时,其初始状态为`LOCKED`。然后,它发送一个`COIN`事件以转换为`UNLOCKED`状态。下面的示例显示了发生的情况: + +贝壳 1 号 + +``` +sm>sm start +Entry state LOCKED +State machine started + +sm>sm event COIN +Exit state LOCKED +Entry state UNLOCKED +Event COIN send + +sm>sm state +UNLOCKED +``` + +现在,你可以使用与启动第一个状态机相同的命令,打开第二个 shell 实例并启动状态机。你应该看到输入的是分布式状态(`UNLOCKED`),而不是默认的初始状态(`LOCKED`)。 + +下面的示例展示了状态机及其输出: + +贝壳 2 号 + +``` +sm>sm start +State machine started + +sm>sm state +UNLOCKED +``` + +然后从任一个 shell(我们在下一个示例中使用第二个实例)发送一个`PUSH`事件,将其从`UNLOCKED`转换到`LOCKED`状态。下面的示例显示了状态机命令及其输出: + +贝壳 2 号 + +``` +sm>sm event PUSH +Exit state UNLOCKED +Entry state LOCKED +Event PUSH send +``` + +在另一个 shell(如果在第二个 shell 中运行前面的命令,则为第一个 shell)中,你应该看到状态会自动更改,这是基于在 ZooKeeper 中保留的分布式状态。下面的示例显示了状态机命令及其输出: + +贝壳 1 号 + +``` +sm>Exit state UNLOCKED +Entry state LOCKED +``` + +## [](#statemachine-examples-web)web + +Web 是一个分布式状态机示例,它使用 ZooKeeper 状态机来处理分布式状态。见[Zookeeper](#statemachine-examples-zookeeper)。 + +| |这个示例旨在针对多个不同的主机在多个
浏览器会话上运行。| +|---|--------------------------------------------------------------------------------------------------| + +此示例使用来自[Showcase](#statemachine-examples-showcase)的修改过的状态机结构来处理分布式状态机。下图显示了状态机逻辑: + +statechart11 + +| |由于这个示例的性质,`Zookeeper`状态机的一个实例预计将从 localhost 中为每个单独的示例实例提供
。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +这个演示使用了一个启动三个不同示例实例的示例。如果在同一台主机上运行不同的实例,则需要通过在命令中添加`--server.port=`来区分每个实例所使用的端口。否则,每个主机的默认端口是`8080`。 + +在此示例运行中,我们有三个主机:`n1`、`n2`和`n3`。每个都有一个正在运行的本地 ZooKeeper 实例和一个在端口`8080`上运行的状态机示例。 + +在不同的终端中,通过运行以下命令启动三个不同的状态机: + +``` +# java -jar spring-statemachine-samples-web-3.0.1.jar +``` + +当所有实例都在运行时,当你使用浏览器访问它们时,你应该会看到所有实例都显示了类似的信息。状态应该是`S0`,`S1`,和`S11`。名为`foo`的扩展状态变量的值应该为`0`。主要的状态是`S11`。 + +sm dist n1 1 + +当你在任何浏览器窗口中按下`Event C`按钮时,分布状态将更改为`S211,`,这是与类型`C`的事件相关联的转换所表示的目标状态。下图显示了这一变化: + +sm dist n2 2 + +现在我们可以按下`Event H`按钮,看到在所有状态机上运行的内部转换将名为`foo`的扩展状态变量的值从`0`更改为`1`。此更改首先在接收该事件的状态机上完成,然后传播到其他状态机。你应该只看到名为`foo`的变量从`0`更改为`1`。 + +sm dist n3 3 + +最后,我们可以发送`Event K`,它将状态机状态带回到状态`S11`。你应该会在所有浏览器中看到这种情况。下图显示了在一个浏览器中的结果: + +sm dist n1 4 + +## [](#statemachine-examples-scope)范围 + +范围是一个状态机示例,它使用会话范围为每个用户提供一个单独的实例。下图显示了作用域状态机中的状态和事件: + +statechart12 + +这个简单的状态机有三种状态:`S0`、`S1`和`S2`。它们之间的转换由三个事件控制:`A`、`B`和`C`。 + +要启动状态机,请在终端中运行以下命令: + +``` +# java -jar spring-statemachine-samples-scope-3.0.1.jar +``` + +当实例运行时,你可以打开浏览器并使用状态机。如果你在不同的浏览器中打开相同的页面(例如,一个在 Chrome 中,一个在 Firefox 中),那么你应该为每个用户获得一个新的状态机实例会话。下图显示了浏览器中的状态机: + +sm scope 1 + +## [](#statemachine-examples-security)安全性 + +安全性是一个状态机示例,它使用了保护状态机的大多数可能组合。它确保发送事件、转换和操作的安全性。下图显示了状态机的状态和事件: + +statechart13 + +要启动状态机,请运行以下命令: + +``` +# java -jar spring-statemachine-samples-secure-3.0.1.jar +``` + +我们通过要求用户具有`USER`的角色来保护事件发送。 Spring 安全性确保没有其他用户可以将事件发送到此状态机。下面的列表保证了事件发送的安全性: + +``` +@Override +public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .autoStartup(true) + .and() + .withSecurity() + .enabled(true) + .event("hasRole('USER')"); +} +``` + +在这个示例中,我们定义了两个用户: + +* 一个名为`user`的用户,他的角色是`USER` + +* 一个名为`admin`的用户,他有两个角色:`USER`和`ADMIN` + +这两个用户的密码都是`password`。下面的清单配置了这两个用户: + +``` +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) +static class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER") + .and() + .withUser("admin") + .password("password") + .roles("USER", "ADMIN"); + } +} +``` + +根据示例开头所示的状态图,我们定义了状态之间的各种转换。只有具有活动`ADMIN`角色的用户才能运行`S2`和`S3`之间的外部转换。类似地,只有`ADMIN`才能运行`S1`状态的内部转换。下面的清单定义了这些转换,包括它们的安全性: + +``` +@Override +public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S0).target(States.S1).event(Events.A) + .and() + .withExternal() + .source(States.S1).target(States.S2).event(Events.B) + .and() + .withExternal() + .source(States.S2).target(States.S0).event(Events.C) + .and() + .withExternal() + .source(States.S2).target(States.S3).event(Events.E) + .secured("ROLE_ADMIN", ComparisonType.ANY) + .and() + .withExternal() + .source(States.S3).target(States.S0).event(Events.C) + .and() + .withInternal() + .source(States.S0).event(Events.D) + .action(adminAction()) + .and() + .withInternal() + .source(States.S1).event(Events.F) + .action(transitionAction()) + .secured("ROLE_ADMIN", ComparisonType.ANY); +} +``` + +下面的列表使用了一个名为`adminAction`的方法,其返回类型是`Action`,以指定该操作是使用`ADMIN`角色进行安全保护的: + +``` +@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) +@Bean +public Action adminAction() { + return new Action() { + + @Secured("ROLE_ADMIN") + @Override + public void execute(StateContext context) { + log.info("Executed only for admin role"); + } + }; +} +``` + +当发送事件`F`时,下面的`Action`运行状态`S`的内部转换。 + +``` +@Bean +public Action transitionAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + log.info("Executed only for admin role"); + } + }; +} +``` + +转换本身使用`ADMIN`角色进行保护,因此,如果当前用户不讨厌该角色,则此转换不会运行。 + +## [](#statemachine-examples-eventservice)活动服务 + +事件服务示例展示了如何使用状态机概念作为事件的处理引擎。这个样本是从一个问题演变而来的: + +我可以使用 Spring StateMachine 作为一种微服务,将事件馈送到不同的状态机实例吗?事实上, Spring StateMachine 可以将事件馈送给潜在的数百万个不同的状态机实例。 + +这个示例使用`Redis`实例来持久化状态机实例。 + +显然,由于内存限制,在 JVM 中使用 100 万个状态机实例是个坏主意。这导致了 Spring StateMachine 的其他特性,这些特性允许你持久化`StateMachineContext`并重用现有实例。 + +对于这个示例,我们假设一个购物应用程序将不同类型的`PageView`事件发送到一个单独的微服务,然后该服务使用状态机跟踪用户的行为。下图显示了状态模型,它具有几个状态,这些状态表示用户导航产品项目列表,从购物车中添加和删除项目,进入付款页面并启动付款操作: + +statechart14 + +实际的购物应用程序将通过(例如)使用 REST 调用将这些事件发送到此服务中。稍后会有更多关于这方面的内容。 + +| |请记住,这里的重点是要有一个公开`REST`API 的应用程序,用户可以使用该 API 发送事件,对于每个请求,
状态机都可以处理这些事件。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的状态机配置对状态图中的内容进行建模。各种动作会更新状态机的`Extended State`,以跟踪进入各种状态的条目的数量,以及`ADD`和`DEL`的内部转换被调用了多少次,以及`PAY`是否已执行: + +``` +@Bean(name = "stateMachineTarget") +@Scope(scopeName="prototype") +public StateMachine stateMachineTarget() throws Exception { + Builder builder = StateMachineBuilder.builder(); + + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true); + + builder.configureStates() + .withStates() + .initial(States.HOME) + .states(EnumSet.allOf(States.class)); + + builder.configureTransitions() + .withInternal() + .source(States.ITEMS).event(Events.ADD) + .action(addAction()) + .and() + .withInternal() + .source(States.CART).event(Events.DEL) + .action(delAction()) + .and() + .withInternal() + .source(States.PAYMENT).event(Events.PAY) + .action(payAction()) + .and() + .withExternal() + .source(States.HOME).target(States.ITEMS) + .action(pageviewAction()) + .event(Events.VIEW_I) + .and() + .withExternal() + .source(States.CART).target(States.ITEMS) + .action(pageviewAction()) + .event(Events.VIEW_I) + .and() + .withExternal() + .source(States.ITEMS).target(States.CART) + .action(pageviewAction()) + .event(Events.VIEW_C) + .and() + .withExternal() + .source(States.PAYMENT).target(States.CART) + .action(pageviewAction()) + .event(Events.VIEW_C) + .and() + .withExternal() + .source(States.CART).target(States.PAYMENT) + .action(pageviewAction()) + .event(Events.VIEW_P) + .and() + .withExternal() + .source(States.ITEMS).target(States.HOME) + .action(resetAction()) + .event(Events.RESET) + .and() + .withExternal() + .source(States.CART).target(States.HOME) + .action(resetAction()) + .event(Events.RESET) + .and() + .withExternal() + .source(States.PAYMENT).target(States.HOME) + .action(resetAction()) + .event(Events.RESET); + + return builder.build(); +} +``` + +暂时不要关注`stateMachineTarget`或`@Scope`,正如我们在本节后面解释的那样。 + +我们设置了一个`RedisConnectionFactory`,它默认为本地主机和默认端口。我们使用`StateMachinePersist`和`RepositoryStateMachinePersist`实现。最后,我们创建一个`RedisStateMachinePersister`,它使用了以前创建的`StateMachinePersist` Bean。 + +然后在处理`Controller`调用的`REST`调用中使用这些函数,如下面的清单所示: + +``` +@Bean +public RedisConnectionFactory redisConnectionFactory() { + return new JedisConnectionFactory(); +} + +@Bean +public StateMachinePersist stateMachinePersist(RedisConnectionFactory connectionFactory) { + RedisStateMachineContextRepository repository = + new RedisStateMachineContextRepository(connectionFactory); + return new RepositoryStateMachinePersist(repository); +} + +@Bean +public RedisStateMachinePersister redisStateMachinePersister( + StateMachinePersist stateMachinePersist) { + return new RedisStateMachinePersister(stateMachinePersist); +} +``` + +我们创建一个名为`stateMachineTarget`的 Bean。状态机实例化是一种相对昂贵的操作,因此最好尝试池实例,而不是为每个请求实例化一个新实例。要做到这一点,我们首先创建一个`poolTargetSource`,它将`stateMachineTarget`封装在一起,并将它的最大大小设置为 3。然后通过使用`request`作用域来代理此`poolTargetSource`与`ProxyFactoryBean`。实际上,这意味着每个`REST`请求都会从 Bean 工厂获得一个池状态机实例。稍后,我们将展示如何使用这些实例。下面的清单显示了我们如何创建`ProxyFactoryBean`并设置目标源: + +``` +@Bean +@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) +public ProxyFactoryBean stateMachine() { + ProxyFactoryBean pfb = new ProxyFactoryBean(); + pfb.setTargetSource(poolTargetSource()); + return pfb; +} +``` + +下面的清单显示了我们设置最大大小和设置目标 Bean Name: + +``` +@Bean +public CommonsPool2TargetSource poolTargetSource() { + CommonsPool2TargetSource pool = new CommonsPool2TargetSource(); + pool.setMaxSize(3); + pool.setTargetBeanName("stateMachineTarget"); + return pool; +} +``` + +现在我们可以进入实际的演示了。你需要在具有默认设置的 localhost 上运行一个 Redis 服务器。然后,你需要通过运行以下命令来运行基于引导的示例应用程序: + +``` +# java -jar spring-statemachine-samples-eventservice-3.0.1.jar +``` + +在浏览器中,你会看到如下内容: + +sm eventservice 1 + +在这个 UI 中,可以使用三个用户:`joe`、`bob`和`dave`。单击按钮会显示当前状态和扩展状态。在单击按钮之前启用单选按钮将为该用户发送特定事件。这种安排可以让你使用 UI。 + +在我们的`StateMachineController`中,我们 autowire`StateMachine`和`StateMachinePersister`。`StateMachine`是`request`的作用域,因此你可以为每个请求获得一个新的实例,而`StateMachinePersist`是一个正常的单例 Bean。下面列出了 AutoWires`StateMachine`和`StateMachinePersist`: + +``` +@Autowired +private StateMachine stateMachine; + +@Autowired +private StateMachinePersister stateMachinePersister; +``` + +在下面的清单中,`feedAndGetState`与 UI 一起使用,以执行与实际`REST`API 可能执行的相同的操作: + +``` +@RequestMapping("/state") +public String feedAndGetState(@RequestParam(value = "user", required = false) String user, + @RequestParam(value = "id", required = false) Events id, Model model) throws Exception { + model.addAttribute("user", user); + model.addAttribute("allTypes", Events.values()); + model.addAttribute("stateChartModel", stateChartModel); + // we may get into this page without a user so + // do nothing with a state machine + if (StringUtils.hasText(user)) { + resetStateMachineFromStore(user); + if (id != null) { + feedMachine(user, id); + } + model.addAttribute("states", stateMachine.getState().getIds()); + model.addAttribute("extendedState", stateMachine.getExtendedState().getVariables()); + } + return "states"; +} +``` + +在下面的清单中,`feedPageview`是一个`REST`方法,它接受带有 JSON 内容的 POST。 + +``` +@RequestMapping(value = "/feed",method= RequestMethod.POST) +@ResponseStatus(HttpStatus.OK) +public void feedPageview(@RequestBody(required = true) Pageview event) throws Exception { + Assert.notNull(event.getUser(), "User must be set"); + Assert.notNull(event.getId(), "Id must be set"); + resetStateMachineFromStore(event.getUser()); + feedMachine(event.getUser(), event.getId()); +} +``` + +在下面的清单中,`feedMachine`将一个事件发送到`StateMachine`中,并通过使用`StateMachinePersister`来保持其状态: + +``` +private void feedMachine(String user, Events id) throws Exception { + stateMachine + .sendEvent(Mono.just(MessageBuilder + .withPayload(id).build())) + .blockLast(); + stateMachinePersister.persist(stateMachine, "testprefix:" + user); +} +``` + +下面的清单显示了用于为特定用户恢复状态机的`resetStateMachineFromStore`: + +``` +private StateMachine resetStateMachineFromStore(String user) throws Exception { + return stateMachinePersister.restore(stateMachine, "testprefix:" + user); +} +``` + +正如通常使用 UI 发送事件一样,你也可以使用`REST`调用来执行相同的操作,如下 curl 命令所示: + +``` +# curl http://localhost:8080/feed -H "Content-Type: application/json" --data '{"user":"joe","id":"VIEW_I"}' +``` + +在这一点上,你应该在 Redis 中使用`testprefix:joe`键的内容,如下例所示: + +``` +$ ./redis-cli +127.0.0.1:6379> KEYS * +1) "testprefix:joe" +``` + +接下来的三张图片显示了`joe`的状态何时从`HOME`更改为`ITEMS`,以及`ADD`动作何时执行。 + +下面这张`ADD`事件正在发送的图片: + +sm eventservice 2 + +现在你仍然处于`ITEMS`状态,并且内部转换导致`COUNT`扩展状态变量增加到`1`,如下图所示: + +sm eventservice 3 + +现在,你可以运行下面的`curl`REST 调用几次(或者通过 UI 执行),并在每次调用时看到`COUNT`变量的增加: + +``` +# curl http://localhost:8080/feed -H "Content-Type: application/json" # --data '{"user":"joe","id":"ADD"}' +``` + +下图显示了这些操作的结果: + +sm eventservice 4 + +## [](#statemachine-examples-deploy)部署 + +部署示例展示了如何使用状态机概念和 UML 建模来提供一个通用的错误处理状态。这个状态机是一个相对复杂的示例,说明了如何使用各种特性来提供一个集中的错误处理概念。下图显示了部署状态机: + +model deployer + +| |前面的状态图是使用 Eclipse Papyrus 插件
(参见[Eclipse 建模支持](#sm-papyrus))设计的,并通过生成的 UML
模型文件导入 Spring Statemachine。在模型中定义的动作和保护是从 Spring 应用程序上下文中解析
的。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在这个状态机场景中,我们有两个用户试图执行的不同行为(`DEPLOY`和`UNDEPLOY`)。 + +在前面的状态图中: + +* 在`DEPLOY`状态下,`INSTALL`和`START`状态是有条件地输入的。如果已经安装了产品,则直接输入`START`,如果安装失败,则无需尝试`START`。 + +* 在`UNDEPLOY`状态下,如果应用程序已经在运行,则有条件地输入`STOP`。 + +* 对于`DEPLOY`和`UNDEPLOY`的条件选择是通过这些状态中的选择伪态完成的,这些选择是由守卫选择的。 + +* 我们使用出口点伪状态来更好地控制`DEPLOY`和`UNDEPLOY`状态的出口。 + +* 在从`DEPLOY`和`UNDEPLOY`退出后,我们通过一个结伪状态来选择是否通过`ERROR`状态(如果将一个错误添加到扩展状态中)。 + +* 最后,我们回到`READY`状态来处理新的请求。 + +现在我们可以开始实际的演示了。通过运行以下命令来运行基于引导的示例应用程序: + +``` +# java -jar spring-statemachine-samples-deploy-3.0.1.jar +``` + +在浏览器中,你可以看到如下图片: + +sm deploy 1 + +| |由于我们没有真正的安装、启动或停止功能,我们
通过检查特定消息头的存在来模拟失败。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +现在,你可以开始向机器发送事件,并选择各种消息头来驱动功能。 + +## [](#statemachine-examples-ordershipping)订单运输 + +订单运输示例展示了如何使用状态机概念来构建一个简单的订单处理系统。 + +下图显示了驱动此订单运输示例的状态图。 + +![SM OrderShipping1](images/sm-ordershipping-1.png) + +在前面的状态图中: + +* 状态机进入`WAIT_NEW_ORDER`(默认)状态。 + +* 事件`PLACE_ORDER`转换为`RECEIVE_ORDER`状态,执行条目操作(`entryReceiveOrder`)。 + +* 如果订单是`OK`,则状态机进入两个区域,一个处理订单生产,另一个处理用户级支付。否则,状态机进入`CUSTOMER_ERROR`,这是一个最终状态。 + +* 状态机在较低的区域中循环以提醒用户进行支付,直到`RECEIVE_PAYMENT`被成功发送以指示正确的支付。 + +* 这两个区域进入等待状态(`WAIT_PRODUCT`和`WAIT_ORDER`),在退出父正交态(`HANDLE_ORDER`)之前,它们被连接在一起。 + +* 最后,状态机通过`SHIP_ORDER`到达其最终状态(`ORDER_SHIPPED`)。 + +下面的命令运行示例: + +``` +# java -jar spring-statemachine-samples-ordershipping-3.0.1.jar +``` + +在浏览器中,你可以看到类似于以下图像的内容。你可以从选择一个客户和一个订单开始创建状态机。 + +![SM OrderShipping2](images/sm-ordershipping-2.png) + +现在已经创建了特定订单的状态机,你可以开始下订单并发送付款。其他设置(例如`makeProdPlan`、`produce`和`payment`)允许你控制状态机的工作方式。下图显示了等待订单的状态机: + +![SM OrderShipping3](images/sm-ordershipping-3.png) + +最后,你可以通过刷新页面来查看机器的功能,如下图所示: + +![SM OrderShipping4](images/sm-ordershipping-4.png) + +## [](#statemachine-examples-datajpa) JPA 配置 + +JPA 配置示例展示了如何在数据库中保存机器配置的情况下使用状态机概念。这个示例使用带有 H2 控制台的嵌入式 H2 数据库(以方便使用数据库)。 + +这个示例使用`spring-statemachine-autoconfigure`(默认情况下,自动配置 JPA 所需的存储库和实体类)。因此,你只需要`@SpringBootApplication`。下面的示例显示了带有`Application`注释的`@SpringBootApplication`类: + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +下面的示例展示了如何创建`RepositoryStateMachineModelFactory`: + +``` +@Configuration +@EnableStateMachineFactory +public static class Config extends StateMachineConfigurerAdapter { + + @Autowired + private StateRepository stateRepository; + + @Autowired + private TransitionRepository transitionRepository; + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository); + } +} +``` + +你可以使用以下命令来运行示例: + +``` +# java -jar spring-statemachine-samples-datajpa-3.0.1.jar +``` + +在`[http://localhost:8080](http://localhost:8080)`上访问应用程序会为每个请求创建一个新构造的机器。然后,你可以选择将事件发送到机器。可能的事件和机器配置都会随每个请求从数据库中更新。下图显示了此状态机启动时创建的 UI 和初始事件: + +![SM Datajpa1](images/sm-datajpa-1.png) + +要访问嵌入式控制台,可以使用 JDBC URL(如果尚未设置,它是`jdbc:h2:mem:testdb`)。下图显示了 H2 控制台: + +![SM Datajpa2](images/sm-datajpa-2.png) + +在控制台上,你可以看到数据库表并根据需要对它们进行修改。下图显示了 UI 中一个简单查询的结果: + +![SM Datajpa3](images/sm-datajpa-3.png) + +既然已经完成了这一步,你可能想知道这些默认状态和转换是如何被填充到数据库中的。 Spring 数据有一个很好的技巧来自动填充存储库,我们通过`Jackson2RepositoryPopulatorFactoryBean`使用了这个功能。下面的示例展示了我们如何创建这样的 Bean: + +``` +@Bean +public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() { + StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean(); + factoryBean.setResources(new Resource[]{new ClassPathResource("data.json")}); + return factoryBean; +} +``` + +下面的清单显示了我们用来填充数据库的数据源: + +``` +[ + { + "@id": "10", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", + "spel": "T(System).out.println('hello exit S1')" + }, + { + "@id": "11", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", + "spel": "T(System).out.println('hello entry S2')" + }, + { + "@id": "12", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", + "spel": "T(System).out.println('hello state S3')" + }, + { + "@id": "13", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", + "spel": "T(System).out.println('hello')" + }, + { + "@id": "1", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState", + "initial": true, + "state": "S1", + "exitActions": ["10"] + }, + { + "@id": "2", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState", + "initial": false, + "state": "S2", + "entryActions": ["11"] + }, + { + "@id": "3", + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState", + "initial": false, + "state": "S3", + "stateActions": ["12"] + }, + { + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition", + "source": "1", + "target": "2", + "event": "E1", + "kind": "EXTERNAL" + }, + { + "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition", + "source": "2", + "target": "3", + "event": "E2", + "actions": ["13"] + } +] +``` + +## [](#statemachine-examples-datapersist)数据持久化 + +数据持久化示例展示了如何使用外部存储库中的持久化机器来实现状态机概念。这个示例使用带有 H2 控制台的嵌入式 H2 数据库(以方便使用数据库)。你还可以选择启用 Redis 或 MongoDB。 + +这个示例使用`spring-statemachine-autoconfigure`(默认情况下,自动配置 JPA 所需的存储库和实体类)。因此,你只需要`@SpringBootApplication`。下面的示例显示了带有`@SpringBootApplication`注释的`Application`类: + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +`StateMachineRuntimePersister`接口在`StateMachine`的运行时级别上工作。它的实现`JpaPersistingStateMachineInterceptor`意在与 JPA 一起使用。下面的列表创建了`StateMachineRuntimePersister` Bean: + +``` +@Configuration +@Profile("jpa") +public static class JpaPersisterConfig { + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister( + JpaStateMachineRepository jpaStateMachineRepository) { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } +} +``` + +下面的示例展示了如何使用非常相似的配置为 MongoDB 创建 Bean: + +``` +@Configuration +@Profile("mongo") +public static class MongoPersisterConfig { + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister( + MongoDbStateMachineRepository jpaStateMachineRepository) { + return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } +} +``` + +下面的示例展示了如何使用非常相似的配置为 Redis 创建 Bean: + +``` +@Configuration +@Profile("redis") +public static class RedisPersisterConfig { + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister( + RedisStateMachineRepository jpaStateMachineRepository) { + return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } +} +``` + +可以使用`withPersistence`配置方法将`StateMachine`配置为使用运行时持久性。下面的清单展示了如何做到这一点: + +``` +@Autowired +private StateMachineRuntimePersister stateMachineRuntimePersister; + +@Override +public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withPersistence() + .runtimePersister(stateMachineRuntimePersister); +} +``` + +这个示例还使用`DefaultStateMachineService`,这使得使用多台机器更容易。下面的清单显示了如何创建`DefaultStateMachineService`的实例: + +``` +@Bean +public StateMachineService stateMachineService( + StateMachineFactory stateMachineFactory, + StateMachineRuntimePersister stateMachineRuntimePersister) { + return new DefaultStateMachineService(stateMachineFactory, stateMachineRuntimePersister); +} +``` + +下面的清单显示了驱动此示例中`StateMachineService`的逻辑: + +``` +private synchronized StateMachine getStateMachine(String machineId) throws Exception { + listener.resetMessages(); + if (currentStateMachine == null) { + currentStateMachine = stateMachineService.acquireStateMachine(machineId); + currentStateMachine.addStateListener(listener); + currentStateMachine.startReactively().block(); + } else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) { + stateMachineService.releaseStateMachine(currentStateMachine.getId()); + currentStateMachine.stopReactively().block(); + currentStateMachine = stateMachineService.acquireStateMachine(machineId); + currentStateMachine.addStateListener(listener); + currentStateMachine.startReactively().block(); + } + return currentStateMachine; +} +``` + +你可以使用以下命令来运行示例: + +``` +# java -jar spring-statemachine-samples-datapersist-3.0.1.jar +``` + +| |默认情况下,`jpa`配置文件在`application.yml`中启用。如果你想尝试
其他后端,请启用`mongo`配置文件或`redis`配置文件。
以下命令指定使用哪个配置文件(`jpa`是默认配置文件,
但我们为了完整起见将其包括在内):

```
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=redis
```| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过[http://localhost:8080](http://localhost:8080)访问应用程序,会为每个请求创建一个新构造的状态机,你可以选择将事件发送到机器。可能的事件和机器配置都会随每个请求从数据库中更新。 + +这个示例中的状态机有一个简单的配置,其中状态的 1’到’6’,事件的’E1’到’E6’,以在这些状态之间转换状态机。可以使用两个状态机标识符(`datajpapersist1`和`datajpapersist2`)来请求特定的状态机。下图显示了允许你选择机器和事件的 UI,并显示了这样做时会发生什么: + +![Sm DatajpaPainstive1](images/sm-datajpapersist-1.png) + +该示例默认使用机器“DataJpaPainstive1”,并转到其初始状态“1”。下图显示了使用这些默认值的结果: + +![Sm DatajpaPastive2](images/sm-datajpapersist-2.png) + +如果将事件`E1`和`E2`发送到`datajpapersist1`状态机,则其状态保持为’s3’。下图显示了这样做的结果: + +![SM DatajpaPainsive3](images/sm-datajpapersist-3.png) + +如果你随后请求状态机`datajpapersist1`但没有发送任何事件,则状态机将被恢复到其持久状态`S3`。 + +## [](#statemachine-examples-datajpamultipersist)数据多持久化 + +多 Ersist 样本是另外两个样本的扩展:[JPA Configuration](#statemachine-examples-datajpa)和[数据持续存在](#statemachine-examples-datapersist)。我们仍然将机器配置保存在数据库中,并将其持久化到数据库中。然而,这一次,我们也有了一个包含两个正交区域的机器,以显示这些区域是如何独立地持久化的。这个示例还使用了带有 H2 控制台的嵌入式 H2 数据库(以方便使用数据库)。 + +这个示例使用`spring-statemachine-autoconfigure`(默认情况下,自动配置 JPA 所需的存储库和实体类)。因此,你只需要`@SpringBootApplication`。下面的示例显示了带有`@SpringBootApplication`注释的`Application`类: + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +与其他数据驱动的示例一样,我们再次创建`StateMachineRuntimePersister`,如下面的清单所示: + +``` +@Bean +public StateMachineRuntimePersister stateMachineRuntimePersister( + JpaStateMachineRepository jpaStateMachineRepository) { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); +} +``` + +a`StateMachineService` Bean 使得使用机器更容易。下面的清单展示了如何创建这样的 Bean: + +``` +@Bean +public StateMachineService stateMachineService( + StateMachineFactory stateMachineFactory, + StateMachineRuntimePersister stateMachineRuntimePersister) { + return new DefaultStateMachineService(stateMachineFactory, stateMachineRuntimePersister); +} +``` + +我们使用 JSON 数据导入配置。下面的示例创建一个 Bean 来执行此操作: + +``` +@Bean +public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() { + StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean(); + factoryBean.setResources(new Resource[] { new ClassPathResource("datajpamultipersist.json") }); + return factoryBean; +} +``` + +下面的列表显示了我们如何得到`RepositoryStateMachineModelFactory`: + +``` +@Configuration +@EnableStateMachineFactory +public static class Config extends StateMachineConfigurerAdapter { + + @Autowired + private StateRepository stateRepository; + + @Autowired + private TransitionRepository transitionRepository; + + @Autowired + private StateMachineRuntimePersister stateMachineRuntimePersister; + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withPersistence() + .runtimePersister(stateMachineRuntimePersister); + } + + @Override + public void configure(StateMachineModelConfigurer model) + throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository); + } +} +``` + +你可以使用以下命令运行示例: + +``` +# java -jar spring-statemachine-samples-datajpamultipersist-3.0.1.jar +``` + +访问`[http://localhost:8080](http://localhost:8080)`上的应用程序会为每个请求创建一个新构造的机器,并允许你将事件发送到机器。可能的事件和状态机配置将从数据库中针对每个请求进行更新。我们还打印出所有状态机上下文和当前根机,如下图所示: + +![SM DatajpamultiPersistent1](images/sm-datajpamultipersist-1.png) + +名为`datajpamultipersist1`的状态机是一个简单的“平面”机器,其中状态`S1`,`S2`和`S3`分别由事件`E1`,`E2`和`E3`来转换。然而,名为`datajpamultipersist2`的状态机在根级别下直接包含两个区域(`R1`和`R2`)。这就是为什么这个根级机器实际上没有状态。我们需要根级别的机器来承载这些区域。 + +区域`R1`和`R2`在`datajpamultipersist2`状态机中包含状态`S10`,`S11`,以及`S12`和`S20`,`S21`和`S22`(分别)。事件`E10`,`E11`,和`E12`用于表示区域`R1`,事件`E20`,`E21`,事件`E22`用于表示区域`R2`。下面的图像显示了当我们将事件`E10`和`E20`发送到`datajpamultipersist2`状态机时会发生什么: + +![SM DatajpamultiPersistent2](images/sm-datajpamultipersist-2.png) + +区域有它们自己的上下文和它们自己的 ID,而实际的 ID 是用`#`和区域 ID 后置的。如下图所示,数据库中的不同区域具有不同的上下文: + +![SM DatajpamultiPersistent3](images/sm-datajpamultipersist-3.png) + +## [](#statemachine-examples-datajpapersist)数据 JPA 持久化 + +数据持久化示例展示了如何使用外部存储库中的持久化机器来实现状态机概念。这个示例使用带有 H2 控制台的嵌入式 H2 数据库(以方便使用数据库)。你还可以选择启用 Redis 或 MongoDB。 + +这个示例使用`spring-statemachine-autoconfigure`(默认情况下,自动配置 JPA 所需的存储库和实体类)。因此,你只需要`@SpringBootApplication`。下面的示例显示了带有`@SpringBootApplication`注释的`Application`类: + +``` +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +`StateMachineRuntimePersister`接口在`StateMachine`的运行时级别上工作。其实现`JpaPersistingStateMachineInterceptor`意在与 JPA 一起使用。下面的列表创建了`StateMachineRuntimePersister` Bean: + +``` +@Configuration +@Profile("jpa") +public static class JpaPersisterConfig { + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister( + JpaStateMachineRepository jpaStateMachineRepository) { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } +} +``` + +下面的示例展示了如何使用非常相似的配置为 MongoDB 创建 Bean: + +``` +@Configuration +@Profile("mongo") +public static class MongoPersisterConfig { + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister( + MongoDbStateMachineRepository jpaStateMachineRepository) { + return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } +} +``` + +下面的示例展示了如何使用非常相似的配置为 Redis 创建 Bean: + +``` +@Configuration +@Profile("redis") +public static class RedisPersisterConfig { + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister( + RedisStateMachineRepository jpaStateMachineRepository) { + return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } +} +``` + +你可以使用`withPersistence`配置方法将`StateMachine`配置为使用运行时持久性。下面的清单展示了如何做到这一点: + +``` +@Autowired +private StateMachineRuntimePersister stateMachineRuntimePersister; + +@Override +public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withPersistence() + .runtimePersister(stateMachineRuntimePersister); +} +``` + +这个示例还使用`DefaultStateMachineService`,这使得使用多台机器更容易。下面的清单显示了如何创建`DefaultStateMachineService`的实例: + +``` +@Bean +public StateMachineService stateMachineService( + StateMachineFactory stateMachineFactory, + StateMachineRuntimePersister stateMachineRuntimePersister) { + return new DefaultStateMachineService(stateMachineFactory, stateMachineRuntimePersister); +} +``` + +下面的清单显示了驱动此示例中`StateMachineService`的逻辑: + +``` +private synchronized StateMachine getStateMachine(String machineId) throws Exception { + listener.resetMessages(); + if (currentStateMachine == null) { + currentStateMachine = stateMachineService.acquireStateMachine(machineId); + currentStateMachine.addStateListener(listener); + currentStateMachine.startReactively().block(); + } else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) { + stateMachineService.releaseStateMachine(currentStateMachine.getId()); + currentStateMachine.stopReactively().block(); + currentStateMachine = stateMachineService.acquireStateMachine(machineId); + currentStateMachine.addStateListener(listener); + currentStateMachine.startReactively().block(); + } + return currentStateMachine; +} +``` + +你可以使用以下命令来运行示例: + +``` +# java -jar spring-statemachine-samples-datapersist-3.0.1.jar +``` + +| |默认情况下,`jpa`配置文件在`application.yml`中启用。如果你想尝试
其他后端,请启用`mongo`配置文件或`redis`配置文件。
下面的命令指定使用哪个配置文件(`jpa`是缺省的,
但为了完整起见我们将其包括在内):

```
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=redis
```| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在[http://localhost:8080](http://localhost:8080)上访问应用程序会为每个请求创建一个新构造的状态机,你可以选择将事件发送到机器。可能的事件和机器配置都会随每个请求从数据库中更新。 + +这个示例中的状态机有一个简单的配置,其中状态的 1’到’6’,事件的’E1’到’E6’,以在这些状态之间转换状态机。可以使用两个状态机标识符(`datajpapersist1`和`datajpapersist2`)来请求特定的状态机。下图显示了允许你选择机器和事件的 UI,并显示了这样做时会发生什么: + +![Sm DatajpaPainstive1](images/sm-datajpapersist-1.png) + +该示例默认使用机器“DataJpaPainstive1”,并转到其初始状态“1”。下图显示了使用这些默认值的结果: + +![Sm DatajpaPastive2](images/sm-datajpapersist-2.png) + +如果将事件`E1`和`E2`发送到`datajpapersist1`状态机,则其状态保持为’S3’。下图显示了这样做的结果: + +![SM DatajpaPainsive3](images/sm-datajpapersist-3.png) + +如果你随后请求状态机`datajpapersist1`但没有发送任何事件,则状态机将被恢复到其持久状态`S3`。 + +## [](#statemachine-examples-monitoring)监测 + +监视示例展示了如何使用状态机概念来监视状态机转换和操作。下面的清单配置了我们在此示例中使用的状态机: + +``` +@Configuration +@EnableStateMachine +public static class Config extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S1") + .state("S2", null, (c) -> {System.out.println("hello");}) + .state("S3", (c) -> {System.out.println("hello");}, null); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S1").target("S2").event("E1") + .action((c) -> {System.out.println("hello");}) + .and() + .withExternal() + .source("S2").target("S3").event("E2"); + } +} +``` + +你可以使用以下命令来运行示例: + +``` +# java -jar spring-statemachine-samples-monitoring-3.0.1.jar +``` + +下图显示了状态机的初始状态: + +![SM 监控 1](images/sm-monitoring-1.png) + +下图显示了在我们执行了一些操作之后状态机的状态: + +![SM 监控 2](images/sm-monitoring-2.png) + +通过运行以下两个`curl`命令(如其输出所示),你可以从 Spring 启动中查看指标: + +``` +# curl http://localhost:8080/actuator/metrics/ssm.transition.duration + +{ + "name":"ssm.transition.duration", + "measurements":[ + { + "statistic":"COUNT", + "value":3.0 + }, + { + "statistic":"TOTAL_TIME", + "value":0.007 + }, + { + "statistic":"MAX", + "value":0.004 + } + ], + "availableTags":[ + { + "tag":"transitionName", + "values":[ + "INITIAL_S1", + "EXTERNAL_S1_S2" + ] + } + ] +} +``` + +``` +# curl http://localhost:8080/actuator/metrics/ssm.transition.transit + +{ + "name":"ssm.transition.transit", + "measurements":[ + { + "statistic":"COUNT", + "value":3.0 + } + ], + "availableTags":[ + { + "tag":"transitionName", + "values":[ + "EXTERNAL_S1_S2", + "INITIAL_S1" + ] + } + ] +} +``` + +你还可以通过运行以下`curl`命令(如其输出所示)来查看从 Spring 启动的跟踪: + +``` +# curl http://localhost:8080/actuator/statemachinetrace + +[ + { + "timestamp":"2018-02-11T06:44:12.723+0000", + "info":{ + "duration":2, + "machine":null, + "transition":"EXTERNAL_S1_S2" + } + }, + { + "timestamp":"2018-02-11T06:44:12.720+0000", + "info":{ + "duration":0, + "machine":null, + "action":"demo.monitoring.StateMachineConfig$Config$$Lambda$576/[email protected]" + } + }, + { + "timestamp":"2018-02-11T06:44:12.714+0000", + "info":{ + "duration":1, + "machine":null, + "transition":"INITIAL_S1" + } + }, + { + "timestamp":"2018-02-11T06:44:09.689+0000", + "info":{ + "duration":4, + "machine":null, + "transition":"INITIAL_S1" + } + } +] +``` + +# [](#statemachine-faq)FAQ + +本章回答了 Spring 机器用户最常问的问题。 + +## [](#state-changes)状态变化 + +我怎样才能自动转到下一个州呢? + +你可以从三种方法中进行选择: + +* 实现一个操作,并向状态机发送一个适当的事件,以触发转换到适当的目标状态。 + +* 在一个状态中定义一个延迟事件,并在发送一个事件之前,发送另一个延迟的事件。这样做会在处理该事件更方便时导致下一个适当的状态转换。 + +* 实现无触发转换,当进入状态并完成其操作时,它会自动导致状态转换到下一个状态。 + +## [](#extended-state)扩展状态 + +如何在状态机启动时初始化变量? + +状态机中的一个重要概念是,除非触发器导致可以触发动作的状态转换,否则不会真正发生任何事情。然而,话虽如此, Spring Statemachine 总是在状态机启动时具有初始转换。通过这个初始转换,你可以运行一个简单的操作,在`StateContext`中,它可以对扩展的状态变量执行任意操作。 + +# [](#appendices)附录 + +## [](#support-content)附录 A:支持内容 + +本附录提供了有关在此参考文档中使用的类和材料的通用信息。 + +### [](#classes-used-in-this-document)本文档中使用的类 + +下面的列表显示了在整个参考指南中使用的类: + +``` +public enum States { + SI,S1,S2,S3,S4,SF +} +``` + +``` +public enum States2 { + S1,S2,S3,S4,S5,SF, + S2I,S21,S22,S2F, + S3I,S31,S32,S3F +} +``` + +``` +public enum States3 { + S1,S2,SH, + S2I,S21,S22,S2F +} +``` + +``` +public enum Events { + E1,E2,E3,E4,EF +} +``` + +## [](#state-machine-concepts)附录 B:状态机概念 + +本附录提供了有关状态机的一般信息。 + +### [](#quick-example)快速示例 + +假设我们有名为`STATE1`和`STATE2`的状态,以及名为`EVENT1`和`EVENT2`的事件,你可以定义状态机的逻辑,如下图所示: + +statechart0 + +下面的清单在前面的图像中定义了状态机: + +``` +public enum States { + STATE1, STATE2 +} + +public enum Events { + EVENT1, EVENT2 +} +``` + +``` +@Configuration +@EnableStateMachine +public class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.STATE1) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.STATE1).target(States.STATE2) + .event(Events.EVENT1) + .and() + .withExternal() + .source(States.STATE2).target(States.STATE1) + .event(Events.EVENT2); + } +} +``` + +``` +@WithStateMachine +public class MyBean { + + @OnTransition(target = "STATE1") + void toState1() { + } + + @OnTransition(target = "STATE2") + void toState2() { + } +} +``` + +``` +public class MyApp { + + @Autowired + StateMachine stateMachine; + + void doSignals() { + stateMachine + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.EVENT1).build())) + .subscribe(); + stateMachine + .sendEvent(Mono.just(MessageBuilder + .withPayload(Events.EVENT2).build())) + .subscribe(); + } +} +``` + +### [](#glossary)术语表 + +**状态机** + +驱动一组状态以及区域、转换和事件的主要实体。 + +**国家** + +状态模拟一种情况,在这种情况下,一定的不变条件成立。状态是状态机的主要实体,在状态机中,状态变化是由事件驱动的。 + +**扩展状态** + +扩展状态是保存在状态机中的一组特殊变量,以减少所需状态的数量。 + +**过渡** + +转换是源状态和目标状态之间的关系。它可能是复合转换的一部分,它将状态机从一个状态配置转换到另一个状态配置,表示状态机对特定类型事件发生的完整响应。 + +**事件** + +一种实体,它被发送到状态机,然后驱动各种状态变化。 + +**初始状态** + +状态机启动的一种特殊状态。初始状态总是绑定到特定的状态机或区域。具有多个区域的状态机可以具有多个初始状态。 + +**结束状态** + +(也称为最终状态)一种特殊的状态,表示封闭区域已完成。如果封闭区域直接包含在状态机中,并且状态机中的所有其他区域也完成了,则整个状态机就完成了。 + +**历史状态** + +一种伪状态,使状态机记住其上一次活动状态。存在两种类型的历史状态:*浅层*(只记住顶层状态)和*深*(记住子机器中的活动状态)。 + +**选择状态** + +允许基于(例如)事件头或扩展状态变量进行转换选择的一种伪状态。 + +**连接状态** + +一种伪状态,与选择状态相对相似,但允许多个传入转换,而选择只允许一个传入转换。 + +**分叉状态** + +一种可控制地进入某一区域的伪状态。 + +**加入状态** + +一种伪状态,它使人可以控制地离开某一区域。 + +**入口点** + +允许受控进入子机的一种伪状态。 + +**出口点** + +允许受控退出子机的一种伪状态。 + +**地区** + +区域是复合状态或状态机的正交部分。它包含状态和转换。 + +**守卫** + +一种基于扩展状态变量和事件参数的值动态评估的布尔表达式。保护条件影响状态机的行为,只在它们的估值为`TRUE`时启用操作或转换,并在它们的估值为`FALSE`时禁用它们。 + +**行动** + +动作是在触发转换过程中运行的行为。 + +### [](#crashcourse)状态机速成课程 + +本附录为状态机概念提供了一个通用的速成课程。 + +#### [](#states)国家 + +状态是状态机可以使用的模型。将状态描述为一个真实世界的例子总是比试图在文档中使用抽象的概念更容易。为此,请考虑一个简单的键盘示例——我们大多数人每天都使用一个键盘。如果你有一个完整的键盘,左边有正常键,右边有数字键盘,你可能已经注意到,数字键盘可能处于两种不同的状态,这取决于 Numlock 是否被激活。如果它不是活动的,按数字垫键将导致导航使用箭头等。如果数字键盘是活动的,按下这些键将导致数字被键入。从本质上讲,键盘的数字键盘部分可以处于两种不同的状态。 + +要将状态概念与编程联系起来,这意味着你可以使用状态、状态变量或与状态机的另一种交互,而不是使用标志、嵌套的 if/else/break 子句或其他不切实际(有时是曲折的)逻辑。 + +#### [](#pseudo-states)伪态 + +伪状态是一种特殊类型的状态,通常通过赋予状态一个特殊的含义(例如初始状态),将更高级的逻辑引入状态机。然后,状态机可以通过执行 UML 状态机概念中可用的各种操作,在内部对这些状态做出反应。 + +##### [](#initial)首字母 + +对于每个状态机,总是需要**初始伪态**状态,无论你是拥有简单的一级状态机还是由子机或区域组成的更复杂的状态机。初始状态定义了状态机启动时的运行位置。没有它,状态机就不会成型。 + +##### [](#end)完 + +**终止伪态**(也称为“结束状态”)表示特定状态机已达到其最终状态。实际上,这意味着状态机不再处理任何事件,也不会传输到任何其他状态。然而,在子机为区域的情况下,状态机可以从其终端状态重新启动。 + +##### [](#choice)选择 + +你可以使用**选择伪态**从这个状态中选择一个动态转换的条件分支。通过保护对动态条件进行评估,从而选择一条支路。通常使用简单的 if/elseif/else 结构来确保选择了一个分支。否则,状态机可能会陷入死锁,并且配置格式不正确。 + +##### [](#junction)交点 + +**连接伪态**在功能上与 choice 相似,因为两者都是用 if/elseif/else 结构实现的。唯一真正的区别是,连接允许多个传入转换,而选择只允许一个。因此,差异在很大程度上是学术性的,但确实有一些差异,例如,在设计状态机时使用的是真实的 UI 建模框架。 + +##### [](#history)历史 + +你可以使用**历史伪国家**来记住最近的活动状态配置。在状态机退出后,你可以使用历史状态来恢复先前已知的配置。有两种类型的历史状态可用:`SHALLOW`(只记住状态机本身的活动状态)和`DEEP`(也记住嵌套状态)。 + +历史状态可以通过监听状态机事件来在外部实现,但是这将很快导致非常困难的逻辑,尤其是在状态机包含复杂的嵌套结构的情况下。让状态机本身来处理历史状态的记录,会使事情变得简单得多。用户只需创建一个转换到历史状态,状态机就会处理所需的逻辑,以返回到上次已知的记录状态。 + +如果转换在历史状态上终止,而该状态以前没有被输入(换句话说,不存在先前的历史)或它已经到达其结束状态,则转换可以通过使用默认的历史机制,强制状态机到特定子状态。此转换起源于历史状态,并终止于包含历史状态的区域的特定顶点(默认历史状态)。只有当其执行导致历史状态并且该状态以前从未处于活动状态时,才会进行此转换。否则,将执行进入该区域的正常历史记录。如果未定义缺省历史转换,则执行该区域的标准缺省条目。 + +##### [](#fork)叉子 + +你可以使用**Fork 伪态**对一个或多个区域进行显式输入。下图显示了叉子的工作原理: + +statechart7 + +目标状态可以是承载区域的父状态,这仅意味着区域通过输入其初始状态而被激活。你还可以直接将目标添加到区域中的任何状态,这允许更多的受控进入状态。 + +##### [](#join)加入 + +**加入伪状态**将源自不同区域的几个转换合并在一起。它通常用于等待和阻止参与区域进入其加入目标状态。下图显示了连接的工作原理: + +statechart8 + +源状态可以是承载区域的父状态,这意味着连接状态是参与区域的终端状态。你还可以将源状态定义为区域中的任何状态,这允许受控地退出区域。 + +##### [](#entry-point)切入点 + +**入口点伪态**表示提供状态机或状态机内部封装的状态机或复合状态的入口点。在 OWNS 入口点的状态机或复合状态的每个区域中,在该区域内最多只有一个从入口点到顶点的转换。 + +##### [](#exit-point)退出点 + +**出口点伪状态**是状态机或复合状态的出口点,它提供了状态机或状态机内部的封装。在复合状态的任何区域(或由子机状态引用的状态机)的出口点终止的转换意味着退出该复合状态或子机状态(执行其相关的退出行为)。 + +#### [](#guard-conditions)防范条件 + +保护条件是根据扩展的状态变量和事件参数计算为`TRUE`或`FALSE`的表达式。保护与动作和转换一起使用,以动态地选择是否应该运行特定的动作或转换。各种防护措施、事件参数和扩展的状态变量的存在使状态机的设计更加简单。 + +#### [](#events)事件 + +事件是用于驱动状态机的最常用的触发行为。还有其他方法可以触发状态机中的行为(例如计时器),但事件才是真正让用户与状态机交互的方法。事件也被称为“信号”。它们基本上表示可能改变状态机状态的东西。 + +#### [](#transitions)转换 + +转换是源状态和目标状态之间的关系。从一种状态切换到另一种状态是由触发器引起的状态转换。 + +##### [](#internal-transition)内部转换 + +当需要运行一个操作而不需要引起状态转换时,使用内部转换。在内部转换中,源状态和目标状态总是相同的,并且在没有状态进入和退出动作的情况下,它与自转换相同。 + +##### [](#external-versus-local-transitions)外部与局部转换 + +在大多数情况下,外部和局部转换在功能上是等价的,除非转换发生在超级状态和次状态之间。如果目标状态是源状态的子状态,则局部转换不会导致源状态的退出和进入。相反,如果目标是源状态的超状态,则局部转换不会导致目标状态的退出和进入。下面的图像显示了具有非常简单的超级和次状态的局部和外部转换之间的区别: + +statechart4 + +#### [](#triggers)触发器 + +触发器开始转换。触发器可以由事件驱动,也可以由计时器驱动。 + +#### [](#actions)动作 + +动作实际上是将状态机状态更改粘附到用户自己的代码中。状态机可以对状态机中的各种更改和步骤(例如进入或退出状态)或进行状态转换运行操作。 + +动作通常具有对状态上下文的访问权限,这使运行中的代码可以选择以各种方式与状态机交互。状态上下文公开了整个状态机,因此用户可以访问扩展的状态变量、事件头(如果转换是基于事件的话),或者是一种实际的转换(在这种转换中,可以看到更详细的关于这种状态变化来自何处以及它将走向何处的信息)。 + +#### [](#hierarchical-state-machines)分层状态机 + +当特定的状态必须同时存在时,层次状态机的概念被用来简化状态设计。 + +分层状态实际上是 UML 状态机相对于传统状态机(如 Mealy 或 Moore)的一种创新。层次结构状态允许你定义某种级别的抽象(类似于 Java 开发人员如何使用抽象类定义类结构)。例如,使用嵌套状态机,你可以在多个状态级别上定义转换(可能具有不同的条件)。状态机总是尝试查看当前状态是否能够处理事件,以及转换保护条件。如果这些条件不求值到`TRUE`,状态机只看到超级状态可以处理什么。 + +#### [](#regions)区域 + +区域(也称为正交区域)通常被视为应用于状态的排他或(异或)操作。用状态机表示的区域的概念通常有点难以理解,但是通过一个简单的示例,事情会变得简单一些。 + +我们中的一些人拥有全尺寸键盘,主键在左侧,数字键在右侧。你可能已经注意到,双方都有自己的状态,如果你按下一个“Numlock”键(它只会改变数字键盘本身的行为),你就会看到这种状态。如果你没有一个全尺寸的键盘,你可以购买一个外部 USB 号码板。考虑到键盘的左侧和右侧可以彼此独立存在,它们必须具有完全不同的状态,这意味着它们在不同的状态机上运行。在状态机术语中,键盘的主要部分是一个区域,而数字键盘是另一个区域。 + +将两个不同的状态机作为完全独立的实体来处理会有点不方便,因为它们仍然以某种方式一起工作。这种独立性允许正交区域在状态机的单个状态中以多个同时状态合并在一起。 + +## [](#appendices-zookeeper)附录 C:分布式状态机技术论文 + +本附录提供了关于使用 Spring Statemachine 的 ZooKeeper 实例的更详细的技术文档。 + +### [](#abstract)摘要 + +在单个 JVM 上运行的单个状态机实例之上引入“分布式状态”是一个困难而复杂的主题。“分布式状态机”的概念在简单状态机的基础上引入了一些相对复杂的问题,这是由于它的运行到完成模型,以及更普遍的是由于它的单线程执行模型,尽管正交区域可以并行运行。另一个自然的问题是,状态机转换执行是由触发器驱动的,触发器基于`event`或`timer`。 + +Spring 状态机试图通过支持分布式状态机来解决通过 JVM 边界跨越通用“状态机”的问题。在这里,我们展示了你可以在多个 JVM 和 Spring 应用程序上下文中使用通用的“状态机”概念。 + +我们发现,如果`Distributed State Machine`抽象是精心选择的,并且支持分布式状态库保证`CP`就绪,那么就有可能创建一个一致的状态机,该状态机可以在集成中的其他状态机之间共享分布式状态。 + +我们的结果表明,如果支持存储库是“CP”(讨论[later](#state-machine-technical-paper-introduction)),则分布式状态更改是一致的。我们期望我们的分布式状态机能够为需要处理共享分布式状态的应用程序提供一个基础。该模型旨在为云应用程序提供更好的方法,使其能够更容易地相互通信,而无需显式地构建这些分布式状态概念。 + +### [](#state-machine-technical-paper-introduction)引言 + +Spring 状态机不强制使用单线程执行模型,因为,一旦使用了多个区域,如果应用了必要的配置,则可以并行执行区域。这是一个重要的主题,因为一旦用户想要执行并行状态机,它就会使独立区域的状态更改更快。 + +当状态更改不再由本地 JVM 或本地状态机实例中的触发器驱动时,转换逻辑需要在任意持久存储中进行外部控制。该存储需要有一种方法,在分布式状态更改时通知参与状态机。 + +[上限定理](https://en.wikipedia.org/wiki/CAP_theorem)指出,分布式计算机系统不可能同时提供以下三种保证:一致性、可用性和分区容限。 + +这意味着,无论选择什么作为支持持久性存储,最好是“CP”。在这种情况下,“CP”的意思是“一致性”和“分区容忍”。很自然,分布式服务器并不关心它的“上限”级别,但在现实中,“一致性”和“分区容忍度”比“可用性”更重要。这就是为什么(例如)ZooKeeper 使用“CP”存储的确切原因。 + +本文中介绍的所有测试都是通过在以下环境中运行定制的 Jepsen 测试来完成的: + +* 具有节点 n1、n2、n3、n4 和 n5 的簇。 + +* 每个节点都有一个`Zookeeper`实例,该实例与所有其他节点一起构造一个集成。 + +* 每个节点都安装了[Web](#statemachine-examples-web)示例,以连接到本地的`Zookeeper`节点。 + +* 每个状态机实例仅与本地`Zookeeper`实例通信。虽然将一台机器连接到多个实例是可能的,但此处不使用它。 + +* 启动所有状态机实例时,使用 ZooKeeper 集成创建`StateMachineEnsemble`。 + +* 每个示例都包含一个自定义 REST API,Jepsen 使用该 API 发送事件并检查特定的状态机状态。 + +`Spring Distributed Statemachine`的所有 Jepsen 测试都可以从[杰普森测试。](https://github.com/spring-projects/spring-statemachine/tree/master/jepsen/spring-statemachine-jepsen)获得 + +### [](#generic-concepts)通用概念 + +`Distributed State Machine`的一个设计决策是,不要让每个单独的状态机实例意识到它是“分布式集成”的一部分。因为`StateMachine`的主要功能和特性可以通过其接口访问,所以将此实例包装在`DistributedStateMachine`中是有意义的,该实例拦截所有状态机通信,并与一个集成协作来协调分布式状态更改。 + +另一个重要的概念是能够保存来自状态机的足够信息,以便将状态机状态从任意状态重置为新的反序列化状态。当一个新的状态机实例与一个集成连接并需要将其内部状态与分布式状态同步时,这是自然需要的。与使用分布式状态和状态持久化的概念一起,创建分布式状态机是可能的。目前,`Distributed State Machine`的唯一支持存储库是通过使用 ZooKeeper 实现的。 + +正如[使用分布状态](#sm-distributed)中提到的,通过将`StateMachine`的实例包装在`DistributedStateMachine`中来启用分布式状态。具体的`StateMachineEnsemble`实现是`ZookeeperStateMachineEnsemble`提供与 ZooKeeper 的集成。 + +### [](#the-role-of-zookeeperstatemachinepersist)`ZookeeperStateMachinePersist`的作用 + +我们希望有一个通用接口(`StateMachinePersist`),它可以将`StateMachineContext`持久化到任意存储中,并且`ZookeeperStateMachinePersist`为`Zookeeper`实现这个接口。 + +### [](#the-role-of-zookeeperstatemachineensemble)`ZookeeperStateMachineEnsemble`的作用 + +虽然分布式状态机使用一组序列化的上下文来更新其自身的状态,但在使用 ZooKeeper 时,我们遇到了一个关于如何侦听这些上下文更改的概念性问题。我们可以将上下文序列化到 ZooKeeper`znode`中,并最终在`znode`数据被修改时进行监听。但是,`Zookeeper`并不能保证每次数据更改都会收到通知,因为对于`znode`已注册的`watcher`一旦触发就会被禁用,并且用户需要重新注册`watcher`。在此短时间内,`znode`数据可以被更改,从而导致事件丢失。通过以并发方式更改来自多个线程的数据,实际上很容易错过这些事件。 + +为了克服这个问题,我们将单个上下文的更改保持在多个`znodes`中,并使用一个简单的整数计数器来标记`znode`是当前活动的。这样做可以让我们重播错过的事件。我们不希望创建越来越多的 Znode,然后稍后删除旧的 Znode。相反,我们使用了一个简单的 Znode 循环集的概念。这使我们能够使用预定义的 Znodes 集,其中当前节点可以通过一个简单的整数计数器来确定。通过跟踪主`znode`数据版本(在`Zookeeper`中,它是一个整数),我们已经拥有了这个计数器。 + +循环缓冲区的大小被要求为 2 的幂,以避免整数溢出时出现麻烦。因此,我们不需要处理任何具体的案件。 + +### [](#distributed-tolerance)分布式公差 + +为了展示在实际生活中针对状态机的各种分布式操作是如何工作的,我们使用一组 Jepsen 测试来模拟在实际分布式集群中可能发生的各种情况。其中包括网络层面上的“大脑分裂”,多个“分布式状态机”的并行事件,以及“扩展状态变量”的变化。Jepsen 测试基于[Web](#statemachine-examples-web)示例,其中此示例实例在多个主机上运行,在运行状态机的每个节点上都有一个 ZooKeeper 实例。从本质上讲,每个状态机样本都连接到一个本地 ZooKeeper 实例,这使我们能够通过使用 Jepsen 来模拟网络条件。 + +本章后面所示的图形包含直接映射到状态图的状态和事件,可以在[Web](#statemachine-examples-web)中找到。 + +#### [](#sm-tech-isolated-events)孤立事件 + +将一个孤立的单个事件发送到集成中的一个状态机中是最简单的测试场景,并且演示了一个状态机中的状态更改在集成中被正确地传播到其他状态机中。 + +在此测试中,我们演示了一台机器中的状态更改最终会导致其他机器中的状态更改一致。下图显示了测试状态机的事件和状态更改: + +sm tech isolated events + +在前面的图片中: + +* 所有机器都报告状态`S21`。 + +* 事件`I`被发送到节点`n1`,并且所有节点报告状态从`S21`更改为`S22`。 + +* 事件`C`被发送到节点`n2`,并且所有节点报告状态从`S22`更改为`S211`。 + +* 事件`I`被发送到节点`n5`,并且所有节点报告状态从`S211`更改为`S212`。 + +* 事件`K`被发送到节点`n3`,并且所有节点报告状态从`S212`更改为`S21`。 + +* 我们循环事件`I`,`C`,`I`,和`K`一次,通过随机节点。 + +#### [](#parallel-events)并行事件 + +多个分布式状态机的一个逻辑问题是,如果同一事件在完全相同的时间被发送到多个状态机,那么其中只有一个事件会导致分布式状态转换。这在某种程度上是一种预期的场景,因为能够更改分布式状态的第一个状态机(对于此事件)控制分布式转换逻辑。实际上,所有接收到相同事件的其他机器都会默默地丢弃该事件,因为分布式状态不再处于可以处理特定事件的状态。 + +在下图所示的测试中,我们证明了在整个集合中由并行事件引起的状态变化最终会在所有机器中引起一致的状态变化: + +sm tech parallel events + +在前面的图像中,我们使用了与前面的示例中使用的相同的事件流([孤立事件](#sm-tech-isolated-events)),不同的是,事件总是被发送到所有节点。 + +#### [](#concurrent-extended-state-variable-changes)并发扩展状态变量更改 + +扩展状态机变量不能保证在任何给定的时间都是原子的,但是,在分布式状态更改之后,集成中的所有状态机都应该具有同步的扩展状态。 + +在这个测试中,我们证明了在一个分布式状态机中扩展状态变量的变化最终会在所有分布式状态机中变得一致。下图显示了这个测试: + +sm tech isolated events with variable + +在前面的图片中: + +* 事件`J`是发送到节点`n5`,带有事件变量`testVariable`的值`v1`。然后,所有节点报告具有一个名为`testVariable`的变量,其值为`v1`。 + +* 事件`J`从变量`v2`重复到`v8`,执行相同的检查。 + +#### [](#partition-tolerance)分区公差 + +我们需要始终假定,集群中的事情迟早会出问题,无论是 ZooKeeper 实例的崩溃、状态机的崩溃,还是“大脑分裂”之类的网络问题。(大脑分裂是指现有集群成员被隔离,因此只有部分宿主能够看到彼此的情况)。通常的情况是,大脑分裂会造成一个整体的少数和多数分割,这样在网络状态得到修复之前,少数群体中的主机无法参与一个整体。 + +在下面的测试中,我们证明了在集合中分裂的各种类型的大脑最终会导致所有分布状态机处于完全同步的状态。 + +在网络中,有两种情况是直脑分割的,其中`Zookeeper`和`Statemachine`实例被分割为两半(假设每个`Statemachine`实例都连接到本地`Zookeeper`实例): + +* 如果当前的动物园管理员领导保持在多数,那么所有与多数相关的客户都会保持正常运行。 + +* 如果当前的 ZooKeeper 领导者是少数人,所有客户都会与其断开连接,并尝试重新连接,直到以前的少数人成员成功地重新加入现有的多数人团队。 + +| |在我们当前的 Jepsen 测试中,我们不能将 ZooKeeper Split-Brain
中的领导者处于多数或少数之间的情况分开,因此我们需要
多次运行测试来完成这种情况。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在下面的图中,我们将状态机错误状态映射到`error`,以表示状态机处于错误状态,而不是
正常状态。在解释图表状态时,请记住这一点。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在第一个测试中,我们展示了,当一个现有的动物园管理员领导者被保留在大多数情况下,五分之三的机器继续保持原状。下图显示了这个测试: + +sm tech partition half 1 + +在前面的图片中: + +* 第一个事件`C`被发送到所有机器,导致状态更改为`S211`。 + +* Jepsen Nemesis 导致大脑分裂,这会导致`n1/n2/n5`和`n3/n4`的分裂。节点`n3/n4`留在少数,节点`n1/n2/n5`构造新的健康多数。大多数节点保持正常运行,没有问题,但少数节点进入错误状态。 + +* Jepsen 修复了网络,一段时间后,节点`n3/n4`重新加入到集成中,并同步其分布式状态。 + +* 最后,将事件`K1`发送到所有状态机,以确保集成正常工作。此状态更改将返回到状态`S21`。 + +在第二个测试中,我们表明,当现有的动物园管理员领导者保持在少数时,所有的机器都会出错。下图显示了第二个测试: + +sm tech partition half 2 + +在前面的图片中: + +* 第一个事件`C`被发送到所有机器,导致状态更改为`S211`。 + +* Jepsen Nemesis 会导致大脑分裂,这会导致分割,使得现有的`Zookeeper`领导者保持在少数,并且所有实例都与集合断开连接。 + +* Jepsen 修复了网络,一段时间后,所有节点都会重新加入到集成中,并同步其分布式状态。 + +* 最后,将事件`K1`发送到所有状态机,以确保 Ensemble 正常工作。此状态更改将返回到状态`S21`。 + +#### [](#crash-and-join-tolerance)崩溃和连接公差 + +在这个测试中,我们演示了杀死一个现有的状态机,然后将一个新实例重新加入到一个集成中,可以保持分布式状态的健康,并且新加入的状态机可以正确地同步它们的状态。下图显示了碰撞和连接公差测试: + +sm tech stop start + +| |在此测试中,不首先检查`X`与最后`X`之间的状态。
因此,该图显示了介于两者之间的一条平直线。检查状态
与`S21`之间状态变化的确切位置。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在前面的图片中: + +* 所有状态机都从初始状态(`S21`)转换为状态`S211`,这样我们就可以在连接期间测试适当的状态同步。 + +* `X`标记特定节点已崩溃并启动的时间。 + +* 同时,我们请求所有机器的状态并绘制结果。 + +* 最后,我们做一个简单的转换,从`S211`返回`S21`,以确保所有状态机仍然正常工作。 + +## [](#devdocs)开发人员文档 + +本附录为可能想要参与的开发人员或其他想要理解状态机如何工作或理解其内部概念的人员提供了通用信息。 + +### [](#devdocs-configmodel)statemachine 配置模型 + +`StateMachineModel`和其他相关的 SPI 类是各种配置和工厂类之间的抽象。这也允许其他人更容易地集成来构建状态机。 + +如下面的清单所示,你可以通过使用配置数据类构建一个模型,然后要求工厂构建一个状态机来实例化状态机: + +``` +// setup configuration data +ConfigurationData configurationData = new ConfigurationData<>(); + +// setup states data +Collection> stateData = new ArrayList<>(); +stateData.add(new StateData("S1", true)); +stateData.add(new StateData("S2")); +StatesData statesData = new StatesData<>(stateData); + +// setup transitions data +Collection> transitionData = new ArrayList<>(); +transitionData.add(new TransitionData("S1", "S2", "E1")); +TransitionsData transitionsData = new TransitionsData<>(transitionData); + +// setup model +StateMachineModel stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, + transitionsData); + +// instantiate machine via factory +ObjectStateMachineFactory factory = new ObjectStateMachineFactory<>(stateMachineModel); +StateMachine stateMachine = factory.getStateMachine(); +``` + +## [](#appendix-reactormigrationguide)附录 D:反应堆迁移指南 + +`3.x`工作的主要任务是在内部和外部尽可能多地从命令式代码移动和更改到一个反应世界。这意味着一些主接口已经添加了新的 reative 方法,并且大多数内部执行 LOCIG(在适用的情况下)已经被转移到由反应堆处理。本质上,这意味着线程处理模型与`2.x`相比有很大的不同。下面几章将对所有这些变化进行回顾。 + +### [](#communicating-with-a-machine)与机器通信 + +我们在`StateMachine`中添加了新的反应方法,同时仍然保留了旧的阻塞事件方法。 + +``` +Flux> sendEvent(Mono> event); + +Flux> sendEvents(Flux> events); + +Mono>> sendEventCollect(Mono> event); +``` + +我们现在只研究 Spring `Message`和反应堆`Mono`和`Flux`类。你可以发送一个`Mono`的`Message`,并接收回一个`Flux`的`StateMachineEventResult`。请记住,在订阅`Flux`之前,不会发生任何事情。有关此返回值的更多信息,请参见[Statemachineeversult](#sm-triggers-statemachineeventresult)。方法`sendEventCollect`只是一个语法糖,用于传递`Mono`并获得`Mono`,将结果包装为列表。 + +``` +Message message = MessageBuilder.withPayload("EVENT").build(); +machine.sendEvent(Mono.just(message)).subscribe(); +``` + +你还可以发送`Flux`消息,而不是单个`Mono`消息。 + +``` +machine.sendEvents(Flux.just(message)).subscribe(); +``` + +所有的反应堆方法都在你的处置中,例如,当事件处理完成时,不要阻塞并执行某些操作,你可以执行类似的操作。 + +``` +Mono> mono = Mono.just(MessageBuilder.withPayload("EVENT").build()); +machine.sendEvent(mono) + .doOnComplete(() -> { + System.out.println("Event handling complete"); + }) + .subscribe(); +``` + +对于已接受的状态,返回`boolean`的旧 API 方法仍然存在,但不建议在将来的版本中删除。 + +``` +boolean accepted = machine.sendEvent("EVENT"); +``` + +### [](#taskexecutor-and-taskscheduler)任务执行器和任务调度程序 + +具有`TaskExecutor`的静态机器执行和具有`TaskScheduler`的状态动作调度已被完全取代,以利于反应堆的执行和调度。 + +从本质上讲,在主线程之外的执行需要在两个地方执行,首先是 *state actions*,它需要是可删除的,其次是*地区*,它应该总是独立执行的。目前,我们选择只使用*反应堆*`Schedulers.parallel()`,这应该会带来相对较好的结果,因为它试图自动使用系统中可用的 CPU 内核数量。 + +### [](#reactive-examples)反应实例 + +虽然大多数示例仍然是相同的,但我们已经对其中的一些示例进行了全面检查,并创建了一些新的示例: + +* tunrstile ractive[旋转门反应式](#statemachine-examples-turnstilereactive) \ No newline at end of file diff --git a/docs/spring-vault/spring-vault.md b/docs/spring-vault/spring-vault.md new file mode 100644 index 0000000..2eca942 --- /dev/null +++ b/docs/spring-vault/spring-vault.md @@ -0,0 +1,2089 @@ +# Spring 保险库-参考文献 + +# [](#preface)序言 + +Spring Vault 项目将核心 Spring 概念应用于使用 HashiCorpVault 的解决方案的开发。我们提供了一个“模板”作为存储和查询文档的高级抽象。你将注意到与 Spring 框架中的 REST 支持的相似之处。 + +这份文件是 Spring Vault 的参考指南。它解释了 Vault 的概念、语义和语法。 + +参考文档的这一部分解释了 Spring Vault 提供的核心功能。 + +[保险库支持](#vault.core)介绍了 Vault 模块功能集。 + +## [](#preface.document-structure)1。文件结构 + +这一部分提供了 Spring 和 Vault 的基本介绍。它包含有关后续开发和如何获得支持的详细信息。 + +文档的其余部分引用了 Spring Vault 特性,并假定用户熟悉[HashiCorp 保险库](https://www.vaultproject.io)以及 Spring 概念。 + +## [](#get-started:first-steps:spring)2。知道 Spring + +Spring Vault 使用 Spring Framework 的[core](https://docs.spring.io/spring/docs/5.3.4/spring-framework-reference/core.html)功能,例如[IoC](https://docs.spring.io/spring/docs/5.3.4/spring-framework-reference//core.html)容器。虽然了解 Spring API 并不重要,但了解它们背后的概念是重要的。至少,对于你选择使用的任何 IOC 容器,IOC 背后的思想应该是熟悉的。 + +Vault 支持的核心功能可以直接使用,而不需要调用 Spring 容器的 IoC 服务。这很像`RestTemplate`,它可以在没有 Spring 容器的任何其他服务的情况下“独立”使用。为了利用 Spring Vault 文档的所有特性,例如会话支持,你将需要使用 Spring 配置库的某些部分。 + +要了解有关 Spring 的更多信息,你可以参考详细解释 Spring 框架的全面(有时是解除武装)文档。有很多关于这个问题的文章、博客条目和书籍--看看 Spring 框架[home page ](https://spring.io/docs),了解更多信息。 + +## [](#get-started:first-steps:vault)3。知道保险库 + +安全性和处理秘密是每个处理数据库、用户凭据或 API 密钥的开发人员关心的问题。Vault 通过提供与访问控制、撤销、密钥滚动和审计相结合的安全存储来介入。简而言之:Vault 是一种安全访问和存储秘密的服务。秘密是你想要严格控制访问权限的任何东西,例如 API 密钥、密码、证书等等。 + +学习跳马的起点是[WWW,VaultProject.io。](https://www.vaultproject.io)。以下是一些有用的资源: + +* 该手册介绍了 Vault,并包含入门指南、参考文档和教程的链接。 + +* Online Shell 结合在线教程提供了一种方便的方式来与 Vault 实例交互。 + +* [HashiCorp 保险库介绍](https://www.vaultproject.io/intro/index.html) + +* [HashiCorp 保险库文档](https://www.vaultproject.io/docs/index.html) + +Spring Vault 提供了用于访问、存储和撤销秘密的客户端支持。有了[HashiCorp 的保险库](https://www.vaultproject.io),你就有了一个中心位置来管理跨所有环境的应用程序的外部秘密数据。Vault 可以管理静态和动态秘密,例如应用程序数据、远程应用程序/资源的用户名/密码,并为外部服务(例如 MySQL、PostgreSQL、 Apache Cassandra、Consul、AWS 等)提供凭据。 + +## [](#requirements)4。所需经费 + +Spring Vault2.x 二进制文件要求 JDK 级别为 8.0 及以上,并且[Spring Framework](https://spring.io/docs)5.3.4 及以上。 + +就保险库而言,[Vault](https://www.vaultproject.io/)至少为 0.6。 + +## [](#get-started:additional-help)5。额外的帮助资源 + +学习一个新的框架并不总是直截了当的。在这一部分中,我们试图提供一种我们认为易于遵循的指南,用于从 Spring Vault 模块开始。然而,如果你遇到问题或你只是在寻求建议,请使用以下链接之一: + +### [](#get-started:help)5.1。支持 + +有几个可用的支持选项: + +#### [](#get-started:help:community)5.1.1。社区论坛 + +在[StackOverflow](https://stackoverflow.com/questions/tagged/spring-vault)上发布有关 Spring Vault 的问题,以共享信息并相互帮助。请注意,需要注册**只有**才能发布。 + +#### [](#get-started:help:professional)5.1.2。专业支持 + +Spring Vault 和 Spring 背后的公司[Pivotal Software,Inc.](https://pivotal.io/)提供专业的、源代码支持,并保证响应时间。 + +### [](#get-started:up-to-date)5.2。后续发展 + +有关 Spring Vault 源代码库、夜间构建和快照工件的信息,请参见[Spring Vault homepage](https://projects.spring.io/spring-vault/)。通过在[StackOverflow](https://stackoverflow.com/questions/tagged/spring-vault)上通过社区与开发人员进行交互,你可以帮助使 Spring Vault 最好地满足 Spring 社区的需求。如果你遇到错误或希望提出改进建议,请在 Spring vault 问题[tracker](https://github.com/spring-projects/spring-vault/issues)上创建一个票证。要了解 Spring 生态系统中的最新消息和公告,请订阅 Spring 社区[Portal](https://spring.io)。最后,你可以关注 Spring [blog ](https://spring.io/blog)或 Twitter 上的项目团队([SpringCentral](https://twitter.com/springcentral))。 + +## [](#new-features)6。新的和值得注意的 + +### [](#new-features.2-3-0)6.1。最新更新在 Spring Vault2.3 中 + +* 支持用于密钥库和信任库使用的 PEM 编码证书。 + +* `ReactiveVaultEndpointProvider`用于`VaultEndpoint`的非阻塞查找。 + +* `VaultKeyValueMetadataOperations`用于键值元数据交互。 + +* 支持`transform`后端(Enterprise 功能)。 + +* [如何使用保险库秘密后端](#vault.core.secret-engines)的文档。 + +* 每次登录尝试都会重新加载 Kubernetes 和 PCF 身份验证的登录凭据。 + +* `SecretLeaseContainer`在成功的秘密旋转时发布`SecretLeaseRotatedEvent`而不是`SecretLeaseExpiredEvent`和`SecretLeaseCreatedEvent`。 + +* `AbstractVaultConfiguration.threadPoolTaskScheduler()` Bean 类型更改为`TaskSchedulerWrapper`而不是`ThreadPoolTaskScheduler`。 + +### [](#new-features.2-2-0)6.2。最新更新在 Spring Vault2.2 中 + +* 通过`@VaultPropertySource`支持键值 v2(版本控制的后端)秘密。 + +* spel 支持`@Secret`。 + +* 添加对 Jetty 的支持作为反应性 HttpClient。 + +* `LifecycleAwareSessionManager`和`ReactiveLifecycleAwareSessionManager`现在发射`AuthenticationEvent`s。 + +* [PCF 认证](#vault.authentication.pcf). + +* 反对`AppIdAuthentication`。使用`AppRoleAuthentication`,而不是按照 HashiCorp 保险库的建议。 + +* `CubbyholeAuthentication`和 wrapped`AppRoleAuthentication`现在默认使用`sys/wrapping/unwrap`端点。 + +* Kotlin 协程支持`ReactiveVaultOperations`。 + +### [](#new-features.2-1-0)6.3。最新更新在 Spring Vault2.1 中 + +* [GCP 计算](#vault.authentication.gcpgce),[GCP IAM](#vault.authentication.gcpiam),以及[Azure](#vault.authentication.azuremsi)身份验证。 + +* 模板 API 支持版本控制和非版本控制的键/值后端和 Vault 包装操作。 + +* 在反应式认证中支持完全拉动模式. + +* 改进了保险库登录失败的异常层次结构。 + +### [](#new-features.2-0-0)6.4。最新更新在 Spring Vault2.0 中 + +* 身份验证步骤 dsl 到[组合认证流](#vault.authentication.steps)。 + +* [反应式保险库客户端](#vault.core.reactive.template)via`ReactiveVaultOperations`。 + +* 基于 Spring 数据键值的[保险库存储库支持](#vault.repositories)。 + +* 支持传输批加密和解密. + +* 存储为 JSON 的策略的策略管理。 + +* 支持 CSR 签名、证书撤销和 CRL 检索。 + +* [Kubernetes 认证](#vault.authentication.kubernetes). + +* roleid/secretid 展开[Approle 身份验证](#vault.authentication.approle)。 + +* [Spring Security integration](#vault.misc.spring-security)基于传输后端的`BytesKeyGenerator`和`BytesEncryptor`。 + +### [](#new-features.1-1-0)6.5。最新更新在 Spring Vault1.1.0 中 + +* [AWS IAM 身份验证](#vault.authentication.awsiam). + +* 为传输密钥配置加密/解密版本。 + +* [Approle 身份验证](#vault.authentication.approle)的拉动模式。 + +* 支持传输批加密和解密. + +* 基于 TTL 的通用秘密旋转。 + +### [](#new-features.1-0-0)6.6。最新更新在 Spring Vault1.0 中 + +* 最初的保险库支持。 + +# [](#reference-documentation)参考文献 + +## [](#vault.core)7。保险库支持 + +Vault 支持包含一系列广泛的功能,这些功能概述如下。 + +* Spring 使用基于 Java 的配置支持 @Configuration Classes + +* `VaultTemplate`帮助类,用于提高执行公共保险库操作的生产率。包括保险库响应和 POJO 之间的集成对象映射。 + +对于大多数任务,你会发现自己正在使用`VaultTemplate`,它利用了丰富的通信功能。`VaultTemplate`是查找访问功能(例如从 Vault 读取数据或发出管理命令)的位置。`VaultTemplate`还提供了回调方法,这样你就可以轻松地获得低级 API 工件,例如`RestTemplate`,从而直接与 Vault 通信。 + +### [](#dependencies)7.1。依赖关系 + +查找 Spring Vault 依赖关系的兼容版本的最简单的方法是依赖于我们提供的 Spring Vault BOM 以及定义的兼容版本。在 Maven 项目中,你将在你的`pom.xml`的``部分中声明此依赖项: + +例 1。使用 Spring 保险库 BOM + +``` + + + + org.springframework.vault + spring-vault-dependencies + 2.3.1 + import + pom + + + +``` + +目前的版本是`2.3.1`。版本名遵循以下模式:`${version}`用于 GA 和服务版本,`${version}-${release}`用于快照和里程碑。`release`可以是下列情况之一: + +* `SNAPSHOT`-当前快照 + +* `M1`,`M2`等-里程碑 + +* `RC1`,`RC2`等-释放候选项 + +例 2。声明对 Spring Vault 的依赖关系 + +``` + + + org.springframework.vault + spring-vault-core + + +``` + +### [](#dependencies.spring-framework)7.2。 Spring 框架 + +Spring Vault 的当前版本需要版本 5.3.4 或更好的 Spring 框架。这些模块还可以与该小版本的旧 Bugfix 版本一起工作。但是,强烈建议你在这一代中使用最新的版本。 + +## [](#vault.core.getting-started)8。开始 + +Spring Vault 支持需要 Vault0.6 或更高版本和 Java SE6 或更高版本。引导设置工作环境的一种简单方法是在[STS](https://spring.io/tools/sts)中创建一个基于 Spring 的项目。 + +首先,你需要设置一个运行的保险库服务器。有关如何启动 Vault 实例的说明,请参阅[Vault](https://www.vaultproject.io/intro/)。 + +要在 STS 中创建 Spring 项目,请转到文件 New Spring Template Project Simple Spring Utility Project,在提示时按 Yes。然后输入一个项目和一个包名,如`org.spring.vault.example`。 + +然后将以下内容添加到`pom.xml`依赖关系部分。 + +例 3。添加 Spring 保险库依赖项 + +``` + + + + + + org.springframework.vault + spring-vault-core + 2.3.1 + + + +``` + +如果你正在使用一个里程碑或候选版本,那么你还需要将 Spring 里程碑存储库的位置添加到你的 Maven `pom.xml`中,该位置与你的``元素处于同一级别。 + +``` + + + spring-milestone + Spring Maven MILESTONE Repository + https://repo.spring.io/libs-milestone + + +``` + +存储库也是[在这里可浏览](https://repo.spring.io/milestone/org/springframework/vault/)。 + +如果正在使用快照,还需要将 Spring 快照库的位置添加到你的 Maven `pom.xml`中,该位置与你的``元素处于同一级别。 + +``` + + + spring-snapshot + Spring Maven SNAPSHOT Repository + https://repo.spring.io/libs-snapshot + + +``` + +存储库也是[在这里可浏览](https://repo.spring.io/snapshot/org/springframework/vault/)。 + +创建一个简单的`Secrets`类以持久存在: + +例 4。映射数据对象 + +``` +package org.spring.vault.example; + +public class Secrets { + + String username; + String password; + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} +``` + +以及要运行的主应用程序 + +例 5。使用 Spring Vault 的示例应用程序 + +``` +package org.springframework.vault.example; + +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.support.VaultResponseSupport; + +public class VaultApp { + + public static void main(String[] args) { + + VaultTemplate vaultTemplate = new VaultTemplate(new VaultEndpoint(), + new TokenAuthentication("00000000-0000-0000-0000-000000000000")); + + Secrets secrets = new Secrets(); + secrets.username = "hello"; + secrets.password = "world"; + + vaultTemplate.write("secret/myapp", secrets); + + VaultResponseSupport response = vaultTemplate.read("secret/myapp", Secrets.class); + System.out.println(response.getData().getUsername()); + + vaultTemplate.delete("secret/myapp"); + } +} +``` + +即使在这个简单的例子中,也没有什么值得注意的地方。 + +* 可以使用`org.springframework.vault.client.VaultEndpoint`对象和`ClientAuthentication`实例化 Spring Vault 的中心类[`VaultTemplate`](#vault.core.template)。你不需要旋转 Spring 上下文来使用 Spring vault。 + +* Vault 将被配置为使用`00000000-0000-0000-0000-000000000000`的根令牌来运行此应用程序。 + +* 该映射器针对标准的 POJO 对象工作,而不需要任何额外的元数据(尽管你可以选择提供该信息)。 + +* 映射约定可以使用字段访问。注意`Secrets`类只有 getter。 + +* 如果构造函数参数名称与存储文档的字段名称匹配,则将使用它们实例化对象。 + +## [](#vault.core.template)9。VaultTemplate 简介 + +类`VaultTemplate`位于包`org.springframework.vault.core`中,是 Spring 的 Vault 支持的中心类,提供了与 Vault 交互的丰富功能集。该模板提供了在 Vault 中读、写和删除数据的方便操作,并提供了域对象和 Vault 数据之间的映射。 + +| |一旦配置完成,`VaultTemplate`是线程安全的,并且可以在
多个实例中重用。| +|---|------------------------------------------------------------------------------------------------| + +Vault 文档和域类之间的映射是通过委托给`RestTemplate`来完成的。 Spring Web 支持提供了映射基础设施。 + +`VaultTemplate`类实现了接口`VaultOperations`。在尽可能多的情况下,`VaultOperations`上的方法是以 Vault API 上可用的方法命名的,以使熟悉 API 和 CLI 的现有 Vault 开发人员熟悉该 API。例如,你将找到诸如“write”、“delete”、“read”和“revoke”等方法。设计目标是使 Vault API 的使用和`VaultOperations`之间的转换变得尽可能容易。这两个 API 之间的一个主要区别是,`VaultOperations`可以传递域对象,而不是 JSON 键-值对。 + +| |引用`VaultTemplate`实例
上的操作的首选方法是通过其接口`VaultOperations`。| +|---|---------------------------------------------------------------------------------------------------------------------| + +虽然在`VaultTemplate`上有许多方便的方法可以帮助你轻松地执行常见任务,如果你需要直接访问 Vault API 以访问`VaultTemplate`未显式暴露的功能,则可以使用几种执行回调方法中的一种来访问底层 API。Execute 回调将为你提供对`RestOperations`对象的引用。有关更多信息,请参见[执行回调](#vault.core.executioncallback)一节。 + +现在,让我们来看看如何在 Spring 容器的上下文中使用 Vault 的示例。 + +### [](#vault.core.template.beans)9.1。注册和配置 Spring Vault bean + +使用 Spring Vault 不需要 Spring 上下文。但是,在托管上下文中注册的`VaultTemplate`和`SessionManager`实例将参与由 Spring IOC 容器提供的[生命周期事件](https://docs.spring.io/spring/docs/5.3.4/spring-framework-reference/core.html#beans-factory-nature)。这对于在应用程序关闭时处理活动的 Vault 会话非常有用。你还可以在应用程序中重用相同的`VaultTemplate`实例。 + +Spring Vault 附带了一个支持配置类,该配置类提供了 Bean 用于在 Spring 上下文中使用的定义。应用程序配置类通常从`AbstractVaultConfiguration`扩展,并且需要提供环境特定的附加细节。 + +从`AbstractVaultConfiguration`扩展需要实现` VaultEndpoint vaultEndpoint()`和`ClientAuthentication clientAuthentication()`方法。 + +例 6。使用基于 Java 的 Bean 元数据注册 Spring Vault 对象 + +``` +@Configuration +public class AppConfig extends AbstractVaultConfiguration { + + /** + * Specify an endpoint for connecting to Vault. + */ + @Override + public VaultEndpoint vaultEndpoint() { + return new VaultEndpoint(); (1) + } + + /** + * Configure a client authentication. + * Please consider a more secure authentication method + * for production use. + */ + @Override + public ClientAuthentication clientAuthentication() { + return new TokenAuthentication("…"); (2) + } +} +``` + +|**1**|创建一个新的`VaultEndpoint`,默认情况下指向`https://localhost:8200`。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|此示例使用`TokenAuthentication`快速启动。
有关支持的身份验证方法的详细信息,请参见[认证方法](#vault.core.authentication)。| + +例 7。使用注入的财产登记 Spring 保险库 + +``` +@Configuration +public class AppConfig extends AbstractVaultConfiguration { + + @Value("${vault.uri}") + URI vaultUri; + + /** + * Specify an endpoint that was injected as URI. + */ + @Override + public VaultEndpoint vaultEndpoint() { + return VaultEndpoint.from(vaultUri); (1) + } + + /** + * Configure a Client Certificate authentication. + * {@link RestOperations} can be obtained from {@link #restOperations()}. + */ + @Override + public ClientAuthentication clientAuthentication() { + return new ClientCertificateAuthentication(restOperations()); (2) + } +} +``` + +|**1**|`VaultEndpoint`可以使用各种工厂方法构建,例如`from(URI uri)`或`VaultEndpoint.create(String host, int port)`。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`ClientAuthentication`方法的依赖关系可以从`AbstractVaultConfiguration`获得,也可以由配置提供。| + +| |在某些情况下,创建自定义配置类可能很麻烦。
看看`EnvironmentVaultConfiguration`,它允许使用现有属性源的
属性和 Spring 的`Environment`进行配置。在[using`EnvironmentVaultConfiguration`](#vault.core.environment-vault-configuration)中阅读更多
。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.core.template.sessionmanagement)9.2。会话管理 + +Spring Vault 需要`ClientAuthentication`才能登录和访问 Vault。有关身份验证的详细信息,请参见[认证方法](#vault.core.authentication)。Vault 登录不应该发生在每个经过身份验证的 Vault 交互上,而是必须在整个会话中重用。该方面由`SessionManager`实现来处理。a`SessionManager`决定它获得令牌的频率,关于撤销和更新。 Spring Vault 有两种实现方式: + +* `SimpleSessionManager`:只需从提供的`ClientAuthentication`获取令牌,而无需刷新和撤销 + +* `LifecycleAwareSessionManager`:如果令牌是可更新的,则此`SessionManager`调度令牌更新,并在处置时撤销登录令牌。更新计划使用`AsyncTaskExecutor`。如果使用`AbstractVaultConfiguration`,则默认配置`LifecycleAwareSessionManager`。 + +### [](#vault.core.environment-vault-configuration)9.3。使用`EnvironmentVaultConfiguration` + +Spring Vault 包括从 Spring 的`Environment`中配置 Vault 客户端的`EnvironmentVaultConfiguration`和一组预定义的属性键。`EnvironmentVaultConfiguration`支持经常应用的配置。从最合适的配置类派生支持其他配置。将`EnvironmentVaultConfiguration`与`@Import(EnvironmentVaultConfiguration.class)`一起包含到现有的基于 Java 的配置类中,并通过 Spring 的`PropertySource`s 中的任何一个提供配置属性。 + +例 8。对属性文件使用 EnvironmentVaultConfiguration + +基于 Java 的配置类 + +``` +@PropertySource("vault.properties") +@Import(EnvironmentVaultConfiguration.class) +public class MyConfiguration{ +} +``` + +Vault.Properties + +``` +vault.uri=https://localhost:8200 +vault.token=00000000-0000-0000-0000-000000000000 +``` + +**属性键** + +* Vault URI:`vault.uri` + +* SSL 配置 + + * 密钥存储库资源:`vault.ssl.key-store`(可选) + + * 密钥存储库密码:`vault.ssl.key-store-password`(可选) + + * 密钥存储库类型:`vault.ssl.key-store-type`(可选的,通常`jks`,还支持`pem`) + + * 信任存储资源:`vault.ssl.trust-store`(可选) + + * 信任存储库密码:`vault.ssl.trust-store-password`(可选) + + * 信任存储类型:`vault.ssl.trust-store-type`(可选的,通常`jks`,也支持`pem`) + +* 认证方法:`vault.authentication`(默认为`TOKEN`,支持的认证方法有:`TOKEN`,`APPID`,`APPROLE`,`AWS_EC2`,`AZURE`,`CUBBYHOLE`,`KUBERNETES`) + +**特定于身份验证的属性密钥** + +**[令牌认证](#vault.authentication.token)** + +* 金库令牌:`vault.token` + +**[APPID 身份验证](#vault.authentication.appid)** + +* APPID 路径:`vault.app-id.app-id-path`(默认为`app-id`) + +* appid:`vault.app-id.app-id` + +* userid:`vault.app-id.user-id`。`MAC_ADDRESS`和`IP_ADDRESS`使用`MacAddressUserId`,相应的`IpAddressUserId`用户 ID 机制。任何其他值都与`StaticUserId`一起使用。 + +**[Approle 身份验证](#vault.authentication.approle)** + +* 路径:`vault.app-role.app-role-path`(默认为`approle`) + +* ROLEID:`vault.app-role.role-id` + +* secretID:`vault.app-role.secret-id`(可选) + +**[AWS-EC2 身份验证](#vault.authentication.awsec2)** + +* AWS EC2 路径:`vault.aws-ec2.aws-ec2-path`(默认为`aws-ec2`) + +* 角色:`vault.aws-ec2.role` + +* ROLEID:`vault.aws-ec2.role-id`(** 不推荐:** 用`vault.aws-ec2.role`代替) + +* 身份证件网址:`vault.aws-ec2.identity-document`(默认为`[http://169.254.169.254/latest/dynamic/instance-identity/pkcs7](http://169.254.169.254/latest/dynamic/instance-identity/pkcs7)`) + +**[Azure(MSI)认证](#vault.authentication.azuremsi)** + +* Azure MSI 路径:`vault.azure-msi.azure-path`(默认为`azure`) + +* 角色:`vault.azure-msi.role` + +* 元数据服务 URL:`vault.azure-msi.metadata-service`(默认为`[http://169.254.169.254/metadata/instance?api-version=2017-08-01](http://169.254.169.254/metadata/instance?api-version=2017-08-01)`) + +* Identity TokenService URL:`vault.azure-msi.identity-token-service`(默认为`[http://169.254.169.254/metadata/identity/oauth2/token?resource=https://vault.hashicorp.com&api-version=2018-02-01](http://169.254.169.254/metadata/identity/oauth2/token?resource=https://vault.hashicorp.com&api-version=2018-02-01)`) + +**[TLS 证书认证](#vault.authentication.clientcert)** + +没有配置选项。 + +**[空穴身份验证](#vault.authentication.cubbyhole)** + +* 初始保险库令牌:`vault.token` + +**[Kubernetes 认证](#vault.authentication.kubernetes)** + +* Kubernetes 路径:`vault.kubernetes.kubernetes-path`(默认为`kubernetes`) + +* 角色:`vault.kubernetes.role` + +* 服务帐户令牌文件的路径:`vault.kubernetes.service-account-token-file`(默认为`/var/run/secrets/kubernetes.io/serviceaccount/token`) + +### [](#vault.core.executioncallback)9.4。执行回调 + +所有 Spring 模板类的一个常见设计特征是,所有功能都被路由到一个模板执行回调方法。这有助于确保执行异常和可能需要的任何资源管理的一致性。虽然在 JDBC 和 JMS 的情况下,这比 Vault 的需要大得多,但它仍然为访问和日志记录的发生提供了一个单一的位置。因此,使用 Execute Callback 是访问 Vault API 的首选方式,以执行我们尚未作为`VaultTemplate`上的方法公开的不常见操作。 + +下面是执行回调方法的列表。 + +* ` T`**DowithVault**`(RestOperationsCallback callback)`执行给定的`RestOperationsCallback`,允许使用`RestOperations`与 Vault 进行交互,而不需要会话。 + +* ` T`**首次会议**`(RestOperationsCallback callback)`执行给定的`RestOperationsCallback`,允许在经过身份验证的保险库中进行交互会话。 + +下面是一个使用`ClientCallback`初始化 Vault 的示例: + +``` +vaultOperations.doWithVault(new RestOperationsCallback() { + + @Override + public VaultInitializationResponse doWithRestOperations(RestOperations restOperations) { + + ResponseEntity exchange = restOperations + .exchange("/sys/init", HttpMethod.PUT, + new HttpEntity(request), + VaultInitializationResponse.class); + + return exchange.getBody(); + } +}); +``` + +## [](#vault.core.secret-engines)10。支持 Vault 的秘密引擎 + +Spring Vault 船有几个扩展,以支持 Vault 的各种秘密引擎。 + +具体地说, Spring 带有扩展的保险库船舶用于: + +* [键值版本 1(“无版本的秘密”)](#vault.core.backends.kv1) + +* [键值版本 2(“版本管理的秘密”)](#vault.core.backends.kv2) + +* [PKI(公开密钥基础设施)](#vault.core.backends.pki) + +* [令牌认证后端](#vault.core.backends.token) + +* 转换(Enterprise 特性) + +* [传输后端](#vault.core.backends.transit) + +* 系统后端 + +你可以通过`VaultTemplate`上的方法直接使用所有其他后端(`VaultTemplate.read(…)`,`VaultTemplate.write(…)`)。 + +### [](#vault.core.backends.kv1)10.1。键值版本 1(“无版本的秘密”) + +`kv`秘密引擎用于在 Vault 配置的物理存储中存储任意秘密。 + +当以一种非版本管理的方式运行`kv`秘密引擎时,只保留密钥的最近写入的值。无版本 KV 的好处是减少了每个键的存储大小,因为不会存储额外的元数据或历史记录。此外,以这种方式配置到后端的请求性能更好,因为对于任何给定的请求,存储调用更少,也没有锁定。 + +Spring Vault 附带一个专用的键值 API,以封装各个键值 API 实现之间的差异。`VaultKeyValueOperations`遵循 Vault CLI 的设计。这是 Vault 提供诸如`vault kv get`、`vault kv put`等命令的主要命令行工具。 + +通过指定版本和挂载路径,你可以将此 API 用于两个键值引擎版本。下面的示例使用键值版本 1: + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); +VaultKeyValueOperations keyValueOperations = operations.opsForKeyValue("secret", + VaultKeyValueOperationsSupport.KeyValueBackend.KV_1); + +keyValueOperations.put("elvis", Collections.singletonMap("password", "409-52-2002")); + +VaultResponse read = keyValueOperations.get("elvis"); +read.getRequiredData().get("social-security-number"); +``` + +`VaultKeyValueOperations`支持所有键值操作,如`put`,`get`,`delete`,`list`。 + +或者,可以通过`VaultTemplate`使用该 API,因为其直接映射和简单的使用,因为键和响应直接映射到输入和输出键。下面的示例演示了在`mykey`处写和读一个秘密。`kv`秘密引擎安装在`secret`: + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); + +operations.write("secret/elvis", Collections.singletonMap("social-security-number", "409-52-2002")); + +VaultResponse read = operations.read("secret/elvis"); +read.getRequiredData().get("social-security-number"); +``` + +你可以在 Vault 参考文档中找到有关[Vault 键-value 版本 1API](https://www.vaultproject.io/api-docs/secret/kv/kv-v1)的更多详细信息。 + +### [](#vault.core.backends.kv2)10.2。键值版本 2(“版本管理的秘密”) + +你可以在两个版本中的一个版本中运行`kv`秘密引擎。本节使用版本 2 进行说明。当运行版本 2 的`kv`后端时,一个键可以保留可配置的版本数量。你可以检索旧版本的元数据和数据。此外,你还可以使用检查和设置操作来避免无意中覆盖数据。 + +与[键值版本 1(“无版本的秘密”)](#vault.core.backends.kv1)类似, Spring Vault 附带了一个专用的键值 API,以封装各个键值 API 实现之间的差异。 Spring Vault 附带一个专用的键值 API,以封装各个键值 API 实现之间的差异。`VaultKeyValueOperations`遵循 Vault CLI 的设计。这是 Vault 的主要命令行工具,提供诸如`vault kv get`、`vault kv put`等命令。 + +通过指定版本和挂载路径,你可以将此 API 用于两个键值引擎版本。下面的示例使用键值版本 2: + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); +VaultKeyValueOperations keyValueOperations = operations.opsForKeyValue("secret", + VaultKeyValueOperationsSupport.KeyValueBackend.KV_2); + +keyValueOperations.put("elvis", Collections.singletonMap("social-security-number", "409-52-2002")); + +VaultResponse read = keyValueOperations.get("elvis"); +read.getRequiredData().get("social-security-number"); +``` + +`VaultKeyValueOperations`支持所有键值操作,如`put`,`get`,`delete`,`list`。 + +你还可以与版本管理的键值 API 的具体内容进行交互。如果你想要获得特定的秘密或需要访问元数据,这是非常有用的。 + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); +VaultVersionedKeyValueOperations versionedOperations = operations.opsForVersionedKeyValue("secret"); + +Versioned.Metadata metadata = versionedOperations.put("elvis", (1) + Collections.singletonMap("social-security-number", "409-52-2002")); + +Version version = metadata.getVersion(); (2) + +Versioned ssn = versionedOperations.get("elvis", Version.from(42)); (3) + +Versioned mappedSsn = versionedOperations.get("elvis", (4) + Version.from(42), SocialSecurityNumber.class); + +Versioned> versioned = Versioned.create(Collections (5) + .singletonMap("social-security-number", "409-52-2002"), + Version.from(42)); + +versionedOperations.put("elvis", version); +``` + +|**1**|将秘密存储在`elvis`上,在`secret/`挂载下可用。| +|-----|---------------------------------------------------------------------------------------------------| +|**2**|将数据存储在版本控制的后端中,将返回元数据,例如版本号。| +|**3**|版本控制的键值 API 允许检索由版本号标识的特定版本。| +|**4**|版本控制的键值秘密可以映射到值对象中。| +|**5**|当使用 CAS 更新受版本控制的秘密时,输入必须引用先前获得的版本。| + +而使用`kv`V2Secrets 引擎通过`VaultTemplate`是可能的。这不是最方便的方法,因为 API 提供了一种不同的方法来处理上下文路径以及如何表示输入/输出。具体地说,与实际秘密的交互需要对数据部分进行包装和解包装,并在挂载和秘密密钥之间引入`data/`路径段。 + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); + +operations.write("secret/data/elvis", Collections.singletonMap("data", + Collections.singletonMap("social-security-number", "409-52-2002"))); + +VaultResponse read = operations.read("secret/data/ykey"); +Map data = (Map) read.getRequiredData().get("data"); +data.get("social-security-number"); +``` + +你可以在 Vault 参考文档中找到有关[Vault 键-value2API](https://www.vaultproject.io/api-docs/secret/kv/kv-v2)的更多详细信息。 + +### [](#vault.core.backends.pki)10.3。PKI(公开密钥基础设施) + +`pki`Secrets 引擎通过实现证书颁发机构操作来表示证书的后端。 + +PKI 机密引擎生成动态 X.509 证书。使用这个秘密引擎,服务可以获得证书,而无需经过通常的手工过程,即生成私钥和 CSR,提交给 CA,并等待验证和签名过程完成。Vault 内置的身份验证和授权机制提供了验证功能。 + +Spring Vault 通过`VaultPkiOperations`支持证书的颁发、签名、撤销和 CRL 检索。所有其他的 PKI 功能都可以通过`VaultOperations`使用。 + +以下示例简要说明了如何颁发和撤销证书的用法: + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); +VaultPkiOperations pkiOperations = operations.opsForPki("pki"); + +VaultCertificateRequest request = VaultCertificateRequest.builder() (1) + .ttl(Duration.ofHours(48)) + .altNames(Arrays.asList("prod.dc-1.example.com", "prod.dc-2.example.com")) + .withIpSubjectAltName("1.2.3.4") + .commonName("hello.example.com") + .build(); + +VaultCertificateResponse response = pkiOperations.issueCertificate("production", request); (2) +CertificateBundle certificateBundle = response.getRequiredData(); + +KeyStore keyStore = certificateBundle.createKeyStore("my-keystore"); (3) + +KeySpec privateKey = certificateBundle.getPrivateKeySpec(); (4) +X509Certificate certificate = certificateBundle.getX509Certificate(); +X509Certificate caCertificate = certificateBundle.getX509IssuerCertificate(); + +pkiOperations.revoke(certificateBundle.getSerialNumber()); (5) +``` + +|**1**|通过使用`VaultCertificateRequest`Builder 构建一个证书请求。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|从 Vault 请求证书。
Vault 充当证书颁发机构,并使用签名的 X.509 证书进行响应。
实际响应是`CertificateBundle`。| +|**3**|你可以直接获得生成的证书,作为包含公钥和私钥以及颁发者证书的 Java 密钥存储库。KeyStore 有广泛的用途,这使得这种格式适合于配置(例如,HTTP 客户机、数据库驱动程序或 SSL 安全的 HTTP 服务器)。| +|**4**|`CertificateBundle`允许直接通过 Java Cryptography Extension API 访问私钥以及公共和发行者证书。| +|**5**|一旦一个证书不再使用(或者它已被破坏),你可以通过它的序列号来撤销它。
Vault 在其 CRL 中包含了已撤销的证书。| + +你可以在 Vault 参考文档中找到有关[Vault PKI 机密 API](https://www.vaultproject.io/api-docs/secret/pki)的更多详细信息。 + +### [](#vault.core.backends.token)10.4。令牌认证后端 + +此后端是不与实际秘密交互的身份验证后端。相反,它提供了访问令牌管理的访问权限。你可以在[认证方法章节](#vault.core.authentication)中阅读有关[基于令牌的身份验证](#vault.authentication.token)的更多信息。 + +`token`身份验证方法是内置的,并且在`/auth/token`自动可用。它允许用户使用令牌进行身份验证,以及创建新令牌、通过令牌撤销秘密等等。 + +当任何其他 auth 方法返回一个标识时,Vault Core 调用令牌方法为该标识创建一个新的唯一令牌。 + +你还可以使用令牌存储来绕过任何其他的 auth 方法。你可以直接创建令牌,也可以对令牌执行各种其他操作,例如更新和撤销。 + +Spring Vault 使用此后端来更新和撤销由配置的[认证方法](#vault.core.authentication)提供的会话令牌。 + +以下示例展示了如何从应用程序中请求、更新和撤销 Vault 令牌: + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); +VaultTokenOperations tokenOperations = operations.opsForToken(); + +VaultTokenResponse tokenResponse = tokenOperations.create(); (1) +VaultToken justAToken = tokenResponse.getToken(); + +VaultTokenRequest tokenRequest = VaultTokenRequest.builder().withPolicy("policy-for-myapp") + .displayName("Access tokens for myapp") + .renewable() + .ttl(Duration.ofHours(1)) + .build(); + +VaultTokenResponse appTokenResponse = tokenOperations.create(tokenRequest); (2) +VaultToken appToken = appTokenResponse.getToken(); + +tokenOperations.renew(appToken); (3) + +tokenOperations.revoke(appToken); (4) +``` + +|**1**|通过应用角色默认值来创建令牌。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用 Builder API,你可以为要请求的令牌定义细粒度的设置。
请求令牌将返回`VaultToken`,该对象用于 Vault 令牌的值对象。| +|**3**|你可以通过令牌 API 更新令牌。通常,这是通过`SessionManager`来完成的,以保持对保险库会话令牌的跟踪。| +|**4**|如果需要,可以通过令牌 API 撤销令牌。通常,这是通过`SessionManager`来完成的,以保持对保险库会话令牌的跟踪。| + +你可以在 Vault 参考文档中找到有关[Vault Token Auth 方法 API](https://www.vaultproject.io/api-docs/auth/token)的更多详细信息。 + +### [](#vault.core.backends.transit)10.5。传输后端 + +传输秘密引擎处理传输中数据的加密功能。Vault 不存储发送到这个秘密引擎的数据。它也可以被看作是“加密作为一种服务”或“加密作为一种服务”。Transit Secrets 引擎还可以对数据进行签名和验证,生成数据的散列和 HMAC,并充当随机字节源。 + +Transit 的主要用例是对来自应用程序的数据进行加密,同时仍将加密的数据存储在一些主数据存储中。这减轻了应用程序开发人员进行适当加密和解密的负担,并将负担推给了 Vault 的运营商。 + +Spring Vault 支持广泛的中转操作: + +* 密钥创建 + +* 密钥重新配置 + +* 加密/解密/重新包装 + +* HMAC 计算 + +* 签名和签名验证 + +`transit`中的所有操作都以键为中心。Transit 引擎支持键和[各种关键类型](https://www.vaultproject.io/docs/secrets/transit)的版本控制。请注意,键类型可能会对可以使用的操作施加限制。 + +以下示例展示了如何创建密钥以及如何对数据进行加密和解密: + +``` +VaultOperations operations = new VaultTemplate(new VaultEndpoint()); +VaultTransitOperations transitOperations = operations.opsForTransit("transit"); + +transitOperations.createKey("my-aes-key", VaultTransitKeyCreationRequest.ofKeyType("aes128-gcm96")); (1) + +String ciphertext = transitOperations.encrypt("my-aes-key", "plaintext to encrypt"); (2) + +String plaintext = transitOperations.decrypt("my-aes-key", ciphertext); (3) +``` + +|**1**|首先,我们需要一个以.
开头的键,每个键都需要指定的类型。`aes128-gcm96`支持加密、解密、密钥派生和收敛加密,在此示例中,我们需要对其进行加密和解密。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|接下来,我们对包含应该加密的纯文本的`String`进行加密,
输入`String`使用默认的`Charset`将字符串编码为其二进制表示,
请求令牌将返回`VaultToken`,它被用作 Vault 令牌的值对象。
`encrypt`方法返回 base64 编码的密文,通常从`vault:`开始。| +|**3**|要将密文解密为纯文本,请调用`decrypt`方法。
它会解密密文并返回一个`String`,并使用默认字符集对其进行解码。| + +前面的示例使用简单的字符串进行加密操作。虽然它是一种简单的方法,但它有 charset 错误配置的风险,并且不是二进制安全的。当纯文本对数据(如图像、压缩数据或二进制数据结构)使用二进制表示时,需要二进制安全性。 + +要对二进制数据进行加密和解密,请使用`Plaintext`和`Ciphertext`值对象,这些对象可以保存二进制值: + +``` +byte [] plaintext = "plaintext to encrypt".getBytes(); + +Ciphertext ciphertext = transitOperations.encrypt("my-aes-key", Plaintext.of(plaintext)); (1) + +Plaintext decrypttedPlaintext = transitOperations.decrypt("my-aes-key", ciphertext); (2) +``` + +|**1**|假设密钥`my-aes-key`已经存在,我们正在加密`Plaintext`对象。
作为回报,`encrypt`方法返回一个`Ciphertext`对象。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`Ciphertext`对象可以直接用于解密,并返回`Plaintext`对象。| + +`Plaintext`和`Ciphertext`带有一个上下文对象,`VaultTransitContext`。它用于为[收敛加密](https://www.vaultproject.io/docs/secrets/transit#convergent-encryption)提供一个 nonce 值,并为一个上下文值提供一个使用键派生的值。 + +Transit 允许对纯文本进行签名并验证给定纯文本的签名。符号操作需要一个不对称的密钥,通常使用椭圆曲线加密或 RSA。 + +| |签名使用公钥/私钥分割来确保真实性。
签名者使用其私钥创建签名。否则,任何人都可以以你的名义对消息进行签名。
验证者使用公钥部分来验证签名。实际的签名通常是一个散列值。

在内部,散列将使用私钥进行计算和加密,以创建最终签名。验证将解密签名消息,计算它们自己的纯文本散列,并比较两个散列值以检查签名是否有效。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +byte [] plaintext = "plaintext to sign".getBytes(); + +transitOperations.createKey("my-ed25519-key", VaultTransitKeyCreationRequest.ofKeyType("ed25519")); (1) + +Signature signature = transitOperations.sign("my-ed25519-key", Plaintext.of(plaintext)); (2) + +boolean valid = transitOperations.verify("my-ed25519-key", Plaintext.of(plaintext), signature); (3) +``` + +|**1**|签名需要一个不对称的密钥。你可以使用任何椭圆曲线加密或 RSA 密钥类型。一旦创建了密钥,你就拥有了创建签名的所有先决条件。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|为纯文本消息创建签名。返回的`Signature`包含一个使用 base64 字符的 ASCII 安全字符串。| +|**3**|要验证签名,验证需要一个签名对象和纯文本消息。作为返回值,你将得到签名是否有效。| + +你可以在 Vault 参考文档中找到有关[Vault Transit 后端](https://www.vaultproject.io/api/secret/transit)的更多详细信息。 + +## [](#vault.core.reactive.template)11。ReactiveVaultTemplate 简介 + +本节涵盖了关于使用 Spring Vault 的反应式编程支持的基本信息。 + +### 11.1.什么是反应式编程? + +简单地说,反应式编程是关于非阻塞的应用程序,它们是异步和事件驱动的,并且需要少量线程来垂直扩展(即在 JVM 内),而不是水平扩展(即通过集群)。 + +反应性应用程序的一个关键方面是反压力的概念,这是一种确保生产者不会压倒消费者的机制。例如,在从数据库扩展到 HTTP 响应的反应性组件的管道中,当 HTTP 连接太慢时,数据存储库也可以减慢速度或完全停止,直到网络容量释放出来。 + +### 11.2.反应式保险库客户端 + +Spring Vault 的反应性客户端支持是建立在[可组合身份验证步骤](#vault.authentication.steps)和 Spring 的功能性`WebClient`之上的,通过 Reactor 内蒂 或 Jetty,这两个功能都具有完全非阻塞的、事件驱动的 HTTP 客户端。 + +它将`VaultTokenSupplier`作为`VaultToken`的供应商公开以验证 HTTP 请求,并将`ReactiveVaultOperations`作为主要入口点。`VaultEndpoint`、`ClientOptions`和[SSL](#vault.client-ssl)的核心配置在各种客户机实现中被重用。 + +类`ReactiveVaultTemplate`位于包`org.springframework.vault.core`中,是 Spring 的 Reactive Vault 支持的中心类,提供了与 Vault 交互的丰富功能集。该模板提供了在 Vault 中读、写和删除数据的方便操作,并提供了域对象和 Vault 数据之间的映射。 + +| |一旦配置完成,`ReactiveVaultTemplate`是线程安全的,并且可以在
多个实例中重用。| +|---|--------------------------------------------------------------------------------------------------------| + +Vault 文档和域类之间的映射是通过委托给`WebClient`及其编解码器来完成的。 + +`ReactiveVaultTemplate`类实现了接口`ReactiveVaultOperations`。在尽可能多的情况下,`ReactiveVaultOperations`上的方法是以 Vault API 上可用的方法命名的,以使熟悉 API 和 CLI 的现有 Vault 开发人员熟悉该 API。例如,你将找到诸如“write”、“delete”和“read”之类的方法。设计目标是使 Vault API 的使用和`ReactiveVaultOperations`之间的转换变得尽可能容易。这两个 API 之间的一个主要区别是,`ReactiveVaultOperations`可以传递域对象,而不是 JSON 键-值对。 + +| |引用`ReactiveVaultTemplate`实例
上的操作的首选方法是通过其接口`ReactiveVaultOperations`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------| + +`ReactiveVaultTemplate`未显式公开的功能你可以使用几种执行回调方法中的一种来访问底层 API。Execute 回调将为你提供对`WebClient`对象的引用。有关更多信息,请参见[执行回调](#vault.core.reactive.executioncallback)一节。 + +现在,让我们来看看如何在 Spring 容器的上下文中使用 Vault 的示例。 + +### [](#vault.core.reactive.template.beans)11.3。注册和配置 Spring Vault bean + +使用 Spring vault 不需要 Spring 上下文。然而,在托管上下文中注册的`ReactiveVaultTemplate`和`VaultTokenSupplier`的实例将参与由 Spring IOC 容器提供的[生命周期事件](https://docs.spring.io/spring/docs/5.3.4/spring-framework-reference/core.html#beans-factory-nature)。这对于在应用程序关闭时处理活动的 Vault 会话非常有用。你还受益于在应用程序中重用相同的`ReactiveVaultTemplate`实例。 + +Spring Vault 附带了一个支持配置类,该配置类提供了 Bean 用于在 Spring 上下文中使用的定义。应用程序配置类通常从`AbstractVaultConfiguration`扩展,并且需要提供环境特定的附加细节。 + +从`AbstractVaultConfiguration`扩展需要实现` VaultEndpoint vaultEndpoint()`和`ClientAuthentication clientAuthentication()`方法。 + +例 9。使用基于 Java 的 Bean 元数据注册 Spring Vault 对象 + +``` +@Configuration +public class AppConfig extends AbstractReactiveVaultConfiguration { + + /** + * Specify an endpoint for connecting to Vault. + */ + @Override + public VaultEndpoint vaultEndpoint() { + return new VaultEndpoint(); (1) + } + + /** + * Configure a client authentication. + * Please consider a more secure authentication method + * for production use. + */ + @Override + public ClientAuthentication clientAuthentication() { + return new TokenAuthentication("…"); (2) + } +} +``` + +|**1**|创建一个新的`VaultEndpoint`,默认情况下指向`https://localhost:8200`。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|此示例使用`TokenAuthentication`快速启动。
有关支持的身份验证方法的详细信息,请参见[认证方法](#vault.core.authentication)。| + +### [](#vault.core.reactive.template.sessionmanagement)11.4。会话管理 + +Spring Vault 需要令牌来验证 Vault 请求。有关身份验证的详细信息,请参见[认证方法](#vault.core.authentication)。反应式客户端需要一个非阻塞令牌供应商,其契约定义在`VaultTokenSupplier`中。令牌可以是静态的,也可以通过[声明的身份验证流程](#vault.authentication.steps)获得。Vault 登录不应该发生在每个经过身份验证的 Vault 交互上,但是会话令牌应该在会话上保存。该方面由实现`ReactiveSessionManager`的会话管理器处理,例如`ReactiveLifecycleAwareSessionManager`。 + +### [](#vault.core.reactive.executioncallback)11.5。执行回调 + +Spring 所有模板类的一个常见设计特征是,所有功能都被路由到一个模板中执行回调方法。这有助于确保执行异常和可能需要的任何资源管理的一致性。虽然在 JDBC 和 JMS 的情况下,这比 Vault 的需要大得多,但它仍然为访问和日志记录的发生提供了一个单一的位置。因此,使用 Execute 回调是访问 Vault API 的首选方式,以执行我们在`ReactiveVaultTemplate`上没有作为方法公开的不常见操作。 + +下面是执行回调方法的列表。 + +* ` T`**DowithVault**`(Function clientCallback)`组成给定的反应序列`WebClient`,允许在没有会话上下文的情况下与 Vault 进行交互。 + +* 会话` T`**首次会议**`(Function clientCallback)`组成给定的反应序列`WebClient`,允许在经过身份验证的保险库中进行交互。 + +下面是一个使用回调来初始化 Vault 的示例: + +``` +reactiveVaultOperations.doWithVault(webClient -> { + + return webClient.put() + .uri("/sys/init") + .syncBody(request) + .retrieve() + .toEntity(VaultInitializationResponse.class); +}); +``` + +## [](#vault.core.propertysupport)12。保险库财产来源支持 + +保险库可以有许多不同的使用方式。一个特定的用例是使用 Vault 存储加密的属性。 Spring Vault 支持 Vault 作为属性源,以使用 Spring 的[PropertySource 抽象](https://docs.spring.io/spring/docs/5.3.4/spring-framework-reference/core.html#beans-property-source-abstraction)获得配置属性。 + +| |你可以引用存储在 Vault 中的其他属性源中的属性,或者使用`@Value(…)`的值注入。当引导需要存储在保险库中的数据的 bean 时,需要特别注意。此时必须初始化`VaultPropertySource`才能从 Vault 检索属性。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring 引导/ Spring 云用户可以受益于[Spring Cloud Vault](https://github.com/spring-cloud/spring-cloud-vault-config)的
配置集成,该集成在应用程序启动期间初始化各种属性源。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 12.1.注册`VaultPropertySource` + +Spring Vault 提供了一个`VaultPropertySource`以与 Vault 一起使用来获得属性。它使用嵌套的`data`元素公开在 Vault 中存储和加密的属性。 + +``` +ConfigurableApplicationContext ctx = new GenericApplicationContext(); +MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); +sources.addFirst(new VaultPropertySource(vaultTemplate, "secret/my-application")); +``` + +在上面的代码中,`VaultPropertySource`在搜索中被添加了最高优先级。如果它包含一个 foo` property, it will be detected and returned ahead of any `foo` property in any other `PropertySource`.`mutablePropertySources` 暴露了许多方法,这些方法允许对属性源集进行精确操作。 + +### 12.2.@VaultPropertySource + +`@VaultPropertySource`注释提供了一种方便的声明性机制,用于将`PropertySource`添加到 Spring 的`Environment`中,以便与`@Configuration`类一起使用。 + +`@VaultPropertySource`采用 vault 路径,如`secret/my-application`,并公开存储在节点`PropertySource`中的数据。`@VaultPropertySource`支持与租赁相关的秘密的租赁续订(即来自`mysql`后端的凭据)和在终端租赁到期时的凭据旋转。默认情况下,租约续订是禁用的。 + +例 10。存储在保险库中的属性 + +``` +{ + // … + + "data": { + "database": { + "password": ... + }, + "user.name": ..., + } + + // … +} +``` + +例 11。声明`@VaultPropertySource` + +``` +@Configuration +@VaultPropertySource("secret/my-application") +public class AppConfig { + + @Autowired Environment env; + + @Bean + public TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.setUser(env.getProperty("user.name")); + testBean.setPassword(env.getProperty("database.password")); + return testBean; + } +} +``` + +例 12。声明带有凭据旋转和前缀的`@VaultPropertySource` + +``` +@Configuration +@VaultPropertySource(value = "aws/creds/s3-access", + propertyNamePrefix = "aws.", + renewal = Renewal.ROTATE) +public class AppConfig { + // provides aws.access_key and aws.secret_key properties +} +``` + +| |从`generic`秘密后端获得的秘密与 TTL(`refresh_interval`)相关联,但不是租赁 ID。 Spring Vault 的`PropertySource`在到达其 TTL 时会旋转通用秘密。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |你可以使用`@VaultPropertySource`从版本控制的键值后端获得最新的秘密版本。确保路径中不包含`data/`段。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在`@VaultPropertySource`路径中存在的任何`${…​}`占位符都将根据已经针对该环境注册的一组属性源进行解析,如下例所示: + +例 13。使用占位符声明`@VaultPropertySource`路径 + +``` +@Configuration +@VaultPropertySource(value = "aws/creds/${my.placeholder:fallback/value}", + propertyNamePrefix = "aws.", + renewal = Renewal.ROTATE) +public class AppConfig { +} +``` + +假设`my.placeholder`存在于已经注册的一个属性源中(例如,系统属性或环境变量),则将占位符解析为相应的值。如果不是,则将`fallback/value`用作默认值。如果没有指定默认值,并且无法解析某个属性,则抛出一个`IllegalArgumentException`。 + +在某些情况下,当使用`@VaultPropertySource`注释时,严格控制属性源排序可能是不可能的或不实用的。例如,如果上面的`@Configuration`类是通过组件扫描注册的,那么排序是很难预测的。在这种情况下(如果重写很重要),建议用户回到使用 PropertySource API。详见[`ConfigurableEnvironment`](https://DOCS. Spring.io/ Spring-framework/DOCS/current/javadoc-api/org/springframework/core/core/ENV/confirablebletermnirtonment.html)和[](https://DOCS. Spring.io/ Spring/DOCS/current/javadoc-api/org/springframework/core/env/mutablepropertysources.html)) + +## [](#vault.repositories)13。保险库 + +使用`VaultTemplate`和映射到 Java 类的响应可以实现基本的数据操作,如读、写和删除。 Spring Vault 存储库在 Vault 之上应用了数据存储库的概念。Vault 存储库公开了基本的增删改查功能,并支持使用限制 ID 属性、分页和排序的谓词进行查询派生。 + +| |在[Spring Data Commons reference documentation](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#repositories)中阅读有关 Spring 数据存储库的更多信息。参考文档将向你介绍 Spring 数据存储库。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.repositories.usage)13.1。用法 + +要访问存储在 Vault 中的域实体,你可以利用存储库支持,从而大大简化这些实现。 + +例 14。示例凭据实体 + +``` +@Secret +public class Credentials { + + @Id String id; + String password; + String socialSecurityNumber; + Address address; +} +``` + +这里有一个非常简单的域对象。请注意,它有一个名为`id`的属性,并对其类型进行了`org.springframework.data.annotation.Id`注释和`@Secret`注释。这两个人负责创建用于在 Vault 内部将对象持久化为 JSON 的实际密钥。 + +| |用`@Id`注释的属性以及那些名为`id`的属性被视为标识符属性。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下一步是声明一个使用域对象的存储库接口。 + +例 15。`Credentials`实体的基本存储库接口 + +``` +public interface CredentialsRepository extends CrudRepository { + +} +``` + +当我们的存储库扩展`CrudRepository`时,它提供了基本的增删改查和查询方法。保险库需要 Spring 个数据组件。确保在类路径中包含`spring-data-commons`和`spring-data-keyvalue`工件。 + +要实现这一点,最简单的方法是设置依赖管理,并将工件添加到`pom.xml`: + +然后将以下内容添加到`pom.xml`依赖关系部分。 + +例 16。使用 Spring 数据 BOM + +``` + + + + org.springframework.data + spring-data-bom + 2020.0.2 + import + pom + + + + + + + + + + org.springframework.vault + spring-vault-core + 2.3.1 + + + + org.springframework.data + spring-data-keyvalue + + + + +``` + +我们需要在两者之间将东西粘在一起的东西是根据 Spring 配置的。 + +例 17。JavaConfig 用于保险库存储库 + +``` +@Configuration +@EnableVaultRepositories +public class ApplicationConfig { + + @Bean + public VaultTemplate vaultTemplate() { + return new VaultTemplate(…); + } +} +``` + +鉴于上述设置,我们可以继续并注入`CredentialsRepository`到我们的组件。 + +例 18。访问个人实体 + +``` +@Autowired CredentialsRepository repo; + +public void basicCrudOperations() { + + Credentials creds = new Credentials("heisenberg", "327215", "AAA-GG-SSSS"); + rand.setAddress(new Address("308 Negra Arroyo Lane", "Albuquerque", "New Mexico", "87104")); + + repo.save(creds); (1) + + repo.findOne(creds.getId()); (2) + + repo.count(); (3) + + repo.delete(creds); (4) +} +``` + +|**1**|将`Credentials`的属性以键模式`keyspace/id`、
存储在 Vault Hash 中,在本例中,将`credentials/heisenberg`的属性存储在通用秘密后端中。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用提供的 ID 来检索存储在`keyspace/id`的对象。| +|**3**|计算在`Credentials`上由`@Secret`定义的键位*证书*内可用的实体总数。| +|**4**|从保险库中删除给定对象的密钥。| + +### [](#vault.repositories.mapping)13.2。对象到 Vault JSON 映射 + +Vault 存储库使用 JSON 作为交换格式在 Vault 中存储对象。JSON 和实体之间的对象映射是通过`VaultConverter`完成的。转换器读取和写入`SecretDocument`中包含来自`VaultResponse`的主体的`SecretDocument`。从 Vault 读取`VaultResponse`s,并通过 Jackson 将正文反序列化为`Map`的`String`和`Object`。默认的`VaultConverter`实现读取带有嵌套的值`Map`和`List`的`Map`对象,并将这些对象转换为实体,反之亦然。 + +给定前几节中的`Credentials`类型,默认映射如下: + +``` +{ + "_class": "org.example.Credentials", (1) + "password", "327215", (2) + "socialSecurityNumber": "AAA-GG-SSSS", + "address": { (3) + "street": "308 Negra Arroyo Lane", + "city": "Albuquerque", + "state": "New Mexico", + "zip":"87104" + } +} +``` + +|**1**|`_class`属性包含在根级别以及任何嵌套接口或抽象类型上。| +|-----|------------------------------------------------------------------------------------------------------| +|**2**|简单属性值由路径映射。| +|**3**|复杂类型的属性被映射为嵌套对象。| + +| |`@Id`属性必须映射到`String`。| +|---|----------------------------------------------| + +| Type |样本| Mapped Value | +|--------------------------------|--------------------------------------------------------|---------------------------------------------------| +| Simple Type
(eg. String) |string firstname=“walter”;| firstname = "Walter" | +|Complex Type
(eg. Address)|Address=New Address(“308Negra Arroyo Lane”);| address: { "street": "308 Negra Arroyo Lane" } | +| List
of Simple Type |list\nicknames=aslist(“Walt”,“Heisenberg”);| nicknames: ["walt", "heisenberg"] | +| Map
of Simple Type |map\atts=asmap(“年龄”,51)| atts : {"age" : 51} | +| List
of Complex Type |list\addresses=aslist(new address("308…|address: [{ "street": "308 Negra Arroyo Lane" }, …]| + +你可以通过在`VaultCustomConversions`中注册`Converter`来定制映射行为。这些转换器可以处理从/转换为诸如`LocalDate`和`SecretDocument`之类的类型,而第一个转换器适合于将简单的属性和最后一个复杂的类型转换为它们的 JSON 表示。第二个选项提供对结果`SecretDocument`的完全控制。将对象写入`Vault`将删除内容并重新创建整个条目,因此未映射的数据将丢失。 + +### [](#vault.repositories.queries)13.3。查询和查询方法 + +查询方法允许从方法名自动派生简单的查询。Vault 没有查询引擎,但需要直接访问 HTTP 上下文路径。Vault 查询方法将 Vault 的 API 可能性转换为查询。查询方法执行在上下文路径下列出子项,对 ID 应用筛选,可选地使用偏移量/限制限制限制 ID 流,并在获取结果后应用排序。 + +例 19。样本库查询方法 + +``` +public interface CredentialsRepository extends CrudRepository { + + List findByIdStartsWith(String prefix); +} +``` + +| |Vault 存储库的查询方法仅支持带有`@Id`属性上的谓词的查询。| +|---|------------------------------------------------------------------------------------------------| + +下面是 Vault 支持的关键字的概述。 + +| Keyword |样本| +|------------------------------------|-------------------------------------------------------------| +| `After`, `GreaterThan` |`findByIdGreaterThan(String id)`| +| `GreaterThanEqual` |`findByIdGreaterThanEqual(String id)`| +| `Before`, `LessThan` |`findByIdLessThan(String id)`| +| `LessThanEqual` |`findByIdLessThanEqual(String id)`| +| `Between` |`findByIdBetween(String from, String to)`| +| `In` |`findByIdIn(Collection ids)`| +| `NotIn` |`findByIdNotIn(Collection ids)`| +|`Like`, `StartingWith`, `EndingWith`|`findByIdLike(String id)`| +| `NotLike`, `IsNotLike` |`findByIdNotLike(String id)`| +| `Containing` |`findByFirstnameContaining(String id)`| +| `NotContaining` |`findByFirstnameNotContaining(String name)`| +| `Regex` |`findByIdRegex(String id)`| +| `(No keyword)` |`findById(String name)`| +| `Not` |`findByIdNot(String id)`| +| `And` |`findByLastnameAndFirstname`| +| `Or` |`findByLastnameOrFirstname`| +| `Is,Equals` |`findByFirstname`,`findByFirstnameIs`,`findByFirstnameEquals`| +| `Top,First` |`findFirst10ByFirstname`,`findTop5ByFirstname`| + +#### 13.3.1.分类和分页 + +查询方法通过在内存中选择从保险库上下文路径检索的子列表 ID 来支持排序和分页。与查询方法谓词不同,排序不限于特定字段。在进行 ID 过滤后,将应用未分页的排序,并从保险库中获取所有产生的秘密。通过这种方式,查询方法只获取作为结果的一部分返回的结果。 + +使用分页和排序需要在过滤 ID 之前进行秘密获取,这会影响性能。排序和分页保证返回相同的结果,即使 Vault 返回的 ID 的 Natural Order 发生了变化。因此,首先从 Vault 获取所有 ID,然后应用排序,然后进行过滤和偏移/限制。 + +例 20。分页和排序存储库 + +``` +public interface CredentialsRepository extends PagingAndSortingRepository { + + List findTop10ByIdStartsWithOrderBySocialSecurityNumberDesc(String prefix); + + List findByIdStarts(String prefix, Pageable pageRequest); +} +``` + +## [](#vault.core.client.support)14。客户支持 + +Spring Vault 支持各种 HTTP 客户端访问 Vault 的 HTTP API。 Spring Vault 使用[`RestTemplate`](https://DOCS. Spring.io/ Spring/DOCS/5.3.4/ Spring-framework-reference/integration.html#rest-resttemplate)作为访问 Vault 的主要接口。专用的客户机支持源自[定制的 SSL 配置](#vault.client-ssl),其作用域仅限于 Spring Vault 的客户机组件。 + +Spring Vault 支持以下 HTTP 命令式客户端: + +* Java 的内置`HttpURLConnection`(默认客户端) + +* Apache HTTP 组件 + +* Netty + +* OKHTTP3 + +Spring Vault 的反应性集成支持以下反应性 HTTP 客户端: + +* 反应堆网状结构 + +* Jetty + +使用特定的客户端需要在 Classpath 上可用的相应的依赖关系,因此 Spring Vault 可以使用可用的客户端与 Vault 进行通信。 + +### 14.1.Java 的内置`HttpURLConnection` + +Java 的内置`HttpURLConnection`是开箱即用的,不需要额外的配置。使用`HttpURLConnection`有一个关于 SSL 配置的限制。 Spring Vault 将不适用[定制的 SSL 配置](#vault.client-ssl),因为它将需要对 JVM 进行深度重新配置。这种配置将影响依赖默认 SSL 上下文的所有组件。使用`HttpURLConnection`配置 SSL 设置需要你将这些设置作为系统属性提供。有关更多详细信息,请参见[定制 JSSE](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization)。 + +### 14.2.外部客户 + +你可以使用外部客户机访问 Vault 的 API。只需向你的项目添加以下依赖项之一。如果使用[Spring Vault’s Dependency BOM](#dependencies),则可以省略版本号。 + +例 21。 Apache HTTP 组件依赖关系 + +``` + + org.apache.httpcomponents + httpclient + +``` + +| |Apache HttpClient 的[电汇测井](https://hc.apache.org/httpcomponents-client-4.5.x/logging.html)可以通过日志配置来启用。确保不会意外地启用有线日志,因为日志可能会以纯文本的形式暴露应用程序和保险库之间的流量(令牌和秘密)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例 22。内蒂依赖 + +``` + + io.netty + netty-all + +``` + +例 23。Square OkHTTP3 + +``` + + com.squareup.okhttp3 + okhttp + +``` + +例 24。反应堆网状结构 + +``` + + io.projectreactor.netty + reactor-netty + +``` + +例 25。 Jetty + +``` + + org.eclipse.jetty + jetty-reactive-httpclient + +``` + +### [](#vault.client-ssl)14.3。Vault 客户端 SSL 配置 + +通过设置各种属性,可以使用`SslConfiguration`配置 SSL。你可以设置`javax.net.ssl.trustStore`来配置 JVM 范围内的 SSL 设置,也可以设置`SslConfiguration`来仅为 Spring Vault 设置 SSL 设置。 + +``` +SslConfiguration sslConfiguration = SslConfiguration.create( (1) + new FileSystemResource("client-cert.jks"), "changeit".toCharArray(), + new FileSystemResource("truststore.jks"), "changeit".toCharArray()); + +SslConfiguration.forTrustStore(new FileSystemResource("keystore.jks"), (2) + "changeit".toCharArray()) + +SslConfiguration.forKeyStore(new FileSystemResource("keystore.jks"), (3) + "changeit".toCharArray()) + +SslConfiguration.forKeyStore(new FileSystemResource("keystore.jks"), (4) + "changeit".toCharArray(), + KeyConfiguration.of("key-password".toCharArray(), + "my-key-alias")) +``` + +|**1**|全配置。| +|-----|-----------------------------------------------------------------------| +|**2**|只配置信任存储区设置。| +|**3**|只配置密钥存储区设置。| +|**4**|只配置密钥存储区设置并提供密钥配置。| + +请注意,提供`SslConfiguration`仅在 Apache HTTP 组件或 OKHTTP 客户端位于你的类路径上时才能应用。 + +SSL 配置还支持 PEM 编码的证书,以替代 Java 密钥存储区。 + +``` +KeyStoreConfiguration keystore = KeyStoreConfiguration + .of(new ClassPathResource("ca.pem")).withStoreType("PEM"); +SslConfiguration configuration = SslConfiguration.forTrustStore(keystore); +``` + +PEM 文件可以包含一个或多个证书(块`-----BEGIN CERTIFICATE-----`和`-----END CERTIFICATE-----`)。添加到底层`KeyStore`的证书使用完整的主题名称作为别名。 + +## [](#vault.core.authentication)15。认证方法 + +不同的组织对安全性和身份验证有不同的要求。Vault 通过提供多种身份验证方法来反映这种需求。 Spring Vault 支持多种身份验证机制。 + +### 15.1.外部化登录凭据 + +获得对安全系统的首次访问称为安全引入。任何客户都需要短暂或永久的凭据才能访问 Vault。外部化凭据是保持代码可维护性高的一种很好的模式,但有可能增加披露的风险。 + +向任何一方披露登录凭据都允许登录到保险库并访问基础角色允许的秘密。选择适当的客户机身份验证并将凭据注入应用程序将受到风险评估的影响。 + +Spring 的[PropertySource 抽象](https://docs.spring.io/spring/docs/5.3.4/spring-framework-reference/core.html#beans-property-source-abstraction)是将配置保持在应用程序代码之外的一种自然适合。你可以使用系统属性、环境变量或属性文件来存储登录凭据。每种方法都有自己的特性。请记住,可以通过适当的 OS 访问级别来内省命令行和环境属性。 + +例 26。将`vault.token`外部化到属性文件 + +``` +@PropertySource("configuration.properties") +@Configuration +public class Config extends AbstractVaultConfiguration { + + @Override + public ClientAuthentication clientAuthentication() { + return new TokenAuthentication(getEnvironment().getProperty("vault.token")); + } +} +``` + +| |Spring 允许以多种方式获得`Environment`。当使用`VaultPropertySource`时,通过`@Autowired Environment environment`注入将不会提供`Environment`,因为环境 Bean 仍在构建中,并且自动布线在较晚的阶段到来。你的配置类应该实现`ApplicationContextAware`,并从`ApplicationContext`获得`Environment`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +参见[`SecurePropertyUsage.java`](https://github.com/ Spring-projects/ Spring-vault/blob/master/ Spring-vault-core/SRC/test/java/org/springframework/vault/demo/securepropertyusage.java),以获取在组件和其他属性源中引用属性的示例。 + +### [](#vault.authentication.token)15.2。令牌认证 + +令牌是在 Vault 中进行身份验证的核心方法。令牌身份验证需要提供一个静态令牌。 + +| |令牌身份验证是默认的身份验证方法。
如果令牌被公开为非预期的一方,则它获得对 Vault 的访问权限,并且
可以为预期的客户端访问秘密。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通常,令牌身份验证用于在外部创建和更新令牌的场景中(例如[HashiCorpVault Service Broker](https://github.com/hashicorp/vault-service-broker))。根据实际的设置,你可能希望也可能不希望令牌更新和撤销。有关 TTL 和令牌撤销的详细信息,请参见[`LifecycleAwareSessionManager`](#vault.authentication.会话)。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + return new TokenAuthentication("…"); + } + + // … +} +``` + +另见: + +* [保险库文档:令牌](https://www.vaultproject.io/docs/concepts/tokens.html) + +* [Vault 文档:使用令牌身份验证后端](https://www.vaultproject.io/docs/auth/token.html) + +### [](#vault.authentication.appid)15.3。APPID 身份验证 + +| |Appid 身份验证被 Vault 反对。用[Approle 身份验证](#vault.authentication.approle)代替。| +|---|-----------------------------------------------------------------------------------------------------------------| + +Vault 支持[AppId](https://www.vaultproject.io/docs/auth/app-id.html)身份验证,该验证由两个难以猜测的令牌组成。APPID 默认为静态配置的`spring.application.name`。第二个标记是 userid,它是由应用程序决定的一部分,通常与运行时环境相关。IP 地址、MAC 地址或 Docker 容器名称都是很好的例子。 Spring Vault 支持 IP 地址、MAC 地址和静态用户 ID(例如,通过系统属性提供)。IP 和 MAC 地址表示为十六进制编码的 SHA256 散列。 + +基于 IP 地址的用户 ID 使用本地主机的 IP 地址。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() + .appId("myapp") + .userIdMechanism(new IpAddressUserId()) + .build(); + + return new AppIdAuthentication(options, restOperations()); + } + + // … +} +``` + +从命令行生成 IP 地址 userid 的相应命令是: + +``` +$ echo -n 192.168.99.1 | sha256sum +``` + +| |包括`echo`的换行将导致不同的散列值
,因此请确保包括`-n`标志。| +|---|-------------------------------------------------------------------------------------------------------------| + +基于 MAC 地址的用户 ID 从本地主机绑定的设备获得他们的网络设备。该配置还允许指定`network-interface`提示来选择正确的设备。`network-interface`的值是可选的,可以是接口名称或接口索引(基于 0)。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() + .appId("myapp") + .userIdMechanism(new MacAddressUserId()) + .build(); + + return new AppIdAuthentication(options, restOperations()); + } + + // … +} +``` + +从命令行生成 MAC 地址 userid 的相应命令是: + +``` +$ echo -n 0AFEDE1234AC | sha256sum +``` + +| |MAC 地址是大写的,不带冒号。
包括`echo`的换行将导致不同的散列值
,因此请确保包含`-n`标志。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 15.3.1.自定义用户 ID + +更高级的方法允许你实现自己的`AppIdUserIdMechanism`。这个类必须位于你的 Classpath 上,并且必须实现`org.springframework.vault.authentication.AppIdUserIdMechanism`接口和`createUserId`方法。 Spring Vault 将在每次使用 APPID 进行身份验证以获得令牌时通过调用来获得用户 ID。 + +MyuseridMechanism.java + +``` +public class MyUserIdMechanism implements AppIdUserIdMechanism { + + @Override + public String createUserId() { + + String userId = … + return userId; + } +} +``` + +另见:[Vault 文档:使用应用程序 ID Auth 后台](https://www.vaultproject.io/docs/auth/app-id.html) + +### [](#vault.authentication.approle)15.4。Approle 身份验证 + +[AppRole](https://www.vaultproject.io/docs/auth/app-id.html)允许机器身份验证,就像不推荐的(自 Vault0.6.1)[APPID 身份验证](#vault.authentication.appid)一样。Approle 身份验证由两个难以猜测的(秘密)令牌组成:ROLEID 和 SECTROTID。 + +Spring Vault 通过仅提供 ROLEID 或与提供的 secretID 一起提供认证支持,并从 Vault 获取 ROLEID/secretID(具有响应展开的推拉模式)。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(RoleId.provided("…")) + .secretId(SecretId.wrapped(VaultToken.of("…"))) + .build(); + + return new AppRoleAuthentication(options, restOperations()); + } + + // … +} +``` + +Spring Vault 还支持全拉模式:如果没有提供 Roleid 和 Secretid, Spring Vault 将使用角色名和初始令牌来检索它们。初始令牌可以与 TTL 和使用限制相关联。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + VaultToken initialToken = VaultToken.of("…"); + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .appRole("…") + .roleId(RoleId.pull(initialToken)) + .secretId(SecretId.pull(initialToken)) + .build(); + + return new AppRoleAuthentication(options, restOperations()); + } + + // … +} +``` + +另见:[Vault 文档:使用 Approle Auth 后端](https://www.vaultproject.io/docs/auth/approle.html) + +### [](#vault.authentication.awsec2)15.5。AWS-EC2 身份验证 + +[aws-ec2](https://www.vaultproject.io/docs/auth/aws-ec2.html)Auth 后端为 AWS EC2 实例提供了一种安全的引入机制,允许自动检索保险库令牌。与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 AWS 视为受信任的第三方,并使用以密码签名的动态元数据信息来唯一地表示每个 EC2 实例。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + return new AwsEc2Authentication(restOperations()); + } + + // … +} +``` + +在默认情况下,AWS-EC2 身份验证使 Nonce 能够遵循信任第一次使用(Tofu)原则。任何意外获得 PKCS#7 身份元数据访问权限的一方都可以对 Vault 进行身份验证。 + +在第一次登录期间, Spring Vault 生成一个 Nonce,该 Nonce 存储在实例 ID 旁边的 auth 后端中。重新验证需要发送相同的 nonce。其他任何一方都没有 Nonce,可以在 Vault 中发出警报,以进行进一步的调查。 + +nonce 保存在内存中,并在应用程序重新启动时丢失。 + +AWS-EC2 身份验证角色是可选的,并且是 AMI 的默认值。可以通过在`AwsEc2AuthenticationOptions`中设置身份验证角色来配置身份验证角色。 + +另见:[Vault 文档:使用 AWS-EC2Auth 后端](https://www.vaultproject.io/docs/auth/aws-ec2.html) + +### [](#vault.authentication.awsiam)15.6。AWS-IAM 身份验证 + +[aws](https://www.vaultproject.io/docs/auth/aws.html)Auth 后台允许使用现有的 AWS IAM 凭据进行 Vault 登录。 + +AWS IAM 身份验证创建一个已签名的 HTTP 请求,该请求由 Vault 执行,以使用 AWS STS方法获得签名者的身份。AWSV4 签名需要 IAM 凭据。 + +IAM 凭据可以从运行时环境获得,也可以从外部提供。具有分配的 IAM 主体的 AWS-EC2、Lambda 和 ECS 等运行时环境不需要特定于客户机的凭据配置,但可以从其元数据源获得这些凭据。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + AwsIamAuthenticationOptions options = AwsIamAuthenticationOptions.builder() + .credentials(new BasicAWSCredentials(…)).build(); + + return new AwsIamAuthentication(options, restOperations()); + } + + // … +} +``` + +例 27。使用 AWS-EC2 实例配置文件作为凭证源 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + AwsIamAuthenticationOptions options = AwsIamAuthenticationOptions.builder() + .credentialsProvider(InstanceProfileCredentialsProvider.getInstance()).build(); + + return new AwsIamAuthentication(options, restOperations()); + } + + // … +} +``` + +`AwsIamAuthentication`需要 AWS Java SDK 依赖项(`com.amazonaws:aws-java-sdk-core`),因为身份验证实现使用 AWS SDK 类型作为凭据和请求签名。 + +你可以通过`AwsIamAuthenticationOptions`配置身份验证。 + +另见: + +* [Vault 文档:使用 AWS Auth 后端](https://www.vaultproject.io/docs/auth/aws.html) + +* [AWS 文档:STS GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) + +### [](#vault.authentication.azuremsi)15.7。Azure(MSI)认证 + +[azure](https://www.vaultproject.io/docs/auth/azure.html)Auth 后端为 Azure VM 实例提供了一种安全的引入机制,允许自动检索 Vault 令牌。与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 Azure 视为受信任的第三方,并使用可绑定到 VM 实例的托管服务标识和实例元数据信息。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder() + .role(…).build(); + + return new AzureMsiAuthentication(options, restOperations()); + } + + // … +} +``` + +Azure 身份验证需要有关 VM 环境的详细信息(订阅 ID、资源组名称、VM 名称)。这些细节可以通过`AzureMsiAuthenticationOptionsBuilder`进行配置。如果不进行配置,`AzureMsiAuthentication`将查询 Azure 的实例元数据服务,以获取这些详细信息。 + +另见: + +* [Vault 文档:使用 Azure Auth 后台](https://www.vaultproject.io/docs/auth/azure.html) + +* [Azure 文档:托管服务标识](https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview) + +### [](#vault.authentication.gcpgce)15.8。GCP-GCE 认证 + +[gcp](https://www.vaultproject.io/docs/auth/gcp.html)Auth 后端允许 Vault 通过使用现有的 GCP(Google Cloud Platform)IAM 和 GCE 凭据登录。 + +GCPGCE(Google 计算引擎)身份验证为服务帐户创建 JSON Web 令牌形式的签名。使用[实例标识](https://cloud.google.com/compute/docs/instances/verifying-instance-identity)从 GCE 元数据服务获得计算引擎实例的 JWT。该 API 创建了一个 JSON Web 令牌,该令牌可用于确认实例标识。 + +与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 GCP 视为受信任的第三方,并使用加密签名的动态元数据信息,该信息唯一地表示每个 GCP 服务帐户。 + +你可以通过`GcpComputeAuthenticationOptions`配置身份验证。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + GcpComputeAuthenticationOptions options = GcpComputeAuthenticationOptions.builder() + .role(…).build(); + + GcpComputeAuthentication authentication = new GcpComputeAuthentication(options, + restOperations()); + } + + // … +} +``` + +另见: + +* [Vault 文档:使用 GCPAuth 后端](https://www.vaultproject.io/docs/auth/gcp.html) + +* [GCP 文件:验证实例的身份](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) + +### [](#vault.authentication.gcpiam)15.9。GCP-IAM 认证 + +[gcp](https://www.vaultproject.io/docs/auth/gcp.html)Auth 后端允许 Vault 通过使用现有的 GCP(Google Cloud Platform)IAM 和 GCE 凭据登录。 + +GCP,IAM 身份验证以 JSON Web 令牌的形式为服务帐户创建签名。通过调用 GCPIAM 的[`projects.serviceAccounts.signJwt`](https://cloud.google.com/iam/reference/rest/v1/projects.serviceaccounts/signjwt)API,可以获得服务帐户的 JWT。调用者针对 GCPIAM 进行身份验证,并由此证明其身份。此保险库后端将 GCP 视为受信任的第三方。 + +IAM 凭据可以从运行时环境获得,也可以从外部提供,例如 JSON。JSON 是首选的表单,因为它带有调用`projects.serviceAccounts.signJwt`所需的项目 ID 和服务帐户标识符。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .role(…).credential(GoogleCredentials.getApplicationDefault()).build(); + + GcpIamAuthentication authentication = new GcpIamAuthentication(options, + restOperations()); + } + + // … +} +``` + +`GcpIamAuthenticationOptions`需要 Google Cloud Java SDK 依赖项(`com.google.apis:google-api-services-iam`和`com.google.auth:google-auth-library-oauth2-http`),因为身份验证实现使用 Google API 进行凭据和 JWT 签名。 + +你可以通过`GcpIamAuthenticationOptions`配置身份验证。 + +| |Google 凭据需要一个 OAuth2 令牌来维护令牌的生命周期。所有 API
都是同步的,因此,`GcpIamAuthentication`不支持`AuthenticationSteps`这是
需要的反应性使用。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另见: + +* [Vault 文档:使用 GCPAuth 后端](https://www.vaultproject.io/docs/auth/gcp.html) + +* [GCP 文档:projects.serviceaccounts.signjwt](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt)[]() + +### [](#vault.authentication.pcf)15.10。PCF 认证 + +[pcf](https://www.vaultproject.io/docs/auth/pcf.html)Auth 后端允许对 PCF 实例进行 Vault 登录。它利用[PCF 的应用程序和容器身份保证](https://content.pivotal.io/blog/new-in-pcf-2-1-app-container-identity-assurance-via-automatic-cert-rotation)。 + +PCF 身份验证使用实例密钥和证书来创建由 Vault 验证的签名。如果签名匹配,并且可能绑定的组织/空间/应用程序 ID 匹配,Vault 将发出一个范围适当的令牌。 + +实例凭据可从`CF_INSTANCE_CERT`和`CF_INSTANCE_KEY`变量的文件中获得。 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + PcfAuthenticationOptions options = PcfAuthenticationOptions.builder() + .role(…).build(); + + PcfAuthentication authentication = new PcfAuthentication(options, + restOperations()); + } + + // … +} +``` + +`PcfAuthenticationOptions`需要[Bouncycastle](https://www.bouncycastle.org/latest_releases.html)库来创建 RSA-PSS 签名。 + +你可以通过`PcfAuthenticationOptions`配置身份验证。 + +另见: + +* [Vault 文档:使用 PCF Auth 后端](https://www.vaultproject.io/docs/auth/pcf.html) + +### [](#vault.authentication.clientcert)15.11。TLS 证书认证 + +`cert`Auth 后端允许使用 SSL/TLS 客户机证书进行身份验证,这些证书由 CA 签名或自签名。 + +要启用`cert`身份验证,你需要: + +1. 使用 SSL,参见[Vault 客户端 SSL 配置](#vault.client-ssl) + +2. 配置包含客户端证书和私钥的 Java`Keystore` + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + ClientCertificateAuthenticationOptions options = ClientCertificateAuthenticationOptions.builder() + .path(…).build(); + + return new ClientCertificateAuthentication(options, restOperations()); + } + + // … +} +``` + +另见:[Vault 文档:使用 CERTAuth 后端](https://www.vaultproject.io/docs/auth/cert.html) + +### [](#vault.authentication.cubbyhole)15.12。空穴身份验证 + +Cubbyhole 身份验证使用 Vault 原语提供安全的身份验证工作流。Cubbyhole 身份验证使用令牌作为主要登录方法。一个短暂的令牌用于从 Vault 的 Cubbyhole 秘密后端获得第二个登录 VaultToken。登录令牌通常寿命更长,并用于与 Vault 交互。可以从包装的响应或`data`部分检索登录令牌。 + +**创建一个包装好的令牌** + +| |令牌创建的响应包装需要 Vault0.6.0 或更高版本。| +|---|--------------------------------------------------------------------| + +例 28。排版和存储令牌 + +``` +$ vault token-create -wrap-ttl="10m" +Key Value +--- ----- +wrapping_token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +wrapping_token_ttl: 0h10m0s +wrapping_token_creation_time: 2016-09-18 20:29:48.652957077 +0200 CEST +wrapped_accessor: 46b6aebb-187f-932a-26d7-4f3d86a68319 +``` + +例 29。包装的令牌响应用法 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions + .builder() + .initialToken(VaultToken.of("…")) + .wrapped() + .build(); + + return new CubbyholeAuthentication(options, restOperations()); + } + + // … +} +``` + +**使用存储令牌** + +例 30。排版和存储令牌 + +``` +$ vault token create +Key Value +--- ----- +token f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819 +token_accessor 4eee9bd9-81bb-06d6-af01-723c54a72148 +token_duration 0s +token_renewable false +token_policies [root] + +$ vault token create -use-limit=2 -orphan -no-default-policy -policy=none +Key Value +--- ----- +token 895cb88b-aef4-0e33-ba65-d50007290780 +token_accessor e84b661c-8aa8-2286-b788-f258f30c8325 +token_duration 0s +token_renewable false +token_policies [none] + +$ export VAULT_TOKEN=895cb88b-aef4-0e33-ba65-d50007290780 +$ vault write cubbyhole/token token=f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819 +``` + +例 31。存储令牌响应用法 + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions + .builder() + .initialToken(VaultToken.of("…")) + .path("cubbyhole/token") + .build(); + + return new CubbyholeAuthentication(options, restOperations()); + } + + // … +} +``` + +**剩余 TTL/可再生性** + +在创建令牌时,从与非零 TTL 相关联的 Cubbyhole 检索的令牌开始其 TTL。这个时间不一定与应用程序启动相同。为了补偿初始延迟,Cubbyhole 身份验证对与非零 TTL 相关的令牌执行自查找,以检索剩余的 TTL。在没有 TTL 的情况下,Cubbyhole 身份验证将不会自我查找包装的令牌,因为零 TTL 表示没有关联的 TTL。 + +非包装令牌不提供有关可更新性和 TTL 的详细信息,只检索令牌。自我查找将查找可再生性和剩余的 TTL。 + +另见: + +* [保险库文档:令牌](https://www.vaultproject.io/docs/concepts/tokens.html) + +* [Vault 文档:Cubbyhole 秘密后端](https://www.vaultproject.io/docs/secrets/cubbyhole/index.html) + +* [保险库文档:响应包装](https://www.vaultproject.io/docs/concepts/response-wrapping.html) + +### [](#vault.authentication.kubernetes)15.13。Kubernetes 认证 + +Vault 支持使用 Kubernetes 令牌的基于 0.8.3[kubernetes](https://www.vaultproject.io/docs/auth/kubernetes.html)的身份验证。 + +使用 Kubernetes 身份验证需要一个 Kubernetes 服务帐户令牌,通常挂载在`/var/run/secrets/kubernetes.io/serviceaccount/token`。该文件包含被读取并发送到 Vault 的令牌。Vault 在登录时使用 Kubernetes 的 API 验证其有效性。 + +配置 Kubernetes 身份验证至少需要提供角色名: + +``` +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder() + .role(…).jwtSupplier(…).build(); + + return new KubernetesAuthentication(options, restOperations()); + } + + // … +} +``` + +你可以通过`KubernetesAuthenticationOptions`配置身份验证。 + +另见: + +* [Vault 文档:使用 Kubernetes Auth 后台](https://www.vaultproject.io/docs/auth/kubernetes.html) + +* [Kubernetes 文档:为 PODS 配置服务帐户](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) + +### [](#vault.authentication.steps)15.14。认证步骤 + +`ClientAuthentication`对象描述身份验证流程并执行实际的身份验证步骤。预先组合的身份验证很容易使用,并通过与同步执行的紧密绑定进行配置。 + +身份验证方法的组合和重用常见步骤,例如将登录有效负载发布到 Vault 或从 HTTP 源检索身份验证输入,并不打算使用`ClientAuthentication`对象。 + +身份验证步骤提供了公共身份验证活动的可重用性。通过`AuthenticationSteps`创建的步骤以功能风格描述了一个身份验证流程,将实际的身份验证执行留给了特定的执行者。 + +例 32。存储令牌验证流. + +``` +AuthenticationSteps.just(VaultToken.of(…)); (1) +``` + +|**1**|仅从`VaultToken`创建`AuthenticationSteps`。| +|-----|-------------------------------------------------------| + +可以从单个输入创建单步身份验证流。声明多个身份验证步骤的流以`Supplier`或`HttpRequest`开始,这些流提供了一个身份验证状态对象,可用于将其映射或发布到 Vault 以进行登录。 + +例 33。Approle 认证流程 + +``` +AuthenticationSteps.fromSupplier( (1) + + () -> getAppRoleLogin(options.getRoleId(), options.getSecretId())) (2) + + .login("auth/{mount}/login", options.getPath()); (3) +``` + +|**1**|开始声明`AuthenticationSteps`接受`Supplier`。
状态对象类型取决于`Supplier`响应类型,该响应类型可以在以后的步骤中映射。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|实际的`Supplier`实现。
在这种情况下创建`Map`。| +|**3**|通过将状态对象(`Map`)发布到 Vault 端点来执行 Vault 登录,以创建 Vault 令牌。
注意,模板变量受 URL 转义的影响。| + +身份验证流需要一个执行器来执行实际的登录。我们为不同的执行模型提供了两个执行器: + +* `AuthenticationStepsExecutor`作为同步`ClientAuthentication`的插入替换。 + +* `AuthenticationStepsOperator`用于反应式执行。 + +许多`ClientAuthentication`都带有静态工厂方法,可以为它们的身份验证特定选项创建`AuthenticationSteps`: + +例 34。同步`AuthenticationSteps`执行 + +``` +CubbyholeAuthenticationOptions options = … +RestOperations restOperations = … + +AuthenticationSteps steps = CubbyholeAuthentication.createAuthenticationSteps(options); + +AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor(steps, restOperations); + +VaultToken token = executor.login(); +``` + +### [](#vault.authentication.session)15.15。令牌生命周期 + +Vault 的令牌可以与生存时间相关联。通过身份验证方法获得的令牌旨在在会话处于活动状态时使用,并且在应用程序处于活动状态时不应过期。 + +Spring Vault 提供了[`LifecycleAwareSessionManager`](https://DOCS. Spring.io/ Spring-vault/DOCS/2.3.1/api/org/springframework/vault/authentication/lifecycleawaresessionmanager.html)会话管理器,它可以更新令牌,直到它到达其终端 TTL,然后执行另一个登录,以获得与会话相关联的下一个令牌。 + +根据身份验证方法的不同,登录可以创建两种令牌: + +* [`VaultToken`](https://DOCS. Spring.io/ Spring-vault/DOCS/2.3.1/api/org/springframework/vault/support/vaulttoken.html):封装实际令牌的通用令牌。 + +* [`LoginToken`](https://DOCS. Spring.io/ Spring-vault/DOCS/2.3.1/api/org/springframework/vault/support/logintoken.html):与可再生性/ttl 相关联的令牌。 + +诸如[`TokenAuthentication`](https://DOCS. Spring.io/ Spring-vault/DOCS/2.3.1/api/org/springframework/vault/authentication/tokenauthentication.html)之类的认证方法只需创建一个`VaultToken`,其中不包含任何可再生性/TTL 详细信息。`LifecycleAwareSessionManager`将在令牌上运行自我查找,以从 Vault 检索可更新性和 TTL。如果启用了自我查找,`VaultToken`将定期更新。注意,`VaultToken`永远不会被撤销,只有`LoginToken`才会被撤销。 + +直接创建`LoginToken`的身份验证方法(所有基于登录的身份验证方法)已经为设置令牌更新提供了所有必要的详细信息。如果会话Manager 被关闭,则`LifecycleAwareSessionManager`将撤销从登录中获得的令牌。 + +## [](#vault.misc)16。杂项 + +在本章中学习一些值得一提的细节,比如 Spring 安全集成。 + +### [](#vault.misc.spring-security)16.1。 Spring 安全 + +Spring Vault 通过为[`BytesKeyGenerator`](https://DOCS. Spring.io/ Spring-security/site/DOCS/current/reference/htmlsingle/# Spring-security-crypto-keygenerators)和[<<`BytesEncryptor`](https://DOCS. Spring.io/ Spring-security/site/DOCS/current/reference/htmlsingle/# Spring-security-crypto-crypto-cryp 这两种实现都使用 Vault 的`transit`后端。 + +例 35。`VaultBytesKeyGenerator`示例 + +``` +VaultOperations operations = …; +VaultBytesKeyGenerator generator = new VaultBytesKeyGenerator(operations); + +byte[] key = generator.generateKey(); +``` + +例 36。`VaultBytesEncryptor`示例 + +``` +VaultTransitOperations transit = …; + +VaultBytesEncryptor encryptor = new VaultBytesEncryptor(transit, "my-key-name"); + +byte[] ciphertext = encryptor.encrypt(plaintext); + +byte[] result = encryptor.decrypt(ciphertext); +``` + +Vault 封装了一个熵源,该熵源与服务器端密钥管理一起与你的 JVM 分离。这减轻了应用程序开发人员进行适当加密/解密的负担,并将负担推给了 Vault 的运营商。Vault 的操作人员通常包括组织中的安全团队,这意味着他们可以确保数据被正确地加密/解密。此外,由于加密/解密操作必须进入审计日志,因此任何解密事件都会被记录。 + +后端还支持键旋转,这允许生成指定键的新版本。所有使用该密钥加密的数据都将使用该密钥的最新版本;以前加密的数据可以使用该密钥的旧版本进行解密。管理员可以控制可用于解密的密钥的先前版本,以防止攻击者获得旧的密文副本以成功解密该密钥。 + +毕竟,Vault 是一种网络服务,每一次操作都会有一个延迟。大量使用加密或随机字节生成的组件可能会在吞吐量和性能方面遇到差异。 \ No newline at end of file -- GitLab