原文连接:"犯罪心理"解读Mybatis拦截器java
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); } }
这时,客户端调用方式以下:this
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 的时候能够传入自定义的拦截器:3d
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 拦截器的整个设计动机与理念,大道至简.
关注公众号了解更多能够提升工做效率的工具,同时带你像看侦探小说同样趣味学习 Java 技术