一:commons-dbutils簡介mysql
commons-dbutils 是 Apache 組織提供的一個開源 JDBC工具類庫,它是對JDBC的簡單封裝,學習成本極低,而且使用dbutils能極大簡化jdbc編碼的工做量,同時也不會影響程序的性能。sql
DbUtils提供了三個包,分別是:
org.apache.commons.dbutils;
org.apache.commons.dbutils.handlers;
org.apache.commons.dbutils.wrappers;數據庫
(1)org.apache.commons.dbutils
DbUtils 關閉連接等操做
QueryRunner 進行查詢的操做apache
(2)org.apache.commons.dbutils.handlers
ArrayHandler :將ResultSet中第一行的數據轉化成對象數組
ArrayListHandler將ResultSet中全部的數據轉化成List,List中存放的是Object[]
BeanHandler :將ResultSet中第一行的數據轉化成類對象
BeanListHandler :將ResultSet中全部的數據轉化成List,List中存放的是類對象
ColumnListHandler :將ResultSet中某一列的數據存成List,List中存放的是Object對象
KeyedHandler :將ResultSet中存成映射,key爲某一列對應爲Map。Map中存放的是數據
MapHandler :將ResultSet中第一行的數據存成Map映射
MapListHandler :將ResultSet中全部的數據存成List。List中存放的是Map
ScalarHandler :將ResultSet中一條記錄的其中某一列的數據存成Object數組
(3)org.apache.commons.dbutils.wrappers
SqlNullCheckedResultSet :對ResultSet進行操做,改版裏面的值
StringTrimmedResultSet :去除ResultSet中中字段的左右空格。Trim()安全
二:JDBC開發中的事務處理多線程
開發中,對數據庫的多個表或者對一個表中的多條數據執行更新操做時要保證對多個更新操做要麼同時成功,要麼都不成功,這就涉及到對多個更新操做的事務管理問題了。併發
(1)在數據訪問層(Dao)中處理事務app
/**
* @Method: transfer
* @Description:
* 在開發中,DAO層的職責應該只涉及到CRUD,
* 因此在開發中DAO層出現這樣的業務處理方法是徹底錯誤的
* @throws SQLException
*/
public void transfer() throws SQLException{
Connection conn = null;
try{
conn = JdbcUtils.getConnection();
//開啓事務
conn.setAutoCommit(false);jsp
/**
* 在建立QueryRunner對象時,不傳遞數據源給它,是爲了保證這兩條SQL在同一個事務中進行,
* 咱們手動獲取數據庫鏈接,而後讓這兩條SQL使用同一個數據庫鏈接執行
*/
QueryRunner runner = new QueryRunner();
String sql1 = "update account set money=money-100 where name=?";
String sql2 = "update account set money=money+100 where name=?";
Object[] paramArr1 = {param1};
Object[] paramArr2 = {param2};
runner.update(conn,sql1,paramArr1);
//模擬程序出現異常讓事務回滾
int x = 1/0;
runner.update(conn,sql2,paramArr2);
//sql正常執行以後就提交事務
conn.commit();
}catch (Exception e) {
e.printStackTrace();
if(conn!=null){
//出現異常以後就回滾事務
conn.rollback();
}
}finally{
//關閉數據庫鏈接
conn.close();
}
}
在開發中,DAO層的職責應該只涉及到基本的CRUD,不涉及具體的業務操做,因此在開發中DAO層出現這樣的業務處理方法是一種很差的設計
(2)在業務層(Service)處理事務
先改造DAO:
public class TextDao {
//接收service層傳遞過來的Connection對象
private Connection conn = null;
public TextDao(Connection conn){
this.conn = conn;
}
public TextDao(){
}
/**
* @Method: update
* @Description:更新
* @Anthor:
* @param user
* @throws SQLException
*/
public void update(Account account) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "update account set name=?,money=? where id=?";
Object params[] = {account.getName(),account.getMoney(),account.getId()};
//使用service層傳遞過來的Connection對象操做數據庫
qr.update(conn,sql, params);
}
/**
* @Method: find
* @Description:查找
* @Anthor:
* @param id
* @return
* @throws SQLException
*/
public Account findById(int id) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "select * from account where id=?";
//使用service層傳遞過來的Connection對象操做數據庫
return (Account) qr.query(conn,sql, id, new BeanHandler(Account.class));
}
}
接着對Service(業務層)中的transfer方法的改造,在業務層(Service)中處理事務
/**
* @ClassName: TextService
* @Description: 業務邏輯處理層
* @author:
* @date:
*
*/
public class TextService {
/**
* @Method: transfer
* @Description:這個方法是用來處理兩個用戶之間的轉帳業務
* @Anthor:
* @param sourceid
* @param tartgetid
* @param money
* @throws SQLException
*/
public boolean transfer(int sourceid,int tartgetid,float money) throws SQLException{
Connection conn = null;
boolean flag = false;
try{
//獲取數據庫鏈接
conn = JdbcUtils.getConnection();
//開啓事務
conn.setAutoCommit(false);
//將獲取到的Connection傳遞給TextDao,保證dao層使用的是同一個Connection對象操做數據庫
TextDao dao = new TextDao(conn);
Account source = dao.find(sourceid);
Account target = dao.find(tartgetid);
source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);
dao.update(source);
//模擬程序出現異常讓事務回滾
int x = 1/0;
dao.update(target);
//提交事務
conn.commit();
flag = true;
}catch (Exception e) {
e.printStackTrace();
//出現異常以後就回滾事務
conn.rollback();
flag = false;
}finally{
conn.close();
}
return flag;
}
}
這樣TextDao只負責CRUD,裏面沒有具體的業務處理方法了,職責就單一了,而TextService則負責具體的業務邏輯和事務的處理,須要操做數據庫時,就調用TextDao層提供的CRUD方法操做數據庫。
(3)使用ThreadLocal進行更加優雅的事務處理
注:ThreadLocal使用場合主要解決多線程中數據因併發產生不一致問題(解決線程安全)。ThreadLocal爲每一個線程中併發訪問的數據提供一個獨立副本,副本之間相互獨立(獨立操做),這樣每個線程均可以隨意修改本身的變量副本,而不會對其餘線程產生影響。經過訪問副原本運行業務,這樣的結果是耗費了內存,單大大減小了線程同步所帶來性能消耗,也減小了線程併發控制的複雜度。
(ThreadLocal和Synchonized都用於解決多線程併發訪問,synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal爲每個線程都提供了變量的副本,使得每一個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享)
Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。
ThreadLocal類的使用範例以下:
public class ThreadLocalTest {
public static void main(String[] args) {
//獲得程序運行時的當前線程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread);
//ThreadLocal一個容器,向這個容器存儲的對象,在當前線程範圍內均可以取得出來
ThreadLocal<String> t = new ThreadLocal<String>();
//把某個對象綁定到當前線程上 對象以鍵值對的形式存儲到一個Map集合中,對象的的key是當前的線程,如: map(currentThread,"aaa")
t.set("aaa");
//獲取綁定到當前線程中的對象
String value = t.get();
//輸出value的值是aaa
System.out.println(value);
}
}
1:使用ThreadLocal類進行改造數據庫鏈接工具類JdbcUtils,改造後的代碼以下:
/**
* @ClassName: JdbcUtils2
* @Description: 數據庫鏈接工具類
* @author:
* @date:
*
*/
public class JdbcUtils2 {
private static ComboPooledDataSource ds = null;
//使用ThreadLocal存儲當前線程中的Connection對象
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
//在靜態代碼塊中建立數據庫鏈接池
static{
try{
//經過代碼建立C3P0數據庫鏈接池
/*ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbcstudy");
ds.setUser("root");
ds.setPassword("XDP");
ds.setInitialPoolSize(10);
ds.setMinPoolSize(5);
ds.setMaxPoolSize(20);*/
//經過讀取C3P0的xml配置文件建立數據源,C3P0的xml配置文件c3p0-config.xml必須放在src目錄下
//ds = new ComboPooledDataSource();//使用C3P0的默認配置來建立數據源
ds = new ComboPooledDataSource("MySQL");//使用C3P0的命名配置來建立數據源
}catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* @Method: getConnection
* @Description: 從數據源中獲取數據庫鏈接
* @Anthor:
* @return Connection
* @throws SQLException
*/
public static Connection getConnection() throws SQLException{
//從當前線程中獲取Connection
Connection conn = threadLocal.get();
if(conn==null){
//從數據源中獲取數據庫鏈接
conn = getDataSource().getConnection();
//將conn綁定到當前線程
threadLocal.set(conn);
}
return conn;
}
/**
* @Method: startTransaction
* @Description: 開啓事務
* @Anthor:
*
*/
public static void startTransaction(){
try{
Connection conn = threadLocal.get();
if(conn==null){
conn = getConnection();
//把 conn綁定到當前線程上
threadLocal.set(conn);
}
//開啓事務
conn.setAutoCommit(false);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Method: rollback
* @Description:回滾事務
* @Anthor:
*/
public static void rollback(){
try{
//從當前線程中獲取Connection
Connection conn = threadLocal.get();
if(conn!=null){
//回滾事務
conn.rollback();
}
}catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Method: commit
* @Description:提交事務
* @Anthor:
*/
public static void commit(){
try{
//從當前線程中獲取Connection
Connection conn = threadLocal.get();
if(conn!=null){
//提交事務
conn.commit();
}
}catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Method: close
* @Description:關閉數據庫鏈接(注意,並非真的關閉,而是把鏈接還給數據庫鏈接池)
* @Anthor:
*
*/
public static void close(){
try{
//從當前線程中獲取Connection
Connection conn = threadLocal.get();
if(conn!=null){
conn.close();
//解除當前線程上綁定conn
threadLocal.remove();
}
}catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Method: getDataSource
* @Description: 獲取數據源
* @Anthor:
* @return DataSource
*/
public static DataSource getDataSource(){
//從數據源中獲取數據庫鏈接
return ds;
}
}
2:對TextDao進行改造,數據庫鏈接對象再也不須要service層傳遞過來,而是直接從JdbcUtils2提供的getConnection方法去獲取,改造後的TextDao以下:
/**
* @ClassName: TextDao
* @Description: 針對Account對象的CRUD
* @author:
* @date:
*
*/
public class TextDao2 {
public void update(Account account) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "update account set name=?,money=? where id=?";
Object params[] = {account.getName(),account.getMoney(),account.getId()};
//JdbcUtils2.getConnection()獲取當前線程中的Connection對象
qr.update(JdbcUtils2.getConnection(),sql, params);
}
public Account find(int id) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "select * from account where id=?";
//JdbcUtils2.getConnection()獲取當前線程中的Connection對象
return (Account) qr.query(JdbcUtils2.getConnection(),sql, id, new BeanHandler(Account.class));
}
}
3:對TextService進行改造,service層再也不須要傳遞數據庫鏈接Connection給Dao層,改造後的TextService以下:
public class TextService2 {
/**
* @Method: transfer
* @Description:在業務層處理兩個帳戶之間的轉帳問題
* @Anthor:
* @param sourceid
* @param tartgetid
* @param money
* @throws SQLException
*/
public boolean transfer(int sourceid,int tartgetid,float money) throws SQLException{
boolean flag = false;
try{
//開啓事務,在業務層處理事務,保證dao層的多個操做在同一個事務中進行
JdbcUtils2.startTransaction();
TextDao2 dao = new TextDao2 ();
Account source = dao.find(sourceid);
Account target = dao.find(tartgetid);
source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);
dao.update(source);
//模擬程序出現異常讓事務回滾
int x = 1/0;
dao.update(target);
//SQL正常執行以後提交事務
JdbcUtils2.commit();
flag = true;
}catch (Exception e) {
e.printStackTrace();
//出現異常以後就回滾事務
JdbcUtils2.rollback();
flag = false;
}finally{
//關閉數據庫鏈接
JdbcUtils2.close();
}
return flag;
}
}
ThreadLocal類在開發中使用得是比較多的,程序運行中產生的數據要想在一個線程範圍內共享,只須要把數據使用ThreadLocal進行存儲便可。
(4)ThreadLocal + Filter 處理事務
ThreadLocal + Filter進行統一的事務處理,這種方式主要是使用過濾器進行統一的事務處理
一、編寫一個事務過濾器TransactionFilter
/**
* @ClassName: TransactionFilter
* @Description:ThreadLocal + Filter 統一處理數據庫事務
* @author:
* @date:
*
*/
public class TransactionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
Connection connection = null;
try {
//一、獲取數據庫鏈接對象Connection
connection = JdbcUtils.getConnection();
//二、開啓事務
connection.setAutoCommit(false);
//三、利用ThreadLocal把獲取數據庫鏈接對象Connection和當前線程綁定
ConnectionContext.getInstance().bind(connection);
//四、把請求轉發給目標Servlet
chain.doFilter(request, response);
//五、提交事務
connection.commit();
} catch (Exception e) {
e.printStackTrace();
//六、回滾事務
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
//req.setAttribute("errMsg", e.getMessage());
//req.getRequestDispatcher("/error.jsp").forward(req, res);
//出現異常以後跳轉到錯誤頁面
res.sendRedirect(req.getContextPath()+"/error.jsp");
}finally{
//七、解除綁定
ConnectionContext.getInstance().remove();
//八、關閉數據庫鏈接
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public void destroy() {
}
}
2:咱們在TransactionFilter中把獲取到的數據庫鏈接使用ThreadLocal綁定到當前線程以後,在DAO層還須要從ThreadLocal中取出數據庫鏈接來操做數據庫,所以須要編寫一個ConnectionContext類來存儲ThreadLocal,ConnectionContext類的代碼以下:
/**
* @ClassName: ConnectionContext
* @Description:數據庫鏈接上下文
* @author:
* @date:
*/
public class ConnectionContext {
/**
* 構造方法私有化,將ConnectionContext設計成單例
*/
private ConnectionContext(){
}
//建立ConnectionContext實例對象
private static ConnectionContext connectionContext = new ConnectionContext();
/**
* @Method: getInstance
* @Description:獲取ConnectionContext實例對象
* @Anthor:
* @return
*/
public static ConnectionContext getInstance(){
return connectionContext;
}
/**
* @Field: connectionThreadLocal
* 使用ThreadLocal存儲數據庫鏈接對象
*/
private ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<Connection>();
/**
* @Method: bind
* @Description:利用ThreadLocal把獲取數據庫鏈接對象Connection和當前線程綁定
* @Anthor:
* @param connection
*/
public void bind(Connection connection){
connectionThreadLocal.set(connection);
}
/**
* @Method: getConnection
* @Description:從當前線程中取出Connection對象
* @Anthor:
* @return
*/
public Connection getConnection(){
return connectionThreadLocal.get();
}
/**
* @Method: remove
* @Description: 解除當前線程上綁定Connection
* @Anthor:
*
*/
public void remove(){
connectionThreadLocal.remove();
}
}
3:在DAO層想獲取數據庫鏈接時,就可使用ConnectionContext.getInstance().getConnection()來獲取,以下所示:
/**
* @ClassName: AccountDao
* @Description: 針對Account對象的CRUD
* @author:
* @date:
*
*/
public class TextDao3 {
public void update(Account account) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "update account set name=?,money=? where id=?";
Object params[] = {account.getName(),account.getMoney(),account.getId()};
//ConnectionContext.getInstance().getConnection()獲取當前線程中的Connection對象
qr.update(ConnectionContext.getInstance().getConnection(),sql, params);
}
public Account find(int id) throws SQLException{
QueryRunner qr = new QueryRunner();
String sql = "select * from account where id=?";
//ConnectionContext.getInstance().getConnection()獲取當前線程中的Connection對象
return (Account) qr.query(ConnectionContext.getInstance().getConnection(),sql, id, new BeanHandler(Account.class));
}
}
4:Service層也不用處理事務和數據庫鏈接問題了,這些統一在TransactionFilter中統一管理了,Service層只須要專一業務邏輯的處理便可,以下所示:
public class TextService3 {
/**
* @Method: transfer
* @Description:在業務層處理兩個帳戶之間的轉帳問題
* @Anthor:
* @param sourceid
* @param tartgetid
* @param money
* @throws SQLException
*/
public void transfer(int sourceid, int tartgetid, float money)
throws SQLException {
TextDao3 dao = new TextDao3 ();
Account source = dao.find(sourceid);
Account target = dao.find(tartgetid);
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
dao.update(source);
// 模擬程序出現異常讓事務回滾
int x = 1 / 0;
dao.update(target);
}
}
5:Web層的Servlet調用Service層的業務方法處理用戶請求,須要注意的是:調用Service層的方法出異常以後,繼續將異常拋出,這樣在TransactionFilter就能捕獲到拋出的異常,繼而執行事務回滾操做,以下所示:
public class AccountServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { TextService3 service = new TextService3(); try { service.transfer(1, 2, 100); } catch (SQLException e) { e.printStackTrace(); //注意:調用service層的方法出異常以後,繼續將異常拋出,這樣在TransactionFilter就能捕獲到拋出的異常,繼而執行事務回滾操做 throw new RuntimeException(e); } } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }