Bean 管理是什么
Bean 管理可以理解为:对象从"出生"到"退场"的全过程都由 Spring 容器托管。具体包括:
- 创建:何时创建对象、用哪个构造器创建、是否单例。
- 装配:把依赖注入进去(依赖谁、按什么规则找、找不到怎么办)。
- 生命周期:初始化前后做什么、销毁时释放什么资源。
- 作用域:一个容器里是共享一个实例,还是每次都新建实例。
在没有容器之前,这些事情通常散落在各处:某个地方 new、某个地方手动 set、某个地方 try/catch 关闭资源。Spring 把它们统一收口,让对象关系更清晰、出错更早暴露、维护成本更低。
两条主流路径
- XML 配置:显式、集中,适合遗留或需外置配置的场景。它的好处是"配置与代码分离",不改代码也能调整对象装配;缺点是配置量大时不易维护,且类型检查弱(拼写错误往往要运行才知道)。
- 注解/Java Config:简洁、类型安全,是当前主流做法。注解把"这个类是什么角色、依赖谁"直接写在代码附近,阅读和重构都更顺手;配合
@Configuration/@Bean还能写出更可控的装配逻辑(例如条件装配、根据环境创建不同实现)。
很多真实项目会混用:业务层主要用注解,少量第三方或复杂装配用 @Bean 或 XML 显式声明。
XML 管理与注入示例
类定义
java
package demo.xml;
public class UserRepository {}
public class UserService {
private UserRepository userRepository;
private String appName;
public void setUserRepository(UserRepository repo) { this.userRepository = repo; }
public void setAppName(String appName) { this.appName = appName; }
public String hello() { return "Hi from " + appName + " with " + userRepository; }
}
这是一种典型的 Setter 注入 写法:类本身不依赖 Spring API,只提供 setter 让容器在创建对象后"把依赖塞进去"。优点是简单、对框架无侵入;缺点是依赖不是强制的(你可以忘记注入),如果依赖必不可少,最好改用构造器注入来强约束。
XML 配置
xml
<bean id="userRepo" class="demo.xml.UserRepository"/>
<bean id="userService" class="demo.xml.UserService">
<property name="userRepository" ref="userRepo"/>
<property name="appName" value="XMLApp"/>
</bean>
这里有两个关键点:
ref表示引用另一个 Bean(对象引用注入),例如把userRepo注入给userService。value表示注入简单值(字符串、数字等)。真实项目中更常见的是注入外部配置(例如数据库地址、开关配置),避免写死在 XML 里。
常见坑:property name="userRepository" 必须与 setter 匹配(即 setUserRepository),否则会报属性找不到或注入失败。
作用域与懒加载
xml
<bean id="cache" class="demo.xml.CacheService" scope="prototype" lazy-init="true"/>
这一行同时演示了两个常用控制项:
scope="prototype":每次取cache都是新对象,适合"有状态、不能共享"的场景。但要注意:prototype Bean 的销毁回调不一定由容器完整托管,资源释放需要你更谨慎。lazy-init="true":懒加载,第一次用到再创建。适合启动期不一定会用到的重资源 Bean(例如某些外部连接),但也可能把错误推迟到运行时才暴露。
注解/Java Config 管理与注入示例
常用注解
- 组件:
@Component/@Service/@Repository/@Controller - 注入:
@Autowired、@Qualifier、@Value - 配置:
@Configuration、@Bean、@ComponentScan - 范围与懒加载:
@Scope("prototype")、@Lazy
这组注解可以按"职责"理解:
@Component是最通用的组件标记;@Service/@Repository/@Controller是语义化的细分,能让代码层次更清晰(有些场景还会启用额外能力,例如@Repository与异常转换相关)。@Autowired负责"按类型注入";当同一类型有多个实现时,通常需要@Qualifier明确指定。@Value用来注入配置值,既可以来自 properties/yaml,也可以写默认值(如:DemoApp)。@Configuration/@Bean是"显式装配",适合第三方类(无法加注解)或需要写一点创建逻辑的 Bean。
构造器优先注入(推荐)
java
package demo.anno;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
public class UserRepository {}
@Service
public class UserService {
private final UserRepository repo;
@Value("${app.name:DemoApp}")
private String appName;
public UserService(UserRepository repo) { this.repo = repo; }
public String hello() { return "Hi from " + appName + " with repo " + repo; }
}
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan("demo.anno")
@PropertySource("classpath:application.properties")
public class AnnoConfig {}
为什么推荐构造器注入?
- 强约束:依赖是构造器参数,创建对象时就必须提供;避免"忘记注入导致空指针"。
- 更易测试 :单测里可以直接
new UserService(mockRepo),不必启动 Spring 容器。 - 更可维护:依赖列表一目了然,类的职责边界更清晰。
@Value("${app.name:DemoApp}") 表示:先从配置里找 app.name,找不到就用默认值 DemoApp。这是一种很实用的"安全兜底"写法,避免缺配置直接启动失败(当然,对强依赖配置也可以选择不兜底,让它尽早失败)。
application.properties
app.name=MyBlogApp
启动:
java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AnnoApp {
public static void main(String[] args) {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AnnoConfig.class)) {
UserService svc = ctx.getBean(UserService.class);
System.out.println(svc.hello());
}
}
}
这里用 try (...) 语法自动关闭 AnnotationConfigApplicationContext,相当于触发容器的销毁流程。真实应用中(Web/Boot)一般由框架管理生命周期,但在学习阶段手动 close() 能帮助你理解销毁回调与资源释放。
@Bean 显式定义
java
@Configuration
public class BeanConfig {
@Bean
public UserRepository userRepository() { return new UserRepository(); }
@Bean
public UserService userService(UserRepository repo) { return new UserService(repo); }
}
@Bean 的两个典型使用场景:
- 第三方类:例如某个库里的
Client不能加@Component,你就用@Bean把它交给 Spring 管。 - 需要自定义创建逻辑:例如根据环境选择不同实现、设置初始化参数、做连接校验等。
另外注意:userService(UserRepository repo) 这种写法本身就是"依赖注入"------Spring 会自动把容器里的 UserRepository 作为参数传进来,这种方式既显式又类型安全。
纯注解最小 IOC 示例
java
@Configuration
@ComponentScan("demo.min")
public class MiniConfig {}
@Component
class A { }
@Component
class B {
private final A a;
public B(A a) { this.a = a; }
}
public class MiniApp {
public static void main(String[] args) {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MiniConfig.class)) {
System.out.println(ctx.getBean(B.class));
}
}
}
这个最小示例想说明两件事:
@ComponentScan让容器自动发现并注册 Bean;你不需要手动写一堆<bean>或@Bean。B只声明依赖A,容器会负责把A注入给B。当依赖链变长时(A→B→C→D),容器的价值会成倍体现出来:你不用再写一层层工厂去组装对象。
依赖注入方式对比
- 构造器注入:首选,保证必需依赖、利于不可变性和测试。适合绝大多数 Service/Domain 对象,尤其是依赖不可缺少的场景。
- Setter 注入:适合"可选依赖"或需要运行时替换/延迟设置的场景。它也更容易与某些老代码/框架适配,但要小心空指针与对象处于"半初始化"状态的风险。
- 字段注入:不推荐。它隐藏依赖(从类的 API 看不出来需要什么)、不利于单测(必须靠反射或启动容器),也不利于不可变对象设计。团队规范里通常会禁止或限制字段注入。
补充:当同一类型存在多个实现时,@Autowired 可能会报 "NoUniqueBeanDefinitionException"。解决方式一般是:
- 给其中一个实现标
@Primary,作为默认注入候选; - 或使用
@Qualifier("beanName")明确指定; - 或把注入类型改为
List<Interface>/Map<String, Interface>获取全部实现再自行选择。
最佳实践
- 拆分配置:按领域/层次(如 DataConfig、ServiceConfig、WebConfig)。配置类过大时会变成"垃圾场",拆分后更利于定位与复用。
- 外部化配置:把环境相关内容(地址、账号、开关、阈值)放到 properties/yaml。这样同一套代码可以在开发/测试/生产用不同配置运行,避免硬编码和频繁改包发布。
- 循环依赖:尽量通过重构消除(例如拆分职责、引入事件/回调、抽接口)。必要时用 Setter 或
@Lazy作为缓解,但这通常意味着设计上存在耦合过深的问题。 - 作用域:默认单例最省资源,但单例要避免持有"可变共享状态"(尤其是 Web 并发场景)。确需每次新对象才用
prototype,并明确资源释放策略。 - 日志与监控:把通用日志、耗时统计、链路追踪等放到 AOP/拦截器中统一处理,避免业务代码里到处
start = now()、log.info()。 - 命名与分层:组件注解尽量符合语义(Repository/Service/Controller),包结构清晰(如
controller/service/repository),这会显著提升团队协作效率。
小结
XML 与注解都能完成 Bean 管理与依赖注入:XML 更显式集中,注解/Java Config 更简洁现代。无论哪种方式,推荐以 构造器注入 作为默认方案,并坚持 外部化配置 与清晰分层。掌握"注入规则(按类型/按名称)+ 作用域 + 生命周期"三件事,你在排查 Bean 冲突、启动失败、并发问题时会非常有底气。