【踩坑日记】记一次静态导入引起Lombok失效,导致编译失败的惨案

摘要:
所以我们检查了Pom、Maven私有服务器和Jenkins的配置,发现一切正常。上一个分支没有问题。很容易认为与代码生成相关的组件存在问题,Lombok将是第一个受到指责的人。起初,我认为这是一个龙目版本冲突。我没有看到IDEA的依赖关系图有什么不同。我只能求助于StackOverflow。果然,这是一个老bug,Lombok开发团队无意修复它。不难发现,Lombook中所有注释的保留期都是SOURCE,因为它还使用注释处理器生成模板代码。我们还需要安装特定的插件来在IDEA中使用Lombok。

背景

时间:   某个普通的周一

天气:   晴,万里无云

内容:   开开心心写完需求,提交代码,打包部署。

[INFO] --------------------------------------------------------
[ERROR] COMPILATION ERROR : 
[INFO] -------------------------------------------------------------
[ERROR] cannot find symbol
[ERROR] non-static method cannot be referenced from a static context
[ERROR] invalid method reference
[ERROR] cannot find symbol
[ERROR] cannot find symbol
[ERROR] cannot find symbol
[ERROR] cannot find symbol
......

  好家伙,突然爆了上百个编译错误,我寻思我也妹干啥啊,只是写了一些业务代码。遂检查了项目Pom、Maven私服、Jenkins配置,结果一切正常,打先前的分支压根没毛病。然而在本地和远程打这个新分支就是不行。


朔源

  排除了环境问题,剩下的肯定就是代码问题了。这里观察Maven打包日志,错误发生在编译期,内容大致都是找不到符号、方法不存在等等。很容易想到是代码生成相关的组件出问题了,那么首当其冲就要问罪Lombok了。

  初以为是Lombok版本冲突之类的问题,用IDEA的Dependencies Diagram没看到异样。难道这里面有坑?只能求助于StackOverflow了,果不其然...原来这是一个陈年老BUG了,并且Lombok开发组并没有任何意愿修复。

罪魁祸首

  长话短说,罪魁祸首就是项目中的静态导入! 我们可以简单复现下:

package com.mycompany.lombokutilityclassbug;

@UtilityClass
public class MyUtilityClass {

    void foo() { System.out.println ("hi"); }
}

这里创建一个类,加上 @UtilityClass 注解,使其成员方法自动加上static修饰符。

package com.mycompany.lombokutilityclassbug;

import static com.mycompany.lombokutilityclassbug.MyUtilityClass.foo;

public class Main { }

然后在别处 静态导入 这个工具类下一个或多个类方法。

注意,只有静态导入指定类成员,而不是使用通配符才能复现错误!也就是使用通配符是没有任何问题的!

import static com.mycompany.lombokutilityclassbug.MyUtilityClass.*;    //works pretty fine.
import static com.mycompany.lombokutilityclassbug.MyUtilityClass.foo;  //broken

然后编译一波,就会看到和本文开头一样的错误了。

但奇怪的是开发时Idea并不会报错,简直是波澜不惊,错误只发生在编译时。



Why?

下面摘录Lombok开发组大佬的一段话

This is a known bug, and not something that's easy to fix. Static imports are resolved before the annotation processors are run. This is a problem in javac, not lombok.

简单阐述下,这是一个已知的Bug,并且很难修复。静态导入在 注解处理器(annotaion processors) 运行之前就被编译器解析了,这是javac的问题,这锅爷不背。

注解处理器

那么什么是注解处理器呢(Annotation Processor)?
在定义自己的注解时,必须用到Java预置的一组元注解,其中呢有个东西叫 @Retention,用于声明你创建的这个注解的保留周期,可接受RetentionPolicy枚举。

/**
 * Annotation retention policy.  The constants of this enumerated type
 * describe the various policies for retaining annotations.  They are used
 * in conjunction with the {@link Retention} meta-annotation type to specify
 * how long annotations are to be retained.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

默认行为呢,是选用CLASS级别,意味着注解将在编译期保留于class文件中,但运行时不会加载到JVM内存里。

RUNTIME级别,同时保留于class文件和JVM中,反射组件 能且仅能 扫描到的注解就是这一类。

SOURCE级别,注解在编译期就会被遗弃,相比于CLASS更多是用于开发辅助和代码生成。

可见度:SOURCE < CLASS < RUNTIME

而注解处理器就主要应用于SOURCE级别,在编译阶段通过向编译器注册自定义的注解处理器,可以进行一些额外操作,如代码生成、代码检查。

@Override就是一个SOURCE注解,现代IDE可以在识别到未覆盖父类方法的就打上@Override注解时进行报错标识,其原理便是注解处理器。

不难发现Lombok的所有注解其保留周期都是SOURCE,因为其也是使用注解处理器来生成模板代码的。

结合上述概念,为什么这个锅大佬不背我们似乎有点眉目了。java编译过程似乎存在着一种时序问题,下面对编译器工作时序作一次大胆的猜测(还没修炼到精通编译器阶段,大佬轻喷):

  • 编译代码元素
  • 组织包定义、导入语句
  • 加载注解处理器
  • etc...

在这种时序下,静态导入语句的编译先于注解处理器的运行,这时模板代码还没生成,自然就会报一堆编译错误了~

那为什么在IDE中没有报此类错误呢?我们在IDEA使用Lombok还需要安装特定的插件。就结果来看其编译过程和原生JDK的有所不同,或许这其中存在某些干预和优化,导致了错误被掩盖。



如何避免

鉴于官方已经甩锅不修,也修不了。我们只能修改开发代码来规避问题了,对于这类问题我们总结一个通解(java工具书风格):

使用静态导入时,如果导入的代码元素是通过注解处理器动态生成的,不要使用具名静态导入,可以使用通配符。鉴于IDE通常会自动优化导入语句,可能会将通配符缩减成具名,而又不会报告这个编译器级别的漏洞,最佳方案是两者不要同时使用。



What's More

关于Lombok的时序问题,Lombok作者撰写了一篇Wiki: Lombok概念:解析,有兴趣的同学可以研究一下。作者将之比喻为“先有鸡还是先有蛋的问题”。了解其背后的矛盾后,也就能解释Lombok的一些局限性了,比如Builder/AllArgsConstructor/...为何不能继承父类字段等等。





参考:

[1] static import not working in lombok builder in intelliJ - (2017/12/06)
https://stackoverflow.com/questions/47674264/static-import-not-working-in-lombok-builder-in-intellij

[2] @Builder not work with static import in Intellij 2016.2.4 - (2016/09/27)
https://github.com/mplushnikov/lombok-intellij-plugin/issues/291

[3] JEP 216: Process Import Statements Correctly - (2014/08/26)
http://openjdk.java.net/jeps/216

[4] LOMBOK CONCEPT: Resolution - (2018/06/12)
https://github.com/rzwitserloot/lombok/wiki/LOMBOK-CONCEPT:-Resolution

免责声明:文章转载自《【踩坑日记】记一次静态导入引起Lombok失效,导致编译失败的惨案》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇spark streaming job生成与运行ArcGIS中国工具(ArcGISCTools)2.0正式发布下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Linux基础-配置网络、集群内主机名设定、ssh登入、bash命令、通配符(元字符)

作业一:临时配置网络(ip,网关,dns)+永久配置 设置临时网络配置: 配置IP ifcongfigens33192.168.16.177/24 (ifconfig 网卡 ip地址 /24代表它子网掩码) 配置网关 route add default gw192.168.16.177netmask255.255.255.0 添加默认网关 配置 DN...

android 内存泄漏,以及检测方法

1、为什么会产生内存泄漏 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。 2、内存泄漏对程序的影响 内存泄漏是造成应用程序OOM的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多...

VMware虚拟机中配置静态IP的方法

    VMnet0:用于虚拟桥接网络下的虚拟交换机 桥接网络是指本地物理网卡和虚拟网卡通过VMnet0虚拟交换机进行桥接,物理网卡和虚拟网卡在拓扑图上处于同等地位。 VMnet1:用于虚拟Host-Only网络下的虚拟交换机(仅主机) 在Host-Only模式下,虚拟网络是一个全封闭的网络,它唯一能够访问的就是主机。 VMnet8:用于虚拟NAT网络下的...

PHP的Calling Scope(::调用非静态方法)

今天在群里发现有人说,PHP可以用::调用非静态方法,一致没这么试过,发现了鸟哥的blog写了这个问题的具体解释,就搬过来: 这个问题乍看, 确实很容易让人迷惑, 但实际上, 造成这样的误解的根本原因在于: 在PHP中, 判断静态与否不是靠”::”(PAAMAYIM_NEKUDOTAYIM)符号, 而是靠calling scope. 那么, 什么是call...

C#中Form设计器打开失败的错误及解决方案

错误信息是这样的: Form1 可以进行设计,但不是文件中的第一个类。Visual Studio 要求设计器使用文件中的第一个类。移动类代码使之成为文件中的第一个类,然后尝试重新加载设计器。    也就是点击“查看设计器”时不能看到Form和控件只有报错信息。 猪悟能看到如图1所示的错误已经不是一次两次了,前几依据这个错误提示死活解决不了问题,只得骂骂咧...

iOS—静态方法(类方法)和实例方法

1.实例方法/动态方法     a).标识符:-     b).调用方式:(实例对象    函数)     c).实例方法在堆栈上。 2.静态方法/类方法     a).标识符:+     b).调用方式:(类    函数)     c).静态方法在堆上分配内存。 3.静态方法和实例方法的区分      a).静态方法常驻内存,实例方法不是,所以静态方法效...