正文
一、常用的对象拷贝工具基本介绍
属性拷贝工具有很多,也许你用过如下的一些:
- Apache commons-beanutils
- Spring BeanUtils
- cglib BeanCopier
- HuTool BeanUtils
- MapStruct
- getter & setter
这些属性拷贝工具各自有什么特点和区别?在日常开发使用中,我们该如何做出选择?
1.1 Apache BeanUtils
参数顺序和其它的工具正好相反,导致使用不顺手,容易产生问题;
阿里巴巴代码扫描插件会给出明确的告警;
基于反射实现,性能较差;
不推荐使用;
1.2 Spring BeanUtils
基于内省+反射,借助getter/setter方法实现属性拷贝,性能比apache高;
在简单的属性拷贝场景下推荐使用;
1.3 cglib BeanCopier
通过动态代理的方式来实现属性拷贝;
性能高效;
在简单的属性拷贝场景下推荐使用;
1.4 HuTool BeanUtils
性能介于apache和Spring之间;
需要额外引入HuTool的依赖;
1.5 MapStruct
基于getter/setter方法实现属性拷贝,在编译时自动生成实现类的代码;
性能媲美getter & setter;
强大的功能可以实现深度拷贝;
缺点是需要声明bean的转换接口类;
1.6 getter & setter
性能最高,但是需要手动拷贝;
1.7 总结
经过第三方的对比结果,总的下来,推荐使用顺序为:
apache < HuTool < Spring < cglib < Mapstruct
二、使用介绍
2.1 准备工作
<!-- 导入MapStruct的核心注释 --> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> </dependencies> ... <!-- MapStruct在编译时工作,并且会集成到像Maven和Gradle这样的构建工具上,我们还必须在<build中/>标签中添加一个插件maven-compiler-plugin,并在其配置中添加annotationProcessorPaths,该插件会在构建时生成对应的代码。 --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
2.2 映射
2.2.1 基本映射
我们现在需要实现一个场景,Car是一个domain层的对象实例,在从数据库读取出来后传递给service层需要转换为CarDTO,这两个实例的所有属性全部相同,现在需要使用mapstruct来完成这个目标。
public class Car { private String brand; private Double price; private Boolean onMarket; ... // setters + getters + toString }
public class CarDTO { private String brand; private Double price; private Boolean onMarket; ... // setters + getters + toString }
我们需要新建一个Mapper接口,来映射这两个对象之间的属性。
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); CarDTO carToCarDTO(Car car); }
然后就可以进行测试了:
@Test public void test1(){ Car wuling = new Car(); wuling.setBrand("wuling"); wuling.setPrice(6666.66); wuling.setOnMarket(true); CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling); // 结果为:Car{brand='wuling', price=6666.66, onMarket=true} System.out.println("结果为:" + wulingDTO); }
可以看到,mapstruct很好地完成了我们的目标,那么它是如何做到的呢?我们查看CarMapper.INSTANCE.carToCarDTO(wuling)的实现类,可以看到在编译过程中自动生成了如下内容的接口实现类:
public class CarMapperImpl implements CarMapper { @Override public CarDTO carToCarDTO(Car car) { if ( car == null ) { return null; } CarDTO carDTO = new CarDTO(); carDTO.setBrand( car.getBrand() ); carDTO.setPrice( car.getPrice() ); carDTO.setOnMarket( car.getOnMarket() ); return carDTO; } }
所以,mapstruct并没有使用反射的机制,而是使用了普通的set和get方法来进行属性拷贝的,因此要求我们的对象也一定要有set和get方法。
2.2.2 不同属性名映射
在如上示例中,我们源对象和目标对象的属性名称全都一致,但是在很多的场景下,源对象和目标对象的同一个字段很可能名称是不同的,这种情况下,只需要在映射接口类中指定即可:
public class Car { ... private Boolean onMarket; ... // setters + getters + toString }
public class Car { ... private Boolean onMarket; ... // setters + getters + toString }
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "car.onMarket", target = "onSale") CarDTO carToCarDTO(Car car); }
如此,生成的接口实现类如下:
@Override public CarDTO carToCarDTO(Car car) { if ( car == null ) { return null; } CarDTO carDTO = new CarDTO(); carDTO.setOnSale( car.getOnMarket() ); carDTO.setBrand( car.getBrand() ); carDTO.setPrice( car.getPrice() ); return carDTO; }
2.2.3 不同个数属性映射
我们假设Car和CarDTO各有一个对方没有的属性,那么在进行对象拷贝时会发生什么?
public class Car { ... private Date birthdate; // setters + getters + toString }
public class CarDTO { ... private String owner; // setters + getters + toString }
p
@Test public void test1(){ Car wuling = new Car(); wuling.setBrand("wuling"); wuling.setPrice(6666.66); wuling.setOnMarket(true); wuling.setBirthdate(new Date()); CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling); System.out.println("结果为:" + wulingDTO); }
然后我们执行如上转换的案例,发现并没有报错,从Car拷贝属性到CarDTO时,CarDTO由于没有birthdate属性,则不会赋值;同时,CarDTO的owner因为Car中没有,因此也不会被赋值,生成的接口实现类如下:
@Override public CarDTO carToCarDTO(Car car) { if ( car == null ) { return null; } CarDTO carDTO = new CarDTO(); carDTO.setOnSale( car.getOnMarket() ); carDTO.setBrand( car.getBrand() ); carDTO.setPrice( car.getPrice() ); return carDTO; }
因此,mapstruct只会对共有的交集属性进行拷贝操作。
2.2.4 多个源合并映射
我们新增一个Person类,其中的name属性对应CarDTO中的owner属性。
public class Person { private String name; private String age; // setters + getters + toString }
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "car.onMarket", target = "onSale") @Mapping(source = "person.name", target = "owner") CarDTO carToCarDTO(Car car, Person person); }
自动生成的接口实现类如下:
@Override public CarDTO carToCarDTO(Car car, Person person) { if ( car == null && person == null ) { return null; } CarDTO carDTO = new CarDTO(); if ( car != null ) { carDTO.setOnSale( car.getOnMarket() ); carDTO.setBrand( car.getBrand() ); carDTO.setPrice( car.getPrice() ); } if ( person != null ) { carDTO.setOwner( person.getName() ); } return carDTO; }
2.2.5 子对象映射
如果需要转换的Car对象中的某个属性不是基本数据类型,而是一个对象怎么处理呢。
public class Person { private String name; private String age; // setters + getters + toString } public class PersonDTO { private String name; private String age; // setters + getters + toString }
public class Car { private String brand; private Double price; private Boolean onMarket; private Person owner; // setters + getters + toString } public class CarDTO { private String brand; private Double price; private Boolean onMarket; private PersonDTO owner; // setters + getters + toString }
@Mapper public interface PersonMapper { PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class); PersonDTO personToPersonDTO(Person person); }
@Mapper(uses = {PersonMapper.class}) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "onMarket", target = "onSale") CarDTO carToCarDTO(Car car); }
@Test public void test1(){ Person jack = new Person(); jack.setName("jack"); jack.setAge("22"); Car wuling = new Car(); wuling.setBrand("wuling"); wuling.setPrice(6666.66); wuling.setOnMarket(true); wuling.setOwner(jack); CarDTO wulingDTO = CarMapper.INSTANCE.carToCarDTO(wuling); // 结果为:CarDTO{brand='wuling', price=6666.66, onSale=true, owner=Person{name='jack', age='22'}} System.out.println("结果为:" + wulingDTO); }
这里最重要的是:
- 需要增加PersonMapper接口,让mapstruct能够对Person和PersonDTO进行转换;
- CarMapper中需要引入PersonMapper,如果存在多个对象属性,此处就要引入多个对象属性的Mapper接口;