FlatFileItemReader

平面文件是包含最多二维(表格)数据的任何类型的文件。在 Spring Batch 框架中读取平面文件由 FlatFileItemReader 类提供便利,该类提供读取和解析平面文件的基本功能。FlatFileItemReader 的两个最重要的必需依赖项是 ResourceLineMapperLineMapper 接口将在下一节中进行更多探讨。资源属性表示 Spring Core Resource。有关如何创建此类型 bean 的文档可以在 Spring Framework,第 5 章。资源中找到。因此,本指南不会深入探讨创建 Resource 对象的细节,除了展示以下简单示例

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

在复杂的批处理环境中,目录结构通常由企业应用集成 (EAI) 基础设施管理,其中为外部接口设置了接收区,用于将文件从 FTP 位置移动到批处理位置,反之亦然。文件移动实用程序超出了 Spring Batch 架构的范围,但批处理作业流将文件移动实用程序作为作业流中的步骤并不少见。批处理架构只需要知道如何定位要处理的文件。Spring Batch 从这个起点开始将数据馈送到管道中。然而,Spring Integration 提供了许多此类服务。

FlatFileItemReader 中的其他属性允许您进一步指定如何解释数据,如下表所示

表 1. FlatFileItemReader 属性
财产 类型 描述

comments

String[]

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

encoding

字符串

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

lineMapper

LineMapper

String 转换为表示项的 Object

linesToSkip

int

文件中顶部要忽略的行数。

recordSeparatorPolicy

RecordSeparatorPolicy

用于确定行尾在哪里,并执行诸如在带引号的字符串中跨行尾继续之类的操作。

resource

资源

要读取的资源。

skippedLinesCallback

LineCallbackHandler

一个接口,将文件中要跳过的行的原始行内容传递给它。如果 linesToSkip 设置为 2,则此接口将被调用两次。

strict

布尔值

在严格模式下,如果输入资源不存在,读取器会在 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:用于记录中的字段由分隔符分隔的文件。最常见的分隔符是逗号,但也经常使用管道或分号。

  • FixedLengthTokenizer:用于记录中的字段具有“固定宽度”的文件。每个记录类型都必须定义每个字段的宽度。

  • PatternMatchingCompositeLineTokenizer:通过检查模式来确定应该对特定行使用哪一个分词器。

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;
    }
}

上述功能在默认实现中提供,而不是内置到读取器本身(如框架的早期版本中所做),以允许用户在控制解析过程方面具有更大的灵活性,特别是当需要访问原始行时。

简单分隔文件读取示例

以下示例说明了如何使用实际的领域场景读取平面文件。这个特定的批处理作业从以下文件中读取足球运动员

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

按名称映射字段

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;
   }
}

自动将 FieldSet 映射到域对象

对于许多人来说,编写特定的 FieldSetMapper 与为 JdbcTemplate 编写特定的 RowMapper 一样麻烦。Spring Batch 通过提供一个 FieldSetMapper 来简化这一点,该 FieldSetMapper 使用 JavaBean 规范通过将字段名与对象上的 setter 匹配来自动映射字段。

  • Java

  • XML

再次使用足球示例,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();
}

再次使用足球示例,BeanWrapperFieldSetMapper 配置在 XML 中如下所示

XML 配置
<bean id="fieldSetMapper"
      class="org.springframework.batch.infrastructure.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 个不同的字段

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

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

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

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

在配置 FixedLengthLineTokenizer 时,必须以范围的形式提供这些长度。

  • Java

  • XML

以下示例演示了如何在 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;
}

以下示例演示了如何在 XML 中为 FixedLengthLineTokenizer 定义范围

XML 配置
<bean id="fixedLengthLineTokenizer"
      class="org.springframework.batch.infrastructure.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

支持上述范围语法要求在 ApplicationContext 中配置一个专门的属性编辑器 RangeArrayPropertyEditor。但是,当使用批处理命名空间时,此 bean 会自动在 ApplicationContext 中声明。

因为 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 单独读取每一行,但我们必须指定不同的 LineTokenizerFieldSetMapper 对象,以便 ItemWriter 接收正确的项。PatternMatchingCompositeLineMapper 通过允许配置模式到 LineTokenizers 的映射以及模式到 FieldSetMappers 的映射来简化这一点。

  • Java

  • XML

Java 配置
@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 定义范围

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 中匹配未被任何其他模式匹配的行

Java 配置
...
tokenizers.put("*", defaultLineTokenizer());
...

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

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

还有一个 PatternMatchingCompositeLineTokenizer,可以单独用于标记化。

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

平面文件中的异常处理

在对行进行标记化时,许多情况下可能会抛出异常。许多平面文件不完善,包含格式不正确的记录。许多用户选择跳过这些错误行,同时记录问题、原始行和行号。这些日志稍后可以手动检查或由另一个批处理作业检查。为此,Spring Batch 提供了一个用于处理解析异常的异常层次结构:FlatFileParseExceptionFlatFileFormatException。当尝试读取文件时遇到任何错误时,FlatFileItemReader 会抛出 FlatFileParseExceptionLineTokenizer 接口的实现会抛出 FlatFileFormatException,表示在标记化时遇到的更具体的错误。

IncorrectTokenCountException

DelimitedLineTokenizerFixedLengthTokenizer 都能够指定可用于创建 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。但是,它仅包含其余值的空标记。

© . This site is unofficial and not affiliated with VMware.