当我看技术文章的时候,我在想什么?
你好呀,我是 why 哥。
其实这篇文章我老早就想写了,应该感觉这篇文章写起来也不是一件特别费时间的事情,但是这不是一直拖着的嘛。
为啥拖着呢?
其实也没啥特别的原因,就是因为我懒呗。
好了,说回文章。
不知道你有没有在其他的公众号推送或者网上看到这样的一篇技术文章:
据我考证,这篇文章最早的出处应该是这里:
https://www.jianshu.com/p/357b55852efc
一个叫做“绝色天龙”的小伙伴写于 2018 年 7 月,总阅读 12w 多还挺高的。
我发现这个小伙子文章写的还挺有趣的,也擅长用比较轻松的语言、适当的表情包去表达自己想要表达的东西。
他这篇文章说的是关于使用 Spring 的 BeanUtils 的 copyProperties 方法的时候遇到的一个需要注意的点。具体是个啥需要注意的点呢,我简单的转述一下。
啥注意的点?
天龙同学在和第三方联调的时候,用了对方的 SDK 包。然后这个 SDK 包里面的一个 Request 对象存在不能序列化的问题。
所以天龙同学就在自己的项目中自定义了一个和该 Request 对象一模一样的对象。
然后通过 Spring 的 BeanUtils 进行对象的拷贝。
BeanUtils.copyProperties(自定义的 Request, 对方的 Request);
请求发过去后,对方反馈请求参数中缺少了一个必要的字段,要求排查一下。
最后发现这个字段属于 Request 对象中的一个静态内部类中的字段。
然后他发现 BeanUtils.copyProperties 竟然没有把他的内部类对象拷贝过去。
他的测试用例是这样的。
先给出两个对象 CopyTest1 和 CopyTest2,对象的结构和里面的属性看起来是一模一样的:
在这里天龙同学还说了这样的一句话:
这里遇到了第一个坑,一开始图省事,属性写为 public,想着省掉了 getter 和 setter 方法,没加 @Data 注解,结果运行完 test2 所有属性都为 null,一个都没 copy 过去,加上 @Data 继续跑,果然,基本属性(String)复制过去了,但是内部类在 test2 中还是 null。
他说他加 @Data 注解的目的是生成 get/set 方法嘛。
其实我觉得从 Java 封装的角度来看,加了 get/set 方法后,字段的访问级别就应该从 public 修改为 private。
一个小细节,不影响流程,只是提一下。
我这边就还用他给的代码做演示。
有了这两个对象之后,他给了一个 main 方法:
跑起来一看结果,你猜怎么着:
内部类确实没有被拷贝过去。
用天龙同学的原话:
有点不敢相信自己的眼睛,毕竟线上跑了这么久的代码...
他给出的解决方案是单独设置一下内部类,单独 copy。
如果内部类的 bean 属性较多或者递归的 bean 属性很多,那可以自己封装一个方法,用于递归拷贝,他这里只有一层,所以直接额外 copy 一次。
问题得到解决了,皆大欢喜了。
然后他给出了四点总结:
1.Spring 的 BeanUtils 的 CopyProperties 方法需要对应的属性有 getter 和 setter 方法;
2.如果存在属性完全相同的内部类,但是不是同一个内部类,即分别属于各自的内部类,则 spring 会认为属性不同,不会 copy;
3.泛型只在编译期起作用,不能依靠泛型来做运行期的限制;
4.最后,spring 和 apache 的 copy 属性的方法源和目的参数的位置正好相反,所以导包和调用的时候都要注意一下。
最后,他附了一段 String copyProperties 方法的源码,文章就结束了。
总体来说其实是写的很好的,总结的这四点也没啥毛病。
于是这篇文章就被四处搬运,然后我就看到了。
但是我看到之后我总觉得差点意思。
主要是针对他总结的第二点。
在这之前我不知道 Spring 的 BeanUtils 的 CopyProperties 方法拷贝内部类的时候有问题。
现在我看了文章知道,但是 why?
为什么有问题?
没有呈现到源码级别的答案,都只是表象的答案。
于是,我去探寻了一番。
源码之下无秘密
首先,内部类这个玩意其实就是 Java 的一颗语法糖而已。
比如常用的自动拆箱、自动装箱、高级 for 循环、泛型等等,包括 JDK 10 那个不三不四的局部变量类型推断功能,就是那个 var,也是语法糖而已。
那么内部类这颗语法糖长什么样子呢?
好,你现在先记住这个几个 class 类。
我带你去源码里面走一圈:
版本:spring-benas-4.3.8.RELEASE
方法:org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object, java.lang.Class<?>, java.lang.String...)
这个方法的源码其实很短,只有 44 行,我给大家把关键的地方写上注释,截图如下:
所以从上面的源码解读中我可以得到这样的几条结论:
1.对于要复制的属性,目标对象必须要有对应的 set 方法(上图的第 27 行),源对象必须要有对应的 get 方法(上图的第 34 行)。
2.对于要复制的属性,目标对象和源对象的属性名称得一模一样。(上图的第 34 行)。
3.目标对象对应的 set 方法的入参和源对象对应的 get 方法的返回类型必须得一致。
源码读完了,答案就也出来了。
上面这三条,你说天龙同学的例子违反了哪一条呢?
你要脚指头想也知道是第三条了。
我们来验证一下。
源对象 set 方法的入参是 CopyTest2,而目标对象方法的返参是 InnerClass。
不满足我们前面总结出来的第三点,所以不会拷贝成功。
道理就是这么一个道理。
所以你看,有的时候看到的技术文章,你稍微往前再走一步,就会发现另外一片天地,而这一片天地就是你的意外收获。
另外说一句,CopyProperties 我一般不用,在我心中 get/set 才是王道。
如果加了一个字段,我调用了它的 set 方法,却找不到 get 方法被调用的地方。
我会很慌。
另一个案例
另外一个案例也是曾经在公众号里面被转来转去的一片文章:
就是 Spring 的 Controller 是单例还是多例?
由于被转发的次数太多,导致我已经找不到这篇文章的出处了。
文章写的没有任何毛病,结论就是 Controller 默认是单例的。
然后文章里面给了一个非常简单的代码示例:
文章里面说首先访问 http://localhost:8080/testScope,得到的答案是 1;
然后再访问 http://localhost:8080/testScope2,得到的答案是 2。
得到的不同的值,这是线程不安全的。
最后文章在 ScopeTestController 类上加了一个注解 @Scope("prototype")。
再次进行重复的验证 http://localhost:8080/testScope,得到的答案是 1;
再访问 http://localhost:8080/testScope2,得到的答案还是 1。
哇塞,好神奇啊。都是 1 了。
然后,文章就结束了。
这只是一个表象啊?原因呢?
为什么加上 @Scope("prototype") 注解之后同样的验证方式就得出了不一样的结论呢?
不知道你有没有想过,反正我看到这篇文章的时候我就感觉是个半成品。
只分析了表象,没有把根挖出来。
其实真正的答案就藏在 org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
方法里面。
当然,我在这篇文章里面也不会去给你解析了。
我只是提个醒。
这就是 Spring 的源码,而且是一段比较重要的源码。
这个案例不就是一个很好的去了解、调试 Spring 的切入口吗?
很多同学说不知道源码怎么去看。
不,你不是不知道,而是每次都是就差一步的时候,你就觉得到终点了。
举个简单的例子,比如你去调试这部分源码的时候,你可以看到这行代码:
Set default singleton scope, if not configured before.
还需要我翻译吗?
再举个例子:
这里有三个分支。
前两个,一个是作用域为单例的情况、一个是作用域为多例(原型)的情况。
你说一个 bean 的作用域既不是单例、也不是多例,那会是什么?
很明显就是自定义作用域了嘛。
单例和多例是 Spring 去帮我们管理实例的方式而已。
Spring 当然也留下了口子,让我们按照自己的想法去管理自己的实例。
那就是自定义作用域:
org.springframework.beans.factory.config.Scope
这是一个接口,这个接口就是口子。
而这个自定义作用域对应的 Spring 源码的入口就是 doGetBean
方法。
你看,多想一个为什么,就会有这么多意外收获。
所以带着怀疑的眼光去看博客,带着求证的想法去证伪。
多想想 why?
好了,不写了。本文就到这吧。
版权声明: 本文为 InfoQ 作者【why技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/267bbeb6968cab8d549714805】。文章转载请联系作者。
评论 (1 条评论)