Lombok,简化代码的神器

  • 来源:开源中国

本文给大家介绍一个代码简化的神器 --  Lombok,主要包含Lombok安装Lombok的原理Lombok使用前和使用后代码示例等几方面来讲。

Lombok安装

Lombok官网,下载Lombok.jar

点击下载好的lombok.jar, 会出现如下的界面,然后选择Eclipse.exe的路径。

点击"Install / Update" 按钮,然后出现如下弹窗,点击“确定” ,安装完毕。

添加Lombok.jar到工程中去,如果Maven可以添加dependency,如:

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.16.12</version>
			<scope>provided</scope>
		</dependency>

然后,随便在工程建一个Book类,如:

import java.util.List;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
public class Book {

    private String name;

    private List<String> authors;

    public void print() {
        System.out.println("Hello world");
        log.info("info ==> {}", "hello world!!!!");
    }

}

Lombok分析

Java源码编译

在弄清Lombok是如何工作的之前,我们先来看一下OpenJDK上对Java源码编译过程的一个说明:

Java 源码编译一般可以分成三个不同的阶段:

  • 解析和输入
  • 注解处理
  • 语义分析和生成class文件

在解析和输入阶段,编译器会解析源文件到一个抽象语法树( Abstract Syntax Tree, 简称AST)。 如果语法不合法将会抛出错误。

注解处理阶段,自定义的注解处理器将会被调用,这可以被认为是预编译阶段。注解处理器可以做一些譬如验证类正确性、产生新的资源包含源文件等操作。

如果新的源码文件是注解处理的结果,那么编译循环回到解析和输入阶段重复这个过程,直到没有新的源文件生产为止。

在最后一个阶段,即对抽象语法树(AST) 进行语义分析,编译器根据产生的抽象语法树生成class文件(字节码文件)。

Lombok基本原理

大致了解了Java源码编译的过程之后,我们再来看一下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 看到编译路径上的这个文件时,会在编译期间使用定义在这个文件中的注解处理器。

定义的注解处理器主要有两个AnnotationProcessor以及ClaimingProcessor

AnnotationProcessor以及ClaimingProcessor在Lombok中的源代码如下:

package lombok.launch;

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;

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) {
			instance.init(processingEnv);
			super.init(processingEnv);
		}
		
		@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.createShadowClassLoader();
			try {
				Class<?> mc = cl.loadClass("lombok.core.AnnotationProcessor");
				return (AbstractProcessor) mc.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

Lombok实例

接下来,我们就一起来看看使用Lombok能带来什么神奇的效果吧~~ Here we GO~~

@Getter @Setter

为了测试Lombok@Getter和@Setter注解,先定义一个简单Book类,只包含书名、ISBN以及作者三个属性,并为这个三个属性添加Getter和Setter方法,如:

import java.io.Serializable;
import java.util.List;

/** * * @author wangmengjun * */
public class Book implements Serializable {

    private static final long serialVersionUID = 7793012904749570902L;

    /**书名*/
    private String name;

    /**ISBN*/
    private String isbn;

    /**作者*/
    private List<String> authors;


    /** * @return the name */
    public String getName() {
        return name;
    }

    /** * @param name the name to set */
    public void setName(String name) {
        this.name = name;
    }

    /** * @return the isbn */
    public String getIsbn() {
        return isbn;
    }

    /** * @param isbn the isbn to set */
    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    /** * @return the authors */
    public List<String> getAuthors() {
        return authors;
    }

    /** * @param authors the authors to set */
    public void setAuthors(List<String> authors) {
        this.authors = authors;
    }

}

使用Lombok的@Getter和@Setter注解,可以简化Book类,如:

import java.io.Serializable;
import java.util.List;

import lombok.Getter;
import lombok.Setter;

/** * * @author wangmengjun * */
@Getter
@Setter
public class Book implements Serializable {

    private static final long serialVersionUID = 7793012904749570902L;

    /**书名*/
    private String name;

    /**ISBN*/
    private String isbn;

    /**作者*/
    private List<String> authors;

}

从下图右上角的红框中可以看到,Book类中的三个属性都已经包含了Getter和Setter方法。

构造函数

还是以为上述Book类为例,假设Book类,需要包含两个构造函数,一个是没有参数的,另外一个则包含全部参数,如:

    
    public Book(){
    }
    
    /** * @param name * @param isbn * @param authors */
    public Book(String name, String isbn, List<String> authors) {
        super();
        this.name = name;
        this.isbn = isbn;
        this.authors = authors;
    }

使用Lombok之后,这样情况只要添加@AllArgsConstructor@NoArgsConstructor两个注解即可。

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BookLombok implements Serializable {

    private static final long serialVersionUID = 8654008145451673239L;

    /**书名*/
    private String name;

    /**ISBN*/
    private String isbn;

    /**作者*/
    private List<String> authors;

}

如果,Book类需要增加一个出版社press的属性,那么,全部参数的构造函数需要重新添加一个属性,如:

    public Book(){
    }

    /** * @param name * @param isbn * @param authors * @param press */
    public Book(String name, String isbn, List<String> authors, String press) {
        super();
        this.name = name;
        this.isbn = isbn;
        this.authors = authors;
        this.press = press;
    }

在这个时候,使用@AllArgsConstructor注解的BookLombok 类将不用再修改任何代码。

@ToString

使@ToString注解,可以为类添加toString方法,如:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

/** * @author wangmengjun * */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class BookLombok implements Serializable {

    private static final long serialVersionUID = 8654008145451673239L;

    /**书名*/
    private String name;

    /**ISBN*/
    private String isbn;

    /**作者*/
    private List<String> authors;

    /**出版社*/
    private String press;
}

测试一下,

public class Main {

    public static void main(String[] args) {
        BookLombok book = new BookLombok("软件开发之韵", "ISBN-1-2-3-4", Arrays.asList("Eric", "John"),
                "XX出版社");
        System.out.println(book);
    }
}

运行上述程序,控制台输出如下内容:

BookLombok(name=软件开发之韵, isbn=ISBN-1-2-3-4, authors=[Eric, John], press=XX出版社)

@EqualsAndHashCode

使用@EqualsAndHashCode注解,等价于根据类中定义的变量生成

 hashCode 和 equals 方法。

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class BookLombok implements Serializable {

    private static final long serialVersionUID = 8654008145451673239L;

    /**书名*/
    private String name;

    /**ISBN*/
    private String isbn;

    /**作者*/
    private List<String> authors;

    /**出版社*/
    private String press;
}

添加@EqualsAndHashCode注解之后,我们可以在右上角看到BookLombok类已经包含了

 hashCode 和 equals 方法。

@Data

使用@Data注解,其效果等价于:

All together now: A shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, and @Setter on all non-final fields, and @RequiredArgsConstructor!

  • 添加@ToString, @EqualsAndHashCode 和 @RequiredArgsConstructor
  • 所有变量增加@Getter
  • 所有非final类型的变量增加@Setter

日志记录

日志记录是一个非常重要的东西,来看一下一般我们使用SLF4J来做记录的示例:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** * @author wangmengjun * */
public class LogExample {

    private static final Logger log = LoggerFactory.getLogger(LogExample.class);

    public void greeting(String name) {
        System.out.println("Hello, name");
        log.debug("Method greeting, name is {}", name);
    }

}

测试一下

public class Main {

    public static void main(String[] args) {
        LogExample example = new LogExample();
        example.greeting("WMJ");
    }
}
Hello, name
11:32:20.982 [main] DEBUG test.LogExample - Method greeting, name is WMJ

在某个具体类中,如果想要使用日志记录信息,则都需要采用如下的方式,先定义一个Logger对象,如:

 private static final Logger log = LoggerFactory.getLogger(LogExample.class);

使用Lombok的日志注解,可以简单代码中对日志对象的定义。

同样使用SLF4J来做,修改后的LogExample类如下:

@Slf4j
public class LogExample {

    public void greeting(String name) {
        System.out.println("Hello, name");
        log.debug("Method greeting, name is {}", name);
    }

}

同样,我们测试一下

public class Main {

    public static void main(String[] args) {
        LogExample example = new LogExample();
        example.greeting("WMJ");
    }
}

控制台输出如下内容:

Hello, name
11:37:06.828 [main] DEBUG test.LogExample - Method greeting, name is WMJ

相当简单,方便。

其实,Lombok已经为我们提供了多种日志记录的选择,具体有如下几种:

@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);

@Cleanup

使用@CleanUp可以完成自动资源管理,如添加close()方法关闭资源。

参考官方示例如下:

https://projectlombok.org/features/Cleanup.html

等价于

Lombok还提供了很多其它的注解,在本篇文章中就不一一列举出来了。

有兴趣的读者可以访问如下网站获取更多的信息:

https://projectlombok.org/features/index.html

小结

本文主要对Lombok的安装、Lombok原理、使用Lombok的示例等进行了介绍。

Lombok能够简化诸如Getter/Setter , 日志对象创建等代码,使得代码更加的简洁。

因为,Lombok是通过修改抽象语法树,从而代码字节码修改的效果。

那么问题来了 -- 这种方式安全吗

看看Stackoverflow上,大家怎么说?

个人觉得使用日志注解等,还是挺不错的,后面用Lombok来完成一个小工程先玩玩,看看效果如何。

还不知道国内哪些公司或者项目已经在使用,效果如何?

如大家有相关的信息,也请反馈一下。

【参考】

[1]https://projectlombok.org

[2]http://stackoverflow.com/questions/3852091/is-it-safe-to-use-project-lombok?noredirect=1&lq=1

[3]http://notatube.blogspot.jp/2010/11/project-lombok-trick-explained.html

[4]http://notatube.blogspot.jp/2010/12/project-lombok-creating-custom.html

[5]http://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html