原文链接:"犯罪心理"解读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. Diffie-Hellman Key Exchange – A Non-Mathematician’s Explanation

    The Complete Diffie-Hellman Key Exchange Diagram The process begins when each side of the communicat ...

  2. 一个2013届毕业生(踏上IT行业)的迷茫(2)

    初中的时光是一段艰辛,但幸福的时光,在这一段时光中同样我遇到了我人生中第二个贵人.记得在小学毕业的那个暑假里,我知道上了初中会开一门叫做英语的课程,那时候在我们那里有好多上过初中.高中的在我们小学开英 ...

  3. PAT练习题概览

    PAT(pat.zju.edu.cn)是一个面向 C/C++程序的 Online Judge 系统.相比 ZOJ,HDOJ,POJ 等 ACM 题库,PAT 的题目非常基础,对于数据结构.算法的入门是 ...

  4. HTML:描述语义

    一.HTML HTML:Hypertext Markup Launguage,超文本标记语言,是网页的就文件格式,用于描述网页语义. 二.HTML骨架 DTD手册:http://www.w3schoo ...

  5. poj 3071 可能DP

    http://poj.org/problem? id=3071 推方程不难,可是难在怎么算 dp[i][j]表示第i场时第j仅仅队伍存活下来的概率 方程:dp[i][j]=sigma(dp[i-1][ ...

  6. WPF中取得系统字体列表

    原文:WPF中取得系统字体列表 在GDI+中,我们可以通过如下方式取得系统所有字体: foreach(FontFamily f in FontFamily.Families){   // 处理代码} ...

  7. 基于WPF实现双色球

    原文:基于WPF实现双色球 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/m0_37591671/article/details/82959625 ...

  8. 文章之间的基本总结Activity生命周期

    子曰:溫故而知新,能够為師矣.<論語> 学习技术也一样,对于技术文档或者经典的技术书籍来说,指望看一遍就全然掌握,那基本不大可能,所以我们须要常常回过头再细致研读几遍,以领悟到作者的思想精 ...

  9. ubuntu进不去桌面

    今天折腾ubunu的时候,总是进不去桌面,开机直接进入啦终端模式.在google帮助终于解决. sudo apt install --reinstall gnome-shell ubuntu-desk ...

  10. LeetCode - 4 - Longest Substring Without Repeating Characters

    题目 URL:https://leetcode.com/problems/median-of-two-sorted-arrays/ 解法 二分法. 总的思想是将 2 个数组用 2 个指针“整体”二分. ...