day12總結
今日內容
l 事務 l 連接池
一、事務
事務概述
爲了方便演示事務,我們需要創建一個account表:
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(30),
balance NUMERIC(10.2)
);
INSERT INTO account(NAME,balance) VALUES('zs', 100000);
INSERT INTO account(NAME,balance) VALUES('ls', 100000);
INSERT INTO account(NAME,balance) VALUES('ww', 100000);
SELECT * FROM account;
1、什麼是事務
銀行轉賬!張三轉10000塊到李四的賬戶,這其實需要兩條SQL語句:
l 給張三的賬戶減去10000元;
l 給李四的賬戶加上10000元。
如果在第一條SQL語句執行成功後,在執行第二條SQL語句之前,程序被中斷了(可能是拋出了某個異常,也可能是其他什麼原因),那麼李四的賬戶沒有加上10000元,而張三卻減去了10000元。這肯定是不行的!
你現在可能已經知道什麼是事務了吧!事務中的多個操作,要麼完全成功,要麼完全失敗!不可能存在成功一半的情況!也就是說給張三的賬戶減去10000元如果成功了,那麼給李四的賬戶加上10000元的操作也必須是成功的;否則給張三減去10000元,以及給李四加上10000元都是失敗的!
2、事務的四大特性(ACID)( 面試!)
事務的四大特性是:
l 原子性(Atomicity):事務中所有操作是不可再分割的原子單位。事務中所有操作要麼全部執行成功,要麼全部執行失敗。
l 一致性(Consistency):事務執行後,數據庫狀態與其它業務規則保持一致。如轉賬業務,無論事務執行成功與否,參與轉賬的兩個賬號餘額之和應該是不變的。
l 隔離性(Isolation):隔離性是指在併發操作中,不同事務之間應該隔離開來,使每個併發中的事務不會相互干擾。
l 持久性(Durability):一旦事務提交成功,事務中所有的數據操作都必須被持久化到數據庫中,即使提交事務後,數據庫馬上崩潰,在數據庫重啓時,也必須能保證通過某種機制恢復數據。
MySQL中的事務
在默認情況下,MySQL每執行一條SQL語句,都是一個單獨的事務。如果需要在一個事務中包含多條SQL語句,那麼需要開啓事務和結束事務。
l 開啓事務:start transaction;
l 結束事務:commit或rollback。
在執行SQL語句之前,先執行start transaction,這就開啓了一個事務(事務的起點),然後可以去執行多條SQL語句,最後要結束事務,commit表示提交,即事務中的多條SQL語句所做出的影響會持久化到數據庫中。或者rollback,表示回滾,即回滾到事務的起點,之前做的所有操作都被撤消了!
下面演示zs給li轉賬10000元的示例:
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; ROLLBACK; |
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; COMMIT; |
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; quit; |
JDBC事務
1、JDBC中的事務
Connection的三個方法與事務相關:
l setAutoCommit(boolean):設置是否爲自動提交事務,如果true(默認值就是true)表示自動提交,也就是每條執行的SQL語句都是一個單獨的事務,如果設置false,那麼就相當於開啓了事務了;
l commit():提交結束事務;
l rollback():回滾結束事務。
public void transfer(boolean b) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手動提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where id=?"; pstmt = con.prepareStatement(sql); //操作 pstmt.setDouble(1, -10000); pstmt.setInt(2, 1); pstmt.executeUpdate(); // 在兩個操作中拋出異常 if(b) { throw new Exception(); } pstmt.setDouble(1, 10000); pstmt.setInt(2, 2); pstmt.executeUpdate(); //提交事務 con.commit(); } catch(Exception e) { //回滾事務 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //關閉 JdbcUtils.close(con, pstmt); } } |
2、保存點(瞭解)
保存點是JDBC3.0的東西!當要求數據庫服務器支持保存點方式的回滾。
校驗數據庫服務器是否支持保存點!
boolean b = con.getMetaData().supportsSavepoints(); |
保存點的作用是允許事務回滾到指定的保存點位置。在事務中設置好保存點,然後回滾時可以選擇回滾到指定的保存點,而不是回滾整個事務!注意,回滾到指定保存點並沒有結束事務!!!只有回滾了整個事務纔算是結束事務了!
Connection類的設置保存點,以及回滾到指定保存點方法:
l 設置保存點:Savepoint setSavepoint();
l 回滾到指定保存點:void rollback(Savepoint)。
/* * 李四對張三說,如果你給我轉1W,我就給你轉100W。 * ========================================== * * 張三給李四轉1W(張三減去1W,李四加上1W) * 設置保存點! * 李四給張三轉100W(李四減去100W,張三加上100W) * 查看李四餘額爲負數,那麼回滾到保存點。 * 提交事務 */ @Test public void fun() { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手動提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where name=?"; pstmt = con.prepareStatement(sql); //操作1(張三減去1W) pstmt.setDouble(1, -10000); pstmt.setString(2, "zs"); pstmt.executeUpdate(); //操作2(李四加上1W) pstmt.setDouble(1, 10000); pstmt.setString(2, "ls"); pstmt.executeUpdate(); // 設置保存點 Savepoint sp = con.setSavepoint(); //操作3(李四減去100W) pstmt.setDouble(1, -1000000); pstmt.setString(2, "ls"); pstmt.executeUpdate(); //操作4(張三加上100W) pstmt.setDouble(1, 1000000); pstmt.setString(2, "zs"); pstmt.executeUpdate(); //操作5(查看李四餘額) sql = "select balance from account where name=?"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "ls"); ResultSet rs = pstmt.executeQuery(); rs.next(); double balance = rs.getDouble(1); //如果李四餘額爲負數,那麼回滾到指定保存點 if(balance < 0) { con.rollback(sp); System.out.println("張三,你上當了!"); } //提交事務 con.commit(); } catch(Exception e) { //回滾事務 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //關閉 JdbcUtils.close(con, pstmt); } } |
事務隔離級別
1 、事務的併發讀問題
l 髒讀:讀取到另一個事務未提交數據;
l 不可重複讀:兩次讀取不一致;
l 幻讀(虛讀):讀到另一事務已提交數據。
2、五大併發事務問題
因爲併發事務導致的問題大致有5類,其中兩類是更新問題,三類是讀問題。
l 髒讀(dirty read):讀到未提交更新數據
時間 |
轉賬事務A |
取款事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
查詢賬戶餘額爲1000元 |
|
T4 |
取出500元把餘額改爲500元 |
|
T5 |
查看賬戶餘額爲500元(髒讀) |
|
T6 |
撤銷事務,餘額恢復爲1000元 |
|
T7 |
匯入100元把餘額改爲600元 |
|
T8 |
提交事務 |
A事務查詢到了B事務未提交的更新數據,A事務依據這個查詢結果繼續執行相關操作。但是接着B事務撤銷了所做的更新,這會導致A事務操作的是髒數據。(這是絕對不允許出現的事情)
l 虛讀(phantom read):讀到已提交插入數據
時間 |
統計金額事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
統計總存款數爲10000元 |
|
T4 |
新增一個存款賬戶,存款爲100元 |
|
T5 |
提交事務 |
|
T6 |
再次統計總存款數爲10100元 |
A事務第一次查詢時,沒有問題,第二次查詢時查到了B事務已提交的新插入數據,這導致兩次查詢結果不同。(在實際開發中,很少會對相同數據進行兩次查詢,所以可以考慮是否允許虛讀)
l 不可重複讀(unrepeatable read):讀到已提交更新數據
時間 |
取款事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
查詢賬戶餘額爲1000元 |
|
T4 |
查詢賬戶餘額爲1000元 |
|
T5 |
取出100元,把餘額改爲900元 |
|
T6 |
提交事務 |
|
T7 |
查詢賬戶餘額爲900元(與T4讀取的一不一致) |
不可重複讀與虛讀有些相似,都是兩次查詢的結果不同。後者是查詢到了另一個事務已提交的新插入數據,而前者是查詢到了另一個事務已提交的更新數據。
3、四大隔離級別
隔離級別 |
髒讀 |
不可重複讀 |
虛讀 |
第一類丟失更新 |
第二類丟失更新 |
READ UNCOMMITTED |
允許 |
允許 |
允許 |
不允許 |
允許 |
READ COMMITTED |
不允許 |
允許 |
允許 |
不允許 |
允許 |
REPEATABLE READ |
不允許 |
不允許 |
允許 |
不允許 |
不允許 |
SERIALIZABLE |
不允許 |
不允許 |
不允許 |
不允許 |
不允許 |
4、哪種隔離級別最好
4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的數據併發問題的能力是不同的。
SERIALIZABLE(串行化)
當數據庫系統使用SERIALIZABLE隔離級別時,一個事務在執行過程中完全看不到其他事務對數據庫所做的更新。當兩個事務同時操作數據庫中相同數據時,如果第一個事務已經在訪問該數據,第二個事務只能停下來等待,必須等到第一個事務結束後才能恢復運行。因此這兩個事務實際上是串行化方式運行。
REPEATABLE READ(可重複讀)
當數據庫系統使用REPEATABLE READ隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,但是不能看到其他事務對已有記錄的更新。
READ COMMITTED(讀已提交數據)
當數據庫系統使用READ COMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,而且還能看到其他事務已經提交的對已有記錄的更新。
READ UNCOMMITTED(讀未提交數據)
當數據庫系統使用READ UNCOMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務沒有提交的新插入的記錄,而且還能看到其他事務沒有提交的對已有記錄的更新。
你可能會說,選擇SERIALIZABLE,因爲它最安全!沒錯,它是最安全,但它也是最慢的!而且也最容易產生死鎖。四種隔離級別的安全性與性能成反比!最安全的性能最差,最不安全的性能最好!
MySQL的默認隔離級別爲REPEATABLE READ,這是一個很不錯的選擇吧!
5、MySQL隔離級別
MySQL的默認隔離級別爲Repeatable read,可以通過下面語句查看:
select @@session.tx_isolation select @@global.tx_isolation |
也可以通過下面語句來設置當前連接的隔離級別:
set global transaction isolationlevel [4先1] set session transaction isolationlevel [4先1] |
6、JDBC設置隔離級別
二、數據庫連接池con. setTransactionIsolation(int level)
參數可選值如下:
l Connection.TRANSACTION_READ_UNCOMMITTED;
l Connection.TRANSACTION_READ_COMMITTED;
l Connection.TRANSACTION_REPEATABLE_READ;
l Connection.TRANSACTION_SERIALIZABLE。
事務總結:
l 事務的特性:ACID;
l 事務開始邊界與結束邊界:開始邊界(con.setAutoCommit(false)),結束邊界(con.commit()或con.rollback());
l 事務的隔離級別: READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。多個事務併發執行時才需要考慮併發事務。
1、數據庫連接池的概念
用池來管理Connection,這可以重複使用Connection。有了池,所以我們就不用自己來創建Connection,而是通過池來獲取Connection對象。當使用完Connection後,調用Connection的close()方法也不會真的關閉Connection,而是把Connection“歸還”給池。池就可以再利用這個Connection對象了。
2、JDBC數據庫連接池接口(DataSource)
Java爲數據庫連接池提供了公共的接口:javax.sql.DataSource,各個廠商可以讓自己的連接池實現這個接口。這樣應用程序可以方便的切換不同廠商的連接池!
3、自定義連接池(ItcastPool)
分析:ItcastPool需要有一個List,用來保存連接對象。在ItcastPool的構造器中創建5個連接對象放到List中!當用人調用了ItcastPool的getConnection()時,那麼就從List拿出一個返回。當List中沒有連接可用時,拋出異常。
我們需要對Connection的close()方法進行增強,所以我們需要自定義ItcastConnection類,對Connection進行裝飾!即對close()方法進行增強。因爲需要在調用close()方法時把連接“歸還”給池,所以ItcastConnection類需要擁有池對象的引用,並且池類還要提供“歸還”的方法。
ItcastPool.java
public class ItcastPool implements DataSource { private static Properties props = new Properties(); private List<Connection> list = new ArrayList<Connection>(); static { InputStream in = ItcastPool.class.getClassLoader() .getResourceAsStream("dbconfig.properties"); try { props.load(in); Class.forName(props.getProperty("driverClassName")); } catch (Exception e) { throw new RuntimeException(e); } } public ItcastPool() throws SQLException { for (int i = 0; i < 5; i++) { Connection con = DriverManager.getConnection( props.getProperty("url"), props.getProperty("username"), props.getProperty("password")); ItcastConnection conWapper = new ItcastConnection(con, this); list.add(conWapper); } } public void add(Connection con) { list.add(con); } public Connection getConnection() throws SQLException { if(list.size() > 0) { return list.remove(0); } throw new SQLException("沒連接了"); } ...... } |
ItcastConnection.java
public class ItcastConnection extends ConnectionWrapper { private ItcastPool pool; public ItcastConnection(Connection con, ItcastPool pool) { super(con); this.pool = pool; } @Override public void close() throws SQLException { pool.add(this); } } |
DBCP
1、什麼是DBCP?
DBCP是Apache提供的一款開源免費的數據庫連接池!
Hibernate3.0之後不再對DBCP提供支持!因爲Hibernate聲明DBCP有致命的缺欠!DBCP因爲Hibernate的這一毀謗很是生氣,並且說自己沒有缺欠。
2、DBCP的使用
public void fun1() throws SQLException { BasicDataSource ds = new BasicDataSource(); ds.setUsername("root"); ds.setPassword("123"); ds.setUrl("jdbc:mysql://localhost:3306/mydb1"); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setMaxActive(20); ds.setMaxIdle(10); ds.setInitialSize(10); ds.setMinIdle(2); ds.setMaxWait(1000); Connection con = ds.getConnection(); System.out.println(con.getClass().getName()); con.close(); } |
3、DBCP的配置信息
下面是對DBCP的配置介紹:
#基本配置 driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mydb1 username=root password=123 #初始化池大小,即一開始池中就會有10個連接對象 默認值爲0 initialSize=0 #最大連接數,如果設置maxActive=50時,池中最多可以有50個連接,當然這50個連接中包含被使用的和沒被使用的(空閒) #你是一個包工頭,你一共有50個工人,但這50個工人有的當前正在工作,有的正在空閒 #默認值爲8,如果設置爲非正數,表示沒有限制!即無限大 maxActive=8 #最大空閒連接 #當設置maxIdle=30時,你是包工頭,你允許最多有20個工人空閒,如果現在有30個空閒工人,那麼要開除10個 #默認值爲8,如果設置爲負數,表示沒有限制!即無限大 maxIdle=8 #最小空閒連接 #如果設置minIdel=5時,如果你的工人只有3個空閒,那麼你需要再去招2個回來,保證有5個空閒工人 #默認值爲0 minIdle=0 #最大等待時間 #當設置maxWait=5000時,現在你的工作都出去工作了,又來了一個工作,需要一個工人。 #這時就要等待有工人回來,如果等待5000毫秒還沒回來,那就拋出異常 #沒有工人的原因:最多工人數爲50,已經有50個工人了,不能再招了,但50人都出去工作了。 #默認值爲-1,表示無限期等待,不會拋出異常。 maxWait=-1 #連接屬性 #就是原來放在url後面的參數,可以使用connectionProperties來指定 #如果已經在url後面指定了,那麼就不用在這裏指定了。 #useServerPrepStmts=true,MySQL開啓預編譯功能 #cachePrepStmts=true,MySQL開啓緩存PreparedStatement功能, #prepStmtCacheSize=50,緩存PreparedStatement的上限 #prepStmtCacheSqlLimit=300,當SQL模板長度大於300時,就不再緩存它 connectionProperties=useUnicode=true;characterEncoding=UTF8;useServerPrepStmts=true;cachePrepStmts=true;prepStmtCacheSize=50;prepStmtCacheSqlLimit=300 #連接的默認提交方式 #默認值爲true defaultAutoCommit=true #連接是否爲只讀連接 #Connection有一對方法:setReadOnly(boolean)和isReadOnly() #如果是隻讀連接,那麼你只能用這個連接來做查詢 #指定連接爲只讀是爲了優化!這個優化與併發事務相關! #如果兩個併發事務,對同一行記錄做增、刪、改操作,是不是一定要隔離它們啊? #如果兩個併發事務,對同一行記錄只做查詢操作,那麼是不是就不用隔離它們了? #如果沒有指定這個屬性值,那麼是否爲只讀連接,這就由驅動自己來決定了。即Connection的實現類自己來決定! defaultReadOnly=false #指定事務的事務隔離級別 #可選值:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE #如果沒有指定,那麼由驅動中的Connection實現類自己來決定 defaultTransactionIsolation=REPEATABLE_READ |
C3P0
1、C3P0簡介
C3P0也是開源免費的連接池!C3P0被很多人看好!
2、C3P0的使用
C3P0中池類是:ComboPooledDataSource。
public void fun1() throws PropertyVetoException, SQLException { ComboPooledDataSource ds = new ComboPooledDataSource(); ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb1"); ds.setUser("root"); ds.setPassword("123"); ds.setDriverClass("com.mysql.jdbc.Driver"); ds.setAcquireIncrement(5); ds.setInitialPoolSize(20); ds.setMinPoolSize(2); ds.setMaxPoolSize(50); Connection con = ds.getConnection(); System.out.println(con); con.close(); } |
c3p0也可以指定配置文件,而且配置文件可以是properties,也可騍xml的。當然xml的高級一些了。但是c3p0的配置文件名必須爲c3p0-config.xml,並且必須放在類路徑下。
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <default-config> <property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="user">root</property> <property name="password">123</property> <property name="acquireIncrement">3</property> <property name="initialPoolSize">10</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">10</property> </default-config> <named-config name="oracle-config"> <property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="user">root</property> <property name="password">123</property> <property name="acquireIncrement">3</property> <property name="initialPoolSize">10</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">10</property> </named-config> </c3p0-config> |
c3p0的配置文件中可以配置多個連接信息,可以給每個配置起個名字,這樣可以方便的通過配置名稱來切換配置信息。上面文件中默認配置爲mysql的配置,名爲oracle-config的配置也是mysql的配置,呵呵。
public void fun2() throws PropertyVetoException, SQLException { ComboPooledDataSource ds = new ComboPooledDataSource(); Connection con = ds.getConnection(); System.out.println(con); con.close(); } |
public void fun2() throws PropertyVetoException, SQLException { ComboPooledDataSource ds = new ComboPooledDataSource("orcale-config"); Connection con = ds.getConnection(); System.out.println(con); con.close(); } |
Tomcat配置連接池
1、Tomcat配置JNDI資源
JNDI(Java Naming and Directory Interface),Java命名和目錄接口。JNDI的作用就是:在服務器上配置資源,然後通過統一的方式來獲取配置的資源。
我們這裏要配置的資源當然是連接池了,這樣項目中就可以通過統一的方式來獲取連接池對象了。
下圖是Tomcat文檔提供的:
配置JNDI資源需要到<Context>元素中配置<Resource>子元素:
l name:指定資源的名稱,這個名稱可以隨便給,在獲取資源時需要這個名稱;
l factory:用來創建資源的工廠,這個值基本上是固定的,不用修改;
l type:資源的類型,我們要給出的類型當然是我們連接池的類型了;
l bar:表示資源的屬性,如果資源存在名爲bar的屬性,那麼就配置bar的值。對於DBCP連接池而言,你需要配置的不是bar,因爲它沒有bar這個屬性,而是應該去配置url、username等屬性。
<Context> <Resource name="mydbcp" type="org.apache.tomcat.dbcp.dbcp.BasicDataSource" factory="org.apache.naming.factory.BeanFactory" username="root" password="123" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1/mydb1" maxIdle="3" maxWait="5000" maxActive="5" initialSize="3"/> </Context> |
<Context> <Resource name="myc3p0" type="com.mchange.v2.c3p0.ComboPooledDataSource" factory="org.apache.naming.factory.BeanFactory" user="root" password="123" classDriver="com.mysql.jdbc.Driver" jdbcUrl="jdbc:mysql://127.0.0.1/mydb1" maxPoolSize="20" minPoolSize ="5" initialPoolSize="10" acquireIncrement="2"/> </Context> |
2、獲取資源
配置資源的目的當然是爲了獲取資源了。只要你啓動了Tomcat,那麼就可以在項目中任何類中通過JNDI獲取資源的方式來獲取資源了。
下圖是Tomcat文檔提供的,與上面Tomcat文檔提供的配置資源是對應的。
獲取資源:
l Context:javax.naming.Context;
l InitialContext:javax.naming.InitialContext;
l lookup(String):獲取資源的方法,其中”java:comp/env”是資源的入口(這是固定的名稱),獲取過來的還是一個Context,這說明需要在獲取到的Context上進一步進行獲取。”bean/MyBeanFactory”對應<Resource>中配置的name值,這回獲取的就是資源對象了。
Context cxt = new InitialContext(); DataSource ds = (DataSource)cxt.lookup("java:/comp/env/mydbcp"); Connection con = ds.getConnection(); System.out.println(con); con.close(); |
Context cxt = new InitialContext(); Context envCxt = (Context)cxt.lookup("java:/comp/env"); DataSource ds = (DataSource)env.lookup("mydbcp"); Connection con = ds.getConnection(); System.out.println(con); con.close(); |
上面兩種方式是相同的效果。
修改JdbcUtils
因爲已經學習了連接池,那麼JdbcUtils的獲取連接對象的方法也要修改一下了。
JdbcUtils.java
public class JdbcUtils { private static DataSource dataSource = new ComboPooledDataSource(); public static DataSource getDataSource() { return dataSource; } public static Connection getConnection() { try { return dataSource.getConnection(); } catch (Exception e) { throw new RuntimeException(e); } } } |
ThreadLocal
1、ThreadLocal API
ThreadLocal類只有三個方法:
l void set(T value):保存值;
l T get():獲取值;
l void remove():移除值。
2、ThreadLocal的內部是Map
ThreadLocal內部其實是個Map來保存數據。雖然在使用ThreadLocal時只給出了值,沒有給出鍵,其實它內部使用了當前線程做爲鍵。
class MyThreadLocal<T> { private Map<Thread,T> map = new HashMap<Thread,T>(); public void set(T value) { map.put(Thread.currentThread(), value); } public void remove() { map.remove(Thread.currentThread()); } public T get() { return map.get(Thread.currentThread()); } } |
BaseServlet
1、BaseServlet的作用
在開始客戶管理系統之前,我們先寫一個工具類:BaseServlet。
我們知道,寫一個項目可能會出現N多個Servlet,而且一般一個Servlet只有一個方法(doGet或doPost),如果項目大一些,那麼Servlet的數量就會很驚人。
爲了避免Servlet的“膨脹”,我們寫一個BaseServlet。它的作用是讓一個Servlet可以處理多種不同的請求。不同的請求調用Servlet的不同方法。我們寫好了BaseServlet後,讓其他Servlet繼承BaseServlet,例如CustomerServlet繼承BaseServlet,然後在CustomerServlet中提供add()、update()、delete()等方法,每個方法對應不同的請求。
2、BaseServlet分析
我們知道,Servlet中處理請求的方法是service()方法,這說明我們需要讓service()方法去調用其他方法。例如調用add()、mod()、del()、all()等方法!具體調用哪個方法需要在請求中給出方法名稱!然後service()方法通過方法名稱來調用指定的方法。
無論是點擊超鏈接,還是提交表單,請求中必須要有method參數,這個參數的值就是要請求的方法名稱,這樣BaseServlet的service()才能通過方法名稱來調用目標方法。例如某個鏈接如下:
<a href=”/xxx/CustomerServlet?method=add”>添加客戶</a>
3、BaseServlet代碼
public class BaseServlet extends HttpServlet { /* * 它會根據請求中的m,來決定調用本類的哪個方法 */ protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); res.setContentType("text/html;charset=utf-8"); // 例如:http://localhost:8080/demo1/xxx?m=add String methodName = req.getParameter("method");// 它是一個方法名稱 // 當沒用指定要調用的方法時,那麼默認請求的是execute()方法。 if(methodName == null || methodName.isEmpty()) { methodName = "execute"; } Class c = this.getClass(); try { // 通過方法名稱獲取方法的反射對象 Method m = c.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); // 反射方法目標方法,也就是說,如果methodName爲add,那麼就調用add方法。 String result = (String) m.invoke(this, req, res); // 通過返回值完成請求轉發 if(result != null && !result.isEmpty()) { req.getRequestDispatcher(result).forward(req, res); } } catch (Exception e) { throw new ServletException(e); } } } |
DBUtils
1、DBUtils簡介
DBUtils是Apache Commons組件中的一員,開源免費!
DBUtils是對JDBC的簡單封裝,但是它還是被很多公司使用!
DBUtils的Jar包:dbutils.jar
2、DBUtils主要類
l DbUtils:都是靜態方法,一系列的close()方法;
l QueryRunner:
Ø update():執行insert、update、delete;
Ø query():執行select語句;
Ø batch():執行批處理。
3、QueryRunner之更新
QueryRunner的update()方法可以用來執行insert、update、delete語句。
1. 創建QueryRunner
構造器:QueryRunner();
2. update()方法
int update(Connection con, String sql, Object… params)
@Test public void fun1() throws SQLException { QueryRunner qr = new QueryRunner(); String sql = "insert into user values(?,?,?)"; qr.update(JdbcUtils.getConnection(), sql, "u1", "zhangSan", "123"); } |
還有另一種方式來使用QueryRunner
1. 創建QueryRunner
構造器:QueryRunner(DataSource)
2. update()方法
int update(String sql, Object… params)
這種方式在創建QueryRunner時傳遞了連接池對象,那麼在調用update()方法時就不用再傳遞Connection了。
@Test public void fun2() throws SQLException { QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource()); String sql = "insert into user values(?,?,?)"; qr.update(sql, "u1", "zhangSan", "123"); } |
4、ResultSetHandler
我們知道在執行select語句之後得到的是ResultSet,然後我們還需要對ResultSet進行轉換,得到最終我們想要的數據。你可以希望把ResultSet的數據放到一個List中,也可能想把數據放到一個Map中,或是一個Bean中。
DBUtils提供了一個接口ResultSetHandler,它就是用來ResultSet轉換成目標類型的工具。你可以自己去實現這個接口,把ResultSet轉換成你想要的類型。
DBUtils提供了很多個ResultSetHandler接口的實現,這些實現已經基本夠用了,我們通常不用自己去實現ResultSet接口了。
l MapHandler:單行處理器!把結果集轉換成Map<String,Object>,其中列名爲鍵!
l MapListHandler:多行處理器!把結果集轉換成List<Map<String,Object>>;
l BeanHandler:單行處理器!把結果集轉換成Bean,該處理器需要Class參數,即Bean的類型;
l BeanListHandler:多行處理器!把結果集轉換成List<Bean>;
l ColumnListHandler:多行單列處理器!把結果集轉換成List<Object>,使用ColumnListHandler時需要指定某一列的名稱或編號,例如:new ColumListHandler(“name”)表示把name列的數據放到List中。
l ScalarHandler:單行單列處理器!把結果集轉換成Object。一般用於聚集查詢,例如select count(*) from tab_student。
Map處理器
Bean處理器
Column處理器
Scalar處理器
5、QueryRunner之查詢
QueryRunner的查詢方法是:
public <T> T query(String sql, ResultSetHandler<T> rh, Object… params)
public <T> T query(Connection con, String sql, ResultSetHandler<T> rh, Object… params)
query()方法會通過sql語句和params查詢出ResultSet,然後通過rh把ResultSet轉換成對應的類型再返回。
@Test public void fun1() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student where number=?"; Map<String,Object> map = qr.query(sql, new MapHandler(), "S_2000"); System.out.println(map); } @Test public void fun2() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student"; List<Map<String,Object>> list = qr.query(sql, new MapListHandler()); for(Map<String,Object> map : list) { System.out.println(map); } } @Test public void fun3() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student where number=?"; Student stu = qr.query(sql, new BeanHandler<Student>(Student.class), "S_2000"); System.out.println(stu); } @Test public void fun4() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student"; List<Student> list = qr.query(sql, new BeanListHandler<Student>(Student.class)); for(Student stu : list) { System.out.println(stu); } } @Test public void fun5() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student"; List<Object> list = qr.query(sql, new ColumnListHandler("name")); for(Object s : list) { System.out.println(s); } } @Test public void fun6() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select count(*) from tab_student"; Number number = (Number)qr.query(sql, new ScalarHandler()); int cnt = number.intValue(); System.out.println(cnt); } |
5、QueryRunner之批處理
QueryRunner還提供了批處理方法:batch()。
我們更新一行記錄時需要指定一個Object[]爲參數,如果是批處理,那麼就要指定Object[][]爲參數了。即多個Object[]就是Object[][]了,其中每個Object[]對應一行記錄:
@Test public void fun10() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "insert into tab_student values(?,?,?,?)"; Object[][] params = new Object[10][];//表示 要插入10行記錄 for(int i = 0; i < params.length; i++) { params[i] = new Object[]{"S_300" + i, "name" + i, 30 + i, i%2==0?"男":"女"}; } qr.batch(sql, params); } |