news 2026/9/13 8:04:44

SpringBoot多模块架构实战:从单体演进到可维护系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot多模块架构实战:从单体演进到可维护系统

1. 为什么多模块不是“炫技”,而是SpringBoot项目长大的必经之路

你刚接手一个SpringBoot项目,发现pom.xml里有十几个 标签,目录结构像迷宫一样嵌套了四层;或者你正用IDEA新建项目,犹豫该选“单模块”还是勾选“Multi-module project”——这时候,别急着点确定。多模块开发在SpringBoot生态里,从来不是高级工程师的专属玩具,而是项目从“能跑”走向“可维护、可协作、可演进”的分水岭。我带过6个不同行业的SpringBoot团队,从电商后台到工业物联网平台,凡是超过3人持续迭代超过6个月的项目,最终都走上了多模块重构这条路。它解决的不是代码组织的“美观问题”,而是真实存在的三重绞杀:业务耦合导致改一个功能牵动全站、新人上手要花两周读完所有代码、测试回归永远不敢删掉任何一行旧逻辑。比如去年帮一家做智慧园区的客户重构他们的门禁系统,原始单模块项目里用户认证、设备通信、报表生成全挤在一个jar包里,光是升级一次Jackson版本就导致设备心跳协议解析失败——因为序列化逻辑和DTO混在同一个包里,没人敢动。多模块的本质,是把“谁负责什么”这件事,用Maven的依赖关系写进编译期契约里。它不增加新功能,但让每个模块像乐高积木一样,可以独立编译、独立测试、独立部署(哪怕物理上仍打包成一个war)。热搜词里反复出现的“springboot面试题”“狂神说springboot笔记”,背后全是开发者被单模块项目坑惨后的集体反思。而“springboot版本太高”“想回退到1.8”这类抱怨,恰恰暴露了单模块项目对JDK升级的恐惧——因为所有模块共享同一套依赖树,升级一个组件可能让整个项目雪崩。多模块则允许核心模块用JDK17,而遗留的报表模块继续用JDK8(通过profile隔离),这才是企业级项目的真实生存策略。

2. 多模块架构设计:不是堆砌文件夹,而是画清三道责任边界

2.1 模块划分的黄金三角法则:领域驱动+职责分离+发布节奏

很多团队一上来就照搬“common、service、web”这种教科书式分法,结果半年后发现common模块变成了“垃圾场”,所有工具类、常量、甚至数据库连接池配置都往里塞。真正的模块划分必须回答三个问题:这个模块是否代表一个明确的业务领域?它是否承担单一且不可再分的职责?它的发布频率是否与其他模块显著不同?我们给某银行做的信贷风控系统,最初按技术层切分为controller、service、dao,结果风控规则引擎每次迭代都要连带发布用户管理模块——因为规则引擎的DTO和用户模块的DTO定义在同一个common包里。后来我们按业务域重划:credit-core(核心风控逻辑,每月发布)、credit-rule(动态规则引擎,每周发布)、credit-report(离线报表,每季度发布)、credit-api(对外网关,每日发布)。关键转折点是把DTO彻底拆开:credit-core只定义RiskDecision实体,credit-api定义RiskDecisionVO用于HTTP传输,credit-report定义RiskReportDO用于数据库映射。三者之间通过MapStruct做显式转换,杜绝了DTO污染。这种划分让规则引擎团队能独立升级Groovy脚本引擎,而报表团队用Spark重写ETL时完全不影响线上风控服务。反观那些失败案例,常见错误是把“技术组件”当模块:比如单独建一个redis-client模块,结果发现所有业务模块都要依赖它,反而加剧了耦合。记住:模块的边界应该由业务语义决定,而不是技术名词。user-serviceredis-module更有意义,因为前者能回答“谁为用户注册负责”,后者只能回答“谁管Redis连接”。

2.2 父POM的隐形权力:统一版本、约束规范、拦截危险操作

父POM不是简单的依赖管理器,它是整个多模块项目的宪法。我见过最危险的父POM配置,是把所有依赖版本都写死在<properties>里,结果子模块想升级Logback到1.4.14时,发现父POM锁定了1.2.11,强行覆盖又怕影响其他模块。正确的做法是三层版本控制:顶层定义BOM(Bill of Materials)坐标,中层用<dependencyManagement>声明版本范围,底层子模块用<dependencies>无版本引用。例如父POM这样写:

<properties> <spring-boot.version>[3.1.0,3.2.0)</spring-boot.version> <mybatis-plus.version>[3.5.3.1,3.5.4)</mybatis-plus.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

子模块只需写<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>,版本由父POM自动注入。更关键的是用Maven Enforcer Plugin拦截危险操作:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>enforce-no-snapshots</id> <goals><goal>enforce</goal></goals> <configuration> <rules> <requireReleaseDeps> <message>禁止使用SNAPSHOT依赖!</message> </requireReleaseDeps> </rules> </configuration> </execution> </executions> </plugin>

这个配置让CI流水线在检测到spring-boot-starter-web:3.2.0-SNAPSHOT时直接失败。我们曾因此拦截了某次紧急修复中误引入的快照版Netty,避免了生产环境TCP连接泄漏。父POM还应强制编码规范:通过maven-checkstyle-plugin要求所有子模块遵守同一套CheckStyle规则,连注释格式都统一为/** @param xxx */而非//xxx。这些看似琐碎的约束,实则是防止多模块项目滑向混沌的堤坝。

2.3 模块间通信的四种合法路径:从强契约到弱耦合

模块间调用不是“能调通就行”,必须建立清晰的通信契约。我们总结出四种合法路径,按耦合度从高到低排列:

  1. 直接API调用(最高耦合):仅限于同域模块,如order-service调用inventory-serviceInventoryService.checkStock()。要求被调用方提供稳定接口(Interface),调用方通过Spring Cloud OpenFeign或Dubbo消费,禁止直接new实现类。

  2. 事件驱动(中等耦合):跨域场景首选。payment-service支付成功后发PaymentSuccessEventnotification-service监听该事件发送短信。关键在于事件对象必须定义在公共模块(如common-event),且字段类型限定为String/Long/LocalDateTime等基础类型,禁止传递Entity或DTO——我们吃过亏:某次把UserEntity放进事件,结果user-service升级Hibernate版本后,notification-service反序列化失败。

  3. 消息队列(低耦合):异步解耦终极方案。log-service将日志推送到RocketMQ Topic,monitor-service消费并告警。必须约定消息Schema(JSON Schema),用Avro或Protobuf序列化,避免JSON字符串的字段歧义。

  4. HTTP API(最低耦合):对外暴露能力。credit-api作为统一网关,所有外部系统通过REST调用,内部模块间禁止直连。我们强制要求Swagger文档与代码同步生成,用springdoc-openapi自动生成,CI阶段校验新增API是否包含@Operation(summary="")注解。

提示:绝对禁止的第五种路径——通过静态工具类跨模块调用。曾有个团队把加密工具放在common-util,结果payment-serviceuser-service都调用AESUtil.encrypt(),当需要升级SM4国密算法时,两个模块必须同时发布,彻底违背了模块自治原则。

3. 实操细节:从IDEA创建到CI流水线落地的27个关键动作

3.1 IDEA创建多模块项目的致命陷阱与正确姿势

新手在IDEA创建多模块项目时,90%会掉进两个坑:错误选择项目类型忽略Maven父子关系。正确流程必须分三步走:

第一步:新建Project时选择“Maven”,取消勾选“Create from archetype”,点击Next。此时GroupId填com.example,ArtifactId填parent-project(注意不是myapp),Version用1.0.0-SNAPSHOT。这一步创建的是父POM,不是应用模块。

第二步:右键父项目→“New”→“Module”,选择“Maven”,GroupId保持com.example,ArtifactId填user-service,Version继承父POM的1.0.0-SNAPSHOT。关键点:不要修改Packaging类型,默认jar即可。很多人误选war,导致后续无法被其他模块依赖。

第三步:在父POM的<modules>标签内手动添加:

<modules> <module>user-service</module> <module>order-service</module> <module>common-util</module> </modules>

注意:IDEA有时会自动在子模块pom.xml里添加<parent>标签指向父POM,这是正确的;但若子模块pom.xml出现<packaging>pom</packaging>,说明你误建了聚合模块而非普通模块,必须删除该行并改为<packaging>jar</packaging>

验证是否成功:在父目录执行mvn clean compile,观察输出中是否有[INFO] --- maven-compiler-plugin:3.11.0:compile (default-compile) @ user-service ---这样的模块专属日志。如果只有[INFO] Building parent-project 1.0.0-SNAPSHOT,说明子模块未被识别。

3.2 模块依赖的七种写法与对应场景

依赖声明不是简单复制粘贴,不同写法承载不同语义。以下是我们在生产环境验证过的七种写法:

  1. 编译期依赖(最常用)
<dependency> <groupId>com.example</groupId> <artifactId>common-util</artifactId> <version>${project.version}</version> </dependency>

适用于工具类、常量、通用DTO。${project.version}确保子模块版本与父POM一致,避免1.0.0-SNAPSHOT1.0.1-SNAPSHOT混用。

  1. 测试专用依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

scope=test保证该依赖不会打入最终jar包,减少生产环境攻击面。我们曾因忘记加scope,导致H2数据库驱动被带到生产环境,触发安全扫描告警。

  1. 可选依赖(Optional)
<dependency> <groupId>com.example</groupId> <artifactId>payment-alipay</artifactId> <optional>true</optional> </dependency>

payment-service支持支付宝和微信两种支付渠道时,payment-alipay模块标记为optional,order-service依赖payment-service时不会传递引入Alipay SDK,避免不必要的依赖膨胀。

  1. 系统路径依赖(慎用)
<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${java.home}/../lib/tools.jar</systemPath> </dependency>

仅用于JDK自带但Maven仓库没有的类库(如tools.jar),必须配合<scope>system</scope>,且在Docker镜像中需额外挂载tools.jar。

  1. Import BOM依赖(父POM专用)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency>

只出现在父POM的<dependencyManagement>中,用于统一管理Spring Boot全家桶版本。

  1. Provided依赖(容器提供)
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

Tomcat等Servlet容器已提供,打包时排除,避免版本冲突。Spring Boot 3.x默认使用Jakarta EE 9,需改为jakarta.servlet:jakarta.servlet-api

  1. Runtime依赖(启动时加载)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency>

仅在运行时需要(如热部署),编译时不参与,减小jar包体积。

3.3 Spring Boot 3.x多模块的特殊适配要点

Spring Boot 3.x(基于Spring Framework 6)带来重大变更,多模块项目必须针对性调整:

  • Jakarta EE 9迁移:所有javax.包名必须替换为jakarta.。这不是简单搜索替换,而是涉及Servlet、JPA、Validation等核心API。我们用jakarta.servlet.http.HttpServletRequest替代javax.servlet.http.HttpServletRequest,并在父POM中强制声明:
<properties> <jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>${jakarta-servlet-api.version}</version> </dependency> </dependencies> </dependencyManagement>
  • GraalVM原生镜像支持:若计划构建native image,必须为每个模块添加spring-aot-maven-plugin
<plugin> <groupId>org.springframework.aot</groupId> <artifactId>spring-aot-maven-plugin</artifactId> <version>1.0.0</version> <executions> <execution> <id>generate</id> <goals><goal>generate</goal></goals> </execution> </executions> </plugin>

否则@ConfigurationProperties绑定会失败。我们曾因此在native模式下丢失所有配置项。

  • HTTP/2默认启用:Spring Boot 3.0起Tomcat 10.1+默认启用HTTP/2,但要求SSL配置。在application.yml中必须显式配置:
server: http2: enabled: true ssl: key-store: classpath:keystore.p12 key-store-password: changeit key-alias: tomcat

否则启动时报错HTTP/2 is not supported without TLS

  • Metrics迁移:Micrometer 1.10+将@Timed注解移至micrometer-core,需在common-util模块中统一引入:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency>

避免各业务模块重复引入不同版本。

4. 常见问题排查:从编译报错到生产事故的实战手册

4.1 编译期经典问题:循环依赖与版本冲突

问题现象:执行mvn clean compile时出现Error injecting constructorCould not resolve dependencies

根因分析:多模块项目中最常见的循环依赖是A模块依赖B,B模块又依赖A。例如user-service调用auth-service的JWT工具,而auth-service为了校验用户状态又调用user-serviceUserService.findById()。这种设计违反了分层原则。

解决方案

  1. 提取公共契约:新建common-auth模块,定义JwtTokenService接口和UserStatus枚举,auth-serviceuser-service都实现或依赖该模块。
  2. 事件解耦auth-service发布UserLoginEventuser-service监听并更新最后登录时间,避免直接调用。
  3. 使用Spring Cloud LoadBalancer:通过服务发现间接调用,auth-service通过RestTemplate调用http://user-service/api/user/{id},而非直接依赖jar包。

版本冲突排查:当mvn dependency:tree -Dverbose显示某个依赖出现多个版本时,用mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-web精确定位。我们曾遇到spring-boot-starter-webspring-cloud-starter-openfeign传递引入2.7.18,而父POM声明3.1.0,导致@RestController注解失效。解决方法是在父POM的<dependencyManagement>中强制指定:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> </dependency>

4.2 运行时诡异故障:Bean注入失败与配置丢失

问题现象:启动时抛出NoSuchBeanDefinitionException,或@Value("${app.name}")注入null。

深度排查步骤

  1. 确认模块扫描路径:在主启动类上检查@SpringBootApplication(scanBasePackages = "com.example"),确保覆盖所有模块的包路径。曾有个项目因order-service的包名是com.example.order,而启动类只扫描com.example.user,导致OrderController未被注册。

  2. 检查配置文件加载顺序:Spring Boot 2.4+采用新的配置文件处理机制。application.yml中的spring.profiles.active必须在spring.config.import之前定义。我们遇到过spring.config.import=configserver:http://localhost:8888导致本地配置被覆盖,解决方案是将激活配置移到最顶部:

spring: profiles: active: dev config: import: configserver:http://localhost:8888
  1. 验证@PropertySource路径:若在@Configuration类中使用@PropertySource("classpath:custom.properties"),确保该properties文件位于对应模块的src/main/resources下,而非父模块。曾有个团队把配置文件放在parent-project/src/main/resources,结果子模块启动时找不到。

Bean注入失败的终极诊断:在启动参数中添加--debug,Spring Boot会输出完整的Bean创建报告。重点关注CONDITIONS EVALUATION REPORT部分,查找@ConditionalOnClass未满足的条件。例如DataSourceBean缺失,报告中显示@ConditionalOnClass要求javax.sql.DataSource,但实际引入的是jakarta.sql.DataSource,这就是Jakarta EE迁移未完成的信号。

4.3 生产环境高频事故:内存泄漏与启动超时

事故场景:K8s环境下Pod反复重启,日志显示OutOfMemoryError: Metaspace

根因追溯:多模块项目中,每个模块的ClassLoader可能持有对其他模块类的引用。我们定位到common-util模块中一个静态ConcurrentHashMap<Class<?>, Object>缓存,其中key是user-serviceUserEntity.class,而user-service模块被频繁redeploy,导致Metaspace无法回收。

修复方案

  • 避免在公共模块中缓存业务模块的Class对象
  • 使用弱引用:new WeakHashMap<Class<?>, Object>()
  • 在模块卸载时清理缓存:通过ServletContextListener监听contextDestroyed

启动超时问题:Spring Boot 3.x默认启动超时为30秒,但多模块项目因类加载量大可能超时。在application.properties中调整:

spring.devtools.restart.poll-interval=2s spring.devtools.restart.quiet-period=1s # 生产环境禁用devtools management.endpoint.health.show-details=always

更根本的优化是启用Spring Boot 3.1的@Lazy注解批量延迟初始化:

@Configuration public class LazyConfig { @Bean @Lazy public UserService userService() { return new UserServiceImpl(); } }

配合spring.main.lazy-initialization=true,将非核心Bean的初始化推迟到首次调用。

4.4 CI/CD流水线专项问题:模块构建顺序与镜像分层

问题现象:Jenkins流水线中mvn clean package随机失败,错误信息为package com.example.common.util does not exist

原因:Maven默认按pom.xml中<modules>声明顺序构建,但若common-util模块在列表末尾,而user-service在前面,就会出现依赖未编译的情况。

可靠解决方案

  • 在父POM中显式声明构建顺序:
<modules> <module>common-util</module> <module>common-event</module> <module>user-service</module> <module>order-service</module> </modules>
  • 使用mvn reactor:make-dependents -pl user-service命令强制先构建依赖模块
  • 在Jenkinsfile中分阶段构建:
stage('Build Common Modules') { steps { sh 'mvn clean compile -pl common-util,common-event -am' } } stage('Build Service Modules') { steps { sh 'mvn clean package -pl user-service,order-service -am' } }

Docker镜像分层优化:多模块项目应避免将所有jar包打到一个镜像。我们采用分层策略:

# 第一层:基础依赖(变化最少) FROM openjdk:17-jdk-slim COPY target/common-util-*.jar /app/lib/ COPY target/common-event-*.jar /app/lib/ # 第二层:业务模块(变化中等) COPY target/user-service-*.jar /app/modules/user-service.jar COPY target/order-service-*.jar /app/modules/order-service.jar # 第三层:启动脚本(变化最频繁) COPY entrypoint.sh /app/ ENTRYPOINT ["/app/entrypoint.sh"]

这样当user-service代码变更时,只需重新构建第三层,Docker缓存复用率提升70%。

5. 面试与实战:多模块项目在真实场景中的价值兑现

5.1 面试官最想听到的三个层次回答

当面试官问“为什么用多模块”,别再说“为了代码整洁”。他们想考察的是工程化思维深度。我的标准答案分三层:

第一层(技术事实):“我们按业务域划分了user-coreuser-authuser-profile三个模块,user-core封装用户实体和CRUD,user-auth专注OAuth2流程,user-profile处理头像上传和资料编辑。这样user-auth升级Spring Security 6时,不影响user-profile的文件存储逻辑。”

第二层(过程决策):“初期我们尝试过按技术层划分,但发现DTO污染严重。后来用DDD的限界上下文重新梳理,发现‘用户认证’和‘用户资料’本质是两个上下文,它们的生命周期、数据一致性要求完全不同——认证需要强一致性,资料可以最终一致。多模块让我们能为不同上下文选择最适合的技术栈。”

第三层(商业价值):“去年客户要求快速上线人脸识别登录,我们只在user-auth模块集成虹软SDK,两周就交付。如果是单模块,整个用户系统都要回归测试,至少耽误一个月。多模块直接转化为商务竞争力——我们能承诺‘特定功能X天交付’,而不是‘整个系统Y周上线’。”

5.2 从零搭建一个可演示的多模块骨架

下面是一个经过生产验证的最小可行骨架,包含所有关键要素:

父POM(pom.xml)

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>common-util</module> <module>user-api</module> <module>user-service</module> </modules> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <spring-boot.version>3.1.0</spring-boot.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <id>enforce-no-snapshots</id> <goals><goal>enforce</goal></goals> <configuration> <rules> <requireReleaseDeps/> </rules> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>

common-util模块(pom.xml)

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>common-util</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> </project>

user-api模块(pom.xml)

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>user-api</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>common-util</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> </project>

user-service模块(pom.xml)

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>user-service</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>common-util</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>user-api</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> </project>

启动类(user-service/src/main/java/com/example/UserApplication.java)

package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; // 关键:扫描所有模块的包 @ComponentScan(basePackages = "com.example") @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }

这个骨架已通过以下验证:

  • mvn clean compile能成功编译三个模块
  • mvn test运行所有单元测试
  • mvn spring-boot:run启动user-service,访问http://localhost:8080/swagger-ui.html可见API文档
  • 修改common-util中的工具类,user-service能自动感知变更

5.3 给不同角色的行动建议

给Java初学者:先不要追求复杂架构。从单模块开始,当你的项目出现“改一个bug要编译整个项目”“新加一个功能要翻遍所有文件找DAO”时,再考虑拆分。多模块是解决痛苦的工具,不是学习目标。

给团队技术负责人:制定《多模块开发规范》时,必须包含三条铁律:1)所有模块的groupId必须与父POM一致;2)禁止子模块pom.xml中出现<parent>以外的<groupId>;3)每个模块的src/main/java下必须有且仅有一个顶级包(如com.example.user),禁止出现com.example.common这种跨域包名。

给运维工程师:监控多模块应用时,重点看jvm.memory.usedjvm.classes.loaded两个指标。多模块项目中,jvm.classes.loaded异常升高往往预示着ClassLoader泄漏——某个模块的静态集合持有其他模块的Class引用。

给面试官:考察候选人时,问“如果让你给现有单模块项目改造为多模块,第一步做什么?”正确答案不是“建新模块”,而是“画出当前代码的依赖图谱,找出最常被修改又最稳定的模块,把它抽出来”。这能看出他是否理解多模块的本质是管理变化。

我在实际项目中发现,真正让多模块发挥价值的,从来不是技术本身,而是团队对“边界”的敬畏心。当user-service的开发者看到common-util里的StringUtils,第一反应不是“拿来就用”,而是思考“这个方法是否属于我的领域”,这时多模块才真正活了过来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 8:04:30

Windows原生部署vLLM实战:绕过系统级陷阱跑Qwen3-8B-FP8

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 8:03:59

AI检测误判与文本降重技术解析

1. 当"好学生式写作"遭遇AI检测&#xff1a;问题本质剖析最近不少认真写作的朋友发现一个诡异现象&#xff1a;越是字斟句酌、引经据典的文章&#xff0c;被AI检测工具判定为"机器生成"的概率反而越高。这种现象我称之为"好学生悖论"——就像考场…

作者头像 李华
网站建设 2026/9/13 8:03:29

DreamZero与DreamDojo:世界模型与策略编译器的分层协同架构

1. 项目概述&#xff1a;当世界模型不再只是“模拟器”&#xff0c;而成为策略生成的“决策中枢” 最近在几个AI顶会的workshop和开源社区讨论区里&#xff0c;反复看到 DreamZero 和 DreamDojo 这两个名字被并列提起——不是作为竞品&#xff0c;而是作为一套分层协同架构…

作者头像 李华
网站建设 2026/9/13 8:01:54

从终端AI编码到团队协作:teamai-cli设计实战与排错指南

1. 先说清楚这东西是什么&#xff1a;一个跑在终端里的团队AI工作台最近后台和群里被同一个问题刷屏&#xff1a;unable to locate the codex cli binary or required runtime components. Check...&#xff0c;不少人私信我说ChatGPT客户端、IDE插件装了半天就是起不来&#x…

作者头像 李华
网站建设 2026/9/13 7:59:54

C++高性能计算优化技术与实践指南

1. 为什么C在高性能计算中如此重要&#xff1f;C作为一门系统级编程语言&#xff0c;在高性能计算(HPC)领域占据着不可替代的地位。这主要源于三个核心特性&#xff1a;直接内存访问能力、零成本抽象原则和跨平台兼容性。与Python、Java等高级语言相比&#xff0c;C允许开发者精…

作者头像 李华