欢迎来到:http://observer.blog.51cto.com

代码的封装是一门艺术,封装得好,不但给自己便利,还可以给自己的维护提供帮助;同时,封装得好,还可以给看自己代码的人以赏心悦目的感觉,团队之间的合作可以得到更良好的沟通;封装,不但要给人以便利,还要把自己的需求目的达到,这是一门相当有味的艺术。在此,博主一直热衷于代码的封装,有点经验,共享出来,写得不好之处希望指出,以便改过,觉得写得好的给个赞,有疑问之处请留言,在此先行拜谢!
   数据库的使用,现在已经是随处可见,此博文着重于jdbc的封装。

(一)封装的要点:
   首先,我们知道jdbc是java数据库连接,数据库的连接,咱们应该让它方便跨数据库。
   其次,方便咱们进行获取java.sql.Connection的连接与关闭,以便进行数据库操作。
   再其次,咱们应该让它方便我们的事务处理。
   最后,也是本博文最关注的一点,便是伴随着数据库封装之后出现的多并发的问题

(二)封装:
第一:
   跨数据库的封装,在此使用配置文件:jdbc.properties存放数据库连接的相关信息,然后封装一个获取数据库连接信息的类:JdbcPropertiesUtil以便信息的读取。
   我的数据库为mysql,jdbc.properties文件信息如下:

jdbc.driverClassName=com.mysql.jdbc.Driver
   jdbc.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8
   jdbc.username=root
   jdbc.password=root

我的JdbcPropertiesUtil.java文件如下:

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/*** 获取配置文件中的数据类* @author observer**/
public class JdbcPropertiesUtil {private  Properties properties = new Properties();/*** 定义一个制定配置文件名的构造函数* @param propertiesName 需要获取的配置文件名,不包括后缀*/public JdbcPropertiesUtil(String propertiesName) {try {InputStream inputStream =Thread.currentThread().getContextClassLoader().getResourceAsStream(propertiesName+".properties");properties.load(inputStream);inputStream.close();} catch (IOException e) {e.printStackTrace();}}/*** 获取配置文件中数据库的加载class* @return 加载class的字符串*/public String getDriverClassName(){return properties.getProperty("jdbc.driverClassName");}/*** 获取配置文件中数据库连接的url* @return url的字符串*/public String getUrl(){return properties.getProperty("jdbc.url");}/*** 获取配置文件中数据库连接的用户名* @return 用户名的字符串*/public String getUsername(){return properties.getProperty("jdbc.username");}/*** 获取配置文件中数据库连接的密码* @return 密码的字符串*/public String getPassword(){return properties.getProperty("jdbc.password");}
}

在此,如果想进一步封装的话,可以将获取inputStream的参数放在一个固定的配置文件properties中,如:a.properties中配置database.pathname=jdbc,然后根据文件中的参数得到"jdbc",然后再创建inputStream。这样的好处就是,在换数据库时,直接修改database.pathname的值,然后创建其相应名称的properties文件并配置就可以。如将mysql换成oracle,将a.properties中配置database.pathname=oracle;然后创建oracle.pathname并配置好即可。要是想要换回去,不用再创建,直接换database.pathname即可。在这里,小型系统一旦确定数据库,极少会再更改,所以这里就不再过分封装了。

第二:
   封装数据库连接类DatabaseUtil。
   首先因为数据库驱动的加载只要在该类第一次被访问时加载一次即可,所以在此把数据库驱动加载代码放到了一段静态代码中。
   然后就是封装Connection,Connection是数据库的一个连接类,数据库的操作离不开它,楼主的第一个反应就是将该类的对象设置为一个静态变量,这样就可以一劳永逸了,不用再每次需要进行数据库操作时都对数据库连接一次,而且可以节省每次连接数据库的时间。但是,没有happy多久,这个想法就被虐杀在摇篮中,因为有事务处理与多并发带来的各种问题。
举个很经典的例子,一个银行账户,在一台计算机上进行转账,在另一台计算机上进行网购,当转账转到一半时(此时刚好将来源账户的钱扣除,但是没有将钱打进目标账户),另一台计算机进行网购,然后将事务commit了;因为从始至终只有一个Connection,所以网购完成了,同时这边的转账出现异常也在中途结束了,最终的结果就是转账失败了,但是钱却没了
   通常的情况下,就是不设任何Connection,这样事情当然也就没有了。但是,这样做的话,与数据库打交道的类中(如DAO,以下均称DAO),每一项DAO的方法都不会知道后面的应用中是否会需要进行事物处理,也就是说DAO的每一个方法都需要传进一个Connection参数。同时,每一次需要用到DAO时,不管是否需要进行事务处理,都要在业务逻辑中得到Connection,然后才能调用DAO。这显然用着体验不是很好,起码楼主是这样觉得。
   这个问题,曾经也困扰着楼主一段时间,直到某一天发现了一个很有趣的API:java.lang.ThreadLocal。该类有一个特性,那就是如果将该类的对象设为static,那么不同的线程访问它时,它都会生成一个局部变量,它独立于变量的初始化副本;意思就是说,每一个线程第一次访问它时都是不同的一个副本,而同一个线程再次访问时,访问的还是上一次访问的副本,祥情可以产看java的API(原理上是否这样暂且不管,只要知道达到了这个效果就成)。这么有意思的API,简直是相见恨晚!就跟为事务处理与多并发封装量身定制的一样。
   既然有这么好用的一个API,那么封装条件就已经具备了,咱们说做就做,先上代码再解释:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/*** 数据库连接工具类* @author observer*/
public class DatabaseUtil {private static JdbcPropertiesUtil jpu = new JdbcPropertiesUtil("jdbc");
//  private static final ThrL threadLocal = new ThrL();private static final ThreadLocal<AutoCommit> threadLocal =new ThreadLocal<AutoCommit>();static {try {Class.forName(jpu.getDriverClassName());} catch (ClassNotFoundException ex) {System.out.println("数据库驱动加载失败");}}
//  static class ThrL{
//      public static AutoCommit autoCommit= null;;
//      public AutoCommit get(){
//          return this.autoCommit;
//      }
//
//      public void set(AutoCommit autoCommit){
//          this.autoCommit = autoCommit;
//      }
//
//      public void remove(){
//          autoCommit = null;
//      }
//  }/*** 事务管理辅助类*/private static class AutoCommit{private Connection conn = null;private boolean autoCommit = true;public void close(){if (conn != null) {try {conn.close();conn = null;autoCommit = true;} catch (SQLException e1) {e1.printStackTrace();}}}}/*** 如果threadLocal中已经存在AutoCommit(如调用过begin方法),这返回AutoCommit中的Connection,* 否则新建一个AutoCommit,然后创建一个Connection放到AutoCommit中,* 然后把AutoCommit放到threadLocal中,并返回Connection* @return 连接好数据库的java.sql.Connection*/public static Connection getConnection() {AutoCommit autoCommit = threadLocal.get();if(autoCommit==null){autoCommit = new AutoCommit();try {autoCommit.conn = DriverManager.getConnection(jpu.getUrl(),jpu.getUsername(), jpu.getPassword());threadLocal.set(autoCommit);} catch (SQLException e) {autoCommit.close();threadLocal.remove();e.printStackTrace();}}return autoCommit.conn;}/*** 如果threadLocal中的AutoCommit不为null,* 这创建一个AutoCommit并且创建数据库连接Connection并打开事务管理* 然后将AutoCommit放到threadLocal中*/public static void begin(){AutoCommit autoCommit = threadLocal.get();if(autoCommit==null){autoCommit = new AutoCommit();threadLocal.set(autoCommit);try {autoCommit.conn = DriverManager.getConnection(jpu.getUrl(),jpu.getUsername(), jpu.getPassword());autoCommit.conn.setAutoCommit(false);autoCommit.autoCommit = false;} catch (SQLException e) {autoCommit.close();threadLocal.remove();e.printStackTrace();}}}/*** 关闭java.sql.Connection*/public static void colseConn() {AutoCommit autoCommit = threadLocal.get();if (autoCommit.autoCommit && autoCommit.conn!=null) {autoCommit.close();threadLocal.remove();}}/*** 关闭java.sql.Statement* @param stml 需要关闭的java.sql.Statement*/public static void colseStml(Statement stml) {if (stml != null) {try {stml.close();} catch (SQLException e1) {e1.printStackTrace();}}}/*** 关闭java.sql.ResultSet* @param rs 关闭java.sql.ResultSet*/public static void colseRs(ResultSet rs) {if (rs != null) {try {rs.close();} catch (SQLException e1) {e1.printStackTrace();}}}/*** 当事务管理启动后,调用此方法进行提交事物,并且关闭java.sql.Connection*/public static void commit(){AutoCommit autoCommit = threadLocal.get();if(!autoCommit.autoCommit && autoCommit.conn!=null){try {autoCommit.conn.commit();} catch (SQLException e) {e.printStackTrace();}finally{autoCommit.close();threadLocal.remove();}}}/*** 当事务处理失败时调用此方法进行事务回滚,并且关闭java.sql.Connection*/public static void rollback(){AutoCommit autoCommit = threadLocal.get();if(!autoCommit.autoCommit && autoCommit.conn!=null){try {autoCommit.conn.rollback();} catch (SQLException e) {e.printStackTrace();}finally{autoCommit.close();threadLocal.remove();}}}
}

以上工具类中,定义一个ThreadLocal,ThreadLocal中放AutoCommit,AutoCommit是自己定义的一个内部类,该类用于辅助事务管理,定义Connection与autoCommit属性。每一个不同的线程第一次访问ThreadLocal时,里面的AutoCommit都为null,同一个线程再次访问时,如果上一次访问时没有remove,那么这一次访问的照样是同一个AutoCommit;
   定义getConnection方法,如果不用进行事务处理,那么直接调用该方法创建Connection;
   定义begin方法,如果需要进行事务管理,那么在事务前调用该方法,创建一个Connection,并将内部类AutoCommit的autoCommit属性设为false,将Connection的自动提交关闭,当需要进行数据库处理时,该线程调用getConnection方法得到的Connection就是已经关闭自动提交的Connection;
   定义colseConn,如果自动提交已经关闭,那么调用该方法是不能关闭Connection的,只能调用后面定义的commit方法才能进行事务的提交并且关闭Connection。
   然后就是定义rollback,该方法用于在事务提交失败时进行事务回滚,并且关闭Connection与清空AutoCommit与ThreadLocal;

更具体的东西与理解,可以下载附件看源码。

(三)验证:
   在代码中是不是看到一些注释掉的代码?不急,咱们说再多的理论都没用,事实胜于雄辩。咱对该工具类做一下验证。
第一:搭建测试环境
   首先在上面的工具类中,有一些注释,该注释掉的是一个仿ThreadLocal的内部类,它没有ThreadLocal类的多线程特性,只是一个普通的类,如果将private static final ThreadLocal<AutoCommit> threadLocal = new ThreadLocal<AutoCommit>();这句代码注释掉,换成private static final ThrL threadLocal = new ThrL();那么我们就相当于得到了一个将Connection当作静态属性的DatabaseUtil工具类。现在我们把第一种ThreadLocal假设为模式A,把ThrL假设为模式B;
   创建mysql数据库(自己创建自己对应的数据库与配置相对应的数据库配置文件):

create database test;
user test;
create table testuser(
toid int not null  auto_increment primary key comment '主键,自动增长',
testname varchar(20) not null  comment '用户名',
remaining int comment '用户余额'
);
insert into testuser(testname, remaining) value("observer1",1000);
insert into testuser(testname, remaining) value("observer2",1000);

第二:创建依赖类
   在这里楼主创建了pojo,dao接口,daoImpl,error还有manager,这里就不一一解释了,因为这些不创建也是可以的,很容易理解,不过只要有条件,楼主喜欢把他们封装好(可以下载附件源码看),咱注重于多并发带来的问题,就检重要的来讲。
   在此贴出manager以便解释:

import com.observer.database.dao.TestUserDao;
import com.observer.database.dao.impl.TestUserDaoImpl;
import com.observer.database.errer.DAOException;
import com.observer.database.errer.FormatException;
import com.observer.database.model.TestUser;
import com.observer.database.util.DatabaseUtil;
/*** TestUser的业务逻辑管理类* @author observer*/
public class TestUserManager {private static TestUserDao testUserDao = new TestUserDaoImpl();/*** 取款方法,判断用户中余额是否够取款金额取款,如果够则取款,否则抛出异常,取款失败* @param toid 需要取款账户的toid* @param money 需要取款的金额* @throws FormatException 余额不足时抛出com.observer.database.errer.FormatException* @throws DAOException 取款时出现SQL错误时回滚事务并抛出com.observer.database.errer.DAOException* @throws Exception 取款时出现FormatException,DAOException以外的异常时回滚事务并抛出java.lang.Exception*/public static void drawMoney(int toid, float money) throws FormatException,DAOException, Exception{try{//取款之前停止一下,让其他线程先运行,模拟真实情况try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}DatabaseUtil.begin();TestUser test = testUserDao.find(1);if(test.getRemaining()-money>=0){testUserDao.draw(test.getToid(), money);}else{throw new FormatException("取款失败", "您的余额不足");}DatabaseUtil.commit();}catch(DAOException e){DatabaseUtil.rollback();e.printStackTrace();throw e;}catch (Exception e) {DatabaseUtil.rollback();e.printStackTrace();throw e;}}/*** 转账取款方法,判断来源用户余额是否够转账金额转账,如果够则进行转账,否则抛出异常,转账失败* @param fromId 转账来源账户* @param toId 转账目标账户* @param money 转账金额* @throws FormatException 来源用户余额不足时抛出com.observer.database.errer.FormatException* @throws DAOException 转账时出现SQL错误时回滚事务并抛出com.observer.database.errer.DAOException* @throws Exception 转账时出现FormatException,DAOException以外的异常时回滚事务并抛出java.lang.Exception*/public static void transfer(int fromId, int toId, float money) throws FormatException, DAOException, Exception{try {DatabaseUtil.begin();TestUser test = testUserDao.find(fromId);if(test.getRemaining()-money>=0){testUserDao.draw(fromId, money);//当转账到一半时停止一下,让其他线程运行,模拟真实情况try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}if(true){throw new DAOException("断电了", "断电了,转账失败");}testUserDao.storage(toId, money);DatabaseUtil.commit();}else{throw new FormatException("转账失败", "您的余额不足");}}catch(DAOException e){DatabaseUtil.rollback();e.printStackTrace();throw e;} catch (Exception e) {DatabaseUtil.rollback();e.printStackTrace();throw e;}}
}

以上业务逻辑管理类中,分别为取款与转账,在取款中设置Thread.sleep(1000);让转账先运行,然后在转账到一半时(此时刚好将来源账户的钱扣除,但是没有将钱打进目标账户),设置Thread.sleep(2000);让取款运行,最后自己再运行,然后出现异常,模拟刚才的转账与取款的例子。

第三:编写测试类
   因为serlvet可是以多线程的方式运行的,现在使用多线程模拟一下,创建类如下:

import com.observer.database.errer.DAOException;
import com.observer.database.errer.FormatException;
import com.observer.database.manager.TestUserManager;
/*** 测试方法* @author observer**/
public class Test {public static void main(String[] args) {new Thread(new Transfer(1, 2, 100)).start();new Thread(new GetManey(1,100)).start();}
}
/*** 定义一个线程类,测试取款* @author observer**/
class GetManey implements Runnable{private int toid;private float money;public GetManey(int toid,float money){this.money = money;}public void run() {try {TestUserManager.drawMoney(toid, money);System.out.println("取款成功 : "+money);} catch (FormatException e) {e.printStackTrace();System.out.println(e.getTitle()+" : "+e.getDetails());}catch(DAOException e){e.printStackTrace();System.out.println("取款失败 : "+e.getDetails());} catch (Exception e) {e.printStackTrace();System.out.println("取款失败 : 请从新再试");}}
}
/*** 定义一个线程类测试转账* @author observer**/
class Transfer implements Runnable{private int fromId;private int toId;private float money;public void run() {try {TestUserManager.transfer(fromId, toId, money);System.out.println("转账成功 : "+money);} catch (FormatException e) {e.printStackTrace();System.out.println(e.getTitle()+" : "+e.getDetails());}catch(DAOException e){e.printStackTrace();System.out.println("转账失败 : "+e.getDetails());}  catch (Exception e) {e.printStackTrace();System.out.println("转账失败 : ");}}public Transfer(int fromId, int toId, float money) {this.fromId = fromId;this.toId = toId;this.money = money;}
}

第四:测试
   好了,一切就绪,咱们首先测试模式B,出现如下情况:
163556398.png

查看数据库如下:
164053221.png

好吧!问题终于出现了。咱们再用模式A试试,把private static final ThrL threadLocal = new ThrL();注释掉,然后使用private static final ThreadLocal<AutoCommit> threadLocal = new ThreadLocal<AutoCommit>();
   在数据库中将toid=1的数据修改到1000:update testuser set remaining=1000 where toid=1;然后运行测试得到如下结果:
163752957.png

查看数据库如下:
164227334.png

事实证明了,没有任何问题,数据非常完整,happy一下。

第五:另一个问题
   到此,楼主没有happy多久,又发现了另外一个问题,那就是存款余额的最低限制。当转账的金额与取款的金额刚刚好为存款的余额时,如果出现以上例子,而且中途没有产生异常,那么存款就会变为负值,这是相当大的一个bug。
   咱们来测试一下,先将以下代码注释掉:

//  if(true){
//      throw new DAOException("断电了", "断电了,转账失败");
//  }

这样就没有断电的异常了,然后将转账与存款金额改为存款余额,如:1000,运行得到以下结果:
164837600.png

查看数据库如下:
165026979.png

好了,要是金额多一点的话,相当于我可以不用办任何手续贷款了......
   解决这个问题,一个是在查询数据到修改数据之间进行线程锁的锁住,但是这样做会妨碍到多并发的运行,不是一个良好的决绝方案。
   在此,楼主的解决方案就是限制数据库的字段范围,设定其存款余额不能小于0,否则会出现异常,这样的话,哪个线程先提交数据库处理,那么就由哪个线程的处理成功。
   随后设置数据库表如下:

drop table testuser;
create table testuser(
toid int not null  auto_increment primary key comment '主键,自动增长',
testname varchar(20) not null  comment '用户名',
remaining int unsigned comment '用户余额'
);
insert into testuser(testname, remaining) value("observer1",1000);
insert into testuser(testname, remaining) value("observer2",1000);

然后运行测试得到如下结果:
165349298.png

数据库里面的数据如下:
165417291.png

到此,数据非常完整,而且反馈信息也都正确,终于可以happy了。

(源码请到这里下载:http://down.51cto.com/data/896293)


   到此,本封装就已经结束了,封装这门艺术,多姿多彩,不一定就一定要像我这样封装,说不定哪天博主功力有所提升,或者哪位朋友,觉得哪样封装更好,再次封装也是正常的事情。当然了,其实要真想应用到实际中去,那还是直接下载个数据库连接池用着痛快,连接池技术真的是没得说。
   注:本博文仅能为理解底层逻辑提供参考,大道在于连接池......