ExposeInvocationInterceptor源码分析

master
linlei 2024-04-23 15:05:18 +08:00
parent 06a981d273
commit 431ca04c08
10 changed files with 440 additions and 12 deletions

View File

@ -225,6 +225,7 @@
- [AdvisorChainFactory](spring-aop/spring-aop-advisorChainFactory/README.md)创建Advisor链的工厂接口。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img> - [AdvisorChainFactory](spring-aop/spring-aop-advisorChainFactory/README.md)创建Advisor链的工厂接口。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img>
- [AdvisorAdapterRegistry](spring-aop/spring-aop-advisorAdapterRegistry/README.md)适配各种Advice到AOP拦截器注册和管理Advisor适配器。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img> - [AdvisorAdapterRegistry](spring-aop/spring-aop-advisorAdapterRegistry/README.md)适配各种Advice到AOP拦截器注册和管理Advisor适配器。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img>
- [AopContext](spring-aop/spring-aop-aopContext/README.md)获取Spring AOP代理对象的工具。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img> - [AopContext](spring-aop/spring-aop-aopContext/README.md)获取Spring AOP代理对象的工具。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img>
- [ExposeInvocationInterceptor](spring-aop/spring-aop-exposeInvocationInterceptor/README.md)暴露Spring AOP方法调用上下文的拦截器。<img src="https://img.shields.io/badge/Level-%E7%AE%80%E5%8D%95-0099ff"></img>
+ Spring AOT + Spring AOT

View File

@ -38,6 +38,7 @@
<module>spring-aop-advised</module> <module>spring-aop-advised</module>
<module>spring-aop-aopContext</module> <module>spring-aop-aopContext</module>
<module>spring-aop-targetSourceCreator</module> <module>spring-aop-targetSourceCreator</module>
<module>spring-aop-exposeInvocationInterceptor</module>
</modules> </modules>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -4,11 +4,12 @@
- [一、基本信息](#一基本信息) - [一、基本信息](#一基本信息)
- [二、基本描述](#二基本描述) - [二、基本描述](#二基本描述)
- [三、主要功能](#三主要功能) - [三、主要功能](#三主要功能)
- [四、最佳实践](#四最佳实践) - [四、类源码](#四类源码)
- [五、源码分析](#五源码分析) - [五、最佳实践](#五最佳实践)
- [六、源码分析](#六源码分析)
- [JDK动态代理拦截器](#jdk动态代理拦截器) - [JDK动态代理拦截器](#jdk动态代理拦截器)
- [CGLIB动态代理拦截器](#cglib动态代理拦截器) - [CGLIB动态代理拦截器](#cglib动态代理拦截器)
- [六、常见问题](#六常见问题) - [七、常见问题](#七常见问题)
### 一、基本信息 ### 一、基本信息
@ -32,7 +33,78 @@
+ 在一些特定场景下可能需要在不同的方法间传递AOP代理对象而不是直接调用`this`。`AopContext`类提供了一种解决方案可以在方法调用间传递AOP代理对象。 + 在一些特定场景下可能需要在不同的方法间传递AOP代理对象而不是直接调用`this`。`AopContext`类提供了一种解决方案可以在方法调用间传递AOP代理对象。
### 四、最佳实践 ### 四、类源码
`AopContext`类提供了用于获取当前AOP调用信息的静态方法集合。通过`currentProxy()`方法可以获取当前AOP代理对象前提是AOP框架已配置为暴露代理对象。这对目标对象或通知进行增强调用以及查找通知配置非常有用。然而由于性能成本较高Spring的AOP框架默认不会暴露代理对象。
```java
/**
* 用于获取当前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()`。 使用Spring AOP创建一个代理对象并在代理对象的方法调用前应用自定义的前置通知。首先通过`ProxyFactory`创建了一个代理工厂,并设置了要被代理的目标对象`MyService`。然后通过`proxyFactory.setExposeProxy(true)`来暴露代理对象,以便在方法内部可以使用`AopContext`类访问到代理对象。接着,使用`proxyFactory.addAdvisor()`方法添加了一个切面通知器,将自定义的前置通知`MyMethodBeforeAdvice`应用到被`MyAnnotation`注解标记的方法上。最后,通过`proxyFactory.getProxy()`获取代理对象,并调用其方法`foo()`。
@ -111,7 +183,7 @@ Before method bar is called.
bar... bar...
``` ```
### 、源码分析 ### 、源码分析
在Spring AOP框架中无论是在JDK动态代理还是CGLIB动态代理的拦截器中都对`AopContext.setCurrentProxy(proxy)`进行了赋值操作。这个赋值操作的目的是将当前AOP代理对象设置为当前线程的上下文中以便在方法内部可以通过`AopContext.currentProxy()`获取代理对象。 在Spring AOP框架中无论是在JDK动态代理还是CGLIB动态代理的拦截器中都对`AopContext.setCurrentProxy(proxy)`进行了赋值操作。这个赋值操作的目的是将当前AOP代理对象设置为当前线程的上下文中以便在方法内部可以通过`AopContext.currentProxy()`获取代理对象。
@ -174,20 +246,14 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy
} }
``` ```
### 、常见问题 ### 、常见问题
1. **代理对象为空问题** 1. **代理对象为空问题**
+ 如果在没有启用`exposeProxy`选项的情况下尝试使用`AopContext.currentProxy()`来获取代理对象则可能会导致返回的代理对象为空因为AOP代理对象并未暴露给方法内部。 + 如果在没有启用`exposeProxy`选项的情况下尝试使用`AopContext.currentProxy()`来获取代理对象则可能会导致返回的代理对象为空因为AOP代理对象并未暴露给方法内部。
2. **多线程环境下的线程安全问题** 2. **多线程环境下的线程安全问题**
+ `AopContext`是基于线程的,如果在多线程环境下并发调用了`AopContext.setCurrentProxy(proxy)`和`AopContext.currentProxy()`,可能会出现线程安全问题,因此需要谨慎处理多线程情况。 + `AopContext`是基于线程的,如果在多线程环境下并发调用了`AopContext.setCurrentProxy(proxy)`和`AopContext.currentProxy()`,可能会出现线程安全问题,因此需要谨慎处理多线程情况。
3. **性能问题**
+ 在某些情况下,频繁地使用`AopContext.currentProxy()`来获取代理对象可能会带来性能开销,因为每次调用都需要对当前线程的上下文进行操作。
4. **可读性问题** 4. **可读性问题**
+ 在方法内部频繁地使用`AopContext.currentProxy()`来获取代理对象可能会降低代码的可读性,因为会使代码变得复杂,难以理解 + 在方法内部频繁地使用`AopContext.currentProxy()`来获取代理对象可能会降低代码的可读性,因为会使代码变得复杂,难以理解

View File

@ -0,0 +1,278 @@
## ExposeInvocationInterceptor
- [ExposeInvocationInterceptor](#exposeinvocationinterceptor)
- [一、基本信息](#一基本信息)
- [二、基本描述](#二基本描述)
- [三、主要功能](#三主要功能)
- [四、类源码](#四类源码)
- [五、最佳实践](#五最佳实践)
- [六、源码分析](#六源码分析)
- [七、常见问题](#七常见问题)
### 一、基本信息
✒️ **作者** - Lex 📝 **博客** - [掘金](https://juejin.cn/user/4251135018533068/posts) 📚 **源码地址** - [github](https://github.com/xuchengsheng/spring-reading)
### 二、基本描述
`ExposeInvocationInterceptor`是Spring AOP中的一个拦截器类主要功能是在AOP调用链中暴露当前方法调用的上下文信息通过暴露`MethodInvocation`对象,使其他拦截器或切面能够访问并处理方法调用的相关信息。
### 三、主要功能
1. **暴露当前方法调用的上下文信息**
+ 通过暴露`MethodInvocation`对象,允许其他拦截器或切面访问当前方法调用的相关信息,如目标对象、方法、参数等。
2. **提供`currentInvocation()`方法**
+ 允许在拦截器或切面中调用`currentInvocation()`方法来获取当前方法调用的`MethodInvocation`对象,从而获取方法调用的上下文信息。
3. **支持AOP调用链的处理**
+ 作为Spring AOP的一个拦截器`ExposeInvocationInterceptor`能够被添加到AOP代理链中确保在调用链的初始阶段就将`MethodInvocation`对象暴露出来,以便后续的拦截器或切面可以使用。
### 四、类源码
`ExposeInvocationInterceptor`拦截器其主要目的是将当前的方法调用上下文暴露为线程本地对象。它允许在Spring AOP中获取方法调用的详细信息例如目标对象、方法、参数等。这个拦截器在AOP链中通常是第一个用于确保其他拦截器或切面能够访问方法调用的完整上下文。
```java
/**
* 拦截器,将当前{@link org.aopalliance.intercept.MethodInvocation}暴露为线程本地对象。
* 仅在必要时使用此拦截器例如当切点例如AspectJ表达式切点需要知道完整的调用上下文时。
*
* <p>除非绝对必要否则不要使用此拦截器。目标对象通常不应知道Spring AOP
* 因为这会创建对Spring API的依赖。目标对象应尽可能是纯POJO。
*
* <p>如果使用,此拦截器通常将是拦截器链中的第一个。
*
* @author Rod Johnson
* @author Juergen Hoeller
*/
@SuppressWarnings("serial")
public final class ExposeInvocationInterceptor implements MethodInterceptor, PriorityOrdered, Serializable {
/** 此类的单例实例。 */
public static final ExposeInvocationInterceptor INSTANCE = new ExposeInvocationInterceptor();
/**
* 此类的单例顾问。在使用Spring AOP时请使用它因为它可以避免创建新的Advisor来包装该实例。
*/
public static final Advisor ADVISOR = new DefaultPointcutAdvisor(INSTANCE) {
@Override
public String toString() {
return ExposeInvocationInterceptor.class.getName() +".ADVISOR";
}
};
private static final ThreadLocal<MethodInvocation> invocation =
new NamedThreadLocal<>("Current AOP method invocation");
/**
* 返回与当前调用关联的AOP Alliance MethodInvocation对象。
* @return 与当前调用关联的调用对象
* @throws IllegalStateException 如果当前没有AOP调用
* 或者ExposeInvocationInterceptor未添加到此拦截器链中
*/
public static MethodInvocation currentInvocation() throws IllegalStateException {
MethodInvocation mi = invocation.get();
if (mi == null) {
throw new IllegalStateException(
"No MethodInvocation found: Check that an AOP invocation is in progress and that the " +
"ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that " +
"advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor! " +
"In addition, ExposeInvocationInterceptor and ExposeInvocationInterceptor.currentInvocation() " +
"must be invoked from the same thread.");
}
return mi;
}
/**
* 确保只能创建规范实例。
*/
private ExposeInvocationInterceptor() {
}
@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
MethodInvocation oldInvocation = invocation.get();
invocation.set(mi);
try {
return mi.proceed();
}
finally {
invocation.set(oldInvocation);
}
}
@Override
public int getOrder() {
return PriorityOrdered.HIGHEST_PRECEDENCE + 1;
}
/**
* Required to support serialization. Replaces with canonical instance
* on deserialization, protecting Singleton pattern.
* <p>Alternative to overriding the {@code equals} method.
*/
private Object readResolve() {
return INSTANCE;
}
}
```
### 五、最佳实践
创建了一个基于注解的应用程序上下文,从中获取了一个名为 `MyService` 的 bean并调用了其 `doSomething()` 方法。
```java
public class ExposeInvocationInterceptorDemo {
public static void main(String[] args) {
// 创建一个基于注解的应用程序上下文
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从上下文中获取 MyService
MyService myService = context.getBean(MyService.class);
// 调用方法
myService.doSomething();
}
}
```
使用了 `@EnableAspectJAutoProxy` 注解启用了 AspectJ 自动代理功能,并且通过 `@ComponentScan` 注解扫描了包 `com.xcs.spring` 下的组件。
```java
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.xcs.spring")
public class AppConfig {
}
```
一个服务类,用于业务逻辑的实现。
```java
@Service
public class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}
```
`MyMethodInterceptor`标记为 `@Aspect``@Component`,表明它是一个切面,并且由 Spring 容器进行管理。其中包含一个名为 `before()` 的方法,使用 `@Before` 注解标记,表示在目标方法执行之前执行。方法内部调用了 `LogUtil.print()` 方法,用于记录日志或执行其他操作。这个切面主要是针对 `com.xcs.spring.MyService` 类中所有公共方法的执行,在方法执行之前添加了特定的逻辑。
```java
@Aspect
@Component
public class MyMethodInterceptor {
@Before("execution(public * com.xcs.spring.MyService.*(..))")
public void before() {
LogUtil.print();
}
}
```
通过 `ExposeInvocationInterceptor.currentInvocation()` 获取当前方法调用的 `ProxyMethodInvocation` 对象,然后打印了方法名称、参数长度、目标对象以及代理对象的类名。
```java
public class LogUtil {
public static void print() {
ProxyMethodInvocation methodInvocation = (ProxyMethodInvocation) ExposeInvocationInterceptor.currentInvocation();
System.out.println("Method = " + methodInvocation.getMethod());
System.out.println("Arguments Length = " + methodInvocation.getArguments().length);
System.out.println("Target = " + methodInvocation.getThis());
System.out.println("Proxy Class = " + methodInvocation.getProxy().getClass());
}
}
```
运行结果,通过`ExposeInvocationInterceptor.currentInvocation()`获取方法调用上下文实现日志打印。
```java
Method = public void com.xcs.spring.MyService.doSomething()
Arguments Length = 0
Target = com.xcs.spring.MyService@49964d75
Proxy Class = class com.xcs.spring.MyService$$EnhancerBySpringCGLIB$$f30643a6
Doing something...
```
### 六、源码分析
在`org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#extendAdvisors`方法中,在开头添加了一个 `ExposeInvocationInterceptor`
```java
/**
* 将{@link ExposeInvocationInterceptor}添加到advice链的开头。
* <p>在使用AspectJ切点表达式和AspectJ风格的advice时需要此额外的Advisors。
*/
@Override
protected void extendAdvisors(List<Advisor> candidateAdvisors) {
AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}
```
在`org.springframework.aop.aspectj.AspectJProxyUtils#makeAdvisorChainAspectJCapableIfNecessary`方法中用于在代理链中添加特殊的拦截器以确保与包含AspectJ建议的代理链一起正常工作。具体来说它将 `ExposeInvocationInterceptor` 添加到建议列表的开头。这样做的目的是为了暴露当前Spring AOP调用对于某些AspectJ切点匹配是必要的并使当前AspectJ JoinPoint可用。如果建议链中不存在AspectJ建议则此调用不会产生任何效果。方法返回 `true` 表示成功向建议列表中添加了 `ExposeInvocationInterceptor`,否则返回 `false`
```java
/**
* 如果需要向包含AspectJ建议的代理链中添加特殊的建议
* 具体来说,在列表的开头添加{@link ExposeInvocationInterceptor}。
* <p>这将暴露当前Spring AOP调用对于某些AspectJ切点匹配是必要的
* 并使当前AspectJ JoinPoint可用。如果建议链中没有AspectJ建议则调用不会产生任何效果。
* @param advisors 可用的建议列表
* @return 如果向列表中添加了{@link ExposeInvocationInterceptor},则返回{@code true},否则返回{@code false}
*/
public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
// 不要向空列表添加建议;这可能表示不需要代理
if (!advisors.isEmpty()) {
boolean foundAspectJAdvice = false;
for (Advisor advisor : advisors) {
// 谨慎使用不带保护的Advice因为这可能会急切地实例化非单例的AspectJ切面...
if (isAspectJAdvice(advisor)) {
foundAspectJAdvice = true;
break;
}
}
// 如果在建议链中找到AspectJ建议并且没有ExposeInvocationInterceptor.ADVISOR则添加
if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
return true;
}
}
return false;
}
```
在`org.springframework.aop.aspectj.AspectJProxyUtils#isAspectJAdvice`方法中,判断给定的 Advisor 是否包含 AspectJ Advice。它检查 Advisor 实例是否属于特定类型或者其 Advice 是否是 AbstractAspectJAdvice 的子类,或者其 Pointcut 是否是 AspectJExpressionPointcut 的实例。
```java
/**
* 判断给定的 Advisor 是否包含 AspectJ Advice。
* @param advisor 要检查的 Advisor
*/
private static boolean isAspectJAdvice(Advisor advisor) {
return (advisor instanceof InstantiationModelAwarePointcutAdvisor ||
advisor.getAdvice() instanceof AbstractAspectJAdvice ||
(advisor instanceof PointcutAdvisor &&
((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut));
}
```
### 七、常见问题
1. **什么时候应该使用 `ExposeInvocationInterceptor`**
- 当需要在 Spring AOP 中暴露方法调用的完整上下文时,通常使用 `ExposeInvocationInterceptor`。这在一些特定的切面编程场景下是必要的,比如需要在切点中访问方法参数、目标对象或其他方法调用信息时。
2. **为什么调用 `ExposeInvocationInterceptor.currentInvocation()` 抛出异常?**
- 这可能是因为没有在拦截器链的开头正确配置 `ExposeInvocationInterceptor`,或者在不正确的线程上下文中调用了 `currentInvocation()` 方法。另外,如果没有正在进行的 AOP 调用,也会抛出异常。

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.xcs.spring</groupId>
<artifactId>spring-aop</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-aop-exposeInvocationInterceptor</artifactId>
</project>

View File

@ -0,0 +1,12 @@
package com.xcs.spring;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.xcs.spring")
public class AppConfig {
}

View File

@ -0,0 +1,15 @@
package com.xcs.spring;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ExposeInvocationInterceptorDemo {
public static void main(String[] args) {
// 创建一个基于注解的应用程序上下文
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从上下文中获取 MyService
MyService myService = context.getBean(MyService.class);
// 调用方法
myService.doSomething();
}
}

View File

@ -0,0 +1,15 @@
package com.xcs.spring;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
public class LogUtil {
public static void print() {
ProxyMethodInvocation methodInvocation = (ProxyMethodInvocation) ExposeInvocationInterceptor.currentInvocation();
System.out.println("Method = " + methodInvocation.getMethod());
System.out.println("Arguments Length = " + methodInvocation.getArguments().length);
System.out.println("Target = " + methodInvocation.getThis());
System.out.println("Proxy Class = " + methodInvocation.getProxy().getClass());
}
}

View File

@ -0,0 +1,15 @@
package com.xcs.spring;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyMethodInterceptor {
@Before("execution(public * com.xcs.spring.MyService.*(..))")
public void before() {
LogUtil.print();
}
}

View File

@ -0,0 +1,11 @@
package com.xcs.spring;
import org.springframework.stereotype.Service;
@Service
public class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}