Spring深入分析讲解BeanUtils的实现

目录
  • 背景
    • DO
    • BO
    • DTO
    • VO
  • 数据实体转换
    • 使用方式
    • 原理&源码分析
  • 属性赋值类型擦除
  • 总结

背景

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);

原理&源码分析

直接看方法签名:

/**
 * Copy the property values of the given source bean into the target bean.
 * <p>Note: The source and target classes do not have to match or even be derived
 * from each other, as long as the properties match. Any bean properties that the
 * source bean exposes but the target bean does not will silently be ignored.
 * <p>This is just a convenience method. For more complex transfer needs,
 * consider using a full BeanWrapper.
 * @param source the source bean
 * @param target the target bean
 * @throws BeansException if the copying failed
 * @see BeanWrapper
 */
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)实现:

/**
 * Retrieve the JavaBeans {@code PropertyDescriptor}s of a given class.
 * @param clazz the Class to retrieve the PropertyDescriptors for
 * @return an array of {@code PropertyDescriptors} for the given class
 * @throws BeansException if PropertyDescriptor look fails
 */
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,看上去没什么问题,并且代码也是编译通过的,但是运行后发现:


20220621111461.jpg" />

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

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

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


20220621111462{C}{C}.jpg" />

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

总结

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

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

(0)

相关推荐

  • JSP 开发之Spring BeanUtils组件使用

    JSP 开发之Spring BeanUtils组件使用 用于演示的javabean import java.util.Date; public class People { private String name; private int age; private Date birth; public People(String name, int age, Date birth) { super(); this.name = name; this.age = age; this.birth =

  • 浅析Java中Apache BeanUtils和Spring BeanUtils的用法

    # 前言 在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型. # 对象拷贝 在具体介绍两种 BeanUtils 之前,先来补充一些基础知识.它们两种工具本质上就是对象拷贝工具,而对象拷贝又分为深拷贝和浅拷贝,下面进行详细解释. # 什么是浅拷贝和

  • 基于Spring BeanUtils的copyProperties方法使用及注意事项

    如下所示: package com.demo; import lombok.Data; import org.springframework.beans.BeanUtils; import java.util.Arrays; import java.util.List; /** * @author xiaobu * @version JDK1.8.0_171 * @date on 2019/10/8 10:04 * @description */ public class BeanUtilsTe

  • Spring BeanUtils忽略空值拷贝的方法示例代码

    目录 简介 获取null属性名(工具类) 示例 工具类 Entity Controller 测试 其他文件 其他网址 简介 说明 本文用示例介绍Spring(SpringBoot)如何使用BeanUtils拷贝对象属性(忽略空值). BeanUtils类所在的包 有两个包都提供了BeanUtils类: Spring的(推荐):org.springframework.beans.BeanUtilsApache的:org.apache.commons.beanutils.BeanUtils 忽略nu

  • Spring深入分析讲解BeanUtils的实现

    目录 背景 DO BO DTO VO 数据实体转换 使用方式 原理&源码分析 属性赋值类型擦除 总结 背景 DO DO是Data Object的简写,叫做数据实体,既然是数据实体,那么也就是和存储层打交道的实体类,应用从存储层拿到的数据是以行为单位的数据,不具备java特性,那么如果要和java属性结合起来或者说在业务中流转,那么一定要转换成java对象(反过来java要和持久层打交道也要把java对象转换成行数据),那么就需要DO作为行数据的一个载体,把行的每一个列属性映射到java对象的每一

  • Spring Boot深入分析讲解日期时间处理

    目录 GET请求及POST表单日期时间字符串格式转换 使用自定义参数转换器(Converter) 使用Spring注解 使用ControllerAdvice配合initBinder JSON入参及返回值全局处理 修改 application.yml 文件 利用Jackson的JSON序列化和反序列化 总结 GET请求及POST表单日期时间字符串格式转换 这种情况要和时间作为Json字符串时区别对待,因为前端json转后端pojo底层使用的是Json序列化Jackson工具(HttpMessgeC

  • Spring超详细讲解BeanUtils改造

    目录 1.基本原理 2.使用 3.性能 4.提醒 1.基本原理 原理:https://www.jb51.net/article/252384.htm 浅拷贝:https://www.jb51.net/article/221283.htm BeanUtils.copyProperties();确实为我们做了很多事情,虽然不能完美完成深拷贝,但是对于 po.vo.dto 的拷贝已经足够用了.但是其还是有一些不够完美的地方. 不足几点如下: 不能拷贝 list,而拷贝 list 的情况又大量存在,因此

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

    目录 何时需要削峰 通过消息队列的削峰方法有两种 消费延时控流 总结 何时需要削峰 当上游调用下游服务速率高于下游服务接口QPS时,那么如果不对调用速率进行控制,那么会发生很多失败请求 通过消息队列的削峰方法有两种 控制消费者消费速率和生产者投放延时消息,本质都是控制消费速度 通过消费者参数控制消费速度 先分析那些参数对控制消费速度有作用 1.PullInterval: 设置消费端,拉取mq消息的间隔时间. 注意:该时间算起时间是rocketMq消费者从broker消息后算起.经过PullInt

  • C++深入分析讲解智能指针

    目录 1.简介 2.unique_ptr指针(独占指针) 3.shared_ptr指针(共享所有权) 4.weak_ptr(辅助作用) 5.自实现初级版智能指针 6.总结 1.简介 程序运行时存在静态空间.栈和堆区,用堆来存储动态分配空间的对象即那些在程序运行时分配空间的对象,若该对象不再使用,我们必须显式的销毁它们,避免内存泄漏. 智能指针是一个可以像指针一样工作的对象,有unique_ptr(独占指针),shared_ptr与weak_ptr等智能指针,定义在<memory>头文件中,可以

  • Java开发深入分析讲解二叉树的递归和非递归遍历方法

    目录 前言 1.递归遍历 2.非迭代遍历 3.二叉树的统一迭代法 前言 二叉树的遍历方法分为前序遍历,中序遍历,后续遍历,层序遍历. 1.递归遍历 对于递归,就不得不说递归三要素:以前序遍历为例 递归入参参数和返回值 因为要打印出前序遍历节点的数值,所以参数里需要传入List在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: public void preorder(TreeNode root, List<Integer> resu

  • Spring深入讲解实现AOP的三种方式

    [重点] 使用AOP织入 需要导入一个依赖包 <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.9.1</version> </dependency> </dependencies> 方式一:使用原生Spring AP

  • C++深入分析讲解函数与重载知识点

    目录 函数的默认(缺省)参数 1.默认参数的定义 2.默认参数的注意点 占位参数 1.占位参数 函数内部无法使用 2.占位参数 可以设置成缺省参数 函数重载 函数的默认(缺省)参数 1.默认参数的定义 c++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有传递该参数值,编译器会自动用默认值代替. //函数的默认参数 指定x的默认值为10 y为20 int my_add(int x=10,int y=20) { return x+y; } void test

  • C++深入分析讲解类的知识点

    目录 知识点引入 类的初识 1.封装 2.权限 3.类的定义(定义类型) 4.类的成员函数与类中声明及类外定义 Person类的设计 设计立方体类 点Point和圆Circle的关系 知识点引入 C语言中 数据 和 方法 是独立: //c语言的思想:数据 方法 分开 //人 typedef struct { char name[32]; int age; }Person; //动物 typedef struct { char name[32]; int age; int type; }Dog;

  • Java深入分析讲解反射机制

    目录 反射的概述 获取Class对象的三种方式 通过反射机制获取类的属性 通过反射机制访问Java对象的属性 反射机制与属性配置文件的配合使用 资源绑定器 配合使用样例 通过反射机制获取类中方法 通过反射机制调用Java对象的方法 通过反射机制获取类中的构造方法 通过反射机制创建对象(调用构造方法) 通过反射机制获取一个类的父类和父接口 反射的概述 JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的

随机推荐