让Spring BeanPropertyRowMapper支持枚举类型

周末下雨无处可去,来篇技术博客!

最近在做一个新项目,DAO层采用的是Spring JDBC,Spring JDBC最麻烦的事情莫过于手工去写RowMapper了,因此Spring JDBC提供了一个BeanPropertyRowMapper的实现,能够自动将数据库表中读出的字段与Domain对象的属性进行映射,比如把表字段"user_name"映射到Java对象属性"userName",但是有一个问题,我经常在Domain对象中使用枚举类型,而枚举类型在数据库中所对应的类型可能会是int或者字符串,让BeanPropertyRowMapper对象去在没有任何“提示信息”的情况下把数据库中的一个int或者字符串映射成Java世界中的枚举类型是不可能的。

怎么办?因为这个事情而从此在Domain对象设计的时候不用枚举类型的属性了?那可不好!这样我非疯了不可!要解决这个问题,就要找到BeanPropertyRowMapper的扩展点,能让自己把枚举类型和数据库字段类型的映射规则整合进去!于是就开始读BeanPropertyRowMapper的源码找出这个扩展点。

BeanPropertyRowMapper 的大致工作流程是这样的,首先会根据传入的Domain对象类型(Class<?>对象), 反射拿到Domain类的所有属性信息,属性信息是通过java.beans.PropertyDescriptor来进行包装,PropertyDescriptor是一个JavaBean规范的抽象,包含属性类型,以及属性所对应的读写方法(getter/setter)等属性。拿到这些属性,最终会构建起一个Map,key是自动生成的数据库字段名,比如“userName”会变为“user_name”, value就是PropertyDescriptor对象。这个工作是在BeanPropertyRowMapper的构造函数中进行。从类中获取属性再将其包装成PropertyDescriptor需要用到反射操作,是一件很耗性能的事情,在这里我注意到,因为Spring整个框架需要依靠大量的反射操作,所以它采用了一套统一的cache机制来缓存反射的结果,有兴趣的可以参考org.springframework.beans.CachedIntrospectionResults这个类的实现,自己平时在编写framework的时候难免会遇到反射结果的缓存问题,可以复用或者借鉴Spring的这个解决方案。

做完这些,然后就是核心的mapRow方法了,首先会通过反射调用newInstance方法,构建出一个要填充的Domain对象,接下来会通过ResultSet对象获取ResultSetMetaData对象,这个对象是个很有意思的对象,我们可以通过它来获取数据库的一些Meta信息,比如列名甚至是当初建表时候指定的每一列的comment。有了读取结果的列名,然后就是遍历这些列名,从上文提到的map中根据列名拿到PropertyDescriptor对象对Bean进行操作了,将DB中读取的Object类型的值填充给对象。这种对Bean进行的操作,Spring是通过一个BeanWrapper接口来实现的,而并不是想象中的直接使用field.setValue或者method.invoke之类的方式。看来要找的扩展点就在这一步了,要让BeanWrapper知道PropertyDescriptor描述的属性与数据库中读出的Object值的映射规则。

继续跟进代码到org.springframework.beans.BeanWrapperImpl,这块代码层次非常复杂,一堆if...else,一个方法几百行的样子,顿时就没有用肉眼细读的勇气了,直接开debug模式跟进执行,发现最终的类型转换是落实到java.beans.PropertyEditor的实现中去了,这个接口也在Spring中被大量的使用到用于类型转换,关于此接口的一些介绍:http://www.blogjava.net/orangewhy/archive/2007/06/26/126371.html

Spring提供了一个PropertyEditorRegistry的接口来管理PropertyEditor,这个接口的设计思路跟Spring中BeanDefinitionRegistry之类的接口一样,就是一个注册容器。Spring自身提供了很多的PropertyEditor,有基本类型的,还有一些常用类型比如Decimal、Date之类,都会注册到这个容器中,当要进行类型转换的时候,就会默认从此容器中去获取,BeanWrapperImpl自身就实现了这个接口,如果在这里取不到对应的PropertyEditor,那么就会使用org.springframework.beans.BeanUtils.findEditorByConvention(Class<?> targetType)来查找Editor,这个方法定义了一个查找Editor类的规则:String editorName = targetType.getName() + "Editor";

嗯,代码分析此时可以终止了。针对这个问题我们可以有两种解法:一是继承BeanPropertyRowMapper类,并且覆盖它的initBeanWrapper方法, like this:

private BeanPropertyRowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class) {
                                       
        @Override
        protected void initBeanWrapper(BeanWrapper bw) {
            super.initBeanWrapper(bw);
            bw.registerCustomEditor(UserRole.class, new UserRoleEditor());
        }
                                       
    };

public class UserRoleEditor extends PropertyEditorSupport {
                                    
    @Override
    public void setValue(Object value) {
        int intVal = Integer.parseInt(value.toString());
        super.setValue(UserRole.getByValue(intVal));
    }
                                    
    @Override
    public String getAsText() {
        UserRole userRole = (UserRole) this.getValue();
        return userRole.getRole() + "";
    }
}

值的注意的是,PropertyEditor会持有状态,并且多数时候不是线程安全的(如果你真要把它做成线程安全并且单例的话恐怕会效率低的要命),BeanWrapper 的实现也同样如此,因此每次mapRow 的时候都会去new一个新的BeanWrapper对象和对应的一系列PropertyEditor对象来保证线程安全。

第二种解决的方法就更简单了,完全不用覆盖BeanPropertyRowMapper的任何方法,直接按照BeanUtils里面的查找规则来定义PropertyEditor就可以了。

另外,不仅仅适应于枚举,这种解决方案适合所有类型的对象。

[END]

你可能感兴趣的:(java,spring,spring,jdbc)