微服务实现说明

微服务实现说明

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


image-20260912100021313

一、项目形态:按业务拆成多个独立 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 — — 空模块(预留)

各服务可独立部署、独立升级,这就是”微服务”最基本的形态。

image-20260912100042434


二、微服务的三大核心机制

项目使用了SpringCloudAlibaba

image-20260912100059429

机制 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 头冒充他人。教学环境同机部署未做网络隔离,这点要清楚。


上一篇
下一篇