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; }
上述代码中对匹配条件做了进一步的细化
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 :设置拦截器的参数类型
六、参考资料