- 1、MapStruct介绍
- 2、简单的属性拷贝
- 3、属性类型相同名称不同
- 4、 List类型转化到List型
- 5、实体类中嵌套list,填充值
- 6、根据类型枚举转换code->name
- 7、boot集成mapstruct
- 8、在@mapper中注入对象
- 9、类共有属性,复用
一、什么是 MapStruct
MapStruct 核心概念
MapStruct是一个Java注解处理器,它的主要功能是自动生成类型安全、高性能且无依赖的bean映射代码。这个工具基于“约定优于配置”的原则,极大地简化了Java Bean类型之间的映射实现过程。
在多层架构的应用中,经常需要在不同的对象模型之间进行转换,例如在持久层的实体和传输层的DTO(Data Transfer Object,数据传输对象)之间。手动编写这种映射代码是一项繁琐且容易出错的任务。MapStruct通过自动化的方式解决了这个问题,它可以在编译时生成映射代码,从而保证了高性能、快速的开发反馈以及严格的错误检查。
具体来说,使用MapStruct时,开发者只需要定义一个接口,并在接口中定义转换方法。然后,MapStruct会自动生成实现这些方法的代码。这些生成的代码使用纯方法调用,因此速度快、类型安全且易于理解。
MapStruc主要特性
1、类型安全:MapStruct在编译时生成映射代码并进行类型检查,如果源对象和目标对象的属性不匹配,会在编译阶段就报错。
2、性能优秀:由于MapStruct在编译时就生成了映射代码,运行时无需通过反射进行属性拷贝,因此性能较高。
3、灵活性:MapStruct支持复杂的映射,如嵌套映射、集合映射、自定义转换规则等。
4、简洁性:MapStruct使用注解来定义映射规则,使得映射规则的定义更加直观和简洁。
5、无依赖:MapStruct不依赖于任何第三方库,可以很容易地集成到任何项目中。
6、集成Spring:MapStruct也可以与Spring框架集成,允许在映射器中注入Spring管理的bean。
使用MapStruct,开发者只需要定义一个接口,并在接口中声明源对象和目标对象之间的映射关系,MapStruct会在编译时自动生成映射实现类。这极大地提高了代码的可读性和可维护性,同时也避免了手动编写繁琐的转换代码。
二、MapStruct和BeanUtils区别
MapStruct和BeanUtils都是Java中常用的对象属性映射工具,但它们在使用方式和性能上有一些区别。
1、使用方式:
BeanUtils:使用反射机制进行属性拷贝,使用简单,无需写额外的映射代码。
MapStruct:需要定义映射接口,在编译阶段生成映射实现类,使用注解来定义源对象和目标对象之间的映射关系。
2、性能:
BeanUtils:由于使用了反射机制,性能较低。
MapStruct:在编译阶段就生成了映射代码,运行时无需通过反射进行属性拷贝,因此性能较高。
3、灵活性和安全性:
BeanUtils:由于是动态映射,如果源对象和目标对象的属性不匹配,可能会在运行时出现错误。
MapStruct:在编译阶段就进行了类型检查,如果源对象和目标对象的属性不匹配,会在编译阶段就报错,提高了类型安全性。另外,也支持复杂的映射,如嵌套映射、集合映射等。
对象转换次数 | 属性个数 | BeanUtils耗时 | Mapstruct耗时 |
5千万次 | 6 | 14秒 | 1秒 |
5千万次 | 15 | 36秒 | 1秒 |
5千万次 | 25 | 55秒 | 1秒 |
Mapstruct 依赖
//这是一个坑,lombok 依赖必须在 mapstruct 前面。
//lombok 依赖必须在 mapstruct 前面。
//lombok 依赖必须在 mapstruct 前面。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<!-- jdk8以下就使用mapstruct -->
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</dependency>
简单的属性拷贝
下面我们先来看下Mapstruct最简单的使用方式。
当两个对象的属性类型和名称完全相同时,Mapstruct会自动拷贝;假设我们现在需要把UserPo的属性值拷贝到UserEntity中,我们需要做下面几件事情:
- 定义UserPo和UserEntity
- 定义转换接口
- 编写测试main方法
▐ 首先定义UserPo和UserEntity
UserPo和UserEntity的属性类型和名称完全相同。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserPo {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick;
private String userVerified;
}
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick;
private String userVerified;
}
▐ 定义转换接口
定义mapstruct接口,在接口上打上@Mapper注解。
接口中有一个常量和一个方法,常量的值是接口的实现类,这个实现类是Mapstruct默认帮我们实现的,下文会讲到。定义了一个po2entity的转换方法,表示把入参UserPo对象,转换成UserEntity。
注意@Mapper是Mapstruct的注解,不要引错了。
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
//用UserPo 转 UserEntity
UserEntity po2entity(UserPo userPo );
}
▐ 测试类
创建一个UserPo对象,并使用Mapstruct做转化。
UserPo 属性较多,或者UserEntity 属性较多,多可以映射,没有的自动为null
public class MapStructTest {
public static void main(String[] args) {
//正常转换,类型和名字都能对应上
testNormal();
}
private static void testNormal() {
System.out.println("-----------testNormal-----start------");
UserPo userPo = new UserPo(1L, new Date(), new Date(), 2L, 3L, "test", "testmp");
System.out.println("1234" + userPo);
//获取到mapstruct的实例
IPersonMapper instance = IPersonMapper.Instance;
System.out.println(instance.po2entity(userPo));
System.out.println("-----------testNormal-----ent------");
}
}
▐ 测试结果
可以看到,所有赋值的属性都做了处理,且两边的值都一样,结果符合预期。
Mapstruct 性能优于 BeanUtils 的原因
Java程序执行的过程,是由编译器先把java文件编译成class字节码文件,然后由JVM去解释执行class文件。Mapstruct正是在java文件到class这一步帮我们实现了转换方法,即做了预处理,提前编译好文件,如果用过lombok的同学一定能理解其好处,通过查看class文件,可以看出IPersonMapper被打上org.mapstruct.Mapper注解后,编译器自动会帮我们生成一个实现类IPersonMapperImpl,并实现了po2entity这个方法,看下面的截图。
▐ IPersonMapperImpl代码
从生成的代码可以看出,转化过程非常简单,只使用了UserPo的get方法和UserEntity的set方法,没有复杂的逻辑处理,清晰明了,所以性能很高
▐ Spring的BeanUtils源码
BeanUtils部分源码如下,转换的原理是使用的反射,反射的效率相对来说是低的,因为jvm优化在这种场景下有可能无效,所以在对性能要求很高或者经常被调用的程序中,尽量不要使用。我们平时在研发过程中,也会遵守这个原则,非必要,不反射。
从下面的BeanUtils代码中可以看出,转化逻辑非常复杂,有很多的遍历,去获取属性,获取方法,设置方法可访问,然后执行,所以执行效率相对Mapstruct来说,是非常低的。回头看Mapstruct自动生成的实现类,简洁、高效。
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
属性类型相同名称不同
对于属性名称不同的属性进行处理时,需要使用@Mapping,比如修改UserEntity中的userNick为userNick1,然后进行转换
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick1;
private String userVerified;
}
如果没有映射执行会报错:
▐ @Mapping注解指定source和target字段名称对应关系
@Mapping(target = “userNick1”, source = “userNick”),此处的意思就是在转化的过程中,将UserPo的userNick属性值赋值给UserEntity的userNick1属性。
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
//用UserPo 转 UserEntity
@Mapping(source = "userNick", target = "userNick1")
UserEntity po2entity(UserPo userPo );
}
▐ 执行结果
可以看到,正常映射,符合预期。
▐ 查看class文件
可以看到,Mapstruct帮我们做了处理,把po的userNick属性赋值给了entity的userNick1。
String转日期&String转数字&忽略某个字端&给默认值等
//时间字符串的模板
@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
@Mapping(target = "age", source = "age", numberFormat = "#0.00")
@Mapping(target = "id", ignore = true)
@Mapping(target = "userVerified", defaultValue = "defaultValue-2")
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
//用UserPo 转 UserEntity
@Mapping(source = "userNick", target = "userNick1")
@Mapping(source = "createTime", target = "createTime",dateFormat = "yyyy-MM-dd")
UserEntity po2entity(UserPo userPo );
}
设置默认值:
@Mapper
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
//用UserPo 转 UserEntity
@Mapping(source = "userNick", target = "userNick1")
@Mapping(source = "createTime", target = "createTime",dateFormat = "yyyy-MM-dd")
@Mapping(target = "userVerified", defaultValue = "defaultValue-2")
UserEntity po2entity(UserPo userPo );
}
▐ 执行结果
List类型转化到List型
list类型的实体类UserPo 转成UserEntity类型的实体类
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
List<UserEntity> po2entity(List<UserPo> userPos);
}
▐ 测试方法
import com.example.msstruct.mapstruct.IPersonMapper;
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author GJ
* @date 2024/8/16 16:24
*/
public class MapStructTest {
public static void main(String[] args) {
//正常转换,类型和名字都能对应上
// testNormal();
//list 封装转换
testList();
}
private static void testList() {
System.out.println("-----------testNormal-----start------");
Date date = new Date();
ArrayList<UserPo> userPos = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UserPo userPo = new UserPo(1L, date, date , 20L, 10L, "test", "test");
userPos.add(userPo);
}
IPersonMapper instance = IPersonMapper.Instance;
List<UserEntity> userEntities = instance.po2entity(userPos);
System.out.println(userEntities);
System.out.println("-----------testNormal-----ent------");
}
}
▐ 测试结果
List 封装的结果展示出来
实体类中嵌套list,填充值
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick1;
private String userVerified;
private List<Account> accounts;
}
import lombok.Data;
@Data
public class Account {
private String id;
private String name;
}
▐ @Mapping 实体中封装List
import com.example.msstruct.pojo.Account;
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
accountList 属性名要 和实体类中的 属性名accounts 对应上。
@Mapping(source = "accountList", target = "accounts")
UserEntity po2account(List<Account> accountList,UserPo userPo);
}
▐ 测试方法
public class MapStructTest {
public static void main(String[] args) {
//正常转换,类型和名字都能对应上
// testNormal();
//list 封装转换
// testList();
//accountList 封装转换UserEnity
testAccountList();
}
private static void testAccountList() {
System.out.println("-----------testAccountList-----start------");
Date date = new Date();
ArrayList<UserPo> userPos = new ArrayList<>();
for (int i = 0; i < 1; i++) {
UserPo userPo = new UserPo(1L, date, date , 20L, 10L, "test", "test");
userPos.add(userPo);
}
ArrayList<Account> accounts = new ArrayList<>();
for (int j = 0; j < 10; j++) {
Account account = new Account();
account.setId("1");
account.setName("test");
accounts.add(account);
}
IPersonMapper instance = IPersonMapper.Instance;
UserEntity userEntity = instance.po2account(accounts, userPos.get(0));
System.out.println(userEntity);
System.out.println("-----------testAccountList-----ent------");
}
}
▐ 测试结果
6、根据枚举类型code–>name
▐ 枚举类
public enum TypeEnum {
/**
* 类型枚举
*/
TYPE1("1", "类型1"),
TYPE2("2", "类型2");
private String code;
private String name;
TypeEnum(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public static String getByCode(String code) {
for (TypeEnum type : TypeEnum.values()) {
if (type.getCode().equals(code)) {
return type.getName();
}
}
return null;
}
}
▐ @Mapping 映射
@Mapping(source = "createTime", target = "createTime",dateFormat = "yyyy-MM-dd")
// @Mapping(target = "userVerified", defaultValue = "defaultValue-2")
// 通过枚举转化 expression ="java(里面填写类名+方法名(参数))"
@Mapping(target = "name", expression = "java(com.example.msstruct.enums.TypeEnum.getByCode(userPo.getCode()))")
UserEntity po2entity(UserPo userPo );
▐ 测试类
public class MapStructTest {
public static void main(String[] args) {
//正常转换,类型和名字都能对应上
testNormal();
}
private static void testNormal() {
System.out.println("-----------testNormal-----start------");
Date date = new Date();
UserPo userPo = new UserPo(1L, date, date , 20L, 10L, "test", "test","1");
System.out.println("------" + userPo);
//获取到mapstruct的实例
IPersonMapper instance = IPersonMapper.Instance;
System.out.println(instance.po2entity(userPo));
System.out.println("-----------testNormal-----ent------");
}
}
▐ 测试结果
boot集成mapstruct
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,import= {TypeEnum.class})
public interface IPersonMapper {
// 通过枚举转化
@Mapping(target = "name", expression = "java(TypeEnum.getByCode(userPo.getCode()))")
UserEntity po2entity(UserPo userPo );
}
解释import 和uses:
import 属性就和java中的import是一样的,导入后在expression中就可以不使用类的全限定名称了。例如你的转换用到了一个静态工具类,那么如果不在import中导入此工具类,那么使用的时候就要全限定名了。
没有使用import 并不是全路径,结果报错
使用import 返回结果成功
没有使用import 但添加上全路径名称
使用uses属性,就像 spring中的依赖注入。
只需将@mapper 中添加一个componentModel =“spring”,将接口注册到spring中,
@Mapper注解的uses属性用于指定自定义的类型转换器或映射器接口,以帮助MapStruct处理复杂的映射场景。
1、映射器接口:一个被@Mapper注解标记的Java接口,用于定义对象之间的映射逻辑,MapStruct会自动生成这个接口的实现。
2、类型转换器类:虽然不是MapStruct的直接概念,但在需要自定义类型转换时可能会用到。在MapStruct中,更常见的是通过映射器接口、@Mapping注解的expression属性、@AfterMapping/@BeforeMapping注解等方式来实现类型转换。
//这个地方也是看别的文章
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,
uses = {
GirlFriendMapper.class
}
)
public interface ProgramerConvetor {
//属性之间的映射
@Mapping(target = "girlFriend", source = "programer")
ProgramerDto toProgramerDto(Programer programer);
}
但在Programer 属性 转成ProgramerDto 中的 girlFriend ,需要 girlFriendMapper 这个属性注入。
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-01-08T21:03:17+0800",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
)
@Component
public class ProgramerConvetorImpl implements ProgramerConvetor {
@Autowired
private GirlFriendMapper girlFriendMapper;
@Override
public ProgramerDto toProgramerDto(Programer programer) {
ProgramerDto programerDto = new ProgramerDto();
...
programerDto.setGirlFriend( girlFriendMapper.toGirlFriendDto( programer ) );
return programerDto;
}
}
▐ 测试方法
@RestController
public class MapStructTestController {
@Autowired
private IPersonMapper iPersonMapper;
@GetMapping("/test")
public void test(String[] args) {
//正常转换,类型和名字都能对应上
testNormal();
//list 封装转换
// testList();
//accountList 封装转换UserEnity
// testAccountList();
}
private void testNormal() {
System.out.println("-----------testNormal-----start------");
Date date = new Date();
UserPo userPo = new UserPo(1L, date, date , 20L, 10L, "test", "test","1");
System.out.println("------" + userPo);
//获取到mapstruct的实例
// IPersonMapper instance = IPersonMapper.Instance;
System.out.println(iPersonMapper.po2entity(userPo));
System.out.println("-----------testNormal-----ent------");
}
}
▐ 测试结果
8、在@mapper中注入对象
目的: 在 映射的 mapper中注入 依赖
▐ 实体类
@Data
public class UserEntity {
private Long id;
private Date gmtCreate;
private Date createTime;
private Long buyerId;
private Long age;
private String userNick1;
private String userVerified;
private List<Account> accounts;
private String name;
}
@Data
public class Account {
private String id;
private String name;
}
▐ service
@Service
public class AccountService {
//往account 中赋值
public List<Account> setAccount() {
ArrayList<Account> accounts = new ArrayList<>();
Account account = new Account();
account.setId("123");
account.setName("张三");
accounts.add(account);
return accounts;
}
}
▐ 测试@Mapper
//需要注入对象使用interface 是不合理的,需要使用abstract
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,imports = {TypeEnum.class})
public abstract class IAccountMapper {
@Autowired
protected AccountService accountService;
@Mapping(target = "name", expression = "java(TypeEnum.getByCode(userPo.getCode()))")
@Mapping(target = "accounts", expression = "java(accountService.setAccount())")
public abstract UserEntity po2entity(UserPo userPo) ;
}
▐ 测试方法
@RestController
public class MapStructTestController {
@Autowired
private IAccountMapper accountMapper;
@GetMapping("/test")
public void test(String[] args) {
//正常转换,类型和名字都能对应上
testNormal();
}
private void testNormal() {
System.out.println("-----------testNormal-----start------");
Date date = new Date();
UserPo userPo = new UserPo(1L, date, date , 20L, 10L, "test", "test","1");
System.out.println("------" + userPo);
//获取到mapstruct的实例
System.out.println(accountMapper.po2entity(userPo));
System.out.println("-----------testNormal-----ent------");
}
}
▐ IAccountMapperImpl 代码
▐ 测试结果
9、类共有属性,复用
目的: 返回的实体有同样的属性,并且属性值生成同样的方法,可以定义一个注解的形式进行生成
▐ 注解定义
import org.mapstruct.Mapping;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
//这个地方引用 random 类,需要在IPersonMapper 引入 这个类。
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", expression = "java(Long.parseLong(new Random().nextInt(10) + \"\"))")
public @interface ToEntity {
}
▐ IPersonMapper
import com.example.msstruct.enums.TypeEnum;
import com.example.msstruct.pojo.Account;
import com.example.msstruct.pojo.UserEntity;
import com.example.msstruct.pojo.UserPo;
import com.example.msstruct.pojo.UserPo2;
import org.mapstruct.*;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Random;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = {TypeEnum.class, Random.class})
public interface IPersonMapper {
IPersonMapper Instance = Mappers.getMapper( IPersonMapper.class );
// 通过枚举转化
@ToEntity
@Mapping(target = "name", expression = "java(TypeEnum.getByCode(userPo.getCode()))")
UserEntity po2entity(UserPo userPo );
@ToEntity
UserEntity entity2po(UserPo2 userPo2);
}
▐ IPersonMapperImpl 生成代码
▐ 测试方法
public class MapStructTest {
public static void main(String[] args) {
testBeanUtils();
}
private static void testBeanUtils() {
System.out.println("-----------testNormal-----start------");
UserPo2 userPo2 = new UserPo2();
IPersonMapper instance = IPersonMapper.Instance;
UserEntity userEntity = instance.entity2po(userPo2);
System.out.println(userEntity);
System.out.println("-----------testNormal-----mid------");
UserPo userPo = new UserPo();
UserEntity userEntity1 = instance.po2entity(userPo);
System.out.println(userEntity1);
System.out.println("-----------testNormal-----ent------");
}
}
▐ 测试结果