个人博客


使用opencsv解析csv文件并进行ORM映射为对象集合,可以指定分隔符,主要有以下几种映射方式。

接口策略
MappingStrategy顶级接口
HeaderColumnNameMappingStrategy基于DTO属性名或注解与csv头进行映射
ColumnPositionMappingStrategy基于DTO属性数组顺序或注解指定顺序进行映射
HeaderColumnNameTranslateMappingStrategy基于csv头和DTO属性的map关系进行映射

1、Maven导包

1
2
3
4
5
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>4.6</version>
</dependency>

2、HeaderColumnNameMappingStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id,short_name,name,remark,parent_id,type_name,type_id
1,,大型汽车号牌,1.00,,号牌种类,1
2,,小型汽车号牌,2.00,,号牌种类,1
3,,使馆汽车号牌,3.50,,号牌种类,1
4,,领馆汽车号牌,,,号牌种类,1
5,,境外汽车号牌,,,号牌种类,1
6,,外籍汽车号牌,,,号牌种类,1
7,,普通摩托车号牌,,,号牌种类,1
8,,低速车号牌,,,号牌种类,1
9,,拖拉机号牌,,,号牌种类,1
10,,挂车号牌,,,号牌种类,1
11,,教练汽车号牌,,,号牌种类,1
12,,临时行驶车号牌,,,号牌种类,1
13,,警用汽车号牌,,,号牌种类,1
14,,重型普通半挂车,20,,车辆类型,2
15,,重型厢式半挂车,100,,车辆类型,2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class CarCsvDTOByName {
@CsvBindByName(column = "id")
private String id;

@CsvBindByName(column = "short_name")
private String shortName;

@CsvBindByName(column = "name")
private String name;

@CsvBindByName(column = "remark")
private String remark;

@CsvBindByName(column = "parent_id")
private String parentId;

@CsvBindByName(column = "type_name")
private String typeName;

@CsvBindByName(column = "type_id")
private String typeId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/parseByName")
public List parseByName(MultipartFile file) throws IOException {
InputStreamReader inputStream = new InputStreamReader(file.getInputStream(), CharsetUtil.CHARSET_GBK);
// 设置解析策略,csv的头和POJO属性的名称对应,也可以使用@CsvBindByName注解来指定名称
HeaderColumnNameMappingStrategy strategy = new HeaderColumnNameMappingStrategy();
strategy.setType(CarCsvDTOByName.class);

CsvToBean csvToBean = new CsvToBeanBuilder(inputStream)
.withMappingStrategy(strategy)
.build();
List carCsvDTOList = csvToBean.parse();
return carCsvDTOList;
}
  • 默认基于DTO类的属性名和csv文件第一行的header进行映射。
  • 也可以通过@CsvBindByName注解指定映射字段名。

3、ColumnPositionMappingStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1,,大型汽车号牌,1.00,,号牌种类,1
2,,小型汽车号牌,2.00,,号牌种类,1
3,,使馆汽车号牌,3.50,,号牌种类,1
4,,领馆汽车号牌,,,号牌种类,1
5,,境外汽车号牌,,,号牌种类,1
6,,外籍汽车号牌,,,号牌种类,1
7,,普通摩托车号牌,,,号牌种类,1
8,,低速车号牌,,,号牌种类,1
9,,拖拉机号牌,,,号牌种类,1
10,,挂车号牌,,,号牌种类,1
11,,教练汽车号牌,,,号牌种类,1
12,,临时行驶车号牌,,,号牌种类,1
13,,警用汽车号牌,,,号牌种类,1
14,,重型普通半挂车,20,,车辆类型,2
15,,重型厢式半挂车,100,,车辆类型,2

3.1、基于@CsvBindByPosition注解方式的DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class CarCsvDTOByPosition {
@CsvBindByPosition(position = 0)
private String id;

@CsvBindByPosition(position = 1)
private String shortName;

@CsvBindByPosition(position = 2)
private String name;

@CsvBindByPosition(position = 3)
private String remark;

@CsvBindByPosition(position = 4)
private String parentId;

@CsvBindByPosition(position = 5)
private String typeName;

@CsvBindByPosition(position = 6)
private String typeId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/parseByPosition")
public List parseByPosition(String filePath) throws IOException {
InputStreamReader inputStream = new InputStreamReader(new FileInputStream(filePath), CharsetUtil.CHARSET_GBK);
// 设置解析策略,使用@CsvBindByPosition注解可以指定字段在csv文件头中的位置,从0开始
ColumnPositionMappingStrategy strategy = new ColumnPositionMappingStrategy();
strategy.setType(CarCsvDTOByPosition.class);

CsvToBean csvToBean = new CsvToBeanBuilder(inputStream)
.withMappingStrategy(strategy)
.build();
List carCsvDTOList = csvToBean.parse();
return carCsvDTOList;
}

3.2、自定义头数组(DTO不需要注解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class CarCsvDTOByMappingArray {
private String id;

private String shortName;

private String name;

private String remark;

private String parentId;

private String typeName;

private String typeId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping("/parseByMappingArray")
public List parseByMappingArray(String filePath) throws IOException {
InputStreamReader inputStream = new InputStreamReader(new FileInputStream(filePath), CharsetUtil.CHARSET_GBK);
// 设置解析策略,csv文件不需要头,由程序指定
ColumnPositionMappingStrategy strategy = new ColumnPositionMappingStrategy();
strategy.setType(CarCsvDTOByMappingArray.class);
String headers = "id|shortName|name|remark|parentId|typeName|typeId";
String[] headerArr = headers.split("\\|");
strategy.setColumnMapping(headerArr);

CsvToBean csvToBean = new CsvToBeanBuilder(inputStream)
.withMappingStrategy(strategy)
.build();
List carCsvDTOList = csvToBean.parse();
return carCsvDTOList;
}
  • 基于@CsvBindByPosition注解指定字段位置来进行映射。
  • 也可以通过自定义头数组与csv文件内容按顺序进行映射。
  • csv文件中不需要有文件头,如果有则需要手动跳过。

4、HeaderColumnNameTranslateMappingStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id,short_name,name,remark,parent_id,type_name,type_id
1,,大型汽车号牌,1.00,,号牌种类,1
2,,小型汽车号牌,2.00,,号牌种类,1
3,,使馆汽车号牌,3.50,,号牌种类,1
4,,领馆汽车号牌,,,号牌种类,1
5,,境外汽车号牌,,,号牌种类,1
6,,外籍汽车号牌,,,号牌种类,1
7,,普通摩托车号牌,,,号牌种类,1
8,,低速车号牌,,,号牌种类,1
9,,拖拉机号牌,,,号牌种类,1
10,,挂车号牌,,,号牌种类,1
11,,教练汽车号牌,,,号牌种类,1
12,,临时行驶车号牌,,,号牌种类,1
13,,警用汽车号牌,,,号牌种类,1
14,,重型普通半挂车,20,,车辆类型,2
15,,重型厢式半挂车,100,,车辆类型,2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class CarCsvDTOByTranslate {
private String id;

private String shortName;

private String name;

private String remark;

private String parentId;

private String typeName;

private String typeId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@PostMapping("/parseByMappingByTranslate")
public List parseByMappingByTranslate(String filePath) throws IOException {
InputStreamReader inputStream = new InputStreamReader(new FileInputStream(filePath), CharsetUtil.CHARSET_GBK);
// 设置解析策略,key-csv的头、value-DTO属性
HeaderColumnNameTranslateMappingStrategy strategy = new HeaderColumnNameTranslateMappingStrategy();
strategy.setType(CarCsvDTOByTranslate.class);
Map<String, String> columnMapping = new HashMap<>();
columnMapping.put("id", "id");
columnMapping.put("short_name", "shortName");
columnMapping.put("name", "name");
columnMapping.put("remark", "remark");
columnMapping.put("parent_id", "parentId");
columnMapping.put("type_name", "typeName");
columnMapping.put("type_id", "typeId");
strategy.setColumnMapping(columnMapping);

CsvToBean csvToBean = new CsvToBeanBuilder(inputStream)
.withMappingStrategy(strategy)
.build();
List carCsvDTOList = csvToBean.parse();
return carCsvDTOList;
}

5、字段类型转换和必输项校验

下面演示将字符串转为BigDecimal类型,如果解析时id字段为空则抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id,short_name,name,remark,parent_id,type_name,type_id
1,,大型汽车号牌,1.00,,号牌种类,1
2,,小型汽车号牌,2.00,,号牌种类,1
3,,使馆汽车号牌,3.50,,号牌种类,1
4,,领馆汽车号牌,,,号牌种类,1
5,,境外汽车号牌,,,号牌种类,1
6,,外籍汽车号牌,,,号牌种类,1
7,,普通摩托车号牌,,,号牌种类,1
8,,低速车号牌,,,号牌种类,1
9,,拖拉机号牌,,,号牌种类,1
10,,挂车号牌,,,号牌种类,1
11,,教练汽车号牌,,,号牌种类,1
12,,临时行驶车号牌,,,号牌种类,1
13,,警用汽车号牌,,,号牌种类,1
14,,重型普通半挂车,20,,车辆类型,2
15,,重型厢式半挂车,100,,车辆类型,2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class CarCsvDTOConvertAndValid {
@CsvBindByName(column = "id", required = true)
private String id;

@CsvBindByName(column = "short_name")
private String shortName;

@CsvBindByName(column = "name")
private String name;

@CsvCustomBindByName(column = "remark", converter = ConvertToBigDecimal.class)
private BigDecimal remark;

@CsvBindByName(column = "parent_id")
private String parentId;

@CsvBindByName(column = "type_name")
private String typeName;

@CsvBindByName(column = "type_id")
private String typeId;
}
1
2
3
4
5
6
7
8
9
public class ConvertToBigDecimal extends AbstractBeanField {
@Override
protected Object convert(String value) {
if(StringUtils.isNotBlank(value)) {
return new BigDecimal(value);
}
return new BigDecimal(0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/convertAndValid")
public List convertAndValid(String filePath) throws IOException {
InputStreamReader inputStream = new InputStreamReader(new FileInputStream(filePath), CharsetUtil.CHARSET_GBK);
HeaderColumnNameMappingStrategy strategy = new HeaderColumnNameMappingStrategy();
strategy.setType(CarCsvDTOConvertAndValid.class);
// 校验必输项以及做类型转换
CsvToBean csvToBean = new CsvToBeanBuilder(inputStream)
.withMappingStrategy(strategy)
.build();
List carCsvDTOList = csvToBean.parse();
return carCsvDTOList;
}
  • 默认的解析规则只能支持DTO中定义简单类型字段,其它类型可以在注解中指定转换的实现类。
  • 指定转换类型的注解主要有@CsvCustomBindByName@CsvCustomBindByPosition这2种,分别对应基于字段名的映射和基于字段位置的映射。
  • 同时可以在注解中指定必输项,如果解析时为空则抛出异常。

6、自定义解析

  • 解析时跳过首部指定行数。
  • 指定分隔符。
  • 跳过特定的行不进行解析。
  • 指定必输项校验不通过抛出异常或是忽略不进行解析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
=======================跳过此行=======================
=======================跳过此行=======================
id|short_name|name|remark|parent_id|type_name|type_id
1||大型汽车号牌|1.00||号牌种类|1
2||小型汽车号牌|2.00||号牌种类|1
3||使馆汽车号牌|3.50||号牌种类|1

4||领馆汽车号牌|||号牌种类|1
5||境外汽车号牌|||号牌种类|1


6||外籍汽车号牌|||号牌种类|1
7||普通摩托车号牌|||号牌种类|1
8||低速车号牌|||号牌种类|1
9||拖拉机号牌|||号牌种类|1
10||挂车号牌|||号牌种类|1
11||教练汽车号牌|||号牌种类|1
12||临时行驶车号牌|||号牌种类|1
13||警用汽车号牌|||号牌种类|1
14||重型普通半挂车|20||车辆类型|2
15||重型厢式半挂车|100||车辆类型|2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class CarCsvDTOConvertAndValid {
@CsvBindByName(column = "id", required = true)
private String id;

@CsvBindByName(column = "short_name")
private String shortName;

@CsvBindByName(column = "name")
private String name;

@CsvCustomBindByName(column = "remark", converter = ConvertToBigDecimal.class)
private BigDecimal remark;

@CsvBindByName(column = "parent_id")
private String parentId;

@CsvBindByName(column = "type_name")
private String typeName;

@CsvBindByName(column = "type_id")
private String typeId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping("/parseBySelf")
public List parseBySelf(String filePath) throws IOException {
InputStreamReader inputStream = new InputStreamReader(new FileInputStream(filePath), CharsetUtil.CHARSET_GBK);
HeaderColumnNameMappingStrategy strategy = new HeaderColumnNameMappingStrategy();
strategy.setType(CarCsvDTOConvertAndValid.class);

CsvToBean csvToBean = new CsvToBeanBuilder(inputStream)
.withSkipLines(2) // 跳过行数
.withSeparator('|') // 分隔符
.withFilter(new SkipLineFilter())
.withThrowExceptions(false) // 如果有必输项没有,则不抛异常忽略此行
.withMappingStrategy(strategy)
.build();
List carCsvDTOList = csvToBean.parse();
return carCsvDTOList;
}

跳过特定行

1
2
3
4
5
6
7
public class SkipLineFilter implements CsvToBeanFilter {
@Override
public boolean allowLine(String[] line) {
// 首列为空的行过滤掉
return StringUtils.isNotBlank(line[0]);
}
}

参考链接

代码地址