在我们日常开发的程序中,为了各层之间解耦,一般会定义不同的对象用来在不同层之间传递数据,比如xxxDTO、xxxVO、xxxQO,当在不同层之间传输数据时,不可避免地经常需要将这些对象进行相互转换。
今天给大家介绍一个对象转换工具MapStruct,代码简洁安全、性能高,强烈推荐。
1. MapStruct简介
MapStruct是一个代码生成器,它基于约定优于配置,极大地简化了Java Bean类型之间映射的实现。特点如下:
1.基于注解
2.在编译期自动生成映射转换代码
3.类型安全、高性能、无依赖性、易于理解阅读
2.0 MapStruct入门
2.0.1 简易demo
Car.java
public class Car { private String make; private int numberOfSeats; private CarType type; //constructor, getters, setters etc. }
CarDto .java
public class CarDto { private String make; private int seatCount; private String type; //constructor, getters, setters etc. }
@Mapper (1:下面对此解释) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper( CarMapper.class ); (3:下面对此解释) @Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto(Car car); (2:下面对此解释) }
注释@Mapper(1)将接口标记为映射接口,并允许 MapStruct 处理器在编译期间启动。
实际的映射方法2期望源对象作为参数并返回目标对象。它的名字可以自由选择。
对于源对象和目标对象中具有不同名称的属性,可以使用注释来配置名称。@Mapping
在需要和可能的情况下,将为源和目标中具有不同类型的属性执行类型转换,例如,属性将从枚举类型转换为字符串。type
当然,一个接口中可以有多个映射方法,所有这些方法的实现都将由MapStruct生成。
可以从类中检索接口实现的实例。按照惯例,接口声明一个成员MappersINSTANCE 3,为客户端提供对映射器实现的访问。
shouldMapCarToDto.java
@Test public void shouldMapCarToDto() { //given Car car = new Car( "Morris", 5, CarType.SEDAN ); //when CarDto carDto = CarMapper.INSTANCE.carToCarDto( car ); //then assertThat( carDto ).isNotNull(); assertThat( carDto.getMake() ).isEqualTo( "Morris" ); assertThat( carDto.getSeatCount() ).isEqualTo( 5 ); assertThat( carDto.getType() ).isEqualTo( "SEDAN" ); }
如想了解更多请查看官网内容(https://mapstruct.org/)
2.1. 引入依赖
这里使用Gradle构建
dependencies { implementation 'org.mapstruct:mapstruct:1.4.2.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' }
maven仓库地址;
// https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor implementation group: 'org.mapstruct', name: 'mapstruct-processor', version: '1.4.2.Final'
maven地址:
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> </dependency>
2.2. 需要转换的对象
创建两个示例对象(EG: 将Demo对象转换为DemoDto对象)
保证对象之间的值是相同的;
/** * 源对象 */ @Data public class Demo { private Integer id; private String name; } /** * 目标对象 */ @Data public class DemoDto { private Integer id; private String name; }
2.3. 创建转换器
只需要创建一个转换器接口类,并在类上添加 @Mapper 注解即可(官方示例推荐以 xxxMapper 格式命名转换器名称)
@Mapper public interface DemoMapper { //使用Mappers工厂获取DemoMapper实现类 DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class); //定义接口方法,参数为来源对象,返回值为目标对象 DemoDto toDemoDto(Demo demo); }
2.4. 验证
public static void main(String[] args) { Demo demo = new Demo(); demo.setId(111); demo.setName("hello"); DemoDto demoDto = DemoMapper.INSTANCE.toDemoDto(demo); System.out.println("目标对象demoDto为:" + demoDto); //输出结果:目标对象demoDto为:DemoDto(id=111, name=hello) }
测试结果如下:
目标对象demoDto为:DemoDto(id=111, name=hello)
达到了我们的预期结果。
2.5. 自动生成的实现类
为什么声明一个接口就可以转换对象呢?
我们看一下MapStruct在编译期间自动生成的实现类:
@Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2022-09-01T17:54:38+0800", comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.jar, environment: Java 1.8.0_231 (Oracle Corporation)" ) public class DemoMapperImpl implements DemoMapper { @Override public DemoDto toDemoDto(Demo demo) { if ( demo == null ) { return null; } DemoDto demoDto = new DemoDto(); demoDto.setId( demo.getId() ); demoDto.setName( demo.getName() ); return demoDto; } }
可以看到,MapStruct帮我们将繁杂的代码自动生成了,而且实现类中用的都是最基本的get、set方法,易于阅读理解,转换速度非常快。
3.0 MapStruct进阶
上面的例子只是小试牛刀,下面开始展示MapStruct的强大之处。
(限于篇幅,这里不展示自动生成的实现类和验证结果,大家可自行测试)
场景1:属性名称不同、(基本)类型不同
- 属性名称不同: 在方法上加上 @Mapping 注解,用来映射属性
- 属性基本类型不同: 基本类型和String等类型会自动转换
关键字:@Mapping注解
/** * 来源对象 */ @Data public class Demo { private Integer id; private String name; } /** * 目标对象 */ @Data public class DemoDto { private String id; private String fullname; } /** * 转换器 */ @Mapper public interface DemoMapper { DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class); @Mapping(target = "fullname", source = "name") DemoDto toDemoDto(Demo demo); }
场景2:统一映射不同类型
下面例子中,time1、time2、time3都会被转换,具体说明看下面的注释:
/** * 来源对象 */ @Data public class Demo { private Integer id; private String name; /** * time1、time2名称相同,time3转为time33 * 这里的time1、time2、time33都是Date类型 */ private Date time1; private Date time2; private Date time3; } /** * 目标对象 */ @Data public class DemoDto { private String id; private String name; /** * 这里的time1、time2、time33都是String类型 */ private String time1; private String time2; private String time33; } /** * 转换器 */ @Mapper public interface DemoMapper { DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class); @Mapping(target = "time33", source = "time3") DemoDto toDemoDto(Demo demo); //MapStruct会将所有匹配到的: //源类型为Date、目标类型为String的属性, //按以下方法进行转换 static String date2String(Date date) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String strDate = simpleDateFormat.format(date); return strDate; } }
场景3:固定值、忽略某个属性、时间转字符串格式
一个例子演示三种用法,具体说明看注释,很容易理解:
关键字:ignore、constant、dateFormat
/** * 来源对象 */ @Data public class Demo { private Integer id; private String name; private Date time; } /** * 目标对象 */ @Data public class DemoDto { private String id; private String name; private String time; } /** * 转换器 */ @Mapper public interface DemoMapper { DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class); //id属性不赋值 @Mapping(target = "id", ignore = true) //name属性固定赋值为“hello” @Mapping(target = "name", constant = "hello") //time属性转为yyyy-MM-dd HH:mm:ss格式的字符串 @Mapping(target = "time", dateFormat = "yyyy-MM-dd HH:mm:ss") DemoDto toDemoDto(Demo demo); }
场景4:为某个属性指定转换方法
场景2中,我们是按照某个转换方法,统一将一种类型转换为另外一种类型;而下面这个例子,是为某个属性指定方法:
关键字:@Named注解、qualifiedByName
/** * 来源对象 */ @Data public class Demo { private Integer id; private String name; } /** * 目标对象 */ @Data public class DemoDto { private String id; private String name; } /** * 转换器 */ @Mapper public interface DemoMapper { DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class); //为name属性指定@Named为convertName的方法进行转换 @Mapping(target = "name", qualifiedByName = "convertName") DemoDto toDemoDto(Demo demo); @Named("convertName") static String aaa(String name) { return "姓名为:" + name; } }
场景5:多个参数合并为一个对象
如果参数为多个的话,@Mapping注解中的source就要指定是哪个参数了,用点分隔:
关键字:点(.)
/** * 来源对象 */ @Data public class Demo { private Integer id; private String name; } /** * 目标对象 */ @Data public class DemoDto { private String fullname; private String timestamp; } /** * 转换器 */ @Mapper public interface DemoMapper { DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class); //fullname属性赋值demo对象的name属性(注意这里.的用法) //timestamp属性赋值为传入的time参数 @Mapping(target = "fullname", source = "demo.name") @Mapping(target = "timestamp", source = "time") DemoDto toDemoDto(Demo demo, String time); }