Spring MVC で CSV をダウンロードさせる

思った以上にかんたんに実装できたわりに、ネット上にちょうどいい塩梅のサンプルが転がっていなかったのでメモ。

なお、今回のデモコードの全文は以下にありますので、実際の挙動を確認したい場合はご利用ください。
https://github.com/yo1000/com.yo1000.edu.boot.octetstream

# 環境

- Java 1.8.0_91
- Maven 3.3.9 (Maven wrapper)
- Spring Boot 1.4.1.RELEASE

```
$ ./mvnw --version
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T01:41:47+09:00)
Maven home: /Users/yo1000/.m2/wrapper/dists/apache-maven-3.3.9-bin/2609u9g41na2l7ogackmif6fj2/apache-maven-3.3.9
Java version: 1.8.0_91, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.11.6", arch: "x86_64", family: "mac"
```

# 依存関係の追加

`jackson-dataformat-csv` を使うと、不要な依存を追加せず、かんたんに実現できるので、今回はこれを使用します。(`spring-boot-starter-parent` で、デフォルトバージョンが設定されているため、バージョン指定は不要です。)

```xml:pom.xml&nbsp;(抜粋)
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.4.1.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-csv</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>
```

# モデルの実装

今回はわかりやすいように、かんたんなモデルを作成します。

`@JsonProperty` で設定した値が、CSV の1行目に出力されます。設定しない場合はフィールド名が出力されます。
また、`@JsonPropertyOrder` を設定しない場合は、プロパティ名でソートされた順に出力されてしまうので、任意の順番で出力したい場合には注意が必要です。

```java:Demo.java
package com.yo1000.edu.boot.octetstream.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import java.util.Date;

@JsonPropertyOrder({"ID", "名前", "概要", "更新日時"})
public class Demo {
    @JsonProperty("ID")
    private Long id;
    @JsonProperty("名前")
    private String name;
    @JsonProperty("概要")
    private String desc;
    @JsonProperty("更新日時")
    private Date modified;

    public Demo() {}

    public Demo(Long id, String name, String desc, Date modified) {
        this.id = id;
        this.name = name;
        this.desc = desc;
        this.modified = modified;
    }

    // Getter/Setter
}
```

# コントローラーの実装

Object <-> JSON の変換に、`ObjectMapper` を使うように、ここでは `CsvMapper` というクラスを使用して、オブジェクトを CSV に変換します。

`@GetMapping` の、`produces` 属性に、`"application/octet-stream"`(`MediaType.APPLICATION_OCTET_STREAM_VALUE`) と、`"Content-Disposition: attachment"` を、セミコロンで区切って指定することで、ダウンロードダイアログつきで、CSV を出力できます。
また、`charset` を指定することで、出力する CSV ファイルの文字セットも変更することができます。デフォルトが `UTF-8` なので、今回はあえて `Shift_JIS` を指定してみます。

また、ダウンロードされるファイルのデフォルトの名前は URL に準じるため、`"*.csv"` という URL を割り当てておくだけで、リクエストされたファイル名でそのままダウンロードさせることが可能になります。

```java:CsvController.java
package com.yo1000.edu.boot.octetstream.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import com.yo1000.edu.boot.octetstream.model.Demo;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Controller
@RequestMapping("csv")
public class CsvController {
    @GetMapping(value = "*.csv",
            produces = MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=Shift_JIS; Content-Disposition: attachment")
    @ResponseBody
    public Object getCsv() throws JsonProcessingException {
        List<Demo> demos = new ArrayList<Demo>() {
            {
                add(new Demo(1001L, "aaa", "xxx", new Date()));
                add(new Demo(1002L, "bbb", "yyy", new Date()));
                add(new Demo(1003L, "ccc", "zzz", new Date()));
            }
        };

        CsvMapper mapper = new CsvMapper();
        CsvSchema schema = mapper.schemaFor(Demo.class).withHeader();
        return mapper.writer(schema).writeValueAsString(demos);
    }
}
```

# 動作の確認

実際の動きを見てみましょう。

作成したエンドポイントをリクエストしてみます。
<img width="326" alt="スクリーンショット 2016-10-03 19.37.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/47993/3ac0e9e4-6d32-f2d3-1a75-d1913000cf11.png">

ダウンロードダイアログが現れました！
<img width="712" alt="スクリーンショット 2016-10-03 19.37.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/47993/8fccfd94-b51d-8696-b13c-65112b517a55.png">

ダウンロードしたファイルを開いてみると、`Shift JIS` にて、文字化けなく内容を表示できていることがわかります。
<img width="351" alt="スクリーンショット 2016-10-03 19.45.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/47993/3bd24aba-0e0e-9456-0d63-2b5e4918a805.png">

以上のように、これまでどおりのコントローラー記述と、`jackson-dataformat-csv` を使用して、かんたんに CSV ファイルを出力することができました。Spring では、オブジェクトの変換に Jackson が使用されているので、うまく活用していきたいですね。