当前位置: 首页 > 科技观察

扔掉那些BeanUtils工具类,MapStruct真香!!!_0

时间:2023-03-22 14:44:42 科技观察

在前几天的文章《为什么阿里巴巴禁止使用Apache Beanutils进行属性的copy?》中,我曾经比较过几个用于属性复制的工具类。然后评论区有读者反映说MapStruct真香,于是抽空学习了一下MapStruct。结果发现这真的是神仙架,炸鸡香。本文将简要介绍MapStruct的用法,并与其他几个工具类进行比较。为什么需要MapStruct?先说说MapStruct适用于什么样的场景,为什么市面上有那么多类似的框架。在软件架构设计中,分层结构是最常见也是最重要的结构。很多人对三层架构、四层架构等并不陌生,甚至有人说:“计算机科学领域的任何问题都可以通过增加一个间接中间层来解决,如果不行,那就加两层。”“跨数据模型会面临相互转换的问题。通常,我们在代码中可以看到各种各样的O,比如DO、DTO、VO等。一般情况下,对于同一个数据模型,我们需要在不同的层级使用不同的数据模型。比如在数据存储层,我们用DO来抽象一个业务实体;在业务逻辑层,我们使用DTO来表示数据传输对象;在表现层,我们将对象封装成VO,与前端进行交互。那么,数据从前端透传到数据持久层(从持久层到前端)需要对象之间的相互转换,即不同对象模型之间的映射。通常我们可以使用get/set来逐一进行字段映射操作,如:personDTO.setName(personDO.getName());personDTO.setAge(personDO.getAge());personDTO.setSex(personDO.getSex());personDTO.setBirthday(personDO.getBirthday());然而,编写这样的映射代码是一项冗长且容易出错的任务。像MapStruct这样的框架旨在通过自动化来尽可能地简化这项工作。使用MapStructMapStruct(https://mapstruct.org/)是一种代码生成器,它基于“约定优于配置”方法极大地简化了Javabean类型之间映射的实现。生成的映射代码使用纯方法调用,因此速度快、类型安全且易于理解。约定优于配置,也称为按约定编程,是一种软件设计范式,旨在减少软件开发人员需要做出的决策数量,在不牺牲灵活性的情况下获得简单性的好处。假设我们有两个类需要互相转换,分别是PersonDO和PersonDTO。类定义如下:publicclassPersonDO{privateIntegerid;privateStringname;privateintage;privateDatebirthday;privateStringgender;}publicclassPersonDTO{privateStringuserName;privateIntegerage;privateDatebirthday;privateGendergender;}我们演示如何使用MapStruct进行bean映射。想使用MapStruct,首先需要依赖他的相关的jar包,使用maven依赖方式如下:...1.3.1.Final...org.mapstructmapstruct${org.mapstruct.version}...org.apache.maven.pluginsmaven-compiler-plugin3.8.1<配置>1.81.8org.mapstructmapstruct-processor<版本>${org.mapstruct.version}因为MapStruct需要在编译器中生成转换代码,所以需要在maven-compiler-plugin中配置mapstruct-processorplugin这部分参考后面会再介绍。之后,我们需要定义一个映射接口。主要代码如下:@MapperinterfacePersonConverter{PersonConverterINSTANCE=Mappers.getMapper(PersonConverter.class);@Mappings(@Mapping(source="name",target="userName"))PersonDTOdo2dto(PersonDOperson);}使用注解@Mapper定义一个Converter接口,并在里面定义一个do2dto方法。该方法的入参类型为PersonDO,出参类型为PersonDTO。此方法用于将PersonDO转换为PersonDTO。测试代码如下:publicstaticvoidmain(String[]args){PersonDOpersonDO=newPersonDO();personDO.setName("Hollis");personDO.setAge(26);personDO.setBirthday(newDate());personDO.setId(1);personDO.setGender(Gender.MALE.name());PersonDTOpersonDTO=PersonConverter.INSTANCE.do2dto(personDO);System.out.println(personDTO);}输出结果:PersonDTO{userName='Hollis',age=26,birthday=SatAug0819:00:44CST2020,gender=MALE}可以看到我们使用MapStruct将PersonDO完美转换为PersonDTO。从上面的代码可以看出,MapStruct的用法比较简单,主要是依赖@Mapper注解。但是我们知道,在大多数情况下,我们需要相互转换的两个类之间的属性名、类型等并不完全相同,有些情况下我们并不想直接做映射,那么如何处理它?其实MapStruct在这方面也做得很好。MapStruct处理字段映射首先可以明确的告诉大家,如果要转换的两个类中的源对象属性和目标对象属性的类型和名称相同,就会自动映射对应的属性。那么,特殊情况如何处理呢?如何映射不一致的名称?如上面的例子,在PersonDO中用name表示用户名,在PersonDTO中用userName表示用户名,那么如何进行参数映射呢。这时候就需要用到@Mapping注解了。你只需要在方法签名上使用这个注解,并指定源对象的名称和要转换的目标对象的名称。比如将name的值映射到userName,可以使用下面的方法:@Mapping(source="name",target="userName")可以自动映射类型除了name不一致,还有一个特殊的case,也就是类型不一致,如上例,在PersonDO中使用String类型表示用户的性别,而在PersonDTO中使用了一个Genter枚举来表示用户的性别。这时候类型不一致,就需要涉及到相互转换的问题。其实MapStruct会自动映射一些类型,不需要我们额外配置。比如例子中,我们自动将String类型转换为枚举类型。一般来说,可以针对以下情况进行自动类型转换:基本类型及其对应的包装类型。基本类型的包装类型和String类型之间的自定义常量String类型和枚举类型之间的自定义常量如果我们想在转换和映射过程中为一些属性定义一个固定的值,我们可以使用constant@Mapping(source=”name",constant="hollis")类型不一致怎么映射还是上面的例子,如果我们需要在Person对象中添加家庭住址的属性,那么我们一般会在PersoDTO中单独定义一个HomeAddress类来表示家庭住址,而在Person类中,我们一般使用String类型来表示家庭住址。这需要使用JSON在HomeAddress和String之间进行转换。在这种情况下,MapStruct也可以支持。publicclassPersonDO{privateStringname;privateStringaddress;}publicclassPersonDTO{privateStringuserName;privateHomeAddressaddress;}@MapperinterfacePersonConverter{PersonConverterINSTANCE=Mappers.getMapper(PersonConverter.class);@Mapping(source="userName",target="name")@Mapping(target="address")",expression="java(homeAddressToString(dto2do.getAddress()))")PersonDOdto2do(PersonDTOdto2do);defaultStringhomeAddressToString(HomeAddressaddress){returnJSON.toJSONString(address);}}我们只需要在PersonConverter中定义一个方法即可(因为PersonConverter是一个接口,所以JDK1.8以后的版本可以定义一个默认的方法,这个方法的作用是将HomeAddress转成String类型。default方法:Java8引入的新语言特性,用关键字default标注,默认标注的方法需要提供实现,子类可以选择实现或者不实现该方法,然后在dto2do上传递如下注解method类型转换可以通过方式实现:@Mapping(target="address",expression="java(homeAddressToString(dto2do.getAddress()))")以上是自定义类型转换,部分类型转换是MapStruct本身支持,比如String和Date之间的转换:@Mapping(target="birthday",dateFormat="yyyy-MM-ddHH:mm:ss")上面简单介绍了一些常用的字段映射方法,也就是几个我在工作中经常遇到的场景。更多情况可以查看官方示例(https://github.com/mapstruct/mapstruct-examples)。MapStruct的性能我们前面提到了很多MapStruct的用法,可以看出MapStruct的使用比较简单,而且字段映射的功能非常强大,那么它的性能如何呢?参考《为什么阿里巴巴禁止使用Apache Beanutils进行属性的copy?》中的例子,我们对MapStruct进行性能测试。执行1000、10000、100000、1000000次映射的耗时分别为:0ms、1ms、3ms、6ms。可以看出MapStruct的耗时相比其他几个工具是非常短的。那么,为什么MapStruct的性能这么好呢?其实MapStruct和其他类型的框架最大的区别是:相对于其他的映射框架,MapStruct在编译时生成bean映射,保证了高性能,可以提前反馈问题,也让开发者做的更透彻错误检查。还记得我们在maven-compiler-plugin中引入MapStruct的依赖,特别是mapstruct-processor的支持吗?而我们在代码中使用了很多MapStruct提供的注解,这使得MapStruct可以直接生成bean映射的代码,相当于代替我们写了很多setter和getter。比如我们在代码中定义了如下的Mapper:@MapperinterfacePersonConverter{PersonConverterINSTANCE=Mappers.getMapper(PersonConverter.class);@Mapping(source="userName",target="name")@Mapping(target="address",表达式="java(homeAddressToString(dto2do.getAddress()))")@Mapping(target="生日",dateFormat="yyyy-MM-ddHH:mm:ss")PersonDOdto2do(PersonDTOdto2do);defaultStringhomeAddressToString(HomeAddressaddress){returnJSON.toJSONString(address);}}代码编译后会自动生成一个PersonConverterImpl:@Generated(value="org.mapstruct.ap.MappingProcessor",date="2020-08-09T12:58:41+0800",comments="version:1.3.1.Final,compiler:javac,environment:Java1.8.0_181(OracleCorporation)")classPersonConverterImplimplementsPersonConverter{@OverridepublicPersonDOdto2do(PersonDTOdto2do){if(dto2do==null){returnnull;}PersonDOpersonDO=newPersonDO();personDO.setName(dto2do.getUserName());if(dto2do.getAge()!=null){personDO.setAge(dto2do.getAge());}if(dto2do.getGender()!=null){personDO.setGender(dto2do.getGender().name());}personDO.setAddress(homeAddressToString(dto2do.getAddress()));returnpersonDO;}}在runtime的时候,映射bean的时候,会直接调用PersonConverterImpl的dto2do方法,所以没什么特别的,在内存中设置和获取即可。所以,因为很多事情都是在编译期做的,所以MapStruct在运行期会表现的很好,而且还有一个好处,就是可以把问题的暴露提前到编译期。这样如果代码中的字段映射出现问题,应用程序将无法编译,迫使开发人员解决这个问题。小结本文介绍了Java中的一个字段映射工具类MapStruct。它的使用比较简单,功能也很齐全,可以处理各种情况下的字段映射。并且因为它会在编译时生成真正的映射代码,所以运行时的性能得到了很大的提升。