互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)

时间:2021-05-19 20:57:36

  简言:今天进行第八天的记录(只是写了八天)。有的时候看的多,有的时候看的少,看的少的时候就攒几天一起写了。而今天这个插件我昨天写了一下午,下班没写完就回去了,今天把尾收了,再加上一个过程图方便下面原理的理解。我这个特别不爱看书的人都看半个多月了,希望我一个月真能养成个戒不掉的习惯~

第八章 插件

  在前一篇介绍了四个对象,是SqlSession执行过程中通过他们来完成数据库操作和结果返回的。(ExecutorStatementHandlerParameterHandlerResultSetHandler)。我昨天的查缺补漏有记载,要是想深入了解就可以去查资料了,或者看我这本书的第七章(书名《JavaEE 互联网轻量级框架整合开发》)

  插件的原理就是在四大对象调度时插入我们的代码去执行一些特殊的要求以满足特殊的场景需求

  斜体字是部分原理。我先在这插入一张流程图,如果有疑问可以看到最后再返回来琢磨一下这个图。

 互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)

  使用方法:(举例:要想在预编译之前在控制台上打印一些东西,就需要拦截执行SQL的StatementHandler对象的预编译方法,也就是prepare方法)

  在MyBatis中使用插件,就必须实现interceptor接口,实现它的三个方法(代码中有注释,应该能知道啥意思):

package com.ssm.chapter8.plugin;
import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.log4j.Logger;

@Intercepts({
    @Signature(
            type=StatementHandler.class,
            method="prepare",
            args = {Connection.class,Integer.class}
            )
})
public class MyPlugin implements Interceptor {
    private Logger log = Logger.getLogger(MyPlugin.class);
    private Properties props = null;
    /**
     * 插件方法,它代替StatementHandler的prepare方法
     * 
     * @param invocation 入参
     * @return 返回预编译后的preparedStatement
     * @throws Throwable 异常
     * */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
        //进行绑定
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;
        /*分离代理对象链(由于目标类可能被多个拦截器[插件]拦截,从而形成多次代理,通过循环可以分离出最原始的目标类)*/
        while(metaStatementHandler.hasGetter("h")){
            object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        statementHandler = (StatementHandler)object;
        String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
        Long parameterObject = (Long)metaStatementHandler.getValue("delegate.boundSql.parameterObject");
        
        log.info("执行的SQL:【"+sql+"】");
        log.info("参数:【"+parameterObject+"】");
        log.info("before......");
        //如果当前代理的事一个非代理对象,那么它就会回调用真实拦截对象的方法
        //如果不是,那么它就会调度下一个插件代理对象的invoke方法
        Object obj = invocation.proceed();
        log.info("after......");
        return obj;
    }
    /**
     * 生成代理对象
     * 
     * @param target 被拦截对象
     * @return 代理对象
     * */
    public Object plugin(Object target) {
        // 采用系统默认的Plugin.wrap方法生成
        return Plugin.wrap(target, this);
    }
    /**
     * 设置参数,MyBatis初始化时,就会生成插件实例,并调用这个方法
     * 
     * @param props 配置参数
     * */
    public void setProperties(Properties props) {
        this.props = props;
        log.info("dbType = "+this.props.getProperty("dbType"));
        
    }
}

  标黄的三个就是要去实现的方法:

  • intercept:英译:拦截。简单说就是拦截签名(指类名上面的注解@Signature)中对应中对应方法。参数Invocation就是被拦截内容的整合。
  • plugin:英译:插件(计算机名词)。target是被拦截的对象
  • setProperties:设置参数。这个参数是要在XML配置中配置的。

  注解@Intercepts说明它是一个拦截器。@Singnature是注册拦截器签名的地方,只有签名满足要求才能拦截,type可以是四大对象中的一个,这里是StatementHandler。method代表要拦截四大对象的某一接口方法,而args则表示该方法的参数(要根据拦截对象的方法进行拦截。)下面贴一段StatementHandler中prepare方法的定义代码。

public abstract Statement prepare(Connection connection, Integer integer) throws SQLException;

  所以args中是一个Connection.class和 Integer.class。拦截后,通过 Invocation对象可以反射调度原来对象的方法。贴一段 Invocation的源代码。

public class Invocation {

    public Invocation(Object target, Method method, Object args[]) {
        this.target = target;
        this.method = method;
        this.args = args;
    }
    public Object getTarget() {
        return target;
    }
    public Method getMethod() {
        return method;
    }
    public Object[] getArgs() {
        return args;
    }
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }
    private final Object target;
    private final Method method;
    private final Object args[];
}

  一看就知道 Target是被拦截的目标对象,Method是被拦截的方法,Args就是注解中的参数,这里的proceed方法是通过反射调用原对象中的方法。

  配置XML:注意MyBatis配置XML中标签的顺序

<plugins><!-- 插件 -->
    <plugin interceptor="com.ssm.chapter8.plugin.MyPlugin">
        <property name="dbType" value="mysql"/>
    </plugin>
</plugins>

  我使用的查询配置:

<select id="getRole" parameterType="Long" resultType="role">
    select id,role_name as roleName,note from t_role where id = #{id}
</select>

  执行结果:

DEBUG - Reader entry: <?xml version="1.0" encoding="UTF-8"?>
DEBUG - Checking to see if class com.learn.ssm.chapter3.mapper.RoleMapper matches criteria [is assignable to Object]
DEBUG - Cache Hit Ratio [com.learn.ssm.chapter3.mapper.RoleMapper]: 0.0
DEBUG - Opening JDBC Connection
DEBUG - Created connection 407858146.
DEBUG - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@184f6be2]
 INFO - 执行的SQL:【select id,role_name as roleName,note from t_role where id = ?】
 INFO - 参数:【1】
 INFO - before......
DEBUG - ==>  Preparing: select id,role_name as roleName,note from t_role where id = ? 
 INFO - after......
DEBUG - ==> Parameters: 1(Long)
DEBUG - <==      Total: 1
DEBUG - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@184f6be2]
DEBUG - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@184f6be2]
DEBUG - Returned connection 407858146 to pool.

  标红为Intercept方法中log.info打印的东西。在before和after中间可以清晰看出,进行了预编译,也就是调用了原有的prepare。

  总结

  梳理一下思路,在介绍一下没提到的部分原理:

  按照程序的进行,在构建Configuration的时候,插件对象被创建。下面是XMLConfigBuilder部分代码。

private void parseConfiguration(XNode root) {
     try {
            /*省略*/
            pluginElement(root.evalNode("plugins"));
            /*省略*/
        } catch (Exception e) {
            throw new BuilderException((new StringBuilder()).append("Error parsing SQL Mapper Configuration. Cause: ")
                    .append(e).toString(), e);
        }
}
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        Interceptor interceptorInstance;
        for (Iterator iterator = parent.getChildren().iterator(); iterator.hasNext(); configuration .addInterceptor(interceptorInstance)) {
            XNode child = (XNode) iterator.next();
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
        } } }

  在初始化插件的时候,调用pluginElement方法。在pluginElement方法中使用反射技术生成插件对应的插件实力,然后调用插件方法中的setProperties方法,设置我们配置的参数,将插件实例保存到配置对象中,以便读取和使用它。所以插件的实力对象是一开始就被初始化的,而不是用的时候才初始化

  其实反射得来的插件实例被存储到interceptorChain中(Chain是链的意思),贴段流程。

互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)

 

 

  插件准备好了,接下来就是利用插件拦截了。在configuration类中。四大对象的初始化方法代码,这里用到了interceptorChain的另一个方法:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
            BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType != null ? executorType : defaultExecutorType;
    executorType = executorType != null ? executorType : ExecutorType.SIMPLE;
    Executor executor;
    if (ExecutorType.BATCH == executorType)
        executor = new BatchExecutor(this, transaction);
    else if (ExecutorType.REUSE == executorType)
        executor = new ReuseExecutor(this, transaction);
    else
        executor = new SimpleExecutor(this, transaction);
    if (cacheEnabled)
        executor = new CachingExecutor(executor);
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

  注意观察标红的代码,interceptorChain.pluginAll方法就是就是用来便利所有的插件,校验是否拦截该对象。

public Object pluginAll(Object target) {
    for (Iterator iterator = interceptors.iterator(); iterator.hasNext();) {
        Interceptor interceptor = (Interceptor) iterator.next();
        target = interceptor.plugin(target);
    }
    return target;
}

  plugin方法,也就是最上面例子中我写的方法。

  现在就差拦截prepare方法并用intercept方法覆盖了

  我上面写了intercept方法拦截了StatementHandler的prepare方法,四大对象传递给plugin方法后就会返回一个代理对象,在使用代理对象的方法(例子中就是StatementHandler的prepare方法)时候就会进入invoke方法进行逻辑处理,这是代理模式的关键,通过逻辑判断就可以不适用prepare方法而返回intercept方法这就是拦截的地方。自己编写代理类的工作量很大,MyBatis提供了一个常用的工具类Plugin,用来生成代理对象。Plugin类实现了InvocationHandler接口,采用的是JDK的动态代理技术。代码如下:

public class Plugin implements InvocationHandler {
    public static Object wrap(Object target, Interceptor interceptor) {
    Map signatureMap = getSignatureMap(interceptor);
    Class type = target.getClass();
    Class interfaces[] = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0)
        return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
                new Plugin(target, interceptor, signatureMap));
    else
        return target;
    }
    public Object invoke(Object proxy, Method method, Object args[]) throws Throwable {
    try {
        Set methods = (Set) signatureMap.get(method.getDeclaringClass());
        if (methods != null && methods.contains(method))
            return interceptor.intercept(new Invocation(target, method, args));
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
    return method.invoke(target, args);
    }
}

  在例子中,MyPlugin的plugin方法里就是调用Plugin.wrap方法来返回一个代理对象。当代理对象调用prepare方法就会进入invoke方法。Signature就是上面提到的签名,如果存在签名的拦截方法,就会调用intercept方法并返回结果。这样我们想要做的事就做好了~