1、分类
检查文件是否存在
判断日期是否是工作日(或者其他约定)
ftp拉文件(复制文件)
删除文件(有就删除)
读取文件
写文件
写数据库
读取数据库
统计后写数据库
校验数据是否存在
2、翻译
https://docs.spring.io/spring-batch/docs/4.3.4/reference/html/readersAndWriters.html#readersAndWriters
所有批处理都可以用其最简单的形式描述为读取大量数据,执行某种类型的计算或转换,然后写出结果。Spring Batch提供了三个关键接口来帮助执行批量读写:ItemReader、ItemProcessor和ItemWriter。
2.1、 ItemReader
尽管这是一个简单的概念,但ItemReader是从许多不同类型的输入中提供数据的手段。最普遍的例子包括:
-
Flat File :平面文件项读取器从平面文件中读取数据行,该平面文件通常描述由文件中的固定位置定义或由某些特殊字符(如逗号)分隔的数据字段的记录。
-
XML:XMLItemReaders独立于用于解析、映射和验证对象的技术来处理XML。输入数据允许根据XSD模式验证XML文件。
-
db :访问数据库资源以返回可映射到对象进行处理的结果集。默认的SQL ItemReader实现调用行映射器来返回对象,如果需要重新启动,则跟踪当前行,存储基本统计信息,并提供一些事务增强功能,这些功能将在后面解释。
还有更多的可能性,但我们将重点放在本章的基本可能性上。所有可用ItemReader实现的完整列表可在附录A中找到。
ItemReader是通用输入操作的基本接口,如以下接口定义所示:
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;
}
read方法定义了ItemReader最基本定义。调用它会返回一个item 或者null。item可以表示文件中的一行、数据库中的一行或XML文件中的一个元素。通常预期这些映射到可用的域对象(如Trade、Foo或其他),但合同中没有这样做的要求。
预计ItemReader接口的实现只能向前。但是,如果底层资源是事务性的(例如JMS队列),那么在回滚场景中,调用read可能会在后续调用中返回相同的逻辑项。还值得注意的是,ItemReader缺少要处理的项不会导致抛出异常。例如,配置了返回0结果的查询的数据库ItemReader在第一次调用read时返回null。
2.2、ItemWriter
ItemWriter在功能上与ItemReader类似,但具有相反的操作。资源仍然需要定位、打开和关闭,但它们的不同之处在于,ItemWriter是写出来的,而不是读入。对于数据库或队列,这些操作可能是插入、更新或发送。输出序列化的格式特定于每个批处理作业。
与ItemReader一样,ItemWriter是一个相当通用的接口,如以下接口定义所示:
public interface ItemWriter<T> {
void write(List<? extends T> items) throws Exception;
}
与ItemReader的read一样,write提供了ItemWriter的基本接口。只要打开,它就会尝试写出传入的项目列表。由于通常预期项目会被“批处理”成一个块,然后输出,因此接口接受一个项目列表,而不是一个项目本身。写出列表后,可以在从write方法返回之前执行任何必要的刷新。例如,如果向Hibernate DAO写入,则可以进行多个写入调用,每个项一个。然后,编写器可以在返回之前调用hibernate会话的flush。
2.3 ItemStream
ItemReader和ItemWriter都能很好地满足各自的需求,但它们都有一个共同的问题,那就是需要另一个接口。一般来说,作为批处理作业范围的一部分,需要打开、关闭和修改,并需要一种保持状态的机制。ItemStream接口就是为了达到这个目的,如下例所示:
public interface ItemStream {
void open(ExecutionContext executionContext) throws ItemStreamException;
void update(ExecutionContext executionContext) throws ItemStreamException;
void close() throws ItemStreamException;
}
2.4 授权和注册 with the Step
请注意,CompositeItemWriter是委托模式的一个示例,这在Spring batch处理中很常见。委托本身可能实现回调接口,例如StepListener。如果它们确实存在,并且作为作业步骤的一部分与Spring Batch Core一起使用,那么几乎肯定需要在该步骤中手动注册它们。如果直接连接到Step的读卡器、写卡器或处理器实现了ItemStream或StepListener接口,那么它会自动注册。但是,由于该步骤不知道代理,因此需要将它们作为侦听器或流(如果合适,也可以同时作为侦听器和流)注入。
@Bean
public Job ioSampleJob() {
return this.jobBuilderFactory.get("ioSampleJob")
.start(step1())
.build();
}
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>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();
}
3.1 Flat Files
交换大容量数据的最常见机制之一一直是Flat Files。与XML不同,XML有一个公认的定义其结构的标准(XSD),任何阅读Flat Files的人都必须提前准确理解文件的结构。通常,所有Flat Files分为两种类型:分隔文件和固定长度文件。分隔文件是指字段由分隔符(如逗号)分隔的文件。固定长度的文件具有设定长度的字段。
- The FieldSet
对于Spring batch来说,输入和输出Flat Files都是重要部分。许多体系结构和库都包含帮助您读入文件的方法,但通常返回字符串或字符串对象数组。这只会让你走到一半。FieldSet是Spring Batch的抽象,用于启用文件资源中字段的绑定。它允许开发人员以与处理数据库输入大致相同的方式处理文件输入。字段集在概念上类似于JDBC ResultSet。字段集只需要一个参数:字符串array 。还可以配置字段的名称,以便可以通过索引或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的最大优点是,它提供了对文件输入的一致性解析。与每个批处理作业以可能意外的方式进行不同的解析不同,它可以是一致的,无论是在处理由格式异常引起的错误时,还是在执行简单的数据转换时。
3.2 FlatFileItemReader
flat file 最多包含二维(表格)数据。在Spring batch 处理框架中读取flat files是通过名为FlatFileItemReader的类来实现的,该类提供了读取和解析文件的基本功能。FlatFileItemReader最重要的两个必需依赖项是Resource和LineMapper。LineMapper界面将在接下来的部分中详细介绍。resource属性表示Spring核心资源。如何创建这种类型bean的文档可以在Spring框架的第5章中找到。因此,本指南除了展示以下简单示例外,不会深入介绍创建资源对象的细节:
Resource resource = new FileSystemResource("resources/trades.csv");
在复杂的batch环境中,目录结构通常由企业应用程序集成(Enterprise Application Integration,EAI)基础设施管理,其中为外部接口建立了放置区,用于将文件从FTP位置移动到批处理位置,反之亦然。文件移动超出了Spring批处理体系结构的范围,但batch处理作业流将文件移动实用程序作为步骤包含在作业流中并不罕见。批处理体系结构只需要知道如何定位要处理的文件。Spring Batch从此起点开始将数据输入管道的过程。然而,Spring集成提供了许多此类服务。
FlatFileItemReader中的其他属性允许您进一步指定数据的解释方式,如下表所述:
属性 | 类型 | 解释 |
---|---|---|
comments | String[] | 指定行前缀 |
encoding | String | 指定字符集. 默认 Charset.defaultCharset(). |
lineMapper | LineMapper | 将字符串转换为表示该项的对象。 |
linesToSkip | int | 顶部要忽略的行数。 |
recordSeparatorPolicy | RecordSeparatorPolicy | 用于确定行尾的位置,如果在带引号的字符串中,则可以在行尾上继续. |
resource | Resource | 要从中读取的资源、 |
skippedLinesCallback | LineCallbackHandler | 该接口传递文件中要跳过的行的原始行内容。如果linesToSkip设置为2,则调用该接口两次. |
strict | boolean | 在严格模式下,如果输入资源不存在,读取器会在ExecutionContext上引发异常。否则,它会记录问题并继续 |
3.2.1 LineMapper
与RowMapper一样,它采用一个低级构造(如ResultSet)并返回一个对象,文件处理需要相同的构造将字符串行转换为对象,如以下接口定义所示:
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
基本约定是,给定当前行及其关联的行号,映射程序应该返回结果域对象。这与RowMapper类似,因为每一行都与其行号关联,就像结果集中的每一行都与其行号关联一样。这允许将行号绑定到生成的域对象,以进行身份比较或进行更详细的日志记录。然而,与RowMapper不同的是,LineMapper提供了一条原始线,如上所述,这只会让您到达一半。该行必须标记为一个字段集,然后可以映射到一个对象,如本文档后面所述。
3.2.2 LineTokenizer
将输入行转换为字段集的抽象是必要的,因为可能有许多格式的平面文件数据需要转换为字段集。在Spring Batch中,此接口是LineTokenizer:
public interface LineTokenizer {
FieldSet tokenize(String line);
}
LineTokenizer的约定是,给定一行输入(理论上,字符串可以包含多行),将返回表示该行的字段集。然后可以将此字段集传递给FieldSetMapper。Spring批处理包含以下LineTokenizer实现:
- DelimitedLineTokenizer:用于记录中的字段由分隔符分隔的文件。最常见的分隔符是逗号,但也经常使用管道或分号。
- FixedLengthTokenizer:用于记录中的字段均为“固定宽度”的文件。必须为每种记录类型定义每个字段的宽度。
- PatternMatchingCompositeLineTokenizer:通过检查模式,确定在特定行上应使用标记器列表中的哪一行标记器。
3.2.3 FieldSetMapper
FieldSetMapper接口定义了一个方法mapFieldSet,它接受一个FieldSet对象并将其内容映射到一个对象。根据作业的需要,此对象可以是自定义DTO、域对象或数组。FieldSetMapper与LineTokenizer结合使用,将资源中的一行数据转换为所需类型的对象,如以下接口定义所示:
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
使用的模式与JdbcTemplate使用的行映射器相同。
3.2.4 DefaultLineMapper
既然已经定义了在平面文件中读取的基本接口,那么显然需要三个基本步骤:
1、从文件中读一行。
2、将字符串行传递到LineTokenizer#tokenize()方法中以检索字段集。
3、将标记化返回的字段集传递给FieldSetMapper,并从ItemReader#read()方法返回结果。
上述两个接口代表两个独立的任务:将一行转换为字段集,并将字段集映射到域对象。由于LineTokenizer的输入与LineMapper的输入(一行)匹配,FieldSetMapper的输出与LineMapper的输出匹配,因此提供了同时使用LineTokenizer和FieldSetMapper的默认实现。DefaultLineMapper表示大多数用户需要的行为,如以下类定义所示:
public class DefaultLineMapper<T> implements LineMapper<>, InitializingBean {
private LineTokenizer tokenizer;
private FieldSetMapper<T> 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<T> fieldSetMapper) {
this.fieldSetMapper = fieldSetMapper;
}
}
上述功能是在默认实现中提供的,而不是内置在读卡器本身中(就像在以前版本的框架中所做的那样),以允许用户更灵活地控制解析过程,尤其是在需要访问原始行的情况下。
3.2.5 Simple Delimited File Reading Example
下面的示例演示了如何在实际的域场景中读取平面文件。此特定批处理作业从以下文件读取足球运动员:
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"
此文件的内容映射到以下玩家域对象:
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...
}
要将字段集映射到玩家对象,需要定义返回玩家的字段集映射器,如下例所示:
protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> {
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<Player> itemReader = new FlatFileItemReader<>();
itemReader.setResource(new FileSystemResource("resources/players.csv"));
DefaultLineMapper<Player> 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。
3.2.6 Mapping Fields by Name
DelimitedLineTokenizer和FixedLengthTokenizer都允许另外一项功能,它的功能类似于JDBC结果集。这些字段的名称可以注入这些LineTokenizer实现中的任何一个,以增加映射函数的可读性。首先,平面文件中所有字段的列名都被注入到标记器中,如下例所示:
tokenizer.setNames(new String[] {"ID", "lastName", "firstName", "position", "birthYear", "debutYear"});
FieldSetMapper可以使用以下信息:
public class PlayerMapper implements FieldSetMapper<Player> {
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;
}
}
3.2.7 自动将字段集映射到域对象
对于许多人来说,必须编写特定的FieldSetMapper与为JdbcTemplate编写特定的行映射器一样麻烦。Spring Batch提供了一个FieldSetMapper,通过使用JavaBean规范将字段名与对象上的setter匹配来自动映射字段,从而简化了这一过程。
再次使用football示例,BeanwrapPerfiedSetMapper配置在Java中类似于以下代码片段:
@Bean
public FieldSetMapper fieldSetMapper() {
BeanWrapperFieldSetMapper fieldSetMapper = new BeanWrapperFieldSetMapper();
fieldSetMapper.setPrototypeBeanName("player");
return fieldSetMapper;
}
@Bean
@Scope("prototype")
public Player player() {
return new Player();
}
对于字段集中的每个条目,映射器在播放器对象的新实例上查找相应的setter(因此,需要prototype作用域),就像Spring容器查找与属性名称匹配的setter一样。字段集中的每个可用字段都被映射,并返回结果播放器对象,无需任何代码。
3.2.8 Fixed Length File Formats
到目前为止,只对分隔文件进行了详细讨论。然而,它们只代表文件读取图片的一半。许多使用平面文件的组织使用固定长度的格式。固定长度文件的示例如下:
UK21341EAH4121131.11customer1
UK21341EAH4221232.11customer2
UK21341EAH4321333.11customer3
UK21341EAH4421434.11customer4
UK21341EAH4521535.11customer5
虽然这看起来像一个大字段,但实际上它代表4个不同的字段:
ISIN:所订购商品的唯一标识符,长度为12个字符。
数量:正在订购的商品的数量-3个字符长。
价格:物品的价格-5个字符长。
客户:订购商品的客户ID-9个字符长。
配置FixedLengthLineTokenizer时,必须以范围的形式提供每个长度。
要支持上述范围语法,需要在ApplicationContext中配置专门的属性编辑器RangeArrayPropertyEditor。但是,这个bean会在使用批处理名称空间的ApplicationContext中自动声明。
以下示例显示了如何在Java中定义FixedLengthLineTokenizer的范围:
@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接口与上面讨论的相同,因此它返回相同的字段集,就像使用了分隔符一样。这允许在处理其输出时使用相同的方法,例如使用BeanWrapperFieldSetMapper。
3.2.9 Multiple Record Types within a Single File
略
3.2.10 Exception Handling in Flat Files
在很多情况下,标记一行可能会导致抛出异常。许多平面文件不完善,包含格式不正确的记录。许多用户选择在记录问题、原始行和行号时跳过这些错误行。这些日志稍后可以手动检查或由其他批处理作业检查。因此,Spring Batch为处理解析异常提供了一个异常层次结构:FlatFileParseException和FlatFileFormatException。FlatFileParseException在尝试读取文件时遇到任何错误时由FlatFileItemReader引发。FlatFileFormatException由LineTokenizer接口的实现引发,指示标记化时遇到的更具体的错误。
- IncorrectTokenCountException
DelimitedLineTokenizer和FixedLengthLineTokenizer都能够指定可用于创建字段集的列名。但是,如果列名的数量与标记行时找到的列的数量不匹配,则无法创建字段集,并引发一个不正确的TokenCountException,其中包含遇到的标记数量和预期的数量,如以下示例所示:
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个标记,因此引发了一个不正确的TokenCountException。
- IncorrectLineLengthException
在解析时,以固定长度格式格式化的文件有额外的要求,因为与分隔格式不同,每一列必须严格遵守其预定义的宽度。如果总行长不等于此列的最宽值,则会引发异常,如下例所示:
tokenizer.setColumns(new Range[] { new Range(1, 5),
new Range(6, 10),
new Range(11, 15) });
try {
tokenizer.tokenize("12345");
fail("Expected IncorrectLineLengthException");
}
catch (IncorrectLineLengthException ex) {
assertEquals(15, ex.getExpectedLength());
assertEquals(5, ex.getActualLength());
}
上述标记器的配置范围为:1-5、6-10和11-15。因此,该线路的总长度为15。然而,在前面的示例中,传入了一条长度为5的行,导致抛出一个IncorrectLineLengthException。在此处引发异常,而不是仅映射第一列,可以使行的处理更早失败,并且比在FieldSetMapper中尝试读取第2列时失败时包含的信息更多。然而,在某些情况下,线的长度并不总是恒定的。因此,可以通过“strict”属性关闭线长度验证,如下例所示:
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));
前面的示例与前面的示例几乎相同,只是标记器不同。调用了setStrict(false)。此设置告诉标记器在标记行时不要强制执行行长度。现在已正确创建并返回字段集。但是,它只包含剩余值的空标记。
3.3、FlatFileItemWriter
向FlatFile中写入与从文件中读入具有相同的问题。一个步骤必须能够以事务方式写入分隔或固定长度的格式。
3.3.1 LineAggregator
正如LineTokenizer接口是获取项目并将其转换为字符串所必需的,文件写入必须能够将多个字段聚合为单个字符串,以便写入文件。在Spring Batch中,这是LineAggregator,如以下接口定义所示:
public interface LineAggregator<T> {
public String aggregate(T item);
}
LineAggregator与LineTokenizer相反。LineTokenizer获取字符串并返回字段集,而LineAggregator获取项并返回字符串。
- PassThroughLineAggregator
LineAggregator接口最基本的实现是PassThroughLineAggregator,它假定对象已经是字符串,或者其字符串表示形式可以写入,如以下代码所示:
public class PassThroughLineAggregator<T> implements LineAggregator<T> {
public String aggregate(T item) {
return item.toString();
}
}
如果需要直接控制字符串的创建,但需要FlatFileItemWriter的优点,例如事务和重启支持,那么前面的实现非常有用。
3.3.2 Simplified File Writing Example
现在已经定义了LineAggregator接口及其最基本的实现PassThroughLineAggregator,可以解释基本的编写流程:
将要写入的对象传递给LineAggregator以获取字符串。
返回的字符串将写入配置的文件。
以下摘录自FlatFileItemWriter的代码表达了这一点:
public void write(T item) throws Exception {
write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
}
配置示例
@Bean
public FlatFileItemWriter itemWriter() {
return new FlatFileItemWriterBuilder<Foo>()
.name("itemWriter")
.resource(new FileSystemResource("target/test-outputs/output.txt"))
.lineAggregator(new PassThroughLineAggregator<>())
.build();
}
3.3.3 FieldExtractor
前面的示例对于写入文件的最基本用途可能很有用。然而,FlatFileItemWriter的大多数用户都有一个域对象需要写出,因此必须转换成一行。在文件读取中,需要执行以下操作:
- 1、从文件中读一行。
- 2、将该行传递到LineTokenizer#tokenize()方法中,以检索字段集。
- 3、将标记化返回的字段集传递给FieldSetMapper,并从ItemReader#read()方法返回结果。
文件写入有相似但相反的步骤:
- 1、将要写入的项目传递给作者。
- 2、将项目上的字段转换为数组。
- 3、将结果数组聚合成一行。
由于框架无法知道需要从对象中写出哪些字段,因此必须编写FieldExtractor来完成将项转换为数组的任务,如以下接口定义所示:
public interface FieldExtractor<T> {
Object[] extract(T item);
}
FieldExtractor接口的实现应该从所提供对象的字段中创建一个数组,然后可以在元素之间使用分隔符或作为固定宽度线的一部分写出该数组。
-
PassThroughFieldExtractor
在许多情况下,需要写出集合,例如array, Collection,FieldSet。从其中一种集合类型中“提取”数组非常简单。为此,请将集合转换为数组。因此,在这个场景中应该使用passthroughfielddextractor。应该注意的是,如果传入的对象不是集合类型,那么passthroughfielddextractor将返回一个仅包含要提取的项的数组。 -
BeanWrapperFieldExtractor
与文件读取部分中描述的BeanwrapPerfiedSetMapper一样,通常最好配置如何将域对象转换为对象数组,而不是自己编写转换。BeanWrapperFieldExtractor提供此功能,如以下示例所示:
BeanWrapperFieldExtractor<Name> 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]);
这个提取器实现只有一个必需的属性:要映射的字段的名称。正如BeanRapperFieldSetMapper需要字段名将字段集上的字段映射到所提供对象上的setter一样,BeanRapperFieldDextractor也需要名称映射到getter以创建对象数组。值得注意的是,名称的顺序决定了数组中字段的顺序。
3.3.4、 Delimited File Writing Example
最基本的平面文件格式是所有字段用分隔符分隔的格式。这可以使用DelimitedLineAggregator实现。下面的示例写出了一个简单的域对象,它表示客户帐户的信用:
public class CustomerCredit {
private int id;
private String name;
private BigDecimal credit;
//getters and setters removed for clarity
}
使用domain object必须提供FieldExtractor接口的实现,以及要使用的分隔符。
以下示例使用带delimiter的FieldExtractor:
@Bean
public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
BeanWrapperFieldExtractor<CustomerCredit> fieldExtractor = new BeanWrapperFieldExtractor<>();
fieldExtractor.setNames(new String[] {"name", "credit"});
fieldExtractor.afterPropertiesSet();
DelimitedLineAggregator<CustomerCredit> lineAggregator = new DelimitedLineAggregator<>();
lineAggregator.setDelimiter(",");
lineAggregator.setFieldExtractor(fieldExtractor);
return new FlatFileItemWriterBuilder<CustomerCredit>()
.name("customerCreditWriter")
.resource(outputResource)
.lineAggregator(lineAggregator)
.build();
}
在这个示例中,本章前面介绍的BeanWrapperFieldExtractor用于将CustomerCredit中的name和credit字段转换为对象数组,然后在每个字段之间用逗号写出。
也可以使用FlatFileItemWriterBuilder。DelimitedBuilder自动创建BeanWrapperFieldExtractor和DelimitedLineAggregator,如下例所示:
@Bean
public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
return new FlatFileItemWriterBuilder<CustomerCredit>()
.name("customerCreditWriter")
.resource(outputResource)
.delimited()
.delimiter("|")
.names(new String[] {"name", "credit"})
.build();
}
3.3.5 Fixed Width File Writing Example
略
3.3.6 Handling File Creation 处理文件创建
FlatFileItemReader与文件资源的关系非常简单。
当reader初始化时,它打开文件(如果存在),如果不存在,则抛出异常。
文件编写并不是那么简单。乍一看,FlatFileItemWriter似乎也应该有一个类似的流程:
如果文件已经存在,抛出一个异常,
如果不存在,则创建它并开始编写。
但是,重新启动作业可能会导致问题。
在正常重启场景中,流程是反向的:
如果文件存在,从最后一个已知的正确位置开始写入,如果不存在,则抛出异常。
但是,如果此作业的文件名始终相同,会发生什么情况?
在这种情况下,如果文件存在,您可能希望删除它,除非是重新启动。
由于这种可能性,FlatFileItemWriter包含属性shouldDeleteIfExists。将此属性设置为true会导致在打开编写器时删除同名的现有文件。
4、 XML Item Readers and Writers 略
5、 JSON Item Readers And Writers
6、 Multi-File Input
7、 Database
8、 Reusing Existing Services 复用已存在services
9、 Preventing State Persistence 预防状态保持
10、 Creating Custom ItemReaders and ItemWriters
创建自定义的ItemReaders和ItemWriters
到目前为止,本章已经讨论了Spring Batch中读写的基本契约以及一些常见的实现。然而,这些都是相当通用的,并且有许多潜在的场景可能不在开箱即用的实现中。本节通过一个简单的示例展示如何创建自定义ItemReader和ItemWriter实现,并正确实现它们的契约。ItemReader还实现ItemStream,以说明如何使读卡器或写卡器可重启。
10.1 Custom ItemReader Example
在本例中,我们创建了一个简单的ItemReader实现,它从提供的列表中读取数据。我们首先实现ItemReader最基本的契约,read方法,如下代码所示:
public class CustomItemReader<T> implements ItemReader<T> {
List<T> items;
public CustomItemReader(List<T> items) {
this.items = items;
}
public T read() throws Exception, UnexpectedInputException,
NonTransientResourceException, ParseException {
if (!items.isEmpty()) {
return items.remove(0);
}
return null;
}
}
前面的类获取一个item列表,并一次返回一个item,从列表中删除每个item。当list为空时,它返回null,从而满足ItemReader的最基本要求,如以下测试代码所示:
List<String> 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());
- Making the ItemReader Restartable 重启
最后一个挑战是使ItemReader可重启。目前,如果处理被中断并再次开始,ItemReader必须从头开始。这实际上在许多情况下都是有效的,但有时最好在批处理作业停止的地方重新启动。关键的区别通常是读者是有状态的还是无状态的。无状态读卡器不需要担心可重启性,但有状态读卡器必须在重启时重新构建其最后一个已知状态。因此,如果可能的话,我们建议您保持自定义读卡器无状态,这样您就不必担心可重启性。
如果确实需要存储状态,则应使用ItemStream接口:
public class CustomItemReader<T> implements ItemReader<T>, ItemStream {
List<T> items;
int currentIndex = 0;
private static final String CURRENT_INDEX = "current.index";
public CustomItemReader(List<T> 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 {}
}
10.2
11、Item Reader and Writer Implementations
Decorators 装饰器