第6课:SpringBoot4.x 核心注解全解:Component、Bean、Scope、Lazy等IOC相关注解

文章目录

    • [1. 课前导读](#1. 课前导读)
    • [2. 知识目标](#2. 知识目标)
    • [3. 理论讲解](#3. 理论讲解)
      • [3.1 IoC容器的核心概念回顾](#3.1 IoC容器的核心概念回顾)
      • [3.2 Bean定义注解:从@Component到@Bean](#3.2 Bean定义注解:从@Component到@Bean)
      • [3.3 Spring Boot 4.x中IoC注解的破坏性变更](#3.3 Spring Boot 4.x中IoC注解的破坏性变更)
      • [3.4 Bean作用域:@Scope的六种类型](#3.4 Bean作用域:@Scope的六种类型)
      • [3.5 @Lazy:延迟初始化的双刃剑](#3.5 @Lazy:延迟初始化的双刃剑)
      • [3.6 @Qualifier与@Primary:解决Bean冲突](#3.6 @Qualifier与@Primary:解决Bean冲突)
      • [3.7 条件装配:@Conditional与@Profile](#3.7 条件装配:@Conditional与@Profile)
      • [3.8 BeanRegistrar:Spring Boot 4.x的新特性](#3.8 BeanRegistrar:Spring Boot 4.x的新特性)
      • [3.9 4.x中IoC注解速查表](#3.9 4.x中IoC注解速查表)
    • [4. 环境搭建](#4. 环境搭建)
      • [4.1 项目创建](#4.1 项目创建)
      • [4.2 pom.xml](#4.2 pom.xml)
      • [4.3 application.yml](#4.3 application.yml)
    • [5. 代码实战](#5. 代码实战)
      • [5.1 依赖注入:构造器注入的最佳实践](#5.1 依赖注入:构造器注入的最佳实践)
      • [5.2 Bean作用域演示](#5.2 Bean作用域演示)
      • [5.3 @Lazy延迟初始化](#5.3 @Lazy延迟初始化)
      • [5.4 @Qualifier与@Primary](#5.4 @Qualifier与@Primary)
      • [5.5 条件装配](#5.5 条件装配)
      • [5.6 BeanRegistrar动态注册](#5.6 BeanRegistrar动态注册)
      • [5.7 IoC综合Controller](#5.7 IoC综合Controller)
      • [5.8 启动类](#5.8 启动类)
      • [5.9 运行与验证](#5.9 运行与验证)
    • [6. 底层源码剖析](#6. 底层源码剖析)
      • [6.1 组件扫描的核心机制](#6.1 组件扫描的核心机制)
      • [6.2 依赖注入的解析时机](#6.2 依赖注入的解析时机)
      • [6.3 @Configuration的proxyBeanMethods机制](#6.3 @Configuration的proxyBeanMethods机制)
      • [6.4 @Lazy的代理机制](#6.4 @Lazy的代理机制)
      • [6.5 BeanRegistrar的源码实现](#6.5 BeanRegistrar的源码实现)
    • [7. 常见坑点与生产问题排查](#7. 常见坑点与生产问题排查)
    • [8. 本章总结](#8. 本章总结)
    • [9. 课后作业](#9. 课后作业)
  • [🔗 SpringBoot 4.x 从入门到企业级实战系列课程](#🔗 SpringBoot 4.x 从入门到企业级实战系列课程)

本文是《SpringBoot 4.x 从入门到精通》系列第6课,全文约13000字,建议阅读时间50分钟。

环境要求:Spring Boot 4.x(基于4.0.x GA),Spring Framework 7.0.x,Java 17+(推荐Java 21/25)


1. 课前导读

前5课我们走完了Spring Boot 4.x的"基础设施"建设------环境搭建、项目构建、自动配置、配置文件、配置绑定。从这一课开始,我们正式进入Spring Boot应用开发的核心能力:IoC容器中的注解使用。

IoC(控制反转)是Spring框架的根基。理解了IoC,才能理解为什么一个@Service注解标注的类能出现在Controller的构造器参数中;才能理解为什么@Scope("prototype")能让每次注入得到不同的实例;才能理解为什么一个@Lazy注解能解决循环依赖。

但这里有一个关键问题:Spring Boot 4.x中,IoC注解的用法发生了一系列破坏性变更。

javax.annotation和javax.inject命名空间下的注解被彻底移除,必须迁移到jakarta.annotation和jakarta.inject。@Autowired注解在单构造器类中现在可以完全省略------Spring会自动推断构造器注入。@Configuration的proxyBeanMethods属性在Spring Framework 7中默认值变为false,这意味着之前依赖CGLIB代理的@Bean方法调用行为发生了变化。Spring Boot 4还引入了全新的BeanRegistrar接口,让动态Bean注册从几十行模板代码缩减为几行。

如果带着Spring Boot 3.x的习惯来写4.x的IoC注解代码,可能会遇到编译失败、运行期行为变化、甚至应用启动报错。本节课将系统讲解Spring Boot 4.x中IoC相关注解的最新写法 ,从Bean定义的四大类注解(@Component、@Service、@Repository、@Controller)到核心的@Bean、@Scope、@Lazy、@Qualifier、@Primary,每一个注解都会给出4.x中的正确用法和实战代码。

前置知识:第2课的Maven依赖管理、第3课的自动配置原理、第5课的配置绑定。如果对Spring的IoC概念还不熟悉,建议先理解"容器管理Bean"的基本思想。

学习难点 :理解单构造器类中@Autowired的省略规则;掌握@Configuration的proxyBeanMethods默认值变化带来的影响;理解BeanRegistrar相比传统@Bean方法的优势;掌握@Lazy在解决循环依赖时的代理机制与潜在风险。

【配图描述:Spring Boot 4.x IoC注解全景图------从Bean定义注解到依赖注入注解的完整分类】


2. 知识目标

学完本节课,你必须掌握以下核心知识点:

  1. Bean定义注解的四大分类 :@Component及其派生注解(@Service、@Repository、@Controller/@RestController)的语义差异与使用规范;@Bean与@Configuration的协作方式。
  2. 依赖注入注解的最新用法 :@Autowired在单构造器类中的省略规则;@Qualifier与@Primary解决多个同类型Bean的冲突;@Value的适用场景。
  3. Bean作用域与生命周期注解 :@Scope的六种作用域及其适用场景;@Lazy的延迟初始化机制与循环依赖解决方案;@PostConstruct/@PreDestroy在4.x中的Jakarta迁移。
  4. 条件装配注解体系 :@Conditional及其派生注解(@ConditionalOnProperty、@ConditionalOnClass等)的使用方式;@Profile的环境隔离。
  5. Spring Boot 4.x新增的BeanRegistrar :动态Bean注册的新方式,相比传统@Bean方法的优势与适用场景。

3. 理论讲解

3.1 IoC容器的核心概念回顾

在深入注解之前,我们需要先理解IoC容器的基本工作模型。

BeanFactory与ApplicationContext的职责边界 。BeanFactory是Spring IoC容器的根接口,只定义了最基本的能力:getBean()、containsBean()、isSingleton()等,语义上是一个懒加载、按需服务的对象工厂。ApplicationContext继承自BeanFactory,同时叠加了国际化、事件发布、资源加载等能力。两者最容易被追问的区别是初始化时机:BeanFactory是懒加载的(调用getBean()时才真正实例化),而ApplicationContext在refresh()流程的最后阶段会主动把所有非懒加载的单例Bean全部创建完毕。

这种设计的好处是:配置错误、依赖缺失这类问题能在应用启动阶段集中暴露,而不是分散到运行期某次请求第一次触碰到某个Bean时才报错------对于一个需要长期稳定运行的服务,"启动即报错"远比"运行到一半才报错"更可控。

BeanDefinition的两阶段分离 。容器启动的本质是"把配置元数据(XML/注解/配置类)解析成统一的BeanDefinition,再按BeanDefinition批量创建Bean"这两阶段的分离。注解的作用是在第一阶段------告诉容器"这个类需要被注册为一个Bean"。

3.2 Bean定义注解:从@Component到@Bean

Spring Boot中定义Bean的方式主要有两类:类级别注解 和方法级别注解。

类级别注解 。@Component是最基础的类级别Bean定义注解。Spring Boot在其基础上派生了三个语义化的注解:

注解 语义 典型使用场景
@Component 通用组件 工具类、辅助类
@Service 业务服务 Service层
@Repository 数据访问 DAO层,自动转换异常
@Controller Web控制器 返回视图
@RestController REST控制器 返回JSON(@Controller+@ResponseBody)

这四个注解在功能上等价------它们都会被组件扫描注册为Bean。差异在于语义表达 :@Service告诉开发者"这是一个业务服务",@Repository告诉Spring"这个类需要异常转换"。使用语义化注解能让代码自解释,是Spring Boot的推荐实践。

方法级别注解 。@Bean注解标注在@Configuration类的方法上,表示该方法的返回值应被注册为Bean。@Bean方式的优势在于:可以对第三方库的类进行Bean定义(你无法给第三方类加@Component),以及可以使用Java代码动态决定Bean的创建逻辑。

关键差异 :@Component方式由Spring通过反射调用无参构造器创建Bean;@Bean方式由开发者显式调用new来创建Bean。前者更简洁,后者更灵活。

3.3 Spring Boot 4.x中IoC注解的破坏性变更

这是本节课最核心的内容。Spring Boot 4.0对IoC相关注解做了一系列破坏性变更,直接影响代码的编写方式。

变更一:javax命名空间彻底移除。 Spring Boot 4.0不再支持javax.annotation和javax.inject包中定义的注解。@javax.annotation.PostConstruct、@javax.annotation.PreDestroy、@javax.inject.Inject、@javax.inject.Named等注解必须全部替换为jakarta.*命名空间下的对应注解:

java 复制代码
// Spring Boot 3.x 中的写法(4.x 中编译失败)
import javax.annotation.PostConstruct;
import javax.inject.Inject;

// Spring Boot 4.x 中的正确写法
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;

变更二:@Autowired在单构造器类中可省略。 Spring Boot 4.x中,如果类只有一个构造器,@Autowired注解可以完全省略------Spring会自动推断为构造器注入。这是Spring Framework 4.3引入的特性,但在4.x中已成为推荐实践:

java 复制代码
// 显式 @Autowired(仍然有效,但略显冗余)
@Service
public class UserService {
    private final UserRepository repository;

    @Autowired
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
}

// 省略 @Autowired(4.x 推荐)
@Service
public class UserService {
    private final UserRepository repository;

    public UserService(UserRepository repository) {  // 单构造器,自动注入
        this.repository = repository;
    }
}

变更三:@Configuration的proxyBeanMethods默认值。 Spring Framework 7中,@Configuration的proxyBeanMethods属性默认值变为false。在Spring Boot 3.x中,@Configuration默认proxyBeanMethods = true,会生成CGLIB代理,保证@Bean方法之间的直接调用返回同一个实例。4.x中默认false意味着:

java 复制代码
@Configuration
public class AppConfig {

    @Bean
    public ServiceA serviceA() {
        return new ServiceA();
    }

    @Bean
    public ServiceB serviceB() {
        return new ServiceB(serviceA());  // 4.x 中这会创建新的 ServiceA 实例!
    }
}

如果@Bean方法之间存在直接调用,proxyBeanMethods = false会导致每次调用都创建新实例。解决方案:将依赖的Bean作为方法参数注入,而非直接调用:

java 复制代码
@Configuration
public class AppConfig {

    @Bean
    public ServiceA serviceA() {
        return new ServiceA();
    }

    @Bean
    public ServiceB serviceB(ServiceA serviceA) {  // 通过参数注入
        return new ServiceB(serviceA);
    }
}

变更四:BeanRegistrar新特性。 Spring Boot 4引入了BeanRegistrar接口,用于编程式动态注册Bean。相比传统的@Bean方法配合@ConditionalOnProperty,BeanRegistrar可以在一个方法中根据环境变量动态决定注册哪个Bean,代码量大幅减少。

3.4 Bean作用域:@Scope的六种类型

@Scope注解定义了Bean的生命周期和可见范围。Spring支持六种作用域:

作用域 说明 适用场景
singleton 默认,容器中只有一个实例 无状态服务
prototype 每次获取都创建新实例 有状态组件
request 每个HTTP请求一个实例 Web请求级数据
session 每个HTTP会话一个实例 用户会话数据
application 每个ServletContext一个实例 全局Web应用数据
websocket 每个WebSocket会话一个实例 WebSocket通信

singleton与prototype的核心差异 :singleton Bean在容器启动时创建(除非标记@Lazy),整个应用生命周期共享同一个实例。prototype Bean在每次getBean()或注入时创建新实例,Spring不管理其销毁。

重要提醒 :将一个prototype Bean注入到singleton Bean中时,singleton Bean只会持有prototype Bean的一个实例(在注入时创建)。如果需要每次获取新的prototype实例,应使用ObjectFactory或ApplicationContext。

3.5 @Lazy:延迟初始化的双刃剑

@Lazy注解将Bean的初始化时机从容器启动时推迟到第一次使用时。

@Lazy的三种用法:

用法一:标注在Bean类上,全局延迟初始化。

java 复制代码
@Component
@Lazy
public class ExpensiveService {
    public ExpensiveService() {
        // 耗时的初始化逻辑
    }
}

用法二:标注在注入点上,只延迟当前注入。

java 复制代码
@Service
public class OrderService {

    @Lazy
    @Autowired
    private UserService userService;  // 第一次调用时才初始化
}

用法三:标注在@Bean方法上。

java 复制代码
@Bean
@Lazy
public ExpensiveBean expensiveBean() {
    return new ExpensiveBean();
}

@Lazy解决循环依赖的机制 。当两个Bean相互依赖时(A依赖B,B依赖A),Spring默认通过三级缓存提前暴露Bean的早期引用解决循环依赖。但如果是构造器注入的循环依赖,三级缓存无法解决------因为构造器需要完整的依赖对象。此时@Lazy会在注入点生成一个代理对象,实际调用时才初始化真实Bean,从而打破循环。

@Lazy的风险 :延迟初始化会将配置错误从启动阶段推迟到运行时。如果被@Lazy标注的Bean存在初始化异常,容器启动时不会报错,但第一次调用时会抛出异常。此外,@Lazy对prototype Bean无效(prototype本身就是每次新建),对request/session作用域的Bean使用时需要确保在正确的上下文中。

3.6 @Qualifier与@Primary:解决Bean冲突

当容器中存在多个同类型的Bean时,Spring的自动注入会失败,抛出NoUniqueBeanDefinitionException。解决方案有两种:

@Primary:标记一个Bean为"首选",当存在多个候选时优先注入它。

java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource primaryDataSource() {
        return new HikariDataSource();
    }

    @Bean
    public DataSource secondaryDataSource() {
        return new DruidDataSource();
    }
}

@Qualifier:在注入点指定要注入的Bean名称。

java 复制代码
@Service
public class UserService {

    private final DataSource dataSource;

    public UserService(@Qualifier("secondaryDataSource") DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

最佳实践 :优先使用@Qualifier进行精确匹配,@Primary作为默认兜底方案。避免过度使用@Primary,因为它会"隐藏"多个Bean存在的设计意图。

3.7 条件装配:@Conditional与@Profile

@Conditional是Spring的条件装配注解,只有当指定条件满足时,Bean才会被注册。

@Profile:基于环境Profile的条件装配。

java 复制代码
@Configuration
@Profile("dev")
public class DevConfig {
    @Bean
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder().build();
    }
}

@Configuration
@Profile("prod")
public class ProdConfig {
    @Bean
    public DataSource prodDataSource() {
        return new HikariDataSource();
    }
}

@ConditionalOnProperty:基于配置属性的条件装配,是Spring Boot自动配置中最常用的条件注解。

java 复制代码
@Configuration
public class MessageConfig {

    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "email")
    public MessageService emailService() {
        return new EmailMessageService();
    }

    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "sms")
    public MessageService smsService() {
        return new SmsMessageService();
    }
}

@ConditionalOnClass / @ConditionalOnMissingClass:基于类路径中类是否存在。

@ConditionalOnBean / @ConditionalOnMissingBean :基于容器中Bean是否存在。@ConditionalOnMissingBean是自动配置"兜底"机制的核心------它保证只有当用户没有定义某个Bean时,自动配置的默认Bean才会生效。

3.8 BeanRegistrar:Spring Boot 4.x的新特性

Spring Boot 4引入了BeanRegistrar接口,提供了一种更简洁的编程式Bean注册方式。

传统方式的痛点 。当需要根据配置动态注册不同的Bean时,传统做法是写多个@Bean方法配合@ConditionalOnProperty:

java 复制代码
// 旧方案:每个变体一个方法,代码冗余
@Configuration
public class OldStyleConfig {

    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "email")
    public MessageService emailService() {
        return new EmailMessageService();
    }

    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "sms")
    public MessageService smsService() {
        return new SmsMessageService();
    }

    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "push")
    public MessageService pushService() {
        return new PushMessageService();
    }
}

每增加一种消息类型,就需要新增一个@Bean方法。如果条件更复杂(如"当region=CN且time=night时用SMS"),还需要自定义Condition实现,复杂度急剧上升。

BeanRegistrar的新方案:

java 复制代码
public class MessageServiceRegistrar implements BeanRegistrar {

    @Override
    public void register(BeanRegistry registry, Environment env) {
        String type = env.getProperty("app.msg", "email");
        registry.registerBean(
            "messageService",
            switch (type) {
                case "email" -> EmailMessageService.class;
                case "sms" -> SmsMessageService.class;
                case "push" -> PushMessageService.class;
                default -> throw new IllegalArgumentException("Unknown type: " + type);
            },
            spec -> spec.description("Auto-registered by BeanRegistrar")
        );
    }
}

然后在配置类中通过@Import引入:

java 复制代码
@Configuration
@Import(MessageServiceRegistrar.class)
public class AppConfig {}

BeanRegistrar的优势 :代码量从每个变体一个方法缩减为一个方法内的switch表达式;支持Java 17+的模式匹配;可以在注册前执行任意逻辑判断;注册时可以添加Bean描述等元数据。

3.9 4.x中IoC注解速查表

注解 4.x中的变化 推荐用法
@Component 无变化 通用组件
@Service / @Repository 无变化 语义化组件
@Bean 无变化 第三方库Bean定义
@Configuration proxyBeanMethods默认false 避免@Bean方法直接调用
@Autowired 单构造器可省略 优先构造器注入
@Qualifier 无变化 精确匹配多Bean
@Primary 无变化 默认兜底
@Lazy 无变化 循环依赖/启动优化
@Scope 无变化 prototype/request/session
@PostConstruct javax→jakarta 初始化回调
@PreDestroy javax→jakarta 销毁回调
BeanRegistrar 4.x新增 动态Bean注册

4. 环境搭建

4.1 项目创建

本节课创建一个专门的IoC注解实验项目,涵盖Bean定义、依赖注入、作用域、条件装配和BeanRegistrar。

项目结构

复制代码
ioc-annotations-demo/
├── pom.xml
└── src/main/
    ├── java/com/example/iocdemo/
    │   ├── IocAnnotationsApplication.java
    │   ├── bean/
    │   │   ├── UserService.java
    │   │   ├── OrderService.java
    │   │   ├── ExpensiveService.java
    │   │   └── MessageService.java
    │   ├── config/
    │   │   ├── BeanConfig.java
    │   │   ├── MessageServiceRegistrar.java
    │   │   └── AppConfig.java
    │   ├── scope/
    │   │   ├── SingletonBean.java
    │   │   └── PrototypeBean.java
    │   └── controller/
    │       └── IocController.java
    └── resources/
        └── application.yml

4.2 pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.8</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>ioc-annotations-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>ioc-annotations-demo</name>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <!-- Jakarta 注解支持 -->
        <dependency>
            <groupId>jakarta.inject</groupId>
            <artifactId>jakarta.inject-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4.3 application.yml

yaml 复制代码
spring:
  application:
    name: ioc-annotations-demo

server:
  port: 8080

# 消息服务类型(用于 BeanRegistrar 演示)
app:
  msg: email

# 日志级别
logging:
  level:
    com.example: DEBUG

5. 代码实战

5.1 依赖注入:构造器注入的最佳实践

UserRepository.java (@Repository语义化注解):

java 复制代码
package com.example.iocdemo.bean;

import org.springframework.stereotype.Repository;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户数据访问层。
 * 
 * @Repository 注解有两层含义:
 * 1. 语义化:标识这是数据访问组件
 * 2. 技术性:Spring 会为 @Repository Bean 自动添加
 *    PersistenceExceptionTranslationPostProcessor,
 *    将底层数据库异常转换为 Spring 的 DataAccessException
 *    层次结构,方便统一处理。
 */
@Repository
public class UserRepository {

    private final Map<Long, String> store = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    public Long save(String username) {
        Long id = idGenerator.getAndIncrement();
        store.put(id, username);
        return id;
    }

    public String findById(Long id) {
        return store.get(id);
    }
}

UserService.java (@Service + 构造器注入,省略@Autowired):

java 复制代码
package com.example.iocdemo.bean;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * 用户服务层。
 * 
 * Spring Boot 4.x 中的关键变化:
 * 1. 单构造器类不需要 @Autowired,Spring 自动推断为构造器注入
 * 2. @PostConstruct / @PreDestroy 必须使用 jakarta.annotation 包
 *    (javax.annotation 在 4.x 中已移除)
 */
@Service
public class UserService {

    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    private final UserRepository userRepository;

    /**
     * 单构造器注入------Spring Boot 4.x 推荐写法。
     * @Autowired 可以完全省略。
     */
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
        log.info(">>> UserService 构造器被调用,注入 UserRepository: {} <<<",
                userRepository.getClass().getSimpleName());
    }

    /**
     * 初始化回调------使用 jakarta.annotation.PostConstruct。
     * 在依赖注入完成后、Bean 投入使用前执行。
     */
    @PostConstruct
    public void init() {
        log.info(">>> UserService @PostConstruct 被调用 <<<");
        // 预加载数据、建立连接等初始化逻辑
    }

    /**
     * 销毁回调------使用 jakarta.annotation.PreDestroy。
     * 在容器关闭时执行。
     */
    @PreDestroy
    public void destroy() {
        log.info(">>> UserService @PreDestroy 被调用 <<<");
        // 释放资源、关闭连接等清理逻辑
    }

    public Long createUser(String username) {
        return userRepository.save(username);
    }

    public String getUser(Long id) {
        return userRepository.findById(id);
    }
}

5.2 Bean作用域演示

SingletonBean.java:

java 复制代码
package com.example.iocdemo.scope;

import org.springframework.stereotype.Component;
import java.util.UUID;

/**
 * 单例 Bean(默认作用域)。
 * 容器中只有一个实例,整个应用生命周期共享。
 */
@Component
public class SingletonBean {

    private final String instanceId = UUID.randomUUID().toString();

    public String getInstanceId() {
        return instanceId;
    }
}

PrototypeBean.java:

java 复制代码
package com.example.iocdemo.scope;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;

/**
 * 原型 Bean。
 * 每次获取都创建新实例,Spring 不管理其销毁。
 * 
 * @Scope("prototype") 明确声明多例作用域。
 */
@Component
@Scope("prototype")
public class PrototypeBean {

    private final String instanceId = UUID.randomUUID().toString();

    public String getInstanceId() {
        return instanceId;
    }
}

ScopeController(验证作用域):

java 复制代码
package com.example.iocdemo.controller;

import com.example.iocdemo.scope.PrototypeBean;
import com.example.iocdemo.scope.SingletonBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * 作用域验证接口。
 */
@RestController
@RequestMapping("/api/scope")
public class ScopeController {

    private final SingletonBean singletonBean;
    private final ObjectFactory<PrototypeBean> prototypeBeanFactory;

    /**
     * 注意:直接注入 PrototypeBean 只会得到同一个实例。
     * 使用 ObjectFactory 才能在每次调用时获取新实例。
     */
    public ScopeController(SingletonBean singletonBean,
                          ObjectFactory<PrototypeBean> prototypeBeanFactory) {
        this.singletonBean = singletonBean;
        this.prototypeBeanFactory = prototypeBeanFactory;
    }

    @GetMapping("/singleton")
    public Map<String, String> singleton() {
        return Map.of(
                "scope", "singleton",
                "instanceId", singletonBean.getInstanceId()
        );
    }

    @GetMapping("/prototype")
    public Map<String, String> prototype() {
        // 每次调用 prototypeBeanFactory.getObject() 都会创建新实例
        PrototypeBean bean = prototypeBeanFactory.getObject();
        return Map.of(
                "scope", "prototype",
                "instanceId", bean.getInstanceId()
        );
    }
}

5.3 @Lazy延迟初始化

ExpensiveService.java:

java 复制代码
package com.example.iocdemo.bean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

/**
 * 模拟初始化耗时的服务。
 * 
 * @Lazy 将初始化时机推迟到第一次使用时。
 * 
 * 风险提示:如果该 Bean 的初始化存在问题(如依赖缺失),
 * 容器启动时不会报错,但第一次调用时会抛出异常。
 * 因此 @Lazy 不应用于核心业务 Bean。
 */
@Service
@Lazy
public class ExpensiveService {

    private static final Logger log = LoggerFactory.getLogger(ExpensiveService.class);

    public ExpensiveService() {
        log.info(">>> ExpensiveService 构造器被调用(模拟耗时初始化)<<<");
        // 模拟耗时操作,如加载大数据集、建立远程连接
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public String process() {
        return "ExpensiveService 处理完成";
    }
}

5.4 @Qualifier与@Primary

MessageService.java(接口):

java 复制代码
package com.example.iocdemo.bean;

/**
 * 消息服务接口。
 * 有多个实现类,用于演示 @Qualifier 和 @Primary。
 */
public interface MessageService {
    String send(String content);
}

EmailMessageService.java:

java 复制代码
package com.example.iocdemo.bean;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

/**
 * 邮件消息服务------标记为 @Primary,作为默认实现。
 */
@Service
@Primary
public class EmailMessageService implements MessageService {

    @Override
    public String send(String content) {
        return "[邮件] " + content;
    }
}

SmsMessageService.java:

java 复制代码
package com.example.iocdemo.bean;

import org.springframework.stereotype.Service;

/**
 * 短信消息服务。
 */
@Service
public class SmsMessageService implements MessageService {

    @Override
    public String send(String content) {
        return "[短信] " + content;
    }
}

MessageController.java(演示@Qualifier):

java 复制代码
package com.example.iocdemo.controller;

import com.example.iocdemo.bean.MessageService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/message")
public class MessageController {

    private final MessageService defaultService;  // @Primary 的 EmailMessageService
    private final MessageService smsService;       // 通过 @Qualifier 指定

    public MessageController(
            MessageService defaultService,  // 自动注入 @Primary Bean
            @Qualifier("smsMessageService") MessageService smsService) {
        this.defaultService = defaultService;
        this.smsService = smsService;
    }

    @GetMapping("/send")
    public Map<String, String> send(@RequestParam(defaultValue = "Hello") String content,
                                     @RequestParam(defaultValue = "default") String channel) {
        MessageService service = "sms".equals(channel) ? smsService : defaultService;
        return Map.of(
                "channel", channel,
                "result", service.send(content)
        );
    }
}

5.5 条件装配

MessageConfig.java(使用@ConditionalOnProperty):

java 复制代码
package com.example.iocdemo.config;

import com.example.iocdemo.bean.MessageService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 条件装配配置类。
 * 
 * 注意:4.x 中 @Configuration 的 proxyBeanMethods 默认值为 false。
 * @Bean 方法之间不应直接调用,应通过方法参数注入。
 */
@Configuration
public class MessageConfig {

    /**
     * 当 app.msg=email 时注册邮件服务。
     * 注意:EmailMessageService 同时被 @Service 标注,
     * 这里演示的是 @Bean + @ConditionalOnProperty 的组合用法。
     */
    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "email")
    public String messageConfigInfo() {
        return "当前消息服务配置: 邮件";
    }

    @Bean
    @ConditionalOnProperty(name = "app.msg", havingValue = "sms")
    public String messageConfigInfoSms() {
        return "当前消息服务配置: 短信";
    }
}

5.6 BeanRegistrar动态注册

MessageServiceRegistrar.java:

java 复制代码
package com.example.iocdemo.config;

import com.example.iocdemo.bean.MessageService;
import com.example.iocdemo.bean.SmsMessageService;
import com.example.iocdemo.bean.EmailMessageService;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.core.env.Environment;

/**
 * BeanRegistrar 示例------Spring Boot 4.x 新增特性。
 * 
 * 相比传统的 @Bean + @ConditionalOnProperty 方式:
 * 1. 代码量更少:一个方法处理所有分支
 * 2. 逻辑更清晰:switch 表达式直接表达分支
 * 3. 启动更快:避免扫描所有 @Bean 方法
 */
public class MessageServiceRegistrar implements BeanRegistrar {

    @Override
    public void register(BeanRegistry registry, Environment env) {
        String type = env.getProperty("app.msg", "email");

        registry.registerBean(
                "dynamicMessageService",
                switch (type) {
                    case "email" -> EmailMessageService.class;
                    case "sms" -> SmsMessageService.class;
                    default -> throw new IllegalArgumentException(
                            "不支持的消息类型: " + type);
                },
                spec -> spec.description("由 BeanRegistrar 动态注册的消息服务")
        );
    }
}

AppConfig.java(导入Registrar):

java 复制代码
package com.example.iocdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * 应用配置类。
 * 
 * 通过 @Import 引入 BeanRegistrar,
 * Registrar 会在配置类解析阶段执行 Bean 注册。
 */
@Configuration
@Import(MessageServiceRegistrar.class)
public class AppConfig {
}

5.7 IoC综合Controller

java 复制代码
package com.example.iocdemo.controller;

import com.example.iocdemo.bean.ExpensiveService;
import com.example.iocdemo.bean.UserService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * IoC 注解综合验证接口。
 */
@RestController
@RequestMapping("/api/ioc")
public class IocController {

    private final UserService userService;
    private final ExpensiveService expensiveService;

    public IocController(UserService userService,
                         ExpensiveService expensiveService) {
        this.userService = userService;
        this.expensiveService = expensiveService;
    }

    @PostMapping("/user")
    public Map<String, Object> createUser(@RequestParam String username) {
        Long id = userService.createUser(username);
        return Map.of("id", id, "username", username);
    }

    @GetMapping("/user/{id}")
    public Map<String, Object> getUser(@PathVariable Long id) {
        String username = userService.getUser(id);
        return Map.of("id", id, "username", username != null ? username : "未找到");
    }

    @GetMapping("/expensive")
    public Map<String, String> expensive() {
        return Map.of("result", expensiveService.process());
    }
}

5.8 启动类

java 复制代码
package com.example.iocdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * IoC 注解实验应用启动入口。
 * 
 * 注意:启动类上只应标注 @SpringBootApplication,
 * 不应再添加 @EnableAsync、@EnableScheduling 等 @Enable* 注解。
 * 这些注解应放在专门的配置类上。
 */
@SpringBootApplication
public class IocAnnotationsApplication {
    public static void main(String[] args) {
        SpringApplication.run(IocAnnotationsApplication.class, args);
    }
}

5.9 运行与验证

编译并启动

bash 复制代码
cd ioc-annotations-demo
mvn clean install
mvn spring-boot:run

启动日志观察 :注意ExpensiveService的构造器日志。由于标注了@Lazy,启动时不会看到它的构造器输出,只有第一次访问/api/ioc/expensive时才会出现。

测试接口

bash 复制代码
# 创建用户(验证 @Service + @Repository 注入链)
curl -X POST "http://localhost:8080/api/ioc/user?username=Alice"
# 输出:{"id":1,"username":"Alice"}

# 查询用户
curl http://localhost:8080/api/ioc/user/1
# 输出:{"id":1,"username":"Alice"}

# 验证单例作用域------多次调用 instanceId 相同
curl http://localhost:8080/api/scope/singleton
curl http://localhost:8080/api/scope/singleton
# 两次输出的 instanceId 相同

# 验证原型作用域------每次调用 instanceId 不同
curl http://localhost:8080/api/scope/prototype
curl http://localhost:8080/api/scope/prototype
# 两次输出的 instanceId 不同

# 验证 @Primary 和 @Qualifier
curl "http://localhost:8080/api/message/send?content=Hello&channel=default"
# 输出:{"channel":"default","result":"[邮件] Hello"}
curl "http://localhost:8080/api/message/send?content=Hello&channel=sms"
# 输出:{"channel":"sms","result":"[短信] Hello"}

# 触发 @Lazy 初始化
curl http://localhost:8080/api/ioc/expensive
# 第一次调用时控制台输出构造器日志

验证条件装配 :将application.yml中的app.msg改为sms,重启应用,观察messageConfigInfoSms Bean是否生效。

验证BeanRegistrar :将app.msg改为push,重启应用,观察启动是否报错(BeanRegistrar会抛出IllegalArgumentException)。


6. 底层源码剖析

6.1 组件扫描的核心机制

组件扫描的入口是@ComponentScan注解,它通过ClassPathBeanDefinitionScanner实现:

java 复制代码
// 源码路径:org.springframework.context.annotation.ComponentScan
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    String[] value() default {};
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    // ...
}

ClassPathBeanDefinitionScanner扫描指定包下的所有.class文件,通过ASM读取字节码中的注解信息,判断是否标注了@Component或其派生注解。如果是,则将该类注册为一个BeanDefinition。

关键点 :@SpringBootApplication组合了@ComponentScan,默认扫描启动类所在包及其子包。这就是为什么启动类应该放在项目根包下------确保所有组件都在扫描范围内。

6.2 依赖注入的解析时机

依赖注入的解析发生在AbstractAutowireCapableBeanFactory#populateBean()方法中:

java 复制代码
// 源码路径:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
protected void populateBean(String beanName, RootBeanDefinition mbd, 
                             @Nullable BeanWrapper bw) {
    // ...
    if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME || 
        mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
        // 按名称或类型自动注入
    }
    // 应用 InstantiationAwareBeanPostProcessor
    // 处理 @Autowired、@Value 等注解
    if (hasInstantiationAwareBeanPostProcessors()) {
        for (InstantiationAwareBeanPostProcessor bp : 
                getBeanPostProcessors()) {
            bp.postProcessProperties(pds, bw, beanName);
        }
    }
    // ...
}

@Autowired的解析由AutowiredAnnotationBeanPostProcessor完成。它扫描字段、方法和构造器上的@Autowired注解,通过DefaultListableBeanFactory#resolveDependency()方法解析依赖。

@Autowired省略的源码依据 :Spring Framework 4.3引入了一个优化------如果一个类只有一个有参构造器,且没有无参构造器,Spring会自动将其视为构造器注入,无需@Autowired。这个逻辑在AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors()中实现:

java 复制代码
// 源码路径:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, 
                                                        String beanName) {
    // ...
    // 如果只有一个有参构造器,且构造器上没有 @Autowired
    // 则将其作为候选构造器(Spring 4.3+ 的自动推断)
    if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
        // 返回该构造器作为候选
    }
    // ...
}

6.3 @Configuration的proxyBeanMethods机制

@Configuration的proxyBeanMethods属性控制是否为配置类生成CGLIB代理。当proxyBeanMethods = true时,Spring会通过CGLIB创建配置类的子类,拦截@Bean方法的调用,确保直接调用@Bean方法时返回的是容器中的单例实例。

Spring Framework 7中的变化 :proxyBeanMethods默认值变为false。这意味着:

java 复制代码
@Configuration  // 默认 proxyBeanMethods = false
public class AppConfig {
    @Bean
    public ServiceA serviceA() { return new ServiceA(); }

    @Bean
    public ServiceB serviceB() {
        return new ServiceB(serviceA());  // 4.x 中每次调用都创建新 ServiceA!
    }
}

为什么改变默认值? proxyBeanMethods = true需要CGLIB生成代理类,增加了启动开销和内存占用。对于大多数配置类,@Bean方法之间并不相互调用,因此代理是不必要的。4.x将默认值改为false,让不需要代理的配置类获得更好的性能。

源码实现 :在ConfigurationClassPostProcessor中,当proxyBeanMethods = false时,配置类不会被CGLIB增强;@Bean方法的调用直接执行方法体,不经过代理拦截。

6.4 @Lazy的代理机制

@Lazy注解的实现依赖于ContextAnnotationAutowireCandidateResolver。当在注入点标注@Lazy时,Spring会为该依赖生成一个代理对象,而非立即解析真实Bean:

java 复制代码
// 源码路径:org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver
protected TargetSource getLazyResolutionProxyIfNecessary(
        DependencyDescriptor descriptor, @Nullable String beanName) {
    return (isLazy(descriptor) ? 
            buildLazyResolutionProxy(descriptor, beanName) : null);
}

代理对象在第一次方法调用时触发真实Bean的初始化和注入。这就是@Lazy能解决循环依赖的原理:容器启动时注入的是代理对象,而非真实Bean,从而打破了"A需要B的完整实例,B需要A的完整实例"的循环。

6.5 BeanRegistrar的源码实现

BeanRegistrar是Spring Framework 7引入的新接口:

java 复制代码
// 源码路径:org.springframework.beans.factory.BeanRegistrar
@FunctionalInterface
public interface BeanRegistrar {
    void register(BeanRegistry registry, Environment environment);
}

BeanRegistry提供了registerBean()方法,支持通过BeanDefinitionCustomizer自定义BeanDefinition:

java 复制代码
// 源码路径:org.springframework.beans.factory.BeanRegistry
public interface BeanRegistry {
    void registerBean(String beanName, Class<?> beanClass, 
                      BeanDefinitionCustomizer... customizers);
    // ...
}

BeanRegistrar在ConfigurationClassPostProcessor的配置类解析阶段被调用,与@Bean方法的处理时机相近,但注册逻辑完全由开发者控制,避免了@Bean方法扫描和条件评估的开销。


7. 常见坑点与生产问题排查

坑点1:javax.annotation注解在4.x中编译失败

报错现象 :import javax.annotation.PostConstruct; 编译报错"程序包javax.annotation不存在"。

原因 :Spring Boot 4.0移除了对javax.annotation和javax.inject命名空间的支持。

解决方案 :将所有javax.annotation.*和javax.inject.*的import替换为jakarta.annotation.*和jakarta.inject.*。@PostConstruct→jakarta.annotation.PostConstruct,@PreDestroy→jakarta.annotation.PreDestroy,@Inject→jakarta.inject.Inject。

坑点2:@Configuration的proxyBeanMethods默认值变化导致Bean实例不一致

报错现象 :@Bean方法A直接调用了@Bean方法B,期望返回同一个B实例,但实际得到了不同的实例。

原因 :Spring Framework 7中proxyBeanMethods默认值为false,@Bean方法之间的直接调用不再经过CGLIB代理拦截。

解决方案 :将依赖的Bean作为方法参数注入,而非直接调用@Bean方法。如果需要保持旧行为,可以显式设置@Configuration(proxyBeanMethods = true),但这会带来CGLIB代理的开销。

坑点3:@Lazy导致初始化异常延迟暴露

报错现象 :应用启动正常,但第一次调用某个接口时抛出NullPointerException或NoSuchBeanDefinitionException。

原因 :被@Lazy标注的Bean的初始化被推迟到第一次使用时,初始化阶段的异常在启动时不会暴露。

解决方案 :避免在核心业务Bean上使用@Lazy。@Lazy仅适用于初始化耗时的非核心Bean,或用于解决循环依赖。在CI/CD流程中加入接口冒烟测试,尽早发现延迟初始化的异常。

坑点4:@Qualifier名称拼写错误导致注入失败

报错现象 :NoSuchBeanDefinitionException: No qualifying bean of type ...。

原因 :@Qualifier指定的Bean名称与容器中的Bean名称不匹配。默认情况下,Bean名称是类名首字母小写的驼峰形式(如smsMessageService)。如果自定义了Bean名称(如通过@Service("customName")),必须使用自定义名称。

解决方案 :通过/actuator/beans端点或启动日志确认Bean的实际名称。使用IDE的Bean依赖分析功能检查注入点。

坑点5:prototype Bean注入到singleton Bean中只创建一次

报错现象 :将@Scope("prototype")的Bean注入到singleton Bean中,期望每次调用得到新实例,但实际始终是同一个实例。

原因:singleton Bean在创建时解析依赖,prototype Bean只被创建一次并注入。Spring不会在singleton Bean每次使用时重新创建prototype Bean。

解决方案 :使用ObjectFactory<PrototypeBean>或ApplicationContext.getBean()在需要时动态获取新实例。

坑点6:BeanRegistrar注册的Bean名称与@Bean冲突

报错现象 :同时存在BeanRegistrar注册的Bean和@Bean方法定义的Bean,启动时报BeanDefinitionOverrideException。

原因 :BeanRegistrar注册的Bean名称与@Bean方法生成的Bean名称相同,且Spring Boot默认禁止Bean覆盖。

解决方案 :为BeanRegistrar注册的Bean使用唯一的名称。如果确实需要覆盖,可以在application.yml中设置spring.main.allow-bean-definition-overriding=true,但更好的做法是避免Bean名称冲突。


8. 本章总结

本节课围绕Spring Boot 4.x的IoC核心注解,从Bean定义、依赖注入、作用域、条件装配到BeanRegistrar,构建了完整的IoC注解知识体系。

Bean定义注解 :@Component及其派生注解(@Service、@Repository、@Controller)用于类级别Bean定义。@Bean用于方法级别Bean定义,适合第三方库和需要Java代码控制创建逻辑的场景。

Spring Boot 4.x的破坏性变更 :javax.annotation和javax.inject命名空间被移除,必须迁移到jakarta.*。@Autowired在单构造器类中可以省略。@Configuration的proxyBeanMethods默认值变为false,@Bean方法之间应通过参数注入而非直接调用。BeanRegistrar是4.x新增的编程式Bean注册方式,代码量更少,逻辑更清晰。

Bean作用域与生命周期 :@Scope支持singleton(默认)、prototype、request、session等六种作用域。@Lazy将初始化推迟到第一次使用,可用于解决循环依赖和优化启动速度,但需注意初始化异常的延迟暴露风险。

依赖注入 :@Qualifier和@Primary解决多个同类型Bean的冲突。优先使用@Qualifier进行精确匹配。

条件装配 :@Conditional及其派生注解(@ConditionalOnProperty、@ConditionalOnClass、@ConditionalOnMissingBean等)是Spring Boot自动配置的核心机制。


9. 课后作业

基础题1

创建一个Spring Boot 4.x项目,实现以下场景:

  • 定义一个NotificationService接口,有两个实现类:EmailNotificationService和SmsNotificationService
  • 将EmailNotificationService标记为@Primary
  • 在NotificationController中,分别通过默认注入和@Qualifier("smsNotificationService")注入两个实现类
  • 提供GET /api/notify?type=email和GET /api/notify?type=sms接口,返回对应实现类的处理结果
  • 将@PostConstruct和@PreDestroy注解应用于EmailNotificationService,在启动和关闭时打印日志

验证要求 :使用/actuator/beans查看容器中所有Bean的定义信息,确认@Primary标记的Bean和@Qualifier指定的Bean是否被正确注册。

基础题2

创建一个@Scope("prototype")的ShoppingCart Bean,包含一个List<String>存储商品。

  • 使用ObjectFactory<ShoppingCart>在Controller中动态获取实例
  • 提供POST /api/cart/add接口,每次调用向当前购物车添加商品
  • 提供GET /api/cart接口,返回当前购物车中的商品列表
  • 验证每次getObject()返回的是新实例(购物车内容为空)

提示 :直接注入ShoppingCart到singleton Controller中只会得到同一个实例,必须使用ObjectFactory或@Lookup。

综合实战题

构建一个完整的IoC注解实战项目,要求:

  1. Bean定义 :创建一个PaymentService接口,包含AlipayService、WechatPayService、UnionPayService三个实现类

  2. @Lazy循环依赖:

    • 创建OrderService和InventoryService,两者相互依赖
    • 使用@Lazy在其中一个的注入点上打破循环依赖
    • 解释如果不加@Lazy,Spring是否能自动解决这个循环依赖(提示:构造器注入 vs setter注入)
  3. BeanRegistrar:

    • 实现一个PaymentServiceRegistrar,根据配置app.payment.type动态注册对应的支付服务Bean
    • 使用Java 17的switch表达式处理分支
    • 在AppConfig中通过@Import引入Registrar
  4. 条件装配:

    • 使用@Profile创建开发环境和生产环境的不同数据源配置
    • 使用@ConditionalOnProperty控制是否启用某个支付服务
  5. REST接口:

    • POST /api/order:创建订单(需要OrderService和InventoryService协作)
    • GET /api/payment/supported:返回所有可用的支付方式
    • GET /api/payment/pay?type=alipay&amount=100:调用对应的支付服务
  6. 验证要求:

    • 使用--debug模式启动,查看CONDITIONS EVALUATION REPORT中条件装配的评估结果
    • 将app.payment.type改为不存在的值,验证BeanRegistrar是否抛出明确异常
    • 使用/actuator/beans查看所有Bean的名称、类型和作用域

扩展挑战 :实现一个自定义的@Conditional注解,判断当前操作系统是否为Linux(通过System.getProperty("os.name")),创建一个仅在Linux环境下注册的LinuxSpecificService Bean。

🔗 SpringBoot 4.x 从入门到企业级实战系列课程

去订阅

相关推荐
paopaokaka_luck1 小时前
美食推荐系统(协同过滤算法推荐、AI助手、HanLP中文分词、 ECharts可视化分析、菜谱学习与话题交流)
java·spring boot·mysql·tomcat·echarts
开源推荐官1 小时前
2026 年开源商城源码选型参考:技术栈、功能与适用场景梳理
java·架构·开源
sunburn-1 小时前
数据库操作完整教程:从基础到实战
java·数据库·mysql
文丑颜不良啊1 小时前
LeetCode576~600题目解析
java·leetcode
Linux运维技术栈2 小时前
CVE-2026-91403 高危漏洞预警:Java JSSE 信任链被攻破 全场景修复方案与Linux系统级彻底清理实操
java·linux·安全·jdk·信任链
秋饼2 小时前
Spring AI + MCP 工具调用可靠性实战:参数校验、权限防线与人工确认生产级落地
java·ai·技术分享·后端开发
步行cgn2 小时前
RESTful 是什么?怎么记住它?
java
随性而行3602 小时前
企业微信 Webhook 接收不到消息?网络连通性、URL 签名验签与 API 返回错误码排查指南
java·网络·人工智能·后端·微信·企业微信
薛之谦_2 小时前
JD Hotkey 项目实战:京东开源的热点探测与秒杀系统
java·安全