Spring Boot 框架相关注解

@Aspect注解的使用?如何定义切面和通知?结合 Spring 框架聊聊

@Aspect注解是 Spring AOP 中定义切面类的核心注解,需要配合@Component注册为 Bean。一个标准的切面类需要同时使用@Aspect和@Component两个注解

Spring AOP 基于代理机制,所以只有通过 Spring 容器调用的方法才能被拦截,这也是为什么我们要确保被拦截的类要注册为 Spring Bean 的原因。

切面定义通过在类上添加@Aspect实现,切点定义使用@Pointcut注解指定拦截规则,比如@Pointcut("execution(* com.example.service.*.*(..))")表示拦截 service 包下所有方法。

谈到 @Aspect 时,你需要明确 Spring AOP 和 AspectJ 的关系:Spring AOP 实际上借鉴了 AspectJ 的注解体系,包括 @Aspect、@Pointcut、@Before 等注解都来自 AspectJ,但底层实现机制不同。Spring AOP 基于动态代理实现,只能拦截 Spring 管理的 Bean 的方法调用,而 AspectJ 是通过字节码织入实现的,功能更强大但也更复杂。在日常开发中,Spring AOP 已经能满足大部分场景需求。

通知类型包括五种:@Before在目标方法执行前运行,常用于参数校验;@After在方法执行后运行,用于资源清理;@AfterReturning在方法正常返回后执行,可获取返回值进行后置处理;@AfterThrowing在方法抛异常时执行,用于异常日志记录;@Around环绕整个方法执行,最为灵活,常用于性能监控和事务控制。

Spring 会在运行时为被拦截的 Bean 创建代理对象,当调用目标方法时实际执行的是代理逻辑,从而实现横切关注点的分离。记住切面只对 Spring 容器管理的 Bean 生效,直接 new 的对象无法被拦截。

对于多个切面可以进行执行顺序控制,当有多个切面作用于同一个方法时,Spring通过@Order注解来控制执行顺序。数值越小,优先级越高。但是对于这些注解并不是优先级高就一定先执行,例如对于@Before通知,Order值小的先执行;对于@After通知,Order值小的后执行

@ComponentScan注解的配置?包扫描的规则和过滤器?结合 Spring 框架聊聊?

@ComponentScan 是 Spring 框架中用于自动扫描和注册 Bean 的核心注解,它告诉 Spring 容器在哪些包路径下寻找标注了 @Component、@Service、@Repository、@Controller 等注解的类。

Spring 容器的自动配置,依赖注入什么的都需要依赖于 Bean 的发现机制,当容器启动时需要知道哪些类应该被管理为 Bean,而@ComponentScan就是告诉容器去哪里找

基本配置很简单,通过 basePackages 或 value 属性指定扫描路径,而且还能配置excludeFilters 用于排除不需要的类等灵活处理的情况。如果不指定路径,默认扫描当前配置类所在包及其子包。你也可以用 basePackageClasses 指定基准类,Spring 会扫描这些类所在的包,这种方式在微服务和重构包名时的情况不容易出错。

包扫描规则遵循递归原则,在Spring容器的 refresh() 方法执行过程中,会有一个专门的阶段处理组件扫描,会递归遍历指定包路径下的所有 class 文件,通过反射机制检查每个类上的注解,符合条件的类会被实例化并注册到 IoC 容器中。

@Configuration和@Component的区别?配置类的特殊性?结合 Spring 框架聊聊?

@Configuration和@Component都是 Spring 中 标识受容器管理的 Bean 的注解

@Component是通用的 Bean 标识注解,而@Configuration是专门为配置类设计的,承载了 Spring 容器初始化、Bean 定义注册、依赖注入的核心逻辑。

@Configuration是@Component的子类注解,它定义 Bean 的创建规则、管理 Bean 之间的依赖关系,是 Spring Java Config 的核心。

  1. @Component标记的类会被 Spring 容器管理,但方法调用遵循普通Java语义。

    @Configuration标记的类会被CGLIB代理增强,确保@Bean方法之间的调用返回同一个实例,维护Bean的单例特性。

  2. @Configuration作为配置类,支持@Import导入其他配置类,支持@PropertySource加载外部配置文件,支持@Enable*系列注解开启 Spring 特定功能;

    @Component不具备上述内容

  3. @Component仅能通过@Autowired/@Resource实现依赖注入,无法直接通过方法调用管理 Bean 依赖。

    @Configuration支持@Bean方法间的依赖传递,通过方法调用实现 Bean 依赖,无需手动注入。

简单说,@Component用于业务组件,@Configuration专门用于配置Bean的定义和装配,最重要的是区分这个

@Configuration注解的核心在于它会触发Spring的ConfigurationClassPostProcessor后置处理器。Spring容器在解析配置类时,会检测到@Configuration注解,然后使用CGLIB创建一个子类代理,这个代理类会拦截所有@Bean方法的调用。CGLIB代理不是简单的方法拦截,而是保证Bean实例的唯一性。代理的核心作用是确保当一个@Bean方法调用另一个@Bean方法时,返回的是Spring容器中已经存在的Bean实例,而不是重新创建。

在@Component类中定义了多个@Bean方法,并且方法间有调用关系。结果发现每次调用都创建了新的实例,导致数据库连接数异常飙升。后来改成@Configuration注解,问题立即解决。

在实际项目中,一般在三种场景下会选择@Configuration。第一是基础设施配置,比如数据库连接池、Redis客户端这些需要严格单例的组件。第二是第三方框架集成,像Swagger、定时任务框架的配置。第三是复杂的Bean依赖关系配置,特别是Bean之间有相互调用的场景。

@Profile注解的作用是什么?你都如何使用它?多环境配置下如何进行实践来进行环境隔离?

@Profile 注解是 Spring 框架中用于条件化加载 Bean 的一个注解,通过指定激活的环境配置来控制特定组件的实例化。

可以在类级别或方法级别使用它,比如 @Profile("dev") 或 @Profile({"prod", "staging"})。标注在 @Bean 方法上就是精准控制单个 Bean 的加载,标注在配置类上就是控制整个配置类的加载

在多环境配置的实际应用中,我通常会为不同数据源创建专门的配置类,开发环境使用 @Profile("dev") 配置开发用的数据库,生产环境用 @Profile("prod") 配置 MySQL 集群。

激活 Profile 的方式包括在 application.properties 中设置 spring.profiles.active=dev,或通过 JVM 参数 -Dspring.profiles.active=prod 启动应用,这样就能用 @Profile 隔离差异化 Bean

一般情况下,环境隔离需要通过多文件配置策略实现,将 application-dev.yml作为开发环境配置、application-prod.yml作为生产环境配置,等环境特定配置分离,主配置文件只保留公共配置。

对于复杂的微服务架构,可以组合使用多个 Profile,比如 @Profile("prod & !docker") 这样的表达式来处理更精细的环境区分。

Spring Boot 中都有哪些注入 Bean 的方式?@Autowired、@Resource、@Inject注解的区别?为什么 Spring 官方不推荐使用 @Autowired?

虽然我们常说 注解注入,但从底层机制上,Spring 主要支持以下三种方式来进行 DI:

  1. 构造器注入

    这是官方推荐的,它通过类的构造函数来明确依赖关系。通过 final 修饰,能够保证依赖不为空

    1
    2
    3
    4
    5
    6
    private final UserService userService;

    // 4.3+ 单构造器可省略@Autowired
    public UserController(UserService userService) {
    this.userService = userService;
    }
  2. 通过 setXxx 方法注入依赖。

    它比较灵活,允许依赖在之后被修改或重新注入。但是无法保证依赖在对象使用前已完全初始化,可能导致NPE

    1
    2
    3
    4
    5
    6
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
    this.userService = userService;
    }
  3. 字段注入

    直接在成员变量上注解

    1
    2
    @Autowired
    private UserService userService;

而 @Autowired、@Resource、@Inject 这三个注解都能实现自动装配 Bean,但是, @Autowired、@Resource、@Inject本身是 依赖注入的注解,不是 固定的某一种注入方式,只是日常开发中大家最常用在字段上。

它们之间的区别就是

注解 所属规范 匹配优先级 支持的注入形式 核心区别
@Autowired Spring 专属 优先类型(byType)→ 再按名称(byName) 字段 /setter/ 构造器 支持required=false、@Qualifier指定 Bean 名
@Resource JSR-250(Java 标准) 优先名称(byName)→ 再按类型(byType) 字段 /setter(不支持构造器) 可通过name属性指定 Bean 名
@Inject JSR-330(Java 标准) 优先类型(byType) 字段 /setter/ 构造器 需导入javax.inject依赖,支持@Named指定 Bean 名

详细的查找逻辑是这样的

  • @Autowired:

    • 首先根据 类型 (Type) 查找。

    • 如果找到多个同类型的 Bean,则尝试按 名称 (Name) 匹配。

    • 如果名称也匹配不上,且没有 @Qualifier,则报错。

  • @Resource:

    • 首先根据 名称 (Name) 查找(默认取字段名)。

    • 如果按名称没找到,则回退到按 类型 (Type) 查找。

    • 它的逻辑更符合 Java 原生语义,且与框架解耦。

为什么 Spring 官方不推荐使用 @Autowired 字段注入呢?明明只有它是 Spring 生态中的内容?难道考虑的是兼容性吗?只能说是不止

  1. 违反了单一职责原则

    使用字段注入太方便了,以至于开发者很容易在一个类里塞入 10 个甚至 20 个依赖,而不会觉得有什么问题。那么就可能出现了类违反单一职责,循环依赖或无用依赖的情况。如果是构造器注入,看到一个拥有 20 个参数的构造函数,开发者会立刻意识到这个类承载了太多职责,需要重构。

  2. 无法使用 final 修饰符

    字段注入是无法使用 final 修饰符的,因为字段注入是在对象实例化之后通过反射设置的。这破坏了 不可变对象 (Immutable Objects) 的设计模式,可能在高并发等情况下出现问题

  3. 与 Spring 容器过度耦合

    这是最致命的一点。字段注入必须依赖 Spring 的反射机制。如果你想在不启动 Spring 容器的情况下(如纯 JUnit 测试)实例化这个类,你必须手动编写反射代码来注入依赖,否则该字段永远是 null。

  4. 隐藏了循环依赖风险

    如果是构造器注入,存在循环依赖,Spring 在项目启动时就会抛出 BeanCurrentlyInCreationException,提醒你设计有问题。而字段注入因为反射不会在启动时候检查,而是等问题出现才报错。

既然官方不推荐,那为什么我看到的 demo 全是 @Autowired 字段注入?

  • 没办法,字段注入太简单了,以至于在实际开发中,它仍然主流
  • 但是,我们可以配合 Lombok 的 @RequiredArgsConstructor 注解,它能自动生成带 final 字段的构造函数,这样,使用构造器注入就很方便了

@Qualifier注解的作用?如何解决多个候选 Bean 的问题?结合你的实践聊聊?

首先,我们都知道,@Autowired默认按类型匹配进行注入,@Resource默认按名称匹配,而在实际开发中,如果你的项目可能迁移到其他容器或需要标准化,@Resource更具可移植性,所以说我们用的更多的都是@Resource

当 Spring 容器中存在多个同一类型的 Bean 实例时,依赖注入会触发NoUniqueBeanDefinitionException异常。但在实际开发中,同一类型存在多个 Bean 实例 的场景很常见(比如多数据源、多缓存实现、多支付渠道),例如,在项目中,我需要同时集成 PostgreSQL 和 MongoDB 两种数据源,且都实现了DataSource接口,此时容器中会存在两个DataSource类型的 Bean,直接注入就会冲突

除了@Resource加name属性,我们使用@Qualifier + @Autowired也能解决这个问题。

@Qualifier是 Spring 提供的 Bean 名称限定注解,在按类型匹配的基础上,进一步按 Bean 名称筛选,解决多候选 Bean 的歧义问题。

使用方式很直接,在@Autowired旁边添加@Qualifier("beanName")来指定具体的Bean名称。比如你有两个DataSource类型的Bean,一个叫primaryDataSource,另一个叫secondaryDataSource,注入时就用@Qualifier("primaryDataSource")明确告诉Spring要哪个。

如何理解@SpringBootApplication注解,为什么标注了这个注解的类能够作为主启动类?

@SpringBootApplication 是 Spring Boot 核心的组合注解,它整合了 3 个注解。

  • @ComponentScan做组件扫描,扫描当前包及子包下的 @Controller/@Service/@Component 等注解的类
  • @EnableAutoConfiguration开启自动配置,自动加载 Spring Boot 内置的默认配置
  • @SpringBootConfiguration也是标记该类为配置类,本质是 @Configuration

标注了该注解的类之所以能作为主启动类,是因为它完成了 开启自动配置、扫描组件、标记配置类 三大核心工作,是 Spring Boot 约定大于配置 思想的集中体现。

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 核心组合注解
@SpringBootConfiguration // 替代 @Configuration,标记该类为配置类
@EnableAutoConfiguration // 开启自动配置(最核心)
@ComponentScan // 扫描组件(Controller/Service/Mapper等)
public @interface SpringBootApplication { ... }

启动类的核心是 main 方法 + SpringApplication.run(),但真正让它成为 启动入口 的是 @SpringBootApplication 的三大能力:

  1. @ComponentScan 会从启动类所在包开始,递归扫描所有子包下的组件

    如果你的 Controller 放在启动类的上层包,会扫描不到,我被坑过,需要指定路径

  2. @EnableAutoConfiguration 会通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件中的自动配置类,自动配置类通过 @Conditional 注解实现按需生效,只需配置配置文件 yaml 中的相关信息,无需手动创建 Bean,就是自动配置的体现。

  3. @SpringBootConfiguration 让启动类具备配置类的能力,可直接在启动类中通过 @Bean 定义全局组件

执行SpringApplication.run(),进行的是 Spring Boot 项目启动那一套了

  1. 加载 @SpringBootApplication 注解的配置;
  2. 扫描组件并注册到 Spring 容器;
  3. 启动自动配置流程;
  4. 启动内嵌 Tomcat(如果引入 web 依赖);
  5. 初始化 Spring 上下文,项目启动完成。

Spring 中的 @Cacheable 和 @CacheEvict 注解的作用是什么?在缓存这一块你都用了哪些注解?

这两个注解都属于 Spring Cache 抽象的注解,通过 AOP 代理在方法调用前后自动操作缓存。

  • @Cacheable —— 读缓存,将运行结果缓存,缓存命中则跳过方法

    执行逻辑:方法调用前,根据 key 去缓存中查找。如果命中,直接返回缓存值,方法体不执行。未命中,执行方法体,将返回值写入缓存,再返回。

  • @CacheEvict —— 写操作后失效缓存

    当数据发生变更,旧缓存必须被清掉。beforeInvocation = false 是默认,方法成功执行后才删除缓存;设为 true 则在方法执行前删除。

关于其他常用的缓存相关的注解

  • @CachePut:总是执行方法体,并把返回值写入缓存,用于更新场景
  • @Caching :用于在一个方法上组合多个缓存注解,当一次操作需要同时触发多种缓存行为时使用。
  • @CacheConfig:类级别的注解,用于抽取该类中所有缓存操作的公共配置,避免在每个方法上重复写 cacheNames。
  • @EnableCaching :总开关,不加这个注解,上面所有缓存注解都不会生效

Spring 中的 @PostConstruct 和 @PreDestroy 注解的作用是什么?

@PostConstruct :在 Bean 依赖注入完成后执行初始化逻辑,一般是 Bean 自身的资源初始化用 @PostConstruct

@PreDestroy:在容器销毁 Bean 之前执行清理逻辑

@PostConstruct和@PreDestroy标注的方法必须满足:返回 void、不能有参数

Spring 创建 Bean 并完成所有属性注入之后,按以下顺序触发初始化回调:

  • @PostConstruct 标注的方法
  • InitializingBean 接口的 afterPropertiesSet() 方法
  • 自定义的 init-method(@Bean(initMethod = "..."))

容器关闭时,按以下顺序触发销毁回调

  • @PreDestroy 标注的方法
  • DisposableBean 接口的 destroy() 方法
  • 自定义的 destroy-method

为什么推荐用 @PostConstruct 而不是构造方法做初始化? 构造方法执行时依赖尚未注入,无法访问 @Autowired 的 Bean;@PostConstruct 在注入完成后执行,可以安全使用依赖。

@Transactional 注解的实现原理?事务代理的创建过程?结合 Spring 框架聊聊

@Transactional的实现原理核心就是Spring AOP代理机制加上事务管理器的协作。Spring容器在初始化bean的时候,会扫描带有@Transactional注解的类,然后为这些类创建代理对象,把事务控制从业务代码中完全剥离出来,通过TransactionInterceptor拦截器和PlatformTransactionManager事务管理器的配合,实现了事务的自动开启、提交和回滚。

当你调用带有@Transactional注解的方法时,实际调用的是代理对象,代理会在目标方法执行前开启事务,执行后根据方法执行结果决定提交或回滚事务。

事务代理的创建过程如下,基本上,不少 AOP 的增强都是这种形式

  • Spring 在 Bean 完成依赖注入后,会依次调用所有 BeanPostProcessor。其中 AbstractAutoProxyCreator 的 postProcessAfterInitialization 是关键入口。
  • 当 Spring 发现某个bean类或其方法上有@Transactional注解时,会先通过TransactionAttributeSource解析事务属性,只有真正需要事务支持的方法才会触发代理创建
  • 然后根据是否实现接口或者proxyTargetClass=true,选择代理方式
  • 然后调用 createProxy(...),把事务拦截器织入代理,返回代理对象替换原始 Bean。

对于回滚,默认行为只有 RuntimeException 和 Error 才回滚,受检异常不回滚。

注意自调用陷阱,同一个类中,方法 A 调用方法 B,即使 B 上有 @Transactional,事务也不生效,基本上使用 AOP 代理增强的注解都是如此

@Transactional 的核心参数

  • propagation传播行为:控制事务如何传递
    • REQUIRED:默认。有事务就加入,没有就新建一个。当前方法不会创建新的事务,而是直接使用调用它的那个方法已经开启的事务。它们会变成同一个事务,共享同一个数据库连接,一起提交或一起回滚。
    • REQUIRES_NEW:总是新建一个事务
    • SUPPORTS:有事务就用,没有就非事务执行。
    • NOT_SUPPORTED:以非事务方式执行,如果当前有事务则挂起。
    • MANDATORY:必须在事务中运行,否则抛异常。
    • NEVER:必须在非事务中运行,否则抛异常。
    • NESTED:如果当前有事务,则嵌套执行。否则和 REQUIRED 一样。
  • isolation隔离级别:控制事务之间的可见性,主要解决脏读、不可重复读、幻读问题。
    • DEFAULT:使用底层数据库的默认级别
    • READ_UNCOMMITTED:最低级别,允许脏读。
    • READ_COMMITTED:只能读已提交数据,避免脏读。
    • REPEATABLE_READ:保证同一事务内多次读取结果一致,避免不可重复读。
    • SERIALIZABLE:最高级别,串行化执行,避免所有并发问题。
  • rollbackFor:指定哪些异常触发回滚。例如 @Transactional(rollbackFor = Exception.class) 让所有异常都回滚。
  • readOnly:默认为 false。设置为 true 时,告诉数据库这是只读操作,可获得性能优化,但不能执行增删改。

@TransactionalEventListener:事务事件监听器的参数如下

  • phase:决定监听器在事务的哪个阶段被触发。
    • BEFORE_COMMIT:在事务提交前执行。此时修改数据库会包含在同一个事务中。
    • AFTER_COMMIT:默认值。在事务成功提交后执行。常用于发送通知、缓存更新等不应影响主事务的后续操作。
    • AFTER_ROLLBACK:在事务回滚后执行。可用于记录失败日志或补偿。
    • AFTER_COMPLETION:在事务完成后执行(无论提交还是回滚)。
  • fallbackExecution:默认 false。表示如果没有活跃事务,监听器就不执行。如果设为 true,即使没有事务,监听器也会像普通 @EventListener 一样被调用。