Commons CSVを使用して汎用的にCSVを読み込む方法です。
環境
- JDK 21
- Common CSV 1.10.0 (https://commons.apache.org/proper/commons-csv/)
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-csv</artifactId> <version>1.10.0</version> </dependency>
CSV読み込み & マッピング
以下がサンプルコードです。実行が楽なのでテストクラスに作成しています。
CSVRecord.get(int)、CSVRecord.get(string)で読むのが使い方として多いのかなと思います。
読み込むCSVが1種類、以下の場合CsvModelAクラスのみであればこれでもいいでしょう。
しかしCsvModelB、C、Dと複数のCSVを読み込みたい場合に、このコード(主に各モデルクラスに代入するコード)を量産する必要があります。
CommonCSVにはいい感じにモデルにマッピングしてくれる機能はない(はず)のでしょうがないのですが、少々不便に感じました。
import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; public class CommonsCsvTest { @Test public void readCsv() { String filePath = "C:\\develop\\csv\\test.csv"; Charset charset = Charset.forName("Shift-JIS"); try (Reader in = new FileReader(filePath, charset)) { CSVFormat customCsvFormat = CSVFormat.Builder.create() .setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true) .setCommentMarker('#') .setSkipHeaderRecord(true) .setHeader("ヘッダ1", "ヘッダ2", "ヘッダ3") .build(); Iterable<CSVRecord> records = customCsvFormat.parse(in); List<CsvModelA> models = new ArrayList<>(); for (CSVRecord record : records) { CsvModelA modelA = new CsvModelA(); // パターン1 modelA.setColumn1(record.get("ヘッダ1")); modelA.setColumn2(record.get("ヘッダ2")); modelA.setColumn3(record.get("ヘッダ3")); // パターン2 modelA.setColumn1(record.get(0)); modelA.setColumn2(record.get(1)); modelA.setColumn3(record.get(2)); models.add(modelA); System.out.println(modelA.getColumn1() + ", " + modelA.getColumn2() + ", " + modelA.getColumn3()); } } catch (IOException e) { e.printStackTrace(); } } }
import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) public class CsvModelA { private String column1; private String column2; private String column3; }
import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) public class CsvModelB { private String column1; private String column2; private String column3; private String column4; }
汎用的にモデルクラスにマッピングする
どうにか汎用的に実装できないかなと思ってアノテーションを使用して実装してみました。
まずはヘッダを定義するためのアノテーションを作成します。
@CsvHeader(name = “xxx”)とアノテーションを付与します。
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface CsvHeader { String name(); }
このアノテーションを使用してモデルクラスを変更します。
アノテーションのnameにヘッダ名を設定します。
import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) public class CsvModelA { @CsvHeader(name = "ヘッダ1") private String column1; @CsvHeader(name = "ヘッダ2") private String column2; @CsvHeader(name = "ヘッダ3") private String column3; }
import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) public class CsvModelB { @CsvHeader(name = "ヘッダ1") private String column1; @CsvHeader(name = "ヘッダ2") private String column2; @CsvHeader(name = "ヘッダ3") private String column3; @CsvHeader(name = "ヘッダ4") private String column4; }
次にCSV読み込みの実装です。
@CsvHeaderで指定された変数と読み込んだCSVのカラムをマッピング、リフレクションでモデルクラスに値を設定しています。
バリデーションも必要であれば@Size等をモデルクラスに付与することでチェックできます。
なお、以下の実装ではString, int, BigDecimalのみ対応しています。
必要であればbooleanやdoubleなども判定に追加してください。(field.set()の箇所)
import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.math.BigDecimal; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import lombok.Data; import lombok.experimental.Accessors; public class CsvUtil<T> { public CsvResult readCsv(String filePath, Class<T> cls) throws ReflectiveOperationException, IOException { Charset charset = Charset.forName("Shift-JIS"); Map<CSVRecord, Set<ConstraintViolation<T>>> violations = new LinkedHashMap<>(); List<T> csvList = new ArrayList<>(); try (Reader in = new FileReader(filePath, charset)) { CSVFormat customCsvFormat = CSVFormat.Builder.create() .setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true) .setCommentMarker('#') .build(); Iterable<CSVRecord> records = customCsvFormat.parse(in); // ヘッダー行を取得 CSVRecord headers = records.iterator().next(); // モデルクラスのフィールドを取得 Field[] fields = cls.getDeclaredFields(); // ヘッダーとフィールドのマッピングを作成 Map<String, Field> fieldMap = new HashMap<>(); for (Field field : fields) { field.setAccessible(true); CsvHeader header = field.getAnnotation(CsvHeader.class); if (header == null) { continue; } String annotationHeader = header.name(); fieldMap.put(annotationHeader, field); } // バリデーション ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); // CSVファイルの各行をループして、各列のデータをモデルクラスのフィールドにマッピング for (CSVRecord record : records) { try { Constructor<T> constructor = cls.getDeclaredConstructor(); constructor.setAccessible(true); T instance = constructor.newInstance(); for (int i = 0; i < headers.size(); i++) { String value = record.get(i); Field field = fieldMap.get(headers.get(i)); if (field != null) { Class<?> fieldType = field.getType(); // 各カラムの型に変換して値を設定。以下で足りなければ追加。 if (fieldType == String.class) { field.set(instance, value); } else if (fieldType == int.class) { field.set(instance, Integer.parseInt(value)); } else if (fieldType == BigDecimal.class) { field.set(instance, new BigDecimal(value)); } } } Set<ConstraintViolation<T>> vio = validator.validate(instance); if (vio.size() > 0) { violations.put(record, vio); } csvList.add(instance); } catch (SecurityException e) { throw e; } catch (ReflectiveOperationException e) { throw e; } } } catch (IOException e) { throw e; } CsvResult result = new CsvResult(); result.setCsvList(csvList) .setViolations(violations); return result; } @Data @Accessors(chain = true) public class CsvResult { private List<T> csvList; private Map<CSVRecord, Set<ConstraintViolation<T>>> violations; } }
最後に呼び出し元です。
以下の実装ではCsvModelAを使用していますが、CsvModelBに変更しても同様の呼び出し方法で実行できます。
public class CommonsCsvTest2 { @Test public void readCsv() throws ReflectiveOperationException, IOException { String filePath = "C:\\develop\\csv\\test.csv"; CsvUtil<CsvModelA> util = new CsvUtil<>(); var result = util.readCsv(filePath, CsvModelA.class); for (var r : result.getCsvList()) { System.out.println(r.getColumn1() + ", " + r.getColumn2() + ", " + r.getColumn3()); } } }
以上です。
コメント