spring-reading./spring-aop/spring-aop-aopContext/README.md

11 KiB
Raw Blame History

AopContext

一、基本信息

✒️ 作者 - Lex 📝 博客 - 掘金 📚 源码地址 - github

二、基本描述

AopContext类是Spring AOP框架提供的一个工具类用于在方法内部访问当前AOP代理对象。通过currentProxy()方法可以获取当前方法被AOP代理后的代理对象从而在方法内部执行代理对象的其他方法或获取相关信息。

三、主要功能

  1. 获取当前AOP代理对象

    • 通过调用currentProxy()方法可以在方法内部获取到当前的AOP代理对象即被AOP增强后的对象。
  2. 允许在方法内部调用代理对象的方法

    • 获得代理对象后,可以在方法内部直接调用代理对象的其他方法,包括被增强的切面方法或目标对象的方法。
  3. 解决代理对象的传递问题

    • 在一些特定场景下可能需要在不同的方法间传递AOP代理对象而不是直接调用thisAopContext类提供了一种解决方案可以在方法调用间传递AOP代理对象。

四、类源码

AopContext类提供了用于获取当前AOP调用信息的静态方法集合。通过currentProxy()方法可以获取当前AOP代理对象前提是AOP框架已配置为暴露代理对象。这对目标对象或通知进行增强调用以及查找通知配置非常有用。然而由于性能成本较高Spring的AOP框架默认不会暴露代理对象。

/**
 * 用于获取当前AOP调用信息的静态方法集合。
 * 
 * <p>如果AOP框架配置为暴露当前代理对象非默认情况则可使用 {@code currentProxy()} 方法获取正在使用的AOP代理对象。
 * 目标对象或通知可以使用此方法进行增强调用类似于EJB中的 {@code getEJBObject()}。也可用于查找通知配置。
 * 
 * <p>Spring的AOP框架默认不暴露代理对象因为这样做会带来性能开销。
 * 
 * <p>此类中的功能可被目标对象使用以获取调用中的资源。然而当存在合理替代方案时不应使用此方法因为这会使应用程序代码依赖于AOP下的使用和Spring AOP框架。
 * 
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @since 13.03.2003
 */
public final class AopContext {

	/**
	 * 线程本地变量用于保存与该线程关联的AOP代理对象。
	 * 除非控制代理配置的“exposeProxy”属性被设置为“true”否则将包含{@code null}。
	 * @see ProxyConfig#setExposeProxy
	 */
	private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");


	private AopContext() {
	}


	/**
	 * 尝试返回当前AOP代理对象。此方法仅在调用方法通过AOP调用并且AOP框架已设置为暴露代理对象时可用。
	 * 否则此方法将抛出IllegalStateException异常。
	 * @return 当前AOP代理对象永远不会返回{@code null}
	 * @throws IllegalStateException 如果无法找到代理对象因为该方法是在AOP调用上下文之外调用的或者因为AOP框架尚未配置为暴露代理对象
	 */
	public static Object currentProxy() throws IllegalStateException {
		Object proxy = currentProxy.get();
		if (proxy == null) {
			throw new IllegalStateException(
					"Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and " +
							"ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.");
		}
		return proxy;
	}

	/**
	 * 使给定的代理对象可通过{@code currentProxy()}方法访问。
	 * <p>注意,调用者应谨慎地保留旧值。
	 * @param proxy 要暴露的代理对象(或{@code null}以重置)
	 * @return 旧的代理对象,如果没有绑定,则可能为{@code null}
	 * @see #currentProxy()
	 */
	@Nullable
	static Object setCurrentProxy(@Nullable Object proxy) {
		Object old = currentProxy.get();
		if (proxy != null) {
			currentProxy.set(proxy);
		}
		else {
			currentProxy.remove();
		}
		return old;
	}

}

五、最佳实践

使用Spring AOP创建一个代理对象并在代理对象的方法调用前应用自定义的前置通知。首先通过ProxyFactory创建了一个代理工厂,并设置了要被代理的目标对象MyService。然后通过proxyFactory.setExposeProxy(true)来暴露代理对象,以便在方法内部可以使用AopContext类访问到代理对象。接着,使用proxyFactory.addAdvisor()方法添加了一个切面通知器,将自定义的前置通知MyMethodBeforeAdvice应用到被MyAnnotation注解标记的方法上。最后,通过proxyFactory.getProxy()获取代理对象,并调用其方法foo()

public class AopContextDemo {

    public static void main(String[] args) {
        // 创建代理工厂&创建目标对象
        ProxyFactory proxyFactory = new ProxyFactory(new MyService());
        // 暴露代理对象
        proxyFactory.setExposeProxy(true);
        // 创建通知器
        proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(MyAnnotation.class), new MyMethodBeforeAdvice()));
        // 获取代理对象
        MyService proxy = (MyService) proxyFactory.getProxy();
        // 调用代理对象的方法
        proxy.foo();
    }
}

MyMethodBeforeAdvice自定义前置通知类``。

public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("Before method " + method.getName() + " is called.");
    }
}

自定义注解MyAnnotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
}

定义了一个名为MyService的Java类它被@MyAnnotation注解标记。该类包含两个方法,foo()bar()。在foo()方法中,通过调用AopContext.currentProxy()来获取当前的AOP代理对象并使用该代理对象来调用bar()方法以确保AOP切面可以正确地被应用。这种方式避免了直接调用bar()方法而导致AOP切面失效的问题。

@MyAnnotation
public class MyService {

    public void foo() {
        System.out.println("foo...");
        // 直接调用bar会导致切入无效
        // this.bar();
        // 获取代理对象并调用bar
        ((MyService) AopContext.currentProxy()).bar();
    }

    public void bar() {
        System.out.println("bar...");
    }
}

运行结果1foo()方法被调用时,会执行前置通知打印日志,然后调用this.bar()方法,由于直接调用this.bar()方法绕过了AOP代理对象因此不会触发AOP切面的逻辑。

Before method foo is called.
foo...
bar...

运行结果2foo()方法被调用时,会执行前置通知打印日志,然后调用((MyService) AopContext.currentProxy()).bar();方法。由于使用了AopContext.currentProxy()获取了当前的AOP代理对象并调用了该代理对象的bar()方法因此会触发AOP切面的逻辑。

Before method foo is called.
foo...
Before method bar is called.
bar...

六、源码分析

在Spring AOP框架中无论是在JDK动态代理还是CGLIB动态代理的拦截器中都对AopContext.setCurrentProxy(proxy)进行了赋值操作。这个赋值操作的目的是将当前AOP代理对象设置为当前线程的上下文中以便在方法内部可以通过AopContext.currentProxy()获取代理对象。

JDK动态代理拦截器

org.springframework.aop.framework.JdkDynamicAopProxy#invoke方法中是JDK动态代理拦截器的一部分。主要处理了AOP代理的上下文。具体来说在方法执行前如果AOP代理对象已经暴露了this.advised.exposeProxytrue),则通过AopContext.setCurrentProxy(proxy)方法将当前的AOP代理对象设置为当前线程的上下文中以便在方法内部可以通过AopContext.currentProxy()来获取代理对象。在方法执行完成后将之前设置的代理对象恢复以保证AOP代理对象的上下文不会影响其他线程。

@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;
    // ... [代码部分省略以简化]
    try {
        // ... [代码部分省略以简化]
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }
        // ... [代码部分省略以简化]
    }
    finally {
        // ... [代码部分省略以简化]
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

CGLIB动态代理拦截器

org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept方法中是CGLIB动态代理拦截器的一部分。在方法拦截过程中它主要处理了AOP代理的上下文。具体来说如果AOP代理对象已经暴露了this.advised.exposeProxytrue),则通过AopContext.setCurrentProxy(proxy)方法将当前的AOP代理对象设置为当前线程的上下文中以便在方法内部可以通过AopContext.currentProxy()来获取代理对象。在方法执行完成后将之前设置的代理对象恢复以保证AOP代理对象的上下文不会影响其他线程。

@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;
    // ... [代码部分省略以简化]
    try {
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }
        // ... [代码部分省略以简化]
    }
    finally {
        // ... [代码部分省略以简化]
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

七、常见问题

  1. 代理对象为空问题

    • 如果在没有启用exposeProxy选项的情况下尝试使用AopContext.currentProxy()来获取代理对象则可能会导致返回的代理对象为空因为AOP代理对象并未暴露给方法内部。
  2. 多线程环境下的线程安全问题

    • AopContext是基于线程的,如果在多线程环境下并发调用了AopContext.setCurrentProxy(proxy)AopContext.currentProxy(),可能会出现线程安全问题,因此需要谨慎处理多线程情况。
  3. 可读性问题

    • 在方法内部频繁地使用AopContext.currentProxy()来获取代理对象可能会降低代码的可读性,因为会使代码变得复杂,难以理解