1 Cglib复制
BeanCopier
是Cglib包中的一个类,用于对象的复制。
注意:目标对象必须先实例化 而且对象必须要有setter方法
示例:
BeanCopier copier = BeanCopier.create(Source.class, Target.class, false);
copier.copy(source, target, null);
第三个参数useConverter
,是否开启Convert
。默认BeanCopier
只会做同名,同类型属性的copier
,否则就会报错。如果类型需要转换比如Date转换成String则自定义Convert类实现Convert接口
重写convert
方法时,里面的三个参数:value
源对象属性,target
目标对象属性类,context
目标对象setter方法名
public class BeanCopyUtilDemo {
public static void copy (Object source,Object target){
AccountConverter converter = new AccountConverter();
BeanCopier copier = BeanCopier.create(source.getClass(),target.getClass(),true);
copier.copy(source,target,converter);
}
private static class AccountConverter implements Converter{
@Override
// value 源对象属性,target 目标对象属性类,context 目标对象setter方法名
public Object convert(Object value,Class target,Object context){
Logger logger =LoggerFactory.getLogger(this.getClass());
try{
if(Objects.notNull(value) && StringUtils.isNoneBlank(value.toString())){
if(("BigDecimal").equals(target.getSimpleName())){
BigDecimal bd = new BigDecimal(String.valueOf("null".equals(value)?0:value));
return bd;
}
if(value instanceof BigDecimal){
BigDecimal bd = (BigDecimal) value;
return bd.toPlainString();
}
if(("Integer").equals(target.getSimpleName())){
Integer bd = new Integer(String.valueOf("null".equals(value)?0:value));
return bd;
}
if(value instanceof Integer){
Integer bd = (Integer) value;
return bd.toPlainString();
}
}else{
return null;
}
}catch (Exception e){
e.printStackTrace();
}
return value;
}
}
}
2 mapstruct
MapStruct
是一个代码生成器,它基于约定优于配置,极大地简化了Java Bean
类型之间映射的实现。特点如下:
- 基于注解
- 在编译期自动生成映射转换代码
- 类型安全、高性能、无依赖性、易于理解阅读
2.1 引入依赖
pom.xml地址
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Gradle构建
dependencies {
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
2.2 简单示例
2.2.1 需要转换的对象
@Data
public class Demo {
private Integer id;
private String name;
}
/**
* 目标对象
*/
@Data
public class DemoDto {
private Integer id;
private String name;
}
2.2.2 创建转换器
只需要创建一个转换器接口类,并在类上添加 @Mapper 注解即可(官方示例推荐以 xxxMapper 格式命名转换器名称)
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface DemoMapper {
使用Mappers工厂获取DemoMapper实现类
DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class);
定义接口方法,参数为来源对象,返回值为目标对象
DemoDto toDemoDto(Demo demo);
}
验证
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)
}
2.2.3 原理讲解
为什么声明一个接口就可以转换对象呢?我们看一下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方法,易于阅读理解,转换速度非常快。
2.3 注解说明
2.3.1 @Mapper
我们翻开上边提到的 Mapper
注释的源码,该注释的解释是:将接口或抽象类标记为 映射器
,并通过 MapStruct
激活 该类型实现
的生成。我们找到其中的 componentModel
属性,默认值为 default
,它有四种值供我们选择:
-
default
:映射器不使用组件模型,实例通常通过Mappers.getMapper(java.lang.Class)
获取; -
cdi
:生成的映射器是application-scoped
的CDI bean
,可以通过@Inject
获取; -
spring
:生成的映射器是Spring bean
,可以通过@Autowired
获取; -
jsr330
:生成的映射器被@javax.inject.Named
和@Singleton
注释,可以通过@inject
获取;
uses
属性 ,并指定自定义的处理类,如下示例
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CarDto {
private String make;
private int seatCount;
private String type;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CarVo {
private String make;
private int seatCount;
private boolean type;
}
@Mapper(uses = {BooleanStrFormat.class})
public interface CarMapper {
......
}
@Component
public class BooleanStrFormat {
public String toStr(boolean type) {
if(type){
return "Y";
}else{
return "N";
}
}
public boolean toBoolean(String type) {
if (type.equals("Y")) {
return true;
} else {
return false;
}
}
}
/**
* 查看编译后生成的实现类
*/
public CarDto carVoToCarDto(CarVo carVo) {
if (carVo == null) {
return null;
} else {
CarDto carDto = new CarDto();
carDto.setMake(carVo.getMake());
carDto.setSeatCount(carVo.getSeatCount());
//调用自定义的类中的方法
carDto.setType(this.booleanStrFormat.toStr(carVo.isType()));
return carDto;
}
}
2.3.2 @Mapping
@Mapping
可以用来配置一个 bean
属性或枚举常量的映射,默认是将具有相同名称的属性进行映射,当然也可以用 source
、expression
或者 constant
属性手动指定,接下来我们来分析下常用的属性值。
-
target
:属性的目标名称,同一目标属性不能映射多次。如果用于映射枚举常量,则将给出常量成员的名称,在这种情况下,源枚举中的多个值可以映射到目标枚举的相同值。 -
source
:属性的源名称
如果带注释的方法有多个源参数,则属性名称必须使用参数名称限定,例如"addressParam.city"
;
当找不到匹配的属性时,MapStruct
将查找匹配的参数名称;
当用于映射枚举常量时,将给出常量成员的名称;
该属性不能与constant
或expression
一起使用; -
dateFormat
:通过SimpleDateFormat
实现String
到Date
日期之间相互转换。 -
numberFormat
:通过DecimalFormat
实现Number
与String
的数值格式化。 -
constant
:设置指定目标属性的常量字符串
当指定的目标属性的类型为:primitive
或boxed
(例如Long
)时,MapStruct
检查是否可以将该primitive
作为有效的文本分配给primitive
或boxed
类型。
如果可能,MapStruct
将分配为文字;如果不可能,MapStruct
将尝试应用用户定义的映射方法。
另外,MapStruct
将常量作为字符串处理,将通过应用匹配方法、类型转换方法或内置转换来转换该值。此属性不能与source
、defaultValue
、defaultExpression
或expression
一起使用。 -
expression
:是一个表达式,根据该表达式设置指定的目标属性。它的属性不能与source
、defaultValue
、defaultExpression
、constant
一起使用。 -
ignore
: 忽略这个字段
我们用 expression
这个属性来实现一下上边用 uses
实现的案例:
@Mapping(target = "type", expression = "java(new com.ittest.controller.BooleanStrFormat().toStr(carVo.isType()))")
CarDto carVoToDtoWithExpression(CarVo carVo);
生成的实现类
@Override
public CarDto carVoToDtoWithExpression(CarVo carVo) {
if ( carVo == null ) {
return null;
}
CarDto carDto = new CarDto();
carDto.setMake( carVo.getMake() );
carDto.setSeatCount( carVo.getSeatCount() );
carDto.setType( new com.ittest.controller.BooleanStrFormat().toStr(carVo.isType()) );
return carDto;
}
2.3.3 @Mappings
@Mappings
可以配置多个 @Mapping
,例如
@Mappings({
@Mapping(source = "id", target = "carId"),
@Mapping(source = "name", target = "carName"),
@Mapping(source = "color", target = "carColor")
})
2.3.4 @MappingTarget
@MappingTarget
用于更新已有对象
创建 BMWCar.java 类
@NoArgsConstructor
@AllArgsConstructor
@Data
public class BMWCar {
private String make;
private int numberOfSeats;
private CarType type;
private String color;
private String price;
}
创建Car实体类
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Car {
private String make;
private int numberOfSeats;
private CarType type;
}
mapper中创建更新方法,并查看实现类
// 更新方法
void updateBwmCar(Car car, @MappingTarget BMWCar bwmCar);
// 实现类
public void updateBwmCar(Car car, BMWCar bwmCar) {
if (car != null) {
bwmCar.setMake(car.getMake());
bwmCar.setNumberOfSeats(car.getNumberOfSeats());
bwmCar.setType(car.getType());
}
}
2.4 MapStruct进阶
2.4.1 属性和类型不同
场景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.4.2 统一映射不同类型
场景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;
}
}
2.4.3 固定值、忽略某个属性、时间转字符串格式
场景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);
}
2.4.4 为某个属性指定转换方法
场景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;
}
}
2.4.5 多个参数合并为一个对象
场景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);
}
2.4.6 已有目标对象,将源对象属性覆盖到目标对象
场景6:已有目标对象,将源对象属性覆盖到目标对象
覆盖目标对象属性时,一般null值不覆盖,所以需要在类上的@Mapper
注解中添加属性:
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
代表null值不进行赋值。
关键字:@MappingTarget注解、nullValuePropertyMappingStrategy
/**
* 来源对象
*/
@Data
public class Demo {
private Integer id;
private String name;
}
/**
* 目标对象
*/
@Data
public class DemoDto {
private String id;
private String name;
}
/**
* 转换器
*/
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface DemoMapper {
DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class);
将已有的目标对象当作一个参数传进来
DemoDto toDemoDto(Demo demo, @MappingTarget DemoDto dto);
}
2.4.7 源对象两个属性合并为一个属性
场景7:源对象两个属性合并为一个属性
这种情况可以使用@AfterMapping
注解
关键字:@AfterMapping注解、@MappingTarget注解
/**
* 来源对象
*/
@Data
public class Demo {
private Integer id;
private String firstName;
private String lastName;
}
/**
* 目标对象
*/
@Data
public class DemoDto {
private String id;
private String name;
}
/**
* 转换器
*/
@Mapper
public interface DemoMapper {
DemoMapper INSTANCE = Mappers.getMapper(DemoMapper.class);
DemoDto toDemoDto(Demo demo);
在转换完成后执行的方法,一般用到源对象两个属性合并为一个属性的场景
需要将源对象、目标对象(@MappingTarget)都作为参数传进来,
@AfterMapping
static void afterToDemoDto(Demo demo, @MappingTarget DemoDto demoDto) {
String name = demo.getFirstName() + demo.getLastName();
demoDto.setName(name);
}
}
2.4.8 多个对象映射一个对象
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Mall4S {
private String address;
private String mobile;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Car {
private String make;
private int numberOfSeats;
private CarType type;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Benz4SMall {
private String address;
private String mobile;
private String make;
private int numberOfSeats;
}
mapper 创建转换方法并查看生成的实现类
Benz4SMall mallCarToBenzMall(Car car, Mall4S mall4S);
实现类
public Benz4SMall mallCarToBenzMall(Car car, Mall4S mall4S) {
if (car == null && mall4S == null) {
return null;
} else {
Benz4SMall benz4SMall = new Benz4SMall();
if (car != null) {
benz4SMall.setMake(car.getMake());
benz4SMall.setNumberOfSeats(car.getNumberOfSeats());
}
if (mall4S != null) {
benz4SMall.setAddress(mall4S.getAddress());
benz4SMall.setMobile(mall4S.getMobile());
}
return benz4SMall;
}
}
2.5 深拷贝与浅拷贝
深拷贝
和 浅拷贝
最根本的区别在于是否真正获取一个对象的复制 实体
,而不是引用。
假设 B 复制了 A ,修改 A 的时候,看 B 是否发生变化:如果 B 跟着也变了,说明是浅拷贝(修改堆内存中的同一个值);如果 B 没有改变,说明是深拷贝(修改堆内存中的不同的值)
MapStruct
中是 创建新的对象
,也就是 深拷贝
3 各个映射工具耗时对比
附录: