曾几何时,你是否反感于手写大量的Getter/Setter方法或者看到大量的Getter/Setter方法而眼花缭乱,今天介绍一款Java注解驱动的极简代码利器:Lombok,它通过在编译期修改抽象语法树(AST)生成字节码文件。
准备工作:
-
添加依赖
<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!");
}
注意:
-
只能在本地变量声明的时候使用,不可在类的字段上使用。
-
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语句之后进行的。
注意:
-
使用@NonNull修饰方法或者构造函数的参数,会在方法或构造函数的最开始地方插入一条语句:if (param == null) throw new NullPointerException("param")。
-
如果在构造函数中使用@NonNull,非空检测语句会紧随super或者this方法之后插入。
-
如果已经显式在方法或构造函数最开始地方进行非空检测,那么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.
注意:
-
所有日志注解统一使用log对象进行方法调用。
-
可以通过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);
}
}
注意:
-
JDK7及以上强烈建议使用try-with-resources方式关闭资源,JDK原生自动关闭资源方式。
-
通过添加try/finally语句块代码进行资源自动关闭,默认调用无参close方法。
-
通过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());
}
注意:
-
通过AccessLevel控制访问权限,默认Public访问权限。
-
设置AccessLevel访问权限为NONE,Lombok不生成对应Getter/Setter方法,可以根据业务重写Getter/Setter方法。
-
在类上使用@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);
}
注意:
-
@ToString注解默认会打印一个类似className(fieldName=fieldValue, fieldName=fieldValue)字符串。
-
@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));
}
注意:
-
对没有任何继承的类设置callSuper属性为true会导致编译错误。
-
final类和继承自Object的类不会生成canEquals方法。
-
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;
@NonNullprivate 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');}
注意:
-
@NoArgsConstructor为类自动生成一个无参构造函数。如果类含有final字段,会出现编译错误,通过指定属性force为true,为final字段进行初始化。
-
@RequiredArgsConstructor为类生成一个以所有final字段和@NonNull标注的字段为形参的构造函数。
-
@AllArgsConstructor为类生成一个全参构造函数。
-
三个注解都包含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());
}
注意:
-
不能通过@Data指定其他注解的一些个性化属性,需要单独显示单独指定。
-
默认生成Getter/Setter方法访问权限是public。
-
所有被transient修饰的变量不会参与equals和hashCode方法计算。
-
如果在类中已经显式创建了同名方法,Lombok不会再次生成这个方法。
-
@Data仅支持staticConstructor,该属性私有构造函数,对外暴露静态工厂方法创建对象。
-
可以通过为方法或构造函数添加@Tolerate注解让Lombok忽略显式添加的方法或构造函数,避免Lombok检测到同名方法或构造函数不自动生成的问题。
十:@Value
定义:@Data注解的不可变变体实现
注意:
-
@Value注解集final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter于一身。
-
类本身以及类中所有的字段都是private final类型的,不会生成Setter方法。
-
可以通过显式指定某个注解覆盖掉默认的属性。
-
可以通过为方法或构造函数添加@Tolerate注解让Lombok忽略显式添加的方法或构造函数,避免Lombok检测到同名方法或构造函数不自动生成的问题。
-
通过@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());
}
注意:
-
通过@Builder.Default为字段指定初始值。
-
通过为集合添加@Singular注解,可以增加对集合的add方法以及clear方法。
-
可以通过@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!");
}
}
注意:
-
仅仅用于静态或者实例方法。
-
在静态方法上使用@Synchronized,会锁在Lombok生成的名为$Lock的字段上,实例方法会锁在Lombok生成的名为$lock的字段上。
-
显式为注解指定名字(如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;
}
}
单元测试:
注意:
-
“懒加载”适用于占用大量CPU,内存的场景,在真正需要数据的时候才进行计算。
-
为@Getter添加lazy属性,从上图可以看出,在没有执行真正的getCached方法调用之前,example对象仅仅只是持有一个空的AtomicReference引用,并没有真正去计算,从而实现了我们所说的“懒加载”。
-
变量必须是private final的。
更多可以参考Lombok官方网站:
https://projectlombok.org/features/index.html
个人认为,源码就是最好的教材,Lombok每个注解中的注释十分详细,随用随查。
本文从定义,示例,注意三个方面介绍了Lombok的十几个注解,正如你所见到的这样,可以大大减少手写重复代码,使代码保持高度整洁,当然,任何事情都有两面性,它也降低了代码的可读性。