Spring Boot 六边形架构(Hexagonal Architecture)实战到可测试!

举报
bug菌 发表于 2026/01/13 11:40:55 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

前言:我到底为什么要折腾这个“六边形”啊?😅

先承认一件事:我第一次听到“六边形架构”的时候,脑子里出现的画面是——“又来一个新名词骗我重构?”(别笑,你肯定也有过这种瞬间🤣)

但很快我就被现实教育了:

  • Controller 里塞满业务逻辑,改一个需求要翻三层 if/else;
  • Service 一边算业务一边拼 SQL,测个逻辑得先起 DB;
  • 单元测试像写小说:@SpringBootTest 一开,十几秒过去了,测试还在“热身”;
  • 更惨的是,业务其实不复杂,复杂的是**“它和外部世界黏得太紧”**。

于是我开始认真看六边形架构——不是看二手总结那种“概念风”,而是去看这个模式最初的表述:Ports & Adapters(端口与适配器),核心目的就是把“外部世界”(Web、DB、MQ、第三方支付……)跟“业务核心”隔开,让业务像个硬核选手一样:

你随便换框架、换数据库、换协议都行,但别动我业务。
这味儿对了。

0. 先把共识讲清楚:六边形架构到底在解决什么?

六边形架构(Hexagonal Architecture)常被画成一个六边形——说实话这图像有点“营销”,但它表达的东西很朴素:

  • 业务核心(Inside):领域模型、业务规则、用例编排
  • 外部世界(Outside):HTTP、数据库、消息队列、文件系统、第三方 API
  • 端口(Ports):业务核心对外的“接口契约”(输入端口 / 输出端口)
  • 适配器(Adapters):把外部世界的协议/技术细节翻译成端口能理解的样子(Web Adapter / Persistence Adapter 等)

你可以把它理解成:

业务核心只会说“普通话”(端口接口),适配器负责做“同声传译”(HTTP、JPA、Kafka…)。

这就带来一个很现实的收益:

  • 业务逻辑可以脱离 Web、脱离 DB 独立测试
  • 依赖倒置(DIP)落实到结构上:核心依赖抽象(端口),外部实现抽象(适配器)
  • 技术栈更换不再牵一发动全身

1. 核心理念:Ports & Adapters(端口与适配器)到底怎么落地?

别急着画图,先把“端口”分清楚,否则你会写着写着又写回“三层架构换皮”。

1.1 输入端口(Inbound Port):别人怎么“驱动”你的业务?

输入端口通常对应用例(Use Case),比如:

  • TransferMoneyUseCase(转账用例)
  • OpenAccountUseCase(开户用例)
  • RedeemPointsUseCase(积分兑换用例)

它表达的是:系统允许外部对我做什么
外部可以是 Web Controller、消息消费端、定时任务……反正都是“驱动者”。

1.2 输出端口(Outbound Port):业务要完成用例,需要“依赖”什么外部能力?

比如转账用例通常需要:

  • 读写账户:AccountRepositoryPort
  • 记流水:LedgerPort
  • 发送通知:NotificationPort
  • 获取汇率:FxRatePort

注意这里的关键:

端口是业务语言,不是技术语言。
AccountRepositoryPort 没问题,但别整成 JpaAccountRepository 这种技术味儿的名字——一旦你这么命名,你的脑子就已经开始向 JPA 投降了🤣

1.3 适配器(Adapter):把外部世界“翻译”成端口

  • Web Adapter:HTTP 请求 → 用例命令 → 调用输入端口
  • Persistence Adapter:JPA/SQL → 实现输出端口
  • Messaging Adapter:Kafka 消息 → 调用输入端口 / 实现输出端口

适配器只做两件事:

  1. 协议转换(HTTP/JSON、SQL、消息体…)
  2. 调用端口(实现端口 或 调用端口)
    它不应该长脑子。
    如果适配器里开始出现一堆业务判断,你就等着未来某天重构到怀疑人生吧😇

2. 在 Spring Boot 中拆分 Domain / Application / Infrastructure(别拆成“形式主义”)

我见过最常见的“翻车方式”是:包名改成 domain/application/infrastructure 了,结果代码还是原来的味儿——Controller 直接注入 JPA Repository,Service 里拼 DTO,测试还得起容器……
这不叫六边形,这叫“换了个文件夹骗自己”。

我们用一个“转账”小项目来实战,结构先摆出来(建议 Maven/Gradle 都行):

com.example.banking
├── domain
│   ├── model
│   │   ├── Account.java
│   │   ├── Money.java
│   │   └── TransferRule.java
│   └── service
│       └── TransferDomainService.java
├── application
│   ├── port
│   │   ├── in
│   │   │   └── TransferMoneyUseCase.java
│   │   └── out
│   │       ├── LoadAccountPort.java
│   │       ├── UpdateAccountPort.java
│   │       └── RecordTransferPort.java
│   ├── command
│   │   └── TransferMoneyCommand.java
│   └── service
│       └── TransferMoneyService.java
└── infrastructure
    ├── adapter
    │   ├── in
    │   │   └── web
    │   │       └── TransferController.java
    │   └── out
    │       └── persistence
    │           ├── AccountJpaAdapter.java
    │           ├── AccountJpaEntity.java
    │           ├── AccountSpringDataRepository.java
    │           └── LedgerJpaAdapter.java
    └── config
        └── WiringConfig.java

一眼看过去你会发现:

  • domain:纯业务概念(Money、Account、规则、领域服务)
  • application:用例编排 + 端口定义(in/out ports)
  • infrastructure:Web/JPA 等技术细节(适配器 + 配置装配)

3. 依赖倒置原则(DIP):别背概念,Spring 里要“真倒置”

在 Spring 世界里,“依赖倒置”最容易被误解成:

“我用 @Autowired 注入了,所以我解耦了。”
不不不,这只是**依赖注入(DI)**的机制,解耦与否看你注入的是什么。

Spring 的 IoC/DI 本质是:对象不自己找依赖,而由容器注入,这样更易测试、也更易替换实现。
而我们要做的“倒置”是:

  • 业务核心依赖抽象(端口接口)
  • 基础设施去实现这些抽象
  • Spring 容器负责把“实现”注入到“核心”里

这才是完整闭环。

4. 实战:从 0 写一个“可脱离 Web/DB 测试”的转账用例

目标很明确:不用起 Web、不用连 DB,照样把核心业务测得明明白白。
你要的就是这种“硬气”对吧?😄

4.1 Domain:先写业务语言(Money / Account)

Money(值对象)

package com.example.banking.domain.model;

import java.math.BigDecimal;
import java.util.Objects;

public final class Money {
    private final BigDecimal amount;

    private Money(BigDecimal amount) {
        this.amount = amount;
    }

    public static Money of(long value) {
        return new Money(BigDecimal.valueOf(value));
    }

    public Money plus(Money other) {
        return new Money(this.amount.add(other.amount));
    }

    public Money minus(Money other) {
        return new Money(this.amount.subtract(other.amount));
    }

    public boolean isNegative() {
        return amount.compareTo(BigDecimal.ZERO) < 0;
    }

    public boolean isLessThan(Money other) {
        return this.amount.compareTo(other.amount) < 0;
    }

    public BigDecimal toBigDecimal() {
        return amount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money money)) return false;
        return Objects.equals(amount, money.amount);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount);
    }

    @Override
    public String toString() {
        return amount.toPlainString();
    }
}

这里我刻意让 Money 是不可变的值对象——不是装高级,而是为了少踩坑:钱这种东西你要是可变,后面调试会很刺激(刺激到想摔键盘那种😇)。

Account(聚合根)

package com.example.banking.domain.model;

import java.util.UUID;

public class Account {
    private final UUID id;
    private Money balance;

    public Account(UUID id, Money balance) {
        this.id = id;
        this.balance = balance;
    }

    public UUID id() {
        return id;
    }

    public Money balance() {
        return balance;
    }

    public void withdraw(Money money) {
        this.balance = this.balance.minus(money);
    }

    public void deposit(Money money) {
        this.balance = this.balance.plus(money);
    }
}

4.2 Domain Service:领域规则(比如不允许透支)

package com.example.banking.domain.service;

import com.example.banking.domain.model.Account;
import com.example.banking.domain.model.Money;

public class TransferDomainService {

    public void transfer(Account from, Account to, Money amount) {
        if (amount.isNegative()) {
            throw new IllegalArgumentException("amount must be positive");
        }
        if (from.balance().isLessThan(amount)) {
            throw new IllegalStateException("insufficient funds");
        }
        from.withdraw(amount);
        to.deposit(amount);
    }
}

领域服务的定位是:规则 + 操作模型
它不关心数据库、不关心 HTTP,不关心“请求是谁发的”,它只关心:转账这件事在业务上能不能成立

5. Application:用例(输入端口)+ 端口(输出)+ 用例实现

5.1 输入端口(In Port):TransferMoneyUseCase

package com.example.banking.application.port.in;

import com.example.banking.application.command.TransferMoneyCommand;

public interface TransferMoneyUseCase {
    void transfer(TransferMoneyCommand command);
}

5.2 命令对象(Command):把用例输入“收拢”起来

package com.example.banking.application.command;

import java.util.UUID;

public record TransferMoneyCommand(UUID fromAccountId, UUID toAccountId, long amount) { }

我喜欢用 record:

  • 天生不可变
  • 天生数据载体
  • 看着就不容易乱塞逻辑(对,就是在防止未来的我犯蠢😅)

5.3 输出端口(Out Port):Load / Update / Record

package com.example.banking.application.port.out;

import com.example.banking.domain.model.Account;

import java.util.UUID;

public interface LoadAccountPort {
    Account load(UUID accountId);
}
package com.example.banking.application.port.out;

import com.example.banking.domain.model.Account;

public interface UpdateAccountPort {
    void update(Account account);
}
package com.example.banking.application.port.out;

import java.util.UUID;

public interface RecordTransferPort {
    void record(UUID fromId, UUID toId, long amount);
}

5.4 用例实现(Application Service):TransferMoneyService

package com.example.banking.application.service;

import com.example.banking.application.command.TransferMoneyCommand;
import com.example.banking.application.port.in.TransferMoneyUseCase;
import com.example.banking.application.port.out.LoadAccountPort;
import com.example.banking.application.port.out.RecordTransferPort;
import com.example.banking.application.port.out.UpdateAccountPort;
import com.example.banking.domain.model.Money;
import com.example.banking.domain.service.TransferDomainService;

public class TransferMoneyService implements TransferMoneyUseCase {

    private final LoadAccountPort loadAccountPort;
    private final UpdateAccountPort updateAccountPort;
    private final RecordTransferPort recordTransferPort;
    private final TransferDomainService transferDomainService;

    public TransferMoneyService(
            LoadAccountPort loadAccountPort,
            UpdateAccountPort updateAccountPort,
            RecordTransferPort recordTransferPort,
            TransferDomainService transferDomainService
    ) {
        this.loadAccountPort = loadAccountPort;
        this.updateAccountPort = updateAccountPort;
        this.recordTransferPort = recordTransferPort;
        this.transferDomainService = transferDomainService;
    }

    @Override
    public void transfer(TransferMoneyCommand command) {
        var from = loadAccountPort.load(command.fromAccountId());
        var to = loadAccountPort.load(command.toAccountId());

        var amount = Money.of(command.amount());
        transferDomainService.transfer(from, to, amount);

        updateAccountPort.update(from);
        updateAccountPort.update(to);

        recordTransferPort.record(command.fromAccountId(), command.toAccountId(), command.amount());
    }
}

看到没?这个类非常“清爽”:

  • 没有 @RestController
  • 没有 JpaRepository
  • 没有 SQL
  • 甚至没有 Spring 依赖
    它就是纯 Java。这就是我们后面能“脱离 Web/DB 测核心”的根本原因。

6. Infrastructure:适配器(Web / Persistence)想怎么换都行

6.1 Web Adapter:Controller 只做协议转换

package com.example.banking.infrastructure.adapter.in.web;

import com.example.banking.application.command.TransferMoneyCommand;
import com.example.banking.application.port.in.TransferMoneyUseCase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/transfers")
public class TransferController {

    private final TransferMoneyUseCase useCase;

    public TransferController(TransferMoneyUseCase useCase) {
        this.useCase = useCase;
    }

    @PostMapping
    public ResponseEntity<Void> transfer(@RequestBody TransferRequest req) {
        useCase.transfer(new TransferMoneyCommand(
                UUID.fromString(req.fromAccountId()),
                UUID.fromString(req.toAccountId()),
                req.amount()
        ));
        return ResponseEntity.accepted().build();
    }

    public record TransferRequest(String fromAccountId, String toAccountId, long amount) { }
}

我对 Controller 的要求就一句话:

别自作主张,别掺和业务。
它负责把 HTTP 世界翻译成用例能听懂的命令,然后调用输入端口。到此为止。

6.2 Persistence Adapter:实现输出端口(比如先用内存,后用 JPA)

先来一个“内存适配器”(为了测试/演示)

package com.example.banking.infrastructure.adapter.out.persistence;

import com.example.banking.application.port.out.LoadAccountPort;
import com.example.banking.application.port.out.UpdateAccountPort;
import com.example.banking.domain.model.Account;
import com.example.banking.domain.model.Money;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class InMemoryAccountAdapter implements LoadAccountPort, UpdateAccountPort {

    private final Map<UUID, Account> store = new ConcurrentHashMap<>();

    public void initAccount(UUID id, long balance) {
        store.put(id, new Account(id, Money.of(balance)));
    }

    @Override
    public Account load(UUID accountId) {
        var acc = store.get(accountId);
        if (acc == null) {
            throw new IllegalArgumentException("account not found: " + accountId);
        }
        // 这里返回“拷贝”更安全,避免外部拿到引用乱改
        return new Account(acc.id(), acc.balance());
    }

    @Override
    public void update(Account account) {
        store.put(account.id(), new Account(account.id(), account.balance()));
    }

    public Account snapshot(UUID id) {
        return store.get(id);
    }
}

你看,这个适配器不需要 Spring,也不需要 DB。
它就是我们后面“核心业务脱离外部依赖测试”的关键道具之一。

记账端口的内存实现

package com.example.banking.infrastructure.adapter.out.persistence;

import com.example.banking.application.port.out.RecordTransferPort;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class InMemoryLedgerAdapter implements RecordTransferPort {

    public record Record(UUID from, UUID to, long amount) {}

    private final List<Record> records = new ArrayList<>();

    @Override
    public void record(UUID fromId, UUID toId, long amount) {
        records.add(new Record(fromId, toId, amount));
    }

    public List<Record> all() {
        return List.copyOf(records);
    }
}

7. Spring Boot 装配:用 Bean 注入把“倒置”真正落地

到这里很多人会懵一下:

“我写了那么多纯 Java 类,那 Spring 怎么把它们组装起来?”
答案是:用配置类明确装配(Wiring)。这一步很重要,因为它决定了你是不是“真解耦”。

Spring 的 IoC/DI 机制就是为这个干活的:由容器负责创建 bean 并注入依赖。

package com.example.banking.infrastructure.config;

import com.example.banking.application.port.in.TransferMoneyUseCase;
import com.example.banking.application.service.TransferMoneyService;
import com.example.banking.domain.service.TransferDomainService;
import com.example.banking.infrastructure.adapter.out.persistence.InMemoryAccountAdapter;
import com.example.banking.infrastructure.adapter.out.persistence.InMemoryLedgerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WiringConfig {

    @Bean
    public InMemoryAccountAdapter inMemoryAccountAdapter() {
        return new InMemoryAccountAdapter();
    }

    @Bean
    public InMemoryLedgerAdapter inMemoryLedgerAdapter() {
        return new InMemoryLedgerAdapter();
    }

    @Bean
    public TransferDomainService transferDomainService() {
        return new TransferDomainService();
    }

    @Bean
    public TransferMoneyUseCase transferMoneyUseCase(
            InMemoryAccountAdapter accountAdapter,
            InMemoryLedgerAdapter ledgerAdapter,
            TransferDomainService domainService
    ) {
        return new TransferMoneyService(
                accountAdapter,     // LoadAccountPort
                accountAdapter,     // UpdateAccountPort
                ledgerAdapter,      // RecordTransferPort
                domainService
        );
    }
}

这段装配看起来有点“啰嗦”,但它有一个巨大的好处:

  • 业务核心完全不知道 Spring
  • 你在这里想换成 JPA Adapter、Redis Adapter、Fake Adapter,都只改 wiring
  • 依赖倒置变成“结构事实”,不是口号

8. 重头戏:不依赖 Web 和 Database,怎么测试核心业务逻辑?

来了来了,你要的“硬菜”来了🍲。

我们直接写一个纯 JUnit 的测试:

  • 不起 Spring
  • 不起 Web
  • 不起 DB
  • 只测用例 + 领域规则
    这才叫真正意义上的“核心业务单元测试”。

JUnit 5 的生命周期注解(比如 @BeforeEach)用来在每个测试前初始化夹具。

package com.example.banking;

import com.example.banking.application.command.TransferMoneyCommand;
import com.example.banking.application.service.TransferMoneyService;
import com.example.banking.domain.service.TransferDomainService;
import com.example.banking.infrastructure.adapter.out.persistence.InMemoryAccountAdapter;
import com.example.banking.infrastructure.adapter.out.persistence.InMemoryLedgerAdapter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;

class TransferMoneyServiceTest {

    private InMemoryAccountAdapter accountAdapter;
    private InMemoryLedgerAdapter ledgerAdapter;
    private TransferMoneyService service;

    private UUID a;
    private UUID b;

    @BeforeEach
    void setUp() {
        accountAdapter = new InMemoryAccountAdapter();
        ledgerAdapter = new InMemoryLedgerAdapter();

        service = new TransferMoneyService(
                accountAdapter,
                accountAdapter,
                ledgerAdapter,
                new TransferDomainService()
        );

        a = UUID.randomUUID();
        b = UUID.randomUUID();

        accountAdapter.initAccount(a, 100);
        accountAdapter.initAccount(b, 10);
    }

    @Test
    void transfer_success_should_move_money_and_record_ledger() {
        service.transfer(new TransferMoneyCommand(a, b, 40));

        assertEquals("60", accountAdapter.snapshot(a).balance().toString());
        assertEquals("50", accountAdapter.snapshot(b).balance().toString());
        assertEquals(1, ledgerAdapter.all().size());
        assertEquals(40, ledgerAdapter.all().get(0).amount());
    }

    @Test
    void transfer_insufficient_funds_should_fail_and_not_change_anything() {
        var ex = assertThrows(IllegalStateException.class,
                () -> service.transfer(new TransferMoneyCommand(a, b, 999)));

        assertTrue(ex.getMessage().contains("insufficient"));
        assertEquals("100", accountAdapter.snapshot(a).balance().toString());
        assertEquals("10", accountAdapter.snapshot(b).balance().toString());
        assertEquals(0, ledgerAdapter.all().size());
    }
}

看到这里你应该会有一种很踏实的感觉:
测试跑得快、失败定位清晰、完全不受外界环境影响。
这就是 Ports & Adapters 在工程里最真实的价值。

9. 那 Spring Boot 测试怎么办?要不要 @SpringBootTest

我不反对 @SpringBootTest,但我反对“把它当单元测试写”。
很多项目测试慢,不是业务复杂,是你每次都把整个应用拉起来陪你跑步……这谁顶得住啊😂

Spring Boot 官方文档对测试提供了很多支持,并鼓励用更合适的切片测试/自动配置来降低负担。

我一般这么分层(很现实、也很省时间):

  • 纯单测(最快):像上面那样,不用 Spring
  • 装配测试(中等):只测 wiring 是否正确(可以起一个最小上下文)
  • 适配器集成测试(偏慢):比如 JPA Adapter + Testcontainers(这时才需要 DB)
  • 端到端(最慢):起 Web + DB 全链路

9.1 一个“装配是否正确”的最小 Spring 测试思路

你可以写一个非常克制的测试,只验证 Bean 能不能创建、依赖是否注入正确。
(这类测试的意义是:防止某次重构把 wiring 搞断了,线上才发现😇)

具体用哪种注解组合要看你项目的配置方式,但核心思想是:别为了测一行业务,把整个世界都叫来。

10. 常见误区:你以为你在六边形,其实你在“六边形表演”🎭

我来当一次“坏人”,把几种很常见的翻车姿势说透:

误区 1:端口接口里全是 DTO(而且 DTO 还是 Web 的那种)

这会导致你的核心层开始理解 HTTP 语义,最终还是被外部协议绑架。
建议:输入端口用 Command,输出端口用领域对象或领域语义明确的数据结构。

误区 2:Application Service 里写业务规则,Domain 变成贫血模型

用例层负责编排,领域层负责规则。如果用例层堆规则,后面复用就会痛苦。
建议:规则尽量下沉到 Domain(实体/值对象/领域服务),用例层做流程编排。

误区 3:适配器里出现大量 if/else 业务判断

适配器应该“翻译”,不是“决策”。
建议:一旦适配器里开始出现“业务意义”的分支,就问自己一句:

“我是不是又把核心逻辑扔到外面去了?”

误区 4:为了“解耦”把接口拆得碎成渣

端口不是越多越好,端口是围绕用例边界的。
建议:输出端口按“能力”聚合,不要把每个 CRUD 都拆成一个端口,拆到最后你自己都嫌烦🤣

11. 你真正会得到什么?(不是“架构高级感”,是可控性)

说到底,六边形架构不是为了让项目看起来“更像大厂”,而是让你在这些场景下不崩:

  • 产品说:下个月从 MySQL 换 PostgreSQL

    • 你:行,换适配器(别动核心)
  • 测试同学说:你这个核心逻辑能不能不要依赖 DB,我想跑快点

    • 你:行,给你 InMemory adapter / mock port
  • 领导说:我们要加一个消息队列触发转账

    • 你:行,加一个消息适配器去调用输入端口(核心不动)

这种“可控性”才是架构的意义。不是图画得漂亮,而是你半夜线上报警时能少骂两句自己🙂

12. 官方资料依据(我只押“原典”,不押二手)

为了避免“道听途说式架构”,这篇文章的核心依据来自:

  • 六边形架构/Ports & Adapters 的最初表述:Alistair Cockburn 原文说明了端口与适配器的内外不对称视角,以及模式的目的。
  • Spring 的 IoC/DI:Spring 官方文档对 IoC 容器与 Bean 的机制做了系统说明,这是我们用 Bean 注入落实依赖倒置的基础。
  • Spring Boot 测试:Spring Boot 官方文档介绍了测试支持与测试配置策略,强调按场景选择合适的测试方式。
  • JUnit 5 生命周期注解:JUnit 官方文档/接口说明了 @BeforeEach 等注解的语义与约束。

结尾:你真的需要六边形吗?(我给你一个反问😄)

如果你的项目:

  • 业务很轻,生命周期短,改两天就扔;
  • 团队就一两个人,大家默契十足;
  • 测试不要求、迭代不频繁;

那你确实可以不用六边形,别给自己加戏。

但如果你的项目:

  • 业务会长期演进,需求经常变;
  • 外部依赖(Web/DB/第三方)不稳定、经常换;
  • 你想让核心逻辑“可测、可替换、可迭代”;

那我想问你一句:

**你真的愿意把“业务正确性”押在某个框架和数据库的脾气上吗?**🙂

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.cn/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。