如何理解 Spring 当中的 Bean?

面试里经常被问到一个问题:什么是Spring Bean?

工作三五年的开发者,大部分人的回答是「被Spring管理的对象」。这个回答不算错,但信息量约等于零。所有关键的东西都藏在「管理」这两个字里。管理了什么?怎么管理的?为什么需要管理?追问一层就接不住了。

问题出在哪里?这个定义是用Spring来解释Bean,没有跳出框架去理解它。你记住了一个说法,但追问一层你会发现自己说不清楚。

要搞清楚Bean到底是什么,得先把Spring放到一边,回到一个更根本的问题:在没有Spring的时候,我们是怎么写代码的,遇到了什么麻烦。

想象一个典型的订单系统,三层架构:OrderController处理请求,OrderService处理业务逻辑,OrderRepository和UserRepository负责数据库操作,底层共用一个DataSource。没有任何框架,全靠手动组装,启动代码大概长这样:

Java 复制代码
public class Application {
    public static void main(String[] args) {
        // 创建数据源
        DataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");

        // 创建数据访问层
        OrderRepository orderRepository = new OrderRepository(dataSource);
        UserRepository userRepository = new UserRepository(dataSource);

        // 创建业务层
        OrderService orderService = new OrderService(orderRepository, userRepository);

        // 创建控制层
        OrderController orderController = new OrderController(orderService);

        // 启动服务器,把controller注册进去...
    }
}

这段代码功能上完全没问题,小项目跑得很稳。问题出在规模增长之后。现在只有4个对象,手动组装还看得清楚。一个真实的Spring Boot项目里,几十上百个Service、Repository、Controller、各种配置类、中间件客户端,它们之间的依赖关系是一张网。OrderService依赖OrderRepository和UserRepository,PaymentService依赖OrderService和PaymentGateway,NotificationService依赖UserRepository和EmailClient......每加一个新类,你都得回到这个启动代码里,搞清楚它依赖谁、谁依赖它,然后手动把依赖关系对接好。改一个类的构造器签名,所有创建它的地方都要跟着改。

很多人以为Spring的价值是「省了几行new的代码」。不是。手动new十几个对象不费事。费事的是几百个对象之间的依赖关系全靠你自己理清楚,每一次变动都可能牵连好几处。真正的痛点不是对象的创建,而是对象之间依赖关系的管理。

面对这个问题,解决的思路可以是这样子:搞一个统一的地方,专门负责创建所有对象、管理它们之间的依赖关系。你只需要告诉这个地方「我有哪些类、它们各自需要什么」,剩下的创建和组装工作全交给它。

这个思路就是容器的雏形。Spring容器干的事:你告诉我有哪些类需要管理,我来负责创建它们、把它们之间的依赖关系接好、在合适的时机销毁它们。

光说概念太抽象,不如自己写一个最简版本的容器,看看它的核心到底是什么。下面这段代码大概50行,能注册类、创建对象、自动注入依赖:

Java 复制代码
public class SimpleContainer {

    // 存储注册信息:Bean名字 → 类的Class对象
    private Map<String, Class<?>> registry = new HashMap<>();

    // 存储已经创建好的单例对象
    private Map<String, Object> singletonCache = new HashMap<>();

    // 注册一个类,告诉容器需要管理它
    public void register(String name, Class<?> clazz) {
        registry.put(name, clazz);
    }

    // 获取Bean:先查缓存,没有就创建
    public Object getBean(String name) {
        // 先查单例缓存,有就直接返回
        if (singletonCache.containsKey(name)) {
            return singletonCache.get(name);
        }

        Class<?> clazz = registry.get(name);
        if (clazz == null) {
            throw new RuntimeException("没有找到名为 " + name + " 的Bean定义");
        }

        try {
            // 通过反射创建对象
            Object instance = clazz.getDeclaredConstructor().newInstance();

            // 扫描这个对象的所有字段,尝试自动注入依赖
            for (Field field : clazz.getDeclaredFields()) {
                // 在registry里找类型匹配的Bean
                for (Map.Entry<String, Class<?>> entry : registry.entrySet()) {
                    if (field.getType().isAssignableFrom(entry.getValue())) {
                        field.setAccessible(true);
                        // 递归获取依赖的Bean(触发依赖的创建和注入)
                        field.set(instance, getBean(entry.getKey()));
                    }
                }
            }

            // 放入单例缓存
            singletonCache.put(name, instance);
            return instance;
        } catch (Exception e) {
            throw new RuntimeException("创建Bean失败: " + name, e);
        }
    }
}

用起来是这样的:

Java 复制代码
SimpleContainer container = new SimpleContainer();

// 告诉容器有哪些类
container.register("dataSource", HikariDataSource.class);
container.register("orderRepository", OrderRepository.class);
container.register("userRepository", UserRepository.class);
container.register("orderService", OrderService.class);
container.register("orderController", OrderController.class);

// 直接从容器拿,依赖自动接好了
OrderController controller = (OrderController) container.getBean("orderController");

对比之前手动组装的代码,变化在哪?你不再关心「谁该传给谁」这件事了。你只管告诉容器有哪些类,容器自己分析它们的字段类型,去registry里找匹配的Bean,递归地完成整条依赖链的创建和注入。

这段代码虽然粗糙,但它揭示了Spring容器最核心的骨架:一个Map存「有哪些东西需要管理」,另一个Map存「已经创建好的成品」。 所有Spring容器的复杂性,生命周期回调、作用域管理、循环依赖处理、AOP代理,都是在这个骨架上叠加的特性。骨架没变过,只是功能越来越丰富。

有人会说,这也太简化了吧?Spring的真实实现比这复杂得多。确实。这个简化版忽略了很多东西:构造器参数怎么选择、同一个类型有多个Bean怎么办、延迟加载怎么处理、循环依赖怎么解决。这些都是工程实践中必须面对的问题。

但理解一个框架,要先把骨架看清楚,再去看长在骨架上的血肉。

你知道了核心是两个Map,后面接触到每一个高级特性,都能定位到它是在修改哪个Map、在哪个环节介入。

上面的简化容器有一个明显的不足:registry里只存了一个Class对象。也就是说,容器只知道「创建什么」,但不知道很多其他信息。实际场景中,一个对象被容器管理,容器需要知道更多的事情:

  • 这个对象是整个应用只要一份(单例),还是每次请求都新建一份?
  • 它需不需要延迟创建(应用启动时不创建,等真正用到时再创建)?
  • 它依赖其他哪些Bean,这些Bean必须先创建好?
  • 创建完成后,有没有什么初始化动作要执行(比如连接池要预热、缓存要加载)?
  • 应用关闭时,有没有什么清理动作要做(比如关闭连接池、释放文件句柄)?
  • 同一个类型有多个Bean时,哪个是默认优先使用的?

这些信息需要一个结构化的地方来存储。在Spring里,这个结构叫Bean定义,你可以理解成一份「制造说明书」。用代码来表达,大概是这样的结构:

Java 复制代码
public class BeanDefinition {

    // 这个Bean对应的类
    private Class<?> beanClass;

    // 作用域:singleton表示全局一份,prototype表示每次新建
    private String scope = "singleton";

    // 是否延迟初始化
    private boolean lazyInit = false;

    // 依赖的其他Bean名字,这些Bean必须在它之前创建
    private String[] dependsOn;

    // 初始化时要调用的方法名
    private String initMethodName;

    // 销毁时要调用的方法名
    private String destroyMethodName;

    // 同类型多个Bean时,是否优先使用这个
    private boolean primary = false;
}

有了这个视角,Bean和普通Java对象的区别就清晰了。你在代码里写new OrderService(),JVM只是调一下构造器,创建一个对象放在堆上,别的什么都不管。而容器管理的Bean,背后有一份完整的制造说明书,容器知道它什么时候创建、怎么创建、创建之后做什么、销毁之前做什么。Bean = 对象 + 制造说明书 + 容器的全程托管。 三样东西少了任何一个,都不叫Bean。

容器有了说明书,创建一个Bean的过程也就不是简单地调一下构造器了。它有一整套标准化的流程。这里不展开每一步的实现细节,只画一条时间线,先有个整体印象:

用一个不太严格但方便理解的类比:

这个过程像装修一套房子。买到毛坯房是实例化,这时候房子有了,但什么都没有。接水电气是属性填充,把它需要的各种依赖(水、电、网络)接进来。验收检查是初始化回调,确认一切就绪,做最后的调试和测试。然后是入住使用。最后是拆迁,清理所有资源,把占用的东西归还。

生命周期不是框架在故弄玄虚。一个对象从创建到真正可用之间,确实有很多事情要做。DataSource需要初始化连接池,缓存需要预热,消息监听器需要注册到消息队列。这些不是构造器能一步搞定的。容器把这些步骤标准化,每个步骤还留了扩展点,让你在特定阶段插入自己的逻辑。

回到一个更实际的问题:你怎么告诉Spring,你的哪些类需要被当成Bean来管理?

最常见的方式是在类上面加注解。Spring提供了四个用来标记Bean的注解:@Component、@Service、@Repository、@Controller。很多教程告诉你,Service层用@Service,Controller层用@Controller,DAO层用@Repository。但很少有人解释为什么。

这四个注解之间的关系,简单说就是:@Service、@Repository、@Controller都是@Component的「派生版」。它们的定义方式类似这样:

Java 复制代码
// @Service的定义上面标注了@Component
@Component
public @interface Service {
    String value() default "";
}

// @Repository的定义上面标注了@Component
@Component
public @interface Repository {
    String value() default "";
}

// @Controller的定义上面标注了@Component
@Component
public @interface Controller {
    String value() default "";
}

Spring在扫描包路径的时候,查找的是带有@Component注解的类。因为@Service、@Repository、@Controller上面都标注了@Component,所以它们也会被扫描到。从「把一个类注册成Bean」这件事来看,这四个注解完全等价。你把所有@Service换成@Component,项目一样能正常跑。

那为什么不全用@Component?

从两个角度看。一是语义表达。其他开发者看到@Repository就知道这是数据访问层,看到@Service就知道这是业务层。注解在这里承担了代码文档的角色。在三五个人的小团队里,这点区别可能感受不明显。几十人的项目里,注解的语义信号对代码理解效率的影响比想象中大得多。

二是框架层面的区别对待。虽然注册Bean时这四个注解等价,但Spring在后续处理中会对特定注解做额外的事情。@Repository标记的类,Spring会给它加一层异常转译,把数据访问层抛出的底层异常(比如JDBC的SQLException)自动包装成Spring统一的数据访问异常体系。@Controller标记的类,Spring MVC会把它识别为请求处理器,扫描里面的@RequestMapping方法来建立请求映射。这些特殊处理不在Bean注册阶段,而是在各自模块的后处理阶段。

@Component系列注解适用于你自己写的类。遇到第三方库的类,比如你想把一个RestTemplate注册成Bean,没法去改RestTemplate的源码加@Component。这时候用@Bean注解,在一个配置类里写一个方法,方法返回值就是你要注册的Bean:

Java 复制代码
@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

@Component是标记一个类让Spring自动扫描并注册,@Bean是在方法上手动告诉Spring怎么创建一个对象。两种方式的终点相同:往容器的注册表里写入一条Bean定义。

这里有一个容易忽略的细节:上面这个配置类用的是@Configuration,不是@Component。虽然@Configuration本身也是@Component的派生注解(也会被扫描为Bean),但它有一个@Component不具备的特殊行为。

看这个场景:

Java 复制代码
@Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:mysql://localhost:3306/demo");
        ds.setUsername("root");
        ds.setPassword("123456");
        return ds;
    }

    @Bean
    public OrderRepository orderRepository() {
        // 注意:这里调用了dataSource()方法
        return new OrderRepository(dataSource());
    }

    @Bean
    public UserRepository userRepository() {
        // 这里也调用了dataSource()方法
        return new UserRepository(dataSource());
    }
}

orderRepository()和userRepository()各调用了一次dataSource()。如果这是普通的Java方法调用,两次调用会执行两次new HikariDataSource(),创建出两个不同的数据源对象。两个Repository各持有一个独立的连接池,这显然不是你想要的。

实际运行时,两个Repository拿到的是同一个DataSource实例。原因是Spring在启动时给@Configuration类生成了一个动态子类(通过CGLIB字节码技术)。这个子类覆盖了所有@Bean方法的执行逻辑:当你在方法内部调用dataSource()时,执行的不是原始的new逻辑,而是先去容器的单例缓存里查。如果dataSource这个Bean已经创建过了,直接返回缓存里的实例。

我以前一直觉得@Configuration和@Component没什么本质区别,直到在一个项目里踩了坑。排查一个连接池告警,发现数据库连接数莫名其妙翻倍了。查到最后,是有人把一个配置类的@Configuration改成了@Component(可能是觉得既然都能注册Bean,用哪个都一样)。改完之后,两个Repository各自持有了一个独立的DataSource,连接池被创建了两份。这才意识到@Configuration的CGLIB代理在保证@Bean方法的单例语义上是不可少的。

有一个相关的优化值得提一下:如果你的@Configuration类里的@Bean方法之间没有互相调用的情况,可以用@Configuration(proxyBeanMethods = false)主动关掉CGLIB代理。Spring Boot的很多自动配置类就是这么做的,能加快应用启动速度。这是一个知道原理之后才能做出的优化判断。

容器创建好了Bean,接下来的问题是怎么把它们之间的依赖关系接好。这就是依赖注入。日常开发中最常用的两个注入注解是@Autowired和@Resource,它们的核心行为差异用一句话就能说清:@Autowired按类型找,@Resource按名字找。

@Autowired的匹配逻辑是:先按字段的类型去容器里查,如果只找到一个,直接注入。如果同一个类型有多个Bean,再看字段名是否和某个Bean的名字匹配。@Resource正好反过来,默认按字段名去容器里查Bean名字,名字匹配不上再按类型找。

实际项目里选哪个?团队保持一致就好。Spring官方现在更推荐构造器注入,连@Autowired注解都可以不写。当一个类只有一个构造器时,Spring会自动把构造器参数从容器中注入:

Java 复制代码
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;

    // 只有一个构造器,Spring自动注入参数,不需要写@Autowired
    public OrderService(OrderRepository orderRepository, UserRepository userRepository) {
        this.orderRepository = orderRepository;
        this.userRepository = userRepository;
    }
}

构造器注入有一个好处:依赖字段可以声明为final,对象创建之后依赖关系不可变。字段注入做不到这一点。

日常开发中的Bean

讲了这么多原理,回到一个实际的问题:你每天写的代码里,哪些东西是Bean?

下面这张表覆盖了一个典型Spring Boot项目中常见的Bean来源:

分类 典型代表 谁注册的 你需要做什么
你自己写的业务类 XxxService、XxxController、XxxRepository 你加@Component系列注解 在类上标注注解
你手动配置的Bean RestTemplate、ObjectMapper、线程池、拦截器 你在@Configuration里写@Bean方法 写配置类和@Bean方法
Spring Boot自动配置的 DataSource、JdbcTemplate、RedisTemplate、事务管理器 spring-boot-autoconfigure里的自动配置类 引入对应的starter依赖即可,不需要写任何代码
Spring MVC基础设施 DispatcherServlet、HandlerMapping、参数解析器 Spring MVC自动注册 引入spring-boot-starter-web
AOP相关 你写的@Aspect类、各种通知 Spring AOP框架 加@Aspect和切点表达式
事件机制 ApplicationEventPublisher、你写的@EventListener方法所在的类 Spring容器 实现接口或加注解
你以为不是Bean但其实是的 @Configuration类本身、@Aspect类本身 Spring自动识别并注册 通常不需要额外感知

一个容易产生误区的地方:不是所有Java对象都应该注册成Bean。Entity、DTO、VO这些数据传输对象不该是Bean。它们的生命周期跟随请求或业务流程,每次请求可能产生不同的实例,这和容器管理单例的模式是冲突的。工具类如果全是静态方法,也不需要注册成Bean。

判断标准就两条:这个对象需不需要被别的Bean依赖注入?需不需要容器管理它的生命周期(初始化、销毁)?两个答案都是否,它不该是Bean。

下面这张对比表可以帮你快速区分Bean和普通对象:

维度 Spring Bean 普通Java对象
创建方式 容器根据Bean定义创建 代码里直接new
生命周期 容器全程托管(创建→注入→初始化→使用→销毁) 创建者管理,GC回收
依赖注入 容器自动完成 手动通过构造器或setter传参
默认实例数 单例(整个应用一份) 每次new都是新对象
AOP支持 可以被代理增强(事务、日志、权限等) 不支持
能否被其他Bean依赖 可以,容器知道它的存在 不行,容器感知不到
典型代表 Service、Controller、Repository、配置类、中间件客户端 Entity、DTO、VO、工具类

小结

回到开头那个面试问题。读到这里,对Bean的认知应该不再停留在「被Spring管理的对象」这个层面了。容器的核心是两个Map:一个存制造说明书,一个存创建好的成品。Bean是容器基于说明书创建、组装依赖、管理全生命周期的对象。你用@Component告诉容器哪些类需要管理,用@Bean告诉容器怎么创建第三方类的实例,容器负责剩下的一切。

做了十几年项目,我觉得理解Bean这个概念有一个容易走偏的方向:过度关注容器的内部实现细节,去追每一行源码的执行路径,反而忘了停下来想一想「什么该交给容器管理、什么不该」。项目里见过不少把DTO、值对象甚至临时变量都注册成Bean的代码,容器里塞了一堆不需要被管理的东西,代码反而更难读了。Bean的设计初衷是让你专注于业务逻辑,把对象的创建和组装交给框架。如果一个对象被注册成Bean之后,代码没有因此变得更简单,那它可能不该是Bean。

希望这篇内容可以帮到你。

最近在知乎出了秒杀专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:

老码头的技术浮生录

它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥。

我的知乎账号:

  • SamDeepThinking
相关推荐
Nyarlathotep01132 小时前
类加载机制(2):虚拟机类加载过程
jvm·后端
敖正炀2 小时前
阻塞队列-0-3-最佳实践
java
kevinzeng2 小时前
Java Stream 流式编程 10天系统学习计划
java
Leo8992 小时前
rocketmq从零单排
后端
摇滚侠2 小时前
Java 零基础全套视频教程,面向对象(进阶),笔记 90-103
java·开发语言·笔记
一点一一2 小时前
nestjs+langchain:Output Parsers+调用本地大模型
人工智能·后端
椰羊~王小美2 小时前
C、Java、Go、Python 对比
java·c语言
Gerardisite2 小时前
企业微信自动化开发新思路: RPA 接入方案
java·python·自动化·企业微信·rpa