背景
本文是《Java 后端从小白到大神》修仙系列之框架学习,Java框架之Spring框架第二篇
。本篇文章主要聊Java框架
,那么必然从Spring
框架开始,可以说Spring框架是Java企业级开发的基石,若想详细学习请点击首篇博文,我们现在开始吧。
文章概览
- 面向切面编程 (AOP)
Spring框架
面向切面编程 (AOP)
1. AOP 基本概念
让我们先从一个案例
说起,用户登录系统:
- 希望所有登录相关的方法,都统一加日志记录。
- 又希望某些敏感操作,要统一加权限校验。
graph TD
subgraph 用户登录系统
A[UserController] --> B[UserService]
B --> C[login]
B --> D[logout]
B --> E[adminOperation]
end
subgraph AOP实施过程
F[日志切面 LoggingAspect] --> G[切点1 @Pointcut execution * login * ..]
F --> H[通知1 @Before logMethodCall]
I[权限切面 SecurityAspect] --> J[切点2 @Pointcut execution * admin * ..]
I --> K[通知2 @Before checkPermission]
C -->|连接点1 JoinPoint| G
E -->|连接点2 JoinPoint| J
G -->|织入 Weaving| H
J -->|织入 Weaving| K
end
style F fill:#f9f,stroke:#333
style I fill:#6f9,stroke:#333
style G fill:#ff6,stroke:#333
style J fill:#ff6,stroke:#333
style H fill:#6cf,stroke:#333
style K fill:#6cf,stroke:#333
style C fill:#cdf,stroke:#333
style E fill:#cdf,stroke:#333
2. 场景说明与术语对应
-
切面 (Aspect) - 功能模块
日志切面 (LoggingAspect)
:粉紫色模块
权限切面 (SecurityAspect)
:绿色模块
如同保安小队,每个切面负责特定功能
-
切点 (Pointcut) - 选择规则
切点1
:黄色模块 - "execution(* login*(..))"
选择所有以login
开头的方法
切点2
:黄色模块 - "execution(* admin*(..))"
选择所有以admin
开头的方法
如同保安队长的指令:“只检查VIP入口”
-
连接点 (JoinPoint) - 可插入位置
login()
:蓝色方法 - 连接点1
adminOperation()
:蓝色方法 - 连接点2
如同大楼的入口,每个方法调用点都是潜在切入点
-
通知 (Advice) - 具体动作
logMethodCall()
:天蓝色模块 - 前置通知
在login*方法执行前记录日志
checkPermission()
:天蓝色模块 - 前置通知
在admin*方法执行前检查权限
如同保安的具体动作:登记或检查证件
-
织入 (Weaving) - 植入过程
- 切点1 → 通知1 的橙色箭头
- 切点2 → 通知2 的橙色箭头
如同保安实际到达岗位执勤
3. 使用场景决策指南
需求场景 |
AOP组件选择 |
配置示例 |
说明 |
需要添加新功能模块 |
切面(Aspect) |
@Aspect public class XxxAspect |
如日志、安全等横切关注点 |
需要选择特定方法 |
切点(Pointcut) |
@Pointcut("execution(* save*(..))") |
用表达式定义过滤规则 |
需要在方法前后执行操作 |
通知(Advice) |
@Before , @After , @Around |
定义执行时机和具体操作 |
需要获取当前方法信息 |
连接点(JoinPoint) |
log(JoinPoint jp) |
可获取方法签名、参数等 |
需要让代理生效 |
织入(Weaving) |
配置@EnableAspectJAutoProxy |
启用AOP自动代理 |
4. 实际代码对应示例
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
|
// 切面:日志记录模块
@Aspect
public class LoggingAspect {
// 切点:选择所有login开头的方法
@Pointcut("execution(* login*(..))")
public void loginMethods() {} // 切点定义
// 通知:在切点方法执行前执行
@Before("loginMethods()")
public void logMethodCall(JoinPoint jp) { // 连接点作为参数
System.out.println("调用方法: " + jp.getSignature().getName());
}
}
// 切面:权限检查模块
@Aspect
public class SecurityAspect {
// 切点:选择所有admin开头的方法
@Pointcut("execution(* admin*(..))")
public void adminMethods() {} // 切点定义
// 通知:在切点方法执行前执行
@Before("adminMethods()")
public void checkPermission(JoinPoint jp) { // 连接点作为参数
if (!UserContext.isAdmin()) {
throw new SecurityException("无权限操作!");
}
}
}
|
5. 通知 (Advice) 类型
通知类型 |
注解 |
含义 |
前置通知 |
@Before |
方法执行之前执行 |
后置通知 |
@After |
方法执行完成后执行(不管成功/异常都会执行) |
返回通知 |
@AfterReturning |
方法成功返回后执行 |
异常通知 |
@AfterThrowing |
方法抛出异常后执行 |
环绕通知 |
@Around |
方法执行前后都可以控制,可以手动决定是否执行原方法 (proceed() 调用),还能控制返回值,最强大 |
5.1 通知类型测试代码
5种通知类型示例
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
46
|
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
// 切点表达式:拦截所有 service 包下的方法
@Pointcut("execution(* com.example.service..*(..))")
public void serviceMethods() {
}
// 1. 前置通知
@Before("serviceMethods()")
public void beforeAdvice() {
System.out.println("【前置通知】方法即将执行!");
}
// 2. 后置通知
@After("serviceMethods()")
public void afterAdvice() {
System.out.println("【后置通知】方法已经执行完成(无论成功失败)!");
}
// 3. 返回通知
@AfterReturning("serviceMethods()")
public void afterReturningAdvice() {
System.out.println("【返回通知】方法成功返回!");
}
// 4. 异常通知
@AfterThrowing("serviceMethods()")
public void afterThrowingAdvice() {
System.out.println("【异常通知】方法出现异常了!");
}
// 5. 环绕通知
@Around("serviceMethods()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("【环绕通知-前】即将执行方法:" + pjp.getSignature());
Object result = pjp.proceed(); // 执行目标方法,既原方法
System.out.println("【环绕通知-后】方法执行完毕:" + pjp.getSignature());
return result;
}
}
|
启动测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Service
public class UserService {
public void register(String username) {
System.out.println(username + " 注册成功!");
// throw new RuntimeException("模拟异常"); // 取消注释可以测试异常通知
}
}
调用逻辑:
userService.register("Alice");
控制台输出:
【环绕通知-前】即将执行方法:void com.example.service.UserService.register(String)
【前置通知】方法即将执行!
Alice 注册成功!
【后置通知】方法已经执行完成(无论成功失败)!
【返回通知】方法成功返回!
【环绕通知-后】方法执行完毕:void com.example.service.UserService.register(String)
|
5.2 Spring AOP 通知执行顺序
graph TD
A[外部调用目标方法] --> B{环绕通知 - 前置部分};
B --> C[前置通知 @Before];
C --> D[目标方法执行];
D -- 正常返回 --> E[后置通知 @After];
D -- 抛出异常 --> F[后置通知 @After];
E --> G[返回通知 @AfterReturning];
F --> H[异常通知 @AfterThrowing];
G --> I{环绕通知 - 后置部分};
H --> I;
I --> J[方法执行结束];
style A fill:#f9f,stroke:#333,stroke-width:2px;
style B fill:#ccf,stroke:#333,stroke-width:2px;
style C fill:#9cf,stroke:#333,stroke-width:2px;
style D fill:#aaf,stroke:#333,stroke-width:2px;
style E fill:#ddf,stroke:#333,stroke-width:2px;
style F fill:#ddf,stroke:#333,stroke-width:2px;
style G fill:#9fd,stroke:#333,stroke-width:2px;
style H fill:#f99,stroke:#333,stroke-width:2px;
style I fill:#ccf,stroke:#333,stroke-width:2px;
style J fill:#f9f,stroke:#333,stroke-width:2px;
6. 切点表达式 (Pointcut Expressions)
示例:拦截com.example.service 包下的所有 *Service 类,方法名以 find 开头,并且方法上标了自定义注解 @MyLog,在方法执行前打印日志。
1. 创建注解 @MyLog
1
2
3
4
5
6
7
8
|
package com.example.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
|
2. 创建一个业务类 UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package com.example.service;
import com.example.annotation.MyLog;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@MyLog
public String findUserById(String id) {
System.out.println("执行 findUserById 逻辑,参数:" + id);
return "User:" + id;
}
public String updateUser(String id) {
System.out.println("执行 updateUser 逻辑,参数:" + id);
return "Updated User:" + id;
}
}
注意:
- `findUserById` 有 `@MyLog`
- `updateUser` 没有 `@MyLog`
|
3. 创建切面类 LogAspect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
// execution(* com.example.service.*Service.find*(..)) , ➔ 包 + 类名 + 方法名规则匹配。
// @annotation(com.example.annotation.MyLog) , ➔ 方法必须标注了 @MyLog 注解。
// 两者 && 联合生效
@Before("execution(* com.example.service.*Service.find*(..)) && @annotation(com.example.annotation.MyLog)")
public void logBefore(JoinPoint joinPoint) {
System.out.println("[日志切面] 方法调用前:" + joinPoint.getSignature());
}
}
|
4. 启动测试
- Spring Boot 配置启用 AOP
application.properties:
spring.aop.proxy-target-class=true
默认开启了 AOP,不需要特别额外操作。
如果是纯 Spring 项目,需要加:@EnableAspectJAutoProxy
1
2
3
4
5
6
7
8
9
10
11
12
|
@Autowired
private UserService userService;
@Test
public void testFindUser() {
userService.findUserById("123");
userService.updateUser("456");
}
[日志切面] 方法调用前:String com.example.service.UserService.findUserById(String)
执行 findUserById 逻辑,参数:123
执行 updateUser 逻辑,参数:456
|
示例:拦截任意 service 方法,方法执行前后都可以插入逻辑,可以拿到参数、可以改返回值、可以统一异常处理!
1. 先定义一个注解:@Monitor
1
2
3
4
5
6
7
8
9
|
package com.example.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitor {
String value() default "";
}
|
2. 写业务方法:OrderService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package com.example.service;
import com.example.annotation.Monitor;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Monitor("下单接口")
public String createOrder(String productId) {
System.out.println("执行 createOrder,商品ID:" + productId);
return "订单号: " + System.currentTimeMillis();
}
public String cancelOrder(String orderId) {
System.out.println("执行 cancelOrder,订单ID:" + orderId);
return "取消成功";
}
}
|
3. 创建一个超强 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
|
package com.example.aspect;
import com.example.annotation.Monitor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MonitorAspect {
@Around("@annotation(monitor)")
public Object around(ProceedingJoinPoint pjp, Monitor monitor) throws Throwable {
String methodName = pjp.getSignature().toShortString();
Object[] args = pjp.getArgs();
long start = System.currentTimeMillis();
System.out.println("[Monitor] 开始调用方法:" + methodName + ",描述:" + monitor.value());
System.out.println("[Monitor] 参数:" + java.util.Arrays.toString(args));
Object result = null;
try {
result = pjp.proceed(); // 继续执行目标方法
System.out.println("[Monitor] 方法正常返回,结果:" + result);
} catch (Throwable ex) {
System.out.println("[Monitor] 方法抛出异常:" + ex.getMessage());
throw ex; // 记得重新抛出
} finally {
long end = System.currentTimeMillis();
System.out.println("[Monitor] 方法耗时:" + (end - start) + " ms");
}
// 可以在这里,改变返回值
return result;
}
}
|
4. 启动测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Autowired
private OrderService orderService;
@Test
public void testCreateOrder() {
String result = orderService.createOrder("P001");
System.out.println("最终返回结果:" + result);
}
@Test
public void testCancelOrder() {
String result = orderService.cancelOrder("O123");
System.out.println("最终返回结果:" + result);
}
|
6. 测试输出
1
2
3
4
5
6
|
[Monitor] 开始调用方法:OrderService.createOrder(..),描述:下单接口
[Monitor] 参数:[P001]
执行 createOrder,商品ID:P001
[Monitor] 方法正常返回,结果:订单号: 1714333444110
[Monitor] 方法耗时:12 ms
最终返回结果:订单号: 1714333444110
|
7. Spring AOP 实现原理
1
2
3
4
5
6
7
8
9
10
11
12
|
Bean初始化流程
↓
AnnotationAwareAspectJAutoProxyCreator
↓
扫描所有切面(Advisor)
↓
给当前Bean匹配合适的Advisor
↓
需要增强?
↓
是 → 创建代理对象(JDK / CGLIB)
否 → 保持原样
|
详细分步骤版
步骤 |
内容 |
1 |
Spring 启动时,@Aspect 注解的切面类会被解析,注册成 Advisor。 |
2 |
每次有 Bean 初始化时,AnnotationAwareAspectJAutoProxyCreator 介入。 |
3 |
给这个 Bean 找一圈,哪些 Advisor 能作用在它身上。 |
4 |
如果找到了,就用 JDK Proxy / CGLIB 生成代理对象。 |
5 |
把代理对象注册进容器,代替原来的 Bean。 |
6 |
调用代理对象时,会走 Advice 逻辑,比如前置、后置、环绕通知。 |
Spring AOP = 容器后处理 + 自动织入 + 动态代理
示例:开启注解驱动
1
2
3
4
5
6
7
8
9
10
|
// 声明这是一个配置类(相当于 XML 配置文件)
@Configuration
// 自动向容器中注入了 AnnotationAwareAspectJAutoProxyCreator 这个超核心的 BeanPostProcessor!
// 这个 Processor 能:扫描所有的切面(@Aspect),动态为匹配的 Bean 生成代理对象,实现横切逻辑(Before、After、Around等)
// proxyTargetClass = false(默认),false:优先用 JDK 动态代理(接口代理),如果有接口就用接口;true:强制用 CGLIB 子类代理(即使有接口也不用 JDK Proxy)。
// exposeProxy是否把代理对象暴露到当前线程上下文?true:可以通过 AopContext.currentProxy() 获取到当前的代理对象;false:默认不暴露。
@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)
// 扫描 com.example 包,自动注册组件(比如你的 Service、Controller、Aspect类)
@ComponentScan("com.example")
public class AopConfig {}
|
8.使用与测试
示例:
1. 依赖准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<!-- AspectJ Runtime (织入支持) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- Spring Test 测试支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<!-- JUnit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
|
2. 配置类(AopConfig)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false) // EnableAspectJAutoProxy 开启 AOP 自动代理,自动寻找bean中带有aspect注解的类,为期创建代理类,proxyTargetClass = false 优选JDK代理
@ComponentScan("com.example") // 扫描 Service 和 Aspect
public class AopConfig {
@Bean
public MyService myService() {
return new MyService();
}
@Bean
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}
|
3. 业务类(Service)
1
2
3
4
5
6
|
@Service
public class MyService {
public void doWork(String taskName) {
System.out.println("Doing work: " + taskName);
}
}
|
4. 切面类(Aspect)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.MyService.doWork(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[AROUND BEFORE] - " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 调用目标方法
System.out.println("[AROUND AFTER] - " + joinPoint.getSignature().getName());
return result;
}
@Before("execution(* com.example.MyService.doWork(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("[BEFORE] - " + joinPoint.getSignature().getName());
}
@AfterReturning("execution(* com.example.MyService.doWork(..))")
public void afterReturningAdvice(JoinPoint joinPoint) {
System.out.println("[AFTER RETURNING] - " + joinPoint.getSignature().getName());
}
}
|
5. 测试启动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 让 JUnit5 支持 Spring
@ExtendWith(SpringExtension.class)
// 告诉测试用哪个 Spring 配置
@ContextConfiguration(classes = {AopConfig.class, MyService.class, LoggingAspect.class})
public class AopTest {
@Autowired
private MyService service;
@Test
void testAspect() {
service.doWork("test");
// 观察控制台:[AROUND BEFORE] ... [BEFORE] ... Doing work: test ... [AFTER RETURNING] ... [AROUND AFTER]
}
}
|
6. 测试结果
1
2
3
4
5
|
[AROUND BEFORE] - doWork
[BEFORE] - doWork
Doing work: test
[AFTER RETURNING] - doWork
[AROUND AFTER] - doWork
|
示例:
1. 目录结构
1
2
3
4
5
6
7
8
9
10
11
|
src/main/java/
└── com/example/
├── config/
│ └── MyConfig.java
├── aspect/
│ └── LoggingAspect.java
├── service/
│ └── MyService.java
src/test/java/
└── com/example/
└── AopTest.java
|
2. 配置类:MyConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package com.example.config;
import com.example.aspect.LoggingAspect;
import com.example.service.MyService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class MyConfig {
@Bean
public MyService myService() {
return new MyService();
}
@Bean
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}
|
3. 业务类:MyService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class MyService {
public void doWork(String task) {
System.out.println("Doing work on: " + task);
}
public void doOtherWork() {
System.out.println("Doing other work");
}
public String riskyOperation(String input) {
if (input == null) {
throw new IllegalArgumentException("Input must not be null");
}
return "Processed: " + input;
}
}
|
4. 切面类:LoggingAspect.java
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
46
47
48
49
50
51
52
|
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 只拦截 doWork 方法
@Pointcut("execution(* com.example.service.MyService.doWork(..))")
public void workPointcut() {}
// 只拦截 riskyOperation 方法
@Pointcut("execution(* com.example.service.MyService.riskyOperation(..))")
public void riskyPointcut() {}
// 通用 Before
@Before("workPointcut()")
public void beforeWork(JoinPoint joinPoint) {
System.out.println("[Before] " + joinPoint.getSignature().getName() + ", args: " + joinPoint.getArgs()[0]);
}
// Around 带参数打印
@Around("workPointcut()")
public Object aroundWork(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Around Before] " + pjp.getSignature().getName());
Object result = pjp.proceed();
System.out.println("[Around After] " + pjp.getSignature().getName());
return result;
}
// AfterReturning
@AfterReturning(value = "workPointcut()", returning = "result")
public void afterReturningWork(JoinPoint joinPoint, Object result) {
System.out.println("[AfterReturning] " + joinPoint.getSignature().getName());
}
// AfterThrowing 专门处理 riskyOperation 抛异常的情况
@AfterThrowing(value = "riskyPointcut()", throwing = "ex")
public void afterThrowingRisky(JoinPoint joinPoint, Exception ex) {
System.out.println("[AfterThrowing] " + joinPoint.getSignature().getName() + ", Exception: " + ex.getMessage());
}
// 通用 After
@After("workPointcut() || riskyPointcut()")
public void afterAny(JoinPoint joinPoint) {
System.out.println("[After] " + joinPoint.getSignature().getName());
}
}
|
5. 测试类:AopTest.java
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
|
package com.example;
import com.example.config.MyConfig;
import com.example.service.MyService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = MyConfig.class)
public class AopTest {
@Autowired
private MyService myService;
// Around Before ➔ Before ➔ 业务方法 ➔ AfterReturning ➔ After ➔ Around After
@Test
void testDoWork() {
myService.doWork("TestTask");
}
@Test
void testDoOtherWork() {
myService.doOtherWork(); // 这个不会被AOP拦截
}
// 正常流程:业务方法 ➔ After
@Test
void testRiskyOperation_success() {
String result = myService.riskyOperation("safe input");
System.out.println("Result: " + result);
}
// 抛异常:AfterThrowing 捕获异常 ➔ After
@Test
void testRiskyOperation_fail() {
try {
myService.riskyOperation(null);
} catch (Exception ignored) {
}
}
}
|
总结
Spring 框架是 Java 生态的基石,必知必会。