乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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) 执行。

上一页
@EnableAspectJAutoProxy
下一页
@Pointcut