资讯动态

黑马商城微服务实战:Spring Cloud多模块与Feign调用全解析

发布时间:2026/9/12 1:22:44 来源:尧图企业网站定制
简介基于SpringCloud构建的黑马商城微服务源码面向正在学习微服务架构建模与Java后端开发的读者尤其适合围绕商城业务场景进行SpringCloud组件实战的人群。源码按照商城核心服务拆分覆盖用户、商品、搜索、订单、网关及Feign接口调用等模块能够帮助理解服务注册、配置中心、路由转发与声明式服务调用的落地方式。压缩包共76个文件以61个Java源文件为主线辅以7个XML配置文件、5个YML配置文件、factories工厂配置、gitignore忽略清单和txt说明文档整体仅109KB结构紧凑且目录按服务模块划分便于快速导入工程研读。目前已有1377人学习下载适合有一定Spring Boot基础、希望结合真实业务串联SpringCloud各组件的中高级学习者。通过阅读源码目录与配置可直观看到网关路由、微服务间调用、统一配置等典型实践还能根据配套说明文档快速定位各服务入口省去从零搭建环境的时间是一份轻量但覆盖关键链路的微服务参考资料。1. 黑马商城微服务拆了一刀之后我的第一个跨服务下单接口是怎么跑通的把一个单体商城拆成SpringCloud微服务之后真正决定开发效率的不是用什么注册中心而是服务边界切得够不够干净。黑马商城这套源码把item、user、order、search四个业务服务单独拆开再配一个gateway-api网关和一个feign-api接口工程文件结构一眼就能对应到业务链路。我当时照着它跑通第一个“查商品→查用户→创建订单”的流程Debug时发现order-service既是调用方又不直接依赖商品和用户服务的Jar包所有远程调用都走Feign接口这种解耦方式比单体里写Service互相调用要清晰得多。如果你正在学SpringCloud却总觉得组件之间关系飘着或者准备做一套能继续扩展的服务端骨架这套源码值得拆开看一遍。2. 多模块Maven工程黑马商城的父POM是如何把五个服务拧成一股绳的一个微服务项目如果拆成多个独立Maven工程版本一致性是最先爆炸的点A服务用2.1.0的SpringCloudB服务用2.2.0Feign生成的客户端方法签名一旦有变化联调时几乎无法排查。黑马商城把gateway-api、user-service、item-service、order-service、search-service全部放到同一个父工程的modules下面根目录只有一个pom.xml子模块通过parent坐标指向它。这种结构在IDE里只导入一次根POM就能同时管理五个微服务SpringCloud的组件版本也只在父POM里锁一份。2.1 为什么要用父POM统一管理版本而不是各自维护微服务的服务数量一多依赖版本失控只是时间问题。假设item-service用了SpringCloud 2021.0.1order-service用了2021.0.3两者底层的Ribbon、OpenFeign、Gateway实现都有差异联调时经常会出现“我这边明明能跑”的尴尬。把版本收敛到父POM的dependencyManagement里子模块只需要声明groupId和artifactId版本号由父工程统一给出这是SpringCloud微服务工程最基础也最容易忽略的规范。黑马商城的根POM里模块声明很直观。项目正文里提到的item-service、user-service、search-service、order-service、gateway-api、feign-api在Maven聚合工程中是这样挂载的groupIdcom.heima/groupId artifactIdhm-platform/artifactId version1.0.0-SNAPSHOT/version packagingpom/packaging modules moduleitem-service/module moduleuser-service/module moduleorder-service/module modulesearch-service/module modulegateway-api/module modulefeign-api/module /modules这里有一点要特别留意只有packaging为pom时modules节点才生效。父POM本身不写业务代码它只负责聚合子模块、管理依赖版本和插件配置。子模块之间允许存在依赖关系比如order-service依赖feign-api这种依赖在Maven构建时会被自动纳入构建队列。2.2 父POM的dependencyManagement到底管理了什么dependencyManagement与dependencies不同。子模块的dependencies里如果没写version才会向上查找父POM中dependencyManagement锁定的版本如果子模块显式写了version则以子模块为准。这个机制保证了灵活性又避免了“全工程同一个版本”的死板。黑马商城的父POM里会看到一批SpringCloud组件和SpringBoot启动器写法类似下面dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring-cloud-alibaba.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version${spring-cloud.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这段配置的意思是把SpringCloud和SpringCloud Alibaba的BOM导入到当前工程的依赖管理范围里。后续任何子模块需要引入openfeign、gateway、nacos-discovery时都不用再写版本号直接继承这套约束。版本号本身定义在父POM的properties节点中比如spring-cloud.version2021.0.5/spring-cloud.version这种形式。实际使用中我不建议在子模块里重复写版本号。如果一个微服务排障时发现当前版本有Bug你只需要改父POM中的properties属性然后对整个项目重新构建。如果某个服务偷偷写死了版本排障时就会漏掉几个模块这是最常见的多模块工程失控原因。2.3 子服务POM如何声明自己的差异子模块的POM非常简洁先声明parent指向根工程再声明自己的artifactId随后按需引入依赖。比如order-service的POM里会出现openfeign、spring-boot-starter-web以及feign-api的依赖而gateway-api的POM里引入的是spring-cloud-starter-gateway它不会直接依赖web starter因为网关底层是WebFlux如果误引入spring-boot-starter-web会导致路由规则失效。一个子服务POM的基础形态是这样的parent groupIdcom.heima/groupId artifactIdhm-platform/artifactId version1.0.0-SNAPSHOT/version /parent artifactIdorder-service/artifactId dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency dependency groupIdcom.heima/groupId artifactIdfeign-api/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies这里没有写版本号版本统一交给父POM。注意order-service依赖feign-api而feign-api本身又是一个独立模块所以在Maven打包时父工程会优先构建feign-api再构建order-service。这也是我在本地先执行根目录mvn clean install而不是直接跑服务的原因。2.4 从源码文件树看服务边界项目正文中给出的文件结构很典型每个服务目录下都有独立的pom.xml和src/main路径search-service还带了src/test目录。这说明项目本身就按照标准Maven布局组织一个服务就是一条可独立启动的业务线。这种文件树还有一个好处做CI/CD时可以用路径匹配触发只有某个服务相关的构建任务而不是每次全量打包。模块角色关键依赖gateway-api统一入口路由转发gateway, nacos-discoveryfeign-api定义远程调用接口openfeignitem-service商品服务web, mybatis, nacosuser-service用户服务web, mybatis, nacosorder-service订单服务web, openfeign, feign-apisearch-service搜索服务web, elasticsearch或rest-client这张表是我根据源码目录结构倒推出来的实际项目中各服务的表可能不同但依赖方向基本一致gateway-api不依赖业务服务Jar包order-service也不直接依赖item-service和user-service而是依赖feign-api中声明的接口。这就是微服务工程与单体内多模块工程最大的差别。3. 业务服务的接口与数据流item、user、order、search 四个模块怎么协同前面把工程骨架讲清楚了接下来看真正的业务逻辑。黑马商城四个业务服务不是平行出现的item-service和user-service是基础数据服务order-service负责串联它们search-service则面向查询场景。理解这个协作模型才能明白为什么Feign接口会被单独抽到一个feign-api模块里。3.1 item-service商品详情的查询主体item-service承担商品详情的读取Controller层开头通常是标准的RestController写法。我会把返回类型单独封装成一个Result对象但为了不干扰源码阅读这里用最朴素的VO直接返回RestController RequestMapping(/item) public class ItemController { Autowired private ItemService itemService; GetMapping(/{id}) public ItemVO getItemById(PathVariable Long id) { return itemService.queryItemById(id); } }这段代码的逻辑很直白GetMapping(/{id})接收路径参数queryItemById方法负责从数据库查出商品记录并映射成ItemVO。注意这里没有跨服务调用item-service是数据提供方它只暴露HTTP接口不关心谁来调用。它的Service层一般会调用MapperMapper通过XML或注解执行SQL。如果商品详情需要从搜索服务回查那就会形成反向依赖黑马商城这里默认不这样做商品服务只对上层提供数据。这种单一职责在微服务设计中比“万能服务”更安全因为你改商品表结构时只需评估item-service自身的接口兼容性。3.2 user-service用户信息的获取与兜底user-service与item-service结构相似但也有一些不同。用户服务通常涉及更多的状态管理和账号体系订单详情页需要展示用户昵称、头像等信息时order-service不可能直接查用户库而是通过Feign调用user-service的接口。user-service的Controller一般会返回用户信息和缓存标记示例RestController RequestMapping(/user) public class UserController { Autowired private UserService userService; GetMapping(/{id}) public UserVO getUserById(PathVariable Long id) { return userService.getById(id); } }与商品服务相比用户服务在高并发场景下更容易被频繁命中所以user-service内部往往会加一层Redis缓存。但黑马商城这套源码的重点在微服务通信缓存逻辑不是核心。需要注意的是user-service接口的返回值要设计成稳定的VO结构不要直接把数据库实体暴露出去否则一旦表结构调整所有下游服务都要跟着重新编译。3.3 order-service一个下单流程里调了两个服务order-service是黑马商城源码中最能体现SpringCloud价值的地方。创建订单时它需要校验商品是否存在、获取用户信息、然后插入订单数据。在单体架构中这只是一个Service方法的几行代码在微服务中业务方变成了协调者真正的数据在另外两个服务里。order-service通过feign-api中定义的Client接口发起远程调用Service public class OrderServiceImpl implements OrderService { Autowired private ItemClient itemClient; Autowired private UserClient userClient; Autowired private OrderMapper orderMapper; Override public OrderVO createOrder(OrderDTO dto) { ItemVO item itemClient.getItemById(dto.getItemId()); UserVO user userClient.getUserById(dto.getUserId()); // 校验商品状态与用户状态 Order order new Order(); order.setItemId(item.getId()); order.setUserId(user.getId()); order.setAmount(item.getPrice()); orderMapper.insert(order); return new OrderVO(order); } }这里的OrderDTO是入参OrderVO是出参ItemClient和UserClient都是接口类型具体实现由SpringCloud在运行时为Feign生成代理类。这个Service方法看起来和调用本地Service没有区别但代理类内部会做服务发现、负载均衡、序列化和HTTP请求。如果item-service返回结果有延迟order-service不会立刻报错但会触发Feign的超时等待。把这段代码放在源码阅读的第一优先位因为它把微服务架构中“调用方看到的是接口运行时才绑定实现”这一概念落到了真实业务里。如果想把某个服务替换成新的实现只需要改Feign接口的fallback或替换服务地址order-service核心逻辑不用动。3.4 search-service搜索入口与数据一致性边界search-service在四个业务服务里定位比较特殊。它的数据来源不是自己独立写的业务表而是从商品库同步到索引中。黑马商城源码中的search-service同时包含src/test目录说明它比另外几个服务多了测试模块常见做法是搜索接口先做单元测试再通过网关联调。搜索服务的Controller通常面向关键字查询接口参数可能是query、page、size返回的是搜索结果列表。这个服务本身不调用其他服务但它依赖商品数据的变更有时序要求。为了保证数据一致常见方案是在item-service中发送MQ消息search-service消费消息后刷新索引。源码里没有明确给出MQ部分时可以先用定时全量同步来替代不影响理解微服务模块划分。从数据流来看order-service是生产者item-service和user-service是基础服务search-service是数据二次加工后的消费方。四个服务之间有调用但不共享数据库这是微服务数据隔离原则的体现。如果你发现某个服务直接连接另一个服务的库表那就要警惕架构退化了。4. Gateway网关路由与Feign-api解耦黑马商城的远程调用是怎么组织的微服务暴露给前端通常不能是一堆分散的端口否则前端的baseURL要维护好几份。黑马商城把入口收敛到gateway-api前端只访问网关端口由网关按路径前缀把请求转发给对应服务。这个模块在源码里单独占了一个pom.xml也说明它和业务服务完全不在同一层。4.1 网关路由/api/item/** 和 /api/order/** 分别转发到哪里Gateway的路由配置写法很固定黑马商城的application.yml里大概率是这样一组route定义spring: cloud: gateway: routes: - id: item-route uri: lb://item-service predicates: - Path/api/item/** filters: - StripPrefix1 - id: order-route uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1这段配置里lb://item-service表示从注册中心找到名叫item-service的服务再用负载均衡策略选择实例。前缀/api/item在转发给item-service前会剥掉第一节/api这样item-service的Controller只需要写/item/{id}即可不用关心网关前缀。在本地调试时我曾经把StripPrefix配置漏掉结果网关把/api/order/create整个路径转发出去order-service里始终匹配不到接口。这个坑后面还会再提但你要记住Predicate负责“这个请求要不要接”filter负责“接收后怎么改”。StripPrefix的值决定剥掉几层路径不是固定为1。4.2 feign-api 独立工程的价值接口与实现自动分离黑马商城没有把Feign接口写在order-service内部而是单独划出一个feign-api模块这是一个非常有“工程感”的决定。如果Feign接口散落在各服务里服务A调用服务B时A的依赖里就得把B整个模块引入这等于让A强依赖B的部分实现和配置。用feign-api模块后所有客户端接口都放在这里item-service和user-service可以引用它来标注Controller接口order-service也引用它来做远程调用。在feign-api中接口定义一般是这样FeignClient(name item-service) public interface ItemClient { GetMapping(/item/{id}) ItemVO getItemById(PathVariable Long id); }这个接口不写实现。SpringCloud会在项目启动时扫描FeignClient注解为每个接口生成代理Bean注入到Spring容器。name属性不是任意起的它必须匹配注册中心里的服务名。如果注册中心里服务名是item-service但这里写成了item-service-01运行时就会报UnknownHostException。4.3 OpenFeign的声明式客户端定义与降级Feign接口的优点是把远程调用伪装成本地方法调用因此参数注解、返回值映射都必须严格对齐提供方。比如item-service的Controller方法是GetMapping(/{id})Feign接口里就必须写成GetMapping(/item/{id})这个路径指的是提供方接口的完整路径不是网关的对外路径。很多新手在网关配置了StripPrefix又在Feign里重复写前缀最后导致404一半是路径问题。如果希望接口具备降级能力可以给FeignClient指定fallback类FeignClient(name item-service, fallback ItemClientFallback.class) public interface ItemClient { GetMapping(/item/{id}) ItemVO getItemById(PathVariable Long id); }fallback类必须实现ItemClient且返回兜底值。但是fallback生效的前提是开启了feign.circuitbreaker.enabledtrue否则降级类不会被注入。黑马商城源码如果不涉及熔断组件通常会把这块配置注释掉避免绕太多概念。4.4 从日志看一次跨服务调用的完整链路联调时最直观的验证方式是观察order-service调用item-client时网关是否参与。如果前端请求先到网关网关转发到order-serviceorder-service再通过Feign调item-service此时Feign发起的请求并不会再次通过网关而是直接从order-service到item-service。这是因为Feign客户端默认从注册中心拿服务地址链路是“前端→网关→order-service→item-service”。我在排查问题时会在item-service的日志里搜索“item-controller”如果有请求进来说明order-service的Feign调用成功。如果日志里只有网关的访问记录没有item-service日志就检查order-service是不是注册到了同一个Nacos以及Feign接口的服务名是否写对。5. 从五个YML文件读懂Nacos注册与本地启动顺序项目正文中特别提到有5个YML配置文件数量与五个微服务模块一一对应gateway-api、item-service、user-service、order-service、search-service各一个。这五个文件承担的是服务名注册、端口定义和中间件连接配置读懂它们就能复现本地启动流程。5.1 服务名、端口、注册中心地址的约定微服务之间通信依赖注册中心黑马商城这类专题工程目前常见接入方式是Nacos。每个服务的application.yml里都会有类似下面的配置spring: application: name: item-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 server: port: 8081spring.application.name的值是给Nacos看的也是Feign接口的name属性要匹配的内容。server.port是服务实例的对外端口本地启动时不能冲突。根据经典黑马商城端口规划我一般会这样分配item-service是8081user-service是8082order-service是8083search-service是8084gateway-api是80或8080。如果你本地8080被占用gateway换一个端口也能跑前提是前端请求地址同步改。服务模块服务名建议端口主要配置项item-serviceitem-service8081MySQL数据源、Nacos地址user-serviceuser-service8082MySQL数据源、Nacos地址order-serviceorder-service8083MySQL数据源、Feign超时search-servicesearch-service8084ES地址、Nacos地址gateway-apigateway-api8080路由列表、CORS这些端口不是绝对的但服务名必须和FeignClient里的name严格一致。项目正文里5个YML与五个服务对应正是为了确保每个服务都能单独调整自己的端口和连接参数。5.2 bootstrap.yml与application.yml的分工严格来说SpringCloud项目中还常见一个bootstrap.yml用于在应用启动早期连接配置中心。黑马商城这里的5个YML大概率是application.yml因为Nacos服务发现只需要在应用阶段读取配置即可。如果源码里同时出现了bootstrap.yml和application.yml则意味着还要从Nacos config拉取公共配置比如数据源密码、Redis连接串。在阅读源码时你只需要区分一件事application.yml里写的是“这个服务自身怎么启动”bootstrap.yml里写的是“启动前从哪里拿配置”。如果只见到application.yml就别在本地强行添加bootstrap.yml否则应用可能会尝试连接一个不存在的配置中心挂掉。5.3 本地启动顺位与验证命令在本地运行这套黑马商城微服务时必须按照依赖关系确定启动顺序。第一步启动Nacos确认http://127.0.0.1:8848/nacos能打开控制台第二步启动基础服务item-service和user-service第三步启动order-service和search-service第四步启动gateway-api。如果Nacos还没就绪就启动服务服务会反复重试注册日志里出现connect refused提示。启动完成后可以用两个简单命令验证服务是否正常注册到Nacoscurl http://127.0.0.1:8848/nacos/v1/ns/service/list?pageNo1pageSize10 curl http://127.0.0.1:8080/api/item/1第一条命令查看Nacos上的服务名列表第二条命令访问网关检查路由是否把/api/item/1转发到item-service的/item/1。如果第二条返回JSON商品数据说明注册中心、网关、业务服务三层都通了。如果返回404优先检查网关StripPrefix和服务的context-path。5.4 一个常见的坑服务注册上去了但调用失败服务成功注册到Nacos不一定代表Feign能调用成功。最常见原因是多个服务连接的不是同一个Nacos或者服务名写错。我这里遇到过一种情况item-service注册的服务名是item-service别的服务都能看到它但order-service通过Feign调用时始终报连接拒绝最后发现order-service的Nacos地址被某个环境变量覆盖成了远端地址。排查这类问题不要盯着代码先看Nacos控制台里的实例IP端口是否与order-service所在机器一致。另一个容易踩的是本地多网卡问题。Nacos注册时可能注册成Docker网卡的IP别的服务访问不到需要在application.yml里加spring.cloud.nacos.discovery.ip和spring.cloud.nacos.discovery.port强制指定本机IP。这个问题在Windows服务器上部署SpringCloud系统时尤其常见本地跑课程源码也可能撞上。6. 微服务联调最容易翻车的三个点Feign超时、网关StripPrefix、多模块打包最后把我实际跑源码时踩过的三个雷集中说一下。这些点不会出现在源码首页但几乎每个从单体转微服务的人都会遇到。Feign默认的超时时间往往很短如果在order-service里调用item-service做复杂查询或者首次连接时需要初始化线程池很容易超过默认值导致报错。我习惯在order-service的application.yml里显式设置超时feign: client: config: default: connectTimeout: 3000 readTimeout: 5000connectTimeout是建立TCP连接的时间readTimeout是等待响应的时间。如果业务里有慢SQLreadTimeout可以放到8000甚至更大但不要全局调大否则调用链路整体的失败响应会变慢。第二个问题是网关StripPrefix的层级。对外路径是/api/item/{id}网关转发给item-service时如果没剥掉/apiitem-service里的RequestMapping(/item)就永远匹配不上。这一类的报错信息是404但网关日志里能看到路由命中记录。我会在本地先用curl直接访问item-service的http://127.0.0.1:8081/item/1如果能通再对比网关转发后的路径问题就锁定在filter上。第三个问题是多模块打包。直接用IDEA启动order-service前如果feign-api还没有执行过installorder-service的编译会报找不到ItemClient类。因为feign-api的Jar包没有进入本地Maven仓库。正确的处理方式是在根目录执行全量构建或者用Maven的指定模块构建命令mvn clean install -pl order-service -am这里的-pl order-service表示只构建order-service模块-am表示同时构建它依赖的其他模块也就是feign-api。如果依赖链更深这个命令会比全量打包更快且更安全。在CI脚本里我一般会在根目录执行mvn clean package -DskipTests保证所有子模块同步构建避免出现本地能跑、服务器上找不到feign-api类的情况。黑马商城这套源码的功能链路不复杂把这三个坑提前处理掉后面的SpringCloud学习才会更集中在服务治理而不是环境排错上。本文还有配套的精品资源点击获取

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

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

免费获取报价