目录
1.什么是AOP?
1.1基本概念
1.2具体应用
2.AOP是怎么怎么实现的?
2.1静态代理
2.2动态代理
2.2.1cglib 动态代理
2.2.2 JDK 动态代理
3.AOP中的核心概念
4.AOP具体实现(权限校验)
1.详细版
2.精简版
5总结
大家好!今天咱们来聊聊Spring框架中一个非常核心且实用的特性——AOP。很多小伙伴刚接触AOP时会觉得抽象,比如“什么是切面?”“动态代理到底是啥?”“实际开发中我该怎么用?”。这篇文章就从基础概念到核心原理,再到实际应用,一步步把Spring AOP讲明白,小白也能轻松看懂~,发车咯~
使用 Spring 的面向切面编程 :: Spring 框架 - Spring 框架(官网地址)
1.什么是AOP?
1.1基本概念
英文名叫Aspect-Oriented Programming,就是面向切面编程嘛!,将冗余代码抽离出来,组成一个切面,想把刀子一样插入到原来的代码中,是为了让我们尽量少的写代码的。有个叫AspectJ的框架的东西,可以将属性、方法、代码块批量增强。AOP就借鉴到了AspecJ框架。
1.2具体应用
在许多情况下,我们需要对用户操作进行权限校验,因为在有的场景下只有系统中管理员才有权限操作的,我们就假设在我们的系统中新增用户、更新用户需要管理员权限。
在下图中新增用户、更新用户这两个方法里,都重复写了 “用户权限校验” 的代码 —— 如果有 10 个类似的方法,就要写 10 遍权限校验,既冗余又难维护。
这时天使宝宝AOP来了, 把 “用户权限校验” 抽成一个独立的切面,通过 AOP 动态 “切入” 到新增、更新方法的执行流程中。
总结一下:AOP将那些与业务逻辑无关,但多个业务模块都需要的公共功能(比如日志、权限、事务),抽取出来单独维护,然后在不修改原有业务代码的前提下,动态地切入到业务方法的指定位置。
简单说,AOP就是“抽离公共代码,动态植入业务”,实现代码解耦和复用。
2.AOP是怎么怎么实现的?
AOP是通过代理来实现的,想象成一个小助理可以代理我们的类来执行对应的代码逻辑。代理也分为静态代理和动态代理两种。
2.1静态代理
静态代理,静态代理是通过代码运行前,通过修改class文件来完成代理,功能强大,较为复杂,运行效率比动态代理块。静态代理分为,编译前,编译后,加载时三种。这三种作为了解即可。
- 编译前:在代码编写完成后、编译为.class 文件前,手动编写代理类并关联目标类,代理逻辑与目标逻辑在编码阶段已绑定。
- 编译后:通过字节码修改已编译生成的.class 文件,将代理逻辑织入目标类的字节码中。
- 加载时:在类加载器将.class 文件加载到JVM的过程中,通过自定义类加载器拦截加载流程,动态修改类字节码以植入代理逻辑。
2.2动态代理
动态代理分为两种分为cglib和jdk两种 Spring 框架会根据目标类是否实现接口,自动选择Jdk或cglib动态代理
2.2.1cglib 动态代理
- 基于字节码生成目标类的子类,可代理未实现接口的类;
- 依赖第三方库(需引入 cglib 包),但不支持代理 final 类 / 方法。
2.2.2 JDK 动态代理
- 基于接口实现,仅能代理实现了接口的类;
- 利用 Java反射机制生成代理对象,无需额外依赖。
3.AOP中的核心概念
这些概念先在脑子里留个印象就信,我们等下将一些概念实际应用~~~
1.切面(Aspect):抽离出来的公共功能模块(比如“日志切面”“权限切面”)。一个切面里包含了“要做什么”(通知)和“在哪里做”(切入点)。
2.通知(Advice):切面里具体要执行的逻辑(比如“记录接口入参”“记录接口耗时”)。Spring提供了5种通知类型,对应不同的切入时机:
- 前置通知(Before):业务方法执行前执行(比如记录请求开始时间)
- 后置通知(After):业务方法执行后执行(无论成功还是失败,比如记录请求结束)
- 返回通知(AfterReturning):业务方法成功执行后执行(比如记录接口返回结果)
- 异常通知(AfterThrowing):业务方法抛出异常时执行(比如记录异常信息)
- 环绕通知(Around):包裹业务方法,在方法执行前后都能执行(功能最强,比如统计接口耗时)
3.切入点(Pointcut):定义“哪些业务方法需要被切入”。切入点通常用表达式来描述。
4.连接点(JoinPoint):程序运行中可能被切入的(比如方法执行前、执行后、抛出异常时)。每个切入点对应的都是多个连接点。
5.目标对象(Target):被切入的业务对象(比如咱们的UserController、OrderService)。
6.代理对象(Proxy):Spring AOP通过动态代理技术,为目标对象创建的代理对象。实际调用时,咱们调用的是代理对象,代理对象会先执行切面逻辑,再执行目标对象的业务方法。
7.织入(Weaving):将切面逻辑动态植入到目标对象方法中的过程(Spring在运行时完成织入)。
其中,通知(Advice)是切面的核心逻辑,Spring 提供了 5 种通知类型,对应不同的切入时机,实战中最常用的是环绕通知。
4.AOP具体实现(权限校验)
了解了 AOP 的原理和核心概念后,咱们来做一个企业级实战案例—— 用 AOP 实现接口的权限校验,这是项目中非常非常非常常用的功能。我们分为详细版和精简版实现,可以选择食用!!!
实战需求
- 定义一个权限校验注解 @AuthCheck,可指定方法所需的角色(如 admin、user);
- 用 AOP 环绕通知拦截所有标注了 @AuthCheck 的方法;
- 校验逻辑:未登录 → 抛未授权异常;已登录但角色不匹配 → 抛无权限异常;角色匹配 → 执行目标方法。
1.详细版
1.1 创建自定义权限注解 @AuthCheck
注解用于标记需要进行权限校验的方法,指定方法所需的角色。
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限校验注解 * 用于标记需要进行角色权限校验的方法,仅作用于方法级别 */ @Target(ElementType.METHOD) // 注解仅能作用于方法上 @Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留,可通过反射获取 public @interface AuthCheck { /** * 方法执行所需的必选角色标识 * 示例:"admin" 表示需要管理员角色,"user" 表示需要普通用户角色 * 默认值为空字符串,表示无需特定角色(仅需登录即可) * @return 角色标识字符串 */ String mustRole() default ""; }1.2 创建自定义异常类
定义未授权(未登录)和无权限(角色不匹配)的自定义异常,用于权限校验失败时抛出。
/** * 未授权异常(对应HTTP 401状态码) * 用于用户未登录时抛出 */ public class UnauthorizedException extends RuntimeException { public UnauthorizedException(String message) { super(message); } } /** * 禁止访问异常(对应HTTP 403状态码) * 用于用户已登录但角色不匹配时抛出 */ public class ForbiddenException extends RuntimeException { public ForbiddenException(String message) { super(message); } }1.3 创建角色枚举和常量(可选)
为了规范角色标识,避免硬编码,可创建角色枚举或常量类。
/** * 角色枚举类 */ public enum UserRoleEnum { ADMIN("admin", "管理员"), USER("user", "普通用户"); private final String value; private final String desc; UserRoleEnum(String value, String desc) { this.value = value; this.desc = desc; } public String getValue() { return value; } public String getDesc() { return desc; } } /** * 角色常量类(也可直接使用枚举) */ public class UserConstant { public static final String ADMIN_ROLE = "admin"; public static final String USER_ROLE = "user"; }1.4 实现 AOP 权限拦截器
创建切面类,使用环绕通知实现权限校验逻辑,核心流程:
- 从请求中获取当前登录用户;
- 校验用户是否已登录;
- 从 @AuthCheck 注解中获取所需角色;
- 校验用户角色是否匹配;
- 角色匹配则执行目标方法,不匹配则抛出异常。
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; /** * 权限校验AOP拦截器 * 拦截所有标注@AuthCheck注解的方法,执行角色权限校验 */ @Aspect // 标识该类为AOP切面类 @Component // 交给Spring容器管理,使切面生效 public class AuthInterceptor { /** * 注入用户服务,用于获取当前登录用户信息 * 实际项目中,UserService需实现从请求中解析登录用户(如通过Token解析) */ @Resource private UserService userService; /** * 环绕通知:拦截所有标注@AuthCheck注解的方法 * @param joinPoint 切入点对象,可获取目标方法信息、执行目标方法 * @param authCheck 目标方法上的@AuthCheck注解实例,用于获取所需角色 * @return 目标方法的执行结果 * @throws Throwable 目标方法执行过程中抛出的异常 */ @Around("@annotation(authCheck)") // 切入点表达式:拦截所有标注@AuthCheck注解的方法 public Object doIntercept(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { // 1. 获取注解配置的必选角色 String mustRole = authCheck.mustRole(); // 2. 获取当前HTTP请求对象(从请求上下文获取) ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); // 3. 从请求中获取当前登录用户(实际项目中需通过Token、Session等方式解析) User loginUser = userService.getLoginUser(request); // 4. 基础校验:未登录则抛出未授权异常 if (loginUser == null) { throw new UnauthorizedException("未登录,无法执行该操作"); } // 5. 无指定必选角色:仅需登录即可,直接执行目标方法 if ("".equals(mustRole)) { return joinPoint.proceed(); // 执行目标方法 } // 6. 管理员角色校验:如果注解指定角色为admin,需校验用户是否为管理员 if (UserRoleEnum.ADMIN.getValue().equals(mustRole)) { if (!isAdmin(loginUser)) { throw new ForbiddenException("无管理员权限,禁止执行该操作"); } return joinPoint.proceed(); // 权限通过,执行目标方法 } // 7. 普通角色校验:当前用户角色需与注解指定角色完全匹配 String userRole = loginUser.getUserRole(); if (!mustRole.equals(userRole)) { throw new ForbiddenException(String.format("无[%s]角色权限,禁止执行该操作", mustRole)); } // 8. 所有校验通过,执行目标方法并返回结果 return joinPoint.proceed(); } /** * 辅助方法:判断当前登录用户是否为管理员 * @param user 登录用户对象 * @return true-管理员,false-非管理员 */ private boolean isAdmin(User user) { return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole()); } }1.5 在业务方法中使用注解
在需要进行权限校验的业务方法上标注 @AuthCheck 注解,并指定所需角色即可。
import org.springframework.stereotype.Service; /** * 用户业务服务类 */ @Service public class UserService { /** * 新增用户(仅管理员可执行) * 标注@AuthCheck,指定必须有admin角色 */ @AuthCheck(mustRole = UserRoleEnum.ADMIN.getValue()) public void addUser(User user) { // 核心业务逻辑:新增用户到数据库 userMapper.insert(user); } /** * 更新用户信息(普通用户可执行) * 标注@AuthCheck,指定必须有user角色 */ @AuthCheck(mustRole = UserRoleEnum.USER.getValue()) public void updateUser(User user) { // 核心业务逻辑:更新用户信息 userMapper.updateById(user); } /** * 查询用户信息(仅需登录,无特定角色要求) * 标注@AuthCheck,不指定mustRole(默认空字符串) */ @AuthCheck public User getUserById(Long userId) { // 核心业务逻辑:查询用户信息 return userMapper.selectById(userId); } // 其他方法... }2.精简版
2.1编写权限校验注解创建一个接口
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限校验注解 * 用于标记需要进行角色权限校验的方法,仅作用于方法级别 */ @Target(ElementType.METHOD) // 注解仅能作用于方法上 @Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留,可通过反射获取 public @interface AuthCheck { /** * 方法执行所需的必选角色标识 * 示例:"admin" 表示需要管理员角色,"user" 表示需要普通用户角色 * 默认值为空字符串,表示无需特定角色(仅需登录即可) * @return 角色标识字符串 */ String mustRole() default ""; }2.2编写校验AOP,采取环绕通知。
@Aspect @Component public class AuthInterceptor { @Resource private UserService userService; /** * 执行拦截 * * @param joinPoint 切入点 * @param authCheck 权限校验注解 */ @Around("@annotation(authChek)") //@annotation(...) 表示 “拦截所有被括号中指定注解标记的方法”。 public Object doIntercept(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { //逻辑校验,若校验失败则抛出异常 // //代码省略........... // return joinPoint.proceed(); } }2.3在想要校验的方法上添加注解
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)5总结
Spring AOP是一种非常强大的编程思想,核心是通过代理将公共功能抽离为切面,动态植入业务方法,实现解耦和代码复用。
核心要点回顾
- AOP 的核心思想:抽离公共代码,动态植入业务
- 底层实现:静态代理(AspectJ)和动态代理(JDK/CGLIB),Spring AOP默认使用动态代理
- 核心概念:切面、通知、切入点、连接点、目标对象、代理对象、织入
- 实战关键:通过自定义注解 + 环绕通知,可快速实现权限校验、日志记录等功能