什么是动态代理?它在主流开源框架中有哪些应用?

89 阅读11分钟

在系统设计过程中,对象之间相互依赖会造成耦合度过高,我们需要引入一个中间类来消除或缓解在直接访问目标对象时所带来的问题。但是对于发起访问的对象而言,通常希望这个中间类的存在是无感知的,这时候我们就可以引入动态代理机制。那么,问题就来了,作为一种通用型的技术组件,动态代理在分布式系统构建过程中起到什么作用呢?本文内容将和你一起探讨这个话题。

 

在系统设计领域,代理(Proxy)是一种常见的技术组件,主要目的就是在访问目标对象之前添加一层代理对象,用于消除或缓解在直接访问目标对象时带来的问题。而在现实应用中,代理机制也非常常见,可以说处处是代理。举一个简单的场景,假设我们需要在服务层组件中调用数据访问层组件,并记录一个操作日志。通常,服务层组件有很多方法,而对所有方法操作都需要添加日志。显然,在每个方法中手工调用同一个日志方法不是一种很好的解决方案,会造成代码冗余,增加维护成本。这个时候,代理机制就可以派上用场了。我们可以构建一个代理对象,然后由这个代理对象统一实现日志记录操作,如下图所示。

WPS图片(1).png  

可以看到,通过代理机制,一个对象就可以在承接另一个对象功能的基础之上,同时添加新的功能。相比直接在原有对象中嵌入代码,代理机制为我们提供了更为优雅的解决方案。

 

可能你看了前面这段描述之后,会觉得代理机制很简单,但事实并非如此,你可以先来看一下下面这些现实中的问题:

l 静态代理和动态代理的本质区别是什么?

l 你了解的动态代理实现技术有哪些?

l 你知道哪些场景中用到了动态代理机制吗?

l JDK自带的动态代理机制组成结构是怎么样的?

l Dubbo中的“远程调用本地化”机制背后使用的是什么技术?

l 为什么Mybatis中的Mapper接口没有实现类但却能提供SQL执行功能?

l 如果让你实现类似动态代理的执行效果,你有什么思路?

 

如何应对这些问题呢?首先,就理论知识而言,我们需要明确代理机制的概念和分类。关于代理机制的概念,你可以结合设计模式中的代理模式一起进行理解。而关于代理机制的分类,一般认为具体的表现形式有两种,一种是静态代理机制,一种是动态代理机制。静态代理机制相对比较简单,重点要做展开的是动态代理机制。

 

然后,我们需要对动态代理机制的主流实现技术展开讨论。在Java的世界中,想要实现动态代理,主要有三种实现方式,即JDK自带的代理类以及第三方的Cglib和Javassist。这些实现技术的使用方式大致相同,但背后的原理却各有特点。我们至少需要对其中的一种实现方式有深入的理解,建议可以从JDK自带的代理类进行切入。

 

最后,明确的概念和技术之后,接下来就是具体的应用场景了。可以说,动态代理机制在各个主流的开源框架中应用非常广泛。因为它是一种通用型的技术组件,所以可以根据不同的应用场景解决不同的问题。这里也需要我们对主流开源框架中涉及到动态代理的常见应用场景和实现方式有足够的了解。

 

代理机制

 

通过“问题背景”部分所介绍的应用场景,实际上我们可以梳理代理机制中存在的三种不同的角色,即抽象角色、代理角色和真实角色,如下图所示。

WPS图片(2).png  

可以看到,代理角色和真实角色一样实现了抽象角色,但是它是真实角色的一种代理。通过代理角色,开发人员可以在真实角色的基础上添加各种定制化的处理逻辑。

 

上图展示代理机制中的三种角色及其对应的职责,转化为面向对象领域的表现形式,就是如下图所示的类层结构。

WPS图片(3).png  

上图中,代理接口扮演的就是抽象角色,而代理类和委托类则分别充当了代理角色和真实角色,请注意它们都实现了代理接口。

 

前面已经提到,代理机制在具体实现上一般有两种方式,一种是静态代理机制,一种是动态代理机制。在Dubbo、Mybatis等常见开源框架中,这两种实现方式都有应用。本讲内容重点对动态代理展开讨论,主要因为动态代理理解和实现起来相对比较复杂,而且在Dubbo、Mybatis等框架中的应用方式和实现过程也值得我们学习和模仿。在接下来的内容中,我们以JDK自带的代理类为例给出具体的实现方式。

 

现在假设存在一个Account接口,然后需要在调用其open方法的前后记录日志。显然,通过静态代理完全能做到这一点,而使用JDK自带的动态代理也并不复杂。在JDK自带的动态代理中存在一个InvocationHandler接口,我们首先要做的就是提供该接口的一个实现类,如下所示。

 

public class AccountHandler implements InvocationHandler{

private Object obj;  

public AccountHandler(Object obj) {

super();

this.obj = obj;

}

 

@Override

public Object invoke(Object proxy, Method method, Object[] arg) throws Throwable {

Object result = null;

doBefore();

result = method.invoke(obj, arg);

doAfter();

return result;

}

 

public void doBefore() {

System.out.println("开户前");

}

public void doAfter() {

System.out.println("开户后");

}

}

 

可以看到InvocationHandler中包含一个invoke方法,我们必须实现这一方法。在这一方法中,我们通常需要调用method.invoke方法执行原有对象的代码逻辑,然后可以在该方法前后添加相应的代理实现。在上述代码中,我们只是简单打印了日志。

 

然后,我们编写测试类来应用上述AccountHandler类,如下所示。

 

public class AccountTest {

public static void main(String[] args) {

Account account = new RealAccount("tianyalan");

InvocationHandler handler = new AccountHandler(account);   

Account proxy = (Account)Proxy.newProxyInstance( account.getClass().getClassLoader(),  account.getClass().getInterfaces(), handler);

proxy.open();

}

}

 

Proxy.newProxyInstance方法的作用就是生成代理类,当该方法被调用时,RealAccount类的实例被传入。然后执行到代理类的open方法时,AccountHandler中的invoke方法就会被执行,从而触发代理逻辑。这里的类层结构如下图所示。

 

WPS图片(4).png

 

仔细分析上述代码结构,可以发现其遵循设计并实现业务接口→实现Handler→创建代理类这一实现流程,然后在Handler中构建具体的代理逻辑。

 

上述流程展示了基本的代理机制实现过程。我们可以联想一下很多基于AOP机制的拦截器实际上就是类似的流程。

 

案例解析

 

在接下来的内容中,我们将分别基于Dubbo框架和Mybatis框架来深入分析在主流开源框架中动态代理机制的应用场景和实现原理。。

 

Dubbo远程访问中的代理机制

 

当使用Dubbo进行远程服务调用时,我们所做的事情就是在配置文件或代码中添加对某个服务的引用,整个过程让人感觉并没有执行任何与远程方法调用相关的网络连接、数据传输、序列化等操作,这就是所谓的“远程调用本地化”。远程调用本地化得以实现的背后用到的实际上就是动态代理机制。

 

在Dubbo中,我们知道执行远程调用的是Invoker对象。因此,当该对象被创建出来之后,我们就需要为它生成对应的代理对象,完成这一操作的是ProxyFactory工厂类,该工厂类的getProxy方法如下所示。

 

public interface ProxyFactory {

    @Adaptive({Constants.PROXY_KEY})

     T getProxy(Invoker invoker) throws RpcException;

 

    @Adaptive({Constants.PROXY_KEY})

Invoker getInvoker(T proxy, Class type, URL url) throws RpcException;

}

 

在Dubbo中,ProxyFactory的直接实现类是AbstractProxyFactory。该类是一个抽象类,除了为每个服务自动添加回声(Echo)功能之外,还预留了一个getProxy抽象方法供子类进行实现。在Dubbo中存在两个ProxyFactory的实现类,即JavassistProxyFactory和JdkProxyFactory。其中的JdkProxyFactory的实现比较典型,接下来我们就对JdkProxyFactory进行展开,该类的getProxy方法如下所示。

 

public T getProxy(Invoker invoker, Class<?>[] interfaces) {

        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));

}

 

这里看到了熟悉的Proxy.newProxyInstance方法,这是典型的JDK动态代理的用法。根据传入的接口获得动态代理类,当调用这些接口的方法时都会转而调用InvokerInvocationHandler。基于JDK动态代理的实现机制,可以想象InvokerInvocationHandler类必定实现了InvocationHandler接口,如下所示。

 

public class InvokerInvocationHandler implements InvocationHandler {

    private final Invoker<?> invoker;

 

    public InvokerInvocationHandler(Invoker<?> handler) {

        this.invoker = handler;

    }

 

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        String methodName = method.getName();

        Class<?>[] parameterTypes = method.getParameterTypes();

        if (method.getDeclaringClass() == Object.class) {

            return method.invoke(invoker, args);

        }

        if ("toString".equals(methodName) && parameterTypes.length == 0) {

            return invoker.toString();

        }

        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {

            return invoker.hashCode();

        }

        if ("equals".equals(methodName) && parameterTypes.length == 1) {

            return invoker.equals(args[0]);

        }

        return invoker.invoke(new RpcInvocation(method, args)).recreate();

    }

}

 

可以看到,这里只是把方法的执行转向了invoker.invoke方法。关于Invoker的介绍不是本讲内容的重点,我们已经在第 X 介绍服务调用时对其进行了详细的展开,你可以做一些回顾。

 

Mybatis数据访问中的代理机制

 

在Mybatis中,应用动态代理的场景实际上非常多,我们无意对所有的场景都一一展开。这里列举一个最典型的应用场景,即Mapper层的动态代理,用于根据Mapper层接口获取SQL执行结果。

 

在开始介绍Mybatis中的代理机制之前,我们先来回顾一下Mybatis的执行主流程,如下图所示。可以看到Mybatis是通过MapperProxy动态代理Mapper。

  WPS图片(5).png

 

使用过Mybatis的同学都应该接触过这样一种操作,我们只需要定义Mapper层的接口而不需要对其进行具体的实现,该接口却能够正常调用并完成SQL执行等一系列操作,听起来很神奇,这是怎么做到的呢?让我们梳理一下整个调用流程。

 

在使用Mybatis时,业务层代码中调用各种Mapper的一般做法是通过SqlSession这个外观类,如下所示。

 

TestMapper testMapper = sqlSession.getMapper(TestMapper.class);

 

我们来到SqlSession的实现类DefaultSqlSession,该类的getMapper方法如下所示。

 

@Override

public T getMapper(Class type) {

    return configuration.getMapper(type, this);

}

 

作为外观类,DefaultSqlSession把这一操作转移给了Configuration对象,该对象中的getMapper方法如下所示。

 

public T getMapper(Class type, SqlSession sqlSession) {

    return mapperRegistry.getMapper(type, sqlSession);

}

 

让我们来到这里出现的MapperRegistry类,会发现真正负责创建Mapper实例对象的是MapperProxyFactory类。请注意,mapperProxyFactory.newInstance方法的传入参数是一个SqlSession,如下所示。

 

public T newInstance(SqlSession sqlSession) {

    final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);

    return newInstance(mapperProxy);

}

 

这里引出了另一个核心类MapperProxy,从命名上看,我们可以猜想该类就是一个代理类,因此势必使用了前面介绍的某种动态代理技术。可以先看一下MapperProxy类的签名,如下所示。

 

public class MapperProxy implements InvocationHandler, Serializable

 

显然,这里用到的是JDK自带的基于InvocationHandler的动态代理实现方案,因此,在MapperProxy类中同样肯定存在一个invoke方法,如下所示。

 

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    try {

      if (Object.class.equals(method.getDeclaringClass())) {

        return method.invoke(this, args);

      } else if (method.isDefault()) {

        return invokeDefaultMethod(proxy, method, args);

      }

    } catch (Throwable t) {

      throw ExceptionUtil.unwrapThrowable(t);

    }

    final MapperMethod mapperMethod = cachedMapperMethod(method);

    return mapperMethod.execute(sqlSession, args);

}

 

对于执行SQL语句的方法而言,MapperProxy会把这部分工作交给MapperMethod处理。而在MapperMethod的execute方法中,我们传入了SqlSession以及相关的参数。在这个execute方法内部,根据SQL命令的不同类型(insert、update、delete、select)分别调用SqlSession的不同方法。

 

目前为止,我们看到了MapperProxy类实现InvocationHandler接口,但还没有看到Proxy.newProxyInstance方法的调用,该方法实际上也同样位于MapperProxyFactory类中,该类还存在一个newInstance重载方法,通过传入mapperProxy的代理对象最终完成代理方法的执行,如下所示。

 

protected T newInstance(MapperProxy mapperProxy) {

    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);

}

 

作为总结,我们梳理了Mybatis中Mapper层动态代理相关类的类层结构,如下图所示。

    WPS图片(6).png

总体而言,基于代理机制,Mybatis中Mapper层的接口调用过程还是比较简单明确的,这里采用的实现方案也比较经典,相关的类层结构设计也可以用做日常开发工作的参考。