<本博客的内容语言组织还要进一步精炼,修订后的示范代码稍后提交!>
在JavaWeb开发的教学中,我们通常都会带着学生用JSP+Servlet+JavaBean技术完成一个或多个小型管理信息系统。在教学中,为了加深学生对灵活的软件设计的理解,采用三层架构的方式搭建应用框架。在实际操作过程中,有3个难点的地方比较难理解。
1.各层的接口声明中返回什么值?
2.异常如何处理?
3.service层如何实现事务?
接下来,我们就以上三个问题逐一分析并提出解决方案。
注:也许有人认为这个没有必要,用struts、hibernate等框架来做,整个系统架构更清楚。但是《Java Web开发》这门课程是教给学生Servlet、JSP、JavaBean等技术。只有真正地掌握了这些技术,再用框架的时候,才能更深刻的理解框架带来的好处。另外,一些小的MIS系统,完全没有必要搞的那么复杂,一堆的配置文件或注解。
首先,我们简单来回顾三层架构方面的知识。虽然我们经常谈三层架构,实际上,在使用JSP进行分层开发的时候,通常是4层,也就是业务逻辑层分离出一个Serlvet层,主要负责获取用户提交数据,业务流程跳转等。具体如下图一所示。根据面向对象的一些基本思想:面向抽象编程(方便后期的维护和拓展)。
图1 三层架构与四层架构
每一层都公开了一些接口给调用它的上层,但是对上层隐藏了自身的实现。一般的做法就是每一层提供一个接口,然后提供一个实现了该接口的类。接下来我们通过一个简单的案例,来探讨上面提出的三个问题。为了尽可能的精简代码,此处设计了两张最简单的表。具体MySQL 的DDL如下:
CREATE DATABASE `three` ;
USE `three`;
DROP TABLE IF EXISTS `log`;
CREATE TABLE `log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`info` varchar(6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1
ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1
ROW_FORMAT=DYNAMIC;
第二个问题就是异常的处理,按照异常的使用原则,异常能处理就处理,不能处理就抛出。在DAO层中,不可避免的要处理SQLException。如果在DAO中直接生命抛出异常,如下错误示范:
public void LogDao{
public void addLog(Log log) throws SQLException;
......
}
这样的话接口被污染了,如果我们的项目采用其他框架如Hibernate来持久化,Hibernate中数据库操作产生的异常类不是SQLException。这样的话,这个接口的设计就没有意义了,需要调整接口,进而影响service层对它的调用。失去了面向对象灵活的特性了。因此此处,我们的接口不应该抛出异常,而是采用自定义的异常RuntimeException类。
此处我们定义一个
public class MyRunException extends RuntimeException {
public MyRunException(String message) {
super(message);
}
public MyRunException() {
}
}
在DAO中,碰到SQL异常的时候,就直接捕获之后,抛出一个MyRunException异常的实例。由于该异常是运行时异常,不用在方法体中显式抛出。因此我们的接口可以非常干净。那么DAO中的方法就可以如下写:
public class LogDao {
...
public void addLog(Log log){
...
try {
...
} catch (SQLException ex) {
throw new MyRunException();
}
}
}
第三个问题就是service的事务处理。JDBC支持事务处理,我们的案例中MySQL也支持事务,为什么还要讨论了。在实际的业务逻辑中,往往设计对多表的操作。通常在DAO中,每一个DAO针对的是一个数据表的CRUD。这样的话,我们就需要将一个Connection传递给多个DAO,然后在service层commit业务。如此一来,我们的service的代码就类似这样的操作:
public void register(Student student){
Connection conn = DBHelp.getConn();
StudentDao sDao = new StudentDao(conn);
LogDao lDao = new LogDao(conn);
conn.setAutoCommit(false);
try{
sDao.add(student);
lDao.add(new Log(...));
}catch(MyRunException e){
try{
conn.commmit();
}catch(SQLException e){
.....
}
}
}
如此一来,我们的service还是要与SQLException耦合在一起。如何解决这个问题呢?我们引入自己的类包装一下Connection,具体代码如下:
DBExecute
public class DBExecute {
private Connection conn;
public DBExecute(Connection conn) {
this.conn = conn;
}
public void setAutoCommit(boolean flag){
try {
conn.setAutoCommit(flag);
} catch (SQLException ex) {
throw new UserSQLException(ex.getMessage());
}
}
public void commit(){
try {
conn.commit();
} catch (SQLException ex) {
throw new UserSQLException(ex.getMessage());
}
}
public Connection getConn() {
return conn;
}
}
我们的数据库的操作,数据库的连接都封装DBExecute中。我们的service将彻底地从JDBC这些类和异常中分离出来。
修改后的Dao实现如下所示:
....
public class LogDaoJdbcImpl implements LogDao {
private DBExecute execute;
public LogDaoJdbcImpl(DBExecute execute) {
this.execute = execute;
}
public void addLog(Log log){
String sql = "insert into log ( info ) values( ? ) ";
PreparedStatement ps;
try {
ps = execute.getConn().prepareStatement(sql);
ps.setString(1, log.getInfo());
ps.execute();
} catch (SQLException ex) {
throw new UserSQLException();
}
}
}
到此为止,我们解决了以上提出的三个问题。
这些操作只是介绍了基本的思想,真正要进行良好的,可以通用的类,我们的DBExecute还要封装更多的操作。就像Apache的DBUtil一样,对JDBC作了轻量级的封装。