Spring MVC ファイルのアップロード

Spring MVCを使うと、Webブラウザからアップロードされたファイルを容易に受け取ることができます。
ここでは、基本的な実装方法から、少し掘り下げたファイルのアップロード方法を解説します。

# 基本的な実装方法

ファイルのアップロード画面を作成します。ここではテンプレートエンジンとしてThymeleafを使用します。
アップロードするファイルを選択するフィールドと、アップロードボタンがあるだけのシンプルな画面です。

```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>File Upload</title>
</head>
<body>
    <form action="/sample/upload" th:action="@{/sample/upload}" method="post" enctype="multipart/form-data">
        <input type="file" name="upload_file" /><br />
        <button type="submit">upload</button>
    </form>
</body>
</html>
```

次にアップロードボタンをクリックしたときのsubmit先のコントローラーメソッドを作成します。

```java
@PostMapping("upload")
public String upload(@RequestParam("upload_file") MultipartFile multipartFile, Model model) {
    model.addAttribute("originalFilename", multipartFile.getOriginalFilename());

    return "sample/result";
}
```

コントローラーの引数にMultipartFileを追加して、その引数に@RequestParamアノテーションを指定します。この@RequestParamアノテーションのvalue属性に、画面のアップロードするファイルを選択するフィールドのname属性の値を指定するところがポイントです。

```html
<input type="file" name="upload_file" />
```

```java
public String upload(@RequestParam("upload_file") MultipartFile multipartFile, ...) {
```

## ファイルを選択するフィールドとのマッピング

先ほどのサンプルでは、画面のファイルを選択するフィールドのname属性と、コントローラーの引数のMultipartFileに指定した@RequestParamアノテーションでマッピングしました。
しかし、もう少し簡単にマッピングすることもできます。@RequestParamアノテーションを使用せず、画面のファイルを選択するフィールドのname属性と、コントローラーの引数のMultipartFileの変数名を同一にすることで、自動的にマッピングされます。

```html
<input type="file" name="multipartFile" />
```

```java
public String upload(MultipartFile multipartFile, ...) {
```

コントローラーの実装がシンプルになったことがわかります。

## Formへのバインド
通常の入力フィールドと同様、ファイルを選択するフィールドもFormのメンバ変数にバインドすることができます。

まずは、Formクラスを作成します。lombokを使用して、アクセッサメソッドの実装を省略しています。

```java
@Getter
@Setter
public class UploadForm {
    private MultipartFile multipartFile;
}
```

画面のファイルを選択するフィールドを、Formクラスのメンバ変数とマッピングさせるために、少し修正します。

```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>File Upload</title>
</head>
<body>
    <form action="/sample/upload" th:action="@{/sample/upload}" method="post" enctype="multipart/form-data" th:object="${uploadForm}">
        <input type="file" th:field="*{multipartFile}" /><br />
        <button type="submit">upload</button>
    </form>
</body>
</html>
```

コントローラーはFormクラスを引数にするだけで、アップロードしたファイルを受け取ることができます。

```java
@PostMapping("upload")
public String upload(UploadForm uploadForm, Model model) {
    model.addAttribute("originalFilename", uploadForm.getMultipartFile()
            .getOriginalFilename());

    return "sample/result";
}
```

# アップロードファイルのバリデーション

通常の入力フィールドと同様、アップロードするファイルも検証が必要となるケースが多いでしょう。
受け取ったMultipartFileオブジェクトの検証処理をコントローラー内に実装してもよいのですが、検証処理と通常の処理は分離しておいたほうがソースの見通しがよくなります。

## Bean Validationによる検証

Bean Validationにより、Formクラスのメンバ変数にアノテーションを指定することで、アップロードファイルの検証を実装してみます。
とはいえ、Bean Validationではアップロードファイル向けのバリデーションは提供されていません。

> Bean ValidationはJSR(Java仕様要求)で定義されているものなので、Spring Frameworkの独自仕様であるMultipartFileによるファイルアップロードに対応していないのは仕方のないことでしょう。

Bean Validationでは、開発者が独自のカスタムバリデーションを実装する方法が用意されているので、この仕組みを利用してアップロードファイル向けのバリデーションを作成します。

ここでは、ファイルの指定を必須とする@FileRequiredアノテーションを作成します。

```java
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FileRequiredValidator.class)
public @interface FileRequired {
    String message() default "{com.example.demo.validation.FileRequired.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        FileRequired[] value();
    }
}
```

次に、アノテーションに対応するバリデータークラスを作成します。検証処理自体は、このクラスに実装することになります。
アップロードファイルが指定されなかった場合、MultipartFileオブジェクトのgetOriginalFilename()メソッドで返却されるファイル名が空文字列("")になるので、このメソッドの返却値を検証に使用します。

```java
public class FileRequiredValidator implements ConstraintValidator<FileRequired, MultipartFile> {
    @Override
    public void initialize(FileRequired constraint) {}

    @Override
    public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {
        return multipartFile != null
                && !multipartFile.getOriginalFilename().isEmpty();
    }
}
```

ここまででカスタムバリデーションの実装は完了です。あとは、通常のBean Validationと同様の実装で、アップロードファイルの検証が可能です。
具体的には、Formのメンバ変数にバリデーションのアノテーションを指定します。

```java
@FileRequired
private MultipartFile multipartFile;
```

次に、コントローラーのForm引数に@Validatedアノテーションを指定、BindingResultを追加し、バリデーションエラーがある場合の処理を追加します。

```java
@PostMapping("upload")
public String upload(@Valid UploadForm uploadForm, BindingResult result, Model model) {
    if (result.hasErrors()) {
        return "sample/upload";
    }
```

通常のBean Validationと同様の実装で、アップロードファイルの検証ができました。
よくあるアップロードファイルの検証は、Bean Validationを用意しておくと検証処理の実装コストが軽減されます。
たとえば、次のようなBean Validationは再利用性が高いと思われます。

* ファイルの指定必須。
* ファイルが空(0バイト)でない。
* ファイルの種類が特定のものである。

## Spring Validatorによる検証

Bean Validationは再利用性が高い、汎用的な検証を実装して、Formクラスのメンバ変数ごとに設定しました。それに対し、Spring Validatorはフォーム単位で検証を実装します。
ここではSpring Validatorを使って、アップロードファイルの検証を実装します。

org.springframework.validation.Validatorインターフェースを実装したクラスに検証処理を実装します。

```java
@Component
public class MultipartFileValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return UploadForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        UploadForm uploadForm = (UploadForm) target;
        if (uploadForm.getMultipartFile().getOriginalFilename().isEmpty()) {
            errors.rejectValue("multipartFile", "com.example.demo.validation.FileLinesLarge.message");
        }
    }
}
```

これだけでSpring Validatorの実装は完了です。あとは、コントローラークラスでSpring ValidatorでFormを検証するように設定します。

```java
@Autowired
private MultipartFileValidator multipartFileValidator;

@InitBinder
private void initBinderFileBucket(WebDataBinder binder) {
    binder.setValidator(multipartFileValidator);
}
```

コントローラーのメソッドの変更は、Bean Validatorと同様です。

```java
@PostMapping("upload")
public String upload(@Valid UploadForm uploadForm, BindingResult result, Model model) {
    if (result.hasErrors()) {
        return "sample/upload";
    }
```

# ファイルアップロードの設定
Springでは、ファイルのアップロードに関するいくつかの設定項目を持っています。
ここでは、Spring Bootを使ったファイルアップロードの設定を解説します。

## spring.servlet.multipart.max-file-size
アップロードするファイルごとの最大サイズを指定します。デフォルトは「1MB」です。数値の後に単位となる「MB」もしくは「KB」をつけて最大サイズを指定します。
1つのファイルの最大サイズであることに注意しましょう。例えば、「1MB」を指定した場合、「1MB」のファイルを10個アップロードすることはできても、「10MB」のファイルを1個アップロードすることはできません。

```properties
spring.servlet.multipart.max-file-size=50MB
```

> このプロパティーのキー名はSpring Boot 2.0.0で変更されたものです。Spring Boot 2.0.0より前のバージョンを使用する場合は、以前のキー名「spring.http.multipart.max-file-size」を使用する必要があります。

## spring.servlet.multipart.max-request-size
Multipartリクエスト全体の最大サイズを指定します。デフォルトは「10MB」です。数値の後に単位となる「MB」もしくは「KB」をつけて最大サイズを指定します。
この「最大サイズ」は、ファイルアップロードだけではなく、ほかの入力項目など、サーバに送信されるすべてのデータの合計の「最大サイズ」であることに注意しましょう。

```properties
spring.servlet.multipart.max-request-size=100MB
```

> このプロパティーのキー名はSpring Boot 2.0.0で変更されたものです。Spring Boot 2.0.0より前のバージョンを使用する場合は、以前のキー名「spring.http.multipart.max-request-size」を使用する必要があります。

## spring.servlet.multipart.enabled
Multipartリクエストを処理するSpringのデフォルト実装であるMultipartResolverを有効にするかどうかを指定します。デフォルトはtrueでMultipartResolverが有効です。
MultipartのファイルアップロードのインターフェースはRFC 1867で定められており、容易に交換が可能になっています。Springのデフォルト実装であるMultipartResolver以外の実装を使用する場合は、このプロパティーにfalseを設定します。

```properties
spring.servlet.multipart.enabled=false
```

> このプロパティーのキー名はSpring Boot 2.0.0で変更されたものです。Spring Boot 2.0.0より前のバージョンを使用する場合は、以前のキー名「spring.http.multipart.enabled」を使用する必要があります。

## spring.servlet.multipart.file-size-threshold
アップロードしたファイルがサーバーのディスクに書き込まれるファイルサイズの閾値です。このプロパティーの値以上のサイズのファイルをアップロードすると、そのファイルはサーバーのディスクに書き込まれます。このプロパティー未満のファイルサイズの場合、アップロードしたファイルはディスクには書き込まれず、メモリ上に保持されます。デフォルトは「0」です。数値の後に単位となる「MB」もしくは「KB」をつけて閾値を指定します。

```properties
spring.servlet.multipart.file-size-threshold=100MB
```

> このプロパティーのキー名はSpring Boot 2.0.0で変更されたものです。Spring Boot 2.0.0より前のバージョンを使用する場合は、以前のキー名「spring.http.multipart.file-size-threshold」を使用する必要があります。

## spring.servlet.multipart.location
アップロードしたファイルをサーバ上のディスクに中間ファイルとして書き込むときの場所(ディレクトリ)を指定します。デフォルトは未指定で、アプリケーションサーバー既定の一時ファイル用のディレクトリが使用されます。

```properties
spring.servlet.multipart.location=/upload/temp
```

> このプロパティーのキー名はSpring Boot 2.0.0で変更されたものです。Spring Boot 2.0.0より前のバージョンを使用する場合は、以前のキー名「spring.http.multipart.location」を使用する必要があります。

## spring.servlet.multipart.resolve-lazily
アップロードされたファイルの確認を遅延させるか否かを設定します。デフォルトはfalseでファイルの確認を遅延させません。
MultipartExceptionをハンドリングしたい場合にtrueを設定します。
遅延させることで、アップロードしたファイルのサイズが大きかったときに発生するMultipartExceptionの発生タイミングが変わり、コントローラーの@ExceptionHandlerで補足できるようになります。

```properties
spring.servlet.multipart.resolve-lazily=true
```

> このプロパティーのキー名はSpring Boot 2.0.0で変更されたものです。Spring Boot 2.0.0より前のバージョンを使用する場合は、以前のキー名「spring.http.multipart.resolve-lazily」を使用する必要があります。