Mybatis技巧之附加额外的上下文信息

又一次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内部分别是在DefaultParameterHandlerTextSqlNode中针对简单类型进行了专门处理,所以为了确保向后兼容,需要作专门的处理。当然如果是新项目或者历史债不重的话,推荐直接进行制度约束,在这里追求灵活性完全是自找无趣,增加无谓的复杂性。 本人最终选择了 直接在接收用户传入的参数层面【BaseServiceImpl】进行归一化(简单类型或null全部归一化为Map类型), 所以这里不再考虑简单类型 。
2. 在ContextMap.get的实现中,每次判断之后没有选择将获取到的值推入super中。是因为参考 —— Mybatis中为了修正issue #61 do not modify the context when reading问题,而选择删除了类似的逻辑。
3. 还能想到的优化点,就是抽取开发人员编写的原始映射SQL内容,只有在使用到了这些参数时,才附加上这些额外的配置信息。

  1. Mybatis源码研究之DynamicContext

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/80820327
今日推荐