0 前言
业务变的越来越庞大复杂后,整个业务也被划分为很多层级功能,各层级功能各司其职,共同实现业务目标。代表各层级的数据对象如PO、DAO、DTO、VO、BO、QO等在这些层级间传递、转换、提取、组合和维护管理。所以这些数据对象之间的映射在业务主干数据流程中出现的非常频繁,也出现了非常多的对象映射组件,如apache BeanUtils、spring BeanUtils、CGLIB BeanCopier、Dozer、Orika、ModelMapper、JMapper、Selma等。
mapstruct由于基于注解和注解处理器在编译期自动生成java代码文件,编译后直接调用java bean的getter()
、setter()
方法,实现bean的字段提取和赋值操作。跟其他bean mapping组件相比,mapstruct无需进行反射、运行时生成字节码等操作,效果上相当于手工编写代码,只是mapstruct把代码编码过程自动化了。所以mapstruct在性能上也基本相当于人工编写代码(只细微低于),但是节省了工程师大量的时间,也避免了低级错误发生的概率。由于mapstruct在编译期生成代码文件,我们甚至可以对生成的文件进一步进行手工修改,以追求极致预期,但是我没看到需要这样做的场景。同时mapstruct考虑各种应用场景,提供了非常丰富的操作。
1 基本功能介绍
1.1 package依赖
要使用mapstruct,需要引入它的package依赖:
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
同时,由于mapstruct是基于注解处理器在编译期自动生成代码的,除了引入mapstruct的基本功能包依赖,还需要配置相应的compiler注解处理器。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
由于项目中使用lombok,lombok也是基于注解处理器在编译期自动生成class文件字节码的,所以同时配置了lombok和lombok-mapstruct-binding。
PS:
- 据个人一点经验,如果修改了java bean或mapstruct的mapper文件内容,再次编译前一定要先执行mvn clean将之前的package内容清除掉,否则会编译报错或生成的java文件功能不符合预期。
- java注解处理器的使用有点麻烦,注解处理器功能也是我们编写的代码,需要先编译成字节码再处理注解,但是编译时处理注解又要能找到注解处理器的字节码,是一个鸡生蛋、蛋生鸡的问题。如果项目中有多个注解处理器,它们之间可能会相互影响,比如项目中同时有lombok和mapstruct,一定要配置一个lombok-mapstruct-binding。如果项目中还有其他的使用了注解处理器的组件,需要特别留意这类问题。
- 看到网上有人介绍踩的坑,说lombok的注解处理器配置需要写在mapstruct之前,虽然看不出其中的逻辑,大家如果遇到一些奇怪的问题,也可以参考一下
1.2 一个简单的例子
先定义一个SrcObj和DstObj类:
package com.javatest.mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SrcObj {
private String name;
private int age;
private double x;
private String y;
}
package com.javatest.mapstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DstObj {
private String name;
private Integer age;
private String x;
private Double y;
}
然后定义一个mapstruct映射类
package com.javatest.mapstruct;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface BaseMapper {
BaseMapper MAPPER = Mappers.getMapper(BaseMapper.class);
DstObj toDst(SrcObj src);
}
@Mapper
注解是mapstruct定义的一个注解,mapstruct在编译期扫描所有的添加了这个注解的接口(也可以是抽象类),会自动生成一个这个接口的实现类。BaseMapper MAPPER = Mappers.getMapper(BaseMapper.class)
则是获取这个接口实现类实例的静态接口,它可以放在任何地方,只是为了代码管理方便,一般放在mapstruct映射接口里。
可以看到,实现从SrcObj映射到DstObj的功能,我们不需要写任何代码,只要定义一个接口就可以了(跟spring data repository类似)。下面我们看下测试代码和结果:
package com.javatest.mapstruct;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@Slf4j
@SpringBootApplication
public class MapstructApplication {
public static void main(String[] args) {
SpringApplication.run(MapstructApplication.class, args);
}
@Bean
@SuppressWarnings("unchecked")
public CommandLineRunner runner() {
return (args) -> {
SrcObj src = new SrcObj("john", 30, 80.6D, "3.5");
DstObj dst = BaseMapper.MAPPER.toDst(src);
log.info("src: {}", new ObjectMapper().writeValueAsString(src));
log.info("dst: {}", new ObjectMapper().writeValueAsString(dst));
};
}
}
编译执行上面代码后,打印结果如下:
2021-11-10 16:38:51.224 INFO 99570 --- [ main] c.j.mapstruct.MapstructApplication : src: {"name":"john","age":30,"x":80.6,"y":"3.5"}
2021-11-10 16:38:51.225 INFO 99570 --- [ main] c.j.mapstruct.MapstructApplication : dst: {"name":"john","age":30,"x":"80.6","y":3.5}
同时我们可以在项目的编译打包结果目录target/generated-sources/annotations/com/javatest/mapstruct
下看到mapstruct自动生成的接口实现文件BaseMapperImpl.java
,文件内容为:
package com.javatest.mapstruct;
import com.javatest.mapstruct.DstObj.DstObjBuilder;
import javax.annotation.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-11-10T16:38:48+0800",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_292 (Azul Systems, Inc.)"
)
public class BaseMapperImpl implements BaseMapper {
@Override
public DstObj toDst(SrcObj src) {
if ( src == null ) {
return null;
}
DstObjBuilder dstObj = DstObj.builder();
dstObj.name( src.getName() );
dstObj.age( src.getAge() );
dstObj.x( String.valueOf( src.getX() ) );
if ( src.getY() != null ) {
dstObj.y( Double.parseDouble( src.getY() ) );
}
return dstObj.build();
}
}
1.3 基础功能
从 1.2 节的例子我可以看到,mapstruct不仅自动实现了SrcObj和DstObj两个bean之间相同类型相同名字的字段间的拷贝,同时还自动支持基本数据类型(int)与它对应的装箱(boxing)类型以及基本数据类型与String类型间的自动类型转换。当然,非常不建议实际项目在java bean中定义int、double等基本数据类型的字段,这里只是做个功能演示。
事实上,mapstruct支持以下情况的自动类型转换
1、基本数据类型与它对应的装箱类型之间相互转换。
2、不同基本数据类型(及装箱类型)之间相互转换,如int与long,int与Long。
2、String类型与基本数据类型(及装箱类型)之间相互转换
3、Enum类型与String类型的Enum名字之间相互转换
1.4 字段名字不同
映射方法上添加@Mapping
注解,指定源和目的字段名字即可,如下所示:
@Mapper
public interface BaseMapper {
@Mapping(source = "aliasName", target = "name")
DstObj toDst(SrcObj src);
}
1.5 ignore指定字段不映射
@Mapper
public interface BaseMapper {
@Mapping(source = "aliasName", target = "name")
@Mapping(source = "x", target = "x", ignore = true)
DstObj toDst(SrcObj src);
}
1.6 多个源对象映射到一个对象
mapstruct支持从多个源对象映射到一个目的对象(组合多个源对象的信息),只需要针对多个源对象中字段名字相同的字段明确指定使用哪个源对象的值即可,其他不冲突的字段跟一对一映射时处理一样。
@Mapper
public interface BaseMapper {
// 将src1对象中的name字段映射到DstObj,忽略src2中name字段的值
@Mapping(source = "src1.name", target = "name")
DstObj toDst(SrcObj src1, Src2Obj src2);
}
1.7 源对象成员字段的字段直接映射到目的字段中
@Mapper
public interface BaseMapper {
// subObj是SrcObj对象中的一个字段,它有两个字段名字分别为x和y,分别映射到DstObj的字段x和y
@Mapping(source = "subObj.x", target = "x")
@Mapping(source = "subObj.y", target = "y")
DstObj toDst(SrcObj src);
}
1.8 更新现有目标对象的值,而不是创建一个新的实例
@Mapper
public interface BaseMapper {
// 使用 @MappingTarget 注解即可
void toDst(SrcObj src, @MappingTarget DstObj dst);
}
1.9 集合间的映射
直接支持,什么都不用做
@Mapper
public interface BaseMapper {
List<DstObj> toDstList(List<SrcObj> src);
Set<DstObj> toDstSet(Set<SrcObj> src);
Set<DstObj> toDstSet(List<SrcObj> src);
Map<String, DstObj> toDstMap(Map<String, SrcObj> src);
}
2 类型转换
2.1 不同Enum类型之间的转换
不同的Enum类型之间转换时,相同的Enum名字值会直接映射,不同名字的值使用@ValueMapping
指定映射关系即可
public enum SrcEnum {
AA,
B,
C,
D
}
public class SrcObj {
private SrcEnum type;
}
public enum DstEnum {
A,
B,
C
}
public class DstObj {
private DstEnum type;
}
@Mapper
public interface BaseMapper {
@ValueMapping(source = "AA", target = "A")
// 找不到映射关系的,全部映射到C
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "C")
DstObj toDst(SrcObj src);
}
2.2 mapstruct mapper实现字段类型转换
如下所示,SrcObj有一个成员字段SubSrcObj sub
,DstObj有一个成员字段SubDstObj sub
,需要把SubSrcObj sub
内容映射到SubDstObj sub
public class SubSrcObj {
private String name;
}
public class SrcObj {
private SubSrcObj sub;
}
public class SubDstObj {
private String name;
}
public class DstObj {
private SubDstObj sub;
}
这种情况可以通过一下两种方式之一解决:
1)在同一个mapper接口定义字段类型转换方法
@Mapper
public interface BaseMapper {
SubDstObj toSubDst(SubSrcObj sub);
DstObj toDst(SrcObj src);
}
2)在不同mapper接口定义字段转换方法,然后通过uses映入依赖mapper,如下所示
@Mapper
public interface SubMapper {
SubDstObj toSubDst(SubSrcObj sub);
}
@Mapper(uses = {SubMapper.class})
public interface BaseMapper {
DstObj toDst(SrcObj src);
}
2.3 expression表达式转换
使用expression表达式可以调用任意java方法来进行映射。例如,实现上面一样的功能,自定义一个映射方法
package com.javatest.mapstruct;
public class SubSrcToDstConverter {
public static SubDstObj toDst(SubSrcObj src) {
return new SubDstObj(src.getName());
}
}
然后通过expression指定映射方法:
@Mapper
public interface BaseMapper {
@Mapping(target = "sub", expression = "java(com.javatest.mapstruct.SubSrcToDstConverter.toDst(src.getSub()))")
DstObj toDst(SrcObj src);
}
3 其他特性
3.1 为目标对象赋值常量
@Mapper
public interface BaseMapper {
// 不管src对象中是否有name字段,以及值是什么,目的对象name字段值始终为harry
@Mapping(target = "name", constant = "harry")
DstObj toDst(SrcObj src);
}
3.2 默认值
@Mapper
public interface BaseMapper {
// 如果src对象的name字段值为null,则为目的对象name字段值赋值harry
@Mapping(source = "name", target = "name", defaultValue = "harry")
DstObj toDst(SrcObj src);
}
3.3 mapstruct mapper为抽象类
前面介绍功能时,举的例子@Mapper注解都是注解在接口上,其实@Mapper
也可以注解抽象类,这样抽象类可以引入其他对象或实现其他功能,例如实现@BeforeMapping
,@AfterMapping
,让mapper在执行映射前后分别执行特定的功能。例子代码如下:
package com.javatest.mapstruct;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;
@Slf4j
@Mapper
public abstract class AbstractMapper {
@BeforeMapping
public void doBefore() {
log.info("before mapping");
}
@AfterMapping
public void doAfter() {
log.info("after mapping");
}
abstract DstObj toDst(SrcObj src);
}
3.4 spring环境下为mapper实例注册spring bean实例
在@Mapper
注解中直接提供componentModel
名字,即spring bean名字即可
@Mapper(componentModel = "baseMapper")
public interface BaseMapper {
DstObj toDst(SrcObj src);
}