DynamicContext
为POJO对象Map化提供了很好的借鉴,抹平了访问POJO和Map对象的差异.
1. 由来
在Mybatis提供的动态SQL功能中, 作为底层支撑的关键类SqlNode
接口.其签名如下:
package org.apache.ibatis.scripting.xmltags;
public interface SqlNode {
boolean apply(DynamicContext context);
}
- 被所有动态sql节点类(诸如
ChooseSqlNode
等)继承的接口. 作为参数的DynamicContext
重要性就不言而喻了. - 参数
DynamicContext
的作用就是为各个动态sql节点实现类(诸如ChooseSqlNode
等)提供进行判断的上下信息.确保判断等操作的完整实现. - 依然如一直强调的, 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
ContextMap
作为一个继承了HashMap
的对象,作用就是用于统一参数的访问方式:用Map
接口方法来访问数据。- 在
DynamicContext
类的静态构造块中,ContextMap
的读取者是设置为ContextAccessor
,来注册进OgnlRuntime
中的.- 所以在进行OGNL操作时, 最终是使用
ContextAccessor
来数据读取的. ContextAccessor
的讲解参见下面.
- 所以在进行OGNL操作时, 最终是使用
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
ContextAccessor
也是DynamicContext
的内部类.- 本内部类是用来专职处理
ContextMap
的. 具体可以参见DynamicContext
类的静态构造块. - 因为
ContextMap
自身就是继承自HashMap<String, Object>
. 所以ContextAccessor
所实现的getProperty
方法中,target参数肯定是Map
类型 . ContextAccessor
类实现了Ognl中的PropertyAccessor
接口,为Ognl提供了如何使用ContextMap参数对象的说明.- 这个类也为整个参数对象“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. 总结
DynamicContext.ContextMap
覆写的get
方法将POJO类型的参数里的值进行暴露.其对于原本就是Map类型的参数,直接不管.- 而与之相匹配的 在OGNL中的注册的
DynamicContext.ContextAccessor
,则是负责读取原本就是Map类型的参数里的值. - Mybatis中的参数传递和使用过程了:将传入的参数对象统一封装为
ContextMap
对象(其继承了HashMap
对象),然后Ognl运行时环境在动态计算sql语句时,会按照ContextAccessor
中描述的Map
接口的方式来访问和读取ContextMap
对象,获取计算过程中需要的参数。ContextMap
对象内部可能封装了一个普通的POJO对象,也可以是直接传递的Map对象,当然从外部是看不出来的,因为都是使用Map
的接口来读取数据。 - Mybatis参数获取过程中,对
Map
对象和普通POJO对象的无差别化,因为在内部,两者都会被封装,然后通过Map
接口来访问!