此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Batch 文档 5.2.2! |
FlatFileItemReader
平面文件是最多包含二维(表格)数据的任何类型的文件。
在 Spring Batch 框架中读取平面文件是通过名为FlatFileItemReader
,它提供了读取和解析平面的基本功能
文件。两个最重要的必需依赖项FlatFileItemReader
是Resource
和LineMapper
.这LineMapper
界面将在下文中详细探讨
部分。资源属性表示 Spring CoreResource
.文档
解释如何创建这种类型的 bean,可以在 Spring 中找到
框架,第 5 章。资源。因此,本指南不详细介绍
创建Resource
对象,除了显示以下简单示例之外:
Resource resource = new FileSystemResource("resources/trades.csv");
在复杂的批处理环境中,目录结构通常由企业应用程序集成 (EAI) 管理 基础结构,其中为移动文件建立了外部接口的放置区 从 FTP 位置到批处理位置,反之亦然。文件移动实用程序 超出了 Spring Batch 架构的范围,但对于 batch 来说并不罕见 作业流,以将文件移动实用程序作为作业流中的步骤包括在内。批次 架构只需要知道如何定位要处理的文件。弹簧批次 从此起点开始将数据馈送到管道的过程。但是,Spring Integration 提供了许多 这些类型的服务。
中的其他属性FlatFileItemReader
让您进一步指定数据的状态
解释,如下表所述:
属性 | 类型 | 描述 |
---|---|---|
评论 |
字符串[] |
指定指示注释行的行前缀。 |
编码 |
字符串 |
指定要使用的文本编码。默认值为 |
线映射器 |
|
转换一个 |
行跳过 |
整数 |
文件顶部要忽略的行数。 |
记录分隔符策略 |
记录分隔符策略 |
用于确定行尾的位置 并执行诸如在带引号的字符串内以结尾的行继续等作。 |
资源 |
|
要从中读取的资源。 |
skippedLinesCallback |
行回调处理程序 |
传递原始行内容的接口
文件中要跳过的行。如果 |
严格 |
布尔 |
在严格模式下,读取器会在 |
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
interface 定义了一个方法,mapFieldSet
,它需要一个FieldSet
对象并将其内容映射到对象。此对象可以是自定义 DTO、domain 对象或数组,具体取决于作业的需要。 这FieldSetMapper
是 与LineTokenizer
将资源中的一行数据转换为所需类型的对象,如以下接口定义所示:
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
使用的模式与RowMapper
使用者JdbcTemplate
.
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
domain 对象:
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
并且在功能上类似于JDBCResultSet
. 字段的名称可以注入到其中任何一个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
和写作一样繁琐
一个特定的RowMapper
对于一个JdbcTemplate
.Spring Batch 通过提供
一个FieldSetMapper
通过将字段名称与 setter 匹配来自动映射字段
使用 JavaBean 规范在对象上。
-
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
对象(因此,prototype scope 是必需的)
与 Spring 容器查找与属性名称匹配的 setter 的方式相同。每个可用
字段中的FieldSet
被映射,结果Player
对象,没有
需要代码。
固定长度文件格式
到目前为止,只详细讨论了分隔文件。但是,它们代表 只有一半的文件阅读图片。许多使用平面文件的组织使用 长度格式。下面是一个固定长度文件示例:
UK21341EAH4121131.11customer1 UK21341EAH4221232.11customer2 UK21341EAH4321333.11customer3 UK21341EAH4421434.11customer4 UK21341EAH4521535.11customer5
虽然这看起来像一个大字段,但它实际上代表 4 个不同的字段:
-
ISIN:所订购商品的唯一标识符 - 12 个字符长。
-
数量:订购商品的编号 - 3 个字符长。
-
价格:物品价格 - 5 个字符长。
-
客户:订购商品的客户的 ID - 9 个字符。
配置FixedLengthLineTokenizer
,则必须提供这些长度中的每一个
以范围的形式。
-
Java
-
XML
以下示例演示如何定义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
在 XML:
<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;
}
以下示例演示如何定义FixedLengthLineTokenizer
在 XML:
<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
.FlatFileParseException
是
由FlatFileItemReader
当尝试读取
文件。FlatFileFormatException
是由LineTokenizer
接口,并指示在标记化时遇到的更具体的错误。
IncorrectTokenCountException
双DelimitedLineTokenizer
和FixedLengthLineTokenizer
有能力指定
可用于创建FieldSet
.但是,如果列数
names 与标记行时找到的列数不匹配,则FieldSet
无法创建,并且IncorrectTokenCountException
被抛出,其中包含
遇到的Tokens数和预期数,如以下示例所示:
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 个列名,但在
文件中,一个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
被扔掉。抛出一个
异常,而不是仅映射第一列,允许处理
行更早地失败,并且包含的信息比它失败时包含的信息更多,而它失败时
尝试在第 2 列中读取FieldSetMapper
.但是,在某些情况下,
线的长度并不总是恒定的。因此,行长度的验证可以
通过“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
现在已正确创建,并且
返回。但是,它仅包含其余值的空标记。