Spring Bean vs Component:为什么同样是“组件”,Java 却要发明一个 Bean?
一、同一个「组件」,两个世界都听不懂对方在说啥
你是不是也遇到过这种对话——
前端同事:
「这个页面的组件有点大了,我打算拆成三个组件。」
后端同事:
「可以啊,我这边也会拆成几个组件……呃等等,你说的是 React Component 吧?我说的是 Spring 的组件扫描那个 Component……」
再比如:
新来的全栈同事:
「你把这个组件通过依赖注入注进来就行。」
你:
「注进来的是前端组件还是后端组件?」
表面上大家都在说同一个词:Component / 组件。 但一到细节,就发现:
-
前端脑子里想的是:React/Vue 里的 UI 组件,JSX、模板、样式那一套;
-
后端脑子里想的是:Spring 容器里的 Bean / Service / Repository,依赖注入那一套。
更魔幻的是,在 Spring 世界里你还能看到:
@Component
public class UserService {
}
你去问别人:UserService 是 Component 还是 Bean? 大部分人会下意识回一句:都是。
听起来像废话,但它背后确实藏着一点门道:
-
为什么 Spring 世界一定要单独搞一个「Bean」这个名词?
-
@Component、@Service、@Repository这些注解到底算什么? -
它们和我们心里的「组件」到底是同一类东西,还是两个世界?
先别管其他框架里的 Component,我们先把 Spring 自己这摊事理清楚。
二、先把 Spring 自己讲明白:Bean 到底是什么鬼?
先想象一个没有 Spring 的普通 Java 项目。
你想写一个用户服务,一般会这么干:
public class UserRepository {
// 省略 JDBC / ORM 代码
}
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String username) {
// 省略业务逻辑
}
}
在 main 方法里你会:
public static void main(String[] args) {
UserRepository repo = new UserRepository();
UserService userService = new UserService(repo);
userService.register("renda");
}
一切明明白白:
-
你自己
new出UserRepository; -
再手动
new出UserService; -
谁依赖谁、谁创建谁,全靠你控制。
现在把 Spring 拉进来。
你通常会写成这样:
@Repository
public class UserRepository {
// ...
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String username) {
// ...
}
}
然后在启动类上写一个:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
神奇的地方来了:你不再自己 new 任何一个对象, UserService、UserRepository 却都能被用得好好的。
那这些对象是谁创建的?是谁把依赖塞进去的? 答案只有一个:
Spring 容器。
而 被容器创建、管理、装配好的这些对象,Spring 给它们起了一个统一的称呼:Bean。
可以用一句话粗暴概括:
在 Spring 世界里,只要是交给容器托管的对象实例,都叫 Bean。
不管它最开始是怎么声明出来的:
-
类上写了
@Component/@Service/@Repository; -
配置类里有一个
@Bean方法返回它; -
甚至是你用编程方式注册
BeanDefinition;
只要最终进了 ApplicationContext,就完成了「转正」,它就是一个 Bean。
所以:
-
UserService这个「类」,是 Java 层面的东西; -
userService这个「被容器创建出来的实例」,在 Spring 口径下就叫一个 Bean。
这也是为什么你会看到那句非常经典的话:
Spring 是一个 IoC 容器,
它管理着很多 Bean,
然后通过 依赖注入 把 Bean 们串联起来。
搞懂了这一层,「Bean」这个词就不再神秘,它只是对 “被容器托管的对象” 的一个统一称呼而已。
接下来问题来了:
-
那我在类上写的
@Component、@Service又算什么? -
它们到底是 Bean 还是 Component?
要回答这个问题,我们得看一眼从「类」到「Bean」的那几步。
三、@Component 只是入口,不是主角:从「类」到「Bean」的三步路
很多同学第一次看到 @Component 的时候,都会以为:
「
@Component标注的就是 Component,对吧?」
从语法上看没问题,但在 Spring 的世界观里,更精确的说法应该是:
@Component标注的是: “这个类将来要被扫描并注册成一个 Bean,请容器记一下。”
也就是说,@Component 更像是一个报名表,真正的主角仍然是 最终生成的 Bean 实例。
如果把「从类变成 Bean」的过程拆开看,大概是这三步:
第 1 步:类上贴标签 —— 「我要加入容器」
你在类上写下:
@Component
public class EmailSender {
// ...
}
或者更具体一点:
@Service
public class UserService {
// ...
}
这一刻发生的事情其实很简单:
-
就是在 Class 元数据上加了一个注解;
-
表达的意思是:“麻烦帮我自动注册为 Bean”。
Spring 并不会「看见注解就立刻 new 一个对象」, 它只是把这件事记在心里:这个类有资格成为 Bean 候选人。
第 2 步:组件扫描 —— 「把有资格的人找出来登记」
当应用启动时,@SpringBootApplication 会触发一堆自动配置,其中就包括:
-
扫描指定包路径下的所有类;
-
找到带有
@Component、@Service、@Repository、@Controller等注解的类; -
给它们生成一份份「Bean 定义」(
BeanDefinition)。
你可以把 BeanDefinition 理解成一张配置单:
-
这个 Bean 的类型是什么?
-
作用域是 singleton 还是 prototype?
-
创建它需要调用哪个构造器?
-
需要注入哪些依赖?
到这一步为止,还没有真正创建实例。 只是把「建档」工作做好了。
第 3 步:容器正式出手 —— 「按照档案造出真的 Bean」
等容器进入实例化阶段时,会根据这些 BeanDefinition:
-
选择构造器;
-
解决依赖(找到需要注入的其他 Bean);
-
创建对象实例;
-
做一些初始化回调(比如
@PostConstruct、InitializingBean那些)。
当 EmailSender、UserService 这些实例被真正创建出来并存放到容器里后,我们才说:
「现在容器里有了一个
emailSenderBean,一个userServiceBean。」
于是你在别的类里就可以像这样依赖注入:
@Service
public class NotificationService {
private final EmailSender emailSender;
public NotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
这里注入的 emailSender,就是那一步步加工出来的 Bean 实例。
所以,把这个过程串起来看,就是:
源码里的类(Class)
│ ① 贴上 @Component/@Service 等标签
▼
候选组件(等着被扫描)
│ ② 组件扫描,生成 BeanDefinition(档案)
▼
BeanDefinition(怎么创建、叫什么、依赖谁)
│ ③ 容器按档案 new 对象、注入依赖、做初始化
▼
真正的 Bean 实例(活在容器里的对象)
这时候再回答那两个问题就简单了:
-
@Component是不是 Bean? 不是,它只是个 告诉容器“我要当 Bean” 的标记。 -
@Service、@Repository和@Component有什么区别?-
在「能不能变成 Bean」这件事上,区别不大——本质都是候选人;
-
主要差别在语义和一些小功能,比如:
-
@Repository会多做一次数据访问异常转换; -
@Service、@Controller主要是给人和工具看的,让大家一眼知道这是哪一层的 Bean。
-
-
用一句话总结:
在 Spring 里:「类 + 注解」只是报名入口; 真正参与业务、被注入来注入去的,是容器里那一粒粒 Bean。
接下来,我们再把视角放大,看看其他技术栈口中的「Component」到底在干什么活,为什么容易跟 Spring 的概念搅在一起。
四、换个技术栈,Component 其实完全不是一回事
前面我们一直站在 Spring 的视角说话。 现在把镜头拉远一点,你会发现一个更有趣的现象:
不同技术栈嘴里的 “Component”,根本不是一回事。
4.1 前端世界:Component = 会动的界面砖块
先看最耳熟能详的 React。
你写一个组件,大概是这样:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
这里的 Component 是什么?
-
有自己的 视图(JSX 那一段);
-
有自己的 状态(
useState); -
有自己的 行为(
onClick里的函数); -
是 UI 树上的一个 节点,可以被嵌套、组合、复用。
Vue 也类似,一个 .vue 文件就是一个组件:模版 + 脚本 + 样式打包在一起; Angular 的 @Component 也是把 TS 类、模版和样式绑成一坨。
在前端世界里,“组件”这个词,几乎就是:
“界面上的一小块,可复用的那种。”
它解决的问题是:
-
这个页面太大了,怎么拆成一块一块维护?
-
这块 UI 下次还能在哪些地方用?
-
这块 UI 的状态和行为能不能封装起来?
4.2 后端世界:Component = 可以被注入的模块/服务
再看后端的几个例子。
-
在 NestJS 里,你会看到大量的
@Injectable()、@Controller()、@Module(); 这些东西说白了就是 Nest 的 “组件”,被 DI 容器托管,可以互相注入。 -
在 .NET Core 里,你会写:
services.AddScoped<IUserService, UserService>();注册的
UserService其实也可以叫一个 “组件”,只不过大家习惯叫它 Service。
这些后台框架并不执着于 “Component” 这个单词,有的叫 Provider,有的叫 Service,有的叫 Module。 但本质都差不多:
「被容器托管,参与业务逻辑的模块」 = 它们口中的组件。
和前端 UI 组件相比,这些组件一般不负责渲染界面,而是:
-
处理业务逻辑;
-
访问数据库、缓存、外部系统;
-
被框架自动创建、注入、管理生命周期。
4.3 Spring 为什么干脆叫 Bean?
你现在再回头看 Spring 的命名策略:
-
前端已经把 “Component” 用成 UI 单元了;
-
后端世界很多框架对 “Component” 的用法也各不相同;
-
Java 世界历史上还有 JavaBeans、Enterprise JavaBeans(EJB)这一堆“豆子”传统。
Spring 索性说一句:
“行,我干脆统一叫 Bean。
只要是我容器托管的对象实例,都叫 Bean,别跟外面那些 Component 搞混了。”
于是我们就得到了这样一个局面:
-
Spring Bean:Spring 容器里的那一粒粒对象;
-
各家 Component:要么是 UI 砖块,要么是 DI 容器里的模块;
-
名字相似,角色其实差挺远。
所以当你跨栈时听到对方说 “Component”,第一反应最好不是“哦,原来就是 Bean”,
而是先问一句:“你这个 Component,是在画界面,还是在干业务?”
五、那 Bean 和 Component 谁更「独立」?先别急,看看真实代码
很多人的第一直觉是这样的:
Spring Bean:离开 Spring 就活不下去,很依赖框架。
前端 Component:只是一个 UI 模块,随便 import 就能用,好像更独立?
听起来有点道理,但真把代码摊开,你会发现事情没这么简单。
5.1 一个「干净」的 Spring Bean 长什么样?
先看这样一个 Service:
public class DiscountCalculator {
private final int defaultRate;
public DiscountCalculator(int defaultRate) {
this.defaultRate = defaultRate;
}
public int calcPrice(int originalPrice, Integer customRate) {
int rate = (customRate != null ? customRate : defaultRate);
return originalPrice * (100 - rate) / 100;
}
}
注意两点:
-
这个类 没有任何 Spring 注解;
-
也没有实现什么 Spring 接口,没有
ApplicationContext、没有@Autowired。
如果你想把它变成一个 Bean,很简单:
@Configuration
public class DiscountConfig {
@Bean
public DiscountCalculator discountCalculator() {
return new DiscountCalculator(10);
}
}
或者粗暴一点直接:
@Component
public class DiscountCalculator {
// 甚至可以把 defaultRate 写死
}
但关键是:它本身不需要知道自己是个 Bean。
你在单元测试里完全可以这样用:
@Test
void should_calc_price_correctly() {
DiscountCalculator calculator = new DiscountCalculator(10);
int price = calculator.calcPrice(100, null);
assertEquals(90, price);
}
-
不需要起 Spring 容器;
-
不需要 Mock ApplicationContext;
-
它就是一个纯 Java 类,Spring 只是“顺路帮你管理了一把”。
这就是 Spring 一直强调的:
Bean 理想情况下就是 POJO,容器只是一个外壳。
当然,你也可以写出很“黏” Spring 的 Bean,比如:
@Component
public class DirtyBean implements ApplicationContextAware {
private ApplicationContext ctx;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.ctx = applicationContext;
}
public void doSomething() {
LogService logService = ctx.getBean(LogService.class);
// ...
}
}
这种写法就真的对 Spring 强依赖 了: 离开容器,它啥也干不了,测试也很难写。
但是——这是我们自己的选择,不是 Bean 概念本身的限制。 所以「Bean 很依赖框架」这件事,其实很多时候是我们代码写出来的结果。
5.2 一个典型的 React Component 呢?
再来看一个看似简单的 React 组件:
import { useState, useEffect } from "react";
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
return <div>{now.toLocaleTimeString()}</div>;
}
export default Clock;
这个组件表面上也挺“清爽”的,但仔细想想:
-
里面直接用了
useState、useEffect这些 React 的 Hook API; -
依赖 JSX 语法,需要编译;
-
必须挂在 ReactDOM 渲染树上才能跑起来。
你要在一个完全没有 React 的环境里单独拿这段代码来用,几乎不可能:
-
useState是谁?——找不到; -
useEffect是谁?——找不到; -
JSX
<div>...</div>也需要编译才能变成 JS 调用。
换句话说,这个 Component 是“长”在 React runtime 上的:
没有 React,这个组件连加载都过不了,更别说渲染。
Angular、Vue 组件也类似:
-
模版语法、指令、生命周期钩子,全都依赖框架;
-
一离开那套 runtime,组件就失去意义。
5.3 真正的对比:逻辑依赖 vs 代码依赖
把这两个例子并排看,你会发现一个挺反直觉的结论:
-
Spring Bean:
-
从“设计目标”看,Spring 鼓励你写成 不依赖 Spring 的普通类;
-
你可以让它非常干净,以至于脱离容器也能 new 出来用、写单测;
-
它对 Spring 的依赖,更多是 配置层面的(由谁创建、由谁注入谁)。
-
-
前端 Component:
-
它天生就是框架的一部分,直接使用框架 API;
-
没有框架提供的渲染和调度机制,组件无法工作;
-
它对框架的依赖是 代码层面的、语法层面的。
-
所以如果你问:
「Bean 和 Component 谁更独立?」
一个更准确的回答是:
-
“看你怎么写 Bean,大部分情况下 Bean 反而更有机会保持独立;
-
绝大多数 UI Component 则天然紧贴框架 runtime。”
更工程化一点的总结:
Spring Bean:
- ✔ 可以写成不 import 任何 Spring 的 POJO
- ✔ 可以在普通 Java 程序里 new 出来用
- ✔ 可以不通过 Spring 就写单测
- 依赖:主要是“由谁创建和注入”的外部配置
UI Component(React/Vue/Angular):
- ✖ 必须依赖对应框架 API(hook/指令/模版)
- ✖ 脱离框架 runtime 无法渲染
- ✖ 测试时通常也要引入框架的测试工具链
- 依赖:直接写死在代码和语法层面
也就是说,「Bean 很依赖、Component 很独立」这个印象,大多只是:
——我们看到 Bean 一定出现在 Spring 项目里,
——看到 Component 大多在 demo 里很轻巧,
所以产生了一个“错觉”。
真正决定“独立不独立”的,并不是它叫 Bean 还是 Component, 而是:
-
你有没有把框架 API 写进业务代码里;
-
你有没有给它保留“脱离框架也能跑/能测”的余地。
六、生命周期的差异:后端的「长命豆子」 vs 前端的「一次性组件」
如果说“依赖性”是横轴,那“生命周期”就是纵轴。 把这两条轴画出来,你会立刻发现:Spring Bean 和前端 Component 根本不活在同一个节奏里。
6.1 Spring Bean:一次种下,长期供货的「长命豆子」
先看最常见的单例 Bean。
在一个普通的 Spring Boot 项目里,大部分 Bean 的生命周期大致是这样的:
容器启动
├─ 扫描、注册 BeanDefinition
├─ 实例化 Bean(调用构造器)
├─ 注入依赖(构造注入 / Setter / 字段)
├─ 调用初始化回调(@PostConstruct / afterPropertiesSet)
└─ Bean 就绪,开始被使用
...
应用正常跑(Bean 一直活着)
...
容器关闭
└─ 调用销毁回调(@PreDestroy / destroy)
几个关键词:
-
长寿命: 默认
@Scope("singleton"),从容器启动一直活到容器关闭。 一个UserServiceBean,正常情况下只会被创建一次。 -
初始化很讲究: 配置、连接池、HTTP 客户端之类的重资源,集中在启动阶段准备好。
-
销毁也有仪式感: 关连接、刷缓存、发事件,都可以放在
@PreDestroy或类似回调里做。
你可以把单例 Bean 想象成公司里的 正式员工:
-
入职那天会办很多手续(创建、注入、初始化);
-
在职期间一直为整个系统服务;
-
公司关门或服务下线时,才统一办理离职(销毁)。
当然,Spring 也有「短命」一点的 Bean,比如:
-
@Scope("prototype"):每次要的时候 new 一个; -
@RequestScope、@SessionScope:跟请求/会话一起活。
但大多数业务 Bean 是那种“养久一点更划算”的长命豆子。
6.2 前端 Component:上台、演一段、退场的「一次性演员」
对比一下前端组件的生命周期,你会发现节奏完全不一样。
以 React 函数组件为例,它的生命周期更像这样:
第一次渲染
├─ 执行函数(渲染 UI)
├─ 运行 useEffect 的「首次」逻辑
之后每次状态 / props 变化
├─ 再执行一遍函数(重新渲染)
├─ 先运行上一次 effect 的清理函数
└─ 再运行新的 effect
从界面上消失
└─ 执行最后一次清理(unmount)
关键点:
-
短周期、反复出现: 用户进一个页面 → 挂载一堆组件; 切个路由 → 前一批组件卸载,新一批组件挂上; 同一个组件类型会被创建好多次。
-
跟用户行为强相关: 滚动、点击、导航、搜索……都可能触发组件的更新、销毁和重建。
-
清理非常重要: 不清理 event listener / 定时器 / 订阅,就会造成内存泄漏或诡异 bug。
Angular / Vue 也类似:
-
有
ngOnInit / ngOnDestroy,或onMounted / onUnmounted; -
组件随路由、条件渲染、列表循环,频繁地出现和消失。
如果把它比作公司组织结构的话:
-
Spring Bean 是正式员工(或部门);
-
前端 Component 更像是项目组的临时演员,
-
有戏的时候上台表演,
-
没戏的时候就退场回后台。
-
6.3 生命周期差异对设计的真实影响
这两种生命周期,会强烈影响你的设计思路:
-
写 Bean 时,你要关心:
-
初始化成本高不高?能不能在启动阶段一次搞定?
-
是不是线程安全?(因为很可能是单例,被很多线程共享)
-
有没有需要在应用退出时优雅清理的资源?
-
-
写 前端 Component 时,你更关心:
-
每次渲染成本会不会太高?(比如 diff 大不大)
-
状态放哪里合适?组件内 / 全局 store / URL?
-
副作用有没有正确清理?否则来回切页就会爆炸。
-
一旦把“生命周期”这条轴拉出来,你就不会再纠结:
「为什么 Spring 要叫 Bean,不叫 Component?」
因为它负责的事情、活在的时间线,
和前端同学嘴里的 Component,已经是两个时空的生物了。
七、一个统一的脑内模型:当你再听到「组件」这个词时,应该先问三件事
说了这么多,最有用的不是背概念,而是:
以后再听到别人说「组件 / Component / Bean / Service / Provider」时,
你脑子里有一套「快速定位」的小问卷。
我推荐你记住这 三个问题。
7.1 问题一:它主要是在「画界面」,还是在「干业务」?
- 画界面:
-
基本就是 React/Vue/Angular 那一挂;
-
关注点是:UI 状态、布局、交互。
-
- 干业务:
-
基本就是各家后端框架的 Bean / Service / Provider;
-
关注点是:业务规则、数据访问、调用外部系统。
-
先分清是 UI 组件,还是业务组件。
这一步决定你应该用“前端那套脑子”,还是“后端那套脑子”来看它。
7.2 问题二:它的生命周期,是跟「应用」走,还是跟「页面 / 请求」走?
- 跟应用走:
-
Spring 单例 Bean、Nest Provider、.NET Service 大多都是这个节奏;
-
更适合做长期存在的服务、缓存、连接池等。
-
- 跟页面/请求走:
-
前端 Component、请求作用域 Bean;
-
更适合保存“这一次交互”的上下文状态。
-
这个问题会帮你想清楚:
-
要不要考虑线程安全?(长命单例很敏感)
-
要不要重点关注创建/销毁成本?(短命组件关注“每次创建是不是太重”)
7.3 问题三:它是不是由某个「容器 / 框架」自动创建 & 注入的?
- 是的:
-
Spring 容器、Nest DI、.NET DI、前端框架 runtime……
- 这些东西往往有:
-
生命周期管理;
-
依赖注入;
-
一些“魔法能力”(AOP、拦截器、指令、hook…)。
-
-
- 不是:
-
纯工具类、纯函数库、
new出来的对象; -
更适合写成完全无框架依赖的代码。
-
这个问题会提醒你:
“我现在写的是『容器托管的部件』, 还是一个『谁都能 new 的普通库』?”
也会倒逼你去思考:
哪些逻辑可以下沉到普通库里,减少对框架的耦合。
7.4 用这三个问题,给几个典型角色定位
你可以拿这三个问题,套几个常见角色试试:
- Spring Bean(典型 Service)
-
干业务
-
跟应用走
-
被 Spring 容器创建 & 注入
-
- React Component
-
画界面
-
跟页面 / 视图走
-
被 React runtime 管理
-
- NestJS Provider
-
干业务
-
跟应用走
-
被 Nest 容器创建 & 注入
-
- 一个纯工具类
MoneyUtils-
既不画界面,也不直接干业务,只是数学运算
-
生命周期 = 谁 new 它谁说了算
-
不依赖任何容器
-
你会发现:
虽然名字千奇百怪(Bean、Component、Service、Provider……),
但一旦丢进这三道题里,位置就非常清晰了。
7.5 这样做的好处
-
跨栈沟通不再懵: 听到“组件”两个字,不再条件反射想到自己那一套,而是先问三件事。
-
学新框架更快: 官网文档里只要一出现“XX 组件”,你先用这三问定位角色,再去看 API,理解会快很多。
- 写代码更有边界感: 你会更自然地区分:
-
哪些东西应该是「容器托管的组件」;
-
哪些应该是彻底独立、随处可用的纯工具。
-
到这一步,你基本就具备了一个 跨技术栈的「组件/Bean 心智模型」:
以后不管遇到什么新玩意儿,只要先问这三件事,概念就不会绕成一团。
八、实战建议:如何写出既优雅又「跨栈友好」的 Bean?
聊了这么多,如果落不回到代码,其实对日常开发帮助不大。
下面这几条就是你 明天在项目里就能用 的 Bean 设计准则。
8.1 把 Bean 写成「不知道自己在用 Spring」的普通类
先看一个“有点黏”的写法:
@Service
public class OrderService {
@Autowired
private Environment environment;
@Autowired
private ApplicationContext applicationContext;
public void createOrder() {
String region = environment.getProperty("app.region");
PaymentClient client = applicationContext.getBean(PaymentClient.class);
// ...
}
}
这类代码的特点是:
-
到处
@Autowired框架对象; -
业务逻辑里面直接跟 Spring API「勾肩搭背」。
同样的功能,用更「干净」的方式写:
public class OrderService {
private final String region;
private final PaymentClient paymentClient;
public OrderService(String region, PaymentClient paymentClient) {
this.region = region;
this.paymentClient = paymentClient;
}
public void createOrder() {
// 业务逻辑只关心 region & paymentClient
}
}
然后在 Spring 这边,用配置把「Spring 世界」和「业务世界」接起来:
@Configuration
public class OrderConfig {
@Value("${app.region}")
private String region;
@Bean
public OrderService orderService(PaymentClient paymentClient) {
return new OrderService(region, paymentClient);
}
}
好处很直接:
-
OrderService本身完全不 import Spring; -
要测试时随手 new 一个就能测,不用起容器;
-
有一天不用 Spring 了,这个类几乎可以原封不动搬走。
规则一:能让 Bean 保持 POJO,就不要把 Spring API 写进去。
8.2 把「框架味」推到边缘:分清 Domain / Application / Infrastructure
一个很实用的拆法是三层心智模型:
- Domain 层(领域层):
-
实体(Entity)、值对象(Value Object)、领域服务(Domain Service);
-
尽量不 import 任何框架:不 Spring、不 MyBatis、不 JPA;
-
只关心纯业务。
-
- Application 层(应用层):
-
用例/场景(Application Service);
-
负责「编排」多个领域对象完成一个用户场景;
-
可以适当感知事务、日志之类,但也尽量克制。
-
- Infrastructure 层(基础设施层):
-
Repository 实现、Controller、Adapter、Client…
-
这里才放各种
@Service、@Repository、@RestController等注解; -
负责把外部世界(HTTP、DB、MQ 等)对接到内部的 Domain / Application。
-
简单示意:
[ 外部世界:HTTP / DB / MQ / 第三方API ]
│
(Infrastructure 层)
│
(Application 层)
│
(Domain 层)
你可以刻意训练自己:
-
Domain 层的类 不允许出现任何 Spring 注解;
-
Application 层可以有少量 Spring 注解,但尽量只是「壳」;
-
真正「充满框架味」的地方收敛在 Infrastructure 层(Controller / Repository 实现)。
这样写出来的 Bean 有一个很棒的特性:
越靠核心的 Bean,越不依赖 Spring,
越靠边缘的 Bean,越负责跟 Spring、HTTP、DB 打交道。
这就是所谓「框架在外,业务在内」。
8.3 让测试来「倒逼设计」:Bean 能不能不用容器就测?
一个非常实用的检验标准:
这个 Bean 要写单测,能不能不启动 Spring 容器?
比如下面这样一个 Bean:
@Service
public class PriceService {
public int calc(int a, int b) {
return a + b;
}
}
写单测完全可以这样:
@Test
void should_calc_price() {
PriceService priceService = new PriceService();
assertEquals(3, priceService.calc(1, 2));
}
你甚至不需要 @SpringBootTest,这说明这个 Bean 设计得很健康。
相比之下,那种一上来就:
@SpringBootTest
class PriceServiceTest {
@Autowired
private PriceService priceService;
...
}
往往说明:
-
这个 Bean 把太多东西耦合到 Spring;
-
或者你 懒得 把依赖解耦出来。
可以把测试策略分两层:
-
单元测试(不依赖 Spring)
-
直接 new Bean,自行构造依赖(可以用 Mockito 等 mock);
-
专注于业务逻辑本身是否正确。
-
-
集成测试(使用 Spring)
-
使用
@SpringBootTest或@WebMvcTest等; -
验证「Bean + 配置 + 外部系统」整体协作是否正常。
-
很多时候,只要你想把 Bean 写成「不用 Spring 也能测」,自然就会:
-
把依赖通过构造器传进来;
-
避免到处拿容器、拿 Environment;
-
让 Bean 更像一个可移植的模块。
8.4 跨栈团队协作:别只说「组件」,学会「翻译」
作为后端,如果你经常和前端 / 全栈同事协作,可以试试这样说话:
-
不要只说:
「我这边会拆成几个组件。」
改成:
「我会拆成几个 Service Bean,你可以理解成你们那边的 service 层组件。」
-
当前端说:
「这个组件负责拉数据、展示列表。」
你脑子里可以自动翻译:
「它类似我们这边的 Controller + 一小块 View。」
-
当你给别人解释架构时:
-
前端那边:
「你们的组件树,就像我们这边的一堆 Bean 调用关系图。」
-
后端这边:
「我们的 Service Bean,就像你们拆出来的一些逻辑组件。」
-
目的不是抖机灵,而是:
让不同技术栈的人,都能把你说的「组件」,
映射到自己熟悉的那类东西上。
这一点会大幅降低沟通成本,也让你看文档、看源码时更快进入状态。
九、结尾:别被名词牵着走,先看「它在系统里干啥」
最后,稍微收个尾。
这一圈聊下来,其实就想说明几件很简单的事:
1) 在 Spring 里,Bean 这个词很朴素:
「只要是被 Spring 容器创建、管理的对象实例,都叫 Bean。」
它不神秘,也不高级,只是对托管对象的一个统称。
2) @Component / @Service / @Repository 这些注解只是「报名入口」,不是主角:
-
贴在类上是告诉容器:“请把我变成 Bean。”
-
真正在系统里跑来跑去、被注入、被调用的,是 Bean 实例本身。
3) 其他技术栈嘴里的 Component,往往干的是完全不一样的活:
-
前端:UI 组件,负责“长什么样、怎么交互”;
-
后端:被 DI 容器托管的模块 / 服务;
-
Spring 索性用「Bean」这个词,避免和这些“组件”混在一起。
4) Bean 和 Component 谁更「独立」?要看你怎么写:
-
Spring Bean 完全可以是一个不 import Spring 的 POJO;
-
大部分 UI Component 天然深度依赖框架 runtime;
-
真正决定耦合度的,是你的设计,不是名词。
5) 以后再听到「组件」这个词,先用那三问帮自己定位:
-
它是在画界面,还是在干业务?
-
它的生命周期,跟着应用走,还是跟着页面 / 请求走?
-
它是不是由某个容器 / 框架自动创建 & 注入的?
三个问题一过,你立刻就知道:
「哦,原来这是前端组件、后端 Bean,
还是一个纯工具模块。」
写到这,其实最想强调的是一句:
名字只是包装,角色才是核心。
当你能一句话说清楚:
-
这个 Bean 在系统里负责什么;
-
谁会用它;
-
它活多久;
-
它能不能在没有 Spring 的情况下跑和测
—— 你就已经远远超过了「只会背概念」的阶段,开始真的在用架构视角写代码了。
更多推荐
所有评论(0)