前言

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类如下所示。

@Datapublic 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()办法做了什么事件,如下所示。

@Overridepublic 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中的四大对象理论运行时,会先调用到插件的逻辑(如果有插件的话),而后才会调用到四大对象自身的逻辑。