Spring Boot 六边形架构(Hexagonal Architecture)实战到可测试!
🏆本文收录于《滚雪球学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 消息 → 调用输入端口 / 实现输出端口
适配器只做两件事:
- 协议转换(HTTP/JSON、SQL、消息体…)
- 调用端口(实现端口 或 调用端口)
它不应该长脑子。
如果适配器里开始出现一堆业务判断,你就等着未来某天重构到怀疑人生吧😇
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 -
- 点赞
- 收藏
- 关注作者
评论(0)