Java的強大重要的一點在於第三方庫的豐富多樣,只是對於初學者,很多框架的原理總是顯得高深莫測。事實上,大部分的框架都會與反射技術有莫大關聯。今天我們就來使用反射技術和泛型技術實現一個簡單的通用DAO。
關於什麼是DAO,什麼是泛型,我們暫且不論,權當大家熟悉不過的東西。我們從反射開始吧。
反射是什麼?我覺得應該從“正”這個方向來考慮。從組合語言時代開始,我們的程式設計方式基本就確定下來了:編寫原始碼->編譯程式->使用(連結為exe檔案或者是直接使用)。也就是說我們的程式的行為(資料操作和邏輯操作)必須在編寫程式碼的時候就確定下來,這種特性某種程度上限制了我們大腦的思考能力,自然也限制了程式的“能力”。同樣,如果我們要完全瞭解程式的功能,比如有哪些方法屬性,也必須有原始碼。如果我們需要在程式執行過程中讓程式自動根據需要自動執行某些操作,例如根據需要記錄日誌,根據資料庫表結構自動將查詢結果設定到實體物件中等等,我們必須要修改程式碼(或者說“我們”要考慮到所有情況)來適應這些變化的情況,例如每個表的結構都不一樣,自然查詢結果的列名,資料型別等也會有各種不同情況。而反射則允許我們使用特定API實現在程式“執行過程”中實現資料和邏輯的操作。下面的例子可以對一個物件的屬性賦值:
實體類
class ClassA{ public int a ; public String b ;}
設定屬性值的方法
public void setVal(ClassA ca,String b,int a){ ca.a= a; ca.b = b;}
看起來很簡單,我們來考慮下面的兩個問題:
問題1:如果這個類的屬性很多呢?setVal方法會隨著實體類屬性的增多而線性增長。
問題2:這個才是最悲劇的,如果我們沒有實體類A的原始碼呢?您打算怎麼寫這個setVal方法?(據說可以點出來,但是Eclipse的這個技術也是基於反射的呢)
下面的方法使用了反射技術,可以解決上面的兩個問題:
public void setVal(ClassA ca,Map params){
Class clz = ca.getClass();
Field[] fields= clz.getFields();
for (Field f : fields) {
if(params.containsKey(f.getName()))
{
try { f.set(ca,params.get(f.getName()));
}
catch (IllegalArgumentException e){ e.printStackTrace();
}
catch (IllegalAccessException e)
{ e.printStackTrace();
} } }}
初看這個方法,有點太複雜了吧?但是這個方法卻可以做到:給實體類新增或者減少屬性不需要修改setVal方法,我們也不需要ClassA的原始碼,因為我們這個方法中沒有出現任何實體類的具體資訊。
反射就是這樣一個技術:一方面它可以直接根據編譯後的結果(反)來獲取類的屬性、方法等資訊,另一方面反射還可以來操作這些屬性和方法等。
回頭來分析這個setVal方法,拋開異常處理來看,其實也很簡單。
STEP1:
Class clz = ca.getClass();//獲取一個物件的類資訊,通過這個類資訊,我們可以訪問操作方法和屬性等類的組成結構。
STEP2:
Field[] fields= clz.getFields();//通過類資訊獲取類的所有公共屬性(準確說應該叫欄位)
STEP3:
f.set(ca,params.get(f.getName()));//對屬性賦值,值來源於一個map集合
實際程式設計過程中,我們往往不是直接訪問欄位而是通過getter/setter訪問器訪問,這就涉及了方法的訪問:
public void setVal1(ClassA ca, Map params) {
Class clz = ca.getClass();
Field[] fields = clz.getDeclaredFields();
for (Field f : fields)
{ if (params.containsKey(f.getName()))
{ try { String fName = f.getName();
String methodName="set"+ fName.toUpperCase().charAt(0) +fName.substring(1); Method m = clz.getMethod(methodName, f.getType()); m.invoke(ca, params.get(fName));
} catch (IllegalArgumentException e)
{ e.printStackTrace();
} catch (IllegalAccessException e)
{ e.printStackTrace();
} catch (SecurityException e)
{ e.printStackTrace();
} catch (NoSuchMethodException e)
{ e.printStackTrace(); } catch (InvocationTargetException e)
{ e.printStackTrace(); } } }}
這個方法複雜了一點點,主要在於我們需要根據屬性名找見對應setter的方法名。細心的讀者會發現這行程式碼:
Field[] fields = clz.getDeclaredFields();
這個方法和getFields()的區別在於它可以得到私有欄位(靜態欄位則需要通過getDeclaredField(String fieldName))。
另外就是如何得到指定名稱和引數形式的方法了,程式碼為:
Method m = clz.getMethod(methodName, f.getType());
f.getType()則是用來獲取屬性的資料型別的。
具備這些反射的基礎知識後,如果需要進一步操作,我們可能需要對JDBC結果集元資料有所瞭解,下面的程式碼演示了JDBC結果集元資料的功能:
ResultSetMetaData metaData = rs.getMetaData();int columnCount = metaData.getColumnCount();for(int i=1;i<=columnCount;i++){ System.out.println("列名:"+metaData.getColumnName(i) +",型別:"+metaData.getColumnTypeName(i));}
ResultSetMetaData 代表結果集元資訊,即描述結果集的相關資訊,比如列名,列的型別等等。
結合泛型,我們可以寫出如下方法:
public List getAll(T t)
{ List list = new ArrayList();
Class c = t.getClass();
String sql = "select * from [" + c.getSimpleName() + "]"; System.out.println("SQL:" + sql); Connection conn = BaseDAO.getConn()
; try { PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery(); Method[] methods = c.getMethods(); while (rs.next()) { T t1 = (T) t.getClass().newInstance();
for (Method m : methods)
{ if (m.getName().startsWith("set"))
{ String colName = Character.toLowerCase(m.getName().charAt(3)) + m.getName().substring(4); if (rs.getObject(colName) != null)
{ m.invoke(t1, rs.getObject(colName)); } } } list.add(t1); } } catch (Exception e)
{ e.printStackTrace(); } return list;}
這並不是真正的終點,實際上,往往實體欄位型別和JDBC資料型別並不一定能夠自動對應,列名和屬性名也不一定一一對應,只是作為學習,我們做了很多假設的處理而已。這些對應關係,我們完全可以使用XML實現可配置,不再螯述。DAO的其他方法,例如新增資料,插入資料,條件查詢等等功能的程式碼,均可依葫蘆畫瓢,一一完成。