【Spring】SpringIoC&DI

SpringIoC&DI

  • [Spring IoC & DI 概述](#Spring IoC & DI 概述)
    • [什么是 IoC(控制反转)](#什么是 IoC(控制反转))
    • [什么是 DI(依赖注入)](#什么是 DI(依赖注入))
    • [IoC 与 DI 的关系](#IoC 与 DI 的关系)
    • [Spring IoC 容器的工作流程](#Spring IoC 容器的工作流程)
  • [什么是 Bean?](#什么是 Bean?)
  • [创建 Bean 的方式](#创建 Bean 的方式)
  • [Bean 对象的获取](#Bean 对象的获取)
    • [通过 ApplicationContext 获取](#通过 ApplicationContext 获取)
      • [Bean 的名称规则](#Bean 的名称规则)
      • [获取不同方式创建的 Bean](#获取不同方式创建的 Bean)
    • 通过依赖注入获取⭐
      • [@Autowired 详解](#@Autowired 详解)
      • [@Resource 详解](#@Resource 详解)
      • [@Autowired 与 @Resource 对比⭐](#@Autowired 与 @Resource 对比⭐)
      • [获取不同方式创建的 Bean](#获取不同方式创建的 Bean)
      • [1. 属性注入(Field Injection)](#1. 属性注入(Field Injection))
      • [2. 构造方法注入(Constructor Injection)](#2. 构造方法注入(Constructor Injection))
      • [3. Setter方法注入(Setter Injection)](#3. Setter方法注入(Setter Injection))
      • 三种注入方式对比
    • [通过 ApplicationContextAware 获取](#通过 ApplicationContextAware 获取)
  • 扫描路径
      • 默认扫描范围
      • 不在默认扫描范围的情况
      • 自定义扫描路径
        • 方式一:@ComponentScan
        • [方式二:@SpringBootApplication 的 scanBasePackages 属性](#方式二:@SpringBootApplication 的 scanBasePackages 属性)
        • [方式三:使用 AnnotationConfigApplicationContext 手动指定](#方式三:使用 AnnotationConfigApplicationContext 手动指定)
      • 扫描路径的常见问题
        • [问题 1:Bean 找不到(NoSuchBeanDefinitionException)](#问题 1:Bean 找不到(NoSuchBeanDefinitionException))
        • [问题 2:@SpringBootApplication 与 @ComponentScan 同时使用](#问题 2:@SpringBootApplication 与 @ComponentScan 同时使用)

Spring IoC & DI 概述

什么是 IoC(控制反转)

IoC(Inversion of Control,控制反转) 是 Spring 框架的核心思想。传统开发中,对象由开发者自己通过 new 关键字创建和管理,对象的创建权在开发者手中。而 IoC 将这一控制权反转给 Spring 容器,由容器负责对象的创建、组装和生命周期管理。

一句话理解 :原来你亲自 new 对象,现在把钥匙交给 Spring 容器,让它帮你 new 并管理。

IoC 带来的好处:

  • 降低耦合:对象之间不再硬编码依赖关系,而是通过容器统一管理。
  • 提高可测试性:可以轻松替换依赖的实现,方便单元测试。
  • 统一生命周期管理:对象的创建、初始化、销毁都由容器统一控制。

什么是 DI(依赖注入)

DI(Dependency Injection,依赖注入) 是 IoC 的具体实现方式。当容器创建好一个对象后,会自动将它所依赖的其他对象注入进来,无需开发者手动组装。

java 复制代码
// 没有 DI 的传统方式
public class UserController {
    private UserService userService;
    
    public UserController() {
        // 开发者手动创建依赖
        this.userService = new UserService();
    }
}

// 使用 DI 的方式
@Component
public class UserController {
    @Autowired  // 容器自动注入依赖
    private UserService userService;
}

IoC 与 DI 的关系

概念 角色 类比
IoC(控制反转) 设计思想 把"做饭"这件事交给餐厅,而不是自己下厨
DI(依赖注入) 实现方式 餐厅把做好的菜直接送到你桌上

IoC 是目标,DI 是手段。Spring 通过 DI 来实现 IoC,让容器管理对象的创建和依赖关系。

Spring IoC 容器的工作流程

① 配置元数据

(注解/XML/Java Config)
② Spring IoC 容器

读取配置
③ 实例化 Bean

(创建对象)
④ 依赖注入

(组装对象)
⑤ Bean 就绪

可供使用

流程说明

  1. 配置元数据 :通过注解(@Component@Bean 等)、XML 或 Java Config 告诉容器需要管理哪些类。
  2. 容器读取:Spring IoC 容器解析配置,构建 Bean 定义信息。
  3. 实例化:容器根据 Bean 定义创建对象实例。
  4. 依赖注入:容器自动将依赖的对象注入到需要的地方。
  5. 就绪:Bean 完全初始化,可以被应用程序使用。

什么是 Bean?

在 Spring 框架中,Bean 是由 Spring IoC 容器实例化、组装和管理的 Java 对象。简单来说,传统 Java 应用中对象由开发者自己 new 出来,而在 Spring 中,你把对象的创建权交给容器,容器创建出来的对象就叫做 Bean。

Bean 的核心特征:

  • 由容器管理:对象的创建、初始化、销毁都由 Spring 容器负责。
  • 默认单例:默认情况下,每个 Bean 在容器中只有一个实例,多次获取返回同一个对象。
  • 可依赖注入 :一个 Bean 可以自动引用另一个 Bean,无需手动 new

在 Spring 框架中,将对象存储到 IoC 容器是依赖注入的基础。

创建 Bean 的方式

Spring 提供了多种注解来实现这一目标,主要分为类注解方法注解两大类。

类注解

类注解用于将当前类的实例注册为 Spring 管理的 Bean。Spring 提供了以下 5 个核心类注解,它们都能将类注册为 Bean,但语义和功能上各有侧重:

注解 说明 典型应用层 特殊功能
@Controller 控制器存储 前端控制器(MVC 中的 Controller) 支持 @ResponseBody 语义、异常处理等 MVC 特性
@Service 服务存储 业务逻辑层(Service) 无特殊功能,纯语义标记
@Repository 仓库存储 数据访问层(DAO / Repository) 支持持久层异常转换
@Component 组件存储 通用组件,不属于以上三层时使用 无特殊功能,纯语义标记
@Configuration 配置存储 声明配置类,通常配合 @Bean 使用 支持 CGLIB 代理,确保 @Bean 方法单例

重要说明 :虽然这 5 个注解都能将类注册为 Bean,但 @Controller 不能随意替换为其他注解@Controller 是 Spring MVC 中处理 Web 请求的核心注解,它除了注册 Bean 外,还支持:

  • 配合 @RequestMapping 处理请求映射
  • 配合 @ResponseBody 返回 JSON 数据
  • 全局异常处理(@ExceptionHandler
  • 视图解析等 MVC 特性

如果控制层使用 @Component@Service 代替 @Controller,虽然 Bean 能被注册,但 Spring MVC 不会将其识别为控制器,请求映射等功能将失效。因此,控制层必须使用 @Controller,其他层根据语义选择对应的注解。

方法注解

方法注解要配合类注解使用

@Bean 必须放在一个被类注解(如 @Configuration@Component)修饰的类中才能生效。Spring 容器会自动创建 UserService 的实例,并交给 AppConfig 中的 @Bean 方法返回。

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

UserService 类的完整定义:

java 复制代码
public class UserService {
    public void sayHello() {
        System.out.println("Hello from UserService Bean!");
    }
}

定义多个对象

一个配置类中可以定义多个 @Bean 方法,每个方法对应一个 Bean。Spring 容器会自动创建 UserServiceOrderService 的实例,并交给 AppConfig 中的 @Bean 方法返回。

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
    @Bean
    public OrderService orderService() {
        return new OrderService();
    }
}

OrderService 类的完整定义:

java 复制代码
public class OrderService {
    public void createOrder() {
        System.out.println("Order created by OrderService Bean!");
    }
}

重命名 Bean

默认情况下,Bean 的名称就是方法名。可以通过 @Bean("customName")@Bean(name = "customName") 来指定自定义名称。

java 复制代码
@Configuration
public class AppConfig {
    @Bean("myUserService")
    public UserService userService() {
        return new UserService();
    }
}

重命名后,再通过原方法名(如 userService)去获取 Bean 是找不到的,必须使用新名称 myUserService

导入配置类:@Import

@Import 用于在一个配置类中导入另一个配置类(或普通类),被导入的类也会被 Spring 容器管理。

适用场景:当你把配置分散在多个类中,需要在一个主配置类中统一引入时。

java 复制代码
@Configuration
@Import({DbConfig.class, CacheConfig.class})
public class AppConfig {
    // 这里可以继续定义 @Bean
}

这样 DbConfigCacheConfig 中定义的 Bean 也会被注册到容器中。

FactoryBean 接口

FactoryBean 是一个特殊的接口,实现它可以让 Spring 通过一个工厂方法来创建 Bean。当你需要复杂的创建逻辑(比如根据配置动态创建不同的实现类)时,可以用它。

java 复制代码
@Component
public class MyFactoryBean implements FactoryBean<UserService> {
    @Override
    public UserService getObject() {
        // 这里可以写复杂的创建逻辑
        return new UserServiceImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return UserService.class;
    }
}

注意:FactoryBean 本身也是一个 Bean,但通过 getObject() 返回的对象才是真正被注入使用的 Bean。

方式 适用场景 常用程度
@Bean 方法注解 创建第三方类、需要自定义初始化逻辑 ⭐⭐⭐⭐⭐
XML 配置 维护老项目 ⭐⭐
@Import 模块化配置、导入其他配置类 ⭐⭐⭐⭐
FactoryBean 复杂创建逻辑、动态选择实现类 ⭐⭐⭐

Bean 对象的获取

将 Bean 存储到 Spring 容器后,需要从容器中获取这些 Bean 来使用。Spring 提供了多种获取 Bean 的方式,下面逐一介绍。

通过 ApplicationContext 获取

Bean 的名称规则

通过名称获取 Bean 时,需要知道 Bean 的名称是如何确定的。Spring 中 Bean 的名称规则如下:

创建方式 默认名称规则 示例
类注解 1.类名首字母小写(驼峰命名) 2.如果类名前两个字母都是大写,则保持原样 UserServiceuserService URLParserURLParser
@Bean 方法注解 方法名即为 Bean 名称 @Bean public UserService userService()userService
@Bean 指定名称 @Bean("customName")@Bean(name = "customName") 名称变为 customName

获取不同方式创建的 Bean

ApplicationContext 是 Spring 容器的核心接口,可以通过它直接获取 Bean。下面演示如何通过 ApplicationContext 获取类注解方法注解指定名称三种方式创建的 Bean。

首先,在com.example.demo.service路径下准备三个 Bean 类:

java 复制代码
// 1. 类注解 ------ @Service 创建 Bean
@Service
public class UserService {
    public void sayHello() {
        System.out.println("Hello from @Service Bean!");
    }
}

// 2. 方法注解 ------ @Bean 创建 Bean
public class OrderService {
    public void createOrder() {
        System.out.println("Order from @Bean method!");
    }
}

// 3. 指定名称 ------ @Bean("customName") 创建 Bean
public class ReportService {
    public void generateReport() {
        System.out.println("Report from named Bean!");
    }
}

然后在在com.example.demo.configuration路径下,注册后两种 Bean:

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public OrderService orderService() {
        return new OrderService();
    }

    @Bean("myReportService")
    public ReportService reportService() {
        return new ReportService();
    }
}

最后在启动类中分别获取:

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

import com.example.demo.service.OrderService;
import com.example.demo.service.ReportService;
import com.example.demo.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        // 1. 获取类注解创建的 Bean(按类型)
        UserService userService = context.getBean(UserService.class);
        userService.sayHello();

        // 2. 获取 @Bean 方法注解创建的 Bean(按方法名)
        OrderService orderService = (OrderService) context.getBean("orderService");
        orderService.createOrder();

        // 3. 获取指定名称的 Bean(按自定义名称)
        ReportService reportService = (ReportService) context.getBean("myReportService");
        reportService.generateReport();
    }
}

下面用 Mermaid 图梳理三个 Bean 类、两种创建方式与启动类之间的整体关系:
Bean 类定义
方法注解创建
类注解创建
按类型获取
按方法名获取
按自定义名称获取
UserService

@Service
启动类 DemoApplication

context.getBean
AppConfig @Configuration
orderService @Bean
reportService @Bean

指定名称 myReportService
OrderService
ReportService

图例说明

  • 🟦 蓝色:类注解(@Service)创建的 Bean
  • 🟧 橙色:方法注解(@Bean)及配置类
  • 🟩 绿色:启动类,负责获取 Bean
  • 🟥 粉色:普通的 Java 类(被 @Bean 方法返## 通过依赖注入获取

在 Spring 管理的 Bean 中,最常用的方式是通过 @Autowired@Resource 注解自动注入,无需手动调用 `context.getBean()

通过依赖注入获取⭐

在 Spring 管理的 Bean 中,最常用的方式是通过依赖注入(Dependency Injection)自动装配依赖对象,无需手动调用 context.getBean()。Spring 支持三种主要的依赖注入方式:属性注入构造方法注入Setter方法注入

@Autowired 详解

@Autowired 是 Spring 提供的原生注解,默认按类型(byType) 注入,可用于三种注入方式。

java 复制代码
@Component
public class UserController {
    // 属性注入
    @Autowired
    private UserService userService;
    
    // 构造方法注入
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    // Setter方法注入
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

@Autowired 的注入规则

步骤 行为 说明
1 按类型查找 容器中查找 UserService 类型的 Bean
2 找到唯一 Bean 直接注入,成功
3 找到多个同类型 Bean 尝试按字段名/参数名/方法名匹配 Bean 名称
4 名称匹配成功 注入该名称对应的 Bean
5 名称匹配失败 抛出 NoUniqueBeanDefinitionException

解决多个同类型 Bean 的冲突

java 复制代码
// 方案一:@Primary 指定首选 Bean
@Primary
@Service
public class PrimaryUserService implements UserService { ... }

// 方案二:@Qualifier 指定名称
@Component
public class UserController {
    @Autowired
    @Qualifier("vipUserService")  // 指定要注入的 Bean 名称
    private UserService userService;
}

// 方案三:@Resource 按名称注入(见下文)

注意@Autowired 默认要求 Bean 必须存在 ,如果允许为空可以加 required = false

java 复制代码
@Autowired(required = false)
private UserService userService;  // 容器中没有 UserService 时,注入 null

@Resource 详解

@Resource 是 JSR-250 规范定义的注解,默认按名称(byName) 注入。

java 复制代码
@Component
public class UserController {
    @Resource(name = "userService")
    private UserService userService;  // 按名称注入
}

@Resource 的注入规则

步骤 行为 说明
1 指定了 name 属性 按指定名称查找 Bean
2 未指定 name 按字段名/方法名查找 Bean
3 按名称找到唯一 Bean 直接注入
4 按名称未找到 回退为按类型查找
5 按类型找到唯一 Bean 注入成功
6 按类型找到多个 抛出异常

@Resource 的常见用法

java 复制代码
// 1. 按字段名注入(默认)
@Resource
private UserService userService;  // 查找名称为 "userService" 的 Bean

// 2. 按指定名称注入
@Resource(name = "vipUserService")
private UserService userService;  // 查找名称为 "vipUserService" 的 Bean

// 3. 按类型注入(不推荐,语义不清晰)
@Resource(type = UserService.class)
private UserService userService;

@Autowired 与 @Resource 对比⭐

对比维度 @Autowired @Resource
来源 Spring 原生注解 JSR-250 规范(Java 标准)
注入方式 先按类型,再按名称 先按名称,再按类型
指定名称 需配合 @Qualifier 直接使用 name 属性
required 属性 支持 required = false 不支持(默认必须存在)
适用场景 大多数情况,按类型注入更简洁 需要按名称注入时更直观
可移植性 仅限 Spring 项目 可在其他 Java EE 容器中使用

选择建议

  • 如果项目中只有一个同类型的 Bean → 推荐 @Autowired,代码更简洁
  • 如果同一类型有多个 Bean → 推荐 @Resource(name = "..."),按名称注入更直观
  • 如果希望代码脱离 Spring 也能运行 → 推荐 @Resource(JSR 标准)

获取不同方式创建的 Bean

下面演示如何通过依赖注入获取类注解方法注解指定名称三种方式创建的 Bean。

首先,准备三个 Bean 类(与上一节相同):

java 复制代码
// 1. 类注解 ------ @Service 创建 Bean
@Service
public class UserService {
    public void sayHello() {
        System.out.println("Hello from @Service Bean!");
    }
}

// 2. 方法注解 ------ @Bean 创建 Bean
public class OrderService {
    public void createOrder() {
        System.out.println("Order from @Bean method!");
    }
}

// 3. 指定名称 ------ @Bean("customName") 创建 Bean
public class ReportService {
    public void generateReport() {
        System.out.println("Report from named Bean!");
    }
}

然后在配置类中注册后两种 Bean:

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public OrderService orderService() {
        return new OrderService();
    }

    @Bean("myReportService")
    public ReportService reportService() {
        return new ReportService();
    }
}

最后,通过依赖注入在一个组件中获取所有 Bean:

java 复制代码
@Component
public class BeanDemoComponent {

    // 1. 获取类注解创建的 Bean(按类型注入)
    @Autowired
    private UserService userService;

    // 2. 获取 @Bean 方法注解创建的 Bean(按名称注入)
    @Resource(name = "orderService")
    private OrderService orderService;

    // 3. 获取指定名称的 Bean(按自定义名称注入)
    @Resource(name = "myReportService")
    private ReportService reportService;

    public void executeAll() {
        userService.sayHello();
        orderService.createOrder();
        reportService.generateReport();
    }
}
java 复制代码
package com.example.demo;

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

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);

        // 从容器中获取 BeanDemoComponent
        BeanDemoComponent demo = context.getBean(BeanDemoComponent.class);
        demo.executeAll();
    }
}

说明

  • 类注解(@Service)创建的 Bean,推荐使用 @Autowired 按类型注入,Spring 会自动匹配。
  • @Bean 方法注解创建的 Bean,默认名称是方法名,可以使用 @Resource(name = "orderService") 按名称注入。
  • 通过 @Bean("myReportService") 指定名称后,必须使用自定义名称 myReportService 注入,原方法名 reportService 不再有效。

下面用 Mermaid 图梳理三个 Bean 类、两种创建方式与 BeanDemoComponent 之间的依赖注入关系:
Bean 类定义
方法注解创建
类注解创建
@Autowired 按类型注入
@Resource(name='orderService') 按名称注入
@Resource(name='myReportService') 按名称注入
UserService

@Service
BeanDemoComponent

@Component
AppConfig @Configuration
orderService @Bean
reportService @Bean

指定名称 myReportService
OrderService
ReportService

图例说明

  • 🟦 蓝色:类注解(@Service)创建的 Bean
  • 🟧 橙色:方法注解(@Bean)及配置类
  • 🟩 绿色:BeanDemoComponent,通过依赖注入获取所有 Bean
  • 🟥 粉色:普通的 Java 类(被 @Bean 方法返回)

1. 属性注入(Field Injection)

属性注入是最简单、最常用的注入方式,直接在字段上使用 @Autowired@Resource 注解。

java 复制代码
@Component
public class UserController {
    
    // 使用 @Autowired 进行属性注入
    @Autowired
    private UserService userService;
    
    // 使用 @Resource 进行属性注入
    @Resource
    private OrderService orderService;
    
    public void execute() {
        userService.sayHello();
        orderService.createOrder();
    }
}

优点

  • 代码简洁,无需编写额外的构造方法或Setter方法
  • 易于理解和维护

缺点

  • 不利于单元测试(需要反射或Spring容器)
  • 隐藏了依赖关系,不易发现循环依赖
  • 字段被声明为final时无法使用

2. 构造方法注入(Constructor Injection)

构造方法注入通过构造方法参数来注入依赖,是Spring官方推荐的注入方式。

java 复制代码
@Component
public class UserController {
    
    private final UserService userService;
    private final OrderService orderService;
    
    // 构造方法注入(Spring 4.3+ 可以省略 @Autowired)
    @Autowired
    public UserController(UserService userService, OrderService orderService) {
        this.userService = userService;
        this.orderService = orderService;
    }
    
    // Spring 4.3+ 可以完全省略 @Autowired
    public UserController(UserService userService, OrderService orderService) {
        this.userService = userService;
        this.orderService = orderService;
    }
    
    public void execute() {
        userService.sayHello();
        orderService.createOrder();
    }
}

优点

  • 不可变性 :依赖可以声明为 final,确保线程安全
  • 明确依赖:通过构造方法参数明确声明所有必需依赖
  • 易于测试:可以直接通过构造方法传入mock对象进行单元测试
  • 避免循环依赖:Spring在创建Bean时就能发现循环依赖问题
  • Spring官方推荐 :从Spring 4.3开始,单构造方法可以省略@Autowired

缺点

  • 当依赖较多时,构造方法参数列表会很长

3. Setter方法注入(Setter Injection)

Setter方法注入通过Setter方法来注入依赖,适用于可选依赖或需要重新配置的依赖。

java 复制代码
@Component
public class UserController {
    
    private UserService userService;
    private OrderService orderService;
    
    // Setter方法注入
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
    @Resource
    public void setOrderService(OrderService orderService) {
        this.orderService = orderService;
    }
    
    // 可选依赖的Setter方法(required = false)
    @Autowired(required = false)
    public void setOptionalService(OptionalService optionalService) {
        this.optionalService = optionalService;
    }
    
    public void execute() {
        if (userService != null) {
            userService.sayHello();
        }
        if (orderService != null) {
            orderService.createOrder();
        }
    }
}

优点

  • 灵活性高,可以在Bean创建后重新配置依赖
  • 适用于可选依赖(通过 required = false
  • 符合JavaBean规范

缺点

  • 依赖可能为null,需要做空值检查
  • 不能保证依赖在对象创建时就完全初始化

三种注入方式对比

特性 属性注入 构造方法注入 Setter方法注入
代码简洁性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
不可变性
明确依赖 ⭐⭐⭐
易于测试 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
循环依赖检测 运行时检测 启动时检测 运行时检测
可选依赖支持 ✅(required=false)
Spring官方推荐 ⭐⭐
适用场景 快速开发、简单场景 必需依赖、不可变对象 可选依赖、需要重新配置

通过 ApplicationContextAware 获取

如果需要在非 Spring 管理的普通类中获取容器,可以实现 ApplicationContextAware 接口。

java 复制代码
@Component
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }
}

使用示例:

java 复制代码
// 在任意地方(包括非 Spring 管理的类中)获取 Bean
UserService userService = SpringContextUtil.getBean(UserService.class);
userService.sayHello();

扫描路径

Spring 容器需要知道去哪里查找带有 @Component@Service@Controller@Repository 等注解的类,这个过程叫做组件扫描(Component Scan)

默认扫描范围

在 Spring Boot 项目中,默认的扫描范围是启动类所在包及其所有子包

java 复制代码
package com.example.demo;  // 启动类所在包

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

假设项目结构如下:

复制代码
com.example.demo
├── DemoApplication.java          ← 启动类
├── controller
│   └── UserController.java       ← 会被扫描到
├── service
│   └── UserService.java          ← 会被扫描到
├── config
│   └── AppConfig.java            ← 会被扫描到
└── utils
    └── StringUtil.java           ← 会被扫描到

所有在 com.example.demo 及其子包(如 controllerserviceconfigutils)中的组件都会被自动扫描并注册到容器中。

不在默认扫描范围的情况

如果某个类放在启动类所在包之外,则不会被自动扫描:

复制代码
com.example.demo
└── DemoApplication.java          ← 启动类

com.other.module
└── OtherService.java             ← ❌ 不会被扫描到

此时 OtherService 上的 @Service 注解不会生效,Spring 容器中不会有这个 Bean。

自定义扫描路径

如果需要扫描启动类所在包之外的组件,可以通过以下方式指定扫描路径。

方式一:@ComponentScan

在启动类或配置类上添加 @ComponentScan 注解,指定要扫描的包路径:

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("com.other.module")  // 额外扫描 com.other.module 包
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

也可以指定多个包路径:

java 复制代码
@ComponentScan({"com.other.module", "com.another.module"})
方式二:@SpringBootApplication 的 scanBasePackages 属性

@SpringBootApplication 本身包含了 @ComponentScan,可以直接通过 scanBasePackages 属性指定扫描路径,更加简洁:

java 复制代码
@SpringBootApplication(scanBasePackages = {"com.example.demo", "com.other.module"})
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

注意 :使用 scanBasePackages 后,默认的扫描范围(启动类所在包)不会自动生效,需要把启动类所在包也显式写进去,否则该包下的组件不会被扫描。

方式三:使用 AnnotationConfigApplicationContext 手动指定

在非 Spring Boot 的纯 Spring 项目中,可以通过 AnnotationConfigApplicationContext 手动指定扫描路径:

java 复制代码
// 只注册 AppConfig 中的 @Bean,不会扫描 @Service 等类注解
ApplicationContext context1 = new AnnotationConfigApplicationContext(AppConfig.class);

// 指定包路径进行扫描,会扫描该包下所有带类注解的类
ApplicationContext context2 = new AnnotationConfigApplicationContext("com.example.demo");

// 从 context2 中可以获取 @Service 创建的 Bean
UserService userService = context2.getBean(UserService.class);
userService.sayHello();

扫描路径的常见问题

问题 1:Bean 找不到(NoSuchBeanDefinitionException)

最常见的原因是组件不在扫描路径内。解决方案:

java 复制代码
// 1. 检查类是否在启动类所在包或其子包中
// 2. 如果不在,使用 @ComponentScan 或 scanBasePackages 指定路径
// 3. 或者把类移到启动类所在包内
问题 2:@SpringBootApplication 与 @ComponentScan 同时使用

@SpringBootApplication 已经包含了 @ComponentScan,如果同时使用,@ComponentScan覆盖默认的扫描路径,导致启动类所在包不再被扫描。

java 复制代码
// ❌ 错误用法:@ComponentScan 覆盖了默认扫描路径
@SpringBootApplication
@ComponentScan("com.other.module")  // 此时 com.example.demo 不会被扫描
public class DemoApplication { ... }

// ✅ 正确用法:把默认包也加进去
@SpringBootApplication
@ComponentScan({"com.example.demo", "com.other.module"})
public class DemoApplication { ... }

// ✅ 或者使用 scanBasePackages 属性
@SpringBootApplication(scanBasePackages = {"com.example.demo", "com.other.module"})
public class DemoApplication { ... }
相关推荐
xixingzhe23 小时前
spring构造函数注入对比@Resource
java·后端·spring
宋哥转AI4 小时前
Java搭RAG实战(三):检索问答全链路,从架构分层到SSE流式
java·agent
测试员周周4 小时前
【Appium 系列】第17节-XMind用例转换 — 从思维导图到 YAML
java·服务器·人工智能·单元测试·appium·测试用例·xmind
NiceCloud喜云4 小时前
Claude API PDF 文档问答实战:从原生解析到分页引用的完整方案
java·服务器·前端·网络·数据库·人工智能·pdf
彦为君4 小时前
JavaSE-03-集合框架(详细版)
java·开发语言·python
Dicky-_-zhang4 小时前
API接口签名验证实战
java·jvm
java1234_小锋4 小时前
Redis 支持哪些数据类型?请分别说明它们的使用场景
java·数据库·redis
:1214 小时前
java基础---一些没注意的
java·开发语言
计算机安禾4 小时前
【c++面向对象编程】第48篇:Lambda表达式与std::function:OOP中的函数式编程
java·c++·算法