https://www.imooc.com/article/293314

  1. 应用分层&分层领域模型为什么重要?
  2. 典型的领域模型都有哪些?
  3. 模型转换需要注意的问题是啥?
  4. 常见的模型转换方法了解下!

为什么要应用分层&分层领域模型?

我们在软件开发设计及开发过程中,习惯将软件横向拆分为几个层。比如常见的三层架构:表现层(UI)/业务逻辑层(BAL)/数据访问层(DAL)。
image.png
那应用系统为什么要分层呢?
其实主要是解决一下几个问题:

  • 解耦

有一句计算机名言—-软件的所有问题都可以通过增加一层来解决。
当系统越大,团队越多,需求变化越快时,越需要保证程序之间的依赖关系越少。而分层/面向接口编程,会使我们在应对变化时越容易。

  • 简化问题

当我们想不明白从用户操作一直到数据落盘整个过程的交互情况时,我们应该换种方式思考。想想各层应该提供哪些支持,通过对各层分工的明确定义,复杂问题就变成了如何将各层功能组合起来的“积木搭建”。

  • 降低系统维护与升级成本

这里体现了面向接口编程的优势。我们抽象出数据访问层后,只需要保证对外提供的接口不变,底层数据库使用Oracle还是MySql,上层结构是感知不到的。

  • 逻辑复用/代码复用

通过分层,明确定义各层职责,再也不会出现系统中多个地方查询同一个数据库表的代码。因为查询某个数据库表的工作只会由一个数据访问层类来统一提供。

  • 提高团队开发效率

如果开发团队很多,通过分层和接口定义。各团队只需要遵循接口标准/开发规范,就可以并行开发。
一个比较形象的比喻:分层化相当于把软件横向切几刀,模块化相当于把软件纵向切几刀。

在《阿里巴巴Java开发手册》中,对应用分层的建议是这样的:
image.png

  • 开放接口层:可直接封装Service方法暴露成RPC接口;通过Web封装成http接口;进行网关安全控制/流量控制等。
  • 终端显示层:各个端的模版渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等。
  • Web层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service层:相对集体的业务逻辑服务层。
  • Manager层:通用业务处理层,它有如下特征:
    • 对第三方平台封装的层,预处理返回结果及转化异常信息。
    • 对Service层通用能力的下沉,如缓存方案/中间件通用处理。
    • 与DAO层交互,对多个DAO的组合复用。
  • DAO层:数据访问层,与底层MySQL、Oracle、HBase等进行数据交互。
  • 外部接口或第三方平台:包括其他部门RPC开放接口,基础平台,其他公司的HTTP接口。

以上的层级只是在原来三层架构的基础上进行了细分,而这些细分的层级仅仅是为了满足业务的需要。千万不要为了分层而分层。过多的层会增加系统的复杂度和开发难度。
应用被细分为多个层次,每个层关注的点不同。所以在这基础上,抽象出不同的领域模型。也就是我们常见的DTO,DO等等。
其本质的目的还是为了达到分层解耦的效果。


典型的领域模型都有哪些?

我们还是来看看《阿里开发手册》提供的分层领域模型规约参考:

  • DO(Data Object):此对象与数据库表结构一一对应,通过DAO层想上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
  • BO(Business Object):业务对象,由Service层输出的封装业务逻辑的对象。
  • AO(Application Object):应用对象,在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
  • VO(View Object):显示层对象,通常是Web向模版渲染引擎层传输的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类来传输。

这里的结构图大概是这样:

image.png

在给出的参考中并没有对模型对象进行非常明确的划分,特别是对BO、AO、DTO的界限不是非常明确。这也是因为系统处理的业务不同、复杂度不同导致的。所以在设计系统分层和建模的时候,需要综合考虑实际应用场景。

可能有些小伙伴会觉得麻烦,为什么要弄出这么多O?转来转去的多累!

在这里我举个例子:

比如你查询自己网上购物的订单,可能会在网页上看到这样的信息
image.png

其中包含:订单编号,下单日期,店铺名称,用户信息,总金额,支付方式,订单状态还有一个订单商品明细的集合。

对终端显示层来说,这些信息是可以封装成一个VO对象的。因为显示层的关注点就是这些信息。为了方便显示层展示,我们可以将所有属性都弄成字符串类型。

image.png

再来看看对于业务逻辑层来说,它关心的是什么呢?显然跟显示层关注的不一样,它更加关注的是内部的逻辑关系。

image.png

可以看到,下单日期使用的Date类型,金额使用BigDecimal,支付方式和订单状态使用枚举值表示,商铺名称和用户名称变成了商铺信息/用户信息对象,明细集合中的商品也变成了DTO类型的对象。

在业务逻辑层面,更多的是关注由多种信息组合而成的关系。因为它在系统中起到信息传递的作用,所以它携带的信息也是最多的。

好,那我们再来看看数据持久层。

上面也提到了,数据持久层与数据库是一一对应的关系,而上一层的订单信息其实可以拆解为多个持久层对象,其中包含:订单持久层对象(OrderDO),商铺持久层对象(ShopDO),用户持久层对象(UserDO)还有一堆的商品持久层对象(ProductDO)。

相信通过描述大家也可以理解具体的拆分方法了。

回过头来想想,如果我们一路拿着最开始的OrderVO对象来操作,当我们想要将它持久化时,会遇到多少坑就可想而知了。

所以分层/拆分的本质还是简化我们思考问题的方式,各层只关注自己感兴趣的内容。

可这样的拆分确实增加了许多工作量,不同模型之间转来转去的确实头疼。

那就让我们来梳理一下,在模型转换时都需要注意哪些问题~~

模型转换需要注意的问题是啥?

在进行不同领域对象转换时,有些问题是需要我们考虑的。

image.png

  1. 原对象和目标对象相同属性的类型不一样,有的是Date,有的是BigDecimal,还有的是枚举
  2. 属性的名称也不一样
  3. 集合类属性中的泛型也不一样
  4. 能不能只复制一部分属性
  5. 能不能自定义转换逻辑
  6. 嵌套对象是深拷贝还是浅拷贝

这么多需要考虑的地方,咱们要怎么处理,才能优雅的进行模型转换呢?

常见的模型转换方法了解下!

常见的模型转换方法了解下!

这里我调研了大概有10种方法,有些使用起来比较复杂就没有下大力气去深入研究,如果有感兴趣的小伙伴,咱们可以一起讨论。

做为测试和讲解的案例,咱们就以上面说到的OrderDTO转OrderVO为例,来说说下面的各种方法。

我们的原对象OrderDTO的内容如下:

  1. {
  2. "orderDate":1570558718699,
  3. "orderId":201909090001,
  4. "orderStatus":"CREATED",
  5. "orderedProducts":[
  6. {
  7. "price":799.990000000000009094947017729282379150390625,
  8. "productId":1,
  9. "productName":"吉他",
  10. "quantity":1
  11. },
  12. {
  13. "price":30,
  14. "productId":2,
  15. "productName":"变调夹",
  16. "quantity":1
  17. }
  18. ],
  19. "paymentType":"CASH",
  20. "shopInfo":{
  21. "shopId":20000101,
  22. "shopName":"慕课商铺"
  23. },
  24. "totalMoney":829.990000000000009094947017729282379150390625,
  25. "userInfo":{
  26. "userId":20100001,
  27. "userLevel":2147483647,
  28. "userName":"张小喜"
  29. }
  30. }

期望转换后得到的目标对象OrderVO如下:

  1. {
  2. "orderDate":"2019-10-09 15:49:24.619",
  3. "orderStatus":"CREATED",
  4. "orderedProducts":[
  5. {
  6. "productName":"吉他",
  7. "quantity":1
  8. },
  9. {
  10. "productName":"变调夹",
  11. "quantity":1
  12. }
  13. ],
  14. "paymentType":"CASH",
  15. "shopName":"慕课商铺",
  16. "totalMoney":"829.99",
  17. "userName":"张小喜"
  18. }

下面让我们来逐一看看各种转换方法。

第一种:人肉赋值

也是最简单粗暴的方法,直接通过Set/Get方式来进行人肉赋值。代码我就不贴了,相信大家都会。

特点如下:

  • 直观,简单,处理速度快
  • 属性过多的时候,人容易崩溃

第二种:FastJson

利用序列化和反序列化,这里我们采用先使用FastJson的toJSONString的方法将原对象序列化为字符串,再使用parseObject方法将字符串反序列化为目标对象。

使用方式:

  1. // JSON.toJSONString将对象序列化成字符串,JSON.parseObject将字符串反序列化为OderVO对象
  2. orderVO = JSON.parseObject(JSON.toJSONString(orderDTO), OrderVO.class);

结果:

  1. // 目标对象
  2. {
  3. "orderDate":"1570558718699",
  4. "orderId":201909090001,
  5. "orderStatus":"CREATED",
  6. "orderedProducts":[
  7. {
  8. "productName":"吉他",
  9. "quantity":1
  10. },
  11. {
  12. "productName":"变调夹",
  13. "quantity":1
  14. }
  15. ],
  16. "paymentType":"CASH",
  17. "totalMoney":"829.990000000000009094947017729282379150390625"
  18. }

可以看到

  • 日期不符合我们的要求
  • 金额也有问题
  • 最严重的是,当属性名不一样时,不复制

    第三种:Apache工具包PropertyUtils工具类

    使用方式:

    1. PropertyUtils.copyProperties(orderVO, orderDTO);

    转换过程中报错

    1. java.lang.IllegalArgumentException: Cannot invoke com.imooc.demo.OrderVO.setTotalMoney on bean class 'class com.imooc.demo.OrderVO' - argument type mismatch - had objects of type "java.math.BigDecimal" but expected signature "java.lang.String"

    结果:

    1. // 目标对象
    2. {
    3. "orderId":201909090001
    4. }

    结论:

  • 属性类型不一样时报错

  • 不能部分属性复制
  • 得到的目标对象部分属性成功(这点很要命,部分成功,部分失败!)

    第四种:Apache工具包BeanUtils工具类

    使用方式:

    1. BeanUtils.copyProperties(orderVO, orderDTO);

    结果:

    1. // 目标对象
    2. {
    3. "orderDate":"Wed Oct 09 02:36:25 CST 2019",
    4. "orderId":201909090001,
    5. "orderStatus":"CREATED",
    6. "orderedProducts":[
    7. {
    8. "price":799.990000000000009094947017729282379150390625,
    9. "productId":1,
    10. "productName":"吉他",
    11. "quantity":1
    12. },
    13. {
    14. "price":30,
    15. "productId":2,
    16. "productName":"变调夹",
    17. "quantity":1
    18. }
    19. ],
    20. "paymentType":"CASH",
    21. "totalMoney":"829.990000000000009094947017729282379150390625"
    22. }

    结论:

  • 日期不符合要求

  • 属性名不一样时不复制
  • 目标对象中的商品集合变成了DTO的对象,这是因为List的泛型被擦除了,而且是浅拷贝,所以造成这种现象。

    第五种:Spring封装BeanUtils工具类

    使用方式:

    1. // 对象属性转换,忽略orderedProducts字段
    2. BeanUtils.copyProperties(orderDTO, orderVO, "orderedProducts");

    结果:

    1. // 目标对象
    2. {
    3. "orderId":201909090001
    4. }

    结论:

  • 可以忽略部分属性

  • 属性类型不同,不能转换
  • 属性名称不同,不能转换

apache的BeanUtils和spring的BeanUtils中拷贝方法的原理都是先用jdk中 java.beans.Introspector类的getBeanInfo()方法获取对象的属性信息及属性get/set方法,接着使用反射(Method的invoke(Object obj, Object… args))方法进行赋值。

易错记录:
如果赋值对象有值,那么不会继续进行操作
image.png

第六种:cglib工具包BeanCopier

cglib的BeanCopier采用了不同的方法:它不是利用反射对属性进行赋值,而是直接使用ASM的MethodVisitor直接编写各属性的get/set方法生成class文件,然后进行执行。由于是直接生成字节码执行,所以BeanCopier的性能较采用反射的BeanUtils有较大提高。

使用方式:

  1. // 构造转换器对象,最后的参数表示是否需要自定义转换器
  2. BeanCopier beanCopier = BeanCopier.create(orderDTO.getClass(), orderVO.getClass(), true);
  3. // 转换对象,自定义转换器处理特殊字段
  4. beanCopier.copy(orderDTO, orderVO, (value, target, context) -> {
  5. // 原始数据value是Date类型,目标类型target是String
  6. if (value instanceof Date) {
  7. if ("String".equals(target.getSimpleName())) {
  8. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
  9. return sdf.format(value);
  10. }
  11. }
  12. // 未匹配上的字段,原值返回
  13. return value;
  14. });

报错

  1. java.lang.ClassCastException: com.imooc.demo.OrderStatus cannot be cast to java.lang.String

结果:

  1. // 目标对象
  2. {
  3. "orderDate":"2019-10-09 03:07:13.768",
  4. "orderId":201909090001
  5. }

结论:

  • 字节码技术,速度快
  • 提供自己自定义转换逻辑的方式
  • 转换逻辑自己写,比较复杂,繁琐
  • 属性名称相同,类型不同,不会拷贝(原始类型和包装类型也被视为类型不同)

    第七种:Dozer框架

    使用以上类库虽然可以不用手动编写get/set方法,但是他们都不能对不同名称的对象属性进行映射。在定制化的属性映射方面做得比较好的有Dozer,Dozer支持简单属性映射、复杂类型映射、双向映射、隐式映射以及递归映射。可使用xml或者注解进行映射的配置,支持自动类型转换,使用方便。但Dozer底层是使用reflect包下Field类的set(Object obj, Object value)方法进行属性赋值,执行速度上不是那么理想。

使用方式:

  1. // 创建转换器对象,强烈建议创建全局唯一的,避免不必要的开销
  2. DozerBeanMapper mapper = new DozerBeanMapper();
  3. // 加载映射文件
  4. mapper.addMapping(TransferTest.class.getResourceAsStream("/mapping.xml"));
  5. // 转换
  6. orderVO = mapper.map(orderDTO, OrderVO.class);

结果:

  1. // 目标对象
  2. {
  3. "orderDate":"2019-10-09 15:49:24.619",
  4. "orderStatus":"CREATED",
  5. "orderedProducts":[
  6. {
  7. "productName":"吉他",
  8. "quantity":1
  9. },
  10. {
  11. "productName":"变调夹",
  12. "quantity":1
  13. }
  14. ],
  15. "paymentType":"CASH",
  16. "shopName":"慕课商铺",
  17. "totalMoney":"829.99",
  18. "userName":"张小喜"
  19. }

配置的字段映射文件:

  1. <mappings xmlns="http://dozer.sourceforge.net"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://dozer.sourceforge.net
  4. http://dozer.sourceforge.net/schema/beanmapping.xsd">
  5. <!-- 一组类映射关系 -->
  6. <mapping>
  7. <!-- 类A和类B -->
  8. <class-a>com.imooc.demo.OrderDTO</class-a>
  9. <class-b>com.imooc.demo.OrderVO</class-b>
  10. <!-- 一组需要映射的特殊属性 -->
  11. <field>
  12. <a>shopInfo.shopName</a>
  13. <b>shopName</b>
  14. </field>
  15. <!-- 将嵌套对象中的某个属性值映射到目标对象的指定属性上 -->
  16. <field>
  17. <a>userInfo.userName</a>
  18. <b>userName</b>
  19. </field>
  20. <!-- 将Date对象映射成指定格式的日期字符串 -->
  21. <field>
  22. <a>orderDate</a>
  23. <b date-format="yyyy-MM-dd HH:mm:ss.SSS">orderDate</b>
  24. </field>
  25. <!-- 自定义属性转化器 -->
  26. <field custom-converter="com.imooc.demo.DozerCustomConverter">
  27. <a>totalMoney</a>
  28. <b>totalMoney</b>
  29. </field>
  30. <!-- 忽略指定属性 -->
  31. <field-exclude>
  32. <a>orderId</a>
  33. <b>orderId</b>
  34. </field-exclude>
  35. </mapping>
  36. </mappings>

自定义转换器:

  1. public class DozerCustomConverter implements CustomConverter {
  2. @Override
  3. public Object convert(Object destination, Object source, Class<?> destClass, Class<?> sourceClass) {
  4. // 如果原始属性为BigDecimal类型
  5. if (source instanceof BigDecimal) {
  6. // 目标属性为String类型
  7. if ("String".equals(destClass.getSimpleName())) {
  8. return String.valueOf(((BigDecimal) source).doubleValue());
  9. }
  10. }
  11. return destination;
  12. }
  13. }

结论:

  • 支持多种数据类型自动转换(双向的)
  • 支持不同属性名之间转换
  • 支持三种映射配置方式(注解方式,API方式,XML方式)
  • 支持配置忽略部分属性
  • 支持自定义属性转换器
  • 嵌套对象深拷贝

前面介绍的几种方式应该属于转换工具类,而Dozer属于转换框架,所以它在转换效果上是最好的,支持的功能也是最多的。

类似的框架还有如下几种,支持的功能也基本相同。

第八种:MapStruct框架:

基于JSR269的Java注解处理器,通过注解配置映射关系,在编译时自动生成接口实现类。类似于Lombok的原理一样。

第九种:Orika框架:

支持在代码中注册字段映射,通过javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件。

第十种:ModelMapper框架:

基于反射原理进行赋值或者直接对成员变量赋值。

总结

介绍的这些转换方法中,在性能上基本遵循:手动赋值 > 字节码生成 > 动态代理 > 反射。

在实际项目中,需要综合使用上述方法进行模型转换。比如较低层的DO,因为涉及到的嵌套对象少,结构简单,所以可以使用BeanUtils直接转。如果是速度、稳定优先的系统,还是乖乖使用Set、Get实现吧。

一篇文章讲清楚VO,BO,PO,DO,DTO的区别
image.png