介紹
Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.
大概的意思:Lombok是一個Java庫,能自動插入編輯器并構建工具,簡化Java開發。通過添加注解的方式,不需要為類編寫getter或eques方法,同時可以自動化日志變量。
簡而言之:Lombok能以簡單的注解形式來簡化java代碼,提高開發人員的開發效率。
Lombok的使用
第一步:安裝插件
使用Lombok還需要插件的配合,我使用開發工具為idea,這里只講解idea中安裝lombok插件,使用eclipse和myeclipse的小伙伴和自行google安裝方法。
打開idea的設置,點擊Plugins,點擊Browse repositories,在彈出的窗口中搜索lombok,然后安裝即可。
?第二步:開啟注解處理器
Annotation Processors > Enable annotation processing
。
第三步:添加maven依賴
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.4</version><scope>provided</scope> </dependency>
?
示例
下面舉兩個栗子,看看使用lombok和不使用的區別。
創建一個用戶類
不使用Lombok
public class User implements Serializable {private static final long serialVersionUID = -8054600833969507380L;private Integer id;private String username;private Integer age;public User() {}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", age=" + age +'}';}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}User user = (User) o;return Objects.equals(id, user.id) &&Objects.equals(username, user.username) &&Objects.equals(age, user.age);}@Overridepublic int hashCode() {return Objects.hash(id, username, age);}}
使用Lombok后:?
@Data
public class User implements Serializable {private static final long serialVersionUID = -8054600833969507380L;private Integer id;private String username;private Integer age;}
?編譯源文件,然后反編譯class文件,反編譯結果如下圖。說明@Data注解在類上,會為類的所有屬性自動生成setter/getter、equals、canEqual、hashCode、toString方法,如為final屬性,則不會為該屬性生成setter方法。
@Slf4j
@RestController
@RequestMapping(("/user"))
public class UserController {@GetMapping("/getUserById/{id}")public User getUserById(@PathVariable Integer id) {User user = new User();user.setUsername("風清揚");user.setAge(21);user.setId(id);if (log.isInfoEnabled()) {log.info("用戶 {}", user);}return user;}}
通過反編譯可以看到@Slf4j注解生成了log日志變量(嚴格意義來說是常量),無需去聲明一個log就可以在類中使用log記錄日志。
Lombok工作原理
在Lombok使用的過程中,只需要添加相應的注解,無需再為此寫任何代碼。自動生成的代碼到底是如何產生的呢?
核心之處就是對于注解的解析上。JDK5引入了注解的同時,也提供了兩種解析方式。
運行時解析
運行時能夠解析的注解,必須將@Retention設置為RUNTIME,這樣就可以通過反射拿到該注解。java.lang.reflect反射包中提供了一個接口AnnotatedElement,該接口定義了獲取注解信息的幾個方法,Class、Constructor、Field、Method、Package等都實現了該接口,對反射熟悉的朋友應該都會很熟悉這種解析方式。
編譯時解析
編譯時解析有兩種機制,分別簡單描述下:1)Annotation Processing Tool
apt自JDK5產生,JDK7已標記為過期,不推薦使用,JDK8中已徹底刪除,自JDK6開始,可以使用Pluggable Annotation Processing API來替換它,apt被替換主要有2點原因:
api都在com.sun.mirror非標準包下
沒有集成到javac中,需要額外運行
2)Pluggable Annotation Processing APIJSR 269自JDK6加入,作為apt的替代方案,它解決了apt的兩個問題,javac在執行的時候會調用實現了該API的程序,這樣我們就可以對編譯器做一些增強,javac執行的過程如下:
Lombok本質上就是一個實現了“JSR 269 API”的程序。在使用javac的過程中,它產生作用的具體流程如下:
javac對源代碼進行分析,生成了一棵抽象語法樹(AST)
運行過程中調用實現了“JSR 269 API”的Lombok程序
此時Lombok就對第一步驟得到的AST進行處理,找到@Data注解所在類對應的語法樹(AST),然后修改該語法樹(AST),增加getter和setter方法定義的相應樹節點
javac使用修改后的抽象語法樹(AST)生成字節碼文件,即給class增加新的節點(代碼塊)
通過讀Lombok源碼,發現對應注解的實現都在HandleXXX中,比如@Getter注解的實現在HandleGetter.handle()。還有一些其它類庫使用這種方式實現,比如Google Auto、Dagger等等。
常用注解
@Setter 注解在類或字段,注解在類時為所有字段生成setter方法,注解在字段上時只為該字段生成setter方法。
@Getter 使用方法同上,區別在于生成的是getter方法。
@ToString 注解在類,添加toString方法。
@EqualsAndHashCode 注解在類,生成hashCode和equals方法。
@NoArgsConstructor 注解在類,生成無參的構造方法。
@RequiredArgsConstructor 注解在類,為類中需要特殊處理的字段生成構造方法,比如final和被@NonNull注解的字段。
@AllArgsConstructor 注解在類,生成包含類中所有字段的構造方法。
@Data 注解在類,生成setter/getter、equals、canEqual、hashCode、toString方法,如為final屬性,則不會為該屬性生成setter方法。
@Slf4j 注解在類,生成log變量,嚴格意義來說是常量。private static final Logger log = LoggerFactory.getLogger(UserController.class);
@NonNull
該注解用在屬性或構造器上,Lombok會生成一個非空的聲明,可用于校驗參數,能幫助避免空指針。示例代碼:
//成員方法參數加上@NonNull注解,構造方法也一樣,在此不做演示 public String getName(@NonNull Person p){return p.getName(); }
實際效果相當于:
public String getName(Person p){if(p==null){throw new NullPointerException("person");}return p.getName(); }
?
@Getter
和@Setter
:在JavaBean或類JavaBean中使用,使用此注解相當于為成員變量生成對應的get和set方法,方法默認修飾符為public,同時還可以使用AccessLevel為生成的方法指定訪問修飾符。這兩個注解還可以直接用在類上,可以為此類里的所有非靜態成員變量生成對應的get和set方法。示例代碼:
public class Student{@Getter@Setterprivate String name;@Setter(AccessLevel.PROTECTED)private int age;@Getter(AccessLevel.PUBLIC)private String language; }
實際效果相當于:
public class Student{private String name;private int age;private String language;public void setName(String name){this.name = name;}public String getName(){return name;}protected void setAge(int age){this.age = age;}public String getLanguage(){return language;} }
@Cleanup
這個注解用在變量前面,可以保證此變量代表的資源會被自動關閉,默認是調用資源的close()方法,如果該資源有其它關閉方法,可使用@Cleanup(“methodName”)來指定要調用的方法。示例代碼:
public static void main(String[] args) throws IOException {@Cleanup InputStream in = new FileInputStream(args[0]);@Cleanup OutputStream out = new FileOutputStream(args[1]);byte[] b = new byte[1024];while (true) {int r = in.read(b);if (r == -1) break;out.write(b, 0, r);}}
實際效果相當于:
public static void main(String[] args) throws IOException {InputStream in = new FileInputStream(args[0]);try {OutputStream out = new FileOutputStream(args[1]);try {byte[] b = new byte[10000];while (true) {int r = in.read(b);if (r == -1) break;out.write(b, 0, r);}} finally {if (out != null) {out.close();}}} finally {if (in != null) {in.close();}} }
@ToString
在JavaBean或類JavaBean中使用,使用此注解會自動重寫對應的toStirng方法,默認情況下,會輸出類名、所有屬性(會按照屬性定義順序),用逗號來分割,通過callSuper參數來指定是否引用父類,includeFieldNames參數設為true,就能明確的輸出toString()屬性。<code>@ToString(exclude=”column”)</code>
意義:排除column列所對應的元素,即在生成toString方法時不包含column參數;
<code>@ToString(exclude={“column1″,”column2″})</code>
意義:排除多個column列所對應的元素,其中間用英文狀態下的逗號進行分割,即在生成toString方法時不包含多個column參數;
<code>@ToString(of=”column”)</code>
意義:只生成包含column列所對應的元素的參數的toString方法,即在生成toString方法時只包含column參數;;
<code>@ToString(of={“column1″,”column2”})</code>
意義:只生成包含多個column列所對應的元素的參數的toString方法,其中間用英文狀態下的逗號進行分割,即在生成toString方法時只包含多個column參數;示例代碼:
@ToString(exclude="id") public class ToStringExample {private static final int STATIC_VAR = 10;private String name;private Shape shape = new Square(5, 10);private String[] tags;private int id;public String getName() {return this.getName();}@ToString(callSuper=true, includeFieldNames=true)public static class Square extends Shape {private final int width, height;public Square(int width, int height) {this.width = width;this.height = height;}} }
實際效果相當于:
public class ToStringExample {private static final int STATIC_VAR = 10;private String name;private Shape shape = new Square(5, 10);private String[] tags;private int id;public String getName() {return this.getName();}public static class Square extends Shape {private final int width, height;public Square(int width, int height) {this.width = width;this.height = height;}@Override public String toString() {return "Square(super=" + super.toString() + ", width=" + this.width + ", height=" + this.height + ")";}}@Override public String toString() {return "ToStringExample(" + this.getName() + ", " + this.shape + ", " + Arrays.deepToString(this.tags) + ")";} }
@EqualsAndHashCode
默認情況下,會使用所有非靜態(non-static)和非瞬態(non-transient)屬性來生成equals和hasCode,也能通過exclude注解來排除一些屬性。示例代碼:
@EqualsAndHashCode(exclude={"id", "shape"}) public class EqualsAndHashCodeExample {private transient int transientVar = 10;private String name;private double score;private Shape shape = new Square(5, 10);private String[] tags;private int id;public String getName() {return this.name;}@EqualsAndHashCode(callSuper=true)public static class Square extends Shape {private final int width, height;public Square(int width, int height) {this.width = width;this.height = height;}} }
@NoArgsConstructor
、@RequiredArgsConstructor
和@AllArgsConstructor
這三個注解都是用在類上的,第一個和第三個都很好理解,就是為該類產生無參的構造方法和包含所有參數的構造方法,第二個注解則使用類中所有帶有@NonNull注解的或者帶有final修飾的成員變量生成對應的構造方法,當然,成員變量都是非靜態的,另外,如果類中含有final修飾的成員變量,是無法使用@NoArgsConstructor注解的。
三個注解都可以指定生成的構造方法的訪問權限,同時,第二個注解還可以用@RequiredArgsConstructor(staticName=”methodName”)的形式生成一個指定名稱的靜態方法,返回一個調用相應的構造方法產生的對象。
示例代碼:
@RequiredArgsConstructor(staticName = "myShape") @AllArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor public class Shape {private int x;@NonNullprivate double y;@NonNullprivate String name; }
實際效果相當于:
public class Shape {private int x;private double y;private String name;public Shape(){}protected Shape(int x,double y,String name){this.x = x;this.y = y;this.name = name;}public Shape(double y,String name){this.y = y;this.name = name;}public static Shape myShape(double y,String name){return new Shape(y,name);} }
@Data
注解在類上,會為類的所有屬性自動生成setter/getter、equals、canEqual、hashCode、toString方法,如為final屬性,則不會為該屬性生成setter方法。@Value
注解和@Data
類似,區別在于它會把所有成員變量默認定義為private final修飾,并且不會生成set方法。官方實例如下:
@Data public class DataExample {private final String name;@Setter(AccessLevel.PACKAGE) private int age;private double score;private String[] tags;@ToString(includeFieldNames=true)@Data(staticConstructor="of")public static class Exercise<T> {private final String name;private final T value;} }
實際效果相當于:
public class DataExample {private final String name;private int age;private double score;private String[] tags;public DataExample(String name) {this.name = name;}public String getName() {return this.name;}void setAge(int age) {this.age = age;}public int getAge() {return this.age;}public void setScore(double score) {this.score = score;}public double getScore() {return this.score;}public String[] getTags() {return this.tags;}public void setTags(String[] tags) {this.tags = tags;}@Override public String toString() {return "DataExample(" + this.getName() + ", " + this.getAge() + ", " + this.getScore() + ", " + Arrays.deepToString(this.getTags()) + ")";}protected boolean canEqual(Object other) {return other instanceof DataExample;}@Override public boolean equals(Object o) {if (o == this) return true;if (!(o instanceof DataExample)) return false;DataExample other = (DataExample) o;if (!other.canEqual((Object)this)) return false;if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;if (this.getAge() != other.getAge()) return false;if (Double.compare(this.getScore(), other.getScore()) != 0) return false;if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;return true;}@Override public int hashCode() {final int PRIME = 59;int result = 1;final long temp1 = Double.doubleToLongBits(this.getScore());result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());result = (result*PRIME) + this.getAge();result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));result = (result*PRIME) + Arrays.deepHashCode(this.getTags());return result;}public static class Exercise<T> {private final String name;private final T value;private Exercise(String name, T value) {this.name = name;this.value = value;}public static <T> Exercise<T> of(String name, T value) {return new Exercise<T>(name, value);}public String getName() {return this.name;}public T getValue() {return this.value;}@Override public String toString() {return "Exercise(name=" + this.getName() + ", value=" + this.getValue() + ")";}protected boolean canEqual(Object other) {return other instanceof Exercise;}@Override public boolean equals(Object o) {if (o == this) return true;if (!(o instanceof Exercise)) return false;Exercise<?> other = (Exercise<?>) o;if (!other.canEqual((Object)this)) return false;if (this.getName() == null ? other.getValue() != null : !this.getName().equals(other.getName())) return false;if (this.getValue() == null ? other.getValue() != null : !this.getValue().equals(other.getValue())) return false;return true;}@Override public int hashCode() {final int PRIME = 59;int result = 1;result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());result = (result*PRIME) + (this.getValue() == null ? 43 : this.getValue().hashCode());return result;}} }
@SneakyThrows
這個注解用在方法上,可以將方法中的代碼用try-catch語句包裹起來,捕獲異常并在catch中用Lombok.sneakyThrow(e)把異常拋出,可以使用@SneakyThrows(Exception.class)的形式指定拋出哪種異常,很簡單的注解,直接看個例子:public class SneakyThrows implements Runnable {@SneakyThrows(UnsupportedEncodingException.class)public String utf8ToString(byte[] bytes) {return new String(bytes, "UTF-8");}@SneakyThrowspublic void run() {throw new Throwable();} }
實際效果相當于:
public class SneakyThrows implements Runnable {public String utf8ToString(byte[] bytes) {try{return new String(bytes, "UTF-8");}catch(UnsupportedEncodingException uee){throw Lombok.sneakyThrow(uee);}}public void run() {try{throw new Throwable();}catch(Throwable t){throw Lombok.sneakyThrow(t);}} }
@Synchronized
這個注解用在類方法或者實例方法上,效果和synchronized關鍵字相同,區別在于鎖對象不同,對于類方法和實例方法,synchronized關鍵字的鎖對象分別是類的class對象和this對象,而@Synchronized得鎖對象分別是私有靜態final對象LOCK和私有final對象lock,當然,也可以自己指定鎖對象,例子也很簡單:public class Synchronized {private final Object readLock = new Object();@Synchronizedpublic static void hello() {System.out.println("world");}@Synchronizedpublic int answerToLife() {return 42;}@Synchronized("readLock")public void foo() {System.out.println("bar");} }
實際效果相當于:
public class Synchronized {private static final Object $LOCK = new Object[0];private final Object $lock = new Object[0];private final Object readLock = new Object();public static void hello() {synchronized($LOCK) {System.out.println("world");}}public int answerToLife() {synchronized($lock) {return 42;}}public void foo() {synchronized(readLock) {System.out.println("bar");}}}
@Log
這個注解用在類上,可以省去從日志工廠生成日志對象這一步,直接進行日志記錄,具體注解根據日志工具的不同而不同,同時,可以在注解中使用topic來指定生成log對象時的類名。不同的日志注解總結如下(上面是注解,下面是實際作用):@CommonsLog private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class); @JBossLog private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class); @Log private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName()); @Log4j private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class); @Log4j2 private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class); @Slf4j private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class); @XSlf4j private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);
注解使用有風險
Lombok的優點顯而易見,可以幫助我們省去很多冗余代碼?
那它都有哪些缺點?
在使用Lombok過程中,如果對于各種注解的底層原理不理解的話,很容易產生意想不到的結果。
舉一個簡單的例子:我們知道,當我們使用@Data定義一個類的時候,會自動幫我們生成equals()方法 。但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的話,會默認是@EqualsAndHashCode(callSuper=false),這時候生成的equals()方法只會比較子類的屬性,不會考慮從父類繼承的屬性,無論父類屬性訪問權限是否開放,這就可能得到意想不到的結果。
使用過程中如果不小心,在一定程度上就會破壞代碼的封裝性。
舉個簡單的例子,我們定義一個購物車類,并且使用了@Data注解:
@Data public class ShoppingCart { //商品數目private int itemsCount; //總價格private double totalPrice; //商品明細private List items = new ArrayList<>(); }
我們知道,購物車中商品數目、商品明細以及總價格三者之前其實是有關聯關系的,如果需要修改的話是要一起修改的。但是,我們使用了Lombok的@Data注解,對于itemsCount 和 totalPrice這兩個屬性,雖然我們將它們定義成 private 類型,但是提供了 public 的 getter、setter 方法。
外部可以通過 setter 方法隨意地修改這兩個屬性的值,我們可以隨意調用 setter 方法,來重新設置 itemsCount、totalPrice 屬性的值,這也會導致其跟 items 屬性的值不一致。
而面向對象封裝的定義是:通過訪問權限控制,隱藏內部數據,外部僅能通過類提供的有限的接口訪問、修改內部數據。所以,暴露不應該暴露的 setter 方法,明顯違反了面向對象的封裝特性。
好的做法應該是不提供getter/setter,而是只提供一個public的addItem方法,同時取修改itemsCount、totalPrice以及items三個屬性。
因此,在此種情況下,就不適合使用Lombok,或者只用@Getter不用@Setter,而別直接使用@Data,在使用過程中,需要多多小心。