Mybatis插件原理
本篇文章是通过看视频学习总结的内容, 如有错误的地方请谅解,并联系博主及时修改,谢谢您的阅读.
前言: 根据上一篇博客《mybatis手写分页插件》已经参与过一次手写插件的过程,那么在本篇文章中,主要讲解 mybatis
是如何注册插件并且实现对四大对象的不同方法进行拦截的.
在之前书写 《Mybatis(二) - Mybatis是如何创建出SqlSessionFactory的》 一文中,简单讲述了 mybatis
加载 mybatis-config.xml
并将内容初始化到 Configuration
对象中,因为我们配置的插件是在 xml
文件中,所以最终我们自己的 Interceptor
一定是存在于 Configurator
对象中的,那么继续回顾一下解析xml
拿到所有的Interceptor
的过程
- 1 单元测试,进入
build(inputStream)
方法
@Before
public void prepare() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
- 2 找到
XMLConfigBuilder#parse()
进入
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 进入这里的 parse()
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
- 3 这里就能直观的看到 直接解析
root(/configuration)
节点,然后进入方法parseConfiguration()
中
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 这里直接就拿到 root 节点,然后进入方法parseConfiguration() 中
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
- 4 找到解析
plugins
节点的解析方法,并且进入方法
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
// 插件 ----------------- 这里就是要进入的方法
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
- 5 关键的地方来了,这里就是将
plugins
节点下的所有interceptor
解析出来放入到Configuration
对象中interceptorChain
容器中
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 循环 plugins 父节点,拿到每个子节点
for (XNode child : parent.getChildren()) {
// 拿到 xml 中自己配置的 intercptor 的类全路径
String interceptor = child.getStringAttribute("interceptor");
// 读取到 pugin 节点的子节点,并且包装成 Properties 对象
Properties properties = child.getChildrenAsProperties();
// 这里就是最关键的地方,是将我们配置在 xml上 interceptor属性通过反射,使用构造器创建一个实例
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// 最终调用 configuration 对象中的 addInterceptor,将实例化完成的 interceptor 放入到 interceptorChain容器中
configuration.addInterceptor(interceptorInstance);
}
}
}
// 放入容器
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
- 6 我很好奇
interceptorChain
到底是个什么数据结构,我相信看的人也比较好奇,那么随我一同进去看看吧
/**
* @author Clinton Begin
*/
public class InterceptorChain implements Serializable {
private final List<Interceptor> interceptors = new ArrayList<>();
// 这个方法很重要,后续在初始化 Exector 的时候,就会调用该方法
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);
}
}
- 7
6
小结中的interceptor.plugin(target)
不是很明白,那不如继续进入看看mybatis
是怎么写的.
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
// 调用这里的这个 default 的方法,我们实现的不就是这个 Interceptor 接口中这三个方法吗???
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
- 8 既然自定义
mybatis
插件就是实现的这个三个方法,那么又回过头看看自己自定义的Interceptor
中plugin
方法是怎么写的
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
这里为什么要这么写? 如果不写这个方法,回出现什么情况?
那么在自定义的interceptor
中删除这些方法,又会出现什么情况,我已经删了,并且跑了一次,没任何异常情况,插件也能被使用。为什么呢?
其实啊, 接口中的
default
修饰的方法,是可以不用被实现的,只要某一个地方是调用了Interceptor
接口中的plugin
方法, 那么其实都能被执行的,因为在自定义interceptor
类中plugin
方法和Interceptor
接口中pugin
方法逻辑是一样的,都是调用了Plugin.wrap()
- 9 既然知道了这个方法的重写是可选的,那么应该先知道这个方法到底是怎么被调用的,这个方法内部又做了什么事情,我选择进入方法去看看。
public static Object wrap(Object target, Interceptor interceptor) {
// 这里是解析@Interceptors 注解,拿到被拦截的类和被拦截的方法
// 结合自己的拦截器,这里的内容就是 {Executor.class, [query()] }
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 拿到被拦截的目标类 Executor.class
Class<?> type = target.getClass();
// 拿到被拦截的类的所有实现的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 通过动态代理,创建被拦截的类的实例
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
// 这里的 invocationHandler 是 plugin 对象,只要一旦执行了 Executor#query方法,就会去调用plugin对象中的 invocke() 方法。
new Plugin(target, interceptor, signatureMap));
}
return target;
}
/**
* 这个方法其实就是将拦截的类和方法做映射
* 这里面有个容易被忽略的内容就是 sig.args(),这个参数是自定义Interceptor类上的注解中,传入的参数,
* 其目的是为了在自定义Intecptor 中能使用到这些参数。这些参数也是有规则的,不能随意的传入。
*/
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<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
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;
}
wrap()
方法其实的作用,就是将所有的被拦截到的类(@Interceptor注解去拦截的类,在这里的是Executor类,和被拦截的方法 query)
,并且为这个Executor
生成一个动态代理的类,只要方法执行到为被代理的Executor
的query
方法,那么就来执行代理类InvocationHandler
类的invock()
,接下来再看看Plugin
类中invocke()
干了什么事情.
- 10
Plugin#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)) {
// 很明显,直接去调用实现类的 intercept 方法,并且传入参数 Invocation 对象,这不就是自定义拦截器实现Interceptor接口的intercept 方法吗???
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
恍然大悟,原来插件也可以这么玩儿
总结
mybatis
插件的工作原理其实就是根据动态代理,代理一个被拦截的类,然后在使用到被代理的拦截的方法的时候,会调用动态代理中InvocationHandler
中invoke
方法,invoke
方法再调用实现了Interceptor
接口的intercept
方法,即可完成插件拦截的效果- 本篇博客是博主通过学习,总结的笔记,如有错误的地方,请联系博主及时修改