返回

Lombok Builder 构建嵌套 List 成员的最佳实践

java

Lombok 生成嵌套 List 成员的 Builder

遇到了一个 Lombok Builder 的问题,我想为一个 Spring JDBC 的 SimpleJdbcCall 生成配置信息,但是遇到了一些障碍。

我有一个用 Lombok 生成的类:

@Data
@Builder
public class DatabaseCallInformation {

    private String dbPackage;
    private String dbProcedure;
    private List<InParam> inParams;
    private List<OutParam> outParams;

    @Data
    @Builder
    public static class InParam {
        
        private String paramName;
        private int sqlType;
        private String value;

    }

    @Data
    @Builder
    public static class OutParam {
        
        private String paramName;
        private int sqlType;
        private SqlReturnType sqlReturnType;
        private RowMapper<?> rowMapper;

    }
}

我希望能够这样调用 builder:

    DatabaseCallInformation result = DatabaseCallInformation.builder()
        .dbPackage("MY_PACKAGE")
        .dbProcedure("MY_PROCEDURE")
        .inparam()
            .paramName("ID")
            .sqlType(Types.NUMBER)
        .and()
        // 可能还有更多 inparams
        .outParam()
            .paramName("result")
            .sqlType(Types.VARCHAR)
        .and()
        // 可能还有更多 outParams
        .build()

我需要帮助来实现 inParam()outParam()and() 方法(不一定是这几个方法名,能实现类似功能即可)。 主要目标是能流畅地构建 DatabaseCallInformation 对象,并能方便地添加多个 InParamOutParam 对象。

问题根源

Lombok 的 @Builder 注解默认不会为 List 类型的成员生成添加单个元素的 builder 方法。 它只会生成设置整个 List 的方法,用起来不太灵活。我们需要自定义构建逻辑,来达到类似链式添加元素的效果。

解决方案

以下提供几种解决方案。

方案一: 使用 @Builder.Default 和 自定义 Builder

  1. 原理: 我们可以使用 @Builder.Default 注解给 inParamsoutParams 字段一个默认值 (空的 ArrayList)。然后,重写 Builder 类中的方法, 以便更灵活的添加inParamsoutParams 元素.

  2. 代码示例:

import lombok.Builder;
import lombok.Data;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;

@Data
@Builder
public class DatabaseCallInformation {

    private String dbPackage;
    private String dbProcedure;
    @Builder.Default
    private List<InParam> inParams = new ArrayList<>();
    @Builder.Default
    private List<OutParam> outParams = new ArrayList<>();

    @Data
    @Builder
    public static class InParam {

        private String paramName;
        private int sqlType;
        private String value;

    }

    @Data
    @Builder
    public static class OutParam {

        private String paramName;
        private int sqlType;
      //  private SqlReturnType sqlReturnType;   Assuming you will include, not defined
       // private RowMapper<?> rowMapper;  Assuming you will include, not defined

    }
    
    public static class DatabaseCallInformationBuilder {
        private InParamBuilder inParamBuilder;
        private OutParamBuilder outParamBuilder;
      

        public InParamBuilder inparam() {
            if (inParamBuilder == null) {
                inParamBuilder = new InParamBuilder(this);
            }
            return inParamBuilder;
        }
          
          public OutParamBuilder outParam() {
            if (outParamBuilder == null) {
                outParamBuilder = new OutParamBuilder(this);
            }
            return outParamBuilder;
        }
        
        public  DatabaseCallInformationBuilder addInParam(InParam inParam){
            this.inParams.add(inParam);
            return this;
        }

         public  DatabaseCallInformationBuilder addOutParam(OutParam outParam){
            this.outParams.add(outParam);
             return this;
        }
    }

      public static class InParamBuilder {
        private final DatabaseCallInformationBuilder parent;
        private String paramName;
        private int sqlType;
        private String value;

        InParamBuilder(DatabaseCallInformationBuilder parent) {
            this.parent = parent;
        }

        public InParamBuilder paramName(String paramName) {
            this.paramName = paramName;
            return this;
        }

        public InParamBuilder sqlType(int sqlType) {
            this.sqlType = sqlType;
            return this;
        }
        
          public InParamBuilder value(String value) {
            this.value = value;
            return this;
        }

        public DatabaseCallInformationBuilder and() {
            parent.addInParam(new InParam(paramName, sqlType, value));
            return parent;
        }
    }
      
       public static class OutParamBuilder {
        private final DatabaseCallInformationBuilder parent;
        private String paramName;
        private int sqlType;

        OutParamBuilder(DatabaseCallInformationBuilder parent) {
            this.parent = parent;
        }

        public OutParamBuilder paramName(String paramName) {
            this.paramName = paramName;
            return this;
        }

        public OutParamBuilder sqlType(int sqlType) {
            this.sqlType = sqlType;
            return this;
        }

        public DatabaseCallInformationBuilder and() {
            parent.addOutParam(new OutParam(paramName, sqlType, null, null)); // replace null with other actual values for returnType, and rowMapper
            return parent;
        }
    }

     public static void main(String[] args) {
        DatabaseCallInformation result = DatabaseCallInformation.builder()
        .dbPackage("MY_PACKAGE")
        .dbProcedure("MY_PROCEDURE")
        .inparam()
            .paramName("ID")
            .sqlType(Types.NUMBER)
        .and()
           .inparam()
            .paramName("NAME")
            .sqlType(Types.VARCHAR)
            .value("TEST_NAME")
        .and()
        .outParam()
            .paramName("result")
            .sqlType(Types.VARCHAR)
        .and()
        .build();
         
         System.out.println(result);
    }

}

  1. 进阶技巧 : 可以创建泛型方法来添加 inParamoutParam 以减少冗余。
      //Inside DatabaseCallInformationBuilder
       private <T> DatabaseCallInformationBuilder addParam(List<T> list, T param) {
        list.add(param);
        return this;
       }
    
    public DatabaseCallInformationBuilder addInParam(InParam inParam){
            return addParam(this.inParams, inParam);
     }
    
      public DatabaseCallInformationBuilder addOutParam(OutParam outParam){
           return addParam(this.outParams, outParam);
     }
    

方案二: 使用自定义构建器方法(不重写Lombok的builder方法)

  1. 原理: 不修改 Lombok 生成的 Builder 类的基本结构,而是创建额外的静态方法,来辅助创建 InParamOutParam 对象,并在 DatabaseCallInformation 对象的构建过程中将它们添加到列表中。

  2. 代码示例:

import lombok.Builder;
import lombok.Data;

import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Data
@Builder
public class DatabaseCallInformation {

    private String dbPackage;
    private String dbProcedure;
    private List<InParam> inParams;
    private List<OutParam> outParams;

    @Data
    @Builder
    public static class InParam {

        private String paramName;
        private int sqlType;
        private String value;
    }

    @Data
    @Builder
    public static class OutParam {
        private String paramName;
        private int sqlType;
       // private SqlReturnType sqlReturnType; // place holders for required fields, which can't be null in reality
       // private RowMapper<?> rowMapper;  // place holders for required fields, which can't be null in reality

    }
  // Static helper methods to build the lists:

    public static InParam buildInParam(String paramName, int sqlType, String value) {
      return  InParam.builder().paramName(paramName).sqlType(sqlType).value(value).build();
    }

     public static OutParam buildOutParam(String paramName, int sqlType) {  // added return types for real fields
      return  OutParam.builder().paramName(paramName).sqlType(sqlType).build(); // replace the nulls
    }
  
   public static void main(String[] args) {
         DatabaseCallInformation result = DatabaseCallInformation.builder()
                .dbPackage("MY_PACKAGE")
                .dbProcedure("MY_PROCEDURE")
                .inParams(Arrays.asList(
                        buildInParam("ID", Types.INTEGER, "123"),
                        buildInParam("NAME", Types.VARCHAR, "John")
                ))
                .outParams(List.of( // Use List.of for immutability, or Arrays.asList
                       buildOutParam("result", Types.VARCHAR)

                ))
                .build();

       System.out.println(result);

    }
}
  1. 好处 : 这个方案比较简单,不需要修改生成的 Builder 类,只需要添加一些辅助的静态方法。

  2. 局限性: 它不像第一种方案那样流畅。添加参数的动作是独立的, 不在一个链上.

方案三: 完全自定义Builder

  1. 原理: 如果对构建过程有非常特殊的需求,Lombok 的 @Builder 无法满足, 就只能完全抛弃Lombok, 完全自己动手实现 Builder 类。虽然繁琐,但是拥有最大的灵活性。

  2. 代码示例: (仅展示关键部分, 需要根据你的实际情况补全)

import lombok.Data;

import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
@Data //only Data, remove Builder.
public class DatabaseCallInformation {

    private String dbPackage;
    private String dbProcedure;
    private List<InParam> inParams;
    private List<OutParam> outParams;

     @Data // keep data
    @Builder  //and builder on sub classes
    public static class InParam {

        private String paramName;
        private int sqlType;
        private String value;
    }
   
     @Data // keep data
    @Builder  //and builder on sub classes
    public static class OutParam {

        private String paramName;
        private int sqlType;
       // private SqlReturnType sqlReturnType;
       // private RowMapper<?> rowMapper;
    }

    //  The completely custom builder.

      public static class Builder{
        private String dbPackage;
        private String dbProcedure;
        private final List<InParam> inParams = new ArrayList<>();
        private final List<OutParam> outParams = new ArrayList<>();

           public Builder dbPackage(String dbPackage) {
                this.dbPackage = dbPackage;
                return this;
            }
    
            public Builder dbProcedure(String dbProcedure) {
                this.dbProcedure = dbProcedure;
                return this;
            }
            // inner builders.
              public InParamBuilder inParam() {
                  return new InParamBuilder(this);
                }

            public OutParamBuilder outParam() {
                return new OutParamBuilder(this);
            }

          public  Builder addInParam(InParam param) {
                this.inParams.add(param);
                return this;
           }

            public  Builder addOutParam(OutParam param) {
            this.outParams.add(param);
            return this;
           }
    
            public DatabaseCallInformation build() {
                DatabaseCallInformation info = new DatabaseCallInformation();
                info.setDbPackage(dbPackage);
                info.setDbProcedure(dbProcedure);
                info.setInParams(inParams);
                info.setOutParams(outParams);
                return info;
           }

       }

     // inner classes
    public static class InParamBuilder {
      private final Builder parent;
      private String paramName;
        private int sqlType;
        private String value;

        public InParamBuilder(Builder parent) {
            this.parent = parent;
        }

        public InParamBuilder paramName(String name){
            this.paramName = name;
            return this;
        }
         public InParamBuilder sqlType(int type){
            this.sqlType = type;
            return this;
        }
        
         public InParamBuilder value(String value){
            this.value = value;
            return this;
        }

        public Builder and(){
           parent.addInParam(new InParam(paramName, sqlType, value));
            return parent;
        }
    }
      
      
      public static class OutParamBuilder {
          private final Builder parent;
            private String paramName;
            private int sqlType;

          public OutParamBuilder(Builder parent) {
              this.parent = parent;
          }
            public OutParamBuilder paramName(String name){
            this.paramName = name;
            return this;
           }
          
             public OutParamBuilder sqlType(int type){
            this.sqlType = type;
            return this;
        }

            public Builder and() {
                parent.addOutParam(new OutParam(this.paramName, this.sqlType, null, null));  // other params.
                return parent;
            }
      }
      
      // main
      public static void main(String[] args) {

          DatabaseCallInformation result = new DatabaseCallInformation.Builder()
                    .dbPackage("my_package")
                    .dbProcedure("my_procedure")
                      .inParam()
                         .paramName("my_in_param")
                         .sqlType(Types.VARCHAR)
                         .value("a value")
                      .and()
                    .outParam()
                         .paramName("my_out_param")
                         .sqlType(Types.INTEGER)
                      .and()
                    .build();

        }
}
  1. 注意事项: 自己实现构建器需要仔细考虑所有可能的情况,并编写更多的代码,包括处理默认值、线程安全等问题(如果需要)。

总结

处理 Lombok Builder 中 List 成员的构建问题,主要思路就是自定义构建逻辑。方案一比较推荐,既使用了Lombok, 也增加了构建的灵活性. 具体选择哪种方案,取决于你的项目需求和个人偏好。如果对构建过程没有特别复杂的要求,方案二也可以是一个简洁的选择。方案三, 完全自定义Builder可以应对各种需求,代价是代码较多。