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;
}

最后感谢你的浏览,谢谢。