资讯动态

SpringBoot多模块架构实战:从单体到可维护工程的演进路径

发布时间:2026/10/17 0:58:10 来源:尧图企业网站定制
1. 为什么“多模块”不是炫技而是SpringBoot项目长大的必经之路刚接手一个新需求时我总习惯先建个单模块SpringBoot项目spring-boot-starter-web一加application.yml一配Controller写两行接口就跑起来了。快、轻、无负担——这确实是SpringBoot最迷人的起点。但去年参与某高校实验室的跨平台数据中台项目时这种“快乐”只持续了不到三周。项目初期只有用户认证和基础数据查询两个功能代码全塞在demo-api模块里连Git提交记录都透着一股轻松劲儿。可当导师提出要接入第三方设备协议解析、增加离线报表导出、还要为移动端单独提供精简版API时问题来了每次改一个设备驱动的bug整个demo-api都要重新编译、打包、部署前端同事想调用报表接口却得把整套用户权限逻辑也拉进测试环境更头疼的是设备协议SDK本身有独立的版本迭代节奏而我们的业务代码却和它死死耦合在同一个jar包里。这时候“多模块”不再是教科书里的概念而是压在肩膀上的现实选择。它解决的从来不是“能不能做”而是“能不能稳、能不能分、能不能活”。所谓“蓝鲸云”这类名称背后往往对应着真实场景中对系统可维护性、团队协作效率和长期演进能力的硬性要求——不是所有项目都需要拆但一旦业务复杂度越过某个临界点强行维持单模块就像用胶带缠住正在膨胀的气球迟早会听到“啪”的一声。我后来统计过在那个中台项目里模块拆分后日常CI构建时间从平均8分钟降到2分17秒设备协议组和报表组可以完全并行开发互不干扰最关键的是当第三方SDK升级引发兼容性问题时我们只需隔离测试device-protocol模块而不是让整个中台服务停摆两小时。多模块的本质是把“人”的协作边界映射到“代码”的物理边界上。它不改变SpringBoot的启动原理却彻底重构了工程的组织逻辑——这不是架构师画在PPT上的虚线而是开发者每天在IDE里打开不同模块、提交不同Git分支、调试不同端口时实实在在踩出来的路。2. 模块划分的底层逻辑从“功能切分”到“职责收敛”很多初学者一上来就想照着网上教程把项目切成api、service、dao、model四个模块。这看似合理实则埋下了大坑。我见过太多项目user-api模块里混着用户注册的DTO、密码加密工具类、甚至一段发短信的Feign客户端代码而user-service模块里又塞进了数据库连接池配置和Redis缓存策略。结果呢模块名是“service”实际却承担了网络通信、数据持久化、安全校验三重职责。这种划分只是把单模块的混乱复制粘贴成了多个模块的混乱。真正的模块划分必须回归到“单一职责原则”的原始定义一个模块应该且仅应对一类变化负责。这个“变化”不是指代码行数的增减而是指外部依赖、业务规则、技术栈或团队归属的变动频率。比如在蓝鲸云类项目中我通常会这样锚定核心模块core-domain存放领域模型如User、Device、核心业务规则如“用户注册需校验手机号唯一性”、以及领域服务接口UserService。它不依赖任何框架纯Java代码编译后是core-domain-1.0.0.jar。它的变化源只有一个业务需求本身。当导师说“用户角色要增加‘设备管理员’”时这里才需要动。infrastructure-persistence专门处理数据落地。它依赖spring-boot-starter-jdbc实现core-domain中定义的UserRepository接口内部用JdbcTemplate操作MySQL。它的变化源是数据库选型或SQL优化——如果后期要迁移到PostgreSQL只需重写这个模块core-domain完全不动。interface-web纯粹的HTTP入口层。它依赖spring-boot-starter-web接收REST请求调用core-domain的服务再把结果转成JSON返回。它的变化源是API协议变更如从JSON换为Protobuf或网关策略调整。前端同学改个接口路径只在这里改RequestMapping不影响其他任何模块。adapter-device对接第三方设备协议。它封装了SDK的调用细节暴露统一的DeviceDriver接口给core-domain使用。它的变化源是设备厂商发布的SDK新版本。当厂商发布v3.2 SDK时我们只更新这个模块的依赖和适配代码core-domain里关于“获取设备状态”的业务逻辑岿然不动。这种划分下模块间的依赖关系是单向的interface-web→core-domain→infrastructure-persistenceadapter-device通过接口被core-domain调用。我用Maven的dependency标签强制约束这种流向一旦core-domain意外引入了spring-web依赖Maven编译就会报错——这不是限制而是保护。曾有个实习生试图在core-domain里直接写RestTemplate调用外部服务被CI流水线的maven-enforcer-plugin当场拦截他这才明白模块边界不是文件夹而是编译期的防火墙。提示判断模块是否“职责收敛”有个极简测试法——问自己“如果明天这个模块要交给另一个团队维护他们是否能不看其他模块代码仅凭本模块的接口文档和单元测试就完成一次独立发布” 如果答案是否定的说明职责还没收干净。3. 从零搭建手把手构建可落地的多模块骨架现在让我们真正动手。别急着敲命令先明确一个关键前提多模块项目的根目录必须是一个Maven的pom.xml且其packaging必须是pom。这是整个结构的基石就像盖楼的地基。很多新手失败第一步就栽在这里——他们用IDEA的“New Project”向导创建了一个SpringBoot项目然后试图在它下面手动新建文件夹当模块结果Maven根本识别不了。3.1 初始化父POM用最简配置守住底线在空目录下创建pom.xml内容如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion !-- 这是父POM的坐标必须唯一 -- groupIdcom.example.bluewhale/groupId artifactIdbluewhale-parent/artifactId version1.0.0-SNAPSHOT/version packagingpom/packaging !-- 统一管理所有子模块的SpringBoot版本 -- properties java.version17/java.version spring-boot.version3.2.5/spring-boot.version maven.compiler.source${java.version}/maven.compiler.source maven.compiler.target${java.version}/maven.compiler.target /properties !-- 依赖管理声明版本不引入具体依赖 -- dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement !-- 声明子模块 -- modules modulecore-domain/module moduleinfrastructure-persistence/module moduleinterface-web/module moduleadapter-device/module /modules /project注意几个细节packagingpom/packaging是铁律dependencyManagement里只管版本不写scopecompile/scopemodules顺序无关紧要但必须和实际文件夹名严格一致。此时执行mvn clean compileMaven会提示“找不到子模块”这是正常的——因为我们还没创建它们。3.2 创建子模块每个模块都是独立的Maven项目在根目录下依次创建四个文件夹core-domain、infrastructure-persistence、interface-web、adapter-device。每个文件夹内都放一个独立的pom.xml。以core-domain为例?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion !-- 子模块坐标继承父POM的groupIdartifactId自定义 -- parent groupIdcom.example.bluewhale/groupId artifactIdbluewhale-parent/artifactId version1.0.0-SNAPSHOT/version /parent artifactIdcore-domain/artifactId version1.0.0-SNAPSHOT/version !-- 注意这里不是pom而是jar -- packagingjar/packaging !-- 此模块不依赖任何Spring保持纯净 -- dependencies !-- Lombok简化代码非必需但强烈推荐 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies /project关键点在于parent指向根POMpackaging是jar表示它要被打包成可被引用的库dependencies里没有SpringBoot相关依赖——这是core-domain保持“框架无关”的保证。同理interface-web的pom.xml会这样写parent groupIdcom.example.bluewhale/groupId artifactIdbluewhale-parent/artifactId version1.0.0-SNAPSHOT/version /parent artifactIdinterface-web/artifactId version1.0.0-SNAPSHOT/version packagingjar/packaging dependencies !-- 继承父POM管理的SpringBoot版本 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 依赖core-domain模块 -- dependency groupIdcom.example.bluewhale/groupId artifactIdcore-domain/artifactId version1.0.0-SNAPSHOT/version /dependency /dependencies这里出现了关键依赖core-domain。Maven会自动在本地仓库查找这个jar而无需你手动安装。执行mvn clean compile你会发现四个模块依次编译成功——骨架已立。3.3 让SpringBoot启动器“认出”多模块结构很多人卡在最后一步interface-web模块里写了SpringBootApplication但运行时报ClassNotFoundException。原因很简单SpringBoot的启动类默认只扫描自己包路径下的组件。interface-web的启动类在com.example.bluewhale.web包下它不会自动去com.example.bluewhale.domaincore-domain的包里找Service。解决方案是显式指定扫描范围。在interface-web的启动类上添加ComponentScanSpringBootApplication // 显式扫描core-domain和infrastructure-persistence的包 ComponentScan(basePackages { com.example.bluewhale.web, com.example.bluewhale.domain, com.example.bluewhale.infra }) public class InterfaceWebApplication { public static void main(String[] args) { SpringApplication.run(InterfaceWebApplication.class, args); } }同时在core-domain模块中将领域模型和接口放在com.example.bluewhale.domain包下在infrastructure-persistence中将JDBC实现放在com.example.bluewhale.infra包下。这样启动类就能“看到”所有必要组件。我试过用SpringBootApplication(scanBasePackages {...})替代ComponentScan效果一样但ComponentScan语义更清晰明确表达了“这是跨模块扫描”的意图。注意不要在core-domain里放SpringBootApplication它不是启动模块只是一个被依赖的库。只有interface-web或类似入口模块才需要启动类。4. 模块间通信的三种姿势何时用接口何时用事件何时用HTTP模块拆开后最大的困惑不是“怎么建”而是“怎么连”。我见过太多项目interface-web模块里直接new UserServiceImpl()或者core-domain里硬编码调用RestTemplate访问adapter-device的HTTP接口。这等于把模块边界又亲手焊死了。正确的通信方式必须匹配模块的职责层级。4.1 接口注入最常用、最稳妥的强依赖这是core-domain与infrastructure-persistence之间的标准模式。core-domain定义接口// core-domain/src/main/java/com/example/bluewhale/domain/UserRepository.java public interface UserRepository { User findById(Long id); void save(User user); }infrastructure-persistence实现它// infrastructure-persistence/src/main/java/com/example/bluewhale/infra/jdbc/JdbcUserRepository.java Repository // Spring注解让Spring管理这个Bean public class JdbcUserRepository implements UserRepository { private final JdbcTemplate jdbcTemplate; public JdbcUserRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Override public User findById(Long id) { // JDBC查询逻辑 } }interface-web通过Spring的依赖注入获得UserRepository实例RestController public class UserController { private final UserRepository userRepository; // 接口类型非实现类 public UserController(UserRepository userRepository) { this.userRepository userRepository; // Spring自动注入JdbcUserRepository } GetMapping(/users/{id}) public User getUser(PathVariable Long id) { return userRepository.findById(id); } }这种方式的优势在于core-domain完全不知道JdbcUserRepository的存在它只依赖抽象infrastructure-persistence可以随时换成MyBatisUserRepository或MongoUserRepository只要实现同一接口上层代码零修改。这是面向对象设计的黄金法则。4.2 事件驱动解耦“副作用”避免事务污染当一个操作需要触发多个不相关的后续动作时接口注入就显得笨重。比如用户注册成功后需要1发送欢迎邮件2初始化用户仪表盘3通知设备管理模块分配初始设备。如果把这些都写在UserService.register()方法里会导致三个问题1UserService职责爆炸2邮件发送失败导致整个注册事务回滚通常不该如此3adapter-device模块和interface-web模块产生循环依赖interface-web调core-domaincore-domain又调adapter-device。解决方案是Spring的事件机制。在core-domain中定义事件// core-domain/src/main/java/com/example/bluewhale/domain/event/UserRegisteredEvent.java public record UserRegisteredEvent(Long userId) {}在UserService中发布事件Service public class UserService { private final ApplicationEventPublisher eventPublisher; // Spring提供的事件发布器 public UserService(ApplicationEventPublisher eventPublisher) { this.eventPublisher eventPublisher; } public void register(User user) { // 1. 保存用户到数据库 userRepository.save(user); // 2. 发布事件不关心谁来处理 eventPublisher.publishEvent(new UserRegisteredEvent(user.getId())); } }在infrastructure-persistence中监听并发送邮件作为基础设施的一部分Component public class EmailNotificationListener { EventListener public void handleUserRegistered(UserRegisteredEvent event) { // 发送邮件逻辑可异步执行 sendWelcomeEmail(event.userId()); } }在adapter-device中监听并分配设备Component public class DeviceAllocationListener { EventListener public void handleUserRegistered(UserRegisteredEvent event) { // 调用设备SDK分配设备 deviceDriver.allocateInitialDevices(event.userId()); } }事件的核心价值在于发布者和订阅者完全解耦。UserService不需要知道邮件和设备模块是否存在它只负责“发生了什么”而邮件和设备模块也不需要主动去调用UserService它们被动响应。更重要的是事件监听器默认在同一个事务中执行但你可以用Async注解将其移出事务确保邮件发送失败不影响用户注册。4.3 HTTP客户端跨进程、跨语言的终极解耦当adapter-device模块需要被core-domain调用但又不能直接依赖其jar包比如设备SDK是C写的只能提供HTTP API这时就必须用HTTP。但关键在于HTTP调用的发起方必须是core-domain定义的接口而非具体实现。在core-domain中定义// core-domain/src/main/java/com/example/bluewhale/domain/DeviceDriver.java public interface DeviceDriver { DeviceStatus getDeviceStatus(Long deviceId); }在adapter-device中创建一个DeviceDriverImpl它内部用RestTemplate或WebClient调用设备服务// adapter-device/src/main/java/com/example/bluewhale/adapter/device/HttpDeviceDriver.java Component public class HttpDeviceDriver implements DeviceDriver { private final WebClient webClient; public HttpDeviceDriver(WebClient.Builder builder) { this.webClient builder.baseUrl(http://device-service:8080).build(); } Override public DeviceStatus getDeviceStatus(Long deviceId) { return webClient.get() .uri(/devices/{id}/status, deviceId) .retrieve() .bodyToMono(DeviceStatus.class) .block(); // 简化示例生产环境建议用Mono链式调用 } }core-domain的UserService依然只依赖DeviceDriver接口完全不知道HTTP的存在。这种设计让adapter-device模块未来可以无缝替换为gRPC客户端、消息队列消费者甚至一个模拟的MockDeviceDriver用于测试而core-domain代码一行不用改。实操心得我曾在某次压测中发现HttpDeviceDriver的block()调用导致线程阻塞拖垮了整个interface-web。后来改成WebClient的响应式调用并配合AsyncQPS立刻提升了3倍。记住HTTP调用永远是性能瓶颈点必须监控其超时、重试和熔断策略。5. 构建与部署的实战陷阱为什么mvn clean install总失败骨架搭好代码写完一到构建环节各种诡异问题就冒出来了。这些不是SpringBoot的Bug而是多模块项目特有的“集成阵痛”。我把最常见的三个坑连同排查链路一起列出来。5.1 依赖传递失效明明装了Lombok却报Data找不到现象core-domain模块用了Lombok的DataIDE显示正常但执行mvn clean compile时编译器报错“cannot find symbol Data”。检查pom.xmllombok依赖确实在dependencies里且optionaltrue/optional也写了。根因定位过程首先确认Lombok插件是否安装到IDEIntelliJ IDEA需安装Lombok Plugin并在Settings→Build→Compiler→Annotation Processors中勾选“Enable annotation processing”。这是IDE层面的问题和Maven无关。如果IDE正常但Maven编译失败则检查Maven的lombok依赖作用域。optionaltrue/optional意味着该依赖不会传递给依赖core-domain的模块如interface-web。但core-domain自身编译时需要Lombok作为编译期依赖。解决方案在core-domain的pom.xml中将Lombok依赖改为dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId scopeprovided/scope !-- provided表示编译和测试时可用运行时不包含 -- /dependencyprovided比optional更符合Lombok的语义——它只在编译时需要运行时JVM并不加载Lombok类。mvn clean compile后问题消失。5.2 模块间循环依赖Maven报错“Cycle detected”现象interface-web依赖core-domaincore-domain又依赖adapter-device而adapter-device为了调用设备SDK又需要读取interface-web里的全局配置如application.yml中的设备服务地址。Maven执行mvn clean compile时报错[ERROR] Cycle detected in the module dependency graph: [ERROR] com.example.bluewhale:interface-web:jar:1.0.0-SNAPSHOT [ERROR] └─ com.example.bluewhale:core-domain:jar:1.0.0-SNAPSHOT [ERROR] └─ com.example.bluewhale:adapter-device:jar:1.0.0-SNAPSHOT [ERROR] └─ com.example.bluewhale:interface-web:jar:1.0.0-SNAPSHOT这是一个经典的架构错误。core-domain作为最核心的模块绝不能依赖任何基础设施模块如interface-web或adapter-device否则整个分层就崩塌了。修复方案步骤1将设备服务地址等配置从interface-web的application.yml中剥离放入一个独立的config-center模块或直接用Spring Cloud Config由所有模块共同依赖。步骤2adapter-device模块通过Value(${device.service.url})读取配置而不是去interface-web里硬编码。步骤3在bluewhale-parent的dependencyManagement中统一管理config-center的版本确保所有模块用同一份配置。这样依赖链变为interface-web→core-domain→adapter-device而adapter-device只依赖config-center不再反向依赖interface-web。5.3 启动失败No qualifying bean of type X available现象interface-web启动时报错NoSuchBeanDefinitionException: No qualifying bean of type com.example.bluewhale.domain.UserRepository available。明明infrastructure-persistence模块里有JdbcUserRepository且加了Repository注解。排查链路首先检查JdbcUserRepository的包路径是否在ComponentScan的basePackages范围内。如果它在com.example.bluewhale.infra.jdbc而ComponentScan只写了com.example.bluewhale.infra那就不行——必须精确到com.example.bluewhale.infra.jdbc。检查infrastructure-persistence模块的pom.xml是否漏掉了spring-boot-starter-jdbc依赖没有这个JdbcTemplate类都不存在JdbcUserRepository根本无法实例化。最隐蔽的坑infrastructure-persistence模块的pom.xml里parent是否正确指向了bluewhale-parent如果误写成spring-boot-starter-parent那么JdbcUserRepository会被Spring Boot的自动配置机制忽略因为它不在Spring Boot的默认扫描路径下。最终解决方案在interface-web的启动类上加上Import(JdbcUserRepository.class)强制导入但这只是临时手段。根本解法是确保infrastructure-persistence模块的pom.xml正确继承父POM并在ComponentScan中包含其完整包路径。踩坑总结多模块项目的构建失败90%以上源于Maven依赖传递和Spring组件扫描的双重失配。我的固定排查顺序是1mvn dependency:tree -Dverbose看依赖树是否完整2mvn clean compile -X看详细编译日志3检查ComponentScan和包路径4确认所有模块的parent指向正确。这套组合拳能解决我遇到的99%的构建问题。6. 从“能跑”到“能管”多模块项目的可观测性实践项目跑起来只是开始如何在生产环境快速定位问题才是多模块价值的终极体现。单模块时代一个java -jar app.jar日志全打在一个文件里grep一下就完事。但多模块部署后interface-web、adapter-device可能分属不同服务器日志分散调用链断裂。我分享三个在蓝鲸云类项目中验证有效的实践。6.1 日志归集用Logback的appender-ref统一出口每个模块都应有自己的logback-spring.xml但日志输出目标必须统一。在interface-web的logback-spring.xml中configuration !-- 定义一个公共的ConsoleAppender -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 核心所有模块都引用这个appender -- root levelINFO appender-ref refCONSOLE/ /root /configuration而在adapter-device模块中logback-spring.xml只需简单引用configuration !-- 直接引用interface-web定义的appender -- include resourcelogback-console-appender.xml/ root levelINFO appender-ref refCONSOLE/ /root /configuration这样无论哪个模块打印日志格式都一致且可通过-Dlogging.level.com.example.bluewhaleDEBUG统一控制所有模块的日志级别。更重要的是当所有模块都部署在Kubernetes中时我们可以用Filebeat收集所有容器的stdout统一发送到Elasticsearch实现跨模块日志检索。6.2 分布式追踪用Spring Cloud Sleuth串联调用链用户一个请求可能经过interface-web→core-domain→adapter-device→ 设备HTTP服务。传统日志里你只能看到interface-web的200 OK却不知道adapter-device调用设备服务花了多少毫秒。Sleuth就是为此而生。在interface-web和adapter-device的pom.xml中加入dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-sleuth/artifactId /dependency启动后每个日志行会自动带上[interface-web,1234567890abcdef,1234567890abcdef,true]这样的traceId和spanId。当interface-web调用adapter-device的HTTP接口时Sleuth会自动在HTTP Header中注入X-B3-TraceIdadapter-device收到后会沿用同一个traceId打日志。这样你在ELK里搜索traceId1234567890abcdef就能看到一条完整的调用链interface-web处理了12ms其中adapter-device耗时8ms而adapter-device又花了5ms调用设备服务。注意core-domain模块不需要引入Sleuth依赖因为它是纯内存调用不涉及网络traceId由interface-web发起自动透传给adapter-device。强行在core-domain里加Sleuth反而会增加不必要的性能开销。6.3 健康检查为每个模块定制/actuator/healthSpring Boot Actuator的健康端点默认只检查DataSource、Redis等。但在多模块中adapter-device的健康取决于它能否连通设备服务core-domain的健康取决于UserRepository是否可用。我们需要定制。在adapter-device模块中创建一个DeviceHealthIndicatorComponent public class DeviceHealthIndicator implements HealthIndicator { private final DeviceDriver deviceDriver; public DeviceHealthIndicator(DeviceDriver deviceDriver) { this.deviceDriver deviceDriver; } Override public Health health() { try { // 调用设备SDK的ping方法 deviceDriver.ping(); return Health.up().withDetail(status, OK).build(); } catch (Exception e) { return Health.down().withDetail(error, e.getMessage()).build(); } } }在application.yml中启用management: endpoint: health: show-details: always endpoints: web: exposure: include: health,info,metrics这样访问http://adapter-device:8080/actuator/health返回{ status: UP, components: { diskSpace: { status: UP }, ping: { status: UP }, device: { status: UP, details: { status: OK } } } }运维同学可以通过这个端点一键判断设备模块是否就绪而无需登录服务器去查日志。这才是多模块项目交付给运维的真实价值——不是代码更炫而是故障定位更快、系统更透明。7. 我的个人经验多模块不是银弹何时该坚持单模块写到这里必须说句逆耳的话多模块不是万能解药过度拆分比不拆分更可怕。我见过一个只有3个接口的内部工具项目硬生生拆成api、service、dao、model、config五个模块结果每次改一个字段要打开5个IDEA窗口提交5次Git构建时间从15秒变成2分钟。团队抱怨声一片最后不得不回滚。判断是否该上多模块我用三个硬性指标团队规模 ≥ 3人当一个人写不完所有代码且不同成员负责不同功能域如A负责用户B负责设备C负责报表时模块边界就是协作边界的自然映射。2人小团队沟通成本远低于模块管理成本单模块更高效。代码行数 5000行这是经验阈值。少于5000行src/main/java下的包结构controller、service、repository已足够清晰超过此数包名嵌套过深com.example.bluewhale.web.controller.v1.userIDE导航困难此时模块化能大幅降低认知负荷。存在独立演进的外部依赖如设备SDK、支付网关、AI模型服务等。这些依赖有自己的版本号、升级节奏和故障模式。把它们封装进独立模块能隔离风险避免一次SDK升级导致整个系统停摆。如果三个条件都不满足别碰多模块。老老实实用单模块把精力放在写好单元测试、设计好API契约、做好数据库索引上。SpringBoot的魅力恰恰在于它既支持“小而美”的单模块快速交付也支撑“大而稳”的多模块长期演进。选择哪种不取决于技术炫酷而取决于你手头那个真实项目的需求水位线。最后分享一个小技巧在项目初期可以用“伪多模块”过渡。即物理上还是单模块但逻辑上严格按core-domain、infrastructure、interface的包结构组织代码并用ArchUnit写测试禁止domain包下的类调用web包下的类。等哪天真的需要拆了把每个包剪切粘贴到新模块里pom.xml一配5分钟搞定。这比一开始就纠结模块划分务实得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑