Java开源工具库使用之Lombok

文章目录

  • 前言
  • 一、常用注解
    • 1.1 @AllArgsConstructor/@NoArgsConstructor/@RequiredArgsConstructor
    • 1.2 @Builder
    • 1.3 @Data
    • 1.4 @EqualsAndHashCode
    • 1.5 @Getter/@Setter
    • 1.6 @Slf4j/@Log4j/@Log4j2/@Log
    • 1.7 @ToString
  • 二、踩坑
    • 2.1 Getter/Setter 方法名不一样
    • 2.2 @Builder 不会生成无参构造方法
    • 2.3 @Builder 不能build父类属性
    • 2.4 @ToString 栈溢出
    • 2.5 影响单元测试覆盖率
  • 三、源码探秘
    • 3.1 APT与JSR 269
    • 3.2 实现流程
    • 3.3 源码追踪
  • 四、优缺点
    • 4.1 优点
    • 4.2 缺点
  • 参考

前言

Lombok 是一款在 Java 开发中广受欢迎的工具库,它能够显著简化 Java 代码的编写过程并减少样板代码的冗余。在面对频繁的getter和setter方法、构造函数、日志记录等重复性代码任务时,Lombok 的出现为开发者带来了极大的便利,无需手动编写这些重复性的代码,减少了代码量,提高了开发效率。

Lombok的使用非常简单,只需在项目中引入 Lombok 库,并在需要的类上添加相应的注解即可。另外,大多数流行的Java集成开发环境(IDE)也都提供了对Lombok 的支持,可以在代码编辑器中正确显示自动生成的代码, IDEA2021 已经内置 Lombok 了。

文档:https://projectlombok.org/features/

pom 依赖如下:

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

一、常用注解

1.1 @AllArgsConstructor/@NoArgsConstructor/@RequiredArgsConstructor

这三个注解能够生成类的构造器

  1. @AllArgsConstructor 能够生成由所有参数构造的构造方法

    @AllArgsConstructor
    public class Test {private Integer age;private String userName;
    }
    

    生成构造方法,参数顺序为实例类中元素顺序

    public class Test {private Integer age;private String userName;public Test(Integer age, String userName) {this.age = age;this.userName = userName;}
    }
    
  2. @NoArgsConstructor 能够生成无参构造方法

    @NoArgsConstructor
    public class Test {private Integer age = 1;private String userName = "";
    }
    

    生成无参构造方法

    public class Test {private Integer age;private String userName;public Test() {}
    }
    
  3. @RequiredArgsConstructor 可以为类内 final 字段和被 @nonNull 修饰的字段 添加构造方法

    @RequiredArgsConstructor
    public class Test {private Integer age;private String userName;private final  String password;@NonNullprivate  String [] lists;
    }
    

    转化为

    public class Test {private Integer age;private String userName;private final String password;private @NonNull String[] lists;public Test(String password, @NonNull String[] lists) {if (lists == null) {throw new NullPointerException("lists is marked non-null but is null");} else {this.password = password;this.lists = lists;}}
    }
    

1.2 @Builder

@builder 能够生成支持Builder模式的类,提供一种灵活、可读性高且易于维护的方式来构建对象,尤其是当对象具有多个属性,且需要支持可选参数和默认值时,Builder模式特别有用.

@Builder
public class Test {@Builder.Defaultprivate Integer age = 18;private String userName;private final  String password;@NonNullprivate  String [] lists;
}

转化如下:

public class Test {private Integer age;private String userName;private final String password;private @NonNull String[] lists;private static Integer $default$age() {return 18;}Test(Integer age, String userName, String password, @NonNull String[] lists) {if (lists == null) {throw new NullPointerException("lists is marked non-null but is null");} else {this.age = age;this.userName = userName;this.password = password;this.lists = lists;}}public static TestBuilder builder() {return new TestBuilder();}public static class TestBuilder {private boolean age$set;private Integer age$value;private String userName;private String password;private String[] lists;TestBuilder() {}public TestBuilder age(Integer age) {this.age$value = age;this.age$set = true;return this;}public TestBuilder userName(String userName) {this.userName = userName;return this;}public TestBuilder password(String password) {this.password = password;return this;}public TestBuilder lists(@NonNull String[] lists) {if (lists == null) {throw new NullPointerException("lists is marked non-null but is null");} else {this.lists = lists;return this;}}public Test build() {Integer age$value = this.age$value;if (!this.age$set) {age$value = Test.$default$age();}return new Test(age$value, this.userName, this.password, this.lists);}public String toString() {return "Test.TestBuilder(age$value=" + this.age$value + ", userName=" + this.userName + ", password=" + this.password + ", lists=" + Arrays.deepToString(this.lists) + ")";}}
}

1.3 @Data

等价于 @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode

1.4 @EqualsAndHashCode

能够生成equalshashCode方法, 可通过 callSuper = true 来调用父类的同名方法,不参与计算的属性可通过@EqualsAndHashCode.Exclude进行排除

@EqualsAndHashCode
public class Test {private Integer age = 18;private String userName;
}

转化为

public class Test {private Integer age = 18;private String userName;public Test() {}public boolean equals(Object o) {if (o == this) {return true;} else if (!(o instanceof Test)) {return false;} else {Test other = (Test)o;if (!other.canEqual(this)) {return false;} else {Object this$age = this.age;Object other$age = other.age;if (this$age == null) {if (other$age != null) {return false;}} else if (!this$age.equals(other$age)) {return false;}Object this$userName = this.userName;Object other$userName = other.userName;if (this$userName == null) {if (other$userName != null) {return false;}} else if (!this$userName.equals(other$userName)) {return false;}return true;}}}protected boolean canEqual(Object other) {return other instanceof Test;}public int hashCode() {int PRIME = 59; // 这里IDEA反编译有bug,显示为int PRIME = true;int result = 1;Object $age = this.age;result = result * 59 + ($age == null ? 43 : $age.hashCode());Object $userName = this.userName;result = result * 59 + ($userName == null ? 43 : $userName.hashCode());return result;}

1.5 @Getter/@Setter

生成getter和setter方法,默认跳过静态字段和以$开头的字段

@Getter
@Setter
public class Test {private Integer age = 18;private String userName;
}

转化为

public class Test {private Integer age = 18;private String userName;public Test() {}public Integer getAge() {return this.age;}public String getUserName() {return this.userName;}public void setAge(Integer age) {this.age = age;}public void setUserName(String userName) {this.userName = userName;}
}

1.6 @Slf4j/@Log4j/@Log4j2/@Log

在类中生成1个字段log,用于记录日志, 使用不同的日志框架可以使用不同的注解

@log4j2
public class Test {private Integer age = 18;private String userName;public static void main(String[] args) {log.info("{}在哪?", "我");}
}
public class Test {private static final Logger log = LogManager.getLogger(Test.class);private Integer age = 18;private String userName;public Test() {}public static void main(String[] args) {log.info("{}在哪?", new Object[]{"我"});}
}

1.7 @ToString

默认将打印所有非静态字段,可以用@ToString.Exclude注解排除不想打印的字段

@ToString
public class Test {private Integer age;private String userName;
}
public class Test {private Integer age;private String userName;public Test() {}public String toString() {return "Test(age=" + this.age + ", userName=" + this.userName + ")";}
}

二、踩坑

2.1 Getter/Setter 方法名不一样

在类中,开头只有一个小写字母的字段,如 iPhone, 当使用 Lombok 生成 getter、setter 方法时,它生成getter和setter方法如下:

public String getIPhone() {return this.iPhone;
}
public void setIPhone(String iPhone) {this.iPhone = iPhone;
}

和在IDEA中使用快捷键生成的不一样

public String getiPhone() {return iPhone;
}public void setiPhone(String iPhone) {this.iPhone = iPhone;
}

在 SpringBoot 项目中使用 @RequestBody 接收 json 数据时,默认通过 Jackson 处理 ,而 jackson 处理实体,会从getter/setter方法获取具体的字段名,具体源码位于DefaultAccessorNamingStrategy.legacyManglePropertyName, 如下所示:

/*** Method called to figure out name of the property, given * corresponding suggested name based on a method or field name.** @param basename Name of accessor/mutator method, not including prefix*  ("get"/"is"/"set")*/
protected String legacyManglePropertyName(final String basename, final int offset)
{final int end = basename.length();if (end == offset) { // empty name, nopereturn null;}char c = basename.charAt(offset);// 12-Oct-2020, tatu: Additional configurability; allow checking that//    base name is acceptable (currently just by checking first character)if (_baseNameValidator != null) {if (!_baseNameValidator.accept(c, basename, offset)) {return null;}}// next check: is the first character upper case? If not, return as ischar d = Character.toLowerCase(c);if (c == d) {return basename.substring(offset);}// otherwise, lower case initial chars. Common case first, just one charStringBuilder sb = new StringBuilder(end - offset);sb.append(d);int i = offset+1;for (; i < end; ++i) {c = basename.charAt(i);d = Character.toLowerCase(c);if (c == d) {sb.append(basename, i, end);break;}sb.append(d);}return sb.toString();
}

以上代码会将生成的 set/get/is 等方法获取字段, 将方法中 set/get/is 按照偏移量移除,然后找到第一个小写的字符,之前的大写字符都会变为小写,这就会导致问题,IPhone会变为 iphone 和字段 iPhone 不同,会导致问题

Lombok 开发者也意识到这种问题,并提供了解决方案:https://projectlombok.org/features/GetterSetter

lombok.accessors.capitalization = [basic | beanspec] (default: basic)

Controls how tricky cases like uShaped (one lowercase letter followed by an upper/titlecase letter) are capitalized. basic capitalizes that to getUShaped, and beanspec capitalizes that to getuShaped instead.
Both strategies are commonly used in the java ecosystem, though beanspec is more common.

用 Lombok 的配置来解决。在项目resource目录下创建 lombok.config文件,并添加以下配置项

lombok.accessors.capitalization = beanspec

2.2 @Builder 不会生成无参构造方法

当使用@Builder后,会有生成全部参数的构造函数,但是没有无参构造方法,这对Spring IOC等框架不太友好,框架需要无参构造函数构造对象。所以,第一感觉就是再加上@NoArgsConstructor,但是又报错了.

原因分析:如果只是@Builder,那会生成全参构造方法,加上@NoArgsConstructor,全参构造方法就没了。翻看源码文档

If a class is annotated, then a package-private constructor is generated with all fields as arguments (as if @AllArgsConstructor(access = AccessLevel.PACKAGE) is present on the class), and it is as if this constructor has been annotated with @Builder instead. Note that this constructor is only generated if you haven’t written any constructors and also haven’t added any explicit @XArgsConstructor annotations. In those cases, lombok will assume an all-args constructor is present and generate code that uses it; this means you’d get a compiler error if this constructor is not present

翻译一下

如果一个类被注解,那么将生成一个包专用构造函数,其中所有字段都作为参数(就好像类上存在@AllArgsConstructor(access=AccessLevel.package)一样),并且就好像这个构造函数是用@Builder注解的一样。请注意,只有当您没有编写任何构造函数,也没有添加任何显式@XArgsConstructor注解时,才会生成此构造函数。在这些情况下,lombok将假设存在一个all-args构造函数,并生成使用它的代码;这意味着如果这个构造函数不存在,就会出现编译器错误。

文档说的很明白,当加上@NoArgsConstructor时,不会生成全参构造方法,造成编译错误

解决方法:很简单,再加上@AllArgsConstructor

2.3 @Builder 不能build父类属性

有两种方案:

  1. 添加一个构造方法,包含父类的属性

    @Data
    @AllArgsConstructor
    public class Parent {private String foo;private Integer bar;
    }
    
    @ToString(callSuper = true)
    public class Child extends Parent {private Integer age;private String userName;@Builderpublic Child(String foo, Integer bar, Integer age, String userName) {super(foo, bar);this.age = age;this.userName = userName;}
    }
    
  2. 使用@Superbuilder, 这是实验性的 API,不知未来是否删除,慎用

    @Data
    @AllArgsConstructor
    @SuperBuilder
    public class Parent {private String foo;private Integer bar;
    }
    
    @ToString(callSuper = true)
    @SuperBuilder
    public class Child extends Parent {private Integer age;private String userName;}
    

2.4 @ToString 栈溢出

在使用 JPA 时,实体之间为多对多关系,相互引用,在调用toString方法是陷入无限递归,栈溢出

可以使用@ToString.Exclude 注解排除多对多引用的字段

2.5 影响单元测试覆盖率

在项目中使用了**@Data** 注解,在使用 Jacoco 对代码进行单元测试,会发现测试覆盖率比较低,一些自动生成的方法没有覆盖到

解决方法:加上以下配置,Lombok会在为由其生成的构造方法、方法、字段和类型中增加@Generated注解,然后Jacoco借助这个注解来实现更为准去的排除。

config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

三、源码探秘

3.1 APT与JSR 269

编译时注解有以下两种方案:

  1. APT(Annotation Processing Tool),自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:

    • api都在com.sun.mirror非标准包下
    • 没有集成到javac中,需要额外运行
  2. JSR-269(Pluggable Annotation Processing API,插件式注解处理器)JDK6 开始纳入了规范,作为apt的替代方案,它解决了apt的以上两个问题。关于处理注解的包在javax.annotation.processing, 集成到javac中,javac 过程如下:

    handle

    • Parse and Enter:所有在命令行中指定的源文件都被读取,解析成语法树,然后所有外部可见的定义都被输入到编译器的符号表中。
    • Annotation Processing:调用所有适当的注解处理器。如果任何注解处理程序生成任何新的源文件或类文件,则重新开始编译,直到没有创建任何新文件为止。
    • Analyse and Generate:最后,解析器创建的语法树将被分析并转换为类文件。在分析过程中,可能会发现对其他类的引用。编译器将检查这些类的源和类路径,如果在源路径上找到它们,也会编译这些文件,尽管它们不需要进行注解处理。

3.2 实现流程

在Javac 解析成 AST(Abstract Syntax Tree, 抽象语法树)之后, Lombok 根据自己编写的注解处理器,动态地修改 AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成 JVM 可执行的字节码Class文件。

具体流程如下:

  1. 在编译Java源代码时,Java编译器会调用注解处理器API。注解处理器会扫描源代码中的注解,找到Lombok相关的注解。
  2. 注解处理器:Lombok的注解处理器会解析并处理这些注解。它会通过解析AST来了解源代码的结构,并根据注解生成相应的代码。
  3. 代码生成:根据注解的类型,Lombok的注解处理器会生成与注解相关的代码片段。例如,@Getter注解会自动生成对应属性的getter方法,@Setter注解会自动生成对应属性的setter方法。
  4. 代码替换:生成的代码片段将会替换原始源代码中与注解相关的部分。这意味着在编译后的字节码中,生成的代码将取代原始代码,从而实现了代码的增强和简化。
  5. 编译结果:最终,通过注解处理器的处理,源代码中标记了Lombok注解的部分将会被替换为生成的代码。这些生成的代码将包含在编译后的类文件中,以便在运行时使用。

3.3 源码追踪

打开 lombok.jar 文件,会发现不包含许多.class文件,而是包含名为.SCL.lombok的文件。其实.SCL.lombok文件是.class文件, Lombok 的构建脚本在生成 jar 文件时重命名它们,而 ShadowClassLoader 能够加载这些类,并且首字母缩略词 SCL 似乎来自于此,似乎这样做的原因只是"避免使用基于 SC L的 jar 污染任何项目的命名空间

lombok jar包从maven下载源码,有部分代码找不到源码,IDEA反编译为空,暂未找到解决方法

下面以@Get注解为例,查看 lombok 是如何生成getter方法的:

  1. 首先找到的类是LombokProcessor这个类,它继承了AbstractProcessor, 我们知道在自定义一个 APT 的时候需要继承 AbstractProcessor ,并实现其最核心的 process 方法来对当前轮编译的结果进行处理,在 Lombok 中也不例外,Lombok 也是通过一个顶层的 Processor 来接收当前轮的编译结果,而这个 Processor 就是 LombokProcessor 重点关注process方法的这一段

    transformer.transform(prio, javacProcessingEnv.getContext(), cusForThisRound, cleanup);
    
  2. JavacTransformer.transform具体如下

    public void transform(long priority, Context context, List<JCCompilationUnit> compilationUnits, CleanupRegistry cleanup) {for (JCCompilationUnit unit : compilationUnits) {if (!Boolean.TRUE.equals(LombokConfiguration.read(ConfigurationKeys.LOMBOK_DISABLE, JavacAST.getAbsoluteFileLocation(unit)))) {JavacAST ast = new JavacAST(messager, context, unit, cleanup);ast.traverse(new AnnotationVisitor(priority));handlers.callASTVisitors(ast, priority);if (ast.isChanged()) LombokOptions.markChanged(context, (JCCompilationUnit) ast.top().get());}}
    }
    

    获取 AST , traverse 遍历

  3. 继续追踪,找到注解,根据注解位置处理

    public void traverse(JavacASTVisitor visitor) {switch (this.getKind()) {case COMPILATION_UNIT:visitor.visitCompilationUnit(this, (JCCompilationUnit) get());ast.traverseChildren(visitor, this);visitor.endVisitCompilationUnit(this, (JCCompilationUnit) get());break;case TYPE:visitor.visitType(this, (JCClassDecl) get());ast.traverseChildren(visitor, this);visitor.endVisitType(this, (JCClassDecl) get());break;case FIELD:visitor.visitField(this, (JCVariableDecl) get());ast.traverseChildren(visitor, this);visitor.endVisitField(this, (JCVariableDecl) get());break;case METHOD:visitor.visitMethod(this, (JCMethodDecl) get());ast.traverseChildren(visitor, this);visitor.endVisitMethod(this, (JCMethodDecl) get());break;case INITIALIZER:visitor.visitInitializer(this, (JCBlock) get());ast.traverseChildren(visitor, this);visitor.endVisitInitializer(this, (JCBlock) get());break;case ARGUMENT:JCMethodDecl parentMethod = (JCMethodDecl) up().get();visitor.visitMethodArgument(this, (JCVariableDecl) get(), parentMethod);ast.traverseChildren(visitor, this);visitor.endVisitMethodArgument(this, (JCVariableDecl) get(), parentMethod);break;case LOCAL:visitor.visitLocal(this, (JCVariableDecl) get());ast.traverseChildren(visitor, this);visitor.endVisitLocal(this, (JCVariableDecl) get());break;case STATEMENT:visitor.visitStatement(this, get());ast.traverseChildren(visitor, this);visitor.endVisitStatement(this, get());break;case ANNOTATION:switch (up().getKind()) {case TYPE:// @Getter放在类上会执行这段visitor.visitAnnotationOnType((JCClassDecl) up().get(), this, (JCAnnotation) get());break;case FIELD:visitor.visitAnnotationOnField((JCVariableDecl) up().get(), this, (JCAnnotation) get());break;case METHOD:visitor.visitAnnotationOnMethod((JCMethodDecl) up().get(), this, (JCAnnotation) get());break;case ARGUMENT:JCVariableDecl argument = (JCVariableDecl) up().get();JCMethodDecl method = (JCMethodDecl) up().up().get();visitor.visitAnnotationOnMethodArgument(argument, method, this, (JCAnnotation) get());break;case LOCAL:visitor.visitAnnotationOnLocal((JCVariableDecl) up().get(), this, (JCAnnotation) get());break;case TYPE_USE:visitor.visitAnnotationOnTypeUse(up().get(), this, (JCAnnotation) get());break;default:throw new AssertionError("Annotion not expected as child of a " + up().getKind());}break;case TYPE_USE:visitor.visitTypeUse(this, get());ast.traverseChildren(visitor, this);visitor.endVisitTypeUse(this, get());break;default:throw new AssertionError("Unexpected kind during node traversal: " + getKind());}
    }
    
  4. 上述 JavacASTVisitor

    public class JavacASTAdapter implements JavacASTVisitor {...
    }
    
    private class AnnotationVisitor extends JavacASTAdapter {private final long priority;AnnotationVisitor(long priority) {this.priority = priority;}@Override public void visitAnnotationOnType(JCClassDecl type, JavacNode annotationNode, JCAnnotation annotation) {JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();// 执行这段handlers.handleAnnotation(top, annotationNode, annotation, priority);}...
    }
    
  5. 上述handlers时HandlerLibrary类型, HandlerLibrary 中 handleAnnotation如下

    public void handleAnnotation(JCCompilationUnit unit, JavacNode node, JCAnnotation annotation, long priority) {TypeResolver resolver = new TypeResolver(node.getImportList());String rawType = annotation.annotationType.toString();String fqn = resolver.typeRefToFullyQualifiedName(node, typeLibrary, rawType);if (fqn == null) return;List<AnnotationHandlerContainer<?>> containers = annotationHandlers.get(fqn);if (containers == null) return;for (AnnotationHandlerContainer<?> container : containers) {try {if (container.getPriority() == priority) {if (checkAndSetHandled(annotation)) {// 各个注解handler调用各自的handle方法container.handle(node);} else {if (container.isEvenIfAlreadyHandled()) container.handle(node);}}} catch (AnnotationValueDecodeFail fail) {fail.owner.setError(fail.getMessage(), fail.idx);} catch (Throwable t) {String sourceName = "(unknown).java";if (unit != null && unit.sourcefile != null) sourceName = unit.sourcefile.getName();javacError(String.format("Lombok annotation handler %s failed on " + sourceName, container.handler.getClass()), t);}}}
    
  6. @Get 注解相关handler类为 HandleGetter,重要的handle方法如下:

    public void handle(AnnotationValues<Getter> annotation, JCAnnotation ast, JavacNode annotationNode) {handleFlagUsage(annotationNode, ConfigurationKeys.GETTER_FLAG_USAGE, "@Getter");Collection<JavacNode> fields = annotationNode.upFromAnnotationToFields();// 将@Getter注解删除deleteAnnotationIfNeccessary(annotationNode, Getter.class);// 删除lombok 引用包deleteImportFromCompilationUnit(annotationNode, "lombok.AccessLevel");JavacNode node = annotationNode.up();Getter annotationInstance = annotation.getInstance();AccessLevel level = annotationInstance.value();// 判断lazy属性boolean lazy = annotationInstance.lazy();if (lazy) handleFlagUsage(annotationNode, ConfigurationKeys.GETTER_LAZY_FLAG_USAGE, "@Getter(lazy=true)");if (level == AccessLevel.NONE) {if (lazy) annotationNode.addWarning("'lazy' does not work with AccessLevel.NONE.");return;}if (node == null) return;List<JCAnnotation> onMethod = unboxAndRemoveAnnotationParameter(ast, "onMethod", "@Getter(onMethod", annotationNode);// 根据在字段,还是类生成getter方法switch (node.getKind()) {case FIELD:createGetterForFields(level, fields, annotationNode, true, lazy, onMethod);break;case TYPE:if (lazy) annotationNode.addError("'lazy' is not supported for @Getter on a type.");generateGetterForType(node, annotationNode, level, false, onMethod);break;}
    }
    

四、优缺点

4.1 优点

  • 最大的优点就是减少样板代码的编写,提高开发效率
  • 通过使用 Lombok,当类的属性发生变化时,不需要手动更新相应的 getter、setter、equals 和 hashCode 方法等,Lombok 会自动帮助生成更新后的代码,提高代码的维护性
  • 大多数主流的 Java IDE(如 IntelliJ IDEA、Eclipse)都对 Lombok 提供了良好的支持,可以正确地识别和处理 Lombok 的注解,帮助开发者在开发过程中更好地理解和调试代码
  • 避免一些工具不支持 Lombok,提供delombok,将被 Lombok 处理后的字节码重新翻译为java源代码

4.2 缺点

  • 在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果

    举一个简单的例子,我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性

  • 同样的,尽管 Lombok 自动生成的代码可以减少重复性代码,但有时候也可能会导致可读性下降。由于生成的代码被隐藏起来,其他开发人员可能不太容易理解代码的实际逻辑

  • 调试困难:由于Lombok会修改源代码,导致在调试时可能无法准确地查看和追踪生成的代码。这可能会对代码调试和排错造成一定的困扰

  • 版本兼容性:Lombok的注解处理器会直接修改Java源文件,这使得在不同版本的Java编译器和IDE之间使用Lombok可能存在兼容性问题。当你在不同环境中编译或构建项目时,可能需要额外考虑Lombok的版本兼容性

  • 项目编译变慢了

参考

  1. 这个字段我明明传了呀,为什么收不到 - Spring 中首字母小写,第二个字母大写造成的参数问题
  2. Lombok 原理分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/146984.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu Qt 5.15.2 支持 aarch64

概述 AArch64是ARMv8 架构的一种执行状态。 为了更广泛地向企业领域推进&#xff0c;需要引入64 位构架。 同时也需要在ARMv8 架构中引入新的AArch64 执行状态。 AArch64 不是一个单纯的32 位ARM 构架扩展&#xff0c;而是ARMv8 内全新的构架&#xff0c;完全使用全新的A64 指令…

黑豹程序员-架构师学习路线图-百科:Git/Gitee(版本控制)

文章目录 1、什么是版本控制2、特点3、发展历史4、SVN和Git比较5、Git6、GitHub7、Gitee&#xff08;国产&#xff09;8、Git的基础命令 1、什么是版本控制 版本控制系统&#xff08; Version Control &#xff09;版本控制是一种管理和跟踪软件开发过程中的代码变化的系统。它…

【Python】time模块和datetime模块的部分函数说明

时间戳与日期 在说到这俩模块之前&#xff0c;首先先明确几个概念&#xff1a; 时间戳是个很单纯的东西&#xff0c;没有“时区”一说&#xff0c;因为时间戳本质上是经过的时间。日常生活中接触到的“日期”、“某点某时某分”准确的说是时间点&#xff0c;都是有时区概念的…

C#,数值计算——Ranq1的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Recommended generator for everyday use.The period is 1.8E19. Calling /// conventions same as Ran, above. /// </summary> public class Ranq1 { …

2023版 STM32实战4 滴答定时器精准延时

SysTick简介与特性 -1- SysTick属于系统时钟。 -2- SysTick定时器被捆绑在NVIC中。 -3- SysTick可以产生中断,且中断不可屏蔽。 SysTick的时钟源查看 通过时钟树可以看出滴答的时钟最大为72MHZ/89MHZ 通过中文参考手册也可以得到这个结论 代码编写&#xff08;已经验证&a…

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石②

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石② 第十九章 驱动程序基石②19.3 异步通知19.3.1 适用场景19.3.2 使用流程19.3.3 驱动编程19.3.4 应用编程19.3.5 现场编程19.3.6 上机编程19.3.7 异步通知机制内核代码详解 19.4 阻塞与非阻塞19.4.1 应用编程19.4.2 驱动编程…

【算法分析与设计】回溯法(上)

目录 一、学习要点1.1 回溯法1.2 问题的解空间1.3 0-1背包问题的解空间1.4 旅行售货员问题的解空间1.5 生成问题状态的基本方法 二、回溯法的基本思想三、回溯算法的适用条件四、递归回溯五、迭代回溯六、子集树与排列树七、装载问题八、批处理作业调度问题 一、学习要点 理解回…

【数据结构与算法】通过双向链表和HashMap实现LRU缓存 详解

这个双向链表采用的是有伪头节点和伪尾节点的 与上一篇文章中单链表的实现不同&#xff0c;区别于在实例化这个链表时就初始化了的伪头节点和伪尾节点&#xff0c;并相互指向&#xff0c;在第一次添加节点时&#xff0c;不需要再考虑空指针指向问题了。 /*** 通过链表与HashMa…

92、Redis ------- 使用 Lettuce 操作 Redis 的方法和步骤----(文字讲解无代码)

lettuce &#xff1a;英语的意思&#xff1a;生菜 是一个用来操作redis的框架&#xff0c;springboot内置默认支持的也是 lettuce &#xff0c;也可以自己改成 jedis Jedis 也是一个用来操作redis的框架 ★ Lettuce的核心API RedisURI&#xff1a;用于封装Redis服务器的URI信息…

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石⑤

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石⑤ 第十九章 驱动程序基石⑤19.9 mmap19.9.1 内存映射现象与数据结构19.9.2 ARM架构内存映射简介19.9.2.1 一级页表映射过程19.9.2.2 二级页表映射过程 19.9.3 怎么给APP新建一块内存映射19.9.3.1 mmap调用过程19.9.3.2 cach…

数据结构:简单记录顺序表、链表、栈、队列

初学者很容易认为顺序表、链表、栈、队列是四种并列的数据结构&#xff0c;其实仔细想想并不是。 注意区分&#xff1a; 顺序表和链表是指数据的存储结构&#xff0c;是线性表的一种&#xff0c;顺序表一般指的就是数组&#xff0c;数据存储的逻辑顺序和物理顺序都是连续的&a…

nodejs+vue交通违章查询及缴费elementui

第三章 系统分析 10 3.1需求分析 10 3.2可行性分析 10 3.2.1技术可行性&#xff1a;技术背景 10 3.2.2经济可行性 11 3.2.3操作可行性&#xff1a; 11 3.3性能分析 11 3.4系统操作流程 12 3.4.1管理员登录流程 12 3.4.2信息添加流程 12 3.4.3信息删除流程 13 第四章 系统设计与…

Django模板加载与响应

前言 Django 的模板系统将 Python 代码与 HTML 代码解耦&#xff0c;动态地生成 HTML 页面。Django 项目可以配置一个或多个模板引擎&#xff0c;但是通常使用 Django 的模板系统时&#xff0c;应该首先考虑其内置的后端 DTL&#xff08;Django Template Language&#xff0c;D…

Git使用【下】

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;那个传说中的man的主页 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;题目大解析&#xff08;3&#xff09; 目录 &#x1f449;&#x1f3fb;标签管理理解标签标签运用 …

Grander因果检验(格兰杰)原理+操作+解释

笔记来源&#xff1a; 1.【传送门】 2.【传送门】 前沿原理介绍 Grander因果检验是一种分析时间序列数据因果关系的方法。 基本思想在于&#xff0c;在控制Y的滞后项 (过去值) 的情况下&#xff0c;如果X的滞后项仍然有助于解释Y的当期值的变动&#xff0c;则认为 X对 Y产生…

插入排序与希尔排序

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 前言&#xff1a; 这两个排序在思路上有些相似&#xff0c;所以有人觉得插入排序和希尔排序差别不大&#xff0c;事实上&#xff0c;他们之间的差别不小&#xff0c;插入排序只是希尔排序的最后一步。 目录 前言&#xff1a;…

华为数通方向HCIP-DataCom H12-831题库(单选题:161-180)

第161题 某台路由器Router LSA如图所示,下列说法中错误的是? A、本路由器已建立邻接关系 B、本路由器为DR C、本路由支持外部路由引入 D、本路由器的Router ID为10.0.12.1 答案: B 解析: 一类LSA的在transnet网络中link id值为DR的route id ,但Link id的地址不是10.0.12.…

对pyside6中的textedit进行自定义,实现按回车可以触发事件。

以下方法不算最优解。因为这个ui文件很容易重新编译&#xff0c;使写在ui.py里面的代码被删掉。 所以更好的方法应该是在主代码当中单独定义控件。并且使用布局添加控件到界面中。 以下内容纯为旧版实现&#xff0c;仅供参考&#xff1a; 我的实现方法是&#xff0c;先用qt de…

学信息系统项目管理师第4版系列15_资源管理基础

1. 项目资源 1.1. 实物资源 1.1.1. 着眼于以有效和高效的方式&#xff0c;分配和使用完成项目所需的实物资源 1.1.2. 包括设备、材料、设施和基础设施 1.2. 团队资源 1.2.1. 人力资源 1.2.2. 包含了技能和能力要求 2. 人力资源管理 2.1. 不仅是组织中最重要的资源之一&…

设计模式之抽象工厂模式--创建一系列相关对象的艺术(简单工厂、工厂方法、到抽象工厂的进化过程,类图NS图)

目录 概述概念适用场景结构类图 衍化过程业务需求基本的数据访问程序工厂方法实现数据访问程序抽象工厂实现数据访问程序简单工厂改进抽象工厂使用反射抽象工厂反射配置文件衍化过程总结 常见问题总结 概述 概念 抽象工厂模式是一种创建型设计模式&#xff0c;它提供了一种将相…