【源码阅读】dbutil包中BasicRowProcessor内部类CaseInsensiti...

本文就是对我问的问题:http://www.oschina.net/question/1029535_127629?p=1#AnchorAnswer597790 的一个解释。

dbutil包为我们提供了BasicRowProcessor类,用来将查询结果集进行封装。其中有一个toMap方法,该方法的作用就是将结果集转化成一个Map,结果集中的字段名对应map的key,字段值对应map中的value。该方法的独特之处在于:获取的map的key是大小写不敏感的。

先看一下toMap方法:

/**
     * Convert a <code>ResultSet</code> row into a <code>Map</code>.  This
     * implementation returns a <code>Map</code> with case insensitive column
     * names as keys.  Calls to <code>map.get("COL")</code> and
     * <code>map.get("col")</code> return the same value.
     * @see org.apache.commons.dbutils.RowProcessor#toMap(java.sql.ResultSet)
     * @param rs ResultSet that supplies the map data
     * @throws SQLException if a database access error occurs
     * @return the newly created Map
     */
    @Override
    public Map<String, Object> toMap(ResultSet rs) throws SQLException {
        Map<String, Object> result = new CaseInsensitiveHashMap();
        ResultSetMetaData rsmd = rs.getMetaData();
        int cols = rsmd.getColumnCount();

        for (int i = 1; i <= cols; i++) {
            result.put(rsmd.getColumnName(i), rs.getObject(i));
        }

        return result;
    }
该方法里用到了一个类CaseInsensitiveHashMap,在没看该方法的实现之前,我以为它大致应该是这个样子的:
//下面是按照我的思路实现的
public static class CaseInsensitiveHashMap extends HashMap<String, String> {

		private static final long serialVersionUID = -2127888215578997896L;

		@Override
		public String get(Object key) {
			return super.get(key.toString().toLowerCase(Locale.ENGLISH));
		}

		@Override
		public boolean containsKey(Object key) {
			return super.containsKey(key.toString().toLowerCase(Locale.ENGLISH));
		}

		@Override
		public String put(String key, String value) {
			return super.put(key.toString().toLowerCase(Locale.ENGLISH), value);
		}

		// 其他省略。。

	}
思路很简单,就是在put/get/containsKey方法的时候,将传进来的key值统一转化为小写,这样不就能达到大小写不敏感了吗?没错,这样做是对的!而且apache官方的代码中,最开始也是这样做的!(后面我会给出官方最开始的代码)

但是后来我看了一下apache的代码,顿时让我惊呆了,好麻烦,他们并没有按照我的思路实现,他们的思路是:

首先在CaseInsensitiveHashMap中定义一个lowerCaseMap,它是一个普通的hashMap:

/**
         * The internal mapping from lowercase keys to the real keys.
         *
         * <p>
         * Any query operation using the key
         * ({@link #get(Object)}, {@link #containsKey(Object)})
         * is done in three steps:
         * <ul>
         * <li>convert the parameter key to lower case</li>
         * <li>get the actual key that corresponds to the lower case key</li>
         * <li>query the map with the actual key</li>
         * </ul>
         * </p>
         */
        private final Map<String, String> lowerCaseMap = new HashMap<String, String>();
它的作用是存放真实的字段名与其转化为小写后的字段名的对应关系,举个例子来讲就是:如果字段名为tbl_myName,那么lowerCaseMap中就会保存一个key为tbl_myname(小写),value为tbl_myName的键值对。

然后看看他们的其他方法实现:

(1)put方法:

/** {@inheritDoc} */
        @Override
        public Object put(String key, Object value) {
            /*
             * In order to keep the map and lowerCaseMap synchronized,
             * we have to remove the old mapping before putting the
             * new one. Indeed, oldKey and key are not necessaliry equals.
             * (That's why we call super.remove(oldKey) and not just
             * super.put(key, value))
             */
            Object oldKey = lowerCaseMap.put(key.toLowerCase(Locale.ENGLISH), key);
            Object oldValue = super.remove(oldKey);
            super.put(key, value);
            return oldValue;
        }
(2)get方法:
/** {@inheritDoc} */
        @Override
        public Object get(Object key) {
            Object realKey = lowerCaseMap.get(key.toString().toLowerCase(Locale.ENGLISH));
            return super.get(realKey);
        }
其他方法与上面两个方法类似,不做介绍。

put方法很关键:它要负责维护两个Map,一个就是存放小写的字段名与字段值的map,通过这个super.put(key,value)实现,另一个就是要维护lowerCaseMap。

看到这里我就觉得很奇怪了,为什么要这么麻烦呢?用我的方式不也能达到同样的目的吗?干嘛还要在维护一个额外的map?.....

莫非人家的设计有问题?肯定不是,一定有原因,于是我就开始了探索。。。找啊找啊找。。。

找了半天,终于找到答案了:仔细看一下他们CaseInsensitiveHashMap的注释:

/**
     * A Map that converts all keys to lowercase Strings for case insensitive
     * lookups.  This is needed for the toMap() implementation because
     * databases don't consistently handle the casing of column names.
     *
     * <p>The keys are stored as they are given [BUG #DBUTILS-34], so we maintain
     * an internal mapping from lowercase keys to the real keys in order to
     * achieve the case insensitive lookup.
     *
     * <p>Note: This implementation does not allow <tt>null</tt>
     * for key, whereas {@link HashMap} does, because of the code:
     * <pre>
     * key.toString().toLowerCase()
     * </pre>
     */
    private static class CaseInsensitiveHashMap extends HashMap<String, Object> {

哦,原来是为了解决这个bug [BUG #DBUTILS-34],链接:https://issues.apache.org/jira/browse/DBUTILS-34

原来,如果采用我的那种实现思路,map里存放的key将全部是小写的字段名,这样的话,如果用户调用toMap方法获得了map,然后想获取数据库中真实字段名,这个时候问题就来了:我们把数据库中的真是字段名都转为了小写,那么用户怎么知道真实字段名是什么呢?肯定没办法知道啊。所以为了提供给用户更详细的信息,他们将代码改成了现在的模样:

添加一个lowerCaseMap来保存原始字段名和转换成小写后的字段名的对应关系,然后将真实的字段名和字段值作为键值对存放起来。每次get(key)的时候,先从lowerCaseMap获取到原始字段名,然后在调用super.get(原始字段名)获取值。

这样一来,用户获得Map后,如果使用Map.Entry<K,V>的Iterator,这时候,他们获得的Key值就是数据库中真实的字段名了~~~所以,既达到了忽略大小写的目的,又能提供给用户足够的信息,多么精彩的设计啊!

PS:我的解释可能不是特别清楚,但那个bug里已经讲得很明白了,大家可以自己看一下。

前面讲到过了,最初的时候apache也是按照我的思路实现的,可以看一下他们对CaseInsensitiveHashMap的patch记录:

说明:每行前面的减号表示为了改当前bug而去掉的代码(也就是最初的实现代码哦),加号表示新增代码!

Index: BasicRowProcessor.java
===================================================================
--- BasicRowProcessor.java	(revision 536476)
+++ BasicRowProcessor.java	(working copy)
@@ -144,10 +144,36 @@
      * A Map that converts all keys to lowercase Strings for case insensitive
      * lookups.  This is needed for the toMap() implementation because 
      * databases don't consistenly handle the casing of column names. 
+     * 
+     * <p>The keys are stored as they are given [BUG #DBUTILS-34], so we maintain
+     * an internal mapping from lowercase keys to the real keys in order to 
+     * achieve the case insensitive lookup.
+     * 
+     * <p>Note: This implementation does not allow <tt>null</tt>
+     * for key, whereas {@link HashMap} does, because of the code:
+     * <pre>
+     * key.toString().toLowerCase()
+     * </pre>
      */
     private static class CaseInsensitiveHashMap extends HashMap {
 
         /**
+         * The internal mapping from lowercase keys to the real keys.
+         * 
+         * <p>
+         * Any query operation using the key 
+         * ({@link #get(Object)}, {@link #containsKey(Object)})
+         * is done in three steps:
+         * <ul>
+         * <li>convert the parameter key to lower case</li>
+         * <li>get the actual key that corresponds to the lower case key</li>
+         * <li>query the map with the actual key</li>
+         * </ul>
+         * </p>
+         */
+        private Map lowerCaseMap = new HashMap();
+
+        /**
          * Required for serialization support.
          * 
          * @see java.io.Serializable
@@ -158,21 +184,37 @@
          * @see java.util.Map#containsKey(java.lang.Object)
          */
         public boolean containsKey(Object key) {
-            return super.containsKey(key.toString().toLowerCase());
+            Object realKey = lowerCaseMap.get(key.toString().toLowerCase());
+            return super.containsKey(realKey);
+            // Possible optimisation here:
+            // Since the lowerCaseMap contains a mapping for all the keys,
+            // we could just do this:
+            // return lowerCaseMap.containsKey(key.toString().toLowerCase());
         }
 
         /**
          * @see java.util.Map#get(java.lang.Object)
          */
         public Object get(Object key) {
-            return super.get(key.toString().toLowerCase());
+            Object realKey = lowerCaseMap.get(key.toString().toLowerCase());
+            return super.get(realKey);
         }
 
         /**
          * @see java.util.Map#put(java.lang.Object, java.lang.Object)
          */
         public Object put(Object key, Object value) {
-            return super.put(key.toString().toLowerCase(), value);
+            /*
+             * In order to keep the map and lowerCaseMap synchronized,
+             * we have to remove the old mapping before putting the 
+             * new one. Indeed, oldKey and key are not necessaliry equals.
+             * (That's why we call super.remove(oldKey) and not just
+             * super.put(key, value))
+             */
+            Object oldKey = lowerCaseMap.put(key.toString().toLowerCase(), key);
+            Object oldValue = super.remove(oldKey);
+            super.put(key, value);
+            return oldValue;
         }
 
         /**
@@ -191,7 +233,8 @@
          * @see java.util.Map#remove(java.lang.Object)
          */
         public Object remove(Object key) {
-            return super.remove(key.toString().toLowerCase());
+            Object realKey = lowerCaseMap.remove(key.toString().toLowerCase());
+            return super.remove(realKey);
         }
     }

怎么样,如果把前面有加号的行去掉,代码就跟我的实现是一样的啦~~~

下面来一点方法学,来记录一些新的学习方法:

1、事出无常必有妖:每一个不同寻常的地方一定是有原因的!我们要抓住这些不同寻常的细节,追其原因,这样才能找出真相!(如果我真的认为他们的实现是多此一举,而不继续追究,那么也就不能明白他们这样设计的真正原因了)

2、多看源码不仅能让你更透彻的理解一个类库的实现原理,而且在你使用的时候也能够更好的取舍!

3、没事去这些开源类库的jira看看,你会得到更多的灵感!

5、以后多写博客,因为很多时候自己心里明白,但是转化成语言却有一定困难,要多锻炼!!


你可能感兴趣的:(apache,源码,dbutil)