前言
hi,大家好,我是大鱼七成饱。
前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。
环境准备
由于日常使用都是spring,所以后面的示例都是在springboot框架中运行的。关键pom依赖如下:
java
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
场景一:常量转换
这是最简单的一个场景,比如需要设置字符串、整形和长整型的常量,有的又需要日期,或者新建类型。下面举个例子,演示如何转换
vbnet
//实体类
@Data
public class Source {
private String stringProp;
private Long longProp;
}
@Data
public class Target {
private String stringProperty;
private long longProperty;
private String stringConstant;
private Integer integerConstant;
private Long longWrapperConstant;
private Date dateConstant;
}
- 设置字符串常量
- 设置long常量
- 设置java内置类型默认值,比如date
那么mapper这么设置就可以
java
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface SourceTargetMapper {
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1l")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001L")
@Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2023-09-01-")
Target sourceToTarget(Source s);
}
解释下,constant用来设置常量值,source的值如果没有设置,则会使用defaultValue的值,日期可以按dateFormat解析。
Talk is cheap, show me the code !废话不多说,自动生成的转换类如下:
java
@Component
public class SourceTargetMapperImpl implements SourceTargetMapper {
public SourceTargetMapperImpl() {
}
public Target sourceToTarget(Source s) {
if (s == null) {
return null;
} else {
Target target = new Target();
if (s.getStringProp() != null) {
target.setStringProperty(s.getStringProp());
} else {
target.setStringProperty("undefined");
}
if (s.getLongProp() != null) {
target.setLongProperty(s.getLongProp());
} else {
target.setLongProperty(-1L);
}
target.setStringConstant("Constant Value");
target.setIntegerConstant(14);
target.setLongWrapperConstant(3001L);
try {
target.setDateConstant((new SimpleDateFormat("dd-MM-yyyy")).parse("09-01-2014"));
return target;
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
}
}
}
是不是一目了然
场景二:转换中调用表达式
比如id不存在使用UUID生成一个,或者使用已有参数新建一个对象作为属性。当然可以用after mapping,qualifiedByName等实现,感觉还是不够优雅,这里介绍个雅的(代码少点的)。
实体类如下:
typescript
@Data
public class CustomerDto {
public Long id;
public String customerName;
private String format;
private Date time;
}
@Data
public class Customer {
private String id;
private String name;
private TimeAndFormat timeAndFormat;
}
@Data
public class TimeAndFormat {
private Date time;
private String format;
public TimeAndFormat(Date time, String format) {
this.time = time;
this.format = format;
}
}
Dto转customer,加创建TimeAndFormat作为属性,mapper实现如下:
java
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = UUID.class)
public interface CustomerMapper {
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
@Mapping(target = "id", source = "id", defaultExpression = "java( UUID.randomUUID().toString() )")
Customer toCustomer(CustomerDto s);
}
解释下,id为空则走默认的defaultExpression,通过imports引入,java括起来调用。新建对象直接new TimeAndFormat。有的小伙伴喜欢用qualifiedByName自定义方法,可以对比下,哪个合适用哪个,都能调用转换方法。
生成代码如下:
java
@Component
public class CustomerMapperImpl implements CustomerMapper {
public CustomerMapperImpl() {
}
public Customer toCustomer(CustomerDto s) {
if (s == null) {
return null;
} else {
Customer customer = new Customer();
if (s.getId() != null) {
customer.setId(String.valueOf(s.getId()));
} else {
customer.setId(UUID.randomUUID().toString());
}
customer.setTimeAndFormat(new TimeAndFormat(s.getTime(), s.getFormat()));
return customer;
}
}
}
场景三:类共用属性,如何复用
比如下面的Bike和车辆类,都有id和creationDate属性,我又不想重复写mapper属性注解
arduino
public class Bike {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 品牌
*/
private String brandName;
}
public class Car {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 车牌号
*/
private String chepaihao;
}
解决起来很简单,写个共用的注解,使用的时候引入就可以,示例如下:
less
//通用注解
@Retention(RetentionPolicy.CLASS)
//自动生成当前日期
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//忽略id
@Mapping(target = "id", ignore = true)
public @interface ToEntity { }
//使用
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface TransportationMapper {
@ToEntity
@Mapping( target = "brandName", source = "brand")
Bike map(BikeDto source);
@ToEntity
@Mapping( target = "chepaihao", source = "plateNo")
Car map(CarDto source);
}
这里Retention修饰ToEntity注解,表示ToEntity注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,辅助生成mapper实现类。上面定义了creationDate和id的转换规则,新建日期,忽略id。
生成的mapper实现类如下:
typescript
@Component
public class TransportationMapperImpl implements TransportationMapper {
public TransportationMapperImpl() {
}
public Bike map(BikeDto source) {
if (source == null) {
return null;
} else {
Bike bike = new Bike();
bike.setBrandName(source.getBrand());
bike.setCreationDate(new Date());
return bike;
}
}
public Car map(CarDto source) {
if (source == null) {
return null;
} else {
Car car = new Car();
car.setChepaihao(source.getPlateNo());
car.setCreationDate(new Date());
return car;
}
}
}
坚持一下,还剩俩场景,剩下的俩更有意思
场景四:lombok和mapstruct冲突了
啥冲突?用了builder注解后,mapstuct转换不出来了。哎,这个问题困扰了我那同事两天时间。
解决方案如下:
xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
加上lombok-mapstruct-binding就可以了,看下生成的效果:
kotlin
@Builder
@Data
public class Person {
private String name;
}
@Data
public class PersonDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PersonMapper {
Person map(PersonDto dto);
}
@Component
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}
public Person map(PersonDto dto) {
if (dto == null) {
return null;
} else {
Person.PersonBuilder person = Person.builder();
person.name(dto.getName());
return person.build();
}
}
}
从上面可以看到,mapstruct匹配到了lombok的builder方法。
场景五:说个难点的,转换的时候,如何注入springBean
有时候转换方法比不是静态的,他可能依赖spring bean,这个如何导入?
这个使用需要使用抽象方法了,上代码:
kotlin
@Component
public class SimpleService {
public String formatName(String name) {
return "您的名字是:" + name;
}
}
@Data
public class Student {
private String name;
}
@Data
public class StudentDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class StudentMapper {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.formatName(source.getName()))")
public abstract StudentDto map(StudentDto source);
}
接口是不支持注入的,但是抽象类可以,所以采用抽象类解决,后面expression直接用皆可以了,生成mapperimpl如下:
scala
@Component
public class StudentMapperImpl extends StudentMapper {
public StudentMapperImpl() {
}
public StudentDto map(StudentDto source) {
if (source == null) {
return null;
} else {
StudentDto studentDto = new StudentDto();
studentDto.setName(this.simpleService.formatName(source.getName()));
return studentDto;
}
}
}
思考
以上场景肯定还有其他解决方案,遵循合适的原则就可以。驾驭不了的代码,可能带来更多问题,先简单实现,后续在迭代优化可能适合更多的业务场景。
另外文章中的示例代码地址是:github.com/dayuqicheng...