diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 1ddc4275573d83b0d641507bf180a6cb8d3faec0..d5664cf219fec0a23fdf21f2ea86bf27a61c4296 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -532,6 +532,35 @@ module.exports = { initialOpenGroupIndex: 0 // 可选的, 默认值是 0 } ], + '/spring-batch/': [ + { + title: 'Spring Batch 文档', + sidebarDepth: 2, + collapsable: false, + children: [ + "/spring-batch/spring-batch-intro.md", + "/spring-batch/whatsnew.md", + "/spring-batch/domain.md", + "/spring-batch/job.md", + "/spring-batch/step.md", + "/spring-batch/readersAndWriters.md", + "/spring-batch/processor.md", + "/spring-batch/scalability.md", + "/spring-batch/repeat.md", + "/spring-batch/retry.md", + "/spring-batch/testing.md", + "/spring-batch/common-patterns.md", + "/spring-batch/jsr-352.md", + "/spring-batch/spring-batch-integration.md", + "/spring-batch/monitoring-and-metrics.md", + "/spring-batch/appendix.md", + "/spring-batch/schema-appendix.md", + "/spring-batch/transaction-appendix.md", + "/spring-batch/glossary.md" + ], + initialOpenGroupIndex: 0 // 可选的, 默认值是 0 + } + ], '/spring-amqp/': [ { title: 'Spring AMQP 文档', diff --git a/docs/spring-batch/README.md b/docs/spring-batch/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d402360958c1eadb963a8428206d2323bd40a78e --- /dev/null +++ b/docs/spring-batch/README.md @@ -0,0 +1 @@ +# Spring Batch \ 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 0000000000000000000000000000000000000000..c35288401ed3120d2cc8f6a1b9518ea11a089817 --- /dev/null +++ b/docs/spring-batch/appendix.md @@ -0,0 +1,48 @@ +## 附录 A:条目阅读器和条目编写器列表 + +### 条目阅读器 + +| 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`]。| + +### 条目编写者 + +| 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 0000000000000000000000000000000000000000..7180619dde860ba17f2a97498043e4b8f1ce08e2 --- /dev/null +++ b/docs/spring-batch/common-patterns.md @@ -0,0 +1,611 @@ +# 常见的批处理模式 + +## 常见的批处理模式 + +XMLJavaBoth + +一些批处理作业可以完全由 Spring 批处理中现成的组件组装而成。例如,`ItemReader`和`ItemWriter`实现可以被配置为覆盖广泛的场景。然而,在大多数情况下,必须编写自定义代码。应用程序开发人员的主要 API 入口点是`Tasklet`、`ItemReader`、`ItemWriter`和各种侦听器接口。大多数简单的批处理作业可以使用 Spring 批处理中的现成输入`ItemReader`,但是在处理和编写过程中通常存在需要开发人员实现`ItemWriter`或`ItemProcessor`的定制问题。 + +在这一章中,我们提供了几个自定义业务逻辑中常见模式的示例。这些示例主要以侦听器接口为特征。应该注意的是,如果合适的话,`ItemReader`或`ItemWriter`也可以实现侦听器接口。 + +### 记录项目处理和失败 + +一个常见的用例是需要在一个步骤中对错误进行特殊处理,逐项处理,可能是登录到一个特殊的通道,或者将一条记录插入到数据库中。面向块的`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`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 由于业务原因手动停止作业 + +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`来控制。然而,唯一的选择是抛出或不抛出异常,因此这始终是工作的异常结束。 + +### 添加页脚记录 + +通常,当写入平面文件时,在所有处理完成后,必须在文件的末尾附加一个“页脚”记录。这可以使用由 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; + +} +``` + +#### 编写摘要页脚 + +涉及页脚记录的一个常见要求是在输出过程中聚合信息,并将这些信息附加到文件的末尾。这个页脚通常用作文件的摘要或提供校验和。 + +例如,如果一个批处理作业正在将`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`时未启动的地方进行拾取。 + +### 基于项目阅读器的驾驶查询 + +在[关于读者和作家的章节](readersAndWriters.html)中,讨论了利用分页进行数据库输入的问题。许多数据库供应商(例如 DB2)都有非常悲观的锁定策略,如果正在读取的表也需要由在线应用程序的其他部分使用,这些策略可能会导致问题。此外,在非常大的数据集上打开游标可能会导致某些供应商的数据库出现问题。因此,许多项目更喜欢使用“驱动查询”方法来读取数据。这种方法的工作原理是对键进行迭代,而不是对需要返回的整个对象进行迭代,如下图所示: + +![驾驶查询工作](https://docs.spring.io/spring-batch/docs/current/reference/html/images/drivingQueryExample.png) + +图 1。驾驶查询工作 + +正如你所看到的,前面图片中显示的示例使用了与基于游标的示例中使用的相同的“foo”表。但是,在 SQL 语句中只选择了 ID,而不是选择整行。因此,不是从`read`返回`FOO`对象,而是返回`Integer`对象。然后可以使用这个数字来查询“details”,这是一个完整的`Foo`对象,如下图所示: + +![驱动查询示例](https://docs.spring.io/spring-batch/docs/current/reference/html/images/drivingQueryJob.png) + +图 2。驱动查询示例 + +应该使用`ItemProcessor`将从驱动查询中获得的键转换为完整的`Foo`对象。现有的 DAO 可以用于基于该键查询完整的对象。 + +### 多行记录 + +虽然平面文件的情况通常是,每个记录都被限制在单行中,但一个文件的记录可能跨越多行,并具有多种格式,这是很常见的。下面摘自一个文件,展示了这种安排的一个例子: + +``` +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; +} +``` + +### 执行系统命令 + +许多批处理作业要求从批处理作业中调用外部命令。这样的进程可以由调度器单独启动,但是有关运行的公共元数据的优势将会丧失。此外,一个多步骤的工作也需要被分解成多个工作。 + +因为这种需求是如此普遍, Spring Batch 提供了用于调用系统命令的`Tasklet`实现。 + +下面的示例展示了如何调用 XML 中的外部命令: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何在 Java 中调用外部命令: + +Java 配置 + +``` +@Bean +public SystemCommandTasklet tasklet() { + SystemCommandTasklet tasklet = new SystemCommandTasklet(); + + tasklet.setCommand("echo hello"); + tasklet.setTimeout(5000); + + return tasklet; +} +``` + +### 未找到输入时的处理步骤完成 + +在许多批处理场景中,在数据库或文件中找不到要处理的行并不是例外情况。将`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`的状态。 + +### 将数据传递给未来的步骤 + +将信息从一个步骤传递到另一个步骤通常是有用的。这可以通过`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 0000000000000000000000000000000000000000..b0d01eb3bf66aba0814d2bc5f95404d3efd79048 --- /dev/null +++ b/docs/spring-batch/domain.md @@ -0,0 +1,292 @@ +# 批处理的领域语言 + +## 批处理的域语言 + +XMLJavaBoth + +对于任何有经验的批处理架构师来说, Spring 批处理中使用的批处理的总体概念应该是熟悉和舒适的。有“作业”和“步骤”以及开发人员提供的处理单元,分别称为`ItemReader`和`ItemWriter`。然而,由于 Spring 模式、操作、模板、回调和习惯用法,有以下机会: + +* 在坚持明确区分关注事项方面有了显著改善。 + +* 清晰地描述了作为接口提供的体系结构层和服务。 + +* 简单和默认的实现,允许快速采用和易于使用的开箱即用。 + +* 显著增强了可扩展性。 + +下图是使用了几十年的批处理引用体系结构的简化版本。它提供了组成批处理领域语言的组件的概述。这个架构框架是一个蓝图,已经通过过去几代平台(COBOL/大型机、C/UNIX 和现在的 Java/Anywhere)上几十年的实现得到了证明。JCL 和 COBOL 开发人员可能与 C、C# 和 Java 开发人员一样熟悉这些概念。 Spring 批处理提供了通常在健壮的、可维护的系统中发现的层、组件和技术服务的物理实现,这些系统被用于解决创建简单到复杂的批处理应用程序,具有用于解决非常复杂的处理需求的基础设施和扩展。 + +![图 2.1:批处理原型](https://docs.spring.io/spring-batch/docs/current/reference/html/images/spring-batch-reference-model.png) + +图 1。批处理模式 + +前面的图表突出了构成 Spring 批处理的域语言的关键概念。一个作业有一个到多个步骤,每个步骤正好有一个`ItemReader`,一个`ItemProcessor`和一个`ItemWriter`。需要启动一个作业(使用`JobLauncher`),并且需要存储有关当前运行的进程的元数据(在`JobRepository`中)。 + +### 工作 + +这一部分描述了与批处理作业的概念有关的刻板印象。`Job`是封装整个批处理过程的实体。与其他 Spring 项目一样,`Job`与 XML 配置文件或基于 Java 的配置连接在一起。这种配置可以称为“作业配置”。然而,`Job`只是整个层次结构的顶部,如下图所示: + +![工作层次结构]https://docs.spring.io/spring-batch/docs/current/reference/html/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 + +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 + +在讨论了`JobInstance`以及它与约伯有何不同之后,我们自然要问的问题是:“一个`JobInstance`如何与另一个区分开来?”答案是:`JobParameters`。`JobParameters`对象持有一组用于启动批处理作业的参数。它们可以用于标识,甚至在运行过程中作为参考数据,如下图所示: + +![作业参数](https://docs.spring.io/spring-batch/docs/current/reference/html/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 + +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`是一个域对象,它封装了批处理作业的一个独立的、连续的阶段。因此,每一项工作都完全由一个或多个步骤组成。a`Step`包含定义和控制实际批处理所需的所有信息。这必然是一个模糊的描述,因为任何给定的`Step`的内容都是由编写`Job`的开发人员自行决定的。a`Step`可以是简单的,也可以是复杂的,正如开发人员所希望的那样。简单的`Step`可能会将文件中的数据加载到数据库中,只需要很少或不需要代码(取决于使用的实现)。更复杂的`Step`可能具有复杂的业务规则,这些规则作为处理的一部分被应用。与`Job`一样,`Step`具有与唯一的`StepExecution`相关的个体`StepExecution`,如下图所示: + +![图 2.1:带有步骤的工作层次结构](https://docs.spring.io/spring-batch/docs/current/reference/html/images/jobHeirarchyWithSteps.png) + +图 4。带有步骤的工作层次结构 + +#### 分步执行 + +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`表示一组键/值对的集合,这些键/值对由框架持久化并控制,以便允许开发人员有一个存储持久状态的位置,该状态的作用域为`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`是上述所有刻板印象的持久性机制。它为`JobLauncher`、`Job`和`Step`实现提供增删改查操作。当`Job`首次启动时,将从存储库获得`JobExecution`,并且在执行过程中,通过将`StepExecution`和`JobExecution`实现传递到存储库来持久化它们。 + +Spring 批处理 XML 命名空间提供了对配置带有``标记的`JobRepository`实例的支持,如以下示例所示: + +``` + +``` + +当使用 Java 配置时,`@EnableBatchProcessing`注释提供了`JobRepository`作为自动配置的组件之一。 + +### joblauncher + +`JobLauncher`表示用于启动`Job`具有给定的`JobParameters`集的`Job`的简单接口,如以下示例所示: + +``` +public interface JobLauncher { + +public JobExecution run(Job job, JobParameters jobParameters) + throws JobExecutionAlreadyRunningException, JobRestartException, + JobInstanceAlreadyCompleteException, JobParametersInvalidException; +} +``` + +期望实现从`JobRepository`获得有效的`JobExecution`并执行`Job`。 + +### 条目阅读器 + +`ItemReader`是一种抽象,表示对`Step`输入的检索,每次检索一项。当`ItemReader`已经耗尽了它可以提供的项时,它通过返回`null`来表示这一点。有关`ItemReader`接口及其各种实现方式的更多详细信息,请参见[读者和作家](readersAndWriters.html#readersAndWriters)。 + +### item writer + +`ItemWriter`是一种抽象,它表示`Step`的输出,一次输出一个批处理或一大块项目。通常,`ItemWriter`不知道下一步应该接收的输入,只知道当前调用中传递的项。有关`ItemWriter`接口及其各种实现方式的更多详细信息,请参见[读者和作家](readersAndWriters.html#readersAndWriters)。 + +### 项处理器 + +`ItemProcessor`是表示项目的业务处理的抽象。当`ItemReader`读取一个项,而`ItemWriter`写入它们时,`ItemProcessor`提供了一个接入点来转换或应用其他业务处理。如果在处理该项时确定该项无效,则返回`null`表示不应写出该项。有关`ItemProcessor`接口的更多详细信息,请参见[读者和作家](readersAndWriters.html#readersAndWriters)。 + +### 批处理名称空间 + +前面列出的许多域概念需要在 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 0000000000000000000000000000000000000000..1fa34f7f71e9ca2632a16b6bd485f1e11bfcbfb8 --- /dev/null +++ b/docs/spring-batch/glossary.md @@ -0,0 +1,81 @@ +# 词汇表 + +## 附录 A:术语表 + +### 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 0000000000000000000000000000000000000000..efa21cdd06f5cc3ca7e316e47b4ed898ba61af24 --- /dev/null +++ b/docs/spring-batch/job.md @@ -0,0 +1,1064 @@ +# 配置和运行作业 + +## 配置和运行作业 + +XMLJavaBoth + +在[领域部分](domain.html#domainLanguageOfBatch)中,使用以下图表作为指导,讨论了总体架构设计: + +![图 2.1:批处理原型](https://docs.spring.io/spring-batch/docs/current/reference/html/images/spring-batch-reference-model.png) + +图 1。批处理模式 + +虽然`Job`对象看起来像是一个用于步骤的简单容器,但开发人员必须了解许多配置选项。此外,对于如何运行`Job`以及在运行期间如何存储其元数据,有许多考虑因素。本章将解释`Job`的各种配置选项和运行时关注点。 + +### 配置作业 + +[`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”的存储库,这是一个合理的默认值。但是,这可以显式地重写: + +``` + + + + + +``` + +除了步骤之外,作业配置还可以包含有助于并行(``)、声明性流控制(``)和流定义外部化(``)的其他元素。 + +#### 可重启性 + +执行批处理作业时的一个关键问题与`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`。 + +#### 拦截作业执行 + +在作业的执行过程中,通知其生命周期中的各种事件可能是有用的,以便可以执行自定义代码。通过在适当的时间调用`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` + +#### 继承父作业 + +如果一组作业共享相似但不相同的配置,那么定义一个“父”`Job`可能会有所帮助,具体的作业可以从该“父”中继承属性。与 Java 中的类继承类似,“child”`Job`将把它的元素和属性与父元素和属性结合在一起。 + +在下面的示例中,“basejob”是一个抽象的`Job`定义,它只定义了一个侦听器列表。`Job`“job1”是一个具体的定义,它继承了“Basejob”的侦听器列表,并将其与自己的侦听器列表合并,以生成一个`Job`,其中包含两个侦听器和一个`Step`,即“步骤 1”。 + +``` + + + + + + + + + + + + + +``` + +有关更多详细信息,请参见[从父步骤继承](step.html#inheritingFromParentStep)一节。 + +#### JobParametersValidator + +在 XML 命名空间中声明的作业或使用`AbstractJob`的任意子类可以在运行时为作业参数声明验证器。例如,当你需要断言一个作业是以其所有的强制参数启动时,这是有用的。有一个`DefaultJobParametersValidator`可以用来约束简单的强制参数和可选参数的组合,对于更复杂的约束,你可以自己实现接口。 + +验证程序的配置通过 XML 命名空间通过作业的一个子元素得到支持,如下面的示例所示: + +``` + + + + +``` + +验证器可以指定为引用(如前面所示),也可以指定为 bean 名称空间中的嵌套 Bean 定义。 + +通过 Java Builders 支持验证器的配置,如以下示例所示: + +``` +@Bean +public Job job1() { + return this.jobBuilderFactory.get("job1") + .validator(parametersValidator()) + ... + .build(); +} +``` + +### 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(); + } +} +``` + +### 配置 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)列的长度 + +#### 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; +} +``` + +#### 更改表格前缀 + +`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`。 + +| |只有表前缀是可配置的。表和列名不是。| +|---|--------------------------------------------------------------------------| + +#### 内存存储库 + +在某些情况下,你可能不希望将域对象持久化到数据库。原因之一可能是速度;在每个提交点存储域对象需要额外的时间。另一个原因可能是,你不需要为一份特定的工作坚持现状。出于这个原因, 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)所示。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 存储库中的非标准数据库类型 + +如果你使用的数据库平台不在受支持的平台列表中,那么如果 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`接口,并以正常的方式手动连接。 + +### 配置一个 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`返回给调用者,如下图所示: + +![作业启动器序列](https://docs.spring.io/spring-batch/docs/current/reference/html/images/job-launcher-sequence-sync.png) + +图 2。作业启动器序列 + +这个序列很简单,从调度程序启动时效果很好。然而,在尝试从 HTTP 请求启动时会出现问题。在这种情况下,启动需要异步完成,以便`SimpleJobLauncher`立即返回其调用方。这是因为,在长时间运行的进程(如批处理)所需的时间内保持 HTTP 请求的开放状态是不好的做法。下图显示了一个示例序列: + +![异步作业启动器序列](https://docs.spring.io/spring-batch/docs/current/reference/html/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`接口的任何实现都可以用来控制如何异步执行作业。 + +### 运行作业 + +至少,启动批处理作业需要两个条件:启动`Job`和`JobLauncher`。两者都可以包含在相同的上下文中,也可以包含在不同的上下文中。例如,如果从命令行启动一个作业,将为每个作业实例化一个新的 JVM,因此每个作业都有自己的`JobLauncher`。但是,如果在`HttpRequest`范围内的 Web 容器中运行,通常会有一个`JobLauncher`,该配置用于异步作业启动,多个请求将调用以启动其作业。 + +#### 从命令行运行作业 + +对于希望从 Enterprise 调度器运行作业的用户,命令行是主要的接口。这是因为大多数调度程序(Quartz 除外,除非使用 nativeJob)直接与操作系统进程一起工作,主要是通过 shell 脚本开始的。除了 shell 脚本之外,还有许多启动 Java 进程的方法,例如 Perl、Ruby,甚至是 Ant 或 Maven 之类的“构建工具”。但是,由于大多数人都熟悉 shell 脚本,因此本例将重点讨论它们。 + +##### 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 + +当从命令行启动批处理作业时,通常使用 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`的一部分。 + +#### 在 Web 容器中运行作业 + +从历史上看,离线处理(如批处理作业)是从命令行启动的,如上文所述。然而,在许多情况下,从`HttpRequest`发射是更好的选择。许多这样的用例包括报告、临时作业运行和 Web 应用程序支持。因为按定义,批处理作业是长时间运行的,所以最重要的问题是确保异步启动该作业: + +![基于 Web 容器的异步作业启动器序列](https://docs.spring.io/spring-batch/docs/current/reference/html/images/launch-from-request.png) + +图 4。来自 Web 容器的异步作业启动器序列 + +在这种情况下,控制器是 Spring MVC 控制器。关于 Spring MVC 的更多信息可以在这里找到:的`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()); + } +} +``` + +### 高级元数据使用 + +到目前为止,`JobLauncher`和`JobRepository`接口都已经讨论过了。它们一起表示作业的简单启动,以及批处理域对象的基本操作: + +![作业存储库](https://docs.spring.io/spring-batch/docs/current/reference/html/images/job-repository.png) + +图 5。作业存储库 + +a`JobLauncher`使用`JobRepository`来创建新的`JobExecution`对象并运行它们。`Job`和`Step`实现稍后将使用相同的`JobRepository`用于运行作业期间相同执行的基本更新。对于简单的场景,基本的操作就足够了,但是在具有数百个批处理任务和复杂的调度需求的大批处理环境中,需要对元数据进行更高级的访问: + +![作业存储库高级版](https://docs.spring.io/spring-batch/docs/current/reference/html/images/job-repository-advanced.png) + +图 6。高级作业存储库访问 + +下面将讨论`JobExplorer`和`JobOperator`接口,它们添加了用于查询和控制元数据的附加功能。 + +#### 查询存储库 + +在任何高级特性之前,最基本的需求是查询存储库中现有执行的能力。此功能由`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 + +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 + +这是一个 Bean 后处理器,它可以在创建所有作业时注册它们。 + +下面的示例展示了如何为 XML 中定义的作业包括`JobRegistryBeanPostProcessor`: + +XML 配置 + +``` + + + +``` + +下面的示例展示了如何为在 Java 中定义的作业包括`JobRegistryBeanPostProcessor`: + +Java 配置 + +``` +@Bean +public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() { + JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor(); + postProcessor.setJobRegistry(jobRegistry()); + return postProcessor; +} +``` + +虽然这不是严格必要的,但是在示例中的后处理器已经被赋予了一个 ID,以便它可以被包括在子上下文中(例如作为父 Bean 定义),并导致在那里创建的所有作业也被自动注册。 + +##### `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 + +如前所述,`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 + +关于`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(); +} +``` + +#### 停止工作 + +`JobOperator`最常见的用例之一是优雅地停止一项工作: + +``` +Set executions = jobOperator.getRunningExecutions("sampleJob"); +jobOperator.stop(executions.iterator().next()); +``` + +关闭不是立即的,因为无法强制立即关闭,特别是如果当前执行的是框架无法控制的开发人员代码,例如业务服务。但是,一旦将控件返回到框架中,就会将当前`StepExecution`的状态设置为`BatchStatus.STOPPED`,保存它,然后在完成之前对`JobExecution`执行相同的操作。 + +#### 终止作业 + +可以重新启动`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 0000000000000000000000000000000000000000..7c1c6a0c55a5d32963bb20cd4c58f9302160a38c --- /dev/null +++ b/docs/spring-batch/jsr-352.md @@ -0,0 +1,312 @@ +# JSR-352 支援 + +## JSR-352 支持 + +XMLJavaBoth + +截至 Spring,对 JSR-352 的批处理 3.0 支持已经完全实现。本节不是规范本身的替代,而是打算解释 JSR-352 特定概念如何应用于 Spring 批处理。有关 JSR-352 的其他信息可以通过 JCP 在这里找到: + +### 关于 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 接口开发的批处理工件将不能在传统的批处理作业中工作。 + +### 设置 + +#### 应用程序上下文 + +Spring 批处理中的所有基于 JSR-352 的作业都由两个应用程序上下文组成。父上下文,它包含与 Spring 批处理的基础结构相关的 bean,例如`JobRepository`、`PlatformTransactionManager`等,以及包含要运行的作业的配置的子上下文。父上下文是通过框架提供的`jsrBaseContext.xml`定义的。可以通过设置`JSR-352-BASE-CONTEXT`系统属性来重写此上下文。 + +| |对于属性注入之类的事情,JSR-352 处理器不会处理基本上下文,因此
不需要在此配置额外处理的组件。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 启动基于 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 都不是可选的。所有这些都可以被重写到
,根据需要提供定制的功能。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### 依赖注入 + +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。 + +``` + + + + + + +``` + +### 批处理属性 + +#### 属性支持 + +JSR-352 允许通过在 JSL 中的配置在作业、步骤和批处理工件级别定义属性。在每个级别上,按以下方式配置批处理属性: + +``` + + + + +``` + +`Properties`可以在任何批处理工件上进行配置。 + +#### @batchproperty 注释 + +`Properties`在批处理工件中通过使用`@BatchProperty`和`@Inject`注释(这两个注释都是规范所要求的)注释类字段来引用。根据 JSR-352 的定义,属性的字段必须是字符串类型的。任何类型转换都要由实现开发人员来执行。 + +可以将`javax.batch.api.chunk.ItemReader`工件配置为具有上述属性块的属性块,并以这样的方式进行访问: + +``` +public class MyItemReader extends AbstractItemReader { + @Inject + @BatchProperty + private String propertyName1; + + ... +} +``` + +字段“PropertyName1”的值将是“PropertyValue1” + +#### 属性替换 + +属性替换是通过运算符和简单条件表达式来提供的。一般用法是`#{operator['key']}`。 + +支持的操作符: + +* `jobParameters`:访问启动/重新启动作业的作业参数值。 + +* `jobProperties`:在 JSL 的作业级别上配置的访问属性。 + +* `systemProperties`:访问命名的系统属性。 + +* `partitionPlan`:从一个分区步骤的分区计划中访问命名属性。 + +``` +#{jobParameters['unresolving.prop']}?:#{systemProperties['file.separator']} +``` + +赋值的左边是期望值,右边是默认值。在前面的示例中,结果将解析为系统属性文件的值。分隔符 #{jobparamets[’unsolving.prop’]}被假定为不可解析。如果两个表达式都不能解析,将返回一个空字符串。可以使用多个条件,这些条件由“;”分隔。 + +### 处理模型 + +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`的处理相同。 + +#### 基于项目的处理 + +在此上下文中,基于项的处理是由`ItemReader`读取的项数设置的块大小。要以这种方式配置步骤,请指定`item-count`(默认值为 10),并可选择将`checkpoint-policy`配置为项(这是默认值)。 + +``` +... + + + + + + + +... +``` + +如果选择了基于项的检查点,则支持一个附加属性`time-limit`。这为必须处理指定的项数设置了一个时间限制。如果达到了超时,那么不管`item-count`配置为什么,该块都将完成,到那时已经读取了多少项。 + +#### 自定义检查点 + +JSR-352 在步骤“检查点”中调用围绕提交间隔的进程。基于项目的检查点是上面提到的一种方法。然而,在许多情况下,这还不够强大。因此,规范允许通过实现`javax.batch.api.chunk.CheckpointAlgorithm`接口来实现自定义检查点算法。该功能在功能上与 Spring Batch 的自定义完成策略相同。要使用`CheckpointAlgorithm`的实现,请使用自定义`checkpoint-policy`配置你的步骤,如下所示,其中`fooCheckpointer`是指`CheckpointAlgorithm`的实现。 + +``` +... + + + + + + + + +... +``` + +### 运行作业 + +执行基于 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)`。 + +### 上下文 + +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`。 + +### 阶跃流 + +在基于 JSR-352 的作业中,步骤流程的工作方式与 Spring 批处理中的工作方式类似。然而,这里有几个细微的区别: + +* 决策是步骤——在常规的 Spring 批作业中,决策是一种状态,它不具有独立的`StepExecution`,也不具有伴随整个步骤而来的任何权利和责任。然而,在 JSR-352 中,一个决策就像其他任何步骤一样是一个步骤,并且将表现为任何其他步骤(事务性,它得到`StepExecution`等)。这意味着,在重启过程中,它们与其他任何步骤一样受到同等对待。 + +* `next`属性和步骤转换-在常规作业中,允许在相同的步骤中同时出现这些转换。JSR-352 允许在相同的步骤中使用它们,并在计算中优先使用 Next 属性。 + +* 转换元素排序--在标准 Spring 批处理作业中,转换元素从最特定的到最不特定的进行排序,并按照该顺序进行评估。JSR-352 作业按照转换元素在 XML 中指定的顺序对其进行评估。 + +### 缩放 JSR-352 批处理作业 + +Spring 传统的批处理作业有四种缩放方式(最后两种能够跨多个 JVM 执行): + +* 拆分-并行运行多个步骤。 + +* 多个线程-通过多个线程执行一个步骤。 + +* 分区-将数据划分为并行处理(Manager/Worker)。 + +* 远程分块-远程执行处理器逻辑块. + +JSR-352 提供了两种缩放批处理作业的选项。这两个选项都只支持一个 JVM: + +* 拆分-与 Spring 批相同 + +* 分区-概念上与 Spring 批处理相同,但实现方式略有不同。 + +#### 分区 + +从概念上讲,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` |提供为分区
步骤提供补偿逻辑的能力。| + +### 测试 + +由于所有基于 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 0000000000000000000000000000000000000000..f05a5ec409e7608bc7596e707d5f9aa385417190 --- /dev/null +++ b/docs/spring-batch/monitoring-and-metrics.md @@ -0,0 +1,66 @@ +# 监测和量度 + +## 监控和度量 + +自版本 4.2 以来, Spring Batch 提供了对基于[Micrometer](https://micrometer.io/)的批监视和度量的支持。本节描述了哪些度量是开箱即用的,以及如何贡献自定义度量。 + +### 内置度量 + +度量集合不需要任何特定的配置。框架提供的所有指标都注册在[千分尺的全球注册中心](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`。| +|---|------------------------------------------------------| + +### 自定义度量 + +如果你想在自定义组件中使用自己的度量,我们建议直接使用 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; + } +} +``` + +### 禁用度量 + +度量收集是一个类似于日志记录的问题。禁用日志通常是通过配置日志记录库来完成的,对于度量标准来说也是如此。在 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 0000000000000000000000000000000000000000..0f38737074075b3e5f60026ede5a3e2ad8c3422a --- /dev/null +++ b/docs/spring-batch/processor.md @@ -0,0 +1,299 @@ +# 项目处理 + +## 项处理 + +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`是可选的。 + +### 链接项目处理器 + +在许多场景中,执行单个转换是有用的,但是如果你想将多个`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; +} +``` + +### 过滤记录 + +项目处理器的一个典型用途是在将记录传递给`ItemWriter`之前过滤掉它们。过滤是一种不同于跳过的动作。跳过表示记录无效,而筛选只表示不应写入记录。 + +例如,考虑一个批处理作业,它读取包含三种不同类型记录的文件:要插入的记录、要更新的记录和要删除的记录。如果系统不支持记录删除,那么我们将不希望将任何“delete”记录发送到`ItemWriter`。但是,由于这些记录实际上并不是不良记录,我们希望过滤掉它们,而不是跳过它们。因此,`ItemWriter`将只接收“插入”和“更新”记录。 + +要过滤记录,可以从`ItemProcessor`返回`null`。该框架检测到结果是`null`,并避免将该项添加到交付给`ItemWriter`的记录列表中。像往常一样,从`ItemProcessor`抛出的异常会导致跳过。 + +### 验证输入 + +在[项目阅读器和项目编写器](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; +} +``` + +### 容错 + +当块被回滚时,在读取过程中缓存的项可能会被重新处理。如果一个步骤被配置为容错(通常通过使用跳过或重试处理),则所使用的任何`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 0000000000000000000000000000000000000000..39f2bd1a968aa0701db57f89da6cb5336750d3de --- /dev/null +++ b/docs/spring-batch/readersAndWriters.md @@ -0,0 +1,2264 @@ +# 项目阅读器和项目编写器 + +## 条目阅读器和条目编写器 + +XMLJavaBoth + +所有批处理都可以用最简单的形式描述为读取大量数据,执行某种类型的计算或转换,并将结果写出来。 Spring Batch 提供了三个关键接口来帮助执行大容量读写:`ItemReader`、`ItemProcessor`和`ItemWriter`。 + +### `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`在功能上类似于`ItemReader`,但具有反向操作。资源仍然需要定位、打开和关闭,但它们的不同之处在于`ItemWriter`写出,而不是读入。在数据库或队列的情况下,这些操作可以是插入、更新或发送。输出的序列化的格式是特定于每个批处理作业的。 + +与`ItemReader`一样,`ItemWriter`是一个相当通用的接口,如下面的接口定义所示: + +``` +public interface ItemWriter { + + void write(List items) throws Exception; + +} +``` + +与`read`上的`ItemReader`一样,`write`提供了`ItemWriter`的基本契约。它尝试写出传入的项目列表,只要它是打开的。由于通常期望将项目“批处理”到一个块中,然后输出,因此接口接受一个项目列表,而不是一个项目本身。在写出列表之后,可以在从写方法返回之前执行任何必要的刷新。例如,如果对 Hibernate DAO 进行写操作,则可以对每个项进行多个 write 调用。然后,写入器可以在返回之前调用 Hibernate 会话上的`flush`。 + +### `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`。 + +### 委托模式并与步骤一起注册 + +请注意,`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(); +} +``` + +### 平面文件 + +交换大容量数据的最常见机制之一一直是平面文件。与 XML 不同的是,XML 有一个一致的标准来定义它是如何结构化的(XSD),任何读取平面文件的人都必须提前确切地了解文件是如何结构化的。一般来说,所有的平面文件都分为两种类型:定长和定长。分隔符文件是那些字段被分隔符(如逗号)分隔的文件。固定长度文件的字段是固定长度的。 + +#### 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` + +平面文件是最多包含二维(表格)数据的任何类型的文件。 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` + +与`RowMapper`一样,它接受一个低层次的构造,例如`ResultSet`并返回一个`Object`,平面文件处理需要相同的构造来将`String`行转换为`Object`,如以下接口定义所示: + +``` +public interface LineMapper { + + T mapLine(String line, int lineNumber) throws Exception; + +} +``` + +基本的约定是,给定当前行和与其相关联的行号,映射器应该返回一个结果域对象。这类似于`RowMapper`,因为每一行都与其行号关联,就像`ResultSet`中的每一行都与其行号关联一样。这允许将行号绑定到结果域对象,以进行身份比较或进行更有信息量的日志记录。然而,与`RowMapper`不同的是,`LineMapper`给出的是一条未加工的线,正如上面讨论的那样,这条线只能让你达到一半。该行必须标记为`FieldSet`,然后可以映射到对象,如本文档后面所述。 + +##### `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`接口定义了一个方法`mapFieldSet`,它接受一个`FieldSet`对象并将其内容映射到一个对象。该对象可以是自定义 DTO、域对象或数组,具体取决于作业的需要。`FieldSetMapper`与`LineTokenizer`结合使用,以将资源中的一行数据转换为所需类型的对象,如以下接口定义所示: + +``` +public interface FieldSetMapper { + + T mapFieldSet(FieldSet fieldSet) throws BindException; + +} +``` + +使用的模式与`JdbcTemplate`使用的`RowMapper`相同。 + +##### `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; + } +} +``` + +上述功能是在默认实现中提供的,而不是内置在阅读器本身中(就像框架的以前版本中所做的那样),以允许用户在控制解析过程中具有更大的灵活性,尤其是在需要访问原始行的情况下。 + +##### 简单分隔的文件读取示例 + +下面的示例演示了如何在实际的域场景中读取平面文件。这个特定的批处理作业从以下文件中读取足球运动员: + +``` +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`。 + +##### 按名称映射字段 + +还有一个额外的功能块是`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; + } +} +``` + +##### 向域对象自动设置字段集 + +对于许多人来说,必须为`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`对象,不需要任何代码。 + +##### 固定长度文件格式 + +到目前为止,只对分隔的文件进行了详细的讨论。然而,它们只代表了文件阅读图片的一半。许多使用平面文件的组织使用固定长度格式。下面是固定长度文件的示例: + +``` +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`。 + +##### 单个文件中的多个记录类型 + +到目前为止,所有的文件读取示例都为了简单起见做出了一个关键假设:文件中的所有记录都具有相同的格式。然而,情况可能并不总是如此。很常见的一种情况是,一个文件可能具有不同格式的记录,这些记录需要以不同的方式进行标记并映射到不同的对象。下面的文件摘录说明了这一点: + +``` +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`示例中可以找到这种常见模式的演示。 + +##### 平面文件中的异常处理 + +在许多情况下,对一行进行标记化可能会导致抛出异常。许多平面文件是不完美的,包含格式不正确的记录。许多用户在记录问题、原始行号和行号时选择跳过这些错误行。这些日志稍后可以手动检查,也可以通过另一个批处理作业进行检查。出于这个原因, Spring Batch 为处理解析异常提供了一个异常层次结构:`FlatFileParseException`和`FlatFileFormatException`。当试图读取文件时遇到任何错误时,`FlatFileParseException`将抛出`FlatFileItemReader`。`FlatFileFormatException`由`LineTokenizer`接口的实现抛出,并指示在标记时遇到的更具体的错误。 + +###### `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` + +以固定长度格式格式化的文件在解析时有额外的要求,因为与分隔格式不同,每个列必须严格遵守其预定义的宽度。如果行的总长度不等于此列的最大值,则抛出一个异常,如以下示例所示: + +``` +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-1 5.因此,这条线的总长度是 1 5.但是,在前面的示例中,传入了长度为 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` + +写入平面文件也存在从文件读入时必须克服的问题。一个步骤必须能够以事务性的方式编写分隔格式或固定长度格式。 + +##### `LineAggregator` + +正如`LineTokenizer`接口是获取一个项并将其转换为`String`所必需的一样,文件写入必须有一种方法,可以将多个字段聚合到一个字符串中,以便将其写入文件。在 Spring 批处理中,这是`LineAggregator`,如下面的接口定义所示: + +``` +public interface LineAggregator { + + public String aggregate(T item); + +} +``` + +`LineAggregator`是`LineTokenizer`的逻辑对立面。`LineTokenizer`接受一个`String`并返回一个`FieldSet`,而`LineAggregator`接受一个`item`并返回一个`String`。 + +###### `PassThroughLineAggregator` + +`LineAggregator`接口的最基本的实现是`PassThroughLineAggregator`,它假定对象已经是一个字符串,或者它的字符串表示可以用于编写,如下面的代码所示: + +``` +public class PassThroughLineAggregator implements LineAggregator { + + public String aggregate(T item) { + return item.toString(); + } +} +``` + +如果需要直接控制创建字符串,那么前面的实现是有用的,但是`FlatFileItemWriter`的优点,例如事务和重新启动支持,是必要的。 + +##### 简化文件编写示例 + +既然`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` + +前面的示例对于对文件的写入的最基本使用可能是有用的。然而,`FlatFileItemWriter`的大多数用户都有一个需要写出的域对象,因此必须将其转换为一行。在文件阅读中,需要进行以下操作: + +1. 从文件中读出一行。 + +2. 将该行传递到`LineTokenizer#tokenize()`方法中,以便检索`FieldSet`。 + +3. 将从标记化返回的`FieldSet`传递到`FieldSetMapper`,从`ItemReader#read()`方法返回结果。 + +编写文件也有类似但相反的步骤: + +1. 把要写的东西交给作者。 + +2. 将项目上的字段转换为数组。 + +3. 将生成的数组聚合为一条线。 + +因为框架无法知道需要从对象中写出哪些字段,所以必须编写`FieldExtractor`才能完成将项转换为数组的任务,如下面的接口定义所示: + +``` +public interface FieldExtractor { + + Object[] extract(T item); + +} +``` + +`FieldExtractor`接口的实现应该从提供的对象的字段创建一个数组,然后可以在元素之间使用分隔符写出该数组,或者作为固定宽度线的一部分。 + +###### `PassThroughFieldExtractor` + +在许多情况下,需要写出集合,例如一个数组,`Collection`或`FieldSet`。从这些集合类型中的一种“提取”一个数组是非常简单的。要做到这一点,将集合转换为一个数组。因此,在此场景中应该使用`PassThroughFieldExtractor`。应该注意的是,如果传入的对象不是集合的类型,那么`PassThroughFieldExtractor`将返回一个仅包含要提取的项的数组。 + +###### `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 以创建对象数组。值得注意的是,名称的顺序决定了数组中字段的顺序。 + +##### 分隔的文件编写示例 + +最基本的平面文件格式是一种所有字段都用分隔符分隔的格式。这可以使用`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(); +} +``` + +##### 固定宽度文件编写示例 + +分隔符并不是唯一一种平面文件格式。许多人更喜欢为每个列使用一个设置的宽度来划分字段,这通常称为“固定宽度”。 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(); +} +``` + +##### 处理文件创建 + +`FlatFileItemReader`与文件资源的关系非常简单。当读取器被初始化时,它会打开该文件(如果它存在的话),如果它不存在,则会抛出一个异常。写文件并不是那么简单。乍一看,对于`FlatFileItemWriter`似乎应该存在类似的直接契约:如果文件已经存在,则抛出一个异常,如果不存在,则创建它并开始写入。然而,重新启动`Job`可能会导致问题。在正常的重启场景中,契约是相反的:如果文件存在,则从最后一个已知的良好位置开始向它写入,如果不存在,则抛出一个异常。但是,如果此作业的文件名总是相同,会发生什么情况?在这种情况下,如果文件存在,你可能想要删除它,除非是重新启动。由于这种可能性,`FlatFileItemWriter`包含属性`shouldDeleteIfExists`。将此属性设置为 true 将导致在打开 Writer 时删除同名的现有文件。 + +### 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](https://docs.spring.io/spring-batch/docs/current/reference/html/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 绑定](https://docs.spring.io/spring-batch/docs/current/reference/html/images/oxm-fragments.png) + +图 2.OXM 绑定 + +通过介绍 OXM 以及如何使用 XML 片段来表示记录,我们现在可以更仔细地研究阅读器和编写器。 + +#### `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`需要一个`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); +``` + +### 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`将 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`将项的编组委托给`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(); +} +``` + +### 多文件输入 + +在一个`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)`对输入资源进行排序,以确保在重新启动场景中的作业运行之间保留资源排序。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 数据库 + +像大多数 Enterprise 应用程序样式一样,数据库是批处理的中心存储机制。然而,由于系统必须使用的数据集的巨大规模,批处理与其他应用程序样式不同。如果 SQL 语句返回 100 万行,那么结果集可能会将所有返回的结果保存在内存中,直到所有行都被读取为止。 Spring Batch 为此问题提供了两种类型的解决方案: + +* [基于游标的`ItemReader`实现] + +* [分页`ItemReader`实现] + +#### 基于光标的`ItemReader`实现 + +使用数据库游标通常是大多数批处理开发人员的默认方法,因为它是数据库解决关系数据“流”问题的方法。Java`ResultSet`类本质上是一种用于操作游标的面向对象机制。a`ResultSet`维护当前数据行的游标。在`ResultSet`上调用`next`将光标移动到下一行。 Spring 基于批处理游标的`ItemReader`实现在初始化时打开游标,并在每次调用`read`时将游标向前移动一行,返回可用于处理的映射对象。然后调用`close`方法,以确保释放所有资源。 Spring 核心`JdbcTemplate`通过使用回调模式来完全映射`ResultSet`中的所有行,并在将控制权返回给方法调用方之前关闭,从而绕过了这个问题。然而,在批处理中,这必须等到步骤完成。下图显示了基于游标的`ItemReader`如何工作的通用关系图。请注意,虽然示例使用 SQL(因为 SQL 是广为人知的),但任何技术都可以实现基本方法。 + +![游标示例](https://docs.spring.io/spring-batch/docs/current/reference/html/images/cursorExample.png) + +图 3.游标示例 + +这个例子说明了基本模式。给定一个有三列的“foo”表:`ID`、`NAME`和`BAR`,选择 ID 大于 1 但小于 7 的所有行。这将把游标的开头(第 1 行)放在 ID2 上。该行的结果应该是一个完全映射的`Foo`对象。调用`read()`再次将光标移动到下一行,即 ID 为 3 的`Foo`。在每个`read`之后写出这些读取的结果,从而允许对对象进行垃圾收集(假设没有实例变量维护对它们的引用)。 + +##### `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,00 0.如果上面的代码将返回的`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(); + +} +``` + +###### 附加属性 + +因为在 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` + +正如正常的 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`的工作原理与`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`。 + +#### 分页`ItemReader`实现 + +使用数据库游标的一种替代方法是运行多个查询,其中每个查询获取部分结果。我们把这一部分称为一个页面。每个查询必须指定起始行号和我们希望在页面中返回的行数。 + +##### `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` + +分页`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”属性确定每个查询执行从数据库中读取的实体的数量。 + +#### 数据库项目编写器 + +虽然平面文件和 XML 文件都有一个特定的`ItemWriter`实例,但在数据库世界中没有完全相同的实例。这是因为事务提供了所需的所有功能。`ItemWriter`实现对于文件来说是必要的,因为它们必须像事务一样工作,跟踪写好的项目,并在适当的时候刷新或清除。数据库不需要此功能,因为写操作已经包含在事务中了。用户可以创建自己的 DAO 来实现`ItemWriter`接口,或者使用自定义的`ItemWriter`接口,这是为通用处理问题编写的。无论哪种方式,它们的工作都应该没有任何问题。需要注意的一点是批处理输出所提供的性能和错误处理能力。当使用 Hibernate 作为`ItemWriter`时,这是最常见的,但是当使用 JDBC 批处理模式时,可能会有相同的问题。批处理数据库输出没有任何固有的缺陷,前提是我们要小心刷新,并且数据中没有错误。然而,书写时的任何错误都可能导致混淆,因为无法知道是哪个单独的项目导致了异常,或者即使是任何单独的项目是负责任的,如下图所示: + +![刷新错误](https://docs.spring.io/spring-batch/docs/current/reference/html/images/errorOnFlush.png) + +图 4.刷新错误 + +如果项目在写入之前被缓冲,则在提交之前刷新缓冲区之前不会抛出任何错误。例如,假设每个块写 20 个项,第 15 个项抛出一个`DataIntegrityViolationException`。就`Step`而言,所有 20 个项都已成功写入,因为只有在实际写入它们之前,才能知道发生了错误。一旦调用`Session#flush()`,将清空缓冲区并命中异常。在这一点上,`Step`是无能为力的。事务必须回滚。通常,此异常可能会导致跳过该项(取决于跳过/重试策略),然后不会再次写入该项。但是,在批处理场景中,无法知道是哪个项导致了问题。当故障发生时,整个缓冲区正在被写入。解决此问题的唯一方法是在每个项目之后进行刷新,如下图所示: + +![写错误](https://docs.spring.io/spring-batch/docs/current/reference/html/images/errorOnWrite.png) + +图 5.写错误 + +这是一个常见的用例,尤其是在使用 Hibernate 时,而`ItemWriter`的实现的简单准则是在每次调用`write()`时刷新。这样做允许可靠地跳过项, Spring 批处理在内部处理错误后对`ItemWriter`的调用的粒度。 + +### 重用现有服务 + +批处理系统通常与其他应用程序样式结合使用。最常见的是在线系统,但它也可以通过移动每个应用程序样式使用的必要的大容量数据来支持集成,甚至支持厚客户机应用程序。由于这个原因,许多用户希望在其批处理作业中重用现有的 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(); +} +``` + +### 防止状态持久性 + +默认情况下,所有`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`中为其参与的任何执行创建任何条目。 + +### 创建自定义项目阅读器和项目编写器 + +到目前为止,本章已经讨论了 Spring 批处理中的读和写的基本契约,以及这样做的一些常见实现。然而,这些都是相当通用的,并且有许多潜在的场景可能不会被开箱即用的实现所覆盖。本节通过使用一个简单的示例,展示了如何创建自定义`ItemReader`和`ItemWriter`实现,并正确地实现它们的契约。`ItemReader`还实现了`ItemStream`,以说明如何使读取器或写入器重新启动。 + +#### 自定义`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()); +``` + +##### 使`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()`属性,该属性允许重写这个键名。 + +#### 自定义`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; + } +} +``` + +##### 使`ItemWriter`重新启动 + +要使`ItemWriter`可重启,我们将遵循与`ItemReader`相同的过程,添加并实现`ItemStream`接口以同步执行上下文。在这个示例中,我们可能必须计算处理的项目的数量,并将其添加为页脚记录。如果需要这样做,我们可以在`ItemWriter`中实现`ItemStream`,这样,如果流被重新打开,计数器将从执行上下文中重新构造。 + +在许多实际的情况下,自定义`ItemWriters`也会委托给另一个本身是可重启的编写器(例如,当写到文件时),或者它会写到事务资源,因此不需要重启,因为它是无状态的。当你有一个有状态的编写器时,你可能应该确保实现`ItemStream`以及`ItemWriter`。还请记住,Writer 的客户机需要知道`ItemStream`,因此你可能需要在配置中将其注册为流。 + +### 项读取器和编写器实现 + +在本节中,我们将向你介绍在前几节中尚未讨论过的读者和作者。 + +#### 装饰者 + +在某些情况下,用户需要将专门的行为附加到预先存在的`ItemReader`。 Spring Batch 提供了一些开箱即用的装饰器,它们可以将额外的行为添加到你的`ItemReader`和`ItemWriter`实现中。 + +Spring 批处理包括以下装饰器: + +* [`SynchronizedItemStreamReader`] + +* [`SingleItemPeekableItemReader`] + +* [`SynchronizedItemStreamWriter`] + +* [`MultiResourceItemWriter`] + +* [`ClassifierCompositeItemWriter`] + +* [`ClassifierCompositeItemProcessor`] + +##### `SynchronizedItemStreamReader` + +当使用不是线程安全的`ItemReader`时, Spring Batch 提供`SynchronizedItemStreamReader`decorator,该 decorator 可用于使`ItemReader`线程安全。 Spring 批处理提供了一个`SynchronizedItemStreamReaderBuilder`来构造`SynchronizedItemStreamReader`的实例。 + +##### `SingleItemPeekableItemReader` + +Spring 批处理包括向`ItemReader`添加 PEEK 方法的装饰器。这种 peek 方法允许用户提前查看一项。对 Peek 的重复调用返回相同的项,这是从`read`方法返回的下一个项。 Spring 批处理提供了一个`SingleItemPeekableItemReaderBuilder`来构造`SingleItemPeekableItemReader`的实例。 + +| |SingleitemPeekableitemreader 的 Peek 方法不是线程安全的,因为它不可能
在多个线程中执行 Peek。窥视
的线程中只有一个会在下一次调用中获得要读取的项。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### `SynchronizedItemStreamWriter` + +当使用不是线程安全的`ItemWriter`时, Spring Batch 提供`SynchronizedItemStreamWriter`decorator,该 decorator 可用于使`ItemWriter`线程安全。 Spring 批处理提供了一个`SynchronizedItemStreamWriterBuilder`来构造`SynchronizedItemStreamWriter`的实例。 + +##### `MultiResourceItemWriter` + +当当前资源中写入的项数超过`itemCountLimitPerResource`时,`MultiResourceItemWriter`包装一个`ResourceAwareItemWriterItemStream`并创建一个新的输出资源。 Spring 批处理提供了一个`MultiResourceItemWriterBuilder`来构造`MultiResourceItemWriter`的实例。 + +##### `ClassifierCompositeItemWriter` + +`ClassifierCompositeItemWriter`调用用于每个项的`ItemWriter`实现的集合之一,该实现基于通过提供的`Classifier`实现的路由器模式。如果所有委托都是线程安全的,则实现是线程安全的。 Spring 批处理提供了一个`ClassifierCompositeItemWriterBuilder`来构造`ClassifierCompositeItemWriter`的实例。 + +##### `ClassifierCompositeItemProcessor` + +`ClassifierCompositeItemProcessor`是一个`ItemProcessor`,它调用`ItemProcessor`实现的集合之一,该实现基于通过所提供的`Classifier`实现的路由器模式。 Spring 批处理提供了一个`ClassifierCompositeItemProcessorBuilder`来构造`ClassifierCompositeItemProcessor`的实例。 + +#### 消息阅读器和消息编写器 + +Spring Batch 为常用的消息传递系统提供了以下读取器和编写器: + +* [`AmqpItemReader`] + +* [`AmqpItemWriter`] + +* [`JmsItemReader`] + +* [`JmsItemWriter`] + +* [`KafkaItemReader`] + +* [`KafkaItemWriter`] + +##### `AmqpItemReader` + +`AmqpItemReader`是一个`ItemReader`,它使用`AmqpTemplate`来接收或转换来自交换的消息。 Spring 批处理提供了一个`AmqpItemReaderBuilder`来构造`AmqpItemReader`的实例。 + +##### `AmqpItemWriter` + +`AmqpItemWriter`是一个`ItemWriter`,它使用`AmqpTemplate`向 AMQP 交换发送消息。如果提供的`AmqpTemplate`中未指定名称,则将消息发送到无名交换机。 Spring 批处理提供了`AmqpItemWriterBuilder`来构造`AmqpItemWriter`的实例。 + +##### `JmsItemReader` + +对于使用`JmsTemplate`的 JMS,`ItemReader`是`ItemReader`。模板应该有一个默认的目标,它用于为`read()`方法提供项。 Spring 批处理提供了一个`JmsItemReaderBuilder`来构造`JmsItemReader`的实例。 + +##### `JmsItemWriter` + +对于使用`JmsTemplate`的 JMS,`ItemWriter`是`ItemWriter`。模板应该有一个默认的目的地,用于在`write(List)`中发送项。 Spring 批处理提供了一个`JmsItemWriterBuilder`来构造`JmsItemWriter`的实例。 + +##### `KafkaItemReader` + +对于 Apache Kafka 主题,`KafkaItemReader`是`ItemReader`。可以将其配置为从同一主题的多个分区中读取消息。它在执行上下文中存储消息偏移量,以支持重新启动功能。 Spring 批处理提供了一个`KafkaItemReaderBuilder`来构造`KafkaItemReader`的实例。 + +##### `KafkaItemWriter` + +`KafkaItemWriter`是用于 Apache Kafka 的`ItemWriter`,它使用`KafkaTemplate`将事件发送到默认主题。 Spring 批处理提供了一个`KafkaItemWriterBuilder`来构造`KafkaItemWriter`的实例。 + +#### 数据库阅读器 + +Spring Batch 提供以下数据库阅读器: + +* [`Neo4jItemReader`](#NEO4jitemreader) + +* [`MongoItemReader`] + +* [`HibernateCursorItemReader`] + +* [`HibernatePagingItemReader`] + +* [`RepositoryItemReader`] + +##### `Neo4jItemReader` + +`Neo4jItemReader`是一个`ItemReader`,它使用分页技术从图数据库 NEO4j 中读取对象。 Spring 批处理提供了一个`Neo4jItemReaderBuilder`来构造`Neo4jItemReader`的实例。 + +##### `MongoItemReader` + +`MongoItemReader`是一个`ItemReader`,它使用分页技术从 MongoDB 读取文档。 Spring 批处理提供了一个`MongoItemReaderBuilder`来构造`MongoItemReader`的实例。 + +##### `HibernateCursorItemReader` + +`HibernateCursorItemReader`是用于读取在 Hibernate 之上构建的数据库记录的`ItemStreamReader`。它执行 HQL 查询,然后在初始化时,在调用`read()`方法时对结果集进行迭代,依次返回与当前行对应的对象。 Spring 批处理提供了一个`HibernateCursorItemReaderBuilder`来构造`HibernateCursorItemReader`的实例。 + +##### `HibernatePagingItemReader` + +`HibernatePagingItemReader`是一个`ItemReader`,用于读取建立在 Hibernate 之上的数据库记录,并且一次只读取固定数量的项。 Spring 批处理提供了一个`HibernatePagingItemReaderBuilder`来构造`HibernatePagingItemReader`的实例。 + +##### `RepositoryItemReader` + +`RepositoryItemReader`是通过使用`PagingAndSortingRepository`读取记录的`ItemReader`。 Spring 批处理提供了一个`RepositoryItemReaderBuilder`来构造`RepositoryItemReader`的实例。 + +#### 数据库编写者 + +Spring Batch 提供以下数据库编写器: + +* [`Neo4jItemWriter`](#NEO4jitemwriter) + +* [`MongoItemWriter`] + +* [`RepositoryItemWriter`] + +* [`HibernateItemWriter`] + +* [`JdbcBatchItemWriter`] + +* [`JpaItemWriter`] + +* [`GemfireItemWriter`] + +##### `Neo4jItemWriter` + +`Neo4jItemWriter`是一个`ItemWriter`实现,它将写到 NEO4J 数据库。 Spring 批处理提供了一个`Neo4jItemWriterBuilder`来构造`Neo4jItemWriter`的实例。 + +##### `MongoItemWriter` + +`MongoItemWriter`是一个`ItemWriter`实现,它使用 Spring data 的`MongoOperations`的实现将数据写到 MongoDB 存储。 Spring 批处理提供了一个`MongoItemWriterBuilder`来构造`MongoItemWriter`的实例。 + +##### `RepositoryItemWriter` + +`RepositoryItemWriter`是来自 Spring 数据的`ItemWriter`包装器。 Spring 批处理提供了一个`RepositoryItemWriterBuilder`来构造`RepositoryItemWriter`的实例。 + +##### `HibernateItemWriter` + +`HibernateItemWriter`是一个`ItemWriter`,它使用一个 Hibernate 会话来保存或更新不是当前 Hibernate 会话的一部分的实体。 Spring 批处理提供了一个`HibernateItemWriterBuilder`来构造`HibernateItemWriter`的实例。 + +##### `JdbcBatchItemWriter` + +`JdbcBatchItemWriter`是一个`ItemWriter`,它使用`NamedParameterJdbcTemplate`中的批处理特性来为提供的所有项执行一批语句。 Spring 批处理提供了一个`JdbcBatchItemWriterBuilder`来构造`JdbcBatchItemWriter`的实例。 + +##### `JpaItemWriter` + +`JpaItemWriter`是一个`ItemWriter`,它使用 JPA `EntityManagerFactory`来合并不属于持久性上下文的任何实体。 Spring 批处理提供了一个`JpaItemWriterBuilder`来构造`JpaItemWriter`的实例。 + +##### `GemfireItemWriter` + +`GemfireItemWriter`是一个`ItemWriter`,它使用一个`GemfireTemplate`将项目存储在 Gemfire 中,作为键/值对。 Spring 批处理提供了一个`GemfireItemWriterBuilder`来构造`GemfireItemWriter`的实例。 + +#### 专业阅读器 + +Spring Batch 提供以下专门的阅读器: + +* [`LdifReader`] + +* [`MappingLdifReader`] + +* [`AvroItemReader`] + +##### `LdifReader` + +`AvroItemWriter`读取来自`Resource`的 LDIF(LDAP 数据交换格式)记录,对它们进行解析,并为执行的每个`LdapAttribute`返回一个`LdapAttribute`对象。 Spring 批处理提供了一个`LdifReaderBuilder`来构造`LdifReader`的实例。 + +##### `MappingLdifReader` + +`MappingLdifReader`从`Resource`读取 LDIF(LDAP 数据交换格式)记录,解析它们,然后将每个 LDIF 记录映射到 POJO(普通的旧 Java 对象)。每个读都返回一个 POJO。 Spring 批处理提供了一个`MappingLdifReaderBuilder`来构造`MappingLdifReader`的实例。 + +##### `AvroItemReader` + +`AvroItemReader`从资源中读取序列化的 AVRO 数据。每个读取返回由 Java 类或 AVRO 模式指定的类型的实例。读取器可以被可选地配置为嵌入 AVRO 模式的输入或不嵌入该模式的输入。 Spring 批处理提供了一个`AvroItemReaderBuilder`来构造`AvroItemReader`的实例。 + +#### 专业作家 + +Spring Batch 提供以下专业的写作人员: + +* [`SimpleMailMessageItemWriter`] + +* [`AvroItemWriter`] + +##### `SimpleMailMessageItemWriter` + +`SimpleMailMessageItemWriter`是可以发送邮件的`ItemWriter`。它将消息的实际发送委托给`MailSender`的实例。 Spring 批处理提供了一个`SimpleMailMessageItemWriterBuilder`来构造`SimpleMailMessageItemWriter`的实例。 + +##### `AvroItemWriter` + +`AvroItemWrite`根据给定的类型或模式将 Java 对象序列化到一个 WriteableResource。编写器可以被可选地配置为在输出中嵌入或不嵌入 AVRO 模式。 Spring 批处理提供了一个`AvroItemWriterBuilder`来构造`AvroItemWriter`的实例。 + +#### 专用处理器 + +Spring Batch 提供以下专门的处理器: + +* [`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 0000000000000000000000000000000000000000..9dae85d96182112415102d423971e646977c34ec --- /dev/null +++ b/docs/spring-batch/repeat.md @@ -0,0 +1,158 @@ +# 重复 + +## 重复 + +XMLJavaBoth + +### 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 + +`RepeatCallback`的方法参数是`RepeatContext`。许多回调忽略了上下文。但是,如果有必要,它可以作为一个属性包来存储迭代期间的瞬态数据。在`iterate`方法返回后,上下文不再存在。 + +如果正在进行嵌套的迭代,则`RepeatContext`具有父上下文。父上下文有时用于存储需要在对`iterate`的调用之间共享的数据。例如,如果你想计算迭代中某个事件发生的次数,并在随后的调用中记住它,那么就是这种情况。 + +#### 重复状态 + +`RepeatStatus`是 Spring 批处理用来指示处理是否已经完成的枚举。它有两个可能的`RepeatStatus`值,如下表所示: + +| *Value* |*说明*| +|-----------|--------------------------------------| +|CONTINUABLE|还有更多的工作要做。| +| FINISHED |不应再重复。| + +`RepeatStatus`值也可以通过在`RepeatStatus`中使用`and()`方法与逻辑和操作结合。这样做的效果是在可持续的标志上做一个合乎逻辑的操作。换句话说,如果任一状态是`FINISHED`,则结果是`FINISHED`。 + +### 完工政策 + +在`RepeatTemplate`内,`iterate`方法中的循环的终止由`CompletionPolicy`确定,这也是`RepeatContext`的工厂。`RepeatTemplate`负责使用当前策略创建`RepeatContext`,并在迭代的每个阶段将其传递给`RepeatCallback`。回调完成其`doInIteration`后,`RepeatTemplate`必须调用`CompletionPolicy`,以要求它更新其状态(该状态将存储在`RepeatContext`中)。然后,它询问策略迭代是否完成。 + +Spring 批处理提供了`CompletionPolicy`的一些简单的通用实现。`SimpleCompletionPolicy`允许执行多达固定的次数(与`RepeatStatus.FINISHED`一起强制在任何时间提前完成)。 + +对于更复杂的决策,用户可能需要实现自己的完成策略。例如,一旦联机系统投入使用,一个批处理窗口就会阻止批处理作业的执行,这将需要一个自定义策略。 + +### 异常处理 + +如果在`RepeatCallback`中抛出了异常,则`RepeatTemplate`查询`ExceptionHandler`,该查询可以决定是否重新抛出异常。 + +下面的清单显示了`ExceptionHandler`接口定义: + +``` +public interface ExceptionHandler { + + void handleException(RepeatContext context, Throwable throwable) + throws Throwable; + +} +``` + +一个常见的用例是计算给定类型的异常数量,并在达到限制时失败。为此目的, Spring 批提供了`SimpleLimitExceptionHandler`和稍微更灵活的`RethrowOnThresholdExceptionHandler`。`SimpleLimitExceptionHandler`具有一个极限属性和一个异常类型,应该将其与当前异常进行比较。所提供类型的所有子类也被计算在内。给定类型的异常将被忽略,直到达到限制,然后重新抛出它们。其他类型的异常总是被重新抛出。 + +`SimpleLimitExceptionHandler`的一个重要的可选属性是名为`useParent`的布尔标志。默认情况下它是`false`,因此该限制仅在当前的`RepeatContext`中考虑。当设置为`true`时,该限制在嵌套迭代中跨兄弟上下文(例如步骤中的一组块)保持不变。 + +### 听众 + +通常情况下,能够接收跨多个不同迭代的交叉关注点的额外回调是有用的。为此, 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`的调用顺序相反。 + +### 并行处理 + +`RepeatOperations`的实现不限于按顺序执行回调。一些实现能够并行地执行它们的回调,这一点非常重要。为此, Spring Batch 提供了`TaskExecutorRepeatTemplate`,它使用 Spring `TaskExecutor`策略来运行`RepeatCallback`。默认值是使用`SynchronousTaskExecutor`,其效果是在相同的线程中执行整个迭代(与正常的`RepeatTemplate`相同)。 + +### 声明式迭代 + +有时,你知道有一些业务处理在每次发生时都想要重复。这方面的经典示例是消息管道的优化。如果一批消息经常到达,那么处理它们比为每条消息承担单独事务的成本更有效。 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 0000000000000000000000000000000000000000..43c48f1f200d5945457e2a71c9e926b87f74a96e --- /dev/null +++ b/docs/spring-batch/retry.md @@ -0,0 +1,225 @@ +# 重试 + +## 重试 + +XMLJavaBoth + +为了使处理更健壮,更不容易失败,有时自动重试失败的操作会有所帮助,以防随后的尝试可能会成功。容易发生间歇性故障的错误通常是暂时的。例如,对 Web 服务的远程调用由于网络故障或数据库更新中的`DeadlockLoserDataAccessException`而失败。 + +### `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` + +`RetryCallback`的方法参数是`RetryContext`。许多回调忽略了上下文,但如果有必要,它可以作为一个属性包来存储迭代期间的数据。 + +如果同一个线程中有一个正在进行的嵌套重试,则`RetryContext`具有父上下文。父上下文有时用于存储需要在对`execute`的调用之间共享的数据。 + +#### `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 + } +}); +``` + +如果业务逻辑在模板决定中止之前没有成功,那么客户机将有机会通过恢复回调执行一些替代处理。 + +#### 无状态重试 + +在最简单的情况下,重试只是一个 while 循环。`RetryTemplate`可以一直尝试,直到成功或失败为止。`RetryContext`包含一些状态来决定是重试还是中止,但是这个状态在堆栈上,不需要在全局的任何地方存储它,所以我们将其称为无状态重试。无状态重试和有状态重试之间的区别包含在`RetryPolicy`的实现中(`RetryTemplate`可以同时处理这两个)。在无状态的重试中,重试回调总是在它失败时所在的线程中执行。 + +#### 有状态重试 + +在故障导致事务资源无效的情况下,有一些特殊的考虑因素。这不适用于简单的远程调用,因为(通常)没有事务性资源,但有时确实适用于数据库更新,尤其是在使用 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`,因此通常对限制和超时的关注可以被注入到那里(在本章后面描述)。 + +### 重试策略 + +在`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`。例如,通过将一种异常类型映射到另一种策略,可以在失败前重试更多次。 + +用户可能需要实现他们自己的重试策略,以做出更多定制的决策。例如,当存在已知的、特定于解决方案的异常的可重试和不可重试的分类时,自定义重试策略是有意义的。 + +### 退避政策 + +当在短暂的失败之后重试时,在再次尝试之前等待一下通常会有所帮助,因为通常故障是由某些只能通过等待解决的问题引起的。如果`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`。 + +### 听众 + +通常情况下,能够接收跨多个不同重试中的交叉关注点的额外回调是有用的。为此, 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`。 + +### 声明式重试 + +有时,你知道有些业务处理在每次发生时都想要重试。这方面的典型例子是远程服务调用。 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 0000000000000000000000000000000000000000..01052610619fee93b85b516e33db066ef0c4cb2b --- /dev/null +++ b/docs/spring-batch/scalability.md @@ -0,0 +1,334 @@ +# 缩放和并行处理 + +## 缩放和并行处理 + +XMLJavaBoth + +许多批处理问题可以通过单线程、单流程作业来解决,因此在考虑更复杂的实现之前,正确地检查它是否满足你的需求始终是一个好主意。衡量一项实际工作的性能,看看最简单的实现是否首先满足你的需求。即使使用标准的硬件,你也可以在一分钟内读写几百兆的文件。 + +Spring 当你准备好开始用一些并行处理来实现一个作业时, Spring Batch 提供了一系列选项,这些选项在本章中进行了描述,尽管其他地方也介绍了一些特性。在高层次上,有两种并行处理模式: + +* 单过程、多线程 + +* 多进程 + +这些指标也可分为以下几类: + +* 多线程步骤(单进程) + +* 并行步骤(单一过程) + +* 步骤的远程分块(多进程) + +* 划分一个步骤(单个或多个进程) + +首先,我们回顾一下单流程选项。然后,我们回顾了多进程的选择。 + +### 多线程步骤 + +启动并行处理的最简单方法是在步骤配置中添加`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()`,并且只要处理和写入是块中最昂贵的部分,你的步骤仍然可以比在单线程配置中快得多地完成。 + +### 平行步骤 + +只要需要并行化的应用程序逻辑可以划分为不同的职责,并分配给各个步骤,那么就可以在单个流程中进行并行化。并行步骤执行很容易配置和使用。 + +例如,与`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)一节。 + +### 远程分块 + +在远程分块中,`Step`处理被分割到多个进程中,通过一些中间件相互通信。下图显示了该模式: + +![远程分块](https://docs.spring.io/spring-batch/docs/current/reference/html/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)一节。 + +### 分区 + +Spring 批处理还提供了用于分区`Step`执行并远程执行它的 SPI。在这种情况下,远程参与者是`Step`实例,这些实例可以很容易地被配置并用于本地处理。下图显示了该模式: + +![分区概述](https://docs.spring.io/spring-batch/docs/current/reference/html/images/partitioning-overview.png) + +图 2。划分 + +`Job`作为`Step`实例的序列在左侧运行,其中一个`Step`实例被标记为管理器。这张图中的工人都是`Step`的相同实例,它实际上可以代替经理,从而导致`Job`的结果相同。工作人员通常是远程服务,但也可能是执行的本地线程。在此模式中,经理发送给工作人员的消息不需要是持久的,也不需要有保证的交付。 Spring `JobRepository`中的批处理元数据确保每个工作者执行一次,并且对于每个`Job`执行只执行一次。 + +Spring 批处理中的 SPI 由`Step`(称为`PartitionStep`)的特殊实现和需要为特定环境实现的两个策略接口组成。策略接口是`PartitionHandler`和`StepExecutionSplitter`,它们的作用在下面的序列图中显示: + +![分区 SPI](https://docs.spring.io/spring-batch/docs/current/reference/html/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`是了解远程或网格环境结构的组件。它能够将`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`有一个更简单的职责:仅为新的步骤执行生成执行上下文作为输入参数(无需担心重新启动)。它只有一个方法,如下面的接口定义所示: + +``` +public interface Partitioner { + Map partition(int gridSize); +} +``` + +这个方法的返回值将每个步骤执行的唯一名称(`String`)与输入参数(`ExecutionContext`)以`ExecutionContext`的形式关联起来。这些名称稍后会在批处理元数据中显示为分区`StepExecutions`中的步骤名称。`ExecutionContext`只是一组名称-值对,因此它可能包含一系列主键、行号或输入文件的位置。然后,远程`Step`通常使用`#{…​}`占位符(在步骤作用域中的后期绑定)绑定到上下文输入,如下一节所示。 + +步骤执行的名称(由`Partitioner`返回的`Map`中的键)需要在`Job`的步骤执行中是唯一的,但没有任何其他特定的要求。要做到这一点(并使名称对用户有意义),最简单的方法是使用前缀 + 后缀命名约定,其中前缀是正在执行的步骤的名称(它本身在`Job`中是唯一的),后缀只是一个计数器。在使用该约定的框架中有一个`SimplePartitioner`。 + +可以使用一个名为`PartitionNameProvider`的可选接口来提供与分区本身分开的分区名称。如果`Partitioner`实现了这个接口,那么在重新启动时,只会查询名称。如果分区是昂贵的,这可以是一个有用的优化。由`PartitionNameProvider`提供的名称必须与`Partitioner`提供的名称匹配。 + +#### 将输入数据绑定到步骤 + +由`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 0000000000000000000000000000000000000000..ca202faf66b21e7a0b644351697a20cff45d61c1 --- /dev/null +++ b/docs/spring-batch/schema-appendix.md @@ -0,0 +1,292 @@ +# 元数据模式 + +## 附录 A:元数据模式 + +### 概述 + +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](https://docs.spring.io/spring-batch/docs/current/reference/html/images/meta-data-erd.png) + +图 1。 Spring 批处理元数据 ERD + +#### 示例 DDL 脚本 + +Spring 批处理核心 JAR 文件包含用于为许多数据库平台创建关系表的示例脚本(反过来,这些平台由作业存储库工厂 Bean 或等效的名称空间自动检测)。这些脚本可以按原样使用,也可以根据需要修改附加的索引和约束。文件名的形式为`schema-*.sql`,其中“\*”是目标数据库平台的简称。脚本在包`org.springframework.batch.core`中。 + +#### 迁移 DDL 脚本 + +Spring Batch 提供了在升级版本时需要执行的迁移 DDL 脚本。这些脚本可以在`org/springframework/batch/core/migration`下的核心 JAR 文件中找到。迁移脚本被组织到与版本号对应的文件夹中,这些版本号被引入: + +* `2.2`:如果你从`2.2`之前的版本迁移到`2.2`版本,则包含所需的脚本 + +* `4.1`:如果你从`4.1`之前的版本迁移到`4.1`版本,则包含所需的脚本 + +#### 版本 + +本附录中讨论的许多数据库表都包含一个版本列。这一列很重要,因为 Spring 批处理在处理数据库更新时采用了乐观的锁定策略。这意味着每次“触摸”(更新)记录时,Version 列中的值都会增加一个。当存储库返回以保存该值时,如果版本号发生了更改,它将抛出一个`OptimisticLockingFailureException`,表示在并发访问中出现了错误。这种检查是必要的,因为即使不同的批处理作业可能在不同的机器中运行,它们都使用相同的数据库表。 + +#### 恒等式 + +`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`然后在这个序列中增加一列,以便提供类似的功能。 + +### `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`值)。 + +### `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`的标识。 + +请注意,此表没有主键。这是因为该框架不需要一个框架,因此不需要它。如果需要,可以添加主键,也可以添加与数据库生成的键,而不会对框架本身造成任何问题。 + +### `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`:时间戳表示此执行最后一次被持久化的时间。 + +### `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`:时间戳表示此执行最后一次被持久化的时间。 + +### `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`:整个上下文,序列化。 + +### `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`:整个上下文,序列化。 + +### 存档 + +由于每次运行批处理作业时,多个表中都有条目,因此通常需要为元数据表创建归档策略。这些表本身旨在显示过去发生的事情的记录,并且通常不会影响任何作业的运行,只有一些与重新启动有关的明显例外: + +* 该框架使用元数据表来确定特定的`JobInstance`是否已经运行过。如果作业已经运行,并且不能重新启动,那么将抛出一个异常。 + +* 如果`JobInstance`的条目在未成功完成的情况下被删除,则框架认为该作业是新的,而不是重新启动。 + +* 如果重新启动了一个作业,框架将使用已持久化到`ExecutionContext`的任何数据来恢复`Job’s`状态。因此,如果作业没有成功完成,从该表中删除任何条目,将阻止它们在再次运行时从正确的点开始。 + +### 国际和多字节字符 + +如果你在业务处理中使用多字节字符集(例如中文或西里尔),那么这些字符可能需要在 Spring 批处理模式中持久化。许多用户发现,只需将模式更改为`VARCHAR`列的长度的两倍就足够了。其他人更喜欢将[JobRepository](job.html#configuringJobRepository)配置为`max-varchar-length`列长度的一半。一些用户还报告说,他们在模式定义中使用`NVARCHAR`代替`VARCHAR`。最佳结果取决于数据库平台和本地配置数据库服务器的方式。 + +### 建立元数据表索引的建议 + +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 0000000000000000000000000000000000000000..c3743aeaf971d2fcd53924b05c3f1c22162e7099 --- /dev/null +++ b/docs/spring-batch/spring-batch-integration.md @@ -0,0 +1,1091 @@ +# Spring 批处理集成 + +## Spring 批处理集成 + +XMLJavaBoth + +### 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) + +#### 名称空间支持 + +Spring 自批处理集成 1.3 以来,添加了专用的 XML 命名空间支持,目的是提供更简单的配置体验。为了激活命名空间,请将以下命名空间声明添加到 Spring XML 应用程序上下文文件中: + +``` + + + ... + + +``` + +用于 Spring 批处理集成的完全配置的 Spring XML 应用程序上下文文件可能如下所示: + +``` + + + ... + + +``` + +也允许将版本号附加到引用的 XSD 文件中,但是,由于无版本声明总是使用最新的模式,因此我们通常不建议将版本号附加到 XSD 名称中。添加版本号可能会在更新 Spring 批处理集成依赖项时产生问题,因为它们可能需要 XML 模式的最新版本。 + +#### 通过消息启动批处理作业 + +当通过使用核心 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)提供了消息传递图标及其描述的完整概述。 + +![启动批处理作业](https://docs.spring.io/spring-batch/docs/current/reference/html/images/launch-batch-job.png) + +图 1。启动批处理作业 + +##### 将文件转换为 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`响应 + +当执行批处理作业时,将返回一个`JobExecution`实例。此实例可用于确定执行的状态。如果`JobExecution`能够成功创建,则无论实际执行是否成功,它总是被返回。 + +如何返回`JobExecution`实例的确切行为取决于所提供的`TaskExecutor`。如果使用`synchronous`(单线程)`TaskExecutor`实现,则只返回`JobExecution`响应`after`作业完成。当使用`asynchronous``TaskExecutor`时,将立即返回`JobExecution`实例。然后,用户可以使用`JobExecution`的`id`实例(带有`JobExecution.getJobId()`),并使用`JobExplorer`查询`JobRepository`中的作业更新状态。有关更多信息,请参阅关于[查询存储库](job.html#queryingRepository)的 Spring 批参考文档。 + +##### 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(); +} +``` + +##### 示例 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`变量。 + +### 作业启动网关的可用属性 + +作业启动网关具有以下属性,你可以设置这些属性来控制作业: + +* `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`时的调用顺序。 + +### 子元素 + +当`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; +} +``` + +#### 提供反馈信息 + +Spring 由于批处理作业可以运行很长时间,因此提供进度信息通常是至关重要的。例如,如果批处理作业的某些部分或所有部分都失败了,利益相关者可能希望得到通知。 Spring 批处理为正在通过以下方式收集的此信息提供支持: + +* 活动轮询 + +* 事件驱动侦听器 + +在异步启动 Spring 批处理作业时(例如,通过使用`Job Launching Gateway`),将返回一个`JobExecution`实例。因此,通过使用`JobExplorer`从`JobRepository`检索`JobExecution`的更新实例,`JobExecution.getJobId()`可用于连续轮询状态更新。然而,这被认为是次优的,事件驱动的方法应该是首选的。 + +因此, Spring 批提供了侦听器,包括三个最常用的侦听器: + +* `StepListener` + +* `ChunkListener` + +* `JobExecutionListener` + +在下图所示的示例中, Spring 批处理作业已配置为`StepExecutionListener`。因此, Spring 集成接收并处理事件之前或之后的任何步骤。例如,接收到的`StepExecution`可以通过使用`Router`进行检查。基于该检查的结果,可以发生各种事情(例如将消息路由到邮件出站通道适配器),以便可以基于某些条件发送出电子邮件通知。 + +![处理信息消息](https://docs.spring.io/spring-batch/docs/current/reference/html/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()) + ... +} +``` + +#### 异步处理器 + +异步处理器帮助你扩展项目的处理。在异步处理器用例中,`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 的引用。 + +#### 外部化批处理过程执行 + +到目前为止讨论的集成方法建议使用 Spring 集成像外壳一样包装 Spring 批处理的用例。然而, Spring 批处理也可以在内部使用 Spring 集成。 Spring 使用这种方法,批处理用户可以将项目甚至块的处理委托给外部进程。这允许你卸载复杂的处理。 Spring 批处理集成为以下方面提供了专门的支持: + +* 远程分块 + +* 远程分区 + +##### 远程分块 + +![远程分块](https://docs.spring.io/spring-batch/docs/current/reference/html/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 负责配置一些组件,如下图所示: + +![远程组块配置](https://docs.spring.io/spring-batch/docs/current/reference/html/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)的完整示例。 + +##### 远程分区 + +![远程分区](https://docs.spring.io/spring-batch/docs/current/reference/html/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 负责配置一些组件,如下图所示: + +![远程分区配置(使用作业存储库轮询)](https://docs.spring.io/spring-batch/docs/current/reference/html/images/remote-partitioning-polling-config.png) + +图 6。远程分区配置(使用作业存储库轮询) + +![远程分区配置(带有回复聚合)](https://docs.spring.io/spring-batch/docs/current/reference/html/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 0000000000000000000000000000000000000000..7c1921c07bbcac6a4e3b77bc6fa1e8a474dc3cef --- /dev/null +++ b/docs/spring-batch/spring-batch-intro.md @@ -0,0 +1,299 @@ +# Spring 批量介绍 + +## Spring 批介绍 + +Enterprise 领域中的许多应用程序需要大容量处理,以在关键任务环境中执行业务操作。这些业务包括: + +* 对大量信息进行自动化、复杂的处理,在没有用户交互的情况下进行最有效的处理。这些操作通常包括基于时间的事件(例如月末计算、通知或通信)。 + +* 在非常大的数据集中重复处理的复杂业务规则的周期性应用(例如,保险利益的确定或费率调整)。 + +* 从内部和外部系统接收的信息的集成,这些信息通常需要以事务的方式进行格式化、验证和处理,并将其集成到记录系统中。批处理用于企业每天处理数十亿笔交易。 + +Spring 批处理是一种轻量级的、全面的批处理框架,旨在使开发对于 Enterprise 系统的日常操作至关重要的健壮的批处理应用程序成为可能。 Spring 批处理构建在人们所期望的 Spring 框架的特征(生产力、基于 POJO 的开发方法和普遍的易用性)的基础上,同时使开发人员在必要时更容易访问和利用更先进的 Enterprise 服务。 Spring 批处理不是一种调度框架。在商业和开源领域都有许多很好的 Enterprise 调度器(例如 Quartz、Tivoli、Control-M 等)。它的目的是与调度器一起工作,而不是取代调度器。 + +Spring 批处理提供了可重用的功能,这些功能在处理大量记录中是必不可少的,包括日志记录/跟踪、事务管理、作业处理统计、作业重新启动、跳过和资源管理。它还提供了更先进的技术服务和功能,通过优化和分区技术实现了非常大的批量和高性能的批处理作业。 Spring 批处理既可以用于简单的用例(例如将文件读入数据库或运行存储过程),也可以用于复杂的、大容量的用例(例如在数据库之间移动大容量的数据,对其进行转换,等等)。大批量批处理作业可以以高度可伸缩的方式利用框架来处理大量信息。 + +### 背景 + +虽然开放源码软件项目和相关社区更多地关注基于 Web 和基于微服务的架构框架,但明显缺乏对可重用架构框架的关注,以满足基于 Java 的批处理需求,尽管仍然需要在 EnterpriseIT 环境中处理此类处理。缺乏标准的、可重用的批处理体系结构导致了在客户 EnterpriseIT 功能中开发的许多一次性内部解决方案的激增。 + +SpringSource(现为 Pivotal)和埃森哲合作改变了这种状况。埃森哲在实现批处理架构方面的行业和技术经验、SpringSource 的技术经验深度以及 Spring 经过验证的编程模型,共同形成了一种自然而强大的合作关系,以创建高质量的、与市场相关的软件,旨在填补 EnterpriseJava 领域的一个重要空白。这两家公司都与许多正在通过开发基于 Spring 的批处理架构解决方案来解决类似问题的客户合作。这提供了一些有用的附加细节和现实生活中的约束,有助于确保该解决方案可以应用于客户提出的现实世界中的问题。 + +埃森哲为 Spring 批处理项目贡献了以前专有的批处理架构框架,以及用于驱动支持、增强和现有功能集的提交者资源。埃森哲的贡献是基于数十年来在过去几代平台上构建批处理架构的经验:COBOL/大型机、C++/UNIX,以及现在的 Java/Anywhere。 + +埃森哲和 SpringSource 之间的合作旨在促进软件处理方法、框架和工具的标准化,这些方法、框架和工具可以由 Enterprise 用户在创建批处理应用程序时始终如一地加以利用。希望为其 EnterpriseIT 环境提供标准的、经过验证的解决方案的公司和政府机构可以从 Spring 批处理中受益。 + +### 使用场景 + +一个典型的批处理程序通常是: + +* 从数据库、文件或队列中读取大量记录。 + +* 以某种方式处理数据。 + +* 以修改后的形式写回数据。 + +Spring 批处理自动化了这种基本的批处理迭代,提供了将类似的事务作为一个集合来处理的能力,通常是在没有任何用户交互的离线环境中。批处理作业是大多数 IT 项目的一部分, Spring 批处理是唯一提供健壮的、Enterprise 规模的解决方案的开源框架。 + +业务场景 + +* 定期提交批处理过程 + +* 并发批处理:作业的并行处理 + +* 分阶段的、Enterprise 的消息驱动处理 + +* 大规模并行批处理 + +* 失败后手动或计划重启 + +* 依赖步骤的顺序处理(扩展到工作流驱动的批处理) + +* 部分处理:跳过记录(例如,在回滚时) + +* 整批事务,用于小批量或现有存储过程/脚本的情况 + +技术目标 + +* 批处理开发人员使用 Spring 编程模型:专注于业务逻辑,让框架处理基础架构。 + +* 在基础结构、批处理执行环境和批处理应用程序之间明确分离关注点。 + +* 提供公共的、核心的执行服务,作为所有项目都可以实现的接口。 + +* 提供可以“开箱即用”地使用的核心执行接口的简单和默认实现。 + +* 通过在所有层中利用 Spring 框架,易于配置、自定义和扩展服务。 + +* 所有现有的核心服务都应该易于替换或扩展,而不会对基础设施层产生任何影响。 + +* 提供一个简单的部署模型,其体系结构 JAR 与应用程序完全分开,使用 Maven 构建。 + +### Spring 批处理体系结构 + +Spring Batch 的设计考虑到了可扩展性和多样化的最终用户群体。下图显示了支持最终用户开发人员的可扩展性和易用性的分层架构。 + +![Figure 1.1: Spring Batch Layered Architecture](https://docs.spring.io/spring-batch/docs/current/reference/html/images/spring-batch-layers.png) + +图 1. Spring 批处理分层架构 + +这个分层架构突出了三个主要的高级组件:应用程序、核心和基础架构。该应用程序包含由开发人员使用 Spring 批处理编写的所有批处理作业和自定义代码。批处理核心包含启动和控制批处理作业所必需的核心运行时类。它包括`JobLauncher`、`Job`和`Step`的实现。应用程序和核心都是建立在一个共同的基础架构之上的。这个基础结构包含常见的读取器、编写器和服务(例如`RetryTemplate`),应用程序开发人员(读取器和编写器,例如`ItemReader`和`ItemWriter`)和核心框架本身(Retry,这是它自己的库)都使用它们。 + +### 一般批处理原则和准则 + +在构建批处理解决方案时,应考虑以下关键原则、指南和一般考虑因素。 + +* 请记住,批处理架构通常会影响在线架构,反之亦然。在设计时,尽可能使用通用的构建模块,同时考虑到体系结构和环境。 + +* 尽可能地简化,避免在单个批处理应用程序中构建复杂的逻辑结构。 + +* 保持数据的处理和存储在物理上紧密地联系在一起(换句话说,将数据保存在发生处理的位置)。 + +* 尽量减少系统资源的使用,特别是 I/O。在内存中执行尽可能多的操作。 + +* 检查应用程序 I/O(分析 SQL 语句),以确保避免不必要的物理 I/O。特别需要寻找以下四个常见的缺陷: + + * 当数据可以读取一次并缓存或保存在工作存储器中时,为每个事务读取数据。 + + * 重读事务的数据,而该事务的数据是在同一事务中较早读取的。 + + * 导致不必要的表或索引扫描。 + + * 没有在 SQL 语句的 WHERE 子句中指定键值。 + +* 不要在一次批处理中做两次事情。例如,如果出于报告目的需要进行数据汇总,则应该(如果可能的话)在最初处理数据时增加存储总量,这样你的报告应用程序就不必重新处理相同的数据。 + +* 在批处理应用程序的开始阶段分配足够的内存,以避免在处理过程中进行耗时的重新分配。 + +* 对于数据 Integrity,总是假设最坏的情况。插入足够的检查和记录验证,以维护数据的 Integrity。 + +* 在可能的情况下,为内部验证实现校验和。例如,平面文件应该有一个预告片记录,它告诉文件中记录的总数和关键字段的汇总。 + +* 在具有实际数据量的类似于生产的环境中,尽早地计划和执行压力测试。 + +* 在大批量系统中,备份可能是具有挑战性的,特别是如果系统在 24-7 的基础上与在线并发运行。数据库备份通常在联机设计中得到很好的处理,但是文件备份也应该被认为是同样重要的。如果系统依赖于平面文件,那么文件备份过程不仅应该到位并记录在案,还应该定期进行测试。 + +### 批处理策略 + +为了帮助设计和实现批处理系统,基本的批处理应用程序构建块和模式应该以示例结构图和代码 shell 的形式提供给设计人员和程序员。在开始设计批处理作业时,应该将业务逻辑分解为一系列步骤,这些步骤可以使用以下标准构建块来实现: + +* *转换应用程序:*对于由外部系统提供或生成的每种类型的文件,必须创建一个转换应用程序,以将提供的事务记录转换为处理所需的标准格式。这种类型的批处理应用程序可以部分或全部由转换实用程序模块组成(请参见基本批处理服务)。 + +* *验证应用程序:*验证应用程序确保所有输入/输出记录都是正确且一致的。验证通常基于文件头和预告片、校验和和验证算法以及记录级别交叉检查。 + +* *提取应用程序:*一种应用程序,它从数据库或输入文件中读取一组记录,根据预定义的规则选择记录,并将记录写入输出文件。 + +* *提取/更新应用程序:*一种应用程序,它从数据库或输入文件中读取记录,并由每个输入记录中的数据驱动对数据库或输出文件进行更改。 + +* *处理和更新应用程序:*对来自提取或验证应用程序的输入事务执行处理的应用程序。处理通常涉及读取数据库以获取处理所需的数据,可能会更新数据库并创建用于输出处理的记录。 + +* *输出/格式应用程序:*读取输入文件、根据标准格式从该记录重组数据并产生输出文件以用于打印或传输到另一个程序或系统的应用程序。 + +此外,应该为不能使用前面提到的构建块构建的业务逻辑提供一个基本的应用程序 shell。 + +除了主要的构建块之外,每个应用程序可以使用一个或多个标准实用程序步骤,例如: + +* 排序:读取输入文件并产生输出文件的程序,其中记录已根据记录中的排序键域重新排序。排序通常由标准的系统实用程序执行。 + +* 分割:一种程序,它读取单个输入文件,并根据字段的值将每条记录写入多个输出文件中的一个。分割可以由参数驱动的标准系统实用程序进行裁剪或执行。 + +* 合并:一种程序,从多个输入文件中读取记录,并用输入文件的合并数据生成一个输出文件。合并可以由参数驱动的标准系统实用程序进行裁剪或执行。 + +批处理应用程序还可以按其输入源进行分类: + +* 数据库驱动的应用程序由从数据库检索到的行或值驱动。 + +* 文件驱动的应用程序由从文件中检索到的记录或值驱动。 + +* 消息驱动的应用程序是由从消息队列中检索到的消息驱动的。 + +任何批处理系统的基础都是处理策略。影响该策略选择的因素包括:批处理系统的估计容量、与联机系统或其他批处理系统的并发性、可用的批处理窗口。(请注意,随着越来越多的企业希望启动并运行 24x7,清晰的批处理窗口正在消失)。 + +批处理的典型处理选项如下(按实现复杂性的增加顺序排列): + +* 在离线模式下的批处理窗口期间的正常处理。 + +* 并发批处理或在线处理。 + +* 同时并行处理许多不同的批处理运行或作业。 + +* 分区(在同一时间处理同一作业的多个实例)。 + +* 上述选项的组合。 + +这些选项中的一部分或全部可能由商业调度程序支持。 + +下一节将更详细地讨论这些处理选项。重要的是要注意,根据经验,批处理过程采用的提交和锁定策略取决于所执行的处理的类型,并且在线锁定策略也应该使用相同的原则。因此,在设计整体架构时,批处理架构不能仅仅是一个事后的想法。 + +锁定策略可以是仅使用普通的数据库锁,或者在体系结构中实现额外的自定义锁定服务。锁定服务将跟踪数据库锁定(例如,通过将必要的信息存储在专用的 DB-table 中),并向请求 DB 操作的应用程序授予或拒绝权限。该体系结构还可以实现重试逻辑,以避免在锁定情况下中止批处理作业。 + +**1.批处理窗口中的正常处理**对于在单独的批处理窗口中运行的简单批处理过程,其中在线用户或其他批处理过程不需要更新的数据,并发不是一个问题,并且可以在批处理运行结束时进行一次提交。 + +在大多数情况下,更稳健的方法更合适。请记住,批处理系统在复杂性和它们处理的数据量方面都有随着时间推移而增长的趋势。如果没有锁定策略,并且系统仍然依赖于一个提交点,那么修改批处理程序可能会很痛苦。因此,即使对于最简单的批处理系统,也要考虑重新启动-恢复选项的提交逻辑的需求,以及与本节后面描述的更复杂情况有关的信息。 + +**2.并发批处理或在线处理**处理可由联机用户同时更新的数据的批处理应用程序不应锁定联机用户可能需要超过几秒钟的任何数据(数据库或文件中的数据)。此外,在每几个事务结束时,都应该将更新提交给数据库。这将最小化其他进程不可用的数据部分和数据不可用的时间。 + +最小化物理锁定的另一种选择是使用乐观锁定模式或悲观锁定模式实现逻辑行级锁定。 + +* 乐观锁定假设记录争用的可能性较低。它通常意味着在每个数据库表中插入一个时间戳列,该数据库表由批处理和在线处理并发使用。当应用程序获取要处理的行时,它也会获取时间戳。当应用程序尝试更新已处理的行时,更新将使用 WHERE 子句中的原始时间戳。如果时间戳匹配,则数据和时间戳将被更新。如果时间戳不匹配,则表示另一个应用程序在获取和更新尝试之间更新了相同的行。因此,无法执行更新。 + +* 悲观锁定是任何一种锁定策略,该策略假定存在记录争用的高可能性,因此需要在检索时获得物理或逻辑锁定。一种悲观逻辑锁使用数据库表中的专用锁列。当应用程序检索要更新的行时,它会在 Lock 列中设置一个标志。有了标志后,试图从逻辑上检索同一行的其他应用程序将失败。当设置标记的应用程序更新该行时,它也会清除标记,从而使其他应用程序能够检索该行。请注意,在初始获取和标志设置之间也必须保持数据的 Integrity,例如通过使用 DB 锁(例如`SELECT FOR UPDATE`)。还需要注意的是,这种方法与物理锁定有相同的缺点,只是在用户去吃午饭而记录被锁定的情况下,构建一个超时机制来释放锁,会更容易管理。 + +这些模式不一定适合批处理,但它们可能用于并发批处理和在线处理(例如在数据库不支持行级锁定的情况下)。作为一般规则,乐观锁定更适合于在线应用程序,而悲观锁定更适合批处理应用程序。每当使用逻辑锁时,必须对所有访问由逻辑锁保护的数据实体的应用程序使用相同的方案。 + +请注意,这两种解决方案都只解决锁定单个记录的问题。通常,我们可能需要锁定逻辑上相关的一组记录。对于物理锁,你必须非常小心地管理这些锁,以避免潜在的死锁。对于逻辑锁,通常最好构建一个逻辑锁管理器,该管理器了解你想要保护的逻辑记录组,并可以确保锁是一致的和非死锁的。这个逻辑锁管理器通常使用自己的表来进行锁管理、争用报告、超时机制和其他关注事项。 + +**3.并行处理**并行处理允许多个批处理运行或作业并行运行,以最大限度地减少总的批处理时间。只要作业不共享相同的文件、DB-tables 或索引空间,这就不是问题。如果这样做了,则应该使用分区数据来实现此服务。另一种选择是通过使用控制表构建用于维护相互依赖关系的体系结构模块。控制表应该包含每个共享资源的一行,以及应用程序是否正在使用该资源。然后,批处理架构或并行作业中的应用程序将从该表中检索信息,以确定它是否可以访问所需的资源。 + +如果数据访问不是问题,则可以通过使用额外的线程来并行处理来实现并行处理。在大型机环境中,传统上使用并行作业类,以确保所有进程都有足够的 CPU 时间。无论如何,解决方案必须足够健壮,以确保所有正在运行的进程都有时间片。 + +并行处理中的其他关键问题包括负载平衡和一般系统资源(如文件、数据库缓冲池等)的可用性。还要注意,控制表本身很容易成为关键资源。 + +**4.划分**使用分区允许多个版本的大批量应用程序同时运行。这样做的目的是减少处理长批处理作业所需的时间。可以成功分区的进程是那些可以分割输入文件和/或分区主数据库表以允许应用程序在不同的数据集上运行的进程。 + +此外,被分区的进程必须被设计为仅处理其分配的数据集。分区体系结构必须与数据库设计和数据库分区策略紧密联系在一起。请注意,数据库分区并不一定意味着数据库的物理分区,尽管在大多数情况下这是可取的。下图展示了分区方法: + +![图 1.2:分区过程](https://docs.spring.io/spring-batch/docs/current/reference/html/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 0000000000000000000000000000000000000000..1cfc195410e6f38713d0e97aedb6cb5a40b2b994 --- /dev/null +++ b/docs/spring-batch/step.md @@ -0,0 +1,1869 @@ +# 配置一个步骤 + +## 配置`Step` + +XMLJavaBoth + +正如[领域章节](domain.html#domainLanguageOfBatch)中所讨论的,`Step`是一个域对象,它封装了批处理作业的一个独立的、连续的阶段,并包含定义和控制实际批处理所需的所有信息。这必然是一个模糊的描述,因为任何给定的`Step`的内容都是由编写`Job`的开发人员自行决定的。a`Step`可以是简单的,也可以是复杂的,正如开发人员所希望的那样。一个简单的`Step`可能会将文件中的数据加载到数据库中,只需要很少或不需要代码(取决于使用的实现)。更复杂的`Step`可能具有复杂的业务规则,作为处理的一部分,如下图所示: + +![Step](https://docs.spring.io/spring-batch/docs/current/reference/html/images/step.png) + +图 1.步骤 + +### 面向块的处理 + +Spring 批处理在其最常见的实现中使用了一种“面向块”的处理风格。面向块的处理指的是一次读取一个数据,并创建在事务边界内写出的“块”。一旦读取的项数等于提交间隔,`ItemWriter`就会写出整个块,然后提交事务。下图显示了这个过程: + +![面向块的处理](https://docs.spring.io/spring-batch/docs/current/reference/html/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`时的过程: + +![基于项目处理器的面向块处理](https://docs.spring.io/spring-batch/docs/current/reference/html/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)部分。 + +#### 配置`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`是可选的,因为该项可以直接从阅读器传递给编写器。 + +#### 从父节点继承`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`属性应该指代工作流中的步骤,而不是独立的步骤。 + +##### 摘要`Step` + +有时,可能需要定义不是完整的`Step`配置的父`Step`。例如,如果`reader`、`writer`和`tasklet`属性在`Step`配置中被保留,则初始化失败。如果必须在没有这些属性的情况下定义父属性,那么应该使用`abstract`属性。`abstract``Step`只是扩展,不是实例化。 + +在下面的示例中,如果不声明`Step``abstractParentStep`为抽象,则不会对其进行实例化。`Step`、“ConcreteStep2”有“itemreader”、“itemwriter”和 commit-interval=10。 + +``` + + + + + + + + + + + +``` + +##### 合并列表 + +`Steps`上的一些可配置元素是列表,例如``元素。如果父元素和子元素`Steps`都声明一个``元素,那么子元素的列表将覆盖父元素的列表。为了允许子元素向父元素定义的列表中添加额外的侦听器,每个 List 元素都具有`merge`属性。如果元素指定`merge="true"`,那么子元素的列表将与父元素的列表合并,而不是覆盖它。 + +在下面的示例中,使用两个侦听器创建`Step`“concreteStep3”:`listenerOne`和`listenerTwo`: + +``` + + + + + + + + + + + + + + +``` + +#### 提交间隔 + +如前所述,一个步骤读入并写出项,并使用提供的`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`,事务被提交。 + +#### 配置用于重新启动的`Step` + +在“[配置和运行作业](job.html#configureJob)”小节中,讨论了重新启动`Job`。重启对步骤有很多影响,因此,可能需要一些特定的配置。 + +##### 设置启动限制 + +在许多情况下,你可能希望控制`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`。 + +##### 重新启动已完成的`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(); +} +``` + +##### `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`。 + +#### 配置跳过逻辑 + +在许多情况下,在处理过程中遇到的错误不会导致`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`方法调用的顺序并不重要。 + +#### 配置重试逻辑 + +在大多数情况下,你希望异常导致跳过或`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)。 + +#### 控制回滚 + +默认情况下,不管是重试还是跳过,从`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(); +} +``` + +##### 事务读取器 + +`ItemReader`的基本契约是,它只是远期的。该步骤缓冲读写器的输入,以便在回滚的情况下,不需要从读写器重新读取项目。然而,在某些情况下,读取器是建立在事务性资源之上的,例如 JMS 队列。在这种情况下,由于队列与回滚的事务绑定在一起,因此从队列中拉出的消息将被放回。出于这个原因,可以将该步骤配置为不缓冲项。 + +下面的示例展示了如何创建不使用 XML 缓冲项的读取器: + +XML 配置 + +``` + + + + + +``` + +下面的示例展示了如何创建不在 Java 中缓冲项的读取器: + +Java 配置 + +``` +@Bean +public Step step1() { + return this.stepBuilderFactory.get("step1") + .chunk(2) + .reader(itemReader()) + .writer(itemWriter()) + .readerIsTransactionalQueue() + .build(); +} +``` + +#### 事务属性 + +事务属性可用于控制`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(); +} +``` + +#### 用`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 的状态被正确地持久化。 + +#### 拦截`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`表示用于`Step`执行的最通用的侦听器。它允许在`Step`开始之前和结束之后发出通知,无论是正常结束还是失败,如下例所示: + +``` +public interface StepExecutionListener extends StepListener { + + void beforeStep(StepExecution stepExecution); + + ExitStatus afterStep(StepExecution stepExecution); + +} +``` + +`ExitStatus`是`afterStep`的返回类型,以便使侦听器有机会修改在完成`Step`时返回的退出代码。 + +与此接口对应的注释是: + +* `@BeforeStep` + +* `@AfterStep` + +##### `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` + +在前面讨论跳过逻辑时,提到了记录跳过的记录可能是有益的,这样可以在以后处理它们。在读取错误的情况下,可以使用`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` + +就像`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`“监听”项目的写入,如以下接口定义所示: + +``` +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` + +`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` + +###### 跳过侦听器和事务 + +`SkipListener`最常见的用例之一是注销一个跳过的项,这样就可以使用另一个批处理过程甚至人工过程来评估和修复导致跳过的问题。因为在许多情况下原始事务可能会被回滚, Spring Batch 提供了两个保证: + +1. 每个项目只调用一次适当的 Skip 方法(取决于错误发生的时间)。 + +2. 总是在事务提交之前调用`SkipListener`。这是为了确保侦听器调用的任何事务资源不会因`ItemWriter`中的故障而回滚。 + +### `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` + +与`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; +} +``` + +#### 示例`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; +} +``` + +### 控制阶跃流 + +在拥有一份工作的过程中,有了将步骤组合在一起的能力,就需要能够控制工作如何从一个步骤“流动”到另一个步骤。a`Step`失败并不一定意味着`Job`应该失败。此外,可能有不止一种类型的“成功”来决定下一步应该执行哪个`Step`。根据`Steps`组的配置方式,某些步骤甚至可能根本不会被处理。 + +#### 序贯流 + +最简单的流程场景是所有步骤都按顺序执行的作业,如下图所示: + +![顺序流动](https://docs.spring.io/spring-batch/docs/current/reference/html/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 中。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 条件流 + +在上面的例子中,只有两种可能性: + +1. `step`成功,应该执行下一个`step`。 + +2. `step`失败,因此,`job`应该失败。 + +在许多情况下,这可能就足够了。但是,如果`step`的失败应该触发不同的`step`,而不是导致失败,那么在这种情况下该怎么办?下图显示了这样的流程: + +![条件流](https://docs.spring.io/spring-batch/docs/current/reference/html/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”。 + +##### 批处理状态与退出状态 + +在为条件流配置`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`。 + +#### 配置停止 + +在讨论了[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`的状态。 + +##### 以一步结尾 + +配置步骤结束指示`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(); +} +``` + +##### 失败的步骤 + +配置在给定点失败的步骤指示`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(); +} +``` + +##### 在给定的步骤停止作业 + +将作业配置为在特定的步骤停止,将指示`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(); +} +``` + +#### 程序化流程决策 + +在某些情况下,可能需要比`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(); +} +``` + +#### 拆分流 + +到目前为止描述的每个场景都涉及一个`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(); +} +``` + +#### 外部化作业之间的流定义和依赖关系 + +作业中的部分流可以作为单独的 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`通常也是对这个问题的一个很好的回答:“我如何在工作之间创建依赖关系?”这是一种很好的方法,可以将一个大型系统分解成更小的模块,并控制工作流程。 + +### `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 中的引用是强制性的
。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 步骤作用域 + +前面显示的所有延迟绑定示例都在 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 定义: + +``` + +``` + +#### 工作范围 + +在 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 0000000000000000000000000000000000000000..f575d692f9b30b97b87acd22150431308799d1d9 --- /dev/null +++ b/docs/spring-batch/testing.md @@ -0,0 +1,274 @@ +# 单元测试 + +## 单元测试 + +XMLJavaBoth + +与其他应用程序样式一样,对作为批处理作业的一部分编写的任何代码进行单元测试是非常重要的。 Spring 核心文档非常详细地介绍了如何使用 Spring 进行单元和集成测试,因此在此不再赘述。然而,重要的是要考虑如何“端到端”地测试批处理作业,这就是本章所涵盖的内容。 Spring-batch-test 项目包括促进这种端到端测试方法的类。 + +### 创建单元测试类 + +为了让单元测试运行批处理作业,框架必须加载作业的应用上下文。使用两个注释来触发此行为: + +* `@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 { ... } +``` + +### 批处理作业的端到端测试 + +“端到端”测试可以定义为从开始到结束测试批处理作业的完整运行。这允许测试设置测试条件、执行作业并验证最终结果。 + +考虑一个从数据库读取并写入平面文件的批处理作业的示例。测试方法从使用测试数据建立数据库开始。它清除 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()); + } +} +``` + +### 测试单个步骤 + +对于复杂的批处理作业,端到端测试方法中的测试用例可能变得难以管理。如果是这些情况,那么让测试用例自行测试单个步骤可能会更有用。`AbstractJobTests`类包含一个名为`launchStep`的方法,该方法使用一个步骤名并仅运行特定的`Step`。这种方法允许更有针对性的测试,让测试只为该步骤设置数据,并直接验证其结果。下面的示例展示了如何使用`launchStep`方法按名称加载`Step`: + +``` +JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep"); +``` + +### 测试步骤范围内的组件 + +通常,在运行时为你的步骤配置的组件使用步骤作用域和后期绑定从步骤或作业执行中注入上下文。这些作为独立组件进行测试是很棘手的,除非你有一种方法来设置上下文,就好像它们是在一个步骤执行中一样。这是 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; + } +}); +``` + +### 验证输出文件 + +当批处理作业写到数据库时,很容易查询数据库以验证输出是否如预期的那样。然而,如果批处理作业写入文件,那么验证输出也同样重要。 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)); +``` + +### 模拟域对象 + +在为 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 0000000000000000000000000000000000000000..6153585a1b6f8c583b1b7e67aeebe05c4b08fae8 --- /dev/null +++ b/docs/spring-batch/transaction-appendix.md @@ -0,0 +1,230 @@ +# 批处理和交易 + +## 附录 A:批处理和事务 + +### 不需要重试的简单批处理 + +考虑以下简单的嵌套批处理示例,该批处理不需要重试。它展示了批处理的一个常见场景:一个输入源被处理到耗尽,并且我们在处理的“块”结束时定期提交。 + +``` +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)必须回滚整个块。 + +### 简单无状态重试 + +对于非事务性的操作,例如对 Web 服务或其他远程资源的调用,使用重试也很有用,如下面的示例所示: + +``` +0 | TX { +1 | input; +1.1 | output; +2 | RETRY { +2.1 | remote access; +| } +| } +``` + +这实际上是重试中最有用的应用程序之一,因为与数据库更新相比,远程调用更有可能失败并可重试。只要远程访问(2.1)最终成功,事务`TX`(0)就提交。如果远程访问(2.1)最终失败,那么事务`TX`(0)将保证回滚。 + +### 典型的重复重试模式 + +最典型的批处理模式是向块的内部块添加重试,如以下示例所示: + +``` +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)了解每个项目的历史,并可以决定是否对它进行另一次尝试。 + +### 异步块处理 + +通过将外部批配置为使用`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; +| } +| +| } +| } +| +| } +``` + +### 异步项处理 + +在[典型例子](#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)的成本时,它才是有用的。 + +### 批处理和事务传播之间的交互 + +批处理重试和事务管理之间的耦合比我们理想的更紧密。特别是,无状态重试不能用于使用不支持嵌套传播的事务管理器重试数据库操作。 + +下面的示例使用重试而不重复: + +``` +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`模式是最好的。 + +### 特殊情况:使用正交资源的事务 + +对于没有嵌套数据库事务的简单情况,默认传播总是 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)无法提交(例如,因为消息系统不可用)时,可能发生的最坏情况是重复消息。 + +### 无状态重试无法恢复 + +在上面的典型示例中,无状态重试和有状态重试之间的区别很重要。它实际上最终是一个事务性约束,它强制了这种区别,并且这种约束也使区别存在的原因变得很明显。 + +我们首先观察到,除非我们在事务中包装项目处理,否则无法跳过失败的项目并成功提交块的其余部分。因此,我们将典型的批处理执行计划简化如下: + +``` +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 0000000000000000000000000000000000000000..50c5cc7c4e7677bc7e8da66464729daeb2bade53 --- /dev/null +++ b/docs/spring-batch/whatsnew.md @@ -0,0 +1,128 @@ +# 最新更新在 Spring 批 4.3 中 + +## 在 Spring 批 4.3 中最新更新 + +这个版本附带了许多新特性、性能改进、依赖更新和 API 修改。这一节描述了最重要的变化。有关更改的完整列表,请参阅[发行说明](https://github.com/spring-projects/spring-batch/releases/tag/4.3.0)。 + +### 新功能 + +#### 新建同步 ItemStreamWriter + +与`SynchronizedItemStreamReader`类似,该版本引入了`SynchronizedItemStreamWriter`。这个特性在多线程的步骤中很有用,在这些步骤中,并发线程需要同步,以避免覆盖彼此的写操作。 + +#### 用于命名查询的新 JPaqueryProvider + +这个版本在`JpaNativeQueryProvider`旁边引入了一个新的`JpaNamedQueryProvider`,以便在使用`JpaPagingItemReader`时简化 JPA 命名查询的配置: + +``` +JpaPagingItemReader reader = new JpaPagingItemReaderBuilder() + .name("fooReader") + .queryProvider(new JpaNamedQueryProvider("allFoos", Foo.class)) + // set other properties on the reader + .build(); +``` + +#### 新的 jpacursoritemreader 实现 + +JPA 2.2 增加了将结果作为游标而不是只进行分页的能力。该版本引入了一种新的 JPA 项读取器,该读取器使用此功能以类似于`JdbcCursorItemReader`和`HibernateCursorItemReader`的基于光标的方式流式传输结果。 + +#### 新 JobParametersIncrementer 实现 + +与`RunIdIncrementer`类似,这个版本添加了一个新的`JobParametersIncrementer`,它基于 Spring 框架中的`DataFieldMaxValueIncrementer`。 + +#### graalvm 支持 + +这个版本增加了在 GraalVM 上运行 Spring 批处理应用程序的初始支持。该支持仍处于实验阶段,并将在未来的版本中进行改进。 + +#### 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`的记录。 + +### 性能改进 + +#### 在 RepositorYitemWriter 中使用批量写操作 + +直到版本 4.2,为了在`RepositoryItemWriter`中使用`CrudRepository#saveAll`,需要扩展 writer 并覆盖`write(List)`。 + +在此版本中,`RepositoryItemWriter`已更新为默认使用`CrudRepository#saveAll`。 + +#### 在 MongoitemWriter 中使用批量写操作 + +`MongoItemWriter`在 for 循环中使用`MongoOperations#save()`将项保存到数据库中。在此版本中,此 Writer 已更新为使用`org.springframework.data.mongodb.core.BulkOperations`。 + +#### 作业启动/重启时间改进 + +`JobRepository#getStepExecutionCount()`的实现用于在内存中加载所有作业执行和步骤执行,以在框架端完成计数。在这个版本中,实现被更改为使用 SQL Count 查询对数据库执行一个单独的调用,以便计算执行的步骤。 + +### 依赖项更新 + +此版本将依赖 Spring 项目更新为以下版本: + +* Spring 框架 5.3 + +* Spring 数据 2020.0 + +* Spring 集成 5.4 + +* Spring AMQP2.3 + +* Spring 为 Apache 卡夫卡 2.6 + +* 千分尺 1.5 + +### 异议 + +#### 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 中找到。 + +#### SQLFire 支持弃用 + +自 2014 年 11 月 1 日起,SQLfire 一直位于[EOL](https://www.vmware.com/latam/products/pivotal-sqlfire.html)。这个版本取消了使用 SQLFire 作为作业存储库的支持,并计划在 5.0 版本中删除它。 \ No newline at end of file