一、前言mysql
玩过Java web的人应该都接触过JDBC,正是有了它,Java程序才能轻松地访问数据库。JDBC不少人都会,可是为何我还要写它呢?我曾经一度用烂了JDBC,一度认为JDBC不过如此,后来,我对面向对象的理解渐渐深刻,慢慢地学会了如何抽象JDBC代码,再后来,我遇到了commons-dbutils这个轻量级工具包,发现这个工具包也是对JDBC代码的抽象,并且比我写的代码更加优化。在这个过程当中,我体会到了抽象的魅力,我也但愿经过这篇文章,把个人体会分享出来。web
文章大体按必定的逻辑进行:JDBC如何使用-----这样使用有什么问题------如何改进-----分析commons-dbutils的原理sql
二、JDBC如何使用数据库
这一小节经过一个例子来讲明JDBC如何使用。设计模式
咱们大体能够讲JDBC的整个操做流程分为4步:数组
一、获取数据库链接缓存
二、建立statementide
三、执行sql语句并处理返回结果工具
四、释放不须要的资源性能
下面是一个小例子(省略了try-catch代码):
String username="root"; String password="123"; String url="jdbc:mysql://localhost/test"; Connection con=null; Statement st=null; ResultSet rs=null; //一、获取链接 Class.forName("com.mysql.jdbc.Driver");
con=DriverManager.getConnection(url,username,password); //二、建立statement String sql="select * from test_user"; st=con.createStatement(); //三、执行sql语句并处理返回结果 rs=st.executeQuery(sql); while(rs.next()) { //对结果进行处理 } //4、释放资源 rs.close(); st.close(); con.close();
以上的例子是查询的一种用法,除了用Statement外,还能够用PreparedStatement,后者是前者的子类,在前者的基础上增长了预编译和防止sql注入的功能。另外,查询和增删改是不一样的用法,查询会返回ResultSet而增删改不会。
三、这样写代码有什么问题
3.一、这样写代码会形成大量重复劳动,好比获取链接,若是每一个执行sql的方法都要写一遍相同的代码,那么这样的重复代码将充斥整个DAO层。
3.二、这样的代码可读性比较差,几十行代码真正和业务相关的其实就几行
3.三、大量重复代码会形成一个问题,那就是可维护性变差,一旦某个常量改变了,那么就须要把每一个方法都改一遍
3.四、数据库链接是重量级资源,每调用一次方法都去建立一个链接,性能会存在瓶颈
四、如何改进
针对前面的问题中的一、二、3,改进的方法就是抽象,把可重用的代码抽象出去,单独组成一个模块,模块与模块之间实现解耦。因为整个JDBC操做流程分为4步,所以能够从这4步中下手去抽象。
4.一、获取数据库链接
我当时的解决方案是一次初始化不少链接放入list,而后用的时候取,如今的通用方法就是链接池,好比DBCP、C3P0等等。有兴趣的人能够去看看它们的源代码,看看是如何实现的
4.二、建立statement
我当时使用PreparedStatement进行处理,由于PreparedStatement会缓存已经编译过的sql
4.三、执行sql语句并处理返回结果
这块可使用反射,将获得的结果封装成Java bean对象
4.四、释放资源
使用动态代理,改变connection的close方法的行为,将connection放回链接池
五、commons-dbutils的原理
虽然我作出了改进,但距离真正的解耦还差得远,而commons-dbutils做为commons开源项目组中的一个成员,在这方面作得还算不错,经过阅读它的源代码,能够学习如何抽象和解耦JDBC的操做流程。
5.一、总体结构
先看一下它有哪些类:
一共有27个类,但真正经常使用的是三大组件十几个类:门面组件、结果处理组件和行处理组件,其中门面组件提供程序入口,并进行一些参数检验等,结果处理组件则是核心所在,由于返回的结果能够是map,能够是list能够是JavaBean,这一块的变化很大,因此抽象出一个组件出来应对这些变化,行处理组件是从结果处理组件中分离出来的,它是结果处理组件的基础,不管哪一种处理器,最终都要与一行数据打交道,所以,单独抽象出这一组件。
类名 | 描述 |
门面组件 | |
QueryRunner | 执行增删改查的入口 |
结果处理组件 | |
ResultSetHandler | 用于处理ResultSet的接口 |
AbstractKeyedHandler | 将返回结果处理成键值对的抽象类 |
KeyedHandler | 处理数据库返回结果,封装成一个Map,数据库表的一个列名为key,一般能够用主键,数据库中的一行结果以Map的形式做为value |
BeanMapHandler | 处理数据库返回结果,封装成一个Map,和KeyedHandler的惟一的不一样是,每一行结果以Javabean的形式做为value |
AbstractListHandler | 将返回结果处理成链表的抽象类 |
ArrayListHandler | 将返回结果处理成链表,这个链表的每一个 元素都是一个Object数组,保存了数据库中对应的一行数据 |
ColumnListHandler | 若是要取单独一列数据,能够用这个handler,用户指定列名,它返回这个 列的一个list |
MapListHandler | 和ArrayListHandler不一样的是,链表的每一个元素是个Map,这个Map表明数据库里的一行数据 |
ArrayHandler | 将一行数据处理成object数组 |
BeanHandler | 将一行数据处理成一个Java bean |
BeanListHandler | 将全部数据处理成一个list,list的元素时Java bean |
MapHandler | 将一行结果处理成一个Map |
MapListHandler | 将全部结果处理成一个list,list的元素时Map |
ScalarHandler | 这个类经常用于取单个数据,好比某一数据集的总数等等 |
行处理组件 |
|
RowProcessor | 用于处理数据库中一行数据的接口 |
BasicRowProcessor | 基本的行处理器实现类 |
BeanProcessor | 经过反射将数据库数据转换成Javabean |
工具类 | |
DbUtils | 包含不少JDBC工具方法 |
5.2 执行流程
不管是增删改查,都须要调用QueryRunner的方法,所以QueryRunner就是执行的入口。它的每一个方法,都须要用户提供connection、handler、sql以及sql的参数,而返回的则是用户想要的结果,这多是一个List,一个Javabean或者仅仅是一个Integer。
一、以查询为例,QueryRunner内部的每个查询方法都会调用私有方法,先去建立 PreparedStatement,而后执行sql获得ResultSet,而后用handler对结果进行处理,最后释放链接,代码以下:
1 private <T> T query(Connection conn, boolean closeConn, String sql, ResultSetHandler<T> rsh, Object... params) 2 throws SQLException { 3 if (conn == null) { 4 throw new SQLException("Null connection"); 5 } 6 7 if (sql == null) { 8 if (closeConn) { 9 close(conn); 10 } 11 throw new SQLException("Null SQL statement"); 12 } 13 14 if (rsh == null) { 15 if (closeConn) { 16 close(conn); 17 } 18 throw new SQLException("Null ResultSetHandler"); 19 } 20 21 PreparedStatement stmt = null; 22 ResultSet rs = null; 23 T result = null; 24 25 try { 26 stmt = this.prepareStatement(conn, sql); //建立statement 27 this.fillStatement(stmt, params); //填充参数 28 rs = this.wrap(stmt.executeQuery()); //对rs进行包装 29 result = rsh.handle(rs); //使用结果处理器进行处理 30 31 } catch (SQLException e) { 32 this.rethrow(e, sql, params); 33 34 } finally { 35 try { 36 close(rs); 37 } finally { 38 close(stmt); 39 if (closeConn) { 40 close(conn); 41 } 42 } 43 } 44 45 return result; 46 }
二、每一个handler的实现类都是以抽象类为基础,看代码(以AbstractListHandler为例):
1 @Override 2 public List<T> handle(ResultSet rs) throws SQLException { 3 List<T> rows = new ArrayList<T>(); 4 while (rs.next()) { 5 rows.add(this.handleRow(rs)); 6 } 7 return rows; 8 } 9 10 /** 11 * Row handler. Method converts current row into some Java object. 12 * 13 * @param rs <code>ResultSet</code> to process. 14 * @return row processing result 15 * @throws SQLException error occurs 16 */ 17 protected abstract T handleRow(ResultSet rs) throws SQLException;
handle方法都是同样的,这个方法也是QueryRunner内部执行的方法,而不同的在handleRow这个方法的实现上。这里用到了模板方法的设计模式,
将不变的抽象到上层,易变的下方到下层。
三、每一个handleRow的实现都不同,但最终都会使用行处理器组件,行处理器是BasicRowProcessor,有toArray,toBean,toBeanList,toMap这些方法
toArray和toMap是经过数据库的元数据来实现的,而toBean和toBeanList则是经过反射实现,具体能够去看源代码实现,应该是比较好理解的。
5.三、和数据源的结合
从上面能够看出,dbutils抽象了二、三、4(JDBC 4步骤),而没有把链接的获取抽象,其实,链接的获取和维护自己就有其余组件提供,也就是datasource
数据源,dbutils只负责二、三、4,不应它管就无论,这样才能作到解耦。在构造QueryRunner的时候,能够选择传入一个数据源,这样,在调用方法的时候,
就不须要传入connection了。
5.四、总结
使用dbutils再加上DBCP数据源,能够极大的简化重复代码,提升代码可读性和可维护性,如下是使用dbutils的一个小例子:
1 /** 2 * 获取经常使用地址 3 * */ 4 public List<CommonAddr> getCommAddrList(int memID) { 5 String sql = "SELECT `addrID`, `addr`, `phone`, `receiver`, `usedTime` " 6 + "FROM `usr_cm_address` WHERE `memID`=? order by usedTime desc"; 7 8 try { 9 return runner.query(sql, new BeanListHandler<CommonAddr>(CommonAddr.class),memID); 10 } catch (SQLException e1) { 11 logger.error("getCommAddrList error,e={}",e1); 12 } 13 return null; 14 }
若是用最原始的JDBC来写,光把数据库结果转换成List估计都要十几行代码吧。
六、尾声
从JDBC到dbutils,实现的功能没有变,可是代码却简洁了,程序与程序之间的关系也更清晰了,这,也许就是面向对象的精髓吧~