最近想对业务上遇到的属性/对象拷贝的代码进行优化,以前大多使用的是BeanUtils.copyProperties,然后不匹配名称的字段通过set赋值来弥补;但是,经常会出现两个bean大部分字段不匹配的情况,一大堆的set赋值真的是极其恶心。前几天在交流群里请教了几个大厂的朋友,在他们的极力推荐下得知MapStruct是一款高效易用的框架;那么就撸起袖子直接就开干了,肝出此文,并且编写了demo项目辅助理解。
MapStruct简介
what is MapStruct ?
MapStrutc是一款基于约定优于配置原则诞生的代码生成器,它旨在极大简化Java javabean类型之间的映射的实现。由于它生成代码的方式基于普通方法调用,因此具有快速、类型安全和容易理解的特点。
Why we choose MapStruct?
这是因为,应用程序的多个层之间通常需要针对不同的对象模型进行映射(例如do和dto直接映射)。
编写这种映射代码本身是一件非常乏味且容易出错的事情。而MapStruct正是为了解决这个问题而诞生的,它的设计思路就是尽可能通过自动化简化对象模型映射的复杂度。提升开发者的编码效率。减少手动映射出错的概率。
How to use MapStruct?
MapStruct是通过注解处理器,插入Java编译器, 它支持基于命令行进行构建(Maven, Gradle等等),并且原则上支持任意主流的IDE(IDEA, Eclipse等等)。
MapStruct支持使用合理的默认值, 并且支持用户对默认值进行任意的指定,这些都来自于用户的配置或指定的代码实现。
2分钟急速入门
这种工具没有太多的花样,官方文档已经很详细了,那我们就基于官方文档,扛起键盘开始干吧,mapstruct官网[1]
基于maven进行安装配置
由于MapStruct是基于JSR 269[2]
规范实现的Java注解处理器,因此它可以使用命令行或IDE进行构建,目前IDEA已然成为事实上的主流IDE, 我们就用它来尝鲜吧!
因为我只熟悉maven >.<,所以这里使用maven安装;grade, ant等其他安装方式请自行参考官网哈
首先建立一个Maven工程,引入如下依赖和配置
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</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> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
复制代码
MapStruct的插件下载
MapStruct贴心的提供了IDE的插件,用于快速生成javabean的映射代码。
我们可以直接在idea插件中心下载,也可使用以下方式下载后手动安装
MapStruct官网下载:MapStruct IntelliJ IDEA plugin[3]
jetbrains插件仓库下载:MapStruct support for IntelliJ IDEA[4]
编写第一个案例
配置就绪,我们开始编写第一个案例的代码吧!
首先,我们需要定义两个待映射的javabean (Car.java / CarDto.java)
public class Car {
private String make;
private int numberOfSeats;
private CarType type;
public Car(String make, int numberOfSeats, CarType type) {
this.make = make;
this.numberOfSeats = numberOfSeats;
this.type = type;
}
// getter、setter方法省略...
}
public class CarDto {
private String make;
private int seatCount;
private String type;
}
public enum CarType {
SEDAN;
}
复制代码
接着我们需要创建一个mapper,这个是映射的重头戏:
@Mapper
public interface CarMapper {
public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}
复制代码
我们对代码中的重点稍加解释:
@Mapper:凡是被标注该注解的接口,都被指定了基于这个接口来进行javabean的映射,mapstruct会通过这个接口生成实现类,并在实现类的内部实现代码映射;
@Mapping:有时候对两个javabean进行映射时,难免会出现字段名称不匹配,但是我们又希望两个javabean中不同名的两个字段互相映射,这个时候就需要手动指定映射关系;如上面的代码中,我们显式指定将CarDto中的numberOfSeats映射到Car中的seatCount字段。那么当调用映射方式carToCarDto时,就能够实现将 numberOfSeats 的值copy给seatCount 的目的。
继续看我们的代码,由于我们没有使用DI框架(比如:spring 或者 Guice),因此如果此时想要获取mapper实例,则需要通过调用 org.mapstruct.factory.Mappers 类中的 getMapper 方法来实现。由于CarMapper只做javabean的映射,因此没必要每次进行javabean映射时,都去创建一个mapper实例。所以这里做成单例模式。
测试代码如下
public class MapStructTest {
@Test
public void shouldMapCarToDto() {
//given
Car car = new Car( "Morris", 5, CarType.SEDAN );
//when
CarDto carDto = CarMapper.INSTANCE.carToCarDto( car );
//then
Assert.assertNotNull(carDto);
Assert.assertEquals("Morris", carDto.getMake());
Assert.assertEquals(5, carDto.getSeatCount());
Assert.assertEquals("SEDAN", carDto.getType());
}
}
复制代码
运行结果如下:
好了,到这里基本上对如何使用MapStruct我们就有了一个直观的认识,可以看到通过MapStruct实现将car的字段内容映射到carDto中是一件很简单轻松的事情。
不过,我们好像发现一个问题,在spring的环境下mapper里面每次都创建个单例;正常来说,在spring环境下这种单例javabean完全可以交由spring容器去管理,那么我们应该怎么搞呢?
MapStruct其实针对DI注入提供了三种方式:
cdi:使用 @Inject注解注入
spring:使用 @Autowired注解注入
jsr330:使用 @Inject注解注入
简单的说,MapStruct支持我们对其进行依赖注入,由于我们企业级/互联网后端开发基本都是会依赖spring框架的,所以我们可以编写以下代码:
@Mapper(componentModel = "spring")
public interface CarMapper {
public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}
复制代码
这样配置的话,在其他地方想要使用CarMapper的时候,就可以通过 @Autowired注解来注入。
但是,如果每次都写 (componentModel = “spring”),通过在每个mapper上面都进行指定也挺烦的,都是重复的工作;
既然我们能想到了这个问题,MapStruct怎么可能想不到,那么MapStruct是怎么做的呢?
MapStruct在maven中有个配置选项,用于进行全局配置,让MapStruct在编译的时候按照这个全局配置生成javabean的映射代码;那么怎么配置呢?代码如下:
// pom.xml文件中
<build>
<plugins>
<plugin>
// ... 无关的配置就不贴出来了
<configuration>
<compilerArgs>
<compilerArg>
-Amapstruct.defaultComponentModel=spring
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
复制代码
完美,到这里我们就实现在项目全局使用统一的属性进行属性注入Mapper的声明, 而不用每次都手动指定 (componentModel = “spring”)了。
继续入门
为什么要继续入门呢?虽然我们已经算是入门了,而且也可以在spring环境中用起来了;但是这还远远不够,单纯的进行单层javabean之间的映射,连企业中最基本的需求都无法满足,那么我们还差哪些呢?
- javabean的嵌套组合(就是一个javabean引用了另外一个javabean,或另一个javabean的集合)
- 集合的映射(集合中的javabean引用了另一个javabean的集合)
如果连这两个都能够实现,那么主流的使用场景就覆盖的差不多了。因为如果javabean定义的合理,javabean之间的映射基本就以上这些情况。那么接下来我们继续:
复杂场景:javabean里嵌套了一个普通javabean
待映射的两个javabean均嵌套了其他javabean Person -> Apple / PersonDto -> AppleDto
public class Apple {
private String color;
public Apple(String color) {
this.color = color;
}
// getter、setter省略...
@Override
public String toString() {
return "Apple{" +
"color='" + color + '\'' +
'}';
}
}
public class Person {
private String name;
private Apple apple;
// getter、setter省略...
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", apple=" + apple +
'}';
}
}
public class AppleDto {
private String color;
public AppleDto(String color) {
this.color = color;
}
// gettet、setter省略...
@Override
public String toString() {
return "AppleDto{" +
"color='" + color + '\'' +
'}';
}
}
public class PersonDto {
private String name;
private AppleDto appleDto;
// gettet、setter省略...
@Override
public String toString() {
return "PersonDto{" +
"name='" + name + '\'' +
", appleDto=" + appleDto +
'}';
}
}
复制代码
接着我们编写一个嵌套javabean的mapper文件
@Mapper
public interface AppleMapper {
// AppleDto -> apple
Apple toApple(AppleDto appleDto);
// Apple -> AppleDto
@InheritInverseConfiguration
AppleDto fromApple(Apple apple);
}
@Mapper(uses = { AppleMapper.class })
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
// PersonDto -> Person
@Mapping(source = "appleDto", target = "apple")
Person toPerson(PersonDto personDto);
// Person -> PersonDto
@Mapping(source = "apple", target = "appleDto")
@InheritInverseConfiguration
PersonDto fromPerson(Person person);
}
复制代码
从以上代码可以看出,一个javabean嵌套了另一个普通javabean的关键点有3个地方:**
- 嵌套的普通javabean必须有自己的mapper文件,这样可以保证这个普通javabean的属性都可以被映射,甚至当出现字段不一样,还可以进行手动映射
- 因为Apple被嵌套在Person里面,所以在PersonMapper映射文件定义时,需要通过编写代码:@Mapper(uses = {AppleMapper.class}) 指定嵌套关系;
- 使用注解
@InheritInverseConfiguration
,方便javabean的逆向转换。
javabean里面嵌套一个List集合
这里就不贴代码了,这种映射和上面的几乎一样;只要把Apple修改为List appleList就可以了,代码链接我会附在文章末尾。
同样的,如果javabean里面嵌套一个普通的List strings, 映射方法和上面如出一辙,具体参考文末提供的代码连接
好了,到这里算是真正入门了,对于MapStruct这个框架我们也可以真正在项目中使用起来了,但是肯定还会存在一些其他的坑点,例如:
- 有时候在进行javabean映射的时候,可能想对值进行一些特殊处理后再映射过去;例如日期格式转换 Date <—> String
- 映射的时候想要针对一些字段进行忽略,禁止映射
- 映射的时候,如果某个属性没值,是否可以填充默认值
除了上面提到的几个坑外,可能还存在其他的坑、以及对于使用上是否还存在更加优雅的方式、还有MapStruct更高阶的功能如何使用等等,碍于篇幅我们没能面面俱到,还是需要我们一起继续进行学习和研究。
References
[1] mapstruct官网: mapstruct.org/
[2] JSR 269: www.jcp.org/en/jsr/deta…
[3] MapStruct IntelliJ IDEA plugin: mapstruct.org/news/2017-0…
[4] MapStruct support for IntelliJ IDEA: plugins.jetbrains.com/plugin/1003…
[5] 完整demo地址: github.com/ziyuc69/adv…