java bean 规范中没有明说的一个小坑

本贴最后更新于 1937 天前,其中的信息可能已经渤澥桑田

随便在百度上搜索 “java bean 规范” 大致能得到如下几条:

  1. JavaBean 类必须是一个公共类,并将其访问属性设置为 public ,如: public class user{ …}
  2. JavaBean 类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器,例如:public User() {…}
  3. 一个 javaBean 类不应有公共实例变量,类变量都为 private ,如: private int id;
  4. javaBean 属性是具有 getter/setter 方法的成员变量。如果属性类型为 boolean 类型,那么读方法的格式可以是 get 或 is。
    例如名为 abc 的 boolean 类型的属性,它的读方法可以是 getAbc(),也可以是 isAbc();
  5. 一般 JavaBean 属性以小写字母开头,驼峰命名格式,相应的 getter/setter 方法是 get/set 接上首字母大写的属性名。

实际上呢还有一条没有明说的,平时大家都是那么做的,无意识中规避掉了,但实际上是强制要求的规则:
set 方法返回值必须为 void!!!

比如说我们有一个 java bean 类 B,它看起来是这样的

public class B {
  private Integer id;
  private String name;

  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    ****this.name = name;
  }
}

一般来说我们会这样初始化 bean:

B b=new B();
b.setId(1);
b.setName("aoeui");

这样写的弊端非常明显,一旦属性多起来就要写很多个 b.setXxx(),而变量名较长的时候手写就非常麻烦了。一般来说我们
会通过插件来自动生成这块代码。那有没有什么其他办法呢?在 effective java 第二章第二条中提到了,我们可以使用构建器
来初始化。于是我们可以将代码改成这样:

public class B {
  private final int id;
  private final String name;
  //getter
  ...
  public static class Builder {
    private int id;
    private String name;

    public Builder() {}
	
    public Builder id(int val) {
      id = val;
      return this;
    }
	
    public Builder name(String val) {
      name = val;
      return this;
    }

    public B builder() {
      return new B(this);
    }
  }

  private B(Builder builder) {
    id = builder.id;
    name = builder.name;
  }
}

然后我们就可以这样初始化一个类了:

B b=new Builder().id(1).name("aoeui").builder();

这样子得到的是一个只读的 java bean,这种写法不但简化了他的初始化操作,而且我们还得到了一个不可变对象。
如果只是想要一个普通的 java bean 的话把属性上的 final 修饰符去掉,然后添加上 setter 方法就行了。
但是这个写法也有问题,虽然调用的时候简单了,但是写这个类的时候却变得更加麻烦了。一般的插件都没有提供
这种格式的 java bean 类生成,如果不去自己写个代码生成器的话就得给每个 bean 类手写构建器,这显然是不可接受的。
那么有没有更简单的方法呢?
有,那就是将 setter 的返回值设置为类型本身,在 setter 方法的末尾直接返回 this:

public class B {
  private Integer id;
  private String name;

  public Integer getId() {
    return id;
  }

  public B setId(Integer id) {
    this.id = id;
    return this;
  }

  public String getName() {
    return name;
  }

  public B setName(String name) {
    this.name = name;
    return this;
  }
}

偶然间在 idea 的代码生成器的 setter template 的下拉框里发现了这种写法

imagepng

第一次看到的时候真的是感觉醍醐灌顶,但后来发现这就是个坑……
用 mybatis 的时候一切都好好的,每次看到那种啰嗦的写法的时候总觉得自己是无比的高明,
直到有一天我用了 commons-dbutils……

imagepng

所有获取到的数据都会变成这样!
一开始以为是环境出了问题取不到数据,于是重建一个项目,一个个的排除,结果一无所获。
折腾了半天,最后发现问题出在 dbutils 的 BeanProcessor 类的 propertyDescriptors 中。
dbutils 在使用结果集创建对象的时候,首先会用无参构造器创建一个空对象,然后用 setter 方法把值填进去。
而这些 setter 方法的信息就是从 propertyDescriptors 中得到的

private PropertyDescriptor[] propertyDescriptors(Class<?> c)
  throws SQLException {
  // Introspector caches BeanInfo classes for better performance
  ****BeanInfo beanInfo = null;
  try {
  beanInfo = Introspector.getBeanInfo(c);

  } catch (IntrospectionException e) {
  throw new SQLException(
  "Bean introspection failed: " + e.getMessage());
  }

  return beanInfo.getPropertyDescriptors();
}

这就是 dbutils 和 mybatis 结果不一样的原因。
dbutils 并不是使用反射取获取 setter 方法,而是通过 java.beans.Introspector 这个类的 getBeanInfo 方法获取的。

imagepng

查看这个 beanInfo 的状态可以发现,根本就没有 setter 方法!
可以看出 java bean 的规范非常严格,仅仅因为返回值是 void 就认为这个类没有 setter 方法,即使返回值并不是方法签名的一部分。
也就是说我们这个类只是一个普通的 POJO 类,并不是一个通用的 java bean 类。

至于说哪种写法好嘛,我觉得后面这种链式调用("method chaining”或者"fluent interface")风格的 api 显然更好,更加的易读。
这种设计风格也正在变得流行,但是这种风格和 bean 规范不兼容,并且也很可能导致现有的库和框架失效失效
(没有记错的话 spring 是遵循了 bean 规范的)。还有一个比较容易出错的地方是这和 java8 的 Consumer接口是有冲突的,
如果打算在接受该接口作为参数的地方以方法引用的方式使用了一个实践了这种风格的 pojo 类的话。

所以还是老老实实按照标准的来吧,虽然说写法看起来是比较繁琐与啰嗦,但是 java bean 毕竟一个应用非常广泛的规范。
没有办法保证项目中用到的组件在使用 bean 类时会通过反射去获取方法。如果按照这种写法来,万一哪里用到了
Introspector.getBeanInfo 这个方法,出了问题 debug 起来就非常困难了,习惯了这种写法后,下意识的会认为
这里没有问题,毕竟在其他地方都用的好好的。

至于说标准写法有什么好处嘛,也确实是有一个,那就是语义十分的清晰,我们可以很明显的认识到这是在操作同一个对象。
链式调用并没有保证说返回的是对象自身还是一个新的对象,因为在 pojo 中大多数情况是返回 this,但是在很多其他用
途的地方返回的是一个新的对象(比如 java8 的 stream api 中),甚至可以故意返回一个当前对象有同样类型且属性的值
也完全相同的新对象来迷惑使用者!也就是说保不准有人这么写(在有多参数构造器的情况下):

public B id(int id) {
  return new B(id);
}

你以为你设置了值,但是每次返回的都是不同的对象,这样链式调用就失效了,而 debug 的时候也很难意识到这个问题。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3168 引用 • 8207 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • 黑曜石

    黑曜石是一款强大的知识库工具,支持本地 Markdown 文件编辑,支持双向链接和关系图。

    A second brain, for you, forever.

    10 引用 • 85 回帖
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    169 引用 • 799 回帖
  • DevOps

    DevOps(Development 和 Operations 的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。

    40 引用 • 24 回帖
  • 互联网

    互联网(Internet),又称网际网络,或音译因特网、英特网。互联网始于 1969 年美国的阿帕网,是网络与网络之间所串连成的庞大网络,这些网络以一组通用的协议相连,形成逻辑上的单一巨大国际网络。

    96 引用 • 330 回帖
  • 知乎

    知乎是网络问答社区,连接各行各业的用户。用户分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

    10 引用 • 66 回帖
  • 服务

    提供一个服务绝不仅仅是简单的把硬件和软件累加在一起,它包括了服务的可靠性、服务的标准化、以及对服务的监控、维护、技术支持等。

    41 引用 • 24 回帖 • 6 关注
  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖 • 3 关注
  • Docker

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。

    476 引用 • 899 回帖
  • 电影

    这是一个不能说的秘密。

    120 引用 • 597 回帖 • 2 关注
  • Gitea

    Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证。

    4 引用 • 16 回帖 • 1 关注
  • gRpc
    10 引用 • 8 回帖 • 55 关注
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    324 引用 • 1395 回帖
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    49 引用 • 192 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    6560 引用 • 29453 回帖 • 247 关注
  • Love2D

    Love2D 是一个开源的, 跨平台的 2D 游戏引擎。使用纯 Lua 脚本来进行游戏开发。目前支持的平台有 Windows, Mac OS X, Linux, Android 和 iOS。

    14 引用 • 53 回帖 • 513 关注
  • Pipe

    Pipe 是一款小而美的开源博客平台。Pipe 有着非常活跃的社区,可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动(具体细节请浏览 B3log 构思 - 分布式社区网络)。

    这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!

    131 引用 • 1114 回帖 • 151 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    14 引用 • 7 回帖
  • Flume

    Flume 是一套分布式的、可靠的,可用于有效地收集、聚合和搬运大量日志数据的服务架构。

    9 引用 • 6 回帖 • 595 关注
  • Mac

    Mac 是苹果公司自 1984 年起以“Macintosh”开始开发的个人消费型计算机,如:iMac、Mac mini、Macbook Air、Macbook Pro、Macbook、Mac Pro 等计算机。

    164 引用 • 594 回帖
  • HHKB

    HHKB 是富士通的 Happy Hacking 系列电容键盘。电容键盘即无接点静电电容式键盘(Capacitive Keyboard)。

    5 引用 • 74 回帖 • 407 关注
  • Ruby

    Ruby 是一种开源的面向对象程序设计的服务器端脚本语言,在 20 世纪 90 年代中期由日本的松本行弘(まつもとゆきひろ/Yukihiro Matsumoto)设计并开发。在 Ruby 社区,松本也被称为马茨(Matz)。

    7 引用 • 31 回帖 • 175 关注
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    396 引用 • 3416 回帖
  • 小说

    小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。

    28 引用 • 108 回帖
  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    164 引用 • 407 回帖 • 526 关注
  • 强迫症

    强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。

    15 引用 • 161 回帖 • 4 关注
  • 学习

    “梦想从学习开始,事业从实践起步” —— 习近平

    161 引用 • 473 回帖