org.slf4j.Logger 和 org.slf4j.ext.XLogger 有什么区别-@lombok,lombok的@Data注解不想为某个字段生成getter/setter方法
在学习 lombok @Log (https://projectlombok.org/features/log) 时,发现有两个注解:@Slf4j, @XSlf4j, 这两个有什么区别呢?
org.slf4j.Logger 和 org.slf4j.ext.XLogger 都是 SLF4J (Simple Logging Facade for Java) 提供的接口,用于记录日志信息。不过,它们之间有一些显著的区别。
1. org.slf4j.Logger
org.slf4j.Logger 是 SLF4J 提供的核心接口之一,定义了最基本的日志记录方法。这些方法涵盖了不同的日志级别,包括 trace、debug、info、warn 和 error。
核心特性
-
基础日志记录功能:
Logger提供了一组基础的日志记录方法,用于输出不同级别的日志消息。例如:logger.info("This is an info message."); logger.error("This is an error message."); -
性能优化:SLF4J 的
Logger接口采用了惰性日志记录技术,减少了在不必要的情况下拼接字符串的开销。通过使用占位符{},你可以避免不必要的字符串拼接。例如:logger.debug("User {} logged in at {}", userName, loginTime);只有在
debug级别开启时,SLF4J 才会拼接这些字符串。 -
适配各种日志框架:SLF4J 是一个日志门面(Facade),它的
Logger接口可以适配多种日志实现(如 Logback、Log4j、Java Util Logging 等)。
2. org.slf4j.ext.XLogger
org.slf4j.ext.XLogger 是 SLF4J 提供的一个扩展接口,继承了 Logger 接口,并添加了更多的高级日志记录方法。
核心特性
-
高级日志记录方法:
XLogger提供了一些比Logger更加详细和高级的日志记录方法。这些方法允许记录日志时附加更多的信息,比如方法名、类名、行号等。 -
扩展日志级别方法:除了标准的日志级别方法(
trace、debug、info、warn和error),XLogger还增加了细化的日志记录方法:entry(Object... arguments)和exit(Object result):用于记录方法的进入和退出,这对于调试和跟踪代码执行路径非常有用。throwing(Throwable t):用于记录异常信息以及抛出异常的情况。catching(Throwable t):用于记录捕获异常的情况。
-
便捷性:通过这些扩展方法,
XLogger提供了更高的日志记录便捷性,尤其是在需要详细记录代码执行路径或异常的场景下。
示例用法
下面是使用 XLogger 进行更详细日志记录的示例:
import org.slf4j.LoggerFactory;
import org.slf4j.ext.XLogger;
import org.slf4j.ext.XLoggerFactory;
public class Example {
private static final XLogger logger = XLoggerFactory.getXLogger(Example.class);
public void exampleMethod() {
logger.entry(); // 记录方法的进入
try {
// 你的业务逻辑
} catch (Exception e) {
logger.catching(e); // 记录捕获异常
}
logger.exit(); // 记录方法的退出
}
}
在这个示例中,entry 和 exit 方法用于记录方法的进入和退出点,而 catching 方法用于记录捕获的异常。
总结
Logger:适用于大多数常见的日志记录需求,提供了基础的日志级别方法,并支持占位符和惰性日志记录的优化。XLogger:适用于需要更高级的日志记录功能的场景,例如记录方法的进入/退出、异常处理等,更适合调试和跟踪复杂的应用程序流程。
根据你的日志记录需求,可以选择使用 Logger 或 XLogger。如果你只需要基本的日志记录功能,Logger 是足够的;如果你需要更详细的日志信息和控制,XLogger 是一个更好的选择。
// https://mvnrepository.com/artifact/org.slf4j/slf4j-ext
implementation 'org.slf4j:slf4j-ext:2.0.16'
- @Getter 注解在属性(类)上,为属性(所有非静态成员变量)提供 get() 方法
- @Setter 注解在属性(类)上,为属性(所有非静态成员变量)提供 set() 方法
- @ToString 该注解的作用是为类自动生成 toString() 方法
- @EqualsAndHashCode 为对象字段自动生成 hashCode() 和 equals() 实现
- @AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor 顾名思义,为类自动生成对应参数的构造器
- @Data 注解在类上,自动为所有字段添加 @ToString、@EqualsAndHashCode、@Getter。为非 final 字段添加 @Setter 和 @RequiredArgsConstructor。本质上相当于几个注解的综合效果
- @Value 注解和 @Data 类似,区别在于它会把所有成员变量默认定义为 private final 修饰,并且不会生成 set() 方法
- @Log、@Log4j、@Log4j2、@Slf4j、@XSlf4j、@CommonsLog、@JBossLog 注解在类上,自动为类添加对应的日志支持
- @NonNull 注解在方法参数上,用于自动生成空值参数检查,自动帮助我们避免空指针
- @Cleanup 自动帮我们调用 close() 方法,作用在局部变量上,在作用域结束时会自动调用 close() 方法释放资源,可以关闭流
- @Builder 注解在类上,被注解的类加个构造者模式
- @Synchronized 注解在方法上,加个同步锁
- @SneakyThrows 注解在方法上,等同于 try/catch 捕获异常
- 引用来源:SpringBoot - Lombok使用详解1(基本介绍、安装配置、var和val) - SeasonHur - 博客园 (cnblogs.com)

@Getter、@Setter、@ToString、@EqualsAndHashCode
在类或属性上添加@Getter和@Setter注解,可以省略不写属性(非静态成员变量)的get()和set()方法,lombok会自动生成对应方法。
在类上添加@ToString和@EqualsAndHashCode注解,会覆盖toString()方法、equals()和hashCode()方法。
-
-
-
-
-
public class User1 {
-
private String name;
-
private int age;
-
}
-
public class User1 {
-
-
private String name;
-
-
private int age;
-
}

@AllArgsConstructor、@NoArgsConstructor
在类上添加@AllArgsConstructor和@NoArgsConstructor注解,可以省略不写属性的空参构造和全参构造方法,lombok会自动生成对应方法。
-
-
-
public class User2 {
-
private String name;
-
private int age;
-
}

@RequiredArgsConstructor
在类上添加@RequiredArgsConstructor注解,lombok会生成带参构造方法,其中参数为对象被final关键字修饰且没有默认值的属性。
-
-
public class User3 {
-
private String name;
-
private final int height;
-
private final int age = 18;
-
}

@Data
在类上添加@Data注解,功能同@Getter、@Setter、@ToString、@EqualsAndHashCode和@RequiredArgsConstructor。
被final关键字修饰的属性不具有set()方法。
-
-
public class User4 {
-
private String name;
-
private final int height;
-
private final int age = 18;
-
}

@Builder
在类上添加@Data注解,提供构造者模式。
-
-
public class User5 {
-
private String name;
-
private final int height;
-
private final int age = 18;
-
}

-
void test() {
-
User5Builder builder = User5.builder().name("张三").height(169);
-
User5 user5 = builder.build();
-
}
@Value
在类上添加@Value注解,功能与@Data类似,它会把所有成员变量默认定义为 private final 修饰,并且不会生成 set() 方法。
-
-
public class User6 {
-
public String name;
-
public static int height;
-
public int age = 18;
-
}

@NonNull
在参数上添加@NonNull注解,自动执行非空判断。
-
-
public class User7 {
-
private String name;
-
-
public static void main(String[] args) {
-
User7 user7 = new User7();
-
user7.test(user7.name);
-
}
-
-
void test( String i) {
-
System.out.println(i);
-
}
-
}

@Cleanup
在局部变量上添加@Cleanup注解,自动调用close()方法,可以用于自动关闭流。
-
void test() throws FileNotFoundException {
-
-
FileInputStream fileInputStream = new FileInputStream("test.txt");
-
}
@SneakyThrows
在方法上添加@SneakyThrows注解,等同于try/catch捕获异常。
-
-
void test2() {
-
FileInputStream fileInputStream = new FileInputStream("test.txt");
-
}
@Synchronized
在方法上添加@Synchronized注解,功能等于加同步锁。
-
-
void test3() {
-
System.out.println("Synchronized");
-
}
@Log、@Log4j、@Log4j2、@Slf4j、@XSlf4j、@CommonsLog、@JBossLog
在方法上添加以上注解,自动添加对应的日志功能。
-
-
-
-
-
-
-
-
public class User9 {
-
public static void main(String[] args) {
-
log.info("log");
-
}
-
}
DDR(Double Date Rate)→ DDR(Double Data Rate)
lombok为我们提供了@Data注解,帮助我们省略了@Setter,@Getter,@ToString等注解,一般对于普通的实体类使用该注解,不会出现什么问题,但是当我们把这个注解,使用在派生类上,就出现了一个小问题。
基类:

派生类:

@Data注解的地方会出现警告:

出现的警告信息:
-
Generating equals/hashCode implementation but without a call to superclass, even though this class
-
does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)'
-
to your type.
大致意思是默认子类的equals和hashCode方法,不会包含或者考虑基类的属性。我们可以通过反编译工具查看项目target/classes目录下的User.class的hashCode方法,默认情况下属性都是使用的他自身的属性。
-
public int hashCode()
-
{
-
int PRIME = 59;
-
int result = 1;
-
Object $username = getUsername();
-
result = result * 59 + ($username == null ? 43 : $username.hashCode());
-
Object $password = getPassword();
-
result = result * 59 + ($password == null ? 43 : $password.hashCode());
-
Object $mobile = getMobile();
-
result = result * 59 + ($mobile == null ? 43 : $mobile.hashCode());
-
return result;
-
}
当我们根据警告提示,加上注解@EqualsAndHashCode(callSuper=true) ,警告消失。

这时候,我们再来看我们的User.class中的hashCode方法:
-
public int hashCode()
-
{
-
int PRIME = 59;
-
int result = super.hashCode();
-
Object $username = getUsername();
-
result = result * 59 + ($username == null ? 43 : $username.hashCode());
-
Object $password = getPassword();
-
result = result * 59 + ($password == null ? 43 : $password.hashCode());
-
Object $mobile = getMobile();
-
result = result * 59 + ($mobile == null ? 43 : $mobile.hashCode());
-
return result;
-
}
可以看出代码中不一样的地方,默认情况下是int result=1,当添加注解@EqualsAndHashCode(callSuper=true)时,变成了int result=super.hashCode()。
这么一来,好像就解决了在继承情况下使用@Data注解的警告问题。但是问题是,每一个继承的类,都需要这么来解决,也不是很方便。在stackoverflow上,就有人提出了这个问题:https://stackoverflow.com/questions/38572566/warning-equals-hashcode-on-data-annotation-lombok-with-inheritance,另外,lombok作者Roel也给出了解决办法,就是通过自定义lombok.config文件来解决。
按照Roel的说法,lombok.config文件需要放在src/main/java文件夹下的目录中(也可以放在实体同级目录下),放在src/main/resources目录下,不会生效。下面,我们通过这种方式来解决这个警告的问题。
1、新建lombok.config文件,然后配置:
-
config.stopBubbling=true
-
lombok.equalsAndHashCode.callSuper=call
2、pom.xml文件中需要加入如下插件:
-
<plugin>
-
<groupId>org.apache.maven.plugins</groupId>
-
<artifactId>maven-compiler-plugin</artifactId>
-
<configuration>
-
<source>1.8</source>
-
<target>1.8</target>
-
</configuration>
-
</plugin>
然后,我们可以检验一下,警告是否就没有了,这里给出一个动态截图:

可以看到,配置生效了,然后@Data注解这里的警告也立马消失了。
Gradle下 的依赖(kts)
dependencies {
//*Lombok*
// <p>注解处理将不再compile classpath中,需要手动添加到annotation processor path</p>
compileOnly("org.projectlombok:lombok:1.18.4")
annotationProcessor("org.projectlombok:lombok:1.18.4")
testCompileOnly("org.projectlombok:lombok:1.18.4")
testAnnotationProcessor("org.projectlombok:lombok:1.18.4")
}
先介绍这一波最常用的注解:
@NoArgsConstructor/@RequiredArgsConstructor/@AllArgsConstructor
这三个注解都是用在类上的,第一个和第三个都很好理解,就是为该类产生无参的构造方法和包含所有参数的构造方法,第二个注解则使用类中所有带有@NonNull注解的或者带有final修饰的成员变量生成对应的构造方法,当然,和前面几个注解一样,成员变量都是非静态的。
另外,如果类中含有final修饰的成员变量,是无法使用@NoArgsConstructor注解的。
三个注解都可以指定生成的构造方法的访问权限,还可指定生成一个静态方法
@Builder
@Builder提供了一种比较推崇的构建值对象的方式
非常推荐的一种构建值对象的方式。缺点就是父类的属性不能产于builder
@Cleanup
@Cleanup能够自动释放资源
这个注解用在变量前面,可以保证此变量代表的资源会被自动关闭,默认是调用资源的close()方法。如果该资源有其它关闭方法,可使用@Cleanup(“methodName”)来指定要调用的方法,就用输入输出流来举个例子吧:
public static void main(String[] args) throws Exception {
@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);
}
}
编译后:
public static void main(String[] args) throws Exception {
FileInputStream in = new FileInputStream(args[0]);
try {
FileOutputStream out = new FileOutputStream(args[1]);
try {
byte[] b = new byte[1024];
while(true) {
int r = in.read(b);
if (r == -1) {
return;
}
out.write(b, 0, r);
}
} finally {
if (Collections.singletonList(out).get(0) != null) {
out.close();
}
}
} finally {
if (Collections.singletonList(in).get(0) != null) {
in.close();
}
}
}
就这么简单的一个注解,就实现了优雅的关流操作哟。
@Value
@Value注解和@Data类似,区别在于它会把所有成员变量默认定义为private final修饰,并且不会生成set方法。
所以@Value更适合只读性更强的类,所以特殊情况下,还是可以使用的。
@ToString/@EqualsAndHashCode
这两个注解也比较好理解,就是生成toString,equals和hashcode方法,同时后者还会生成一个canEqual方法,用于判断某个对象是否是当前类的实例。,生成方法时只会使用类中的非静态成员变量,这些都比较好理解。毕竟静态的东西并不属于对象本身
@ToString
public class Demo {
private final int finalVal = 10;
private transient String name = "aa";
private int age;
}
public static void main(String[] args) throws Exception {
Demo demo = new Demo();
System.out.println(demo); //Demo(finalVal=10, age=0)
}
我们发现静态字段它是不输出的。
有些关键的属性,可以控制toString的输出,我们可以了解一下:
@ToString(
includeFieldNames = true, //是否使用字段名
exclude = {"name"}, //排除某些字段
of = {"age"}, //只使用某些字段
callSuper = true //是否让父类字段也参与 默认false
)
备注:大多数情况下,使用默认的即可,毕竟大多数情况都是POJO
@Generated:暂时貌似没什么用@Getter/@Setter
这一对注解从名字上就很好理解,用在成员变量上面或者类上面,相当于为成员变量生成对应的
get和set方法,同时还可以为生成的方法指定访问修饰符,当然,默认为public这两个注解直接用在类上,可以为此类里的所有非静态成员变量生成对应的get和set方法。如果是final变量,那就只会有get方法
@NonNull
这个注解可以用在成员方法或者构造方法的参数前面,会自动产生一个关于此参数的非空检查,如果参数为空,则抛出一个空指针异常。
//成员方法参数加上@NonNull注解
public String getName(@NonNull Person p){
return p.getName();
}
编译后:
public String getName(@NonNull Person p){
if(p==null){
throw new NullPointerException("person");
}
return p.getName();
}
@Singular 默认值 暂时也没太大用处
@SneakyThrows
这个注解用在方法上,可以将方法中的代码用try-catch语句包裹起来,捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出,可以使用@SneakyThrows(Exception.class)的形式指定抛出哪种异常
@SneakyThrows(UnsupportedEncodingException.class)
public String utf8ToString(byte[] bytes) {
return new String(bytes, "UTF-8");
}
编译后:
@SneakyThrows(UnsupportedEncodingException.class)
public String utf8ToString(byte[] bytes) {
try{
return new String(bytes, "UTF-8");
}catch(UnsupportedEncodingException uee){
throw Lombok.sneakyThrow(uee);
}
}
这里有必要贴出来Lombok.sneakyThrow的代码:
public static RuntimeException sneakyThrow(Throwable t) {
if (t == null) {
throw new NullPointerException("t");
} else {
return (RuntimeException)sneakyThrow0(t);
}
}
private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T {
throw t;
}
其实就是转化为了RuntimeException,其实我想说,这个注解也没大用。毕竟我们碰到异常,是希望自己处理的
@Synchronized
这个注解用在类方法或者实例方法上,效果和synchronized关键字相同,区别在于锁对象不同,对于类方法和实例方法,synchronized关键字的锁对象分别是类的class对象和this对象,而@Synchronized得锁对象分别是私有静态final对象LOCK和私有final对象lock`,当然,也可以自己指定锁对象
@Synchronized
public static void hello() {
System.out.println("world");
}
@Synchronized
public int answerToLife() {
return 42;
}
@Synchronized("readLock")
public void foo() {
System.out.println("bar");
}
编译后:
public static void hello() {
Object var0 = $LOCK;
synchronized($LOCK) {
System.out.println("world");
}
}
public int answerToLife() {
Object var1 = this.$lock;
synchronized(this.$lock) {
return 42;
}
}
public void foo() {
Object var1 = this.readLock;
synchronized(this.readLock) {
System.out.println("bar");
}
}
我只能说,这个注解也挺鸡肋的。
@Val
@Val 很强的类型推断 var注解,在Java10之后就不能使用了
class Parent {
//private static final val set = new HashSet<String>(); //编译不通过
public static void main(String[] args) {
val set = new HashSet<String>();
set.add("aa");
System.out.println(set); //[aa]
}
}
编译后:
class Parent {
Parent() {
}
public static void main(String[] args) {
HashSet<String> set = new HashSet();
set.add("aa");
System.out.println(set);
}
}
这个和Java10里的Var很像,强大的类型推断。并且不能使用在全局变量上,只能使用在局部变量的定义中。
@Log、CommonsLog、Slf4j、XSlf4j、Log4j、Log4j2等日志注解
这个注解用在类上,可以省去从日志工厂生成日志对象这一步,直接进行日志记录,具体注解根据日志工具的不同而不同,同时,可以在注解中使用topic来指定生成log对象时的类名。不同的日志注解总结如下(上面是注解,下面是实际作用):
@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);
这个注解还是非常有用的,特别是Slf4j这个,在平时开发中挺有用的
@Slf4j
class Parent {
}
编译后:
class Parent {
private static final Logger log = LoggerFactory.getLogger(Parent.class);
Parent() {
}
}
也可topic的名称:
@Slf4j
@CommonsLog(topic = "commonLog")
class Parent {
}
编译后:
class Parent {
private static final Logger log = LoggerFactory.getLogger("commonLog");
Parent() {
}
}
lombok中有experimental的包:
实验性因为:
我们可能想将这些特性和更完全的性质支持概念融为一体(普通话:这些性能还在研究)
新特性-需要社区反馈
@Accessors
@Accessors 一个为getter和setter设计的更流畅的注解
这个注解要搭配@Getter与@Setter使用,用来修改默认的setter与getter方法的形式。所以单独使用是没有意义的
@Accessors(fluent = true)
@Getter
@Setter
public class Demo extends Parent {
private final int finalVal = 10;
private String name;
private int age;
}
编译后:
public class Demo extends Parent {
private final int finalVal = 10;
private String name;
private int age;
public Demo() {
}
public int finalVal() {
Objects.requireNonNull(this);
return 10;
}
public String name() {
return this.name;
}
public int age() {
return this.age;
}
public Demo name(String name) {
this.name = name;
return this;
}
public Demo age(int age) {
this.age = age;
return this;
}
}
它的三个参数解释:
chain链式的形式 这个特别好用,方法连缀越来越方便了fluent流式的形式(若无显示指定chain的值,也会把chain设置为true)prefix生成指定前缀的属性的getter与setter方法,并且生成的getter与setter方法时会去除前缀
@Accessors(prefix = "xxx")
@Getter
@Setter
public class Demo extends Parent {
private final int finalVal = 10;
private String xxxName;
private int age;
}
编译后:
public class Demo extends Parent {
private final int finalVal = 10;
private String xxxName;
private int age;
public Demo() {
}
public String getName() {
return this.xxxName;
}
public void setName(String xxxName) {
this.xxxName = xxxName;
}
}
我们发现prefix可以在生成get/set的时候,去掉xxx等prefix前缀,达到很好的一致性。但是,但是需要注意,因为此处age没有匹配上xxx前缀,所有根本就不给生成,所以使用的时候一定要注意。
属性名没有一个以其中的一个前缀开头,则属性会被lombok完全忽略掉,并且会产生一个警告。
@Delegate
@Delegate注释的属性,会把这个属性对象的公有非静态方法合到当前类
代理模式,把字段的方法代理给类,默认代理所有方法。注意:公共 非静态方法
public class Demo extends Parent {
private final int finalVal = 10;
@Delegate
private String xxxName;
private int age;
}
编译后:把String类的公共 非静态方法全拿来了 个人觉得很鸡肋有木有
public class Demo extends Parent {
private final int finalVal = 10;
private String xxxName;
private int age;
public Demo() {
}
public int length() {
return this.xxxName.length();
}
public boolean isEmpty() {
return this.xxxName.isEmpty();
}
public char charAt(int index) {
return this.xxxName.charAt(index);
}
public int codePointAt(int index) {
return this.xxxName.codePointAt(index);
}
备注:它不能用于基本数据类型字段比如int,只能用在包装类型比如Integer
参数们:
- types:指定代理的方法
- excludes:和types相反
@NonFinal 设置不为Final,@FieldDefaults和@Value也有这功能
@SuperBuilder 本以为它是支持到了父类属性的builder构建,但其实,我们还是等等吧 目前还不好使
@UtilityClass 工具类 会把所有字段方法static掉,没啥用
@Wither 生成withXXX方法,返回类实例 没啥用,因为还有bug
@Builder和@NoArgsConstructor一起使用冲突问题
当我们这么使用时候:
编译报错:
Error:(17, 1) java: 无法将类 com.sayabc.groupclass.dtos.appoint.TeaPoolLogicalDelDto中的构造器 TeaPoolLogicalDelDto应用到给定类型;
需要: 没有参数
找到: java.lang.Long,java.lang.Long,java.lang.Long,java.lang.Integer
原因: 实际参数列表和形式参数列表长度不同
其实原因很简单,自己点进去看编译后的源码一看便知。
只使用@Builder会自动创建全参构造器。而添加上@NoArgsConstructor后就不会自动产生全参构造器
两种解决方式:
- 去掉@NoArgsConstructor
- 添加@AllArgsConstructor(建议使用这种,毕竟无参构造最好保证是有的)
but,枚举值建议这样来就行了,不要加@NoArgsConstructor
我认为这也是Lombok的一个bug,希望在后续版本中能够修复
@builder注解影响设置默认值的问题
例子如下,本来我是想给age字段直接赋一个默认值的:
没有使用lombok,我们这么写:
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(demo); //Demo{id=null, age=10}
}
private static class Demo {
private Integer id;
private Integer age = 10; //放置默认值年龄
//省略手动书写的get、set、方法和toString方法
@Override
public String toString() {
return "Demo{" +
"id=" + id +
", age=" + age +
'}';
}
}
我们发现,这样运行没有问题,默认值也生效了。但是,但是我们用了强大的lombok,我们怎么可能还愿意手写get/set呢?关键是,我们一般情况下还会用到它的@buider注解:
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(demo); //Demo{id=null, age=10}
//采用builder构建 这是我们使用最多的场景吧
Demo demo2 = Demo.builder().build();
System.out.println(demo2); //PeriodAddReq.Demo(id=null, age=null)
}
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
private static class Demo {
private Integer id;
private Integer age = 10; //放置默认值年龄
}
代码简洁了不少。但是我们却发现一个问题。new出来的对象默认值仍然没有问题,但是buider构建出来的demo2对象,默认值却没有设置进去。这是一个非常隐晦的问题,一不小心,就可能留下一个惊天大坑,所以需要注意
其实在执行编译的时候,idea开发工具已经警告我们了:
Warning:(51, 25) java: @Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default. If it is not supposed to be settable during building, make the field final.
方案一:
从它的建议可以看出,把字段标为final就ok了(亲测好用)。但很显然,绝大多数我们并不希望他是final的字段。
因此我们采用第二个方案:
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
private static class Demo {
private Integer id;
@Builder.Default
private Integer age = 10; //放置默认值年龄
}
lombok考虑到了这种现象,因此我们只需要在需要设置默认值的字段上面加上 @Builder.Default注解就ok了。
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(demo); //PeriodAddReq.Demo(id=null, age=null)
//采用builder构建 这是我们使用最多的场景吧
Demo demo2 = Demo.builder().build();
System.out.println(demo2); //PeriodAddReq.Demo(id=null, age=10)
}
但是我们坑爹的发现:builder默认值没问题了,但是new出来又有问题了。见鬼啊,
我认为这是lombok的一个大bug,希望后续版本中能够修复
但是我们不能因为有这么一个问题,咱们就不使用它了。本文主要提醒读者,在使用的时候留心这个问题即可。
备注:@Builder.Default会使得使用@NoArgsConstructor生成的无参构造没有默认值,自己显示写出来的也不会给你设置默认值的,需要注意。
2019年1.18日补充内容:Lombok 1.18.4版本
上面已经指出了Lombok设置默认值的bug,果不其然。官方在1.18.4这个版本修复了这个bug。各位要有版本意识:这个版本级以上版本是好用的,比这版本低的都不行。
用这个版本运行上面例子,默认值没有问题了。
Main.Demo(id=null, age=10)
Main.Demo(id=null, age=10)
我们不用自动生成空构造,显示书写出来呢?如下:
@Getter
@Setter
@Builder
@AllArgsConstructor
@ToString
private static class Demo {
private Integer id;
@Builder.Default
private Integer age = 10; //放置默认值年龄Default
//显示书写出空构造
public Demo() {
}
}
我们发现手动书写出来的空构造,默认值是不生效的。这点需要特别注意。
这个就不说是Lombok的bug了,因为既然你都使用Lombok了,为何还自己写空构造呢?不是作死吗?
Lombok背后的自定义注解原理
作为一个Java开发者来说光了解插件或者技术框架的用法只是做到了“知其然而不知其所以然”,如果真正掌握其背后的技术原理,看明白源码设计理念才能真正做到“知其然知其所以然”。好了,话不多说下面进入本章节的正题,看下Lombok背后注解的深入原理。
可能熟悉Java自定义注解的同学已经猜到,Lombok这款插件正是依靠可插件化的Java自定义注解处理API(JSR 269: Pluggable Annotation Processing API)来实现在Javac编译阶段利用“Annotation Processor”对自定义的注解进行预处理后生成真正在JVM上面执行的“Class文件”。有兴趣的同学反编译带有Lombok注解的类文件也就一目了然了。其大致执行原理图如下:
从上面的Lombok执行的流程图中可以看出,在Javac 解析成AST抽象语法树之后, Lombok 根据自己编写的注解处理器,动态地修改 AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而JDK的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些但是带来的性能损耗更加大。
需要更加深入理解Lombok插件的细节,自己查阅其源代码是必比可少的。
AnnotationProcessor这个类是Lombok自定义注解处理的入口。该类有两个比较重要的方法一个是init方法,另外一个是process方法。在init方法中,先用来做参数的初始化,将AnnotationProcessor类中定义的内部类(JavacDescriptor、EcjDescriptor)先注册到ProcessorDescriptor类型定义的列表中。其中,内部静态类—JavacDescriptor在其加载的时候就将 lombok.javac.apt.LombokProcessor这个类进行对象实例化并注册。在 LombokProcessor处理器中,其中的process方法会根据优先级来分别运行相应的handler处理类。Lombok中的多个自定义注解都分别有对应的handler处理类.
在Lombok中对于其自定义注解进行实际的替换、修改和处理的正是这些handler类。对于其实现的细节可以具体参考其中的代码。

浙公网安备 33010602011771号