日常开发中,我们经常会在PO,VO,DTO之间进行对象的转换,BeanUtils的copy方法很好,但是他不支持集合之间的转换,在网上查阅之后,发现了Mapstruct这个工具,使用之后,真香。

概述

MapStruct是一个Java注释处理器,用于生成类型安全的bean映射类。

您要做的就是定义一个映射器接口,该接口声明任何必需的映射方法。在编译期间,MapStruct将生成此接口的实现。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射,即没有反射或类似内容。

与手动编写映射代码相比,MapStruct通过生成繁琐且易于出错的代码来节省时间。遵循配置方法上的约定,MapStruct使用合理的默认值,但在配置或实现特殊行为时不加理会。

与动态映射框架相比,MapStruct具有以下优点:

  1. 通过使用普通方法调用(settter/getter)而不是反射来快速执行
  2. 编译时类型安全性:只能映射相互映射的对象和属性,不能将order实体意外映射到customer DTO等。
  3. 如果有如下问题,编译时会抛出异常
    1. 映射不完整(并非所有目标属性都被映射)
    2. 映射不正确(找不到正确的映射方法或类型转换)
  4. 可以通过freemarker定制化开发

区别

工具 实现方式 缺点 说明 速度(100w)
MapStruct getter/setter方法 需要了解注解和配置项语法 JSR269注解处理器在编译期自动生成Java Bean转换代码,支持可配置化,扩展性强 54ms
orika 动态生成字节码 首次调用耗时较久,性能适中 采用javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件 6764ms
Spring BeanUtils 反射机制 不支持名称相同但类型不同的属性转换 采用javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件 6700ms
Apache BeanUtils 反射机制 需要处理编译期异常,性能最差 7086ms
dozer 反射机制 性能差 使用reflect包下Field类的set(Object obj, Object value)方法进行属性赋值 6974ms
BeanCopier 反射机制 BeanCopier只拷贝名称和类型都相同的属性。即便基本类型与其对应的包装类型也不能相互转换 使用ASM的MethodVisitor直接编写各属性的get/set方法 6834ms

性能排序:手动get/set > MapStruct > Spring BeanUtils > orika > BeanCopier > dozer > apache BeanUtils

常用注解

image-20230312145034239

工具

可以在idea中搜索MapStruct Support工具来提高开发效率,可以通过点击source/target来直接查看字段之间的映射关系。

image-20230312150301835

Mapstruct的使用

导入依赖

<!--  mapStruct开始  -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>1.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</dependency>
<!-- mapStruct结束 -->

直接使用

@Mapper

@Mapper
public abstract class CarConvert(){
/**
* 提供一个获取CarConvert对象实例的静态属性
*/
public static CarConvert INSTANCE = Mappers.getMapper(CarConvert.class)
/**
* 对象转换的方法
*/
public abstract CarVo abc(CarDTo carDTo);
}

测试

  • 其中@Mapper的默认映射规则
  • 同类型且同名的属性,会自动映射
  • 且Mapstruct会自动进行类型的转换
@Test
public void test(){
CarDTO carDTO = new CarDTO();
//直接调用转换的方法,进行对象的转换
CarVO carVO = CarConvert.INSTANCE.abc(carDTO);
System.out.println(carVO)
}

@Mappings&@Mapping

//这是个对象转换的类
//其中Mapstruct整合spring需要添加一些属性在@Mapping(componentModel = "spring")
@Mapper(componentModel = "spring")
public abstract class CarConvert(){
//提供一个获取CarConvert对象实例的静态属性(整合spring,这句话就可以不需要了)
public static CarConvert INSTANCE = Mappers.getMapper(CarConvert.class)

@Mappings(
value = {
//指定金额的映射规则,数字格式化numberFormat,保留两位小数
@Mapping(source = "totalPrice",target = "totalPrice",numberFormat = "#.00"),
//指定日期的映射规则,日期格式化dateFormat
@Mapping(source = "publishDate",target = "publishDate",dateFormat = "yyyy-MM-dd HH:mm:ss"),
//color属性不想映射的设置
@Mapping(target = "color",ignore = true)
//属性名不同的映射
@Mapping(source = “brand”,target = “brandName”)
//对象里面包含,另一个对象的映射处理
@Mapping(source = “driverDTO”,target = “driverVO”)
}
)
//对象转换的方法对象转换的方法
public abstract CarVo abc(CarDTo carDTo);

//对象中 包含的对象 的转换方法
@Mapping(source = “id”,target = “driverId”)
@Mapping(source = “name”,target = “fullName”)
public abstract DriverVO driverDTO2driverVO(DriverDTO driverDTO);

//@AfterMapping 添加这个注解表示mapstruct在调用完自动转换的方法后,会自动调用本方法
//@MappingTarget 注解表示传过来的CarVO对象是已经赋值过的
@AfterMapping
public void dto2voAfter(CarDTO carDTO, @MappingTarget CarVO carVO){
List<PartDTO> partDTOS = carDTO.getPartDTOS();
boolean hasPart = partDTOS != null && !partDTOS.isEmpty();
carVO.setHasPart(hasPart);
}

//集合对象的批量转换
public abstract List<CarVo> dto2Vos(List<CarDTO> carDTO);

//@BeanMapping(ignoreByDefault = true) 表示开启 忽略mapstruct的默认映射行为,只有配置了@Mapping注解的才能映射
@BeanMapping(ignoreByDefault = true)
@Mapping(source = “id”,target = “id”)
public abstract VehicleVO carDTO2vehicleVO(CarDTO carDTO);
}

在Spring中使用

@Mapper

用@Mapper注解修饰一个转换类的接口。

@Mapper(componentModel = "spring")
public interface GoodsMapper {

@BeanMapping(ignoreByDefault = true)
@Mappings({
@Mapping(source = "goods.id", target = "id"),
@Mapping(source = "goods.goodsName", target = "goodsName"),
@Mapping(source = "goodsInformation.timeUnit", target = "timeUnit"),
@Mapping(source = "goodsInformation.exchangeTimes", target = "exchangeTimes"),
@Mapping(source = "goodsInformation.maxExchangeTimes", target = "maxExchangeTimes"),
})
GoodsVO toGoodsVO(Goods goods, GoodsInfo goodsInfon);
}

使用

在Service中注入Mapper

//注入
@Autowired
private GoodsMapper goodsMapper

...
//使用
goodsMapper.toGoodsVO(goods,goodsInfon);

在编译之后,会自动在编译文件中对接口进行实现。

其他使用方法同上(用法一)

报错

遇到如下报错问题时,有两种解决方案

Unknown property "xxx" in result type xxx. Did you mean "null"?

方案一

代码生成器annotationProcessor标签部分,将lombok放在mapstruct之前。

<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>
<encoding>UTF-8</encoding>
<!--自动生成代码annotationProcessor-->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

方案二

<!-- mapStruct-lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
<scope>provided</scope>
</dependency>

高级用法

@AfterMapping&@MappingTarget

在映射最后一步对属性的自定义映射做处理

//@AfterMapping 添加这个注解表示mapstruct在调用完自动转换的方法后,会自动调用本方法
//@MappingTarget 注解表示传过来的CarVO对象是已经赋值过的
@AfterMapping
public void dto2voAfter(CarDTO carDTO, @MappingTarget CarVO carVO){
List<PartDTO> partDTOS = carDTO.getPartDTOS();
boolean hasPart = partDTOS != null && !partDTOS.isEmpty();
carVO.setHasPart(hasPart);
}

@BeanMapping

  • ignoreByDefault:忽略mapstruct的默认映射行为。避免不需要的赋值、避免属性覆盖
//@BeanMapping(ignoreByDefault = true) 表示开启  
//忽略mapstruct的默认映射行为,只有配置了@Mapping注解的才能映射
@BeanMapping(ignoreByDefault = true)
@Mapping(source = “id”,target = “id”)
public abstract VehicleVO carDTO2vehicleVO(CarDTO carDTO);

@InheritConfiguration

  • 可以继承**@Mapping@BeanMapping@IterableMapping**的映射规则
  • 注解的方法上,有需要映射的字段,它会搜索有相同配置的映射,找到了直接复用此映射;若找到多个方法上都有满足此映射的配置,需要制定**@InheritConfiguration#name**的值,制定继承方法的映射。

更新的场景

避免同样的配置写多份

//@InheritConfiguration 会默认继承传过来的CarVO的映射规则,如果没有这个注解的话,应该该方法后,输出的值和传过来的是一样的(因为没有映射规则);加上注解后,才会被修改
//@MappingTarget 注解表示传过来的CarVO对象是已经赋值过的。
@InheritConfiguration
@Mapping(source = “id”,target = “id”)
public abstract void updateDto2vor(CarDTO carDTO, @MappingTarget CarVO carVO);

@InheritInverseConfiguration

当我们定义了一种对象到另一种对象的映射后,可以通过**@InheritInverseConfiguration **直接进行逆映射,只继承@Mapping注解配置,其他的注解配置不会继承。

//@InheritInverseConfiguration 逆映射注解
//name:指定使用哪一个方法的配置,写该方法的名字
@BeanMapping(ignoreByDefault = true)
@InheritInverseConfiguration(name = "carDTO2vehicleVO")
public abstract VehicleVO vehicleVOCarDTO2(CarDTO carDTO);

@InheritConfiguration的优先级高于@InheritInverseConfiguration。

共享配置

概述

@MapperConfig注解的接口就是共享配置,可以在@Mapper#config指定共享配置,@MapperConfig中的属性和@Mapper相同,@Mapper中的属性会覆盖@MapperConfig。

共享配置中可以设置原型映射,也可以是父类映射,再通过@MapperConfig、@Mapper的mappingInheritanceStrategy就可以实现原型映射的继承。

  • mappingInheritanceStrategy的值有:

    • MappingInheritanceStrategy.EXPLICIT

      默认,要想继承原型映射必须使用@InheritConfiguration或@InheritInverseConfiguration注解方法,且此方法的源类型和目标类型要能赋予原型映射类型;

    • MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG

      不需要@InheritConfiguration注解方法,只需要满足类型条件就能继承,但只能是正映射;

    • MappingInheritanceStrategy.AUTO_INHERIT_REVERSE_FROM_CONFIG

      不需要@InheritInverseConfiguration注解方法,只需要满足类型条件就能继承,但只能是逆映射;

    • MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG

      不需要@InheritInverseConfiguration注解方法,只需要满足类型条件就能继承,正/逆映射都可以。

实例

Base

 @Data
@ToString
public class BaseBO {

private Long id;

private String name;

private String createTimeString;
}


@MapperConfig
public interface BaseConfig {

@Mapping(target = "createTimeString", source = "createTime")
BaseBO toTestBaseBO(BasePO basePO);

}

MappingInheritanceStrategy.EXPLICIT

@Mapper(config = BaseConfig.class, mappingInheritanceStrategy = MappingInheritanceStrategy.EXPLICIT)
public interface TestMapper {

@InheritConfiguration
void updateBO(TestFourPO testFourPO, @MappingTarget TestSixBO testSixBO);

@InheritInverseConfiguration(name = "toTestBaseBO")
void updatePO(TestSixBO testSixBO, @MappingTarget TestFourPO testFourPO);

}

@Component
public class TestMapperImpl implements TestMapper {

@Override
public void updateBO(TestFourPO testFourPO, TestSixBO testSixBO) {
if ( testFourPO == null ) {
return;
}

if ( testFourPO.getCreateTime() != null ) {
testSixBO.setCreateTimeString( new SimpleDateFormat().format( testFourPO.getCreateTime() ) );
}
else {
testSixBO.setCreateTimeString( null );
}
testSixBO.setId( testFourPO.getId() );
testSixBO.setName( testFourPO.getName() );
}

@Override
public void updatePO(TestSixBO testSixBO, TestFourPO testFourPO) {
if ( testSixBO == null ) {
return;
}

try {
if ( testSixBO.getCreateTimeString() != null ) {
testFourPO.setCreateTime( new SimpleDateFormat().parse( testSixBO.getCreateTimeString() ) );
}
else {
testFourPO.setCreateTime( null );
}
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}
testFourPO.setId( testSixBO.getId() );
testFourPO.setName( testSixBO.getName() );
}
}

共享配置的原型映射并不会生成单独的实现方法。

虽然默认继承策略支持正/逆,但是引入共享配置,且mapper中正映射能继承原型映射的情况下,再设置逆映射方法,就必须制定name属性,否则同样有冲突报错。

MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG

 
@Mapper(config = BaseConfig.class, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG)
public interface TestMapper {

void updateBO(TestFourPO testFourPO, @MappingTarget TestSixBO testSixBO);

}

@Component
public class TestMapperImpl implements TestMapper {

@Override
public void updateBO(TestFourPO testFourPO, TestSixBO testSixBO) {
if ( testFourPO == null ) {
return;
}

if ( testFourPO.getCreateTime() != null ) {
testSixBO.setCreateTimeString( new SimpleDateFormat().format( testFourPO.getCreateTime() ) );
}
else {
testSixBO.setCreateTimeString( null );
}
testSixBO.setId( testFourPO.getId() );
testSixBO.setName( testFourPO.getName() );
}
}

MappingInheritanceStrategy.AUTO_INHERIT_REVERSE_FROM_CONFIG

 
@Mapper(config = BaseConfig.class, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface TestMapper {

void updatePO(TestSixBO testSixBO, @MappingTarget TestFourPO testFourPO);

}

@Component
public class TestMapperImpl implements TestMapper {

@Override
public void updatePO(TestSixBO testSixBO, TestFourPO testFourPO) {
if ( testSixBO == null ) {
return;
}

try {
if ( testSixBO.getCreateTimeString() != null ) {
testFourPO.setCreateTime( new SimpleDateFormat().parse( testSixBO.getCreateTimeString() ) );
}
else {
testFourPO.setCreateTime( null );
}
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}
testFourPO.setId( testSixBO.getId() );
testFourPO.setName( testSixBO.getName() );
}
}

MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG

 
@Mapper(config = BaseConfig.class, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface TestMapper {

void updateBO(TestFourPO testFourPO, @MappingTarget TestSixBO testSixBO);

void updatePO(TestSixBO testSixBO, @MappingTarget TestFourPO testFourPO);

}

@Component
public class TestMapperImpl implements TestMapper {

@Override
public void updateBO(TestFourPO testFourPO, TestSixBO testSixBO) {
if ( testFourPO == null ) {
return;
}

if ( testFourPO.getCreateTime() != null ) {
testSixBO.setCreateTimeString( new SimpleDateFormat().format( testFourPO.getCreateTime() ) );
}
else {
testSixBO.setCreateTimeString( null );
}
testSixBO.setId( testFourPO.getId() );
testSixBO.setName( testFourPO.getName() );
}

@Override
public void updatePO(TestSixBO testSixBO, TestFourPO testFourPO) {
if ( testSixBO == null ) {
return;
}

try {
if ( testSixBO.getCreateTimeString() != null ) {
testFourPO.setCreateTime( new SimpleDateFormat().parse( testSixBO.getCreateTimeString() ) );
}
else {
testFourPO.setCreateTime( null );
}
}
catch ( ParseException e ) {
throw new RuntimeException( e );
}
testFourPO.setId( testSixBO.getId() );
testFourPO.setName( testSixBO.getName() );
}
}