您现在的位置是:亿华云 > 热点
请谨慎使用 @Builder 注解!
亿华云2025-10-03 04:21:56【热点】1人已围观
简介来源:阿里云开发者阿里妹导读本文主要指出@Builder存在的一些问题,指出它并不是链式编程的最佳实践。一、前言前一段时间写过一篇《使用 lombok @Builder 注解,设置默认值时要千万小心!
来源:阿里云开发者
阿里妹导读
本文主要指出 @Builder 存在的一些问题,指出它并不是慎使链式编程的最佳实践。
一、注解前言
前一段时间写过一篇 《使用 lombok @Builder 注解,请谨设置默认值时要千万小心!慎使》 的注解文章,文章提到使用 @Builder 注解将会导致设置的请谨默认值失效问题。最近读了一篇文章:《Oh !! Stop using @Builder》[1]也颇受启发,慎使发现很多人的注解确被 @Builder 注解的名字误导了。大多数同学使用 @Builder 无非就是请谨为了链式编程,然而 @Builder 并不是慎使链式编程的最佳实践,它会额外创建内部类,注解存在继承关系时还需要使用 @SuperBuilder 注解[2],请谨设置默认值时也需要额外的慎使 @Builder.Default 去设置默认值[3],无疑增加了很多不必要的注解复杂度。有些同学可能说如果你把这些问题都了解就不会遇到这些坑,没必要“因噎废食”,可是这些问题已经让无数人一次次趟坑,说明它并不是最佳实践,如果有更简便办法可以实现链式编程,网站模板又何必徒增这么多复杂度呢?其实很多人选择使用 @Builder 实现链式编程,无非是它“更常见”和“最熟悉” !二、为什么?
(1)@Builder 生成的构造器不是完美的,它不能区分哪些参数是必须的,哪些是可选的。如果没有提供必须的参数,构造器可能会创建出不完整或者不合法的对象。可以看第三部分的例子, @Builder 注解产生的 Builder 类的构造方法默认并不能限定必传参数。(2)很多人 喜欢 @Builder 和 @Data 搭配使用,导致生成的构造器是可变的,它允许使用 setter 方法修改构造器的状态。这违反了构造器模式的原则,构造器应该是源码下载不可变的,一旦创建就不能被修改。如果非要使用 @Builder ,那么不要用 @Data ,要用 @Getter。相对来说,反而 @Accessors 的行为更符合这个要求。(3)@Builder 生成的构造器不适合用于短暂的对象,它会增加代码的复杂度和冗余。构造器模式更适合用于生命周期较长、有多种变体的对象。实际使用中经常发现 @Builder 滥用的情况,有些仅仅一两个属性的类也都要用 @Builder,真的没必要用,直接用全参的构造方法都比这更简洁。(4)@Builder 生成的构造器不能处理抽象类型的参数,它只能接受具体类型的对象。这限制了构造器的灵活性和扩展性,不能根据不同的源码库需求创建不同风格的对象。(5)继承关系时,子类需要使用 @SuperBuilder。对象继承后,子类的 Builder 因为构造函数的问题,使用不当大概率会报错,并且无法设置父类的属性,还需要使用 @SuperBuilder 来解决问题。(6)设置默认值需要使用 @Builder.Default。很容易因为对此不了解,导致默认值不符合预期导致出现 BUG。三、怎么做?
正如 《Oh !! Stop using @Builder》 所推荐的一样,建议使用 @Accessors(chain = true) 来代替。3.1 不使用 @Builder
package io.gitrebase.demo;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublic class APIResponse<T> { private T payload; private Status status;}使用示例:
package io.gitrebase.demo;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvice(assignableTypes = io.gitrebase.demo.RestApplication.class)public class ApplicationExceptionHandler { @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public APIResponse handleException(Exception exception) { log.error("Unhandled Exception", exception); Status status = new Status(); status.setResponseCode("RESPONSE_CODE_IDENTIFIER"); status.setDescription("Bla Bla Bla"); APIResponse response = new APIResponse(); response.setStatus(status); return response; }}3.2 使用 @Builder
package io.gitrebase.demo;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Builder@Data@NoArgsConstructor@AllArgsConstructorpublic class APIResponse<T> { private T payload; private Status status;}等价代码:
package io.gitrebase.demo;import lombok.AccessLevel;import lombok.AllArgsConstructor;import lombok.Getter;@Getter// Make constructor private so object cannot be created outside the class// Allowing only PersonBuilder.build to create an instance of the class@AllArgsConstructor(access = AccessLevel.PRIVATE)public class Person { private String firstName; private String lastName; private String email; private String mobile; // PersonBuilder can only be created with firstName, lastName // which implies that Person object ll always be instantiated with firstName & lastName public static PersonBuilder builder(String firstName, String lastName) { return new PersonBuilder(firstName, lastName); } public static class PersonBuilder { private final String firstName; private final String lastName; private String email; private String mobile; // Make constructor private so object cannot be created outside the class // Allowing only Person.builder to create an instance of PersonBuilder private PersonBuilder(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public PersonBuilder email(String email) { this.email = email; return this; } public PersonBuilder mobile(String mobile) { this.mobile = mobile; return this; } public Person build() { return new Person(firstName, lastName, email, mobile); } }}使用示例:
package io.gitrebase.demo;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)public class ApplicationExceptionHandler { @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public APIResponse handleException(Exception exception) { log.error("Unhandled Exception", exception); return APIResponse.builder().status(Status.builder() .responseCode("RESPONSE_CODE_IDENTIFIER") .description("Bla Bla Bla") .build()) .build(); }}3.3 使用 @Accessors 注解
package io.gitrebase.demo;import lombok.Data;import lombok.experimental.Accessors;@Data@Accessors(chain = true)public class APIResponse<T> { private T payload; private Status status;}等价代码:
package io.gitrebase.demo;import lombok.experimental.Accessors;@Accessors(chain = true)public class APIResponse<T> { private T payload; private Status status; public T getPayload() { return this.payload; } public APIResponse<T> setPayload(T payload) { this.payload = payload; return this; } public Status getStatus() { return this.status; } public APIResponse<T> setStatus(Status status) { this.status = status; return this; }}使用示例:
package io.gitrebase.demo;import lombok.extern.slf4j.Slf4j;import lombok.var;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)public class ApplicationExceptionHandler { @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public APIResponse handleException(Exception exception) { log.error("Unhandled Exception", exception); var status = new Status().setResponseCode("RESPONSE_CODE_IDENTIFIER").setDescription("Bla Bla Bla"); return new APIResponse().setStatus(status); }}原文中给的示例相对简单,实际运用时,如果属性较多,且分为必传属性和选填属性时,可以将必传参数定义在构造方法中,加上 @Accessors 注解,这样就可以实现必传参数的传入,又可以实现选填参数的链式调用。
假设 Student 类,它的 学生ID和年级和班级是必填的,姓名、性别、住址是选填的,那么示例代码如下:import lombok.Accessors;// 使用 @Accessors 注解,设置 chain = true,生成返回 this 的 setter 方法@Accessors(chain = true)public class Student { // 定义必传属性,使用 final 修饰,不提供 setter 方法 private final int studentId; // 学生ID private final int grade; // 年级 private final int classNum; // 班级 // 定义选填属性,提供 setter 方法 private String name; // 姓名 private String gender; // 性别 private String address; // 住址 // 定义构造方法,接收必传参数 public Student(int studentId, int grade, int classNum) { this.studentId = studentId; this.grade = grade; this.classNum = classNum; } // 省略 getter 和 setter 方法}// 使用示例Student student = new Student(1001, 3, 8) // 创建一个学生对象,传入必传参数 .setName("张三") // 设置姓名 .setGender("男") // 设置性别 .setAddress("北京市朝阳区"); // 设置住址另外该注解还有很多设置项,感兴趣可以进一步研究:
/** * A container for settings for the generation of getters, setters and "with"-ers. * <p> * Complete documentation is found at <a href="https://projectlombok.org/features/experimental/Accessors">the project lombok features page for @Accessors</a>. * <p> * Using this annotation does nothing by itself; an annotation that makes lombok generate getters, setters, or "with"-ers * such as { @link lombok.Setter} or { @link lombok.Data} is also required. */@Target({ ElementType.TYPE, ElementType.FIELD})@Retention(RetentionPolicy.SOURCE)public @interface Accessors { /** * If true, accessors will be named after the field and not include a { @code get} or { @code set} * prefix. If true and { @code chain} is omitted, { @code chain} defaults to { @code true}.<br> * NB: This setting has no effect on { @code @With}; they always get a "with" prefix.<br> * <strong>default: false</strong> * * @return Whether or not to make fluent methods (named { @code fieldName()}, not for example { @code setFieldName}). */ boolean fluent() default false; /** * If true, setters return { @code this} instead of { @code void}. * <strong>default: false</strong>, unless { @code fluent=true}, then <strong>default: true</strong> * * @return Whether or not setters should return themselves (chaining) or { @code void} (no chaining). */ boolean chain() default false; /** * If true, generated accessors will be marked { @code final}. * <strong>default: false</strong> * * @return Whether or not accessors should be marked { @code final}. */ boolean makeFinal() default false; /** * If present, only fields with any of the stated prefixes are given the getter/setter treatment. * Note that a prefix only counts if the next character is NOT a lowercase character or the last * letter of the prefix is not a letter (for instance an underscore). If multiple fields * all turn into the same name when the prefix is stripped, an error will be generated. * * @return If you are in the habit of prefixing your fields (for example, you name them { @code fFieldName}, specify such prefixes here). */ String[] prefix() default { };}3.4 使用 IDEA 默认 Setter 生成功能
如果有些同学担心 @Accessors 在 lombok.experimental 包不够“稳定”,可以参考 @Accessors 的功能自己写一份即可。如果嫌麻烦可以调用 Generate 快捷键(mac 上 Command +N)选择生成 Getter、Setter ,其中 Setter 选择 Builder 即可实现 set 方法返回 this 的效果,干净纯粹,无其他依赖!四、启发
有些同学认为如果能够深入了解 @Builder 注解的每个细节,包括如何正确设置默认值,那么就不容易出错。可是这无疑增加了额外的复杂度,我们常见的使用 @Builder 注解无非就是为了链式编程,而 @Accessors 就可以轻松实现这个效果而且还可以避免底层创建一个 Builder 对象,也可以避免默认值的坑,而且当类存在继承关系时,还需要使用 @SuperBuilder 注解,问题的复杂度又提高了。所以,何必执着于使用 @Builder 注解呢?本文主要指出 @Builder 存在的一些问题,指出它并不是链式编程的最佳实践。但如果你对 @Builder 了如指掌,情有独钟,继续使用无可厚非,如果你认为这的确徒增了很多不必要的复杂度,可以考虑使用 @Accessors 或者自己手动改造 Setter 方法。大家在使用 lombok 注解时,必须对每个注解的效果非常了解,推荐大家主动查看编译后的代码,也可以使用 JClaslib 等插件查看对应的字节码,避免因为理解偏差造成误用。另外,“大家都这么用”、“工具库提供的功能” 并不意味着就是对的(否则很多知名工具类库中也不会有各种 @Deprecated 标识的过期类了)或者是“对的” 但不是最好的。大家在使用某个功能时要独立思考和判断能力,如果不合理可以进行改进。参考链接:
[1]https://medium.com/gitrebase/oh-stop-using-builder-9061a5911d8c
[2]https://www.baeldung.com/lombok-builder-inheritance
[3]https://www.baeldung.com/lombok-builder-default-value很赞哦!(26444)
相关文章
- 如何应对气候危机对数据中心的影响?
- 换新域名(重新来过)
- 便宜域名使用如何?小白可以买到便宜域名吗?
- 2. 不要花大价钱买域名,新手鉴别能力不足,容易投资失误。
- 2023年阻碍数据中心行业增长的五大挑战
- 只要我们做的是从目前的市场情况选择域名,从简单易记,从个性特征上,我们就可以找到一个好域名进行注册。域名注册进行域名记录和解析以及绑定网站后,客户可以通过URL登录您的网站。
- 前面这两个步骤都是在本机完成的。到这里还没有涉及真正的域名解析服务器,如果在本机中仍然无法完成域名的解析,就会真正请求域名服务器来解析这个域名了。
- 域名不仅仅是一个简单的网站。对于有长远眼光的公司来说,在运营网站之前确定一个优秀的域名对有长远眼光的公司来说是非常重要的。这对今后的市场营销、产品营销和企业品牌建设都具有十分重要的意义。优秀的域名是企业在市场竞争中获得持久优势的利器。
- 数据中心的电力需求使伦敦的新房建设更加困难
- 3.dns修改成功后,点击“域名解析”,按提示进行操作。解析格式一般如下:
热门文章
站长推荐
数据中心迁移具有哪些优秀实践?
公司名字不但要与其经营理念、活动识别相统一,还要能反映公司理念,服务宗旨、商品形象,从而才能使人看到或听到公司的名称就能产生愉快的联想,对商店产生好感。这样有助于公司树立良好的形象。
3、商标域名一经注册,就可以作为域名裁决过程中的主要信息之一。这可以大大增加公司被抢注的相关域名胜诉的机会。
在众多公司中,如果我们必须选择一家可信的公司,那当然是信得过的。
数据中心在云端和未来的优势
换新域名(重新来过)
a、变更前的公司证件扫描件(代码证或者营业执照)及联系人身份证复印件、变更后的公司证件扫描件(代码证或者营业执照)及新的联系人身份证复印件;身份证复印件需本人签名,公司证件复印件需加盖公章。
2016年1月1日:注册价格将降至每年7欧元。