前言 今天我们来介绍一款非常优秀的实体对象映射工具,MapStruct
。
在开发的时候我们会看到业务代码之间有很多的JavaBean
之间的相互转化,非常影响美观,但是又不得不存在。
通常的JavaBean
处理方法是通过Spring或者Apache提供的BeanUtils
工具,而对于一些不匹配的属性,通过get/set
方法解决。
由于BeanUtils
使用反射机制,故其在大量使用时可能会影响到性能,同时对于不匹配的属性,如果较多,get/set
起来也非常麻烦和繁琐。
对于一些特殊类型不一致的字段,如DO
里为Date
类型,可能DTO
里需要变为时间的String
类型,我们需要调用指定方法进行处理,再进行get/set
。
正文 而MapStruct
可以让我们从这种复杂而繁琐的工作中解放出来。
MapSturct
是一个生成类型安全,高性能且无依赖的JavaBean
映射代码的注解处理器(annotation processor)。
它可以让我们通过注解的方式,生成JavaBean
之间的映射代码,相较于反射,更安全更高效。
让我们来看一下。
MapStruct引入 首先我们需要在Maven
项目中引入MapSturct
的相关Jar包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <properties > <java.version > 1.8</java.version > <org.mapstruct.version > 1.3.1.Final</org.mapstruct.version > </properties > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > ${org.mapstruct.version}</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > ${org.mapstruct.version}</version > </dependency >
有两个,mapstruct
是主要包,mapstruct-processor
用来辅助生成JavaBean
之间的映射代码。
MapStruct使用 我们根据例子来看如何使用MapSturct
。
我们这儿有两个JavaBean
,PersonDO
和PersonDTO
,现在我们将PersonDO
转为PersonDTO
。
1 2 3 4 5 6 7 8 9 @NoArgsConstructor @AllArgsConstructor @Data public class PersonDO { private Integer id; private String name; private int age; private String gender; }
1 2 3 4 5 6 7 8 @NoArgsConstructor @AllArgsConstructor @Data public class PersonDTO { private String userName; private Integer age; private Gender gender; }
1 2 3 public enum Gender { MAN,MALE }
转化时需要编写Mapper
,即对象映射器,是一个接口,如下。
1 2 3 4 5 6 7 8 @Mapper public interface PersonConverter { PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class); @Mapping(source = "name", target = "userName") PersonDTO do2dto (PersonDO person) ; }
使用注解@Mapper
定义一个Converter
接口,在其中定义一个do2dto
方法,方法的入参类型是PersonDO
,出参类型是PersonDTO
,这个方法就用于将PersonDO
转成PersonDTO
。
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 public class Test { public static void main (String[] args) { PersonDO personDO = new PersonDO(); personDO.setName("Sakura" ); personDO.setAge(27 ); personDO.setId(1 ); personDO.setGender(Gender.MAN.name()); PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO); System.out.println(personDTO); } }
输出结果
1 PersonDTO(userName=Sakura, age=27, gender=MAN)
由上面的例子可以看到MapSturct
成功将PersonDO
转成PersonDTO
。
MapStruct字段映射处理 两个JavaBean
对象进行映射,属性名称一致的字段可以相互映射,不一致的如何处理呢?
两个字段名字不一致 从上面的例子可以看出,我们只需要使用@Mapping
注解。在转换方法上设置
1 @Mapping(source = "name", target = "userName")
即可。其含义是将name
字段的值映射给userName
。
MapStruct可以映射的类型 在上面例子,我们发现age
字段完成映射,其类型一个为int
,另一个为包装类。
gender
字段完成映射,其类型一个为枚举,一个为String
。
一般情况下,MapSturct
会对部分类型做自动映射,而不需要我们额外配置。
基本类型及其他们对应的包装类型。 基本类型的包装类型和String类型之间 String类型和枚举类型之间 自定义常量和默认值 如果我们想在转换中,给某个属性一个固定值,可以使用constant
。
比如PersonDTO
里新增身高字段。而PersonDO
里没有,那么我们可以在转换时赋予默认值。
1 2 3 4 5 6 7 8 9 @NoArgsConstructor @AllArgsConstructor @Data public class PersonDTO { private String userName; private Integer age; private Gender gender; private Integer height; }
1 @Mapping(target = "height",constant = "175")
而对于某一个属性,如果映射后为空,我们可以给予其默认值,使用defaultValue
,如下:
1 @Mapping(target = "gender",defaultValue = "MAN")
类型不一致的映射 在实际转化中,有可能字段类型是不一致的,如何进行映射呢?
我们使用上面的例子,在PersonDO
里增加生日属性,为Date
类型,PersonDTO
里为String
类型。
1 2 3 4 5 6 7 8 9 10 @NoArgsConstructor @AllArgsConstructor @Data public class PersonDO { private Integer id; private String name; private int age; private String gender; private Date birthday; }
1 2 3 4 5 6 7 8 9 10 @NoArgsConstructor @AllArgsConstructor @Data public class PersonDTO { private String userName; private Integer age; private Gender gender; private Integer height; private String birthday; }
这时候我们可以使用如下方法转化。
1 2 3 4 @Mapping(source = "name", target = "userName") @Mapping(target = "height",constant = "175") @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss") PersonDTO do2dto (PersonDO person) ;
运行后可以看到正确输出结果:
1 PersonDTO(userName=Sakura, age=27, gender=MAN, height=175, birthday=2020-08-17 15:20:16)
dateFormat
是MapStruct
自带的属性,如果我们的属性映射方法比较复杂呢?
当然,MapStruct
支持我们自定义属性转换的方法,我们来看一下。
我们新增一个兴趣爱好字段,如下:
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 39 40 41 42 @NoArgsConstructor @AllArgsConstructor @Data public class PersonDO { private Integer id; private String name; private int age; private String gender; private Date birthday; private List<Integer> hobbies; } @NoArgsConstructor @AllArgsConstructor @Data public class PersonDTO { private String userName; private Integer age; private Gender gender; private Integer height; private String birthday; private List<String> strHobbies; } public enum HobbiesEnum { FOOTBALL(1 ,"足球" ), READ_BOOK(2 ,"读书" ), PLAY_GAME(3 ,"玩游戏" ), SINGING(4 ,"唱歌" ), TRAVEL(5 ,"旅行" ); private Integer value; private String desc; public static HobbiesEnum getEnum (Integer value) { return Arrays.stream(HobbiesEnum.values()).filter(e->e.value.equals(value)).findFirst().orElse(null ); } }
需要完成Listhobbies
对ListstrHobbies
的转化,我们可以使用如下代码:
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 @Mapper public interface PersonConverter { PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class); @Mapping(source = "name", target = "userName") @Mapping(target = "height",constant = "175") @Mapping(target = "gender",defaultValue = "MAN") @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss") @Mapping(target = "strHobbies",expression = "java(getHobbiesStr(person.getHobbies()))") PersonDTO do2dto (PersonDO person) ; default List<String> getHobbiesStr (List<Integer> integers) { List<String> list = new ArrayList<>(); for (Integer integer : integers) { HobbiesEnum hobbiesEnum = HobbiesEnum.getEnum(integer); if (hobbiesEnum!=null ){ list.add(hobbiesEnum.getDesc()); } } return list; } }
PS:java1.8接口支持自定义default方法,expression = "java(getHobbiesStr(person.getHobbies()))"
这段代码也可以指向自己写的某个方法,需要具体路径包名。
上面代码还是非常好理解的,这儿就不过多叙述。
多个属性映射 MapStruct
还支持将多个实体数据组装到一个实体中,如下:
比如上面的例子,我们的身高属性来自另一个Bean
,则代码如下:
1 2 3 4 5 6 7 @NoArgsConstructor @AllArgsConstructor @Data public class HeightDO { private String id; private Integer height; }
则组装数据的方法如下:
1 2 3 4 5 6 @Mapping(source = "person.name", target = "userName") @Mapping(source = "height.height",target = "height") @Mapping(target = "gender",defaultValue = "MAN") @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss") @Mapping(target = "strHobbies",expression = "java(getHobbiesStr(person.getHobbies()))") PersonDTO do2dto (PersonDO person,HeightDO height) ;
对于source
属性,需要指定来自哪个DO
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Test { public static void main (String[] args) { PersonDO personDO = new PersonDO(); personDO.setName("Sakura" ); personDO.setAge(27 ); personDO.setId(1 ); personDO.setBirthday(new Date()); personDO.setGender(Gender.MAN.name()); List<Integer> list =new ArrayList<>(); list.add(1 ); list.add(3 ); personDO.setHobbies(list); HeightDO heightDO = new HeightDO(); heightDO.setHeight(178 ); PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO,heightDO); System.out.println(personDTO); } }
输出结果:
1 PersonDTO(userName=Sakura, age=27, gender=MAN, height=178, birthday=2020-08-17 15:52:14, strHobbies=[足球, 玩游戏])
更新现有Bean实例 MapStruct
还支持将一个Bean
的属性更新到另一个Bean
的同名属性里。
1 void updateDTOfromDO (PersonDO person, @MappingTarget PersonDTO personDTO) ;
需要注意这个对于同名属性,如果DO
上有值,那么DTO
上的值将被覆盖,如下:
1 2 3 4 5 6 7 8 9 10 11 12 PersonDTO dto = new PersonDTO(); dto.setAge(15 ); PersonDO aDo = new PersonDO(); aDo.setName("Sakura" ); aDo.setAge(27 ); aDo.setId(1 ); aDo.setBirthday(new Date()); aDo.setGender(Gender.MAN.name()); PersonConverter.INSTANCE.updateDTOfromDO(aDo,dto); System.out.println(dto);
输出结果:
1 PersonDTO(userName=null, age=27, gender=MAN, height=null, birthday=2020/8/17 下午4:29, strHobbies=null)
继承反转配置 MapStruct
在存在DO
转换为DTO
的前提下,还支持将DTO
转换为DO
,而且不用我们复杂的配置。
方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Mapper public interface PersonConverter { PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class); @Mapping(source = "name", target = "userName") @Mapping(target = "height",constant = "175") @Mapping(target = "gender",defaultValue = "MAN") @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss") @Mapping(target = "strHobbies",expression = "java(getHobbiesStr(person.getHobbies()))") PersonDTO do2dto (PersonDO person) ; @InheritInverseConfiguration(name = "do2dto") PersonDO dto2do (PersonDTO personDTO) ; }
我们只需要@InheritInverseConfiguration
注解即可解决,name
属性指向DO
转换为DTO
的方法。
测试:
1 2 3 4 5 6 7 8 9 10 11 12 PersonDTO dto1 = new PersonDTO(); dto1.setAge(27 ); dto1.setBirthday("2018-11-11 12:12:12" ); dto1.setGender(Gender.MAN); dto1.setHeight(175 ); dto1.setUserName("Sakura" ); List<String> list1 = new ArrayList<>(); list1.add("足球" ); list1.add("玩游戏" ); dto1.setStrHobbies(list1); PersonDO personDO1 = PersonConverter.INSTANCE.dto2do(dto1); System.out.println(personDO1);
输出:
1 PersonDO(id=null, name=Sakura, age=27, gender=MAN, birthday=Sun Nov 11 12:12:12 CST 2018, hobbies=null)
这儿需要注意的是我们看到Hobbies
属性并没有转化,这是正常的,这个需要我们配置@Mapping
,写出并指定兴趣爱好转化的逆向方法。
MapStruct的性能 MapStruct
的性能是非常优秀的,我们来测试一下:
为简化代码,我们是DO
和DTO
尽可能的简单。
1 2 3 4 5 6 @AllArgsConstructor @NoArgsConstructor @Data public class TestDO { private Integer a; }
1 2 3 4 5 6 @NoArgsConstructor @AllArgsConstructor @Data public class TestDTO { private Integer a; }
MapStruct
相关方法:
1 2 3 4 5 6 @Mapper public interface TestConverter { TestConverter INSTANCE = Mappers.getMapper(TestConverter.class); TestDTO do2dto (TestDO testDO) ; }
我们使用Spring
自带的BeanUtils
工具类来与其进行比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Test { final static int NUM = 100000 ; public static void main (String[] args) { long start1 = System.currentTimeMillis(); for (int i =0 ;i<NUM;i++){ TestDO testDO = new TestDO(); testDO.setA(i); TestDTO testDTO = new TestDTO(); BeanUtils.copyProperties(testDO,testDTO); } System.out.println("BeanUtils属性拷贝耗时:" +(System.currentTimeMillis()-start1)+"ms" ); long start2 = System.currentTimeMillis(); for (int i =0 ;i<NUM;i++){ TestDO testDO = new TestDO(); testDO.setA(i); TestDTO dto = TestConverter.INSTANCE.do2dto(testDO); } System.out.println("MapStruct处理耗时:" +(System.currentTimeMillis()-start2)+"ms" ); } }
我们以100000个对象为样本,得到如下输出结果:
1 2 BeanUtils属性拷贝耗时:1903ms MapStruct处理耗时:5ms
可以看到差距是非常大的。
MapStruct
为什么有如此优秀的性能呢?
其实核心点在于:MapStruct
在编译期间,就生成了对象映射代码,确保了高性能,同时编译期间也可以发现可能存在的映射问题。
在上面的TestConverter
中,经过代码编译后,MapStruct
会生成一个TestConverterImpl
的实现类,帮我们进行对象属性转换。
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2020-08-17T17:00:26+0800", comments = "version: 1.3.1.Final, compiler: javac, environment: Java 9 (Oracle Corporation)" ) public class TestConverterImpl implements TestConverter { @Override public TestDTO do2dto (TestDO testDO) { if ( testDO == null ) { return null ; } TestDTO testDTO = new TestDTO(); testDTO.setA( testDO.getA() ); return testDTO; } }
这样在运行之前就相当于我们手写get/set
方法,相比反射,速度更快。
其编译工作主要由mapstruct-processor
包完成。
PS:PersonConverterImpl
的相关实现这儿就不展示了,大家可以看下它是如何处理的,比如对于一些自定义方法等。
总结 本文介绍了一款Java对象映射工具,MapStruct
。相比传统BeanUtils
有着更高的性能。
映射代码在编译期间生成,相当于替我们手写了get/set
,不仅相比反射具有很好的性能,同时还可以在编译期间发现可能存在的映射问题。