【踩坑日記】記一次靜態導入引起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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM