ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

谷粒商城微服务工程搭建:基于Spring Cloud Alibaba的多模块聚合工程实战

谷粒商城微服务工程搭建:基于Spring Cloud Alibaba的多模块聚合工程实战

我在整理分布式电商系统的开发教程时,发现"谷粒商城"这个项目非常有代表性。平时带团队做微服务改造,新成员上手最头疼的往往不是业务逻辑,而是整个工程怎么组织、模块怎么拆分、依赖怎么管理。这类教学项目的价值正在于此:它把一套真实电商后端的微服务架构完整铺开,从工程构建的第一行代码开始,让你逐步建立起对分布式系统全貌的认知。

"谷粒商城"是典型的Spring Cloud Alibaba微服务实战项目,模拟的是完整电商业务域:商品、订单、会员、库存、优惠券、支付、搜索、购物车等。本文聚焦第一阶段"构建工程篇",核心是把多模块聚合工程的基础打好,把父工程、公共模块、各微服务模块的骨架全部搭建并跑通。这个阶段虽然不写具体业务,却决定了后续开发的基调:代码放哪个模块、依赖引到哪一层、服务之间如何调用、版本冲突怎么避免。适用人群包括:准备系统学习微服务的学生和开发者、工作中需要搭建微服务工程架构的后端工程师,以及想了解大型电商系统模块边界如何划分的技术管理者。准备好后,我们从工程底座开始捋一遍。

1. 内容整体设计与思路拆解

1.1 为什么是聚合工程而不是多个独立项目

很多人第一次接触微服务时,第一反应是"不就是多建几个Spring Boot工程吗"。一开始我也这么想,直到真正动手做才发现,分布式系统的问题大多出在工程边界和依赖关系上。比如商品模块和公共模块都用了JSON工具类,版本不一致,线上调用某个接口时行为就对不上;订单模块和会员模块各自维护一套统一返回结果,接口协议越来越混乱。这些问题在单体应用里很难出现,因为所有代码在同一个工程里,天然共享一套类库和配置。微服务把进程拆开了,如果工程结构也完全拆开,基础能力就失去了统一管理的地方。

聚合工程的核心价值就在这里。父工程只做两件事:声明所有模块、统一管理依赖版本,它本身不写一行业务代码。公共能力下沉到一个common模块,被所有模块依赖,就能保证统一返回结构、统一异常处理、通用工具类都是同一份代码。各业务模块之间通过Maven依赖互相引用,但依赖方向严格单向,避免循环依赖。这套结构本质上是对模块间依赖关系的强约束,把分布式下的混乱提前扼杀在工程层。

1.2 微服务模块划分的边界逻辑

模块划分不是拍脑袋决定的,谷粒商城将业务域拆成商品、订单、会员、库存、优惠券、网关、鉴权等模块,基本遵循高内聚、低耦合的原则。每个模块只负责自己的核心领域:商品服务只管SPU/SKU、分类、属性;订单服务只管订单创建、取消、支付回调;库存服务管库存扣减、采购单、物流。模块之间通过远程调用协作,不直接读写对方数据库。

拆模块时有一条实用标准:如果两个功能总是同时修改、同时发布,那它们大概率应该在一个模块里。比如商品属性和商品分类,拆开会导致频繁跨服务调用;而商品和订单虽然业务关联强,但变更频率、数据边界都很清晰,拆开更合适。谷粒商城的模块划分基本符合这条标准,这也解释了为什么它最终形成八个左右微服务而不是三个大服务或二十个小服务的形态。

1.3 构建方案的选型分析

构建阶段最大的选型点是"怎么生成基础工程"。传统做法是利用IDEA的Spring Initializr向导生成每个子模块,再由手工方式组合为聚合工程。这类方式的优势是直观,但容易产生一个严重问题:IDE自动生成的模块带有各自的Maven坐标和依赖,合入聚合工程时常常出现版本冲突。另一个方案是纯手工编写pom.xml,完全不依赖IDE生成,虽然起步慢,但每一步都清楚可控。谷粒商城的工程篇采用折中方案:父工程手写pom,子模块基于Spring Initializr生成,生成后立即统一改造pom,归入父工程管理。这样做既利用了IDE的便利性,又保证了版本统一。

还要注意一个重要区别:网关模块必须引入Reactive Web,而不是传统Servlet Web。网关底层基于Spring WebFlux,依赖的是响应式编程模型,如果引入spring-boot-starter-web,运行时必然报冲突。这个坑在新手搭建网关时几乎必踩。

2. 工程结构实操:从0搭建多模块项目

2.1 创建空的聚合父工程

这个操作很多教程一笔带过,实际做起来有不少细节。先在IDEA里新建一个普通的Maven工程,故意不选择任何Archetype模板。工程名建议带上业务含义,比如mall-parent。创建完成后,pom.xml需要手工改造,把packaging设置为pom,同时声明全部子模块。

<?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/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mall</groupId> <artifactId>mall-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <name>mall-parent</name> <description>分布式商城系统聚合父工程</description> <modules> <module>mall-common</module> <module>mall-gateway</module> <module>mall-auth</module> <module>mall-product</module> <module>mall-order</module> <module>mall-member</module> <module>mall-ware</module> <module>mall-coupon</module> </modules> </project>

注意:packaging的值必须是pom,这是聚合工程的基础属性,如果不是pom,Maven会把父工程当作普通Jar包工程处理,子模块聚合关系无法建立。modules列表可以先写全,即使子模块还没创建,之后创建时IDEA会自动识别并入。

生成Spring Initializr工程时,IDE通常附带mvnw、.gitignore等文件。mvnw是Maven包装器,属于特定构建环境产物,微服务聚合工程里不需要;.gitignore建议保留一份修改后的版本,但要统一只保留IDEA和主流编辑器的忽略规则。这些细小动作看似不重要,一旦团队协作起来,能减少大量无谓的版本冲突。

2.2 创建公共模块common

common模块是整个聚合工程最核心的模块之一,作用类似单体项目中的base包。面向电商业务,它至少应该包含:统一返回结构R、业务异常类BizException、全局异常处理器、分页参数对象、常量类、Json工具类、日期工具类、编号生成器。后续所有业务模块都直接依赖common,所以它本身不能依赖任何业务模块。

创建common模块时,直接在父工程右键创建Maven模块,然后引入Web和Lombok依赖。有人会问:common既然是给其他模块用的,为什么自己还要引Web?因为统一返回结果中的HTTP状态码、全局异常处理里的ResponseEntity,都依赖Spring Web的注解和类型。如果不引入,编译直接报错。

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>

2.3 创建各微服务模块及依赖关系

打开IDEA,逐个创建模块,名称分别是mall-gateway、mall-auth、mall-product、mall-order、mall-member、mall-ware、mall-coupon。创建时建议选择Spring Initializr方式,但勾选依赖时只选需要的起步依赖。网关模块特殊,选择Spring Reactive Web,其他业务模块选择Spring Web即可,数据库驱动、Redis等依赖之后按需引入。

模块创建后,需要在每个业务模块中显式依赖common。很多第一次接触聚合工程的人会误以为子模块自动继承父工程的依赖,实际并非如此。父工程通过dependencyManagement统一管理版本,但不负责把依赖传递给子模块;子模块必须显式声明依赖。以商品模块为例,pom中的依赖配置大致如下:

<dependencies> <dependency> <groupId>com.mall</groupId> <artifactId>mall-common</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>

对于common模块自身的版本号,如果模块列表比较多,也可以在父工程中用properties统一维护。不过谷粒商城这类规模下,直接写版本号可读性更强,团队内约定即可。

2.4 统一编码、JDK版本和Maven插件

工程搭建阶段,最容易被忽略的是编译配置。一旦团队中有人用Windows、有人用macOS,编码格式不同会引发大量诡异问题。建议在父工程pom中直接锁定三个关键参数:源码编码UTF-8、编译级别Java 8、Maven编译器插件版本固定。

<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build>

这里不建议直接在父pom里配置spring-boot-maven-plugin,原因是该插件会尝试将模块打包成可执行Jar,而common这类公共模块只需要打成普通Jar被其他模块引用,不需要可执行打包。这类插件配置放到各业务模块自己身上更合理。

3. 核心细节解析与实操要点

3.1 每个模块的启动类到底怎么放

聚合工程里启动类的包名设计直接影响后续所有组件扫描。以商品模块为例,启动类建议放在com.mall.product包下,类名ProductApplication。包名的设计要遵循一个规则:主启动类所在的包,一定是该模块所有业务代码包的父级,这样才能保证Spring Boot默认的组件扫描有效。

对于启动类与common模块的关系,有个非常实际的问题:common模块里的全局异常处理类、配置类,怎么被业务模块的Spring容器加载?如果不做任何处理,业务模块只会扫描自己的包,common里的类根本不会被注册。最简单可靠的做法是在业务模块启动类上显式加包扫描注解,把common的包路径一并扫进去:

@SpringBootApplication @ComponentScan(basePackages = {"com.mall.product", "com.mall.common"}) public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }

另一种方案是利用Spring Boot的自动装配机制,在common模块里定义META-INF/spring.factories,声明需要自动装配的配置类。这种方式更自动化,但对初学者不友好,出现问题时不直观。工程阶段用@ComponentScan更稳妥,后续理解了机制再优化不迟。

3.2 服务端口与服务名规划

一个整容工程下,微服务进程各占一个端口。端口规划如果不提前做好,启动第三个服务时就会报端口冲突。谷粒商城采用了一套好记的端口分配方案:网关88,鉴权8000,商品8001,订单8002,会员8003,库存8004,优惠券8005。网关单独用88,有意识的避开开发中常见端口,减少混用可能。

除了端口,spring.application.name同样重要。这个名称决定服务在注册中心的标识,也是服务间远程调用时的地址名,所有地方的引用必须一致。建议在配置文件中显式定义,而不是依赖默认名称。

server: port: 8001 spring: application: name: mall-product

3.3 清理无用文件与IDEA工程刷新

实操中经常出现的一个场景:子模块创建完毕后,IDEA的Maven面板里不显示新模块。原因通常是IDEA缓存了工程结构,或者模块pom还没被识别。遇到这种情况,不要急着删工程重来,先在父工程上执行Reload All Maven Projects,刷新后再看Maven面板。如果没有生效,检查IDEA的Maven设置是否开启了自动导入。

IDEA在导入聚合工程时还可能出现一个提示:Unregistered VCS root detected。这是因为每个子模块都包含自己的.git目录信息,而IDEA识别到多个版本控制根目录。通常勾选Add root即可统一管理。更干净的做法是在聚合工程根目录放一个.gitignore,统一排除.idea、target、*.iml等文件,从源头规避文件混乱。

3.4 启动类与配置文件创建时机

很多人在搭建工程时不建启动类,想等业务开发时再补。这个习惯对微服务工程特别不好。没有启动类,就无法验证模块的Maven依赖是否正确、端口是否可用、包扫描配置是否生效。建议每创建一个模块,立刻补上主启动类和一个最简单的application.yml,然后启动一次,看到Spring Boot输出的启动日志再继续下一个模块。这样的节奏虽然慢一点,但能确保每个模块的基础配置正确。

启动验证时,数据库、注册中心这些中间件还没有介入,服务可以直接以单机模式启动。如果启动失败,优先排查依赖冲突和端口占用,这两类问题在工程搭建阶段出现频率最高。

4. 父工程依赖统一管理方案

4.1 三个BOM的组合逻辑

Spring Boot负责基础能力,Spring Cloud负责分布式组件,Spring Cloud Alibaba负责中间件集成,三者必须协调工作,任何一组版本不匹配都会导致启动直接失败。谷粒商城选型的组合是:Spring Boot 2.6.3、Spring Cloud 2021.0.1、Spring Cloud Alibaba 2021.1。这三个版本在2022年初是经过大量项目验证的稳定组合。

依赖统一管理的载体是dependencyManagement。它的作用不是直接引入依赖,而是定义一个"版本字典",子模块引入依赖时如果没写version,会自动查找字典中的版本。这种机制解决了微服务架构中最头疼的版本碎片化问题。实际配置如下:

<properties> <spring-boot.version>2.6.3</spring-boot.version> <spring-cloud.version>2021.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version> <mybatis-plus.version>3.5.1</mybatis-plus.version> <mysql.version>8.0.28</mysql.version> <lombok.version>1.18.22</lombok.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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> </dependencyManagement>

注意dependencyManagement里的type必须是pom,scope必须是import,这是使用BOM的标准姿势。三个BOM的import顺序按照Boot、Cloud、Alibaba排列,原因是Alibaba的BOM内部引用Cloud组件,Boot最底层,顺序颠倒容易导致版本解析到错误引用。

4.2 子模块如何正确引入管理好的依赖

正确做法是,子模块引入依赖时只写groupId和artifactId,不写version。比如商品模块引入MyBatis-Plus:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency>

版本自动从父工程继承。如果某个子模块手滑写了version,Maven会优先使用子模块自己的version,这就产生了版本漂移,其他模块可能仍然用父工程版本,最终运行期出现NoSuchMethodError这类异常。这类问题排查成本极高,所以工程阶段就要立规矩:子模块禁止写版本号,统一由父工程管理。

4.3 自定义版本覆盖的正确时机

依赖锁定并不意味着永远不变。随着业务推进,可能遇到某组件需要升级,或者某个BOM版本和实际使用存在兼容问题。这时可以在dependencyManagement中单独引入一个新版本,覆盖BOM中的默认值。推荐只在明确遇到问题后再做覆盖,不要预判式地升级版本。坐标冲突时,先查阅官方兼容矩阵,比如Spring Cloud Alibaba的版本对应关系,确认组合安全后再动手。

5. 实操过程与核心环节实现

5.1 工程搭建的完整步骤记录

为了让大家有完整的体感,我把这一步的操作完整记录下来。这一过程基于IDEA 2022.x、Maven 3.6+、JDK 8。

第一步,创建聚合父工程。IDEA中选择New Project,选择Maven并取消勾选Archetype,输入groupId为com.mall,artifactId为mall-parent,等待生成。生成后修改pom.xml,packaging设为pom,填入完整的modules列表。

第二步,创建common模块。右键父工程,New Module,选择Maven,输入mall-common,修改pom加入Web和Lombok依赖。编写统一返回结果R类和全局异常处理类。

第三步,创建网关模块mall-gateway。注意Spring Initializr向导里依赖选择Spring Reactive Web,不选Web。

第四步,创建鉴权、商品、订单、会员、库存、优惠券模块。这个阶段可以借助IDEA快速创建,但每个模块创建后需立即规范化pom,清理无用的文件。

第五步,在每个业务模块中引入common模块依赖,同时引入父工程中管理的MyBatis-Plus和MySQL驱动依赖。

第六步,为每个模块创建主启动类和application.yml,配置端口与服务名,逐个启动验证。

5.2 统一返回结果类的实现细节

common模块中比较核心的是统一返回结果。参考主流实践,可以设计一个泛型R类,包含code、message、data三个字段,提供success和error静态方法。典型实现大致如下:

@Data public class R<T> { private Integer code; private String message; private T data; public static <T> R<T> success(T data) { R<T> r = new R<>(); r.setCode(0); r.setMessage("success"); r.setData(data); return r; } public static <T> R<T> error(Integer code, String message) { R<T> r = new R<>(); r.setCode(code); r.setMessage(message); return r; } }

此类被所有模块共用后,前端对接的响应格式就完全一致了。全局异常处理器也需要放到common模块里,统一捕获业务异常和未知异常,返回统一结构的错误信息,而不是直接抛出堆栈给前端。这一步看起来简单,却避免了后续每个服务各写一套响应体的问题。

5.3 模块间依赖与启动顺序验证

当模块较多时,可能出现A依赖B、B依赖C、C又依赖A的循环依赖场景,Maven构建会陷入循环报错。工程搭建阶段就应该把依赖方向确定下来:业务模块统一依赖common,不允许业务模块之间互相依赖公共组件。如果后来发现两个业务模块都要引用某段代码,优先下沉到common,再在各自模块引用。

启动顺序上,注册中心没有接入前,业务模块之间不互相调用,启动顺序几乎没有影响。接入注册中心后,建议先启动注册中心、配置中心,再启动网关和鉴权,最后启动业务模块。否则业务模块启动时无法在注册中心完成注册,虽然不会报致命错误,但服务链路会处于不健康状态。

6. 常见问题与排查技巧实录

6.1 端口冲突问题

工程搭建阶段最普遍的报错是Port already in use。主要原因是多个服务都采用了默认的8080端口。解决方法:按照前文端口规划表,为每个模块设置独立端口,并在配置文件中显式指定。如果存在本机其他进程占用端口,排查方法是使用系统命令查看占用进程,确认后释放端口或换端口。

6.2 依赖下载缓慢或失败

首次构建聚合工程时,需要下载大量依赖,受网络环境影响,有概率出现依赖下载中断。建议在Maven的settings.xml中配置国内镜像源,并固定本地仓库路径。一个常见的镜像配置大致如下:

<mirrors> <mirror> <id>aliyun</id> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>

如果某个依赖一直下载失败,优先尝试删除本地仓库中对应的.lastUpdated文件,然后重新构建。

6.3 控制台中文乱码问题

启动日志中文乱码,大多数情况是IDEA控制台编码不是UTF-8。解决方法是在IDEA的Help菜单中修改VM参数,加上-Dfile.encoding=UTF-8,同时把Console的默认编码设置为UTF-8。如果还不行,检查系统环境变量是否配置了JAVA_TOOL_OPTIONS,包含非UTF-8编码相关设置时要清理掉。

6.4 常见问题速查表

问题现象排查思路解决方式
Maven面板不显示子模块modules未声明或IDEA未刷新在父pom中补module后Reload All Maven Projects
启动报端口冲突多个服务使用默认8080按规划为每个模块显式配置不同端口
依赖版本冲突子模块覆盖了父工程版本删除子模块中的version,统一继承父工程
Lombok注解不生效父工程只声明版本未引入依赖子模块内部显式引入Lombok依赖
网关启动报错Reactive环境冲突误引入Servlet Web移除Web依赖,仅保留Reactive Web
common中包扫描不到启动类未扫描common包使用@ComponentScan指定basePackages
中文乱码控制台编码或系统变量问题IDEA设置UTF-8并检查JAVA_TOOL_OPTIONS

提示:工程刚搭完时,先不要急着写业务代码。把每个模块都启动一遍,确认基础健康,再进入后续开发阶段。

7. 写在最后的经验总结

搭建谷粒商城类分布式工程的经历,让我真切体会到:微服务项目里最不值钱的可能是写业务代码,最值钱的往往是工程结构。一个模块边界清晰、依赖方向明确、版本统一可控的工程架子,能让后续开发省下大量互相甩锅的时间。技术选型和版本锁定这个阶段,我给出的建议是不要盲目追新,用社区验证过的稳定版本组合,遇到问题更好查资料、更好解决。

分享一个我个人踩过的坑:刚开始给项目配依赖时,总想"反正以后要升级,先用最新版本",结果Spring Boot 2.6和Spring Cloud 2021.0之间有若干兼容细节,导致启动时不停地报错。后来改用稳定组合,一次通过。做工程,稳定压倒一切。

这套搭建流程跑完后,下一步可以往工程里集成注册中心与配置中心,把服务发现、配置动态刷新加进来;然后做网关路由与统一鉴权,再逐渐填充商品、订单、库存、支付等业务链路。你会在后续开发中不断回到这个工程结构上做扩展,那时你就理解为什么当初要在这些细节上花这么多工夫了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进