springboot beanutils,spring beanutil

  springboot beanutils,spring beanutil

  00-1010后台DOBODTOVO数据实体转换用法原理源代码分析属性赋值类型擦除摘要

  

目录

 

  

背景

DO是数据对象的缩写,称为数据实体。既然是数据实体,也是处理存储层的实体类。应用程序从存储层获得的数据是以行为为单位的数据,不具备java特性。如果要和java属性结合或者在业务中流通,那么就必须转换成java对象(反过来java还要处理持久层,把java对象转换成行数据)。那么就需要DO作为行数据的载体,将行的每个列属性映射到java对象的每个字段。

 

  00-1010BO是业务对象的缩写,是业务对象。它不同于DO的纯数据描述。BO用于在应用程序的各个模块之间循环,它具有一定的业务含义。一般来说,BO是应用程序自己定义的业务实体,它封装了持久层和两方或三方接口的响应结果。在这里,为什么存在和外部依赖的实体类,为什么需要BO?对于域内持久层的交互,有时可以省略BO层(大部分场景字段的属性基本相同),而对于域外两三个服务的交互,添加BO实体的目的主要是减少外部实体对域内其他层的入侵,减少外部实体的签名变化对域内其他层的影响。比如调用订单服务的响应结果在代理层封装成BO供上层使用,那么如果订单实体内部属性的签名发生变化或升级,只需要改变BO即可。

  

DO

DTO是数据传输对象的缩写,称为数据传输对象。它主要用于服务之间的数据传输。如果微服务在公司内部解包,微服务之间的数据交互是以DTO为数据结果响应载体的。此外,DTO的存在还保护了字段中的底层数据不受外部依赖的影响。如果直接把DO还给依赖方,那么我们的表结构就一目了然了,在公司内部也没问题。如果在有兴趣的团队之间采用这种方法进行服务交互,可能会出现安全问题和不必要的争议。

 

  00-1010值对象,其存在主要意味着数据展示,直接包含有业务意义的数据。它处理前端,业务层把DO或BO转换成VO供前端使用。

  前面已经介绍了几种常用的数据实体,所以出现了一个关键问题。由于应用分为这么多层,每一层使用的数据实体可能都不一样,不可避免的会存在实体之间的转换问题,这也是本文需要重点研究的问题。

  00-1010所谓数据实体转换,就是将源数据实体中存储的数据转换到目标数据实体的实例对象存储中。比如BO转换成VO数据,响应前端,需要将源数据实体的属性值一一映射到目标数据实体并赋值,即VO.setXxx(BO.getXxx())。当然,我们可以选择最原始、最繁琐的方式,逐个遍历源数据实体的属性并将其赋给新的数据实体,或者

  目前比较可行和可行的方案中,常用的是逐个设置和工具类赋值。

  在数据实体字段少或者字段类型复杂的情况下,可以考虑逐个赋值。但如果字段相对较多,就会有一个实体类转换,会写几十行甚至几百行代码。这是完全不能接受的,所以我们需要自己实现反射或者使用线程的工具类来实现。当然还有很多工具类。比如apache的常用包有Nutils的实现,Spring Beans有Nutils的实现,Guava也有相关的实现。暂且从源维度分析一下使用Spring Beans的Nutils进行数据实体转换的实现原理和可能存在的漏洞。

  00-1010转换数据实体时,最常用的方法是BeanUtils#copyProperties方法,基本用法是3360。

  //DO是源数据对象,DTO是目标对象。将源类的数据复制到目标对象beans.copyproperties (do,DTO);

  

BO

直接看方法签名:

 

  /** *将给定源豆的属性值复制到目标豆中pNote:只要属性匹配,源类和目标类就不必匹配,甚至不必相互派生。*源豆公开的任何豆属性

  t 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)); }}@Datastatic class Demo1 { private List<String> list; public Demo1(List<String> list) { this.list = list; }}@Datastatic 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内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: