MapStruct使用详解
什么是MapStruct?
MapStruct
是一个实体映射框架。我们经常需要在DO
、DTO
、VO
等模型对象之间做转换,比较常见的就是我们写converter
转换类。MapStruct可以简化转换操作,或者替代我们的converter
转换类。
开发中也很常见的就是用BeanUtils
来拷贝属性,但MapStruct
有比他们更高的效率。MapStruct
通过调用实体类的get()
、set()
方法实现赋值逻辑,这与我们手写代码的方式一致。BeanUtils
使用反射实现赋值,MapStruct
效率更高。
MapStruct
使用了Java Apt
技术,可以在代码编译时生成转换器类,当代码编译完成后,就可以在项目的sources/generated-classes/annotations
目录下看到转换器类的实现类。
不推荐用
BeanUtils
拷贝属性来做转换
除了上述所说的性能差(替换为Spring的BeanUtils可提高性能)之外最主要的就是BeanUtils是浅拷贝,很容易出问题。我们这里就发生或使用BeanUtils浅拷贝导致线上串数据的严重Bug。因为这些都是隐式实现 在代码CR的时候很容易被忽视,不多次测试也很难发现问题。
引入MapStruct依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>test_map</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.24</org.projectlombok.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<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>
<!--这里是为了解决和lombok的兼容问题-->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
注意:
lombok
也使用了Java Apt
技术,因此在同时使用两个工具时,需要在maven
或者gradle
里进行配置,否则会出现MapStruct
找不到lombok
生成的get()
、set()
方法的情况。这一配置在<annotationProcessorPaths>
中。
参考:https://github.com/mapstruct/mapstruct-examples/blob/main/mapstruct-lombok/pom.xml
MapStruct的基本使用
MapStruct
是实体映射框架,首先定义2个实体类:UserDO
、UserPO
@Getter
@Setter
@ToString(callSuper = true)
public class UserDo {
private Long userId;
private String userName;
}
@Getter
@Setter
public class UserPo {
private Long id;
private String name;
}
使用MapStruct
提供的注解,定义转换器接口UserConverter
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.mapstruct.Mapping;
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
UserDo po2do(UserPo po);
@Mapping(source = "userId", target = "id"),
@Mapping(source = "userName", target = "name")
UserPo do2po(UserDo ddo);
}
主函数:
public class BasicMain {
public static void main(String[] args) {
UserDo userDo = new UserDo();
userDo.setUserId(1111L);
userDo.setUserName("test");
System.out.println(userDo);
UserPo userPo = UserConverter.INSTANCE.do2po(userDo);
System.out.println(userPo);
UserDo userDo2 = UserConverter.INSTANCE.po2do(userPo);
System.out.println(userDo2);
}
}
运行结果为:
UserDo(super=testmapstruct.demo3.UserDo@15db9742, userId=1111, userName=test)
UserPo(super=testmapstruct.demo3.UserPo@3d4eac69, userId=1111, userName=test)
UserDo(super=testmapstruct.demo3.UserDo@42a57993, userId=1111, userName=test)
在converter
接口上使用了@Mapper
注解,将其声明为一个MapStruct
转换器。
然后在其中声明了两个接口方法:UserDo po2do(UserPo po);
和UserPo do2po(UserDo ddo);
这两个方法会被MapStruct
实现为对应的转换方法,实现方法会生成在项目的sources/generated-classes/annotations
目录下。声明方法时名称可以自定,只要保证入参和出参分别是目标的转换类即可。
在接口方法上,我们使用@Mapping
注解,声明两个实体类中属性的映射关系。其中source
属性用于指定源实体的属性名,即转换方法的入参实体的属性名。而target
属性用于指定目标实体的属性名,即转换方法的出参实体的属性名。
以其中一条举例:UserDo po2do(UserPo po) @Mapping(source = "id", target = "userId")
。这条注解的含义是,把传入的PO实体的id
属性赋值到一个新的DO的userId
属性中。这里暗含的一条约束是:id
和userId
属性要是同类型的。相对的,如果要在不同类型的属性间相互转换,则需要额外配置,后面会说。
可以看到,我们在接口中定义一个了一个属性INSTANCE
,通过调用Mappers.getMapper()
获取了一个UserConverter
的实例。要注意的是,作为接口的域,INSTANCE
默认带有public staitc final
修饰符,因此我们可以在外部通过UserConverter.INSTANCE
的方式访问这个域,并调用该转换器的转换方法。
以上就是
MapStruct
的简单使用了,可以满足大多数简单业务的转换逻辑,但是MapStruct
的功能远不如此,如果有兴趣可以看下面的详细使用。
MapStruct的详细使用教程
对属性名相同、类型相同的属性的赋值
这种情况下,不需要注解,转换方法会自动进行赋值。
对属性名不同、类型相同的属性的赋值
@Mapping(source = "id", target = "userId")
通过注解的声明源实体source
和目标实体target
的属性名
对属性名相同、类型不同的属性的赋值
这种情况下,需要通过在接口中编写default
方法的方式为两个实体中的对应字段做转换:
定义两个属性名相同类型不同的实体类:
@Getter
@Setter
@ToString(callSuper = true)
public class UserDo {
private Long userId;
private String userName;
private Long number;
}
// ------------------------------------------
@Getter
@Setter
@ToString(callSuper = true)
public class UserPo {
private Long id;
private String name;
private String number;
}
对应的转换器:
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
UserDo po2do(UserPo po);
@Mapping(source = "userId", target = "id")
@Mapping(source = "userName", target = "name")
UserPo do2po(UserDo ddo);
// 通过default方法自定义转换逻辑
default String num2str(Long num){
return num.toString()+" test";
}
// 通过default方法自定义转换逻辑
default Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
}
使用转换器:
public class BasicMain {
public static void main(String[] args) {
UserDo userDo = new UserDo();
userDo.setUserId(1111L);
userDo.setUserName("test");
userDo.setNumber(1111L);
System.out.println(userDo);
UserPo userPo = UserConverter.INSTANCE.do2po(userDo);
System.out.println(userPo);
UserDo userDo2 = UserConverter.INSTANCE.po2do(userPo);
System.out.println(userDo2);
}
}
输出:
UserDo(super=testmapstruct.demo3.UserDo@15db9742, userId=1111, userName=test, number=1111)
UserPo(super=testmapstruct.demo3.UserPo@3d4eac69, id=1111, name=test, number=1111 test)
UserDo(super=testmapstruct.demo3.UserDo@42a57993, userId=1111, userName=test, number=1111)
注意:这里为了展示,我特意在转换方法中增加了自定义转换逻辑(为字符串增加test
后缀)。实际上,对于Long
和String
这样的原生类型,MapStruct
可以自动转换。也就是说,如果没有特殊逻辑,即使不书写str2num
和num2str
两个默认方法,MapStruct
依然能够完成转换(MapStruct
会自动调用Long.toString()
和Long.valueOf(String)
方法)。
对属性名不同、类型不同的属性的赋值
这种情况下,需要通过在接口中编写default
方法的方式为两个实体中的对应字段做转换。并且还需要在方法之上通过@Mapping
注解来声明两个字段的对应关系。
定义两个字段名和类型都不同的实体类:
@Getter
@Setter
@ToString(callSuper = true)
public class UserDo {
private Long userId;
private String userName;
private Long number;
private Date date;
}
// ------------------------------------------------
@Getter
@Setter
@ToString(callSuper = true)
public class UserPo {
private Long id;
private String name;
private String number;
private Long timeStamp;
}
转换器:
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
@Mapping(source = "timeStamp", target = "date")
UserDo po2do(UserPo po);
@Mapping(source = "userId", target = "id")
@Mapping(source = "userName", target = "name")
@Mapping(source = "date", target = "timeStamp")
UserPo do2po(UserDo ddo);
default String num2str(Long num){
return num.toString()+" test";
}
default Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
default Date stamp2date(Long timeStamp) {
return new Date(timeStamp + 1000* 60 * 60L);
}
default Long date2stamp(Date date) {
return date.getTime() + 1000 * 60 * 60L;
}
}
使用转换器:
public class BasicMain {
public static void main(String[] args) {
UserDo userDo = new UserDo();
userDo.setUserId(1111L);
userDo.setUserName("test");
userDo.setNumber(1111L);
Long current = System.currentTimeMillis();
userDo.setDate(new Date(current));
System.out.println(current);
System.out.println(userDo);
UserPo userPo = UserConverter.INSTANCE.do2po(userDo);
System.out.println(userPo);
UserDo userDo2 = UserConverter.INSTANCE.po2do(userPo);
System.out.println(userDo2);
}
}
输出:
1664100691389
UserDo(super=testmapstruct.demo3.UserDo@15db9742, userId=1111, userName=test, number=1111, date=Sun Sep 25 18:11:31 CST 2022)
UserPo(super=testmapstruct.demo3.UserPo@232204a1, id=1111, name=test, number=1111 test, timeStamp=1664104291389)
UserDo(super=testmapstruct.demo3.UserDo@4aa298b7, userId=1111, userName=test, number=1111, date=Sun Sep 25 20:11:31 CST 2022)
指定赋值方法
有时在一个类中会出现多个A类别属性到B类型属性的赋值。如果这些赋值的逻辑一致,那么写一个default
方法甚至不写就可以完成赋值。
但如果这些对应关系中出现了多种不同的赋值逻辑,那么我们需要为这些赋值逻辑分别书写default
方法,并使用@Named
注解声明其名称,最后在对应的@Mapping
注解中通过qualifiedByName
属性声明使用到的default
方法的名称:
定义两个实体类:
@Getter
@Setter
@ToString(callSuper = true)
public class UserPo {
private Long id;
private String name;
private String number;
private Long timeStamp;
private Long preTimeStamp;
}
/*-----------------------------------------------*/
@Getter
@Setter
@ToString(callSuper = true)
public class UserDo {
private Long userId;
private String userName;
private Long number;
private Date date;
private Date preDate;
}
转换器:
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
@Mapping(source = "timeStamp", target = "date", qualifiedByName = "stamp2date1")
@Mapping(source = "preTimeStamp", target = "preDate", qualifiedByName = "stamp2date2")
UserDo po2do(UserPo po);
@Mapping(source = "userId", target = "id")
@Mapping(source = "userName", target = "name")
@Mapping(source = "date", target = "timeStamp", qualifiedByName = "date2stamp1")
@Mapping(source = "preDate", target = "preTimeStamp", qualifiedByName = "date2stamp2")
UserPo do2po(UserDo ddo);
default String num2str(Long num){
return num.toString()+" test";
}
default Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
@Named("stamp2date1")
default Date stamp2date(Long timeStamp) {
return new Date(timeStamp + 1000* 60 * 60L);
}
@Named("date2stamp1")
default Long date2stamp(Date date) {
return date.getTime() + 1000 * 60 * 60L;
}
@Named("stamp2date2")
default Date stamp2date2(Long timeStamp) {
return new Date(timeStamp - 1000* 60 * 60L);
}
@Named("date2stamp2")
default Long date2stamp2(Date date) {
return date.getTime() - 1000 * 60 * 60L;
}
}
使用转换器
public class BasicMain {
public static void main(String[] args) {
UserDo userDo = new UserDo();
Long current = System.currentTimeMillis();
userDo.setUserId(1111L);
userDo.setUserName("test");
userDo.setNumber(1111L);
userDo.setDate(new Date(current));
userDo.setPreDate(new Date(current));
System.out.println(current);
System.out.println(userDo);
UserPo userPo = UserConverter.INSTANCE.do2po(userDo);
System.out.println(userPo);
UserDo userDo2 = UserConverter.INSTANCE.po2do(userPo);
System.out.println(userDo2);
}
}
输出:
1664106359883
UserDo(super=testmapstruct.demo3.UserDo@15db9742, userId=1111, userName=test, number=1111, date=Sun Sep 25 19:45:59 CST 2022, preDate=Sun Sep 25 19:45:59 CST 2022)
UserPo(super=testmapstruct.demo3.UserPo@232204a1, id=1111, name=test, number=1111 test, timeStamp=1664109959883, preTimeStamp=1664102759883)
UserDo(super=testmapstruct.demo3.UserDo@4aa298b7, userId=1111, userName=test, number=1111, date=Sun Sep 25 21:45:59 CST 2022, preDate=Sun Sep 25 17:45:59 CST 2022)
注意:这个类里有2个Long到Date的转换逻辑,因此我首先书写了2组转换方法,然后通过@Named
注解为这两组方法分别命名,最后在@Mapping
注解里通过qualifiedByName
属性将方法与属性对应起来。控制台输出表明,两组属性确实按照我们希望的赋值逻辑进行了对应(UserPO
/UserDO
中的2个时间戳/Date
的值不同,且由于赋值逻辑的存在,其差值在拉大)。
如果愿意,可以只给第2组赋值方法用@Named
注解命名,而不给第1组赋值方法命名。同时在第1组@Mapping
里不使用qualifiedByName
指定方法。这样的话,MapStruct
会自动选择没有命名的赋值方法。
举例:
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
@Mapping(source = "timeStamp", target = "date")
@Mapping(source = "preTimeStamp", target = "preDate", qualifiedByName = "stamp2date2")
UserDo po2do(UserPo po);
@Mapping(source = "userId", target = "id")
@Mapping(source = "userName", target = "name")
@Mapping(source = "date", target = "timeStamp")
@Mapping(source = "preDate", target = "preTimeStamp", qualifiedByName = "date2stamp2")
UserPo do2po(UserDo ddo);
default String num2str(Long num){
return num.toString()+" test";
}
default Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
default Date stamp2date(Long timeStamp) {
return new Date(timeStamp + 1000* 60 * 60L);
}
default Long date2stamp(Date date) {
return date.getTime() + 1000 * 60 * 60L;
}
@Named("stamp2date2")
default Date stamp2date2(Long timeStamp) {
return new Date(timeStamp - 1000* 60 * 60L);
}
@Named("date2stamp2")
default Long date2stamp2(Date date) {
return date.getTime() - 1000 * 60 * 60L;
}
}
这种方式在以下场景中会比较有用:类A与类B的转换中有多个类P到类Q的赋值逻辑,且其中的大多数都使用同一种逻辑,仅有少数几种例外。
则此时可以用上面的方式实现这样的效果:为类P到类Q的转换定义一种通用逻辑,满足大多数情况;为其中的特殊情况定义特殊逻辑并通过命名的方式指定,实现精准定位。
但是,考虑到赋值逻辑的明确,建议在遇到这种有多个转换逻辑的场景时,对所有类P到类Q的转换都使用@Named
注解进行命名,增加代码可读性避免过多的隐式,避免后续维护时的疏忽。此外,如果为所有类P到类Q的转换方法进行了命名,那么在使用@Mapping
注解时是必须使用qualifiedByName
进行方法指定的,这可以视作一种错误提示。
对List的赋值
如果在ClassA
、ClassB
的转换器接口中已经实现了ClassA
到ClassB
的转换方法,那么不需要再为List<ClassA>
到List<ClassB>
额外书写转换逻辑,只需要声明一个对应的方法即可。MapStruct
会自动生成一个转换方法,通过foreach
的方式,逐个调用转换方法完成赋值:
@Mappings({
@Mapping(source = "userId", target = "id"),
@Mapping(source = "userName", target = "name"),
@Mapping(source = "date", target = "timeStamp", qualifiedByName = "date2stamp1"),
@Mapping(source = "preDate", target = "preTimeStamp", qualifiedByName = "date2stamp2")
})
UserPo do2po(UserDo ddo);
List<UserPo> dolist2polist(List<UserDo> doList);
使用抽象类定义转换器
除了接口,MapStruct
还支持使用抽象类来定义转换逻辑。
使用抽象类定义转换器的方式与使用接口基本相同,主要的区别在于Java语法层面对接口和抽象类的不同要求。具体有以下几点:
- 抽象类中不能使用
UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
来定义转换器实例,而应该使用public static final UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
来定义。 - 抽象类中的转换方法要以
abstract
修饰。 - 抽象类中的字段转换方法不能带
default
修饰符。
示例:
@Mapper
public abstract class UserConverter2 {
public static final UserConverter2 INSTANCE = Mappers.getMapper( UserConverter2.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
@Mapping(source = "timeStamp", target = "date", qualifiedByName = "stamp2date1")
@Mapping(source = "preTimeStamp", target = "preDate", qualifiedByName = "stamp2date2")
abstract UserDo po2do(UserPo po);
@Mappings({
@Mapping(source = "userId", target = "id"),
@Mapping(source = "userName", target = "name"),
@Mapping(source = "date", target = "timeStamp", qualifiedByName = "date2stamp1"),
@Mapping(source = "preDate", target = "preTimeStamp", qualifiedByName = "date2stamp2")
})
abstract UserPo do2po(UserDo ddo);
abstract List<UserDo> poList2doList(List<UserPo> poList);
abstract List<UserPo> doList2poList(List<UserDo> doList);
String num2str(Long num){
return num.toString()+" test";
}
Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
@Named("stamp2date1")
Date stamp2date(Long timeStamp) {
return new Date(timeStamp + 1000* 60 * 60L);
}
@Named("date2stamp1")
Long date2stamp(Date date) {
return date.getTime() + 1000 * 60 * 60L;
}
@Named("stamp2date2")
Date stamp2date2(Long timeStamp) {
return new Date(timeStamp - 1000* 60 * 60L);
}
@Named("date2stamp2")
Long date2stamp2(Date date) {
return date.getTime() - 1000 * 60 * 60L;
}
}
查看转换器实现类
上面提到,项目的sources/generated-classes/annotations
目录下能够看到MapStruct
对转换器的实现类。
通过查看可以发现,MapStruct
实际上是通过实现接口或继承抽象类并重写方法的方式,实现了赋值逻辑:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-02-20T21:13:28+0800",
comments = "version: 1.5.2.Final, compiler: javac, environment: Java 1.8.0_202 (Oracle Corporation)"
)
public class UserConverterImpl implements UserConverter {
/*------------------------------*/
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-02-20T21:13:28+0800",
comments = "version: 1.5.2.Final, compiler: javac, environment: Java 1.8.0_202 (Oracle Corporation)"
)
public class UserConverter2Impl extends UserConverter2 {
方法实现:
@Override
public UserDo po2do(UserPo po) {
if ( po == null ) {
return null;
}
UserDo userDo = new UserDo();
userDo.setUserId( po.getId() );
userDo.setUserName( po.getName() );
userDo.setDate( stamp2date( po.getTimeStamp() ) );
userDo.setPreDate( stamp2date2( po.getPreTimeStamp() ) );
userDo.setNumber( str2num( po.getNumber() ) );
return userDo;
}
查看MapStruct
的实现可知,MapStruct
的实现是通过get()
、set()
方法实现的赋值逻辑,因此具有比BeanUtils
等使用反射的框架更好的性能。
@Override
public List<UserDo> poList2doList(List<UserPo> poList) {
if ( poList == null ) {
return null;
}
List<UserDo> list = new ArrayList<UserDo>( poList.size() );
for ( UserPo userPo : poList ) {
list.add( po2do( userPo ) );
}
return list;
}
使用外部转换器
有时我们需要完成以下的转换关系:
这时可以先书写Aclass
的转换器,然后在书写Bclass
的转换器时引用并使用Aclass
的转换器:
@Getter
@Setter
@ToString(callSuper = true)
public class UserPo {
private Long id;
private String name;
private LoverPo loverPo;
}
/*-----------------------------------*/
@Getter
@Setter
@ToString(callSuper = true)
public class UserDo {
private Long userId;
private String userName;
private LoverDo loverDo;
}
/*-----------------------------------*/
@Getter
@Setter
@ToString(callSuper = true)
public class LoverPo {
private String loName;
private Integer loAge;
private String num;
}
/*-----------------------------------*/
@Getter
@Setter
@ToString(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public class LoverDo {
private String loverName;
private int loverAge;
private long num;
}
转换器:
@Mapper(
uses = {
LoverConverter.class,
}
)
public interface UserConverter {
public static final UserConverter INSTANCE = Mappers.getMapper( UserConverter.class );
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
@Mapping(source = "loverPo", target = "loverDo", qualifiedByName = {"convertLover", "po2do"})
abstract UserDo po2do(UserPo po);
@Mappings({
@Mapping(source = "userId", target = "id"),
@Mapping(source = "userName", target = "name"),
@Mapping(target = "loverPo", source = "loverDo", qualifiedByName = {"convertLover", "do2po"}),
})
UserPo do2po(UserDo ddo);
}
/*-----------------------------------*/
@Mapper
@Named("convertLover")
public interface LoverConverter {
public static final LoverConverter INSTANCE = Mappers.getMapper( LoverConverter.class );
@Named("po2do")
@Mappings({
@Mapping(source = "loName", target = "loverName"),
@Mapping(source = "loAge", target = "loverAge")
})
LoverDo po2do(LoverPo po);
@Named("do2po")
@InheritInverseConfiguration
LoverPo do2po(LoverDo ddo);
default String num2str(Long num){
return num.toString()+" test";
}
default Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
}
使用转换器:
public class BasicMain {
public static void main(String[] args) {
UserDo userDo = new UserDo();
userDo.setUserId(1111L);
userDo.setUserName("test");
userDo.setLoverDo(new LoverDo("kevy", 26, 3));
System.out.println(userDo);
UserPo userPo = UserConverter.INSTANCE.do2po(userDo);
System.out.println(userPo);
UserDo userDo2 = UserConverter.INSTANCE.po2do(userPo);
System.out.println(userDo2);
}
}
输出:
UserDo(super=testmapstruct.demo3.UserDo@15db9742, userId=1111, userName=test, loverDo=LoverDo(super=testmapstruct.demo3.LoverDo@6d06d69c, loverName=kevy, loverAge=26, num=3))
UserPo(super=testmapstruct.demo3.UserPo@42a57993, id=1111, name=test, loverPo=LoverPo(super=testmapstruct.demo3.LoverPo@75b84c92, loName=kevy, loAge=26, num=3 test))
UserDo(super=testmapstruct.demo3.UserDo@6bc7c054, userId=1111, userName=test, loverDo=LoverDo(super=testmapstruct.demo3.LoverDo@232204a1, loverName=kevy, loverAge=26, num=3))
想要达成在一个转换器中使用另一个转换器的目的,首先需要在定义被使用的转换器时为他的类和转换方法分别使用@Named
注解进行命名;然后要在使用转换器的转换器的@Mapper
注解上使用uses
属性指定用到的外部转换器;最后要在转换方法的@Mapping
注解上通过qualifiedByName
属性声明使用到的方法。
要注意的是,我们要同时声明被使用的类和被使用的方法,并把这两者以列表的方式组合在一起(即:以{a, b}
的方式组合起来)。
要注意的是,被使用的外部转换器可以是接口格式的MapStruct
转换器,也可以是抽象类格式的MapStruct
转换器,甚至可以是非MapStruct
转换器类的普通转换类,只要他能够提供转换方法,且类和方法上都被@Named
标注。
抽象类格式的MapStruct
转换器:
@Mapper
@Named("convertLover2")
public abstract class LoverConverter2 {
public static final LoverConverter2 INSTANCE = Mappers.getMapper( LoverConverter2.class );
@Named("po2do")
@Mappings({
@Mapping(source = "loName", target = "loverName"),
@Mapping(source = "loAge", target = "loverAge")
})
abstract LoverDo po2do(LoverPo po);
@Named("do2po")
@Mappings({
@Mapping(source = "loverName", target = "loName"),
@Mapping(source = "loverAge", target = "loAge")
})
abstract LoverPo do2po(LoverDo ddo);
String num2str(Long num){
return num.toString()+" test";
}
Long str2num(String str){
return Long.valueOf(str.split(" ")[0]);
}
}
对UserConverter
进行修改,通过将@Mapper
注解的uses
属性及@Mapping
注解的qualifiedByName
属性修改为与@Named
对应的名称,即可使用LoverConverter2
转换器。
调用普通转换类(必须标注@Named
注解):
@Named("convertLover3")
public class LoverConverter3 {
@Named("do2po")
public LoverPo do2po(LoverDo ddo) {
return new LoverPo("lay", 30, "2");
}
@Named("po2do")
public LoverDo do2po(LoverPo po) {
return new LoverDo("lay", 30, 2);
}
}
MapStruct的进阶使用
@Mappings注解
通过使用@Mappings
注解,可以把@Mapping
注解聚拢到一起。
举例:
@Mappings({
@Mapping(source = "userId", target = "id"),
@Mapping(source = "userName", target = "name"),
@Mapping(source = "date", target = "timeStamp", qualifiedByName = "date2stamp1"),
@Mapping(source = "preDate", target = "preTimeStamp", qualifiedByName = "date2stamp2")
})
UserPo do2po(UserDo ddo);
省略赋值
如果不书写某对字段间的关系,且这对字段在转换器内部找不到合适的转换方法,那么就不会发生赋值。
相对的,可以在书写@Mapping
时指定字段并使用ignore
属性,让MapStruct
不为这对字段赋值。有些时候,字段会自动赋值,如名称相同且类别相同或名称相同类别不同但存在可用的默认转换方法。这时使用ignore
就可以忽略对这对字段的赋值。
举例:
@Mapping(source = "age1", target = "age2", ignore = true)
反向赋值
上面的代码中,我们都是为A->B
和B->A
的转换方法分别书写@Mapping
映射关系。但很多场景下,在书写完一个方向的转换逻辑后,另一个方向的逻辑就显而易见了,此时可以使用@InheritInverseConfiguration
注解,就可以让MapStruct
自动生成对应的映射。
但需要注意的是,反向逻辑必须是简易可推导的。也就是说,如果正向逻辑中存在复杂逻辑,比如需要在书写@Mapping
注解时通过qualifiedByName
属性声明转换方法,那么@InheritInverseConfiguration
不会为反向逻辑中的该字段生成映射关系,因为他找不到对应的方法。此时需要在使用@InheritInverseConfiguration
注解后再额外通过@Mapping
注解对那些特殊映射关系进行声明。
链式法则
当存在对象嵌套对象的情况时,可以使用链式法则的方式进行赋值:
@Mapping(source = "ddo.lover.age", target = "loverAge")
UserPo do2po(UserDo ddo);
上面的@Mapping 注解可以解释为如下的Java语句:
po.setLoverAge(ddo.getLover().getAge())
日期格式化
在使用@Mapping
注解编写字段映射时,可以通过使用dateFormat
属性,指定字符串到Date
的转换逻辑,从而替代繁琐的default
方法。
数字格式化
与上面类似的,对于字符串到数字的逻辑,也可以使用@Mapping
注解的numberFormat
属性。
常量
通过指定@Mapping
注解的constant
属性,可以为目标字段指定固定赋值。
默认值
通过指定@Mapping
注解的defaultValue
属性,可以为目标字段指定源字段为null
时的赋值。
多入参
@Mappings({
@Mapping(source = "po.name", target = "userName"),
@Mapping(source = "loverPo.name", target = "loverName")
})
abstract UserDo multi2do(UserPo po, LoverPo loverPo);
定义方法时可以使用多个入参,这些入参都可以作为赋值时的数据源使用。这时,书写@Mapping
需要指定对应的属性名。
更新赋值
void po2do(UserPo po, @MappingTarget UserDo ddo);
定义转换方法时,可以在入参里通过@MappingTarget
指定要赋值的对象。要注意的时,这种方式下调用方法时需要传入要被赋值的对象,转换方法不会生成新对象。因此这种方式更适合用来更新对象的值。
@BeforeMapping @AfterMapping
MapStruct
提供了@BeforeMapping
和@AfterMapping
两个注解,用于在实体转换的前后做一些额外了逻辑。但是,这两个注解只能用于抽象类格式的转换器中。
自动注入
可以通过在@Mapper
注解里配置componentModel
属性,让转换器实现类成为可被自动注入的bean
。
总结
可以看到MapStruct
是一个非常强大好用的转换框架。用好转换框架可以让我们的代码变得整洁,提升代码的可读性和可维护性。但是需要注意的是MapStruct
中包含了大量的隐式转换,一定要万分注意,不需要转换的要用注解忽略掉,另外也不建议使用注解完成太复杂的转换逻辑,这样反而导致代码可读性和可维护性差,包含太多的隐式转换会隐藏问题,不容易发现。