乐趣区

关于插件:Mybatis源码插件的使用和原理

前言

Mybatis中的插件又叫做 拦截器 ,通过插件能够在Mybatis 某个行为执行时进行拦挡并扭转这个行为。通常,Mybatis的插件能够作用于 Mybatis 中的四大接口,别离为 ExecutorParameterHandlerResultSetHandlerStatementHandler,归纳如下表所示。

可作用接口 可作用办法 拦截器用处
Executor update()query()flushStatements()commit()rollback()getTransaction()close()isClosed() 拦挡执行器中的办法
ParameterHandler getParameterObject()setParameters() 拦挡对参数的解决
ResultSetHandler handleResultSets()handleOutputParameters() 拦挡对后果集的解决
StatementHandler prepare()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中的四大对象理论运行时,会先调用到插件的逻辑(如果有插件的话),而后才会调用到四大对象自身的逻辑。

退出移动版