自从去年接触过Lombok
后,就变成它深深的迷弟,毕竟提高了老高的生产力。以至于在新项目上,我会无脑使用Lombok
,在改老项目的时候,我也是尽量使用Lombok
。
之前也听说过Lombok
的缺点,无非是对高版本的JDK
不支持,会强制要求所有开发人员都使用Lombok
。但这对于我们写业务代码人来说,这都不是事。业务系统谁会没事升级JDK
,能提高生产力的工具谁又会拒绝。所以上面俩个缺点对于我来说,都不是事。
但自从上次遇到一个坑之后,发现这事就不那么简单了。在老系统中使用Lombok
替换get
、set
是有坑的,万一踩到,就是生产事故了;而且像Lombok
这样覆盖字节码文件,而不是生成新字节码文件的操作,的确是不太好。只要踩到坑,又会坑到你怀疑人生。
先说我遇到的坑
问题描述
事情起源是是一次feign
切换的需求。我的老项目中,是使用传统HttpUtils
手工来与对应服务通信的。代码大概是这样的:
1 | String resultJson = HttpUtil.get(url + "/fin/test?code=" + code, null); |
说一下重点:这里是使用
fastJson
来反序列化数据的。
后来因为后端系统使用SpringCloud
那一套,大佬觉得使用HttpUtils
太丑了,让我们把之前代码都改成使用feign
来调用。大概就改成下面这样:
1 | RespDTO<Result> respDTO = urlClient.getTest(code); |
再说一下重点:这里是使用
jackson
来反序列化数据。
emm,这里其实就埋下了一个坑,使用不同反序列化工具来反序列化数据。俩种序列化工具采用的反序列化机制是有区别的:
jackson
:默认采用java Bean
规范的属性来反序列化数据fastjson
:默认采用对象属性名来反序列化数据
而如果你刚好满足以下条件:
- 反序列化对象中含有类似
aFiled
这种,第一个字母小写,第二个字母大写的属性名 - 俩种序列化工具均采用默认配置
- 这个反序列化对象使用
Lombok
来生成get、set
方法
恭喜你,你即将得到”成长”!你会突然发现第三方服务返回aFiled
字段是含有值的,而你对象的aFiled
确是没有值。如果你后面代码又用到这个属性值,生产事故就来了。
问题分析
问题的原因就是在某种情况下,Lombok
所生成的get
、set
方法是不满足JavaBean
规范的。
JavaBean
规范中规定属性名是由其get
、set
方法决定的(boolean
特例),而其get
、set
方法名称是由对象的变量名来决定的。通常情况下,就是首字母大写。但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方法名称是getaFiled
、setaFiled
。
而Lombok
不管,它无脑首字母大写,简单粗暴。它生成的get
方法名称是getAFiled
,生成set方法名称是setAFiled
。
当第三方返回aFiled
值,你又使用默认规则的jackson
来反序列化数据时,jackson
就会寻找这个对象是否含有aFiled
属性,也就是找get方法名称是getaFiled
、set方法是setaFiled
。它找不到,aFiled
就没设到值,一切都很合理,直到线上抱空指针错误。
问题解决
jackson
可以使用@JsonProperty
注解来指定属性名称。
问题总结
如果你老项目接口含有aFiled
这种风格的属性,谨慎使用Lombok
。
如果你老项目接收含有boolean
这种风格的属性,谨慎使用Lombok
,因为它生成的get
方法也是不一样的。
如何避免这个问题
站在初级程序员的角度来说,我踩过,我下次就有很大可能避免这个问题。但是如果站在项目管理者的角度来说,你要想办法让一个根本不知道这个坑的人踩不到这个坑。
其实如果你项目里配置了代码检查工具的话,比如说checkstyle
、spotbugs
,你只要在这上面添加一条规则,不允许项目中含有这种命名方式,或者检测到有这种命名方式后,邮件通知你review
这段代码。
Lombok这种Annotation Processor
Annotation
是jdk1.5
引入的一个特性,网上有种说法是Annotation
设计是用来生成新的字节码,而不是覆盖原有的字节码文件。所以说,像Lombok
这种直接覆盖原本字节码的这种方式是不被推荐的。只不过大佬从来们都是踩着原则的,Lombok
这么用,也还是受到很多人的欢迎。
Annotation Processor
过程
- java编译器开始工作
- 所有未工作的
Annotation Processor
开始工作 - 循环处理程序中带注解的元素
- 用已创建的类、方法和字段的元数据生成一个新类的字符串
- 创建一个新的字节码文件并将生成的字符串写入进去
- 编译器检查是否执行了所有
Annotation Processor
程序。如果没有,开始下一轮。
写一个模仿Lombok
的Getter
、Setter
功能
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/