"犯罪心理"解读Mybatis拦截器
原文链接:"犯罪心理"解读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 拦截器的整个设计动机与理念,大道至简.
灵魂追问
- 除了迪米特设计原则,你还知道哪些设计基本原则?
- 你在编写代码时,考虑过怎样利用那些设计原则来规范自己代码吗?
- ...
推荐阅读
- 不得不知的责任链设计模式
- Mybatis拦截器执行过程解析
- 如何使用Mybatis的拦截器实现数据加密与解密
- 如何设计好的RESTful API
- 轻松高效玩转DTO(Data Transfer Object)
那些可以提高效率的工具
关注公众号了解更多可以提高工作效率的工具,同时带你像看侦探小说一样趣味学习 Java 技术


"犯罪心理"解读Mybatis拦截器的更多相关文章
- Mybatis拦截器
Mybatis拦截器
- Mybatis拦截器 mysql load data local 内存流处理
Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...
- MyBatis拦截器原理探究
MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...
- Mybatis拦截器介绍
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...
- Mybatis拦截器实现分页
本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...
- 数据权限管理中心 - 基于mybatis拦截器实现
数据权限管理中心 由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手 需求场景 第一种场景:行级数据处理 原sql: select ...
- 基于Spring和Mybatis拦截器实现数据库操作读写分离
首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...
- 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离
前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...
- 【Mybatis】1、Mybatis拦截器学习资料汇总
MyBatis拦截器原理探究 http://www.cnblogs.com/fangjian0423/p/mybatis-interceptor.html [myBatis]Mybatis中的拦截器 ...
随机推荐
- 3.RabbitMQ相关概念的杂谈
1.vhost,为什么我要有这个vhost呢? 这是因为可能有很多组使用RabbitMQ,有产品组,用户组,vhost,虚拟主机的意思,可以避免命名冲突. 2.Exchange,交换机 有四种交换机 ...
- QPainter的坐标系系统的转换
声明:本文原创于yafeilinux的百度博客,http://hi.baidu.com/yafeilinux 转载请注明出处. 我看了这篇文章很好很容易理解.如果看了Qt助手之后更加的形象. 前面一节 ...
- ASP.NET Core Identity 配置 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core Identity 配置 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Identity 配置 上一章节我们简单介绍了下 Id ...
- 【C语言学习】C语言功能
代码,功能为了更好地实现模块化编程.那么,什么是函数的性质?在函数中定义的变量(全局变量.局部变量.静态变量)如何存储?为什么范围和全局变量和局部变量的寿命是不一样的?只是有一个更深入的了解的功能.能 ...
- IAA32过程调用保护规则注册
因为操作系统共享性质,所以,寄存器已成为各种处理或共享资源的处理.然后,该过程发生 当所谓的.假设呼叫者使用内部寄存器值.但这个寄存器的内容,很可能在该呼叫者的执行的过程中改变,用过程执行之前,对该寄 ...
- String转Color
原文:String转Color 很硬性的转换,谁知道更好的忘不吝赐教啊. /// <summary> /// String To Color /// </summary> // ...
- postgresql 自带函数
替换函数 SELECT replace('abcdefabcdef', 'cd', 'XX') 得到 abXXefabXXef ------------------------------------ ...
- WPF 寻找数据模板中的元素
<Window x:Class="Wpf180706.Window11" xmlns="http://schemas.microsoft.com/wi ...
- js 操作样式
<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"> ...
- 怎么给开源项目提PR?
1. fork 你要的项目 2. 下载到本地 相关步骤如下 在你需要的文件夹下面,右键 git bash 命令,打开 git 命令框 执行如下指令可将项目代码下载到当前目录 ~~~ git clone ...