從 java bean 的內省到 dbutils 的應用

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 + "]";
    }
    
}

輸出如下:

image

注意在輸入裏,其屬性描述符包好一個 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;
}

最後感謝你的瀏覽,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章