小伙伴们好呀,今天 4ye 来和大家分享在项目中遇到的一个特别有意思的 ‘bug’ 😄
请看~
题
import lombok.Data;
public class UserDTO {
private String uName;
private boolean active;
private Boolean closed;
private Boolean isDeleted;
private boolean isActive2;
}
上面的这个 DTO 中,生成的 get/set 方法是啥样子的呢?(注意是 lombok 生成的)
 再也不敢精通Java了——get/set篇_反序列化](https://s2.51cto.com/images/blog/202204/11183408_62540420572c444372.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
比如
- 是 getUName 还是 getuName
- 是 getActive 还是 isActive
- 是 getClosed 还是 isClosed
- 是 getIsDeleted 还是 isDeleted
- 是 getIsActive2 还是 isActive2
上面是 get 的情况,那 set 呢?
请思考下,接下来的答案可能会和你想的有点出入~
 再也不敢精通Java了——get/set篇_java_02](https://s2.51cto.com/images/blog/202204/11183409_625404210593164213.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
答案如下
 再也不敢精通Java了——get/set篇_json_03](https://s2.51cto.com/images/blog/202204/11183407_6254041fd5d5511630.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
 再也不敢精通Java了——get/set篇_反序列化_04](https://s2.51cto.com/images/blog/202204/11183408_6254042051f528176.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
是不是有点吃惊 哈哈
先来点简单的~
Boolean
这个就很简单啦,生成的都是我们我们平时用到的样子,过~
 再也不敢精通Java了——get/set篇_java_05](https://s2.51cto.com/images/blog/202204/11183408_625404203bd1556160.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
boolean
这个 active 是基本数据类型的 boolean ,生成的 get 方法是 isActive , set 方法是 setActive ,很正常🐖
但是你会发现这个 boolean isActive2 很不一样,它生成的 get 方法是 isActive2 , set 方法是 setActive2 。
按理来说应该生成 isIsActive2 方法和 setIsActive2 方法才对呀,结果居然没有!
请问:你觉得这个是 lombok 的锅还是 java 本身的设计 🐷
为了排除嫌疑,我用 idea 自动生成 get/set ,结果它俩居然是一样的,那这个应该就是 java 的某种特点 了
 再也不敢精通Java了——get/set篇_java_06](https://s2.51cto.com/images/blog/202204/11183408_62540420b55be30958.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
不知道小伙伴们还记得 阿里的Java开发手册 没,里面就提到了不要用这个 is 前缀去修饰 pojo 中的 boolean 变量。
 再也不敢精通Java了——get/set篇_json_07](https://s2.51cto.com/images/blog/202204/11183409_625404210be3569048.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
不过应该也很少人在这个 pojo 中定义 boolean 类型了叭~ 这个也在 手册中有提到 ,毕竟 null 也的话还能表示数据接受的异常等
 再也不敢精通Java了——get/set篇_反序列化_08](https://s2.51cto.com/images/blog/202204/11183408_62540420cd60726878.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
String uName
从上面可以发现,lombok 生成的是 getUName 和 setUName ,而如果通过 IDEA 去生成的话,是生成这个 getuName 和 setuName 。
 再也不敢精通Java了——get/set篇_反序列化_09](https://s2.51cto.com/images/blog/202204/11183408_62540420c3e4a11316.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
请先记住这个点,下面正片开始~
 再也不敢精通Java了——get/set篇_反序列化_10](https://s2.51cto.com/images/blog/202204/11183408_62540420db58161035.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
如图所示,这个就是折磨了我快一天的 bug,测试接口时,发现了这么诡异的一幕,后端只定义了这个 tDate 属性,压根就没有 tdate 这个属性,可是前端 post 数据时,居然给我传了这两个参数上来,而且诡异的是,我后端还接受不到!
我当时就懵了,想着这前端写的啥代码,怎么给我搞这出…… 🐷
于是乎,我们愉快的进行了沟通~
结果发现,这个是在更新数据时出现的,而这个 tdate 属性是我传回来的,而且就是 null
 再也不敢精通Java了——get/set篇_java_11](https://s2.51cto.com/images/blog/202204/11183409_625404218312076084.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
我仔细看了下,发现这居然是真的,我的天,我后台明明没有这个 tdate 的!
于是乎,我开始了 扒源码 之路 (就那种直接怼 很笨的做法😅)
直接从 tomcat 到这个 SpringMVC ,最后看到这个 Jackson 时才醒悟过来 (惊呼:我在干什么!🐖)
原理图
 再也不敢精通Java了——get/set篇_json_12](https://s2.51cto.com/images/blog/202204/11183409_625404217177a51529.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
如图 ,后端接收到 request 请求时,要将数据进行 反序列化,转换成我们接口中使用的对象。
您猜怎么着,这反序列化的过程,居然不是直接使用我们定义好的属性字段,而是通过 get/set 方法去推测出来的!! 再也不敢精通Java了——get/set篇_反序列化_13](https://s2.51cto.com/images/blog/202204/11183410_625404221030e31236.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
这个过程比较复杂,先来看这个请求数据 👇
 再也不敢精通Java了——get/set篇_反序列化_14](https://s2.51cto.com/images/blog/202204/11183409_6254042173e5270973.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
这里切入有点唐突~ 因为这个 debug 过程很长,我也记不住,就记住下面这些要点。🐖
请求过程
请求时,会来到这么一个方法,而在进入这个 _addMethods 方法时,这里还是正常的五个属性
 再也不敢精通Java了——get/set篇_反序列化_15](https://s2.51cto.com/images/blog/202204/11183412_6254042433c9565931.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
进入之后,会调用到这个方法 legacyManglePropertyName ,最后会返回这个 uname 属性名字(后面再解释)
 再也不敢精通Java了——get/set篇_json_16](https://s2.51cto.com/images/blog/202204/11183410_62540422a788229409.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
出来后,这个 props 直接变成下面 7 个了,包括这个 isActive2 直接变成 active2 属性。
 再也不敢精通Java了——get/set篇_反序列化_17](https://s2.51cto.com/images/blog/202204/11183413_62540425aa3aa81851.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
接下来的一步,就是执行上面的这个 _removeUnwantedProperties 方法,它会移除不想要的属性。(指上面 _addFields 和 _addMethods 推测出来的属性和方法中,所有 isVisible 值为 false 的会被移除掉 )
 再也不敢精通Java了——get/set篇_反序列化_18](https://s2.51cto.com/images/blog/202204/11183411_625404232d2e267289.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
- 执行 _removeUnwantedAccessor 去移除 不需要的 get/set 方法
- 执行这个 _renameProperties 方法。这个会根据我们使用的 注解 @JsonProperty("uName") 来重命名我们的这个属性。
执行到最后,会变成这样子,方法名字还是 getUNAME/setUNAME , 但是我们这个属性名字却是 uname
关键点
省略一大堆步骤……(怎么提取请求中的body,并获取其中的字段,匹配到相应的请求参数中 等),直接来到关键点这个 反序列化的赋值操作 ,可以看到这里会将我们的 json 请求中的字段提取出来,然后进行匹配,找不到的话,就无法赋值。
这里面还使用了这个 散列数组 _hashArea 来存储这个属性 。
这里已经匹配不上了,所以这个我们的 DTO 中获取不到值
效果如下 👇
 再也不敢精通Java了——get/set篇_java_20](https://s2.51cto.com/images/blog/202204/11183411_62540423603ea79280.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
响应过程
这里就涉及到这个序列化的过程了, 这个 debug 起来也比较简单了 就不过的赘述啦~
 再也不敢精通Java了——get/set篇_json_21](https://s2.51cto.com/images/blog/202204/11183412_62540424219a489456.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
反序列化时会执行到一个 serializeValue 方法 ,会执行到一个 serializeFields 方法 (将字段进行序列化)
 再也不敢精通Java了——get/set篇_java_22](https://s2.51cto.com/images/blog/202204/11183411_62540423d528046787.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
_props 对应的五个属性如下 👇
 再也不敢精通Java了——get/set篇_java_23](https://s2.51cto.com/images/blog/202204/11183412_62540424c2c0e74835.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
很明显这个 uname 就从这里出现的,最后得到的结果就如下了 😅
 再也不敢精通Java了——get/set篇_java_24](https://s2.51cto.com/images/blog/202204/11183413_62540425d6d0238505.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
解决办法也很简单,就是用 @JsonProperty("uName") 去定义好这个 属性名称就好了 。
思考
到这里,我们就简单了解了这个 请求怎么反序列化成为一个对象,以及对象怎么序列化,对客户端进行响应的一个过程。
同时我们也了解到 Jackson 有它自己的获取属性的规则,会将我们的 uName 变成这个 uname
参考上面的这个 legacyManglePropertyName 方法了 👇 (这个在 jackson-databind-2.12.4.jar 版本中,之前2.11的代码是用到那个 BeanUtil 包下的,小伙伴们可以自己看看,不过现在标记为 过期的 了。)
 再也不敢精通Java了——get/set篇_java_25](https://s2.51cto.com/images/blog/202204/11183412_62540424dcd8041545.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
那么 ,lombok 怎么生成这个 get 方法呢?
这里参考下这篇文章 ,了解下 lombok 的工作原理
https://www.cnblogs.com/heyonggang/p/8638374.html
 再也不敢精通Java了——get/set篇_反序列化_26](https://s2.51cto.com/images/blog/202204/11183413_6254042560ab615302.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
那个语法树啥的我也没有试过~,感觉不懂的地方又多了亿点点
 再也不敢精通Java了——get/set篇_反序列化_27](https://s2.51cto.com/images/blog/202204/11183413_625404253b6bf59423.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
不过根据文章给出的信息,我们知道 在 lombok 的源码中有很多 Handle 专门来处理每一个 lombok 注解,如下(源码直接在 github 上下载)
 再也不敢精通Java了——get/set篇_json_28](https://s2.51cto.com/images/blog/202204/11183413_625404259739658493.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
生成 get 方法解密 ,可以看到在源码中,有个很显眼的 toGetterName 方法,
 再也不敢精通Java了——get/set篇_json_29](https://s2.51cto.com/images/blog/202204/11183416_6254042877aae27021.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
它会去调用这个 toAccessorName 方法,可以看到这里传了一个 get 前缀字符串
 再也不敢精通Java了——get/set篇_json_30](https://s2.51cto.com/images/blog/202204/11183418_6254042a46d3358417.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
最后会来到这个 buildAccessorName 方法,没猜错的话,这里就是真正创建的方法了。
 再也不敢精通Java了——get/set篇_json_31](https://s2.51cto.com/images/blog/202204/11183415_6254042721dbe45239.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
果然,可以看到如下代码 ,capitalize 翻译过来就是 把……首字母大写 (那应该没找错了~)
 再也不敢精通Java了——get/set篇_反序列化_32](https://s2.51cto.com/images/blog/202204/11183414_625404268f9ea62307.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
最后,来到这个 CapitalizationStrategy 枚举类中,发现默认用了这 BASIC ,把其中的方法拷贝出来运行下,就可以证实我们的猜测了
 再也不敢精通Java了——get/set篇_反序列化_33](https://s2.51cto.com/images/blog/202204/11183414_62540426e28f410277.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
代码如下
// BASIC
public String capitalize(String in) {
if (in.length() == 0) return in;
char first = in.charAt(0);
if (!Character.isLowerCase(first)) return in;
boolean useUpperCase = in.length() > 2 &&
(Character.isTitleCase(in.charAt(1)) || Character.isUpperCase(in.charAt(1)));
return (useUpperCase ? Character.toUpperCase(first) : Character.toTitleCase(first)) + in.substring(1);
}
// BEANSPEC
public String capitalize2(String in) {
if (in.length() == 0) return in;
char first = in.charAt(0);
if (!Character.isLowerCase(first) || (in.length() > 1 && Character.isUpperCase(in.charAt(1)))) return in;
boolean useUpperCase = in.length() > 2 && Character.isTitleCase(in.charAt(1));
return (useUpperCase ? Character.toUpperCase(first) : Character.toTitleCase(first)) + in.substring(1);
}
void testName(){
System.out.println(capitalize("tDate")); // TDATE
System.out.println(capitalize2("tDate")); // tdate
}
总结
阅读完后,希望你能记住以下几点~
一. 属性名称一定不要弄成有歧义的那种,不然我们都猜不透这个 get/set 是什么样子的!比如 uName 这种第二个字母就大写的!
二. 如果非要写成 uName ,建议自己手写 get/set 或者 使用 @JsonProperty 注解。
三. Jackson 是从get,set方法中推测属性的
四. 使用到 Lombok 相关注解时,它会在编译期根据自己的规则帮我们生成 get/set 方法。
扩展
一. 在阅读 Jackson 源码时,发现它使用到这个 LRUMap ,会推测第一次请求到的对象属性,并缓存到 props 中,最多存 2000 个。
二. Java 中有一个 Introspector 类,这个和 JavaBean 的规范有关 ,地址 https://www.oracle.com/java/technologies/javase/javabeans-spec.html
(我晕了 😵)
 再也不敢精通Java了——get/set篇_json_34](https://s2.51cto.com/images/blog/202204/11183414_625404268b29311029.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
这个方法的作用是 使首字母变小 ,而且在 Spring 的这些包中使用到!貌似也是用来推测属性,小伙伴们可以自行研究~
 再也不敢精通Java了——get/set篇_json_35](https://s2.51cto.com/images/blog/202204/11183415_625404274899423820.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
三. 一开始我以为是 bug,结果来到 Jackson 的 GitHub issue 地址 ,却发现这个 19 年就有了 天呐,早知道我就直接搜 bug 好了,损失了一个 PR 和亿点点时间 🐖,不过也是在这里了解到上面那个 Introspector 的 😂 (好复杂)
https:///FasterXML/jackson-databind/issues/2327
 再也不敢精通Java了——get/set篇_json_36](https://s2.51cto.com/images/blog/202204/11183415_625404275a48829382.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
我是4ye 咱们下期应该……很快再见!! 😆
喜欢的话还可以关注下公众号 Java4ye 支持下 4ye 呀😝
















