☰
Spring Bean vs JavaBean vs EJB:从容器管理看本质区别
2026/10/14 10:48:58 网站建设 项目流程

1. Bean这个词,到底在说什么

1.1 从一次面试场景说起

前阵子帮一个团队做技术面试,候选人简历上写着精通Spring,我就随口问了一句:Spring Bean和JavaBean有什么区别。没想到对方愣了半天,最后憋出一句:Bean不就是JavaBean吗,比较规范的Java对象。这话也不能算错,但要是放在今天,往深了说,这两个东西完全不在同一个维度上。

Bean这个词在Java世界里被撑大了。最早它指的是JavaBean那套规范,后来Spring把自己的容器管理对象也叫Bean,再后来企业级分布式领域又冒出一批EJB、Jakarta Bean。名字都叫Bean,但各自的职责边界、生命周期、运行环境完全不一样。如果你现在还在用“Bean等于JavaBean”这个认知去理解Spring,或者用Spring Bean的思路去套EJB,遇到真实项目大概率会踩坑。

这篇文章我想把三层含义彻底拆开讲清楚:原生JavaBean规范是什么,Spring Bean到底做了什么让控制反转,企业级Bean(EJB和Jakarta Bean)解决的是什么问题。同时会给出实操层面的建议,包括怎么注册一个Bean、生命周期回调的顺序、哪里容易出问题,以及面试时怎么回答才显得有底气。

1.2 三层含义的界限

先给个粗线条的框架,后面逐步展开。

JavaBean是1996年前后随JDK 1.1出现的组件规范,核心目标是复用,让开发工具可以通过反射识别一个对象的结构。它强调的是一套编码约定:无参构造、私有属性配getter/setter、可实现序列化。这里的关键词是约定,没有容器,没有依赖注入,没有生命周期管理,就是一个普通对象加上统一的取名规则。

Spring Bean是Spring IoC容器管理的对象。上一行说“没有容器”,那这一层的核心就是有容器。你写的类只是原料,容器负责创建、装配、存储、销毁,整个对象的命运被容器接管。这才是反转——创建对象的控制权从程序员手里转到了容器手里。所以Spring Bean关注的不只是类的写法,更是Bean的生命周期、作用域、依赖关系、代理方式这些运行时维度。

企业级Bean(EJB、Jakarta Bean)解决的是更大规模的问题:分布式事务、远程调用、跨进程安全。EJB在2000年前后是Java EE的支柱,后来因为过度设计被Spring抢了风头。Jakarta Bean是算上CDI(Contexts and Dependency Injection)之后的新语境,核心还是“上下文中的依赖注入”。它和Spring Bean解决的是同类问题,但属于不同技术栈的不同实现。

看懂这三层,你就能明白为什么大家在项目里聊Bean时,有时指类怎么设计,有时指容器怎么管理,有时直接上升到部署和集群。

2. JavaBean:被“塞进规范”的普通对象

2.1 原生规范的四条约定

JavaBean的本质是一份编码约定,官方文档称之为JavaBeans规范。它要求一个类满足以下四个条件:

  • 类必须有一个公共无参构造方法。如果写了有参构造,必须同时保留无参的,否则反射创建实例时会失败。
  • 属性私有化,通过getter/setter方法访问。布尔类型的getter可以写成isXxx形式。
  • 属性命名要遵循驼峰规范,setXxx对应xxx字段,这样工具才能通过方法名反推出属性名。
  • 最好实现java.io.Serializable接口,用于对象传输和持久化。

你仔细看就会发现,这套规范通篇没有提任何Spring注解,也没有依赖注入,更没有AOP。它要解决的核心问题是:让第三方工具和框架在没有源码、只有运行时对象的情况下,依然能识别和处理这个类。

举个最直观的例子,Java Swing时代的可视化IDE。你在设计器里拖一个按钮,属性面板列出它的text、background、enabled等属性,IDE怎么知道有哪些属性?就是靠getter/setter的命名规律反射解析出来的。没有这套规范,IDE只能硬编码去读字段,读不到私有字段就什么也做不了。

2.2 为什么JavaBean能支撑序列化和工具链

序列化也是JavaBean很重要的应用。比如两个人之间传一个对象,最朴素的做法是整块内存数据直接复制,但这在不同虚拟机、不同时间点上根本不可行。序列化把对象状态变成字节流,再通过网络或文件恢复出来。而JavaBean要求实现Serializable接口,正好为这个过程提供支持。

这里有个连老手都可能忽视的细节:如果你写了自定义的serialVersionUID,反序列化时版本变化就不会直接抛异常;如果不写,JVM会根据类结构自动计算,只要类结构稍微变一下,反序列化就会报InvalidClassException。我的建议是业务上要落库的对象,都显式加一个serialVersionUID,这是JavaBean实践里最便宜的保障。

2.3 为什么现在写业务代码不刻意追求JavaBean

今天你在Spring项目里写的一个Controller、Service、Repository,其实并不符合严格意义上的JavaBean规范——它们大多没有无参构造(尤其是构造器注入的类),也没有为每个业务字段写一堆getter/setter。这没问题,因为JavaBean要解决的是可视化组件的复用问题,而现在的后端开发是另一个战场。

更常见的做法是,把“纯数据载体”单独设计成POJO或者DTO。POJO(Plain Ordinary Java Object)和JavaBean不是一回事。POJO强调“不继承框架类、不实现框架接口、不掺杂容器注解”,比JavaBean更纯粹,也更自由。你可以有有参构造,可以没有getter/setter,只要它是普通Java对象就行。

所以现在的分层实践中,领域模型类通常走POJO路线,配上Lombok之类的工具生成访问方法;而能被Spring容器管理的Bean类,则会把重心放在构造方式、生命周期和依赖关系上。你要是还在纠结“这个类没有无参构造,是不是就不算Bean”,大概率是被JavaBean的旧规范教条绑住了。

3. Spring Bean:容器的世界观(重点)

3.1 一个类变成Bean,到底发生了什么

Spring Bean和JavaBean最大的区别,在于存在一个容器。类还是那个类,但经过容器处理后,它的创建、初始化、销毁都不是由你直接调用的。你只需要告诉容器“这个类需要被管理”,剩下的事交给IoC容器。

用一个生活类比:JavaBean像是菜市场买回来的菜,你拿回家自己洗、自己切、自己做;Spring Bean是去自助餐厅,你只要把选好的菜放到称重台,后厨帮你洗切烹饪,端到你面前的是成品。控制权从“你”转移到了“餐厅后厨”,这就是控制反转。

在Spring里,把一个类注册成Bean有三种主流方式:

  • XML配置:用 标签声明,老项目常见,新项目很少用。
  • 注解扫描:在类上标注@Component及其派生注解,配合@ComponentScan扫描。
  • Java配置类:在@Configuration类中用@Bean方法显式创建并返回实例。

第三种方式特别适合需要自定义创建过程的场景,比如要创建第三方库的对象、或者创建对象依赖某些外部配置。而@Controller、@Service、@Repository、@Configuration这类注解,本质上是@Component的语义化扩展,底层注册机制完全一样。

3.2 依赖注入的三种姿势与循环依赖真相

Bean之间往往有依赖,Spring通过DI来解决Bean的装配问题。三种注入方式各有取舍:

字段注入(@Autowired直接放在字段上)写起来最省事,但副作用也很明显:外部无法方便地构造这个类,单元测试时要么用Spring容器,要么用反射,很别扭。我见过不少团队代码里全是字段注入,代码简洁是真的,可要是想手动组装一个对象用来测试,就会非常痛苦。

Setter注入要求提供setter方法,灵活性比字段注入高,但写起来略繁琐,而且Setter可能被随意调用,容易破坏不可变性。

构造器注入是我个人最推荐的方式。依赖在对象创建那一刻就被确定,后面无法篡改,这个类在普通Java环境下也很容易new出来测。Spring官方文档也说构造器注入是首选。唯一的风险是当依赖图特别深、出现循环依赖时,构造器注入会直接启动失败。

这里要讲清楚循环依赖。Spring解决不了构造器注入的循环依赖,但能通过三级缓存解决Setter注入和字段注入的循环依赖。三级缓存的原理可以简化理解:提前暴露一个半成品对象引用,让A先拿到B的“代理”或“早期引用”,等B初始化完再完成A的后续填充。这样做的代价是可能出现A引用的B对象在初始化前就被使用,从而出现部分字段为null的风险。所以凡是真的无法打破的循环引用,我更推荐用@Lazy打破链条,让一方延迟到实际调用时才真正获取依赖。

3.3 作用域:Bean的“新鲜度”选择

Spring给Bean定义了多种作用域,最常见的是singleton和prototype。

  • singleton:整个容器中只创建一个实例,后续请求都拿同一个。这是Spring默认行为,适合无状态的服务类。
  • prototype:每次获取都创建新实例,容器不负责管理它的完整生命周期(容器只执行初始化,不执行销毁回调,因为后续实例已经脱离了它的跟踪)。

web环境下还有request、session、application作用域。request的作用域是每次HTTP请求一个实例,session是每个用户会话一个实例,application是ServletContext级别共享实例。

你在实际项目里,Service层用singleton几乎没有任何问题,前提是不要在里面保存可变的成员状态。如果确实需要“每来一个请求拿一个独立对象”,不要自己new,用@Scope("prototype")或者request作用域,然后通过代理注入进去。这一点在后面常见问题里会详细说。

3.4 Bean的生命周期与回调顺序

Spring Bean的生命周期可以拆成四段:

  1. 实例化:容器通过构造器或工厂方法创建对象。
  2. 属性填充:根据配置把相关依赖注入进来。
  3. 初始化:先执行Aware接口回调,再执行BeanPostProcessor的before方法,接着执行@PostConstruct或InitializingBean的afterPropertiesSet,最后执行BeanPostProcessor的after方法。
  4. 销毁:容器关闭时,依次执行@PreDestroy、DisposableBean的destroy方法。

@PostConstruct和@PreDestroy是JSR 250的标准注解,不依赖Spring,这让他俩在普通Java环境也能用。InitializingBean和DisposableBean是Spring接口,耦合度更高。两者都能实现相同效果,但工程上更推荐用@PostConstruct标注初始化方法,耦合度低,代码也直观。

另外一个经常被提起的概念区别是BeanFactory和ApplicationContext。BeanFactory是Spring IoC容器的根接口,ApplicationContext在它基础上扩展了国际化、事件发布、资源加载等企业级能力。你用AnnotationConfigApplicationContext创建的就是ApplicationContext实例,它内部持有DefaultListableBeanFactory完成真正的Bean注册和获取。

3.5 为什么说Bean的创建时机是IoC的关键

IoC的反转不只是在“谁调用谁”层面,更重要的是对象创建的时机和时机对应的依赖状态。你在Spring中拿到的每个Bean,几乎都是“已经被容器完整装配好的”对象,这保证了你可以直接调用它而不用关心它的构造前置条件。

但这个便利是有代价的:如果你在某段代码里手动new了一个Service去调用,那这个对象跟Spring容器没有任何关系,注入进它的那些依赖全部可能是null。很多人排查问题找到最后才发现,自己在一个监听器里new了个Service,然后同一个类里的@Autowired字段全是null。这不是Spring bug,是你完全跳出容器之外去用对象了。

4. 企业级Bean:EJB与Jakarta Bean的前世今生

4.1 EJB想解决的问题和历史包袱

JavaBean解决的是组件复用,Spring Bean解决的是容器管理,EJB解决的是分布式企业计算。

2000年前后,EJB 2.x被广泛用在银行、电信这类大型系统中。它的核心卖点是:你再也不用自己处理分布式事务、远程调用和安全认证,把这些交给EJB容器,你只需要写业务Bean。比如SessionBean负责业务逻辑,EntityBean负责持久化,MDB负责消息驱动。听上去很理想,实际操作起来却很痛苦:每个Bean要写Home接口、Remote接口、部署描述符,写一个最简单的HelloWorld都要配置一堆XML;本地调试更是折磨,必须部署到应用服务器里才能跑。

Spring就是踩着EJB的“巨头鞋”发展起来的。Spring不搞远程调用、不强依赖应用服务器,在一个普通Servlet容器甚至纯Java环境就能运行。它以轻量级的容器和依赖注入解决了EJB想解决但没解决好的问题,还把事情简化了一大截。

4.2 Jakarta Bean与CDI的现代语境

EJB到现在并没有完全消失,它演变成了Jakarta EE的一部分。旧名词Jakarta Bean在今天的语境里,通常指的是CDI管理下的Bean,CDI的核心思想是:上下文中依赖注入。

CDI和Spring Boot的组件扫描、@Autowired在思路上非常接近。CDI用@Inject代替@Autowired,用@Named代替@Component,支持@RequestScoped、@SessionScoped、@ApplicationScoped这些上下文作用域。如果你有Jakarta EE背景,转过来看Spring会非常顺手;反过来也一样。

区别主要在生态整合。Spring把容器、AOP、事务、MVC、Boot自动配置全部揉成一站式方案;Jakarta EE则是标准规范先行,各种应用服务器实现自己的版本,部署机制也偏“应用服务器集中部署”。所以选型时不是谁绝对好,而是看部署环境和团队技术栈。

4.3 三套Bean体系的本质差别

我整理了一张对比表,方便你站在更高维度看这个问题。

维度JavaBeanSpring BeanEJB / Jakarta Bean
本质一组类设计约定IoC容器管理的对象服务器容器管理的业务组件
创建者开发者自己newSpring容器负责创建和销毁应用服务器/EJB容器负责
依赖注入无构造器、Setter、字段注入CDI的@Inject等
生命周期无容器管理完整生命周期回调机制由服务器管理,支持分布式回调
典型场景可视化组件、数据封装Web后端服务、微服务大型企业应用、分布式事务
现代地位基础约定,但被POJO/DTO吸收Java服务端开发主流老旧系统遗留技术,仍有新规范演进

为什么标题里把Spring Bean标成重点?因为从求职、做项目、排故障来看,Spring Bean是你真正每天打交道的东西。JavaBean是根,企业级Bean是背景,Spring Bean才是当下的主战场。

5. 实操:让一个普通类变成Spring容器认的Bean

5.1 一步一步拆一个完整示例

假设我们要注册一个用户服务。最常见的注解扫描方式:

@Component public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User findById(Long id) { return userRepository.findById(id); } }

类上标注@Component,Spring在启动时会扫描到它并创建实例。这个类只有一个构造器,Spring会自动把它作为构造器注入的入口,把UserRepository传进去。这就是上面说的构造器注入。

如果你不想用扫描,也可以在配置类里用Java Config注册:

@Configuration public class AppConfig { @Bean public UserService userService(UserRepository userRepository) { return new UserService(userRepository); } }

@Bean方法可以写在配置类里,也可以写在普通的@Component类里。区别是:在@Configuration类中,@Bean方法默认是单例的,Spring会对方法调用做增强,保证每次调用返回同一个Bean;如果在普通组件类中定义@Bean,就不会有这么严格的单例保证。建议统一放在@Configuration类中,语义最清晰。

5.2 编程式注册BeanDefinition

除了扫描和配置类,Spring还支持完全编程式地注册Bean。这在框架开发、动态注册场景里很有用,比如根据某个配置项决定要不要注册某个组件。

从Spring 5开始,可以用GenericApplicationContext的registerBean方法:

GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(UserService.class, () -> new UserService(new UserRepository())); context.refresh(); UserService userService = context.getBean(UserService.class);

这段代码没有XML、没有注解,手动往容器里塞了一个BeanDefinition。你需要注意registerBean只是在注册定义,真正实例化发生在getBean或refresh之后。如果你在refresh之前调用getBean,会得到NoSuchBeanDefinitionException。

这个技巧我平时不太会用,但是看Spring Boot的自动配置源码时会频繁遇到类似调用。理解了BeanDefinition这个抽象,你就知道注解扫描最终也只是把类变成BeanDefinition注册到容器里而已。

5.3 单例Bean不要当全局变量用

这是新手最常见的认知误区:既然singleton作用域是全局唯一,那我是不是可以用它存一些全局状态?比如在Service里搞一个int count,每次调用加一,用来做访问计数。

这种写法在人手一个线程的教科书里当然没事,但真实后端是大量线程并发调用同一个单例对象,成员变量count的读写就会出现并发问题。轻则数据错乱,重则出现各种诡异状态。

正确做法是:保持Bean无状态,需要跨请求传递的数据放到方法参数、ThreadLocal、Redis或者请求上下文中。实在要用会话级状态,就声明request或session作用域,让容器为每个上下文创建新实例。

我见过一个付费项目,开发者在单例的Bean里面放了一个可变Map做本地缓存,上线当天就出现并发覆盖。他的理由是“反正Spring缓存的思想也是这样”,但实际上Spring管理的是线程安全的组件,他写的HashMap毫无线程安全可言。这类问题排查起来相当费劲,因为数据一会儿对一会儿错,根本不像崩溃那样容易定位。

6. 常见问题与排查实录

6.1 “注入进来是null”怎么排查

这个问题的出现频率极高。通常不是容器坏了,而是这个类本身没有进入容器。

排查顺序可以按这几步走:

  1. 确认类是否标注了@Component或其派生注解。Controller、Service、Repository、Configuration都行,但普通类没标不行。
  2. 确认扫描路径覆盖到这个包。如果启动类在com.example.demo,而你写了个类在com.other.module,默认@ComponentScan扫不到。
  3. 确认注入发生在容器管理的对象里。如果在类内部先new了一个对象再调用其方法,被new出来的对象里的@Autowired字段就是null。
  4. 确认不是静态方法直接调用。静态方法是绕过容器获取Bean的重灾区,Spring不会给静态字段注入依赖。

有一次帮别人看一个定时任务的问题,他的Job类标注了@Component,但里面的调度方法直接new了一个Service,然后那个Service里注入的Mapper全为null。我让他把Service注入到Job里,再用实例方法调用,问题立刻消失。

6.2 @Transactional和@Async没生效

Bean被Spring管理不意味着所有功能都能自动生效。@Transactional和@Async都是通过代理实现的,Spring容器拿到的不是你的原始对象,而是代理对象。如果你在类内部用this调用同类的方法,这个调用走的是原生对象,完全绕过了代理,事务和异步自然就失效了。

常见的失效例子:

@Service public class OrderService { @Transactional public void createOrder() { ... } public void handle() { this.createOrder(); // createOrder的事务不会生效 } }

解决办法有三种:把被调用方法拆到另一个Bean里,用依赖注入的方式调用那个Bean;或者从Spring容器里拿当前Bean的代理再调用;再或者在@Configuration里把内部方法拆出去。我通常首选拆类,最多加一圈AopContext去找当前代理,因为拆类的可读性最好,调试也直接。

6.3 循环依赖报错怎么看

启动时如果看到类似“The dependencies of some of the beans in the application context form a cycle”的消息,说明两个Bean互相注入。

Spring默认对Setter注入和字段注入的循环依赖是有兜底方案的,但如果循环里有个Bean用的是构造器注入,就会直接报错。因为A要new出来必须先有B,B要new出来必须先有A,谁都无法先完成实例化,三级缓存也无能为力。

策略上不是去破解这个死结,而是从设计上打破循环。

  • 重构:把A依赖B的部分提取到新的类C里。
  • @Lazy:在其中一个构造器参数上加@Lazy,让依赖延迟到调用时再创建。
  • 面向接口:把公共逻辑抽象成接口,降低强耦合。

我有一次接手一个老系统,Service层互相调了五六个类,启动报循环依赖,排查到后面发现根本原因是业务分层混乱、一个类干了多层的活。最后我把公共查询逻辑抽到一个独立的QueryService里,循环立刻消失。所以遇到循环依赖,第一步不是想着怎么绕,而是反思设计上为什么两个组件必须互相持有对方。

6.4 prototype作用域“失效”的典型情形

当你把prototype的Bean注入到一个singleton的Bean里,这个prototype Bean实际上只会被创建一次,之后永远复用同一个实例。很多新人发帖问“我明明标了@Scope('prototype'),为什么每次拿到的都是同一个对象”,原因就在这里:注入动作只发生一次。

想每次调用都拿到新实例,有几种方式:

  • ObjectProvider ,每次getObject时容器会重新创建。
  • @Lookup方法注解,Spring会对方法做动态代理,每次调用时返回新Bean。
  • 直接使用ApplicationContext,手动getBean获取。

ObjectProvider的写法是:

@Component public class OrderService { private final ObjectProvider<OrderContext> contextProvider; public OrderService(ObjectProvider<OrderContext> contextProvider) { this.contextProvider = contextProvider; } public void create() { OrderContext context = contextProvider.getObject(); } }

相比ApplicationContext.getBean,ObjectProvider在测试里更好模拟注入,依赖方向也更清晰。

6.5 序列化问题:写在Session里的Bean

当Bean被放进HttpSession时,需要实现Serializable,否则应用服务器在集群模式下做会话复制时可能会抛NotSerializableException。这里建议采用“会话中只放必要的数据对象”,不要把整个Service放进Session。Session本来是为了存用户状态而存在,把业务逻辑Bean塞进去,既加重存储压力,又破坏了Bean的生命周期边界。

7. 面试与团队协作:怎么聊Bean才显得有水平

7.1 一分钟现场回答示例

面试官问“谈谈你对Spring Bean的理解”,最忌讳上来背定义。我建议按这个节奏说:

先给一句话结论:Spring Bean是IoC容器创建、组装并通过依赖注入管理的对象,它的一切行为由容器生命周期约束,而不只是Java里一个普通的类。

再给三层拆解:

  • 第一,它是一个Java对象,所以JavaBean的getter/setter约定它可以遵守也可以不遵守,完全看业务定位。
  • 第二,它被容器管理,因此具有作用域、生命周期、依赖注入和AOP代理能力。
  • 第三,它可以通过XML、注解扫描或@Bean方法注册,最终在容器内部都表现为BeanDefinition。

最后举一个生产环境中的例子,比如事务失效是因为方法被内部this调用导致代理绕过。这样一套回答下来,既展示知识广度,又体现实操深度。

7.2 团队代码规范里怎么约束Bean

团队里最容易出现的混乱,是所有的类都往容器里注册,不管有没有必要。我见过一个项目,连工具类都标上@Component,其实它的方法全是静态的,进容器既没有依赖,也没有生命周期管理需求,纯粹增加启动时间。

关于Bean的使用,我建议团队在代码规范中明确几点:

  • 只有被容器管理、有依赖注入需求或需要生命周期回调的类才标记为Bean。
  • 数据载体类(实体、DTO、VO)保持POJO形态,不要加Spring注解。
  • Service层保持无状态,scope默认单例;除非明确需要独立状态才改prototype或request。
  • 优先构造器注入,禁止在字段上直接写@Autowired的新代码(维护老代码时除外)。

还有一处容易混淆:@Component和@Bean的区别。@Component是类级注解,Spring扫描到自动实例化;@Bean用在方法上,通过配置类决定返回什么对象。当你需要注册第三方组件、或者创建过程需要传入参数时,用@Bean会更合适;自己写的业务类用@Component语义更清晰。

7.3 我个人的一点体会

从JavaBean到Spring Bean再到Jakarta Bean,这条技术线的演进本质上是一个“控制权转移”的轨迹。早期把控制权交给工具反射,后来把控制权交给IoC容器,再后来把控制权交给分布式的应用服务器。每次转移都牺牲了一部分直接操作的灵活性,换来的是更大的复杂系统能力。

我个人学Bean的过程分三个阶段。第一阶段背清楚了JavaBean规范,觉得Bean不过如此。第二阶段开始写Spring项目,被循环依赖、作用域失效这类问题整得晕头转向。第三阶段是某次排查一个线上代理失效问题时,彻底搞懂了BeanFactory、BeanDefinition和代理之间的关系,这才把之前零碎的知识串成了一条线。

建议你也顺着这个思路去学:先拿一段代码把Bean的注册、获取、生命周期回调全部打日志看一眼,然后故意制造一个作用域失效或者代理失效,观察它真实的行为,比对着文档背十遍定义都有用。

到了这一步,你再回头看标题里的三个概念,就不会再纠结谁是正宗谁是谁的替代。JavaBean定义了最初的对象规范,Spring Bean把它提升到了容器管理层面,而企业级Bean提醒你用Java写大规模系统时还有分布式和事务这些维度需要考虑。这些都是Bean这个词汇在不同时代留下的沉淀,也都是实际工作中很可能碰到的场景。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询