Mybatis源码分析之插件的原理
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。
默认情况下,可以使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
Mybatis提供的插件很强大,可以由外切入到底层的核心模块,所以用起来要非常小心,至少要熟悉一些底层的原理。
但是使用起来还是很简单,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
接着来剖析下插件的原理。
配置与解析
以开源的某个分页插件为例:
<plugins>
<!-- 分页操作对象 -->
<plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
<property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.MySQLDialect" />
</plugin>
</plugins>
解析配置文件:
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("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);
}
}
处理插件元素:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
Configuration中持有interceptorChain:
protected final InterceptorChain interceptorChain = new InterceptorChain();
把配置文件中的插件添加到InterceptorChain中:
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
InterceptorChain中持有Interceptor集合,每个Interceptor都被添加到这个集合中。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
}
切入并执行
拦截器接口:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
DefaultSqlSessionFactory
final Executor executor = configuration.newExecutor(tx, execType);
使用Configuration的newExecutor方法来创建Executor:
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor 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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
同理还有
- Configuration#newParameterHandler
- Configuration#newResultSetHandler
- Configuration#newStatementHandler
就是对应上面的几处埋点,在Configuration中构造的时候使用了下面的方法挂载插件:
executor = (Executor) interceptorChain.pluginAll(executor);
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
这是一个层层包裹的过程,后面的拦截器会不断包装前面的。最后一个拦截器的方法会优先执行,然后层层代理。
一般在插件的XxInterceptor实现中,会包装一个代理类:
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class OffsetLimitInterceptor implements Interceptor {
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//......
}
Plugin类的包装方法:
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) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
这个包装方法首先会获取拦截器的相关注解(Intercepts,Signature),构造一个
Map
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()]);
}
如果指定的type有接口就创建代理,否则返回其本身。
根据Plugin的实现:
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);
}
}
首先检查签名集合中是否包含指定的方法,如果没有则直接调用目标方法。
如果匹配上就new一个Invocation传给interceptor的intercept方法,到时候在自定义的interceptor中可以直接通过invocation.proceed()调用目标方法:
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
有点类似过滤器的放行。
插件的实现就是在几处需要拦截的地方用动态代理技术来生成代理类,以拦截器的形式完成方法的增强。根据@Signature注解的type元素的接口有无来判断是否生成代理类,根据指定方法和注解的method是否一致来决定是否调用拦截器方法。
使用动态代理技术让我们可以切入到底层的实现却不用修改底层的代码,就像一个楔子插进去也可以拔出来,这就是所谓的插件。
插件相关的总结
插入
- 插件是通过JDK动态代理实现的
这是插件的原理和核心。
了解了动态代理的机制,再写个例子看下JDK为我们生成的代理类,可以有个更清晰的把握。
- MetaObject在插件中的运用
可以通过MetaObject利用反射完成相关属性的赋值,偷梁换柱达到我们的目的。
不管有多少拦截器,只要满足条件,就是一层层的代理,我们可以层层剥离它,如针对StatementHandler的prepare方法的一个插件:
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 层层剥离
while (metaObject.hasGetter("h")) {
Object object = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(object);
}
只要它含有h属性,就说明它是一个代理(生成的代理类是继承自Proxy的,所以继承了InvocationHandler类型的h)。
具体代码可以点此查看:
模拟分页插件
拔出
- Invocation就是让方法切回去
Invocation封装了让原方法执行的必要参数,并提供了一个proceed方法:
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
当你拦截了指定方法,做了该做的事之后,调用Invocation的proceed方法可以让框架继续执行。
- 方法切回去后是否达到目的
当我们为了达到某个目的写了一个插件,要考虑当前的一系列操作是否会对后面方法的执行造成影响。
有时候我们需要更改某些东西,这些东西改完是否能顺利set进去(几大关键类提供的set方法不多);有时候是想获取某些东西,不能影响后续的执行。这些都要对底层的方法有所了解。
Mybatis源码分析之插件的原理的更多相关文章
- 【MyBatis源码分析】插件实现原理
MyBatis插件原理----从<plugins>解析开始 本文分析一下MyBatis的插件实现原理,在此之前,如果对MyBatis插件不是很熟悉的朋友,可参看此文MyBatis7:MyB ...
- MyBatis源码分析(2)—— Plugin原理
@(MyBatis)[Plugin] MyBatis源码分析--Plugin原理 Plugin原理 Plugin的实现采用了Java的动态代理,应用了责任链设计模式 InterceptorChain ...
- MyBatis 源码分析 - 插件机制
1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...
- MyBatis 源码分析 - 缓存原理
1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...
- 精尽MyBatis源码分析 - 插件机制
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- Mybatis源码分析--关联表查询及延迟加载原理(二)
在上一篇博客Mybatis源码分析--关联表查询及延迟加载(一)中我们简单介绍了Mybatis的延迟加载的编程,接下来我们通过分析源码来分析一下Mybatis延迟加载的实现原理. 其实简单来说Myba ...
- MyBatis源码分析(各组件关系+底层原理
MyBatis源码分析MyBatis流程图 下面将结合代码具体分析. MyBatis具体代码分析 SqlSessionFactoryBuilder根据XML文件流,或者Configuration类实例 ...
- Mybatis源码分析之Cache二级缓存原理 (五)
一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Ser ...
- MyBatis 源码分析系列文章合集
1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...
随机推荐
- LeetCode-Sort List[AC源码]
package com.lw.leet4; /** * @ClassName:Solution * @Description: * Sort List * Sort a linked list in ...
- Excel公式
多个IF =IF(ISNUMBER(SEARCH("lz",E7)),"lz", IF(ISNUMBER(SEARCH("zch",E7)) ...
- NOIP模拟赛9
T1U3348 A2-回文数 https://www.luogu.org/problem/show?pid=U3348 考场上钻了牛角尖了,然后0分 #include<cstdio> #i ...
- dubbo可通过指定Url方式绕过注册中心直连指定的服务地址
开发.测试环境可通过指定Url方式绕过注册中心直连指定的服务地址,避免注册中心中服务过多,启动建立连接时间过长,如: <dubbo:reference id="providerServ ...
- 遍历hashmap
转]Java中HashMap遍历的两种方式原文地址: http://www.javaweb.cc/language/java/032291.shtml 第一种: Map map = new HashM ...
- GridControl详解(九)表格中的控件
选择完成控件后,可用+号点开ColumnEdit列,改控件的类型是RepositoryItem类型的,其相应的属性和相应的控件属性是类似的 构建数据如下: DataTable dt = new Dat ...
- 关于NuGet
一.NuGet是什么? NuGet是Microsoft开发平台的程序集包管理器,它由客户端工具和服务端站点组成,客户端工具提供给用户管理和安装/卸载软件程序包,以及打包和发布程序包到NuGet服务端站 ...
- (值类型引用类型)和null的关系
1.null null表示变量没有指向任何对象. 2.值类型 包括 bool.结构体.枚举.int.double.float等等 .在.NET中值类型都继承自ValueType. 3. 引 ...
- 浅析 Spring Aop
aop也是Spring里比较重要的一个点,最近把源码看了下,这里总结一下 使用上主要就下面的点注意下: 相关的Annotaion Around Before After AfterReturning ...
- Discrete Logging(POJ2417 + BSGS)
题目链接:http://poj.org/problem?id=2417 题目: 题意: 求一个最小的x满足a^x==b(mod p),p为质数. 思路: BSGS板子题,推荐一篇好的BSGS和扩展BS ...