Mapstruct的具体介绍
文章目录
问题
微服务架构下,实例类基本上会拆分成 VO、DTO、Entity
三类 POJO,各个公司的规范大同小异
VO: 用于前端接口参数传递,例如用于 http 接口接收请求参数,或者接口返回参数
DTO:用于 rpc 接口参数传递。单独定义,或者继承扩展其他 rpc 接口的 DTO,或者上线文内使用
Entity(PO): 用于 orm 映射处理,与表结构对应,只在服务内部使用,不能对外使用和暴露
往往在使用的时候会涉及到实体和实体之间的转换
前言
官方文档往往是最好的学习参考资料
官网地址:https://mapstruct.org/documentation/stable/reference/html/
一、MapStruct是什么?
MapStruct 是一个 Java 注解处理器,用于生成类型安全的 Bean 映射类。
您所要做的就是定义一个映射器接口,该接口声明任何必需的映射方法。在编译过程中,MapStruct 将生成此接口的实现。此实现使用纯 Java 方法调用在源对象和目标对象之间进行映射,即无反射或类似。
与手动编写映射代码相比,MapStruct 通过生成繁琐且容易出错的代码来节省时间。遵循约定而不是配置方法,MapStruct 使用合理的默认值,但在配置或实现特殊行为时会不妨碍您。
与动态映射框架相比,MapStruct 具有以下优势:
- 通过使用普通方法调用而不是反射来快速执行
- 编译时类型安全:只能映射相互映射的对象和属性,不会意外地将订单实体映射到客户实体等。
- 在构建时清除错误报告
- 如果映射不完整(并非所有目标属性都映射)
- 映射不正确(找不到正确的映射方法或类型转换)
二、依赖maven
...
<properties>
<org.mapstruct.version>1.5.5.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>
...
<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>
...
三、定义映射器
3.1 基本映射
要创建映射器,只需使用所需的映射方法定义一个 Java 接口,然后使用 org.mapstruct.Mapper
注释对其进行注释:
/**
* 1. 用于像静态方式使用
* 2. 用于spring方式注入使用
*/
@Mapper
public interface CarMapper {
// 用于静态导入
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
Car carCovertCarVO(CarVO carVO);
}
@Mapper(componentModel = "spring") //用于spring方式注入
public interface CarMapper {
// 用于静态导入
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
Car carCovertCarVO(CarVO carVO);
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Car implements Serializable {
private Long id;
private String name;
private Integer age;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CarVO implements Serializable {
private Long id;
private String name;
private Integer age;
}
效果
public class CarMapperImpl implements CarMapper {
@Override
public Car carCovertCarVO(CarVO carVO) {
if ( carVO == null ) {
return null;
}
Car.CarBuilder car = Car.builder();
car.id( carVO.getId() );
car.name( carVO.getName() );
car.age( carVO.getAge() );
return car.build();
}
}
3.2 名称对不上的映射
名称对不上的映射需要使用target
代码需要转换后的值,source
代表源数据
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CarDTO implements Serializable {
private Long ids;
private String userName;
}
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
Car carCovertCarVO(CarVO carVO);
@Mappings({
@Mapping(target = "id", source = "ids"),
@Mapping(target = "name", source = "userName")
})
Car carCovertCarDTO(CarDTO carDTO);
}
最后效果
@Override
public Car carCovertCarDTO(CarDTO carDTO) {
if ( carDTO == null ) {
return null;
}
Car.CarBuilder car = Car.builder();
car.id( carDTO.getIds() );
car.name( carDTO.getUserName() );
return car.build();
}
@Mapper 注释会导致 MapStruct 代码生成器在构建时创建 CarMapper 接口的实现。
在生成的方法实现中,源类型(例如 Car )中的所有可读属性都将复制到目标类型(例如 CarDto )中的相应属性中:
当属性与其目标实体对应项具有相同的名称时,它将被隐式映射。
必须在 @Mapping 注释中指定 JavaBeans 规范中定义的属性名称,例如,对于具有访问器方法 getSeatCount() 和 setSeatCount() 的属性,必须指定 seatCount。
通过 @BeanMapping(ignoreByDefault = true) ,默认行为将是显式映射,这意味着所有映射都必须通过 @Mapping 指定,并且不会对缺少的目标属性发出警告。这允许忽略所有字段,但通过 @Mapping 显式定义的字段除外
3.3 list对象的映射
@Mappings({
@Mapping(target = "id", source = "ids"),
@Mapping(target = "name", source = "userName")
})
@Named("carCovertCarDTO")
Car carCovertCarDTO(CarDTO carDTO);
// 普通list转换
List<Car> carCovertCarVOList(List<CarVO> carVO);
// list自定义转换方式
@IterableMapping(qualifiedByName = "carCovertCarDTO")
List<Car> carCovertCarDTOList (List<CarDTO> carVO);
效果
@Override
public List<Car> carCovertCarVOList(List<CarVO> carVO) {
if ( carVO == null ) {
return null;
}
List<Car> list = new ArrayList<Car>( carVO.size() );
for ( CarVO carVO1 : carVO ) {
list.add( carCovertCarVO( carVO1 ) );
}
return list;
}
@Override
public List<Car> carCovertCarDTOList(List<CarDTO> carVO) {
if ( carVO == null ) {
return null;
}
List<Car> list = new ArrayList<Car>( carVO.size() );
for ( CarDTO carDTO : carVO ) {
list.add( carCovertCarDTO( carDTO ) );
}
return list;
}
3.4 更多基础映射
// 多参数映射
// 具有多个源参数的映射方法
@Mapper
public interface AddressMapper {
@Mapping(target = "description", source = "person.description")
@Mapping(target = "houseNumber", source = "address.houseNo")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
// 直接引用源参数的映射方法
@Mapper
public interface AddressMapper {
@Mapping(target = "description", source = "person.description")
@Mapping(target = "houseNumber", source = "hn")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
// 如果不想显式命名嵌套源 Bean 中的所有属性,则可以使用 . 作为目标。这将告诉 MapStruct 将每个属性从源 Bean 映射到目标对象。示例如下
// 使用“Target This”注释”。
// 生成的代码将直接将每个属性从 CustomerDto.record 映射到 Customer ,而无需手动命名其中任何一个。 Customer.account 也是如此。
@Mapper
public interface CustomerMapper {
@Mapping( target = "name", source = "record.name" )
@Mapping( target = ".", source = "record" )
@Mapping( target = ".", source = "account" )
Customer customerDtoToCustomer(CustomerDto customerDto);
}
// 当存在冲突时,可以通过显式定义映射来解决这些问题。
// 例如,在上面的例子中。 name 出现在 CustomerDto.record 和 CustomerDto.account 中。
// 映射 @Mapping( target = "name", source = "record.name" ) 解决了此冲突。
3.5 高级映射
这里介绍了几个高级选项,这些选项允许根据需要微调生成的映射代码的行为
// 例:具有默认值和常量的映射方法
@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001")
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
@Mapping(target = "stringListConstants", constant = "jack-jill-tom")
Target sourceToTarget(Source s);
}
// Expressions 表达式
// 使用表达式的映射方法
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "timeAndFormat", expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}
// 该示例演示如何将源属性 time 和 format 组合成一个目标属性 TimeAndFormat 。
// 请注意,指定了完全限定的包名称,因为 MapStruct 不负责 TimeAndFormat 类的导入
// (除非在 SourceTargetMapper 类中以其他方式显式使用)。这可以通过在 @Mapper 注释上定义 imports 来解决。
// 例如
@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "timeAndFormat", expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}
// 默认表达式
// 使用默认表达式的映射方法
@Mapper( imports = UUID.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
Target sourceToTarget(Source s);
}
// 使用项目中某个工具类的某个方法
// 例:使用StringUtils isEmptyIfStr方法
public class StringUtils{
public static boolean isEmptyIfStr(Object obj) {
if (null == obj) {
return true;
} else if (obj instanceof CharSequence) {
return 0 == ((CharSequence)obj).length();
} else {
return false;
}
}
}
@Mapper( imports = StringUtils.class ) // 这里引入class
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target="id", source="sourceId", defaultExpression = "java( StringUtils.isEmptyIfStr() )")
Target sourceToTarget(Source s);
}
// 指定水果映射的子类映射
@Mapper
public interface FruitMapper {
// 当输入类型和结果类型都具有继承关系时,您希望将正确的专用化映射到匹配的专用化。
// 假设一个 Apple 和一个 Banana ,它们都是 Fruit 的子类
@SubclassMapping( source = AppleDto.class, target = Apple.class )
@SubclassMapping( source = BananaDto.class, target = Banana.class )
Fruit map( FruitDto source );
}
// 条件映射
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
@Condition
default boolean isNotEmpty(String value) {
return value != null && !value.isEmpty();
}
}
// 生成的条件映射
public class CarMapperImpl implements CarMapper {
@Override
public CarDto carToCarDto(Car car) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
if ( isNotEmpty( car.getOwner() ) ) {
carDto.setOwner( car.getOwner() );
}
// Mapping of other properties
return carDto;
}
}
// 带@BeforeMapping和@AfterMapping
// 在自定义映射器时,装饰器可能并不总是满足需求。
// 例如,如果不仅需要对少数选定方法执行自定义,还需要对映射特定超类型的所有方法执行自定义:
// 在这种情况下,可以使用在映射开始之前或映射完成后调用的回调方法。
@Mapper
public abstract class VehicleMapper {
@BeforeMapping
protected void flushEntity(AbstractVehicle vehicle) {
// I would call my entity manager's flush() method here to make sure my entity
// is populated with the right @Version before I let it map into the DTO
}
@AfterMapping
protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
}
public abstract CarDto toCarDto(Car car);
}
public class VehicleMapperImpl extends VehicleMapper {
public CarDto toCarDto(Car car) {
flushEntity( car );
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
// attributes mapping ...
fillTank( car, carDto );
return carDto;
}
}
问题
mapstruct编译后的实现类未进行get Set处理
public class CarMapperImpl implements CarMapper {
@Override
public Car carCovertCarVO(CarVO carVO) {
if ( carVO == null ) {
return null;
}
Car.CarBuilder car = Car.builder();
return car.build();
}
}
解决方案:
项目中是否引入了lombok ,如果引入了lombok,就是lombok版本和mapstruct版本不一致造成的,修改版本即可
从MapStruct 1.2.0. Beta1和Lombok 1.16.14版本开始,并分两种情况
1、如果您使用的是Lombok 1.18.16或更新版本,您还需要添加lombok-mapstruct-binding才能使Lombok和MapStruct一起工作。
2、如果您使用的是较旧的MapStruct或Lombok版本,解决方案是将要由Lombok修改的JavaBeans和要由MapStruct处理的映射器接口分成您项目的两个单独模块。然后Lombok将在第一个模块的编译期间运行,在第二个模块的编译期间运行MapStruct时,使bean类完整。
3. 或者升级mapStuct版本,我从1.5.3.final升级到1.5.5.final解决
END: 本文仅仅简单介绍了mapstruct的使用,更多的往往还是需要看官网的例子