java bean 內省的基礎
java bean 的內省,其實可以算是反射的一種基礎應用,關於 java 的反射,無非就是獲得對應的類、屬性、方法、修飾符等的應用,對於 java 的反射探討,可以點擊參考 java 7 Reflection。在這裏,我們關注一下 java 對 普通 bean 的使用和應用。
在 一個 普通 bean 裏,我們會關注起屬性與其對應的get/set 方法,如在一個類 User 中,關注屬性 name 的同時,我們同樣會關注一下 getName 和 setName 方法。在 java 裏,專門提供了這麼一種機制 Instropector(內省), 是 java 對 bean 屬性、方法的一種缺省處理方法。通過 Introspector ,我們可以獲得一個 bean 的基礎信息 BeanInfo,進而獲取這個 bean 的屬性描述器 PropertyDescriptor,通過屬性描述器,我們可以訪問其對應的 get/set 方法。示例代碼如下(爲了演示方便,這裏只是簡單的拋出異常):
public class TestReflect {
public static void main(String[] args) throws IntrospectionException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
PropertyDescriptor props[] = userBeanInfo.getPropertyDescriptors();
// 輸出 user 裏所有屬性
for (PropertyDescriptor prop : props) {
System.out.print(prop.getName() + " ");
}
System.out.println();
// 獲取第一個屬性 age ,並調用其 set 方法
User user = User.class.newInstance();
Method setter = props[0].getWriteMethod();
setter.invoke(user, 18);
System.out.println(user);
}
}
public class User {
private String name;
private byte sex;
private int age;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte getSex() {
return sex;
}
public void setSex(byte sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User [name=" + name + ", sex=" + sex + ", age=" + age + ", email=" + email + "]";
}
}
輸出如下:
注意在輸入裏,其屬性描述符包好一個 User 類不存在的屬性 class。這裏解釋一下,通過 beanInfo 獲取的屬性描述符,主要是通過 bean 裏 getXxx() 方法來獲取的,換言之及時沒有屬性 password ,如果存在方法 getPassword(),那麼在獲取屬性描述符的時候,同樣也會獲取到 password 屬性。可在在這裏爲什麼會獲取到 class 屬性呢??因爲 User 默認繼承了 Object 類(Object 有一個方法 getClass())。
通過屬性 PropertyDescriptor#getReadMethod()/getWriteMethod() 可以對應的獲取屬性的 get/set 方法(Method),如果不存在對應屬性的 get/set 方法,會返回 null。獲取到對應 Method 之後,就可以調用 invoke 方法了。
dbutils 的裏一個小應用
Apache dbutils 是一個很小巧的 jdbc 處理集,當中就提供了對 ResultSet 轉換爲 java bean,bean List 的機制,其中的原理其實就是 java bean 的內省機制的使用。關於 dbutils 的其他探討可以參考 從 Apache dbutils 所學到的。在此我們探討一下dbutils 是如何將一個 ResultSet 轉換爲 對應的 java bean 的。
在開始看代碼之前,我們可以先思考一下,應該如何處理??
首先,我們應該獲取指定 java bean 的屬性描述符;
第二步:應該就是獲取數據庫對應的列屬性名稱;
第三步:應該就是創建指定的 java bean;
第四步:應該就是匹配屬性名稱與列屬性名稱,如果匹配,就調用java bean 對應的 set 方法。
大概的流程應該是這樣,不過爲了完善一點,我們應該考慮一下。如下情況:1)可能會存在 java bean 屬性和列屬性不一致的情況,可有需要將列的值賦給指定 java bean 的屬性。2)對應數據庫的 null 值,我們應該將其轉換爲 java 裏對應的默認值,如 int 的默認值爲 0 等。3)嗯,大概也就這些了。
現在來看一下 dbutils 的實現,整體函數如:
public <T> T toBean(ResultSet rs, Class<T> type) throws SQLException {
// 獲取指定 java bean 的屬性描述符
PropertyDescriptor[] props = this.propertyDescriptors(type);
// 通過 ResultSetMetaData 獲取數據庫列屬性名稱
ResultSetMetaData rsmd = rs.getMetaData();
// 處理 java bean 屬性和列屬性不一致的情況
int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
// 創建一個指定的 java bean
return this.createBean(rs, type, props, columnToProperty);
}
接着就是分步實現上面的功能:
1)獲取指定 java bean 的屬性描述符,(這個應該很簡單,直接就是 Instropector.getBeanInfo(class),只是需要額外處理一下異常就行了).
private PropertyDescriptor[] propertyDescriptors(Class<?> c)
throws SQLException {
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(c);
} catch (IntrospectionException e) {
throw new SQLException(
"Bean introspection failed: " + e.getMessage());
}
return beanInfo.getPropertyDescriptors();
}
2)第二步,處理java bean 屬性與 列屬性不一致的情況。這個也不難,首先我們一個java bean 屬性到列屬性覆蓋的 HashMap<String,String>(),接着就是使用一個數組 columnToProperty, 用來記錄列屬性的到java bean屬性的索引值(在 PropertyDescriptor[] 的索引,columnToProperty 默認是列屬性沒有與java bean 屬性對應,值爲 -1)。談到這裏,我們就會知道,在這個工具類裏 BeanProcessor 的構造函數應該提供一個HashMap<String,String>(),默認構造函數,可以創建一個沒有元素的空的 HashMap<String,String>()。參考代碼如下:
// columnToProperty 的默認值(沒有覆蓋java bean 的指定屬性)
protected static final int PROPERTY_NOT_FOUND = -1;
// 需要覆蓋的 java bean 屬性
private final Map<String, String> columnToPropertyOverrides;
// 默認的構造函數,創建一個元素爲空的 HashMap
public BeanProcessor() {
this(new HashMap<String, String>());
}
public BeanProcessor(Map<String, String> columnToPropertyOverrides) {
super();
if (columnToPropertyOverrides == null) {
throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null");
}
this.columnToPropertyOverrides = columnToPropertyOverrides;
}
那如何進行記錄列屬性的到java bean屬性的索引值呢??參考代碼如下:
protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
PropertyDescriptor[] props) throws SQLException {
int cols = rsmd.getColumnCount();//獲數據庫表的列的數目,注意其索引從 1 開始
int[] columnToProperty = new int[cols + 1];
Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);// 默認賦值爲 -1
for (int col = 1; col <= cols; col++) {
// 獲取列屬性名的別名
String columnName = rsmd.getColumnLabel(col);
if (null == columnName || 0 == columnName.length()) {
columnName = rsmd.getColumnName(col);// 獲取列屬性名
}
//查找該屬性是否需要覆蓋
String propertyName = columnToPropertyOverrides.get(columnName);
if (propertyName == null) {
propertyName = columnName;
}
//記錄其在 props 屬性描述數組裏的索引位置
for (int i = 0; i < props.length; i++) {
if (propertyName.equalsIgnoreCase(props[i].getName())) {
columnToProperty[col] = i;
break;
}
}
}
return columnToProperty;
}
3)就是根據指定列屬性和java bean 的 set 方法,創建一個 java bean。如下:
創建一個 java bean 簡單(額外處理一些異常),如下:
protected <T> T newInstance(Class<T> c) throws SQLException {
try {
return c.newInstance();
} catch (InstantiationException e) {
throw new SQLException(
"Cannot create " + c.getName() + ": " + e.getMessage());
} catch (IllegalAccessException e) {
throw new SQLException(
"Cannot create " + c.getName() + ": " + e.getMessage());
}
}
2)調用相對應的 set 方法
private <T> T createBean(ResultSet rs, Class<T> type,
PropertyDescriptor[] props, int[] columnToProperty)
throws SQLException {
//創建 bean
T bean = this.newInstance(type);
for (int i = 1; i < columnToProperty.length; i++) {
//該列屬性不需要賦值給 java bean 的屬性
if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
continue;
}
//根據 columnToProperty 的索引獲取對應的屬性描述符
PropertyDescriptor prop = props[columnToProperty[i]];
Class<?> propType = prop.getPropertyType();//獲取屬性描述符的類型
//獲取列屬性的值
Object value = this.processColumn(rs, i, propType);
// 當列屬性爲 null 時,根據其類型賦值其相對應的默認值
if (propType != null && value == null && propType.isPrimitive()) {
value = primitiveDefaults.get(propType);
}
// 調用bean 對一個的 set 方法,注意支持 8 種基本數據類型和String 的set 方法
this.callSetter(bean, prop, value);
}
return bean;
}
4)默認值處理很簡單,根據屬性描述的類型,賦值對應默認值行了。爲了性能上優化,往往也會使用靜態變量來存儲 java 8 種基本數據類型的默認值,這裏採用 map 來存儲,如下:
private static final Map<Class<?>, Object> primitiveDefaults = new HashMap<Class<?>, Object>();
static {
primitiveDefaults.put(Integer.TYPE, Integer.valueOf(0));
primitiveDefaults.put(Short.TYPE, Short.valueOf((short) 0));
primitiveDefaults.put(Byte.TYPE, Byte.valueOf((byte) 0));
primitiveDefaults.put(Float.TYPE, Float.valueOf(0f));
primitiveDefaults.put(Double.TYPE, Double.valueOf(0d));
primitiveDefaults.put(Long.TYPE, Long.valueOf(0L));
primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
primitiveDefaults.put(Character.TYPE, Character.valueOf((char) 0));
}
現在我們已經知道如何將將數據庫表映射到一個bean了,那麼處理 bean list 也就不難,就是創建一個 List<T>,接着就是將每一行映射得到的 bean ,添加到 List裏就可以了。
public <T> List<T> toBeanList(ResultSet rs, Class<T> type) throws SQLException {
List<T> results = new ArrayList<T>();
if (!rs.next()) {
return results;
}
PropertyDescriptor[] props = this.propertyDescriptors(type);
ResultSetMetaData rsmd = rs.getMetaData();
int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
do {
results.add(this.createBean(rs, type, props, columnToProperty));
} while (rs.next());
return results;
}
最後感謝你的瀏覽,謝謝。