AOP 是为了解决“横切关注点”问题的一种编程范式。
在一个项目中,有很多功能不是业务核心逻辑,但又会反复出现在多个地方,例如:
日志记录
权限校验
登录状态检查
统计耗时
异常处理
这些逻辑与“业务方法”不在一个维度上,但又必须“附着在”业务方法上。AOP 就是用来把这些“通用功能”抽出来,统一管理和复用的。
有一个登录流程,希望在不修改源代码的情况下,添加权限判断模块,使得用户在校验用户名、密码成功以后,做一个权限的判断,然后再进入到主页面。
不通过修改源代码的方式,在主干功能里添加新功能。降低了耦合度!
总结:为什么用 AOP?
优势 | 举例 |
---|---|
解耦 | 权限判断逻辑不写在 login 方法里 |
复用 | 一次定义,多个方法可用 |
可插拔 | 加一个注解或切点表达式就生效 |
AOP允许你在不改变原始业务代码的前提下,在方法前后加“额外逻辑”(例如日志、事务、安全控制等)。
AOP 是怎么做到的?
本质:Spring AOP 通过“动态代理”技术来实现对目标方法的增强。
Spring 根据目标类是否实现接口,选择不同的方式生成代理对象:
有两种情况动态代理
第一种 有接口情况,使用 JDK 动态代理
第二种 没有接口情况,使用 CGLIB 动态代理
目的:想要在实现类调login()方法的前后,添加新的功能。
JDK 动态代理只能为接口创建代理对象。
Spring 会为接口生成代理类,拦截接口方法,织入切面逻辑。
目的:在user类的add方法上增强新的方法。
如果目标类没有实现接口,Spring 会使用 CGLIB(Code Generation Library)生成目标类的子类。
代理类是目标类的“子类”,重写方法,加入增强逻辑。
public interface UserService {
void addUser();
}
public class UserServiceImpl implements UserService {
public void addUser() {
System.out.println("执行 addUser 方法");
}
}
使用Proxy类里面的方法:newProxyInstance(),创建代理对象。
public static Object newProxyInstance(
ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)
这个方法是
java.lang.reflect.Proxy
类中的 静态方法,用于创建一个 代理对象。
1. ClassLoader loader
作用:指定当前代理类由哪个类加载器来加载。
一般写法:传入目标对象的类加载器,例如:
target.getClass().getClassLoader()
为什么需要这个?
因为 Java 在运行时会动态生成一个代理类,这个代理类也需要被加载器加载进 JVM 才能使用。
2. Class>[] interfaces
作用:指定代理对象要实现的一组接口。
这些接口是目标对象实现的接口,也是你希望代理对象对外暴露的行为。
一般写法:
target.getClass().getInterfaces()
注意:JDK 动态代理只能代理接口,不能代理普通类(如没有接口的目标类需要用 CGLIB)。
3. InvocationHandler h
作用:代理逻辑的核心,是一个接口,你要提供它的实现类。
当你调用代理对象的方法时,实际执行的是 InvocationHandler
的 invoke()
方法。
接口定义如下:
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
你可以在 invoke()
方法中定义:
方法前后要干什么(增强)
是否真的调用目标对象的方法
假设你有一个接口 UserDao 和一个实现类:
public interface UserDao {
public int add(int a, int b);
public String update(String id);
}
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
System.out.println("add方法执行了......");
return a+b;
}
@Override
public String update(String id) {
System.out.println("update方法执行了......");
return id;
}
}
现在你想通过 JDK 动态代理加一个日志功能:
public class JDKProxy {
public static void main(String[] args) {
// 创建接口的实现类的代理对象
Class[] interfaces = {UserDao.class};
/*Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
// 方法一:匿名内部类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});*/
// 方法二:创建接口:InvocationHandler的实现类
UserDaoImpl userDaoImpl = new UserDaoImpl();
// 创建接口实现类的代理对象
UserDao userDao = (UserDao) Proxy.newProxyInstance(userDaoImpl.getClass().getClassLoader(), interfaces, new UserProxy(userDaoImpl));
int sum = userDao.add(1, 2);
System.out.println("result: " + sum);
}
}
// 创建代理对象代码
class UserProxy implements InvocationHandler{
// 创建的是谁的代理对象,就把谁传递过来
// 有参构造
private Object obj;
public UserProxy(Object obj){
this.obj = obj;
}
// 增强的逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法之前
System.out.println("方法之前执行....." + method.getName() + ": 传递的参数..." + Arrays.toString(args));
// 被增强的方法的执行
Object res = method.invoke(obj, args);
// 方法之后
System.out.println("方法之后执行......" + obj);
return res;
}
}
输出:
参数 | 作用 | 常见写法 |
---|---|---|
ClassLoader |
加载代理类 | target.getClass().getClassLoader() |
Class>[] |
代理哪些接口 | target.getClass().getInterfaces() |
InvocationHandler |
方法增强逻辑 | 自定义实现(如日志、事务) |
public class User {
public void add() { ... }
public void update() { ... }
public void select() { ... }
public void delete() { ... }
}
连接点:程序中所有可以被增强的位置,在 Spring AOP 中就是方法。
对User
类来说:
add()、update()、select()、delete() 这四个方法都是连接点
切入点:你“真正选中”的、打算增强的方法。
所有方法是“连接点”,但你不一定要增强全部。
你可以用表达式挑出感兴趣的方法,比如:只增强 add 和 delete 方法
类比:
连接点是“候选池”,切入点是“真正入选”。
示例表达式(用来定义切入点):
@Pointcut("execution(* com.example.service.User.add(..))")
上面这句意思是:
“我要增强 com.example.service.User 类的 add 方法”
通知:你想“插进去”的代码逻辑(也叫增强代码)
也就是你实际想在方法前后做的事,比如:
打印日志
开启事务
校验权限
报错时写日志
通知类型 | 作用时间点 | 示例含义 |
---|---|---|
前置通知 @Before |
方法执行前 | 比如:打日志、权限验证 |
后置通知 @AfterReturning |
方法执行后(成功返回) | 比如:打印返回结果 |
环绕通知 @Around |
方法执行前后都包住 | 最强,可以控制是否继续执行原方法 |
异常通知 @AfterThrowing |
方法抛出异常时触发 | 比如:记录异常日志 |
最终通知 @After |
方法执行完一定会执行(无论成功或异常) | 类似 finally 块 |
@Before("execution(* com.example.service.User.add(..))")
public void logBefore() {
System.out.println("方法开始前记录日志");
}
这就是一个前置通知,目标是 add()
方法。
概念 | 类比解释 | 在你的例子中举例 |
---|---|---|
连接点 | 所有方法都是“可以增强的点” | add() 、update() 等 |
切入点 | 你选择要增强的那些点 | 只增强 add() |
通知 | 插入的逻辑代码 | 在 add() 前打印日志 |
你定义一个切面类(Aspect):
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.User.add(..))")
public void addPointcut() {}
@Before("addPointcut()")
public void logBefore() {
System.out.println("调用 add() 前打印日志");
}
}
Spring 会自动为 User.add()
方法生成代理对象,在调用时:
logBefore() → add() 原方法 → (可能还有后置/最终通知)
切面(Aspect)= 通知(增强逻辑) + 切入点(在哪增强) 的组合体
用通俗话说:
切面就是你定义的一整套增强规则:你想增强什么地方、增强什么逻辑
在 Spring 中,通常是一个 普通 Java 类,加上 @Aspect
注解,就成了一个切面类。
切面是一个动作,把通知应用到切入点的过程。
@Aspect
@Component
public class LogAspect {
// 切入点:指定要增强哪些方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// 通知:在方法前记录日志
@Before("userServiceMethods()")
public void beforeLog() {
System.out.println("【Log】方法调用前");
}
// 通知:方法执行后记录
@After("userServiceMethods()")
public void afterLog() {
System.out.println("【Log】方法调用后");
}
}
LogAspect
就是一个切面类
它定义了:
切入点:UserService
中的所有方法
通知:在这些方法执行前/后执行打印日志
只要满足两件事:
步骤 | 说明 |
---|---|
1 | 类上加 @Aspect 注解(表示这是一个切面类) |
2 | 类上加 @Component ,让 Spring 扫描到它 |
3 | 在类中定义切入点 + 通知(@Pointcut + @Before / @After等) |
Spring 框架一般都是基于 AspectJ 实现 AOP 操作
什么是 AspectJ?
AspectJ 不是 Spring 组成部分,独立 AOP 框架,一般把 AspectJ 和 Spring 框架一起使用,进行 AOP 操作。
基于 AspectJ 实现 AOP 操作
(1)基于 xml 配置文件实现
(2)基于注解方式实现(使用)
org.springframework
spring-context
5.3.33
org.springframework
spring-aspects
5.3.33
作用:知道对哪个类里面的哪个方法进行增强。
语法的结构:excution([权限修饰符][返回值类型][类的全类名][方法名称][(参数列表)])
示例1:对com.wsbazinga.dao.BookDao类里面的add进行增强。
excution(* com.wsbazinga.dao.BookDao.add(..))
示例1:对com.wsbazinga.dao.BookDao类里面的所有方法进行增强。
excution(* com.wsbazinga.dao.BookDao.*(..))
示例1:对com.wsbazinga.dao包里面的所有类,以及类里面的所有方法进行增强。
excution(* com.wsbazinga.dao.*.*(..))
1、创建普通类,定义方法
目标类(User.java
)
@Component(value = "userPr")
public class User {
public void add(){
System.out.println("User add method.....");
}
}
2、创建增强类,编写增强逻辑
增强类(UserProxy.java
)
@Component // 注册为 Spring 管理的 Bean
@Aspect // 声明为切面类
public class UserProxy {
// 前置通知
@Before("execution(* com.example.User.add(..))")
public void before() {
System.out.println("before......");
}
}
3、 Spring 配置文件(applicationContext.xml
)
添加aop命名空间;
添加:开启注解扫描;开启基于注解的 AOP 自动代理
4、测试类
@Test
public void testXmlAnnotation(){
// 加载spring配置文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = applicationContext.getBean("userPr", User.class);
user.add();
}
输出结果:
// 增强类
@Aspect
@Component
public class UserProxy {
// 前置通知
@Before("execution(* aopAnnotation.User.add(..))")
public void before(){
System.out.println("before......");
}
// 最终通知
@After("execution(* aopAnnotation.User.add(..))")
public void after(){
System.out.println("after......");
}
// 返回通知
@AfterReturning("execution(* aopAnnotation.User.add(..))")
public void afterReturning(){
System.out.println("afterReturning......");
}
@AfterThrowing("execution(* aopAnnotation.User.add(..))")
public void afterThrowing(){
System.out.println("afterThrowing......");
}
@Around("execution(* aopAnnotation.User.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕执行前......");
// 被增强的方法
proceedingJoinPoint.proceed();
System.out.println("环绕执行后......");
}
}
通知类型 | 注解 | 说明 |
---|---|---|
前置通知 | @Before |
在目标方法执行之前执行 |
环绕通知 | @Around |
包围目标方法,最强大的通知,可控制是否执行目标方法 |
返回通知(成功) | @AfterReturning |
在目标方法成功执行之后执行 |
异常通知(抛异常时) | @AfterThrowing |
在目标方法抛出异常时执行 |
最终通知(无论成功或异常) | @After |
在方法执行结束后一定执行(类似 finally) |
1. @Around(环绕通知 - 前)
2. @Before(前置通知)
3. —— 执行目标方法(User.add()) ——
4. @AfterReturning(返回通知)
5. @After(最终通知)
6. @Around(环绕通知 - 后)
如果目标方法抛出异常,比如:
public void add() {
System.out.println("add...");
int a = 1 / 0; // 模拟异常
}
此时的执行顺序变为:
1. @Around(环绕通知 - 前)
2. @Before(前置通知)
3. —— 执行目标方法(报错) ——
4. @AfterThrowing(异常通知)
5. @After(最终通知)
6. @Around(环绕通知 - 后) (注意是否还继续执行由异常处理方式决定)
情况 | 执行顺序 |
---|---|
正常返回 | Around前 → Before → 目标方法 → AfterReturning → After → Around后 |
异常抛出 | Around前 → Before → 目标方法 → AfterThrowing → After → Around后 |
@Around
的 前后行为@Around
的本质是你包裹整个方法执行的代码,比如:
@Around("execution(* aopAnnotation.User.add(..))")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前");
pjp.proceed(); // 方法执行处,可能会抛异常
System.out.println("环绕后"); // ⚠️ 如果上面抛出异常,这里不会执行
}
所以当 proceed()
抛异常时:
proceed()
后面的代码不会执行;
除非你自己用 try-catch 把异常捕获住:
正确写法(如果你想环绕后也执行):
@Around("execution(* aopAnnotation.User.add(..))")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前");
try {
pjp.proceed(); // 目标方法执行
} catch (Throwable e) {
System.out.println("捕获异常:" + e.getMessage());
throw e; // 继续抛出,供 @AfterThrowing 使用
}
System.out.println("环绕后"); // ✅ 只有在不抛出或被 catch 时才会执行
}
这是 Spring AOP 中一个 代码复用 + 可读性提升 的技巧。
在实际开发中,你可能会在多个通知上使用完全相同的切入点表达式,例如:
@Before("execution(* com.example.User.add(..))")
public void before() { ... }
@After("execution(* com.example.User.add(..))")
public void after() { ... }
@AfterReturning("execution(* com.example.User.add(..))")
public void afterReturning() { ... }
这样写虽然功能没问题,但重复太多,如果某天方法签名改了,你要到多个地方同步修改,非常麻烦。
Spring AOP 允许你使用 @Pointcut
注解来将切入点表达式提取成一个方法,然后在通知里复用它。
示例:切入点抽取的写法
@Aspect
@Component
public class UserProxy {
// ① 抽取切入点表达式:不需要方法体
@Pointcut("execution(* com.example.User.add(..))")
public void userAddPointcut() {}
// ② 使用切入点方法名作为引用
@Before("userAddPointcut()")
public void before() {
System.out.println("before...");
}
@After("userAddPointcut()")
public void after() {
System.out.println("after...");
}
@AfterReturning("userAddPointcut()")
public void afterReturning() {
System.out.println("afterReturning...");
}
@AfterThrowing("userAddPointcut()")
public void afterThrowing() {
System.out.println("afterThrowing...");
}
@Around("userAddPointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前...");
joinPoint.proceed();
System.out.println("环绕后...");
}
}
@Pointcut("execution(* com.example.User.add(..))")
public void addPointcut() {}
@Pointcut("execution(* com.example.User.update(..))")
public void updatePointcut() {}
@Pointcut("addPointcut() || updatePointcut()")
public void addOrUpdatePointcut() {}
相同切入点的抽取,就是使用
@Pointcut
注解将切入点表达式封装为方法,然后在多个通知中统一引用,以提高代码复用性和可维护性。
在 Spring AOP 中,多个增强类(切面)对同一个方法进行增强时,是有执行顺序的,并且这个顺序可以通过 设置优先级 来明确控制。
如果你有多个
@Aspect
类,Spring 默认不会保证它们的执行顺序。
例如:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.User.add(..))")
public void log() {
System.out.println("日志记录...");
}
}
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.User.add(..))")
public void check() {
System.out.println("权限检查...");
}
}
执行顺序是不确定的,可能先日志,也可能先权限。
Spring 提供了使用 @Order
注解的方式来设置切面的执行顺序:
@Aspect
@Component
@Order(1) // 数字越小,优先级越高,最先执行
public class LogAspect {
@Before("execution(* com.example.User.add(..))")
public void log() {
System.out.println("日志记录...");
}
}
@Aspect
@Component
@Order(2)
public class SecurityAspect {
@Before("execution(* com.example.User.add(..))")
public void check() {
System.out.println("权限检查...");
}
}
执行顺序:
@Order(1)
→@Order(2)
→ 目标方法执行
即:
日志记录...
权限检查...
User add method...
事项 | 说明 |
---|---|
数字越小越先执行 | 比如 @Order(1) 先于 @Order(5) |
@Around 特别重要 |
因为它包裹整个流程,外层的 @Around 最先执行、最后结束 |
假设你有两个切面都有 @Around
,并都设置了 @Order
:
@Aspect
@Order(1)
public class OuterAspect {
@Around("execution(* com.example.User.add(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Outer 环绕前");
Object result = pjp.proceed();
System.out.println("Outer 环绕后");
return result;
}
}
@Aspect
@Order(2)
public class InnerAspect {
@Around("execution(* com.example.User.add(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Inner 环绕前");
Object result = pjp.proceed();
System.out.println("Inner 环绕后");
return result;
}
}
输出顺序为:
Outer 环绕前
Inner 环绕前
User add method...
Inner 环绕后
Outer 环绕后
多个切面对同一方法增强时,可以用
@Order
设置优先级。数字越小,越先执行。如果有@Around
,它最先进入、最后退出,像一层层的“洋葱”。
这段 XML 的功能是:
对
com.atguigu.spring5.aopxml.Book
类的buy()
方法,执行一个“前置通知”,该通知由BookProxy
类中的before()
方法实现。
完全注解方式指的是:
不使用
applicationContext.xml
,所有的配置都通过@Configuration
、@Component
、@Aspect
、@EnableAspectJAutoProxy
等注解完成。
注解 | 作用 |
---|---|
@Configuration |
声明一个 Java 配置类,相当于 XML 配置 |
@ComponentScan |
扫描指定包下的类,注册为 Bean |
@EnableAspectJAutoProxy |
开启 Spring 的 AOP 支持 |
@Aspect |
声明一个切面类 |
@Before / @After / @Around / @AfterReturning / @AfterThrowing |
声明各种类型的通知 |
【注意】:
所有配置类你只需要加上
@Configuration
即可,不需要再加@Component
,因为:
@Configuration
⊂@Component
:它本身就是一种特殊的@Component
。Spring 会自动把
@Configuration
注解的类注册为 Bean。如果这个类被扫描到了,Spring 就会自动识别它是配置类并处理其中的
@Bean
方法。只要
配置类
在你的@ComponentScan
范围内,它就能被自动加载。
我们写一个示例,对 UserService.add()
方法进行 AOP 增强。
1、目标类:UserService.java
package com.example.service;
import org.springframework.stereotype.Component;
@Component
public class UserService {
public void add() {
System.out.println("UserService add() method...");
}
}
2、切面类:LogAspect.java
package com.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.UserService.add(..))")
public void before() {
System.out.println("前置通知:before...");
}
@After("execution(* com.example.service.UserService.add(..))")
public void after() {
System.out.println("最终通知:after...");
}
@AfterReturning("execution(* com.example.service.UserService.add(..))")
public void afterReturning() {
System.out.println("返回通知:afterReturning...");
}
@AfterThrowing("execution(* com.example.service.UserService.add(..))")
public void afterThrowing() {
System.out.println("异常通知:afterThrowing...");
}
@Around("execution(* com.example.service.UserService.add(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前...");
Object result = pjp.proceed(); // 调用目标方法
System.out.println("环绕后...");
return result;
}
}
3、配置类:AppConfig.java
package com.example.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration // 声明配置类
@ComponentScan("com.example") // 扫描所有组件(含 service 和 aspect)
@EnableAspectJAutoProxy // 开启 AOP 注解自动代理
public class AppConfig {
}
4、测试类:MainApp.java
package com.example;
import com.example.config.AppConfig;
import com.example.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
userService.add(); // 执行方法,触发 AOP
context.close();
}
}
环绕前...
前置通知:before...
UserService add() method...
返回通知:afterReturning...
最终通知:after...
环绕后...
如果在 add()
方法中抛异常:
public void add() {
System.out.println("UserService add() method...");
int a = 1 / 0; // 模拟异常
}
输出变为:
环绕前...
前置通知:before...
UserService add() method...
异常通知:afterThrowing...
最终通知:after...
环绕后... (是否输出视是否捕获异常而定)
步骤 | 内容 |
---|---|
1 | 用 @Component 注解目标类和切面类 |
2 | 切面类加上 @Aspect 注解并写好各种通知方法 |
3 | 写一个 @Configuration 配置类,开启组件扫描和 AOP 功能 |
4 | 使用 AnnotationConfigApplicationContext 启动上下文 |