深入分析mybatis拦截器之动态代理(译)

原文:http://www.programering.com/a/MjN1UjNwATE.html(非直译,如有错误欢迎指正)

我们都知道mybatis自定义拦截器大概的格式类似于下面这种方式:

@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class XXXInterceptor implements Interceptor {
}

关于注解的详细说明是这样子的,其中type表示需要拦截的类,method表示需要拦截的方法,args表示方法的参数。

拦截的类拦截的方法
Executorupdate, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandlergetParameterObject, setParameters
StatementHandlerprepare, parameterize, batch, update, query
ResultSetHandlerhandleResultSets, handleOutputParameters

这里就不再详细说明了。这里我们来讨论下,如果是我们自己来实现一个拦截器的话,我们会如何去进行设计。

先从大的思路说起,假设我们使用JDK自带的动态代理工具去实现简单的拦截器。

需要先定义被代理类的接口:

public interface Target {
    public void execute();
}

下面是实现类:

public class TargetImpl implements Target {
    public void execute() {
        System.out.println("跑一把");
    }
}

下面我们来实现动态代理的代理类:

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("我要代理一下你");
        return method.invoke(target, args);
    }
}

以上我们实现了代理类的具体内容,与一般的Java动态代理的实现没有特殊的地方,下面看一下客户端的调用:

public class Client {
    public static void main(String[] args) {
    
        
        Target target = new TargetImpl();
        target.execute(); 
        
        //使用代理类获取执行实例
        target = (Target)TargetProxy.bind(target);
        target.execute(); 
    }
}

OK,到这里我们已经完成了最基础的拦截器功能,但是离mybatis的拦截器还有很大的距离,这里我们先考虑一个问题,上面的设计有哪些问题,我们先思考一下代理类的实现中有没有什么问题?

--------------------------------------------分割线------------------------------------------

一般我们开发业务代码,不同场景的业务一般我们会怎么做?写死!!!哈哈,这是业务风险最低的方式,其实我不反对写死,因为很多时候业务方提出的需求都是暂时的,我们可能花的一些心思去做设计,结果,过度设计,哈哈。。。

好了,不扯那么远了,回过头来我们继续说说这个动态代理的实现类中TargetProxy中的问题,我们把代理类要实现的内容,写死了,如果以后我要把输出的内容改为“我要代理你一下下”那么咱们就要改代码了,改代码毕竟是有风险的,只有新写一份才保险,这就是著名的开闭原则。(这里有个小故事要提一下,有朋友所在的公司对于出的产线问题的追责还是蛮严重了,所以一般开发对于新接的需求,如果牵涉到原有的功能,一般都会新写一个,所以这个公司的项目是不是都很好维护,毕竟符合了开闭原则,哈哈不是,因为是if写死的)

Ok,那么我们要怎么才能解决动态代理中写死的问题呢,遇事不决接口来试(开个玩笑),好我们定义个拦截器接口。

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) {
        return Proxy.newProxyInstance(target.getClass() .getClassLoader(), 
                target.getClass().getInterfaces(),
                       new TargetProxy(target));
    }
    
    
    public Object invoke(Object proxy, Method method,
                             Object[] args) throws Throwable {
        // 这里的执行拦截器需要执行的方法
        interceptor.intercept();
        return method.invoke(target, args);
    }
}

相对于前面的改动其实不大,这里就不贴客户端的实现了,简单说下吧,定义XXXInterceptor实现刚刚咱们声明的接口,然后定义的动态代理类的构造参数加入这个拦截器的实例,后面都一样的,就不详细说了。当然大部分情况,我们的拦截器的那个方法都是有入参的,不然能实现的拦截功能有限,我们把接口声明稍微修改一下,

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

这里,可能有些人可能会有疑惑,加参数就加参数呗,为什么要加个method,这里是因为我们可以对这个类的特定的方法进行拦截。

其实看到这里我们也已经大概看到了mybatis拦截器的影子,其实这里只解决了我们刚刚提出来的第一个问题,也是比较容易想到跟理解的问题,后面我再想想另一个问题,这个类还有什么问题,先抛出个提示,迪米特原则(又叫作最少知识原则(Least Knowledge Principle 简写LKP),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话)。

其实总结来说就是对于TargetProxy最少知道什么就可以正常工作呢?

-------------------------------------------------分割线-----------------------------------------------------------

我们先来分析一波,一个类的组成分为属性及方法。

1.TargetProxy这个类有2个属性,第一个是target,这个是需要代理的类,第二个是我们后面加的interceptor。

2.然后这个类有三个方法:

  • 构造方法,入参为:target,interceptor
  • bind方法,入参为:target
  • invoke方法,入参为:proxy,method,args

这里需要指明一点,方法的入参也是这个类知道其它的类的含义,那这里我们来总结一下,现在TargetProxy这个类知道了target,interceptor,method,proxy

我们先来看target,这个类我们需要在TargetProxy中知道吗,我们现在来把他去掉,貌似能跑通,那么暂时列为不需要的类,interceptor肯定是需要知道的,proxy也是,method也是,目前为止我们排除掉一个target。

接下来我们来看封装的问题,这个问题跟上面的问题是相互关联的,因为到这里始终觉得TargetProxy需要知道的类太多了,其实代理类就是一个中间人,他只需要知道怎么调用就行了,OK,刚刚标粗的就是我们要思考的重点,我们是不是可以把target,method,args封装为一个调用Invocation,这里为什么又有target的事了呢,因为调用中需要知道。话不多说,上代码:

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;
    }
    
    //执行实际的调用
    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):

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

到这里,这个设计已经蛮OK了,但是还有些问题,我们一开始有提到按照方法的粒度拦截,mybatis注解中的method,假设target中有2个方法,

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

我们定义一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MethodName {
    public String value();
}
@MethodName("execute1")
public class InterceptorImpl implements Interceptor {...}

然后代理类中的invoke方法改造一下:

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");
        
        //If the method name and the method of annotation on the name, to intercept
        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();

这里客户端其实也是有问题的,因为它可以不需要知道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();

完美。

现在我们再回过头来看下mybatis,我们自定义插件的时候一般是这样子的

 <!-- Custom processing of Map results returned interceptor -->
  <plugins>
      <plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />
  </plugins>

mybatis通过读取配置文件,将拦截器注册到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中只能代理4个类, ParameterHandler, ResultSetHandler, StatementHandler and Executor,mybatis中写死的(不是说不能写吗,嘿嘿),下面是其中一个ParameterHandler,其它三个的源码类似。

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    
    //Reads the configuration file in all Interceptor are registered to ParameterHandler, finally through each Interceptor annotations to determine whether to intercept a method of the ParameterHandler. 
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}
  • 拦截ResultSetHandler可以改变返回类型
  • 自动分页机制通过拦截StatementHandler
  • 拦截Executor可以查看SQL执行过程

最后上一下mybatis部分源码:

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

    Object plugin(Object var1);

    void setProperties(Properties var1);
}
public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    public Object getTarget() {
        return this.target;
    }

    public Method getMethod() {
        return this.method;
    }

    public Object[] getArgs() {
        return this.args;
    }

    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return this.method.invoke(this.target, this.args);
    }
}

最后一个是代理类,其中有些方法我给删除了,看一下需要关注的方法就好了

public class Plugin implements InvocationHandler {
    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public static Object wrap(Object target, Interceptor interceptor) {
        Map signatureMap = getSignatureMap(interceptor);
        Class type = target.getClass();
        Class[] interfaces = getAllInterfaces(type, signatureMap);
        return interfaces.length > 0?Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)):target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set e = (Set)this.signatureMap.get(method.getDeclaringClass());
            return e != null && e.contains(method)?this.interceptor.intercept(new Invocation(this.target, method, args)):method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值