什么是 AOP
AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP 在 Spring 中的作用
AOP(面向切面编程)是一种编程范式,用于将关注点(如日志记录、事务管理、安全控制等)从应用的业务逻辑中分离出来,增强代码的模块化性。下面是关于AOP切面编程的一些专业术语的详细解释和示例:
术语 | 含义 | 示例 |
---|---|---|
目标 (Target) | 被通知的对象。通常是指需要增强的类或方法,切面会在这些对象上应用通知。 | 假设有一个UserService 类,其中有一个createUser() 方法。UserService 就是目标对象,createUser() 是目标方法。 |
代理 (Proxy) | 向目标对象应用通知之后创建的代理对象。通过代理可以控制对目标对象的访问,进行增强逻辑的调用。 | 假设UserService 类上有日志切面。通过AOP,创建一个代理对象UserServiceProxy ,该代理对象会在调用createUser() 之前先执行日志记录的通知。 |
连接点 (JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点,指的是代码执行中的某个点(方法调用、字段修改等)。 | 在UserService 类的createUser() 方法执行时,createUser() 方法的执行就是一个连接点。其他方法如deleteUser() 也是连接点。 |
切入点 (Pointcut) | 被切面拦截 / 增强的连接点。切入点通常是指定符合某些条件(如方法名、参数等)的连接点。切入点是连接点的一个子集。 | 可以定义切入点:execution(* com.example.UserService.createUser(..)) ,它指示增强UserService 类中的createUser() 方法。在这个例子中,createUser() 是切入点。 |
通知 (Advice) | 增强的逻辑,或者说拦截到目标对象的连接点之后要执行的操作。它决定了切面在何时执行,做什么事情。 | 假设loggingAdvice() 是一个通知方法,当createUser() 方法被调用时,loggingAdvice() 会被执行。通知有多种类型:前置通知(before)、后置通知(after)、环绕通知(around)等。 |
切面 (Aspect) | 切入点 + 通知,切面是AOP的核心部分,定义了在哪些连接点执行哪些通知。 | 在UserService 类中,可以定义一个切面LoggingAspect ,它包含了loggingAdvice() 通知和createUser() 方法的切入点。这个切面用于日志记录。 |
织入 (Weaving) | 将通知应用到目标对象,并生成代理对象的过程。织入通常发生在编译期、类加载期或运行期。 | 在Spring AOP中,织入通常在运行时进行,通过动态代理技术将loggingAdvice() 通知应用到createUser() 方法的执行过程中,从而生成代理对象。 |
示例:注解方式的 AOP 实现
假设我们有一个UserService
类,它包含一个createUser()
方法:
public class UserService {
public void createUser(String username) {
System.out.println("Creating user: " + username);
}
}
我们想在每次执行createUser()
方法时,记录日志。我们可以通过AOP来实现这一功能。
1. 目标 (Target)
含义:目标对象是指被切面(Aspect)增强的对象。它通常是业务逻辑所在的类或方法,切面对它进行增强(例如增加日志、事务、性能监控等功能)。
示例:假设我们有一个UserService
类,它负责处理用户相关的操作:
public class UserService {
public void createUser(String username) {
System.out.println("Creating user: " + username);
}
}
UserService
类就是目标对象,createUser()
方法是目标方法。
2. 代理 (Proxy)
含义:代理对象是在目标对象和切面之间生成的一个中介对象。通过代理对象,切面通知可以在方法执行前后或围绕方法执行进行插入。
示例:在Spring AOP中,代理对象是在运行时生成的,它会增强目标对象的行为。假设我们通过Spring AOP为UserService
类的createUser()
方法添加日志通知,Spring会为UserService
创建一个代理对象,这个代理对象在调用目标方法时,会先执行通知逻辑(例如记录日志)。
3. 连接点 (JoinPoint)
含义:连接点是程序执行中的一个明确点,通常指方法的执行。连接点表示程序中可能插入通知的地方。在Spring AOP中,连接点一般是方法执行的地方。
示例:在UserService
的createUser()
方法中,方法执行就是一个连接点。每当执行createUser()
方法时,它就是一个连接点。例如:
public void createUser(String username) {
System.out.println("Creating user: " + username);
}
这里的createUser()
方法执行就是一个连接点。但是是通过用一个 JoinPoint
接口来获取切入点所在目标对象。后面可以通过 joinPoint.getSignature();、joinPoint.getArgs();
等来获取这个 createUser()
方法的信息,而不是直接获取,后面有详细介绍。
4. 切入点 (Pointcut)
含义:切入点是指选择连接点的表达式,标识哪些连接点需要被通知拦截。一个切入点可以定义特定的方法或类,当这些方法或类执行时,通知就会被触发。
示例:假设我们希望对UserService
类中的createUser()
方法应用日志记录,我们可以定义一个切入点:
@Pointcut("execution(* com.example.UserService.createUser(..))")
public void createUserPointcut() {}
这个切入点表达式会匹配UserService
类中的createUser()
方法。每当createUser()
方法被调用时,切面会在该连接点触发通知。
- 切面(Aspect) 是由
@Aspect
注解标注的类,而 切入点(Pointcut) 是定义了在哪些连接点(方法执行、方法调用等)应用增强的规则。切入点(@Pointcut
)定义必须放置在标有@Aspect
注解的类中的方法上。 - 方法
public void createUserPointcut() {}
是一个标记方法,不会执行任何代码,仅仅为了给切入点命名。 - 因此,方法名可以随便取,只要它是一个有效的方法名,而且没有实际的逻辑或执行代码,作用仅仅是作为切入点的一个标记。例如,你可以改成
public void logMethodExecution() {}
,切入点表达式仍然是@Pointcut("execution(* com.example.UserService.createUser(..))")
,它会照常工作。
5. 通知 (Advice)
含义:通知是定义在连接点上要执行的代码。通知决定了在何时、如何应用切面。Spring AOP支持不同类型的通知,比如前置通知、后置通知、异常通知、环绕通知等。
示例:假设我们定义一个前置通知(在方法执行前执行):
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.UserService.createUser(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before creating user");
}
}
这个通知会在目标方法(createUser()
)执行之前被调用,用于记录日志。
6. 切面 (Aspect)
含义:切面是通知和切入点的结合体。它将一个通知与多个切入点进行绑定,形成一个增强的模块。一个切面可以包含多个通知,并且每个通知可以应用于不同的连接点。
示例:假设我们有一个日志记录的切面,它既有前置通知,也有后置通知,用于增强UserService
类中的createUser()
方法:
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.UserService.createUser(..))")
public void createUserPointcut() {}
@Before("createUserPointcut()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before creating user");
}
@After("createUserPointcut()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After creating user");
}
}
等价于: (重点理解两种方式的不同)
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.UserService.createUser(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before creating user");
}
@After("execution(* com.example.UserService.createUser(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After creating user");
}
}
- 在这里,
LoggingAspect
就是一个切面,它包含了一个前置通知logBefore()
和一个后置通知logAfter()
。 - 第一种定义方式切入点被复用:该切入点方法 createUserPointcut() 被 @Before 和 @After 注解的增强方法复用。换句话说,createUserPointcut() 定义了切入点,在多个增强方法中都可以复用,直接通过 createUserPointcut() 标识。
- 第二种方式切入点在增强方法上直接定义:在这个例子中,切入点表达式 execution(* com.example.UserService.createUser(…)) 是直接在 @Before 和 @After 注解中定义的,而没有使用 @Pointcut 注解来定义切入点方法。
7. 织入 (Weaving)
含义:织入是指将通知应用到目标对象并生成代理对象的过程。织入可以发生在编译时、类加载时或运行时。在Spring AOP中,织入通常发生在运行时。
示例:在Spring中,织入通过动态代理完成。当Spring容器启动时,会创建目标对象的代理对象,代理对象会在调用目标方法之前和之后插入通知逻辑。织入的结果是,目标对象的行为被增强了。
例如,假设在运行时,Spring AOP通过动态代理为UserService
类创建了一个代理对象,该代理对象会在执行createUser()
方法时,先执行前置通知logBefore()
,然后执行目标方法,最后执行后置通知logAfter()
。
总结示例
- 目标:
UserService
类 - 连接点:
UserService
类的createUser()
方法执行 - 切入点:
execution(* com.example.UserService.createUser(..))
,选择createUser()
方法作为切入点 - 通知:前置通知
logBefore()
,后置通知logAfter()
- 切面:
LoggingAspect
类,包含logBefore()
和logAfter()
通知,并定义了切入点 - 织入:Spring通过动态代理,将
LoggingAspect
织入到UserService
类的createUser()
方法中,形成代理对象
运行时
当调用createUser()
时,代理对象会先调用logBefore()
,然后调用目标方法createUser()
,最后调用logAfter()
。
execute表达式
*
代表匹配任意修饰符及任意返回值,参数列表中..
匹配任意数量的参数
在 Spring AOP 中,execution
表达式是切入点表达式的一种形式,用于定义在哪些方法上应用增强。它是 AspectJ 中非常常用的切入点表达式之一,用于匹配方法执行的连接点。execution
表达式提供了强大的功能,能够根据方法签名、返回值类型、方法名、参数类型等条件来选择匹配的方法。
execution
表达式的语法
execution
表达式的基本语法如下:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(parameter-pattern) throws-pattern?)
modifiers-pattern
: 可选,匹配方法修饰符(如public
、private
等)。return-type-pattern
: 返回类型的模式,指定方法的返回类型。declaring-type-pattern
: 可选,类的模式,指定方法所在的类。method-name-pattern
: 方法名称的模式,指定方法的名称。parameter-pattern
: 参数类型的模式,指定方法的参数类型。throws-pattern
: 可选,指定方法声明的异常类型。
1. execution
表达式的组成部分
1.1 返回类型(return-type-pattern)
返回类型用于指定匹配方法的返回值类型。可以使用具体的类型,也可以使用通配符 *
来匹配任何类型。
execution(public void com.example.MyClass.myMethod()) // 返回类型为 void 的方法
execution(* com.example.MyClass.*(..)) // 返回类型为任意类型的方法
1.2 方法名(method-name-pattern)
方法名用于匹配方法的名称。可以使用具体的方法名,也可以使用通配符 *
来匹配任意方法。
execution(* com.example.MyClass.create*(..)) // 匹配所有以 "create" 开头的方法
execution(* com.example.MyClass.*(..)) // 匹配类中所有的方法
1.3 参数类型(parameter-pattern)
parameter-pattern
用于匹配方法参数的类型。可以使用具体类型,也可以使用通配符 ..
来匹配任意参数。
..
表示匹配零个或多个参数。*
匹配任何单个类型。
例如:
execution(* com.example.MyClass.myMethod(String)) // 匹配一个参数为 String 类型的方法
execution(* com.example.MyClass.myMethod(..)) // 匹配任意参数的方法
execution(* com.example.MyClass.myMethod(String, Integer)) // 匹配具有 String 和 Integer 类型参数的方法
1.4 类或接口的类型(declaring-type-pattern)
declaring-type-pattern
用于指定方法所在的类。你可以指定一个类,或者使用 ..
来匹配所有类的方法。
execution(* com.example.MyClass.myMethod(..)) // 匹配 `MyClass` 类中的 `myMethod` 方法
execution(* com.example.*.*(..)) // 匹配 `com.example` 包中所有类的所有方法
execution(* com.example.MyClass+.*(..)) // 匹配 `MyClass` 类及其所有子类的方法(`+` 表示继承的类)
1.5 修饰符(modifiers-pattern)
modifiers-pattern
用于匹配方法的修饰符,如 public
、private
、protected
等。此部分是可选的。
execution(public * com.example.MyClass.myMethod(..)) // 匹配 `public` 修饰的方法
execution(private * com.example.MyClass.myMethod(..)) // 匹配 `private` 修饰的方法
2. execution
表达式的示例
示例 1:匹配指定类的所有方法
@Pointcut("execution(* com.example.UserService.*(..))")
public void allUserServiceMethods() {}
这个切入点会匹配 com.example.UserService
类中的所有方法。*
表示方法的返回类型,(..)
表示方法的参数,可以匹配任意数量和类型的参数。
示例 2:匹配方法名为 createUser
,参数为任意类型的方法
@Pointcut("execution(* com.example.UserService.createUser(..))")
public void createUserMethod() {}
这个切入点会匹配 com.example.UserService
类中名为 createUser
的方法,且该方法的参数类型不做限制((..)
表示匹配任意数量的参数)。
示例 3:匹配返回值为 void
,并且方法名以 create
开头的方法
@Pointcut("execution(void com.example.UserService.create*(..))")
public void createUserMethods() {}
这个切入点会匹配 com.example.UserService
类中,返回类型为 void
且方法名以 create
开头的方法。
示例 4:匹配 com.example
包中所有类的 createUser
方法
@Pointcut("execution(* com.example.*.createUser(..))")
public void createUserInPackage() {}
这个切入点会匹配 com.example
包下所有类中的 createUser
方法。
示例 5:匹配任何参数的 public
方法
@Pointcut("execution(public * com.example.UserService.*(..))")
public void allPublicMethods() {}
这个切入点会匹配 com.example.UserService
类中所有 public
修饰的方法,不管方法名和参数类型是什么。
示例 6:匹配抛出特定异常的方法
@Pointcut("execution(* com.example.UserService.*(..)) throws java.io.IOException")
public void methodsThrowingIOException() {}
这个切入点会匹配 com.example.UserService
类中抛出 IOException
异常的方法。
3. execution
表达式的常用模式
execution(* com.example.*.*(..))
: 匹配com.example
包中所有类的所有方法。execution(void com.example.UserService.create*(..))
: 匹配com.example.UserService
类中所有以create
开头的void
返回类型的方法。execution(public * com.example.UserService.*(..))
: 匹配com.example.UserService
类中所有public
修饰的方法。execution(* com.example.UserService.*(String))
: 匹配com.example.UserService
类中参数类型为String
的所有方法。
拦截AccountService(类、接口)中定义的所有方法
execution(* com.xyz.service.AccountService.*(..))
拦截包中定义的方法,不包含子包中的方法
拦截com.xyz.service包中所有类中任意方法,不包含子包中的类
execution(* com.xyz.service.*.*(..))
拦截包或者子包中定义的方法
拦截com.xyz.service包或者子包中定义的所有方法
execution(* com.xyz.service..*.*(..))
annotation() 表达式
在 Spring AOP 中,annotation()
表达式用于匹配带有特定注解的方法。它是 Spring AOP 中的一种切点表达式,常用于定义切点时,匹配被特定注解标注的方法。这个功能是基于 Spring 的代理机制,通常在声明切面时使用,结合 @Before
、@After
、@Around
等注解来创建 AOP 切面。
annotation()
的语法
annotation()
表达式的语法如下:
@annotation(<注解类型>)
<注解类型>
是想要匹配的注解的类型。- 这种表达式会匹配所有标记了指定注解的方法。
使用场景
假设有一个方法,它上面标记了一个自定义注解 @LogExecution
,并且你希望在执行该方法之前、之后或者周围插入额外的逻辑。此时,你可以使用 annotation()
来构建切点。
示例
1. 定义一个自定义注解
首先,我们定义一个自定义的注解 @LogExecution
:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
String value() default "Execution Log";
}
2. 定义一个切面类,使用 annotation()
表达式
在切面类中,我们可以使用 @Before
、@After
、@Around
等注解来声明切点,结合 annotation()
表达式来匹配 @LogExecution
注解的方法。
@Aspect
@Component
public class LoggingAspect {
@Before("@annotation(logExecution)") // 拦截带有 @LogExecution 注解的方法
public void logBefore(JoinPoint joinPoint, LogExecution logExecution) {
System.out.println("Before execution: " + logExecution.value());
}
@After("@annotation(logExecution)") // 拦截带有 @LogExecution 注解的方法
public void logAfter(JoinPoint joinPoint, LogExecution logExecution) {
System.out.println("After execution: " + logExecution.value());
}
}
@Before("@annotation(logExecution)")
表示在方法执行之前触发切面,logExecution
是匹配到的LogExecution
注解实例,可以用它来获取注解上的属性值。@After("@annotation(logExecution)")
表示在方法执行之后触发切面,logExecution
用于获取注解中的值。
3. 使用注解的业务类
然后,你可以在你的业务类中使用 @LogExecution
注解:
@Service
public class MyService {
@LogExecution(value = "This is a custom log message.")
public void myMethod() {
System.out.println("Method executed");
}
}
4. 结果
当你调用 myMethod()
方法时,会触发切面逻辑,输出如下内容:
Before execution: This is a custom log message.
Method executed
After execution: This is a custom log message.
注意事项
-
注解只能作用于方法:在 Spring AOP 中,
annotation()
表达式只能匹配标注在方法上的注解,因此它仅适用于方法级别的切面。 -
代理机制:Spring AOP 使用的是基于代理的机制,只有通过代理执行的方法才会被 AOP 拦截。因此,
annotation()
只能拦截代理对象中调用的带注解的方法,不会拦截直接调用同一类中方法的情况。 -
注解属性传递:在切面方法中,你可以通过
@annotation(logExecution)
获取到注解的属性值,并在逻辑中使用这些信息。 -
适用范围:
annotation()
表达式适用于任何注解,既可以是自定义注解,也可以是 Spring 提供的标准注解,如@Transactional
、@Cacheable
等。
匹配类上的注解
通过结合 within
表达式,annotation()
也可以匹配类上的注解。也就是说,若一个类被指定注解标记,则可以匹配该类中的所有方法。例如:
@Pointcut("within(@com.example.MyClassAnnotation *)")
public void classesWithAnnotation() {}
或者
@Pointcut("@within(com.example.MyClassAnnotation *)")
public void classesWithAnnotation() {}
annotation(MyAnnotation)
:用于匹配方法上的注解。@within(MyAnnotation)
或within(@MyAnnotation *)
:用于匹配类上的注解。
组合使用
通过 &&
(与)、||
(或)、!
(非)、within()
、@within()
、@annotation()
、execution()
等组合操作符,Spring AOP 的切入点表达式可以组合成各种复杂的逻辑,满足不同的切面需求。
annotation()
可以与其他切入点表达式组合,从而实现更加精确的匹配。例如,结合 execution
表达式,可以指定仅匹配特定包下带有注解的方法:
1. &&
- 与(AND)操作符
&&
操作符用于组合多个切入点表达式,表示多个条件同时成立时才会匹配。
示例:
@Pointcut("execution(* com.example.service.*.*(..)) && annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethods() {}
在这个例子中,切入点表达式同时需要满足以下两个条件:
- 方法位于
com.example.service
包下。 - 方法被
@Transactional
注解标记。
只有满足这两个条件的方法才会被切面匹配到。
2. ||
- 或(OR)操作符
||
操作符用于组合多个切入点表达式,表示多个条件之一满足时就会匹配。
示例:
@Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.dao.*.*(..))")
public void serviceOrDaoMethods() {}
在这个例子中,切入点表达式匹配位于 com.example.service
包下 或者 位于 com.example.dao
包下的所有方法。
3. !
- 非(NOT)操作符
!
操作符用于否定某个切入点表达式,表示不匹配该条件。
示例:
@Pointcut("execution(* com.example.service.*.*(..)) && !annotation(org.springframework.transaction.annotation.Transactional)")
public void serviceMethodsWithoutTransactional() {}
在这个例子中,切入点表达式会匹配 com.example.service
包下的所有方法,但排除了那些被 @Transactional
注解标记的方法。
4. within()
- 限定范围
within()
操作符用于限定切入点的作用范围,通常用于匹配方法所在的类或者接口。
示例:
@Pointcut("within(com.example.service.*)")
public void methodsInServicePackage() {}
这个表达式会匹配 com.example.service
包下的所有方法。
结合 @
使用,匹配类上的注解:
@Pointcut("within(@com.example.Secure *)")
public void secureClasses() {}
这个表达式会匹配所有被 @Secure
注解标记的类中的方法。
5. @within()
- 匹配类上的注解
@within()
表达式用于匹配具有特定注解的类中的所有方法。
示例:
@Pointcut("@within(com.example.Secure)")
public void secureClassMethods() {}
这个表达式会匹配所有被 @Secure
注解标记的类中的方法。
6. @annotation()
- 匹配方法上的注解
@annotation()
用于匹配方法上带有特定注解的方法。
示例:
@Pointcut("annotation(com.example.Loggable)")
public void loggableMethods() {}
这个表达式会匹配所有被 @Loggable
注解标记的方法。
7. execution()
- 匹配方法执行
execution()
是一个非常常用的操作符,用于匹配方法执行时的切入点。它是最常见的切入点表达式之一,可以通过方法签名来精确控制匹配。
示例:
@Pointcut("execution(* com.example.service.*.*(..))")
public void allMethodsInServicePackage() {}
这个表达式会匹配 com.example.service
包下的所有方法。
JoinPoint 接口介绍
在 Spring AOP 中,JoinPoint
是一个重要的接口,用于在切面方法中获取关于当前连接点(如方法调用)的信息。它提供了关于正在被拦截的目标方法的详细数据,如方法签名、方法参数等。JoinPoint
通常出现在 @Before
、@After
、@Around
等通知方法中,用来访问目标方法的信息。
1. JoinPoint 接口
JoinPoint
是一个接口,它定义了以下几个常用的方法来获取方法执行的上下文信息:
主要方法:
-
getSignature()
: 获取当前连接点的签名(MethodSignature
)。签名包含了方法的名称、参数类型、返回值类型等信息。Signature signature = joinPoint.getSignature();
-
getArgs()
: 获取连接点方法的参数数组。此方法返回的是方法参数的数组,可以在通知中访问目标方法传入的参数。Object[] args = joinPoint.getArgs();
-
getTarget()
: 获取连接点对应的目标对象。也就是说,getTarget()
返回的是被代理的目标对象。Object target = joinPoint.getTarget();
-
getThis()
: 获取代理对象。与getTarget()
不同,getThis()
返回的是代理对象,而getTarget()
返回的是目标对象。Object proxy = joinPoint.getThis();
-
getKind()
: 获取连接点的类型。joinPoint.getKind()
返回一个字符串,通常为method-execution
(表示方法执行的连接点)。
2. 使用场景
JoinPoint
通常出现在切面(Aspect)的通知方法中。当 AOP 拦截到目标方法时,可以通过 JoinPoint
获取到关于该方法执行的信息。下面是一个简单的示例,展示了如何在 @Before
通知中使用 JoinPoint
。
示例:使用 JoinPoint
在 @Before
通知中获取目标方法的参数和签名
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.UserService.createUser(..))")
public void logBefore(JoinPoint joinPoint) {
// 获取方法签名
Signature signature = joinPoint.getSignature();
System.out.println("Method signature: " + signature);
// 获取方法参数
Object[] args = joinPoint.getArgs();
System.out.println("Method arguments: " + Arrays.toString(args));
// 获取目标对象
Object target = joinPoint.getTarget();
System.out.println("Target object: " + target);
}
}
解释:
getSignature()
: 获取目标方法的签名,打印出方法的名称和参数类型。getArgs()
: 获取方法的参数,打印出调用createUser
方法时传入的参数。getTarget()
: 获取目标对象,可以用来查看被代理的目标对象。
3. JoinPoint 在不同通知中的使用
@Before
通知中的 JoinPoint
@Before
通知在目标方法执行之前执行。此时可以使用 JoinPoint
获取目标方法的签名、参数等信息。
@Before("execution(* com.example.UserService.createUser(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("Argument: " + arg);
}
}
@After
通知中的 JoinPoint
@After
通知在目标方法执行之后执行,此时可以通过 JoinPoint
获取方法签名和目标对象的信息。
@After("execution(* com.example.UserService.createUser(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature());
Object target = joinPoint.getTarget();
System.out.println("Target object: " + target);
}
@Around
通知中的 JoinPoint
@Around
通知可以控制目标方法的执行(即在执行目标方法之前、之后均可进行拦截)。在 @Around
通知中,JoinPoint
也可以用来访问方法的签名、参数等,并且可以通过 joinPoint.proceed()
控制目标方法的执行。
@Around("execution(* com.example.UserService.createUser(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法签名
Signature signature = joinPoint.getSignature();
System.out.println("Around method: " + signature);
// 获取方法参数
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("Argument: " + arg);
}
// 执行目标方法
Object result = joinPoint.proceed();
// 返回执行结果
return result;
}
4. JoinPoint 与 ProceedingJoinPoint 区别
JoinPoint
是基础接口,它提供了方法签名、参数等基本信息。而 ProceedingJoinPoint
是 JoinPoint
的子接口,除了提供 JoinPoint
的功能外,它还提供了 proceed()
方法,允许在 @Around
通知中控制目标方法的执行。proceed()
方法可以用来显式地调用目标方法或跳过目标方法的执行。
ProceedingJoinPoint proceedingJoinPoint = (ProceedingJoinPoint) joinPoint;
Object result = proceedingJoinPoint.proceed(); // 执行目标方法
JoinPoint 对象封装了 SpringAop 中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的 JoinPoint 对象。
方法名 | 功能 |
---|---|
Signature getSignature() | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名、所属类的 Class 等信息 |
Object[] getArgs() | 获取传入目标方法的参数对象 |
Object getTarget() | 获取被代理的对象 |
Object getThis() | 获取代理对象 |
ProceedingJoinPoint 对象是 JoinPoint 的子接口,该对象只用在 @Around 的切面方法中
方法名 | 功能 |
---|---|
Object proceed() throws Throwable | 执行目标方法 |
Object proceed(Object[] var1) throws Throwable | 传入的新的参数去执行目标方法 |
5. 总结
JoinPoint
提供了访问目标方法的上下文信息的能力,包括方法签名、方法参数、目标对象等。- 在不同的通知中,
JoinPoint
用来获取目标方法的相关信息,帮助开发者了解方法执行的情况。 ProceedingJoinPoint
是JoinPoint
的一个扩展,允许在@Around
通知中控制目标方法的执行流程。
通知分类
@Before 前置通知
- 前置通知: 在方法执行之前执行
- 前置通知使用@Before注解 将切入点表达式值作为注解的值
@After 后置通知
- 后置通知, 在方法执行之后执行
- 后置通知使用@After注解 ,在后置通知中,不能访问目标方法执行的结果
@AfterRunning 返回通知
- 返回通知, 在方法返回结果之后执行
- 返回通知使用 @AfterRunning 注解
@AfterThrowing 异常通知
- 异常通知, 在方法抛出异常之后执行
- 异常通知使用@AfterThrowing注解
@Around 环绕通知
- 环绕通知, 围绕着方法执行
- 环绕通知使用@Around注解
package com.jason.spring.aop.impl;
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
//把这个类声明为一个切面
//1.需要将该类放入到IOC 容器中
@Component
//2.再声明为一个切面
@Aspect
public class LoggingAspect {
//声明该方法是一个前置通知:在目标方法开始之前执行 哪些类,哪些方法
//作用:@before 当调用目标方法,而目标方法与注解声明的方法相匹配的时候,aop框架会自动的为那个方法所在的类生成一个代理对象,在目标方法执行之前,执行注解的方法
//支持通配符
//@Before("execution(public int com.jason.spring.aop.impl.ArithmeticCaculatorImpl.*(int, int))")
@Before("execution(* com.jason.spring.aop.impl.*.*(int, int))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method " + methodName + " begins " + args);
}
/**
* @Description: 在方法执行后执行的代码,无论该方法是否出现异常
* @param joinPoint
*/
@After("execution(* com.jason.spring.aop.impl.*.*(int, int))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method " + methodName + " end " + args);
}
/**
*
* @Description: 在方法正常结束后执行代码,放回通知是可以访问到方法的返回值
*
* @param joinPoint
*/
@AfterReturning( value="execution(* com.jason.spring.aop.impl.*.*(..))", returning="result")
public void afterReturning(JoinPoint joinPoint ,Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " end with " + result);
}
/**
*
* @Description: 在目标方法出现异常时会执行代码,可以访问到异常对象,且,可以指定出现特定异常时执行通知代码
*
* @param joinPoint
* @param ex
*/
@AfterThrowing(value="execution(* com.jason.spring.aop.impl.*.*(..))",throwing="ex")
public void afterThrowting(JoinPoint joinPoint, Exception ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs exceptions " + ex);
}
/**
*
* @Description: 环绕通知需要携带 ProceedingJoinPoint 类型的参数
* 环绕通知 类似于 动态代理的全过程
* ProceedingJoinPoint:可以决定是否执行目标方法
* 环绕通知必须有返回值,返回值即为目标方法的返回值
*
* @param proceedingJoinPoint
*/
@Around("execution(* com.jason.spring.aop.impl.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
String methodName = proceedingJoinPoint.getSignature().getName();
//执行目标方法
try {
//前置通知
System.out.println("The method " + methodName + "begin with" + Arrays.asList(proceedingJoinPoint.getArgs()));
result = proceedingJoinPoint.proceed();
//后置通知
System.out.println("The method " + methodName + "end with" + result);
} catch (Throwable e) {
//异常通知
System.out.println("The method occurs exception : " + e);
throw new RuntimeException();
}
//后置通知
System.out.println("The method " + methodName + "end with" + result);
return result;
}
}
Spring Boot 整合 AOP
AOP (Aspect Oiented Programn, 面向切面编程)把业务功能分为核心、非核心两部分。
- 核心业务功能: 用户登录、增加数据、删除数据。
- 非核心业务功能: 性能统计、日志、事务管理。
AOP 的体系可以梳理为下图:
首先导入 AOP 的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
使用 execution(路径表达式)
@Slf4j
@Aspect
@Component
public class LogAspect {
ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
* execution 函数用于匹配方法执行的连接点,语法为:
* execution(方法修饰符(可选) 返回类型 方法名 参数 异常模式(可选))
* 参数部分允许使用通配符:
* * 匹配任意字符,但只能匹配一个元素
* .. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和 * 联合使用
* + 必须跟在类名后面,如 Horseman+,表示类本身和继承或扩展指定类的所有类
*/
@Pointcut("execution(public * com.example.aop_demo.controller.*.*(..))")
private void webLog() {}
/**
* 前置通知:在目标方法被调用之前调用通知功能
*/
@Before("webLog()")public void doBefore(JoinPoint jp) {
System.out.println("=====================doBefore======================");
// 接收到请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录请求内容
log.info("URL : {}", request.getRequestURL());
log.info("HTTP 方法 : {}", request.getMethod());
log.info("IP 地址 : {}", request.getRemoteAddr());
log.info("类的方法 : {}.{}", jp.getSignature().getDeclaringTypeName(), jp.getSignature().getName());
log.info("方法参数 : {}", Arrays.toString(jp.getArgs()));
System.out.println("=====================doBefore======================");
}
/**
* 返回通知:在目标方法成功执行之后调用通知
*/
@AfterReturning(pointcut = "webLog()", returning = "result")
public void doAfterReturning(Object result) {
System.out.println("=====================doAfterReturning======================");
// 处理完请求,返回内容
System.out.println("方法的返回值 :" + result);
System.out.println("=====================doAfterReturning======================");
}
/**
* 最终通知:在目标方法完成之后调用通知,不管是抛出异常或者正常退出都会执行
*/
@After("webLog()")
public void doAfter(JoinPoint jp) {
System.out.println("=====================doAfter======================");
System.out.println("方法最后执行.....");
System.out.println("=====================doAfter======================");
}
/**
* 环绕通知:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行,相当于 MethodInterceptor
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint pjp) {
System.out.println("=====================doAround======================");
System.out.println("方法环绕 start.....");
startTime.set(System.currentTimeMillis());
try {
Object o = pjp.proceed();
System.out.println("方法环绕 proceed,结果是 :" + o);
System.out.println("方法执行耗时:" + (System.currentTimeMillis() - startTime.get())+ "ms");
System.out.println("=====================doAround======================");
return o;
} catch (Throwable e) {e.printStackTrace();
return null;
}
}
/**
* 异常通知:在目标方法抛出异常后调用通知
*/
@AfterThrowing(pointcut = "webLog()", throwing = "ex")
public void doThrows(JoinPoint jp, Exception ex) {
System.out.println("=====================doThrows======================");
System.out.println("方法异常时执行 \n 发生的异常:" + ex.getClass().getName()+ "\n 异常信息:" + ex.getMessage());
System.out.println("=====================doThrows======================");
}
}
controller 代码如下,返回当前日期时间
package com.example.aop_demo.controller
@RestController
public class BaseController {
@GetMapping("/api1")
public Map<String, Object> api1() {
Map<String, Object> map = new HashMap<>(16);
map.put("nowTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return map;
}
}
调用接口,控制台输出结果如下:
=====================doAround======================
方法环绕 start.....
=====================doBefore======================
2021-05-09 23:20:58.013 INFO 14772 --- [nio-8080-exec-1] com.example.aop_demo.aop.LogAspect : URL : http://192.168.85.1:8080/api1
2021-05-09 23:20:58.014 INFO 14772 --- [nio-8080-exec-1] com.example.aop_demo.aop.LogAspect : HTTP 方法 : GET
2021-05-09 23:20:58.014 INFO 14772 --- [nio-8080-exec-1] com.example.aop_demo.aop.LogAspect : IP 地址 : 192.168.85.1
2021-05-09 23:20:58.015 INFO 14772 --- [nio-8080-exec-1] com.example.aop_demo.aop.LogAspect : 类的方法 : com.example.aop_demo.controller.BaseController.api1
2021-05-09 23:20:58.016 INFO 14772 --- [nio-8080-exec-1] com.example.aop_demo.aop.LogAspect : 方法参数 : []
=====================doBefore======================
=====================doAfterReturning======================
方法的返回值 : {nowTime=2021-05-09 23:20:58}
=====================doAfterReturning======================
=====================doAfter======================
方法最后执行.....
=====================doAfter======================
方法环绕 proceed,结果是 :{nowTime=2021-05-09 23:20:58}
方法执行耗时:18 ms
=====================doAround======================
使用 annotation(注解)
首先定义一个注解(不想自定义注解使用系统注解也可以,比如 @GetMapping)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value() default "";
}
定义切面
@Slf4j
@Aspect
@Component
public class AnnotationAspect {
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("@annotation(com.example.aop_demo.annotation.MyAnnotation))")
private void myAnnotationCheck() { }
@Before("myAnnotationCheck()")
public void doBefore(JoinPoint jp) {
System.out.println("=====================doBefore======================");
startTime.set(System.currentTimeMillis());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("URL : {}", request.getRequestURL());
log.info("HTTP 方法 : {}", request.getMethod());
log.info("IP 地址 : {}", request.getRemoteAddr());
log.info("类的方法 : {}.{}", jp.getSignature().getDeclaringTypeName(), jp.getSignature().getName());
log.info("方法参数 : {}", Arrays.toString(jp.getArgs()));
System.out.println("=====================doBefore======================");
}
/**
* 后置增强
*/
@AfterReturning(pointcut = "myAnnotationCheck()", returning = "result")
public void doAfterReturning(Object result) {
System.out.println("=====================doAfterReturning======================");
log.info("方法的返回值 : {}", result);
log.info("耗时 : {}ms", (
System.currentTimeMillis() - startTime.get()));
System.out.println("=====================doAfterReturning======================");
}
}
controller 代码如下,先阻塞两秒,观察耗时
@RestController
public class BaseController {
@MyAnnotation
@GetMapping("/api2")
public String api2() throws InterruptedException {TimeUnit.SECONDS.sleep(2);
return "api2 调用成功";
}
}
执行结果如下:
=====================doBefore======================
2021-05-09 23:43:47.144 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : URL : http://192.168.85.1:8080/api2
2021-05-09 23:43:47.144 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : HTTP 方法 : GET
2021-05-09 23:43:47.144 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : IP 地址 : 192.168.85.1
2021-05-09 23:43:47.145 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : 类的方法 : com.example.aop_demo.controller.BaseController.api2
2021-05-09 23:43:47.145 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : 方法参数 : []
=====================doBefore======================
=====================doAfterReturning======================
2021-05-09 23:43:49.152 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : 方法的返回值 : api2 调用成功
2021-05-09 23:43:49.152 INFO 14772 --- [nio-8080-exec-3] com.example.aop_demo.aop.AnnotationAspect : 耗时 : 2008ms
=====================doAfterReturning======================
获取注解属性的方法:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
myAnnotationCheck annotation=signature.getMethod().getDeclaredAnnotation(myAnnotationCheck.class);
// 获取 value 属性
String value = annotation.value();
SpringBoot 使用注解的方式实现 AOP
四、相关注解
注解 | 说明 |
---|---|
@Aspect | 将一个 java 类定义为切面类 |
@Pointcut | 定义一个切入点,定义需要拦截的东西,即上下文中所关注的某件事情的入口,切入点定义了事件触发时机。可以是一个规则表达式(execution() 表达式,annotation() 表达式),比如下例中某个 package 下的所有函数,也可以是一个注解等 |
@Before | 在切入点开始处切入内容 |
@After | 在切入点结尾处切入内容 |
@AfterReturning | 在切入点 return 内容之后处理逻辑 |
@Around | 在切入点前后切入内容,并自己控制何时执行切入点自身的内容 |
@AfterThrowing | 用来处理当切入点内容部分抛出异常之后的处理逻辑 |
@Order (100) | AOP 切面执行顺序,@Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行 |
其中 @Before、@After、@AfterReturning、@Around、@AfterThrowing 都属于通(Advice)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第一个实例
利用AOP+Swagger注解实现日志记录功能,所有接口请求完成时,打印日志记录。
1、创建一个 AOP 切面类,只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现 advice:
@Aspect
@Slf4j
@Component
@Order(1)
public class WebLogAspect {
private ThreadLocal<Long> startTime = new Thre
// 定义一个切点:所有接口中被@ApiOperation 注解修饰的方法会织入advice
@Pointcut("@annotation(operation)")
public void logPointcut(
ApiOperation operation
}
// Before 表示 before 将在目标方法执行前执行
@Before(value = "logPointcut(operation)", argN
public void before(JoinPoint joinPoint, ApiOperation operation) {
startTime.set(System.currentTimeMillis());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String[] moduleName = getModuleName(joinPoint, operation);
log.info("[{}]-[{}]-start {}", moduleName[0], moduleName[1], request.getRequestURL().toString());
log.info("参数 : {}", Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "logPointcut(operation) ", argNames = "joinPoint,ret,operation")
public void after(JoinPoint joinPoint, Object ret, ApiOperation operation) {
// 处理完请求,返回内容
String[] moduleName = getModuleName(joinPoint, operation);
String retMesage = JSON.toJSONString(ret);
log.info("[{}]-[{}]-end [{}ms] 响应:{}", moduleName[0], moduleName[1], (System.currentTimeMillis() - startTime.get()), retMesage);
startTime.remove();
}
private String[] getModuleName(JoinPoint joinPoint, ApiOperation operation) {
Class<?> clazz = joinPoint.getTarget().getClass();
Api api = clazz.getAnnotation(Api.class);
String pInfo = Optional.ofNullable(api).map(Api::value).orElse(clazz.getName());
return new String[]{pInfo, operation.value()};
}
}
2、创建一个接口类,在方法上加上 swagger 的 @ApiOperation 的注解
@PostMapping("/getSupplierOrderDetailInfo")
@ApiOperation(value = "售卖渠道订单查询", notes = "售卖渠道订单查询")
public ApiResultResponse<SupplierOrderDetailResponse> getSupplierOrderDetailInfo(@RequestBody @Valid SupplierOrderDetailRequest request) {
SupplierOrderDetailResponse response = appVipOrderService.getSupplierOrderDetailInfo(request);
return ApiResponseUtils.buildSuccessMsg(response);
}
3、项目启动后,请求该接口时,日志打印如下:
INFO n.b.z.c.l.WebLogAspect - [售卖渠道信息相关]-[售卖渠道资金池余额查询接口]-start http://localhost:8080/openapi/supplier/getSupplier
INFO n.b.z.c.l.WebLogAspect - 参数 : [SupplierAssetsPoolRequest(supplierId=8)]
INFO n.b.z.c.l.WebLogAspect - [售卖渠道信息相关]-[售卖渠道资金池余额查询接口]-end [136ms] 响应:{"apiResult":{"balance":2910},"retCode":"000000","retMsg":"成功","retState":"SUCCESS"}
第二个实例
下面将问题复杂化一些,该例的场景是:
1、自定义一个注解 SupplierCheck
2、创建一个切面类,切点设置为校验所有标注 SupplierCheck 的方法,截取到接口的参数,进行简单的 ip 白名单校验
3、将 SupplierCheck 标注在接口类上面的方法上
具体的实现步骤:
1、让我们来自定义一个注解,注解名为 SupplierCheck,如下所示
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SupplierCheck {
/**
* 校验白名单
* 标识该方法是否需要校验白名单(默认校验),白话文就是说是否需要执行该校验白名单方法
*/
boolean checkIpWhite() default true;
}
1、@Target 注解
用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。可以指定多个位置(@Target({ElementType.METHOD, ElementType.FIELD})),语法如下:
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
类型 | 描述 |
---|---|
ElementType.TYPE | 可以用于类、接口和枚举类型 |
ElementType.FIELD | 可以用于字段(包括枚举常量) |
ElementType.METHOD | 可以用于方法(controller上面的接口里它也是方法) |
ElementType.PARAMETER | 可以用于方法的参数 |
ElementType.CONSTRUCTOR | 可以用于构造函数 |
ElementType.LOCAL_VARIABLE | 可以用于局部变量 |
ElementType.ANNOTATION_TYPE | 可以用于注解类型 |
ElementType.PACKAGE | 可以用于包 |
ElementType.TYPE_PARAMETER | 可以用于类型参数声明(Java 8新增) |
ElementType.TYPE_USE | 可以用于使用类型的任何语句中(Java 8新增) |
2、@Retention 注解
该注解指定了被修饰的注解的生命周期,语法如下:
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
类型 | 描述 |
---|---|
RetentionPolicy.SOURCE | 在源文件中有效(即源文件保留) |
RetentionPolicy.CLASS | 在class文件中有效(即class保留),不会被加载到JVM中 |
RetentionPolicy.RUNTIME | 在运行时有效(即运行时保留),会被加载到JVM中 |
3、@Documented
@Documented 注解表示被它修饰的注解将被 javadoc 工具提取成文档。
4、Inherited
@Inherited 注解表示被它修饰的注解具有继承性,即如果一个类声明了被 @Inherited 修饰的注解,那么它的子类也将具有这个注解。
2、创建一个 AOP 切面类
只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里面实现第一步白名单校验逻辑:
@Aspect
@Order(3)
@Slf4j
@Component
public class SupplierAspect {
@Around("@annotation(supplierCheck)")
public Object around(ProceedingJoinPoint pjp, SupplierCheck supplierCheck) throws Throwable {
try {
Object obj = pjp.getArgs()[0];
// 业务逻辑
doCheck(obj, supplierCheck);
return pjp.proceed(pjp.getArgs());
} catch (BizException e) {
log.warn(e.getLogMsg());
return buildErrorMsg(e.getApiCode(), e.getApiMsg());
} catch (Exception e) {
log.error("SupplierAspect 调用失败 ", e);
return buildErrorMsg(ResponseCode.FAILED.code, ResponseCode.FAILED.desc);
}
}
private void doCheck(Object obj, SupplierCheck,supplierCheck) throws BizException {
AppAuthRequest authRequest = JSON.parseObject(JSON.toJSONString(obj), AppAuthRequest.class);
log.info("authRequest = {}", authRequest);
//白名单校验
if (supplierCheck.checkIpWhite()) {
// 白名单业务逻辑
}
}
3、创建接口类,并在目标方法上标注自定义注解 SupplierCheck :
@PostMapping("/getSupplierOrderDetailInfo")
@ApiOperation(value = "售卖渠道订单查询", notes = "售卖渠道订单查询")
@SupplierCheck
public ApiResultResponse<SupplierOrderDetailResponse> getSupplierOrderDetailInfo(@RequestBody @Valid SupplierOrderDetailRequest request) {
SupplierOrderDetailResponse response = appVipOrderService.getSupplierOrderDetailInfo(request);
return ApiResponseUtils.buildSuccessMsg(response);
}
3、有人会问,上面一个接口设置了多个切面类进行了校验怎么办?这些切面的执行顺序如何管理?
很简单,一个自定义的 AOP 注解可以对应多个切面类,这些切面类执行顺序由 @Order 注解管理,该注解后的数字越小,所在切面类越先执行。
比如上面接口中 @ApiOperation 增强的注解中(第一个实例介绍的)的 @Order(1),那么这个注解先执行,@SupplierCheck 白名单校验的注解的 @Order(3) 后面执行。
第三个实例
编写增强类
package com.example.demo.Aspect;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.SourceLocation;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@Component
@Aspect
@Slf4j
public class LogAspect {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Pointcut("execution(* com.example.demo.Aspect.TestController.doNormal(..))")
public void pointCut(){}
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint){
log.info("@Before通知执行");
//获取目标方法参数信息
Object[] args = joinPoint.getArgs();
Arrays.stream(args).forEach(arg->{ // 大大
try {
log.info(OBJECT_MAPPER.writeValueAsString(arg));
} catch (JsonProcessingException e) {
log.info(arg.toString());
}
});
//aop代理对象
Object aThis = joinPoint.getThis();
log.info(aThis.toString()); //com.xhx.springboot.controller.HelloController@69fbbcdd
//被代理对象
Object target = joinPoint.getTarget();
log.info(target.toString()); //com.xhx.springboot.controller.HelloController@69fbbcdd
//获取连接点的方法签名对象
Signature signature = joinPoint.getSignature();
log.info(signature.toLongString()); //public java.lang.String com.xhx.springboot.controller.HelloController.getName(java.lang.String)
log.info(signature.toShortString()); //HelloController.getName(..)
log.info(signature.toString()); //String com.xhx.springboot.controller.HelloController.getName(String)
//获取方法名
log.info(signature.getName()); //getName
//获取声明类型名
log.info(signature.getDeclaringTypeName()); //com.xhx.springboot.controller.HelloController
//获取声明类型 方法所在类的class对象
log.info(signature.getDeclaringType().toString()); //class com.xhx.springboot.controller.HelloController
//和getDeclaringTypeName()一样
log.info(signature.getDeclaringType().getName());//com.xhx.springboot.controller.HelloController
//连接点类型
String kind = joinPoint.getKind();
log.info(kind);//method-execution
//返回连接点方法所在类文件中的位置 打印报异常
SourceLocation sourceLocation = joinPoint.getSourceLocation();
log.info(sourceLocation.toString());
//log.info(sourceLocation.getFileName());
//log.info(sourceLocation.getLine()+"");
//log.info(sourceLocation.getWithinType().toString()); //class com.xhx.springboot.controller.HelloController
///返回连接点静态部分
JoinPoint.StaticPart staticPart = joinPoint.getStaticPart();
log.info(staticPart.toLongString()); //execution(public java.lang.String com.xhx.springboot.controller.HelloController.getName(java.lang.String))
//attributes可以获取request信息 session信息等
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info(request.getRequestURL().toString()); //http://127.0.0.1:8080/hello/getName
log.info(request.getRemoteAddr()); //127.0.0.1
log.info(request.getMethod()); //GET
log.info("before通知执行结束");
}
/**
* 后置返回
* 如果第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行,
* 参数为Object类型将匹配任何目标返回值
*/
@AfterReturning(value = "pointCut()",returning = "result")
public void doAfterReturningAdvice1(JoinPoint joinPoint,Object result){
log.info("第一个后置返回通知的返回值:"+result);
}
@AfterReturning(value = "pointCut()",returning = "result",argNames = "result")
public void doAfterReturningAdvice2(String result){
log.info("第二个后置返回通知的返回值:"+result);
}
//第一个后置返回通知的返回值:姓名是大大
//第二个后置返回通知的返回值:姓名是大大
//第一个后置返回通知的返回值:{name=小小, id=1}
/**
* 后置异常通知
* 定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
* throwing:限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,
* 对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "pointCut()",throwing = "exception")
public void doAfterThrowingAdvice(JoinPoint joinPoint,Throwable exception){
log.info(joinPoint.getSignature().getName());
if(exception instanceof NullPointerException){
log.info("发生了空指针异常!!!!!");
}
}
@After(value = "pointCut()")
public void doAfterAdvice(JoinPoint joinPoint){
log.info("后置通知执行了!");
}
/**
* 环绕通知:
* 注意:Spring AOP的环绕通知会影响到AfterThrowing通知的运行,不要同时使用
*
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
*/
@Around(value = "pointCut()")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
log.info("@Around环绕通知:"+proceedingJoinPoint.getSignature().toString());
Object obj = null;
try {
obj = proceedingJoinPoint.proceed(); //可以加参数
log.info(obj.toString());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
log.info("@Around环绕通知执行结束");
return obj;
}
}
Controller
package com.example.demo.Aspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class TestController {
@RequestMapping("/doNormal")
public String doNormal(String name, String age) {
log.info("【执行方法】:doNormal");
return "doNormal";
}
@RequestMapping("/doWithException")
public String doWithException(String name, String age) {
log.info("【执行方法】:doWithException");
int a = 1 / 0;
return "doWithException";
}
}
利用 AOP 实现链式调用
在Spring AOP中,多个Advice
(特别是环绕通知)可以通过切面(Aspect)的组合形成拦截器链,围绕连接点(如方法调用)按顺序处理。以下是一个具体示例:
示例场景
假设有一个服务类UserService
,我们需要在执行其createUser
方法时,依次触发以下两个拦截器:
- 日志记录:记录方法调用的开始和结束。
- 事务管理:在方法执行前后开启和提交事务。
代码实现
1. 定义目标类
public class UserService {
public void createUser(String username) {
System.out.println("执行目标方法: 创建用户 " + username);
}
}
2. 定义日志切面(拦截器1)
@Aspect
@Component
@Order(1) // 定义执行顺序为第1位
public class LoggingAspect {
@Around("execution(* UserService.createUser(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[日志拦截器] 方法调用前 - 记录日志");
Object result = joinPoint.proceed(); // 调用链中的下一个拦截器或目标方法
System.out.println("[日志拦截器] 方法调用后 - 记录日志");
return result;
}
}
3. 定义事务切面(拦截器2)
@Aspect
@Component
@Order(2) // 定义执行顺序为第2位
public class TransactionAspect {
@Around("execution(* UserService.createUser(..))")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[事务拦截器] 开启事务");
Object result;
try {
result = joinPoint.proceed(); // 调用目标方法
System.out.println("[事务拦截器] 提交事务");
} catch (Exception e) {
System.out.println("[事务拦截器] 回滚事务");
throw e;
}
return result;
}
}
执行流程
当调用UserService.createUser("Alice")
时,拦截器链的执行顺序如下:
- 日志拦截器的前置处理(
@Order(1)
)。 - 事务拦截器的前置处理(
@Order(2)
)。 - 执行目标方法
createUser
。 - 事务拦截器的后置处理(提交事务)。
- 日志拦截器的后置处理。
输出结果
[日志拦截器] 方法调用前 - 记录日志
[事务拦截器] 开启事务
执行目标方法: 创建用户 Alice
[事务拦截器] 提交事务
[日志拦截器] 方法调用后 - 记录日志
关键点说明
- 拦截器链的形成:通过
@Order
注解指定切面的优先级,Spring会按顺序将多个@Around
Advice组织成链。 ProceedingJoinPoint.proceed()
:每个环绕通知通过调用proceed()
将控制权传递给链中的下一个拦截器或目标方法。- 责任链模式:这种设计模式允许每个拦截器专注于单一职责(如日志、事务),并通过链式调用实现解耦和复用。
通过这种方式,Spring AOP能够将多个Advice
组合成灵活的拦截器链,围绕连接点提供模块化的横切关注点处理。
再理解(感觉下面不用看,是 xml 方法,已经老了)
下面将会围绕 Spring AOP 中的常见概念进行总结,并且给出一个完整的示例。示例会包含「注解方式」与「XML 配置方式」两个版本
一、AOP 核心概念回顾
在 Spring AOP 中,我们通常会接触到如下几个核心术语或概念:
-
横切关注点(Cross-Cutting Concern)
- 定义:跨越应用程序多个模块的方法或功能,与具体业务逻辑无关,但在很多地方都需要被关注或使用。
- 示例:日志记录、权限控制、缓存、事务管理等。
-
切面(Aspect)
- 定义:把某个横切关注点进行模块化封装后的特殊对象,在 Spring 中通常是一个类。
- 示例:日志切面(LoggingAspect)、事务切面(TransactionAspect)等。
-
通知(Advice)
- 定义:切面中具体要完成的“工作”或“操作”,在切面类中一般对应到一个个方法。
- 示例:在日志切面类中,负责打印日志的具体方法。
-
目标(Target)
- 定义:被通知的对象,即实际包含业务逻辑的对象。
-
代理(Proxy)
- 定义:应用了某些横切关注点(切面)之后,Spring 为目标对象创建出来的“代理对象”。
-
切入点(PointCut)
- 定义:用于定义到底要在什么地方(什么类、什么方法、满足什么条件)执行通知(Advice)。
-
连接点(JoinPoint)
- 定义:满足切入点匹配的具体执行点,例如某一个方法调用的具体时刻。
在 Spring AOP 中,我们最常见的就是“使用切面(Aspect)来对某些方法进行前置/后置/异常/最终通知(Advice)”。在下面的示例中,我们会定义一个日志切面,通过它来记录目标业务方法的执行信息。
二、示例背景
我们准备演示一个简易的用户服务接口 UserService
,以及它的实现类 UserServiceImpl
。然后我们会编写一个日志切面 LoggingAspect
,对 UserService
中的方法调用进行日志打印。
为了更好地展示,我们会提供:
- 基于注解的配置
- 基于 XML 的配置
在示例中,我们假设项目结构如下:
└── src
└── main
└── java
└── com
└── example
├── service
│ ├── UserService.java
│ └── impl
│ └── UserServiceImpl.java
└── aspect
└── LoggingAspect.java
└── resources
├── applicationContext-annotation.xml
└── applicationContext-xml.xml
说明:为方便展示,示例省略了
Maven
/Gradle
的配置以及日志框架的完整依赖;本示例只展示与 AOP 相关的核心部分。
三、基于注解的 AOP 示例
1. 创建业务接口与实现
UserService.java
package com.example.service;
public interface UserService {
void addUser(String username);
String getUser(String username);
}
UserServiceImpl.java
package com.example.service.impl;
public class UserServiceImpl implements UserService {
@Override
public void addUser(String username) {
System.out.println("[UserServiceImpl] addUser: " + username);
// 省略真正的添加用户操作
}
@Override
public String getUser(String username) {
System.out.println("[UserServiceImpl] getUser: " + username);
// 省略真正的查询用户操作
return "MockUser(" + username + ")";
}
}
2. 创建日志切面
LoggingAspect.java
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.After;
@Aspect
public class LoggingAspect {
// 定义一个切入点表达式(也可以使用 @Pointcut 注解单独定义)
// 这里的含义是:拦截 com.example.service 包及其子包中所有类的所有方法执行
// execution(* com.example.service..*.*(..)) 表达式仅作示例,可根据需求进行定制
private static final String POINTCUT_EXPRESSION =
"execution(* com.example.service..*.*(..))";
/**
* 前置通知
*/
@Before(POINTCUT_EXPRESSION)
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspect - Before] 方法名称: " + methodName);
}
/**
* 返回通知
*/
@AfterReturning(pointcut = POINTCUT_EXPRESSION, returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspect - AfterReturning] 方法名称: " + methodName + ", 返回值: " + result);
}
/**
* 异常通知
*/
@AfterThrowing(pointcut = POINTCUT_EXPRESSION, throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspect - AfterThrowing] 方法名称: " + methodName + ", 异常信息: " + ex);
}
/**
* 最终通知(无论是否异常都会执行)
*/
@After(POINTCUT_EXPRESSION)
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspect - After] 方法名称: " + methodName);
}
}
3. Spring 配置(基于注解)
applicationContext-annotation.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 1. 启用注解扫描,自动注册 Bean -->
<context:component-scan base-package="com.example" />
<!-- 2. 启用基于注解的 AOP -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<!-- 3. 手动声明业务 Bean(如果没有标注 @Component,可以在此进行声明) -->
<bean id="userService" class="com.example.service.impl.UserServiceImpl" />
<!-- 4. 注册日志切面,如果 LoggingAspect 没有 @Component,也可在此显式声明 -->
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect" />
</beans>
说明:
- 这里使用了
<context:component-scan>
来扫描com.example
包下的类,若UserServiceImpl
和LoggingAspect
上使用了诸如@Component
、@Service
、@Aspect
等注解,则可以省略手动声明<bean>
。<aop:aspectj-autoproxy>
用于启用 AOP 功能,并让 Spring 自动识别带有@Aspect
的切面类。
4. 测试 AOP
AnnotationAopTest.java
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.example.service.UserService;
public class AnnotationAopTest {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext-annotation.xml");
UserService userService = context.getBean("userService", UserService.class);
System.out.println("===== 调用 addUser 方法 =====");
userService.addUser("Alice");
System.out.println("===== 调用 getUser 方法 =====");
String result = userService.getUser("Alice");
System.out.println("getUser 返回结果:" + result);
// 如果想观察异常通知,可以在此刻加一个会抛异常的测试
// ...
}
}
运行结果(示例):
===== 调用 addUser 方法 =====
[LoggingAspect - Before] 方法名称: addUser
[UserServiceImpl] addUser: Alice
[LoggingAspect - AfterReturning] 方法名称: addUser, 返回值: null
[LoggingAspect - After] 方法名称: addUser
===== 调用 getUser 方法 =====
[LoggingAspect - Before] 方法名称: getUser
[UserServiceImpl] getUser: Alice
[LoggingAspect - AfterReturning] 方法名称: getUser, 返回值: MockUser(Alice)
[LoggingAspect - After] 方法名称: getUser
getUser 返回结果:MockUser(Alice)
从日志中可以看到,addUser
和 getUser
方法均被我们的日志切面(LoggingAspect)所增强,执行前后都打印出相应信息。
四、基于 XML 的 AOP 示例
在不使用注解的情况下,Spring 也可以通过 XML 配置来进行 AOP 设置。大体流程相似,不同点在于需要在 XML 中显式地声明切面和通知。我们这里依旧使用同样的业务类 UserService
与 UserServiceImpl
,只是改用 XML 来定义切面和通知。
1. 业务代码与上面相同
UserService
和UserServiceImpl
保持不变。
2. 定义切面类(不使用注解,使用普通类 & 在 XML 中配置)
LoggingAspectXml.java
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
public class LoggingAspectXml {
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspectXml - Before] 方法名称: " + methodName);
}
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspectXml - AfterReturning] 方法名称: " + methodName + ", 返回值: " + result);
}
public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspectXml - AfterThrowing] 方法名称: " + methodName + ", 异常信息: " + ex);
}
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspectXml - After] 方法名称: " + methodName);
}
}
3. Spring 配置(纯 XML 方式)
applicationContext-xml.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 1. 声明业务 Bean -->
<bean id="userService" class="com.example.service.impl.UserServiceImpl" />
<!-- 2. 声明日志切面 Bean -->
<bean id="loggingAspectXml" class="com.example.aspect.LoggingAspectXml" />
<!-- 3. 配置 AOP -->
<aop:config>
<!-- 定义一个切入点,匹配 com.example.service..*.*(..) -->
<aop:pointcut id="servicePointCut"
expression="execution(* com.example.service..*.*(..))" />
<!-- 定义切面 -->
<aop:aspect id="xmlAspect" ref="loggingAspectXml">
<!-- 前置通知 -->
<aop:before
pointcut-ref="servicePointCut"
method="beforeMethod" />
<!-- 返回通知 -->
<aop:after-returning
pointcut-ref="servicePointCut"
method="afterReturningMethod"
returning="result" />
<!-- 异常通知 -->
<aop:after-throwing
pointcut-ref="servicePointCut"
method="afterThrowingMethod"
throwing="ex" />
<!-- 最终通知 -->
<aop:after
pointcut-ref="servicePointCut"
method="afterMethod" />
</aop:aspect>
</aop:config>
</beans>
说明:
<aop:config>
标签用于启用 XML 方式的 AOP 配置。<aop:pointcut>
用于定义切入点表达式;id
用来引用它。<aop:aspect>
用于声明切面,ref
指向我们定义的LoggingAspectXml
Bean。- 在切面内部,通过
<aop:before> / <aop:after-returning> / <aop:after-throwing> / <aop:after>
来分别配置对应的通知。method
属性对应到切面类中的方法。
4. 测试 AOP
XmlAopTest.java
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.example.service.UserService;
public class XmlAopTest {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext-xml.xml");
UserService userService = context.getBean("userService", UserService.class);
System.out.println("===== 调用 addUser 方法 =====");
userService.addUser("Bob");
System.out.println("===== 调用 getUser 方法 =====");
String result = userService.getUser("Bob");
System.out.println("getUser 返回结果:" + result);
// 如果想观察异常通知,可以在此加一个会抛异常的测试
// ...
}
}
运行结果(示例):
===== 调用 addUser 方法 =====
[LoggingAspectXml - Before] 方法名称: addUser
[UserServiceImpl] addUser: Bob
[LoggingAspectXml - AfterReturning] 方法名称: addUser, 返回值: null
[LoggingAspectXml - After] 方法名称: addUser
===== 调用 getUser 方法 =====
[LoggingAspectXml - Before] 方法名称: getUser
[UserServiceImpl] getUser: Bob
[LoggingAspectXml - AfterReturning] 方法名称: getUser, 返回值: MockUser(Bob)
[LoggingAspectXml - After] 方法名称: getUser
getUser 返回结果:MockUser(Bob)
可以看到,这与注解方式的效果是一致的,只是配置方式不同。
五、总结
- 横切关注点(Cross-Cutting Concern):抽象出与业务无关但需要全局处理或多处使用的功能,如日志、权限、事务等。
- 切面(Aspect):将横切关注点独立封装形成的模块化单位,对应到一个类。
- 通知(Advice):切面需要完成的具体动作方法,如前置通知、后置通知等。
- 目标(Target):被增强的业务对象。
- 代理(Proxy):Spring AOP 运行时生成的“动态代理对象”,对原业务对象进行增强。
- 切入点(PointCut):定义要拦截哪些连接点。
- 连接点(JoinPoint):被拦截到的某一个具体执行点(例如某个方法调用)。
1. 使用注解方式
- 更加简洁,主流使用方式更推荐 Annotation +
@Aspect
。 - 通过
@Aspect
、@Before
、@AfterReturning
等注解定义切面和通知。 - 使用
<aop:aspectj-autoproxy>
启用自动代理。
2. 使用 XML 方式
- 更适合对注解不太熟悉或者需要对配置进行集中管理的场景。
- 需要手动在
<aop:config>
中配置<aop:aspect>
、<aop:pointcut>
、<aop:before>
等。 - 功能与注解方式等价,二者可以根据团队习惯和项目情况决定。
补充: 连接点(JoinPoint)具体含义
连接点(JoinPoint) 是程序执行中的某个特定点。可以理解为切面能够插入代码的位置。连接点通常是方法的调用或执行、异常的抛出、字段的访问等。在 Spring AOP 中,连接点主要指的是方法的执行点,即一个方法被调用的那一刻。
Spring AOP 的连接点可以是:
- 方法的开始(前置通知
@Before
) - 方法的结束(后置通知
@After
或@AfterReturning
) - 方法抛出异常(异常通知
@AfterThrowing
)
示例中的 JoinPoint
在前面示例中,我们定义了切入点表达式:
execution(* com.example.service..*.*(..))
它表示我们拦截 com.example.service
包及其子包中的所有类的所有方法。也就是说,所有符合这个条件的方法执行点(调用点)都可以成为连接点。
具体连接点分析
-
在调用
userService.addUser("Alice")
时:- 连接点是
addUser
方法的执行点。 - 切面捕获了
addUser
方法在被执行之前、执行之后、返回结果后,以及抛出异常时的连接点。
- 连接点是
-
在调用
userService.getUser("Alice")
时:- 连接点是
getUser
方法的执行点。 - 切面捕获了
getUser
方法的同样四种情况(执行前、后、返回结果后、异常时)。
- 连接点是
连接点参数 JoinPoint 的作用
在通知方法中,我们可以通过 JoinPoint
对象获取连接点的详细信息,比如:
- 目标方法名称:
- 通过
joinPoint.getSignature().getName()
获取方法名。
- 通过
- 目标类名称:
- 通过
joinPoint.getTarget().getClass().getName()
获取目标类全限定名。
- 通过
- 方法参数:
- 通过
joinPoint.getArgs()
获取方法的参数数组。
- 通过
在示例中的具体使用
例如,在 LoggingAspect
中:
@Before(POINTCUT_EXPRESSION)
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName(); // 获取方法名称
System.out.println("[LoggingAspect - Before] 方法名称: " + methodName);
}
运行 userService.addUser("Alice")
时:
joinPoint.getSignature().getName()
返回"addUser"
,表示当前连接点是addUser
方法。- 日志输出:
[LoggingAspect - Before] 方法名称: addUser
类似地,在 @AfterReturning
中:
@AfterReturning(pointcut = POINTCUT_EXPRESSION, returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[LoggingAspect - AfterReturning] 方法名称: " + methodName + ", 返回值: " + result);
}
运行 userService.getUser("Alice")
时:
joinPoint.getSignature().getName()
返回"getUser"
。result
是"MockUser(Alice)"
。- 日志输出:
[LoggingAspect - AfterReturning] 方法名称: getUser, 返回值: MockUser(Alice)
总结
- 连接点是程序运行中,切面实际增强的位置,例如方法调用点。
- 示例中的连接点:
addUser
方法和getUser
方法被执行的具体时刻就是连接点。
- JoinPoint 的作用:
- 提供关于连接点的上下文信息,比如方法名、参数、目标对象等,便于通知方法进行处理或记录日志。
在实际开发中,JoinPoint
常用来动态获取当前执行上下文,从而实现更灵活的切面逻辑。
XML 与 注解实现 AOP 示例
示例:使用 Spring 实现 AOP(XML 方式)
SpringAOP 中,通过 Advice 定义横切逻辑,Spring 中支持 5 种类型的 Advice:
即 Aop 在不改变原有代码的情况下 , 去增加新的功能 .
【重点】使用 AOP 织入,需要导入一个依赖包!
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
第一种方式:通过 Spring API 实现【主要 SpringAOP 接口实现】
首先编写我们的业务接口和实现类
UserService 接口
public interface UserService {
public void add();
public void delete();
public void update();
public void search();
}
实现类 UserServiceImpl
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加用户");
}
@Override
public void delete() {
System.out.println("删除用户");
}
@Override
public void update() {
System.out.println("更新用户");
}
@Override
public void search() {
System.out.println("查询用户");
}
}
然后去写我们的增强类 , 我们编写两个 , 一个前置增强 一个后置增强(即上面提到的日志,一个前置日志,一个后置日志)
Log
package com.lqh.pojo;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class Log implements MethodBeforeAdvice {
//method : 要执行的目标对象的方法
//objects : 被调用的方法的参数
//Object : 目标对象
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println( target.getClass().getName() + "的" + method.getName() + "方法被执行了");
}
}
AfterLog
package com.lqh.pojo;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class AfterLog implements AfterReturningAdvice {
//returnValue 返回值
//method被调用的方法
//args 被调用的方法的对象的参数
//target 被调用的目标对象
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName()
+"的"+method.getName()+"方法,"
+"返回值:"+returnValue);
}
}
最后去 spring 的文件中注册 , 并实现 aop 切入实现 , 注意导入约束 .
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.lqh.service.UserServiceImpl"/>
<bean id="log" class="com.lqh.log.Log"/>
<bean id="afterLog" class="com.lqh.log.AfterLog"/>
<!--方式一:使用原生Spring API接口-->
<!--配置aop:需要导入aop的约束-->
<aop:config>
<!--切入点 expression(表达式匹配要执行的方法): execution(修饰词 返回值 列名 方法名 参数) 表示要从哪里去执行-->
<aop:pointcut id="pointcut" expression="execution(* com.lqh.service.UserServiceImpl.*(..))"/>
<!--执行环绕; advice-ref执行方法 . pointcut-ref切入点-->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
<aop:pointcut id="pointcut" expression="execution(* com.lqh.service.UserServiceImpl.*(..))"/>
:* 表示任意;com.lqh.service.UserServiceImpl.*
表示 UserServiceImpl 下的所有方法 ;com.lqh.service.UserServiceImpl.*(..)
中 (…) 表示所有方法的所有参数<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
:表示将 log 这个类切入到 pointcut 中的方法上
测试
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
可能报错如下,可以发现是 applicationContext.xml 中 bean 读取错误
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in class path resource [applicationContext.xml]: BeanPostProcessor before instantiation of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor#0': Cannot resolve reference to bean 'pointcut' while setting bean property 'pointcut'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pointcut': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.aop.aspectj.AspectJExpressionPointcut]: No default constructor found;
错误原因为, maven 的依赖 pom.xml 中,需要加入构建:约定大于配置
<!--在build中配置resources,来防止我们资源导出失败的问题-->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
然后刷新 maven 依赖, clean 项目,成功执行
AOP 的重要性:很重要 . 一定要理解其中的思路 , 主要是思想的理解这一块 .
Spring 的 AOP 就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理 .
第二种方式:自定义类来实现 AOP【主要是切面定义】
目标业务类不变依旧是 userServiceImpl
第一步 : 写我们自己定义的一个切入类(新建一个 diy 包)
package com.lqh.diy;
public class DiyPointcut {
public void before(){
System.out.println("---------方法执行前---------");
}
public void after(){
System.out.println("---------方法执行后---------");
}
}
去 applicationContext.xml 中配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.lqh.service.UserServiceImpl"/>
<bean id="log" class="com.lqh.log.Log"/>
<bean id="afterLog" class="com.lqh.log.AfterLog"/>
<!--方式二:自定义类实现 AOP-->
<!--aop的配置-->
<bean id="diy" class="com.lqh.diy.DiyPointcut"/>
<aop:config>
<!--第二种方式:使用AOP的标签实现-->
<aop:aspect ref="diy">
<aop:pointcut id="diyPonitcut" expression="execution(* com.lqh.service.UserServiceImpl.*(..))"/>
<aop:before pointcut-ref="diyPonitcut" method="before"/>
<aop:after pointcut-ref="diyPonitcut" method="after"/>
</aop:aspect>
</aop:config>
</beans>
测试:
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
第三种方式:使用注解实现
第一步:编写一个注解实现的增强类
package com.lqh.diy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
// 方式三:使用注解方式实现 AOP
@Aspect // 标注这个类是一个切面
public class AnnotationPointcut {
@Before("execution(* com.lqh.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("---------方法执行前---------");
}
@After("execution(* com.lqh.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("---------方法执行后---------");
}
//在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点;
@Around("execution(* com.lqh.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕前");
System.out.println("签名:"+jp.getSignature()); // 获得签名
//执行目标方法proceed
Object proceed = jp.proceed(); //执行方法
System.out.println("环绕后");
System.out.println(proceed);
}
}
第二步:在 applicationContext.xml 配置文件中,注册 bean,并增加支持注解的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.lqh.service.UserServiceImpl"/>
<bean id="log" class="com.lqh.log.Log"/>
<bean id="afterLog" class="com.lqh.log.AfterLog"/>
<!--第三种方式:注解实现-->
<bean id="annotationPointcut" class="com.lqh.diy.AnnotationPointcut"/>
<!--开启注解支持! JDK(默认是 proxy-target-class="false") cglib(proxy-target-class="true")-->
<aop:aspectj-autoproxy/>
</beans>
- aop:aspectj-autoproxy:说明
测试
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
- 通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了
- <aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,当配为<aop:aspectj-autoproxy poxy-target-class=“true”/>时,表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。