一、QueryRunner类,利用可插拨的策略执行SQL查询来处理ResultSets,大致看了一下,该类的重载方法确实有够多。来一类一类的解决:
1)构造器有多个重载方法,有必要说明解析一下,现列出部分代码:
- /**
- *QueryRunner 默认构造器
- */
- public QueryRunner() {
- super();
- ds = null;
- }
- /**
- * 允许Oracle驱动程序的解决方案
- * @param pmdKnownBroken 如果是Oracle drivers,则不支持 ParameterMetaData.getParameterType(int)这个方法;
- * if pmdKnownBroken参数设置为true,则我们不做
- * ParameterMetaData.getParameterType(int)方法;
- * 如果为false,那将会尝试获取,如果不支持有异常抛出,则不再使用
- */
- public QueryRunner(boolean pmdKnownBroken) {
- super();
- this.pmdKnownBroken = pmdKnownBroken;
- ds = null;
- }
- /**
- * QueryRunner构造器,Oracle drivers的解决方案. 通过DataSource
- * 获取数据源连接
- * @param ds 数据源,用于获取数据连接Connection
- */
- public QueryRunner(DataSource ds) {
- super();
- this.ds = ds;
- }
- /**
- * QueryRunner构造器,Oracle drivers的解决方案. 通过DataSource
- * 获取数据源连接
- * @param ds 数据源,用于获取数据连接Connection.
- * @param pmdKnownBroken 如果是Oracle drivers,则不支持 ParameterMetaData.getParameterType(int)这个方法;
- * if pmdKnownBroken参数设置为true,则我们不做
- * ParameterMetaData.getParameterType(int)方法;
- * 如果为false,那将会尝试获取,如果不支持有异常抛出,则不再使用
- */
- public QueryRunner(DataSource ds, boolean pmdKnownBroken) {
- super();
- this.pmdKnownBroken = pmdKnownBroken;
- this.ds = ds;
- }
本身这个构造器并没有什么,关键是boolean类型的pmdKnownBroken和DataSource类型的ds。DataSource呢,很明显,是通过它来获取数据库连接,如何程式获取DataSource对象,这得需要借助于commons里的dbcp和pool这两个组件。具体的如何获取,可以参考DBCP组件的官方示例程序,具体网址如下:
http://svn.apache.org/viewvc/commons/proper/dbcp/trunk/doc/BasicDataSourceExample.java?view=markup
这个类中的静态方法setupDataSource就是用来获取数据源的,说个题外话,DBCP和pool这两个组件对于数据源的管理,可谓是鼎鼎大名啊,Spring的数据源管理也是基于该组件,当然了还有另外一个数据源C3P0。关于数据源这一知识点,各位有兴趣的朋友可以参考在下写的“Spring 数据源不同配置 ”,扯远了啊,呵~咱接着说pmdKnownBroken这个变量,虽然现在说起来可能感觉有些抽象。源码的解释是这样的:Oracle的驱动程序不支持ParameterMetaData.getParameterType方法,如果pmdKnownBroken设置为true,则我们甚至不进行尝试处理,而false,我们则会尝试着使用ParameterMetaData.getParameterType方法,如果有异常抛出,则不再使用。
2)一般来说数据库操作的时候,总是会顺口溜似的:增删改查,所以我们先从QueryRunner类的SQL增加操作说起,看了一下这个类的大概实现,实际上呢,update方法它不仅充当了SQL增加操作,同时也充当了更新和删除的操作,所以,一并了解了吧:
- /**
- * 执行一个没有参数的SQL插入、更新或者删除操作
- * Execute an SQL INSERT, UPDATE, or DELETE query without replacement
- * parameters.
- *
- * @param conn 数据连接The connection to use to run the query.
- * @param sql 要执行的SQL语句The SQL to execute.
- * @return 更新的行数The number of rows updated.
- * @throws SQLException 数据库访问异常if a database access error occurs
- */
- public int update(Connection conn, String sql) throws SQLException {
- return this.update(conn, sql, (Object[]) null);
- }
- /**
- * 执行只有一个参数的SQL插入、修改或者删除操作
- * @param conn 执行查询的数据库连接
- * @param sql 要执行的SQL语句
- * @return 更新的行数
- * @throws SQLException 数据库访问异常
- */
- public int update(Connection conn, String sql, Object param)
- throws SQLException {
- return this.update(conn, sql, new Object[] { param });
- }
- /**
- * 执行指定没有参数的插入、修改或者删除的SQL语句。
- * 数据连接通过DataSource(在构造器中指定)获取。
- * 此连接必须在自动提交模式,否则会导致更新操作不会保存。
- * @param sql 要执行的SQL语句
- * @throws SQLException 数据库访问异常
- * @return 更新的行数
- */
- public int update(String sql) throws SQLException {
- return this.update(sql, (Object[]) null);
- }
- /**
- * 执行指定只有一个参数的插入、修改或者删除的SQL语句。
- * 数据连接通过DataSource(在构造器中指定)获取。
- * 此连接必须在自动提交模式,否则会导致更新操作不会保存。
- *
- * @param sql 要执行的SQL语句
- * @param param 参数
- * @throws SQLException 数据库访问异常
- * @return 更新的行数
- */
- public int update(String sql, Object param) throws SQLException {
- return this.update(sql, new Object[] { param });
- }
- /**
- * 执行指定的插入、修改或者删除的SQL语句。
- * 数据连接通过DataSource(在构造器中指定)获取。
- * 此连接必须在自动提交模式,否则会导致更新操作不会保存。
- *
- * @param sql 要执行的SQL语句
- * @param params 初始化PreparedStatement参数
- * @throws SQLException 数据库访问异常
- * @return 更新的行数
- */
- public int update(String sql, Object... params) throws SQLException {
- Connection conn = this.prepareConnection();
- try {
- return this.update(conn, sql, params);
- } finally {
- close(conn);
- }
- }
真是巨多啊!我在每个方法上,都将源码上面的一些说明解释成了中文,各位有兴趣的可以看看。
挑两个具有代表性的方法来读一下:
2-1)获取数据库连接,这个呢,源码上面的说明也说了,是通过DataSource来获取的,具体看看prepareConnection()这个方法:
- protected Connection prepareConnection() throws SQLException {
- if(this.getDataSource() == null) {
- throw new SQLException("QueryRunner requires a DataSource to be " +
- "invoked in this way, or a Connection should be passed in");
- }
- return this.getDataSource().getConnection();
- }
这个方法比较的简单,首先是获取数据源实例,如果数据源为空,则抛出异常:必须要有一个DataSource,然后呢,就会获取一个Connection实例返回。这个DataSource实例呢,是在实例化的时候指定的,当然了,我们也可以子类重写这个prepareConnection方法,来实现一个指定的获取数据库连接的方法。
2-2)
- /**
- * 执行一个SQL插入、更新或者删除操作
- * @param conn 执行查询的数据库连接
- * @param sql 要执行的SQL语句
- * @return 更新的行数
- * @throws SQLException 数据库访问异常
- */
- ublic int update(Connection conn, String sql, Object... params)
- throws SQLException {
- PreparedStatement stmt = null;
- int rows = 0;
- try {
- stmt = this.prepareStatement(conn, sql);//通过Connection和sql获取PreparedStatement实例
- this.fillStatement(stmt, params);
- rows = stmt.executeUpdate();
- } catch (SQLException e) {
- this.rethrow(e, sql, params);
- } finally {
- close(stmt);
- }
- return rows;
来,一步一步的执行这个核心方法,首先,通过prepareStatement这个方法,传入数据库连接和SQL这两个参数
得到一个PreparedStatement对象实例;然后通过fillStatement方法填充参数值,看看具体实现:
- /**
- * 通过指定对象填充PreparedStatement的代替参数。
- * @param stmt PreparedStatement to fill
- * @param params 查询替代参数; null也是有效的参数。
- * @throws SQLException 数据库访问异常
- */
- public void fillStatement(PreparedStatement stmt, Object... params)
- throws SQLException {
- if (params == null) {//参数为空,则返回
- return;
- }
- ParameterMetaData pmd = null;
- if (!pmdKnownBroken){//false,we try it
- pmd = stmt.getParameterMetaData();//获取关于PreparedStatement 对象中参数的类型和属性信息的对象
- if (pmd.getParameterCount() < params.length) {//如果PreparedStatement需要的参数数量少于指定参数数量,则抛出数量不匹配异常
- throw new SQLException("Too many parameters: expected "
- + pmd.getParameterCount() + ", was given " + params.length);
- }
- }
- //循环参数
- for (int i = 0; i < params.length; i++) {
- if (params[i] != null) {//如果指定的参数不为空,则指定参数值
- stmt.setObject(i + 1, params[i]);
- } else {
- // VARCHAR类型可以与许多的驱动工作,而不管真实的列类型.
- // 奇怪的是,NULL和OTHER与Oracle的驱动不能工作.
- // VARCHAR works with many drivers regardless
- // of the actual column type. Oddly, NULL and
- // OTHER don't work with Oracle's drivers.
- int sqlType = Types.VARCHAR;
- if (!pmdKnownBroken) {//false
- try {
- sqlType = pmd.getParameterType(i + 1);//获取特定的参数类型
- } catch (SQLException e) {
- pmdKnownBroken = true;//如果不支持getParameterType方法,则不再尝试使用
- }
- }
- stmt.setNull(i + 1, sqlType);//为特定类型赋空值
- }
- }
- }
我已经对这个方法做了一些必要的说明,实际上呢,最需要强调的,就是pmdKnownBroken参数以及Oracle驱动的关系,pmdKnownBroken这个参数呢,我们已经在构造器那一块说过了,它实际上用于区别Oracle驱动, 说是Oracle驱动不支持getParameterType方法,我是不清楚了,没有使用过,所以没有发言权,但我想这个问题应该会有所解决.另一个批量查询方法batch,主要方法是与update方法类似的,故不再解析。现在呢,主要的方法体功能已经了解完了。
3)接下来呢,理论上应该是SQL的查询方法解析了,但我看了一下query方法,需要说明的,我们都已经在之前的update方法里拜读过了,唯一不同的是就是多了一个ResultSetHandler<T>参数,之前呢,我有说过ResultSetHandler这个接口,它通过调用handler方法处理ResultSet结果集完成指定类型的转换,本身dbUtils组件呢,提供了众多的ResultSetHandler实现类,它们都位于org.apache.commons.dbutils.handlers的包下,我会在以后的章节中具体解析。
4)具体来说明一下QueryRunner这个类中的fillStatementWithBean这个方法,在整个组件中暂未用到,但,我想在面向对象的Java开发中,通过指定的bean实例,为SQL语句参数指定bean变量值肯定是会广泛应用的,也就是JavaBean与特定数据表的映射了。Hibernate、JPA等框架能够自动完成对象与关系型数据库的映射,底层的实现也诸如此类吧!
- /**
- * 根据bean的属性值填充PerparedStatement的参数
- * @param stmt
- * 待填充值的PreparedStatement
- * @param bean
- * JavaBean对象
- * @param propertyNames
- * 有序的属性名称数组(这些名字应该有
- * getters/setters方法匹配);这个属性数组顺序与statement的插入参数顺序匹配
- * @throws SQLException
- * 数据访问异常
- */
- public void fillStatementWithBean(PreparedStatement stmt, Object bean,
- String... propertyNames) throws SQLException {
- PropertyDescriptor[] descriptors;
- try {
- descriptors = Introspector.getBeanInfo(bean.getClass())
- .getPropertyDescriptors(); //4-1
- }catch(IntrospectionException e){
- throw new RuntimeException("Couldn't introspect bean " + bean.getClass().toString(), e);
- }
- PropertyDescriptor[] sorted = new PropertyDescriptor[propertyNames.length];//4-2
- //参数名与Bean的属性进行比较,确保属性的完整性
- //确保为每个属性名找到在bean中对应的PropertyDescriptor实例
- for (int i = 0; i < propertyNames.length; i++) {
- String propertyName = propertyNames[i];
- if (propertyName == null) {//属性列表里的属性不能为空
- throw new NullPointerException("propertyName can't be null: " + i);
- }
- boolean found = false;
- for (int j = 0; j < descriptors.length; j++) {//4-3
- PropertyDescriptor descriptor = descriptors[j];
- if (propertyName.equals(descriptor.getName())) {
- sorted[i] = descriptor;//此属性在bean中存在,赋于PropertyDescriptor实例
- found = true;
- break;
- }
- }
- if (!found) {
- throw new RuntimeException("Couldn't find bean property: "
- + bean.getClass() + " " + propertyName);
- }
- }
- fillStatementWithBean(stmt, bean, sorted);//4-4
- }
这个方法做的事情是这样的,就是通过指定一个Javabean实例和一个PreparedStatement的参数名数组为PreparedStatement填充对应的参数值。当然了,这些参数名都是JavaBean实例里的属性了。来看看具体的实现过程:
4-1)通过内省机制获取指定bean实例的PropertyDescriptor[]数组,这样呢,就有了对bean属性的直接操作能力了;
4-2)根据指定的参数名数组propertyNames,实例化一个PropertyDescriptor[]数组,这个数组,主要的呢,就是存储PreparedStatement指定的参数名对应bean实例中的属性的PropertyDescriptor对象;
4-3)循环4-1)中Bean实例的PropertyDescriptor数组,如果通过PropertyDescriptor实例获取的属性名与指定的propertyNames相同,则将对应的PeropertyDescriptor实例赋给4-2)中声明的数组;
4-4)4-1~4-3,这个应该算是真正实现PreparedStatement赋值的前期初始化工作吧,这个fillStatementWithBean的重载方法通过指定stmt中参数的bean实例对应的属性描述数组,下面来看看具体的实现代码吧:
- /**
- *
- * 根据bean的属性值填充PerparedStatement的参数
- * @param stmt
- * 待填充值的PreparedStatement
- * @param bean
- * JavaBean对象
- * @param properties
- * 指定顺序数组;与PreparedStatement的参数顺序一致
- * @throws SQLException
- * 数据访问异常
- */
- public void fillStatementWithBean(PreparedStatement stmt, Object bean,
- PropertyDescriptor[] properties) throws SQLException {
- Object[] params = new Object[properties.length];//属性值的数组
- for (int i = 0; i < properties.length; i++) {
- PropertyDescriptor property = properties[i];
- Object value = null;
- Method method = property.getReadMethod();//获取属性对应的getter方法
- if (method == null) {
- throw new RuntimeException("No read method for bean property "
- + bean.getClass() + " " + property.getName());
- }
- try {
- value = method.invoke(bean, new Object[0]);//通过反射调用获取属性值
- } catch (InvocationTargetException e) {
- throw new RuntimeException("Couldn't invoke method: " + method, e);
- } catch (IllegalArgumentException e) {
- throw new RuntimeException("Couldn't invoke method with 0 arguments: " + method, e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Couldn't invoke method: " + method, e);
- }
- params[i] = value;//设置属性值
- }
- fillStatement(stmt, params);
- }
这个方法我还真的有点懒得再说明下去了,浪费太多面板罗,OK,这个类的解析到此为此吧。
二、QueryLoader类,是一个从一个文件加载查询到一个Map的简单的类。然后,当需要的时候,你从Map中选择一些查询。当然了,这个方法的实现是比较简单的。现看看文件载入的源代码:
- /**
- * 载入一个查询命名和SQL值映射的Map集合.
- * 此Map被缓存以便以后相同路径的请求可以返回被缓存的Map
- * Loads a Map of query names to SQL values. The Maps are cached so a
- * subsequent request to load queries from the same path will return
- * the cached Map.
- *
- * @param path The path that the ClassLoader will use to find the file.
- * ClassLoader通过path查找文件
- *
- * This is <strong>not</strong> a file system path. If you had a jarred
- * Queries.properties file in the com.yourcorp.app.jdbc package you would
- * pass "/com/yourcorp/app/jdbc/Queries.properties" to this method.
- * 这不是一个文件系统路径。如果你有一个Queries.properties文件在com.yourcorp.app.jdbc这个包下,
- * 那么你应该传递"/com/yourcorp/app/jdbc/Queries.properties"参数到这个方法.
- *
- * @throws IOException if a file access error occurs
- * @throws IllegalArgumentException if the ClassLoader can't find a file at
- * the given path.
- * @return Map of query names to SQL values
- */
- public synchronized Map<String,String> load(String path) throws IOException {
- Map<String,String> queryMap = (Map<String,String>) this.queries.get(path);
- if (queryMap == null) {
- queryMap = this.loadQueries(path);
- this.queries.put(path, queryMap);
- }
- return queryMap;
- }
这个方法加入了同步锁机制,所以是线程安全的,这个方法需要注意的一点就是传入的路径,因为是通过ClassLoader载入,所以,传入的路径是绝对路径名。首先呢,先从本地的Map集合queries拿到路径名里对应的集合,如果为空,则说明没有缓存对不对,OK,没有就加呗,来loadQueries方法:
- /**
- * Loads a set of named queries into a Map object. This implementation
- * reads a properties file at the given path.
- *
- * 加载命名查询集到一个Map对象中.
- * 这个实现用于读取给定路径的Properties文件
- *
- * @param path The path that the ClassLoader will use to find the file.
- * ClassLoader使用指定的path去查找file
- * @throws IOException file访问异常
- * @throws IllegalArgumentException ClassLoader查找不到指定路径的文件
- * @since DbUtils 1.1
- * @return Map of query names to SQL values 查询名称到SQL值的映射集合
- */
- @SuppressWarnings("unchecked")
- protected Map<String,String> loadQueries(String path) throws IOException {
- // Findbugs flags getClass().getResource as a bad practice; maybe we should change the API?
- InputStream in = getClass().getResourceAsStream(path);//获取指定文件的流对象
- if (in == null) {
- throw new IllegalArgumentException(path + " not found.");
- }
- Properties props = new Properties();
- props.load(in);
- // Copy to HashMap for better performance
- return new HashMap(props);
- }
哝,有没有?!通过ClassLoader载入指定文件流对象,如果为空,则会抛出找不到文件的异常。否则呢,载入Properties文件并以HashMap返回。
载入了未缓存的properties文件,那么,存一个:
- this.queries.put(path, queryMap);
完成了载入、缓存,缓存这个东西呢是要占内存的,所以呢,不要缓存太多或者大的对象,除非有必要,建议各位用完就remove掉:
- /**
- * Removes the queries for the given path from the cache.
- * 从缓存中删除指定的路径
- * @param path The path that the queries were loaded from.
- * queries载入的路径
- */
- public synchronized void unload(String path){
- this.queries.remove(path);
- }
好了,DbUtils这两个类的解析就到此为此吧,回头看看,发现DbUtils组件的一些主要的类都已经解析完成了,继续努力!