Java项目分层-以Onemall电商开源项目为例(转)
add by zhj: 正在开发Java项目,Java比Python是复杂的多,比如结构化的参数要定义为bean,所以有DTO, BO, VO,很繁琐。
如果项目是前后端分离的,建议只用DTO和BO就可以了
原文:http://www.iocoder.cn/Onemall/Application-layer/ 「芋道源码」
1. 概述
本文,我们来分享下 Onemall 电商开源项目的后端的应用分层规范。
目前,Onemall 的电商后端,采用 Spring Boot + Dubbo 的方式,提供给前端接口,也就是说,采用前后端分离的方式。为什么要提这一点呢,我们往下来瞅瞅。
2. 阿里巴巴规范
在说 Onemall 的应用分层规范之前,我们先来看看阿里巴巴分享的应用分层规范。
如下内容,引用自 《阿里巴巴Java开发手册(详尽版)》 。
考虑到排版,下面内容就不使用引用先。
- 【推荐】图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于Web层,也可以直接依赖于Service层,依此类推:
- 开放接口层:可直接封装Service方法暴露成RPC接口;通过Web封装成http接口;进行网关安全控制、流量控制等。
- 终端显示层:各个端的模板渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等。
- Web层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
- Service层:相对具体的业务逻辑服务层。
- Manager层:通用业务处理层,它有如下特征: - 1) 对第三方平台封装的层,预处理返回结果及转化异常信息; - 2) 对Service层通用能力的下沉,如缓存方案、中间件通用处理; - 3) 与DAO层交互,对多个DAO的组合复用。
- DAO层:数据访问层,与底层MySQL、Oracle、Hbase等进行数据交互。
- 外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。
-
【参考】(分层异常处理规约)在DAO层,产生的异常类型有很多,无法用细粒度的异常进行catch,使用catch(Exception e)方式,并throw new DAOException(e),不需要打印日志,因为日志在Manager/Service层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在Service层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。如果Manager层与Service同机部署,日志方式与DAO层处理一致,如果是单独部署,则采用与Service一致的处理方式。Web层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该跳转到友好错误页面,加上用户容易理解的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。
-
【参考】分层领域模型规约:
- 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类来传输。
3. Onemall 的选择
看到阿里巴巴的规范之后,胖友是不是一脸懵逼,竟然有这么多 POJO ?!每个公司的业务复杂度不同,架构不同,所以 POJO 的选择实际会有不同。当然,艿艿觉得,原则上是 Service 不将 DO 数据库实体从 Service 暴露到 Controller ,避免后续数据库设计的变化,影响暴露出去的方法。
🔥 如下图,是 Onemall 的应用分层选择:
我们按照自下而上,来看看各层的选择。
- 按照 Controller、Service、DAO 分成三层,去掉 Manager 层。
- 大多数业务场景下,无需与第三方平台对接。
- 当然,如果需要和第三方对接,还是会封装成 Client ,例如说 Pay Client 和 第三方支付平台的对接。😈 所以实际还是有“隐藏”的 Manager 层。
- DAO 层
- 入参,使用 DO(Data Object)。
- 出参,使用 DO(Data Object)。
- Service 层
- 入参,使用 DTO(Data Transfer Object)。
- 需要加上 Bean Validation 注解,从而校验参数。
- 需要加上 Swagger API 注解,因为后续 Controller 很大可能性会使用到它,从而生成 API 文档。更细的原因,我们在 Controller 层一起讲。
- 示例:AdminAddDTO 和 AdminUpdateDTO 。
- 出参,使用 BO(Business Object)。
- 入参,使用 DTO(Data Transfer Object)。
- Controller 层
- 入参,使用 DTO(Data Transfer Object)。
- 因为前后端分离之后,Controller 大多数情况下,基本是将 Service 进行封装,提供 API 接口。所以大多数情况,Service DTO 可以重用,所以就默许使用 Service DTO 。😈 当然,这块有不同意见的胖友,可以一起来讨论下,我也挺纠结的。
- 当然,如果 Service DTO 不够用的情况下,可以自己在创建下 Controller DTO 。
- 本来想 Controller 单独在取个 XXO 的名字,结果想了半天没想出来,就继续沿用 Service DTO 了。
- 所以,因为是这样的设定,我们就要求 Service DTO 上,增加 Swagger API 注解。
- 出参,使用 BO(Business Object)。
- 原因,也是同 Controller 入参。
- 当然,如果 Service BO 不够用的情况下,可以自己在创建下 Controller VO 。
- 入参,使用 DTO(Data Transfer Object)。
艿艿:每个公司的分层架构不同,欢迎一起讨论。妥妥的。
因为分层规范是后来调整的,所以项目中可能有部分不符合这样的规范,具体以示例为主。
🔥 聊完了应用分层的话题,我们在一起讨论下 Service 逻辑异常的时候,如何进行返回。这里的逻辑异常,我们指的是,例如说用户名已经存在,商品库存不足等。一般来说,常用的方案选择,有两种:
- 封装统一的业务异常类 ServiceException ,里面有错误码和错误提示,然后进行
throws
抛出。 - 封装通用的返回类 CommonResult ,里面有错误码和错误提示,然后进行
return
返回。
一开始,我们选择了 CommonResult ,结果发现如下情况:
- 因为 Spring
@Transactional
声明式事务,是基于异常进行回滚的,如果使用 CommonResult 返回,则事务回滚会非常麻烦。 - 当调用别的方法时,如果别人返回的是 CommonResult 对象,还需要不断的进行判断,写起来挺麻烦的。
所以,后来我们采用了抛出业务异常 ServiceException 的方式。
🔥 可能机智的小伙伴会问,如果抛出异常,Controller 如何做通用的处理,答案在 GlobalExceptionHandler 类。结果 Spring MVC 的 Exception 处理机制,我们会将 ServiceException 转换成 CommonResult 对象,返回给前端。
当然,故事还没有结束,Controller 虽然返回的是 BO / VO 对象,我们选择在外面包了一层 CommonResult ,用于返回可能存在的业务逻辑错误的情况。因为呢,HTTP API 是语言无关,无法使用 Java Excpetion 。
不过哈,最初我们使用了 @ControllerAdvice
机制,自动全局将 BO / VO 对象,包装成 CommonResult 对象,但是和基友 didi 讨论了下这个选择,建议还是 Controller 显示声明 CommonResult 返回,考虑点是 AOP 不应该破坏方法的 Schema ,即有一天去掉这个 AOP ,依然返回的是 CommonResult 。
4. 彩蛋
🔥 最后的最后,大家总是会讨论到的一个问题,这么多 POJO 对象,如何进行复制呢?Onemall 采用 mapstruct ,因为广告法的原因,我们不能说它是最好用的,但是的确是(并且,效果还非常非常非常的高),哈哈哈哈。具体的示例,可以看看 AdminConvert 。