@Aspect
定义与作用
@Aspect 是 AspectJ 框架定义的注解,被 Spring AOP 借用,用于声明一个类为切面类(Aspect Class)。被 @Aspect 标注的类可以包含 Pointcut 定义和 Advice 定义,是 AOP 编程的"载体"。
需要特别注意:@Aspect 本身不具备 Spring 组件扫描能力。也就是说,仅有 @Aspect 而没有 @Component、@Service 等 Spring 组件注解时,Spring 容器不会将该类识别为 Bean,AOP 功能自然也无法生效。
适用位置与常用属性
适用位置
@Aspect 只能标注在类上。一个典型的切面类结构如下:
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.feixiang.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
// 前置通知逻辑
}
}
常用属性
@Aspect 注解本身没有属性,它是一个纯粹的标记注解(Marker Annotation)。其元注解包含 @Retention(RetentionPolicy.RUNTIME) 和 @Target(ElementType.TYPE),确保在运行期通过反射识别。
| 特性 | 说明 |
|---|---|
| 属性数量 | 无 |
| 元注解 | @Retention(RUNTIME)、@Target(TYPE) |
| 必须配合 | @Component 或 @Service 等 Spring 组件注解(否则不被容器管理) |
| 必须配合 | @EnableAspectJAutoProxy(否则不创建代理) |
核心原理
@Aspect 切面类在 Spring 容器中的识别流程
当 Spring 容器启动并扫描组件时,AnnotationAwareAspectJAutoProxyCreator 会专门识别带有 @Aspect 注解的 Bean,解析其中的 @Pointcut 和 Advice 注解,生成对应的 Advisor 对象:
切面类的内部结构
一个标准的 @Aspect 切面类通常包含以下成员:
设计规范:
@Pointcut方法通常声明为public void,方法名即切点名,供其他 Advice 引用- Advice 方法(
@Before、@After等)可以是public或private,但建议统一为public - 切面类本身可以注入其他 Spring Bean,例如注入
HttpServletRequest获取请求信息
完整示例:飞翔科技学生管理系统权限校验切面
场景简述
广州飞翔科技公司的产品经理孔蓝提出:学生成绩管理系统中,"删除学生成绩"和"修改成绩"操作只能由教师角色执行,学生只能查询自己的成绩。后端开发小崔决定使用 AOP 实现统一的权限校验,将权限逻辑从业务代码中剥离。
架构师白歌 review 代码时强调:切面类必须同时标注 @Aspect 和 @Component,否则 Spring 容器不会识别。
操作前代码/配置
在未使用 @Aspect 之前,小崔在每个 Service 方法中手动编写权限校验:
@Service
public class StudentScoreService {
public void deleteScore(Long studentId) {
// 重复!每个方法都要写权限校验
if (!getCurrentUserRole().equals("TEACHER")) {
throw new AccessDeniedException("只有教师可以删除成绩");
}
// 业务逻辑
scoreDao.deleteByStudentId(studentId);
}
public void updateScore(Long studentId, Double newScore) {
// 重复!权限校验代码再次出现在业务方法中
if (!getCurrentUserRole().equals("TEACHER")) {
throw new AccessDeniedException("只有教师可以修改成绩");
}
// 业务逻辑
scoreDao.updateScore(studentId, newScore);
}
public Score queryScore(Long studentId) {
// 查询权限不同:学生只能查自己
String currentUser = getCurrentUserId();
String role = getCurrentUserRole();
if (role.equals("STUDENT") && !currentUser.equals(String.valueOf(studentId))) {
throw new AccessDeniedException("只能查询自己的成绩");
}
return scoreDao.findByStudentId(studentId);
}
private String getCurrentUserRole() {
// 模拟获取当前用户角色
return SecurityContextHolder.getContext().getAuthentication().getAuthorities()
.iterator().next().getAuthority();
}
private String getCurrentUserId() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}
问题:权限校验代码散落在各个业务方法中,业务逻辑被淹没。如果孔蓝后续要求增加"记录操作日志",每个方法又要再次修改。
使用该注解的完整代码
第一步:定义自定义权限注解(用于 Pointcut 匹配)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireTeacher {
}
第二步:使用 @Aspect 定义权限校验切面
@Aspect
@Component
public class PermissionAspect {
// 切点:所有标注了 @RequireTeacher 的方法
@Pointcut("@annotation(com.feixiang.student.annotation.RequireTeacher)")
public void teacherOperation() {}
// 切点:所有查询方法(方法名以 query 或 get 开头)
@Pointcut("execution(* com.feixiang.student.service.*.query*(..)) || " +
"execution(* com.feixiang.student.service.*.get*(..))")
public void queryOperation() {}
// 教师操作权限校验
@Before("teacherOperation()")
public void checkTeacherRole(JoinPoint joinPoint) {
String role = getCurrentUserRole();
if (!"TEACHER".equals(role)) {
String methodName = joinPoint.getSignature().getName();
throw new AccessDeniedException(
"[飞翔科技-权限] 方法 " + methodName + " 需要教师权限,当前角色:" + role);
}
System.out.println("[飞翔科技-权限] 教师权限校验通过");
}
// 学生查询权限校验:只能查自己
@Before("queryOperation() && args(studentId)")
public void checkStudentQuery(JoinPoint joinPoint, Long studentId) {
String role = getCurrentUserRole();
String userId = getCurrentUserId();
if ("STUDENT".equals(role) && !userId.equals(String.valueOf(studentId))) {
throw new AccessDeniedException(
"[飞翔科技-权限] 学生只能查询自己的成绩,尝试查询学号:" + studentId);
}
System.out.println("[飞翔科技-权限] 查询权限校验通过");
}
private String getCurrentUserRole() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getAuthorities().iterator().next().getAuthority() : "ANONYMOUS";
}
private String getCurrentUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : "unknown";
}
}
第三步:纯净的业务 Service
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
@RequireTeacher
public void deleteScore(Long studentId) {
// 纯粹的业务逻辑,无任何权限校验代码
scoreDao.deleteByStudentId(studentId);
System.out.println("[业务] 已删除学生 " + studentId + " 的成绩");
}
@RequireTeacher
public void updateScore(Long studentId, Double newScore) {
// 纯粹的业务逻辑
scoreDao.updateScore(studentId, newScore);
System.out.println("[业务] 已更新学生 " + studentId + " 的成绩为 " + newScore);
}
public Score queryScore(Long studentId) {
// 纯粹的业务逻辑,权限校验由 AOP 统一处理
return scoreDao.findByStudentId(studentId);
}
}
第四步:配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
操作后运行结果及分析
测试代码:
public class AspectDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
// 模拟教师登录
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("teacher01", null,
Collections.singletonList(new SimpleGrantedAuthority("TEACHER")))
);
service.updateScore(2024001L, 92.5);
ctx.close();
}
}
控制台输出:
[飞翔科技-权限] 教师权限校验通过
[业务] 已更新学生 2024001 的成绩为 92.5
分析:
@Aspect将PermissionAspect声明为切面类,Spring 容器识别并解析其中的@Pointcut和@Before@Component确保切面类被 Spring 容器管理,成为可注入的 Bean- 业务方法上的
@RequireTeacher注解与@Pointcut("@annotation(...)")配合,实现了"声明式权限控制" - 如果删除
@Aspect,PermissionAspect退化为普通 Bean,权限校验逻辑不再自动织入到目标方法中
易错场景与面试考点
易错场景一:只加 @Aspect 不加 @Component 导致切面不生效
反例:
@Aspect
// 错误!缺少 @Component,Spring 不会扫描此类
public class PermissionAspect {
@Before("execution(* com.feixiang.student.service.*.*(..))")
public void check() {
System.out.println("权限校验");
}
}
现象:应用正常启动,没有任何报错,但 Advice 始终不执行。
原因:@Aspect 是 AspectJ 的注解,不是 Spring 的组件注解。Spring 的组件扫描只识别 @Component、@Service、@Repository、@Controller 及其派生注解。没有这些注解,类不会被注册为 Bean,AnnotationAwareAspectJAutoProxyCreator 也就无法发现它。
正确做法:
@Aspect
@Component // 必须添加!
public class PermissionAspect {
}
易错场景二:@Aspect 类中的 @Pointcut 方法被误调用
反例:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.feixiang.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore() {
// 正确:通过切点名引用
}
}
有些初学者误以为 @Pointcut 方法可以像普通方法一样调用:
// 错误!不要在代码中直接调用 @Pointcut 方法
logAspect.serviceLayer(); // 这是一个空方法,没有任何意义
理解:@Pointcut 方法的本质是"载体"——Spring 在解析切面时读取方法上的 @Pointcut 注解中的表达式字符串,方法体本身不会被调用。方法名仅用于在 Advice 注解中引用(如 @Before("serviceLayer()"))。
面试高频考点
考点一:@Aspect 注解来自哪个框架?Spring 能独立使用它吗?
@Aspect来自 AspectJ 框架(org.aspectj.lang.annotation.Aspect)。Spring AOP 借用了 AspectJ 的注解语法,但运行时仍基于 Spring 自己的代理机制实现,而非 AspectJ 的编译期/加载期织入。因此 Spring 可以"独立使用"这些注解,但底层实现与原生 AspectJ 不同。
考点二:为什么 @Aspect 必须配合 @Component 使用?
因为
@Aspect是 AspectJ 的标记注解,不具备 Spring 组件扫描语义。Spring 容器通过组件扫描注册 Bean,只有被注册为 Bean 的类才能被AnnotationAwareAspectJAutoProxyCreator识别为切面。没有@Component,类不会被注册为 Bean,AOP 功能自然失效。
考点三:一个项目中可以有多个 @Aspect 类吗?它们的执行顺序如何控制?
可以。多个
@Aspect类同时作用于同一连接点时,默认按照 Bean 名称的字母顺序执行。可以通过@Order注解(或实现Ordered接口)精确控制顺序,数值越小优先级越高。例如@Order(1)的切面先于@Order(2)执行。