utamaro’s blog

誰かの役に立つ情報を発信するブログ

SpringBootでLocalDateTimeを含んだデータをCSV形式で出力する方法

SpringBootを使って、データ内にLocalDateTime型のフィールドがある場合のCSVファイル作成方法です。

CSVファイルを作成して、そのファイルをダウンロードするのではなく、CSV文字列を返すイメージです。

ブラウザでGETリクエストをするとダウンロードできるようにしてみました。

依存関係を追加する

依存関係をgradleで追加します。

dependencies {
    compile(
        //https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-csv
        'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.2.3',

        //https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310
        // jacksonでjava8のdate型を使うため
        'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.7',
    )

jackson-dataformat-csvcsvをモデルクラスから作るためのもの。

jackson-datatype-jsr310はjava8のDateがモデル内にあった場合に必要になるもの。

資料参照しつつ試したこと

csvデータの作成

github(jackson-dateformats-csv)の使用方法を見ました。 https://github.com/FasterXML/jackson-dataformats-text/tree/master/csv

ドキュメントの中間あたりに以下のコードがあったのでこれを参考にしてます。

CsvMapper mapper = new CsvMapper();
Pojo value = ...;
CsvSchema schema = mapper.schemaFor(Pojo.class); // schema from 'Pojo' definition
String csv = mapper.writer(schema).writeValueAsString(value);

こんなことを考えました。

  • PojoクラスはModelクラスみたいなものだろう。たぶんgetterとかsetterがあれば良いはず。
  • CsvMapperはObjectMapperみたいなものだろう。継承してるし、configureの設定とかできそう
  • 最後にschemaで設定したクラスをもとにして、valuecsv形式の文字列にしてるのかな

結果として、動きませんでした。

その時のエラーがこちらです。

com.fasterxml.jackson.core.JsonGenerationException: CSV generator does not support Object values for properties
    at com.fasterxml.jackson.core.JsonGenerator._reportError(JsonGenerator.java:1961)
    at com.fasterxml.jackson.dataformat.csv.CsvGenerator.writeStartObject(CsvGenerator.java:327)
    at com.fasterxml.jackson.core.base.GeneratorBase.writeStartObject(GeneratorBase.java:286)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:151)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)

なにかのパラメータが原因でcsvが作れてないのかな?と考えましたが、わからないので片っ端から調べました。

どうやらLocalDateTimeが原因らしいというのがわかったので、解決方法を調べました。

以下のコードを追加することで解消できました。

このコードはstackoverflowから見つけたものです。(タブを消してしまって、もう一度検索して見つけることができませんでした。orz)

    JavaTimeModule javaTimeModule = new JavaTimeModule();
    javaTimeModule.addDeserializer(
        LocalDateTime.class, 
        new LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME)
    );
    csvMapper.registerModule(javaTimeModule);

おそらく、マッピングするさいにjava8のLocalDateTimeに対応していないマッパーを、ISO_DATE_TIMEで文字列に変換できるようにしているのだと思います。

上記のコードではなく、Modelクラスに@DateTimeFormatを追加して対応してみましたが、結果は変わりませんでした。

ダウンロードの設定

headerと、ファイル名を指定していご、レスポンスを返しています。

HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/csv;");
String  filename = "hoge";
headers.setContentDispositionFormData("filename", filename + ".csv");
return new ResponseEntity<byte[]>(csv.getBytes(), headers, HttpStatus.OK);

完成コード

※ 一つのControllerに処理を書いていますが、一般的にはServiceクラスに分けた方が良いです。

@ResponseBody
@RequestMapping(value = "/download/csv", method = RequestMethod.GET)
public Object downloadCsv() {
    CsvMapper csvMapper = new CsvMapper();
    csvMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    CsvSchema schema = csvMapper.schemaFor(DataModel.class).withHeader();
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    // Hack time module to allow 'Z' at the end of string (i.e. javascript json's)
    javaTimeModule.addDeserializer(
            LocalDateTime.class,
            new LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME)
    );
    csvMapper.registerModule(javaTimeModule);
    // ↓DBからデータをセレクト
    List<DataModel> dataList = service.selectAll();
    String csv = null;
    try {
        csv = csvMapper.writer(schema).writeValueAsString(dataList);
    } catch (JsonProcessingException e) {
        // 本来はExceptionとか出します。
        e.printStackTrace();
    }
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/csv;");
    String  filename = "hoge";
    headers.setContentDispositionFormData("filename", filename + ".csv");
    return new ResponseEntity<>(csv.getBytes(), headers, HttpStatus.OK);
}