Lombok:极简代码利器

6,653 阅读10分钟
原文链接: mp.weixin.qq.com

    曾几何时,你是否反感于手写大量的Getter/Setter方法或者看到大量的Getter/Setter方法而眼花缭乱,今天介绍一款Java注解驱动的极简代码利器:Lombok,它通过在编译期修改抽象语法树(AST)生成字节码文件。

准备工作:

  1. 添加依赖

    <dependency>

            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
        </dependency>

    2.为IDE添加Lombok插件,否则使用Lombok注解在编译期会报错。

一:val

定义:一种本地变量的修饰符,通过定义进行类型判断。

示例:

@Test
public void testValExample() {
    val hello = "Hello World!";
    assertSame(hello.getClass(), String.class);
    assertEquals(hello, "Hello World!");
}

注意:

  1. 只能在本地变量声明的时候使用,不可在类的字段上使用。

  2. val修饰的变量本身是final类型的,不能被修改。

二:@NonNull

定义:修饰方法、构造函数的参数或者类字段,Lombok自动生成一个非空检测语句。

我们不支持不鼓励为方法传递一个null参数,建议使用JDK8的Optional或者Google Guava中的Optional来避免使用null。

示例:

public class Example {
    public Example(@NonNull String something) {
        System.out.println("do something in Example class : " + something);
    }
}


public class NonNullExample extends Example {
    public NonNullExample(@NonNull String something) {
        super(something);
        System.out.println("do something in NonNullExample : " + something);
    }
}


@Test
public void testNonNullExample() {
    try {
        NonNullExample nonNullExample = new NonNullExample("say hello");
        NonNullExample nullExample = new NonNullExample(null);
    }catch (NullPointerException e) {
        e.printStackTrace();
    }
}

单元测试结果:

do something in Example class : say hello

do something in NonNullExample : say hello

java.lang.NullPointerException: something

at com.iqiyi.mp.lombok.Example.<init>(Example.java:10)

at com.iqiyi.mp.lombok.NonNullExample.<init>(NonNullExample.java:11)

at com.iqiyi.mp.test.LombokTest.testNonNullExample(LombokTest.java:28)

可以看到当传入一个非空参数的时候,优先打印父类构造函数中的语句,再打印子类构造函数的语句,当传入一个空参数的时候,抛出NLP,通过异常栈顶信息可以判断NLP出现在Example父类中,由此,我们断定子类中Lombok进行的非空检测是在super语句之后进行的。

注意:

  1. 使用@NonNull修饰方法或者构造函数的参数,会在方法或构造函数的最开始地方插入一条语句:if (param == null) throw new NullPointerException("param")。

  2. 如果在构造函数中使用@NonNull,非空检测语句会紧随super或者this方法之后插入。

  3. 如果已经显式在方法或构造函数最开始地方进行非空检测,那么Lombok不会重复生成非空检测。

三:日志注解。

定义:通过使用@ComonsLog,@JbossLog,@Log,@Log4j,@Log4j2,@Slf4j,@XSlf4j决定你具体使用的日志框架,统一通过log对象进行调用。

示例:

@Slf4j(topic = "HelloLombok")
public class LogExample {
    public void printLog() {
        log.info("hello, lombok.");
    }
}

单元测试:

@Test
public void testLogExample() {
    LogExample logExample = new LogExample();
    logExample.printLog();
}

单元测试结果:

2017-03-26 22:41:11,374 [main] INFO  HelloLombok printLog 12 - hello, lombok.

注意:

  1. 所有日志注解统一使用log对象进行方法调用。

  2. 可以通过topic属性获取一个指定命名Logger对象,默认topic使用类名。

四:@Cleanup

定义:在本地变量添加@Cleanup注解,Lombok会自动关闭打开的资源。

示例:

@Test
public void testCleanupExample() throws IOException {
    @Cleanup
    InputStream in = new FileInputStream("fileIn.txt");
    @Cleanup
    OutputStream out = new FileOutputStream("fileOut.txt");
    byte[] b = new byte[10000];
    while (true) {
        int r = in.read(b);
        if (r == -1) break;
        out.write(b, 0, r);
    }
}

注意:

  1. JDK7及以上强烈建议使用try-with-resources方式关闭资源,JDK原生自动关闭资源方式。

  2. 通过添加try/finally语句块代码进行资源自动关闭,默认调用无参close方法。

  3. 通过value属性显式指定清理资源的方法,但是所有清理资源的方法都不能有任何参数,否则不会被调用。

五:@Getter和@Setter

定义:为字段或类添加@Getter和@Setter注解,Lombok自动生成Getter和Setter方法。

示例:

public class GetterSetterExample {
    @Getter
    @Setter
    private String name;

    @Getter(AccessLevel.PROTECTED)
    @Setter(AccessLevel.PRIVATE)
    private int age;
}

单元测试:

@Test
public void testGetterSetterExample() {
    val example = new GetterSetterExample();
    example.setName("DBQ");
    assertEquals("DBQ", example.getName());
}

注意:

  1. 通过AccessLevel控制访问权限,默认Public访问权限。

  2. 设置AccessLevel访问权限为NONE,Lombok不生成对应Getter/Setter方法,可以根据业务重写Getter/Setter方法。

  3. 在类上使用@Getter、@Setter注解,Lombok为该类下所有非static字段生成Getter/Setter方法。

六:@ToString

定义:为类添加@ToString注解,Lombok自动为该类生成toString方法。

示例:

@ToString(exclude = "name")

public class ToStringExample {
    private String name;
    private int age;
}

单元测试:

@Test
public void testToStringExample() {
    val example = new ToStringExample();
    String result = example.toString();
    assertEquals("ToStringExample(age=0)", result);
}

注意:

  1. @ToString注解默认会打印一个类似className(fieldName=fieldValue, fieldName=fieldValue)字符串。

  2. @ToString注解中的includeFieldNames属性,默认为true,显式输出字段名,exclude属性,显式指定要排除的字段,of属性,显示指定要输出的字段,callSuper属性,输出父类的toString方法,doNotUseGetters属性,默认为false,如果代码中有Getter方法,会优先使用Getter方法获取值。

七:@EqualsAndHashCode

定义:为类添加@EqualsAndHashCode注解,Lombok会为非static,非transient字段自动生成equals和hashCode方法。

示例:

@EqualsAndHashCode(exclude = "age")
@AllArgsConstructor
public class EqualsAndHashCodeExample{
    private String name;
    private int age;
}

单元测试:

@Test
public void testEqualsAndHashCodeExample() {
    val example = new EqualsAndHashCodeExample("DBQ", 25);
    val copyExample = new EqualsAndHashCodeExample("DBQ", 0);
    assertTrue(example.equals(copyExample));
}   

注意:

  1. 对没有任何继承的类设置callSuper属性为true会导致编译错误。

  2. final类和继承自Object的类不会生成canEquals方法。

  3. exclude属性排除某些字段参与equals和hashCode方法重写(如上,name相同即认为两个对象相同),callSuper属性指定是否调用父类equals和hashCode方法,默认为false。

八:@NoArgsConstructor, @RequiredArgsConstructor,@AllArgsConstructor

定义:Lombok为添加注解的类自动生成无参构造函数,指定参数构造函数或所有参数构造函数。

示例:

@NoArgsConstructor(force = true)
@RequiredArgsConstructor(staticName = "create")
@AllArgsConstructor(staticName = "of")
public class ConstructorExample {
    private final String name;
    @NonNull 

    private int age;
    private char gender;
}

单元测试:

@Test
public void testConstructorExample() {

    val noArgsExample = new ConstructorExample();
    val requiredArgsExample = ConstructorExample.create("DBQ", 25);
    val allArgsExample = ConstructorExample.of("DBQ", 20, 'M');

}

注意:

  1. @NoArgsConstructor为类自动生成一个无参构造函数。如果类含有final字段,会出现编译错误,通过指定属性force为true,为final字段进行初始化。

  2. @RequiredArgsConstructor为类生成一个以所有final字段和@NonNull标注的字段为形参的构造函数。

  3. @AllArgsConstructor为类生成一个全参构造函数。

  4. 三个注解都包含staticName属性,该属性会私有构造函数,并对外暴露一个引用了私有构造函数的静态工厂方法,access属性指定构造函数的访问权限,默认为public。

九:@Data

定义:将@ToString, @EqualsAndHashCode, @Getter / @Setter 和 @RequiredArgsConstructor这些注解捆绑在一起,通过@Data统一引入。

示例:

@Data(staticConstructor = "of")
public class DataExample {
    @NonNull
    private String name;
}

单元测试:

@Test
public void testDataExample() {
    DataExample example = DataExample.of("DBQ");
    assertEquals("DBQ", example.getName());
    example.setName("Code");
    assertEquals("Code", example.getName());
    assertEquals("DataExample(name=Code)", example.toString());
}

注意:

  1. 不能通过@Data指定其他注解的一些个性化属性,需要单独显示单独指定。

  2. 默认生成Getter/Setter方法访问权限是public。

  3. 所有被transient修饰的变量不会参与equals和hashCode方法计算。

  4. 如果在类中已经显式创建了同名方法,Lombok不会再次生成这个方法。

  5. @Data仅支持staticConstructor,该属性私有构造函数,对外暴露静态工厂方法创建对象。

  6. 可以通过为方法或构造函数添加@Tolerate注解让Lombok忽略显式添加的方法或构造函数,避免Lombok检测到同名方法或构造函数不自动生成的问题。

十:@Value

定义:@Data注解的不可变变体实现

注意:

  1. @Value注解集final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter于一身。

  2. 类本身以及类中所有的字段都是private final类型的,不会生成Setter方法。

  3. 可以通过显式指定某个注解覆盖掉默认的属性。

  4. 可以通过为方法或构造函数添加@Tolerate注解让Lombok忽略显式添加的方法或构造函数,避免Lombok检测到同名方法或构造函数不自动生成的问题。

  5. 通过@NonFinal注解修饰的字段,不是final类型的。

十一:@Builder

定义:一种链式创建对象的模式。

示例:

@Builder
@ToString
public class BuilderExample {
    @Builder.Default
    private String name = "DBQ";
    private int age;
    @Singular
    private List<String> hobbies;
}

单元测试:

@Test
public void testBuilderExample() {
    val hobbies = new ArrayList<String>();
    hobbies.add("ping-pong");
    hobbies.add("baseball");
    BuilderExample example = BuilderExample.builder()
            .age(20)
            .hobby("basketball")
            .hobby("football")
            .hobbies(hobbies)
            .build();
    assertEquals(
            "BuilderExample(name=DBQ, age=20, hobbies=[basketball, football, ping-pong, baseball])"
            , example.toString());
}

注意:

  1. 通过@Builder.Default为字段指定初始值。

  2. 通过为集合添加@Singular注解,可以增加对集合的add方法以及clear方法。

  3. 可以通过@Builder注解中的builderMethodName、buildMethodName和buildClassName个性化指定创建的类名和方法名。

十二:@SneakyThrows

定义:将受检异常转换为非受检异常,避免throws或try语句。

示例:

public class SneakyThrowsExample {
    @SneakyThrows
    public void nlp() {
        throw new NullPointerException();
    }
}

单元测试:

@Test
public void testSneakyThrowsExample() {
    new SneakyThrowsExample().nlp();
}

注意:

    这个特性有一定争议,使用的时候需要慎重思考,个人感觉这个功能比较鸡肋。

十三:@Synchronized

定义:synchronized修饰符的一种变体。

示例:

public class SynchronizedExample {
    private final Object readLock = new Object();
    @Synchronized
    public static void printName() {
        System.out.println("DBQ");
    }
    @Synchronized
    public int getAge() {
        return 25;
    }
    @Synchronized("readLock")
    public void sayHello() {
        System.out.println("Hello Lombok!");
    }
}

注意:

  1. 仅仅用于静态或者实例方法。

  2. 在静态方法上使用@Synchronized,会锁在Lombok生成的名为$Lock的字段上,实例方法会锁在Lombok生成的名为$lock的字段上。

  3. 显式为注解指定名字(如readLock)的时候,该字段一定要存在。

十四:@Getter(lazy=true)

定义:

示例:

public class GetterLazyExample {
    @Getter(lazy=true)
    private final double[] cached = expensive();
    private double[] expensive() {
        double[] result = new double[1000000];
        for (int i = 0; i < result.length; i++) {
            result[i] = Math.asin(i);
        }
        return result;
    }
}

单元测试:

注意:

  1. “懒加载”适用于占用大量CPU,内存的场景,在真正需要数据的时候才进行计算。

  2. 为@Getter添加lazy属性,从上图可以看出,在没有执行真正的getCached方法调用之前,example对象仅仅只是持有一个空的AtomicReference引用,并没有真正去计算,从而实现了我们所说的“懒加载”。

  3. 变量必须是private final的。

更多可以参考Lombok官方网站:

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

     个人认为,源码就是最好的教材,Lombok每个注解中的注释十分详细,随用随查。

    本文从定义,示例,注意三个方面介绍了Lombok的十几个注解,正如你所见到的这样,可以大大减少手写重复代码,使代码保持高度整洁,当然,任何事情都有两面性,它也降低了代码的可读性。