我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Spring深入分析讲解BeanUtils的实现

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Spring深入分析讲解BeanUtils的实现

背景

DO

DO是Data Object的简写,叫做数据实体,既然是数据实体,那么也就是和存储层打交道的实体类,应用从存储层拿到的数据是以行为单位的数据,不具备java特性,那么如果要和java属性结合起来或者说在业务中流转,那么一定要转换成java对象(反过来java要和持久层打交道也要把java对象转换成行数据),那么就需要DO作为行数据的一个载体,把行的每一个列属性映射到java对象的每一个字段。

BO

BO是Business Object的简写,是业务对象,区别于DO的纯数据描述,BO用于在应用各个模块之间流转,具备一定的业务含义,一般情况像BO是应用自己定义的业务实体,对持久层和二方或三方接口接口响应结果的封装,这里插一句,为什么有了DO和外部依赖的实体类,为什么还需要BO?对于领域内持久层交互来说,BO层有时候可以省略(大部分场景字段属性基本一致),而对于和领域外二方或三方服务交互来说,增加BO实体的目的主要是降低外部实体对领域内其它层的侵入,以及降低外部实体签名变更对领域内其它层的影响,举个例子将调用订单服务的响应结果在代理层封装成BO供上层使用,那么如果订单实体内部属性签名发生变更或者升级,那么只需要改BO即可,只影响应用的代理层,中间业务流转层完全不受影响。

DTO

DTO是Data Transfer Object的缩写,叫做数据传输对象,主要用于跨服务之间的数据传输,如公司内部做了微服务拆封,那么微服务之间的数据交互就是以DTO作为数据结果响应载体,另外DTO的存在也是对外部依赖屏蔽了领域内底层数据的结构,假如直接返回DO给依赖方,那么我们的表结构也就一览无余了,在公司内部还好,对于也利益关系的团队之间有服务交互采取这种方式,那么就可能产生安全问题和不必要的纠纷。

VO

值对象(Value Object),其存在的意思主要是数据展示,其直接包含具有业务含义的数据,和前端打交道,由业务层将DO或者BO转换为VO供前端使用。

前边介绍了几种常用的数据实体,那么一个关键的问题就出现了,既然应用分了那么多层,每个层使用的数据实体可能不一样,也必然会存在实体之间的转换问题,也是本篇文章需要重点讲述的问题。

数据实体转换

所谓数据实体转换,就是将源数据实体存储的数据转换到目标实体的实例对象存储,比如把BO转换成VO数据响应给前端,那么就需要将源数据实体的属性值逐个映射到目标数据实体并赋值,也就是VO.setXxx(BO.getXxx()),当然我们可以选择最原始最笨重的方式,逐个遍历源数据实体的属性然后赋值给新数据实体,也可以利用java的反射来实现。

就目前比较可行的以及可行的方案中,比较常用的有逐个set,和利用工具类赋值。

在数据实体字段比较少或者字段类型比较复杂的情况下,可以考虑使用逐个字段赋值的方式,但是如果字段相对较多,那么就会出现一个实体类转换就写了几十行甚至上百行的代码,这是完全不能接受的,那么我们就需要自己实现反射或者使用线程的工具类来实现了,当然工具类有很多,比如apache的common包有BeanUtils实现,spring-beans有BeanUtils实现以及Guava也有相关实现,其他的暂且不论,这里我们就从源码维度分析一下使用spring-beans的BeanUtils做数据实体转换的实现原理和可能会存在的坑。

使用方式

在数据实体转换时,用的最多的就是BeanUtils#copyProperties方法,基本用法就是:

//DO是源数据对象,DTO是目标对象,把源类的数据拷贝到目标对象
BeanUtils.copyProperties(DO,DTO);

原理&源码分析

直接看方法签名:


public static void copyProperties(Object source, Object target) throws BeansException {
  copyProperties(source, target, null, (String[]) null);
}

方法注释的大致意思是,将给定的源bean的属性值复制到目标bean中,源类和目标类不必匹配,甚至不必派生

彼此,只要属性匹配即可,源bean中有但目标bean中没有的属性将被忽略。

上述方法直接调用了重载方法,多了两个入参:


private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
    @Nullable String... ignoreProperties) throws BeansException {
  Assert.notNull(source, "Source must not be null");
  Assert.notNull(target, "Target must not be null");
  //目标Class
  Class<?> actualEditable = target.getClass();
  if (editable != null) {
    if (!editable.isInstance(target)) {
      throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
          "] not assignable to Editable class [" + editable.getName() + "]");
    }
    actualEditable = editable;
  }
    //1.获取目标Class的属性描述
  PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
  List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
  //2.遍历源Class的属性
  for (PropertyDescriptor targetPd : targetPds) {
        //源Class属性的写方法,setXXX
    Method writeMethod = targetPd.getWriteMethod();
        //3.如果存在写方法,并且该属性不忽略,继续往下走,否则跳过继续遍历
    if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
            //4.获取源Class的与目标属性同名的属性描述
      PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
      //5.如果源属性描述不存在直接跳过,否则继续往下走
            if (sourcePd != null) {
                //获取源属性描述的读方法
        Method readMethod = sourcePd.getReadMethod();
                //6.如果源属性描述的读防范存在且返回数据类型和目标属性的写方法入参类型相同或者派生
                //继续往下走,否则直接跳过继续下次遍历
        if (readMethod != null &&
            ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
          try {
                        //如果源属性读方法修饰符不是public,那么修改为可访问
            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
              readMethod.setAccessible(true);
            }
                        //7.读取源属性的值
            Object value = readMethod.invoke(source);
                        //如果目标属性的写方法修饰符不是public,则修改为可访问
            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
              writeMethod.setAccessible(true);
            }
                        //8.通过反射将源属性值赋值给目标属性
            writeMethod.invoke(target, value);
          }
          catch (Throwable ex) {
            throw new FatalBeanException(
                "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
          }
        }
      }
    }
  }
}

方法的具体实现中增加了详细的注释,基本上能够看出来其实现原理是通过反射,但是里边有两个地方我们需要关注一下:

//获取目标bean属性描述
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
//获取源bean指定名称的属性描述
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());

其实两个调用底层实现一样,那么我们就对其中一个做一下分析即可,继续跟进看getPropertyDescriptors(actualEditable)实现:


public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
  CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
  return cr.getPropertyDescriptors();
}

该方法是获取指定Class的属性描述,调用了CachedIntrospectionResults的forClass方法,从名称中可以知道改方法返回一个缓存的自省结果,然后返回结果中的属性描述,继续看实现:

@SuppressWarnings("unchecked")
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
  //1.从强缓存获取beanClass的内省结果,如果有数据直接返回
    CachedIntrospectionResults results = strongClassCache.get(beanClass);
  if (results != null) {
    return results;
  }
    //2.如果强缓存中不存在beanClass的内省结果,则从软缓存中获取beanClass的内省结果,如果存在直接返回
  results = softClassCache.get(beanClass);
  if (results != null) {
    return results;
  }
  //3.如果强缓存和软缓存都不存在beanClass的自省结果,则创建一个
  results = new CachedIntrospectionResults(beanClass);
  ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;
  //4.如果beanClass是缓存安全的,或者beanClass的类加载器是配置可接受的,缓存引用指向强缓存
  if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
      isClassLoaderAccepted(beanClass.getClassLoader())) {
    classCacheToUse = strongClassCache;
  }
  else {
        //5.如果不是缓存安全,则将缓存引用指向软缓存
    if (logger.isDebugEnabled()) {
      logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
    }
    classCacheToUse = softClassCache;
  }
  //6.将beanClass内省结果放入缓存
  CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
  //7.返回内省结果
    return (existing != null ? existing : results);
}

该方法中有几个比较重要的概念,强引用、软引用、缓存、缓存安全、类加载和内省等,简单介绍一下概念:

  • 强引用: 常见的用new方式创建的引用,只要有引用存在,就算出现OOM也不会回收这部分内存空间
  • 软引用: 引用强度低于强引用,在出现OOM之前垃圾回收器会尝试回收这部分存储空间,如果仍不够用则报OOM
  • 缓存安全:检查beanClass是否是CachedIntrospectionResults的类加载器或者其父类加载器加载的
  • 类加载:双亲委派
  • 内省:是java提供的一种获取对bean的属性、事件描述的方式

方法的作用是先尝试从强引用缓存中获取beanClass的自省结果,如果存在则直接返回,如果不存在则尝试从软引用缓存中获取自省结果,如果存在直接返回,否则利用java自省特性生成beanClass属性描述,如果缓存安全或者beanClass的类加载器是可接受的,将结果放入强引用缓存,否则放入软引用缓存,最后返回结果。

属性赋值类型擦除

我们在正常使用BeanUtils的copyProperties是没有问题的,但是在有些场景下会出现问题,我们看下面的代码:

public static void main(String[] args) {

    Demo1 demo1 = new Demo1(Arrays.asList("1","2","3"));

    Demo2 demo2 = new Demo2();
    BeanUtils.copyProperties(demo1,demo2);
    for (Integer integer : demo2.getList()) {
        System.out.println(integer);
    }
    for (String s : demo1.getList()) {
        demo2.addList(Integer.valueOf(s));
    }
}
@Data
static class Demo1 {
    private List<String> list;
    public Demo1(List<String> list) {
        this.list = list;
    }
}
@Data
static class Demo2 {
    private List<Integer> list;
    public void addList(Integer target) {
        if(null == list) {
            list = new ArrayList<>();
        }
        list.add(target);
    }
}

很简单,就是利用BeanUtils将demo1的属性值复制到demo2,看上去没什么问题,并且代码也是编译通过的,但是运行后发现:

类型转换失败,为什么?这里提一下泛型擦除的概念,说白了就是所有的泛型类型(除extends和super)编译后都换变成Object类型,也就是说上边的例子中代码编译后两个类的list属性的类型都会变成List<Object>,主要是兼容1.5之前的无泛型类型,那么在使用BeanUtils工具类进行复制的时候发现连个beanClass的类型名称和类型都是匹配的,直接将原来的值赋值给demo2的list,但是程序运行的时候由于泛型定义,会尝试自动将demo2中list中的元素当成Integer类型处理,所以就出现了类型转换异常。

把上面的代码稍微做下调整:

for (Object obj : demo2.getList()) {
    System.out.println(obj);
}

运行结果正常打印,因为demo2的list实际存储的是String,这里把String当成Object处理完全没有问题。

总结

通过本篇的描述我们对常见的数据实体转换方式的使用和原来有了大致的了解,虽然看起来实现并不复杂,但是整个流程下来里边涉及了很多java体系典型的知识,有反射、引用类型、类加载、内省、缓存安全和缓存等众多内容,从一个简单的对象属性拷贝就能看出spring源码编写人员对于java深刻的理解和深厚的功底,当然我们更直观的看到的是spring架构设计的优秀和源码编写的优雅,希望通过本篇文章能够加深对spring框架对象赋值工具类使用方式和实现原理的理解,以及如何避免由于使用不当容易踩到的坑。

到此这篇关于Spring深入分析讲解BeanUtils的实现的文章就介绍到这了,更多相关Spring BeanUtils内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Spring深入分析讲解BeanUtils的实现

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Spring@Lookup深入分析实现原理

这篇文章主要介绍了Spring@Lookup实现原理,我们知道在spring容器中单独的一个抽象类是不能成为一个bean的,那么有没有办法呢?这个时候我们可以使用Lookup注解
2023-01-03

Golangsync.Map原理深入分析讲解

go中map数据结构不是线程安全的,即多个goroutine同时操作一个map,则会报错,因此go1.9之后诞生了sync.Map,sync.Map思路来自java的ConcurrentHashMap
2022-12-17

AndroidView的事件分发机制深入分析讲解

事件分发从手指触摸屏幕开始,即产生了触摸信息,被底层系统捕获后会传递给Android的输入系统服务IMS,通过Binder把消息发送到activity,activity会通过phoneWindow、DecorView最终发送给ViewGroup。这里就直接分析ViewGroup的事件分发
2023-01-29

ReactHooks核心原理深入分析讲解

这篇文章主要介绍了reacthooks实现原理,文中给大家介绍了useStatedispatch函数如何与其使用的FunctionComponent进行绑定,节后实例代码给大家介绍的非常详细,需要的朋友可以参考下
2022-12-17

Android权限机制深入分析讲解

Android的权限管理遵循的是“最小特权原则”,即所有的Android应用程序都被赋予了最小权限。一个Android应用程序如果没有声明任何权限,就没有任何特权
2022-12-08

Python魔术方法深入分析讲解

所谓魔法函数(MagicMethods),是Python的⼀种⾼级语法,允许你在类中⾃定义函数(函数名格式⼀般为__xx__),并绑定到类的特殊⽅法中。⽐如在类A中⾃定义__str__()函数,则在调⽤str(A())时,会⾃动调⽤__str__()函数,并返回相应的结果
2023-02-08

Vuecomputed实现原理深入讲解

computed又被称作计算属性,用于动态的根据某个值或某些值的变化,来产生对应的变化,computed具有缓存性,当无关值变化时,不会引起computed声明值的变化。产生一个新的变量并挂载到vue实例上去
2022-11-13

RocketMq深入分析讲解两种削峰方式

当上游调用下游服务速率高于下游服务接口QPS时,那么如果不对调用速率进行控制,那么会发生很多失败请求,通过消息队列的削峰方法有两种,这篇文章主要介绍了RocketMq深入分析讲解两种削峰方式
2023-01-28

Golang汇编之控制流深入分析讲解

这篇文章主要介绍了Golang汇编之控制流,程序执行的流程主要有顺序、分支和循环几种执行流程,本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码,感兴趣的同学可以参考下文
2023-05-20

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录