Mybatis的插件设计你知道多少?
本文主要分为两部分,第一部分我们看插件设计原理和如何从 Mybatis
中学习设计插件,第二部分我们学习如何开发Mybatis
插件。
一、插件设计原理
Mybatis
中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor
来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis
是如何处理插件。
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
...
}
名称 | 类型 | 描述 |
---|---|---|
Interceptor |
接口 | 插件都需要实现的接口,封装代理执行方法及参数信息 |
InterceptorChain |
类 | 拦截链 |
InvocationHandler |
接口 | JDK代理的接口,凡是JDK中的代理都要实现该接口 |
@Intercepts |
注解 | 用于声明要代理和 @Signature 配合使用 |
@Signature |
注解 | 用于声明要代理拦截的方法 |
Plugin |
类 | 代理的具体生成类 |
1. Interceptor
插件都需要实现的接口,封装代理执行方法及参数信息
public interface Interceptor {
// 执行方法体的封装,所有的拦截方法逻辑都在这里面写。
Object intercept(Invocation invocation) throws Throwable;
// 如果要代理,就用Plugin.wrap(...),如果不代理就原样返回
Object plugin(Object target);
// 可以添加配置,主要是xml配置时候可以从xml中读取配置信息到拦截器里面自己解析
void setProperties(Properties properties);
}
2. InterceptorChain
拦截链,为什么需要拦截链,假如我们要对A进行代理, 具体的代理类有B和C。 我们要同时将B和C的逻辑都放到代理类里面,那我们会首先将A和B生成代理类,然后在前面生成代理的基础上将C和前面生成的代理类在生成一个代理对象。这个类就是要做这件事 pluginAll
public class InterceptorChain {
private final List interceptors = new ArrayList();
// 这里target就是A,而List中的Interceptor就相当于B和C,通过循环方式生成统一代理类
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
//1. 是否需要代理,需要代理生成代理类放回,不需要原样返回。通过for循环的方式将所有对应的插件整合成一个代理对象
target = interceptor.plugin(target);
}
return target;
}
...
}
3. InvocationHandler
JDK代理的接口,凡是JDK中的代理都要实现该接口。这个比较基础,如果这个不清楚,那么代理就看不懂了。所以就不说了。
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
4. @Intercepts
和 @Signature
这两个注解是配合使用的,用于指定要代理的类和方法。前面①说了,插件的核心逻辑是拦截执行器的方法,那么这里我们看下如何声明要拦截的类和方法。我们看一下分页插件如何声明拦截。
Signature
中 type
就是要拦截的类, method
要拦截的方法, args
要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数)
@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }))
public class MybatisPagerPlugin implements Interceptor {
}
args
要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数)
比如 Executor
中就有2个 query
方法。所以要通过args来确定要拦截哪一个。
Mybatis
这种插件管理模式, 在 Mybatis
的架构中, 是有指定的,并不是说可以拦截任何类的任何方法,。它具体可以拦截什么类及方法,我们可以通过阅读官方文档 查看。
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
但是这种插件管理模式我们项目中也是可以用的。比如看下面例子。
public class Test {
public static void main(String[] args) {
InterceptorChain chain = new InterceptorChain();
PrintInterceptor printInterceptor = new PrintInterceptor();
Properties properties = new Properties();
properties.setProperty("name","https://blog.springlearn.cn");
printInterceptor.setProperties(properties);
chain.addInterceptor(printInterceptor);
Animal person = (Animal) chain.pluginAll(new Person());
String nihao = person.say("nihao");
System.out.println(nihao);
}
public interface Animal{
String say(String message);
String say(String name, String message);
}
public static class Person implements Animal {
public String say(String message) {
return message;
}
public String say(String name, String message) {
return name + " say: " + message;
}
}
@Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class}))
public static class PrintInterceptor implements Interceptor {
private String name;
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println(name + ": before print ...");
Object proceed = invocation.proceed();
System.out.println(name + ": after print ...");
return proceed;
}
@Override
public Object plugin(Object target) {
if (target instanceof Person) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
this.name = properties.getProperty("name");
}
}
}
5. Plugin
代理的具体生成类,解析 @Intercepts
和 @Signature
注解生成代理。
我们看几个重要的方法。
方法名 | 处理逻辑 |
---|---|
getSignatureMap | 解析@Intercepts和@Signature,找到要拦截的方法 |
getAllInterfaces | 找到代理类的接口,jdk代理必须要有接口 |
invoke | 是否需要拦截判断 |
public class Plugin implements InvocationHandler {
//解析@Intercepts和@Signature找到要拦截的方法
private static Map, Set> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map, Set> signatureMap = new HashMap, Set>();
for (Signature sig : sigs) {
Set methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet();
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, Set> signatureMap) {
Set> interfaces = new HashSet>();
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()]);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//执行时候看当前执行的方法是否需要被拦截,如果需要就调用拦截器中的方法
Set 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);
}
}
}
6. 总结
以上就是本篇文章的第一部分,主要讲 "插件设计原理和如何从 Mybatis
中学习设计插件“
原理: 代理 ,并通过 @Intercepts
和 @Signature
配合指定要代理的方法。 注意Mybatis中那些类能指定是有限制的哦。
我们可以通过阅读官方文档 查看。
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
Mybatis
的插件模式,我们在项目中可以直接引入使用。可以参考上面的例子。
二、如何开发Mybatis
插件代码
如何开发 Mybatis
插件,首先要知道原理, Mybatis
的原理前面就说了就是代理核心类的核心方法。前面我们也知道如何定义一个插件了。即就是用 @Intercepts
和 @Signature
来声明要拦截的类和方法。 但是知道这些只能说会定义插件了,具体插件代码怎么写。我们要在看下 Mybatis
官方限制的那几个类都有什么能力。
图片描述的不是很具体,但是大概意思是这样。 下面会一一简述。
1. Executor
public interface Executor {
int update(MappedStatement ms, Object parameter) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
}
数据库操作的第一步就是先调用 Executor
, 如果要对sql语句进行增强 ,或者说是所有操作都进行增强都可以再这个里面处理。
2. ParameterHandler
sql入参会在这里被解析并进行操作,哎呀,这么说真的太抽象了。举例来说
public interface UserMapper {
@Insert("insert into bbs_role (role_id,role_name,created_date,updated_date,created_by,updated_by) values(#{user" +
".roleId}," +
"#{user.roleName},#{user.createdDate},#{user.updatedDate},#{user.createdBy},#{user.updatedBy})")
Integer insert(@Param("user") User user);
}
insert
方法中的user对象,如何填充到 sql
中,就是在 ParameterHandler
里面完成的。
-
第一步将sql中占位符替换成
?
符号, 然后解析参数类型到ParameterMapping
最终这些信息都会在BoundSql
中保存。 总的来说 Sql信息(包括入参的信息)都会放在BoundSql
中保存。 这里我们认识了一个在ORM框架中非常重要的一个类
BoundSql
如果想动态的修改sql就要跟着这个类的步伐。 将已经解析好的sql提交给
PreparedStatement
进行处理。
而ParameterHandler
重要的一步就是将BoundSql
里面的sql及入参的放到PreparedStatement
里面进行数据查询或者其他操作。PreparedStatement
不解释了,学JDBC的时候老师应该都讲过了。
如果要对sql到PreparedStatement的过程进行增强就可以代理整个类。
3. StatementHandler
代理 StatementHandler
能做什么?
前面 ParameterHandler
已经可以将Sql信息写入到 Statement
中,但是调用的逻辑就在 StatementHandler
里面来处理了。如果要对这部分代码做处理就可以拦截该方法。
4. ResultSetHandler
从名字就知道这个是对数据库查询后的记过进行处理的一个类。就是将jdbc的API返回数据转换成方法签名中的返回值。
public interface UserMapper {
@Select("select * from bbs_role")
List query();
}
这里就是将 Statement
返回值转换成 List
以上就是Mybatis给我们提供插件增强的地方,以及每个地方要做的事情
但是到这里真的会写插件了吗? 我们还必须要参与实践。如果我们要做一个功能将数据库的sql信息打印出来,应该知道在哪里处理了吧,只要获取BoundSql对象打印sql即可。如果我们要写分页那就是对sql后面加上分页的语法,这些说起来简单,其实并不简单,因为 Mybatis
提供对很多数据库的支持, 每个数据库的语法可能还不一样,所以在写插件时候要考虑的东西还是很多的, 如果我们不需要写插件,也没兴趣做开源项目其实了解到这里已经可以了。
但是如果感兴趣的话可以关注我哦!
感谢您的阅读,本文由 程序猿升级课 版权所有。如若转载,请注明出处:程序猿升级课(https://blog.springlearn.cn/)