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方式
基本配置
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
| ... <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。
主要在编译插件配置中加上一些配置:
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
| <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
。见如下配置:
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 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
来定义转换类。
1 2 3 4 5
| @Mapper public interface VenueTransformer { VenueDTO toVenueDTO(VenueDO venueDO); List<VenueDTO> toVenueDTOList(List<VenueDO> venueDOList); }
|
定义这样一个接口(也可以是抽象类),在编译时,MapStruct将根据参数与返回类型,为其中字段自动生成转换代码。
以下是自动生成的代码,会在target/generate-sources目录下
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
| @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
注解来自定义字段的映射关系。
1 2 3 4 5
| @Mapper public 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
注解。如下:
1 2 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
来加以区分。
1 2 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”的方式指代深层的成员字段。通过这种方式,可以指定多个源类型中的部分字段,来合并成一个新的类型对象。
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
| @Data public 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类型。
1 2
| @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引入进来即可。
1 2 3 4 5
| public class DateMapper { public String asString(Date date) { return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null; } }
|
引入DateMapper
1 2 3 4
| @Mapper(uses=DateMapper.class) public interface CarMapper { CarDto carToCarDto(Car car); }
|
映射Map类型
有时会涉及到Map之间的相互转换,譬如Map<String, String>转换为Map<Long, Date>,可以使用@MapMapping
注解:
1 2
| @MapMapping(valueDateFormat = "yyyy-MM-dd") Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> sourceMap);
|
映射枚举类型
可以使用MapStruct将一种枚举类型映射为另一种枚举类型。默认情况是根据枚举常量名进行映射,如果需要自定义,可以使用下面的方式:
1 2 3 4 5 6 7 8 9
| @Mapper public 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
参数即可。
1 2
| @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
中定义特定的表达式,譬如:
1 2 3 4 5 6
| @Mapper public interface SourceTargetMapper { @Mapping(target = "timeAndFormat", expression = "java(new org.sample.TimeAndFormat(s.getTime(), s.getFormat()))") Target sourceToTarget(Source s); }
|
配合@imports
注解,在使用表达式时,可以不使用全限定名称。
1 2 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