深入探索MyBatis插件:定制化扩展与增强应用
【摘要】 MyBatis插件是一种强大的扩展机制,可以对MyBatis的核心功能进行定制化扩展和增强。本文将深入研究MyBatis插件的机制和应用,详细解析插件的开发方式、扩展点以及常见应用场景。我们将探讨如何编写、注册和配置MyBatis插件,以及如何利用插件实现自定义功能,如日志记录、性能监控、审计等。
MyBatis 插件
MyBatis 的插件机制允许你在 MyBatis 的执行过程中动态地拦截方法调用,并在方法调用前后执行自定义的逻辑。这种机制使得在不修改原始代码的情况下,能够对 MyBatis 的行为进行增强或者定制化。
插件可以用于很多用途,比如日志记录、性能监控、参数加密解密等。通过插件机制,你可以非常灵活地定制 MyBatis 的行为,以满足特定的需求。
MyBatis 插件编写
通过本专栏前面的内容我们已经了解到,MyBatis 底层将 SQL 的执行交过了 Executor 来处理,而 Executor 又利用 StatementHandler、ParameterHandler、ResultSetHandler 这三个核心的操作类来处理 SQL 的实际执行。
所谓的插件,更确切的说就是拦截器,拦截上述 Executor、StatementHandler、ParameterHandler、ResultSetHandler 的相关方法,在执行前后加上我们想要的逻辑即可。
插件机制的核心是通过实现 MyBatis 提供的 Interceptor
接口来创建插件。这个接口包含了三个方法:
intercept
:这是拦截方法调用的核心方法。在这个方法中,你可以对被拦截的方法进行增强或者修改。你可以在方法执行前后执行自定义逻辑,也可以决定是否执行原始方法。plugin
:这个方法主要用于拦截器链式的传递,包装目标对象并返回一个代理对象。该代理对象会拦截目标对象的方法调用,以便在方法调用前后执行插件逻辑。setProperties
:这个方法用于设置插件的属性。这些属性可以在插件配置时进行设置,并在插件初始化时传递给插件实例。
要创建一个自定义的插件,你需要实现 Interceptor
接口,并在插件的配置中指定该插件。在配置文件中,你可以使用 <plugins>
元素来指定插件,并在插件元素中使用 <plugin>
元素来定义每个插件的类和属性。
定义拦截器类
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
})
public class HelloPlugin implements Interceptor {
private String message;
/**
* 执行的拦截逻辑
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("传递的属性 message = " + message);
System.out.println("开始执行 SQL ...");
Object proceed = invocation.proceed();
System.out.println("SQL 执行完毕 ...");
return proceed;
}
/**
* 拦截器的传递,这个方法就是固定这么写!
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 获取拦截器相关的属性
*/
@Override
public void setProperties(Properties properties) {
// 通过定义成员变量的方式,将属性进行赋值
message = (String) properties.get("message");
}
}
@Intercepts 注解
用于标识一个类是一个 MyBatis 插件,并且指定该插件要拦截的目标方法。它有一个 value 属性,类型为一个 @Signature
数组,用于指定要拦截的目标方法。
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class ExamplePlugin implements Interceptor {
// 插件逻辑实现
}
@Signature 注解
用于定义要拦截的目标方法的签名。它有三个属性:
- type:被拦截方法所在的接口或者类,通常为 Executor(常用)、StatementHandler(最常用)、ParameterHandler(不常用)、ResultSetHandler(不常用)。
- method:要拦截的方法名,通常为 query、update。
- args:要拦截方法的参数列表,注意这里的参数列表必须与上面的 type 类 下的 method 方法的参数列表一模一样,包括顺序!而且要注意导的包对不对…
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
关于 intercept 方法
刚刚介绍导 intercept 方法是拦截调用的核心方法,那么这个方法有一个参数 Invocation invocation
,这个 invocation 就是我们拦截的目标对象、目标方法已经实际运行时的参数。
拦截 StatementHandler 的 prepare 方法
通过分析源码可以得知,StatementHandler 在 query 和 update 之前都会执行 prepare 来构建出 Statement,默认是 PreparedStatement,而我们要执行的 SQL 语句以及一些其他的参数都是放在这个 Statement 中的,所以当我们需要拦截执行,并处理 SQL 的时候,我们拦截 prepare 是非常舒服的,而且观察参数可以看到有数据库的连接对象,那么有了连接对象之后我们就可以像原来的 JDBC 编程一样,为所欲为了。
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
当我们拦截 prepare 方法的时候,得到的 invocation 对象如下所示:
可以明显的看到,接下来将会执行的 SQL 语句以及参数情况。
当我们想要在程序中获取到 SQL 的时候可以一层一层调用 get 方法获取,如下:
RoutingStatementHandler routingStatementHandler = (RoutingStatementHandler) invocation.getTarget();
String sql = routingStatementHandler.getBoundSql().getSql();
我们也可以使用 MyBatis 提供的一个非常方便的反射工具 MetaObject,用这个类我们可以非常方便的获取和赋值目标的所有属性!
// 创建反射的元对象
MetaObject metaObject = SystemMetaObject.forObject(invocation);
// 通过字符串路径的方式即可拿到对应的内容,不需要一层一层的 get
String sql = (String) metaObject.getValue("target.boundSql.sql");
我也可以直接粗暴的修改要执行的 SQL,原来是要执行一个 delete 操作,我直接改成的 select 操作!
metaObject.setValue("target.boundSql.sql", "select * from account where id = ?");
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>
<typeAliases>
<typeAlias type="world.xuewei.mybatis.entity.Account" alias="Account"/>
</typeAliases>
<plugins>
<plugin interceptor="world.xuewei.plugin.HelloPlugin">
<property name="message" value="Hello"/>
</plugin>
</plugins>
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://*.*.*.*/learn?useSSL=false&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/AccountMapper.xml"/>
</mappers>
</configuration>
这里要特别注意
<plugins>
标签的位置,放在别的位置可能 IDEA 就会报错,调整一下位置即可。
测试程序
public class PluginTest {
private SqlSession sqlSession;
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sessionFactory.openSession();
}
@After
public void after() {
sqlSession.commit();
}
@Test
public void testPlugin() {
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
accountDao.getAll().forEach(System.out::println);
}
}
实现原理
拦截器的解析
当我们编写好若干个拦截器后,配置在 MyBatis 的配置文件中,之后 MyBatis 在启动的时候就会去扫描这些拦截器并存储在 Configuration 对象中。通过原有的知识体系 这个解析的过程肯定是由 XMLConfigBuilder
来实现的。
private void parseConfiguration(XNode root) {
try {
// 省略加载 properties、settings、typeAliases 等标签 ...
pluginElement(root.evalNode("plugins"));
// 省略加载 environments、typeHandlers、mappers 等标签 ...
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 按照顺序拿到配置的拦截器
String interceptor = child.getStringAttribute("interceptor");
// 拿到通过 properties 标签为拦截器配置的属性
Properties properties = child.getChildrenAsProperties();
// 反射创建拦截器对象在
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// 创建完对象后调用 setProperties 赋值属性
interceptorInstance.setProperties(properties);
// 添加到 Configuration 对象
configuration.addInterceptor(interceptorInstance);
}
}
}
再次梳理一下 XMLConfigBuilder
的处理过程:
-
parseConfiguration 处理 MyBatis 配置文件中的各个顶级标签,其中就有处理插件的方法 pluginElement。
-
pluginElement 按照顺序解析 plugins 中的 interceptor,通过反射创建对象。
-
完成属性的赋值操作(setProperties),这个也就是我们在自定义插件的时候要实现的 setProperties 方法,通常我们要在插件中定义成员变量去接收。
可以看到,在 MyBatis 启动的时候就已经将属性赋值给了插件!
-
将创建并赋值好的插件添加到 Configuration 核心配置对象中。
接下来要执行的一系列方法:
public class Configuration {
// ...
protected final InterceptorChain interceptorChain = new InterceptorChain();
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
// ...
}
- Configuration 中维护了一个拦截器链(InterceptorChain)的对象,添加拦截器的时候本质上是添加给了这个对象。
public class InterceptorChain {
// 本质上就是一个 ArrayList<Interceptor>
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
// 这个方法就比较关键了!表示将这些插件应用到目标对象
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 按照顺序应用到目标对象,注意这里调用的竟然是我们在拦截器中定义的 plugin 方法!
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
- InterceptorChain 本质上维护了一个
ArrayList<Interceptor>
存储所有的插件。其中还有一个非常关键的方法 pluginAll,稍后我们会详细分析。
至此拦截器的解析工作已经完成了!
- 解析工作首先由 XMLConfigBuilder 开始,在其核心的解析方法 parseConfiguration 中有一个步骤就是用来解析插件,也就是 pluginElement 方法
- pluginElement 方法会按照在 XML 文件中定义的顺序,依次通过反射创建拦截器对象,并完成
<property>
属性的赋值,并将拦截器对象添加给 Configuration 对象,整个过程都是在 for 循环中完成。 - Configuration 本身又是维护了一个拦截器链(InterceptorChain)的对象,其实是将拦截器添加给了它。InterceptorChain 其实就是一个
ArrayList<Interceptor>
,最后其实所有的拦截器都在这个列表里面。
拦截器的应用
拦截器的解析过程完成后将所有的拦截器放置在了 Configuration 下的 InterceptorChain 下的 interceptors 中。
那么文章最开始我们说过,拦截器可以拦截的对象有四个:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
为什么是这四个?这些拦截器是怎么应用到这四类对象上的?接下来我们就来看看这里的实现。
InterceptorChain 中有个非常关键的方法 pluginAll,接收一个 Target 目标,然后将所有的拦截器依次应用到这个目标。那我们就可以看看哪些地方用到了这个方法:
最开始的知识内容介绍导 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四个对象的创建的创建都是交给了 Configuration 对象,而创建的过程中就将这插件应用到了对应的 Executor、StatementHandler、ParameterHandler、ResultSetHandler!
除了这四个地方应用了插件,再也没有其他地方调用了这个方法,所以 MyBatis 的拦截器也只能拦截到这四类对象的方法。
这里扩展一下,Executor、StatementHandler、ParameterHandler、ResultSetHandler 是什么使用创建的呢?
Executor
我们原来在测试程序中都写过这样一段代码:
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sessionFactory.openSession();
}
而 Executor 的创建就是在这个 openSession 方法中,SqlSessionFactory 这个接口的默认实现类是 DefaultSqlSessionFactory,通过源码跟踪可以看到最后会执行 DefaultSqlSessionFactory 的 openSessionFromDataSource,而这个方法中就创建了 Executor。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 这里调用了 Configuration 的方法,创建了 Executor
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
StatementHandler
我们知道,SqlSession 将 SQL 的处理和执行交给了 Executor,而 Executor 又向下传递给了 StatementHandler,所以 StatementHandler 的创建必然是要交给实际的 Executor 来创建。
查阅源码可以发现,StatementHandler 是在 Executor 实际要执行 SQL 操作的时候(更新、查询)创建。在默认情况下,MyBatis 使用的是 SimpleExecutor,所以默认 StatementHandler 都是从 SimpleExecutor 的 更新和查询相关的方法创建的。
ParameterHandler 和 ResultSetHandler
这两个对象是要搭配着 StatementHandler 共同工作的,所以这三个是绑定的关系。查看源码,只有一个地方同时调用了 Configuration 的 newParameterHandler 和 newResultSetHandler,那就是 StatementHandler 接口的适配器实现类 BaseStatementHandler,他的构造方法创建了 ParameterHandler 和 ResultSetHandler。
拦截器的运行
拦截器的运行是仅跟着上一个步骤,我们再来看一下将拦截器应用在 Executor、StatementHandler、ParameterHandler、ResultSetHandler 的时候,调用的 pluginAll 方法:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 按照顺序应用到目标对象,注意这里调用的竟然是我们在拦截器中定义的 plugin 方法!
target = interceptor.plugin(target);
}
return target;
}
这里调用的是我们在拦截器中定义的 plugin 方法,而这个 plugin 方法刚刚说的他这个方法其实就是一个固定的写法:
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
调用的 Plugin 类的静态包装 wrap 方法,返回目标对象和当前拦截器包装后的插件 Plugin 类代理对象(JDK 动态代理),然后在 pluginAll 方法中再次包装,层层套娃(就像洋葱一样)…
Executor、StatementHandler、ParameterHandler、ResultSetHandler 通过 pluginAll 最后得到的是若干个拦截器层层包装得到的代理对象!
**所以在执行的时候是先执行最外层(最后配置)的拦截器,一层一层往里面走,直到执行目标对象的方法,然后从目标对象返回,也是从里面网外层访问!**如下:
拦截器 C 执行开始...
拦截器 B 执行开始...
拦截器 A 执行开始...
目标 Executor 执行...
拦截器 A 执行完毕...
拦截器 B 执行完毕...
拦截器 C 执行完毕...
接下来可以看一下 Plugin 类的具体实现,解密到底是怎么包装的。
public class Plugin implements InvocationHandler {
// 目标对象
private final Object target;
// 拦截器
private final Interceptor interceptor;
// 拦截器的配置拦截策略,拦截哪些类的哪些方法
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
// 获取拦截策略,对我们在拦截器上使用的注解配置的解析处理
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 创建 JDK 动态代理,代理的目标就是将拦截器包装成的插件对象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 调用代理对象的方法就会触发 invoke
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 判断当前执行的方法是不是在拦截器要拦截的方法
if (methods != null && methods.contains(method)) {
// 如果是要拦截的目标,则调用此拦截器的 interceptor 方法,也就是我们自己定义拦截器的核心方法在这里会被执行。
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果不是要拦截的目标,直接放行
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
// 解析自定义拦截器中定义的注解,解析拦截策略
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// 一个拦截器可以配置多个拦截策略
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
// 解析要拦截的类以及对应的方法,维护在 signatureMap 中
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
// 获取目标对象的所有接口定义,根据在拦截器里面配置的类型进行过滤,用于 JDK 动态代理
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)