MapStruct 的基础使用
参考
http://tools.jbritian.com/link/v
介绍
MapStruct 类似于我们熟悉的 BeanUtils, 是一个 Bean 的转换框架。
它与 BeanUtils 最大的不同之处在于,其并不是在程序运行过程中通过反射进行字段复制的,而是在编译期生成用于字段复制的代码(类似于 Lombok 生成 get() 和 set() 方法),这种特性使得该框架在运行时相比于 BeanUtils 有很大的性能提升。
引入
Maven
由于 MapStruct 和 Lombok 都会在编译期生成代码,如果配置不当,则会产生冲突,因此在工程中同时使用这两个包时,应该按照以下方案导入:
1、当 POM 中不包含 Lombok 时:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
</dependency>
2、当 POM 中包含 Lombok 且不包含 <annotationProcessorPaths> 时:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
</dependency>
注意:引入时,mapstruct-processor 必须 lombok 后面。
3、当 POM 中包含 Lombok 且包含 <annotationProcessorPaths> 时:
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.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>1.18.24</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>
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Idea Plugin
搜索 MapStruct Support 安装即可,可以在使用 MapStruct 时获得更加丰富代码提示。
使用
1、字段完全一致
待转换的类:
@Data
@Builder
public class Source {
private Long id;
private Long age;
private String userNick;
}
转换目标类:
@Data
public class Target {
private Long id;
private Long age;
private String userNick;
}
转换器:
// 注意:Mapper是Mapstruct的注解。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
public abstract Target convert(Source source);
}
使用示例:
final Source source = Source.builder()
.id(1L)
.age(18L)
.userNick("Nick")
.build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);
2、对应的字段名不一致、类型不一致
待转换的类:
@Data
@Builder
public class Source {
private Long id;
private Long age;
private String userNick;
}
转换目标类:
@Data
public class Target {
private Long id;
private Integer age;
private String nick;
}
转换目标类修改了 age 字段的类型,和 userNick 字段的名字,这两个类的字段仍然是一一对应的。
转换器:
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
// 字段类型映射修改
@Mapping(source = "age", target = "age", resultType = Integer.class)
// 字段名映射修改
@Mapping(source = "userNick", target = "nick")
public abstract Target convert(Source source);
}
使用示例:
final Source source = Source.builder()
.id(1L)
.age(18L)
.userNick("Nick")
.build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);
3、一对多字段互转
在业务代码中,常出现需要将一个类中的一些字段转换为另一个类的 JSON 字段的情况,以下是一个简单的例子:
互相转换的类:
- VO:前端渲染内容。
@Data
@Builder
public class VO {
private Long id;
private Long age;
private String userNick;
}
- DTO:传输内容,其中仅包含 id,其余字段均存放在 extra 字段中。
@Data
public class DTO {
private Long id;
private String extra;
}
1)多个字段转换为一个字段
- 常用于将多个字段转为 JSON 字段,在以下示例中,为了避免引入第三方包(如 FastJson),仅使用字符串拼接两个字段,Json 方式同理。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
@Mapping(target = "extra", source = "vo", qualifiedByName = "convertToExtra")
public abstract DTO convert(VO vo);
@Named("convertToExtra")
public String convertToExtra(VO vo) {
return String.format("%s,%s", vo.getAge(), vo.getUserNick());
}
}
将多个字段转换为一个字段,需要以下几个步骤:
- 创建自定义转换方法(本例为 convertToExtra()):
- 方法入参类型为被转换的类(本例为 VO),出参为转换好的字段(本例为 extra);
- 为方法加上@Named 注解,并自定义该方法在 mapStruct 中的名字(本例中为 convertToExtra)。
- 在转换方法上增加 Mapping 注解,其中:
- source 字段必须与转换方法入参名字相同(本例中均为 vo);
- target 字段为目标字段(本例中为 extra);
- qualifiedByName 字段为上述自定义的方法名字。
2)将一个字段转换为多个字段
- 该方法常用于从 JSON 字段中取出数据。原理与上述方法类似,定义两个自定义转换方法,用于转换 extra 字段。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
@Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
@Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
public abstract VO convertToVO(DTO dto);
@Named("extractAge")
public Long extractAge(String extra) {
// 从extra中提取第一个值
return Long.valueOf(extra.split(",")[0]);
}
@Named("extractUserNick")
public String extractUserNick(String extra) {
// 从extra中提取第二个值
return extra.split(",")[1];
}
}
使用示例:
final VO vo = VO.builder()
.id(1L)
.age(18L)
.userNick("Nick")
.build();
// 转为DTO
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);
// 转回VO
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);
4、为转换加缓存
在上述的两个方法(extractAge 和 extractUserNick)中,进行了重复的 String.split() 操作,如果该操作更加复杂(如从 JSON 串中提取内容),则会造成资源的浪费。
为此,可以给当前的 converter 加一个缓存字段 extraFieldBufferLocal,如下例所示。在例子中,每次解析 extra 字段前,先判断 buffer 是否存在,如果存在则使用缓存内容。
注:Mapstruct 中使用 xxx.INSTANT 获得的转换器是单例的,因此,如果要在多线程环境中转换时加入缓存,其缓存必须声明为 ThreadLocal 类型。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
/**
* extra字段解析后的buffer,避免多次重复解析
*/
private final ThreadLocal<String[]> extraFieldBufferLocal = new ThreadLocal<>();
@Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
@Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
public abstract VO convertToVO(DTO dto);
@Named("extractAge")
public Long extractAge(String extra) {
if (extraFieldBufferLocal.get() == null) {
extraFieldBufferLocal.set(extractExtraField(extra));
}
return Long.valueOf(extraFieldBufferLocal.get()[0]);
}
@Named("extractUserNick")
public String extractUserNick(String extra) {
if (extraFieldBufferLocal.get() == null) {
extraFieldBufferLocal.set(extractExtraField(extra));
}
return extraFieldBufferLocal.get()[1];
}
/**
* 提取extra字段
*
* @param extra extra字段
* @return extra字段的提取结果
*/
public String[] extractExtraField(final String extra) {
return extra.split(",");
}
}
5、子类字段互转
常用于平铺类和嵌套类之间的转换,例如,前端需要将类中的所有字段打平,就可以参考以下示例代码。
互相转换的类:
VO:
@Data
@Builder
public class VO {
private Long id;
private Date gmtCreate;
private Long age;
private String userNick;
}
DTO:
@Data
public class DTO {
private Long id;
private Date gmtCreate;
private Config config;
@Data
public static class Config{
private String age;
private String userNick;
}
}
在 DTO 中,VO 的 age 和 userNick 字段被放到了子类 Config 中。此时也可以使用上一节展示的自定义转换函数法进行转换,不过 MapStruct 提供了一种更加直观简单的转换方法:
转换器:
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
@Mapping(target = "config.age", source = "age")
@Mapping(target = "config.userNick", source = "userNick")
abstract DTO convertToDTO(VO source);
@Mapping(target = "age", source = "config.age")
@Mapping(target = "userNick", source = "config.userNick")
abstract VO convertToVO(DTO dto);
}
使用示例:
final VO vo = VO.builder()
.id(1L)
.age(10L)
.gmtCreate(new Date())
.userNick("nick")
.build();
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);
6、利用 Spring 进行依赖注入
本文以上示例代码中,都是使用 Converter.INSTANT 来获得 Convert 实例,这在业务代码中可能显得有些突兀,而 MapStruct 提供了依赖注入的机制,让我们能够在 Spring 的环境下,更优雅的获得 Converter,以下是一个例子:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class Converter {
public abstract Target convert(Source source);
}
使用示例:
@Controller
public class MainController {
@Resource
private Converter convert;
@GetMapping("/")
@ResponseBody
public boolean test() {
final Source source = Source.builder()
.id(1L)
.age(18L)
.userNick("nick")
.build();
final Target result = convert.convert(source);
System.out.println(result);
return true;
}
}
全部评论