Introduction to Lombok

Lombok is a Java development plug-in that enables Java developers to eliminate verbose and tedious code in business projects, especially for simple Java model objects (POJOs), by defining annotations. By using Lombok plug-ins in their development environment, Java developers can save a lot of time on repeated builds, methods such as hashCode and equals, and methods such as Accessor and toString for various business object models. For these methods, Lombok can automatically generate them for us during source code compilation, but it doesn’t degrade performance the way reflection does.

Lombok installation

2.1 Building Tools

Gradle

Add lombok dependencies to the build.gradle file:

dependencies {
	compileOnly 'org. Projectlombok: lombok: 1.18.10'
	annotationProcessor 'org. Projectlombok: lombok: 1.18.10'
}
Copy the code
Maven

Add lombok dependencies to your Maven project’s pom.xml file:

<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.10</version>
		<scope>provided</scope>
</dependency>
Copy the code
Ant

Assuming lombok. Jar already exists in the lib directory, then set the Javac task:

<javac srcdir="src" destdir="build" source="1.8">
	<classpath location="lib/lombok.jar" />
</javac>
Copy the code

2.2 the IDE

Because Lombok generates code only at compile time, source code annotated with Lombok is highlighted as an error in the IDE, which can be addressed by installing the IDE’s corresponding plug-in. Setting Up Lombok with Eclipse and IntelliJ is an example of how to install Lombok.

Three, Lombok in detail

Note: The following example uses Lombok version 1.18.10

3.1 @getter and @setter annotations

You can annotate any class or field with @Getter or @setter, and Lombok automatically generates the default Getter/Setter methods.

@ Getter annotations
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
  // If the getter method is not public, you can set the access level
	lombok.AccessLevel value(a) default lombok.AccessLevel.PUBLIC;
	AnyAnnotation[] onMethod() default {};
  // Whether to enable delayed initialization
	boolean lazy(a) default false;
}
Copy the code
@ Setter annotations
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
  // If the setter method is not public, you can set the access level
	lombok.AccessLevel value(a) default lombok.AccessLevel.PUBLIC;
	AnyAnnotation[] onMethod() default {};
	AnyAnnotation[] onParam() default {};
}
Copy the code
Use the sample
package com.semlinker.lombok;

@Getter
@Setter
public class GetterAndSetterDemo {
    String firstName;
    String lastName;
    LocalDate dateOfBirth;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class GetterAndSetterDemo {
    String firstName;
    String lastName;
    LocalDate dateOfBirth;

    public GetterAndSetterDemo(a) {}// Omit the other setter and getter methods
    public String getFirstName(a) {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName; }}Copy the code
Lazy Getter

The @getter annotation supports a lazy property, which defaults to false. When set to true, lazy initialization is enabled, meaning that it is initialized when the getter method is first called.

The sample
package com.semlinker.lombok;

public class LazyGetterDemo {
    public static void main(String[] args) {
        LazyGetterDemo m = new LazyGetterDemo();
        System.out.println("Main instance is created");
        m.getLazy();
    }

    @Getter
    private final String notLazy = createValue("not lazy");

    @Getter(lazy = true)
    private final String lazy = createValue("lazy");

    private String createValue(String name) {
        System.out.println("createValue(" + name + ")");
        return null; }}Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class LazyGetterDemo {
    private final String notLazy = this.createValue("not lazy");
    private final AtomicReference<Object> lazy = new AtomicReference();

    // Omitted part of the code
    public String getNotLazy(a) {
        return this.notLazy;
    }

    public String getLazy(a) {
        Object value = this.lazy.get();
        if (value == null) {
            synchronized(this.lazy) {
                value = this.lazy.get();
                if (value == null) {
                    String actualValue = this.createValue("lazy");
                    value = actualValue == null ? this.lazy : actualValue;
                    this.lazy.set(value); }}}return (String)((String)(value == this.lazy ? null: value)); }}Copy the code

From the above code, we know that when the getLazy method is called, if the value is null, the initialization operation will be performed in the synchronized code block.

3.2 the Constructor Annotations

@ NoArgsConstructor annotations

@NoargsConstructor generates a default constructor for a specified class using the @NoargsConstructor annotation. The definition of the @NoargsConstructor annotation is as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NoArgsConstructor {
  // if this property is set, it will generate a private constructor and a static method with a staticName
	String staticName(a) default "";	
	AnyAnnotation[] onConstructor() default {};
  // Set the level of access to the generate constructor. The default is public
	AccessLevel access(a) default lombok.AccessLevel.PUBLIC;
  // If set to true, all final fields are initialized to 0/ NULL /false
	boolean force(a) default false;
}
Copy the code
The sample
package com.semlinker.lombok;

@NoArgsConstructor(staticName = "getInstance")
public class NoArgsConstructorDemo {
    private long id;
    private String name;
    private int age;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class NoArgsConstructorDemo {
    private long id;
    private String name;
    private int age;

    private NoArgsConstructorDemo(a) {}public static NoArgsConstructorDemo getInstance(a) {
        return newNoArgsConstructorDemo(); }}Copy the code
@ AllArgsConstructor annotations

We can use the @allargsConstructor annotation to generate a constructor containing all members for a specified class. The @Allargsconstructor annotation is defined as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AllArgsConstructor {
  // if this property is set, it will generate a private constructor and a static method with a staticName
	String staticName(a) default "";
	AnyAnnotation[] onConstructor() default {};
  // Set the level of access to the generate constructor. The default is public
	AccessLevel access(a) default lombok.AccessLevel.PUBLIC;
}
Copy the code
The sample
package com.semlinker.lombok;

@AllArgsConstructor
public class AllArgsConstructorDemo {
    private long id;
    private String name;
    private int age;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class AllArgsConstructorDemo {
    private long id;
    private String name;
    private int age;

    public AllArgsConstructorDemo(long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age; }}Copy the code
@ RequiredArgsConstructorDemo annotations

Use the @requiredargsConstructor annotation to generate constructors for member variables that must be initialized by a class, such as final member variables. The @requiredargsConstructor annotation is defined as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface RequiredArgsConstructor {
  // if this property is set, it will generate a private constructor and a static method with a staticName
	String staticName(a) default "";
	AnyAnnotation[] onConstructor() default {};
  // Set the level of access to the generate constructor. The default is public
	AccessLevel access(a) default lombok.AccessLevel.PUBLIC;
}
Copy the code
The sample
package com.semlinker.lombok;

@RequiredArgsConstructor
public class RequiredArgsConstructorDemo {
    private final long id;
    private String name;
    private int age;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class RequiredArgsConstructorDemo {
    private final long id;
    private String name;
    private int age;

    public RequiredArgsConstructorDemo(long id) {
        this.id = id; }}Copy the code

3.3 @ EqualsAndHashCode annotation

The @EqualSandHashCode annotation generates equals and hashCode methods for the specified class. The @EqualSandHashCode annotation is defined as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface EqualsAndHashCode {
  // Specifies a list of fields to exclude from the generated equals and hashCode methods
	String[] exclude() default {};
	
  Non-static,non-transient fields will be used for identity
	String[] of() default {};
	
  // Specifies whether to call the equals and hashCode methods of the parent class before performing the field calculation
	boolean callSuper(a) default false;
	
	boolean doNotUseGetters(a) default false;
	
	AnyAnnotation[] onParam() default {};
	
	@Deprecated
	@Retention(RetentionPolicy.SOURCE)
	@Target({})
	@interface AnyAnnotation {}
	
	@Target(ElementType.FIELD)
	@Retention(RetentionPolicy.SOURCE)
	public @interface Exclude {}
	
	@Target({ElementType.FIELD, ElementType.METHOD})
	@Retention(RetentionPolicy.SOURCE)
	public @interface Include {
		String replaces(a) default ""; }}Copy the code
The sample
package com.semlinker.lombok;

@EqualsAndHashCode
public class EqualsAndHashCodeDemo {
    String firstName;
    String lastName;
    LocalDate dateOfBirth;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class EqualsAndHashCodeDemo {
    String firstName;
    String lastName;
    LocalDate dateOfBirth;

    public EqualsAndHashCodeDemo(a) {}public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if(! (oinstanceof EqualsAndHashCodeDemo)) {
            return false;
        } else {
            EqualsAndHashCodeDemo other = (EqualsAndHashCodeDemo)o;
            if(! other.canEqual(this)) {
                return false;
            } else {
              // A lot of code has been omitted}}public int hashCode(a) {
        int PRIME = true;
        int result = 1;
        Object $firstName = this.firstName;
        int result = result * 59 + ($firstName == null ? 43 : $firstName.hashCode());
        Object $lastName = this.lastName;
        result = result * 59 + ($lastName == null ? 43 : $lastName.hashCode());
        Object $dateOfBirth = this.dateOfBirth;
        result = result * 59 + ($dateOfBirth == null ? 43 : $dateOfBirth.hashCode());
        returnresult; }}Copy the code

3.4 @ the ToString annotation

You can use the @toString annotation to generate a ToString method for a specified class. The @toString annotation is defined as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ToString {
  // Whether to include the field name when printing output
	boolean includeFieldNames(a) default true;
	
  // Lists the fields that need to be excluded when printing output
	String[] exclude() default {};
	
  // An explicit list of fields to print
	String[] of() default {};
	
  // Whether the output contains the result returned by the parent toString method
	boolean callSuper(a) default false;
	
	boolean doNotUseGetters(a) default false;
	
	boolean onlyExplicitlyIncluded(a) default false;
	
	@Target(ElementType.FIELD)
	@Retention(RetentionPolicy.SOURCE)
	public @interface Exclude {}
	
	@Target({ElementType.FIELD, ElementType.METHOD})
	@Retention(RetentionPolicy.SOURCE)
	public @interface Include {
		int rank(a) default 0;
		String name(a) default ""; }}Copy the code
The sample
package com.semlinker.lombok;

@ToString(exclude = {"dateOfBirth"})
public class ToStringDemo {
    String firstName;
    String lastName;
    LocalDate dateOfBirth;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class ToStringDemo {
    String firstName;
    String lastName;
    LocalDate dateOfBirth;

    public ToStringDemo(a) {}public String toString(a) {
        return "ToStringDemo(firstName=" + this.firstName + ", lastName=" + 
          this.lastName + ")"; }}Copy the code

@ 3.5 Data annotation

The @data annotation has the same effect as using the following annotation:

  • @ToString
  • @Getter
  • @Setter
  • @RequiredArgsConstructor
  • @EqualsAndHashCode

The @data annotation is defined as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
	String staticConstructor(a) default "";
}
Copy the code
The sample
package com.semlinker.lombok;

@Data
public class DataDemo {
    private Long id;
    private String summary;
    private String description;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class DataDemo {
    private Long id;
    private String summary;
    private String description;

    public DataDemo(a) {}// Omit the setter and getter methods for the summary and Description member properties
    public Long getId(a) {
        return this.id;
    }
  
    public void setId(Long id) {
        this.id = id;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if(! (oinstanceof DataDemo)) {
            return false;
        } else {
            DataDemo other = (DataDemo)o;
            if(! other.canEqual(this)) {
                return false;
            } else {
               // A lot of code has been omitted}}}protected boolean canEqual(Object other) {
        return other instanceof DataDemo;
    }

    public int hashCode(a) {
        int PRIME = true;
        int result = 1;
        Object $id = this.getId();
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $summary = this.getSummary();
        result = result * 59 + ($summary == null ? 43 : $summary.hashCode());
        Object $description = this.getDescription();
        result = result * 59 + ($description == null ? 43 : $description.hashCode());
        return result;
    }

    public String toString(a) {
        return "DataDemo(id=" + this.getId() + ", summary=" + this.getSummary() + ", description=" + this.getDescription() + ")"; }}Copy the code

3.6 @ the Log notes

If you put the @log variant on the class (which applies to any logging system you use); After that, you will have a static final Log field that you can then use to output logs.

@Log
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());
Copy the code
@Log4j
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);
Copy the code
@Log4j2
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);
Copy the code
@Slf4j
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
Copy the code
@XSlf4j
private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);
Copy the code
@CommonsLog
private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
Copy the code

3.7 @ Synchronized annotation

@synchronized is a more secure variant of the Synchronized method modifier. As with synchronized, this annotation can only be applied to static and instance methods. It operates like the synchronized keyword, but it locks on a different object. The synchronized keyword, when applied to instance methods, locks the this object, and when applied to static methods, locks the class object. For a method declared by the @synchronized annotation, it locks $LOCK or $LOCK. The @synchronized annotation is defined as follows:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Synchronized {
  // Specify the name of the locked field
	String value(a) default "";
}
Copy the code
The sample
package com.semlinker.lombok;

public class SynchronizedDemo {
    private final Object readLock = new Object();

    @Synchronized
    public static void hello(a) {
        System.out.println("world");
    }

    @Synchronized
    public int answerToLife(a) {
        return 42;
    }

    @Synchronized("readLock")
    public void foo(a) {
        System.out.println("bar"); }}Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class SynchronizedDemo {
    private static final Object $LOCK = new Object[0];
    private final Object $lock = new Object[0];
    private final Object readLock = new Object();

    public SynchronizedDemo(a) {}public static void hello(a) {
        synchronized($LOCK) {
            System.out.println("world"); }}public int answerToLife(a) {
        synchronized(this.$lock) {
            return 42; }}public void foo(a) {
        synchronized(this.readLock) {
            System.out.println("bar"); }}}Copy the code

3.8 @ Builder annotations

You can implement the Builder pattern for a specified class using the @Builder annotation, which can be placed on a class, constructor, or method. The definition of @Builder annotation is as follows:

@Target({TYPE, METHOD, CONSTRUCTOR})
@Retention(SOURCE)
public @interface Builder {
	@Target(FIELD)
	@Retention(SOURCE)
	public @interface Default {}

  // The name of the method to create the new Builder instance
	String builderMethodName(a) default "builder";
	// Create the Builder annotation class corresponding to the instance method name
	String buildMethodName(a) default "build";
	// Name of the Builder class
	String builderClassName(a) default "";
	
	boolean toBuilder(a) default false;
	
	AccessLevel access(a) default lombok.AccessLevel.PUBLIC;
	
	@Target({FIELD, PARAMETER})
	@Retention(SOURCE)
	public @interface ObtainVia {
		String field(a) default "";
		String method(a) default "";
		boolean isStatic(a) default false; }}Copy the code
The sample
package com.semlinker.lombok;

@Builder
public class BuilderDemo {
    private final String firstname;
    private final String lastname;
    private final String email;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class BuilderDemo {
    private final String firstname;
    private final String lastname;
    private final String email;

    BuilderDemo(String firstname, String lastname, String email) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.email = email;
    }

    public static BuilderDemo.BuilderDemoBuilder builder(a) {
        return new BuilderDemo.BuilderDemoBuilder();
    }

    public static class BuilderDemoBuilder {
        private String firstname;
        private String lastname;
        private String email;

        BuilderDemoBuilder() {
        }

        public BuilderDemo.BuilderDemoBuilder firstname(String firstname) {
            this.firstname = firstname;
            return this;
        }

        public BuilderDemo.BuilderDemoBuilder lastname(String lastname) {
            this.lastname = lastname;
            return this;
        }

        public BuilderDemo.BuilderDemoBuilder email(String email) {
            this.email = email;
            return this;
        }

        public BuilderDemo build(a) {
            return new BuilderDemo(this.firstname, this.lastname, this.email);
        }

        public String toString(a) {
            return "BuilderDemo.BuilderDemoBuilder(firstname=" + this.firstname + ", lastname=" + this.lastname + ", email=" + this.email + ")"; }}}Copy the code

3.9 @ SneakyThrows annotation

The @sneakyThrows annotation is used to automatically throw checked exceptions without having to explicitly throw them in a method using a throw statement. The @sneakyThrows annotation is defined as follows:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface SneakyThrows {
	// Set the exception class you want to throw up
	Class<? extends Throwable>[] value() default java.lang.Throwable.class;
}
Copy the code
The sample
package com.semlinker.lombok;

public class SneakyThrowsDemo {
    @SneakyThrows
    @Override
    protected Object clone(a) {
        return super.clone(); }}Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class SneakyThrowsDemo {
    public SneakyThrowsDemo(a) {}protected Object clone(a) {
        try {
            return super.clone();
        } catch (Throwable var2) {
            throwvar2; }}}Copy the code

3.10 @ NonNull annotation

You can use the @nonNULL annotation on the parameters of a method or constructor, and it will automatically generate a non-null check statement for you. The @nonNULL annotation is defined as follows:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface NonNull {
}
Copy the code
The sample
package com.semlinker.lombok;

public class NonNullDemo {
    @Getter
    @Setter
    @NonNull
    private String name;
}
Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class NonNullDemo {
    @NonNull
    private String name;

    public NonNullDemo(a) {}@NonNull
    public String getName(a) {
        return this.name;
    }

    public void setName(@NonNull String name) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else {
            this.name = name; }}}Copy the code

3.11 @ the Clean annotation

The @Clean annotation is used to automatically manage resources, to be used before local variables, to automatically Clean up resources before exiting at the end of the current variable scope, and to automatically generate code such as try-finally to close streams.

@Target(ElementType.LOCAL_VARIABLE)
@Retention(RetentionPolicy.SOURCE)
public @interface Cleanup {
  // Set the name of the method used to perform resource cleaning/reclamation. The corresponding method cannot contain any parameters. The default name is close.
	String value(a) default "close";
}
Copy the code
The sample
package com.semlinker.lombok;

public class CleanupDemo {
    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[10000];
        while (true) {
            int r = in.read(b);
            if (r == -1) break;
            out.write(b, 0, r); }}}Copy the code

Lombok compiles the above code to produce the following code:

package com.semlinker.lombok;

public class CleanupDemo {
    public CleanupDemo(a) {}public static void main(String[] args) throws IOException {
        FileInputStream in = new FileInputStream(args[0]);

        try {
            FileOutputStream out = new FileOutputStream(args[1]);

            try {
                byte[] b = new byte[10000];

                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(); }}}}Copy the code

3.11 @ With annotation

When the @With annotation is applied to a field of the class, a withFieldName(newValue) method is automatically generated that calls the corresponding constructor based on newValue to create an instance of the current class. The @with annotation is defined as follows:

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface With {
    AccessLevel value(a) default AccessLevel.PUBLIC;

    With.AnyAnnotation[] onMethod() default {};

    With.AnyAnnotation[] onParam() default {};

    @Deprecated
    @Retention(RetentionPolicy.SOURCE)
    @Target({})
    public @interface AnyAnnotation {
    }
}
Copy the code
The sample
public class WithDemo {
    @With(AccessLevel.PROTECTED)
    @NonNull
    private final String name;
    @With
    private final int age;

    public WithDemo(String name, int age) {
        if (name == null) throw new NullPointerException();
        this.name = name;
        this.age = age; }}Copy the code

Lombok compiles the above code to produce the following code:

public class WithDemo {
    @NonNull
    private final String name;
    private final int age;

    public WithDemo(String name, int age) {
        if (name == null) {
            throw new NullPointerException();
        } else {
            this.name = name;
            this.age = age; }}protected WithDemo withName(@NonNull String name) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else {
            return this.name == name ? this : new WithDemo(name, this.age); }}public WithDemo withAge(int age) {
        return this.age == age ? this : new WithDemo(this.name, age); }}Copy the code

3.12 Other Features

val

Val is used before local variables, making them final, and Lombok does type inference automatically at compile time. Example of using val:

public class ValExample {
  public String example(a) {
    val example = new ArrayList<String>();
    example.add("Hello, World!");
    val foo = example.get(0);
    return foo.toLowerCase();
  }
  
  public void example2(a) {
    val map = new HashMap<Integer, String>();
    map.put(0."zero");
    map.put(5."five");
    for (val entry : map.entrySet()) {
      System.out.printf("%d: %s\n", entry.getKey(), entry.getValue()); }}}Copy the code

The above code is equivalent to:

public class ValExample {
  public String example(a) {
    final ArrayList<String> example = new ArrayList<String>();
    example.add("Hello, World!");
    final String foo = example.get(0);
    return foo.toLowerCase();
  }
  
  public void example2(a) {
    final HashMap<Integer, String> map = new HashMap<Integer, String>();
    map.put(0."zero");
    map.put(5."five");
    for (final Map.Entry<Integer, String> entry : map.entrySet()) {
      System.out.printf("%d: %s\n", entry.getKey(), entry.getValue()); }}}Copy the code

That concludes the introduction to the powerful Lombok tools. If you’re interested in how Lombok works, I recommend reading Ten Minutes to Understand how Lombok works and how it works.

Example project address: Github – Springboot2-Lombok

Iv. Reference Resources

  • Lombok’s official website
  • lombok-cheatsheet
  • The use and principles of The Java development artifact Lombok