又一次Plugin的应用
1. 概述
大致需求是,我们在编写Mybatis映射文件时,可能需要用到一些相对固定的配置信息——例如系统版本号等等,而我们又不希望在每次传参进行Mybatis执行时都需要手动附加上这些固定配置信息,以便Mybatis进行转换时能够获取到这些信息。
我们希望可以在某个位置进行该类参数的统一添加,而在业务开发人员的角度对此应该无感知,他们只需要知道这些参数的存在,并按需使用即可。
2. 最佳实践
2018/6/30 21:32:27 在经过一段时间的检验之后,特意作出如下推荐:
1. 全新项目或者历史债比较轻的情况下,可以选择Plugin的方式。
2. 对于历史债比较重的项目,还是推荐使用<sql>
。相比较之下这种方式出现意外情况的可能性很低。
2. 取巧之法
写完这边文章之后,突然想到另外一个取巧的方法。适用于对plugin机制不熟悉,并且不希望看到引入plugin之后导致额外的情况。
其实我们完全可以不借助Plugin,只使用Mybatis提供的动态SQL功能。
<!--供需要的地方引用; 后期有修改或扩展只需要修改这里即可-->
<!-- 这里需要注意下Mybatis版本的差异, 在Mybatis3.2.2的DTD校验文件中,是不允许 sql中出现bind标签的; 你需要将版本升级到3.2.8 -->
<sql id="addtionalContext">
<bind name="LQ" value="LQ"/>
<bind name="LQ~A~B" value="QL"/>
<bind name="LQ~B~A" value="QLL"/>
</sql>
<select id="queryUserInfoById" parameterType="map" resultType="map">
<!-- 引入额外的上下文; 这里也是这种方式的额外工作量了。不过因为可以按需引入,这点工作量应该算是可以接受的; 当然你甚至可以以占位符的方式打点,在最后发布时使用模板引擎或Maven插件将所有的Mybatis映射文件过滤一遍 -->
<include refid="xx.yy.zz.addtionalContext"></include>
select us_name,us_code from oa_user where us_ident='${LQ}'
</select>
3. 解决方案
按照以上的需求,果然还是得使用Mybatis提供的Plugin机制。
/**
* 为Mybatis执行时,附加上额外的上下文信息
* <p> 如果有疑问就看看 Mybatis中的 {@code TextSqlNode},{@code DynamicContext.ContextMap}
* <p> 额外还需要注意 {@code DefaultParameterHandler} 【处理 #{}时的参数】 和 {@code TextSqlNode} 【处理 ${}时的参数】; 需要注意的是其中对Simle Type的处理
* @author LQ
*/
@Intercepts({
@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }),
@Signature(method = "update", type = Executor.class, args = { MappedStatement.class, Object.class }) })
public class AttachExtraContextMybatisPlugin implements Interceptor {
private static Logger LOG = LoggerFactory.getLogger(AttachExtraContextMybatisPlugin.class);
private final Properties addtionalContext = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
LOG.debug("###[Mybatis Plugin] begin to attch extra context for mybatis execute.");
// 推入额外的上下文信息
final ContextMap contextMap = new ContextMap(invocation.getArgs()[1]);
append(contextMap);
invocation.getArgs()[1] = contextMap;
final Object returnObj = invocation.proceed();
return returnObj;
}
// 附加上额外的上下文信息
private void append(final ContextMap contextMap) {
// 附加上Mybatis plugin载入的额外上下文信息
for (final Object key : addtionalContext.keySet()) {
contextMap.put((String) key, addtionalContext.getProperty((String) key));
}
// 附加上配置文件中的额外上下文信息
// 注意这里的 ~ , 不能是 . 或者 - , 前者 . 会被OGNL当作对象属性链, 后者 - 会被当作四则操作符 -
Map<String, Object> matchKeys = PropertiesUtil.matchKeys("tablename~prefix");
contextMap.putAll(matchKeys);
}
@Override
public Object plugin(Object target) {
// 当目标类是指定类型时,才包装目标类,否则直接返回目标本身,减少目标被代理的次数
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 载入Mybatis plugin中配置的额外上下文信息
if (null != properties) {
addtionalContext.putAll(properties);
}
}
}
辅助类ContextMap
/**
* <p> 借鉴自Mybatis的同名类 {@code DynamicContext.ContextMap}
* <p> 统一化自定义Bean和Map的读取和写入操作
* @author LQ
*/
final class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
private final Object originObj;
private final MetaObject parameterMetaObject;
public ContextMap(Object parameterObject) {
originObj = parameterObject;
if (parameterObject != null) {
parameterMetaObject = SystemMetaObject.forObject(parameterObject);
} else {
parameterMetaObject = null;
}
}
@Override
public Object get(Object key) {
String strKey = (String) key;
// 先从自身取
if (super.containsKey(strKey)) {
return super.get(strKey);
}
// 如果用户传入的原始参数就为null
if (null == originObj) {
return null;
}
// 如果用户传入的参数为简单类型
if (SimpleTypeRegistry.isSimpleType(originObj.getClass())) {
return originObj;
}
if (parameterMetaObject != null) {
Object object = parameterMetaObject.getValue(strKey);
return object;
}
return null;
}
}
这里需要注意下:
1. 需要考虑下 简单类型(SimpleTypeRegistry.isSimpleType
)的处理。因为在Mybatis内部分别是在DefaultParameterHandler
和TextSqlNode
中针对简单类型进行了专门处理,所以为了确保向后兼容,需要作专门的处理。当然如果是新项目或者历史债不重的话,推荐直接进行制度约束,在这里追求灵活性完全是自找无趣,增加无谓的复杂性。 本人最终选择了 直接在接收用户传入的参数层面【BaseServiceImpl】进行归一化(简单类型或null全部归一化为Map类型), 所以这里不再考虑简单类型 。
2. 在ContextMap.get
的实现中,每次判断之后没有选择将获取到的值推入super
中。是因为参考 —— Mybatis中为了修正issue #61 do not modify the context when reading
问题,而选择删除了类似的逻辑。
3. 还能想到的优化点,就是抽取开发人员编写的原始映射SQL内容,只有在使用到了这些参数时,才附加上这些额外的配置信息。