前言

Mybatis中的插件又叫做拦截器,通过插件可以在Mybatis某个行为执行时进行拦截并改变这个行为。通常,Mybatis的插件可以作用于Mybatis中的四大接口,分别为ExecutorParameterHandlerResultSetHandlerStatementHandler,归纳如下表所示。

Executorupdate()query()flushStatements()commit()rollback()getTransaction()close()isClosed()拦截执行器中的方法
ParameterHandlergetParameterObject()setParameters()拦截对参数的处理
ResultSetHandlerhandleResultSets()handleOutputParameters()拦截对结果集的处理
StatementHandlerprepare()parameterize()batch()update()query()拦截SQL构建的处理

本篇文章将对插件怎么用插件的执行原理进行分析。

正文

一. 插件的使用

插件的使用比较简单,在Mybatis配置文件中将插件配置好,Mybatis会自动将插件的功能植入到插件对应的四大接口中。本小节将以一个例子,对自定义插件插件的配置插件的执行效果进行说明。

首先创建两张表,语句如下所示。

CREATE TABLE bookstore(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    bs_name VARCHAR(255) NOT NULL
);

CREATE TABLE book(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    b_name VARCHAR(255) NOT NULL,
    b_price FLOAT NOT NULL,
    bs_id INT(11) NOT NULL,
    FOREIGN KEY book(bs_id) REFERENCES bookstore(id)
)

往表中插入若干数据,如下所示。

INSERT INTO bookstore (bs_name) VALUES ("XinHua");
INSERT INTO bookstore (bs_name) VALUES ("SanYou");

INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2)

现在开始搭建测试工程(非Springboot整合工程),新建一个Maven项目,引入依赖如下所示。

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
</dependencies>

还需要在POM文件中添加如下配置,以满足打包时能将src/main/java下的XML文件(主要想打包映射文件)进行打包。

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>8</source>
                <target>8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

Mybatis的配置文件mybatis-config.xml如下所示,主要是开启日志打印配置事务工厂配置数据源注册映射文件/映射接口

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&amp;serverTimezone=UTC&amp;useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.mybatis.learn.dao"/>
    </mappers>
</configuration>

本示例中,执行一个简单查询,将book表中的所有数据查询出来,查询出来的每条数据用Book类进行映射,Book类如下所示。

@Data
public class Book {

    private long id;
    private String bookName;
    private float bookPrice;

}

映射接口如下所示。

public interface BookMapper {

    List<Book> selectAllBooks();

}

按照规则,编写映射文件,如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.learn.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
    </resultMap>

    <select id="selectAllBooks" resultMap="bookResultMap">
        SELECT
        b.id, b.b_name, b.b_price
        FROM
        book b
    </select>
</mapper>

最后编写测试程序,如下所示。

public class MybatisTest {

    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession = sqlSessionFactory.openSession();
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);

        List<Book> books = bookMapper.selectAllBooks();
        books.forEach(System.out::println);
    }

}

整个测试工程的目录结构如下所示。

运行测试程序,日志打印如下。

现在开始自定义插件的编写,Mybatis官方文档中给出了自定义插件的编写示例,如下所示。

@Intercepts({@Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {

    @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;
    }

}

现在按照Mybatis官方文档的示例,编写一个插件,作用于Executorquery()方法,行为是在query()方法执行前和执行后分别打印一些日志信息。编写的插件如下所示。

@Intercepts(
        {
                @Signature(type = Executor.class, method = "query",
                        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
        }
)
public class ExecutorTestPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("Begin to query.");
        Object result = invocation.proceed();
        System.out.println("End to query.");
        return result;
    }

}

Mybatis配置文件中将编写好的插件进行配置,如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>

    <plugins>
        <plugin interceptor="com.mybatis.learn.plugin.ExecutorTestPlugin"/>
    </plugins>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&amp;serverTimezone=UTC&amp;useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.mybatis.learn.dao"/>
    </mappers>
</configuration>

再次运行测试程序,打印日志信息如下所示。

可以看到,插件按照预期执行了。

二. 插件的原理

本小节将分析插件是如何植入Mybatis四大接口以及插件是如何生效的。因为小节一中自定义的插件是作用于Executor,所以本小节主要是以Executor植入插件进行展开讨论,其余三大接口大体类似,就不再赘述。

Mybatis在获取SqlSession时,会为SqlSession构建Executor执行器,在构建Executor的过程中,会为Executor植入插件的逻辑,这部分内容在Mybatis源码-SqlSession获取中已经进行了介绍。构建Executor是发生在ConfigurationnewExecutor()方法中,如下所示。

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) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    //如果Mybatis配置文件中开启了二级缓存
    if (cacheEnabled) {
        //创建CachingExecutor作为Executor的装饰器,为Executor增加二级缓存功能
        executor = new CachingExecutor(executor);
    }
    //将插件逻辑添加到Executor中
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

将插件逻辑植入到Executor是发生在InterceptorChainpluginAll()方法中。如果在Mybatis的配置文件中配置了插件,那么配置的插件会在加载配置文件的时候被解析成拦截器Interceptor并添加到ConfigurationInterceptorChain中。InterceptorChain是拦截器链,其实现如下所示。

public class InterceptorChain {

    //插件会添加到集合中
    private final List<Interceptor> interceptors = new ArrayList<>();

    //为四大接口添加插件逻辑时会调用pluginAll()方法
    //这里的target参数就是四大接口的对象
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

}

当为Executor添加插件逻辑时,就会调用InterceptorChainpluginAll()方法,在pluginAll()方法中,会遍历插件集合并调用每个插件的plugin()方法,所以插件功能的添加在于Interceptorplugin()方法,其实现如下所示。

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

Interceptorplugin()方法中,调用了Pluginwrap()静态方法,继续看该静态方法的实现。

public static Object wrap(Object target, Interceptor interceptor) {
    //将插件的@Signature注解内容获取出来并生成映射结构
    //Map[插件作用的接口的Class对象, Set[插件作用的方法的方法对象]]
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    //将目标对象实现的所有接口中是当前插件的作用目标的接口获取出来
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
        //为目标对象生成代理对象并返回
        //这是JDK动态代理的应用
        //Plugin实现了InvocationHandler接口
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

Pluginwrap()静态方法中,先判断目标对象实现的接口中是否有当前插件的作用目标,如果有,就为目标对象基于JDK动态代理生成代理对象。同时,Plugin实现了InvocationHandler接口,当代理对象执行方法时,就会调用到Plugininvoke()方法,接下来看一下invoke()方法做了什么事情,如下所示。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        //判断插件是否作用于当前代理对象执行的方法
        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);
    }
}

Plugininvoke()方法首先会判断当前插件是否作用于当前代理对象执行的方法,如果不作用于,则当前代理对象执行的方法直接执行,如果作用于,则生成Invocation并执行插件的逻辑。下面先看一下Invocation是什么,如下所示。

public class Invocation {

    //插件作用的目标对象(四大对象)
    private final Object target;
    //插件作用的目标方法
    private final Method method;
    //插件作用的目标方法的参数
    private final Object[] args;

    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);
    }

}

Invocation用于插件获取插件作用的目标对象的信息,包括:作用对象本身作用的方法参数,同时Invocationproceed()方法可以执行被插件作用的方法。所以插件可以在其实现的intercept()方法中通过Invocation获取到插件作用目标的完整信息,也可以通过Invocationproceed()方法运行作用目标的原本逻辑。

所以到这里可以知道,为Mybatis的四大对象植入插件逻辑时,就是为Mybatis的四大对象生成代理对象,同时生成的代理对象中的Plugin实现了InvocationHandler,且Plugin持有插件的引用,所以当代理对象执行方法时,就可以通过Plugininvoke()方法调用到插件的逻辑,从而完成插件逻辑的植入。此外,如果定义了多个插件,那么会根据插件在Mybatis配置文件中的声明顺序,一层一层的生成代理对象,比如如下的配置中,先后声明了两个插件。

<plugins>
    <plugin intercepter="插件1"></plugin>
    <plugin intercepter="插件2"></plugin>
</plugins>

那么生成的代理对象可以用下图进行示意。

即为四大对象植入插件逻辑时,是根据声明插件时的顺序从里向外一层一层的生成代理对象,反过来四大对象实际运行时,是从外向里一层一层的调用插件的逻辑。

总结

Mybatis中的插件可以作用于Mybatis中的四大对象,分别为ExecutorParameterHandlerResultSetHandlerStatementHandler,在插件的@Signature中可以指定插件的作用目标对象和目标方法,插件是通过为Mybatis中的四大对象生成代理对象完成插件逻辑的植入,Mybatis中的四大对象实际运行时,会先调用到插件的逻辑(如果有插件的话),然后才会调用到四大对象本身的逻辑。

03-05 22:16