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のコピーを隠蔽し、コードをすっきりさせます。
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;
}
これが以下のようにすっきりします。
public Todo convert(TodoForm form) {
Mapper dozerMapper = DozerBeanMapperBuilder.buildDefault();
return dozerMapper.map(form, Todo.class);
}
Dozerの使い方はTERASOLUNAガイドが詳しいので、ぜひ確認してみてください!
dozer-spring-boot-starter
Spring BootでDozerを利用するためのスターターです。
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでDozerを使用するためのBean定義を自動的に行ってくれます。開発者は依存関係にdozer-spring-boot-starterを追加するだけでOKです。
pom.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
@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
@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
@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
の場合は、1カスタムマッピング=1クラスになります。
振る舞いの変更
Dozerではコピー時の振る舞いをカスタマイズすることで、より柔軟なコピーが可能です。
ここでは、コピー元プロパティがnull
の時はコピーしないよう変更してみます。
src/main/resources/application.yml
dozer:
mapping-files:
- classpath:configuration.dozer.xml
src/main/resources/configuration.dozer.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がない
はい、ありません。
全体的な振る舞いの設定は仕方なくXMLの<configuration>
で行うしかありません。
dozer.mapping-files
プロパティにワイルドカードを使えない
仕方なくXMLを使うにあたり、、、
先のTERASOLUNAガイドのノリで、複数のXMLをワイルドカードで読み込もうとすると、FileNotFoundException
になります。
DozerMapper#702を見ると、Springの@Value
とSpring Bootの@ConfigurationProperties
の挙動の違いにより、Bootではワイルドカードが解決できないようです。
dozer.mapping-files
プロパティではXMLを一つ一つ指定する必要があります。
このため、全体的な振る舞いはXML、個別のマッピングはJava Configで定義するよう整理すると良さそうですね。
リファレンスでもDozerPropertiesのJavadocでもワイルドカードで指定してるんですが、、、テストしてないんじゃね?
XMLの解析に利用されるJAXBがJava 11ではJDKに含まれていない
仕方なくXMLを使うにあたり、、、
DozerはXMLの解析にJAXBを使うようになったため、Java 11以降のJAXBが含まれていないJDKを利用するときは注意が必要です。
Note.
XMLの解析にトラディショナルなDozerのパーサーを利用することもできますが、さらに設定ファイルが増えますwsrc/main/resources/dozer.properties
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
@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を実開発で使う場合には、全体的な振る舞いをどこに集約するか、カスタムマッピングをどう管理するか、あたりが課題になると思うので、ある意味切り分けしやすくて良いかもしれません。