BeanUtils: 威力和代價(轉載)

Apache Jakarta Commons項目非常有用。我曾在許多不同的項目上或直接或間接地使用各種流行的commons組件。其中的一個強大的組件就是BeanUtils。我將說明如何使用BeanUtils將local實體bean轉換爲對應的value 對象:


BeanUtils.copyProperties(aValue, aLocal)

上面的代碼從aLocal對象複製屬性到aValue對象。它相當簡單!它不管local(或對應的value)對象有多少個屬性,只管進行復制。我們假設local對象有100個屬性。上面的代碼使我們可以無需鍵入至少100行的冗長、容易出錯和反覆的get和set方法調用。這太棒了!太強大了!太有用了!

現在,還有一個壞消息:使用BeanUtils的成本驚人地昂貴!我做了一個簡單的測試,BeanUtils所花費的時間要超過取數據、將其複製到對應的value對象(通過手動調用get和set方法),以及通過串行化將其返回到遠程的客戶機的時間總和。所以要小心使用這種威力!

  • 如果您有BeanUtils和類似的實用程序方面的體驗,請與我交流分享。
    +prakash

Beanutils用了魔術般的反射技術,實現了很多誇張有用的功能,都是C/C++時代不敢想的。無論誰的項目,始終一天都會用得上它。我算是後知後覺了,第一回看到它的時候居然錯過。

1.屬性的動態getter,setter

在這框架滿天飛的年代,不能事事都保證執行getter,setter函數了,有時候屬性是要需要根據名字動態取得的,就像這樣:  
BeanUtils.getProperty(myBean,"code");
而BeanUtils更強的功能是直接訪問內嵌對象的屬性,只要使用點號分隔。
BeanUtils.getProperty(orderBean, "address.city");
相比之下其他類庫的BeanUtils通常都很簡單,不能訪問內嵌的對象,所以經常要用Commons BeanUtils替換它們。
BeanUtils還支持List和Map類型的屬性。如下面的語法即可取得顧客列表中第一個顧客的名字
BeanUtils.getProperty(orderBean, "customers[1].name");
其中BeanUtils會使用ConvertUtils類把字符串轉爲Bean屬性的真正類型,方便從HttpServletRequest等對象中提取bean,或者把bean輸出到頁面。
而PropertyUtils就會原色的保留Bean原來的類型。

2.beanCompartor 動態排序

還是通過反射,動態設定Bean按照哪個屬性來排序,而不再需要在bean的Compare接口進行復雜的條件判斷。
List peoples = ...; // Person對象的列表 Collections.sort(peoples, new BeanComparator("age"));

如果要支持多個屬性的複合排序,如"Order By lastName,firstName"

ArrayList sortFields = new ArrayList(); sortFields.add(new BeanComparator("lastName")); sortFields.add(new BeanComparator("firstName")); ComparatorChain multiSort = new ComparatorChain(sortFields); Collections.sort(rows,multiSort);

其中ComparatorChain屬於jakata commons-collections包。
如果age屬性不是普通類型,構造函數需要再傳入一個comparator對象爲age變量排序。
另外, BeanCompartor本身的ComparebleComparator, 遇到屬性爲null就會拋出異常, 也不能設定升序還是降序。
這個時候又要藉助commons-collections包的ComparatorUtils.

   Comparator mycmp = ComparableComparator.getInstance();
   mycmp = ComparatorUtils.nullLowComparator(mycmp);  //允許null
   mycmp = ComparatorUtils.reversedComparator(mycmp); //逆序
   Comparator cmp = new BeanComparator(sortColumn, mycmp);

3.Converter 把Request或ResultSet中的字符串綁定到對象的屬性

   經常要從request,resultSet等對象取出值來賦入bean中,下面的代碼誰都寫膩了,如果不用MVC框架的綁定功能的話。

String a = request.getParameter("a"); bean.setA(a); String b = ....

不妨寫一個Binder:

MyBean bean = ...; HashMap map = new HashMap(); Enumeration names = request.getParameterNames(); while (names.hasMoreElements()) { String name = (String) names.nextElement(); map.put(name, request.getParameterValues(name)); } BeanUtils.populate(bean, map);

    其中BeanUtils的populate方法或者getProperty,setProperty方法其實都會調用convert進行轉換。
    但Converter只支持一些基本的類型,甚至連java.util.Date類型也不支持。而且它比較笨的一個地方是當遇到不認識的類型時,居然會拋出異常來。
    對於Date類型,我參考它的sqldate類型實現了一個Converter,而且添加了一個設置日期格式的函數。
要把這個Converter註冊,需要如下語句:

ConvertUtilsBean convertUtils = new ConvertUtilsBean();
   DateConverter dateConverter = new DateConverter();
   convertUtils.register(dateConverter,Date.class);



//因爲要註冊converter,所以不能再使用BeanUtils的靜態方法了,必須創建BeanUtilsBean實例
BeanUtilsBean beanUtils = new BeanUtilsBean(convertUtils,new PropertyUtilsBean());
beanUtils.setProperty(bean, name, value);

4 其他功能

4.1 PropertyUtils,當屬性爲Collection,Map時的動態讀取:
 
Collection: 提供index
   BeanUtils.getIndexedProperty(orderBean,"items",1);
或者
  BeanUtils.getIndexedProperty(orderBean,"items[1]");

Map: 提供Key Value
  BeanUtils.getMappedProperty(orderBean, "items","111");//key-value goods_no=111 
或者
  BeanUtils.getMappedProperty(orderBean, "items(111)")
 
4.2 PropertyUtils,獲取屬性的Class類型
     public static Class getPropertyType(Object bean, String name)
 
4.3 ConstructorUtils,動態創建對象
      public static Object invokeConstructor(Class klass, Object arg)
4.4 MethodUtils,動態調用方法
MethodUtils.invokeMethod(bean, methodName, parameter);
4.5 動態Bean 用DynaBean減除不必要的VO和FormBean 
一、概述
第一次看到BeanUtils包,是在Struts項目中,作爲Struts一個工具來使用的,
估計功能越弄越強,就移到Common項目中了吧。

BeanUtils一共有四個package:
org.apache.commons.beanutils
org.apache.commons.beanutils.converters
org.apache.commons.beanutils.locale
org.apache.commons.beanutils.locale.converters
後三個包主要是用於數據的轉換,圍繞着一個Converter接口,該接口只有一個方法:
java.lang.Object convert(java.lang.Class type, java.lang.Object value) ,
用於將一個value轉換成另一個類型爲type的Object。在一些自動化的應用中應該會有用。
這裏不作評論,以後有興趣了,或者覺得有用了,再行研究。
這裏只講第一個包。

二、測試用的Bean
在開始所有的測試之前,我寫了一個簡單的Bean,以便於測試,代碼如下:
package test.jakarta.commons.beanutils;


public class Month {
 private int value;
 private String name;
 private int[] days={11,22,33,44,55};

 public Month(int v, String n){
   value=v;
   name=n;
 }
 
 
 public String getName() {
   return name;
 }

 
 public int getValue() {
   return value;
 }

 
 public void setName(String name) {
   this.name = name;
 }

 
 public void setValue(int value) {
   this.value = value;
 }

 
 public String toString() {
   return value+"("+name+")";
 }

 public int[] getDays() {
   return days;
 }

 public void setDays(int[] is) {
   days = is;
 }

}

三、BeanUtils
這是一個主要應用於Bean的Util(呵呵,這個解釋很絕吧),以下是其中幾個方法的例子

//static java.util.Map describe(java.lang.Object bean) 
//這個方法返回一個Object中所有的可讀屬性,並將屬性名/屬性值放入一個Map中,另外還有
//一個名爲class的屬性,屬性值是Object的類名,事實上class是java.lang.Object的一個屬性
 Month month=new Month(1, "Jan");
 
 try {
   Map map=BeanUtils.describe(month);
   Set keySet=map.keySet();
   for (Iterator iter = keySet.iterator(); iter.hasNext();) {
     Object element = (Object) iter.next();
     System.out.println("KeyClass:"+element.getClass().getName());
     System.out.println("ValueClass:"+map.get(element).getClass().getName());
     System.out.print(element+"\t");
     System.out.print(map.get(element));
     System.out.println();
   }
 } catch (IllegalAccessException e) {
   e.printStackTrace();
 } catch (InvocationTargetException e) {
   e.printStackTrace();
 } catch (NoSuchMethodException e) {
   e.printStackTrace();
 }
輸出爲:
KeyClass:java.lang.String
ValueClass:java.lang.String
value  1
KeyClass:java.lang.String
ValueClass:java.lang.String
class  class test.jakarta.commons.beanutils.Month
KeyClass:java.lang.String
ValueClass:java.lang.String
name  Jan
注意到所有Map中的key/value都是String,而不管object中實際的值是多少。
與此對應的還有static void populate(java.lang.Object bean, java.util.Map properties)
用於將剛纔describe出的Map再裝配成一個對象。


再看這樣一段代碼
曹曉鋼也許還記得,爲了取一個不確定對象的property,着實花了不少時間,
難度不大,但要做到100%的正確,仍然需要付出很大的精力。
//static java.lang.String getProperty(java.lang.Object bean, java.lang.String name) 
 Month month=new Month(1, "Jan");
 
 try {
   System.out.println(BeanUtils.getProperty(month,"value"));
 } catch (Exception e) {
   e.printStackTrace();
 }
//輸出是:1

與getProperty類似的還有getIndexedProperty, getMappedProperty,
以getIndexedProperty爲例:
 Month month=new Month(1, "Jan");
 
 try {
   System.out.println(BeanUtils.getIndexedProperty(month,"days",1));
   System.out.println(BeanUtils.getIndexedProperty(month,"days[1]"));
 } catch (Exception e) {
   e.printStackTrace();
 }
這兩個調用是相同的。


BeanUtils中還有一個方法:
static void copyProperties(java.lang.Object dest, java.lang.Object orig) 
它真是太有用,還記得struts中滿天飛的都是copyProperties,我甚至懷疑整個BeanUtils最初
是不是因爲這個方法的需求才寫出來的。
它將對象orig中的屬性複製到dest中去。


四、PropertyUtils
這個類和BeanUtils類很多的方法在參數上都是相同的,但返回值不同。
BeanUtils着重於"Bean",返回值通常是String,而PropertyUtils着重於屬性,
它的返回值通常是Object。


五、ConstructorUtils
這個類中的方法主要分成兩種,一種是得到構造方法,一種是創建對象。
事實上多數時候得到構造方法的目的就是創建對象,這裏只介紹一下創建對象。
//static java.lang.Object ConstructorUtils.invokeConstructor
//(java.lang.Class klass, java.lang.Object[] args) 
//根據一個java.lang.Class以及相應的構造方法的參數,創建一個對象。
 Object obj=ConstructorUtils.invokeConstructor(Month.class, {new Integer(1), "Jan"});
 Month month=(Month)obj;
 try {
   System.out.println(BeanUtils.getProperty(month,"value"));
 } catch (Exception e) {
   e.printStackTrace();
 }
輸出證明,構造方法的調用是成功的。
如果需要強制指定構造方法的參數類型,可以這樣調用:
   Object[] args={new Integer(1), "Jan"};
   Class[] argsType={int.class, String.class};
   Object obj;
   obj = ConstructorUtils.invokeExactConstructor(Month.class, args, argsType);
   Month month=(Month)obj;
   System.out.println(BeanUtils.getProperty(month,"value"));
argsType指定了參數的類型。
 
六、ConstructorUtils補遺
創建對象還有一個方法:invokeExactConstructor,該方法對參數要求
更加嚴格,傳遞進去的參數必須嚴格符合構造方法的參數列表。
例如:
Object[] args={new Integer(1), "Jan"};
Class[] argsType={int.class, String.class};
Object obj;
//下面這句調用將不會成功,因爲args[0]的類型爲Integer,而不是int
//obj = ConstructorUtils.invokeExactConstructor(Month.class, args);

//這一句就可以,因爲argsType指定了類型。
obj = ConstructorUtils.invokeExactConstructor(Month.class, args, argsType);
Month month=(Month)obj;
System.out.println(BeanUtils.getProperty(month,"value"));


七、MethodUtils
與ConstructorUtils類似,不過調用的時候,通常需要再指定一個method name的參數。

八、DynaClass/DynaBean
這似乎是BeanUtils中最有趣的部分之一了,很簡單,簡單到光看這兩個接口中的方法會不明白
爲什麼要設計這兩個接口。不過看到ResultSetDynaClass後,就明白了。下面是java doc中的代碼:
   ResultSet rs = ...;
   ResultSetDynaClass rsdc = new ResultSetDynaClass(rs);
   Iterator rows = rsdc.iterator();
   while (rows.hasNext())  {
     DynaBean row = (DynaBean) rows.next();
     ... process this row ...
   }
   rs.close();
原來這是一個ResultSet的包裝器,ResultSetDynaClass實現了DynaClass,它的iterator方法返回一個
ResultSetIterator,則是實現了DynaBean接口。
在獲得一個DynaBean之後,我們就可以用
     DynaBean row = (DynaBean) rows.next();
     System.out.println(row.get("field1")); //field1是其中一個字段的名字

再看另一個類RowSetDynaClass的用法,代碼如下:
String driver="com.mysql.jdbc.Driver";
String url="jdbc:mysql://localhost/2hu?useUnicode=true&characterEncoding=GBK";
String username="root";
String password="";

java.sql.Connection con=null;
PreparedStatement ps=null;
ResultSet rs=null;
try {
Class.forName(driver).newInstance();
con = DriverManager.getConnection(url);
ps=con.prepareStatement("select * from forumlist");
rs=ps.executeQuery();
//先打印一下,用於檢驗後面的結果。
while(rs.next()){
System.out.println(rs.getString("name"));
}
rs.beforeFirst();//這裏必須用beforeFirst,因爲RowSetDynaClass只從當前位置向前滾動

RowSetDynaClass rsdc = new RowSetDynaClass(rs);
rs.close();
ps.close();
List rows = rsdc.getRows();//返回一個標準的List,存放的是DynaBean
for (int i = 0; i
DynaBean b=(DynaBean)rows.get(i);
System.out.println(b.get("name"));
}
} catch (Exception e) {
e.printStackTrace();
}
finally{
try {
con.close();
} catch (Exception e) {
}
}

是不是很有趣?封裝了ResultSet的數據,代價是佔用內存。如果一個表有10萬條記錄,rsdc.getRows()
就會返回10萬個記錄。@_@

需要注意的是ResultSetDynaClass和RowSetDynaClass的不同之處:
1,ResultSetDynaClass是基於Iterator的,一次只返回一條記錄,而RowSetDynaClass是基於
List的,一次性返回全部記錄。直接影響是在數據比較多時ResultSetDynaClass會比較的快速,
而RowSetDynaClass需要將ResultSet中的全部數據都讀出來(並存儲在其內部),會佔用過多的
內存,並且速度也會比較慢。
2,ResultSetDynaClass一次只處理一條記錄,在處理完成之前,ResultSet不可以關閉。
3,ResultSetIterator的next()方法返回的DynaBean其實是指向其內部的一個固定
對象,在每次next()之後,內部的值都會被改變。這樣做的目的是節約內存,如果你需要保存每
次生成的DynaBean,就需要創建另一個DynaBean,並將數據複製過去,下面也是java doc中的代碼:
   ArrayList results = new ArrayList(); // To hold copied list
   ResultSetDynaClass rsdc = ...;
   DynaProperty properties[] = rsdc.getDynaProperties();
   BasicDynaClass bdc =
     new BasicDynaClass("foo", BasicDynaBean.class,
                        rsdc.getDynaProperties());
   Iterator rows = rsdc.iterator();
   while (rows.hasNext()) {
     DynaBean oldRow = (DynaBean) rows.next();
     DynaBean newRow = bdc.newInstance();
     PropertyUtils.copyProperties(newRow, oldRow);
     results.add(newRow);
   }

事實上DynaClass/DynaBean可以用於很多地方,存儲各種類型的數據。自己想吧。嘿嘿。


九、自定義的CustomRowSetDynaClass
兩年前寫過一個與RowSetDynaClass目標相同的類,不過多一個功能,就是分頁,只取需要的數據,
這樣內存佔用就會減少。

先看一段代碼:
String driver="com.mysql.jdbc.Driver";
String url="jdbc:mysql://localhost/2hu?useUnicode=true&characterEncoding=GBK";
String username="root";
String password="";

java.sql.Connection con=null;
PreparedStatement ps=null;
ResultSet rs=null;
try {
Class.forName(driver).newInstance();
con = DriverManager.getConnection(url);
ps=con.prepareStatement("select * from forumlist order by name");
rs=ps.executeQuery();


//第二個參數表示第幾頁,第三個參數表示頁的大小
CustomRowSetDynaClass rsdc = new CustomRowSetDynaClass(rs, 2, 5);
//RowSetDynaClass rsdc = new RowSetDynaClass(rs);
rs.close();
ps.close();
List rows = rsdc.getRows();
for (int i = 0; i
DynaBean b=(DynaBean)rows.get(i);
System.out.println(b.get("name"));
}
} catch (Exception e) {
e.printStackTrace();
}
finally{
try {
con.close();
} catch (Exception e) {
}
}
在這裏用到了一個CustomRowSetDynaClass類,構造方法中增加了page和pageSize兩個參數,
這樣,不管數據庫裏有多少條記錄,最多隻取pageSize條記錄,若pageSize==-1,則功能和
RowSetDynaClass一樣。這在大多數情況下是適用的。該類的代碼如下:

package test.jakarta.commons.beanutils;

import java.io.*;
import java.sql.*;
import java.util.*;

import org.apache.commons.beanutils.*;


public class CustomRowSetDynaClass implements DynaClass, Serializable {

// ----------------------------------------------------------- Constructors


public CustomRowSetDynaClass(ResultSet resultSet) throws SQLException {

this(resultSet, true);

}


public CustomRowSetDynaClass(ResultSet resultSet, boolean lowerCase)
throws SQLException {

this(resultSet, 1, -1, lowerCase);

}

public CustomRowSetDynaClass(
ResultSet resultSet,
int page,
int pageSize,
boolean lowerCase)
throws SQLException {

if (resultSet == null) {
throw new NullPointerException();
}
this.lowerCase = lowerCase;
this.page = page;
this.pageSize = pageSize;

introspect(resultSet);
copy(resultSet);

}

public CustomRowSetDynaClass(ResultSet resultSet, int page, int pageSize)
throws SQLException {
this(resultSet, page, pageSize, true);
}

// ----------------------------------------------------- Instance Variables


protected boolean lowerCase = true;

protected int page = 1;
protected int pageSize = -1;


protected DynaProperty properties[] = null;


protected Map propertiesMap = new HashMap();


protected List rows = new ArrayList();

// ------------------------------------------------------ DynaClass Methods


public String getName() {

return (this.getClass().getName());

}


public DynaProperty getDynaProperty(String name) {

if (name == null) {
throw new IllegalArgumentException("No property name specified");
}
return ((DynaProperty) propertiesMap.get(name));

}


public DynaProperty[] getDynaProperties() {

return (properties);

}


public DynaBean newInstance()
throws IllegalAccessException, InstantiationException {

throw new UnsupportedOperationException("newInstance() not supported");

}

// --------------------------------------------------------- Public Methods


public List getRows() {

return (this.rows);

}

// ------------------------------------------------------ Protected Methods


protected void copy(ResultSet resultSet) throws SQLException {
int abs = 0;
int rowsCount = 0;
int currentPageRows = 0;
resultSet.last();
rowsCount = resultSet.getRow();
if (pageSize != -1) {
int totalPages = (int) Math.ceil(((double) rowsCount) / pageSize);
if (page > totalPages)
page = totalPages;
if (page < 1)
page = 1;
abs = (page - 1) * pageSize;

//currentPageRows=(page==totalPages?rowsCount-pageSize*(totalPages-1):pageSize);
} else
pageSize = rowsCount;
if (abs == 0)
resultSet.beforeFirst();
else
resultSet.absolute(abs);
//int 
while (resultSet.next() && ++currentPageRows <= pageSize) {
DynaBean bean = new BasicDynaBean(this);
for (int i = 0; i < properties.length; i++) {
String name = properties[i].getName();
bean.set(name, resultSet.getObject(name));
}
rows.add(bean);
}

}


protected void introspect(ResultSet resultSet) throws SQLException {

// Accumulate an ordered list of DynaProperties
ArrayList list = new ArrayList();
ResultSetMetaData metadata = resultSet.getMetaData();
int n = metadata.getColumnCount();
for (int i = 1; i <= n; i++) { // JDBC is one-relative!
DynaProperty dynaProperty = createDynaProperty(metadata, i);
if (dynaProperty != null) {
list.add(dynaProperty);
}
}

// Convert this list into the internal data structures we need
properties =
(DynaProperty[]) list.toArray(new DynaProperty[list.size()]);
for (int i = 0; i < properties.length; i++) {
propertiesMap.put(properties[i].getName(), properties[i]);
}

}


protected DynaProperty createDynaProperty(
ResultSetMetaData metadata,
int i)
throws SQLException {

String name = null;
if (lowerCase) {
name = metadata.getColumnName(i).toLowerCase();
} else {
name = metadata.getColumnName(i);
}
String className = null;
try {
className = metadata.getColumnClassName(i);
} catch (SQLException e) {
// this is a patch for HsqlDb to ignore exceptions
// thrown by its metadata implementation
}

// Default to Object type if no class name could be retrieved
// from the metadata
Class clazz = Object.class;
if (className != null) {
clazz = loadClass(className);
}
return new DynaProperty(name, clazz);

}


protected Class loadClass(String className) throws SQLException {

try {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = this.getClass().getClassLoader();
}
return (cl.loadClass(className));
} catch (Exception e) {
throw new SQLException(
"Cannot load column class '" + className + "': " + e);
}

}

}

大部分代碼從BeanUtils的源碼中取得,只做了簡單的修改,沒有加多餘的註釋。如果要正式使用,
需要再做精加工。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章