Mybatis_总结_06_用_插件开发

一、前言

Mybatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。

二、会被拦截的接口

Mybatis 允许在映射语句执行过程中的某一点进行拦截调用。

默认情况下,Mybatis允许使用插件来拦截的接口和方法包括以下几个:

序号 接口 方法 描述
1 Executor update、query、flushStatements、commit、rollback、getTransaction、close、isClosed

拦截执行器的方法

2 ParameterHandler getParameterObject、setParameters  

拦截参数的处理

3 ResultSetHandler handleResultSets、handleCursorResultSets、handleOutputParameters  

拦截结果集的处理

4 StatementHandler prepare、parameterize、batch、update、query  

拦截Sql语法构建的处理

Mybatis是通过动态代理的方式实现拦截的,阅读此篇文章需要先对Java的动态代理机制有所了解。可以参考博客《彻底理解java动态代理》

三、Mybatis四大接口

 

竟然Mybatis是对四大接口进行拦截的,那我们药先要知道Mybatis的四大接口对象 Executor, StatementHandler, ResultSetHandler, ParameterHandler。

 

图1-1   Mybatis框架执行过程

Mybatis插件能够对四大对象进行拦截,包括对Mybatis一次会话的所有操作进行拦截。可见Mybatis的插件的强大。

序号 接口 解读
1 Executor

是Mybatis的内部执行器。

它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射。

另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。

2 StatementHandler

是Mybatis直接和数据库执行sql脚本的对象。

另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。

3 ParameterHandler

是Mybatis实现Sql入参设置的对象。

这里,使用插件可以改变我们Sql的参数默认设置。

4 ResultSetHandler

是Mybatis把ResultSet集合映射成POJO的接口对象。

我们可以定义插件对Mybatis的结果集自动映射进行修改。

四、插件Interceptor

 Mybatis的插件实现要实现Interceptor接口,我们看下这个接口定义的方法。

public interface Interceptor {   
   Object intercept(Invocation invocation) throws Throwable;       
   Object plugin(Object target);    
   void setProperties(Properties properties);
}

这个接口只声明了三个方法。

1.setProperties

在Mybatis的配置文件中配置插件时,可通过此方法来传递参数给插件。

如,在mybatis-config.xml中,一般情况下,拦截器的配置如下:

<plugins>
    <!-- 1.interceptor属性为拦截器实现类的全类名  -->
    <plugin  interceptor="tk.mybatis.simple.plugin.XXXInterceptor">
        <!-- 2.通过property标签来配置参数,配置的参数在拦截器初始化时会通过setProperties方法传递给拦截器。 在拦截器中可以很方便的通过Properties取得配置的参数值  -->
         <property  name="prop1"  value="value1" />
         <property  name="prop2"  value="value2" />
    </plugin>
</plugins>

2.plugin

此方法的参数target就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类时被调用 ???

该方法的实现很简单,只需要调用Mybatis提供的Plugin(org.apache.ibatis.plugin.Plugin)类的wrap静态方法就可以通过Java的动态代理拦截目标对象。

 这个方法的通常实现代码如下:

@Override
public Object plugin(Object target) {
   return Plugin.wrap(target, this);
}

 Plugin.wrap方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象,因此在上面的实现方法中不必做额外的判断逻辑。

 来看一个稍微复杂一点的例子。

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        if (target instanceof Executor) {
            final Executor e = (Executor) target;
            Executor executor = new Executor() {
                public int update(MappedStatement ms, Object parameter) throws SQLException {
                    return e.update(ms, parameter);
                }

                public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
                        ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException {
                    return e.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
                }

                public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
                        ResultHandler resultHandler) throws SQLException {
                    BoundSql boundSql = ms.getBoundSql(parameter);
                    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
                    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }

                public List<BatchResult> flushStatements() throws SQLException {
                    return e.flushStatements();
                }

                public void commit(boolean required) throws SQLException {
                    e.commit(required);
                }

                public void rollback(boolean required) throws SQLException {
                    e.rollback(required);
                }

                public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
                        BoundSql boundSql) {
                    IRequest request = RequestHelper.getCurrentRequest(true);
                    boundSql.setAdditionalParameter("request", request);
                    return e.createCacheKey(ms, parameterObject, rowBounds, boundSql);
                }

                public boolean isCached(MappedStatement ms, CacheKey key) {
                    return e.isCached(ms, key);
                }

                public void clearLocalCache() {
                    e.clearLocalCache();
                }

                public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key,
                        Class<?> targetType) {
                    e.deferLoad(ms, resultObject, property, key, targetType);
                }

                public Transaction getTransaction() {
                    return e.getTransaction();
                }

                public void close(boolean forceRollback) {
                    e.close(forceRollback);
                }

                public boolean isClosed() {
                    return e.isClosed();
                }

                public void setExecutorWrapper(Executor executor) {
                    e.setExecutorWrapper(executor);
                }
            };

            return executor;
            // return Plugin.wrap(executor, this);
        }
        return target;
    }
View Code

 上述代码中对匹配条件做了进一步的细化

3.intercept

此方法是Mybatis运行时要执行的拦截方法,

通过该方法的参数invocation可以得到很多有用的信息。

@Override
public Object intercept(Invocation invocation) throws Throwable{
   Object target = invocation.getTarget();
   Method method = invocation.getMethod();
   Object[] args = invocation.getArgs();
   Object result = invocation.proceed();

   return result;
}

通过调用 invocation.proceed();可以执行被拦截对象真正的方法。proceed()方法实际上执行了method.invoke(target,args)方法、

4.多个插件的调用顺序

当配置多个拦截器时,Mybatis会遍历所有拦截器,按顺序执行拦截器的plugin方法,被拦截的对象就会被层层代理

在执行拦截对象的方法时,会一层一层地调用拦截器,拦截器通过invocation.proceed()调用下一层的方法,直到真正的方法被执行。方法执行的结果会从最里面开始向外一层层返回,所以如果存在按顺序配置的ABC三个签名相同的拦截器,Mybatis会按照 C->B->A-> target.proceed() -> A->B->C 

 五、拦截器注解

 除了需要实现拦截器接口外,还需要给实现类配置以下的拦截器注解:

(1)@Intercepts

(2)@Signature

使用这两个注解可以用来配置拦截器要拦截的接口。

1.注解说明

以拦截 ResultSetHandler 接口的 handleResultSets 方法为例,配置签名如下。

@intercepts({
    @Signature(
        type   = ResultSetHandler.class,
        method = "handleResultSets",
        args   = {Statement.class}
    )
})
public class ResultSetInterceptor implements Interceptor

@Signature 注解主要包含以下三个属性:

(1)type      :设置拦截的接口,可选值是前面提到的四个接口

(2)method :设置拦截接口中的方法名,可选值是前面4个接口对应的方法,需要和接口匹配。

(3)args      :设置拦截器的参数类型

六、参考资料

1.Mybatis插件原理

原文地址:https://www.cnblogs.com/shirui/p/9610911.html