JDBC第三次学习
? ?這是我的JDBC第三次學習了,在學習的過程中,老是會忘掉一些知識,不記下筆記實在不行啊!
? ?使用JDBC調用存儲過程
? ?(1)關于如何使用Navicat(11.1.13) for MySQL如何創建存儲過程。我在另一篇文章寫過,在此不贅述了。
? ?使用Navicat(11.1.13) for MySQL如何創建存儲過程,存儲過程的主要代碼如下:
BEGININSERT INTO user (name, birthday, money) values (pname, birthday, money);SELECT LAST_INSERT_ID() into pid; END? ?注意:
? ?(2)使用JDBC調用存儲過程(即返回當前這條記錄插入后形成的id),代碼如下:
static void ps() throws SQLException {Connection conn = null;CallableStatement cs = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();/** call:是固定寫法,addUser()是我們在數據庫中定義的存儲過程的名字* ()后面指定參數 ,如果沒有任何參數addUser后的括號也要寫上。* 所以,類似于函數 */String sql = "{call addUser(?,?,?,?)}";cs = conn.prepareCall(sql);cs.registerOutParameter(4, Types.INTEGER);//注冊輸出參數/** 設置輸入參數*/cs.setString(1, "pa name");cs.setDate(2, new java.sql.Date(System.currentTimeMillis()));cs.setFloat(3, 100f);cs.executeUpdate();int id = cs.getInt(4);//不注冊輸出參數是不能這樣拿出來的System.out.println("id="+id);} finally {JdbcUtils.free(rs, cs, conn);}}? ?返回當前這條記錄插入后形成的id,還有如下你方法:
static int create() throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {//2.建立連接conn = JdbcUtils.getConnection();//3.創建語句String sql = "insert into user (name,birthday,money) values ('name1 gk','1987-01-01',400)";/** 自動產生主鍵,用參數Statement.RETURN_GENERATED_KEYS拿出產生的主鍵* mysql參數Statement.RETURN_GENERATED_KEYS加不加都可以拿出來* 但是這和不同的數據庫產品以及相應的驅動有關,所以最好寫上!!!*/ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);ps.executeUpdate();/** getGeneratedKeys()這個方法為什么不是返回int類型而是要返回一個ResultSet類型呢?* 因為,一、主鍵不一定是int類型,* 二、聯合(復合)主鍵 ,有可能是一張表中的幾個字段合起來構成一個id,這樣就不能返回一個int類型了,* 如果是聯合主鍵返回的是多列的內容,我們可以遍歷ResultSet得到聯合主鍵列的值。 * 所以返回ResultSet*/rs = ps.getGeneratedKeys();int id = 0;if(rs.next()) {id = rs.getInt(1);}return id;} finally {JdbcUtils.free(rs, ps, conn);}}?
? ?使用JDBC的批處理功能?
? ?批處理,可以大幅度提升大量增、刪、改的速度。
public class BatchTest {//main方法調用測試批量插入與普通的insert所消耗的時間比 public static void main(String[] args) throws SQLException {long start = System.currentTimeMillis();for(int i = 0; i < 100; i++) {create(i);}long end = System.currentTimeMillis();System.out.println("create:"+(end-start));start = System.currentTimeMillis();createBatch();end = System.currentTimeMillis();System.out.println("createBatch:"+(end-start));}//普通方法插入數據static void create(int i) throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {//2.建立連接conn = JdbcUtils.getConnection();//3.創建語句String sql = "insert into user (name,birthday,money) values (?,?,?)";ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);ps.setString(1, "batch name"+i);ps.setDate(2, new java.sql.Date(System.currentTimeMillis()));ps.setFloat(3, 100f+i);ps.executeUpdate();} finally {JdbcUtils.free(rs, ps, conn);}}//批量插入數據static void createBatch() throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {//2.建立連接conn = JdbcUtils.getConnection();//3.創建語句String sql = "insert into user (name,birthday,money) values (?,?,?)";ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);/** 每循環一次都會形成一條新的sql語句被打包,直到循環完成然后進行批量的處理* 那么可不可以無限量的增加呢?其實會產生內存溢出的情況,到底循環多少次進行打包才合適,這個值要經過測試*/for(int i = 0; i < 100; i++) {ps.setString(1, "batch name"+i);ps.setDate(2, new java.sql.Date(System.currentTimeMillis()));ps.setFloat(3, 100f+i);/** 并不是使用批處理就會提高效率* 把sql語句打成一個包* 包不能太大(并不是越大越好),會內存溢出*/ps.addBatch();}int[] is = ps.executeBatch();} finally {JdbcUtils.free(rs, ps, conn);}}}? ?
? ?可滾動結果集與分頁技術?
static void scroll() throws SQLException {Connection conn = null;Statement st = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();/** 在創建一個Statement的時候指定可滾動的結果集的類型* TYPE_SCROLL_SENSITIVE:滾動的過程中,對數據庫是敏感的* (按我的理解就是查詢數據的時候,如果又新增、刪除、更新,那么能感覺得到)* CONCUR_READ_ONLY:字面意思是同意只讀*/st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);/** 在mysql中通過limit關鍵字實現分頁,* 每種數據庫產品的關鍵字不同例如:Oracle使用rownum,sqlServer用top* 現在有幾十種關系型數據庫,如果數據庫不支持這種關鍵字進行分頁的時候可以用滾動的結果集來實現分頁,* 但是性能比較低 *///sql = "select id,name,birthday,money from user limit 150, 10";//mysql支持分頁rs = st.executeQuery("select id,name,birthday,money from user");//5.處理結果while(rs.next()) {System.out.println(rs.getObject("id")+"\t"+rs.getObject("name")+"\t"+rs.getObject("birthday")+"\t"+rs.getObject("money"));}System.out.println("-------------------------------------");/** 絕對定位,可以直接定位到rs所有返回結果中指定的一條記錄上 * 例定位到第150行*/rs.absolute(150);int i = 0;/** 可以通過i來控制循環次數,實現分頁效果* 但是要數據庫產品或者驅動支持此功能! */while(rs.next() && i < 10) {i++;System.out.println(rs.getObject("id")+"\t"+rs.getObject("name")+"\t"+rs.getObject("birthday")+"\t"+rs.getObject("money"));}} finally {JdbcUtils.free(rs, st, conn);}}? ?
? ?可更新和對更新敏感的結果集?
static void read() throws SQLException, InterruptedException {Connection conn = null;Statement st = null;ResultSet rs = null;try {//2.建立連接conn = JdbcUtils.getConnection();//3.創建語句/** 設置滾動結果集的類型為:ResultSet.TYPE_SCROLL_SENSITIVE,就是能感知到數據庫的變化 * CONCUR_UPDATABLE:字面意思是同意更新*/st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);//4.執行語句rs = st.executeQuery("select id,name,birthday,money from user where id < 5");//5.處理結果/** 驗證TYPE_SCROLL_SENSITIVE* 下面讓rs每循環一次rs睡眠10秒鐘,然后再這個過程中我們用mysql客戶端修改數據庫中的數據 * 我們看看它讀出來的是修改前的數據,還是修改后的。我們在上面設置的可滾動的結果集的類型 * 是ResultSet.TYPE_SCROLL_SENSITIVE,也就是能感知數據庫的變化,那如果在rs沒有讀出數據庫里的* 那條數據之前我們在mysql的客戶端將原先的數據修改掉,這里讀出來的數據應該是修改后的數據,但是在* 測試的時候讀出的數據卻依然是修改之前的,這應該和數據庫的驅動有關系 * 但是如果是能感知數據庫的變化,那么數據庫的性能也是降低了,你執行executeQuery()方法后,它已經將數據查詢完成* 打包后給你發送過來了,如果察覺到數據庫的變化那么它要在輸出之前再查詢一遍數據庫,這種需求用的比較少,作為了解即可 */while(rs.next()) {int id = rs.getInt("id");System.out.println("show " + id + "...");Thread.sleep(10000);System.out.println(id+"\t"+rs.getObject("name")+"\t"+rs.getObject("birthday")+"\t"+rs.getObject("money"));/** 查詢的時候可以更新(可更新的結果集)* 可更新的結果集,我們并不建議這樣做因為上面的sql語句是查詢操作 * 但是下面還隱藏著更新操作,對于程序的可讀性不好,這種需求也比較少 * 了解即可*/String name = rs.getString("name");if("lisi".equals(name)) {rs.updateFloat("money", 300f);rs.updateRow();//修改完成后要修改一行 }}} finally {JdbcUtils.free(rs, st, conn);}}??
? ?數據庫的元數據信息?
? ?通過DatabaseMetaData可以獲得數據庫相關的信息如:數據庫版本、數據庫名、數據庫廠商信息、是否支持事務、是否支持某種事務隔離級別,是否支持滾動結果集等。對于我們編寫程序來說不常用,但是在框架的編寫中經常會用到,例如hibernate,它要屏蔽不同數據庫之間的區別,那么它就要知道當前是什么數據庫,然后做出相應的判斷處理:在使用hibernate的時候有一項是配置數據庫的方言,其實就是指定你使用的是什么數據庫產品。如果你不進行指定,hibernate會自動的嘗試著去檢測當前數據庫產品的類型,其實就是根據DatabaseMetaData來檢測的。
? ?示例代碼:
public class DBMO {public static void main(String[] args) throws SQLException {Connection conn = JdbcUtils.getConnection();//得到數據庫的元信息 DatabaseMetaData dbmd = conn.getMetaData();//取出當前使用數據庫的名稱 System.out.println("db name:" + dbmd.getDatabaseProductName());//看看當前數據庫支不支持事務 System.out.println("tx:" + dbmd.supportsTransactions());conn.close();}}?
? ?參數的元數據信息?
public class ParameterMetaTest {public static void main(String[] args) throws SQLException {Object[] params = new Object[] {"lisi", new java.sql.Date(System.currentTimeMillis()), 100f};read("select * from user where name = ? and birthday < ? and money > ?", params);}static void read(String sql, Object[] params) throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();ps = conn.prepareStatement(sql);//得到參數信息的元數據 /*第一種方式給sql語句中的占位符賦值,但是要約定:sql語句中占位符的所表示的類型和個數和參數數組中是一致的 ParameterMetaData pmd = ps.getParameterMetaData();int count = pmd.getParameterCount();for(int i = 1; i <= count; i++) {System.out.print(pmd.getParameterClassName(i) + "\t");System.out.print(pmd.getParameterType(i) + "\t");System.out.println(pmd.getParameterTypeName(i));//Ctrl+T打開其基礎體系ps.setObject(i, params[i-1]);}*//** 第二種方式給sql語句中的占位符賦值*/for(int i = 1; i <= params.length; i++) {ps.setObject(i, params[i-1]);}rs = ps.executeQuery();while(rs.next()) {System.out.println(rs.getInt("id")+"\t"+rs.getString("name")+"\t"+rs.getDate("birthday")+"\t"+rs.getFloat("money"));}} finally {JdbcUtils.free(rs, ps, conn);}} }? ?小知識:快捷鍵Ctrl+T——打開其整個繼承體系。
? ?有可能產生的異常:
java.sql.SQLException: Parameter metadata not available for the given statement? ?解決的方法不難,就是在連接數據庫時的URL后面加上可以返回的元數據類型
? ?例如出異常時,我的URL是這樣寫的:
url = "jdbc:mysql://localhost:3306/jdbc";正確寫法應該是:
url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";出現異常的原因:因為mysql驅動默認generateSimpleParameterMetadata=false只有設置為true,metadata類型會將每一個參數反射為Varchar類型。(時間才過去幾天,就有點不是很清楚了)
? ?
? ?將結果集元數據封裝為Map??
? ?現在我們有一種需求將ResultSet結果集中的數據封裝成Map,map的key是數據庫中字段的值,value就是在字段中的值。
? ?通過ResultSetMetaData可以獲得結果有幾列、各列名、各列別名、各列類型等。
? ?可以將ResultSet放入Map(key:列名 value:列值)。
? ?用反射ResultSetMetaData將查詢結果讀入對象中(簡單的O/RMapping)。
? ? 示例代碼如下:
public class ResultSetMetaDataTest {public static void main(String[] args) throws SQLException {List<Map<String, Object>> data = read("select id, name as n from user where id < 5");System.out.println(data);}static List<Map<String, Object>> read(String sql) throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();ps = conn.prepareStatement(sql);rs = ps.executeQuery();//得到ResultSet的元數據 ResultSetMetaData rsmd = rs.getMetaData();//得到ResultSet元數據的列數 int count = rsmd.getColumnCount();String[] colNames = new String[count];for(int i = 1; i <= count; i++) {System.out.print(rsmd.getColumnClassName(i)+"\t");//得到參數的類名,例java.lang.String System.out.print(rsmd.getColumnName(i)+"\t");//取列的實際名字System.out.println(rsmd.getColumnLabel(i));//取列的別名colNames[i-1] = rsmd.getColumnLabel(i);}List<Map<String, Object>> datas = new ArrayList<Map<String, Object>>();while(rs.next()) {Map<String, Object> data = new HashMap<String, Object>();for(int i = 0; i < colNames.length; i++) {data.put(colNames[i], rs.getObject(colNames[i]));}datas.add(data);}return datas;} finally {JdbcUtils.free(rs, ps, conn);}} }? ?
? ?編寫一個基本的連接池來實現連接的復用?
大家都知道Arraylist的底層使用數組實現的,而LinkedList使用鏈表實現的,所以對于Arraylist讀取速度比較快而對于LinkedList修改和添加比較快,所以我們這個連接池因為要頻繁的操作集合所以用LinkedList來實現。
public class MyDataSource {private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";/** 對集合中的元素進行頻繁的取出,* 用LinkedList*/LinkedList<Connection> connectionsPool = new LinkedList<Connection>();//向我們的LinkedList集合中加入10個鏈接作為我們的連接池 public MyDataSource() {try {for(int i = 0; i < 10; i++) {//將Connection放到鏈表的最后面 this.connectionsPool.addLast(this.createConnection());}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}//得到一個鏈接(先進先出算法)public Connection getConnection() throws SQLException {return this.connectionsPool.removeFirst();}//創建鏈接private Connection createConnection() throws SQLException {return DriverManager.getConnection(url, user, password);}/** 關閉一個鏈接,這個關閉不是真正意義上的關閉,* 而是又把它放回到連接池中,實現了Connection的復用 */public void free(Connection conn) {this.connectionsPool.addLast(conn);} }? ?對基本連接池進行一些工程細節上的優化
? ?在上面實現的連接池中我們只是默認創建了10個連接,但是如果這個時候有10個線程同時都來拿連接,那連接池里就沒有連接了,在有線程過來拿的時候就會報錯了,現在我們進行一些優化。
public class MyDataSource {private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";//規定默認創建的連接數 private static int initCount = 5;//規定最大可以創建的連接數 private static int maxCount = 10;//統計當前共創建了多少個連接 private int currentCount = 0;/** 對集合中的元素進行頻繁的取出,* 用LinkedList*/LinkedList<Connection> connectionsPool = new LinkedList<Connection>();public MyDataSource() {try {for(int i = 0; i < initCount; i++) {this.connectionsPool.addLast(this.createConnection());//每創建一個鏈接,currentCount++ this.currentCount++;}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}public Connection getConnection() throws SQLException {/** 因為Connection不是線程安全的,* 所以我必須保證每個線程拿到的鏈接不是同一個,所以要進行同步:當兩個線程同時來拿的時候 * 另外一個線程必須等待 */synchronized (connectionsPool) {//①連接池中還有連接,取出if(this.connectionsPool.size() > 0)return this.connectionsPool.removeFirst();//②連接池中已沒有連接,并且當前創建的鏈接數沒有到最大值,那就繼續創建鏈接 if(this.currentCount < maxCount) {this.currentCount++;return this.createConnection();}//③大于連接池中的最大數,拋出異常throw new SQLException("已沒有連接");}}public void free(Connection conn) {this.connectionsPool.addLast(conn);}private Connection createConnection() throws SQLException {return DriverManager.getConnection(url, user, password);} }? ?
? ?通過代理模式來保持用戶關閉連接的習慣
? ?在上面的示例中我們在關閉鏈接的時候,調用的是free方法來把這個連接又放回到了池中,但是按照開發人員的使用習慣應該是調用colse()方法來關閉一個鏈接,但是如果調用close方法關閉,那這個連接就真的關閉了,也就是說我們這個方法設計的不符合開發人員的使用習慣。下面我用代理模式(關于代理模式,我在另一篇文章中寫過)的方法來解決這個問題:?
? ?定義一個類實現Connection接口,Connectio接口中有很多的方法,這些方法我們都無法自己完成,我們交給通過構造方法傳遞進來的真正的Connection的對象來完成,我們只是修改它的close方法,在用戶得到鏈接的時候我們返回給用戶這個類的對象,那么當用戶調用close方法關閉鏈接的時候,我們就可以在這個close方法中將用戶要關閉的那個鏈接再次的放到連接池中,這樣鏈接就不會真正的關閉了。
public class MyConnetion implements Connection {private Connection realConnection;private MyDataSource2 dataSource;/** 限制其(連接)最大使用次數*/private int maxUseCount = 5;/** 記錄(連接)當前使用次數*/private int currentUseCount = 0;/** 由于訪問修飾符是default,* 所以只能在包cn.itcast.jdbc.datasource中使用MyConnetion*/MyConnetion(Connection connection, MyDataSource2 dataSource) {this.realConnection = connection;this.dataSource = dataSource;}//清除警告 @Overridepublic void clearWarnings() throws SQLException {this.realConnection.clearWarnings();}@Overridepublic Statement createStatement() throws SQLException {return this.realConnection.createStatement();}@Overridepublic void commit() throws SQLException {this.realConnection.commit();}@Overridepublic void close() throws SQLException {this.currentUseCount++;/** 規定同一個鏈接只能使用maxUseCount次,* 超過這個次數就把真正的鏈接關閉,連接池中就要少一個鏈接* 這個時候再拿鏈接,拿到的就是新創建得一個新的鏈接對象了。 */if(this.currentUseCount < this.maxUseCount)this.dataSource.connectionsPool.addLast(this);else {this.realConnection.close();this.dataSource.currentCount--;}}//Connectio接口中實在是有太多的方法,在此就不寫了,我們主要關注close()方法
}
? ?為了更清楚地表達思想,我們可以慢慢來優化代碼(不好的代碼)
public class MyDataSource2 {private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";private static int initCount = 1;private static int maxCount = 1;int currentCount = 0;LinkedList<MyConnetion> connectionsPool = new LinkedList<MyConnetion>();public MyDataSource2() {try {for(int i = 0; i < initCount; i++) {this.connectionsPool.addLast(this.createConnection());this.currentCount++;}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}public Connection getConnection() throws SQLException {synchronized (connectionsPool) {if(this.connectionsPool.size() > 0)return this.connectionsPool.removeFirst();if(this.currentCount < maxCount) {this.currentCount++;return this.createConnection();}throw new SQLException("已沒有連接");}}public void free(Connection conn) {if(conn instanceof MyConnetion) {this.connectionsPool.addLast((MyConnetion)conn);}}private MyConnetion createConnection() throws SQLException {Connection realConn = DriverManager.getConnection(url, user, password);MyConnetion myConnetion = new MyConnetion(realConn, this);return myConnetion;}}
? ?因為針對接口編程是面向對象的第一原則,所以我們優化代碼為:
public class MyDataSource2 implements DataSource {//實現了DataSource接口之后,就是一個標準的數據源了,我們的程序只和數據源打交道,后面會講DBCP實現private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";private static int initCount = 1;private static int maxCount = 1;int currentCount = 0;/** 針對接口編程——面向對象的第一原則*/LinkedList<Connection> connectionsPool = new LinkedList<Connection>();public MyDataSource2() {try {for(int i = 0; i < initCount; i++) {this.connectionsPool.addLast(this.createConnection());this.currentCount++;}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}public Connection getConnection() throws SQLException {synchronized (connectionsPool) {if(this.connectionsPool.size() > 0)return this.connectionsPool.removeFirst();if(this.currentCount < maxCount) {this.currentCount++;return this.createConnection();}throw new SQLException("已沒有連接");}}public void free(Connection conn) {this.connectionsPool.addLast(conn);}private Connection createConnection() throws SQLException {/** 這是一個真實的connection*/Connection realConn = DriverManager.getConnection(url, user, password);MyConnetion myConnetion = new MyConnetion(realConn, this);return myConnetion;//返回一個代理對象}//實現DataSource接口中一系列方法,較多,不寫
}
? ?
? ?Java的動態代理及使用該技術完善連接代理?
? ?在上面的示例中,我們為了產生一個代理對象實現了Connection接口的所有的方法,但是我們只需要修改它的close方法,別的方法我們都需要交給真正的Connection對象去處理,比較麻煩,我們用動態代理(?)來實現它。
class MyConnectionHandler implements InvocationHandler {private Connection realConnection;private Connection warpedConnection;private MyDataSource2 dataSource;/** 限制其最大使用次數*/private int maxUseCount = 5;/** 記錄當前使用次數*/private int currentUserCount = 0;MyConnectionHandler(MyDataSource2 dataSource) {this.dataSource = dataSource;}Connection bind(Connection realConn) {this.realConnection = realConn;/** Proxy類就像程序員一樣可寫代碼* Proxy寫一個類,此類實現了Connection接口,* 對connection接口的方法調用轉化給調用處理器MyConnectionHandler* * 動態產生warped(包裹的)Connection。*/this.warpedConnection = (Connection)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {Connection.class}, this);return warpedConnection;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if("close".equals(method.getName())) {this.currentUserCount++;if(this.currentUserCount < this.maxUseCount)this.dataSource.connectionsPool.addLast(this.warpedConnection);else {this.realConnection.close();this.dataSource.currentCount--;}}/** 調用方法的時候如果是close方法就執行我們的邏輯(上面的代碼),* 對于其他的所有的方法,全部交給真實Connection對象本身自己去處理 * 調用realConnection實例的method方法*/ return method.invoke(this.realConnection, args);} }? ?當寫完上面的代理類后,我們還是需要修改MyDataSource2類的createConnection()方法來調用我們的代理類,將它需要的參數傳遞給它并把生成的代理類返回:(注意:MyDataSource2類中只須修改createConnection()),部分代碼如下:
private Connection createConnection() throws SQLException {/** 這是一個真實的connection*/Connection realConn = DriverManager.getConnection(url, user, password);MyConnectionHandler proxy = new MyConnectionHandler(this);return proxy.bind(realConn);}?
? ?標準DataSource接口及數據源的總結介紹?
? ?理解數據源的優勢與特點:?
? ?
轉載于:https://www.cnblogs.com/yerenyuan/p/5328115.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: org.apache.catalina.
- 下一篇: Javascript 事件冒泡处理