目录
一、基本介绍
二、Lombok使用说明
1. 基本介绍
2. 安装插件
IDEA在线安装Lombok插件
IDEA离线安装Lombok插件
3. 引入依赖坐标
4. Lombok注解功能说明
@NonNull
@Getter&@Setter
@Cleanup
@ToString
@EqualsAndHashCode
@Constructor
@RequiredArgsConstructor
方式一:注解注入
方式二:构造器注入
方式三:setter注入
方式四:@RequiredArgsConstructor
@Data
@SneakyThrows
@Synchronized
@Slf4j
@Builder
@Accessors
@Accessors(chain=true)
@Accessors(fluent = true)
@Accessors(prefix = "f")
5. Lombok原理分析
5.1 简介
5.2 两种解析
运行时解析
编译时解析
Annotation Processing Tool
Pluggable Annotation Processing API
6. 为什么不推荐使用Lombok
6.1 高侵入性,强迫队友
6.2 代码可调试性降低
6.3 影响版本升级
6.4 注解使用有风险
6.5 可能会破坏封装性
一、基本介绍
官网文档: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.
Project Lombok是一个java库,它可以自动插入到您的编辑器和构建工具中,为您的java增添趣味。
永远不要再写另一个getter或equals方法了,只要有一个注释,你的类就有了一个功能齐全的构建器,自动化你的日志变量,
等等。
Lombok简单理解就是一个Java类库,通过注解的形式帮助开发减少一些结构化代码的开发工作,提高开发效率,
比如通过@Data注解,class在编译的时候会自动生成get,set,equals,hash,toString等方法,
避免写大量的代码,减少了代码量,也使代码看起来更加简洁。
尤其是一些对象属性需要改动的时候,每次改动都需要重新生成get,set,equals,hash,toString等方法,
而使用注解则可以避免此问题。
二、Lombok使用说明
1. 基本介绍
如果要使用lombok,首先开发工具IntelliJ IDEA或者Eclipse需要先安装插件支持,其次需要引入依赖。
2. 安装插件
IDEA在线安装Lombok插件
File > Settings > Plugins >Marketplace,搜索Lombok,点击install,弹窗Accept,然后安装好后Restart IDEA。
IDEA离线安装Lombok插件
首先下载离线插件,这里要选择idea对应的版本,否则不兼容。
下载地址:Lombok - IntelliJ IDEs Plugin | Marketplace
3. 引入依赖坐标
Maven项目可以在pom.xml中配置依赖坐标即可。
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
注意:
provided表示该包只在编译和测试的时候用,项目真正打成包时不会将Lombok包打进去。
Lombok还支持其他构建方法,比如Ant、Gradle、Kobalt,有需要的可以参考官网的Install菜单下的Build Tools,
其他使用方法也可以参考Install菜单。
4. Lombok注解功能说明
官方API文档:https://projectlombok.org/api/
@NonNull
@NonNull该注解用在属性或构造器上
Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。
/**
* 1.@NonNull该注解用在属性或构造器上,Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。
* 参数User为null时产生异常:NullPointerException
*/
public static String getName(@NonNull User user) {
return user.getName();
}
/**
* 等价@NonNull
*/
public static String getName2(User user) {
if (user == null) {throw new NullPointerException("user is marked non-null but is null");}
return user.getName();
}
@Getter&@Setter
@Getter 和@Setter 注解在类或字段
注解在类时为所有字段生成getter,setter方法,注解在字段上时只为该字段生成getter,setter方法。
@ToString(exclude = {"phone"})
public class Student {
@Getter
@Setter
private String name;
/**
* 只生成set方法,且作用范围 修饰符PROTECTED
*/
@Setter(AccessLevel.PROTECTED)
private int age;
/**
* 只生成get方法,且作用范围 修饰符PUBLIC
*/
@Getter(AccessLevel.PUBLIC)
private String address;
@Getter
@Setter
private String phone;
}
@Cleanup
@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);
}
}
@ToString
@ToString 注解在类,添加toString方法
@ToString在JavaBean或类JavaBean中使用,使用此注解会自动重写对应的toStirng方法,
默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割,
通过callSuper参数来指定是否引用父类,includeFieldNames参数设为true,就能明确的输出toString()属性。
- @ToString(exclude=”column”)**意义:**排除column列所对应的元素,即在生成toString方法时不包含column参数;
- @ToString(exclude={“column1″,”column2″})**意义:**排除多个column列所对应的元素,其中间用英文状态下的逗号进行分割,即在生成toString方法时不包含多个column参数;
- @ToString(of=”column”)**意义:**只生成包含column列所对应的元素的参数的toString方法,即在生成toString方法时只包含column参数;;
- @ToString(of={“column1″,”column2”})**意义:**只生成包含多个column列所对应的元素的参数的toString方法,其中间用英文状态下的逗号进行分割,即在生成toString方法时只包含多个column参数;
@EqualsAndHashCode
@EqualsAndHashCode 注解在类,生成hashCode和equals方法
@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;
}
}
}
@Constructor
@NoArgsConstructor、@RequiredArgsConstructor和@AllArgsConstructor
这三个注解都是用在类上的,NoArgsConstructor 注解在类生成无参的构造方法。
@RequiredArgsConstructor 注解在类,为类中需要特殊处理的字段生成构造方法,比如final和被@NonNull注解的字段。
@AllArgsConstructor 注解在类,生成包含类中所有字段的构造方法。
三个注解都可以指定生成的构造方法的访问权限,
同时,第二个注解还可以用@RequiredArgsConstructor(staticName=”methodName”)的形式生成一个指定名称的静态方法,
返回一个调用相应的构造方法产生的对象。
@RequiredArgsConstructor(staticName = "myShape")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
public class Shape {
private int x;
@NonNull
private double y;
@NonNull
private String name;
}
@RequiredArgsConstructor
Lombok提供的 @RequiredArgsConstructor 方式(会生成一个包含常量,和标识了NotNull的变量的构造方法)
在springboot项目中,controller或service层中需要注入多个mapper接口或者另外的service接口,
这时候代码中就会有多个@AutoWired注解,使得代码看起来什么的混乱。
lombok提供了一个注解:
@RequiredArgsConstructor(onConstructor =@_(@Autowired))
写在类上面可以代替@AutoWired注解,需要注意的是:在注入的时候需要用final定义,或者使用@notnull注解
方式一:注解注入
Controller
public class FooController {
@Autowired
//@Inject
private FooService fooService;
//简单的使用例子,下同
public List<Foo> listFoo() {
return fooService.list();
}
}
方式二:构造器注入
Controller
public class FooController {
private final FooService fooService;
@Autowired
public FooController(FooService fooService) {
this.fooService = fooService;
}
//使用方式上同,略
}
方式三:setter注入
@Controller
public class FooController {
private FooService fooService;
//使用方式上同,略
@Autowired
public void setFooService(FooService fooService) {
this.fooService = fooService;
}
}
方式四:@RequiredArgsConstructor
@RequiredArgsConstructor
public class VerifyController {
private final VerifyService verifyService;
private final InvitationService invitationService;
private final VerificationCodeService verificationCodeService;
}
@Data
@Data 注解在类
生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
特别注意:Lombok的@Data注解生成的EqualsAndHashCode默认不支持父类在使用Lombok过程中,
如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。
举一个简单的例子:
我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。
但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,
会默认是@EqualsAndHashCode(callSuper=false),
这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,
无论父类属性访问权限是否开放,这就可能得到意想不到的结果。
以下是测试验证。
User,UserCustomer,UserEmployee
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
}
@Data
public class UserCustomer extends User {
private String customerId;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class UserEmployee extends User{
private String empId;
}
LombokDataTest
/**
* Lombok的@Data注解生成的EqualsAndHashCode默认不支持父类
* 在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。
* 举一个简单的例子:我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。
* 但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),
* 这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放,
* 这就可能得到意想不到的结果。
**/
public class LombokDataTest {
public static void main(String[] args) {
//1.@Data默认@EqualsAndHashCode(callSuper = false)
compareUserCustomerWithCallSuperFalse();
System.out.println("--------------------------------");
//2.@Data指定@EqualsAndHashCode(callSuper = true)
compareUserEmployeeWithCallSuperFalse();
}
/**
* 2.@Data指定@EqualsAndHashCode(callSuper = true)
*/
private static void compareUserEmployeeWithCallSuperFalse() {
UserEmployee userEmployee = new UserEmployee();
userEmployee.setName("jerry");
userEmployee.setEmpId("123");
UserEmployee userEmployee2 = new UserEmployee();
userEmployee2.setName("jerry");
userEmployee2.setEmpId("123456");
UserEmployee userEmployee3 = new UserEmployee();
userEmployee3.setName("jerry2");
userEmployee3.setEmpId("123");
//false,父类中的Name相同,子类中的EmpId不同,可以校验出来
System.out.println("【userEmployee】:" + userEmployee.toString());
System.out.println("【userEmployee2】:" + userEmployee2.toString());
System.out.println("【userEmployee3】:" + userEmployee3.toString());
System.out.println("【userEmployee & userEmployee2】:" + userEmployee.equals(userEmployee2));
//false,父类中的Name不同,子类中的EmpId相同,可以校验出来
System.out.println("【userEmployee & userEmployee3】:" + userEmployee.equals(userEmployee3));
}
/**
* 1.@Data默认@EqualsAndHashCode(callSuper = false)
*/
private static void compareUserCustomerWithCallSuperFalse() {
UserCustomer userCustomer = new UserCustomer();
userCustomer.setName("jerry");
userCustomer.setCustomerId("123");
UserCustomer userCustomer2 = new UserCustomer();
userCustomer2.setName("jerry");
userCustomer2.setCustomerId("123456");
UserCustomer userCustomer3 = new UserCustomer();
userCustomer3.setName("jerry2");
userCustomer3.setCustomerId("123");
System.out.println("【userCustomer】:" + userCustomer.toString());
System.out.println("【userCustomer2】:" + userCustomer2.toString());
System.out.println("【userCustomer3】:" + userCustomer3.toString());
//false,父类中的Name相同,子类中的customerId不同,可以校验出来
System.out.println("【userCustomer & userCustomer2】:" + userCustomer.equals(userCustomer2));
//true,父类中的Name不同,子类中的customerId相同,无法校验出来
System.out.println("【userCustomer & userCustomer3】:" + userCustomer.equals(userCustomer3));
}
}
@SneakyThrows
@SneakyThrows这个注解用在方法上
可以将方法中的代码用try-catch语句包裹起来,捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出,可以使用
public class SneakyThrows implements Runnable {
@SneakyThrows(UnsupportedEncodingException.class)
public String utf8ToString(byte[] bytes) {
return new String(bytes, "UTF-8");
}
@SneakyThrows
public void run() {
throw new Throwable();
}
}
@Synchronized
@Synchronized
这个注解用在类方法或者实例方法上,效果和synchronized关键字相同,区别在于锁对象不同,
对于类方法和实例方法,synchronized关键字的锁对象分别是类的class对象和this对象,
而@Synchronized得锁对象分别是私有静态final对象LOCK和私有final对象lock,
当然,也可以自己指定锁对象
@Slf4j
@Slf4j 注解在类
生成log变量,严格意义来说是常量。
private static final Logger log = LoggerFactory.getLogger(UserController.class)
@Builder
**建造者模式简介:**Builder 使用创建者模式又叫建造者模式。
简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但却可以精细地控制对象的构造过程。
注解类Builder.java
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
String builderMethodName() default "builder";
String buildMethodName() default "build";
String builderClassName() default "";
boolean toBuilder() default false;
AccessLevel access() default AccessLevel.PUBLIC;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Default {
}
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface ObtainVia {
String field() default "";
String method() default "";
boolean isStatic() default false;
}
}
在企业开发中,一般在领域对象实体上标注@Builder,
其作用就相当于@AllArgsConstructor(access = AccessLevel.PRIVATE),@Builder一般与@Getter结合使用。
编写测试实体类
import lombok.Builder;
import lombok.Getter;
@Builder
//@Getter
public class Person {
private String name;
private String id;
private String phoneNumeber;
}
编写测试类
public class Test {
public static void main(String[] args) {
Person.PersonBuilder builder = Person.builder();
builder.phoneNumeber("11111")
.id("1123")
.name("asdd").build();
System.out.println(builder);
}
}
编译并执行的结果为:
Person.PersonBuilder(name=asdd, id=1123, phoneNumeber=11111)
编译后的字节码分析:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class Person {
private String name;
private String id;
private String phoneNumeber;
Person(String name, String id, String phoneNumeber) {
this.name = name;
this.id = id;
this.phoneNumeber = phoneNumeber;
}
public static Person.PersonBuilder builder() {
return new Person.PersonBuilder();
}
public String getName() {
return this.name;
}
public String getId() {
return this.id;
}
public String getPhoneNumeber() {
return this.phoneNumeber;
}
public static class PersonBuilder {
private String name;
private String id;
private String phoneNumeber;
PersonBuilder() {
}
public Person.PersonBuilder name(String name) {
this.name = name;
return this;
}
public Person.PersonBuilder id(String id) {
this.id = id;
return this;
}
public Person.PersonBuilder phoneNumeber(String phoneNumeber) {
this.phoneNumeber = phoneNumeber;
return this;
}
public Person build() {
return new Person(this.name, this.id, this.phoneNumeber);
}
public String toString() {
return "Person.PersonBuilder(name=" + this.name + ", id=" + this.id + ", phoneNumeber=" + this.phoneNumeber + ")";
}
}
}
@Builder的作用
生成一个全属性的构造器
生成了一个返回静态内部类PersonBuilder对象的方法
生成了一个静态内部类PersonBuilder,这个静态内部类包含Person类的三个属性,无参构造器,
三个方法名为属性名的方法,返回Person对象的build方法,输出静态内部类三个属性的toString()方法。
建造者使用过程
Person.PersonBuilder builder = Person.builder();
builder.phoneNumeber("11111")
.id("1123")
.name("asdd").build();
System.out.println(builder);
先实例化内部类对象并返回,然后为调用内部类的方法为内部类的属性赋值,
build()方法就是将内部类PersonBuilder的属性值传入Person构造器中,实例化Person对象。
以上即为对于@Builder的简单使用。
@Accessors
@Accessors(chain=true)
链式访问,该注解设置chain=true,生成setter方法返回this(也就是返回的是对象),代替了默认的返回void。
package com.pollyduan;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain=true)
public class User {
private Integer id;
private String name;
private Integer age;
public static void main(String[] args) {
//开起chain=true后可以使用链式的set
User user=new User().setAge(31).setName("pollyduan");//返回对象
System.out.println(user);
}
}
@Accessors(fluent = true)
与chain=true类似,区别在于getter和setter不带set和get前缀。
package com.pollyduan;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(fluent=true)
public class User {
private Integer id;
private String name;
private Integer age;
public static void main(String[] args) {
//fluent=true开启后默认chain=true,故这里也可以使用链式set
User user=new User().age(31).name("pollyduan");//不需要写set
System.out.println(user);
}
}
@Accessors(prefix = "f")
set方法忽略指定的前缀。不推荐大神们这样去命名。
package com.pollyduan;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(prefix = "f")
public class User {
private String fName = "Hello, World!";
public static void main(String[] args) {
User user=new User();
user.setName("pollyduan");//注意方法名
System.out.println(user);
}
}
5. Lombok原理分析
5.1 简介
Lombok核心之处就是对于注解的解析上。
JDK5引入了注解的同时,也提供了两种解析方式。
5.2 两种解析
运行时解析
运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。
java.lang.reflect反射包中提供了一个接口AnnotatedElement,
该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,
对反射熟悉的朋友应该都会很熟悉这种解析方式。
编译时解析
编译时解析有两种机制,分别简单描述下:
Annotation Processing Tool
apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,
自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:
api都在com.sun.mirror非标准包下没有集成到javac中,需要额外运行
Pluggable Annotation Processing API
JSR 269自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,
javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,javac执行的过程如下:
就是一个实现了“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等等。
6. 为什么不推荐使用Lombok
Lombok的优点显而易见,可以帮助我们省去很多冗余代码,
实际上,从我个人角度来看,Java开发项目中,并不推荐使用Lombok,
下面我们来看一下为什么不推荐使用Lombok,它都有哪些缺点?
6.1 高侵入性,强迫队友
Lombok插件的使用,要求开发者一定要在IDE中安装对应的插件。
不仅自己要安装,任何和你协同开发的人都要安装。
如果有谁未安装插件的话,使用IDE打开一个基于Lombok的项目的话会提示找不到方法等错误,导致项目编译失败。
更重要的是,如果我们定义的一个jar包中使用了Lombok,
那么就要求所有依赖这个jar包的所有应用都必须安装插件,这种侵入性是很高的。
6.2 代码可调试性降低
Lombok确实可以帮忙减少很多代码,因为Lombok会帮忙自动生成很多代码。
但是,这些代码是要在编译阶段才会生成的,所以在开发的过程中,其实很多代码其实是缺失的。
这就给代码调试带来一定的问题,我们想要知道某个类中的某个属性的getter方法都被哪些类引用的话,就没那么简单了。
6.3 影响版本升级
Lombok对于代码有很强的侵入性,就可能带来一个比较大的问题,那就是会影响我们对JDK的升级。
按照如今JDK的升级频率,每半年都会推出一个新的版本,但是Lombok作为一个第三方工具,并且是由开源团队维护的,
那么他的迭代速度是无法保证的。
所以,如果我们需要升级到某个新版本的JDK的时候,若其中的特性在Lombok中不支持的话就会受到影响。
还有一个可能带来的问题,就是Lombok自身的升级也会受到限制。
因为一个应用可能依赖了多个jar包,而每个jar包可能又要依赖不同版本的Lombok,
这就导致在应用中需要做版本仲裁,而我们知道,jar包版本仲裁是没那么容易的,而且发生问题的概率也很高。
6.4 注解使用有风险
在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。
举一个简单的例子:
我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。
但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,
会默认是@EqualsAndHashCode(callSuper=false),
这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放,
这就可能得到意想不到的结果。
6.5 可能会破坏封装性
使用过程中如果不小心,在一定程度上就会破坏代码的封装性。
举个简单的例子:
我们定义一个购物车类,并且使用了@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,在使用过程中,需要多多小心。
Lombok虽好,但缺点也不少,如果你在公司团队开发中被强X了,你就只能使用,
如果新项目开发,能不用就尽量别用了,否则坑也不少的!