Budius
Budius

Reputation: 39856

How to run code after constructor in a Lombok builder

I have a class that I want to use Lombok.Builder and I need pre-process of some parameters. Something like this:

@Builder
public class Foo {
   public String val1;
   public int val2;
   public List<String> listValues;

   public void init(){
       // do some checks with the values.
   }
}

normally I would just call init() on a NoArg constructor, but with the generated builder I'm unable to do so. Is there a way for this init be called by the generated builder? For example build() would generate a code like:

public Foo build() {
   Foo foo = Foo(params....)
   foo.init();
   return foo;
}

I'm aware that I can manually code the all args constructor, that the Builder will call through it and I can call init inside there.

But that is a sub-optimal solution as my class will likely have new fields added every once in a while which would mean changing the constructor too.

Upvotes: 38

Views: 30847

Answers (5)

IndustryUser1942
IndustryUser1942

Reputation: 1255

Extending Budius answer, with Roel Spilker suggestion in a comment

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(buildMethodName = "buildInternal")
public class Foo {

    public String val1;
    public int val2;
    @Singular public List<String> listValues;

    void init() {
        // perform values initialisation
    }

    public static class FooBuilder {

        // rest of the class is generated by lombok

        @Override public Foo build() {
            Foo foo = buildInternal();
            foo.init();
            return foo;
        }
    }
}

Upvotes: 0

Marc
Marc

Reputation: 440

This works for me, not a complete solution, but quick and easy.

@Builder
@AllArgsConstructor
public class Foo {
   @Builder.Default
   int bar = 42;
   Foo init() {
      // perform values initialisation
     bar = 451;   // replaces 314
     return foo;
   }
   static Foo test() {
       return new FooBuilder()  // defaults to 42
           .bar(314)  // replaces 42 with 314
           .build()
           .init();   // replaces 314 with 451
   }
}

Upvotes: -2

J.R.
J.R.

Reputation: 701

I just stumbled upon the same issue. But additionally, I wanted to add an method buildOptional() to the builder to not repeat Optional.of(Foo) each time I need it. This did not work with the approach posted before because the chained methods return FooInternalBuilder objects; and putting buildOptional() into FooInternalBuilder would miss the init() method execution in Builder...

Also, I personally did not like the presence of 2 builder classes.

Here is what I did instead:

@Builder(buildMethodName = "buildInternal")
@ToString
public class Foo {
    public String val1;
    public int val2;
    @Singular  public List<String> listValues;

    public void init(){
        // do some checks with the values.
    }    

    /** Add some functionality to the generated builder class */
    public static class FooBuilder {
        public Optional<Foo> buildOptional() {
            return Optional.of(this.build());
        }

        public Foo build() {
            Foo foo = this.buildInternal();
            foo.init();
            return foo;
        }
    }
}

You can do a quick test with this main method:

public static void main(String[] args) {
    Foo foo = Foo.builder().val1("String").val2(14)
            .listValue("1").listValue("2").build();
    System.out.println(foo);

    Optional<Foo> fooOpt = Foo.builder().val1("String").val2(14)
            .listValue("1").listValue("2").buildOptional();
    System.out.println(fooOpt);
}

Doing so let's you add what I want:

  • Add an init() method which is executed after each object construction automatically
  • Adding new fields do not require additional work (as it would be for an individually written constructor)
  • Possibility to add additional functionality (incl. the init() execution)
  • Retain the complete standard functionality the @Builder annotation brings
  • Don't expose an additional builder class

Even if you solved your problem before I like to share this as the solution. It is a bit shorter and adds a (for me) nice feature.

Upvotes: 13

Roel Spilker
Roel Spilker

Reputation: 34562

In Foo you could manually add a constructor, have that do the initialization, and put @Builder on the constructor. I know that you already know this, but I think it is the right solution, and you won't forget to add the parameter since you do want to use the code in the builder anyway.

Disclosure: I am a lombok developer.

Upvotes: 24

Budius
Budius

Reputation: 39856

After much trial and end error I found a suitable solution: extend the generate builder and call init() myself.

Example:

@Builder(toBuilder = true, builderClassName = "FooInternalBuilder", builderMethodName = "internalBuilder")
public class Foo {

   public String val1;
   public int val2;
   @Singular public List<String> listValues;

   void init() {
      // perform values initialisation
   }

   public static Builder builder() {
      return new Builder();
   }

   public static class Builder extends FooInternalBuilder {

      Builder() {
         super();
      }

      @Override public Foo build() {
         Foo foo = super.build();
         foo.init();
         return foo;
      }
   }
}

Upvotes: 22

Related Questions