"犯罪心理"解读Mybatis拦截器

原文链接:"犯罪心理"解读Mybatis拦截器

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

设计解读

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 实现的核心内容做个对比:
生成代理对象:
Xnip2019-06-24_21-41-09.jpg

拦截指定方法,如果找不到方法,抛出异常:
Xnip2019-06-24_21-44-02.jpg

执行目标方法:
Xnip2019-06-24_21-46-30.jpg

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

灵魂追问

  1. 除了迪米特设计原则,你还知道哪些设计基本原则?
  2. 你在编写代码时,考虑过怎样利用那些设计原则来规范自己代码吗?
  3. ...

推荐阅读

  1. 不得不知的责任链设计模式
  2. Mybatis拦截器执行过程解析
  3. 如何使用Mybatis的拦截器实现数据加密与解密
  4. 如何设计好的RESTful API
  5. 轻松高效玩转DTO(Data Transfer Object)

那些可以提高效率的工具

关注公众号了解更多可以提高工作效率的工具,同时带你像看侦探小说一样趣味学习 Java 技术

a


a (1).png

posted @ 2019-06-27 09:21  日拱一兵  阅读(690)  评论(1编辑  收藏  举报