在开发中,经常会遇到多个Bean之间互相转换,尤其是vo, bo, dto等bean之间的转换更加频繁,而每次手写一段get, set方法更加费力。为了提高开发效率,出现了一些bean之间自动转换的方案,MapStruct就是其中之一。MapStruct是在编译器生成get,set方法实现实体映射,而不是通过反射的方式,所以效率比较高。
MapStruct的官网 https://mapstruct.org/,手册 https://mapstruct.org/documentation/stable/reference/html/#basic-mappings
本文简单介绍下MapStruct的用法。
Maven依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
... <properties> <org.mapstruct.version>1.3.1.Final</org.mapstruct.version> </properties> ... <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> </dependencies> ... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> ... |
基本映射
定义两个 DO 对象 Person 和 User,其中 user 是 Person 的一个属性 ,一个 DTO 对象 PersonDTO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class Person { private Long id; private String name; private String email; private Date birthday; private User user; // get, set方法 } public class User { private Integer age; // get, set方法 } public class PersonDTO { private Long id; private String name; /** * 对应 Person.user.age */ private Integer age; private String email; /** * 与 DO 里面的字段名称(birthDay)不一致 */ private Date birth; /** * 对 DO 里面的字段(birthDay)进行拓展,dateFormat 的形式 */ private String birthDateFormat; /** * 对 DO 里面的字段(birthDay)进行拓展,expression 的形式 */ private String birthExpressionFormat; } |
再写一个 Mapper 接口 PersonConverter,其中两个方法,一个是单实体映射,另一个是List映射。
若源对象属性与目标对象属性名字一致,会自动映射对应属性,不一样的需要指定,也可以用 format 转成自己想要的类型,也支持表达式的方式,可以看到像 id、name、email这些名词一致的我并没有指定 source-target,而birthday-birth指定了,转换格式的 birthDateFormat 加了dateFormat 或者 birthExpressionFormat 加了 expression,如果某个属性你不想映射,可以加个 ignore=true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Mapper public interface PersonConverter { PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class); @Mappings({ @Mapping(source = "birthday", target = "birth"), @Mapping(source = "birthday", target = "birthDateFormat", dateFormat = "yyyy-MM-dd HH:mm:ss"), @Mapping(target = "birthExpressionFormat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthday(),\"yyyy-MM-dd HH:mm:ss\"))"), @Mapping(source = "user.age", target = "age"), @Mapping(target = "email", ignore = true) }) PersonDTO domain2dto(Person person); List<PersonDTO> domain2dto(List<Person> people); } |
在IDE中会自动编译,生成对应的实现类。
写一个单元测试类 PersonConverterTest 测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class PersonConverterTest { @Test public void test() { Person person = new Person(1L,"zhige","zhige.me@gmail.com",new Date(),new User(1)); PersonDTO personDTO = PersonConverter.INSTANCE.domain2dto(person); assertNotNull(personDTO); assertEquals(personDTO.getId(), person.getId()); assertEquals(personDTO.getName(), person.getName()); assertEquals(personDTO.getBirth(), person.getBirthday()); String format = DateFormatUtils.format(personDTO.getBirth(), "yyyy-MM-dd HH:mm:ss"); assertEquals(personDTO.getBirthDateFormat(),format); assertEquals(personDTO.getBirthExpressionFormat(),format); List<Person> people = new ArrayList<>(); people.add(person); List<PersonDTO> personDTOs = PersonConverter.INSTANCE.domain2dto(people); assertNotNull(personDTOs); } } |
多对一
MapStruct 可以将几种类型的对象映射为另外一种类型,比如将多个 DO 对象转换为 DTO
例:两个 DO 对象 Item 和 Sku,一个 DTO 对象 SkuDTO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Item { private Long id; private String title; // get, set方法 } public class Sku { private Long id; private String code; private Integer price; // get, set方法 } public class SkuDTO { private Long skuId; private String skuCode; private Integer skuPrice; private Long itemId; private String itemName; // get, set方法 } |
创建 ItemConverter(映射)接口,MapStruct 就会自动实现该接口
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Mapper public interface ItemConverter { ItemConverter INSTANCE = Mappers.getMapper(ItemConverter.class); @Mappings({ @Mapping(source = "sku.id",target = "skuId"), @Mapping(source = "sku.code",target = "skuCode"), @Mapping(source = "sku.price",target = "skuPrice"), @Mapping(source = "item.id",target = "itemId"), @Mapping(source = "item.title",target = "itemName") }) SkuDTO domain2dto(Item item, Sku sku); } |
创建测试类,讲 Item 和 Sku 两个 DO对象,映射成一个 DTO 对象 SkuDTO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class ItemConverterTest { @Test public void test() { Item item = new Item(1L, "iPhone X"); Sku sku = new Sku(2L, "phone12345", 1000000); SkuDTO skuDTO = ItemConverter.INSTANCE.domain2dto(item, sku); assertNotNull(skuDTO); assertEquals(skuDTO.getSkuId(),sku.getId()); assertEquals(skuDTO.getSkuCode(),sku.getCode()); assertEquals(skuDTO.getSkuPrice(),sku.getPrice()); assertEquals(skuDTO.getItemId(),item.getId()); assertEquals(skuDTO.getItemName(),item.getTitle()); } } |
映射类添加自定义方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 形式如下 default PersonDTO personToPersonDTO(Person person) { //hand-written mapping logic } // 比如在 PersonConverter 里面加入如下 default Boolean convert2Bool(Integer value) { if (value == null || value < 1) { return Boolean.FALSE; } else { return Boolean.TRUE; } } default Integer convert2Int(Boolean value) { if (value == null) { return null; } if (Boolean.TRUE.equals(value)) { return 1; } return 0; } // 测试类 PersonConverterTest 加入 assertTrue(PersonConverter.INSTANCE.convert2Bool(1)); assertEquals((int)PersonConverter.INSTANCE.convert2Int(true),1); |
如果已经有了接收对象,更新目标对象
1 2 3 4 5 6 7 8 9 10 11 |
// 比如在 PersonConverter 里面加入如下,@InheritConfiguration 用于继承刚才的配置 @InheritConfiguration(name = "domain2dto") void update(Person person, @MappingTarget PersonDTO personDTO); // 测试类 PersonConverterTest 加入如下 Person person = new Person(1L,"zhige","zhige.me@gmail.com",new Date(),new User(1)); PersonDTO personDTO = PersonConverter.INSTANCE.domain2dto(person); assertEquals("zhige", personDTO.getName()); person.setName("xiaozhi"); PersonConverter.INSTANCE.update(person, personDTO); assertEquals("xiaozhi", personDTO.getName()); |
Spring 注入的方式
刚才写的例子一直是默认的方式调用映射类方法
1 |
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class); |
要和Spring框架结合,也很方便。在 @Mapper 后面加入 componentModel=”spring”
1 2 3 4 5 6 7 8 9 10 11 |
@Mapper(componentModel="spring") public interface PersonConverter { @Mappings({ @Mapping(source = "birthday", target = "birth"), @Mapping(source = "birthday", target = "birthDateFormat", dateFormat = "yyyy-MM-dd HH:mm:ss"), @Mapping(target = "birthExpressionFormat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthday(),\"yyyy-MM-dd HH:mm:ss\"))"), @Mapping(source = "user.age", target = "age"), @Mapping(target = "email", ignore = true) }) PersonDTO domain2dto(Person person); } |
测试类改一下,用的 spring boot 的形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@RunWith(SpringRunner.class) @SpringBootTest(classes = BaseTestConfiguration.class) public class PersonConverterTest { //这里把转换器装配进来 @Autowired private PersonConverter personConverter; @Test public void test() { Person person = new Person(1L,"zhige","zhige.me@gmail.com",new Date(),new User(1)); PersonDTO personDTO = personConverter.domain2dto(person); assertNotNull(personDTO); assertEquals(personDTO.getId(), person.getId()); assertEquals(personDTO.getName(), person.getName()); assertEquals(personDTO.getBirth(), person.getBirthday()); String format = DateFormatUtils.format(personDTO.getBirth(), "yyyy-MM-dd HH:mm:ss"); assertEquals(personDTO.getBirthDateFormat(),format); assertEquals(personDTO.getBirthExpressionFormat(),format); } } |