Spring BeanUtils

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Spring BeanUtils

叔牙   2022-06-28 我要评论

背景

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,看上去没什么问题,并且代码也是编译通过的,但是运行后发现:

类型转换失败,为什么?这里提一下泛型擦除的概念,说白了就是所有的泛型类型(除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框架对象赋值工具类使用方式和实现原理的理解,以及如何避免由于使用不当容易踩到的坑。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们