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

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

随便在百度上搜索 “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 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8208 回帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖 • 1 关注
  • ReactiveX

    ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的 API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。

    1 引用 • 2 回帖 • 141 关注
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖
  • Mobi.css

    Mobi.css is a lightweight, flexible CSS framework that focus on mobile.

    1 引用 • 6 回帖 • 708 关注
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 82 关注
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    69 引用 • 190 回帖 • 483 关注
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖
  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    311 引用 • 546 回帖 • 1 关注
  • 游戏

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

    171 引用 • 813 回帖
  • Sandbox

    如果帖子标签含有 Sandbox ,则该帖子会被视为“测试帖”,主要用于测试社区功能,排查 bug 等,该标签下内容不定期进行清理。

    379 引用 • 1221 回帖 • 590 关注
  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 2 关注
  • 负能量

    上帝为你关上了一扇门,然后就去睡觉了....努力不一定能成功,但不努力一定很轻松 (° ー °〃)

    87 引用 • 1206 回帖 • 449 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    140 引用 • 441 回帖
  • PostgreSQL

    PostgreSQL 是一款功能强大的企业级数据库系统,在 BSD 开源许可证下发布。

    22 引用 • 22 回帖 • 1 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1458 回帖 • 118 关注
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 44 关注
  • RESTful

    一种软件架构设计风格而不是标准,提供了一组设计原则和约束条件,主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

    30 引用 • 114 回帖
  • TextBundle

    TextBundle 文件格式旨在应用程序之间交换 Markdown 或 Fountain 之类的纯文本文件时,提供更无缝的用户体验。

    1 引用 • 2 回帖 • 45 关注
  • gRpc
    10 引用 • 8 回帖 • 54 关注
  • 国际化

    i18n(其来源是英文单词 internationalization 的首末字符 i 和 n,18 为中间的字符数)是“国际化”的简称。对程序来说,国际化是指在不修改代码的情况下,能根据不同语言及地区显示相应的界面。

    7 引用 • 26 回帖
  • 旅游

    希望你我能在旅途中找到人生的下一站。

    86 引用 • 896 回帖
  • 百度

    百度(Nasdaq:BIDU)是全球最大的中文搜索引擎、最大的中文网站。2000 年 1 月由李彦宏创立于北京中关村,致力于向人们提供“简单,可依赖”的信息获取方式。“百度”二字源于中国宋朝词人辛弃疾的《青玉案·元夕》词句“众里寻他千百度”,象征着百度对中文信息检索技术的执著追求。

    63 引用 • 785 回帖 • 248 关注
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 506 关注
  • Hprose

    Hprose 是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。你无需专门学习,只需看上几眼,就能用它轻松构建分布式应用系统。

    9 引用 • 17 回帖 • 611 关注
  • 机器学习

    机器学习(Machine Learning)是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。

    76 引用 • 37 回帖