DbUtils

作者: Leo_up_up | 来源:发表于2020-05-11 00:15 被阅读0次

    DbUtils,这是一个对原生jdbc操作进行了一定封装的操作数据库的小框架,相对于原生jdbc来说,它的特点是什么呢,对结果集的封装,其最大的特点就是将查询到的结果封装成我们想要的类型。可以封装成我们自身的实体类,即将查询结果封装到我们实体类中的属性中。

    下面我们来系统的看一下这是怎么实现的。

    我们先不说具体实现细节,先来看看它的总体架构。

    DbUtils设计图

    我们可以从上面的图中看到,核心部分是ResultSetHandler,有诸多Handler实现了这个接口,这里运用了策略模式,对于ResultSetHandler,我们有诸多的实现,这里列举了几个比较重要的Handler,分别是BeanHnadler,BeanListHandler,MapHandler,AbstractListHnadler,继续看,这些Handler虽然实现了ResultSetHandler接口,但我们的真正的实现结果集到实体类的转换,还是不在这些策略中的,我们有一个新的接口,RowProcessor,所有的策略都强包含这样一个接口,这才是真正的转换器,这其中有四个方法,分别对于不同的转换有不同的转换方法,其中对于toBean和toBeanList方法,我们将它们放在一个新的类中,BeanProcessor。就是这样一个架构,DbUtils完成了对结果集的转换。这也是它最大的亮点。

    了解了DbUtils的基本架构设计,我们跟踪其中的一个方法,来细节了解一下具体是怎么实现结果集转换的。

    拿toBean()来进行分析。

    public class TestDbUtils {

    // 驱动程序

        static final StringJDBC_DRIVER ="com.mysql.cj.jdbc.Driver";

    // URL连接

        static final StringDB_URL ="jdbc:mysql://localhost:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC";

    //数据库信息

        static final StringUSER ="xxxx";

        static final StringPASS ="xxxx";

    public static void main(String[] args)throws SQLException {

        Connection conn =null;

        QueryRunner queryRunner =new QueryRunner();

        DbUtils.loadDriver(JDBC_DRIVER);

        conn = DriverManager.getConnection(DB_URL,USER,PASS);

        ResultSetHandler<Customer> resultHandler =

             new BeanHandler(Customer.class);//  结果集转换为实体  必须知道所有的策略

            try {

                Customer customer = queryRunner.query(conn,false,"SELECT * FROM customer WHERE  name                      = ?",resultHandler,"a");//  "'a' or 1=1"

                System.out.print("实体: " + customer);

            }finally {

                DbUtils.close(conn);

    }

    }

    }

    上面是我写的一个使用DbUtils的使用BeanHandler的方法案例,通过这个,我们打上断点,就可以深入DbUtils源码里面进行一步步解析。

    if (params !=null && params.length >0) {

    PreparedStatement ps =this.prepareStatement(conn, sql);// //预编译SQL,减少sql执行

        //stat = ps;  // 这一步应该不用

        this.fillStatement(ps, params);// 填充参数到sql语句里面

        rs =this.wrap(ps.executeQuery());// 执行sql获得结果集并且封装

    }else {

    stat = conn.createStatement();

    rs =this.wrap(stat.executeQuery(sql));

    }

    从上面可以看到,我们这里的sql是有参数的,所以就进入了this.fillStatement(ps, params);这不,接下来要做的,就是将我们设置的参数,添加到sql语句里面。具体怎么实现的,我们继续看。

    for (int i =0; i < params.length; i++) {

    if (params[i] !=null) {

      if (call !=null && params[i]instanceof OutParameter) {

      ((OutParameter) params[i]).register(call, i +1);

    }else {

      stmt.setObject(i +1, params[i]);//  这里就行防sql注入操作

            }

    }else {

            int sqlType = Types.VARCHAR;

    if (!pmdKnownBroken) {

    // TODO see DBUTILS-117: does it make sense to catch SQLEx here?

                try {

                    sqlType = pmd.getParameterType(i +1);

    }catch (final SQLException e) {

    pmdKnownBroken =true;

    }

    }

    stmt.setNull(i +1, sqlType);

    }

    从上面看到,遍历我们传来的参数,然后将对应参数放入到对应位置,其中防sql注入也是在这里实现的。

    将对应参数封装到sql语句里面后,然后查询出结果集,我们就要对结果集进行转换。

    result = rsh.handle(rs);

    我们已经获得了结果集:

    1:final PropertyDescriptor[] props =this.propertyDescriptors(type);

    2:final ResultSetMetaData rsmd = rs.getMetaData();

    3:final int[] columnToProperty =this.mapColumnsToProperties(rsmd, props);

    接下来,第一步获得我们要转换成的实体类的所有属性。

    1:这里是通过内审的方式来获得所有属性描述了。如果为实体类,那么第一个属性描述就是class,第二个及以下是自身定义的各种属性。

    2:获得了所有属性描述后,接下来就要获得结果集的元数据。

    3:这两个都获得后,我们就要将这两个数据对应起来,怎么对应起来呢,这里维护了一个数组,具体怎么是西安的,我们再来看:

    /**

    * @param rsmd  结果集元数据

    * @param props bean对象的属性描述集合

    * @return

    */

    private int[] mapColumnsToProperties(final ResultSetMetaData rsmd,final PropertyDescriptor[] props)throws SQLException {

    // 获得结果集中列的数量

        final int cols = rsmd.getColumnCount();

    // 初始化一个 结果数组

        final int[] columnToProperty =new int[cols +1];

      Arrays.fill(columnToProperty,PROPERTY_NOT_FOUND);// 数组内容都初始化为-1

        for (int clo =1; clo <= cols; clo++) {

    String cloName = rsmd.getColumnLabel(clo);// 获得列名

            if (cloName ==null || cloName.equals("")) {

    cloName = rsmd.getColumnName(clo);

    }

    String propertyName =columnToPropertyOverrides.get(cloName);

    if (propertyName ==null) {

    propertyName = cloName;

    }

    for (int i =0; i < props.length; i++) {

      PropertyDescriptor prop = props[i];

      Column column = prop.getReadMethod().getAnnotation(Column.class);

     String propertyColumnName =null;

    // 如果这个方法加了这么一个注解 , 那么 propertyColumnName = column.name() = "" 但是目前为止,好像都是null

          if (column !=null) {

             propertyColumnName = column.name();

    }else {

      propertyColumnName = prop.getName();

    }

    // 如果 bean 的属性描述名字与 列名一样

             if (propertyColumnName.equalsIgnoreCase(propertyName)) {

    // 进行对应 列数组 列名位置 对应值 为 bean 属性数组 的

               columnToProperty[clo] = i;

              break;

    }

    }

    }

    // 如果在 props 中无对应,那么就是初始化的-1

        return columnToProperty;

    }

    从上面可以看到,它通过维护一个数组,来实现一一对应,数组大小为结果集中列的数量+1,为了就是因为,实体类中,我们获得的第一个属性是class,而这个在结果集中是没有的。

    我们通过遍历结果集,获得结果集的列名,这个列名就是从数据库中查到的数据对应的名字,比如“id”,获得这个列名后,我们接下啦就要获得实体类的属性名字,如果这两个名字一样,那我们就将这对数据维护在数组columnToProperty里面,例如此时从数据库中获得的列名是cloName,对应clo为1,此时prop的下标是2,那么就维护成columnToProperty[1] = 2,表示结果集中下标为1的数据对应到实体类属性中是属性集下标为2的那个属性。这里使用的是双层循环,我们可以考虑进行优化。

    现在已经将结果集中的数据以及对应到实体类属性的哪一个属性都确定好了,接下啦就要通过传来的实体创建给实体,并将数据封装到这个类中。具体怎么实现的,我们继续看:

    /**

    * @param rs              结果集

    * @param bean            生成的bean对象

    * @param props            bean对象的属性描述集合

    * @param columnToProperty 结果集中的列索引。

    * @param

    * @return

    * @throws SQLException

    */

    private T populateBean(final ResultSet rs,final T bean, final PropertyDescriptor[] props,final int[] columnToProperty) throws SQLException {

      for (int i =0; i < columnToProperty.length; i++) {

      if (columnToProperty[i] ==PROPERTY_NOT_FOUND) {//该下标没有匹配的属性,则不做处理

                continue;

      }

    final PropertyDescriptor prop = props[columnToProperty[i]];// 获取对应属性描述对象

            final Class propType = prop.getPropertyType();// 获取属性类型

            Object value =null;

      if (propType !=null) {

    // 从结果集中根据属性获得值

                value =this.processColumn(rs, i, propType);//获取rs对应的值

                // 原生类型的值为空,则设定默认值

                if (value ==null && propType.isPrimitive()) {

    // primitiveDefaults中通过静态块代码已将原生类型的默认值初始好了

                    value =primitiveDefaults.get(propType);

    }

    }

    // 设定属性值

            this.callSetter(bean, prop, value);

    }

    return bean;

    }

    封装数据,就是对维护的对应数组进行循环,然后获得实体类的属性名称,因为已经维护好了对应关系,我们直接通过反射,将数据set到实体类属性中就可以。

    到这里,封装好的实体类就生成成功了。

    总体来看上述流程,这个框架设计还是挺巧妙的。

    相关文章

      网友评论

        本文标题:DbUtils

        本文链接:https://www.haomeiwen.com/subject/hdehnhtx.html