Lombok's @ Builder doesn't work well? Let's try this

Posted by simply on Tue, 07 Dec 2021 20:49:36 +0100

I'm sure Lombok plug-in will be familiar to you. A common annotation is @ builder, which can help us quickly implement a builder mode. Take the common commodity model as an example:

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ItemDTO {
    /**
     * Item ID
     */
    private Long itemId;
    /**
     * Product title
     */
    private String itemTitle;
    /**
     * The original price of the commodity, in cents
     */
    private Long price;
    /**
     * Preferential price of goods, in cents
     */
    private Long promotionPrice;
}

A new product can be constructed in one line of code:

ItemDTO itemDTO = ItemDTO.builder()
        .itemId(6542744309L)
        .itemTitle("Please don't shoot the small tomato 500 for the test g/box")
        .price(500L)
        .promotionPrice(325L)
        .build();
System.out.println(itemDTO);

This is not only beautiful, but also saves a lot of useless code.

Restrictions on the use of Builder annotations

When our entity objects have inherited design, the Builder annotation is not so easy to use. Take the commodity entity as an example. If the commodity classes inherit from a BaseDTO

@Builder
@NoArgsConstructor
public class BaseDTO {
    /**
     * Business identity
     */
    private String bizType;
    /**
     * scene
     */
    private String scene;
}

At this time, when we use the builder annotation again, we will find that the member variables in the parent class cannot be constructed through the builder method in the subclass

Adding Builder annotations to BaseDTO won't have any effect. In fact, the Builder annotation only takes care of the annotated class, not its parent or child class. If this is the case, if you encounter an inherited class, you have to return to the original shape and write a bunch of setter methods.

Try SuperBuilder

This problem was difficult to solve before Lombok v1.18.2, but a new annotation @ SuperBuilder was officially introduced in this version, which can not solve the problem of building parent classes

The @SuperBuilder annotation produces complex builder APIs for your classes. In contrast to @Builder, @SuperBuilder also works with fields from superclasses. However, it only works for types. Most importantly, it requires that all superclasses also have the@SuperBuilder annotation.

According to the official documents, in order to use the build method, you only need to add @ SuperBuilder annotation on the subclass and parent class. Let's try it

Sure enough, you can now build the member variable of the parent class in the instance of the child class

Lombok's principle

The implementation of Lombok automatic code generation also depends on the open extension point of the JVM, so that it can modify the abstract syntax tree during compilation, thus affecting the final generated bytecode

Picture source address: http://notatube.blogspot.com/2010/12/project-lombok-creating-custom.html

Why can't Builder handle member variables of the parent class

We can turn to Lombok's source code. Lombok has two sets of implementations for all annotations, javac and eclipse. Because our running environment is Idea, we choose the implementation of javac. The implementation of javac version is in
lombok.javac.handlers.HandleBuilder#handle

JavacNode parent = annotationNode.up();
if (parent.get() instanceof JCClassDecl) {
   job.parentType = parent;
   JCClassDecl td = (JCClassDecl) parent.get();
   
   ListBuffer<JavacNode> allFields = new ListBuffer<JavacNode>();
   boolean valuePresent = (hasAnnotation(lombok.Value.class, parent) || hasAnnotation("lombok.experimental.Value", parent));
  // Fetch all member variables
   for (JavacNode fieldNode : HandleConstructor.findAllFields(parent, true)) {
      JCVariableDecl fd = (JCVariableDecl) fieldNode.get();
      JavacNode isDefault = findAnnotation(Builder.Default.class, fieldNode, false);
      boolean isFinal = (fd.mods.flags & Flags.FINAL) != 0 || (valuePresent && !hasAnnotation(NonFinal.class, fieldNode));
      // Balabala, omit
}

The annotationNode here is the builder annotation. From the perspective of the abstract syntax tree, the class modified by the annotation is obtained by calling the up method, that is, the class that needs to generate the builder method.

By viewing the source code, @ Builder annotation can modify classes, constructors and methods. For simplicity, the above code only intercepts @ Builder modified classes. The key point of this code is to call
The HandleConstructor.findAllFields method obtains all member variables in the class:

public static List<JavacNode> findAllFields(JavacNode typeNode, boolean evenFinalInitialized) {
   ListBuffer<JavacNode> fields = new ListBuffer<JavacNode>();
  // Starting from the abstract syntax tree, traverse all member variables of the class
   for (JavacNode child : typeNode.down()) {
      if (child.getKind() != Kind.FIELD) continue;
      JCVariableDecl fieldDecl = (JCVariableDecl) child.get();
      //Skip fields that start with $
      if (fieldDecl.name.toString().startsWith("$")) continue;
      long fieldFlags = fieldDecl.mods.flags;
      //Skip static fields.
      if ((fieldFlags & Flags.STATIC) != 0) continue;
      //Skip initialized final fields
      boolean isFinal = (fieldFlags & Flags.FINAL) != 0;
      if (evenFinalInitialized || !isFinal || fieldDecl.init == null) fields.append(child);
   }
   return fields.toList();
}

This code is relatively simple, that is, it filters the member variables in the class. For example, static variables cannot be constructed by @ Builder method. An interesting point is that although $can legally appear in java variable names, Lombok filters such variables, so variable names starting with $cannot be constructed by @ Builder. After our verification, it is true.

If we use JDT AstView to look at the abstract syntax tree structure of ItemDTO, we find that Java's abstract syntax tree design is indeed that each class contains only explicitly declared variables, not the member variables of the parent class (the plug-in supports clicking the syntax tree node to interact with the source file, and the number is only 4, which is consistent with the number of member variables declared by ItemDTO)

Because the findAllFields method starts from the abstract syntax tree of the current class to find all member variables, it can only find the member variables of the current class, not the member variables of the parent class

A mirror problem is that since the @ Builder annotation cannot construct the member variable of the parent class, how does @ SuperBuilder do it? Turn over the source code of @ SuperBuilder. The core logic is
lombok.javac.handlers.HandleSuperBuilder#handle

// Balabala omitted
JCClassDecl td = (JCClassDecl) parent.get();
// Gets the abstract syntax tree of the inherited parent class
JCTree extendsClause = Javac.getExtendsClause(td);
JCExpression superclassBuilderClass = null;
if (extendsClause instanceof JCTypeApply) {
   // Remember the type arguments, because we need them for the extends clause of our abstract builder class.
   superclassTypeParams = ((JCTypeApply) extendsClause).getTypeArguments();
   // A class name with a generics type, e.g., "Superclass<A>".
   extendsClause = ((JCTypeApply) extendsClause).getType();
}
if (extendsClause instanceof JCFieldAccess) {
   Name superclassName = ((JCFieldAccess) extendsClause).getIdentifier();
   String superclassBuilderClassName = superclassName.toString() + "Builder";
   superclassBuilderClass = parent.getTreeMaker().Select((JCFieldAccess) extendsClause, parent.toName(superclassBuilderClassName));
} else if (extendsClause != null) {
   String superclassBuilderClassName = extendsClause.toString() + "Builder";
   superclassBuilderClass = chainDots(parent, extendsClause.toString(), superclassBuilderClassName);
}
// Balabala omitted

You can see that the abstract syntax tree of the inherited parent class is obtained and processed in the following logic, which will not be repeated here

Topics: Java Back-end