共计 10212 个字符,预计需要花费 26 分钟才能阅读完成。
前言
Mybatis
中的插件又叫做 拦截器 ,通过插件能够在Mybatis
某个行为执行时进行拦挡并扭转这个行为。通常,Mybatis
的插件能够作用于 Mybatis
中的四大接口,别离为 Executor
,ParameterHandler
,ResultSetHandler
和StatementHandler
,归纳如下表所示。
可作用接口 | 可作用办法 | 拦截器用处 |
---|---|---|
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&serverTimezone=UTC&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
官网文档的示例,编写一个插件,作用于 Executor
的query()
办法,行为是在 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&serverTimezone=UTC&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
是产生在 Configuration
的newExecutor()
办法中,如下所示。
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
是产生在 InterceptorChain
的pluginAll()
办法中。如果在 Mybatis
的配置文件中配置了插件,那么配置的插件会在加载配置文件的时候被解析成拦截器 Interceptor
并增加到 Configuration
的InterceptorChain
中。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
增加插件逻辑时,就会调用 InterceptorChain
的pluginAll()
办法,在 pluginAll()
办法中,会遍历插件汇合并调用每个插件的 plugin()
办法,所以插件性能的增加在于 Interceptor
的plugin()
办法,其实现如下所示。
default Object plugin(Object target) {return Plugin.wrap(target, this);
}
Interceptor
的 plugin()
办法中,调用了 Plugin
的wrap()
静态方法,持续看该静态方法的实现。
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;
}
在 Plugin
的wrap()
静态方法中,先判断指标对象实现的接口中是否有以后插件的作用指标,如果有,就为指标对象基于 JDK
动静代理生成代理对象。同时,Plugin
实现了 InvocationHandler
接口,当代理对象执行办法时,就会调用到 Plugin
的invoke()
办法,接下来看一下 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);
}
}
Plugin
的 invoke()
办法首先会判断以后插件是否作用于以后代理对象执行的办法,如果不作用于,则以后代理对象执行的办法间接执行,如果作用于,则生成 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
用于插件获取插件作用的指标对象的信息,包含:作用对象自身 , 作用的办法 和参数 ,同时Invocation
的proceed()
办法能够执行被插件作用的办法。所以插件能够在其实现的 intercept()
办法中通过 Invocation
获取到插件作用指标的残缺信息,也能够通过 Invocation
的proceed()
办法运行作用指标的本来逻辑。
所以到这里能够晓得,为 Mybatis
的四大对象植入插件逻辑时,就是为 Mybatis
的四大对象生成代理对象,同时生成的代理对象中的 Plugin
实现了 InvocationHandler
,且Plugin
持有插件的援用,所以当代理对象执行办法时,就能够通过 Plugin
的invoke()
办法调用到插件的逻辑,从而实现插件逻辑的植入。此外,如果定义了多个插件,那么会依据插件在 Mybatis
配置文件中的申明程序,一层一层的生成代理对象,比方如下的配置中,先后申明了两个插件。
<plugins>
<plugin intercepter="插件 1"></plugin>
<plugin intercepter="插件 2"></plugin>
</plugins>
那么生成的代理对象能够用下图进行示意。
即为四大对象植入插件逻辑时,是依据申明插件时的程序 从里向外 一层一层的生成代理对象,反过来四大对象理论运行时,是 从内向里 一层一层的调用插件的逻辑。
总结
Mybatis
中的插件能够作用于 Mybatis
中的四大对象,别离为 Executor
,ParameterHandler
,ResultSetHandler
和StatementHandler
,在插件的 @Signature
中能够指定插件的作用指标对象和指标办法,插件是通过为 Mybatis
中的四大对象生成代理对象实现插件逻辑的植入,Mybatis
中的四大对象理论运行时,会先调用到插件的逻辑(如果有插件的话),而后才会调用到四大对象自身的逻辑。