一、同一个「组件」,两个世界都听不懂对方在说啥

你是不是也遇到过这种对话——

前端同事:

「这个页面的组件有点大了,我打算拆成三个组件。」

后端同事:

「可以啊,我这边也会拆成几个组件……呃等等,你说的是 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");
}

一切明明白白:

  • 你自己 newUserRepository

  • 再手动 newUserService

  • 谁依赖谁、谁创建谁,全靠你控制。


现在把 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 任何一个对象UserServiceUserRepository 却都能被用得好好的。

那这些对象是谁创建的?是谁把依赖塞进去的? 答案只有一个:

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);

  • 创建对象实例;

  • 做一些初始化回调(比如 @PostConstructInitializingBean 那些)。

EmailSenderUserService 这些实例被真正创建出来并存放到容器里后,我们才说:

「现在容器里有了一个 emailSender Bean,一个 userService Bean。」

于是你在别的类里就可以像这样依赖注入:

@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;

这个组件表面上也挺“清爽”的,但仔细想想:

  • 里面直接用了 useStateuseEffect 这些 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"),从容器启动一直活到容器关闭。 一个 UserService Bean,正常情况下只会被创建一次。

  • 初始化很讲究: 配置、连接池、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;

  • 或者你 懒得 把依赖解耦出来。

可以把测试策略分两层:

  1. 单元测试(不依赖 Spring)

    • 直接 new Bean,自行构造依赖(可以用 Mockito 等 mock);

    • 专注于业务逻辑本身是否正确。

  2. 集成测试(使用 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) 以后再听到「组件」这个词,先用那三问帮自己定位:

  1. 它是在画界面,还是在干业务?

  2. 它的生命周期,跟着应用走,还是跟着页面 / 请求走?

  3. 它是不是由某个容器 / 框架自动创建 & 注入的?

三个问题一过,你立刻就知道:

「哦,原来这是前端组件后端 Bean

还是一个纯工具模块。」


写到这,其实最想强调的是一句:

名字只是包装,角色才是核心。

当你能一句话说清楚:

  • 这个 Bean 在系统里负责什么;

  • 谁会用它;

  • 它活多久;

  • 它能不能在没有 Spring 的情况下跑和测

—— 你就已经远远超过了「只会背概念」的阶段,开始真的在用架构视角写代码了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐