[动态代理]java动态代理
程序员文章站
2022-04-26 18:43:26
...
动态代理前面的问题问到了动态代理,我们一起看看,它到底是解决什么问题?
首先,它是一个代理机制。如果熟悉设计模式中的代理模式,我们会知道,代理可以看作是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成。其实很多动态代理场景,我认为也可以看作是装饰器(Decorator)模式的应用,我会在后面的专栏设计模式主题予以补充。通过代理可以让调用者与实现者之间解耦。比如进行 RPC 调用,框架内部的寻址、序列化、反序列化等,对于调用者往往是没有太大意义的,通过代理,可以提供更加友善的界面。代理的发展经历了静态到动态的过程,源于静态代理引入的额外工作。类似早期的 RMI 之类古董技术,还需要 rmic 之类工具生成静态 stub 等各种文件,增加了很多繁琐的准备工作,而这又和我们的业务逻辑没有关系。利用动态代理机制,相应的 stub 等类,可以在运行时生成,对应的调用操作也是动态完成,极大地提高了我们的生产力。改进后的 RMI 已经不再需要手动去准备这些了,虽然它仍然是相对古老落后的技术,未来也许会逐步被移除。这么说可能不够直观,我们可以看 JDK 动态代理的一个简单例子。下面只是加了一句 print,在生产系统中,我们可以轻松扩展类似逻辑进行诊断、限流等。
public class MyDynamicProxy {
public static void main (String[] args) {
HelloImpl hello = new HelloImpl();
MyInvocationHandler handler = new MyInvocationHandler(hello);
// 构造代码实例
Hello proxyHello = (Hello)Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
// 调用代理方法 proxyHello.sayHello();
}
}
interface Hello {
void sayHello();
}
class HelloImpl implements Hello {
@Override
public void sayHello() {
System.out.println("Hello World");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Invoking sayHello");
Object result = method.invoke(target, args);
return result;
}
}
上面的 JDK Proxy 例子,非常简单地实现了动态代理的构建和代理操作。首先,实现对应的 InvocationHandler;然后,以接口 Hello 为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑,代理为应用插入额外逻辑(这里是 println)提供了便利的入口。从 API 设计和实现的角度,这种实现仍然有局限性,因为它是以接口为中心的,相当于添加了一种对于被调用者没有太大意义的限制。我们实例化的是 Proxy 对象,而不是真正的被调用类型,这在实践中还是可能带来各种不便和能力退化。如果被调用者没有实现接口,而我们还是希望利用动态代理机制,那么可以考虑其他方式。我们知道 Spring AOP 支持两种模式的动态代理,JDK Proxy 或者 cglib,如果我们选择 cglib 方式,你会发现对接口的依赖被克服了。
cglib 动态代理采取的是创建目标类的子类的方式,因为是子类化,我们可以达到近似使用被调用者本身的效果。在 Spring 编程中,框架通常会处理这种情况,当然我们也可以显式指定。关于类似方案的实现细节,我就不再详细讨论了。那我们在开发中怎样选择呢?我来简单对比下两种方式各自优势。JDK Proxy 的优势:最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。代码实现简单。基于类似 cglib 框架的优势:
有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。只操作我们关心的类,而不必为其他相关类增加工作量。高性能。另外,从性能角度,我想补充几句。记得有人曾经得出结论说 JDK Proxy 比 cglib 或者 Javassist 慢几十倍。坦白说,不去争论具体的 benchmark 细节,在主流 JDK 版本中,JDK Proxy 在典型场景可以提供对等的性能水平,数量级的差距基本上不是广泛存在的。而且,反射机制性能在现代 JDK 中,自身已经得到了极大的改进和优化,同时,JDK 很多功能也不完全是反射,同样使用了 ASM 进行字节码操作。我们在选型中,性能未必是唯一考量,可靠性、可维护性、编程工作量等往往是更主要的考虑因素,毕竟标准类库和反射编程的门槛要低得多,代码量也是更加可控的,如果我们比较下不同开源项目在动态代理开发上的投入,也能看到这一点。动态代理应用非常广泛,虽然最初多是因为 RPC 等使用进入我们视线,但是动态代理的使用场景远远不仅如此,它完美符合 Spring AOP 等切面编程。我在后面的专栏还会进一步详细分析 AOP 的目的和能力。简单来说它可以看作是对 OOP 的一个补充,因为 OOP 对于跨越不同对象或类的分散、纠缠逻辑表现力不够,比如在不同模块的特定阶段做一些事情,类似日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等,你可以参考下面这张图
首先,它是一个代理机制。如果熟悉设计模式中的代理模式,我们会知道,代理可以看作是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成。其实很多动态代理场景,我认为也可以看作是装饰器(Decorator)模式的应用,我会在后面的专栏设计模式主题予以补充。通过代理可以让调用者与实现者之间解耦。比如进行 RPC 调用,框架内部的寻址、序列化、反序列化等,对于调用者往往是没有太大意义的,通过代理,可以提供更加友善的界面。代理的发展经历了静态到动态的过程,源于静态代理引入的额外工作。类似早期的 RMI 之类古董技术,还需要 rmic 之类工具生成静态 stub 等各种文件,增加了很多繁琐的准备工作,而这又和我们的业务逻辑没有关系。利用动态代理机制,相应的 stub 等类,可以在运行时生成,对应的调用操作也是动态完成,极大地提高了我们的生产力。改进后的 RMI 已经不再需要手动去准备这些了,虽然它仍然是相对古老落后的技术,未来也许会逐步被移除。这么说可能不够直观,我们可以看 JDK 动态代理的一个简单例子。下面只是加了一句 print,在生产系统中,我们可以轻松扩展类似逻辑进行诊断、限流等。
public class MyDynamicProxy {
public static void main (String[] args) {
HelloImpl hello = new HelloImpl();
MyInvocationHandler handler = new MyInvocationHandler(hello);
// 构造代码实例
Hello proxyHello = (Hello)Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
// 调用代理方法 proxyHello.sayHello();
}
}
interface Hello {
void sayHello();
}
class HelloImpl implements Hello {
@Override
public void sayHello() {
System.out.println("Hello World");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Invoking sayHello");
Object result = method.invoke(target, args);
return result;
}
}
上面的 JDK Proxy 例子,非常简单地实现了动态代理的构建和代理操作。首先,实现对应的 InvocationHandler;然后,以接口 Hello 为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑,代理为应用插入额外逻辑(这里是 println)提供了便利的入口。从 API 设计和实现的角度,这种实现仍然有局限性,因为它是以接口为中心的,相当于添加了一种对于被调用者没有太大意义的限制。我们实例化的是 Proxy 对象,而不是真正的被调用类型,这在实践中还是可能带来各种不便和能力退化。如果被调用者没有实现接口,而我们还是希望利用动态代理机制,那么可以考虑其他方式。我们知道 Spring AOP 支持两种模式的动态代理,JDK Proxy 或者 cglib,如果我们选择 cglib 方式,你会发现对接口的依赖被克服了。
cglib 动态代理采取的是创建目标类的子类的方式,因为是子类化,我们可以达到近似使用被调用者本身的效果。在 Spring 编程中,框架通常会处理这种情况,当然我们也可以显式指定。关于类似方案的实现细节,我就不再详细讨论了。那我们在开发中怎样选择呢?我来简单对比下两种方式各自优势。JDK Proxy 的优势:最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。代码实现简单。基于类似 cglib 框架的优势:
有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。只操作我们关心的类,而不必为其他相关类增加工作量。高性能。另外,从性能角度,我想补充几句。记得有人曾经得出结论说 JDK Proxy 比 cglib 或者 Javassist 慢几十倍。坦白说,不去争论具体的 benchmark 细节,在主流 JDK 版本中,JDK Proxy 在典型场景可以提供对等的性能水平,数量级的差距基本上不是广泛存在的。而且,反射机制性能在现代 JDK 中,自身已经得到了极大的改进和优化,同时,JDK 很多功能也不完全是反射,同样使用了 ASM 进行字节码操作。我们在选型中,性能未必是唯一考量,可靠性、可维护性、编程工作量等往往是更主要的考虑因素,毕竟标准类库和反射编程的门槛要低得多,代码量也是更加可控的,如果我们比较下不同开源项目在动态代理开发上的投入,也能看到这一点。动态代理应用非常广泛,虽然最初多是因为 RPC 等使用进入我们视线,但是动态代理的使用场景远远不仅如此,它完美符合 Spring AOP 等切面编程。我在后面的专栏还会进一步详细分析 AOP 的目的和能力。简单来说它可以看作是对 OOP 的一个补充,因为 OOP 对于跨越不同对象或类的分散、纠缠逻辑表现力不够,比如在不同模块的特定阶段做一些事情,类似日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等,你可以参考下面这张图
上一篇: JSF —— Facelets 标签