如何通过注解(@Component 等)声明一个 Bean?Spring 是如何找到这些注解的?
如何通过注解声明一个 Bean?
最核心的注解是 @Component
。它是一个通用的、万能的标记。
import org.springframework.stereotype.Component;@Component // 嘿,Spring!这个类是一个组件,请帮我创建一个Bean。
public class MyGenericComponent {public void doSomething() {System.out.println("Doing something...");}
}
为了让代码的意图更清晰,Spring 提供了几个继承自 @Component
的、具有特定语义的注解。它们在功能上与 @Component
几乎完全相同,但能更好地表达该组件在分层架构中的角色。
-
@Service
: 用于标注业务逻辑层(Service Layer)的组件。import org.springframework.stereotype.Service;@Service // 这个组件是处理业务逻辑的 public class UserServiceImpl implements UserService {// ... }
-
@Repository
: 用于标注数据访问层/持久层(Repository/DAO Layer)的组件。- 额外功能: 除了标记为 Bean,
@Repository
还能将特定于数据访问技术的异常(如 JDBC 的SQLException
)转译为 Spring 统一的、与技术无关的DataAccessException
异常体系。这使得你的业务层代码无需处理底层的特定异常。
import org.springframework.stereotype.Repository;@Repository // 这个组件是用来访问数据库的 public class MySqlUserRepository implements UserRepository {// ... }
- 额外功能: 除了标记为 Bean,
-
@Controller
/@RestController
: 用于标注表现层/控制层(Presentation Layer)的组件,通常用于 Spring MVC 或 Spring WebFlux。import org.springframework.web.bind.annotation.RestController;@RestController // 这个组件是处理Web请求的API控制器 public class UserController {// ... }
-
@Configuration
: 用于标注配置类。配置类本身也是一个 Bean,它通常包含用@Bean
注解的方法,用于以 Java 代码的方式定义其他的 Bean。import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean;@Configuration // 这个类是专门用来定义Bean的 public class AppConfig {@Bean // 这个方法返回的对象也要作为一个Bean来管理public MyCustomService myCustomService() {return new MyCustomService();} }
总结:你只需要根据组件的角色,在类上加上 @Component
、@Service
、@Repository
或 @Controller
中的一个,就完成了 Bean 的声明。
Spring 是如何找到这些注解的?(Spring 做什么)
我们仅仅在类上加了注解,Spring 是如何知道要去扫描这些类并创建 Bean 的呢?答案是:组件扫描(Component Scanning)。
这个过程就像雷达扫描一样,Spring 会在你指定的“空域”(包路径)中进行扫描,寻找带有特定注解的类。
这个过程由 @ComponentScan
注解触发。
工作流程详解:
-
启动指令:
@ComponentScan
你必须在一个配置类(一个标注了@Configuration
的类)上使用@ComponentScan
来告诉 Spring:“请开始扫描!”。import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan(basePackages = "com.myapp.service") // 告诉Spring:请扫描这个包以及它的所有子包 public class AppConfig {// ... }
basePackages
: 明确指定要扫描的包路径。可以指定多个。- 默认行为: 如果不指定
basePackages
,Spring 会默认从当前配置类所在的包开始扫描。这是 Spring Boot 的核心约定,@SpringBootApplication
注解内部就包含了@ComponentScan
,所以它会自动扫描主启动类所在的包及其所有子包。
-
扫描执行:
ClassPathBeanDefinitionScanner
当 Spring IoC 容器启动并加载AppConfig
时,它会解析到@ComponentScan
注解。然后,它会内部创建一个名为ClassPathBeanDefinitionScanner
的扫描器。 -
遍历类路径
这个扫描器会像一个文件浏览器一样,根据你提供的basePackages
,去文件系统(或 JAR 包)的类路径下,递归地查找所有.class
文件。 -
读取元数据
对于找到的每一个.class
文件,Spring 不会立即加载整个类到 JVM 中(这样效率太低)。它会使用一种称为 ASM 的字节码操作库,直接读取.class
文件的二进制内容,从中解析出类的元数据,比如类名、接口、以及最重要的——类上的注解。 -
识别候选者
扫描器会检查每个类的元数据,看它是否被@Component
或其衍生注解(@Service
,@Repository
等)所标记。 -
创建 BeanDefinition
如果一个类被识别为候选者,Spring 并不会立刻创建这个类的实例。相反,它会为这个类创建一个“Bean 的定义信息”对象,即BeanDefinition
。
BeanDefinition
就像一个菜谱,它描述了如何创建这个 Bean 的所有信息,包括:- Bean 的类名 (e.g.,
com.myapp.service.UserServiceImpl
)。 - Bean 的作用域(Scope),默认是
singleton
(单例)。 - 是否是懒加载(Lazy Init)。
- 依赖关系(比如它需要注入哪些其他的 Bean)。
- Bean 的类名 (e.g.,
-
注册 BeanDefinition
所有创建好的BeanDefinition
都会被注册到一个名为BeanDefinitionRegistry
的中央注册表中。至此,扫描阶段完成。容器现在已经“知道”了所有需要它管理的 Bean 的蓝图。 -
实例化 Bean
最后,在容器生命周期的后续阶段(通常是容器刷新完成时,或者第一次请求该 Bean 时),容器会根据BeanDefinitionRegistry
中的“菜谱”(BeanDefinition
),通过反射来创建 Bean 的实例,执行依赖注入,并完成初始化,最终将一个功能完备的 Bean 放入容器的单例缓存池中,以备后用。
流程总结
你做什么 (声明) | Spring 做什么 (发现和管理) |
---|---|
1. 在你的类上添加 @Component 或其衍生注解 (@Service , @Repository 等)。 | 1. 启动时加载一个配置类 (@Configuration )。 |
2. 在配置类上添加 @ComponentScan ,告诉 Spring 从哪里开始扫描。 | 2. 发现 @ComponentScan 注解,获取要扫描的包路径。 |
3. 使用扫描器在指定的类路径下查找所有 .class 文件。 | |
4. 高效读取每个 .class 文件的元数据(特别是注解)。 | |
5. 识别出被 @Component 等注解标记的类。 | |
6. 为每个识别出的类创建一个 BeanDefinition (Bean 的蓝图/菜谱)。 | |
7. 将所有 BeanDefinition 注册到容器的注册表中。 | |
8. 在需要时,根据 BeanDefinition 实例化、注入并初始化真正的 Bean 对象。 |
这个“注解声明 + 组件扫描”的机制是 Spring 框架实现自动化配置和依赖注入的基石,也是让 Spring Boot 能够实现“约定优于配置”的关键技术。