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

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

## 今回の目的

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

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

* spring-boot-starter:2.2.0.M4
* dozer-spring-boot-starter:6.5.0
* spring-boot-starter-test:2.2.0.M4
* lombok:1.18.8

### Dozerとは

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) {

    Mapper dozerMapper = DozerBeanMapperBuilder.buildDefault();
    return dozerMapper.map(form, Todo.class);
}
```

Dozerの使い方は[TERASOLUNAガイド](http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/ArchitectureInDetail/GeneralFuncDetail/Dozer.html)が詳しいので、ぜひ確認してみてください！

## dozer-spring-boot-starter

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

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

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>com.github.dozermapper</groupId>
            <artifactId>dozer-spring-boot-starter</artifactId>
            <version>6.5.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アプリのデフォルトから何も変更していません。

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

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

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

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

    // (1)
    @Autowired
    private Mapper dozerMapper;

    @Autowired
    private TodoService todoService;

    @PostMapping
    public String create(TodoForm form) {

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

        // ommitted.
    }
}
```

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

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

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

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

Dozerの設定はXMLとJava Configのいずれかで定義することができます。
使いたいほうを使ってもらえればOKですが、XMLには後述の注意点があります。

今回はJava ConfigのSpring Bootに合わせて、Java Configでカスタムマッピングを定義します。

* マッピング定義XML

**src/main/java/*/DozerConfig.java**

```java
@Configuration
public class DozerConfig {
    @Bean
    public BeanMappingBuilder mappingDestinationTodo() {
        return new BeanMappingBuilder() {
            @Override
            protected void configure() {
                mapping(type(TodoForm.class), type(Todo.class))
                    .fields(field("todiTitle"), field("title"));
            }
        };
    }
}
```

カスタムマッピングのため、BeanMappingBuilderのBeanを定義します。
BeanMappingBuilderはBean定義すれば、自動的に適用されます。

フィールド名は文字列指定なので、必ずテストで指定が正しいことを確認しましょうw

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

### 振る舞いの変更

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

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

```yaml
dozer:
  mapping-files:
  - classpath:configuration.dozer.xml
```

**src/main/resources/configuration.dozer.xml**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping http://dozermapper.github.io/schema/bean-mapping.xsd">
    <configuration>
        <map-null>false</map-null>        
    </configuration>
</mappings>
```

DozerのセットアップでXMLから設定が読み込まれ、コピー元プロパティが`null`の時はコピーされなくなります。

さきほどはJava Configで設定すると言っておきながら、当然のようにXMLを使い始めましたw
なぜこんなことをしているのかというと、、、

### Spring Boot + Dozerの注意点

#### XMLの`<configuration>`に対応するJava Configがない

[はい、ありません。](https://dozermapper.github.io/gitbook/documentation/apiConfiguration.html)
全体的な振る舞いの設定は**仕方なく**XMLの`<configuration>`で行うしかありません。

#### `dozer.mapping-files`プロパティにワイルドカードを使えない

**仕方なく**XMLを使うにあたり、、、

先のTERASOLUNAガイドのノリで、複数のXMLをワイルドカードで読み込もうとすると、`FileNotFoundException`になります。
[DozerMapper#702](https://github.com/DozerMapper/dozer/issues/702)を見ると、Springの`@Value`とSpring Bootの`@ConfigurationProperties`の挙動の違いにより、Bootではワイルドカードが解決できないようです。

`dozer.mapping-files`プロパティではXMLを一つ一つ指定する必要があります。
このため、全体的な振る舞いはXML、個別のマッピングはJava Configで定義するよう整理すると良さそうですね。

[リファレンス](https://dozermapper.github.io/gitbook/documentation/springBootIntegration.html)でも[DozerPropertiesのJavadoc](https://github.com/DozerMapper/dozer/blob/master/dozer-integrations/dozer-spring-support/dozer-spring-boot-autoconfigure/src/main/java/com/github/dozermapper/springboot/autoconfigure/DozerProperties.java#L31)でもワイルドカードで指定してるんですが、、、~~テストしてないんじゃね？~~

#### XMLの解析に利用されるJAXBがJava 11ではJDKに含まれていない

**仕方なく**XMLを使うにあたり、、、

DozerはXMLの解析にJAXBを使うようになったため、Java 11以降のJAXBが含まれていないJDKを利用するときは注意が必要です。

> Note.
> XMLの解析にトラディショナルなDozerのパーサーを利用することもできますが、さらに設定ファイルが増えますw
>
> **src/main/resources/dozer.properties**
> 
> ```properites
> dozer.xml.use-jaxb-mapping-engine = false
> ```
>
> システムプロパティや環境変数でもOKですが、動作環境によってセットすべきか判断は現実的ではないですね。


### JUnitテストケース

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

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

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

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

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

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

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

```java
@WebMvcTest
@ImportAutoConfiguration(DozerAutoConfiguration.class) // (1)
@Import(DozerConfig.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`でDozerのセットアップを行う`DozerAutoConfiguration`クラスを読み込みます。

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

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

## まとめ

DozerのStarterは古いAPIに引き摺られているせいか、BootのStarterとしてはちょっと最適化されてない感がありますね。

Java Configを使いたい場合もXMLと併用する必要があるのはちょっと、、、ですが、
Dozerを実開発で使う場合には、全体的な振る舞いをどこに集約するか、カスタムマッピングをどう管理するか、あたりが課題になると思うので、ある意味切り分けしやすくて良いかもしれません。