javabuilderfluent

Can a Lombok builder be used fluently?


I want to populate an object based on the field names which I will pass in at runtime. I created an object with a Lombok builder and a setField static method to populate the object with the runtime field names (gathered from a BDD step definition). I returned the passed in instance of the builder from the method. Here is the code:

import lombok.Builder;
import lombok.ToString;

import java.lang.reflect.Method;

@Builder
@ToString
public class MyClass {
  private String field1;
  private int field2;
  private boolean field3;

  public static MyClassBuilder setFieldValue(MyClassBuilder builder, String fieldName, Object fieldValue) throws Exception {
    Class<?> builderClass = builder.getClass();

    Method setterMethod = null;
    for (Method method : builderClass.getDeclaredMethods()) {
      if (method.getName().equalsIgnoreCase(fieldName)) {
        setterMethod = method;
        break;
      }
    }

    if (setterMethod != null) {
      setterMethod.invoke(builder, fieldValue);
    }

    return builder;
  }
}

And the Main method to call the code

public class Main {
  public static void main(String[] args) throws Exception {
    MyClass.MyClassBuilder builder = MyClass.builder();

    MyClass myObject = MyClass.setFieldValue(builder, "field1", "Hello")
        .setFieldValue(builder, "field2", 123)
        .setFieldValue(builder, "field3", true)
        .build();

    System.out.println(myObject);
  }
}

The issue is the fluent second call to setFieldValue is not recognised and when I run main I get the error:

java: cannot find symbol
  symbol:   method setFieldValue(MyClass.MyClassBuilder,java.lang.String,int)
  location: class MyClass.MyClassBuilder

The ide highlights the second call to setFieldValue and says ‘Cannot resolve method ‘setFieldValue’ in ‘MyClassBuilder’. It’s like the method is not fluent. What am I missing?

A


Solution

  • This has nothing to do with lombok specifically, that's just.. not valid java code.

    'fluent' is not magic and not a special java rule. Any instance method invocation looks like a.b() where a can be any expression. In this example:

    Person.builder()
      .name("Jane")
      .favouriteColour(BLUE)
      .build()
    

    We are simply invoking .name on whatever Person.builder() returns, and invoking .favouriteColour on whatever Person.builder().name("Jane") returns.

    javac (or any compliant compiler) has no idea that the expression Person.builder() and the expression Person.builder().name("Jane") have the exact same value (because the name method ends in return this;). It does not care about it and in fact cannot care about it, because perhaps one day that method returns something else. (Sure, us humans look at the builder and recognize that having some builder 'setter' method return a completely different builder would be a bizarre and utterly backwards incompatible break, but javac can't know that!) javac just knows that the name() method returns a Person.PersonBuilder instance. It does not know, or care, that it return this;.

    You told javac to chain a whole bunch of method invocations together, each invocation to be performed on whatever object the previous invocation returned. Which is something javac can do just fine.

    In your example, you have a static method setFieldValue which is in class MyClass. It returns an expression of type MyClassBuilder.

    MyClassBuilder is not MyClass, and MyClassBuilder does not have a setFieldValue method, thus, this does not work.

    So, let's make it work!

    Lombok allows you to shove methods into your builder just fine. To do this, manually write out the builder class name and put the methods you want to add in there. Lombok will fill in all the other stuff for you:

    import lombok.Builder;
    import lombok.ToString;
    
    import java.lang.reflect.Method;
    
    @Builder
    @ToString
    public class MyClass {
      private String field1;
      private int field2;
      private boolean field3;
    
      public static class MyClassBuilder {
        public MyClassBuilder setFieldValue(String fieldName, Object fieldValue) throws Exception {
        for (Method method : MyClassBuilder.class.getDeclaredMethods()) {
          if (method.getName().equalsIgnoreCase(fieldName)) {
            method.invoke(this, fieldValue);
            return this;
          }
        }
    
        throw new IllegalArgumentException("Wow, not having this throw statement is such horrible code fail!");
      }
    }
    

    You can now write:

    public class Main {
      public static void main(String[] args) throws Exception {
        MyClass myObject = MyClass.builder()
          .setFieldValue(builder, "field1", "Hello")
          .setFieldValue(builder, "field2", 123)
          .setFieldValue(builder, "field3", true)
          .build();
    
        System.out.println(myObject);
      }
    }
    

    A note of warning

    This sounds great but seems completely pointless. 'stringly typed' programming is a really bad idea. You break a whole bunch of java's main strengths. For example, editors will not recognize that you're sticking identifier names in strings. If you use refactor scripts to rename a field, one of the strengths of specifically lombok (because you don't have to re-generate all your setters and such), the refactor script will not recognize the change. Even worse, you won't even get a compiler error. Even worse, in your code, silently nothing happens. At least I fixed that grievous error and you get a runtime exception.

    Presumably the names of fields aren't hardcoded in string quotes in your source file; instead they are obtained from some dynamic source (such as user input, an API call, etcetera). But if that's what you have, most of the point of builder is gone. It's a bit odd that you have, say, 4 'hardcoded' properties you want to set, and then 2 more dynamic ones. Which is where builder would be nice.

    This in passing explains why Lombok does not let you specify that you want one of these setFieldValue things in your builders: Disregarding situations where it is easy to rewrite the code to be vastly better styled, this feature basically never comes up1.


    [1] Not opinion; I can veto lombok changes as main contributor and I'd veto this request.