問題提出
現在幾乎所有的應用系統都無法避免使用資料庫系統。在JAVA世界裡訪問資料庫是一件非常輕鬆的事情,JDBC為JAVA應用程式訪問資料庫提供了一個統一的介面,通過使用JDBC介面開發者無需關心繫統最終採用哪種資料庫,因為JDBC僅僅是定義了訪問幾個JAVA的介面類,具體的實現是由資料庫廠商提供的,這種做法其實與其他資料庫連線方式例如ODBC是類似的。但是在實際的應用過程中,開發者發現離JDBC設計的初衷還是有一定距離,就比如說在儲存字串時的編碼問題,我想很多開發者都會遇見這個問題,倒不是因為說解決它有什麼技術方面的難度,而是它的的確確非常繁瑣。我們必須在每次寫入或者讀出字串的時候進行編碼和反編碼處理;或者說我們可以寫一個方法可以進行編碼處理的,但又必須在每次資料庫操作的時候呼叫,雖然呼叫很簡單,可是我非得這樣嗎?要是忘了編碼那又要DEBUG了。當然你可能覺得這並沒有什麼,或者你可能很勤快,喜歡寫大量重複的程式碼,可是你難道沒有覺得這種繁瑣的工作正在浪費你過於寶貴的青春嗎?停止你的鍵盤輸入,讓我們來解決這個問題吧!
解決思路
在傳統的應用程式中資料庫操作部分我們可以想象成兩層,如圖所示:一個是資料庫的"連線池",另外一個業務資料操作層。在這裡資料庫的連線池是廣義的,你可以把JDBC中的DriverManager也當成是連線池,具體的意思就是我們可以通過這層來獲取到指定資料庫的連線而不去關心它是怎麼獲取的。如果這個時候資料庫系統(有如Informix,SQL Server)要求對字串進行轉碼才能儲存(例如最常見的GBK->;ISO8859_1轉碼),那我們就必須在業務資料操作層來進行,這樣有多少業務資料操作我們就要做多少編碼轉碼的工作,太麻煩了,程式碼中充斥中大量重複的內容。本文提出的解決方案就是利用對獲取到的資料庫連線例項進行二次封裝,也就是在資料庫連線池與業務資料操作層之間加入了連線封裝層,當然了,我們也完全可以直接將連線封裝整合到資料庫連線池內部。
我們知道進行編碼和轉碼工作都是集中在JDBC的兩個介面PreparedStatement和ResultSet上進行的,主要涉及PreparedStatement的setString方法以及ResultSet的getString方法。前面我們講過需要加入一個連線封裝層來對資料庫連線例項進行二次封裝,但是怎麼通過這個封裝來改變PreparedStatement和ResultSet這兩個介面的行為呢?這個問題其實也很簡單,因為PreparedStatement介面必須通過Connection介面來獲取例項,而ResultSet介面又必須從Statement或者PreparedStatement介面來獲取例項,有了這樣的級聯關係,問題也就迎刃而解了。還是利用我在文章《使用JAVA動態代理實現資料庫連線池》中使用的動態介面代理技術。首先我們設計Connection介面的代理類_Connection,這個代理類接管了Connection介面中所有可能獲取到Statement或者PreparedStatement介面例項的方法,例如:prepareStatement和createStatement。改變這兩個方法使之返回的是經過接管後的Statement或者PreparedStatement例項。通過對於Statement介面也有相應的代理類_Statement,這個代理類接管用於獲取ResultSet介面例項的所有方法,包括對setString方法的接管以決定是否對字串進行編碼處理。對於介面ResultSet的接管類_ResultSet就相應的比較簡單,它只需要處理getString方法即可。
關鍵程式碼
前面我們大概介紹了這個解決方案的思路,下面我們給出關鍵的實現程式碼包括Connection的代理類,Statement的代理類,ResultSet的代理類。這些程式碼是在原來關於資料庫連線池實現的基礎上進行擴充使之增加對自動編碼處理的功能。有需要原始碼打包的可以通過電子郵件跟我聯絡。
_Connection.java /* * Created on 2003-10-23 by Liudong */package lius.pool;import java.sql.*;import java.lang.reflect.*; / * * * 資料庫連線的代理類 * @author Liudong */ class _Connection implements InvocationHandler{ private Connection conn = null; private boolean coding = false; //指定是否進行字串轉碼操作 _Connection(Connection conn, boolean coding){ this.conn = conn; this.coding = coding; initConnectionParam(this.conn); } /** * Returns the conn. * @return Connection */ public Connection getConnection() { Class[] interfaces =conn.getClass().getInterfaces(); |
if(interfaces==null||interfaces.length==0){
interfaces = new Class[1];
interfaces[0] = Connection.class;
}
Connection conn2 = (Connection)Proxy.newProxyInstance(
conn.getClass().getClassLoader(), interfaces,this);
return conn2;
}
/**
* @see java.lang.reflect.InvocationHandler#invoke
*/
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
String method = m.getName();
//呼叫相應的操作
Object obj = null;
try{
obj = m.invoke(conn, args);
//接管用於獲取語句控制程式碼例項的方法
if((CS.equals(method)||PS.equals(method))&&coding)
return new _Statement((Statement)obj,true).getStatement();
} catch(InvocationTargetException e) {
throw e.getTargetException();
}
return obj;
} private final static String PS = "prepareStatement";
private final static String CS = "createStatement";}
_Statement.java
/*
* Created on 2003-10-23 by Liudong
*/
package lius.pool;
import java.sql.*;
import java.lang.reflect.*;
/**
* 資料庫語句物件例項的代理類
* @author Liudong
*/
class _Statement implements InvocationHandler{
private Statement statement ; //儲存所接管物件的例項
private boolean decode = false; //指定是否進行字串轉碼
public _Statement(Statement stmt,boolean decode) {
this.statement = stmt;
this.decode = decode;
}
/**
* 獲取一個接管後的物件例項
* @return
*/
public Statement getStatement() {
Class[] interfaces = statement.getClass().getInterfaces();
if(interfaces==null||interfaces.length==0){
interfaces = new Class[1];
interfaces[0] = Statement.class;
}
Statement stmt = (Statement)Proxy.newProxyInstance(
statement.getClass().getClassLoader(),
interfaces,this);
return stmt;
}
/**
* 方法接管
*/
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
String method = m.getName(); //接管setString方法
if(decode && SETSTRING.equals(method)) {
try{
String param = (String)args[1];
if(param!=null)
param = new String(param.getBytes(),"8859_1");
return m.invoke(statement,new Object[]{args[0],param});
} catch(InvocationTargetException e){
throw e.getTargetException(); }
}
//接管executeQuery方法
if(decode && EXECUTEQUERY.equals(method)){
try{
ResultSet rs = (ResultSet)m.invoke(statement,args);
return new _ResultSet(rs,decode).getResultSet();
}catch(InvocationTargetException e){
throw e.getTargetException();
}
}
try{
return m.invoke(statement, args);
} catch(InvocationTargetException e) {
throw e.getTargetException();
}
}
//兩個要接管的方法名
private final static String SETSTRING = "setString";
private final static String EXECUTEQUERY = "executeQuery";
}
_ResultSet.java
/*
* Created on 2003-10-23 by Liudong
*/
package lius.pool;
import java.sql.ResultSet;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 資料庫結果集的代理類
* @author Liudong
*/
class _ResultSet implements InvocationHandler{
private ResultSet rs = null;
private boolean decode = false;
public _ResultSet(ResultSet rs,boolean decode) {
this.rs = rs;
this.decode = decode;
}
public ResultSet getResultSet(){
Class[] interfaces = rs.getClass().getInterfaces();
if(interfaces==null||interfaces.length==0){
interfaces = new Class[1];
interfaces[0] = ResultSet.class;
}
ResultSet rs2 = (ResultSet)Proxy.newProxyInstance(rs.getClass().getClassLoader(),
interfaces,this);
return rs2;
}
/**
* 結果getString方法
*/
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
String method = m.getName();
if(decode && GETSTRING.equals(method)){
try{
String result = (String)m.invoke(rs,args);
if(result!=null)
return new String(result.getBytes("8859_1"));
return null;
}catch(InvocationTargetException e){
throw e.getTargetException();
}
}
try{
return m.invoke(rs, args);
}catch(InvocationTargetException e){
throw e.getTargetException();
}
}
private final static String GETSTRING = "getString";
}
現在我們已經把三個介面的代理類做好了,下一步就是怎麼來使用這三個類。其實對於使用者來講並不需要關心三個類,只需要瞭解_Connection就可以了,因為另外兩個是_Connection直接呼叫的。為了使用_Connection我們必須傳入兩個引數,第一個是資料庫實際的資料庫連線例項,另外一個是布林值代表是否進行轉碼處理。我們必須先通過實際的情況獲取到資料庫連線後再傳入_Connection的建構函式作為引數,下面例子告訴你如何來使用_Connection這個類:
Connection conn = getConnection(); //獲取資料庫連線
boolean coding = false; //從配置或者其他地方讀取是否進行轉碼的配置
//接管資料庫連線例項
_Connection _conn = new _Connection(conn,coding);
//獲得接管後的資料庫連線例項,以後直接使用conn2而不是conn
Connection conn2 = _conn.getConnection();
因為對一個應用系統來講,資料庫連線的獲取必然有統一的方法,在這個方法中加入對連線的接管就可以一勞永逸的解決資料庫的編碼問題。
效能比較
功能沒有問題了,開發者接下來就會關心效能的問題,因為在進行一些對響應速度要求很高或者大資料量的處理情況下效能就成為一個非常突出的問題。由於JAVA中的動態介面代理採用的是反射(Reflection)機制,同時又加入我們自己的一些程式碼例如方法名判斷,字串轉碼等操作因此在效能上肯定比不上直接使用沒有經過接管的資料庫連線。但是這點效能上的差別是不是我們可以忍受的呢,為此我做了一個試驗對二者進行了比較:
測試環境簡單描述:
使用ACCESS資料庫,建兩張結構一樣的表,計算從獲取連線後到插入資料完畢後的時間差,兩個程式(直連資料庫和使用連線接管)都進行的字串的轉碼操作。
測試結果:
插入記錄數 直連資料庫程式耗時 單位:ms 使用連線接管程式耗時 效能比較
1000 2063 2250 9.0%
5000 8594 8359 -2.7%
10000 16750 17219 2.8%
15000 22187 23000 3.6%
20000 27031 27813 2.9%
從上面這張測試結果表中來看,二者的效能的差別非常小,儘管在兩萬條資料的批量插入的時候時間差別也不會多於一秒鐘,這樣的結果應該說還是令人滿意的,畢竟為了程式良好的結構有時候犧牲一點點效能還是值得的。
本文算是我之前文章《使用JAVA動態代理實現資料庫連線池》中提出的資料庫連線池實現的進一步完善,同樣使用動態介面代理的技術來解決資料庫編碼的問題。JAVA的這個高階技術可以用來解決許多實際中非常棘手的問題,就像本文提到的編碼問題的處理以及資料庫連線池的實現,同時在WEB開發框架的實現上也有非常大的作為。歡迎對這方面感興趣的朋友來信共同來研究。