Spring Bean 管理与依赖注入实践

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 冲突、启动失败、并发问题时会非常有底气。

相关推荐
派大鑫wink2 小时前
【Day38】Spring 框架入门:IOC 容器与 DI 依赖注入
java·开发语言·html
独自破碎E2 小时前
什么是Spring Bean?
java·后端·spring
手握风云-2 小时前
JavaEE 进阶第十期:Spring MVC - Web开发的“交通枢纽”(四)
前端·spring·java-ee
人道领域2 小时前
JavaWeb从入门到进阶(Maven的安装和在idea中的构建)
java·maven
XXOOXRT2 小时前
基于SpringBoot的留言板
java·spring boot·后端
小楼v2 小时前
常见的Java线程八股
java·后端·线程
沛沛老爹2 小时前
从Web到AI:金融/医疗/教育行业专属Skills生态系统设计实战
java·前端·人工智能·git·金融·架构
洛_尘2 小时前
Java EE进阶4:Spring Web MVC入门
java·java-ee
寂寞旅行2 小时前
IDEA 中使用 claude code 插件
java·ide·intellij-idea