mybatis的插件机制

 

作用:插件是MyBatis提供的一个拓展机制,通过插件机制我们可在SQL执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现Interceptor接口。然后在插件类上添加@Intercepts和@Signature注解,用于指定想要拦截的目标方法。
MyBatis允许拦截下面接口中的一些方法:

  • Executor 上层的对象,SQL 执行全过程,包括组装参数,组装结果集返回和执行SQL 过程(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler SQL 参数组装的过程(getParameterObject, setParameters)
  • ResultSetHandler 结果的组装(handleResultSets, handleOutputParameters)
  • StatementHandler 执行SQL 的过程,最常用的拦截对象(prepare, parameterize, batch, update, query)

 

插件实现

1、插件编写

  1. 实现Interceptor 接口

  2. 添加@Intercepts({@Signature()}),指定拦截的对象和方法、方法参数方法名称+参数类型,构成了方法的签名,决定了能够拦截到哪个方法。、

  3. 实现接口的3 个方法

// 用于覆盖被拦截对象的原有方法(在调用代理对象Plugin 的invoke()方法时被调用)
Object intercept(Invocation invocation) throws Throwable;
// target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
Object plugin(Object target);
// 设置参数
void setProperties(Properties properties);

 

2、插件注册,在mybatis-config.xml 中注册插件

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="offsetAsPageNum" value="true"/>
        ……后面全部省略……
    </plugin>
</plugins>
或者使用javaconfig方式注册

 @Bean(name = "sqlSessionFactory")
    public SqlSessionFactoryBean instantiationSqlSessionFactory(@Qualifier("dataSourceProxy") DataSourceProxy dataSourceProxy) throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + mapperPath;
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
        sqlSessionFactoryBean.setDataSource(MultipleDataSource());
		sqlSessionFactoryBean.setDataSource(dataSourceProxy);
       sqlSessionFactoryBean.setPlugins(new Interceptor[]{new MultipleDataSourceInterceptor()});


        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setUseColumnLabel(true);
        configuration.setUseGeneratedKeys(true);
        configuration.setLogImpl(Slf4jImpl.class);

        sqlSessionFactoryBean.setConfiguration(configuration);
        // 指定VFS确保可以扫描到实体类
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        sqlSessionFactoryBean.setTypeAliasesPackage(entityPackage);
        return sqlSessionFactoryBean;
    }

 

 

3、插件登记

MyBatis 启动时扫描 标签, 注册到Configuration 对象的InterceptorChain 中。property 里面的参数,会调用setProperties()方法处理。

 

插件原理

先来看一下 SqlSession 开启的过程。

public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 省略部分逻辑
        
        // 创建 Executor
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } 
    catch (Exception e) {...} 
    finally {...}
}

 

Executor 的创建过程封装在 Configuration 中,我们跟进去看看看。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    
    // 根据 executorType 创建相应的 Executor 实例
    if (ExecutorType.BATCH == executorType) {...} 
    else if (ExecutorType.REUSE == executorType) {...} 
    else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    
    // 植入插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

 

 

如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。

public class InterceptorChain {

    private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

    public Object pluginAll(Object target) {
        // 遍历拦截器集合  这里使用的是责任链模式
        for (Interceptor interceptor : interceptors) {
            // 调用拦截器的 plugin 方法植入相应的插件逻辑
            target = interceptor.plugin(target);
        }
        return target;
    }
    
    /** 添加插件实例到 interceptors 集合中 */
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    /** 获取插件列表 */
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

 

以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一个层层嵌套的代理类.

plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,所以下面找个示例分析一下。

 

// -☆- ExamplePlugin
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

// -☆- Plugin
public static Object wrap(Object target, Interceptor interceptor) {
    /*
     * 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:
     * {
     *     Executor.class : [query, update, commit],
     *     ParameterHandler.class : [getParameterObject, setParameters]
     * }
     */
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 获取目标类实现的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
        // 通过 JDK 动态代理为目标类生成代理类
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

   /*
     * 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:
     * {
     *     Executor.class : [query, update, commit],
     *     ParameterHandler.class : [getParameterObject, setParameters]
     * }
     */
----
 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

 

可以看到这个方法的逻辑也很简单,但是需要注意的是MyBatis插件是通过JDK动态代理来实现的,而JDK动态代理的条件就是被代理对象必须要有接口,这一点和Spring中不太一样,Spring中是如果有接口就采用JDK动态代理,没有接口就是用CGLIB动态代理。

 

正因为MyBatis的插件只使用了JDK动态代理,所以我们上面才强调了一定要实现Interceptor接口。 而代理之后会执行Plugin的invoke方法,我们最后再来看看invoke方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        /*
         * 获取被拦截方法列表,比如:
         *    signatureMap.get(Executor.class),可能返回 [query, update, commit]
         */
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        // 检测方法列表是否包含被拦截的方法
        if (methods != null && methods.contains(method)) {
            // 执行插件逻辑
            return interceptor.intercept(new Invocation(target, method, args));
        }
        // 执行被拦截的方法
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

 

nvoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept 中,该方法的参数类型为 Invocation。Invocation 主要用于存储目标类,方法以及方法参数列表.

 

mybatis的插件机制

上一篇:过滤器(Filter)与拦截器(Interceptor )区别


下一篇:将OpenOffice.org变成一个文档格式转换工具