Mybaitis 原理之动态代理拦截器

由浅入深分析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文件中:
<!-- 自定义处理Map返回结果的拦截器 -->
  <plugins>
      <plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />
  </plugins>
通过读取配置文件中的Interceptor,通过反射构造其实例,将所有的Interceptor保存到InterceptorChain中。
public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  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<Interceptor> 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执行过程等等。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值