本文给大家介绍一个Java代码简化的神器 -- Lombok。主要从如下几个方面进行展开:
1、Lombok介绍
根据Lombok官网https://projectlombok.org/的介绍如下:
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官网的描述可以看出:
Project Lombok是一个java库,其可以自动插入到你的编辑器和构建工具中,
使java代码更加生动。
使用Lombok提供的注解将会带来诸多改变,如:
-- 不需要再写getter、equals等方法
-- 一个注解将为类提供功能齐全的Builder,后续我们将会演示@Builder注解
-- 自动插入日志变量等等
看着还挺吸引人吧,那接下来就来安装一下,然后来小试一下其提供的神奇注解。
2、Lombok安装
访问Lombok官网(https://projectlombok.org/download)下载Lombok.jar。使用java -jar lombok.jar运行, 选择指定IDE路径,本文以Eclipse为示例,其它IDE的安装类似。选择Eclipse.exe。
点击Install / update按钮即可完成安装。
3、Lombok注解使用
引入Lombok依賴包:
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
Lombok提供的注解可以在lombok的package找到,如下图所示:
本文将给出如下注解的使用示例,包括:
3.1 @Getter / @Setter
package com.wangmengjun.tutorial.lombok;
import java.io.Serializable;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Book implements Serializable {
private static final long serialVersionUID = -8212889116567064704L;
/**书名*/
private String name;
/**ISBN*/
private String isbn;
/**作者*/
private List<String> authors;
}
代码自动为各个属性增加了getter、setter方法,如下图所示:
利用反编译工具查看,其效果等价于如下代码:
package com.wangmengjun.tutorial.lombok;
import java.io.Serializable;
import java.util.List;
public class Book
implements Serializable
{
private static final long serialVersionUID = 0x8e05f4dcbb592f80L;
private String name;
private String isbn;
private List authors;
public Book()
{
}
public String getName()
{
return name;
}
public String getIsbn()
{
return isbn;
}
public List getAuthors()
{
return authors;
}
public void setName(String name)
{
this.name = name;
}
public void setIsbn(String isbn)
{
this.isbn = isbn;
}
public void setAuthors(List authors)
{
this.authors = authors;
}
}
3.2 @NoArgsConstructor @AllsConstructor
从注解的名字上就可以看出,其作用是产生无参以及包含全部参数的构造函数,如下图高亮部分所示:
如果,Book类需要增加一个出版社press的属性,那么,全部参数的构造函数需要重新添加一个属性。在这个时候,使用@AllArgsConstructor注解的BookLombok 类将不用再修改任何代码。
3.3 @EqualsAndHashCode
该注解将为类产生equals,hasCode,其中还产生了一个用于比较的canEquals方法,如下图高亮部分所示:
反编译后增加的代码如所示:
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof Book))
return false;
Book other = (Book)o;
if (!other.canEqual(this))
return false;
Object this$name = getName();
Object other$name = other.getName();
if (this$name != null ? !this$name.equals(other$name) : other$name != null)
return false;
Object this$isbn = getIsbn();
Object other$isbn = other.getIsbn();
if (this$isbn != null ? !this$isbn.equals(other$isbn) : other$isbn != null)
return false;
Object this$authors = getAuthors();
Object other$authors = other.getAuthors();
return this$authors != null ? this$authors.equals(other$authors) : other$authors == null;
}
protected boolean canEqual(Object other)
{
return other instanceof Book;
}
public int hashCode()
{
int PRIME = 59;
int result = 1;
Object $name = getName();
result = result * 59 + ($name != null ? $name.hashCode() : 43);
Object $isbn = getIsbn();
result = result * 59 + ($isbn != null ? $isbn.hashCode() : 43);
Object $authors = getAuthors();
result = result * 59 + ($authors != null ? $authors.hashCode() : 43);
return result;
}
3.4 @ToString
该注解将为类产生toString方法,如下图高亮部分所示:
public String toString()
{
return (new StringBuilder("Book(name=")).append(getName()).append(", isbn=").append(getIsbn()).append(", authors=").append(getAuthors()).append(")").toString();
}
3.5 @Data
使用@Data注解,其效果等价于:
All together now: A shortcut for
@ToString, @EqualsAndHashCode,
@Getter on all fields, and @Setter on all non-final fields,
and @RequiredArgsConstructor!
只有一个@Data注解的类如下:
package com.wangmengjun.tutorial.lombok;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
@Data
public class Book implements Serializable {
private static final long serialVersionUID = -8212889116567064704L;
/**书名*/
private String name;
/**ISBN*/
private String isbn;
/**作者*/
private List<String> authors;
}
反编译查看一下,
package com.wangmengjun.tutorial.lombok;
import java.io.Serializable;
import java.util.List;
public class Book
implements Serializable
{
private static final long serialVersionUID = 0x8e05f4dcbb592f80L;
private String name;
private String isbn;
private List authors;
public String getName()
{
return name;
}
public String getIsbn()
{
return isbn;
}
public List getAuthors()
{
return authors;
}
public void setName(String name)
{
this.name = name;
}
public void setIsbn(String isbn)
{
this.isbn = isbn;
}
public void setAuthors(List authors)
{
this.authors = authors;
}
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof Book))
return false;
Book other = (Book)o;
if (!other.canEqual(this))
return false;
Object this$name = getName();
Object other$name = other.getName();
if (this$name != null ? !this$name.equals(other$name) : other$name != null)
return false;
Object this$isbn = getIsbn();
Object other$isbn = other.getIsbn();
if (this$isbn != null ? !this$isbn.equals(other$isbn) : other$isbn != null)
return false;
Object this$authors = getAuthors();
Object other$authors = other.getAuthors();
return this$authors != null ? this$authors.equals(other$authors) : other$authors == null;
}
protected boolean canEqual(Object other)
{
return other instanceof Book;
}
public int hashCode()
{
int PRIME = 59;
int result = 1;
Object $name = getName();
result = result * 59 + ($name != null ? $name.hashCode() : 43);
Object $isbn = getIsbn();
result = result * 59 + ($isbn != null ? $isbn.hashCode() : 43);
Object $authors = getAuthors();
result = result * 59 + ($authors != null ? $authors.hashCode() : 43);
return result;
}
public String toString()
{
return (new StringBuilder("Book(name=")).append(getName()).append(", isbn=").append(getIsbn()).append(", authors=").append(getAuthors()).append(")").toString();
}
public Book()
{
}
}
可以看到所有属性的Getter、Setter、默认构造函数、toString方法、equas和hasCode方法都具备了。
3.6 @NonNull
此注解用于标注属性不能为null,如果setter方法传值为null,则抛出NullpointerException。
package com.wangmengjun.tutorial.lombok;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import lombok.NonNull;
@Data
public class Book implements Serializable {
private static final long serialVersionUID = -8212889116567064704L;
/**书名*/
private @NonNull String name;
/**ISBN*/
private String isbn;
/**作者*/
private List<String> authors;
}
上述示例对name增加了@NonNull, 反编译后,
public void setName(String name)
{
if (name == null)
{
throw new NullPointerException("name is marked non-null but is null");
} else
{
this.name = name;
return;
}
}
3.7 @Builder
此注解将产生Builder类,可以采用构建者模式完成赋值,如:
@Getter
@Setter
@Builder
public class Book implements Serializable {
private static final long serialVersionUID = -8212889116567064704L;
/**书名*/
private String name;
/**ISBN*/
private String isbn;
/**作者*/
private List<String> authors;
}
可以看到使用@Builder之后,自动生成了BookBuilder类和build方法, 如下图高亮部分所示:
反编译后,其效果如下:
package com.wangmengjun.tutorial.lombok;
import java.io.Serializable;
import java.util.List;
public class Book
implements Serializable
{
public static class BookBuilder
{
private String name;
private String isbn;
private List authors;
public BookBuilder name(String name)
{
this.name = name;
return this;
}
public BookBuilder isbn(String isbn)
{
this.isbn = isbn;
return this;
}
public BookBuilder authors(List authors)
{
this.authors = authors;
return this;
}
public Book build()
{
return new Book(name, isbn, authors);
}
public String toString()
{
return (new StringBuilder("Book.BookBuilder(name=")).append(name).append(", isbn=").append(isbn).append(", authors=").append(authors).append(")").toString();
}
BookBuilder()
{
}
}
private static final long serialVersionUID = 0x8e05f4dcbb592f80L;
private String name;
private String isbn;
private List authors;
Book(String name, String isbn, List authors)
{
this.name = name;
this.isbn = isbn;
this.authors = authors;
}
public static BookBuilder builder()
{
return new BookBuilder();
}
public String getName()
{
return name;
}
public String getIsbn()
{
return isbn;
}
public List getAuthors()
{
return authors;
}
public void setName(String name)
{
this.name = name;
}
public void setIsbn(String isbn)
{
this.isbn = isbn;
}
public void setAuthors(List authors)
{
this.authors = authors;
}
}
Buider创建对象示例如下:
Book book = Book.builder().name("Eric").isbn("1234567890")
.authors(Arrays.asList("Eric","Joan")).build();
3.8 @Singular
此注解与@Builder一起使用,增加单个赋值的方法,如对作者Authors变量设置此变量:
@Getter
@Setter
@Builder
public class Book implements Serializable {
private static final long serialVersionUID = -8212889116567064704L;
/**书名*/
private String name;
/**ISBN*/
private String isbn;
/**作者*/
private @Singular List<String> authors;
}
Builder支持单个author的赋值,增加一个方法
public BookBuilder author(String author)
{
if (authors == null)
authors = new ArrayList();
authors.add(author);
return this;
}
这样时候,Authors列表的赋值,支持一个一个地增加,如:
Book book = Book.builder().name("Eric").isbn("1234567890asdfghjkl")
.author("Eirc")
.author("Joan")
.build();
3.9 @CleanUp
使用@CleanUp可以完成自动资源管理,如添加close()方法关闭资源。
import lombok.Cleanup;
import java.io.*;
public class CleanupExample {
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[10000];
while (true) {
int r = in.read(b);
if (r == -1) break;
out.write(b, 0, r);
}
}
}
等价于
import java.io.*;
public class CleanupExample {
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();
}
}
}
}
3.10 @Synchronized
For non-static methods, a field named {@code $lock} is used,
and for static methods, {@code $LOCK} is used.
从@Synchronized注解描述可以看出,其对非静态方法会使用变量$lock,而对静态方法使用的是$LOCK。如:
public class SynchronizedExample {
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");
@Synchronized
public String synchronizedFormat(Date date) {
return format.format(date);
}
@Synchronized
public static String synchronizedStaticMethod() {
String message = "hello synchronized";
return message;
}
}
从下图右侧的高亮部分可以看到包含了对象$lock 和 $LOCK。
反编译查看代码,
package com.wangmengjun.tutorial.lombok;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SynchronizedExample
{
private final Object $lock = new Object[0];
private static final Object $LOCK = new Object[0];
private DateFormat format;
public SynchronizedExample()
{
format = new SimpleDateFormat("MM-dd-YYYY");
}
public String synchronizedFormat(Date date)
{
Object obj = $lock;
JVM INSTR monitorenter ;
return format.format(date);
obj;
JVM INSTR monitorexit ;
throw ;
}
public static String synchronizedStaticMethod()
{
Object obj = $LOCK;
JVM INSTR monitorenter ;
String message = "hello synchronized";
return message;
obj;
JVM INSTR monitorexit ;
throw ;
}
}
上述的moniterenter和moniterexit其实就是synchronized的实现,所以代码也就等价于:
package com.wangmengjun.tutorial.lombok;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SynchronizedExample
{
private final Object $lock = new Object[0];
private static final Object $LOCK = new Object[0];
private DateFormat format;
public SynchronizedExample()
{
format = new SimpleDateFormat("MM-dd-YYYY");
}
public String synchronizedFormat(Date date)
{
synchronized($lock) {
return format.format(date);
}
}
public static String synchronizedStaticMethod()
{
synchronized($LOCK) {
String message = "hello synchronized";
return message;
}
}
}
3.11@Slf4j
使用@Slf4j,其会在代码增加一个log对象,我们直接可以使用log进行日志记录。
package com.wangmengjun.tutorial.lombok;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogExample {
public void greeting(String name) {
System.out.println("Hello," +name);
log.info("Method greeting, name is {}", name);
}
public static void main(String[] args) {
LogExample example = new LogExample();
example.greeting("mengjun");
}
}
其等价于:
package com.wangmengjun.tutorial.lombok;
import java.io.PrintStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogExample
{
private static final Logger log = LoggerFactory.getLogger(com/wangmengjun/tutorial/lombok/LogExample);
public LogExample()
{
}
public void greeting(String name)
{
System.out.println((new StringBuilder("Hello,")).append(name).toString());
log.info("Method greeting, name is {}", name);
}
public static void main(String args[])
{
LogExample example = new LogExample();
example.greeting("mengjun");
}
}
运行LogEample类,可以直接可以使用log日志记录,可以说非常方便。
Hello,mengjun
10:49:33.765 [main] INFO com.wangmengjun.tutorial.lombok.LogExample - Method greeting, name is mengjun
注意,使用相关的日志注解,需要导入相关的日志,本示例中采用的SLF4j,需要引入如下依赖包:
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
当然,Lombok支持其它日志的记录,如Apache Common, JBOSS、Log4j等,只要使用对应的注解即可。
@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
Lombok还提供了很多其它的注解,在本篇文章中就不一一列举出来了。
那么问题来了,既然Lombok有这么好用的功能,其实现的原理又是什么呢?
4、Lombok原理分析
4.1 Java源码编译
在弄清Lombok是如何工作的之前,我们先来看一下OpenJDK上对Java源码编译过程的一个说明:
http://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html
Java 源码编译一般可以分成三个不同的阶段:
在解析和输入阶段,编译器会解析源文件到一个抽象语法树( Abstract Syntax Tree, 简称AST)。如果语法不合法将会抛出错误。
在注解处理阶段,自定义的注解处理器将会被调用,这可以被认为是预编译阶段。注解处理器可以做一些譬如验证类正确性、产生新的资源包含源文件等操作。
如果新的源码文件是注解处理的结果,那么编译循环回到解析和输入阶段,重复这个过程,直到没有新的源文件生产为止。
在最后一个阶段,即对抽象语法树(AST) 进行语义分析,编译器根据产生的抽象语法树生成class文件(字节码文件)。
大致了解了Java源码编译的过程之后,我们再来看一下Lombok是如何做的?
4.2 Lombok基本原理
Lombok的魔法就在于其修改了AST,分析和生成class阶段使用了修改后的AST,也就最终改变了生成的字节码文件。如,添加一个方法节点 ( Method Node )到AST,那么产生的class文件时将会包含新的方法。
通过修改AST,Lombok可以产生新的方法(如getter、setter等),或者注入代码到已存在的方法中去,比如 ( Lombok 提供的@Cleanup注解 -- 这个可以本文示例中找到 )。
Project Lombok使用了JSR 269 Pluggable Annotation Processing API ,
lombok.jar 包含一个名字为
/META-INF/services/javax.annotation.processing.Processor的文件。
当 javac 看到编译路径上的这个文件时,
会在编译期间使用定义在这个文件中的注解处理器。
https://www.jcp.org/en/jsr/detail?id=269
定义的注解处理器主要有两个AnnotationProcessor以及ClaimingProcessor。
AnnotationProcessor以及ClaimingProcessor在Lombok中的源代码如下:
package lombok.launch;
import java.lang.reflect.Field;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Completion;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import org.mapstruct.ap.spi.AstModifyingAnnotationProcessor;
import sun.misc.Unsafe;
class AnnotationProcessorHider {
//省略其它
public static class AnnotationProcessor extends AbstractProcessor {
private final AbstractProcessor instance = createWrappedInstance();
@Override public Set<String> getSupportedOptions() {
return instance.getSupportedOptions();
}
@Override public Set<String> getSupportedAnnotationTypes() {
return instance.getSupportedAnnotationTypes();
}
@Override public SourceVersion getSupportedSourceVersion() {
return instance.getSupportedSourceVersion();
}
@Override public void init(ProcessingEnvironment processingEnv) {
disableJava9SillyWarning();
AstModificationNotifierData.lombokInvoked = true;
instance.init(processingEnv);
super.init(processingEnv);
}
// sunapi suppresses javac's warning about using Unsafe; 'all' suppresses eclipse's warning about the unspecified 'sunapi' key. Leave them both.
// Yes, javac's definition of the word 'all' is quite contrary to what the dictionary says it means. 'all' does NOT include 'sunapi' according to javac.
@SuppressWarnings({"sunapi", "all"})
private void disableJava9SillyWarning() {
// JVM9 complains about using reflection to access packages from a module that aren't exported. This makes no sense; the whole point of reflection
// is to get past such issues. The only comment from the jigsaw team lead on this was some unspecified mumbling about security which makes no sense,
// as the SecurityManager is invoked to check such things. Therefore this warning is a bug, so we shall patch java to fix it.
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe u = (Unsafe) theUnsafe.get(null);
Class<?> cls = Class.forName("jdk.internal.module.IllegalAccessLogger");
Field logger = cls.getDeclaredField("logger");
u.putObjectVolatile(cls, u.staticFieldOffset(logger), null);
} catch (Throwable t) {
// We shall ignore it; the effect of this code failing is that the user gets to see a warning they remove with various --add-opens magic.
}
}
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return instance.process(annotations, roundEnv);
}
@Override public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
return instance.getCompletions(element, annotation, member, userText);
}
private static AbstractProcessor createWrappedInstance() {
ClassLoader cl = Main.getShadowClassLoader();
try {
Class<?> mc = cl.loadClass("lombok.core.AnnotationProcessor");
return (AbstractProcessor) mc.getDeclaredConstructor().newInstance();
} catch (Throwable t) {
if (t instanceof Error) throw (Error) t;
if (t instanceof RuntimeException) throw (RuntimeException) t;
throw new RuntimeException(t);
}
}
}
@SupportedAnnotationTypes("lombok.*")
public static class ClaimingProcessor extends AbstractProcessor {
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return true;
}
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
}
}
Project Lombok充当了一个注解处理器的角色。注解处理器扮演了一个分发器,其委托Lombok注解处理器来处理,Lombok注解处理器声明了具体要处理的注解。当委托给一个处理器时,Lombok注解处理器会通过注入新的节点(如,方法、表达式等)的方式去修改抽象语法树 (AST)。在注解处理阶段之后,编译器会根据修改后的AST,生成字节码。
Lombok在源码编译中,大致处理的过程如下图所示:
图片来源:http://notatube.blogspot.jp/2010/12/project-lombok-creating-custom.html
5、小结
本文先对Lombok进行了介绍,大致理解其是做什么用的。然后给出安装步骤,结合示例对多个注解进行简单说明,相信经过这些例子的说明,读者已经有了一个更好的体感,建议亲手操作一下,效果更佳;最后,对Lombok的原理进行了简单分析,可以更好地理解Lombok。
任何事物都具有两面性。尽管,Lombok能带给我们诸多方便,可以减少诸如Getter、 Setter代码,其中@Builder可以采用构建者模式的方式对变量进行赋值,直观方便;@CleanUp可以防止程序忘关闭流等等。但是,在某些场景,其也存在使用的限制,也存在一些缺点,请访问如下链接查看https://code.google.com/p/projectlombok/issues/list。
Lombok也可以在诸多互联网公司的开源代码中看到,大家可以根据自身的需要进行选择。
另外,友情提示一下:如果你的项目组是需要统计代码量,建议还是不要用了 你懂得