FlatFileItemReader

  • Resource:表示 Spring Core Resource,用于识别待处理文件的路径。

  • LineMapper:将 String 行转换为对象,允许将平面文件数据标记化为字段集和映射为域对象。

LineMapper 接口涉及两个基本任务:将行标记化为字段集和将字段集映射到域对象。Spring Batch 提供了抽象,例如 LineTokenizer(标记化行)和 FieldSetMapper(映射字段集)来执行这些任务。

默认情况下,DefaultLineMapper 使用 LineTokenizer 和 FieldSetMapper 来转换行。对于简单的分隔文件,可以使用 DelimitedLineTokenizer 和 PlayerFieldSetMapper 等预构建的实现。

对于定长文件格式,FixedLengthTokenizer 提供了标记化功能,而 BeanWrapperFieldSetMapper 可以根据 JavaBean 规范自动映射字段。

为了处理文件中具有不同格式的记录,可以配置 PatternMatchingCompositeLineMapper,它通过模式匹配将行映射到 LineTokenizers 和 FieldSetMappers。

Spring Batch 还提供异常处理功能,当在标记化行时遇到问题时会抛出异常。这些异常包括 FlatFileParseException 和 FlatFileFormatException,它们提供有关错误原因的详细信息。

平面文件是最多包含二维(表格)数据的任何类型的文件。通过称为 FlatFileItemReader 的类可以轻松地在 Spring Batch 框架中读取平面文件,该类提供了读取和解析平面文件の基本的な機能。FlatFileItemReader 的两个最重要的必需依赖项是 ResourceLineMapperLineMapper 接口将在下一部分中进行详细介绍。资源属性表示 Spring Core Resource。有关如何创建此类型的 Bean 的文档可以在 Spring Framework, Chapter 5. Resources 中找到。因此,本指南不会详细介绍创建 Resource 对象,而仅展示以下简单的示例:

Resource resource = new FileSystemResource("resources/trades.csv");

在复杂批处理环境中,目录结构通常由企业应用程序集成 (EAI) 基础设施管理,其中外部接口的放置区域用于将文件从 FTP 位置移动到批处理位置,反之亦然。文件移动工具超出了 Spring Batch 架构的范围,但批处理作业流中包含文件移动工具作为作业流中的步骤并不罕见。批处理架构只需知道如何找到要处理的文件。Spring Batch 从此开始将数据引入管道。但是, Spring Integration 提供了许多此类服务。 FlatFileItemReader 中的其他属性允许您进一步指定如何解释数据,如下表所述:

Table 1. FlatFileItemReader Properties
Property Type Description

comments

String[]

指定表示注释行的行前缀。

encoding

String

指定要使用的文本编码。默认值为 UTF-8

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> {

    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: 用于字段按分隔符分隔的文件。最常见的分隔符是逗号,但竖线或分号也可用于 as。

  • FixedLengthTokenizer: 用于字段各为 "固定宽度" 的文件。必须为每个记录类型定义每个字段的宽度。

  • PatternMatchingCompositeLineTokenizer: 通过针对模式进行检查,确定应在特定行上使用标记列表中的哪个 LineTokenizer

FieldSetMapper

FieldSetMapper 接口定义了一种方法 mapFieldSet,该方法采用一个 FieldSet 对象并将它的内容映射到一个对象。根据作业的需要,此对象可以是自定义 DTO、域对象或数组。FieldSetMapperLineTokenizer 结合使用,将资源中的数据行转换为所需类型的一个对象,如下面的接口定义所示:

public interface FieldSetMapper<T> {

    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 的输出相匹配,所以提供了使用 LineTokenizerFieldSetMapper 的默认实现。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;
    }
}

上述功能是在默认实现中提供的,而不是内置在读取器自身中(如框架的先前版本中所做的那样),以允许用户在控制解析过程中享有更大的灵活性,尤其是在需要访问原始行的​​情况下。

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"

这个文件的内容被映射到以下 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

Mapping Fields by Name

DelimitedLineTokenizerFixedLengthTokenizer 提供的其他一个功能类似于 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;
   }
}

Automapping FieldSets to Domain Objects

对于大多数人来说,必须编写一个特定的 FieldSetMapper 与编写一个特定的 RowMapper 以获得 JdbcTemplate 一样麻烦。Spring Batch 通过提供一个 FieldSetMapper 来简化此过程,该 FieldSetMapper 使用 JavaBean 规范,通过将字段名与对象上的 setter 进行匹配来自动映射字段。

Java

仍然使用足球示例,BeanWrapperFieldSetMapper 配置看上去如 Java 中的以下代码片段所示:

Java Configuration
@Bean
public FieldSetMapper fieldSetMapper() {
	BeanWrapperFieldSetMapper fieldSetMapper = new BeanWrapperFieldSetMapper();

	fieldSetMapper.setPrototypeBeanName("player");

	return fieldSetMapper;
}

@Bean
@Scope("prototype")
public Player player() {
	return new Player();
}
XML

仍然使用足球示例,BeanWrapperFieldSetMapper 配置看上去如 XML 中的以下代码片段所示:

XML Configuration
<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 对象,而不需要任何代码。

Fixed Length File Formats

到目前为止,主要详细讨论了分隔文件。然而,它们只占文件读取图的一半。许多使用平面文件的组织都使用定长格式。接下来是一个定长文件示例:

UK21341EAH4121131.11customer1
UK21341EAH4221232.11customer2
UK21341EAH4321333.11customer3
UK21341EAH4421434.11customer4
UK21341EAH4521535.11customer5

虽然这看起来像一个大字段,但实际上它表示 4 个不同的字段:

  1. ISIN:要订购的商品的唯一标识符 - 长 12 个字符。

  2. 数量:要订购的商品数量 - 长 3 个字符。

  3. 价格:商品价格 - 长 5 个字符。

  4. 客户:订购该商品的客户 ID - 长 9 个字符。

在配置 FixedLengthLineTokenizer 时,每个长度都必须以范围的形式提供。

Java

以下示例显示如何在 FixedLengthLineTokenizer 中定义范围:

Java
Java Configuration
@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

以下示例显示如何在 XML 中定义 FixedLengthLineTokenizer 的范围:

XML Configuration
<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 与使用分隔符返回的 FieldSet 相同。这允许使用相同的方法来处理它的输出,例如使用 BeanWrapperFieldSetMapper

为了支持范围的上述语法,需要在 ApplicationContext 中配置一个专门的属性编辑器 RangeArrayPropertyEditor。然而,此 bean 在使用了批命名空间的 ApplicationContext 中会自动声明。

由于 FixedLengthLineTokenizer 使用前面讨论过的相同 LineTokenizer 接口,因此它返回的 FieldSet 与使用分隔符返回的 FieldSet 相同。这允许使用相同的方法来处理它的输出,例如使用 BeanWrapperFieldSetMapper

Multiple Record Types within a Single File

到目前为止,所有的文件读取示例都对简单性做了一个关键的假设:文件中所有的记录都有相同的格式。然而,情况可能并非总是如此。一个文件非常可能包含有不同格式的记录,需要对它们进行不同的标记化和映射到不同的对象。以下文件摘录对此进行了说明:

USER;Smith;Peter;;T;20014539;F
LINEA;1044391041ABC037.49G201XX1383.12H
LINEB;2134776319DEF422.99M005LI

在这个文件中,我们有三种类型的记录,“USER”、“LINEA”和“LINEB”。一条“USER”行对应一个 User 对象。“LINEA”和“LINEB”都对应 Line 对象,尽管“LINEA”的信息比“LINEB”多。

ItemReader 单独读取每一行,但是我们必须指定不同的 LineTokenizerFieldSetMapper 对象,以便 ItemWriter 接收正确的条目。PatternMatchingCompositeLineMapper 通过允许将模式映射到 LineTokenizers 和模式映射到 FieldSetMappers 进行配置,从而简化了此过程。

Java
Java Configuration
@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

以下示例显示如何在 XML 中定义 FixedLengthLineTokenizer 的范围:

XML Configuration
<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

以下示例展示了如何在 Java 中匹配其他任何模式都未匹配的行:

Java Configuration
...
tokenizers.put("*", defaultLineTokenizer());
...
XML

以下示例展示了如何在 XML 中匹配其他任何模式都未匹配的行:

XML Configuration
<entry key="*" value-ref="defaultLineTokenizer" />

还存在可用于仅进行标记化的 PatternMatchingCompositeLineTokenizer

平面文件通常包含跨越多行的记录。若要处理这种情况,需要更复杂的策略。可在 multiLineRecords 采样中找到此常见模式的演示。

Exception Handling in Flat Files

在标记化行时,可能会引发异常的情况有很多。许多平面文件不完善,包含格式不正确的记录。许多用户选择在记录该问题、原始行和行号的同时跳过这些错误行。这些日志随后可以通过人工或其他批处理作业来检查。因此,Spring Batch 提供了一个处理解析异常的异常层次结构:FlatFileParseExceptionFlatFileFormatException。当尝试读取文件时遇到任何错误时,FlatFileItemReader 抛出 FlatFileParseExceptionFlatFileFormatExceptionLineTokenizer 接口的实现抛出,表明在标记化时遇到了更具体的错误。

IncorrectTokenCountException

DelimitedLineTokenizerFixedLengthLineTokenizer 都能够指定用于创建 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 列时失败所包含的更多信息。但是,存在行的长度并非始终恒定的情况。因此,可以通过“严格”属性关闭行长度验证,如下例所示:

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 已正确创建并返回。但是,它仅为剩余值包含空标记。