FlatFileItemReader
平面文件是任何类型包含最多二维(表格)数据的文件。在 Spring Batch 框架中读取平面文件由名为 FlatFileItemReader
的类提供便利,该类提供读取和解析平面文件的基本功能。FlatFileItemReader
的两个最重要的必需依赖项是 Resource
和 LineMapper
。LineMapper
接口将在接下来的部分中详细介绍。资源属性表示 Spring Core Resource
。有关如何创建此类型 bean 的文档可以在 Spring Framework,第 5 章。资源 中找到。因此,本指南不会深入介绍创建 Resource
对象的细节,而只是展示以下简单示例
Resource resource = new FileSystemResource("resources/trades.csv");
在复杂的批处理环境中,目录结构通常由企业应用集成 (EAI) 基础设施管理,其中为外部接口建立了放置区,用于将文件从 FTP 位置移动到批处理位置,反之亦然。文件移动实用程序超出了 Spring Batch 架构的范围,但批处理作业流中通常会包含文件移动实用程序作为作业流中的步骤。批处理架构只需要知道如何定位要处理的文件。Spring Batch 从此起点开始将数据馈送到管道中。但是,Spring Integration 提供了许多此类服务。
FlatFileItemReader
中的其他属性允许您进一步指定如何解释数据,如以下表格所述
属性 | 类型 | 描述 |
---|---|---|
注释 |
String[] |
指定指示注释行的行前缀。 |
encoding |
String |
指定要使用的文本编码。默认值为 |
lineMapper |
|
将 |
linesToSkip |
int |
要忽略的文件顶部的行数。 |
recordSeparatorPolicy |
RecordSeparatorPolicy |
用于确定行结束符的位置,并在引号字符串内执行诸如跨越行结束符的操作。 |
resource |
|
要从中读取的资源。 |
skippedLinesCallback |
LineCallbackHandler |
将文件中要跳过的行的原始行内容传递给该接口。如果 |
strict |
boolean |
在严格模式下,如果输入资源不存在,读取器会在 |
LineMapper
与 RowMapper
相似,它接受 ResultSet
等低级构造并返回 Object
,平面文件处理需要相同的构造来将 String
行转换为 Object
,如以下接口定义所示
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
基本约定是,给定当前行及其关联的行号,映射器应返回一个结果域对象。这类似于 RowMapper
,因为每行都与其行号相关联,就像 ResultSet
中的每一行都与其行号相关联一样。这允许将行号绑定到结果域对象以进行身份比较或更具信息量的日志记录。但是,与 RowMapper
不同,LineMapper
被赋予了一条原始行,如上所述,这只能让你走一半路程。该行必须被标记化为 FieldSet
,然后才能映射到对象,如本文档后面所述。
LineTokenizer
将输入行转换为FieldSet
的抽象是必要的,因为可能存在许多需要转换为FieldSet
的平面文件数据格式。在Spring Batch中,此接口为LineTokenizer
public interface LineTokenizer {
FieldSet tokenize(String line);
}
LineTokenizer
的约定是,给定一行输入(理论上,String
可以包含多行),返回表示该行的FieldSet
。然后,可以将此FieldSet
传递给FieldSetMapper
。Spring Batch包含以下LineTokenizer
实现
-
DelimitedLineTokenizer
:用于记录中的字段由分隔符分隔的文件。最常见的分隔符是逗号,但管道或分号也经常使用。 -
FixedLengthTokenizer
:用于记录中的字段都是“固定宽度”的文件。必须为每种记录类型定义每个字段的宽度。 -
PatternMatchingCompositeLineTokenizer
:通过检查模式,确定应在特定行上使用一组标记器中的哪个LineTokenizer
。
FieldSetMapper
FieldSetMapper
接口定义了一个单一方法mapFieldSet
,该方法接受一个FieldSet
对象,并将它的内容映射到一个对象。此对象可以是自定义DTO、域对象或数组,具体取决于作业的需求。FieldSetMapper
与LineTokenizer
一起使用,将资源中的一行数据转换为所需类型的对象,如下面的接口定义所示
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
使用的模式与JdbcTemplate
使用的RowMapper
相同。
DefaultLineMapper
现在,已经定义了用于读取平面文件的基本接口,很明显需要三个基本步骤
-
从文件中读取一行。
-
将
String
行传递到LineTokenizer#tokenize()
方法以检索FieldSet
。 -
将从标记化返回的
FieldSet
传递给FieldSetMapper
,返回ItemReader#read()
方法的结果。
上面描述的两个接口代表了两个独立的任务:将一行转换为FieldSet
,并将FieldSet
映射到域对象。因为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;
}
}
上述功能在默认实现中提供,而不是内置于读取器本身(如框架的先前版本中所做的那样),以允许用户在控制解析过程方面具有更大的灵活性,尤其是在需要访问原始行时。
简单分隔文件读取示例
以下示例说明了如何使用实际的域场景读取平面文件。此特定批处理作业从以下文件读取足球运动员
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<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
。
按名称映射字段
DelimitedLineTokenizer
和FixedLengthTokenizer
都允许使用一项额外的功能,该功能在功能上类似于JDBC ResultSet
。可以将字段的名称注入到这两个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;
}
}
自动将FieldSet映射到域对象
对于许多人来说,必须编写特定的FieldSetMapper
与为JdbcTemplate
编写特定的RowMapper
一样繁琐。Spring Batch通过提供一个FieldSetMapper
来简化此操作,该FieldSetMapper
通过使用 JavaBean 规范将字段名称与对象上的 setter 进行匹配来自动映射字段。
-
Java
-
XML
再次使用足球示例,BeanWrapperFieldSetMapper
配置在 Java 中看起来像以下代码段
@Bean
public FieldSetMapper fieldSetMapper() {
BeanWrapperFieldSetMapper fieldSetMapper = new BeanWrapperFieldSetMapper();
fieldSetMapper.setPrototypeBeanName("player");
return fieldSetMapper;
}
@Bean
@Scope("prototype")
public Player player() {
return new Player();
}
再次使用足球示例,BeanWrapperFieldSetMapper
配置在 XML 中看起来像以下代码段
<bean id="fieldSetMapper"
class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="player" />
</bean>
<bean id="player"
class="org.springframework.batch.samples.domain.Player"
scope="prototype" />
对于FieldSet
中的每个条目,映射器都会在Player
对象的新实例(为此,需要原型作用域)上查找相应的 setter,就像 Spring 容器查找与属性名称匹配的 setter 一样。FieldSet
中的每个可用字段都会被映射,并且会返回生成的Player
对象,无需任何代码。
固定长度文件格式
到目前为止,我们已经详细讨论了分隔符文件。但是,它们只代表了文件读取的一半。许多使用平面文件的组织使用固定长度格式。以下是一个固定长度文件的示例
UK21341EAH4121131.11customer1 UK21341EAH4221232.11customer2 UK21341EAH4321333.11customer3 UK21341EAH4421434.11customer4 UK21341EAH4521535.11customer5
虽然这看起来像一个大字段,但它实际上代表了 4 个不同的字段
-
ISIN:订购商品的唯一标识符 - 长度为 12 个字符。
-
数量:订购商品的数量 - 长度为 3 个字符。
-
价格:商品的价格 - 长度为 5 个字符。
-
客户:订购商品的客户 ID - 长度为 9 个字符。
在配置 FixedLengthLineTokenizer
时,必须以范围的形式提供每个长度。
-
Java
-
XML
以下示例展示了如何在 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;
}
以下示例展示了如何在 XML 中为 FixedLengthLineTokenizer
定义范围
<bean id="fixedLengthLineTokenizer"
class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
<property name="names" value="ISIN,Quantity,Price,Customer" />
<property name="columns" value="1-12, 13-15, 16-20, 21-29" />
</bean>
由于 FixedLengthLineTokenizer
使用与之前讨论的相同的 LineTokenizer
接口,因此它返回与使用分隔符时相同的 FieldSet
。这允许使用相同的方法来处理其输出,例如使用 BeanWrapperFieldSetMapper
。
支持上述范围语法需要在 |
由于 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
来简化此过程。
-
Java
-
XML
@Bean
public PatternMatchingCompositeLineMapper orderFileLineMapper() {
PatternMatchingCompositeLineMapper lineMapper =
new PatternMatchingCompositeLineMapper();
Map<String, LineTokenizer> tokenizers = new HashMap<>(3);
tokenizers.put("USER*", userTokenizer());
tokenizers.put("LINEA*", lineATokenizer());
tokenizers.put("LINEB*", lineBTokenizer());
lineMapper.setTokenizers(tokenizers);
Map<String, FieldSetMapper> mappers = new HashMap<>(2);
mappers.put("USER*", userFieldSetMapper());
mappers.put("LINE*", lineFieldSetMapper());
lineMapper.setFieldSetMappers(mappers);
return lineMapper;
}
以下示例展示了如何在 XML 中为 FixedLengthLineTokenizer
定义范围
<bean id="orderFileLineMapper"
class="org.spr...PatternMatchingCompositeLineMapper">
<property name="tokenizers">
<map>
<entry key="USER*" value-ref="userTokenizer" />
<entry key="LINEA*" value-ref="lineATokenizer" />
<entry key="LINEB*" value-ref="lineBTokenizer" />
</map>
</property>
<property name="fieldSetMappers">
<map>
<entry key="USER*" value-ref="userFieldSetMapper" />
<entry key="LINE*" value-ref="lineFieldSetMapper" />
</map>
</property>
</bean>
在本例中,“LINEA” 和 “LINEB” 拥有独立的 LineTokenizer
实例,但它们都使用相同的 FieldSetMapper
。
PatternMatchingCompositeLineMapper
使用 PatternMatcher#match
方法来选择每个行的正确委托。PatternMatcher
允许使用两个具有特殊含义的通配符:问号(“?”)匹配一个字符,而星号(“*”)匹配零个或多个字符。请注意,在前面的配置中,所有模式都以星号结尾,使其实际上成为行的前缀。PatternMatcher
始终匹配最具体的模式,而与配置中的顺序无关。因此,如果 “LINE*” 和 “LINEA*” 都被列为模式,则 “LINEA” 将匹配模式 “LINEA*”,而 “LINEB” 将匹配模式 “LINE*”。此外,单个星号(“*”)可以作为默认值,通过匹配任何未被其他模式匹配的行来实现。
-
Java
-
XML
以下示例展示了如何在 Java 中匹配未被其他模式匹配的行
...
tokenizers.put("*", defaultLineTokenizer());
...
以下示例展示了如何在 XML 中匹配未被其他模式匹配的行
<entry key="*" value-ref="defaultLineTokenizer" />
还有一个 PatternMatchingCompositeLineTokenizer
可用于单独进行标记化。
平面文件通常包含跨越多行的记录。为了处理这种情况,需要更复杂的策略。multiLineRecords
示例中展示了这种常见模式。
平面文件中的异常处理
在对行进行标记化时,可能会出现许多导致异常抛出的情况。许多平面文件并不完美,包含格式错误的记录。许多用户选择跳过这些错误行,同时记录问题、原始行和行号。这些日志可以稍后手动检查或由另一个批处理作业检查。为此,Spring Batch 提供了用于处理解析异常的异常层次结构:FlatFileParseException
和 FlatFileFormatException
。当尝试读取文件时遇到任何错误时,FlatFileItemReader
会抛出 FlatFileParseException
。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-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));
前面的示例与之前的示例几乎相同,只是调用了 tokenizer.setStrict(false)
。此设置告诉标记器在标记行时不要强制执行行长度。现在正确创建并返回了 FieldSet
。但是,它只包含剩余值的空标记。