MapStruct
[TOC]
什么是MapStruct
业务系统开发过程中,常常有分层概念,如持久层,业务层,数据传输层,视图层。
每个分层各自有自己的bean命名规则,对应上面提到的,如DO,BO,DTO,VO等。虽然命名不同,但绝大部分情况下,bean的内容都大同小异。所以,相同业务对象Bean在不同层交换数据时常常会涉及到类型的映射转换。MapStruct就是其中一种。
与其他Bean映射工具对比
市面上有很多映射工具类,比较流行的有Spring BeanUtils,Cglib BeanCopier,Apache BeanUtils,Dozer,orika,以及这次讲的MapStruct。
对上面提到的框架进行归类:
| 框架 | 深拷贝/浅拷贝 | 实现方式 | 执行阶段 | 
| Spring BeanUtils | 浅拷贝 | Java反射 | 运行期 | 
| Cglib BeanCopier | 浅拷贝 | 修改字节码生成代理类,代理类实现get、set | 运行期 | 
| Apache BeanUtils | 浅拷贝 | Java反射 | 运行期 | 
| Dozer | 深拷贝 | Java反射 | 运行期 | 
| orika | 深拷贝 | javassist生成字节码 | 编译期 | 
| MapStruct | 深拷贝 | Annotation Processor生成源码 | 编译期 | 
性能对比:
就通常使用上来说,性能由高到底为:MapStruct > orika > Cglib BeanCopier > Spring BeanUtils > Apache BeanUtils > Dozer 
MapStruct优势在于功能丰富,编译期生成Java源码进行映射,性能高。
具体对比结果网上有很多,不细说。
使用方式
配置依赖
maven方式
基本配置
| 12
 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
 
 | ...<properties>
 <org.mapstruct.version>1.4.2.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.8.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>
 ...
 
 | 
集成Spring
如果是Spring项目,建议配置一下,可以将MapStruct生成的Mapper类直接依赖注入到其他Bean。
主要在编译插件配置中加上一些配置:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | <plugin><groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>3.8.1</version>
 <configuration>
 <source>1.8</source>
 <target>1.8</target>
 <annotationProcessorPaths>
 <path>
 <groupId>org.mapstruct</groupId>
 <artifactId>mapstruct-processor</artifactId>
 <version>${mapstruct-processor.version}</version>
 </path>
 </annotationProcessorPaths>
 <compilerArgs>
 <arg>
 -Amapstruct.suppressGeneratorTimestamp=true
 </arg>
 
 
 <arg>
 -Amapstruct.defaultComponentModel=spring
 </arg>
 </compilerArgs>
 </configuration>
 </plugin>
 
 | 
集成lombok
一般业务项目中,Lombok也是常用的一款工具,需要注意的是,Lombok也依赖于Annotation Processor,所以也需要配置Lombok的Annotation Processor Paths,编译时才能进行代码生成。
注意:Lombok 在1.18.16版本中引入了一项重大改动changelog,必须要再额外添加一个Annotation Processor:lombok-mapstruct-binding。见如下配置:
| 12
 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
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 
 | <properties><org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
 <org.projectlombok.version>1.18.16</org.projectlombok.version>
 <maven.compiler.source>1.8</maven.compiler.source>
 <maven.compiler.target>1.8</maven.compiler.target>
 </properties>
 
 <dependencies>
 <dependency>
 <groupId>org.mapstruct</groupId>
 <artifactId>mapstruct</artifactId>
 <version>${org.mapstruct.version}</version>
 </dependency>
 
 <dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <version>${org.projectlombok.version}</version>
 <scope>provided</scope>
 </dependency>
 </dependencies>
 
 <build>
 <plugins>
 <plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>3.8.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>
 <path>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <version>${org.projectlombok.version}</version>
 </path>
 
 
 <path>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok-mapstruct-binding</artifactId>
 <version>0.1.0</version>
 </path>
 </annotationProcessorPaths>
 </configuration>
 </plugin>
 </plugins>
 </build>
 
 | 
映射器 Mapper
定义映射器
MapStruct使用@Mapper来定义转换类。
| 12
 3
 4
 5
 
 | @Mapperpublic interface VenueTransformer {
 VenueDTO toVenueDTO(VenueDO venueDO);
 List<VenueDTO> toVenueDTOList(List<VenueDO> venueDOList);
 }
 
 | 
定义这样一个接口(也可以是抽象类),在编译时,MapStruct将根据参数与返回类型,为其中字段自动生成转换代码。
以下是自动生成的代码,会在target/generate-sources目录下
| 12
 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
 
 | @Generated(
 value = "org.mapstruct.ap.MappingProcessor",
 comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_131 (Oracle Corporation)"
 )
 @Component
 public class VenueTransformerImpl implements VenueTransformer {
 
 @Override
 public VenueDTO toVenueDTO(VenueDO venueDO) {
 if ( venueDO == null ) {
 return null;
 }
 VenueDTO venueDTO = new VenueDTO();
 venueDTO.setAdderName( venueDO.getAdderName() );
 venueDTO.setAdderNo( venueDO.getAdderNo() );
 venueDTO.setAddTime( venueDO.getAddTime() );
 venueDTO.setUpdaterName( venueDO.getUpdaterName() );
 venueDTO.setUpdaterNo( venueDO.getUpdaterNo() );
 venueDTO.setUpdateTime( venueDO.getUpdateTime() );
 venueDTO.setVenueId( venueDO.getVenueId() );
 venueDTO.setVenueName( venueDO.getVenueName() );
 return venueDTO;
 }
 
 @Override
 public List<VenueDTO> toVenueDTOList(List<VenueDO> venueDOList) {
 if ( venueDOList == null ) {
 return null;
 }
 List<VenueDTO> list = new ArrayList<VenueDTO>( venueDOList.size() );
 for ( VenueDO venueDO : venueDOList ) {
 list.add( toVenueDTO( venueDO ) );
 }
 return list;
 }
 }
 
 | 
可以发现,源类型与目标类型中的字段名如果一样,MapStruct会自动映射上。单个Bean以及Bean List都会生成,拓展开也可以是Set,又或者是一个具体的实现类,如ArrayList等。
获取映射器
如果如前文所述,在maven配置时,配置了defaultComponentModel=spring,那么可以直接像注入其他@Service一样注入这个@Mapper即可。
如果没有配置,也可以通过Mappers.getMapper( VenueTransformer.class )来获取映射器实例。
定义字段映射规则
如果源类型与目标类型中字段名不一样,我们也可以通过@Mapping注解来自定义字段的映射关系。
| 12
 3
 4
 5
 
 | @Mapperpublic interface ExhibitionTransformer {
 @Mapping(source = "exhibitionName", target = "exhibitionShowName")
 ExhibitionDTO toExhibitionDTO(ExhibitionDO exhibitionDO);
 }
 
 | 
此处会将ExhibitionDO.exhibitionName映射到ExhibitionDTO.exhibitionShowName上。
MapStruct会自动处理一些类型转换,比如一个Integer类型要映射到String类型上,则会调用String.valueOf(int),具体见隐式类型转换。
自定义Bean映射规则
如果映射过程比较复杂,定制性很高,MapStruct也提供了自己写代码的转换方式。只需要在接口里写个default方法即可。
消除歧义
返回值类型相同
如果一个Mapper(包括@Mapper.use()的其他Mapper)中,出现了多个方法,他们的入参类型相同且返回值类型相同,会出现歧义,MapStruct会不知道要用哪一个具体的转换器方法。此时,我们可以通过给方法”起名字”的方式来消除歧义性,给方法加上@Named注解。如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | @Mapper(imports = {EncryptUtil.class, CompanyLevel.class})public interface CompanyTransformer {
 CompanyDTO toCompanyDTO(CompanyDO companyDO);
 
 
 
 
 
 @Named("customToCompanyDTO")
 default CompanyDTO customToCompanyDTO(CompanyDO companyDO) {
 if (companyDO == null) {
 return null;
 }
 CompanyDTO companyDTO = new CompanyDTO();
 companyDTO.setEncodeComId(EncryptUtil.encrypt(companyDO.getComId()));
 companyDTO.setComShowName("公司名:" + companyDO.getComName());
 companyDTO.setAddress(companyDO.getAddress());
 companyDTO.setCompanyLevel(CompanyLevel.determineByValue(companyDO.getLevel()));
 companyDTO.setStatus(companyDO.getStatus());
 return companyDTO;
 }
 }
 
 | 
返回值类型存在继承关系
譬如有两个返回类型B,C,都继承于类型A,那么如果在同一个Mapper中定义返回值类型为A,那么会出现歧义,我们实际需要B还是C?
这时候可以通过@BeanMapping.resultType来加以区分。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | @Mapper( uses = MyFactory.class )public interface MyMapper {
 @BeanMapping( resultType = B.class )
 A map( Source source );
 }
 
 public class MyFactory {
 public B createB() {
 return new B();
 }
 public C createC() {
 return new C();
 }
 }
 
 | 
将多个不同类型Bean合并为一个其他类型的Bean
@Mapping注解中,source以及target均可以通过”obj.field”的方式指代深层的成员字段。通过这种方式,可以指定多个源类型中的部分字段,来合并成一个新的类型对象。
| 12
 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
 
 | @Datapublic class CompanyDTO extends BaseDTO {
 private String encodeComId;
 private String comShowName;
 private String address;
 private CompanyLevel companyLevel;
 private Short status;
 private Integer deleteFlag;
 }
 @Data
 public class Exhibits {
 private String exhibitsName;
 private String producerName;
 private CompanyDTO producerDetail;
 private String authorName;
 private CompanyEmployee authorDetail;
 }
 @Data
 public class CompanyEmployee extends EmployeeDO {
 private String departmentName;
 }
 @Data
 public class EmployeeDO extends BaseDO {
 private String empNo;
 private String empName;
 }
 
 @Mapper
 public interface ExhibitsTransformer {
 @Mapping(source = "exhibitsName", target = "exhibitsName")
 @Mapping(source = "companyDTO.comShowName", target = "producerName")
 @Mapping(source = "companyDTO", target = "producerDetail")
 @Mapping(source = "companyEmployee.empName", target = "authorName")
 @Mapping(source = "companyEmployee", target = "authorDetail")
 Exhibits buildExhibits(String exhibitsName, CompanyDTO companyDTO, CompanyEmployee companyEmployee);
 }
 
 | 
通过一个Bean更新另一个Bean的内容
这里会用到一个@MappingTarget,指向需要被更新的Bean类型。
| 12
 
 | @Mapping(source = "companyDTO.comShowName", target = "producerName")void updateProducerInfo(@MappingTarget Exhibits exhibits, CompanyDTO companyDTO);
 
 | 
这里也可以返回Exhibits类型,可以配合其他映射方法实现链式转换。
调用其他映射器
可以通过@Mapper(use=AnotherTransformer.class)来将另一个Mapper引入到当前Mapper来,这样做的好处是,其他Mapper写的类型转换可以实现复用。比如,其他Mapper定义了一个Date到String的类型转换,我这个Mapper中涉及到的Date到String也需要如此转换,那么就可以直接把对应的Mapper引入进来即可。
| 12
 3
 4
 5
 
 | public class DateMapper {public String asString(Date date) {
 return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;
 }
 }
 
 | 
引入DateMapper
| 12
 3
 4
 
 | @Mapper(uses=DateMapper.class)public interface CarMapper {
 CarDto carToCarDto(Car car);
 }
 
 | 
映射Map类型
有时会涉及到Map之间的相互转换,譬如Map<String, String>转换为Map<Long, Date>,可以使用@MapMapping注解:
| 12
 
 | @MapMapping(valueDateFormat = "yyyy-MM-dd")Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> sourceMap);
 
 | 
映射枚举类型
可以使用MapStruct将一种枚举类型映射为另一种枚举类型。默认情况是根据枚举常量名进行映射,如果需要自定义,可以使用下面的方式:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | @Mapperpublic interface OrderMapper {
 @ValueMappings({
 @ValueMapping(source = "EXTRA", target = "SPECIAL"),
 @ValueMapping(source = "STANDARD", target = "DEFAULT"),
 @ValueMapping(source = "NORMAL", target = "DEFAULT")
 })
 ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
 }
 
 | 
如上会将OrderType.EXTRA映射到ExternalOrderType.SPECIAL。
高级配置项
默认值
使用@Mapping注解中的defaultValue参数,可以配置映射项的默认值,如果源值为null,那么就会取这个defaultValue的值作为目标值。
如果目标字段是一个新的字段,只需要给上默认常量呢?可以使用constant参数即可。
| 12
 
 | @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
 
 | 
表达式
常见的如日期格式化,MapStruct提供了Mapping.dateFormat,也可以自定义其他的format方式。
一种方式是单独写一个工具类,添加方法并引入当前Mapper中(见“调用其他映射器”章节DateMapper使用),我们也可以直接在@Mapping中定义特定的表达式,譬如:
| 12
 3
 4
 5
 6
 
 | @Mapperpublic interface SourceTargetMapper {
 @Mapping(target = "timeAndFormat",
 expression = "java(new org.sample.TimeAndFormat(s.getTime(), s.getFormat()))")
 Target sourceToTarget(Source s);
 }
 
 | 
配合@imports注解,在使用表达式时,可以不使用全限定名称。
| 12
 3
 4
 5
 6
 7
 8
 
 | imports org.sample.TimeAndFormat;
 @Mapper( imports = TimeAndFormat.class )
 public interface SourceTargetMapper {
 @Mapping(target = "timeAndFormat",
 expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
 Target sourceToTarget(Source s);
 }
 
 | 
写在最后
该文例举了MapStruct的配置方式,以及一些最为常见的用法,还有很多其他高级功能没有提及,具体请看官方文档:MapStruct 1.4.2.Final Reference Guide