再也不敢精通Java了——get/set篇

Java技术迷

共 5305字,需浏览 11分钟

 ·

2022-01-01 02:05

点击关注公众号,Java干货及时送达

小伙伴们好呀,今天 4ye 来和大家分享在项目中遇到的一个特别有意思的 ‘bug’ 😄请看~

看题

import lombok.Data;
@Data
public class UserDTO {
    private String uName;
    private boolean active;
    private Boolean closed;
    private Boolean isDeleted;
    private boolean isActive2;
}

上面的这个 DTO 中,生成的 get/set 方法是啥样子的呢?(注意是 lombok 生成的

image-20211216074356505

比如

  1. 是 getUName 还是 getuName
  2. 是 getActive 还是 isActive
  3. 是 getClosed 还是 isClosed
  4. 是 getIsDeleted 还是 isDeleted
  5. 是 getIsActive2 还是 isActive2

上面是 get 的情况,那 set 呢?

请思考下,接下来的答案可能会和你想的有点出入~


答案如下

lombok版

是不是有点吃惊 哈哈

先来点简单的~

Boolean

这个就很简单啦,生成的都是我们我们平时用到的样子,过~

boolean

这个 active 是基本数据类型的 boolean ,生成的 get 方法是 isActive ,  set 方法是 setActive ,很正常🐖

但是你会发现这个 boolean isActive2 很不一样,它生成的 get 方法是 isActive2 , set 方法是 setActive2

按理来说应该生成 isIsActive2 方法和 setIsActive2 方法才对呀,结果居然没有!

请问:你觉得这个是 lombok 的锅还是 java 本身的设计 🐷

为了排除嫌疑,我用 idea 自动生成 get/set ,结果它俩居然是一样的,那这个应该就是 java 的某种特点

不知道小伙伴们还记得 阿里的Java开发手册 没,里面就提到了不要用这个 is 前缀去修饰 pojo 中的 boolean 变量。

不过应该也很少人在这个 pojo 中定义 boolean 类型了叭~  这个也在 手册中有提到 ,毕竟 null 也的话还能表示数据接受的异常等

String uName

从上面可以发现,lombok 生成的是 getUName 和 setUName ,而如果通过 IDEA 去生成的话,是生成这个 getuName 和 setuName 。

请先记住这个点,下面正片开始~

如图所示,这个就是折磨了我快一天的 bug,测试接口时,发现了这么诡异的一幕,后端只定义了这个 tDate 属性,压根就没有 tdate 这个属性,可是前端 post 数据时,居然给我传了这两个参数上来,而且诡异的是,我后端还接受不到!

我当时就懵了,想着这前端写的啥代码,怎么给我搞这出…… 🐷

于是乎,我们愉快的进行了沟通~

结果发现,这个是在更新数据时出现的,而这个 tdate 属性是我传回来的,而且就是 null

我仔细看了下,发现这居然是真的,我的天,我后台明明没有这个 tdate  的!

于是乎,我开始了 扒源码 之路 (就那种直接怼 很笨的做法😅)

直接从 tomcat  到这个 SpringMVC ,最后看到这个 Jackson 时才醒悟过来 (惊呼:我在干什么!🐖)

原理图

如图 ,后端接收到 request 请求时,要将数据进行 反序列化,转换成我们接口中使用的对象。

您猜怎么着,这反序列化的过程,居然不是直接使用我们定义好的属性字段,而是通过 get/set 方法去推测出来的!!

这个过程比较复杂,先来看这个请求数据 👇

发出的请求

这里切入有点唐突~  因为这个 debug 过程很长,我也记不住,就记住下面这些要点。🐖

请求过程

请求时,会来到这么一个方法,而在进入这个 _addMethods 方法时,这里还是正常的五个属性

image-20211219220118077

进入之后,会调用到这个方法 legacyManglePropertyName ,最后会返回这个  uname 属性名字(后面再解释)

出来后,这个 props 直接变成下面 7 个了,包括这个 isActive2 直接变成 active2 属性。

接下来的一步,就是执行上面的这个 _removeUnwantedProperties 方法,它会移除不想要的属性。(指上面 _addFields 和 _addMethods 推测出来的属性和方法中,所有 isVisible 值为 false 的会被移除掉

  • 执行  _removeUnwantedAccessor  去移除 不需要的 get/set 方法
  • 执行这个  _renameProperties 方法。这个会根据我们使用的 注解 @JsonProperty("uName") 来重命名我们的这个属性。

执行到最后,会变成这样子,方法名字还是 getUNAME/setUNAME , 但是我们这个属性名字却是 uname

关键点

省略一大堆步骤……(怎么提取请求中的body,并获取其中的字段,匹配到相应的请求参数中 等),直接来到关键点这个 反序列化的赋值操作 ,可以看到这里会将我们的 json 请求中的字段提取出来,然后进行匹配,找不到的话,就无法赋值。

这里面还使用了这个 散列数组 _hashArea 来存储这个属性  。

这里已经匹配不上了,所以这个我们的 DTO 中获取不到值

效果如下 👇


响应过程

这里就涉及到这个序列化的过程了, 这个 debug 起来也比较简单了 就不过的赘述啦~

反序列化时会执行到一个 serializeValue 方法 ,会执行到一个 serializeFields 方法 (将字段进行序列化)

_props 对应的五个属性如下 👇

很明显这个 uname 就从这里出现的,最后得到的结果就如下了 😅

解决办法也很简单,就是用  @JsonProperty("uName") 去定义好这个 属性名称就好了

思考

到这里,我们就简单了解了这个 请求怎么反序列化成为一个对象,以及对象怎么序列化,对客户端进行响应的一个过程

同时我们也了解到 Jackson 有它自己的获取属性的规则,会将我们的 uName 变成这个 uname

参考上面的这个  legacyManglePropertyName 方法了 👇 (这个在  jackson-databind-2.12.4.jar 版本中,之前2.11的代码是用到那个 BeanUtil 包下的,小伙伴们可以自己看看,不过现在标记为 过期的 了。)


那么 ,lombok 怎么生成这个 get 方法呢?

这里参考下这篇文章 ,了解下 lombok 的工作原理

https://www.cnblogs.com/heyonggang/p/8638374.html

那个语法树啥的我也没有试过~,感觉不懂的地方又多了亿点点

这道题太难了!我不会!

不过根据文章给出的信息,我们知道 在 lombok 的源码中有很多 Handle 专门来处理每一个 lombok 注解,如下(源码直接在 github 上下载)

生成 get 方法解密 ,可以看到在源码中,有个很显眼的 toGetterName 方法,

它会去调用这个 toAccessorName 方法,可以看到这里传了一个 get 前缀字符串

最后会来到这个 buildAccessorName 方法,没猜错的话,这里就是真正创建的方法了。

果然,可以看到如下代码 ,capitalize  翻译过来就是 把……首字母大写 (那应该没找错了~)

最后,来到这个 CapitalizationStrategy 枚举类中,发现默认用了这 BASIC ,把其中的方法拷贝出来运行下,就可以证实我们的猜测了

代码如下

// BASIC
public String capitalize(String in) {
    if (in.length() == 0return 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() == 0return 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);
}

@Test
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

(我晕了 😵)

这个方法的作用是 使首字母变小 ,而且在 Spring 的这些包中使用到!貌似也是用来推测属性,小伙伴们可以自行研究~

三. 一开始我以为是 bug,结果来到 Jackson 的 GitHub issue 地址 ,却发现这个 19 年就有了 天呐,早知道我就直接搜 bug 好了,损失了一个 PR 和亿点点时间 🐖,不过也是在这里了解到上面那个 Introspector  的 😂 (好复杂)

https://github.com/FasterXML/jackson-databind/issues/2327

最后

终于写完这篇文章了,吸收了好多奇奇怪怪的知识点,这 Java 也太复杂了叭!幸好毕业那会写过精通两字,不亏 哈哈哈  从今以后都不敢写了 ,一个 get,set 都这么难~ 



1、Log4j2维护者吐槽没工资还要挨骂,GO安全负责人建议开源作者向公司收费
2、太难了!让程序员崩溃的8个瞬间
3、2021年程序员们都在用的神级数据库
4、Windows重要功能被阉割,全球用户怒喷数月后微软终于悔改
5、牛逼!国产开源的远程桌面火了,只有9MB 支持自建中继器!
6、摔到老三的 Java,未来在哪?
7、真香!用 IDEA 神器看源码,效率真高!

点分享

点收藏

点点赞

点在看

浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报