Mybatis源码研究之DynamicContext

DynamicContext为POJO对象Map化提供了很好的借鉴,抹平了访问POJO和Map对象的差异.

1. 由来

在Mybatis提供的动态SQL功能中, 作为底层支撑的关键类SqlNode接口.其签名如下:

package org.apache.ibatis.scripting.xmltags;
public interface SqlNode {
 		boolean apply(DynamicContext context);
}
  1. 被所有动态sql节点类(诸如ChooseSqlNode等)继承的接口. 作为参数的DynamicContext重要性就不言而喻了.
  2. 参数DynamicContext的作用就是为各个动态sql节点实现类(诸如ChooseSqlNode等)提供进行判断的上下信息.确保判断等操作的完整实现.
  3. 依然如一直强调的, Context非常类似《程序员修炼之道——从小工到专家》中"黑板"的概念.

2. DynamicContext

public class DynamicContext {
  // 在编写映射文件时, '${_parameter}','${_databaseId}'分别可以取到当前用户传入的参数, 以及当前执行的数据库类型
  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  public static final String DATABASE_ID_KEY = "_databaseId";

  static {
	// Mybatis中采用了Ognl来计算动态sql语句,DynamicContext类中的这个静态初始块,很好的说明了这一点
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }
	
  // 构造函数, 对传入的parameterObject对象进行“map”化处理; 
  // 也就是说,你传入的pojo对象,会被当作一个键值对数据来源来进行处理,读取这个pojo对象的接口,依然是Map对象(依然是以Map接口方式来进行读取)。
  // 
  public DynamicContext(Configuration configuration, Object parameterObject) {
	/*
	 * 在DynamicContext的构造函数中,可以看到:
	 *    1. 根据传入的参数对象是否为Map类型,有两个不同构造ContextMap的方式。
	 *    2. 而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体来说:
	 *    	   2.1 当传入的参数对象不是Map类型时,Mybatis会将传入的POJO对象用MetaObject对象来封装,
	 *         2.2 当动态计算sql过程需要获取数据时,用Map接口的get方法包装 MetaObject对象的取值过程。
	 *         2.3 ContextMap覆写的get方法正是为了上述目的.具体参见下面的`ContextMap`覆写的get方法里的详细解释.
	 *    3. 这里结合着DefaultSqlSession类中的私有方法wrapCollection一起看效果更佳. wrapCollection方法保证了即使用户传入集合类型时,在构造DynamicContext时使用parameterObject参数依然是个Map类型.
	*/

      // 当用户传入的参数是普通的POJO
	if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
	  // 当用户传入的参数null或Map类型时
      bindings = new ContextMap(null);
    }
	// 向刚构造出来的ContextMap实例中推入用户本次传入的参数parameterObject.
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
	// 向刚构造出来的ContextMap实例中推入用户配置的DatabaseId.
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }
}

3. ContextMap

  1. ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。
  2. DynamicContext类的静态构造块中, ContextMap的读取者是设置为ContextAccessor,来注册进OgnlRuntime中的.
    1. 所以在进行OGNL操作时, 最终是使用ContextAccessor来数据读取的.
    2. ContextAccessor的讲解参见下面.
static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;

    private MetaObject parameterMetaObject;
    public ContextMap(MetaObject parameterMetaObject) {
      this.parameterMetaObject = parameterMetaObject;
    }
	
	// 这里的get方法完全负责处理 当客户端传入的参数是自定义的POJO时
	// 因为当用户传入的是Map类型时, 这里的get方法返回的就是null(为了严谨性, 不考虑用户要取_parameter和_databaseId这两个键)
    @Override
    public Object get(Object key) {  // 
      String strKey = (String) key;
	  //数据来源是
	  //  1. 下面的super.put(key,object). 
	  //  2. 客户端主动向ContextMap插入的键值对(例如DynamicContext构造函数中就插入了两个内置的:_parameter和_databaseId), 或者在bind标签中添加的(XMLScriptBuilder.BindHandler类)
	  // 
      if (super.containsKey(strKey)) { 			      							 
        return super.get(strKey);
      }

	  // 下面这部分就是将POJO的值以Map接口的get方式暴露;
	  // 细节就是:
	  //    1. 下面这部分一旦从POJO中取到了对应key的值,则推入到将其进行封装的ContextMap实例中(即super.put(strKey, object);因为其直接继承自HashMap).
	  //    2. 而上面这部分的super.get(key)正是和这个super.put(strKey, object)对应的
      if (parameterMetaObject != null) { //如果构造ContextMap时传入的是POJO;
		// 使用MetaObject类型的getValue从POJO中获取值
        Object object = parameterMetaObject.getValue(strKey);
		// 如果取到的值不是null
        if (object != null) { 
          super.put(strKey, object); //将原本属于POJO的字段名和字段值,往ContextMap实例中也推入一份.这样下次读取同一个字段值的时候就直接从ContextMap实例中读取了
        }

        return object;
      }

	  //始终无法取到, 直接返回null
      return null;
    }
  }

4. ContextAccessor

  1. ContextAccessor也是DynamicContext的内部类.
  2. 本内部类是用来专职处理ContextMap的. 具体可以参见DynamicContext类的静态构造块.
  3. 因为ContextMap自身就是继承自HashMap. 所以ContextAccessor所实现的getProperty方法中,target参数肯定是Map类型 .
  4. ContextAccessor类实现了Ognl中的PropertyAccessor接口,为Ognl提供了如何使用ContextMap参数对象的说明.
  5. 这个类也为整个参数对象“map”化划上了最后一笔。
static class ContextAccessor implements PropertyAccessor {
	// ?? 这个context参数里是啥??
    public Object getProperty(Map context, Object target, Object name)
        throws OgnlException {
      Map map = (Map) target; // target为ContextMap,所以可以安全地转换为Map

	  //这里调用的ContextMap覆写的get方法;也就是缓存的POJO中的属性对;
	  //    如果你要是在其中取 PARAMETER_OBJECT_KEY 对应的对象,返回的result就是用户传入的parameterObject.
	  // 这里不为null时, 说明用户传入的是POJO
      Object result = map.get(name);
      if (result != null) { 
        return result;
      }

	  // --- 这里就是处理当用户传入的参数是Map时
	  // 构造DynamicContext实例时,插入到ContextMap实例中的键值对:{ "_parameter" : parameterObject }
      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
	  // 用户显式传入的就是Map类型
      if (parameterObject instanceof Map) {
    	  return ((Map)parameterObject).get(name);
      }

      return null;
    }
  }

5. 总结

  1. DynamicContext.ContextMap覆写的get方法将POJO类型的参数里的值进行暴露.其对于原本就是Map类型的参数,直接不管.
  2. 而与之相匹配的 在OGNL中的注册的DynamicContext.ContextAccessor,则是负责读取原本就是Map类型的参数里的值.
  3. Mybatis中的参数传递和使用过程了:将传入的参数对象统一封装为ContextMap对象(其继承了HashMap对象),然后Ognl运行时环境在动态计算sql语句时,会按照ContextAccessor中描述的Map接口的方式来访问和读取ContextMap对象,获取计算过程中需要的参数。ContextMap对象内部可能封装了一个普通的POJO对象,也可以是直接传递的Map对象,当然从外部是看不出来的,因为都是使用Map的接口来读取数据。
  4. Mybatis参数获取过程中,对Map对象和普通POJO对象的无差别化,因为在内部,两者都会被封装,然后通过Map接口来访问!

6. Links

  1. Mybatis中几个重要类

你可能感兴趣的:(MyBatis3)