如何优雅地将CSV数据读取至Immutable对象?
如何将CSV数据直接映射到Immutable对象?
问题场景
我基于CSV数据定义了一个Immutable视图:
@Value.Style(stagedBuilder = true) @Value.Immutable public interface MyImmutablePerson { String getName(); Optional<Integer> getAge(); Optional<String> getAddress(); }
对应的CSV数据如下:
name,age,address John,10,123 Main St Jack,11,123 Main St Jake,12,123 Main St
目前只能手动拆分CSV行数据并构建对象:
String[] record = reader.readLine().split(','); ImmutableMyImmutablePerson.builder() .name(record[0]) .age(record[1]) .address(record[2]) .build();
但实际场景存在大量列,手动构建过于繁琐,希望找到能直接将CSV行映射到Immutable对象的CSV库。
可行方案
1. Jackson CSV 数据绑定(最简洁)
Jackson支持CSV格式的序列化与反序列化,只需给Immutable接口添加@JsonDeserialize注解指定Builder类,即可自动完成映射,还能处理Optional类型的转换。
步骤1:给Immutable接口添加注解
@Value.Style(stagedBuilder = true) @Value.Immutable @JsonDeserialize(builder = ImmutableMyImmutablePerson.Builder.class) public interface MyImmutablePerson { String getName(); Optional<Integer> getAge(); Optional<String> getAddress(); }
步骤2:读取CSV示例
public List<MyImmutablePerson> readWithJackson(File file) throws IOException { CsvMapper mapper = new CsvMapper(); // 使用CSV表头作为字段匹配依据 CsvSchema schema = CsvSchema.emptySchema().withHeader(); MappingIterator<MyImmutablePerson> iterator = mapper.readerFor(MyImmutablePerson.class) .with(schema) .readValues(file); return iterator.readAll(); }
2. OpenCSV(自定义映射策略)
OpenCSV通过自定义MappingStrategy适配Immutable的Builder模式,适合需要精细控制映射逻辑的场景。
示例代码
// 自定义映射策略,返回Immutable对象的Builder实例 public class MyImmutablePersonMappingStrategy extends ColumnPositionMappingStrategy<ImmutableMyImmutablePerson.Builder> { @Override public ImmutableMyImmutablePerson.Builder createBean() throws InstantiationException, IllegalAccessException { return ImmutableMyImmutablePerson.builder(); } } // 读取CSV并映射 public List<MyImmutablePerson> readCsv(File file) throws IOException { try (CSVReader reader = new CSVReader(new FileReader(file))) { MyImmutablePersonMappingStrategy strategy = new MyImmutablePersonMappingStrategy(); strategy.setType(ImmutableMyImmutablePerson.Builder.class); // 绑定CSV列与Builder方法 strategy.setColumnMapping("name", "age", "address"); CsvToBean<ImmutableMyImmutablePerson.Builder> csvToBean = new CsvToBeanBuilder<ImmutableMyImmutablePerson.Builder>(reader) .withMappingStrategy(strategy) .withSkipLines(1) // 跳过表头行 .build(); // 将Builder实例构建为最终Immutable对象 return csvToBean.parse().stream() .map(ImmutableMyImmutablePerson.Builder::build) .collect(Collectors.toList()); } }
3. Apache Commons CSV(反射调用Builder)
利用Apache Commons CSV读取行数据,通过反射自动调用Builder的对应方法,适合列名与Builder方法名完全匹配的场景。
示例代码
public List<MyImmutablePerson> readWithCommonsCsv(File file) throws IOException { List<MyImmutablePerson> result = new ArrayList<>(); try (CSVParser parser = CSVParser.parse(file, StandardCharsets.UTF_8, CSVFormat.DEFAULT.withHeader())) { for (CSVRecord record : parser) { ImmutableMyImmutablePerson.Builder builder = ImmutableMyImmutablePerson.builder(); // 遍历所有表头,反射调用Builder方法 for (String header : parser.getHeaderNames()) { String value = record.get(header); if (value != null && !value.isBlank()) { try { // 根据列名匹配Builder方法 Method method = builder.getClass().getMethod(header, getParameterType(header)); method.invoke(builder, convertValue(value, getParameterType(header))); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("映射列" + header + "失败", e); } } } result.add(builder.build()); } } return result; } // 辅助方法:根据列名获取对应参数类型 private Class<?> getParameterType(String header) { return switch (header) { case "age" -> Integer.class; case "name", "address" -> String.class; default -> throw new IllegalArgumentException("未知列:" + header); }; } // 辅助方法:将CSV字符串值转换为对应类型 private Object convertValue(String value, Class<?> type) { if (type == Integer.class) { return Integer.parseInt(value); } return value; }
内容的提问来源于stack exchange,提问作者mitriola
相关产品推荐
相关产品推荐

