Java Web应用的代码分层最佳实践
代码分层,对于任何一个Java Web开发来说应该都不陌生。一个好的层次划分不仅可以能使代码结构更加清楚,还可以使项目分工更加明确,可读性大大提升,更加有利于后期的维护和升级。
从另外一个角度来看,好的代码分层架构,应该是可以很好的匹配上单一职责原则的。这样就可以降低层与层之间的依赖,还能最大程度的复用各层的逻辑。本文就来介绍下Java Web项目的代码到底应该如何分层。
三层架构
在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。微软推荐的分层式结构一般分为三层,从下至上分别为:数据访问层、业务逻辑层(又或称为领域层)、表示层。这也是Java Web中重要的三层架构中的三个层次。区分层次的目的即为了“高内聚低耦合”的思想。
所谓三层体系结构,是在客户端与数据库之间加入了一个“中间层”,也叫组件层。这里所说的三层体系,不是指物理上的三层,不是简单地放置三台机器就是三层体系结构,也不仅仅有B/S应用才是三层体系结构,三层是指逻辑上的三层,即把这三个层放置到一台机器上。
数据访问层
主要是对非原始数据(数据库或者文本文件等存放数据的形式)的操作层,而不是指原始数据,也就是说,是对数据库的操作,而不是数据,具体为业务逻辑层或表示层提供数据服务。
业务逻辑层
主要是针对具体的问题的操作,也可以理解成对数据层的操作,对数据业务逻辑处理,如果说数据层是积木,那逻辑层就是对这些积木的搭建。
界面层
主要表示WEB方式。如果逻辑层相当强大和完善,无论表现层如何定义和更改,逻辑层都能完善地提供服务。
三层架构与MVC的区别
MVC(模型Model-视图View-控制器Controller)是一种架构模式,可以用它来创建在域对象和UI表示层对象之间的区分。
同样是架构级别的,相同的地方在于他们都有一个表现层,但是他们不同的地方在于其他的两个层。
在三层架构中没有定义Controller的概念。这是最不同的地方。而MVC也没有把业务的逻辑访问看成两个层,这是采用三层架构或MVC搭建程序最主要的区别。
分层的最佳实践
随着网站的用户量的不断提升,系统架构也在不断的调整。有时候,随着业务越来越复杂,有时候三层架构好像不够用了。比如,我们的应用除了要给用户提供页面访问以外,还需要提供一些开放接口,供外部系统调用。这个接口既不属于界面层,也不应该属于业务逻辑层,因为他还可能包含一些和业务逻辑无关的处理,如权限控制、流量控制等。
还有,随着微服务的盛行,我们应用中可能要依赖很多外部接口或第三方平台。这部分代码放下业务逻辑层和数据访问层也都不合适。
所以,渐渐的,在三层架构的基础上,系统架构的分层变得更加复杂了。也正是因为复杂,就非常考验架构设计能力,因为层次划分的不好,很可能会影响后面的开发,给代码维护带来很大的困难。
下图,是阿里巴巴(参考《阿里巴巴Java开发手册》)提倡的应用分层结构:
开放接口层
可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行网关安全控制、流量控制等。
终端显示层
各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。
Web 层
主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
Service 层
相对具体的业务逻辑服务层。
Manager 层
通用业务处理层,它有如下特征: 1) 对第三方平台封装的层,预处理返回结果及转化异常信息; 2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理; 3) 与 DAO 层交互,对多个 DAO 的组合复用。
DAO 层
数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
外部接口或第三方平台
包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。
事务处理
在了解了分层之后,我们再来看一下写Java Web代码的时候,大家比较关心的一个问题,那就是涉及到数据库操作的时候,事务处理应该在哪一层控制呢?
关于这个问题,仁者见仁,智者见智。作者认为,事务处理应该放在Service层和Manager层。
DAO层不应该有事务,应该只是很纯的 CRUD 等比较通用的数据访问方法。一个DAO应该只处理和自己相关的操作,不要有任何组合。组合的事情交给上层。
Service层和Manager层一般会组合多个DAO的CRUD操作,例如:在注册一个用户的时候需要往日志表里 INSERT 日志,那么就在 Service 层构造事务,在该事务中调用 Dao 层的 User.Insert () 与 Log.Insert ()。
异常处理
异常处理是Java中比较重要的一个话题,在《Effective Java》中有很多关于异常处理的最佳实践,这里不详细介绍了,本文主要简单说一下在应用代码分层之后,各个层次之间的异常应该如何处理,是自己捕获,还是向上一层抛出。
首先,每一层都是可能发生异常的。由于每一层的职责都不通,处理方式也可能有差别。
DAO层
在 DAO 层,产生的异常类型可能有很多,可能是SQL相关的异常,也可能是数据库连接相关的异常。
这一层的处理方式可以简单一点,直接try-catch(Exception),然后封装成DAOException抛给上一层。这一层一般不需要打印日志,交给Service或者Manager层来打印。
try{ CRUD }catch(Exception e){ throw new DAOException(e); }
Manager/Service
首先,对于DAO层抛上来的异常一定要捕获的,并且记录日志打印现场。
但是值得注意的是,如果是需要事务控制的方法,要注意捕获到异常之后再向上抛一个新的异常,如 TransactionRolledbackException,否则事务无法回滚。
这两层发生的异常可以根据情况决定是继续向上抛还是自己处理掉。如果是自己可以处理的异常,就捕获,打日志,然后通过ErrorCode等方式返回给上一层。如果是自己无法处理或者不知道该如何处理的异常,就直接抛给上一层来处理。
Web
首先,可以明确的一点:Web层不应该再往外抛异常,因为这一层一旦抛异常,就可能会导致用户跳转到不友好的错误页面甚至看到错误信息等。
如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,加上用户容易理解的错误提示信息。
开放接口层
这一层和Web层一样,不可以抛出异常。一般通过ErrorCode和ErrorMessage反馈给外部调用方。
这一层,要自己处理好所有的异常,定义好ErrorCode,并记录好日志,便于日后排查问题。