dbUtils源码学习总结

dbUtils源码学习总结

介绍

duUtils 是对查询结果集的处理工具,进行一些简单的ORM映射(对象关系映射)
ORM对象关系映射用于数据库结果集转化为对象
本人根据源码实现的仓库地址:https://github.com/lldwb/dbutils.git

需要学习的知识点

这里只是介绍知识点,这篇文章主要是介绍dbUtils源码

  1. 面向对象oo
  2. 设计原则
  3. 设计模式
  4. 泛型
  5. 反射
  6. 自定义注解
  7. SPI机制

sql执行器(增删改部分)

首先分析一下传统的增删改的执行步骤:

  1. 加载驱动
  2. 创建连接对象
  3. 获取 PreparedStatement 对象并预编译发送sql
  4. 设置参数
  5. 执行返回影响的行数

那么为了减少代码的复用和降低耦合,就需要把一下代码进行封装
保证通用性的情况下,哪些执行步骤可以封装起来呢
3-5可以封装起来。1-2不可以,因为保证可以在不同数据库的情况下使用
封装后的步骤(下面有案例):

  1. 获取连接对象(通过构造方法)
  2. 传入sql语句和参数,获取 PreparedStatement 对象并预编译发送sql
  3. 通过 setParameters 方法设置参数
  4. 执行返回影响的行数
duUtils 案例
/**
 *
 * SQL执行器,用于执行sql语句,以及处理查询结果
 * 这个是核心类,封装Connection、PreparedStatement、ResultSet接口的底层操作,简化JDBC的使用
 *
 */
public class SqlExecutor {

    /**
     * 连接对象,从外部传入,不需要自己生成
     */
    private Connection connection;

    /**
     * 构造方法传入连接对象
     * @param connection 连接对象
     */
    public SqlExecutor(Connection connection) {
        this.connection = connection;
    }

    /**
     * 执行增删改的方法
     * @param sql 执行的sql语句
     * @param params 可变参数(数组),sql语句中需要设置的参数
     *               (也是就是sql中的"?")
     * @return
     */
    public int executeUpdate(String sql, Object...params) {
        if(connection == null) {
            throw new RuntimeException("Null connection.");
        }
        if(sql == null) {
            throw new RuntimeException("Null SQL statement");
        }
        //通过connection对象得到一个PreparedStatement对象用于发送sql语句
        PreparedStatement ps = null;
        try {
            //创建PreparedStatement并预编译发送sql
            ps = connection.prepareStatement(sql);
            //设置参数
            setParameters(ps, params);
            //执行execute方法返回影响的行数
            return ps.executeUpdate();
        } catch (SQLException e) {
            //将捕获的sql异常抛出去给调用者
            throw new RuntimeException("Execute sql error." + e);
        } finally {
            //关闭Statement
            close(ps);
            //关闭连接
            close();
        }
    }

    /**
     * 给sql语句设置参数
     * @param ps
     * @param params
     */
    private void setParameters(PreparedStatement ps, Object[] params) throws SQLException {
        int count = 1;
        for (Object object : objects) {
            //通过setObject方法来设置参数,注意:jdbc的参数是从1开始
            preparedStatement.setObject(count, object);
            count++;
        }
    }

    /**
     * 关闭Statement
     * @param st
     */
    private void close(Statement st) {
        if(st != null) {
            try {
                st.close();
            } catch (SQLException e) {
                throw new RuntimeException("Close statement fail.", e);
            }
        }
    }

    /**
     * 关闭连接对象
     */
    private void close() {
        if(connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException("Close connection fail.", e);
            }
        }
    }
}

sql执行器(查询部分)

结果集中的数据是一个类似于表格的数据
如何处理结果集,有两类方式:

  1. 单行处理器
  2. 多行处理器

单行处理器是对单独一行数据进行处理成对应的类型。
而多行处理器是循环遍历结果集,每遍历结果集的一行就调用对应的单行处理器,并返回结果到集合中。

行处理器接口

无规矩不成方圆

行处理器接口定义了一种规范,用于处理不同类型的数据行。这个接口可以有多个实现类,每个实现类负责处理特定类型的数据行。通过使用接口,可以达到以下目标:

  1. 规范性:接口定义了一组必须实现的方法,确保了所有行处理器都有相同的处理方式,从而提高了代码的规范性。

  2. 可扩展性:当需要处理新类型的数据行时,只需创建一个新的实现类,并实现接口定义的方法,而不需要修改现有的代码。这样可以轻松地扩展系统功能。

  3. 可维护性:由于所有的行处理器都遵循相同的规范,因此维护和修改代码变得更加容易。如果需要修改处理逻辑,只需修改相应的实现类,而不会影响其他部分的代码。

  4. 代码复用:可以在不同的地方使用相同的行处理器接口,从而实现代码的复用。这样可以避免重复编写相似的处理逻辑。

  5. 降低耦合度:接口将行处理器的定义与具体的实现分离开来,降低了不同部分之间的耦合度。这使得系统更加灵活,易于维护和修改。

综上所述,行处理器接口是一种非常有用的设计模式,它可以帮助我们管理和处理不同类型的数据行,提高了代码的质量和可维护性。它强调了面向接口编程的重要性,以确保代码具有良好的可扩展性和可维护性。

duUtils 案例
public static Object toColumn(ResultSet resultSet, int columnIndex) throws SQLException {
    return resultSet.getObject(columnIndex);
}

Object[]行处理器

public static Object[] taArray(ResultSet resultSet) throws SQLException {
    // 先得到表的总列数(通过元数据获取)
    int count = resultSet.getMetaData().getColumnCount();
    // 创建Object[]数组,数组的长度为列的总数
    Object[] objects = new Object[count];
    for (int i = 0; i < count; i++) {
        objects[i] = resultSet.getObject(i + 1);
    }
    return objects;
}

Map行处理器

public static Map<String, Object> toMap(ResultSet resultSet) throws SQLException {
    // 先得到表的元数据
    ResultSetMetaData metaData = resultSet.getMetaData();
    Map<String, Object> map = new HashMap<>();
    for (int i = 1; i <= metaData.getColumnCount(); i++) {
        map.put(metaData.getColumnLabel(i), resultSet.getObject(i));
    }
    return map;
}

Column行处理器

由于handler方法只能传入结果集,但是需要columnIndex判断获取的下标,所以需要构造函数传入columnIndex

Column处理器

public class ColumnHandler<T> implements ResultSetHandler<T> {
    private int columnIndex;

    public ColumnHandler(int columnIndex) {
        this.columnIndex = columnIndex;
    }

    @Override
    public T handler(ResultSet resultSet) throws SQLException {
        return resultSet.next() ? (T) RowProcessor.toColumn(resultSet, columnIndex) : null;
    }
}

javaBean行处理器特别篇

使用反射+Map实现

大致思路(流程):

  1. 获取并写入实体类的字段集合<注解值(没有注解为字段名),字段>
  2. 得到表的元数据(用于获取总列数和列名)
  3. 接收数据转换成合适的格式并返回
public static <T> T toBean(ResultSet resultSet, Class<T> clazz) throws SQLException {
        /**
        * 获取并写入实体类的字段集合<注解值(没有注解为字段名),字段>
        **/
        // 创建实体类的字段集合
        Map<String, Field> map = new HashMap<>();
        // 遍历实体类的所有字段并存储到Map中
        for (Field field : clazz.getDeclaredFields()) {
            // 打开访问权限(不推荐,后面使用内省)
            field.setAccessible(true);
            // 判断是否有注解
            if (field.isAnnotationPresent(Column.class)){
                // 将<注解值,字段>传入Map中
                map.put(field.getAnnotation(Column.class).value(),field);
            }else{
                // 将<字段名,字段>传入Map中
                map.put(field.getName(), field);
            }
        }

        /**
        * 得到表的元数据(用于获取总列数和列名)
        **/
        ResultSetMetaData metaData = resultSet.getMetaData();

        /**
        * 接收数据转换成合适的格式并返回
        **/
        try {
            // 创建用于接收数据的对象
            T t = clazz.newInstance();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                // 获取列名
                String name = metaData.getColumnLabel(i);
                // 判断字段集合是否有对应的字段
                if (map.containsKey(name)) {
                    // 获取字段
                    Field field = map.get(name);
                    // 通过字段写入接收数据的对象
                    field.set(t, resultSet.getObject(i));
                }
            }
            return t;
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

使用内省

其中为什么会把一些代码封装成方法:

  1. 单一原则
  2. 把可能会发生异常的代码提取到方法中运行,把编译时异常转换成运行时异常

大致思路(流程):

  1. 创建对象

  2. 通过内省获取属性描述器数组

  3. 获取结果集元数据

  4. 遍历结果集列

  5. 获取列名(优先别名)

  6. 遍历属性描述器数组

  7. 根据属性描述器获取字段名称或者注解,并且判断是否和表的列名相符

  8. 通过结果集获取数据并且进行类型转换(如果使用反射就不需要类型转换,但是需要打开权限开关)

  9. 获取并调用set方法进行赋值操作

  10. 返回结果

public class BeanProcessor {

    /**
     * 根据结果集创建Bean实例
     */
    public Object createBean(ResultSet resultSet, Class<?> clazz) throws SQLException {
        // 1. 创建对象
        Object object = newInstance(clazz);
        // 2. 通过内省获取属性描述器数组
        PropertyDescriptor[] propertyDescriptors = propertyDescriptors(clazz);
        // 3. 获取结果集元数据
        ResultSetMetaData metaData = resultSet.getMetaData();
        // 4. 遍历结果集列
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            // 5. 获取列名(优先别名)
            String columnLabel = metaData.getColumnLabel(i);
            // 6. 遍历属性描述器数组
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                // 7. 根据属性描述器获取字段名称或者注解,并且判断是否和表的列名相符
                if (hasColumnLabel(columnLabel, propertyDescriptor, clazz)) {
                    // 8. 通过结果集获取数据并且进行类型转换
                    Object value = processColumn(propertyDescriptor, columnLabel, resultSet);
                    // 9. 获取并调用set方法进行赋值操作
                    callSetter(propertyDescriptor, object, value);
                }
            }
        }
        // 10. 返回结果
        return object;
    }

    /**
     * 根据Class对象创建实例
     *
     * @param clazz javaBean类型
     * @return javaBean对象
     */
    private Object newInstance(Class<?> clazz) {
        try {
            // 获取构造方法并且创建对象
            return clazz.getConstructor().newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 通过内省获取属性描述器数组
     *
     * @param clazz javaBean类型
     * @return 属性描述器数组
     */
    private PropertyDescriptor[] propertyDescriptors(Class<?> clazz) {
        try {
            // Introspector类为工具提供了一种了解目标Java Bean所支持的属性、事件和方法的标准方法。
            // 返回BeanInfo类实例,包含实体的所有信息(方法、属性、事件和其他特性)
            BeanInfo beanInfo = Introspector.getBeanInfo(clazz,Object.class);
            // 返回bean的所有属性的描述符
            return beanInfo.getPropertyDescriptors();
        } catch (IntrospectionException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据属性描述器获取字段名称或者注解,并且判断是否和表的列名相符(根据属性描述器解析出表的列名)
     *
     * @param columnLabel        表的列名
     * @param propertyDescriptor 属性描述器:获取字段名称或者注解
     * @param clazz              javaBean类型
     * @return 判断是否和表的列名相符
     */
    private boolean hasColumnLabel(String columnLabel, PropertyDescriptor propertyDescriptor, Class<?> clazz) {
        try {
            // 字段名称
            String fieldName = propertyDescriptor.getName();
            // 通过字段名称获取字段
            Field field = clazz.getDeclaredField(fieldName);
            // 判断是否有对应注解
            if (field.isAnnotationPresent(Column.class)) {
                // 注解的值代替字段名称
                fieldName = field.getAnnotation(Column.class).value();
            }
            // 将此字符串与另一个字符串进行比较,忽略大小写考虑
            return fieldName.equalsIgnoreCase(columnLabel);
        } catch (NoSuchFieldException e) {
//            throw new RuntimeException(e);
            return false;
        }
    }

    /**
     * 通过结果集获取数据并且进行类型转换
     *
     * @param propertyDescriptor 属性描述器:用于获取数据类型进行类型转换
     * @param columnLabel        列名:根据特定的列名获取数据
     * @param resultSet          结果集:用于获取数据
     * @return 返回类型转换后的数据
     */
    private Object processColumn(PropertyDescriptor propertyDescriptor, String columnLabel, ResultSet resultSet) {
        try {
            // 获取数据类型
            Class<?> fieldType = propertyDescriptor.getPropertyType();
            // 根据特定的列名获取数据
            Object value = resultSet.getObject(columnLabel);
            // 如果字段不是基本数据类型,同时数据为空(因为基本数据类型不允许为空,如果传入数据为空会报错)
            if (!fieldType.isPrimitive() && value == null) {
                return null;
            }
            // 类型转换链
            return BeanUtils.toBean(propertyDescriptor,value);
//            return null;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取并调用set方法进行赋值操作
     *
     * @param propertyDescriptor 属性描述器:用于获取set方法进行赋值操作
     * @param object             需要赋值的对象
     * @param value              赋值的值
     */
    private void callSetter(PropertyDescriptor propertyDescriptor, Object object, Object value) {
        try {
            // 获取并调用set方法进行赋值操作
            propertyDescriptor.getWriteMethod().invoke(object, value);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
}

类型转换链

多行处理抽象类

首先多行处理抽象类需要继承负责处理的接口,并确定接口的类型为List<T> 抽象类定义为T

/**
 * 模板方法模式,把要变化的放到子类中实现,把固定重复的放到父类中反复调用
 * 实现ResultSetHandler<List<T>>,这样handler返回类型为List
 * 同时handler负责接收数据
 * 创建一个抽象方法handleRow负责让子类重写并调用子类对应的单行处理器
 *
 * @param <T>
 */
public abstract class AbstractListHandler<T> implements ResultSetHandler<List<T>> {
    @Override
    public List<T> handler(ResultSet resultSet) throws SQLException {
        // 创建集合用于接收数据
        List<T> list = new ArrayList<>();
        // 遍历结果集
        while (resultSet.next()) {
            // 调用子类实现的单行查询的抽象方法,将数据传入集合中
            list.add(handleRow(resultSet));
        }
        return list;
    }

    /**
     * 单行查询的抽象方法提供给子类实现
     * @param resultSet
     * @return
     * @throws SQLException
     */
    public abstract T handleRow(ResultSet resultSet) throws SQLException;
}

javaBean多行处理器BeanListHandler

其他的倒推
public class BeanListHandler<T> extends AbstractListHandler<T>{
    private Class<T> clazz;

    public BeanListHandler(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public T handleRow(ResultSet resultSet) throws SQLException {
        return RowProcessor.toBean(resultSet,clazz);
    }
}

过时

面向对象oo

封装

BeanProcessor:根据结果集创建Bean实例中,通过方法的封装把编译时异常转换成运行时异常。因为编译时异常(又称已检查异常)调用时需要开发者去处理,如果转换成运行时异常调用时不需要抛出异常

继承

一个结果集处理的接口,所有结果集

设计原则(单一职责、开闭原则、依赖倒置、里氏替换、接口隔离)

ResultSetHandler:结果集处理器接口以及实现类xxxHandler单行处理器

单一职责

实现类xxxHandler单行处理器只负责一种数据处理

开闭原则-面向扩展开放,面向修改关闭

每需要一种处理器时,根据需要实现ResultSetHandler接口

依赖倒置-面向抽象编程

SqlExecutor:SQL执行器中依赖的是ResultSetHandler接口,不是具体的实现类

里氏替换-父类出现的地方一定可以用子类的对象替换,子类不可以改变父类的行为和状态

SqlExecutor:SQL执行器中ResultSetHandler父类出现的地方一定可以用实现类的对象替换

接口隔离-不需要强制实现不必要的方法

ResultSetHandler接口只关注结果集的处理,而 AbstractListHandler 抽象类则扩展了这个接口。

  1. ResultSetHandler 接口:专注于将单行结果集转化为 Java 对象的功能,只有一个方法 T handle(ResultSet rs)
  2. AbstractListHandler:扩展了 ResultSetHandler 接口,但它是一个抽象类,专注于将多行结果集转化为列表(List)的功能,并在内部实现了 handle 方法。如果需要自定义列表的构建逻辑,子类只需重写 handleRow 方法。

这两者的分工明确,符合接口隔离原则,客户端可以根据需要选择实现 ResultSetHandler 接口或扩展 AbstractListHandler 类,而不需要强制实现不必要的方法。

设计模式

责任链

TypeSwitchChain责任链:负责遍历TypeSwitch接口的实现类
TypeSwitch接口:把判断和转换拆分开成两个方法,先判断通过后执行转换

模板方法模式

把要变化的放到子类中实现,把固定重复的放到父类中反复调用
AbstractListHandler抽象类:
handler方法(重复):负责将单行数据转换成多行数据
handleRow抽象方法(变化):单行查询的抽象方法提供给子类实现

泛型

泛型的类型确定

实现类需要返回List<T>,但是接口的handle抽象方法的返回类型是接口的泛型类型。
只需要在实现接口时把接口的泛型定义为List<T>,那么接口的handle抽象方法的返回类型就是List<T>

反射

内省

BeanProcessor
propertyDescriptors:通过内省获取属性描述器数组
hasColumnLabel:根据属性描述器获取字段名称或者注解,并且判断是否和表的列名相符(根据属性描述器解析出表的列名)
callSetter:获取并调用set方法进行赋值操作

自定义注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value();
}

用于解决字段名和属性名不一致的问题,当不一致时只需要把该注解放到对应的属性时并且注解的值是字段名。

反射中的作用

使用反射判断属性是否有对应注解,来判断是否有不一致的问题

SPI机制

类型转换责任链TypeSwitchChain
使用 ServiceLoader 类加载了服务提供者 TypeSwitch 的实现类。ServiceLoader 是 Java 标准库中用于加载服务提供者的工具类,它会查找配置文件 META-INF/services/xxx,其中 xxx 是服务接口的全限定名,找到配置文件后会加载其中的实现类。

感谢和总结

只要打好java基础,不管是看还是(根据思想)实现都DButils不难