一、commons-dbutils簡介
commons-dbutils 是 Apache 組織提供的一個開源 JDBC工具類庫,它是對JDBC的簡單封裝,學習成本極低,並且使用dbutils能極大簡化jdbc編碼的工作量,同時也不會影響程序的性能。因此dbutils成爲很多不喜歡hibernate的公司的首選。
commons-dbutilsAPI介紹:
- org.apache.commons.dbutils.QueryRunner
- org.apache.commons.dbutils.ResultSetHandler
工具類
- org.apache.commons.dbutils.DbUtils
二、QueryRunner類使用講解
該類簡單化了SQL查詢,它與ResultSetHandler組合在一起使用可以完成大部分的數據庫操作,能夠大大減少編碼量。
QueryRunner類提供了兩個構造方法:
- 默認的構造方法
- 需要一個 javax.sql.DataSource 來作參數的構造方法。
2.1、QueryRunner類的主要方法
public Object query(Connection conn, String sql, Object[] params, ResultSetHandler rsh) throws SQLException:執行一個查詢操作,在這個查詢中,對象數組中的每個元素值被用來作爲查詢語句的置換參數。該方法會自行處理 PreparedStatement 和 ResultSet 的創建和關閉。
public Object query(String sql, Object[] params, ResultSetHandler rsh) throws SQLException: 幾乎與第一種方法一樣;唯一的不同在於它不將數據庫連接提供給方法,並且它是從提供給構造方法的數據源(DataSource) 或使用的setDataSource 方法中重新獲得 Connection。
public Object query(Connection conn, String sql, ResultSetHandler rsh) throws SQLException : 執行一個不需要置換參數的查詢操作。
public int update(Connection conn, String sql, Object[] params) throws SQLException:用來執行一個更新(插入、更新或刪除)操作。
public int update(Connection conn, String sql) throws SQLException:用來執行一個不需要置換參數的更新操作。
2.2、使用QueryRunner類實現CRUD
1 package me.gacl.test;
2
3 import java.util.Date;
4 import java.util.List;
5 import java.io.File;
6 import java.io.FileReader;
7 import java.io.IOException;
8 import java.sql.SQLException;
9 import javax.sql.rowset.serial.SerialClob;
10 import me.gacl.domain.User;
11 import me.gacl.util.JdbcUtils;
12 import org.apache.commons.dbutils.QueryRunner;
13 import org.apache.commons.dbutils.handlers.BeanHandler;
14 import org.apache.commons.dbutils.handlers.BeanListHandler;
15 import org.junit.Test;
16
17 /**
18 * @ClassName: DBUtilsCRUDTest
19 * @Description:使用dbutils框架的QueryRunner類完成CRUD,以及批處理
20 * @author: 孤傲蒼狼
21 * @date: 2014-10-5 下午4:56:44
22 *
23 */
24 public class QueryRunnerCRUDTest {
25
26 /*
27 *測試表
28 create table users(
29 id int primary key auto_increment,
30 name varchar(40),
31 password varchar(40),
32 email varchar(60),
33 birthday date
34 );
35 */
36
37 @Test
38 public void add() throws SQLException {
39 //將數據源傳遞給QueryRunner,QueryRunner內部通過數據源獲取數據庫連接
40 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
41 String sql = "insert into users(name,password,email,birthday) values(?,?,?,?)";
42 Object params[] = {"孤傲蒼狼","123", "[email protected]", new Date()};
43 //Object params[] = {"白虎神皇","123", "[email protected]", "1988-05-07"};
44 qr.update(sql, params);
45 }
46
47 @Test
48 public void delete() throws SQLException {
49
50 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
51 String sql = "delete from users where id=?";
52 qr.update(sql, 1);
53
54 }
55
56 @Test
57 public void update() throws SQLException {
58 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
59 String sql = "update users set name=? where id=?";
60 Object params[] = { "ddd", 5};
61 qr.update(sql, params);
62 }
63
64 @Test
65 public void find() throws SQLException {
66 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
67 String sql = "select * from users where id=?";
68 Object params[] = {2};
69 User user = (User) qr.query(sql, params, new BeanHandler(User.class));
70 System.out.println(user.getBirthday());
71 }
72
73 @Test
74 public void getAll() throws SQLException {
75 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
76 String sql = "select * from users";
77 List list = (List) qr.query(sql, new BeanListHandler(User.class));
78 System.out.println(list.size());
79 }
80
81 /**
82 * @Method: testBatch
83 * @Description:批處理
84 * @Anthor:孤傲蒼狼
85 *
86 * @throws SQLException
87 */
88 @Test
89 public void testBatch() throws SQLException {
90 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
91 String sql = "insert into users(name,password,email,birthday) values(?,?,?,?)";
92 Object params[][] = new Object[10][];
93 for (int i = 0; i < 10; i++) {
94 params[i] = new Object[] { "aa" + i, "123", "[email protected]",
95 new Date() };
96 }
97 qr.batch(sql, params);
98 }
99
100 //用dbutils完成大數據(不建議用)
101 /***************************************************************************
102 create table testclob
103 (
104 id int primary key auto_increment,
105 resume text
106 );
107 **************************************************************************/
108 @Test
109 public void testclob() throws SQLException, IOException{
110 QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
111 String sql = "insert into testclob(resume) values(?)"; //clob
112 //這種方式獲取的路徑,其中的空格會被使用“%20”代替
113 String path = QueryRunnerCRUDTest.class.getClassLoader().getResource("data.txt").getPath();
114 //將“%20”替換回空格
115 path = path.replaceAll("%20", " ");
116 FileReader in = new FileReader(path);
117 char[] buffer = new char[(int) new File(path).length()];
118 in.read(buffer);
119 SerialClob clob = new SerialClob(buffer);
120 Object params[] = {clob};
121 runner.update(sql, params);
122 }
123 }
三、ResultSetHandler接口使用講解
該接口用於處理java.sql.ResultSet,將數據按要求轉換爲另一種形式。
ResultSetHandler接口提供了一個單獨的方法:Object handle (java.sql.ResultSet .rs)
ResultSetHandle結果集處理類
3.1、ResultSetHandler接口的實現類
- ArrayHandler:把結果集中的第一行數據轉成對象數組。
- ArrayListHandler:把結果集中的每一行數據都轉成一個數組,再存放到List中。
- BeanHandler:將結果集中的第一行數據封裝到一個對應的JavaBean實例中。
- BeanListHandler:將結果集中的每一行數據都封裝到一個對應的JavaBean實例中,存放到List裏。
- ColumnListHandler:將結果集中某一列的數據存放到List中。
- KeyedHandler(name):將結果集中的每一行數據都封裝到一個Map裏,再把這些map再存到一個map裏,其key爲指定的key。
- MapHandler:將結果集中的第一行數據封裝到一個Map裏,key是列名,value就是對應的值。
- MapListHandler:將結果集中的每一行數據都封裝到一個Map裏,然後再存放到List
3.2、測試dbutils各種類型的處理器
1 package me.gacl.test;
2
3 import java.sql.SQLException;
4 import java.util.Arrays;
5 import java.util.List;
6 import java.util.Map;
7 import me.gacl.util.JdbcUtils;
8 import org.apache.commons.dbutils.QueryRunner;
9 import org.apache.commons.dbutils.handlers.ArrayHandler;
10 import org.apache.commons.dbutils.handlers.ArrayListHandler;
11 import org.apache.commons.dbutils.handlers.ColumnListHandler;
12 import org.apache.commons.dbutils.handlers.KeyedHandler;
13 import org.apache.commons.dbutils.handlers.MapHandler;
14 import org.apache.commons.dbutils.handlers.MapListHandler;
15 import org.apache.commons.dbutils.handlers.ScalarHandler;
16 import org.junit.Test;
17
18 /**
19 * @ClassName: ResultSetHandlerTest
20 * @Description:測試dbutils各種類型的處理器
21 * @author: 孤傲蒼狼
22 * @date: 2014-10-6 上午8:39:14
23 *
24 */
25 public class ResultSetHandlerTest {
26
27 @Test
28 public void testArrayHandler() throws SQLException{
29 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
30 String sql = "select * from users";
31 Object result[] = (Object[]) qr.query(sql, new ArrayHandler());
32 System.out.println(Arrays.asList(result)); //list toString()
33 }
34
35 @Test
36 public void testArrayListHandler() throws SQLException{
37
38 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
39 String sql = "select * from users";
40 List<Object[]> list = (List) qr.query(sql, new ArrayListHandler());
41 for(Object[] o : list){
42 System.out.println(Arrays.asList(o));
43 }
44 }
45
46 @Test
47 public void testColumnListHandler() throws SQLException{
48 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
49 String sql = "select * from users";
50 List list = (List) qr.query(sql, new ColumnListHandler("id"));
51 System.out.println(list);
52 }
53
54 @Test
55 public void testKeyedHandler() throws Exception{
56 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
57 String sql = "select * from users";
58
59 Map<Integer,Map> map = (Map) qr.query(sql, new KeyedHandler("id"));
60 for(Map.Entry<Integer, Map> me : map.entrySet()){
61 int id = me.getKey();
62 Map<String,Object> innermap = me.getValue();
63 for(Map.Entry<String, Object> innerme : innermap.entrySet()){
64 String columnName = innerme.getKey();
65 Object value = innerme.getValue();
66 System.out.println(columnName + "=" + value);
67 }
68 System.out.println("----------------");
69 }
70 }
71
72 @Test
73 public void testMapHandler() throws SQLException{
74
75 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
76 String sql = "select * from users";
77
78 Map<String,Object> map = (Map) qr.query(sql, new MapHandler());
79 for(Map.Entry<String, Object> me : map.entrySet())
80 {
81 System.out.println(me.getKey() + "=" + me.getValue());
82 }
83 }
84
85
86 @Test
87 public void testMapListHandler() throws SQLException{
88 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
89 String sql = "select * from users";
90 List<Map> list = (List) qr.query(sql, new MapListHandler());
91 for(Map<String,Object> map :list){
92 for(Map.Entry<String, Object> me : map.entrySet())
93 {
94 System.out.println(me.getKey() + "=" + me.getValue());
95 }
96 }
97 }
98
99 @Test
100 public void testScalarHandler() throws SQLException{
101 QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());
102 String sql = "select count(*) from users"; //[13] list[13]
103 int count = ((Long)qr.query(sql, new ScalarHandler(1))).intValue();
104 System.out.println(count);
105 }
106 }
三、DbUtils類使用講解
DbUtils :提供如關閉連接、裝載JDBC驅動程序等常規工作的工具類,裏面的所有方法都是靜態的。主要方法如下:
public static void close(…) throws java.sql.SQLException: DbUtils類提供了三個重載的關閉方法。這些方法檢查所提供的參數是不是NULL,如果不是的話,它們就關閉Connection、Statement和ResultSet。
public static void closeQuietly(…): 這一類方法不僅能在Connection、Statement和ResultSet爲NULL情況下避免關閉,還能隱藏一些在程序中拋出的SQLEeception。
public static void commitAndCloseQuietly(Connection conn): 用來提交連接,然後關閉連接,並且在關閉連接時不拋出SQL異常。
public static boolean loadDriver(java.lang.String driverClassName):這一方裝載並註冊JDBC驅動程序,如果成功就返回true。使用該方法,你不需要捕捉這個異常ClassNotFoundException。
四、JDBC開發中的事務處理
在開發中,對數據庫的多個表或者對一個表中的多條數據執行更新操作時要保證對多個更新操作要麼同時成功,要麼都不成功,這就涉及到對多個更新操作的事務管理問題了。比如銀行業務中的轉賬問題,A用戶向B用戶轉賬100元,假設A用戶和B用戶的錢都存儲在Account表,那麼A用戶向B用戶轉賬時就涉及到同時更新Account表中的A用戶的錢和B用戶的錢,用SQL來表示就是:
1 update account set money=money-100 where name='A'
2 update account set money=money+100 where name='B'
4.1、在數據訪問層(Dao)中處理事務
對於這樣的同時更新一個表中的多條數據的操作,那麼必須保證要麼同時成功,要麼都不成功,所以需要保證這兩個update操作在同一個事務中進行。在開發中,我們可能會在AccountDao寫一個轉賬處理方法,如下:
1 /**
2 * @Method: transfer
3 * @Description:這個方法是用來處理兩個用戶之間的轉賬業務
4 * 在開發中,DAO層的職責應該只涉及到CRUD,
5 * 而這個transfer方法是處理兩個用戶之間的轉賬業務的,已經涉及到具體的業務操作,應該在業務層中做,不應該出現在DAO層的
6 * 所以在開發中DAO層出現這樣的業務處理方法是完全錯誤的
7 * @Anthor:孤傲蒼狼
8 *
9 * @param sourceName
10 * @param targetName
11 * @param money
12 * @throws SQLException
13 */
14 public void transfer(String sourceName,String targetName,float money) throws SQLException{
15 Connection conn = null;
16 try{
17 conn = JdbcUtils.getConnection();
18 //開啓事務
19 conn.setAutoCommit(false);
20 /**
21 * 在創建QueryRunner對象時,不傳遞數據源給它,是爲了保證這兩條SQL在同一個事務中進行,
22 * 我們手動獲取數據庫連接,然後讓這兩條SQL使用同一個數據庫連接執行
23 */
24 QueryRunner runner = new QueryRunner();
25 String sql1 = "update account set money=money-100 where name=?";
26 String sql2 = "update account set money=money+100 where name=?";
27 Object[] paramArr1 = {sourceName};
28 Object[] paramArr2 = {targetName};
29 runner.update(conn,sql1,paramArr1);
30 //模擬程序出現異常讓事務回滾
31 int x = 1/0;
32 runner.update(conn,sql2,paramArr2);
33 //sql正常執行之後就提交事務
34 conn.commit();
35 }catch (Exception e) {
36 e.printStackTrace();
37 if(conn!=null){
38 //出現異常之後就回滾事務
39 conn.rollback();
40 }
41 }finally{
42 //關閉數據庫連接
43 conn.close();
44 }
45 }
然後我們在AccountService中再寫一個同名方法,在方法內部調用AccountDao的transfer方法處理轉賬業務,如下:
1 public void transfer(String sourceName,String targetName,float money) throws SQLException{
2 AccountDao dao = new AccountDao();
3 dao.transfer(sourceName, targetName, money);
4 }
上面AccountDao的這個transfer方法可以處理轉賬業務,並且保證了在同一個事務中進行,但是AccountDao的這個transfer方法是處理兩個用戶之間的轉賬業務的,已經涉及到具體的業務操作,應該在業務層中做,不應該出現在DAO層的,在開發中,DAO層的職責應該只涉及到基本的CRUD,不涉及具體的業務操作,所以在開發中DAO層出現這樣的業務處理方法是一種不好的設計。
4.2、在業務層(BusinessService)處理事務
由於上述AccountDao存在具體的業務處理方法,導致AccountDao的職責不夠單一,下面我們對AccountDao進行改造,讓AccountDao的職責只是做CRUD操作,將事務的處理挪到業務層(BusinessService),改造後的AccountDao如下:
1 package me.gacl.dao;
2
3 import java.sql.Connection;
4 import java.sql.SQLException;
5 import org.apache.commons.dbutils.QueryRunner;
6 import org.apache.commons.dbutils.handlers.BeanHandler;
7 import me.gacl.domain.Account;
8 import me.gacl.util.JdbcUtils;
9
10 /*account測試表
11 create table account(
12 id int primary key auto_increment,
13 name varchar(40),
14 money float
15 )character set utf8 collate utf8_general_ci;
16
17 insert into account(name,money) values('A',1000);
18 insert into account(name,money) values('B',1000);
19 insert into account(name,money) values('C',1000);
20
21 */
22
23 /**
24 * @ClassName: AccountDao
25 * @Description: 針對Account對象的CRUD
26 * @author: 孤傲蒼狼
27 * @date: 2014-10-6 下午4:00:42
28 *
29 */
30 public class AccountDao {
31
32 //接收service層傳遞過來的Connection對象
33 private Connection conn = null;
34
35 public AccountDao(Connection conn){
36 this.conn = conn;
37 }
38
39 public AccountDao(){
40
41 }
42
43 /**
44 * @Method: update
45 * @Description:更新
46 * @Anthor:孤傲蒼狼
47 *
48 * @param account
49 * @throws SQLException
50 */
51 public void update(Account account) throws SQLException{
52
53 QueryRunner qr = new QueryRunner();
54 String sql = "update account set name=?,money=? where id=?";
55 Object params[] = {account.getName(),account.getMoney(),account.getId()};
56 //使用service層傳遞過來的Connection對象操作數據庫
57 qr.update(conn,sql, params);
58
59 }
60
61 /**
62 * @Method: find
63 * @Description:查找
64 * @Anthor:孤傲蒼狼
65 *
66 * @param id
67 * @return
68 * @throws SQLException
69 */
70 public Account find(int id) throws SQLException{
71 QueryRunner qr = new QueryRunner();
72 String sql = "select * from account where id=?";
73 //使用service層傳遞過來的Connection對象操作數據庫
74 return (Account) qr.query(conn,sql, id, new BeanHandler(Account.class));
75 }
76 }
接着對AccountService(業務層)中的transfer方法的改造,在業務層(BusinessService)中處理事務
1 package me.gacl.service;
2
3 import java.sql.Connection;
4 import java.sql.SQLException;
5 import me.gacl.dao.AccountDao;
6 import me.gacl.domain.Account;
7 import me.gacl.util.JdbcUtils;
8
9 /**
10 * @ClassName: AccountService
11 * @Description: 業務邏輯處理層
12 * @author: 孤傲蒼狼
13 * @date: 2014-10-6 下午5:30:15
14 *
15 */
16 public class AccountService {
17
18 /**
19 * @Method: transfer
20 * @Description:這個方法是用來處理兩個用戶之間的轉賬業務
21 * @Anthor:孤傲蒼狼
22 *
23 * @param sourceid
24 * @param tartgetid
25 * @param money
26 * @throws SQLException
27 */
28 public void transfer(int sourceid,int tartgetid,float money) throws SQLException{
29 Connection conn = null;
30 try{
31 //獲取數據庫連接
32 conn = JdbcUtils.getConnection();
33 //開啓事務
34 conn.setAutoCommit(false);
35 //將獲取到的Connection傳遞給AccountDao,保證dao層使用的是同一個Connection對象操作數據庫
36 AccountDao dao = new AccountDao(conn);
37 Account source = dao.find(sourceid);
38 Account target = dao.find(tartgetid);
39
40 source.setMoney(source.getMoney()-money);
41 target.setMoney(target.getMoney()+money);
42
43 dao.update(source);
44 //模擬程序出現異常讓事務回滾
45 int x = 1/0;
46 dao.update(target);
47 //提交事務
48 conn.commit();
49 }catch (Exception e) {
50 e.printStackTrace();
51 //出現異常之後就回滾事務
52 conn.rollback();
53 }finally{
54 conn.close();
55 }
56 }
57 }
程序經過這樣改造之後就比剛纔好多了,AccountDao只負責CRUD,裏面沒有具體的業務處理方法了,職責就單一了,而AccountService則負責具體的業務邏輯和事務的處理,需要操作數據庫時,就調用AccountDao層提供的CRUD方法操作數據庫。
4.3、使用ThreadLocal進行更加優雅的事務處理
上面的在businessService層這種處理事務的方式依然不夠優雅,爲了能夠讓事務處理更加優雅,我們使用ThreadLocal類進行改造,ThreadLocal一個容器,向這個容器存儲的對象,在當前線程範圍內都可以取得出來,向ThreadLocal裏面存東西就是向它裏面的Map存東西的,然後ThreadLocal把這個Map掛到當前的線程底下,這樣Map就只屬於這個線程了
ThreadLocal類的使用範例如下:
1 package me.gacl.test;
2
3 public class ThreadLocalTest {
4
5 public static void main(String[] args) {
6 //得到程序運行時的當前線程
7 Thread currentThread = Thread.currentThread();
8 System.out.println(currentThread);
9 //ThreadLocal一個容器,向這個容器存儲的對象,在當前線程範圍內都可以取得出來
10 ThreadLocal<String> t = new ThreadLocal<String>();
11 //把某個對象綁定到當前線程上 對象以鍵值對的形式存儲到一個Map集合中,對象的的key是當前的線程,如: map(currentThread,"aaa")
12 t.set("aaa");
13 //獲取綁定到當前線程中的對象
14 String value = t.get();
15 //輸出value的值是aaa
16 System.out.println(value);
17 }
18 }
使用使用ThreadLocal類進行改造數據庫連接工具類JdbcUtils,改造後的代碼如下:
1 package me.gacl.util;
2
3 import java.sql.Connection;
4 import java.sql.SQLException;
5 import javax.sql.DataSource;
6 import com.mchange.v2.c3p0.ComboPooledDataSource;
7
8 /**
9 * @ClassName: JdbcUtils2
10 * @Description: 數據庫連接工具類
11 * @author: 孤傲蒼狼
12 * @date: 2014-10-4 下午6:04:36
13 *
14 */
15 public class JdbcUtils2 {
16
17 private static ComboPooledDataSource ds = null;
18 //使用ThreadLocal存儲當前線程中的Connection對象
19 private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
20
21 //在靜態代碼塊中創建數據庫連接池
22 static{
23 try{
24 //通過代碼創建C3P0數據庫連接池
25 /*ds = new ComboPooledDataSource();
26 ds.setDriverClass("com.mysql.jdbc.Driver");
27 ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbcstudy");
28 ds.setUser("root");
29 ds.setPassword("XDP");
30 ds.setInitialPoolSize(10);
31 ds.setMinPoolSize(5);
32 ds.setMaxPoolSize(20);*/
33
34 //通過讀取C3P0的xml配置文件創建數據源,C3P0的xml配置文件c3p0-config.xml必須放在src目錄下
35 //ds = new ComboPooledDataSource();//使用C3P0的默認配置來創建數據源
36 ds = new ComboPooledDataSource("MySQL");//使用C3P0的命名配置來創建數據源
37
38 }catch (Exception e) {
39 throw new ExceptionInInitializerError(e);
40 }
41 }
42
43 /**
44 * @Method: getConnection
45 * @Description: 從數據源中獲取數據庫連接
46 * @Anthor:孤傲蒼狼
47 * @return Connection
48 * @throws SQLException
49 */
50 public static Connection getConnection() throws SQLException{
51 //從當前線程中獲取Connection
52 Connection conn = threadLocal.get();
53 if(conn==null){
54 //從數據源中獲取數據庫連接
55 conn = getDataSource().getConnection();
56 //將conn綁定到當前線程
57 threadLocal.set(conn);
58 }
59 return conn;
60 }
61
62 /**
63 * @Method: startTransaction
64 * @Description: 開啓事務
65 * @Anthor:孤傲蒼狼
66 *
67 */
68 public static void startTransaction(){
69 try{
70 Connection conn = threadLocal.get();
71 if(conn==null){
72 conn = getConnection();
73 //把 conn綁定到當前線程上
74 threadLocal.set(conn);
75 }
76 //開啓事務
77 conn.setAutoCommit(false);
78 }catch (Exception e) {
79 throw new RuntimeException(e);
80 }
81 }
82
83 /**
84 * @Method: rollback
85 * @Description:回滾事務
86 * @Anthor:孤傲蒼狼
87 *
88 */
89 public static void rollback(){
90 try{
91 //從當前線程中獲取Connection
92 Connection conn = threadLocal.get();
93 if(conn!=null){
94 //回滾事務
95 conn.rollback();
96 }
97 }catch (Exception e) {
98 throw new RuntimeException(e);
99 }
100 }
101
102 /**
103 * @Method: commit
104 * @Description:提交事務
105 * @Anthor:孤傲蒼狼
106 *
107 */
108 public static void commit(){
109 try{
110 //從當前線程中獲取Connection
111 Connection conn = threadLocal.get();
112 if(conn!=null){
113 //提交事務
114 conn.commit();
115 }
116 }catch (Exception e) {
117 throw new RuntimeException(e);
118 }
119 }
120
121 /**
122 * @Method: close
123 * @Description:關閉數據庫連接(注意,並不是真的關閉,而是把連接還給數據庫連接池)
124 * @Anthor:孤傲蒼狼
125 *
126 */
127 public static void close(){
128 try{
129 //從當前線程中獲取Connection
130 Connection conn = threadLocal.get();
131 if(conn!=null){
132 conn.close();
133 //解除當前線程上綁定conn
134 threadLocal.remove();
135 }
136 }catch (Exception e) {
137 throw new RuntimeException(e);
138 }
139 }
140
141 /**
142 * @Method: getDataSource
143 * @Description: 獲取數據源
144 * @Anthor:孤傲蒼狼
145 * @return DataSource
146 */
147 public static DataSource getDataSource(){
148 //從數據源中獲取數據庫連接
149 return ds;
150 }
151 }
對AccountDao進行改造,數據庫連接對象不再需要service層傳遞過來,而是直接從JdbcUtils2提供的getConnection方法去獲取,改造後的AccountDao如下:
1 package me.gacl.dao;
2
3 import java.sql.Connection;
4 import java.sql.SQLException;
5 import org.apache.commons.dbutils.QueryRunner;
6 import org.apache.commons.dbutils.handlers.BeanHandler;
7 import me.gacl.domain.Account;
8 import me.gacl.util.JdbcUtils;
9 import me.gacl.util.JdbcUtils2;
10
11 /*
12 create table account(
13 id int primary key auto_increment,
14 name varchar(40),
15 money float
16 )character set utf8 collate utf8_general_ci;
17
18 insert into account(name,money) values('A',1000);
19 insert into account(name,money) values('B',1000);
20 insert into account(name,money) values('C',1000);
21
22 */
23
24 /**
25 * @ClassName: AccountDao
26 * @Description: 針對Account對象的CRUD
27 * @author: 孤傲蒼狼
28 * @date: 2014-10-6 下午4:00:42
29 *
30 */
31 public class AccountDao2 {
32
33 public void update(Account account) throws SQLException{
34
35 QueryRunner qr = new QueryRunner();
36 String sql = "update account set name=?,money=? where id=?";
37 Object params[] = {account.getName(),account.getMoney(),account.getId()};
38 //JdbcUtils2.getConnection()獲取當前線程中的Connection對象
39 qr.update(JdbcUtils2.getConnection(),sql, params);
40
41 }
42
43 public Account find(int id) throws SQLException{
44 QueryRunner qr = new QueryRunner();
45 String sql = "select * from account where id=?";
46 //JdbcUtils2.getConnection()獲取當前線程中的Connection對象
47 return (Account) qr.query(JdbcUtils2.getConnection(),sql, id, new BeanHandler(Account.class));
48 }
49 }
對AccountService進行改造,service層不再需要傳遞數據庫連接Connection給Dao層,改造後的AccountService如下:
1 package me.gacl.service;
2
3 import java.sql.SQLException;
4 import me.gacl.dao.AccountDao2;
5 import me.gacl.domain.Account;
6 import me.gacl.util.JdbcUtils2;
7
8 public class AccountService2 {
9
10 /**
11 * @Method: transfer
12 * @Description:在業務層處理兩個賬戶之間的轉賬問題
13 * @Anthor:孤傲蒼狼
14 *
15 * @param sourceid
16 * @param tartgetid
17 * @param money
18 * @throws SQLException
19 */
20 public void transfer(int sourceid,int tartgetid,float money) throws SQLException{
21 try{
22 //開啓事務,在業務層處理事務,保證dao層的多個操作在同一個事務中進行
23 JdbcUtils2.startTransaction();
24 AccountDao2 dao = new AccountDao2();
25
26 Account source = dao.find(sourceid);
27 Account target = dao.find(tartgetid);
28 source.setMoney(source.getMoney()-money);
29 target.setMoney(target.getMoney()+money);
30
31 dao.update(source);
32 //模擬程序出現異常讓事務回滾
33 int x = 1/0;
34 dao.update(target);
35
36 //SQL正常執行之後提交事務
37 JdbcUtils2.commit();
38 }catch (Exception e) {
39 e.printStackTrace();
40 //出現異常之後就回滾事務
41 JdbcUtils2.rollback();
42 }finally{
43 //關閉數據庫連接
44 JdbcUtils2.close();
45 }
46 }
47 }
這樣在service層對事務的處理看起來就更加優雅了。ThreadLocal類在開發中使用得是比較多的,程序運行中產生的數據要想在一個線程範圍內共享,只需要把數據使用ThreadLocal進行存儲即可。
4.4、ThreadLocal + Filter 處理事務
上面介紹了JDBC開發中事務處理的3種方式,下面再介紹的一種使用ThreadLocal + Filter進行統一的事務處理,這種方式主要是使用過濾器進行統一的事務處理,如下圖所示:
1、編寫一個事務過濾器TransactionFilter
代碼如下:
1 package me.gac.web.filter;
2
3 import java.io.IOException;
4 import java.sql.Connection;
5 import java.sql.SQLException;
6 import javax.servlet.Filter;
7 import javax.servlet.FilterChain;
8 import javax.servlet.FilterConfig;
9 import javax.servlet.ServletException;
10 import javax.servlet.ServletRequest;
11 import javax.servlet.ServletResponse;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14 import me.gacl.util.JdbcUtils;
15
16 /**
17 * @ClassName: TransactionFilter
18 * @Description:ThreadLocal + Filter 統一處理數據庫事務
19 * @author: 孤傲蒼狼
20 * @date: 2014-10-6 下午11:36:58
21 *
22 */
23 public class TransactionFilter implements Filter {
24
25 @Override
26 public void init(FilterConfig filterConfig) throws ServletException {
27
28 }
29
30 @Override
31 public void doFilter(ServletRequest request, ServletResponse response,
32 FilterChain chain) throws IOException, ServletException {
33
34 Connection connection = null;
35 try {
36 //1、獲取數據庫連接對象Connection
37 connection = JdbcUtils.getConnection();
38 //2、開啓事務
39 connection.setAutoCommit(false);
40 //3、利用ThreadLocal把獲取數據庫連接對象Connection和當前線程綁定
41 ConnectionContext.getInstance().bind(connection);
42 //4、把請求轉發給目標Servlet
43 chain.doFilter(request, response);
44 //5、提交事務
45 connection.commit();
46 } catch (Exception e) {
47 e.printStackTrace();
48 //6、回滾事務
49 try {
50 connection.rollback();
51 } catch (SQLException e1) {
52 e1.printStackTrace();
53 }
54 HttpServletRequest req = (HttpServletRequest) request;
55 HttpServletResponse res = (HttpServletResponse) response;
56 //req.setAttribute("errMsg", e.getMessage());
57 //req.getRequestDispatcher("/error.jsp").forward(req, res);
58 //出現異常之後跳轉到錯誤頁面
59 res.sendRedirect(req.getContextPath()+"/error.jsp");
60 }finally{
61 //7、解除綁定
62 ConnectionContext.getInstance().remove();
63 //8、關閉數據庫連接
64 try {
65 connection.close();
66 } catch (SQLException e) {
67 e.printStackTrace();
68 }
69 }
70 }
71
72 @Override
73 public void destroy() {
74
75 }
76 }
我們在TransactionFilter中把獲取到的數據庫連接使用ThreadLocal綁定到當前線程之後,在DAO層還需要從ThreadLocal中取出數據庫連接來操作數據庫,因此需要編寫一個ConnectionContext類來存儲ThreadLocal,ConnectionContext類的代碼如下:
1 package me.gac.web.filter;
2
3 import java.sql.Connection;
4
5 /**
6 * @ClassName: ConnectionContext
7 * @Description:數據庫連接上下文
8 * @author: 孤傲蒼狼
9 * @date: 2014-10-7 上午8:36:01
10 *
11 */
12 public class ConnectionContext {
13
14 /**
15 * 構造方法私有化,將ConnectionContext設計成單例
16 */
17 private ConnectionContext(){
18
19 }
20 //創建ConnectionContext實例對象
21 private static ConnectionContext connectionContext = new ConnectionContext();
22
23 /**
24 * @Method: getInstance
25 * @Description:獲取ConnectionContext實例對象
26 * @Anthor:孤傲蒼狼
27 *
28 * @return
29 */
30 public static ConnectionContext getInstance(){
31 return connectionContext;
32 }
33
34 /**
35 * @Field: connectionThreadLocal
36 * 使用ThreadLocal存儲數據庫連接對象
37 */
38 private ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<Connection>();
39
40 /**
41 * @Method: bind
42 * @Description:利用ThreadLocal把獲取數據庫連接對象Connection和當前線程綁定
43 * @Anthor:孤傲蒼狼
44 *
45 * @param connection
46 */
47 public void bind(Connection connection){
48 connectionThreadLocal.set(connection);
49 }
50
51 /**
52 * @Method: getConnection
53 * @Description:從當前線程中取出Connection對象
54 * @Anthor:孤傲蒼狼
55 *
56 * @return
57 */
58 public Connection getConnection(){
59 return connectionThreadLocal.get();
60 }
61
62 /**
63 * @Method: remove
64 * @Description: 解除當前線程上綁定Connection
65 * @Anthor:孤傲蒼狼
66 *
67 */
68 public void remove(){
69 connectionThreadLocal.remove();
70 }
71 }
在DAO層想獲取數據庫連接時,就可以使用ConnectionContext.getInstance().getConnection()來獲取,如下所示:
1 package me.gacl.dao;
2
3 import java.sql.SQLException;
4 import org.apache.commons.dbutils.QueryRunner;
5 import org.apache.commons.dbutils.handlers.BeanHandler;
6
7 import me.gac.web.filter.ConnectionContext;
8 import me.gacl.domain.Account;
9
10 /*
11 create table account(
12 id int primary key auto_increment,
13 name varchar(40),
14 money float
15 )character set utf8 collate utf8_general_ci;
16
17 insert into account(name,money) values('A',1000);
18 insert into account(name,money) values('B',1000);
19 insert into account(name,money) values('C',1000);
20
21 */
22
23 /**
24 * @ClassName: AccountDao
25 * @Description: 針對Account對象的CRUD
26 * @author: 孤傲蒼狼
27 * @date: 2014-10-6 下午4:00:42
28 *
29 */
30 public class AccountDao3 {
31
32 public void update(Account account) throws SQLException{
33
34 QueryRunner qr = new QueryRunner();
35 String sql = "update account set name=?,money=? where id=?";
36 Object params[] = {account.getName(),account.getMoney(),account.getId()};
37 //ConnectionContext.getInstance().getConnection()獲取當前線程中的Connection對象
38 qr.update(ConnectionContext.getInstance().getConnection(),sql, params);
39
40 }
41
42 public Account find(int id) throws SQLException{
43 QueryRunner qr = new QueryRunner();
44 String sql = "select * from account where id=?";
45 //ConnectionContext.getInstance().getConnection()獲取當前線程中的Connection對象
46 return (Account) qr.query(ConnectionContext.getInstance().getConnection(),sql, id, new BeanHandler(Account.class));
47 }
48 }
businessService層也不用處理事務和數據庫連接問題了,這些統一在TransactionFilter中統一管理了,businessService層只需要專注業務邏輯的處理即可,如下所示:
1 package me.gacl.service;
2
3 import java.sql.SQLException;
4 import me.gacl.dao.AccountDao3;
5 import me.gacl.domain.Account;
6
7 public class AccountService3 {
8
9 /**
10 * @Method: transfer
11 * @Description:在業務層處理兩個賬戶之間的轉賬問題
12 * @Anthor:孤傲蒼狼
13 *
14 * @param sourceid
15 * @param tartgetid
16 * @param money
17 * @throws SQLException
18 */
19 public void transfer(int sourceid, int tartgetid, float money)
20 throws SQLException {
21 AccountDao3 dao = new AccountDao3();
22 Account source = dao.find(sourceid);
23 Account target = dao.find(tartgetid);
24 source.setMoney(source.getMoney() - money);
25 target.setMoney(target.getMoney() + money);
26 dao.update(source);
27 // 模擬程序出現異常讓事務回滾
28 int x = 1 / 0;
29 dao.update(target);
30 }
31 }
Web層的Servlet調用businessService層的業務方法處理用戶請求,需要注意的是:調用businessService層的方法出異常之後,繼續將異常拋出,這樣在TransactionFilter就能捕獲到拋出的異常,繼而執行事務回滾操作,如下所示:
1 package me.gacl.web.controller;
2
3 import java.io.IOException;
4 import java.sql.SQLException;
5 import javax.servlet.ServletException;
6 import javax.servlet.http.HttpServlet;
7 import javax.servlet.http.HttpServletRequest;
8 import javax.servlet.http.HttpServletResponse;
9 import me.gacl.service.AccountService3;
10
11 public class AccountServlet extends HttpServlet {
12
13 public void doGet(HttpServletRequest request, HttpServletResponse response)
14 throws ServletException, IOException {
15 AccountService3 service = new AccountService3();
16 try {
17 service.transfer(1, 2, 100);
18 } catch (SQLException e) {
19 e.printStackTrace();
20 //注意:調用service層的方法出異常之後,繼續將異常拋出,這樣在TransactionFilter就能捕獲到拋出的異常,繼而執行事務回滾操作
21 throw new RuntimeException(e);
22 }
23 }
24
25 public void doPost(HttpServletRequest request, HttpServletResponse response)
26 throws ServletException, IOException {
27 doGet(request, response);
28 }
29 }