[TOC]
我们在软件开发设计及开发过程中,习惯将软件横向拆分为几个层。比如常见的三层架构:表现层(VIEW/UI)、业务逻辑层(SERVICE/BAL)、数据访问层(DAO/DAL)。如下图:
那应用系统为什么要分层呢?其实主要是解决以下几个问题:
第一是解耦:
有一句计算机名言:软件的所有问题都可以通过增加一层来解决。当系统越大,团队越多,需求变化越快时,越需要保证程序之间的依赖关系越少。而分层/面向接口编程,会使我们在应对变化时越容易。
第二是简化问题:
当我们想不明白从用户操作一直到数据落盘整个过程的交互情况时,我们应该换种方式思考。想想各层应该提供哪些支持,通过对各层分工的明确定义,复杂问题就变成了如何将各层功能组合起来的“积木搭建”。
第三是降低系统维护与升级成本:
这里体现了面向接口编程的优势。我们抽象出数据访问层后,只需要保证对外提供的接口不变,底层数据库使用Oracle还是MySql,上层结构是感知不到的。
第四是逻辑复用/代码复用:
通过分层,明确定义各层职责,再也不会出现系统中多个地方查询同一个数据库表的代码。因为查询某个数据库表的工作只会由一个数据访问层类来统一提供。
如果开发团队很多,通过分层和接口定义。各团队只需要遵循接口标准/开发规范,就可以并行开发。有一个形容比较贴切:分层化相当于把软件横向切几刀,模块化相当于把软件纵向切几刀。
在《阿里巴巴Java开发手册》中,对应用分层的建议是这样的:
以上的层级只是在原来三层架构的基础上进行了细分,而这些细分的层级仅仅是为了满足业务的需要。千万不要为了分层而分层。
过多的层会增加系统的复杂度和开发难度。因为应用被细分为多个层次,每个层关注的点不同。所以在这基础上,抽象出不同的领域模型。也就是我们常见的DTO,DO等等。其本质的目的还是为了达到分层解耦的效果。
以上我们简单了解了分层的重要性,那么随着分层引入的典型领域模型都有哪些?我们还是来看看《阿里开发手册》提供的分层领域模型规约参考:
各个领域模型在分层上的传输关系大概是这样:
在给出的参考中并没有对模型对象进行非常明确的划分,特别是对BO、AO、DTO的界限不是非常明确。这也是因为系统处理的业务不同、复杂度不同导致的。所以在设计系统分层和建模的时候,需要综合考虑实际应用场景。
数据在上传下达的过程中就会出现转换的工作,可能有些小伙伴会觉得麻烦,为什么要弄出这么多O?转来转去的多累!
在这里我举个例子,比如你查询自己网上购物的订单,可能会在网页上看到这样的信息:
其中包含:订单编号,下单日期,店铺名称,用户信息,总金额,支付方式,订单状态还有一个订单商品明细的集合。
对终端显示层来说,这些信息是可以封装成一个VO对象的。因为显示层的关注点就是这些信息。为了方便显示层展示,我们可以将所有属性都弄成字符串类型。如下示例,可以看到,除了订单id外,都是String类型:
public class OrderVO {
/**
* 订单id
*/
Long orderId;
/**
* 下单日期
*/
String orderDate;
/**
* 总金额
*/
String totalMoney;
/**
* 支付方式
*/
String paymentType;
/**
* 订单状态
*/
String orderStatus;
/**
* 商铺名称
*/
String shopName;
/**
* 用户名称
*/
String userName;
/**
* 订单商品明细集合
*/
List<ProductVO> orderedProducts;
}
再来看看对于业务逻辑层来说,它关心的是什么呢?显然跟显示层关注的不一样,它更加关注的是内部的逻辑关系。如下示例:
public class OrderVO {
/**
* 订单id
*/
Long orderId;
/**
* 下单日期
*/
Date orderDate;
/**
* 总金额
*/
BigDecimal totalMoney;
/**
* 支付方式
*/
PaymentType paymentType;
/**
* 订单状态
*/
OrderStatus orderStatus;
/**
* 商铺信息
*/
ShopDTO shopInfo;
/**
* 用户信息
*/
UserDTO userInfo;
/**
* 订单商品明细集合
*/
List<ProductDTO> orderedProducts;
}
从如上代码可以看到,下单日期使用的Date类型,金额使用BigDecimal,支付方式和订单状态使用枚举值表示,商铺名称和用户名称变成了商铺信息/用户信息对象,明细集合中的商品也变成了DTO类型的对象。
在业务逻辑层面,更多的是关注由多种信息组合而成的关系。因为它在系统中起到信息传递的作用,所以它携带的信息也是最多的。
那我们再来看看数据持久层,上面也提到了,数据持久层与数据库是一一对应的关系,而上一层的订单信息其实可以拆解为多个持久层对象,其中包含:订单持久层对象(OrderDO),商铺持久层对象(ShopDO),用户持久层对象(UserDO)还有一堆的商品持久层对象(ProductDO)。相信通过描述大家也可以理解具体的拆分方法了。
回过头来想想,如果我们一路拿着最开始的OrderVO对象来操作,当我们想要将它持久化时,会遇到多少坑就可想而知了。所以分层/拆分的本质还是简化我们思考问题的方式,各层只关注自己感兴趣的内容。
可这样的拆分确实增加了许多工作量,不同模型之间转来转去的确实头疼。那就让我们来梳理一下,在模型转换时都需要注意哪些问题。在进行不同领域对象转换时,有些问题是需要我们考虑的。
例如,上面这两个不同的模型在转换时,我们就需要考虑一些问题:
这么多需要考虑的地方,咱们要怎么处理,才能优雅的进行模型转换呢?
这里我调研了大概有10种方法,有些使用起来比较复杂就没有下大力气去深入研究,如果有感兴趣的小伙伴,可以自行深入研究下。
做为测试和讲解的案例,咱们就以上面说到的OrderDTO转OrderVO为例,来说说下面的各种方法。源对象OrderDTO大体结构是这样的:
{
"orderDate":1570558718699,
"orderId":201909090001,
"orderStatus":"CREATED",
"orderedProducts":[
{
"price":799.990000000000009094947017729282379150390625,
"productId":1,
"productName":"吉他",
"quantity":1
},
{
"price":30,
"productId":2,
"productName":"变调夹",
"quantity":1
}
],
"paymentType":"CASH",
"shopInfo":{
"shopId":20000101,
"shopName":"乐韵商铺"
},
"totalMoney":829.990000000000009094947017729282379150390625,
"userInfo":{
"userId":20100001,
"userLevel":2147483647,
"userName":"尼古拉斯赵四"
}
}
我们期待转换完的OrderVO对象是这样的:
{
"orderDate":"2019-10-09 15:49:24.619",
"orderStatus":"CREATED",
"orderedProducts":[
{
"productName":"吉他",
"quantity":1
},
{
"productName":"变调夹",
"quantity":1
}
],
"paymentType":"CASH",
"shopName":"乐韵商铺",
"totalMoney":"829.99",
"userName":"尼古拉斯赵四"
}
先来看第一种方法:
也是最简单粗暴的方法,直接通过Set/Get方式来进行人肉赋值。代码我就不贴了,相信大家都会。
说一说它的优缺点:
优点:直观,简单,执行速度快
缺点:属性过多的时候,人容易崩溃,代码显得臃肿不好复用
第二种:FastJson:
利用序列化和反序列化,这里我们采用先使用FastJson的toJSONString的方法将原对象序列化为字符串,再使用parseObject方法将字符串反序列化为目标对象。
// JSON.toJSONString将对象序列化成字符串,JSON.parseObject将字符串反序列化为OderVO对象
orderVO = JSON.parseObject(JSON.toJSONString(orderDTO), OrderVO.class);
转换后的结果如下:
// 目标对象
{
"orderDate":"1570558718699",
"orderId":201909090001,
"orderStatus":"CREATED",
"orderedProducts":[
{
"productName":"吉他",
"quantity":1
},
{
"productName":"变调夹",
"quantity":1
}
],
"paymentType":"CASH",
"totalMoney":"829.990000000000009094947017729282379150390625"
}
可以看到转换后的数据格式有几个问题:
这就是第二种使用JSON处理,好像也不能满足我们的要求
第三种,Apache工具包PropertyUtils工具类,代码如下:
PropertyUtils.copyProperties(orderVO, orderDTO);
转换代码看着很简单,但是转换过程会报错:
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"
转换结果:
// 目标对象
{
"orderId":201909090001
}
缺点:
第四种,Apache工具包BeanUtils工具类,代码如下:
BeanUtils.copyProperties(orderVO, orderDTO);
转换后的结果是这样:
// 目标对象
{
"orderDate":"Wed Oct 09 02:36:25 CST 2019",
"orderId":201909090001,
"orderStatus":"CREATED",
"orderedProducts":[
{
"price":799.990000000000009094947017729282379150390625,
"productId":1,
"productName":"吉他",
"quantity":1
},
{
"price":30,
"productId":2,
"productName":"变调夹",
"quantity":1
}
],
"paymentType":"CASH",
"totalMoney":"829.990000000000009094947017729282379150390625"
}
缺点:
第五种,Spring封装BeanUtils工具类,代码如下:
// 对象属性转换,忽略orderedProducts字段
BeanUtils.copyProperties(orderDTO, orderVO, "orderedProducts");
在忽略了部分属性后,转换结果就只剩下:
// 目标对象
{
"orderId":201909090001
}
apache的BeanUtils
和spring的BeanUtils
中拷贝方法的原理都是先用jdk中 java.beans.Introspector
类的getBeanInfo()
方法获取对象的属性信息及属性get/set方法,接着使用反射(Method
的invoke(Object obj, Object... args)
)方法进行赋值。
前面五种都不能满足我们的需要,其实想想也挺简单。对象转换本来就很复杂,人工不介入很难做到完美转换。
第六种,cglib工具包BeanCopier:
cglib的BeanCopier
采用了不同的方法:它不是利用反射对属性进行赋值,而是直接使用ASM的MethodVisitor
直接编写各属性的get/set
方法生成class文件,然后进行执行。
使用方法如下,注释写的很清楚。我们通过自定义的转换器来处理Date转String的操作:
// 构造转换器对象,最后的参数表示是否需要自定义转换器
BeanCopier beanCopier = BeanCopier.create(orderDTO.getClass(), orderVO.getClass(), true);
// 转换对象,自定义转换器处理特殊字段
beanCopier.copy(orderDTO, orderVO, (value, target, context) -> {
// 原始数据value是Date类型,目标类型target是String
if (value instanceof Date) {
if ("String".equals(target.getSimpleName())) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return sdf.format(value);
}
}
// 未匹配上的字段,原值返回
return value;
});
转换结果如下,对于我们自定义处理的属性可以完美支持,其他未处理的属性就不行了:
// 目标对象
{
"orderDate":"2019-10-09 03:07:13.768",
"orderId":201909090001
}
优缺点:
第七种,Dozer框架:
注意,这已经不是一个工具类了,而是框架。使用以上类库虽然可以不用手动编写get/set
方法,但是他们都不能对不同名称的对象属性进行映射。在定制化的属性映射方面做得比较好的就是Dozer了。
Dozer支持简单属性映射、复杂类型映射、双向映射、隐式映射以及递归映射。可使用xml或者注解进行映射的配置,支持自动类型转换,使用方便。但Dozer底层是使用reflect
包下Field
类的set(Object obj, Object value)
方法进行属性赋值,执行速度上不是那么理想。代码示例:
// 创建转换器对象,强烈建议创建全局唯一的,避免不必要的开销
DozerBeanMapper mapper = new DozerBeanMapper();
// 加载映射文件
mapper.addMapping(TransferTest.class.getResourceAsStream("/mapping.xml"));
// 转换
orderVO = mapper.map(orderDTO, OrderVO.class);
使用方式很简单,关键在于配置:
<mappings xmlns="http://dozer.sourceforge.net"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozer.sourceforge.net
http://dozer.sourceforge.net/schema/beanmapping.xsd">
<!-- 一组类映射关系 -->
<mapping>
<!-- 类A和类B -->
<class-a>com.imooc.demo.OrderDTO</class-a>
<class-b>com.imooc.demo.OrderVO</class-b>
<!-- 一组需要映射的特殊属性 -->
<field>
<a>shopInfo.shopName</a>
<b>shopName</b>
</field>
<!-- 将嵌套对象中的某个属性值映射到目标对象的指定属性上 -->
<field>
<a>userInfo.userName</a>
<b>userName</b>
</field>
<!-- 将Date对象映射成指定格式的日期字符串 -->
<field>
<a>orderDate</a>
<b date-format="yyyy-MM-dd HH:mm:ss.SSS">orderDate</b>
</field>
<!-- 自定义属性转化器 -->
<field custom-converter="com.imooc.demo.DozerCustomConverter">
<a>totalMoney</a>
<b>totalMoney</b>
</field>
<!-- 忽略指定属性 -->
<field-exclude>
<a>orderId</a>
<b>orderId</b>
</field-exclude>
</mapping>
</mappings>
在配置文件中对特殊属性进行了特殊定义,转换结果符合我们的要求:
// 目标对象
{
"orderDate":"2019-10-09 15:49:24.619",
"orderStatus":"CREATED",
"orderedProducts":[
{
"productName":"吉他",
"quantity":1
},
{
"productName":"变调夹",
"quantity":1
}
],
"paymentType":"CASH",
"shopName":"乐韵商铺",
"totalMoney":"829.99",
"userName":"尼古拉斯赵四"
}
Dozer支持自定义转换器,如下示例:
public class DozerCustomConverter implements CustomConverter {
@Override
public Object convert(Object destination, Object source, Class<?> destClass, Class<?> sourceClass) {
// 如果原始属性为BigDecimal类型
if (source instanceof BigDecimal) {
// 目标属性为String类型
if ("String".equals(destClass.getSimpleName())) {
return String.valueOf(((BigDecimal) source).doubleValue());
}
}
return destination;
}
}
它的特点如下:
第八种,MapStruct框架:
基于JSR269的Java注解处理器,通过注解配置映射关系,在编译时自动生成接口实现类。类似于Lombok的原理一样,所以在执行速度上和Setter、Getter差不多。我目前个人使用较多的是MapStruct和BeanCopier,后期有空会单独写一篇文章介绍MapStruct的使用。
第九种,Orika框架:
支持在代码中注册字段映射,通过javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件。
第十种,ModelMapper框架:
基于反射原理进行赋值或者直接对成员变量赋值。相当于是BeanUtils
的进阶版
其他几种框架就没有深入研究了。但看使用情况应该都能满足实际场景的要求。介绍的这些转换方法中,在性能上基本遵循:手动赋值 > cglib > 反射 > Dozer > 序列化。
在实际项目中,需要综合使用上述方法进行模型转换。比如较低层的DO,因为涉及到的嵌套对象少,改动也少,所以可以使用BeanUtils直接转。如果是速度、稳定优先的系统,还是乖乖使用Set、Get实现吧。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。