AOP:面向切面编程

AOP:面向切面编程

AOP:首先需要明确一点,AOP不是spring、甚至不是java特有的概念。

面向切面编程(AOP,Aspect-Oriented Programming)是一种代码组织和复用范式,其核心思想是将传统的纵向处理,改为横向切割,从而实现通用业务核心业务逻辑分离。可以达到代码的复用、解耦。

img

引入

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.24.RELEASE</version>
</dependency>

配置

在Springconfig中如下配置

1
2
3
4
5
6
@Configuration
@ComponentScan
@EnableAspectJAutoProxy //激活AOP
public class SpringConfig {

}

好比我们接下来要设定一个创建新用户——权限检验——成功创建的逻辑

新建用户类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Spring;


import org.springframework.stereotype.Component;

@Component //让Spring扫描到
public class Userdemo {
public void add_user ()
{
System.out.println("添加用户");
}
public void get_user ()
{
System.out.println("已获取用户:");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Spring.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Authaspect {
// 切入点
@Pointcut("bean(userdemo)")
public void pointCut() {
}


// 切入后你要进行什么操作
@Before("pointCut()")
public void doLog() {
System.out.println("正在进行权限校验");
}

}

AOP就是锚定指定类的受Spring管理下的对象,拦截他们的方法并在执行之前执行开发者想要执行的逻辑。

执行,在SpringDemo中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Spring;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringDemo {
public static void main(String[] args)
{
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Userdemo nmsl = context.getBean(Userdemo.class);
nmsl.add_user();
System.out.println("------------------"); //两个操作的分界线
nmsl.get_user();
}
}

我来更正一下说法,我们基于AOP,对源对象方法的拦截插入等操作一般叫做“增强”,因为拦截插入使得整个程序有了统一的出口和入口,整体上性能有所提升。并且,在方法之前插入或者之后插入被称为“通知”。

几种通知类型:

执行前通知:Before

执行后通知:After

正常返回结果后通知: AfterReturning

抛出异常后通知:AfterThrowing

环绕通知:Around

使用通式就是这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package Spring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Authaspect {
// 你要对谁进行切面处理?
@Pointcut("bean(userdemo)")
public void pointCut() {
}


// 你要进行什么增强
@Before("pointCut()")
public void doLog() {
System.out.println("正在进行权限校验————————权限校验通过");
}

@After("pointCut()")
public void doAfter() {
System.out.println("doAfter");
}

@AfterReturning("pointCut()")
public void doAfterReturning() {
System.out.println("doAfterReturning");
}

@AfterThrowing("pointCut()")
public void doAfterThrowing() {
System.out.println("doAfterThrowing");
}

@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("doAround before");
Object obj = joinPoint.proceed();
System.out.println("doAround after");
return obj;
}

}

有时候,为每个类单独锚定切入点是很困难的,所以我们会使用别的查找方法来批量切入。

一、execution:方法执行(最常用)

AOP 中的 execution方法是定义切入点(Pointcut)的核心方式,它通过编写表达式来精确匹配哪些方法需要被增强。你可以把它想象成一个功能强大的“瞄准镜”,负责在众多方法中锁定那些需要被注入通用功能(如日志、事务等)的目标。

语法结构

1
2
3
execution(
[修饰符] 返回类型 [包路径.类名.方法名](参数列表) [throws 异常]
)

通配符说明

符号 含义 示例
* 匹配任意单个元素 java.*.Service
.. 匹配多个元素(包/参数) com.example..*

典型示例

1
2
3
4
5
6
7
8
// 匹配Service包下所有类的public方法
@Pointcut("execution(public * com.example.service.*.*(..))")

// 匹配UserService中所有get开头的方法
@Pointcut("execution(* com.example.service.UserService.get*(..))")

// 匹配所有返回String的无参方法
@Pointcut("execution(String *())")

二、within:类/包范围限定

使用场景

快速匹配整个类或包下的所有方法

代码示例

1
2
3
4
5
6
7
8
// 匹配service包下所有类
@Pointcut("within(com.example.service.*)")

// 匹配service包及其子包
@Pointcut("within(com.example.service..*)")

// 匹配具体实现类(对接口无效)
@Pointcut("within(com.example.service.UserServiceImpl)")

三、this:代理对象类型匹配

适用场景

基于JDK动态代理时匹配接口类型

1
2
// 匹配代理对象实现UserService接口的方法
@Pointcut("this(com.example.service.UserService)")

四、target:目标对象类型匹配

适用场景

基于CGLIB代理时匹配实现类

1
2
// 匹配目标对象是UserServiceImpl类的方法
@Pointcut("target(com.example.service.UserServiceImpl)")

五、args:参数类型匹配

特殊用法

1
2
3
4
5
6
7
8
// 匹配第一个参数为Long类型的方法
@Pointcut("args(Long, ..)")

// 配合通知获取参数值
@Before("args(userId, ..) && target(service)")
public void log(Long userId, UserService service) {
System.out.println("操作用户ID:" + userId);
}

六、@annotation:方法注解匹配

实战应用

1
2
3
4
5
6
7
8
9
10
11
12
// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {}

// 切入带注解的方法
@Pointcut("@annotation(com.example.AuditLog)")
public void auditPointcut() {}

// 使用
@AuditLog
public void sensitiveOperation() { /*...*/ }

七、@within:类注解匹配

应用场景

匹配带有指定注解的类中的所有方法

1
2
3
4
5
6
7
// 自定义类注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SecureApi {}

// 切入点配置
@Pointcut("@within(com.example.SecureApi)")

八、bean:Spring Bean名称匹配

Spring特有语法

1
2
3
4
5
6
7
8
// 匹配ID为userService的Bean
@Pointcut("bean(userService)")

// 匹配名称以Service结尾的Bean
@Pointcut("bean(*Service)")

// 排除指定Bean
@Pointcut("bean(userService) && !bean(adminService)")

组合表达式技巧

运算符 说明 示例
&& 逻辑与 execution(* save*(..)) && within(com.example.service)
|| 逻辑或 `@annotation(AuditLog)
! 逻辑非 execution(* *(..)) && !bean(testService)

复杂组合示例

1
2
3
4
// 匹配service包下非测试环境的方法调用
@Pointcut("execution(* com.example.service.*.*(..)) " +
"&& !@annotation(DevOnly) " +
"&& !within(com.example.service.test.*)")

性能优化建议

  1. 缩小匹配范围:优先使用within缩小包范围
  2. 避免过度通配*Service*.*效率高10倍以上
  3. 缓存切入点:使用@Pointcut定义可复用的切入点

AOP中的一些术语

只是帮助理解

  • 切面:一系列切点+通知的集合,一个关注点的模块化。(例如日志切面主要负责处理日志,认证切面主要负责处理认证相关)
  • 连接点:程序执行过程中的一个点,例如方法的执行或异常的处理。在Spring AOP中,连接点始终表示方法执行。我们可以在连接点来进行拦截和增强。(例子:spring中,所有可以被增强的方法都是连接点)(秀女选妃,所有秀女都是连接点)(反向例子:我想在方法的一半来进行拦截,spring中是做不到的)
  • 通知:简单来说,就是你要干嘛。(例如我要记录日志、权限校验等。)
  • 切点:简单来说,就是我要在哪些连接点进行增强。不增强的点叫连接点,增强的点就是切点。(秀女选妃,选上了的就是切点)
  • 引入:特殊增强:为类动态添加新接口。
  • 目标对象:简单来说,就是我们切面进行了增强的原始对象。
  • 代理:在spring中,主要使用JDK动态代理(基于接口)CGLIB代理(基于类继承)
  • 织入:就是把我们的通知插入切点的过程。