AOP
基于动态代理实现AOP,spring aop代理必须使用代理对象才能生效
概念
概念 | 说明 |
---|---|
切⾯(aspect) | 按关注点进⾏模块分解时,横切关注点就表⽰为⼀个切⾯ |
连接点(join point) | 程序执⾏的某⼀刻,在这个点上可以添加额外的动作 |
通知(advice) | 切⾯在特定连接点上执⾏的动作 |
切⼊点(pointcut) | 切⼊点是⽤来描述连接点的,它决定了当前代码与连接点是否匹配 |
JDK 动态代理与 CGLIB 代理的区别
必须要实现接⼝ | ⽀持拦截 public ⽅法 | ⽀持拦截 protected ⽅法 | 拦截默认作⽤域⽅法 | |
---|---|---|---|---|
JDK 动态代理 | 是 | 是 | 否 | 否 |
CGLIB 代理 | 否 | 是 | 是 | 是 |
基于 @Aspect 的配置
- 引入依赖,以便使⽤
Aspect
相关的注解和功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 要开启
@Aspect
⽀持,可以在 Java 配置类上增加@EnableAspectJAutoProxy
注解@EnableAspectJAutoProxy
有两个属性,proxyTargetClass
⽤于选择是否开
启基于类的代理(是否使⽤ CGLIB 来做代理);exposeProxy
⽤于选择是否将代
理对象暴露到AopContext
中,两者默认值都是false
。- 在 Spring Boot 中,会⾃动配置使⽤基于类的代理
@Configuration
@EnableAspectJAutoProxy
public class Config {...}
声明切入点
@Pointcut
中的⼀些常⽤ PCD
PCD | 说明 |
---|---|
execution | 最常⽤的⼀个 PCD,⽤来匹配特定⽅法的执⾏ |
within | 匹配特定范围内的类型,可以⽤通配符来匹配某个 Java 包内的所有类 |
this | Spring AOP 代理对象这个 Bean 本⾝要匹配某个给定的类型 |
target | ⽬标对象要匹配某个给定的类型,⽐ this 更常⽤⼀些 |
args | 传⼊的⽅法参数要匹配某个给定的类型,它也可以⽤于绑定请求参数 |
bean | Spring AOP 特有的⼀个 PCD,匹配 Bean 的 ID 或名称,可以⽤通配符 |
@target | 执⾏的⽬标对象带有特定类型注解 |
@args | 传⼊的⽅法参数带有特定类型注解 |
@annotation | 拦截的⽅法上带有特定类型注解 |
-
切⼊点表达式⽀持与、或、⾮运算,运算符分别为 &&、||和 !
-
execution
⽤得⾮常多,下⾯详细描述⼀下它的表达式,[] 代表可选项,<>
代表必选项:execution([修饰符] <返回类型> [全限定类名.]<⽅法>(<参数>) [异常])
- 每个部分都可以使⽤
*
通配符 - 类名中使⽤
.*
表⽰包中的所有类,..*
表⽰当前包与⼦包中的所有类 - 参数主要分为以下⼏种情况:
- () 表⽰⽅法⽆参数
- (…) 表⽰有任意个参数
- (*) 表⽰有⼀个任意类型的参数
- (String) 表⽰有⼀个 String 类型的参数
- (String,String) 代表有两个 String 类型的参数
- 每个部分都可以使⽤
package learning.spring.helloworld;
public class HelloPointcut {
@Pointcut("target(learning.spring.helloworld.Hello)")
public void helloType() {} // ⽬标对象是learning.spring.helloworld.Hello类型
@Pointcut("execution(public * say())")
public void sayOperation() {} // 执⾏public的say()⽅法
@Pointcut("helloType() && sayOperation()") // 复⽤其他切⼊点
public void sayHello() {} // 执⾏Hello类型中public的say()⽅法
}
// learning.spring.helloworld及其⼦包中所有类⾥的say⽅法
// 该⽅法可以返回任意类型,第⼀个参数必须是String,后⾯可以跟任意参数
execution(* learning.spring.helloworld..*.say(String,..))
// learning.spring.helloworld及其⼦包
within(learning.spring.helloworld..*)
// ⽅法的参数仅有⼀个String
args(java.lang.String)
// ⽬标类型为Hello及其⼦类
target(learning.spring.helloworld.Hello+)
// 类上带有@AopNeeded注解
@target(learning.spring.helloworld.AopNeeded)
由于 Spring AOP 的实现基于动态代理,因⽽只能匹配普通⽅法的执⾏,像静态
初始化、静态⽅法、构造⽅法、属性赋值等操作都是拦截不到的
声明通知
import com.example.dto.ParamVo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AspectDemo {
@Pointcut("execution(public * demo(..))")
public void helloType() {}
/**
* 前置通知的⽅法没有返回值,因为它在被拦截的⽅法前执⾏,
* 就算有返回值也没地⽅使⽤,
* 但是它可以对被拦截⽅法的参数进⾏加⼯,通过 args 这个 PCD 能
* 明确参数,并将其绑定到前置通知⽅法的参数上。
*/
@Before("helloType() && args(vo)")
public void before(ParamVo vo) {
System.out.println("Before Advice");
}
/**
* 添加了 @After 注解的⽅法必须要能够处理正常与异常这两种情况,
* 但它⼜获取不到返回值或异常对象,所以⼀般只⽤来做⼀些资源清理的⼯作
*/
@After("helloType()")
public void after() {
System.out.println("After Advice");
}
/**
* ⽅法的参数 result 就是被拦截⽅法的返回值,⽽且此处限定了
* 该通知只拦截返回值是 String 类型的调⽤。
* 需要提醒的是,returning 中给定的名字必须与⽅法的参数名保持⼀致。
* @param result
*/
@AfterReturning(pointcut = "helloType()",returning = "result")
public void afterReturning(String result) {
System.out.println("AfterReturning Advice");
}
/**
* 想要拦截抛出异常的调⽤
* @param exception
*/
@AfterThrowing(pointcut = "helloType()",throwing = "exception")
public void afterThrowing(Exception exception) {
System.out.println("AfterThrowing Advice");
}
/**
* 第⼀个参数必须是 ProceedingJoinPoint 类型的,
* ⽅法的返回类型是被拦截⽅法的返回类型,或者直接⽤ Object 类型。
* @param pjp
*/
@Around("helloType()")
public Object around(ProceedingJoinPoint pjp) {
System.out.println("Around Advice");
try {
//其中的 pjp.proceed() 就是调⽤具体的连接点进⾏的处理,proceed() ⽅法,也接受 Ojbect[] 参数,可以替代原先的参数。
return pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
- 同时存在多个通知作⽤于同⼀处,可以让切⾯类实现 Ordered 接⼝,或者在上⾯添加 @Order 注解。指定的值越低,优先级则越⾼,在最终的代理对象执⾏时也会先执⾏优先级⾼的逻辑。