SpringBatch从入门到精通-6.1 读和写处理-实战【掘金日新计划】

327 阅读5分钟

\

持续创作,加速成长,6月更文活动来啦!| 掘金·日新计划

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

SpringBatch从入门到精通-1【掘金日新计划】

SpringBatch从入门到精通-2-StepScope作用域和用法【掘金日新计划】

SpringBatch从入门到精通-3-并行处理【掘金日新计划】

SpringBatch从入门到精通-3.2-并行处理-远程分区【掘金日新计划】

SpringBatch从入门到精通-3.3-并行处理-远程分区(消息聚合)【掘金日新计划】

SpringBatch从入门到精通-4 监控和指标【掘金日新计划】

SpringBatch从入门到精通-4.2 监控和指标-原理【掘金日新计划】

SpringBatch从入门到精通-5 数据源配置相关【掘金日新计划】

SpringBatch从入门到精通-5.2 数据源配置相关-原理【掘金日新计划】

SpringBatch从入门到精通-6 读和写处理【掘金日新计划】

简单的分隔文件读取示例

以下示例说明了如何读取具有实际场景的FlatFile。这个特定的批处理作业从以下文件中读取足球运动员:

ID、姓氏、名字、职位、出生年份、出道年份
"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,亚当斯,查理,wr,19792003

此文件的内容映射到以下 Player域对象:

public class Player implements Serializable {
​
    private String ID;
    private String lastName;
    private String firstName;
    private String position;
    private int birthYear;
    private int debutYear;
    @Override
    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,如下例所示:

public class PlayerFieldSetMapper implements FieldSetMapper<Player> {
    
    @Override
    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"});

AFieldSetMapper可以按如下方式使用此信息:

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. FieldSetMapperSpring Batch 通过使用 JavaBean 规范将字段名称与对象上的 setter 匹配来自动映射字段,从而使这变得更容易。

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

对于 中的每个条目FieldSet,映射器在对象的新实例上查找相应的设置器Player(因此,需要原型范围),就像 Spring 容器查找与属性名称匹配的设置器一样。FieldSet映射中的每个可用字段,并Player返回结果对象,无需代码。

固定长度文件格式

到目前为止,只详细讨论了分隔文件。但是,它们仅代表文件读取图片的一半。许多使用平面文件的组织使用固定长度格式。示例固定长度文件如下:

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

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

  1. ISIN:所订购商品的唯一标识符 - 12 个字符长。
  2. 数量:订购商品的数量 - 3 个字符长。
  3. 价格:商品的价格 - 5 个字符长。
  4. 客户:订购商品的客户 ID - 9 个字符长。

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

支持范围的上述语法需要 RangeArrayPropertyEditor在ApplicationContext. ApplicationContext但是,此 bean 会在使用批处理命名空间的地方自动声明。

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

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使用了与LineTokenizer上面讨论的相同的接口,所以它返回的结果与FieldSet使用了分隔符一样。这使得在处理其输出时可以使用相同的方法,例如使用 BeanWrapperFieldSetMapper.

单个文件中的多种记录类型

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

用户;史密斯;彼得;;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 配置

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

在此示例中,“LINEA”和“LINEB”具有不同LineTokenizer的实例,但它们都使用相同的FieldSetMapper.

PatternMatchingCompositeLineMapper使用该方法PatternMatcher#match为每一行选择正确的委托。允许使用两个具有特殊含义的PatternMatcher通配符:问号 ("?") 匹配一个字符,而星号 (" ") 匹配零个或多个字符。请注意,在前面的配置中,所有模式都以星号结尾,使它们有效地作为行的前缀。无论配置中的PatternMatcher顺序如何,始终匹配最具体的模式。因此,如果“LINE”和“LINEA ”都被列为模式,“LINEA”将匹配模式“LINEA”,而“LINEB”将匹配模式“LINE ”。此外,单个星号(“

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

Java 配置

...
tokenizers.put("*", defaultLineTokenizer());
...

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

FlatFile通常包含跨越多行的记录。为了处理这种情况,需要更复杂的策略。

FlatFile中的异常处理

标记一行可能会导致抛出异常的情况有很多。许多平面文件并不完美,并且包含格式不正确的记录。许多用户在记录问题、原始行和行号时选择跳过这些错误行。以后可以手动或通过另一个批处理作业检查这些日志。出于这个原因,Spring Batch 提供了一个层次结构的异常来处理解析异常: FlatFileParseException和FlatFileFormatException. 在尝试读取文件时遇到任何错误时FlatFileParseException抛出。由 接口的实现抛出并指示在标记化时遇到的更具体的错误。FlatFileItemReaderFlatFileFormatExceptionLineTokenizer

IncorrectTokenCountException

两者DelimitedLineTokenizer都FixedLengthLineTokenizer可以指定可用于创建FieldSet. 但是,如果列名的数量与标记行时找到的列数不匹配,FieldSet 则无法创建并IncorrectTokenCountException抛出 an,其中包含遇到的标记数和预期的数量,如以下示例所示:

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抛出 。在此处抛出异常而不是仅映射第一列允许该行的处理更早地失败,并且如果在尝试读取 第 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));

代码位置: github.com/jackssybin/…