微服务实现说明
本文档基于本项目真实代码,讲解这套微服务架构从模块划分、服务注册与发现、网关路由、服务间调用到鉴权的完整实现流程。每个机制都有对应的代码片段。

一、项目形态:按业务拆成多个独立 Spring Boot 微服务
根 pom.xml 用 Maven 多模块把项目拆成 12 个模块,其中 8 个业务服务 + 1 个网关 = 9 个可独立启动的 Spring Boot 应用,另外 3 个是公共库/空模块:
<!-- pom.xml modules -->
mall-admin mall-product mall-common mall-gateway mall-api
mall-category mall-service mall-user mall-cart mall-shipping
mall-order mall-web
每个有业务能力的模块都是一个能独立启动的 Spring Boot 应用,有自己的启动类、独立端口,相当于一个”微服务”:
| 模块 | 服务名(spring.application.name) | 端口 | 职责 |
|---|---|---|---|
| mall-gateway | gateway-service |
9000 | 统一入口、路由、JWT 鉴权(WebFlux) |
| mall-admin | admin-service |
8082 | 后台管理员/登录/验证码 |
| mall-product | product-service |
8081 | 商品 CRUD、库存扣减、图片 |
| mall-category | category-service |
8083 | 商品分类树 |
| mall-service | service-service |
8084 | 通用能力(验证码、OSS 上传、定时清理) |
| mall-user | user-service |
8085 | 前台用户、AI 客服(DeepSeek) |
| mall-cart | cart-service |
8086 | 购物车 |
| mall-shipping | shipping-service |
8087 | 收货地址 |
| mall-order | order-service |
8088 | 订单、支付宝支付、秒杀、MQ |
| mall-common | — | — | 公共代码(配置/切面/工具/拦截器/锁),被各服务依赖,不单独启动 |
| mall-api | — | — | Feign 调用契约(@FeignClient + 共享实体) |
| mall-web | — | — | 空模块(预留) |
各服务可独立部署、独立升级,这就是”微服务”最基本的形态。

二、微服务的三大核心机制
项目使用了SpringCloudAlibaba

机制 A:服务注册与发现(Nacos)
每个服务启动时,把自己注册到 Nacos 注册中心。配置在各自的 application.yml:
# 以 mall-product/src/main/resources/application.yml 为例
spring:
application:
name: product-service # 服务名:别的服务/网关靠它找自己
cloud:
nacos:
server-addr: 127.0.0.1:8848 # 注册中心地址
spring.application.name:服务在注册中心里的唯一身份,谁要调它就按这个名字来,不写死 IP。- Nacos 会维护一张”服务名 → 实例列表”的映射,实现注册 + 发现。
依赖来自父 POM 泛化的 spring-cloud-starter-alibaba-nacos-discovery(各服务继承)。
所有的注册模块实现pom
<!-- 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
机制 B:统一入口 + 负载均衡(Spring Cloud Gateway)
前端只认一个地址:网关 gateway-service:9000。mall-gateway 是 spring-cloud-gateway 应用,用路由表把不同路径分发到不同服务,并通过 lb:// 做负载均衡。
# mall-gateway/src/main/resources/application.yml(server.port=9000)
spring:
cloud:
gateway:
routes:
- id: admin-service # 以下 uri 都用 lb:// 按服务名负载均衡
uri: lb://admin-service
predicates: [Path=/admin/**]
- id: product-service
uri: lb://product-service
predicates: [Path=/product/**]
- id: category-service
uri: lb://category-service
predicates: [Path=/category/**]
- id: service-service
uri: lb://service-service
predicates: [Path=/service/**]
- id: user-service
uri: lb://user-service
predicates: [Path=/user/**]
- id: cart-service
uri: lb://cart-service
predicates: [Path=/cart/**]
- id: shipping-service
uri: lb://shipping-service
predicates: [Path=/shipping/**]
- id: order-service
uri: lb://order-service
predicates: [Path=/order/**]
- id: seckill # 秒杀链路在 order-service 内,单独路由 /seckill/**
uri: lb://order-service
predicates: [Path=/seckill/**]
mall-gateway/pom.xml 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency> <!-- 负载均衡 -->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency> <!-- Nacos 发现 -->
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
转发过程:请求到网关 → 路由表按 Path 匹配 → lb://服务名 让 LoadBalancer 从 Nacos 拉到该服务的实例列表 → 挑一个健康实例转发。
机制 C:服务间调用(Feign + mall-api)
微服务之间互调,不直接依赖对方代码,而是用 Feign。mall-api 专门存放”远程调用契约”。
以”商品服务联查分类名”为例。商品服务在查商品列表时,需要每个商品对应的分类名,而分类在独立的 category-service:
// mall-api/.../api/category/CategoryClient.java
@FeignClient(value = "category-service") // 调谁
public interface CategoryClient {
@GetMapping("/category/name/{id}") // 调对方哪个接口
String selectNameById(@PathVariable Long id);
}
在商品服务里像调本地方法一样使用:
// mall-product/.../service/impl/ProductServiceImpl.java
@Autowired
private CategoryClient categoryClient;
List<ProductVO> list = page.getRecords().stream().map(product -> {
ProductVO vo = new ProductVO();
BeanUtils.copyProperties(product, vo);
vo.setCategoryName(categoryClient.selectNameById(product.getCategoryId())); // 远程调分类服务
return vo;
}).toList();
Feign 会把这次”看似本地调用”变成一次 HTTP 请求:按服务名 category-service 从 Nacos 找到实例 → 调用 /category/name/{id}。
启动类要加 @EnableFeignClients 才会扫描 @FeignClient:
// mall-product/.../ProductApplication.java
@EnableCaching
@EnableFeignClients // 扫描 Feign 客户端
@SpringBootApplication
@MapperScan("com.situ.mall.mapper")
public class ProductApplication { ... }
三、完整链路走查:前端一次请求的过程
以”浏览商品列表”为例:
浏览器
│ GET /api/product?page=1&limit=10
▼
前端 vite 开发代理 --------- 把 /api 去掉,转发到网关 9000
│ GET /product?page=1&limit=10
▼
网关 gateway-service:9000
│ 路由表匹配 Path=/product/** → lb://product-service → Nacos 发现实例 → 转发
▼
商品服务 product-service:8081
│ ProductController.list()
▼
商品服务内部 —— Feign 调 category-service
│ categoryClient.selectNameById(分类id) → GET /category/name/{id}
▼
返回:商品 + 分类名 → 网关 → 前端
四、鉴权:网关统一验 JWT + 下游 ThreadLocal
本项目鉴权集中在网关,业务服务信任网关注入的身份,自己不再验 JWT。分三段:
第一段:网关全局过滤器验签并透传身份
mall-gateway/.../filter/AuthGlobalFilter.java(实现 GlobalFilter,WebFlux 响应式):
// 白名单:登录、验证码、支付宝服务器回调不带 token
if (path.startsWith("/service/captcha") || path.startsWith("/admin/login")
|| path.startsWith("/user/login") || path.startsWith("/order/notify")) {
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
try {
Map<String, Object> map = JwtUtil.parseToken(token); // 验签+验过期
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-Login-Id", String.valueOf(map.get("id"))) // 注入内部头
.header("X-Login-Name", String.valueOf(map.get("name")))
.build();
return chain.filter(exchange.mutate().request(request).build());
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 非法/过期 → 401
return exchange.getResponse().setComplete();
}
登录成功由 user/admin 服务用 JwtUtil.createToken({id,name}) 签发(HMAC256,1h)。
第二段:业务服务拦截器读内部头存 ThreadLocal
mall-common/.../interceptor/LoginInterceptor.java:
public boolean preHandle(...) {
String id = request.getHeader("X-Login-Id"); // 读网关注入的头,不再读 Authorization
String name = request.getHeader("X-Login-Name");
if (StringUtils.hasText(id) && StringUtils.hasText(name)) {
Map<String,Object> map = new HashMap<>();
map.put("id", Long.valueOf(id)); map.put("name", name);
LoginContext.setLoginInfo(map); // 存 ThreadLocal
}
return true; // 只搬运身份,不拦截
}
public void afterCompletion(...) { LoginContext.removeLoginInfo(); } // 防线程复用污染
WebConfig 对所有路径注册该拦截器,白名单统一在网关维护,业务服务这层不再重复配 exclude。
第三段:业务代码取当前用户 + Feign 继续透传
- 业务里随时
LoginContext.getLoginInfo().get("id")取当前登录人; - 服务间 Feign 调用时
DefaultFeignConfig(RequestInterceptor)把 ThreadLocal 里的 id/name 再写回X-Login-*头,保证跨服务身份不断流(定时任务/MQ 等无 HTTP 上下文时为 null,跳过)。
完整时序、前端 Pinia/localStorage 存 token、JWT 结构与生产改进见《JWT登录鉴权笔记.md》。
安全前提:业务端口不能被外网直连,只能经网关进入,否则可伪造
X-Login-Id头冒充他人。教学环境同机部署未做网络隔离,这点要清楚。