简言:今天进行第八天的记录(只是写了八天)。有的时候看的多,有的时候看的少,看的少的时候就攒几天一起写了。而今天这个插件我昨天写了一下午,下班没写完就回去了,今天把尾收了,再加上一个过程图方便下面原理的理解。我这个特别不爱看书的人都看半个多月了,希望我一个月真能养成个戒不掉的习惯~

第八章 插件

  在前一篇介绍了四个对象,是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使用及原理)的更多相关文章

  1. 互联网轻量级框架SSM-查缺补漏第六天【级联+延迟加载特辑】

    简言:本来这是昨天看的,但是因为想好好写一下[级联]这个东西,所以就看完之后今天来整理一下. 级联 1. 什么是级联 级联是一个数据库实体的概念.比如教师就需要存在学生与之对应,这样就有教师学生表,一 ...

  2. 互联网轻量级框架SSM-查缺补漏第一天

    简言:工欲其事必先利其器,作为一个大四的准毕业生,在实习期准备抽空补一下基础.SSM框架作为互联网的主流框架,在会使用的基础上还要了解其原理,我觉得会对未来的职场会有帮助的.我特意的买了一本<J ...

  3. Java EE互联网轻量级框架整合开发— SSM框架(中文版带书签)、原书代码

    Java EE互联网轻量级框架整合开发 第1部分 入门和技术基础 第1章 认识SSM框架和Redis 2 1.1 Spring框架 2 1.2 MyBatis简介 6 1.3 Spring MVC简介 ...

  4. Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解

    上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定 ...

  5. Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  6. Android查缺补漏(IPC篇)-- 款进程通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  7. Android查缺补漏(线程篇)-- AsyncTask的使用及原理详细分析

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8515304.html 一.AsyncTask的使用 AsyncTask是一种轻 ...

  8. Android查缺补漏(IPC篇)-- 进程间通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  9. CSS查缺补漏篇

    前面的话:关于CSS,之前我已经做过一些基础的知识点介绍.CSS主要是用来给页面设置样式的,一般说来,在一个网站中,CSS应该独立封装在一个单独的.css外部文件中.样式的设置总体来说是不难的,但是需 ...

随机推荐

  1. css3边角旋转

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. “全栈2019”Java第七十九章:类中可以嵌套接口吗?

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  3. “全栈2019”Java第七十八章:内部类可以继承其他类吗?

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  4. “全栈2019”Java第六十二章:接口与常量详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  5. FPGA基础学习(8) --内部结构之存储单元

    目录 1. 基本结构 2. BRAM与DRAM的比较 3. BRAM的特点 4. Block Memory的使用 4.1 配置为RAM或ROM 4.2. 配置为FIFO 参考文献: 上一篇中提到了SL ...

  6. appium关于当前网络情况测试,实现打开关闭网络(python3.4版)

    appium关于当前网络情况测试,实现打开关闭网络(不需要root测试机) # python from appium.webdriver.connectiontype import Connectio ...

  7. Jquery ajax, Axios, Fetch区别

    1.   Jquery ajax, Axios, Fetch区别之我见 2.   ajax.axios.fetch之间的详细区别以及优缺点

  8. 浅谈Supermap iClient for JavaScript 弹窗类

    地图作为信息的载体和呈现方式,是GIS的重要组成部分,它是一个浏览信息的窗口,在信息日益发达的今天 ,各种地图应用如雨后春笋一般出现在大众眼前,而不是像以往一样太过局限于专业的领域.而弹窗,是作为地图 ...

  9. 我的Python升级打怪之路【五】:Python模块

    模块,是一些代码实现了某个功能的集合 模块的分类: 自定义模块 第三方模块 内置模块 导入模块 import module from module.xx.xx import xx from modul ...

  10. Linux du与df命令的差异

    今天上午查看磁盘空间,df命令查看的时候:93%,du命令查看的时候:90%.回想起昨天在用ftp传输过程中,rm掉文件,应该是文件虽然表明上删除掉了,但是空间实际是未释放的. 由于du与df命令实施 ...