mapstruct这么用,同事也开始模仿

小菜鸟 1年前 (2023-11-07) 阅读数 956 #编程日记
文章标签 后端

前言

hi,大家好,我是大鱼七成饱。

前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。

1641341087201917.jpeg

环境准备

由于日常使用都是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); } } } }

是不是一目了然

image-20231105105234857.png

场景二:转换中调用表达式

比如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; } } }

坚持一下,还剩俩场景,剩下的俩更有意思

image-20231105111309795.png

场景四: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

image-20231105112031297.png 有时候转换方法比不是静态的,他可能依赖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…

image-20231105112515470.png

文章源地址:https://juejin.cn/post/7297222349731627046
热门
标签列表