常用Bean拷贝工具一览
前言
对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。
1.深拷贝和浅拷贝浅拷贝的概念
Java对象拷贝分为深拷贝和浅拷贝, 深拷贝和浅拷贝是指在赋值一个对象时,拷贝的深度不同。
在进行深拷贝时,会拷贝所有的属性,并且如果这些属性是对象,也会对这些对象进行深拷贝,直到最底层的基本数据类型为止。这意味着,对于深拷贝后的对象,即使原对象的属性值发生了变化,深拷贝后的对象的属性值也不会受到影响。
2.深拷贝和浅拷贝的区别
对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容称为深拷贝。
- 浅拷贝仅仅复制对象的引用,而不是对象本身,新旧对象共享同一块内存。
- 深拷贝会拷贝所有的属性,并且如果这些属性是对象,也会对这些对象进行深拷贝,直到最底层的基本数据类型为止。新对象和原对象没有任何关联,修改新对象不影响原对象。
3. 赋值和拷贝的区别
区别1:赋值是指赋值者与被赋值者指向的是同一地址,只是标签不同,当a的内容被修改之后,b也发生改变;而拷贝是指把原来的值复制了一份存在另一个地址,当原地址的内容被改变之后,拷贝的内容不变。
区别2:赋值侧重于更新,构造侧重于构造。 等号不等于赋值,他可能是拷贝构造。
4. 工具类
目前常用的属性拷贝工具,包括Apache,Spring,的BeanUtils、Hutool的 BeanUtil 、Cglib的BeanCopier、mapstruct都是浅拷贝
介绍一下常用的工具类:
-
Apache BeanUtils
-
Spring BeanUtils
-
Hutool BeanUtil
-
cglib BeanCopier
-
Mapstruct
spring 和 apache,hutool使用的都是反射,cglib是基于字节码文件的操作.都是在都代码运行期间动态执行的;
Mapstruct不同,它在编译期间就生成了 Bean属性复制的代码,运行期间就无需使用反射或者字节码技术,
所以具有很高的性能。
Apache BeanUtils
BeanUtils是Apache commons组件里面的成员,由Apache提供的一套开源 api,用于简化对javaBean的操作,能够对基本类型自动转换。
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
方法1. org.apache.commons.beanutils.BeanUtils
//dest目标文件,orig原始的、源文件
public static void copyProperties(Object dest, Object orig)
throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}
方法2. org.apache.commons.beanutils.PropertyUtils
//dest目标文件,orig原始的、源文件
public static void copyProperties(final Object dest, final Object orig)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
PropertyUtilsBean.getInstance().copyProperties(dest, orig);
}
注意
- 方法1和的BeanUtils浅拷贝不对属性类型进行处理 不同类型无法复制,必须类型和名称一致
- spring和apache的copy属性的方法源和目的参数的位置正好相反,所以导包和调用的时候都要注意一下
- 方法3 PropertyUtils 支持不同类型同名称复制
Spring BeanUtils
spring框架下自带的工具,如果使用的spring项目时不需要单独引入依赖,单独使用时需要引入坐标:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
示例: A,B为对象 BeanUtils.copyProperties(A, B);
☆☆ A拷贝到B
org.springframework.beans.BeanUtils
//source 源文件,target 目标文件
public static void copyProperties(Object source, Object target)
throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
BeanUtils不对属性类型进行处理 不同类型无法复制,必须类型和名称一致
spring和apache的copy属性的方法源和目的参数的位置正好相反,所以导包和调用的时候都要注意一下
注意
在阿里巴巴的开发手册中,强制避免使用apache BeanUtils进行拷贝,建议使用Spring BeanUtils或下面要介绍的BeanCopier。主要原因还是在于Spring并没有与 apache一样对反射做了过多校验,另外Spring BeanUtils内部使用了缓存,加快转换的速度。此外,由于我们的大多项目已经集成了Spring ,如果没有其他特殊的需求,直接使用它的BeanUtils就能满足我们的基本需求。
Hutool BeanUtil
hutool是个人平常使用比较频繁的一个工具包,对文件、加密解密、转码、正则、线程、XML等JDK方法进行封装,并且也可以进行对象的拷贝。在使用前引入坐标:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
cn.hutool.core.bean.BeanUtil
/**
* 复制Bean对象属性
*
* @param source 源Bean对象
* @param target 目标Bean对象
*/
public static void copyProperties(Object source, Object target) {
copyProperties(source, target, CopyOptions.create());
}
hutool的BeanUtil还提供了很多其他实用的方法:
注意
-
支持不同类型同名称复制
-
使用浅拷贝方式
-
个人在使用中感觉Bean和Map的互相转换还是很常用的,有时在使用Map接收参数时,后期能够很方便的把Map转换为Bean
cglib BeanCopier
cglib(Code Generation Library)是一个强大的、高性能、高质量的代码生成类库,BeanCopier依托于cglib的字节码增强能力,动态生成实现类,完成对象的拷贝。
如果工程内含有spring-core包的依赖,也不需要额外引入依赖,否则需要引入坐标:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
import org.springframework.cglib.beans.BeanCopier;
public static BeanCopier create(Class source, Class target, boolean useConverter) { Generator gen = new Generator(); gen.setSource(source); gen.setTarget(target); gen.setUseConverter(useConverter); return gen.create(); }
1.使用浅拷贝方式,类型不同或者包装类基本类之间无法正常拷贝
Mapstruct
官网 :https://mapstruct.org/documentation/stable/reference/html/
MapStruct
是一个代码生成器,它基于约定优于配置方法极大地简化了 Java bean
类型之间映射的实现。自动生成的映射转换代码只使用简单的方法调用,因此速度快、类型安全而且易于理解阅读,总的来说,有如下三个特点:
- 基于注解
- 在编译期自动生成映射转换代码
- 类型安全、高性能、无依赖性
使用Mapstruct需要需要引入下面的依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
//需要额外写一个接口来实现
@Mapper(componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy =ReportingPolicy.IGNORE)
interface ConvertMapper {
ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);
Bean2 B2CyB1(Bean1 bean1);
// 对象属性类型不同或名称不同 可如下配置
//-------------
// 单个书写
// // 默认值
// @Mapping(source = "resourceId", target = "resourceId", defaultValue = "可以指定默认值")
// // 常量
// @Mapping(source = "resourceType", target = "resourceType",constant = "100")
// // 日期转换
// @Mapping(source = "dateTime1", target = "dateTime", dateFormat = "yyyy-MM-dd")
// 批量写法 与上述二选一
@Mappings({
// Integer 到 String的转换
// @Mapping(source = "id", target = "numId", numberFormat = "$#.00"),
//@Mapping(source = "name", target = "userName"),
//Date 转字符串
@Mapping(source = "dateTime1", target = "dateTime", dateFormat = "yyyy/MM/dd",ignore = true),
// 使用java可执行代码或函数
@Mapping(target = "dateTime2", expression = "java(java.time.LocalDateTime.now().toString())")
})
Bean3 B3ToB1(Bean1 bean1);
}
//-------------------------------------------------------------------------------
//这里的@Mapper注解不是用于mybatis的注解,而是org.mapstruct.Mapper。使用起来也非常简单:
ConvertMapper mapper = Mappers.getMapper(ConvertMapper.class);
Bean2 bean2=mapper.B2CyB1(bean1);
Bean3 bean3=mapper.B3ToB1(bean1);
查看编译后的target目录,编译时将我们定义的ConvertMapper 接口,生成了ConvertMapperImpl实现类,并实现了B2CyB1方法。看一下编译生成的文件:
可以看到方法中为每一个属性生成了set方法,并且对于引用对象,生成了一个新的对象,使用深拷贝的方式,所以修改之前的引用对象,这里的值也不会改变。并且,这种使用set/get的方式比使用反射的速度更快。
获取转换器(Mapper)的几种方式
获取转换器的方式根据 @Mapper
注解的 componentModel
属性不同而不同,支持以下四种不同的取值:
- default 默认方式,默认方式,使用工厂方式(
Mappers.getMapper(Class)
)来获取 - cdi 此时生成的映射器是一个应用程序范围的
CDI bean
,使用@Inject
注解来获取 - spring
Spring
的方式,可以通过@Autowired
注解来获取,在Spring
框架中推荐使用此方式 - jsr330 生成的映射器用
@javax.inject.Named
和@Singleton
注解,通过@Inject
来获取
① 通过工厂方式获取
上文的示例中都是通过工厂方式获取的,也就是使用 MapStruct
提供的 Mappers.getMapper(Class<T> clazz)
方法来获取指定类型的 Mapper
。然后在调用的时候就不需要反复创建对象了,方法的最终实现是通过我们定义接口的类加载器加载 MapStruct
生成的实现类(类名称规则为:接口名称 + Impl
),然后调用该类的无参构造器创建对象。核心源码如下所示:
② 使用依赖注入方式获取
对于依赖注入(dependency injection
),使用 Spring
框架开发的朋友们应该很熟悉了,工作中经常使用。MapStruct
也支持依赖注入的使用方式,并且官方也推荐使用依赖注入的方式获取。使用 Spring
依赖注入的方式只需要指定 @Mapper
注解的 componentModel = "spring"
即可,示例代码如下:
@Mapper(componentModel = "spring")
public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
@Mapping(source = "sourceName", target = "targetName")
Target toTarget(Source source);
}
我们可以使用 @Autowired
获取的原因是 SourceMapper
接口的实现类已经被注册为容器中一个 Bean
了,通过如下生成的接口实现类的代码也可以看到,在类上自动加上了 @Component
注解。
最后还有两个注意事项:① 当两个转换对象的属性不一致时(比如 DoctorDTO
中不存在 Doctor
对象中的某个字段),编译时会出现警告提示。可以在@Mapping
注解中配置 ignore = true
,或者当不一致字段比较多时,可以直接设置 @Mapper
注解的 unmappedTargetPolicy
属性或unmappedSourcePolicy
属性设置为 ReportingPolicy.IGNORE
。② 如果你项目中也使用了 Lombok,需要注意一下 Lombok
的版本至少是 1.18.10
或者以上才行,否则会出现编译失败的情况。
性能对比
更多数据请查看网址----- 无惧性能烦恼-12款Bean拷贝工具压测大比拼
拷贝选型
经过以上分析,随着数据量的增大,耗时整体呈上升趋势
- 整体情况下,Apache BeanUtils的性能最差,日常使用过程中不建议使用
- 在数据规模不大的情况下,spring、cglib、mapstruct差异不大,spring框架下建议使用spring的beanUtils,不需要额外引入依赖包
- 数据量大的情况下,建议使用cglib和mapstruct
- 涉及大量数据转换,属性映射,格式转换的,建议使用mapstruct
工具类
1.基于动态代理 BeanCopier 工具类
package com.tuling;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.cglib.beans.BeanCopier;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* menu:BeanCopier cglib beanCooy类 同名不同类型copy无效
*
* @author: superMa
* @Version 1.0
* @date:2023/9/14
*/
public class BeanUtilsCopy {
/**
* BeanCopier的缓存,避免频繁创建,高效复用
*/
private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIER_MAP_CACHE = new ConcurrentHashMap<String, BeanCopier>();
/**
* BeanCopier的copyBean,高性能推荐使用,增加缓存
*
* @param source 源文件的
* @param target 目标文件
*/
public static void copyBean(Object source, Object target) {
String key = genKey(source.getClass(), target.getClass());
BeanCopier beanCopier;
if (BEAN_COPIER_MAP_CACHE.containsKey(key)) {
beanCopier = BEAN_COPIER_MAP_CACHE.get(key);
} else {
beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
BEAN_COPIER_MAP_CACHE.put(key, beanCopier);
}
beanCopier.copy(source, target, null);
}
/**
* 不同类型对象数据copylist
*
* @param sourceList
* @param targetClass
* @param <T>
* @return
*/
public static <T> List<T> copyListProperties(List<?> sourceList, Class<T> targetClass) throws Exception {
if (CollectionUtils.isNotEmpty(sourceList)) {
List<T> list = new ArrayList<T>(sourceList.size());
for (Object source : sourceList) {
T target = copyProperties(source, targetClass);
list.add(target);
}
return list;
}
return new ArrayList<T>();
}
/**
* 返回不同类型对象数据copy,使用此方法需注意不能覆盖默认的无参构造方法
*
* @param source
* @param targetClass
* @param <T>
* @return
*/
public static <T> T copyProperties(Object source, Class<T> targetClass) throws Exception {
T target = targetClass.newInstance();
copyBean(source, target);
return target;
}
/**
* @param srcClazz 源class
* @param tgtClazz 目标class
* @return string
*/
private static String genKey(Class<?> srcClazz, Class<?> tgtClazz) {
return srcClazz.getName() + tgtClazz.getName();
}
}
2.基于hutool_cglib
/*
*单对象基于class创建拷贝
*单对象基于对象创建拷贝
*列表对象基于class创建拷贝
*对象转Map
*map基于class拷贝到bean
*map基于对象拷贝到bean
*map拷贝到map
/**
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BeanCopyUtils {
/**
* 单对象基于class创建拷贝
*
* @param source 数据来源实体
* @param desc 描述对象 转换后的对象
* @return desc
*/
public static <T, V> V copy(T source, Class<V> desc) {
if (ObjectUtil.isNull(source)) {
return null;
}
if (ObjectUtil.isNull(desc)) {
return null;
}
final V target = ReflectUtil.newInstanceIfPossible(desc);
return copy(source, target);
}
/**
* 单对象基于对象创建拷贝
*
* @param source 数据来源实体
* @param desc 转换后的对象
* @return desc
*/
public static <T, V> V copy(T source, V desc) {
if (ObjectUtil.isNull(source)) {
return null;
}
if (ObjectUtil.isNull(desc)) {
return null;
}
BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(source.getClass(), desc.getClass(), null);
beanCopier.copy(source, desc, null);
return desc;
}
/**
* 列表对象基于class创建拷贝
*
* @param sourceList 数据来源实体列表
* @param desc 描述对象 转换后的对象
* @return desc
*/
public static <T, V> List<V> copyList(List<T> sourceList, Class<V> desc) {
if (ObjectUtil.isNull(sourceList)) {
return null;
}
if (CollUtil.isEmpty(sourceList)) {
return CollUtil.newArrayList();
}
return StreamUtils.toList(sourceList, source -> {
V target = ReflectUtil.newInstanceIfPossible(desc);
copy(source, target);
return target;
});
}
/**
* bean拷贝到map
*
* @param bean 数据来源实体
* @return map对象
*/
public static <T> Map<String, Object> copyToMap(T bean) {
if (ObjectUtil.isNull(bean)) {
return null;
}
return BeanMap.create(bean);
}
/**
* map拷贝到bean
*
* @param map 数据来源
* @param beanClass bean类
* @return bean对象
*/
public static <T> T mapToBean(Map<String, Object> map, Class<T> beanClass) {
if (MapUtil.isEmpty(map)) {
return null;
}
if (ObjectUtil.isNull(beanClass)) {
return null;
}
T bean = ReflectUtil.newInstanceIfPossible(beanClass);
return mapToBean(map, bean);
}
/**
* map拷贝到bean
*
* @param map 数据来源
* @param bean bean对象
* @return bean对象
*/
public static <T> T mapToBean(Map<String, Object> map, T bean) {
if (MapUtil.isEmpty(map)) {
return null;
}
if (ObjectUtil.isNull(bean)) {
return null;
}
BeanMap.create(bean).putAll(map);
return bean;
}
/**
* map拷贝到map
*
* @param map 数据来源
* @param clazz 返回的对象类型
* @return map对象
*/
public static <T, V> Map<String, V> mapToMap(Map<String, T> map, Class<V> clazz) {
if (MapUtil.isEmpty(map)) {
return null;
}
if (ObjectUtil.isNull(clazz)) {
return null;
}
Map<String, V> copyMap = new LinkedHashMap<>(map.size());
map.forEach((k, v) -> copyMap.put(k, copy(v, clazz)));
return copyMap;
}
/**
* BeanCopier属性缓存<br>
* 缓存用于防止多次反射造成的性能问题
*/
public enum BeanCopierCache {
/**
* BeanCopier属性缓存单例
*/
INSTANCE;
private final SimpleCache<String, BeanCopier> cache = new SimpleCache<>();
/**
* 获得类与转换器生成的key在{@link BeanCopier}的Map中对应的元素
*
* @param srcClass 源Bean的类
* @param targetClass 目标Bean的类
* @param converter 转换器
* @return Map中对应的BeanCopier
*/
public BeanCopier get(Class<?> srcClass, Class<?> targetClass, Converter converter) {
final String key = genKey(srcClass, targetClass, converter);
return cache.get(key, () -> BeanCopier.create(srcClass, targetClass, converter != null));
}
/**
* 获得类与转换器生成的key
*
* @param srcClass 源Bean的类
* @param targetClass 目标Bean的类
* @param converter 转换器
* @return 属性名和Map映射的key
*/
private String genKey(Class<?> srcClass, Class<?> targetClass, Converter converter) {
final StringBuilder key = StrUtil.builder()
.append(srcClass.getName()).append('#').append(targetClass.getName());
if (null != converter) {
key.append('#').append(converter.getClass().getName());
}
return key.toString();
}
}
}
3.基于 spring BeanUtils 的 工具类
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
/**
* 对象拷贝工具
* @author DELL
* @version 1.0
*/
public class BeanObjectCopyUtils{
private static Logger LOGGER = LoggerFactory.getLogger( BeanObjectCopyUtils.class );
/**
* 拷贝对象
* @param desObj 目标对象
* @param origObj 源对象
* @return desObj 目标对象
*/
public static <T,E> T copyObject(T desObj, E origObj){
if(origObj!=null && desObj!=null){
try {
BeanUtils.copyProperties(origObj, desObj);
}
catch (Exception e) {
LOGGER.error("object copy error",e);
throw new RuntimeException("object copy error",e);
}
}
return desObj;
}
/**
* 拷贝List对象到另一个list对象
* @param desClass 源List对象
* @param sourceList 目标List对象
* @return List
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T> List<T> copyListObjToListObj(Class<T> desClass, List sourceList){
List desList=new ArrayList();
if(sourceList!=null){
for(int i=0; i<sourceList.size(); i++){
try {
Object sourceObj = sourceList.get(i);
Object desObj = desClass.newInstance();
BeanUtils.copyProperties(sourceObj, desObj);
desList.add(desObj);
}
catch (Exception e) {
LOGGER.error("list copy error",e);
throw new RuntimeException("list copy error",e);
}
}
}
return desList;
}
4.基于 huttol 的 工具类
package com.tuling;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import java.util.ArrayList;
import java.util.List;
/**
* menu: java 实现 实体类之间的转换工具类
*
* @author: superMa
* @Version 1.0
* @date:2023/9/8
*/
public class BeanUtils {
/**
* 实体类集合转化
*
* @param objs
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> copy(List<?> objs, Class<T> clazz) {
List<T> list = new ArrayList<>();
if (objs.isEmpty()) {
return list;
}
for (Object source : objs) {
//把源对象类型强制转换为目标对象
T target = JSON.parseObject(JSON.toJSONString(source), clazz);
//把源对象属性赋值给目标对象(忽略大小写)
BeanUtil.copyProperties(source, target,true);
list.add(target);
}
return list;
}
/**
* 实体类转化
*
* @param obj
* @param clazz
* @return
*/
public static <T> T copyEntity(Object obj, Class<T> clazz) {
if (obj == null) {
return null;
}
//把源对象类型强制转换为目标对象
T target = JSON.parseObject(JSON.toJSONString(obj), clazz);
//把源对象属性赋值给目标对象
BeanUtil.copyProperties(obj, target);
//BeanUtil.copyProperties(obj, target,true);
return target;
}
}