原文链接:"犯罪心理"解读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. Excel创button宏调用

    今天,匆匆写了一个宏,但发现已被用来创建button开发工具菜单不见了. 在十分钟找Excel转了个遍,终究Excel通常使用在中的选项,首先Mark下一个,离开同样找不到鞋. 几个截面图.促进突然, ...

  2. 画廊视图(Gallery)的功能和用法

    Gallery与Spinner组件有共同的父类:AbsSpinner,表明Gallery和Spinner都是一个列表框.它们之间的区别在于Spinner显示的是一个垂直的列表选择框吗,而Gallery ...

  3. hdu1845 Jimmy’s Assignment --- 完整匹配

    意甲冠军: 它需要一个特殊的图,以找到最大匹配.该图的特征是:无向图,度的每个节点3.这是一个双边连接组件(the graph is 2-edge-connected (that is, at lea ...

  4. Bean行为破坏之前,

    而在自定义初始化阶段的行为之一似.Spring此外,它提供了两种方法来定制Bean具体的行为破坏之前. 例如下列: 1.采用destroy-method属性. 2.达到DisposableBean介面 ...

  5. javascript的回调函数 同步 异步

    后一个任务等待前一个任务结束再执行.程序执行顺序与任务排列顺序一致的,同步的. 参考: http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%B ...

  6. Arraylist 和 linkedlist || hashset 和treeset. || hashMap 和 TreeMap

    参考:http://liuyuan418921673.iteye.com/blog/2256120 1. ArrayList和LinkedList的区别和使用场景   ArryList 与linked ...

  7. 转载来自朱小厮的博客的NIO相关基础篇

    用户空间以及内核空间概念 我们知道现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操心系统的核心是内核,独立于普通的应用程序,可以访问受保 ...

  8. 基于JUnit和Ant测试程序正在运行使用Kieker(AspectJ)监测方法

    这篇日志的目的从标题里能够看出来.这也是我们实验须要,必须总结一下,方便其它师弟师妹在这个基础上做实验. 我已经介绍了非常多基于Kieker的监控方法,这里以Prefuse这个开源可视化Java框架为 ...

  9. 给WPF文字加多条修饰线

    原文:给WPF文字加多条修饰线 这是上篇WPF中的文字修饰--上划线,中划线,基线与下划线 最后留给朋友思考的问题. 效果图: XAML代码:<Page xmlns="http://s ...

  10. uml系列(七)——互动图

    互动图uml描述如何对象的描述在系统交互动作 . 废话不多说,还是来张图: 概念          交互图,主要描写叙述的是系统中的一组对象的消息的传递的.为对象间的交互定义了一个可视的表示方法. 构 ...