“犯罪心理”解读 Mybatis 拦截器

640?wx_fmt=png


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

640?wx_fmt=jpeg

设计解读

 
 

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 实现的核心内容做个对比:生成代理对象:640?wx_fmt=jpeg拦截指定方法,如果找不到方法,抛出异常:

640?wx_fmt=jpeg

执行目标方法:

640?wx_fmt=jpeg


到这里,没错,犯罪现场完美推测出,真相就是这样!!!墙裂建议先看 Mybatis拦截器执行过程解析 ,然后回看该文章,了解 Mybatis 拦截器的整个设计动机与理念,大道至简. 

灵魂追问

  1. 除了迪米特设计原则,你还知道哪些设计基本原则?

  2. 你在编写代码时,考虑过怎样利用那些设计原则来规范自己代码吗?

  3. ...


带着疑问去思考,然后串联,进而归纳总结,不断追问自己,进行自我辩证,像侦查嫌疑案件一样看待技术问题,漆黑的街道,你我一起寻找线索,你就是技术界大侦探福尔摩斯


提高效率工具

640?wx_fmt=png


推荐阅读


640?wx_fmt=png

欢迎思想碰撞,点我留言交流


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值