Mybatis的插件设计你知道多少?
本文主要分为两部分,第一部分我们看插件设计原理和如何从 Mybatis
中学习设计插件,第二部分我们学习如何开发Mybatis
插件。
一、插件设计原理
Mybatis
中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor
来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis
是如何处理插件。
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> 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<Interceptor> interceptors = new ArrayList<Interceptor>();
// 这里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<Class<?>, Set<Method>> 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<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
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()]);
}
@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);
}
}
}
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;
<E> List<E> 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<User> query();
}
这里就是将 Statement
返回值转换成 List<User>
以上就是Mybatis给我们提供插件增强的地方,以及每个地方要做的事情
但是到这里真的会写插件了吗? 我们还必须要参与实践。如果我们要做一个功能将数据库的sql信息打印出来,应该知道在哪里处理了吧,只要获取BoundSql对象打印sql即可。如果我们要写分页那就是对sql后面加上分页的语法,这些说起来简单,其实并不简单,因为 Mybatis
提供对很多数据库的支持, 每个数据库的语法可能还不一样,所以在写插件时候要考虑的东西还是很多的, 如果我们不需要写插件,也没兴趣做开源项目其实了解到这里已经可以了。
n/)
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章