代理模式是一种通过中间代理访问目标对象,以达到增强或拓展原对象功能目的的设计模式,举个例子来说,我们在购买飞机票时往往会通过一些第三方平台来购买,在这里第三方平台就可看成代理对象,目标对象则是各大航空公司,常见的代理方式有静态代理、动态代理以及Cglib代理。
静态代理
静态代理属于比较典型的代理模式,它的类图如下所示,从图中可以看到客户端是通过代理类的接口来访问目标对象的接口,也就是目标对象和代理类是一一对应的,如果有多个目标接口需要代理则产生多个代理类,实现方式比较冗余,另外如果拓展接口,对应的目标对象和代理类也需修改,不易维护。
动态代理
动态代理通过Java反射机制或者ASM字节码技术,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。它与静态代理的主要区别在与动态代理的代理类是在运行期才会生成的,也就是说不会在编译期代理类的Class文件。常见的动态代理有JDK动态代理和Cglib动态代理。
JDK动态代理
JDK动态代理又称接口代理,它要求目标对象必须实现接口,否则不能代理。动态代理是基于java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
类来实现的,其中Proxy
是拦截发生的地方,而InvocationHandler
则是发生调用地方,newProxyInstance
方法返回一个指定接口的代理类实例。
newProxyInstance方法
public static Object newProxyInstance(ClassLoader loader, //目标对象的类加载器
Class>[] interfaces, // 目标对象所实现的接口
InvocationHandler h) // 事件处理器
InvocationHandler的Invoke方法
public Object invoke(Object obj, Object... args) // 该方法会调用目标对象对应的方法
在这里抛出一个问题,JDK动态代理为什么必须实现接口才能代理?要弄明白这个问题,我们需要拿到生成的代理类,下面是通过技术手段拿到的运行期的代理类,可以看到$Proxy0
代理类已经继承Proxy
类,由于Java是单继承的,所以只能通过实现接口的方式来实现。
public final class $Proxy0 extends Proxy implements IUserDao {
private static Method m1;
private static Method m2;
private static Method m0;
private static Method m3;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
...
public final void register() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
...
}
CGLIB代理
CGLib相对于JDK动态代理更加灵活,它是通过生成子类来拓展目标对象的功能,使用cglib代理的对象无需实现接口,可以做到代理类无侵入,另外因CGLib具备很好的性能,所以被很多AOP框架所引用,比如Spring、Hibernate。
Cglib代理方式是通过继承来实现,其中代理对象是由Enhancer创建(Enhancer是Cglib字节码增强器,可以很方便对类进行拓展),另外,可以通过实现MethodInterceptor
接口来定义方法拦截器。
public Object getProxyInstance() {
Enhancer en = new Enhancer();
// 继承被代理类
en.setSuperclass(target.getClass());
// 设置回调函数
en.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("开启事务");
// 执行目标对象的方法
Object returnValue = method.invoke(target, objects);
System.out.println("关闭事务");
return null;
}
});
return en.create();
}
UserDao$$EnhancerByCGLIB$$b0e8b18d
是获取到的UserDao的Cglib代理,可以看到它继承了UserDao方法,并为UserDao的每个方法生成了2个代理方法(这里只保留了register方法),第一个代理方法CGLIB$register$0()
是直接调用父类的方法,第二个方法register()
是代理类真正调用的方法,它会判断是否实现了MethodInterceptor
接口,如果实现就会调用intercept
方法,MethodInterceptor
即为setCallback
时注入的MethodInterceptor
的实现类。
public class UserDao$$EnhancerByCGLIB$$b0e8b18d extends UserDao implements Factory {
...
final void CGLIB$register$0() {
super.register();
}
public final void register() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
// 判断是否实现了MethodInterceptor接口
if (var10000 != null) {
var10000.intercept(this, CGLIB$register$0$Method, CGLIB$emptyArgs, CGLIB$register$0$Proxy);
} else {
super.register();
}
}
...
}
Spring AOP
Spring AOP是基于动态代理实现的对代码无侵入的代码增强方式,它从本质上来说,是将Spring生成代理类对象放入IOC容器中,每次获取目标对象bean时都是通过getBean()
方法,如果一个类被代理,那么实际通过getBean
方法获取的就是代理类的对象,这也是Spring AOP为什么只能作用于IOC容器中的对象。
Spring AOP默认使用的JDK动态代理,如果目标对象没有实现接口,才会使用CGLib来代理,当然也可以强制使用CGLib代理,只需加上@EnableAspectJAutoProxy(proxyTargetClass = true)
注解,@EnableAspectJAutoProxy
一般用来开启Aspect
注解配置,如果是基于xml配置的,在配置文件添加
即可。
在org.aopalliance
包下有两个核心接口,分别是MethodInvocation
和MethodInterceptor
,这两个接口也是Spring AOP中的核心类
- MethodInvocation: AOP对需要增强方法的封装,它是真正执行AOP拦截的,该接口只包含
getMethod()
方法。 - MethodInterceptor:AOP方法拦截器,AOP的相关操作一般在其内部完成
下面代码是JdkDynamicAopProxy
类,它是Spring AOP中JDK动态代理的具体实现,其中invoke()
方法作为代理对象的回调函数被触发,通过invoke
方法具体实现来完成对目标对象方法调用拦截或者功能增强,在invoke()
方法中会创建一个ReflectiveMethodInvocation
对象,该对象的proceed()
方法会调用下一个拦截器,直至拦截器链被调用结束。
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
try {
...
//获得定义好的拦截器链(增强处理)
List
解决自我调用时无法增强的问题
当TestProxyImpl
被Spring Aop增强时,testA()
方法内部调用tesB()
方法,那么testB()
也会被增强吗?实际是不会的,从下面的输出结果可以看到testB()
方法未被增强,可以很容易想到testB()
未被增强的根本原因是this指的目标对象而非代理类对象
@Component
public class TestProxyImpl implements ITestProxy {
@Override
public void testA() {
System.out.println("testA() execute ...");
this.testB();
}
@Override
public void testB() {
System.out.println("testB() execute ...");
}
}
// 输出
[AOP] Before ...
testA() execute ...
testB() execute ...
如果想在testA()
方法调用testB()
方法时增强testB()
方法,即实际调用代理对象的testB()
方法,下面有两种方法可以做到。
设置expose-proxy
属性为true
如果是Spring Boot项目可以直接使用@EnableAspectJAutoProxy(exposeProxy = true)
来暴露代理对象,如果是使用XML配置的,则用
配置即可。该方法的原理就是使用ThreadLocal暂存代理对象,然后通过AopContext.currentProxy()
方法重新拿到代理对象。
// JdkDynamicAopProxy类invoke方法中的代码片段
// 判断expose-proxy属性是否true
if (this.advised.exposeProxy) {
// 暂存到ThreadLocal中,可点入setCurrentProxy方法查看
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
为了能拿到代理对象,可以testA()
方法做如下修改
public void testA() {
System.out.println("testA() execute ...");
//从ThreadLocal中取出代理对象,前提已设置expose-proxy属性为true,暴露了代理对象
ITestProxy proxy = (ITestProxy) AopContext.currentProxy();
proxy.testB();
}
获取代理对象的Bean
还有一种方式和上面方法的原理差不多,都是获取的代理对象再调用testB()
方法,不过该方法直接从Spring容器中获取,下面直接贴代码了~
@Component(value = "testProxy")
public class TestProxyImpl implements ITestProxy,ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void testA() {
System.out.println("testA() execute ...");
applicationContext.getBean("testProxy", ITestProxy.class).testB();
}
@Override
public void testB() {
System.out.println("testB() execute ...");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
本文相关代码地址:https://github.com/LJWLgl/java-demo/tree/master/design-patterns/
原文链接:https://blog.ganzhiqiang.wang/2019/02/17/设计模式——代理模式的思考/#more