原文链接:"犯罪心理"解读Mybatis拦截器

Mybatis拦截器执行过程解析 文章写过之后,我觉得 “Mybatis 拦截器案件”背后一定还隐藏着某种设计动机,里面大量的使用了 Java 动态代理手段,它是怎样应用这个手段优雅的设计出整个拦截事件的?就像抓到罪犯要了解它犯罪动机是什么一样,我们需要解读 Mybatis拦截器的设计理念:

设计解读

Java 动态代理我们都懂得,我们先用它设计一个基本拦截器

首先定义目标对象接口:

 public interface Target {
public void execute();
}

然后,定义实现类实现其接口:

public class TargetImpl implements Target {
public void execute() {
System.out.println("Execute");
}
}

最后,使用 JDK 动态代理定义一个代理类,用于为目标类生成代理对象:

public class TargetProxy implements InvocationHandler {
private Object target;
private TargetProxy(Object target) {
this.target = target;
} //代理对象生成目标对象
public static Object bind(Object target) {
return Proxy.newProxyInstance(target.getClass() .getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target));
} //
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
System.out.println("Begin");
return method.invoke(target, args);
}
}

这时,客户端调用方式如下:

public class Client {
public static void main(String[] args) { //没被代理之前
Target target = new TargetImpl();
target.execute();
//执行结果:
//Execute //被代理之后
target = (Target)TargetProxy.bind(target);
target.execute();
//执行结果:
//Begin
//Execute
}
}

应用上面的设计方式,拦截逻辑是写死在代理类中的:

public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
//拦截逻辑在代理对象中写死了,这样到这客户端没有灵活的设置来拦截其逻辑
System.out.println("Begin");
return method.invoke(target, args);
}

这样的设计方式不够灵活和高可用,可能满足 ClientA 的拦截需求,但是不能满足 ClientB 的拦截需求,这不是一个好的拦截方案,所以我们需要进一步更改设计方案:

将拦截逻辑封装成一个类,客户端绑定在调用TargetProxy()方法时将拦截逻辑一起作为参数,这样客户端可以灵活定义自己的拦截逻辑,为实现此功能,我们需要定一个拦截器接口 Interceptor

public interface Interceptor {
public void intercept();
}

将代理类做一个小改动,在客户端实例化 TargetProxy 的时候可以传入自定义的拦截器:

public class TargetProxy implements InvocationHandler {

    private Object target;
//拦截器
private Interceptor interceptor; private TargetProxy(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
} //通过传入客户端封装好 interceptor 的方式为 target 生成代理对象,使得客户端可以灵活使用不同的拦截器逻辑
public static Object bind(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target, interceptor));
} public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
//客户端实现自定义的拦截逻辑
interceptor.intercept();
return method.invoke(target, args);
}
}

通过这样,就解决了“拦截内容固定死”的问题了,再来看客户端的调用方式:

//客户端可以在此处定义多种拦截逻辑
Interceptor interceptor = new Interceptor() {
public void intercept() {
System.out.println("Go Go Go!!!");
}
};
target = (Target)TargetProxy.bind(target, interceptor);
target.execute();

上面的 interceptor() 是个无参方法,难道犯罪分子冒着生命危险拦截目标只为听目标说一句话 System.out.println(“Go Go Go!!!”)? 很显然它需要了解目标行为(Method)和注意目标的身外之物(方法参数),继续设置"圈套",将拦截接口做个改善:

public interface Interceptor {
public void intercept(Method method, Object[] args);
}

同样需要改变代理类中拦截器的调用方式,将 method 和 args 作为参数传递进去

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//拦截器拿到method和args信息可以做更多事情,而不只是打招呼了
interceptor.intercept(method, args);
return method.invoke(target, args);
}

进行到这里,方案看似已经不错了,静待客户上钩,但这违背了做一名有追求罪犯的基本原则:「迪米特法则」

迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解, 不和陌生人说话。英文简写为: LoD,是一种解耦的方式.

上面代码中,method 需要知道 target 和 args;interceptor 需要知道 method 和 args,这样就可以在 interceptor 中调用 method.invoke,但是拦截器中并没有 invoke 方法需要的关键参数 target,所以我们将 target,method,args再进行一次封装成 Invocation类,这样拦截器只需要关注 Invocation 即可.

public class Invocation {
private Object target;
private Method method;
private Object[] args; public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
} //成员变量尽可能在自己的内部操作,而不是 Intereptor 获取自己的成员变量来操作他们
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
} public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
}

这样拦截器接口变了样子:

public interface Interceptor {
public Object intercept(Invocation invocation)throws Throwable ;
}

代理类也随之做了改变:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return interceptor.intercept(new Invocation(target, method, args));
}

这样客户端调用,在拦截器中,拦截器写了自己拦截逻辑之后,执行 invocation.proceed() 即可触发原本 target 的方法执行:

Interceptor interceptor = new Interceptor() {
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
};

到这里,我们经过一系列的调整和设计,结果已经很好了,但仔细想,这种拦截方式会拦截 target 的所有方法,假如 Target 接口有多个方法:

public interface Target {
/**
* 去银行存款
*/
public void execute1(); /**
* 倒垃圾
*/
public void execute2();
}

以上两个方法,当然是拦截 target 去银行存款才是利益价值最大化的拦截,拦截 target 去倒垃圾有什么用呢?(避免没必要的拦截开销),所以我们标记拦截器只有在发生去银行存款的行为时才采取行动,先自定义一个注解用来标记拦截器

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MethodName {
public String value();
}

在拦截器实现类上添加该标识:

//去银行存款时拦截
@MethodName("execute1")
public class InterceptorImpl implements Interceptor {
...
}

修改代理类,如果注解标记的方法是否与 method 的方法一致,则执行拦截器:

public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
MethodName methodName = this.interceptor.getClass().getAnnotation(MethodName.class);
if (ObjectUtils.isNull(methodName)){
throw new NullPointerException("xxxx");
}
//如果方法名称和注解标记的方法名称相同,则拦截
String name = methodName.value();
if (name.equals(method.getName())){
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(this.target, args);
}

到这里,户端的调用变成了这个样子:

Target target = new TargetImpl();
Interceptor interceptor = new InterceptorImpl();
target = (Target)TargetProxy.bind(target, interceptor);
target.execute();

从上面可以看出,客户端第一步创建 target 对象和 interceptor 对象,通过传入 target 和 interceptor 调用 bind 方法生成代理对象,最终代理对象调用 execute 方法,根据迪米特法则,客户端不需要了解 TargetProxy,只需要关注拦截器的内部逻辑和可调用的方法即可,所以我们需要继续修改设计方案,添加 register(Object object)方法,:

public interface Interceptor {
public Object intercept(Invocation invocation) throws Throwable ;
public Object register(Object target);
}

修改拦截器的实现,拦截器对象通过调用 register 方法为 target 生成代理对象:

@MethodName("execute1")
public class InterceptorImpl implements Interceptor { public Object intercept(Invocation invocation)throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
} public Object register(Object target) {
return TargetProxy.bind(target, this);
}
}

现在,客户端调用变成了这个样子:

Target target = new TargetImpl();
Interceptor interceptor = new InterceptorImpl(); target = (Target)interceptor.register(target);
target.execute1();

客户端只需要实例化拦截器对象,并调用拦截器相应的方法即可,非常清晰明朗

一系列的设计改变,恰巧符合 Mybatis拦截器的设计思想,我们只不过用一个非常简单的方式去理解它

Mybatis 将自定义的拦截器配置添加到 XML 文件中,或者通过注解的方式添加到上下文中,以 XML 形式举例:

 <plugins>
<plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />
</plugins>

通过读取配置文件,将所有拦截器都添加到 InterceptorChain 中

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 该方法和我们上面自定义拦截器中 register 方法功能一致
target = interceptor.plugin(target);
}
return target;
} public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
} public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
} }

但 Mybatis 框架逻辑限制,只能为:ParameterHandler,ResultSetHandler,StatementHandler 和 Executor 创建代理对象

我们在此将我们的简单实现与 Mybatis 实现的核心内容做个对比:

生成代理对象:

拦截指定方法,如果找不到方法,抛出异常:

执行目标方法:

到这里,没错,犯罪现场完美推测出,真相就是这样!!!

墙裂建议先看 Mybatis拦截器执行过程解析 ,然后回看该文章,了解 Mybatis 拦截器的整个设计动机与理念,大道至简.

灵魂追问

  1. 除了迪米特设计原则,你还知道哪些设计基本原则?
  2. 你在编写代码时,考虑过怎样利用那些设计原则来规范自己代码吗?
  3. ...

推荐阅读

  1. 不得不知的责任链设计模式
  2. Mybatis拦截器执行过程解析
  3. 如何使用Mybatis的拦截器实现数据加密与解密
  4. 如何设计好的RESTful API
  5. 轻松高效玩转DTO(Data Transfer Object)

那些可以提高效率的工具

关注公众号了解更多可以提高工作效率的工具,同时带你像看侦探小说一样趣味学习 Java 技术


"犯罪心理"解读Mybatis拦截器的更多相关文章

  1. Mybatis拦截器

    Mybatis拦截器

  2. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  3. MyBatis拦截器原理探究

    MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...

  4. Mybatis拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...

  5. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

  6. 数据权限管理中心 - 基于mybatis拦截器实现

    数据权限管理中心 由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手 需求场景 第一种场景:行级数据处理 原sql: select ...

  7. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  8. 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离

    前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...

  9. 【Mybatis】1、Mybatis拦截器学习资料汇总

    MyBatis拦截器原理探究 http://www.cnblogs.com/fangjian0423/p/mybatis-interceptor.html [myBatis]Mybatis中的拦截器 ...

随机推荐

  1. Java网络编程注意事项1

    网络编程的基础知识 什么是计算机网络,就是把分布在不同地理区域的计算机与专门的外部设备通信线路互连成一个规模大.功能强的网络系统. 计算机网络主要能做些下面功能: 1)资源共享 2)信息传输与集中处理 ...

  2. events(事件): 基础1

    1    所有能触发事件的对象都是 EventEmitter 类的实例. 这些对象开放了一个 eventEmitter.on() 函数,允许将一个或多个函数绑定到会被对象触发的命名事件上. 事件名称通 ...

  3. 【 D3.js 入门系列 --- 0 】 简介及安装

    家是我的个人博客: http://www.ourd3js.com/  ,csdn博客首页为:http://blog.csdn.net/lzhlzz/.转载请注明出处,谢谢. D3的全称是(Data-D ...

  4. javascript中间AJAX

    兼容访问XMLHttpRequest物: var xhr = null; if(window.XMLHttpRequest){ //非IE浏览器 xhr = window.XMLHttpRequest ...

  5. [转] Java的打包apk, jar、war、ear包

    apk, war, ear可用zip压缩,看起来这四个包都是用简单方式zip/jar即可生成. ---------------------------------------------------- ...

  6. 显示dll里的QWidget

    1 新建库->C++库 2 命名(此处为mydll)并选择共享库--下一步--下一步 3 选择所需要的模块(有使用到的都选上)此处勾选前三项QtCore+QtGui+QtWidgets 4 完成 ...

  7. java学习笔记(6)——序列化

    一.序列化与基本类型序列化 1,将类型int转换为4byte,或将其它数据类型(如long->8byte)的过程, 即将数据转换为n个byte序列叫序列化(数据->n byte) 如:0x ...

  8. android延时处理任务范例

    今天要做一个任务,要求图片做button开关,点击出发对应事件.点击打开,图片左边显示几行字体,这几行字体是延时显示的.以下将主要代码附上.以下是main.xml <?xml version=& ...

  9. 卷积(convolution)与相关(correlation)(matlab 实现)

    1. 卷积(convolution) 输出 y(n) 是作为在 x(k) 和 h(n−k)(反转和移位)重叠之下的样本和求出的. 考虑下面两个序列: x(n)=[3,11,7,0,−1,4,2],−3 ...

  10. TCP 三次握手(相当于寄信需要回执,第一次握手:我寄给你一封信。第二次握手:你回我一封信。第三次握手:我再给你一个回执,这样你才能确认我收到信了)

    TCP 连接是通过三次握手进行初始化的.三次握手的目的是同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.以下步骤概述了通常情况下客户端计算机联系服务器计算机的过程: 1. 客户端向服务器发送 ...