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());
}
}
}
以上です。

コメント