各种值对象辨析
贫血或失血模型的 Java Web 系统, 数据与操作是彼此分离的. 操作主要体现在服务层的接口方法以及实现, 而所谓数据基本上就是常见的各种 O 了. 如:
- BO Business Object
- PO Persistent Object
- VO View Object
- DTO Data Transfer Object
- POJO Plan Old Java Object
BO 即业务对象, 也称为 Model, 即数据模型. PO 为持久化对象, 即 Entity 实体, 是模型在数据访问层的化身, 用于存取关系或非关系数据库. VO 是模型在控制层的化身, 通常用于向前端接口或图形界面传递数据. DTO 是模型在数据传递时的化身, 主要用于数据在不同服务间的转换和传递. POJO 即普通 Java 对象, 原来是针对 J2EE 规范中各种 Bean, 如 EntityBean, Session Bean 等作区分, 用来指代非 Bean 的简单对象. 后来 J2EE 没落, 在很多文章中也就用来泛指这些值对象了. 除了上述带 O 的, 还有一些不带 O 的值对象变种, 例如 xxxQuery, 可能就是加了分页信息的 VO; 又如 xxxResult, 也许即是封了调用结果的 DTO 等等. 凡此种种, 不一而足.
为什么不能只使用 BO, 而非要在不同的层使用不同的化身呢? 一, 是因为虽然大致类似, 但不同的 O 关注点不同, 其中属性多少会有些区别. 数据库中保存的, 控制层可能不需要展示; 控制层展示的, 业务逻辑层计算可能又用不到. 二, 是为了架构清晰, 便于各层独立封装. 如单独提取控制层, 可以做到不依赖底层 PO, 两层可独立变化. 所以开发时要注意不要一个 Entity 一路从前用到后, 贯穿所有分层. 同理, 尽管属性相似或雷同, VO 也不应继承 PO, 而是应该彼此分开. 实际开发中, 根据项目具体需要, 各种值对象一般也不会全部使用, 不过 PO 和 VO 基本上是必不可少的.
值对象比较简单, 算是基础中的基础, 会写 Java 的应该没有人不会写值对象, 然而要写得规范统一则需要自觉和约束. 比如控制层代码都是 VO, 就别突然冒出个什么 Query 来; 还有 dto 的包名下, 就别突然有弄了个 BO 出来. 这种鹤立鸡群, 驼立羊群指明了代码是未做修改就从别的系统粘了过来, 显得十分不专业. 关于值对象的规范, 阿里巴巴的 Java 开发手册写得不错. 如果所在团队有约定规范, 服从团队规范; 若有些部分团队没有明确规约, 则可以参考阿里的 Java 开发手册. 具体但不限于: 值对象类命名的大小写, 属性名的大小写, Boolean 类型的字段的命名不要加 "is", 属性类型推荐使用包装类型等等, 在此不再赘述. 阿里的 Java 开发手册也提供了 IDEA 插件, 可以在开发中对不符合规范的代码进行提示. 如有条件, 也可接入 Sonar, 配置规则对代码进行静态扫描, 强制执行规范.
实际工程中, 一般都会使用 ORM 框架, 那么 PO 可通过 mybatis-generator
或是 hibernate-tools
逆向工程, 根据数据库表直接生成. 如果有维护良好的代码生成器, 各层值对象均可通过数据库或填写字段自动生成.
值对象方法
值对象的方法比较简单, 无外就是 getter
, setter
, toString
, hashCode
和 equals
. 其中 getter
和 setter
的建议只作存取, 不要加特殊逻辑, 否则很容易自寻烦恼. 同时也要注意方法名与属性名保持一致, 大部分框架都是基于此存取属性值的. 有些找不到对应属性直接报错还好, 如果会忽略, 则这种不一致会导致错误发现时间后延, 从而造成不必要的损失. toString
, hashCode
和 equals
源自 Object
, 可根据实际需要决定是否需要覆写. 如果覆写, 那么就别忘了一个面试中问滥了的问题: equals
和 hashCode
为什么要一起覆写? 具体而言, 这些方法要怎么来写, 总结起来, 大致有五种方式:
代码生成器
一般使用代码生成器生成的值对象类, 会同时附带这些方法.IDE
现代开发工具都会提供相关功能, 可以根据不同选项快速生成上述方法.Apache Commons Lang
Apache Commons Lang 提供的一系列 Builder 工具, 常用的如EqualsBuilder
,HashCodeBuilder
,ToStringBuilder
,CompareToBuilder
用于辅助实现一些常用方法. 其原理是利用反射获得类的属性, 从而进行比较, 拼接或计算.lombok
使用 lombok 提供的注解:@Getter
,@Setter
,@ToString
,@EqualsAndHashCode
或@Data
来标注值对象. lombok 的原理是在编译时修改字节码, 故只能在 class 文件中看到这些生成的方法, java 的源码中则眼不见心不烦, 显得比较整洁. 也是因为如此, lombok 对泛型的支持不是很好, 而且 IDE 也需要安装插件才能不提示编译错误.手写
手工编写这些方法的代码.
上述五种方式中, 个人推荐用第 5 种, 手工编写的方式, 虽然费时而且还容易出错, 但这能让你在 Boss 面前显得很忙碌, 工作量很饱和.
在实际工程中, 如果代码生成器及模板如果良好可用, 则最好使用代码生成器, 省时省力, 还可以与部门其它项目保持一致. 如无代码生成器或模板疏于维护, 个人倾向于使用 lombok, 一是步骤少打字少, 提高效率; 二是因为字节码技术, 所以源码看上去清爽干净. 如果直接使用 @Data
需要注意, 值对象若有继承关系, @ToString
和@EqualsAndHashCode
注解的 callSuper
属性需改为 true
, 才会使非 Object
的父类属性参与到 toString
, hashCode
和 equals
的生成之中, 该属性默认为 false
.
值对象转换
除了内部方法, 值对象之间的主要方法就是来回来去互相转换了. 这些转换总结下来, 大致也有五种方式:
Apache BeanUtils / PropertyUtils
Apache 提供的类转换类, BeanUtils 在对 Bean 赋值时会进行类型转化, 而 PropertyUtils 不会.Spring BeanUtils
Spring 提供的类转换类, 与 Apache 提供的工具类同名, 但是实际使用和特性均有不同.BeanCopier
由 cglib 提供的工具类, 由于采用字节码技术, 转换效率比前两种基于反射的 BeanUtils 都要高.dozer
开源工具包, 如用在 spring 项目中, 需要整合. dozer 比较灵活, 可以配置 xml 文件, 为值对象中的属性转化添加自己的逻辑.手写
手工编写代码转换.
之前说 setter
和 getter
手写只是笑谈, 现实中真没见过有谁手敲的, 不过值对象转换手写的就普遍的多了, 而且不是不同名不同类型的属性手敲, 而是从头敲到尾, 大概是要这样才能够获得一种安全感吧. 手写转换的相比反射转换的唯一优势, 可能就在于执行效率高了. 然而从系统整体运行考虑, 相对于数据库和网络传输, 这一点执行效率几乎可以忽略不计, 更何况如果真在乎这点效率, 还有基于字节码的 BeanCopier 呢.
基于工具的转换, 尽管方便, 但是也有问题需要注意. 比如同名不同类型属性的转换, setter
和 getter
是否匹配, Date
和 BigDecimal
等特殊类型以及 null
值的处理等等. Apache 的工具类在这些工具中是转换效率最低的, 对于日期类型的处理很容易出错, 而且还需要处理异常, 故此实际项目中很少使用. 不过因为与 spring 的工具同名, 代码中经常看到不做区分, 乱用一气的, 估计是引包自动提示先引到哪个就用了哪个. 这两个工具类参数 source 和 target 位置相反, 混淆不分比较容易出现复制方向搞反的低级错误. 不论使用哪一个, 出现对象属性时都要警觉, 因为其拷贝均为浅拷贝. BeanCopier 是基于字节码技术, 生成动态代理来进行对象装换, 因此效率比基于反射的 BeanUtils 要高不少. 不过代码中也常看到不做封装, 每转换一次都新生成一个代理类的, 也不知道是不是为了日后优化故意留的一个口子. dozer 只是了解, 实际生产中还没有使用过. 其配置最为灵活, 可通过 xml, 添加特定的映射转换规则. 个人认为, 对于属性差异性很大的对象来说转换方式统一便捷, 但是对于值对象这种大部分属性名称和类型均相同, 只有小部分差异的对象, 使用 dozer 以及其它可扩展的 converter 有些过重, 反而增加了代码复杂度.
因此, 实际项目当中最普及的用法, 还是在值对象属性的命名和类型做到尽量统一的前提下, 先使用 spring 的 BeanUtils 拷贝一致的属性, 再手工写上几行代码, 转换不一致的属性.