java中的对象拷贝(包括BeanUtils和Mapstruct)

对象拷贝

借鉴:

  1. https://blog.csdn.net/weixin_43811057/article/details/122853749
  2. https://blog.csdn.net/weixin_42036952/article/details/105697234?spm=1001.2101.3001.6650.8&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-8-105697234-blog-122853749.235%5Ev43%5Epc_blog_bottom_relevance_base4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-8-105697234-blog-122853749.235%5Ev43%5Epc_blog_bottom_relevance_base4&utm_relevant_index=16
  3. https://blog.csdn.net/ytgytg28/article/details/132197917?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-132197917-blog-122853749.235%5Ev43%5Epc_blog_bottom_relevance_base4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-132197917-blog-122853749.235%5Ev43%5Epc_blog_bottom_relevance_base4&utm_relevant_index=5
  4. https://github.com/vanDusty/Frame-Home/blob/master/spring-case/bean-copy/src/main/java/cn/van/spring/copy/beancopier/converter/UserDomainConverter.java

几种拷贝的性能对比

有哪些常见的拷贝技术:

  1. Apache的BeanUtils:BeanUtils是Apache commens组件里面的成员,由Apache提供的一套开源api,用于简化对javaBean的操作,能够对基本类型自动转换。

  2. Spring的BeanUtils:BeanUtils是Spring框架提供的对Java反射和自省API的包装。其主要目的是利用反射机制对JavaBean的属性进行处理。

  3. Mapstruct:MapStruct基于注解处理器,在编译时生成映射代码,避免了运行时的性能开销。

    以下是MapStruct对象拷贝技术的一些特点和优势:

    1. 类型安全:MapStruct在编译时会进行类型检查,确保源对象和目标对象的属性类型是兼容的,避免了在运行时可能出现的类型转换错误。
    2. 自定义映射规则:开发人员可以通过自定义的映射方法来定义复杂的属性映射规则,例如格式转换、条件映射等。
    3. 支持构造函数映射:MapStruct可以根据构造函数来进行对象映射,避免了手动设置属性值的繁琐操作。
    4. 支持集合映射:MapStruct可以处理集合类型之间的映射,包括List、Set、Map等。
    5. 可扩展性:MapStruct提供了丰富的注解和SPI(Service Provider Interface)机制,可以通过扩展接口和插件来实现更复杂的映射需求。

    使用MapStruct进行对象拷贝通常需要以下步骤:

    1. 定义映射接口:创建一个带有@Mapper注解的接口,定义需要映射的方法。
    2. 编写映射方法:在映射接口中定义方法,使用@Mapping注解来指定属性之间的映射关系。
    3. 生成映射代码:在编译时,MapStruct会根据映射接口生成对应的映射实现类。
    4. 调用映射方法:在代码中调用生成的映射方法,实现对象之间的属性拷贝。

    总的来说,MapStruct是一个强大且易于使用的对象拷贝工具,能够帮助开发人员简化对象映射的操作,提高代码质量和开发效率。

  4. BeanCopier:BeanCopier是Cglib包中的一个类,用于对象的复制。目标对象必须先实例化而且对象必须要有setter方法。但是拷贝的速度还是很快的。在要拷贝的数据量比较大的时候使用比较合适。

速度对比:

image-20240411232252004

image-20240411232316162

Spring提供的BeanUtils(比较慢,数据量不大时已经很够用了)

测试类:

package com.copy;

import com.ruoyi.RuoYiApplication;
import com.ruoyi.common.utils.BeanUtilCopy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author yimeng
 * @Date 2024/4/11 22:16
 * @PackageName:com.sell.test
 * @ClassName: TestCopy
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest(classes = RuoYiApplication.class)
@Slf4j
public class TestCopy {
    /**
     * 单个对象拷贝
     */
    @Test
    public void commonCopy() {
        Dept dept1=new Dept(1,"开发部");
        UserDO userDO = new UserDO(1L, "Van", 18, 1,999,dept1);
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        log.info("userVO:{}",userVO);// 输出:userVO:UserVO(userId=null, userName=Van, sex=null, aaa=null, height=999, dept=Dept(deptId=1, deptName=开发部))
        // 结论:两个类的属性类型和属性名完全一样才能成功拷贝。类型是包装类和普通类之间的关系也不能拷贝。要拷贝到的类多的属性不会被拷贝复制,但是不会报错。被拷贝的类多的属性不会被拷贝,也不会报错。
    }

    /**
     * 集合拷贝(错误,但是不会抛异常,只是没有进行拷贝而已)
     */
    @Test
    public void listCopyFalse() {
        List<UserDO> userDOList = new ArrayList();
        Dept dept1=new Dept(1,"开发部");
        Dept dept2=new Dept(2,"财务部");
        UserDO user1=new UserDO(1L, "Van", 18, 1,999,dept1);
        UserDO user2=new UserDO(2L, "VanVan", 20, 2,666,dept2);
        userDOList.add(user1);
        userDOList.add(user2);
        List<UserVO> userVOList = new ArrayList();
        BeanUtils.copyProperties(userDOList, userVOList);
        log.info("userVOList:{}",userVOList);// 输出:userVOList:[]
        // 结论:不能直接拷贝两个集合。不会被拷贝,但是也不会错误。
    }

    /**
     * 暴力的集合拷贝(不建议,写起来麻烦)
     */
    @Test
    public void listCopyCommon() {
        List<UserDO> userDOList = new ArrayList();
        Dept dept1=new Dept(1,"开发部");
        Dept dept2=new Dept(2,"财务部");
        UserDO user1=new UserDO(1L, "Van", 18, 1,999,dept1);
        UserDO user2=new UserDO(2L, "VanVan", 20, 2,666,dept2);
        userDOList.add(user1);
        userDOList.add(user2);
        List<UserVO> userVOList = new ArrayList();
        userDOList.forEach(userDO ->{
            UserVO userVO = new UserVO();
            BeanUtils.copyProperties(userDO, userVO);
            userVOList.add(userVO);
        });
        log.info("userVOList:{}",userVOList);// 输出:userVOList:[UserVO(userId=null, userName=Van, sex=null, aaa=null, height=999, dept=Dept(deptId=1, deptName=开发部)), UserVO(userId=null, userName=VanVan, sex=null, aaa=null, height=666, dept=Dept(deptId=2, deptName=财务部))]

        // 结论:可以直接遍历来一个个拷贝元素到新的集合去。这种做法大家都能想到,但是就是写起来比较麻烦。其实就是多次拷贝单个元素而已。
    }

    /**
     * 集合拷贝
     */
    @Test
    public void listCopyUp() {
        List<UserDO> userDOList = new ArrayList();
        Dept dept1=new Dept(1,"开发部");
        Dept dept2=new Dept(2,"财务部");
        UserDO user1=new UserDO(1L, "Van", 18, 1,999,dept1);
        UserDO user2=new UserDO(2L, "VanVan", 20, 2,666,dept2);
        userDOList.add(user1);
        userDOList.add(user2);

        List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new);
        log.info("userVOList:{}",userVOList);// 输出:userVOList:[UserVO(userId=null, userName=Van, sex=null, aaa=null, height=999, dept=Dept(deptId=1, deptName=开发部)), UserVO(userId=null, userName=VanVan, sex=null, aaa=null, height=666, dept=Dept(deptId=2, deptName=财务部))]

        // 结论:这里封装了工具类,用这个工具类可以很简单地进行集合的拷贝,其实和用BeanUtils.copyProperties(userDO, userVO);完成集合的拷贝效果一样的,但是拷贝集合的操作更简单了,效果还是和一个拷贝一样的。如果要拷贝一个对象,那么就直接调用BeanUtils.copyProperties方法就行了,不用使用这个工具类,这个工具类只是为了方便集合拷贝而已.这里利用了函数式接口提高了灵活性。
    }

    /**
     * 带特定转换的集合拷贝
     */
    @Test
    public void listCopyUpWithCallback() {
        List<UserDO> userDOList = new ArrayList();
        Dept dept1=new Dept(1,"开发部");
        Dept dept2=new Dept(2,"财务部");
        UserDO user1=new UserDO(1L, "Van", 18, 1,999,dept1);
        UserDO user2=new UserDO(2L, "VanVan", 20, 2,666,dept2);
        userDOList.add(user1);
        userDOList.add(user2);
        List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new, (userDO, userVO) -> {
            // 这里可以定义特定的转换规则
            userVO.setSex(SexEnum.getDescByCode(userDO.getSex()).getDesc());
        });
        log.info("userVOList:{}",userVOList);// 输出:userVOList:[UserVO(userId=null, userName=Van, sex=男生, aaa=null, height=999, dept=Dept(deptId=1, deptName=开发部)), UserVO(userId=null, userName=VanVan, sex=女生, aaa=null, height=666, dept=Dept(deptId=2, deptName=财务部))]

        // 结论:这种封装的工具类如果你想要把那些属性类型和属性名不一样的属性也进行拷贝,可以很简单通过写lambda表达式来自定义逻辑。其实效果相当于是把元素一个个通过BeanUtils.copyProperties(userDO, userVO);拷贝然后调用set方法去设置自己想要设置的值到某个属性里面去。就是这种工具类让我们使用更加简单了,其实就是相当于让程序用lambda表达式中的做法去进行拷贝那些属性类型和属性名完全一样的数据而已。这里定义了枚举是为了抽取出来键和值的对应关系,这种把映射关系抽取为枚举的做法就和前端字典的做法异曲同工。
    }

    /**
     * 测试是否为深拷贝
     */
    @Test
    public void testWhetherItIsADeepCopy() {
        List<UserDO> userDOList = new ArrayList();
        Dept dept1=new Dept(1,"开发部");
        Dept dept2=new Dept(2,"财务部");
        UserDO user1=new UserDO(1L, "Van", 18, 1,999,dept1);
        UserDO user2=new UserDO(2L, "VanVan", 20, 2,666,dept2);
        userDOList.add(user1);
        userDOList.add(user2);
        List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new, (userDO, userVO) -> {
            // 这里可以定义特定的转换规则
            userVO.setSex(SexEnum.getDescByCode(userDO.getSex()).getDesc());
        });
        user1.setUserName("字符串改变了");
        user1.setHeight(2);
        dept1.setDeptName("开发部改了");
        log.info("userVOList:{}",userVOList);// 输出:userVOList:[UserVO(userId=null, userName=Van, sex=男生, aaa=null, height=999, dept=Dept(deptId=1, deptName=开发部改了)), UserVO(userId=null, userName=VanVan, sex=女生, aaa=null, height=666, dept=Dept(deptId=2, deptName=财务部))]

        String s1="张三";
        String copy=s1;
        System.out.println(copy==s1);//true
        s1="李四";
        System.out.println(copy==s1);//false

        // 结论:BeanUtils是浅拷贝,如果是基本类型改变,那么不会影响拷贝后的值(看height)。如果是引用类型改变,那么会影响拷贝后的值(看dept)。
        // 但是对于String类型的改变,会影响拷贝后的值(看userName)。原因如下:

        // 你其实可以理解为拷贝后的副本都不会因为你拷贝前对象中的数据变化而变化,那为什么对于引用类型的拷贝,改变之前的值会影响拷贝后的值呢?
        // 你拷贝后,副本对象和被拷贝的对象中的内容,如果是基本数据类型就存了值,如果是引用类型就存了地址。你拷贝后把副本中某个是引用类型的属性的地址改为别的对象的地址。其实对副本中这个变量是没有任何影响的。
        // 我们说的改变引用类型对象中的数据,会影响拷贝后的副本,其实就是因为拷贝前对象中这个属性的地址值没有变化,拷贝后的这个对象的这个属性的地址值也没有变化。
        // 他们都指向了一个对象。我们把他们共同指向的对象中的某个属性改了,自然就影响了拷贝前对象那个属性地址代表的值也同时影响了拷贝后对象那个属性地址代表的值。
        // 你String值改变了为什么对拷贝后的对象没有变化呢?
        // 其实是因为String内部的机制是,每次改变都相当于是创建了一个新的String对象,然后把值给那个变量。比如copy=s1相当于是把值改copy,就是把值给copy这个变量,上面把s1的值改了,就相当于是创建了一个新的对象并把地址值给s1,但是copy还是老的值,所以结果他们不一样了。
        // 所以对于String类型的拷贝,然后改值,相当于是改了老对象中的地址值。所以不会影响到拷贝后的对象的地址指向的对象。
    }
}
@Data
class UserVO{
    private Long userId;
    private String userName;
    private String sex;
    private Integer aaa;// 多的
    private int height;
    private Dept dept;
}
@Data
@AllArgsConstructor
class UserDO {
    private long id;// 类型不一样,但是是包装类和基础类型的区别。
    private String userName;
    private int age;// 多的
    private Integer sex;// 类型完全不一样
    private int height;
    private Dept dept;
}
enum SexEnum {
    UNKNOW("未设置",0),
    MEN("男生", 1),
    WOMAN("女生",2),

    ;
    private String desc;
    private int code;

    SexEnum(String desc, int code) {
        this.desc = desc;
        this.code = code;
    }

    public static SexEnum getDescByCode(int code) {
        SexEnum[] typeEnums = values();
        for (SexEnum value : typeEnums) {
            if (code == value.getCode()) {
                return value;
            }
        }
        return null;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}
@Data
@AllArgsConstructor
class Dept{
    private Integer deptId;
    private String deptName;
}

两个工具类:

package com.ruoyi.common.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.BeanUtils;
/**
 * @Author yimeng
 * @Date 2024/4/11 22:28
 * @PackageName:com.ruoyi.common.utils
 * @ClassName: BeanUtilCopy
 * @Description: TODO
 * @Version 1.0
 */
public class BeanUtilCopy extends BeanUtils {

    /**
     * 集合数据的拷贝
     * @param sources: 数据源类
     * @param target: 目标类::new(eg: UserVO::new)。eg是比如的意思。
     * @return
     */
    public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target) {
        return copyListProperties(sources, target, null);
    }


    /**
     * 带回调函数的集合数据的拷贝(可自定义字段拷贝规则)
     * @param sources: 数据源类
     * @param target: 目标类::new(比如: UserVO::new)。java.util.function.Supplier是一个函数式接口,用于提供一个对象实例。它只定义了一个方法get(),没有参数,作用是返回一个指定类型的对象实例。您可以通过lambda表达式或方法引用来实现Supplier接口。然后调用这个get方法,就是返回那个实例对象。
     * @param callBack: 回调函数
     * @return
     */
    public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target, BeanUtilCopyCallBack<S, T> callBack) {
        List<T> list = new ArrayList<>(sources.size());
        for (S source : sources) {
            // 使用Supplier接口创建目标类的实例.比如下面这样调用copyListProperties方法,那么target.get()就是返回一个UserVO对象实例。
            /**
            * List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new, (userDO, userVO) -> {
            *      userVO.setSex(SexEnum.getDescByCode(userDO.getSex()).getDesc());
            * });
            */
            T t = target.get();
            copyProperties(source, t);
            list.add(t);
            if (callBack != null) {
                // 回调
                callBack.callBack(source, t);
            }
        }
        return list;
    }
}
package com.ruoyi.common.utils;

/**
 * @Author yimeng
 * @Date 2024/4/11 22:29
 * @PackageName:com.ruoyi.common.utils.uuid
 * @ClassName: BeanUtilCopyCallBack
 * @Description: TODO
 * @Version 1.0
 */
@FunctionalInterface
public interface BeanUtilCopyCallBack<S, T> {

    /**
     * 定义默认回调方法
     * @param s: 数据源类 ,
     * @param t: 目标类
     */
    void callBack(S s, T t);
}

是浅拷贝!

MapStruct的使用(快)

知识点

简单了解一下

MapStruct是一个Java注释处理器,用于生成类型安全的bean映射类。

我们使用MapStruct来做对象的拷贝,要做的事情就是定义一个映射器接口,并且在该接口声明映射所需的方法。在编译期间,MapStruct将生成此接口的实现。我们到时候只要用这个实现对我们的对象进行拷贝就行了。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射赋值,即没有反射或类似内容。所以速度会很快。我们手写get和set方法来把一个对象中的内容拷贝到另一个对象中去是最快的,比反射快多了,但是我们手动来写就比较繁琐,我们使用Spring提供的BeanUtils有一些属性名或者类型不同的就不能进行映射了,并且Spring提供的BeanUtils使用的是反射的技术,数据量大的时候,这样的赋值是比较慢的。当然我们除了使用Spring提供的BeanUtils外,你还能使用其他的一些工具类来拷贝,有一些工具类可以自定义转换器,这样属性名或者类型不一样的两个类也能成功进行拷贝了,但是很多工具类其实都是反射的,反正没有MapStruct快。

与动态映射框架相比,MapStruct具有以下优点:

  1. 通过使用普通方法调用(settter/getter)而不是反射来快速执行
  2. 编译时类型安全性:只能映射相互映射的对象和属性,不能将order实体意外映射到customer DTO等。
  3. 如果有如下问题,编译时会抛出异常
    • 映射不完整(并非所有目标属性都被映射)
    • 映射不正确(找不到正确的映射方法或类型转换)

MapStruct 注解的关键词:

  1. @Mapper:只有在接口加上这个注解, MapStruct 才会去实现该接口;
  2. @Mappings:配置多个@Mapping;
  3. @Mapping:属性映射,若源对象属性与目标对象名字一致,会自动映射对应属性。
    • source:源属性;
    • target:目标属性;
    • dateFormat:字符串与日期之间相互转换;
    • ignore: 忽略这个,某个属性不想映射,可以加个 ignore=true;

对于基于Maven的项目,需要将以下内容添加到您的POM文件中以使用MapStruct:

<dependency>
	<groupId>org.mapstruct</groupId>
	<!-- jdk8以下就使用mapstruct -->
	<artifactId>mapstruct-jdk8</artifactId>
	<version>1.2.0.Final</version>
</dependency>
<dependency>
	<groupId>org.mapstruct</groupId>
	<artifactId>mapstruct-processor</artifactId>
	<version>1.2.0.Final</version>
</dependency>

image-20240417000132671

Lombok依赖:(版本最好在1.16.16以上,否则会出现问题)通常是和lombok一起使用的

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

image-20240415233422487

还有就是maven-compiler-plugin的版本要在3.6以上:

image-20240417002401355

这两个变量定义在这里:

image-20240417213902114

使用这个MapStruct你可以下载idea插件来帮助开发(这个插件不是必须的,但是挺好用的)

idea中下载 mapstruct support 插件,安装重启Idea:

image-20240415235514686

装上这个工具类后,在参数上,按 ctrl + 鼠标左键 ,能够自动进入参数所在类文件

img

在要写映射关系的 target和source中 按 代码提示键(我的是ctrl+ alt + /),能自动给出代码提示,如果出不来,可能需要设置一下手动唤出代码提示功能。

Intellij IDEA是一款优秀的编程软件,相比较Eclipse之下它的用户群较小,但并不代表它的功能就比Eclipse差,如果用顺手了还是特别好用的。代码提示功能对于程序员来说非常重要,那么我们应该怎么打开IDEA自带的代码提示功能呢?

默认情况下,我们输入代码时IDEA会自动弹出代码提示框,这时候是不用按快捷键的,直接使用方向键选择我们需要的条目就可以了。

img

如果我们想主动 使用代码提示时,默认的快捷键是CTRL+空格,但是中文系统这个快捷键为切换输入法,我们应该调整一下这个快捷键的功能。

img

我们在左侧菜单选择KeyMap,然后按照图中的路径,找到Basic和Cyclic Expand Word两个快捷键设置。

img

在Cyclic Expand Word上点击右键,移除原本的快捷键(AIT+/)。

img

Basic就是代码提示功能的选项了,我们在这条项目上点击右键,选择第一项,增加快捷键。

img

在这个界面,我们按下的键就会成为快捷键,此时我们按下AIT+/ 把它作为代码提示的快捷键。

img

添加完之后代码提示有了两个快捷键了,我们再次点击右键,删除原来的AIT+空格的快捷键就可以了!

使用
自己测试写写

定义一个Java接口,然后使用MapStruct的注解指定映射就行了。

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/16 23:41
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: Person
 * @Description: TODO
 * @Version 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    String describe;
    private String id;
    private String personName;
    private int age;
    private BigDecimal source;
    private double height;
    private Date createTime;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:23
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
    String describe;
    private Long id;
    private String name;
    private String age;
    private String source;
    private String height;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:20
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
// mapper
@Mapper
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "name", source = "personName")
    @Mapping(target = "id", ignore = true) // 忽略id,不进行映射
    PersonDTO convert(Person person);
}
package com.example.springbootdemo;


import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;

/**
 * @Author yimeng
 * @Date 2024/4/25 23:01
 * @PackageName:com.example.springbootdemo
 * @ClassName: MapStructTest
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest {
    @Test
    public void test() {
        Person person = new Person();
        person.setId("11");
        person.setDescribe("测试");
        person.setAge(18);
        person.setPersonName("张三");
        person.setHeight(170.5);
        person.setSource(new BigDecimal("100"));

        PersonDTO dto = PersonMapper.INSTANCE.convert(person);

        System.out.println(dto);// 输出:PersonDTO(describe=测试, id=null, name=张三, age=18, source=100, height=170.5)

        // 结论:id没有映射到目标类,因为加了ignore = true。
        // describe映射成功了,虽然没有指定映射关系,但是默认会映射属性名一样的字段.
        // age映射成功了,虽然目标类是String,源是int,但是名字一样,它还是会自动映射的。当然这里肯定有一些默认映射的转换关系,如果是类型不好转为另一个类型的,两个类型差别很大,那么就算名字一样也不能强转,这是肯定的。
        // 如果目标和源的字段名不一样,那么需要通过 @Mapping 指定。比如,name成功映射到personName。
    }
}

image-20240425233947603

如果想要用使用@Autowired@Resource等注解来注入映射器实例,得设置@Mapper(componentModel = “spring”)才行,不然会出错。比如:

package com.example.springbootdemo;


import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.math.BigDecimal;

/**
 * @Author yimeng
 * @Date 2024/4/25 23:01
 * @PackageName:com.example.springbootdemo
 * @ClassName: MapStructTest
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest {
    @Resource
    private PersonMapper personMapper;
    @Test
    public void test() {
        Person person = new Person();
        person.setId("11");
        person.setDescribe("测试");
        person.setAge(18);
        person.setPersonName("张三");
        person.setHeight(170.5);
        person.setSource(new BigDecimal("100"));

//        PersonDTO dto = PersonMapper.INSTANCE.convert(person);

        PersonDTO dto = personMapper.convert(person);
        System.out.println(dto);// 输出:PersonDTO(describe=测试, id=null, name=张三, age=18, source=100, height=170.5)

        // 结论:id没有映射到目标类,因为加了ignore = true。
        // describe映射成功了,虽然没有指定映射关系,但是默认会映射属性名一样的字段.
        // age映射成功了,虽然目标类是String,源是int,但是名字一样,它还是会自动映射的。当然这里肯定有一些默认映射的转换关系,如果是类型不好转为另一个类型的,两个类型差别很大,那么就算名字一样也不能强转,这是肯定的。
        // 如果目标和源的字段名不一样,那么需要通过 @Mapping 指定。比如,name成功映射到personName。
    }
}

image-20240425234036934

正确的做法:

package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:20
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
// mapper
@Mapper(componentModel = "spring")
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "name", source = "personName")
    @Mapping(target = "id", ignore = true) // 忽略id,不进行映射
    PersonDTO convert(Person person);
}

image-20240425234147780

image-20240425234417255

指定了componentModel = "spring",表示该映射器接口将由Spring框架进行管理和注入(注意,要把PersonMapper放在启动类所在的包或子包下才行,因为这样才能被spring扫描到。必须放在test下的和启动类一样的包下或者main下的和启动类一样的包或子包下)。当然,你使用原来的映射类中添加PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);是为了好用常量的方式来使用这个映射类。Mappers.getMapper()方法是MapStruct提供的一个静态方法,用于获取Mapper接口的实例。通过Mappers.getMapper()方法获取Mapper接口的实例后,就可以使用这个实例进行对象之间的映射转换操作。然后因为在接口中都是常量嘛,所以我们上面的写法,就可以直接用常量一样的方式来拿到Mappers.getMapper()方法返回的Mapper实例了。

当然,我们不使用PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);也一样可以通过Spring注入获取那个对象哈,但是必须要加上componentModel = "spring"才行。但是这种写法没有上面写法使用起来简单,因为你还要声明成员变量才能使用。

总之,加了componentModel = “spring”,使用PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);的INSTANCE来使用映射类的方法是OK的,使用@Autowired或者@Resource也一样可以注入。

image-20240425234750249

正式开始学习
简单例子

举一个例子:

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:47
 * @PackageName:com.entity
 * @ClassName: Car
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {
    private String carName;
    private String color;
    private String wheel;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:48
 * @PackageName:com.entity
 * @ClassName: CarConverter
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface CarConverter {
    // 实例
    CarConverter INSTANCE = Mappers.getMapper(CarConverter.class);

    /**
     属性名一样的,会自动映射并填充,属性名不一样的用下面这个 @Mapping 注解,定义映射关系
     source 为数据来源,target为要填充的目标属性
     */
    @Mapping(source = "wheel", target = "whe")
    CarDto carToDto(Car car);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:48
 * @PackageName:com.entity
 * @ClassName: CarDto
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CarDto {
    private String carName;
    private String color;
    private String whe;
}
package com.example.springbootdemo;


import com.example.springbootdemo.entity.Car;
import com.example.springbootdemo.entity.CarConverter;
import com.example.springbootdemo.entity.CarDto;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Car car = new Car("五菱宏光", "白", "4");
        CarDto carDto = CarConverter.INSTANCE.carToDto(car);
        System.out.println(carDto);
        // 输出为: CarDto(carName=五菱宏光, color=白, whe=4)
    }
}

结果:

image-20240425235205359

解释一下,MapStruct的原理:

在程序编译时,会自动生成我们所写的转换接口(@Mapper注解修饰的接口)的实现类,这个实现类在classes路径下,上面的例子生成的实现类如下:

// 这个类是自动生成,不需要自己写
public class CarConverterImpl implements CarConverter {
    public CarConverterImpl() {
    }
 
    public CarDto carToDto(Car car) {
        if (car == null) {
            return null;
        } else {
            CarDto carDto = new CarDto();
            carDto.setWhe(car.getWheel());
            carDto.setCarName(car.getCarName());
            carDto.setColor(car.getColor());
            return carDto;
        }
    }
}

可以看到,它就是调用的目标生成类的 setter方法 和 数据源类的 getter方法,来进行对象的创建的.

我们可以在target目录下看到这个自动生成的类:

package com.example.springbootdemo.entity;

import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-04-25T23:50:16+0800",
    comments = "version: 1.2.0.Final, compiler: javac, environment: Java 1.8.0_231 (Oracle Corporation)"
)
public class CarConverterImpl implements CarConverter {

    @Override
    public CarDto carToDto(Car car) {
        if ( car == null ) {
            return null;
        }

        CarDto carDto = new CarDto();

        carDto.setWhe( car.getWheel() );
        carDto.setCarName( car.getCarName() );
        carDto.setColor( car.getColor() );

        return carDto;
    }
}

image-20240425235334073

多个入参构造一个对象

如下例子:通过User和Role,构造一个UserRoleDTO

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: Role
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String name;
    private String roleId;
    private String description;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;

    private String name;

    private String address;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:48
 * @PackageName:com.entity
 * @ClassName: CarConverter
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface UserRoleConverter {
    // 实例
    UserRoleConverter INSTANCE = Mappers.getMapper(UserRoleConverter.class);


    // 多个Mapping时,用 Mappings 包起来
    @Mappings({
            @Mapping(source = "user1.userId", target = "id"), // 把user中的userId绑定到目标对象的id属性中
            @Mapping(source = "user1.name", target = "name"), // user和 role中都有name属性,所以需要指定一个来传给UserRoleDto
    })
    UserRoleDto toUserRoleDto(User user1, Role role);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:56
 * @PackageName:com.entity
 * @ClassName: UserRoleDTO
 * @Description: TODO
 * @Version 1.0
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleDto {
    private Long id;
    private String name;
    private String address;
    private String roleName;
    private String roleId;
    private String description;
}

注意:上面代码中需要注意的是,不管有多少入参,当目标类中的属性名和入参类型中的属性名一致时,都不用写Mapping,会自动映射,除非多个参数的类型中有同名的属性,比如这里user和role都有name这个属性,那么这种情况就需要指定用哪个source了,通过类似 user1.name的方式(user1 是参数名)来调用。

测试:

package com.example.springbootdemo;

import com.example.springbootdemo.entity.Role;
import com.example.springbootdemo.entity.User;
import com.example.springbootdemo.entity.UserRoleConverter;
import com.example.springbootdemo.entity.UserRoleDto;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        User user = new User(1L, "小王八蛋", "北京");
        Role role = new Role("管理员", "2", "描述");
        UserRoleDto userRoleDTO = UserRoleConverter.INSTANCE.toUserRoleDto(user, role);
        System.out.println(userRoleDTO);
        // 输出UserRoleDto(id=1, name=小王八蛋, address=北京, roleName=null, roleId=2, description=描述)
    }
}

image-20240427233940227

数据类型转换(int、long等常见数据类型 -> String 等)

常见数据类型能和String进行转换,但是得写映射。

还有就是你可以指定转换后的数据格式。这里举几个常见的指定格式做法,如果你要想了解更多自己百度。

package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:23
 * @PackageName:com.entity
 * @ClassName: DataFormatMapper
 * @Description: TODO
 * @Version 1.0
 */

@Mapper
public interface DataFormatMapper {

    DataFormatMapper INSTANCE = Mappers.getMapper(DataFormatMapper.class);

    /**
     * numberFormat 指定基本数据类型与String之间的转换
     * dateFormat 指定Date和String之间的转换
     */
    @Mappings({
            // 这里 # 表示多个数字,  0 表示一个数字(0-9)
            @Mapping(source = "price", target = "price", numberFormat = "#.00元"),
            @Mapping(source = "stock", target = "stock", numberFormat = "#个"),
            @Mapping(target = "saleTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            @Mapping(target = "validTime", dateFormat = "yyyy-MM-dd HH:mm")
    })
    ProductDTO toDto(Product product);;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:22
 * @PackageName:com.entity
 * @ClassName: Product
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    private int id;
    private int aaa;
    private long bbb;
    private double price;
    private int stock;
    private Date saleTime;
    private Date validTime;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:19
 * @PackageName:com.entity
 * @ClassName: ProductDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDTO {
    private Integer id;
    private String aaa;
    private int bbb;
    private String price;
    private String stock;
    private String saleTime;
    private String validTime;
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.DataFormatMapper;
import com.example.springbootdemo.entity.Product;
import com.example.springbootdemo.entity.ProductDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Product product =new Product(1,999,77,1000.0,3,new Date(),new Date());
        ProductDTO productDTO = DataFormatMapper.INSTANCE.toDto(product);
        System.out.println(productDTO);
        // 输出:ProductDTO(id=1, aaa=null, bbb=0, price=1000.00元, stock=3个, saleTime=2024-04-17 22:32:12, validTime=2024-04-17 22:32)
    }
}

image-20240427234245434

名字一样的,但是类型不一样不会自动转换,但是如果是基本数据类型和包装类型的话,会自动转换。你可以通过numberFormat 和 dateFormat 指定转换后的得到目标对象的属性的格式。

类中包含其他类的映射
属性类型相同

如果目标和源对象中属性是相同的类型的话,写映射关系后,是简单的浅拷贝(只拷贝引用),不会新建对象。

如:User–> UserRoleDTO(User源对象和UserRoleDTO都包含Role成员),映射时只拷贝role的引用,不会生成一个新的role对象给目标类的。

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: Role
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String name;
    private String roleId;
    private String description;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;
    private String name;
    private String address;
    private Role role;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:44
 * @PackageName:com.entity
 * @ClassName: UserConverter
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface UserConverter {

    // 实例
    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);

    // role 的属性名一样,类型也一样,不用写Mapping,还有就是 role的赋值是浅拷贝 只拷贝引用,不生成新的对象
    @Mapping(source = "userId", target = "id") // 把user中的userId绑定到目标对象的id属性中
    UserRoleDto toUserRoleDto(User user);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:56
 * @PackageName:com.entity
 * @ClassName: UserRoleDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleDto {
    private Long id;
    private String name;
    private String address;
    private Role role;
}

package com.example.springbootdemo;

import com.example.springbootdemo.entity.Role;
import com.example.springbootdemo.entity.User;
import com.example.springbootdemo.entity.UserConverter;
import com.example.springbootdemo.entity.UserRoleDto;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Role role=new Role("测试1","测试11","123456");
        User user=new User(1L,"123456","123456",role);
        UserRoleDto userRoleDto = UserConverter.INSTANCE.toUserRoleDto(user);
        System.out.println(userRoleDto);
        // 输出:UserRoleDto(id=1, name=123456, address=123456, role=Role(name=测试1, roleId=测试11, description=123456))
        role.setRoleId("999");
        System.out.println(userRoleDto);
        // 输出:UserRoleDto(id=1, name=123456, address=123456, role=Role(name=测试1, roleId=999, description=123456))
    }
}

image-20240427234613310

看到MapStruct也是浅拷贝。其实你想要知道是不是浅拷贝,看MapStruct自动实现的类的对应方法就知道了,如果就是简单的set,那么就必然是浅拷贝,因为赋值就是把地址值赋值给目标变量而已,不会重新申请堆空间的。

属性类型不同

如果目标和源对象中属性是不同的类型的话。举一个例子就知道了。

比如有两个类分别如下,其中括号中是属性的类型。我们要把User转换为UserRoleDTO,并且把User中的Role转为UserRoleDTO中的RoleDTO

分别是:User(Role)—》 UserRoleDTO(RoleDTO)

做法:要将User 转换成 UerRoleDTO:除了写一个User转UerRoleDTO的转换方法外,还要额外写一个 Role转成RoleDTO的转换方法,当执行User转UerRoleDTO时,会自动的调用这个Role转成RoleDTO的方法来生成对象。(是通过查找 **本类中参数类型和返回类型与我们所需转换的两个类相符合的方法 **来执行的)

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: Role
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String name;
    private String roleId;
    private String description;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/19 22:30
 * @PackageName:com.entity
 * @ClassName: RoleDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoleDTO {
    private String name;
    private String id;
    private String description;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;
    private String name;
    private String address;
    private Role role;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:44
 * @PackageName:com.entity
 * @ClassName: UserConverter
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface UserConverter {

    // 实例
    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);

    @Mapping(source = "userId", target = "id") // 把user中的userId绑定到目标对象的id属性中
    UserRoleDTO toUserRoleDto(User user);
    // 当上面那个toUserRoleDto方法调用时,会自动调用这个方法来完成 User类中的Role 到 UserRoleDTO类中的RoleDTO 的转换
    @Mapping(source = "roleId", target = "id")
    RoleDTO toRoleDto(Role role);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:56
 * @PackageName:com.entity
 * @ClassName: UserRoleDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleDTO {
    private Long id;
    private String name;
    private String address;
    private RoleDTO role;
}

测试:

package com.example.springbootdemo;

import com.example.springbootdemo.entity.Role;
import com.example.springbootdemo.entity.User;
import com.example.springbootdemo.entity.UserConverter;
import com.example.springbootdemo.entity.UserRoleDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Role role=new Role("测试1","测试11","123456");
        User user=new User(1L,"123456","123456",role);
        System.out.println(user);
        // 输出:User(userId=1, name=123456, address=123456, role=Role(name=测试1, roleId=测试11, description=123456))
        UserRoleDTO userRoleDto = UserConverter.INSTANCE.toUserRoleDto(user);
        System.out.println(userRoleDto);
        // 输出:UserRoleDTO(id=1, name=123456, address=123456, role=RoleDTO(name=测试1, id=测试11, description=123456))
    }
}

image-20240427234928676

注意,两个方法得写在一个类里面才行。不然他会用他内部默认的映射规则来自动进行映射。

即,只映射同名的属性。

  • 如果不指定@Mapping,默认映射name相同的field
  • 如果映射的对象field name不一样,通过 @Mapping 指定。

默认的规则是:

  1. 属性名一样,基本类型可以映射给包装类型,比如int可以映射给Integer,Integer类型可以映射给int类型的变量。注意,属性名字要一样,或者你指定源的属性名和目标的属性名也行,反正默认是能把这两个类型进行转换的。
  2. 整型之间会自动映射,比如int到long,或者long给int。int给Long也行。Long给int也行。Integer和Long相互转换也行。int给double也行,比如int的1给double,那么就会变为1.0,但是如果是double的1.2给int那么就变为1了,double的2.8转为int会变为2。相当于是舍弃小数(double转Integer和double转int的规则一样)。注意,属性名字要一样,或者你指定源的属性名和目标的属性名也行,反正默认是能把这两个类型进行转换的。
  3. 整形和String相互映射也行。int给String行,String给int也行。注意,属性名字要一样,或者你指定源的属性名和目标的属性名也行,反正默认是能把这两个类型进行转换的。

举一个例子:

package com.example.springbootdemo.entity;

import org.mapstruct.Named;

/**
 * @Author yimeng
 * @Date 2024/4/19 23:11
 * @PackageName:com.entity
 * @ClassName: DoneFormatter
 * @Description: TODO
 * @Version 1.0
 */
// 自定义的映射方法:转换boolean为String时,做一些判断然后返回对应的值。
@Named("DoneFormatter")
public class DoneFormatter {

    @Named("DoneFormatter")
    public String toStr(Boolean isDone) {
        if (isDone) {
            return "已完成";
        } else {
            return "未完成";
        }
    }

    @Named("DoneDetailFormatter")
    public String toDetail(Boolean isDone) {
        if (isDone) {
            return "该产品已完成";
        } else {
            return "该产品未完成";
        }
    }

    public Boolean toBoolean(String str) {
        if (str.equals("已完成")) {
            return true;
        } else {
            return false;
        }
    }
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/19 23:12
 * @PackageName:com.entity
 * @ClassName: ObjectQualiferMapper
 * @Description: TODO
 * @Version 1.0
 */
// 通过uses 来导入上面我们写的 自定义映射方法
@Mapper(uses = {DoneFormatter.class})
public interface ObjectQualifierMapper {

    ObjectQualifierMapper INSTANCE = Mappers.getMapper(ObjectQualifierMapper.class);

    // 当有多个方法 拥有一样的参数和返回类型时,需要指定使用其中的哪一个,使用qualifiedByName指定
    @Mappings({
                    @Mapping(source = "id", target = "id1"),// 名字不一样,我们可以通过指定来解决。但是转换的规则用它默认的。double转Integer是ok的。
                    @Mapping(source = "isDone", target = "isDone", qualifiedByName = "DoneDetailFormatter"),// 自定义规则
    })
    ProductDTO toDto(Product product);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:22
 * @PackageName:com.entity
 * @ClassName: Product
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    private double id;//double转Integer
    private Integer aaa;//Integer转Long
    private long bbb;//long转int
    private double price;//double转String
    private String stock;//String转int
    private Date saleTime;//Date转String
    private Date validTime;//Date转String

    private String status;//String转Boolean
    private Boolean isDone;//Boolean转String
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:19
 * @PackageName:com.entity
 * @ClassName: ProductDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDTO {
    private Integer id1;
    private Long aaa;
    private int bbb;
    private String price;
    private int stock;
    private String saleTime;
    private String validTime;

    private Boolean status;
    private String isDone;
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.ObjectQualifierMapper;
import com.example.springbootdemo.entity.Product;
import com.example.springbootdemo.entity.ProductDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test() {
        Product product = new Product(2.8, 999, 77, 1000.0, "3", new Date(), new Date(), "已完成", true);
        ProductDTO productDTO = ObjectQualifierMapper.INSTANCE.toDto(product);
        System.out.println(productDTO);
        // 输出:ProductDTO(id1=2, aaa=999, bbb=77, price=1000.0, stock=3, saleTime=24-4-28 下午10:25, validTime=24-4-28 下午10:25, status=false, isDone=该产品已完成)
    }
}

image-20240428222641267

自定义映射方式
引入外部的类
package com.example.springbootdemo.entity;

import org.mapstruct.Named;

/**
 * @Author yimeng
 * @Date 2024/4/19 23:11
 * @PackageName:com.entity
 * @ClassName: DoneFormatter
 * @Description: TODO
 * @Version 1.0
 */
// 自定义的映射方法:转换boolean为String时,做一些判断然后返回对应的值。
@Named("DoneFormatter")
public class DoneFormatter {

    @Named("DoneFormatter")
    public String toStr(Boolean isDone) {
        if (isDone) {
            return "已完成";
        } else {
            return "未完成";
        }
    }

    @Named("DoneDetailFormatter")
    public String toDetail(Boolean isDone) {
        if (isDone) {
            return "该产品已完成";
        } else {
            return "该产品未完成";
        }
    }

    public Boolean toBoolean(String str) {
        if (str.equals("已完成")) {
            return true;
        } else {
            return false;
        }
    }
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/19 23:12
 * @PackageName:com.entity
 * @ClassName: ObjectQualiferMapper
 * @Description: TODO
 * @Version 1.0
 */
// 通过uses 来导入上面我们写的 自定义映射方法
@Mapper( uses = {DoneFormatter.class})
public interface ObjectQualifierMapper {

    ObjectQualifierMapper INSTANCE = Mappers.getMapper(ObjectQualifierMapper.class);

    // 当有多个方法 拥有一样的参数和返回类型时,需要指定使用其中的哪一个,使用qualifiedByName指定.
    @Mapping(source = "isDone", target = "isDone", qualifiedByName = "DoneDetailFormatter")
    ProductDTO toDto(Product product);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:22
 * @PackageName:com.entity
 * @ClassName: Product
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    private int id;
    private Integer aaa;
    private long bbb;
    private double price;
    private int stock;
    private Date saleTime;
    private Date validTime;

    private String status;
    private Boolean isDone;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:19
 * @PackageName:com.entity
 * @ClassName: ProductDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDTO {
    private Integer id;
    private Long aaa;
    private int bbb;
    private String price;
    private String stock;
    private String saleTime;
    private String validTime;

    private Boolean status;
    private String isDone;
}

测试:

package com.example.springbootdemo;

import com.example.springbootdemo.entity.ObjectQualifierMapper;
import com.example.springbootdemo.entity.Product;
import com.example.springbootdemo.entity.ProductDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Product product =new Product(1,999,77,1000.0,3,new Date(),new Date(),"已完成",true);
        ProductDTO productDTO= ObjectQualifierMapper.INSTANCE.toDto(product);
        System.out.println(productDTO);
        // 输出:ProductDTO(id=1, aaa=999, bbb=77, price=1000.0, stock=3, saleTime=24-4-19 下午11:23, validTime=24-4-19 下午11:23, status=false, isDone=该产品已完成)
    }
}

image-20240428214213610

相当于是,转换器上面写了

@Mapper( uses = {DoneFormatter.class})

会自动使用我们指定的自定义映射方法来进行映射,把一个类型映射为另一个类型。这里的规则是:

  1. 如果实体类中从一个类型到另一个类型,我们没有定义,那么还是会用内部默认的映射器来映射的,比如Product中的private long bbb;会自动映射为ProductDTO中的private int bbb;,虽然我们没有定义。
  2. 如果实体类中从一个类型到另一个类型是我们定义的,比如把String类型转换为Boolean类型,我们定义过了(即能找到一个方法把一个类型转为另一个类型),所以属性名一样的,且源是String类型的,目标为Boolean会自动调用toBoolean方法转换。如果有多个转换的方法 拥有一样的参数和返回类型时,我们映射的时候需要指定使用其中的哪一个,使用qualifiedByName指定。具体例子看上面就行了。
不引入外部的类

就直接在接口中写默认方法就行了。

package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/19 23:12
 * @PackageName:com.entity
 * @ClassName: ObjectQualiferMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface ObjectQualifierMapper {

    ObjectQualifierMapper INSTANCE = Mappers.getMapper(ObjectQualifierMapper.class);

    // 当有多个方法 拥有一样的参数和返回类型时,需要指定使用其中的哪一个,使用qualifiedByName指定.
    @Mapping(source = "isDone", target = "isDone", qualifiedByName = "DoneDetailFormatter")
    ProductDTO toDto(Product product);

    @Named("DoneFormatter")
    default String toStr(Boolean isDone) {
        if (isDone) {
            return "已完成";
        } else {
            return "未完成";
        }
    }

    @Named("DoneDetailFormatter")
    default String toDetail(Boolean isDone) {
        if (isDone) {
            return "该产品已完成";
        } else {
            return "该产品未完成";
        }
    }

    default Boolean toBoolean(String str) {
        if (str.equals("已完成")) {
            return true;
        } else {
            return false;
        }
    }
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:22
 * @PackageName:com.entity
 * @ClassName: Product
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    private int id;
    private Integer aaa;
    private long bbb;
    private double price;
    private int stock;
    private Date saleTime;
    private Date validTime;

    private String status;
    private Boolean isDone;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:19
 * @PackageName:com.entity
 * @ClassName: ProductDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDTO {
    private Integer id;
    private Long aaa;
    private int bbb;
    private String price;
    private String stock;
    private String saleTime;
    private String validTime;

    private Boolean status;
    private String isDone;
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.ObjectQualifierMapper;
import com.example.springbootdemo.entity.Product;
import com.example.springbootdemo.entity.ProductDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Product product =new Product(1,999,77,1000.0,3,new Date(),new Date(),"已完成",true);
        ProductDTO productDTO= ObjectQualifierMapper.INSTANCE.toDto(product);
        System.out.println(productDTO);
        // 输出:ProductDTO(id=1, aaa=999, bbb=77, price=1000.0, stock=3, saleTime=24-4-19 下午11:23, validTime=24-4-19 下午11:23, status=false, isDone=该产品已完成)
    }
}

image-20240428234347488

list映射
package com.example.springbootdemo.entity;

import org.mapstruct.MapMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.Date;
import java.util.Map;

/**
 * @Author yimeng
 * @Date 2024/4/22 23:36
 * @PackageName:com.entity
 * @ClassName: MapMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface MapMapper {

    MapMapper INSTANCE = Mappers.getMapper(MapMapper.class);

    // Map示例配置(转换每个值和键的值)请注意一下这里使用的注解是MapMapping
    @MapMapping(valueDateFormat = "dd.MM.yyyy") // 数据格式转换
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);


    // 下面mapToUserMap执行时会自动调用这个方法把Student转换为User,要是,不写这个studentToUser方法,那么Student中的studentId不会自动映射到这个userId中去的
    @Mapping(source = "studentId", target = "userId")
    User studentToUser(Student student);

    Map<String, User> mapToUserMap(Map<Long, Student> source);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: Role
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String name;
    private String roleId;
    private String description;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/22 23:14
 * @PackageName:com.entity
 * @ClassName: Student
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private int studentId;
    private String name;
    private String address;
    private Role role;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;
    private String name;
    private String address;
    private Role role;
}
package com.example.springbootdemo;


import com.example.springbootdemo.entity.MapMapper;
import com.example.springbootdemo.entity.Role;
import com.example.springbootdemo.entity.Student;
import com.example.springbootdemo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.util.*;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Map<Long, Date> map = new HashMap<>();
        map.put(1L,new Date());
        // 获取明天的日期
        LocalDate tomorrow = LocalDate.now().plusDays(1);
        // 将LocalDate转换为Date
        Date date = java.sql.Date.valueOf(tomorrow);
        map.put(2L,date);
        Map<String, String> stringStringMap = MapMapper.INSTANCE.longDateMapToStringStringMap(map);
        System.out.println(stringStringMap);
        // 输出:{1=22.04.2024, 2=23.04.2024}
    }
    @Test
    void test2(){
        Role role = new Role("管理员", "2", "描述");
        Student student=new Student(1234,"张三","浙江杭州",role);
        Map<Long, Student> map = new HashMap<>();
        map.put(1L,student);
        Map<String, User> stringUserMap = MapMapper.INSTANCE.mapToUserMap(map);
        System.out.println(stringUserMap);
        // 输出:{1=User(userId=1234, name=张三, address=浙江杭州, role=Role(name=管理员, roleId=2, description=描述))}
    }
}

image-20240428223633658

image-20240428223708654

map映射
package com.example.springbootdemo.entity;

import org.mapstruct.MapMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.Date;
import java.util.Map;

/**
 * @Author yimeng
 * @Date 2024/4/22 23:36
 * @PackageName:com.entity
 * @ClassName: MapMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface MapMapper {

    MapMapper INSTANCE = Mappers.getMapper(MapMapper.class);

    // Map示例配置(转换每个值和键的值)请注意一下这里使用的注解是MapMapping
    @MapMapping(valueDateFormat = "dd.MM.yyyy") // 数据格式转换
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);


    // 下面mapToUserMap执行时会自动调用这个方法把Student转换为User,要是,不写这个studentToUser方法,那么Student中的studentId不会自动映射到这个userId中去的
    @Mapping(source = "studentId", target = "userId")
    User studentToUser(Student student);

    Map<String, User> mapToUserMap(Map<Long, Student> source);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: Role
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String name;
    private String roleId;
    private String description;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/22 23:14
 * @PackageName:com.entity
 * @ClassName: Student
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private int studentId;
    private String name;
    private String address;
    private Role role;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;
    private String name;
    private String address;
    private Role role;
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.MapMapper;
import com.example.springbootdemo.entity.Role;
import com.example.springbootdemo.entity.Student;
import com.example.springbootdemo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.util.*;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        Map<Long, Date> map = new HashMap<>();
        map.put(1L,new Date());
        // 获取明天的日期
        LocalDate tomorrow = LocalDate.now().plusDays(1);
        // 将LocalDate转换为Date
        Date date = java.sql.Date.valueOf(tomorrow);
        map.put(2L,date);
        Map<String, String> stringStringMap = MapMapper.INSTANCE.longDateMapToStringStringMap(map);
        System.out.println(stringStringMap);
        // 输出:{1=22.04.2024, 2=23.04.2024}
    }
    @Test
    void test2(){
        Role role = new Role("管理员", "2", "描述");
        Student student=new Student(1234,"张三","浙江杭州",role);
        Map<Long, Student> map = new HashMap<>();
        map.put(1L,student);
        Map<String, User> stringUserMap = MapMapper.INSTANCE.mapToUserMap(map);
        System.out.println(stringUserMap);
        // 输出:{1=User(userId=1234, name=张三, address=浙江杭州, role=Role(name=管理员, roleId=2, description=描述))}
    }
}

image-20240428224128075

image-20240428224148722

枚举映射

枚举的映射关系使用的是ValueMapping

package com.example.springbootdemo.entity;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:06
 * @PackageName:com.entity
 * @ClassName: ExternalOrderType
 * @Description: TODO
 * @Version 1.0
 */
public enum ExternalOrderType {
    SPECIAL(1, "特殊订单"), DEFAULT(2, "默认订单");

    private final Integer type;
    private final String describe;

    ExternalOrderType(Integer type, String describe) {
        this.type = type;
        this.describe = describe;
    }

    public Integer getType() {
        return type;
    }

    public String getDescribe() {
        return describe;
    }
}
package com.example.springbootdemo.entity;

import org.mapstruct.*;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:02
 * @PackageName:com.entity
 * @ClassName: OrderMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface OrderMapper {

    OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );

    @ValueMappings({
            @ValueMapping(target = "SPECIAL", source = "EXTRA"),
            @ValueMapping(target = "DEFAULT", source = "STANDARD"),
            @ValueMapping(target = "DEFAULT", source = "NORMAL")
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);

    // 调用下面方法会自动使用上面的枚举映射
    @Mappings({
            @Mapping(source = "userId", target = "id"), // 把user中的userId绑定到目标对象的id属性中
            @Mapping(source = "orderType", target = "externalOrderType"),
    })
    UserRoleDTO toUserRoleDto(User user);
}
package com.example.springbootdemo.entity;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:05
 * @PackageName:com.entity
 * @ClassName: OrderType
 * @Description: TODO
 * @Version 1.0
 */
public enum OrderType {
    EXTRA(1, "超大"), STANDARD(2, "标准尺寸"), NORMAL(3, "一般尺寸");

    private final Integer type;
    private final String describe;

    OrderType(Integer type, String describe) {
        this.type = type;
        this.describe = describe;
    }

    public Integer getType() {
        return type;
    }

    public String getDescribe() {
        return describe;
    }
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:57
 * @PackageName:com.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userId;
    private String name;
    private String address;
    private OrderType orderType;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:56
 * @PackageName:com.entity
 * @ClassName: UserRoleDTO
 * @Description: TODO
 * @Version 1.0
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleDTO {
    private Long id;
    private String name;
    private String address;
    private ExternalOrderType externalOrderType;
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    void test(){
        OrderType orderType = OrderType.STANDARD;
        ExternalOrderType externalOrderType = OrderMapper.INSTANCE.orderTypeToExternalOrderType(orderType);

        System.out.println("映射的外部订单类型:" + externalOrderType.name() + " (Type: " + externalOrderType.getType() + ", Describe: " + externalOrderType.getDescribe() + ")");
        // 输出:映射的外部订单类型:DEFAULT (Type: 2, Describe: 默认订单)
    }

    @Test
    void test2(){
        User user=new User(1L,"123456","123456",OrderType.STANDARD);
        UserRoleDTO userRoleDTO = OrderMapper.INSTANCE.toUserRoleDto(user);
        System.out.println(userRoleDTO);
        // 输出:UserRoleDTO(id=1, name=123456, address=123456, externalOrderType=DEFAULT)
    }
}

image-20240428224542463

image-20240428224557146

默认值和常量

下面代码的效果是:

  1. 如果s.getStringProp() ==null,则目标属性stringProperty将设置为 “undefined”,而不是应用来自stringProp的值,如果s.getStringProp() !=null那么就设置为它s.getStringProp()的值
  2. 如果s.getLongProperty() == null,则目标属性longProperty将设置为-1。
  3. 字符串"Constant Value"按原样设置到目标属性stringConstant中去。
  4. 日期属性可以给一个日期格式
  5. 使用StringListMapper把"jack-jill-tom"字符串转为对应的格式给stringListConstants属性。
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:53
 * @PackageName:com.entity
 * @ClassName: Source
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Source {
    private String stringProp;
    private Long longProp;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:51
 * @PackageName:com.entity
 * @ClassName: SourceTargetMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {

    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
    @Mapping(target = "stringConstant", constant = "Constant Value")
    @Mapping(target = "integerConstant", constant = "14")
    @Mapping(target = "longWrapperConstant", constant = "3001")
    @Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2014-01-22")
    @Mapping(target = "stringListConstants", constant = "jack-jill-tom")
    Target sourceToTarget(Source s);
}
package com.example.springbootdemo.entity;

import java.util.Arrays;
import java.util.List;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:59
 * @PackageName:com.entity
 * @ClassName: StringListMapper
 * @Description: TODO
 * @Version 1.0
 */
public class StringListMapper {
    public List<String> mapStringToStringList(String value) {
        return Arrays.asList(value.split("-"));
    }

    public String mapStringListToString(List<String> value) {
        return String.join("-", value);
    }
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;

/**
 * @Author yimeng
 * @Date 2024/4/23 23:52
 * @PackageName:com.entity
 * @ClassName: Target
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Target {
    private String stringProperty;
    private Long longProperty;
    private String stringConstant;
    private int integerConstant;
    private Long longWrapperConstant;
    private Date dateConstant;
    private List<String> stringListConstants;
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.Source;
import com.example.springbootdemo.entity.SourceTargetMapper;
import com.example.springbootdemo.entity.Target;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;


/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testSourceToTargetMapping() {
        Source source = new Source();
        source.setStringProp("Test String");

        Target target = SourceTargetMapper.INSTANCE.sourceToTarget(source);
        System.out.println(target);
        // 输出:Target(stringProperty=Test String, longProperty=-1, stringConstant=Constant Value, integerConstant=14, longWrapperConstant=3001, dateConstant=Wed Jan 22 00:00:00 CST 2014, stringListConstants=[jack, jill, tom])
    }
}

执行结果:

image-20240428234516560

我们可以通过target下的对应类找到这个mapStruct底层自动给我们实现的样子是什么样的:

image-20240428225043045

这个语句的效果是:将一个特定日期字符串(“2014-01-22”)解析为Date日期对象,并且指定解析的字符串的格式为yyyy-MM-dd,并将该日期对象设置为 target 对象的一个属性。

表达式

在使用表达式时,有几点注意事项:

  1. 表达式要放在双引号中,且前面需要加上 java(),以告诉 MapStruct 这是一个 Java 表达式。
  2. 在表达式中可以使用源对象的方法和属性,但需要通过 source 来引用。在表达式中,source 表示源对象,可以调用源对象的方法和属性,然后根据条件返回不同的值,这个值会被映射到目标对象的属性上。
  3. 表达式中可以包含条件语句、数学运算、逻辑运算等 Java 表达式语言的语法。
  4. 表达式的语法是 Java 表达式语言 (EL表达式)。

例子:

假设有一个源对象 Source 和一个目标对象 Target,它们分别包含以下属性:

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/25 21:51
 * @PackageName:com.entity
 * @ClassName: MySource
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MySource {
    private int age;
    private String name;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/25 21:52
 * @PackageName:com.entity
 * @ClassName: MyTarget
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyTarget {
    private String message;
}

我们想要根据源对象的 age 属性来设置目标对象的 message 属性,如果 age 大于等于 18,则设置为 “成年人”,否则设置为 “未成年人”。这时就可以使用表达式来实现这个逻辑。

package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/25 21:52
 * @PackageName:com.entity
 * @ClassName: MySourceTargetMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface MySourceTargetMapper {
    MySourceTargetMapper INSTANCE = Mappers.getMapper( MySourceTargetMapper.class );

    @Mapping(target = "message", expression = "java(source1.getAge() >= 18 ? \"成年人\" : \"未成年人\")")
    MyTarget sourceToTarget(MySource source1);
}

package com.example.springbootdemo;

import com.example.springbootdemo.entity.MySource;
import com.example.springbootdemo.entity.MySourceTargetMapper;
import com.example.springbootdemo.entity.MyTarget;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testSourceToTargetMapping() {
        MySource source = new MySource();
        source.setAge(19);
        source.setName("张三");

        MyTarget target = MySourceTargetMapper.INSTANCE.sourceToTarget(source);
        System.out.println(target);// 输出:MyTarget(message=成年人)
    }
}

image-20240428225311976

例子2:

package com.example.springbootdemo.entity;

/**
 * @Author yimeng
 * @Date 2024/4/25 22:06
 * @PackageName:com.entity
 * @ClassName: MathUtils
 * @Description: TODO
 * @Version 1.0
 */
public class MathUtils {
    public static int addAndReturn0(Integer p1, Integer p2) {
        if (p1 == null || p2 == null) {
            return 0;
        } else {
            return p1 + p2 + 10000;
        }
    }
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/25 22:03
 * @PackageName:com.entity
 * @ClassName: ObjectExpressionMapper
 * @Description: TODO
 * @Version 1.0
 */
// 使用外部的类,需要通过这种形式导入
@Mapper(imports = MathUtils.class)
public interface ObjectExpressionMapper {

    ObjectExpressionMapper INSTANCE = Mappers.getMapper(ObjectExpressionMapper.class);

    //expression中可以直接写Java代码
    @Mappings({

            // 设置默认值
            @Mapping(target = "productId", source = "id", defaultValue = "123"), //当product的id为null,设置为0
            // 表达式
            @Mapping(target = "price", expression = "java(product1.getPrice1() + product1.getPrice2())"),//直接相加
            @Mapping(target = "price2", expression = "java(MathUtils.addAndReturn0(product1.getPrice1(), product1.getPrice2()))"), //使用工具类处理
            // 常量
            @Mapping(target = "stock", constant = "100"), //固定设置为100, 常量
            // 默认值表达式
            @Mapping(target = "discount", expression = "java(product1.getDiscount() != null ? product1.getDiscount() : 0)") //如果discount为null,则设置为0
    })
    ProductDTO toDTO(Product product1);
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


/**
 * @Author yimeng
 * @Date 2024/4/17 22:22
 * @PackageName:com.entity
 * @ClassName: Product
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    private int id;
    private Integer stock;
    private Integer price1;
    private Integer price2;
    private Integer discount;
    private String status;
    private Boolean isDone;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/17 22:19
 * @PackageName:com.entity
 * @ClassName: ProductDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDTO {
    private Integer productId;
    private int stock;
    private Integer price;
    private Integer price2;
    private Integer discount;

    private Boolean status;
    private String isDone;
}

测试:

package com.example.springbootdemo;

import com.example.springbootdemo.entity.ObjectExpressionMapper;
import com.example.springbootdemo.entity.Product;
import com.example.springbootdemo.entity.ProductDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testMapping() {
        // 创建一个 Product 对象
        Product product = new Product(1, 999, 50, 100,null, "Active", true);
        // 使用 ObjectExpressionMapper 进行映射
        ProductDTO productDTO = ObjectExpressionMapper.INSTANCE.toDTO(product);

        // 打印映射后的 ProductDTO 对象
        System.out.println(productDTO);
        // 输出:ProductDTO(productId=1, stock=100, price=150, price2=10150, discount=0, status=false, isDone=true)

        // 再创建一个 Product 对象
        Product product1 = new Product(1, 999, 50, 100,666, "Active", true);
        productDTO = ObjectExpressionMapper.INSTANCE.toDTO(product1);
        System.out.println(productDTO);
        // 输出:ProductDTO(productId=1, stock=100, price=150, price2=10150, discount=666, status=false, isDone=true)
    }
}

image-20240428225534328

日期格式化

dateFormat后面跟上要展示成的样式就行了。

法1
package com.example.springbootdemo.entity;

import org.mapstruct.Named;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:24
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: DateFormat
 * @Description: TODO
 * @Version 1.0
 */
@Named("DateFormat")
public class DateFormat {
    @Named("formatDate")
    String formatDate(Date date) {
        System.out.println("执行");
        if (date == null) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
        return sdf.format(date);
    }

    @Named("formatDateTime")
    String formatDateTime(Date date) {
        if (date == null) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        return sdf.format(date);
    }
}
package com.example.springbootdemo.entity;

import lombok.Data;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: Person
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class Person {
    private String name;
    private Date createTime;
    private Date updateTime;
}
package com.example.springbootdemo.entity;

import lombok.Data;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class PersonDTO {
    private String name;
    private String createTime1;
    private String createTime2;
    private String updateTime1;
    private String updateTime2;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper( uses = {DateFormat.class})
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "createTime1", source = "createTime", dateFormat = "yyyy-MM-dd")// yyyy-MM-dd这种格式,你可以这样写
    @Mapping(target = "createTime2", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")// 展示日期和时分秒
    @Mapping(target = "updateTime1", source = "updateTime",  qualifiedByName = "formatDate")// 这样写可以展示xxx年xxx月xxx日
    @Mapping(target = "updateTime2", source = "updateTime",  qualifiedByName = "formatDateTime")// 展示日期+时分秒
    PersonDTO convert(Person person);
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testMapping() {
        Person person = new Person();
        person.setName("Alice");
        person.setCreateTime(new Date());
        person.setUpdateTime(new Date());

        PersonDTO personDTO = PersonMapper.INSTANCE.convert(person);

        System.out.println("Name: " + personDTO.getName());
        System.out.println("Create Time1: " + personDTO.getCreateTime1());
        System.out.println("Create Time2: " + personDTO.getCreateTime2());
        System.out.println("Update Time1: " + personDTO.getUpdateTime1());
        System.out.println("Update Time2: " + personDTO.getUpdateTime2());
    }
}

结果:

image-20240428233402145

法2

更简单的写法,不用外部引入自定义映射方式,直接在接口里面写映射关系:

package com.example.springbootdemo.entity;

import lombok.Data;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: Person
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class Person {
    private String name;
    private Date createTime;
    private Date updateTime;
}
package com.example.springbootdemo.entity;

import lombok.Data;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class PersonDTO {
    private String name;
    private String createTime1;
    private String createTime2;
    private String updateTime1;
    private String updateTime2;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "createTime1", source = "createTime", dateFormat = "yyyy-MM-dd")// yyyy-MM-dd这种格式,你可以这样写
    @Mapping(target = "createTime2", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")// 展示日期和时分秒
    @Mapping(target = "updateTime1", source = "updateTime",  qualifiedByName = "formatDate")// 这样写可以展示xxx年xxx月xxx日
    @Mapping(target = "updateTime2", source = "updateTime",  qualifiedByName = "formatDateTime")// 展示日期+时分秒
    PersonDTO convert(Person person);

    @Named("formatDate")
    default String formatDate(Date date) {
        System.out.println("执行");
        if (date == null) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
        return sdf.format(date);
    }

    @Named("formatDateTime")
    default String formatDateTime(Date date) {
        if (date == null) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        return sdf.format(date);
    }
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testMapping() {
        Person person = new Person();
        person.setName("Alice");
        person.setCreateTime(new Date());
        person.setUpdateTime(new Date());

        PersonDTO personDTO = PersonMapper.INSTANCE.convert(person);

        System.out.println("Name: " + personDTO.getName());
        System.out.println("Create Time1: " + personDTO.getCreateTime1());
        System.out.println("Create Time2: " + personDTO.getCreateTime2());
        System.out.println("Update Time1: " + personDTO.getUpdateTime1());
        System.out.println("Update Time2: " + personDTO.getUpdateTime2());
    }
}

image-20240428233622807

法3
package com.example.springbootdemo.entity;

import lombok.Data;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: Person
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class Person {
    private String name;
    private Date createTime;
    private Date updateTime;
}
package com.example.springbootdemo.entity;

import lombok.Data;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class PersonDTO {
    private String name;
    private String createTime1;
    private String createTime2;
    private String updateTime1;
    private String updateTime2;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/28 23:04
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "createTime1", source = "createTime", dateFormat = "yyyy-MM-dd")// yyyy-MM-dd这种格式,你可以这样写
    @Mapping(target = "createTime2", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")// 展示日期和时分秒
    @Mapping(target = "updateTime1", source = "updateTime",  dateFormat = "yyyy年MM月dd日")// 这样写可以展示xxx年xxx月xxx日
    @Mapping(target = "updateTime2", source = "updateTime",  dateFormat = "yyyy年MM月dd日 HH时mm分ss秒")// 展示日期+时分秒
    PersonDTO convert(Person person);
}
package com.example.springbootdemo;

import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/17 21:32
 * @PackageName:com.copy
 * @ClassName: MapStructTest2
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testMapping() {
        Person person = new Person();
        person.setName("Alice");
        person.setCreateTime(new Date());
        person.setUpdateTime(new Date());

        PersonDTO personDTO = PersonMapper.INSTANCE.convert(person);

        System.out.println("Name: " + personDTO.getName());
        System.out.println("Create Time1: " + personDTO.getCreateTime1());
        System.out.println("Create Time2: " + personDTO.getCreateTime2());
        System.out.println("Update Time1: " + personDTO.getUpdateTime1());
        System.out.println("Update Time2: " + personDTO.getUpdateTime2());
    }
}

结果:

image-20240428233841181

看这种方式他底层自动生成的实现,其实就是和我们写的是一样的:

image-20240428233923488

忽略(不进行映射)

要忽略的字段只要加@Mapping#ignore() = true就行了。

例子,直接用上面的“自己测试写写”的例子就行了。

package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/16 23:41
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: Person
 * @Description: TODO
 * @Version 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    String describe;
    private String id;
    private String personName;
    private int age;
    private BigDecimal source;
    private double height;
    private Date createTime;
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:23
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
    String describe;
    private Long id;
    private String name;
    private String age;
    private String source;
    private String height;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:20
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
// mapper
@Mapper
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "name", source = "personName")
    @Mapping(target = "id", ignore = true) // 忽略id,不进行映射
    PersonDTO convert(Person person);
}
package com.example.springbootdemo;


import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;

/**
 * @Author yimeng
 * @Date 2024/4/25 23:01
 * @PackageName:com.example.springbootdemo
 * @ClassName: MapStructTest
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest {
    @Test
    public void test() {
        Person person = new Person();
        person.setId("11");
        person.setDescribe("测试");
        person.setAge(18);
        person.setPersonName("张三");
        person.setHeight(170.5);
        person.setSource(new BigDecimal("100"));

        PersonDTO dto = PersonMapper.INSTANCE.convert(person);

        System.out.println(dto);// 输出:PersonDTO(describe=测试, id=null, name=张三, age=18, source=100, height=170.5)

        // 结论:id没有映射到目标类,因为加了ignore = true。
        // describe映射成功了,虽然没有指定映射关系,但是默认会映射属性名一样的字段.
        // age映射成功了,虽然目标类是String,源是int,但是名字一样,它还是会自动映射的。当然这里肯定有一些默认映射的转换关系,如果是类型不好转为另一个类型的,两个类型差别很大,那么就算名字一样也不能强转,这是肯定的。
        // 如果目标和源的字段名不一样,那么需要通过 @Mapping 指定。比如,name成功映射到personName。
    }
}

image-20240425233947603

数字格式化(字符串转数字、数字转字符串)
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;

/**
 * @Author yimeng
 * @Date 2024/4/16 23:41
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: Person
 * @Description: TODO
 * @Version 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    private int age;//整数转字符串
    private double height;//浮点数转字符串
    private String weightStr;//字符串转浮点数
}
package com.example.springbootdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:23
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
    private String ageStr;
    private String heightStr;
    private Double weight;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/16 0:20
 * @PackageName:com.ruoyi.common.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    // 不写Mappings也行的
    //  第一个Mapping的# 表示不带小数点的整数
    //  第二个Mapping的# 表示任意数字,如果存在数字,则显示该数字;如果不存在数字,则不显示。
    //  第二个Mapping的. 表示小数点。
    //  第二个Mapping的## 表示保留两位小数。
    @Mapping(target = "ageStr", source = "age", numberFormat = "#岁")
    @Mapping(target = "heightStr", source = "height", numberFormat = "#.##cm")
    @Mapping(target = "weight", source = "weightStr", qualifiedByName = "removeUnit") // 将带单位的字符串转换为数值并去掉单位
    PersonDTO convert(Person person);

    // 写Mappings也行
    @Mappings({
            @Mapping(target = "ageStr", source = "age", numberFormat = "#岁"),
            @Mapping(target = "heightStr", source = "height", numberFormat = "#.##cm"),
            @Mapping(target = "weight", source = "weightStr", qualifiedByName = "removeUnit") // 将带单位的字符串转换为数值并去掉单位
    })
    PersonDTO convert2(Person person);

    @Named("removeUnit")
    default Double removeUnitFromString(String stringValue) {
        String valueWithoutUnit = stringValue.replaceAll("[^0-9.]", ""); // 去掉非数字和小数点的字符
        return Double.parseDouble(valueWithoutUnit);
    }
}
package com.example.springbootdemo;


import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;

/**
 * @Author yimeng
 * @Date 2024/4/25 23:01
 * @PackageName:com.example.springbootdemo
 * @ClassName: MapStructTest
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testMapping() {
        // 创建一个 Person 对象
        Person person = new Person();
        person.setAge(30);
        person.setHeight(190.768);
        person.setWeightStr("140斤");

        // 使用映射器接口进行映射
        PersonDTO personDTO = PersonMapper.INSTANCE.convert(person);

        // 验证映射结果
        System.out.println(personDTO);// 输出:PersonDTO(ageStr=30岁, heightStr=190.77cm, weight=140.0)

        // 创建一个 Person 对象
        Person person2 = new Person();
        person2.setAge(30);
        person2.setHeight(190.768);
        person2.setWeightStr("140斤");
        // 使用第二个映射方法
        PersonDTO personDTO2 = PersonMapper.INSTANCE.convert2(person2);
        // 验证映射结果
        System.out.println(personDTO2);// 输出:PersonDTO(ageStr=30岁, heightStr=190.77cm, weight=140.0)
        // 看到两种写法结果都是正确的。
    }
}

image-20240429222301839

@Mappings写法和多@Mapping的区别

答:没有区别。看自动实现的源码就行了,例子是上面的例子,自动生成的实现类源码如下。

image-20240429223345801

目标类值的覆盖
package com.example.springbootdemo.entity;

import lombok.Data;

/**
 * @Author yimeng
 * @Date 2024/4/29 23:00
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: User
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class Person {
    private String name;
    private int age;
    private Integer sex;
    private Integer height;
}
package com.example.springbootdemo.entity;

import lombok.Data;

/**
 * @Author yimeng
 * @Date 2024/4/29 23:00
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: UserDTO
 * @Description: TODO
 * @Version 1.0
 */
@Data
public class PersonDTO {
    private String name;
    private int age;
    private Integer sex;
    private Integer height;
}
package com.example.springbootdemo.entity;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

/**
 * @Author yimeng
 * @Date 2024/4/29 23:03
 * @PackageName:com.example.springbootdemo.entity
 * @ClassName: PersonMapper
 * @Description: TODO
 * @Version 1.0
 */
@Mapper
public interface PersonMapper {

    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    // 没有指定height,但是源和目标的属性名一样,所以会自动进行映射
    @Mapping(target = "name", source = "source.name")
    @Mapping(target = "age", source = "source.age")
    @Mapping(target = "sex", source = "source.sex")
    void updatePerson(PersonDTO source, @MappingTarget Person target);
}
package com.example.springbootdemo;


import com.example.springbootdemo.entity.Person;
import com.example.springbootdemo.entity.PersonDTO;
import com.example.springbootdemo.entity.PersonMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Author yimeng
 * @Date 2024/4/25 23:01
 * @PackageName:com.example.springbootdemo
 * @ClassName: MapStructTest
 * @Description: TODO
 * @Version 1.0
 */
@SpringBootTest
@Slf4j
public class MapStructTest2 {
    @Test
    public void testMapping() {
        PersonDTO source = new PersonDTO();
        source.setName("Alice");
        source.setAge(30);
        source.setHeight(30);

        Person target = new Person();
        target.setName("Bob");
        target.setAge(25);
        target.setSex(1);

        PersonMapper.INSTANCE.updatePerson(source, target);

        System.out.println("Updated PersonDTO:");// 输出:Updated PersonDTO:
        System.out.println("Name: " + target.getName());// 输出:Name: Alice
        System.out.println("Age: " + target.getAge());// 输出:Age: 30
        System.out.println("sex: " + target.getSex());// 输出:sex: null
        System.out.println("height: " + target.getHeight());// 输出:height: 30
    }
}

image-20240429231637878

已经源对象中不是null的值会覆盖,如果源对象中是null,会覆盖吗?答:会。

看到源码是把全部指定的或者属性名一样的都进行赋值操作,不管是不是null,是不是0。就是把能映射的都进行直接赋值。

image-20240429231737782

BeanCopier的使用(不加缓存一般快,加缓存快)

这个我就不介绍了,会上面一种应该就够了。

浅拷贝还是深拷贝

Java中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。

对象拷贝分为浅拷贝(浅克隆)与深拷贝(深克隆)

分类浅拷贝深拷贝
区别创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

上面讲的两个几种拷贝都是浅拷贝。

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值