互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)
简言:今天进行第八天的记录(只是写了八天)。有的时候看的多,有的时候看的少,看的少的时候就攒几天一起写了。而今天这个插件我昨天写了一下午,下班没写完就回去了,今天把尾收了,再加上一个过程图方便下面原理的理解。我这个特别不爱看书的人都看半个多月了,希望我一个月真能养成个戒不掉的习惯~
第八章 插件
在前一篇介绍了四个对象,是SqlSession执行过程中通过他们来完成数据库操作和结果返回的。(Executor、StatementHandler、ParameterHandler、ResultSetHandler)。我昨天的查缺补漏有记载,要是想深入了解就可以去查资料了,或者看我这本书的第七章(书名《JavaEE 互联网轻量级框架整合开发》)
插件的原理就是在四大对象调度时插入我们的代码去执行一些特殊的要求以满足特殊的场景需求。
斜体字是部分原理。我先在这插入一张流程图,如果有疑问可以看到最后再返回来琢磨一下这个图。
使用方法:(举例:要想在预编译之前在控制台上打印一些东西,就需要拦截执行SQL的StatementHandler对象的预编译方法,也就是prepare方法)
在MyBatis中使用插件,就必须实现interceptor接口,实现它的三个方法(代码中有注释,应该能知道啥意思):
package com.ssm.chapter8.plugin;
import java.sql.Connection;
import java.util.Properties; import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.log4j.Logger; @Intercepts({
@Signature(
type=StatementHandler.class,
method="prepare",
args = {Connection.class,Integer.class}
)
})
public class MyPlugin implements Interceptor {
private Logger log = Logger.getLogger(MyPlugin.class);
private Properties props = null;
/**
* 插件方法,它代替StatementHandler的prepare方法
*
* @param invocation 入参
* @return 返回预编译后的preparedStatement
* @throws Throwable 异常
* */
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
//进行绑定
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
Object object = null;
/*分离代理对象链(由于目标类可能被多个拦截器[插件]拦截,从而形成多次代理,通过循环可以分离出最原始的目标类)*/
while(metaStatementHandler.hasGetter("h")){
object = metaStatementHandler.getValue("h");
metaStatementHandler = SystemMetaObject.forObject(object);
}
statementHandler = (StatementHandler)object;
String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
Long parameterObject = (Long)metaStatementHandler.getValue("delegate.boundSql.parameterObject"); log.info("执行的SQL:【"+sql+"】");
log.info("参数:【"+parameterObject+"】");
log.info("before......");
//如果当前代理的事一个非代理对象,那么它就会回调用真实拦截对象的方法
//如果不是,那么它就会调度下一个插件代理对象的invoke方法
Object obj = invocation.proceed();
log.info("after......");
return obj;
}
/**
* 生成代理对象
*
* @param target 被拦截对象
* @return 代理对象
* */
public Object plugin(Object target) {
// 采用系统默认的Plugin.wrap方法生成
return Plugin.wrap(target, this);
}
/**
* 设置参数,MyBatis初始化时,就会生成插件实例,并调用这个方法
*
* @param props 配置参数
* */
public void setProperties(Properties props) {
this.props = props;
log.info("dbType = "+this.props.getProperty("dbType")); }
}
标黄的三个就是要去实现的方法:
- intercept:英译:拦截。简单说就是拦截签名(指类名上面的注解@Signature)中对应类中对应方法。参数Invocation就是被拦截内容的整合。
- plugin:英译:插件(计算机名词)。target是被拦截的对象
- setProperties:设置参数。这个参数是要在XML配置中配置的。
注解@Intercepts说明它是一个拦截器。@Singnature是注册拦截器签名的地方,只有签名满足要求才能拦截,type可以是四大对象中的一个,这里是StatementHandler。method代表要拦截四大对象的某一接口方法,而args则表示该方法的参数(要根据拦截对象的方法进行拦截。)下面贴一段StatementHandler中prepare方法的定义代码。
public abstract Statement prepare(Connection connection, Integer integer) throws SQLException;
所以args中是一个Connection.class和 Integer.class。拦截后,通过 Invocation对象可以反射调度原来对象的方法。贴一段 Invocation的源代码。
public class Invocation { public Invocation(Object target, Method method, Object args[]) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
private final Object target;
private final Method method;
private final Object args[];
}
一看就知道 Target是被拦截的目标对象,Method是被拦截的方法,Args就是注解中的参数,这里的proceed方法是通过反射调用原对象中的方法。
配置XML:注意MyBatis配置XML中标签的顺序。
<plugins><!-- 插件 -->
<plugin interceptor="com.ssm.chapter8.plugin.MyPlugin">
<property name="dbType" value="mysql"/>
</plugin>
</plugins>
我使用的查询配置:
<select id="getRole" parameterType="Long" resultType="role">
select id,role_name as roleName,note from t_role where id = #{id}
</select>
执行结果:
DEBUG - Reader entry: <?xml version="1.0" encoding="UTF-8"?>
DEBUG - Checking to see if class com.learn.ssm.chapter3.mapper.RoleMapper matches criteria [is assignable to Object]
DEBUG - Cache Hit Ratio [com.learn.ssm.chapter3.mapper.RoleMapper]: 0.0
DEBUG - Opening JDBC Connection
DEBUG - Created connection 407858146.
DEBUG - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@184f6be2]
INFO - 执行的SQL:【select id,role_name as roleName,note from t_role where id = ?】
INFO - 参数:【1】
INFO - before......
DEBUG - ==> Preparing: select id,role_name as roleName,note from t_role where id = ?
INFO - after......
DEBUG - ==> Parameters: 1(Long)
DEBUG - <== Total: 1
DEBUG - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@184f6be2]
DEBUG - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@184f6be2]
DEBUG - Returned connection 407858146 to pool.
标红为Intercept方法中log.info打印的东西。在before和after中间可以清晰看出,进行了预编译,也就是调用了原有的prepare。
总结:
梳理一下思路,在介绍一下没提到的部分原理:
按照程序的进行,在构建Configuration的时候,插件对象被创建。下面是XMLConfigBuilder部分代码。
private void parseConfiguration(XNode root) {
try {
/*省略*/
pluginElement(root.evalNode("plugins"));
/*省略*/
} catch (Exception e) {
throw new BuilderException((new StringBuilder()).append("Error parsing SQL Mapper Configuration. Cause: ")
.append(e).toString(), e);
}
}
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
Interceptor interceptorInstance;
for (Iterator iterator = parent.getChildren().iterator(); iterator.hasNext(); configuration
.addInterceptor(interceptorInstance)) {
XNode child = (XNode) iterator.next();
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
}
}
}
在初始化插件的时候,调用pluginElement方法。在pluginElement方法中使用反射技术生成插件对应的插件实力,然后调用插件方法中的setProperties方法,设置我们配置的参数,将插件实例保存到配置对象中,以便读取和使用它。所以插件的实力对象是一开始就被初始化的,而不是用的时候才初始化。
其实反射得来的插件实例被存储到interceptorChain中(Chain是链的意思),贴段流程。
插件准备好了,接下来就是利用插件拦截了。在configuration类中。四大对象的初始化方法代码,这里用到了interceptorChain的另一个方法:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
} public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
} public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType != null ? executorType : defaultExecutorType;
executorType = executorType != null ? executorType : ExecutorType.SIMPLE;
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);
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
注意观察标红的代码,interceptorChain.pluginAll方法就是就是用来便利所有的插件,校验是否拦截该对象。
public Object pluginAll(Object target) {
for (Iterator iterator = interceptors.iterator(); iterator.hasNext();) {
Interceptor interceptor = (Interceptor) iterator.next();
target = interceptor.plugin(target);
}
return target;
}
plugin方法,也就是最上面例子中我写的方法。
现在就差拦截prepare方法并用intercept方法覆盖了。
我上面写了intercept方法拦截了StatementHandler的prepare方法,四大对象传递给plugin方法后就会返回一个代理对象,在使用代理对象的方法(例子中就是StatementHandler的prepare方法)时候就会进入invoke方法进行逻辑处理,这是代理模式的关键,通过逻辑判断就可以不适用prepare方法而返回intercept方法这就是拦截的地方。自己编写代理类的工作量很大,MyBatis提供了一个常用的工具类Plugin,用来生成代理对象。Plugin类实现了InvocationHandler接口,采用的是JDK的动态代理技术。代码如下:
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
Map 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));
else
return target;
}
public Object invoke(Object proxy, Method method, Object args[]) throws Throwable {
try {
Set methods = (Set) signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method))
return interceptor.intercept(new Invocation(target, method, args));
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
return method.invoke(target, args);
}
}
在例子中,MyPlugin的plugin方法里就是调用Plugin.wrap方法来返回一个代理对象。当代理对象调用prepare方法就会进入invoke方法。Signature就是上面提到的签名,如果存在签名的拦截方法,就会调用intercept方法并返回结果。这样我们想要做的事就做好了~
互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)的更多相关文章
- 互联网轻量级框架SSM-查缺补漏第六天【级联+延迟加载特辑】
简言:本来这是昨天看的,但是因为想好好写一下[级联]这个东西,所以就看完之后今天来整理一下. 级联 1. 什么是级联 级联是一个数据库实体的概念.比如教师就需要存在学生与之对应,这样就有教师学生表,一 ...
- 互联网轻量级框架SSM-查缺补漏第一天
简言:工欲其事必先利其器,作为一个大四的准毕业生,在实习期准备抽空补一下基础.SSM框架作为互联网的主流框架,在会使用的基础上还要了解其原理,我觉得会对未来的职场会有帮助的.我特意的买了一本<J ...
- Java EE互联网轻量级框架整合开发— SSM框架(中文版带书签)、原书代码
Java EE互联网轻量级框架整合开发 第1部分 入门和技术基础 第1章 认识SSM框架和Redis 2 1.1 Spring框架 2 1.2 MyBatis简介 6 1.3 Spring MVC简介 ...
- Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解
上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定 ...
- Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...
- Android查缺补漏(IPC篇)-- 款进程通讯之AIDL详解
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...
- Android查缺补漏(线程篇)-- AsyncTask的使用及原理详细分析
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8515304.html 一.AsyncTask的使用 AsyncTask是一种轻 ...
- Android查缺补漏(IPC篇)-- 进程间通讯之AIDL详解
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...
- CSS查缺补漏篇
前面的话:关于CSS,之前我已经做过一些基础的知识点介绍.CSS主要是用来给页面设置样式的,一般说来,在一个网站中,CSS应该独立封装在一个单独的.css外部文件中.样式的设置总体来说是不难的,但是需 ...
随机推荐
- Sublime Text3 最新版3207 安装及破解
注:原文地址 https://www.abbeyok.com/archives/337 Sublime Text 3最近更新了新版本,最新版本:3207,之前的license无效了,新版破解方法如下: ...
- 各大SRC中的CSRF技巧
本文作者:i春秋签约作家——Max. 一.CSRF是什么? CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/ses ...
- ArchLinux下十八岁的感悟
这次不写技术文章,谈谈自己.现在我们之间没有博主和来访者的关系,我们就是简单聊聊天.你若愿意,就听我说一说. 或者你可以按下crtl+w退出该页面,博主有其他的技术文章.但我希望你能看看最好可以给我留 ...
- 从零开始用 Flask 搭建一个网站(四)
前言 从零开始用 Flask 搭建一个网站(三) 介绍了网页前端与后端.前端与前端之间数据的交流.本节主要介绍一下如何应用 Flask-OAuthlib, 使用 Flask-OAuthlib 就可以轻 ...
- [spring] Ioc 基础
Ioc的理解:调用类对某一接口的实现类的依赖关系又第三方注入,以移除调用类对接口实现类的依赖.又叫做依赖注入.调用者对接口的选择权利被剥夺,交给了第三方.举个例子,学生本来可以选择哪个老师给他上课的, ...
- iOS 本地时间、UTC时间、时间戳等操作、获取当前年月日
//获得当前时间并且转为字符串 - (NSString *)dateTransformToTimeString { NSDate *currentDate = [NSDate date];//获得当前 ...
- (USB HID) Configuration Descriptor
最近完成了HID的基本收發,使用的配置用了2個Endpoint,把一些特別重要要的地方紀錄下來 整個Configuration 分成4大部分 : 1. Configuration 2. Interfa ...
- 【医学影像】《Dermatologist-level classification of skin cancer with deep neural networks》论文笔记
这是一篇关于皮肤癌分类的文章,核心就是分类器,由斯坦福大学团队发表,居然发到了nature上,让我惊讶又佩服,虽然在方法上没什么大的创新,但是论文本身的工作却意义重大,并且这篇17年见刊的文章,引用量 ...
- python全栈开发_day10_函数的实参和形参
一:函数的实参和形参 实参是在调用函数时()出现的外界的实际的值 形参不能再函数外部直接使用 1)实参的两种形式 实参是调用函数时()中传入的参数 1.位置实参 def a(a): print(a) ...
- JAVA学习1:Maven3环境搭建
好长时间不用Java,今天看了下,Maven集成成主流了,在技术水平与日俱进的同时,感叹下IT行业必须有活到老学到老的精神. 先说下环境: Maven:Maven 3.0.5 解压后路径:F:\Mav ...