【MapStruct】还在用BeanUtils?不如试试MapStruct

1. 什么是MapStruct

MapStruct是一个Java注解处理器,它可以简化Java bean之间的转换。它使用基于生成器的方法创建类型安全的映射代码,这些代码在编译时生成,并且比反射更快、更可靠。使用MapStruct可以避免手动编写大量重复的转换代码,从而提高生产力和代码质量。

MapStruct通过使用注解,在源代码中指定映射规则,MapStruct可以自动生成转换器代码。MapStruct支持各种转换场景,包括简单类型、集合、继承、日期、枚举、嵌套映射等等。同时,它还能够与Spring和CDI等IoC容器无缝集成,方便地将MapStruct转换器注入到应用程序中。

MapStruct的官网:MapStruct – Java bean mappings, the easy way!

在开发中比较常用的用来实现JavaBean之间的转换应该就是org.springframework.beans.BeanUtils,它俩有以下区别:

  1. 编译时生成代码 vs 运行时反射MapStruct生成的映射代码是在编译时生成的,而BeanUtils则是在运行时使用反射机制实现转换
  2. 性能和可扩展性:由于MapStruct生成的代码是类型安全的,因此可以比使用反射更加高效和可靠。同时,MapStruct还能够自定义转换逻辑并支持扩展,使得它更加灵活和可扩展。
  3. 集成方式:MapStruct可以无缝集成到Spring中,也可以与其他IoC容器结合使用;而BeanUtils是Spring框架自带的工具类。
  4. 映射规则的定义方式:MapStruct使用基于注解的方式在源代码中定义映射规则,而BeanUtils则需要手动编写复杂的转换方法。

2. 为什么使用MapStruct

在一些高并发的场景,性能是开发者十分重视的,BeanUtils虽然也可以方便地完成JavaBean之间的转换,但是由于其底层是基于反射实现的,在高并发场景下难免会出现大规模的数据处理和转换操作,这时候还是用BeanUtils会导致接口响应速度有所下降。

这时候,最最最高效的方法就是手动get/set,但是这种需要反复写大量重复的转换代码,并且这些代码难以被反复利用,于是就考虑使用MapStruct。

MapStruct是一种基于注解的代码生成器,它通过生成优化的映射代码来实现高性能的Bean映射。与BeanUtils相比,MapStruct在生成的映射代码中使用了更少的反射调用,并且在类型转换时可以直接使用Javac编译器已经提供的类型转换逻辑,从而避免了额外的性能开销。此外,由于MapStruct是基于注解的,它还可以提供更好的类型检查和编译时错误提示。

以下是当前比较常用的JavaBean之间转化的工具的性能对比:

image-20230602212240984

可见,随着转化次数的增加只有MapStruct的性能最接近get/set的效率。

因此,在高并发场景下,使用MapStruct可以更有效地利用系统资源,提高系统的吞吐量和响应速度。


3. 如何使用MapStruct

接下来用一个案例来说说如何使用MapStruct,更多详细的用法可以查看官方文档。

温馨提示:在生成get/set方法的时候,最好不要用lombok,不然有可能会出现一些奇奇怪怪的问题

先引入依赖:

<!-- MapStruct begin -->
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
</dependency>
<!-- MapStruct end -->

Student类:

public class Student {
    private String sId;
    private String sName;
    private String sSex;

    public String getsId() {
        return sId;
    }

    public void setsId(String sId) {
        this.sId = sId;
    }

    public String getsName() {
        return sName;
    }

    public void setsName(String sName) {
        this.sName = sName;
    }

    public String getsSex() {
        return sSex;
    }

    public void setsSex(String sSex) {
        this.sSex = sSex;
    }
}

StudentVo类:

public class StudentVo {
    private String id;
    private String name;
    private String sSex;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getsSex() {
        return sSex;
    }

    public void setsSex(String sSex) {
        this.sSex = sSex;
    }
}

现在模拟两个业务:

  1. Student类 转 StudentVo类
  2. StudentVo类 转 Student类

首先,需要定义一个对象接口映射的接口

/**
 * @description:对象接口映射
 * @author:lrk
 * @date: 2023/6/2
 */
@MapperConfig
public interface IMapping<SOURCE, TARGET> {
    /**
     * 映射同名属性
     * @param var1 源
     * @return     结果
     */
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    TARGET sourceToTarget(SOURCE var1);

    /**
     * 映射同名属性,反向
     * @param var1 源
     * @return     结果
     */
    @InheritInverseConfiguration(name = "sourceToTarget")
    SOURCE targetToSource(TARGET var1);

    /**
     * 映射同名属性,集合形式
     * @param var1 源
     * @return     结果
     */
    @InheritConfiguration(name = "sourceToTarget")
    List<TARGET> sourceToTarget(List<SOURCE> var1);

    /**
     * 反向,映射同名属性,集合形式
     * @param var1 源
     * @return     结果
     */
    @InheritConfiguration(name = "targetToSource")
    List<SOURCE> targetToSource(List<TARGET> var1);

    /**
     * 映射同名属性,集合流形式
     * @param stream 源
     * @return       结果
     */
    List<TARGET> sourceToTarget(Stream<SOURCE> stream);

    /**
     * 反向,映射同名属性,集合流形式
     * @param stream 源
     * @return       结果
     */
    List<SOURCE> targetToSource(Stream<TARGET> stream);
}

这个接口上需要加org.mapstruct.MapperConfig的注解,表示这是一个构造器。

接下来,就需要构造一个具体类的转换配置了,需要继承IMapping接口

这里就模拟转换两个类的相互转化,其他比如List的转换可以自主实现

/**
 * @description:学生类转换配置
 * @author:lrk
 * @date: 2023/6/2
 */
@Mapper(
        // 指定依赖注入框架
        componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        unmappedSourcePolicy = ReportingPolicy.IGNORE
)
public interface StudentMapping extends IMapping<Student, StudentVo>{

    /**
     * Student 转 StudentVo
     * @param var1 源
     * @return
     */
    @Mapping(target = "id", source = "sId")
    @Mapping(target = "name", source = "sName")
    @Override
    StudentVo sourceToTarget(Student var1);

    /**
     * StudentVo 转 Student
     * @param var1 源
     * @return
     */
    @Mapping(target = "sId", source = "id")
    @Mapping(target = "sName", source = "name")
    @Override
    Student targetToSource(StudentVo var1);
}
  1. 继承接口的泛型为<Student, StudentVo>,指定了Student是源,StudentVo是目标。
  2. 接口上需要有org.mapstruct.Mapper注解,注解里面有三个参数
    • componentModel:指定依赖注入框架,目前只支持springCDI
    • unmappedTargetPolicy:在映射方法的目标对象的属性未填充源值时应用的默认报告策略。
      • ERROR:任何未映射的目标属性都将导致映射代码生成失败
      • WARN:任何未映射的目标属性都将在构建时导致警告
      • IGNORE:忽略未映射的目标属性
    • unmappedSourcePolicy:同上
  3. 认真观察Student与StudentVo可以看见,两个类除了成员变量sSex一样外,其余的成员变量属性均不一样,这时候可以使用org.mapstruct.Mapping注解在接口方法上
    • Mapping:当属性在目标实体中具有不同的名称时,可以通过注释指定其名称

编写测试代码

@Resource
private IMapping<Student, StudentVo> studentMapping;


@Test
public void test_mapstruct() {
    Student student = new Student();
    student.setsId("10086");
    student.setsName("张三");
    student.setsSex("男");
    StudentVo studentVo = studentMapping.sourceToTarget(student);
    log.info("Student:{} 转 StudentVo:{}", JSONUtil.toJsonStr(student), JSONUtil.toJsonStr(studentVo));
    Student student1 = studentMapping.targetToSource(studentVo);
    log.info("StudentVo:{} 转 Student:{}", JSONUtil.toJsonStr(studentVo), JSONUtil.toJsonStr(student1));

}

image-20230602214035903

细心的你估计已经发现了,为什么这里可以用@ResourceIMapping<Student, StudentVo>注入,代码中并没有看见将其放进Spring容器呀?

别急,前面说到MapStruct的转换代码是在编译时生成的,查看编译生成的代码可以发现其中已经加入了@Component注解并实现了StudentMapping

@Component
public class StudentMappingImpl implements StudentMapping {
    public StudentMappingImpl() {
    }

    public List<StudentVo> sourceToTarget(List<Student> var1) {
        if (var1 == null) {
            return null;
        } else {
            List<StudentVo> list = new ArrayList(var1.size());
            Iterator var3 = var1.iterator();

            while(var3.hasNext()) {
                Student student = (Student)var3.next();
                list.add(this.sourceToTarget(student));
            }

            return list;
        }
    }

    public List<Student> targetToSource(List<StudentVo> var1) {
        if (var1 == null) {
            return null;
        } else {
            List<Student> list = new ArrayList(var1.size());
            Iterator var3 = var1.iterator();

            while(var3.hasNext()) {
                StudentVo studentVo = (StudentVo)var3.next();
                list.add(this.targetToSource(studentVo));
            }

            return list;
        }
    }

    public List<StudentVo> sourceToTarget(Stream<Student> stream) {
        return stream == null ? null : (List)stream.map((student) -> {
            return this.sourceToTarget(student);
        }).collect(Collectors.toCollection(ArrayList::new));
    }

    public List<Student> targetToSource(Stream<StudentVo> stream) {
        return stream == null ? null : (List)stream.map((studentVo) -> {
            return this.targetToSource(studentVo);
        }).collect(Collectors.toCollection(ArrayList::new));
    }

    public StudentVo sourceToTarget(Student var1) {
        if (var1 == null) {
            return null;
        } else {
            StudentVo studentVo = new StudentVo();
            studentVo.setId(var1.getsId());
            studentVo.setName(var1.getsName());
            studentVo.setsSex(var1.getsSex());
            return studentVo;
        }
    }

    public Student targetToSource(StudentVo var1) {
        if (var1 == null) {
            return null;
        } else {
            Student student = new Student();
            student.setsId(var1.getId());
            student.setsName(var1.getName());
            student.setsSex(var1.getsSex());
            return student;
        }
    }
}

这是因为前面在StudentMapping指定了依赖注入框架为Spring的原因,所以生成的代码自动将其放进容器,方便我们开发者使用。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
MapStructBeanUtilsJava中常用的对象映射工具,它们都可以用于将一个对象的值映射到另一个对象上。以下是它们的优缺点: MapStruct的优点: 1. 性能优秀:MapStruct在编译期间生成映射代码,相比运行时的反射机制,具有更好的性能表现。 2. 类型安全:MapStruct在编译期间进行类型检查,避免了在运行时可能出现的类型转换错误。 3. 易于使用:MapStruct通过注解配置简单明了,生成的映射代码也易于理解和维护。 MapStruct的缺点: 1. 学习曲线较陡:对于初学者来说,需要一定时间去了解和掌握MapStruct的使用方式和配置方式。 2. 配置复杂:对于复杂的映射场景,可能需要编写自定义的转换器或者使用复杂的配置方式。 BeanUtils的优点: 1. 简单易用:BeanUtils提供了简单的API,易于学习和使用。 2. 动态性:BeanUtils使用反射机制,在运行时可以动态地进行属性复制。 BeanUtils的缺点: 1. 性能较差:由于使用了反射机制,BeanUtils在属性复制过程中性能相对较低,特别是处理大量对象时会有明显的性能损耗。 2. 不支持类型安全:BeanUtils在属性复制时没有类型检查,容易出现类型转换错误。 综上所述,MapStruct在性能和类型安全方面具有优势,适用于需要高性能和类型安全的场景。而BeanUtils则更适用于简单的属性复制场景,对于性能要求不高且不涉及复杂类型转换的情况下使用较为方便。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起名方面没有灵感

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值