Java对象复制之Cglib和mapstruct讲解

2024-06-22 18:08

本文主要是介绍Java对象复制之Cglib和mapstruct讲解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1 Cglib复制

2 mapstruct

2.1 引入依赖

2.2 简单示例

2.2.1 需要转换的对象 

2.2.2 创建转换器

2.2.3 原理讲解

2.3 注解说明

2.3.1 @Mapper

2.3.2 @Mapping

2.3.3 @Mappings

2.3.4 @MappingTarget

2.4 MapStruct进阶

2.4.1 属性和类型不同

2.4.2 统一映射不同类型 

2.4.3 固定值、忽略某个属性、时间转字符串格式

2.4.4 为某个属性指定转换方法

2.4.5 多个参数合并为一个对象

2.4.6 已有目标对象,将源对象属性覆盖到目标对象

2.4.7 源对象两个属性合并为一个属性

2.4.8 多个对象映射一个对象

2.5 深拷贝与浅拷贝

3 各个映射工具耗时对比


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类型之间映射的实现。特点如下:

  1. 基于注解

  2. 在编译期自动生成映射转换代码

  3. 类型安全、高性能、无依赖性、易于理解阅读

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>

或者这样配置也可以

<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><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${org.mapstruct.version}</version></dependency></dependencies>

注意:如果是jdk1.8的话,并且用Lombok的话,Lombok的坐标必须在mapstruct上面 

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 {@Overridepublic 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 属性或枚举常量的映射,默认是将具有相同名称的属性进行映射,当然也可以用 sourceexpression 或者 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 将常量作为字符串处理,将通过应用匹配方法、类型转换方法或内置转换来转换该值。此属性不能与 sourcedefaultValuedefaultExpression 或 expression 一起使用。
  • expression:是一个表达式,根据该表达式设置指定的目标属性。它的属性不能与 source、 defaultValuedefaultExpressionconstant 一起使用。
  • 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)都作为参数传进来,@AfterMappingstatic 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 各个映射工具耗时对比

附录:

这篇关于Java对象复制之Cglib和mapstruct讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085027

相关文章

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

java图像识别工具类(ImageRecognitionUtils)使用实例详解

《java图像识别工具类(ImageRecognitionUtils)使用实例详解》:本文主要介绍如何在Java中使用OpenCV进行图像识别,包括图像加载、预处理、分类、人脸检测和特征提取等步骤... 目录前言1. 图像识别的背景与作用2. 设计目标3. 项目依赖4. 设计与实现 ImageRecogni

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu