Spring Boot キャンプ : Spring Boot + ModelMapper編

Spring Bootキャンプシリーズ、Spring Boot + ModelMapper編です。

## 今回の目的

Spring BootアプリでModelMapperを利用してBeanのコピーを行います。

**本記事では、独自に開発したStarterを使用します。
StarterはMaven Centralで公開しており、誰でも利用できます。**
https://github.com/yoshikawaa/modelmapper-spring-boot-starter

## 今回使用するライブラリ

* spring-boot-starter:2.2.0.M4
* modelmapper-spring-boot-starter:0.1.0
* spring-boot-starter-test:2.2.0.M4
* lombok:1.18.8

### ModelMapperとは

Java Beanのコピーを隠蔽し、コードをすっきりさせます。

```java
public Todo convert(TodoForm form) {

    Todo todo = new Todo();
    todo.setTodoId(form.getTodoId());
    todo.setTodoTitle(form.getTodoTitle());
    todo.setFinished(form.isFinished());
    todo.setCreatedAt(form.getCreatedAt());
    return todo;
}
```

これが以下のようにすっきりします。

```java
public Todo convert(TodoForm form) {

    ModelMapper modelMapper = new ModelMapper();
    return modelMapper.map(form, Todo.class);
}
```

ModelMapperの使い方は[こちらの記事](https://qiita.com/euledge/items/482a113589015590cf19)が詳しいので、ぜひ確認してみてください！

### Spring Bootへの対応

本題に入りますが、ModelMapper公式ではSpring Boot Starterが提供されていません。
Related Projectとして個人が開発したStarterがアナウンスされています。

ただ、このStarterだとちょっと気になる点がありました。

* サポートバージョンの古さ
  - modelmapper:0.7.5
  - spring-boot-starter:1.2.5.RELEASE
* カスタマイズの貧弱さ
  - Property MappingとConverter以外をカスタマイズする場合、Starterのメリットがない

### 作ってみました！

ということで、自分でStarterを作ってMaven Centralで公開してみました！
今回の記事では、こちらのStarterを利用していきます。

## modelmapper-spring-boot-starter

Spring BootでModelMapperを利用するためのスターターです。

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでModelMapperを使用するためのBean定義を自動的に行ってくれます。開発者は依存関係にmodelmapper-spring-boot-starterを追加するだけでOKです。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>io.github.yoshikawaa.modelmapper.spring.boot</groupId>
            <artifactId>modelmapper-spring-boot-starter</artifactId>
            <version>0.1.0</version>
        </dependency>
    </dependencies>
```

**src/main/java/*/Application.java**

```java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

メインクラスはSpring Bootアプリのデフォルトから何も変更していません。

### ModelMapperを使用するクラス

ここではControllerで使用してみます。
ModelMapperを使う以外の要素は極力省略しています。

**src/main/java/*/web/TodoController.java**

```java
@Controller
@RequestMapping("todo")
public class TodoController {

    // (1)
    @Autowired
    private ModelMapper modelMapper;

    @Autowired
    private TodoService todoService;

    @PostMapping
    public String create(TodoForm form) {

        // (2)
        Todo todo = modelMapper.map(form, Todo.class);
        todoService.create(todo);

        // ommitted.
    }
}
```

(1)　ModelMapperは自動的にセットアップされるので、インジェクションするだけで利用できます。

(2)　ModelMapperの`map`メソッドでBeanをコピーします。

### カスタムマッピング

ModelMapperのデフォルト設定では、コピー元と先で名前が同じプロパティを自動的にコピーします。
名前が異なるプロパティをコピーするには、カスタムマッピングが必要になります。

**src/main/java/*/ModelMapperConfig.java**

```java
@Configuration
public class ModelMapperConfig {

    @Bean
    TypeMapConfigurer<TodoForm, Todo> typeMap() {
        return new TypeMapConfigurer<TodoForm, Todo>() {
            @Override
            public void configure(TypeMap<TodoForm, Todo> typeMap) {
                typeMap.addMapping(TodoForm::getTodoTitle, Todo::setTitle);
            }
        };
    }
}
```

カスタムマッピングのため、TypeMapConfigurer<コピー元, コピー先>のBeanを定義します。
TypeMapConfigurerはBean定義すれば、自動的に適用されます。

> Note.
> `@Bean`ではなく`@Component`で定義してもOKです。
> `@Component`の場合は、１カスタムマッピング＝１クラスになります。

#### カスタムマッピングの検証

カスタムマッピングを定義したら、プロパティに抜け漏れがないことを検証することをお勧めします。

**src/main/resources/application.yml**

```yaml
modelmapper:
  validate-enabled: true
```

ModelMapperのセットアップで`validate`メソッドが実行され、プロパティのマッピングに抜け漏れがあればエラーになります。

### 振る舞いの変更

ModelMapperではコピー時の振る舞いをカスタマイズすることで、より柔軟なコピーが可能です。
ここでは、コピー元プロパティが`null`の時はコピーしないよう変更してみます。

**src/main/resources/application.yml**

```yaml
modelmapper:
  skip-null-enabled: true
```

ModelMapperのセットアップで`setSkipNullEnabled`メソッドが実行され、コピー元プロパティが`null`の時はコピーされなくなります。

### ModelMapperの設定を確認する

セットアップされたModelMapperの設定を確認するには、TRACEレベルのログを出力すればOKです。
ログ出力するレベルは、Spring Bootの`logging`プロパティを利用して設定します。

**src/main/resources/application.yml**

```yaml
logging:
  level:
    io.github.yoshikawaa.modelmapper.spring.boot.autoconfigure: trace
```
### JUnitテストケース

JUnitテストケースでは、使用するテスト用Auto-Configにより設定が異なるので注意しましょう。

**`@SpringBootTest`を利用する場合**

すべてのBeanを読み込みます。
この場合は、ModelMapperを利用するために意識することはありません。

**`@WebMvcTest`などを利用する場合**

軽量化のため限定的にBeanを読み込むため、独自のAuto-Configや`@Configuration`クラス等は読み込まれません。
この場合は、ModelMapperを利用するための設定が必要になります。

* `@ImportAutoConfiguration` -> ModelMapperのAuto-Configを読み込みます。
* `@Import` -> カスタムマッピングを読み込みます。

**src/test/java/*/web/TodoControllerTest.java**

```java
@WebMvcTest
@ImportAutoConfiguration(ModelMapperAutoConfiguration.class) // (1)
@Import(ModelMapperConfig.class) // (2)
class TodoControllerTest {

    @Autowired
    MockMvc mvc;

    @Test
    void testCreate() throws Exception {
        // execute & assert
        mvc.perform(post("/todo").param("todoTitle", "sample"))
            .andExpect(status().isOk());
    }
```

(1)　`@ImportAutoConfiguration`でModelMapperのセットアップを行う`ModelMapperAutoConfiguration`クラスを読み込みます。

(2)　`@Import`でカスタムマッピングの`@Configuration`クラスを読み込みます。配列で複数読み込むことも可能です。

> Note.
> カスタムマッピングを`@Component`で作った場合、`@Import`の代わりに`@ComponentScan`で読み込めばOKです。

## まとめ

今回は満足なStarterがなかったので、それなりに手を加えることになりましたが、Starterの作り方も分かったので良かったですw

ModelMapperを実開発で使う場合には、全体的な振る舞いをどこに集約するか、カスタムマッピングをどう管理するか、あたりが課題になると思うので、Starterを使うことによりルール付けされると良いですね。

今回作成したStarterは、記事に書いた以外にもModelMapperの豊富なカスタマイズをサポートしています。
利用される方は[Starterのリファレンス](https://github.com/yoshikawaa/modelmapper-spring-boot-starter)と[ModelMapperのリファレンス](http://modelmapper.org/user-manual/)を確認してみてください！