wangjie_fourth

may the force be with you

0%

使用Lombok遇到的问题

自从去年接触过Lombok后,就变成它深深的迷弟,毕竟提高了老高的生产力。以至于在新项目上,我会无脑使用Lombok,在改老项目的时候,我也是尽量使用Lombok

之前也听说过Lombok的缺点,无非是对高版本的JDK不支持,会强制要求所有开发人员都使用Lombok。但这对于我们写业务代码人来说,这都不是事。业务系统谁会没事升级JDK,能提高生产力的工具谁又会拒绝。所以上面俩个缺点对于我来说,都不是事。

但自从上次遇到一个坑之后,发现这事就不那么简单了。在老系统中使用Lombok替换getset是有坑的,万一踩到,就是生产事故了;而且像Lombok这样覆盖字节码文件,而不是生成新字节码文件的操作,的确是不太好。只要踩到坑,又会坑到你怀疑人生。

先说我遇到的坑

问题描述

事情起源是是一次feign切换的需求。我的老项目中,是使用传统HttpUtils手工来与对应服务通信的。代码大概是这样的:

1
2
String resultJson = HttpUtil.get(url + "/fin/test?code=" + code, null);
RespDTO<Result> respDTO = JSON.parseObject(resultJson, new TypeReference<RespDTO<Result>>() {});

说一下重点:这里是使用fastJson来反序列化数据的。

后来因为后端系统使用SpringCloud那一套,大佬觉得使用HttpUtils太丑了,让我们把之前代码都改成使用feign来调用。大概就改成下面这样:

1
RespDTO<Result> respDTO = urlClient.getTest(code);

再说一下重点:这里是使用jackson来反序列化数据。

emm,这里其实就埋下了一个坑,使用不同反序列化工具来反序列化数据。俩种序列化工具采用的反序列化机制是有区别的:

  • jackson:默认采用java Bean规范的属性来反序列化数据
  • fastjson:默认采用对象属性名来反序列化数据

而如果你刚好满足以下条件:

  • 反序列化对象中含有类似aFiled这种,第一个字母小写,第二个字母大写的属性名
  • 俩种序列化工具均采用默认配置
  • 这个反序列化对象使用Lombok来生成get、set方法

恭喜你,你即将得到”成长”!你会突然发现第三方服务返回aFiled字段是含有值的,而你对象的aFiled确是没有值。如果你后面代码又用到这个属性值,生产事故就来了。

问题分析

问题的原因就是在某种情况下,Lombok所生成的getset方法是不满足JavaBean规范的。

JavaBean规范中规定属性名是由其getset方法决定的(boolean特例),而其getset方法名称是由对象的变量名来决定的。通常情况下,就是首字母大写。但JavaBean规范又留了一手:

8.8 Capitalization of inferred names.
When we use design patterns to infer a property or event name, we need to decide what rulesto follow for capitalizing the inferred name. If we extract the name from the middle of a normalmixedCase style Java name then the name will, by default, begin with a capital letter.
Java programmers are accustomed to having normal identifiers start with lower case letters.Vigorous reviewer input has convinced us that we should follow this same conventional rulefor property and event names.
Thus when we extract a property or event name from the middle of an existing Java name, wenormally convert the first character to lower case. However to support the occasional use of allupper-case names, we check if the first two characters of the name are both upper case and ifso leave it alone. So for example,
“FooBah” becomes “fooBah”
“Z” becomes “z”
“URL” becomes “URL”
We provide a method Introspector.decapitalize which implements this conversion rule.

这就导致aFiled很尴尬了,它不能首字母大写,因为这样就是AFiled属性了。所以,这种情况下,首字母不用变,生成的get方法名称是getaFiledsetaFiled

Lombok不管,它无脑首字母大写,简单粗暴。它生成的get方法名称是getAFiled,生成set方法名称是setAFiled

当第三方返回aFiled值,你又使用默认规则的jackson来反序列化数据时,jackson就会寻找这个对象是否含有aFiled属性,也就是找get方法名称是getaFiled、set方法是setaFiled。它找不到,aFiled就没设到值,一切都很合理,直到线上抱空指针错误。

问题解决

jackson可以使用@JsonProperty注解来指定属性名称。

问题总结

如果你老项目接口含有aFiled这种风格的属性,谨慎使用Lombok

如果你老项目接收含有boolean这种风格的属性,谨慎使用Lombok,因为它生成的get方法也是不一样的。

如何避免这个问题

站在初级程序员的角度来说,我踩过,我下次就有很大可能避免这个问题。但是如果站在项目管理者的角度来说,你要想办法让一个根本不知道这个坑的人踩不到这个坑。

其实如果你项目里配置了代码检查工具的话,比如说checkstylespotbugs,你只要在这上面添加一条规则,不允许项目中含有这种命名方式,或者检测到有这种命名方式后,邮件通知你review这段代码。

Lombok这种Annotation Processor

Annotationjdk1.5引入的一个特性,网上有种说法是Annotation设计是用来生成新的字节码,而不是覆盖原有的字节码文件。所以说,像Lombok这种直接覆盖原本字节码的这种方式是不被推荐的。只不过大佬从来们都是踩着原则的,Lombok这么用,也还是受到很多人的欢迎。

Annotation Processor过程

  • java编译器开始工作
  • 所有未工作的Annotation Processor开始工作
  • 循环处理程序中带注解的元素
  • 用已创建的类、方法和字段的元数据生成一个新类的字符串
  • 创建一个新的字节码文件并将生成的字符串写入进去
  • 编译器检查是否执行了所有Annotation Processor程序。如果没有,开始下一轮。

写一个模仿LombokGetterSetter功能

1、如何生成字节码工具

2、生成的字节码放到哪里

这种方式缺点

暗地修改修改字节码信息,会让后来debug变得很困难。比如说bug出现在你生成字节码那部分。写一个示例

Lombok的其他缺点

1、equals、hashCode重写具有危险
https://stackoverflow.com/questions/6518534/equals-method-overrides-equals-in-superclass-and-may-not-be-symmetric

其实equals是判断俩个对象是否等价,如果俩个父子类的变量都是一致的;你用instanceof来拦截其实也是不对的


https://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/
https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657
https://stackoverflow.com/questions/13690272/code-replacement-with-an-annotation-processor
https://lotabout.me/2017/Notes-on-Java-Annotation-Processor/