博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
由浅入深分析mybatis通过动态代理实现拦截器(插件)的原理
阅读量:6716 次
发布时间:2019-06-25

本文共 9270 字,大约阅读时间需要 30 分钟。

hot3.png

最近在用mybatis做项目,需要用到mybatis的拦截器功能,就顺便把mybatis的拦截器源码大致的看了一遍,为了温故而知新,在此就按照自己的理解由浅入深的理解一下它的设计。 

和大家分享一下,不足和谬误之处欢迎交流。直接入正题。 
首先,先不管mybatis的源码是怎么设计的,先假设一下自己要做一个拦截器应该怎么做。拦截器的实现都是基于代理的设计模式设计的,简单的说就是要创造一个目标类的代理类,在代理类中执行目标类的方法并拦截执行拦截器代码。 
那么我们就用JDK的动态代理设计一个简单的拦截器: 
将被拦截的目标接口:

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);      }

我们可以将拦截逻辑封装到一个类中,客户端在调用TargetProxy的bind()方法的时候将拦截逻辑一起当成参数传入: 

定义一个拦截逻辑封装的接口Interceptor,这才是真正的拦截器接口。

 public interface Interceptor {      public void intercept();  }

那么我们的代理类就可以改成: 

public class TargetProxy implements InvocationHandler {    private Object target;  private Interceptor interceptor;    private TargetProxy(Object target, Interceptor interceptor) {      this.target = target;      this.interceptor = interceptor;  }    //将拦截逻辑封装到拦截器中,有客户端生成目标类的代理类的时候一起传入,这样客户端就可以设置不同的拦截逻辑。  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();

当然,很多时候我们的拦截器中需要判断当前方法需不需要拦截,或者获取当前被拦截的方法参数等。我们可以将被拦截的目标方法对象,参数信息传给拦截器。 

拦截器接口改成: 

public interface Interceptor {      public void intercept(Method method, Object[] args);  }

在代理类执行的时候可以将当前方法和参数传给拦截,即TargetProxy的invoke方法改为: 

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {      interceptor.intercept(method, args);      return method.invoke(target, args);  }

在Java设计原则中有一个叫做迪米特法则,大概的意思就是一个类对其他类知道得越少越好。其实就是减少类与类之间的耦合强度。这是从类成员的角度去思考的。 

什么叫越少越好,什么是最少?最少就是不知道。 
所以我们是不是可以这么理解,一个类所要了解的类应该越少越好呢? 
当然,这只是从类的角度去诠释了迪米特法则。 
甚至可以反过来思考,一个类被其他类了解得越少越好。 
A类只让B类了解总要强于A类让B,C,D类都去了解。 
举个例子: 
我们的TargetProxy类中需要了解的类有哪些呢? 
1. Object target 不需要了解,因为在TargetProxy中,target都被作为参数传给了别的类使用,自己不需要了解它。 
2. Interceptor interceptor 需要了解,需要调用其intercept方法。 
3. 同样,Proxy需要了解。 
4. Method method 参数需要了解,需要调用其invoke方法。 
同样,如果interceptor接口中需要使用intercept方法传过去Method类,那么也需要了解它。那么既然Interceptor都需要使用Method,还不如将Method的执行也放到Interceptor中,不再让TargetProxy类对其了解。Method的执行需要target对象,所以也需要将target对象给Interceptor。将Method,target和args封装到一个对象Invocation中,将Invocation传给Interceptor。 
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;      }            //将自己成员变量的操作尽量放到自己内部,不需要Interceptor获得自己的成员变量再去操作它们,      //除非这样的操作需要Interceptor的其他支持。然而这儿不需要。      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;      }  }

Interceptor就变成: 

public interface Interceptor {      public Object intercept(Invocation invocation)throws Throwable ;  }

TargetProxy的invoke方法就变成: 

public Object invoke(Object proxy, Method method,                             Object[] args) throws Throwable {      return interceptor.intercept(new Invocation(target,                                                      method, args));  }

那么就每一个Interceptor拦截器实现都需要最后执行Invocation的proceed方法并返回。 

客户端调用: 

Interceptor interceptor = new Interceptor() {      public Object intercept(Invocation invocation)  throws Throwable {          System.out.println("Go Go Go!!!");          return invocation.proceed();      }  };

好了,通过一系列调整,设计已经挺好了,不过上面的拦截器还是有一个很大的不足, 

那就是拦截器会拦截目标对象的所有方法,然而这往往是不需要的,我们经常需要拦截器 
拦截目标对象的指定方法。 
假设目标对象接口有多个方法: 

public interface Target {      public void execute1();      public void execute2();  }

利用在Interceptor上加注解解决。 

首先简单的定义一个注解:

@Retention(RetentionPolicy.RUNTIME)  @Target(ElementType.TYPE)  public @interface MethodName {      public String value();  }

在拦截器的实现类加上该注解: 

@MethodName("execute1")  public class InterceptorImpl implements Interceptor {...}

在TargetProxy中判断interceptor的注解,看是否实行拦截: 

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();

从客户端调用代码可以看出,客户端首先需要创建一个目标对象和拦截器,然后将拦截器和目标对象绑定并获取代理对象,最后执行代理对象的execute()方法。 

根据迪米特法则来讲,其实客户端根本不需要了解TargetProxy类。将绑定逻辑放到拦截器内部,客户端只需要和拦截器打交道就可以了。 
即拦截器接口变为: 

public interface Interceptor {  public Object intercept(Invocation invocation)  throws Throwable ;  public Object register(Object 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();

OK,上面的一系列过程其实都是mybatis的拦截器代码结构,我只是学习了之后用最简单的方法理解一遍罢了。 

上面的TargetProxy其实就是mybatis的Plug类。Interceptor和Invocation几乎一样。只是mybatis的Interceptor支持的注解 
更加复杂。 
mybatis最终是通过将自定义的Interceptor配置到xml文件中: 

   
       
   

通过读取配置文件中的Interceptor,通过反射构造其实例,将所有的Interceptor保存到InterceptorChain中。 

public class InterceptorChain {      private final List
 interceptors = new ArrayList
();      public Object pluginAll(Object target) {      for (Interceptor interceptor : interceptors) {        target = interceptor.plugin(target);      }      return target;    }      public void addInterceptor(Interceptor interceptor) {      interceptors.add(interceptor);    }        public List
 getInterceptors() {      return Collections.unmodifiableList(interceptors);    }    }

mybatis的拦截器只能代理指定的四个类:ParameterHandler、ResultSetHandler、StatementHandler以及Executor。 

这是在mybatis的Configuration中写死的,例如(其他三个类似): 

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {      ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);            //将配置文件中读取的所有的Interceptor都注册到ParameterHandler中,最后通过每个Interceptor的注解判断是否需要拦截该ParameterHandler的某个方法。      parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);      return parameterHandler;  }

所以我们可以自定义mybatis的插件(拦截器)修改mybatis的很多默认行为, 

例如, 
通过拦截ResultSetHandler修改接口返回类型; 
通过拦截StatementHandler修改mybatis框架的分页机制; 
通过拦截Executor查看mybatis的sql执行过程等等。

转载于:https://my.oschina.net/ydsakyclguozi/blog/414589

你可能感兴趣的文章
重要更新-Word 2003查找替换最后一个实例的第四种方法
查看>>
实现大屏幕全国监控各地流量和负载质量
查看>>
高性能HTTP加速器Varnish(安装配置篇)
查看>>
如何取消OneNote的粘贴来源地址
查看>>
编程乐趣:C#实现读取12306余票信息
查看>>
视频编码的常见参数基本概念
查看>>
用python写一个专业的传参脚本
查看>>
Nginx+PHP7 安装及配置
查看>>
OpenIndiana
查看>>
varnish基础概念详解
查看>>
发一个windows8 下QQ应用的测试报告-精彩截图
查看>>
利用Zabbix ODBC monitoring监控MySQL
查看>>
如何设计一款优秀的短视频 SDK
查看>>
实战postfix邮件发送
查看>>
MySQL主从架构由5.5版本升级到5.6方案
查看>>
大数据时代的遨游
查看>>
从Windows 8.1光盘安装.NET Framework 3.5.1
查看>>
Create Oracle VM High Availability (HA)
查看>>
Memcache持久性分布式数据MemcacheDB
查看>>
联想计算机Lenovo ThinkCentre M910t-NO76的重装
查看>>