从零搭建 Spring Boot + Nacos + MySQL + OpenFeign + Docker Compose 微服务项目
本文以一个"用户服务 + 订单服务"的完整示例为主线。完成后,可以通过浏览器访问两个后端服务,订单服务还能通过 Nacos 发现用户服务,并使用 OpenFeign 调用用户接口。
所有微服务启动时,向 Nacos 注册自身,同时从 Nacos 拉取配置并监听变更;客户端请求打到 Gateway 网关,网关从 Nacos 拿到后端服务实例,负载均衡转发;服务之间使用 Feign 远程调用,每次调用都会被 Sentinel 做流量和熔断保护
1. 最终要搭建什么
整个项目包含 4 个容器:
| 容器 | 作用 | 宿主机访问端口 | 容器内部端口 |
|---|---|---|---|
mysql |
保存 Nacos、用户和订单数据 | 3307 |
3306 |
nacos |
服务注册中心 | 8848、9848 |
8848、9848 |
user-service |
查询用户数据 | 8081 |
8081 |
order-service |
查询订单,并调用用户服务 | 8082 |
8082 |
调用关系如下:
text
浏览器
├─ http://localhost:8081/user/list
│ │
│ └─ user-service ──> MySQL 的 spring-cloud-user 数据库
│
└─ http://localhost:8082/tbOrder/101
│
├─ order-service ──> MySQL 的 spring-cloud-order 数据库
│
└─ OpenFeign ──> Nacos 查询 user-service ──> user-service
这里最重要的概念是:
- 每个微服务都是一个独立的 Spring Boot 应用。
- 每个微服务有自己的名称、端口和数据库。
- 服务启动后把自己的地址注册到 Nacos。
- 订单服务调用用户服务时,不写死用户服务的 IP,而是通过服务名
user-service查找。 - Docker Compose 负责一次性创建网络、MySQL、Nacos 和两个后端容器。
2. 准备开发环境
需要安装以下软件:
- JDK 17
- Maven 3.9 或兼容版本
- Docker Desktop
- IntelliJ IDEA(推荐,但不是必须)
安装完成后,在 PowerShell 中检查:
powershell
java -version
mvn -version
docker version
docker compose version
预期结果:
- Java 显示 17。
- Maven 能正常显示版本,并且使用 Java 17。
- Docker 客户端和服务端都能显示信息。
docker compose命令存在。
如果 docker version 只能显示 Client,不能显示 Server,请先启动 Docker Desktop,等待左下角显示 Docker Engine 已运行。
3. 创建项目目录
创建父工程目录:
text
demo01_parent/
├─ pom.xml
├─ docker-compose.yml
├─ init/
│ ├─ 01-user.sql
│ ├─ 02-order.sql
│ └─ nacos.sql
├─ demo01_user/
│ ├─ pom.xml
│ ├─ Dockerfile
│ └─ src/
└─ demo01_order/
├─ pom.xml
├─ Dockerfile
└─ src/
说明:
demo01_parent是 Maven 父工程,同时也是 Compose 项目根目录。demo01_user是用户微服务。demo01_order是订单微服务。init保存 MySQL 第一次启动时自动执行的 SQL。- 每个后端目录都有自己的
Dockerfile。
4. 创建 Maven 父工程
在项目根目录创建 pom.xml:
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>org.example.c</groupId>
<artifactId>demo01_parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>demo01_user</module>
<module>demo01_order</module>
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2023.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
<mysql.version>8.0.33</mysql.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
</properties>
<dependencyManagement>
<dependencies>
<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.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
关键说明:
<packaging>pom</packaging>表示父工程本身不生成可运行 JAR,主要负责管理子模块。<modules>告诉 Maven 一次构建两个子工程。dependencyManagement只管理依赖版本,不会自动把依赖加入子工程。- Spring Boot、Spring Cloud 和 Spring Cloud Alibaba 的版本必须互相兼容,不能随意拼接不同年代的版本。
5. 创建用户服务
5.1 用户服务目录
text
demo01_user/
├─ pom.xml
├─ Dockerfile
└─ src/main/
├─ java/org/example/c/demo01_user/
│ ├─ Demo01UserApplication.java
│ ├─ controller/TbUserController.java
│ ├─ entity/TbUser.java
│ ├─ mapper/TbUserMapper.java
│ ├─ service/TbUserService.java
│ └─ service/impl/TbUserServiceImpl.java
└─ resources/
├─ application.yml
└─ application-compose.yml
5.2 用户服务 pom.xml
创建 demo01_user/pom.xml:
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example.c</groupId>
<artifactId>demo01_parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>demo01_user</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
依赖作用:
spring-boot-starter-web:提供 Spring MVC、Tomcat 和 REST 接口能力。mysql-connector-j:让 Java 可以连接 MySQL。lombok:使用@Data自动生成 getter、setter 等代码。mybatis-plus-spring-boot3-starter:简化数据库 CRUD。spring-cloud-starter-alibaba-nacos-discovery:把服务注册到 Nacos,并发现其他服务。spring-boot-starter-test:提供测试支持。spring-boot-maven-plugin:把程序打包成可以用java -jar启动的 JAR。
5.3 用户服务启动类
创建 Demo01UserApplication.java:
java
package org.example.c.demo01_user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class Demo01UserApplication {
public static void main(String[] args) {
SpringApplication.run(Demo01UserApplication.class, args);
}
}
说明:
@SpringBootApplication是 Spring Boot 主入口。@EnableDiscoveryClient表示启用服务注册与发现。引入 Nacos discovery 依赖后,应用启动时会自动注册。
5.4 用户实体类
创建 entity/TbUser.java:
java
package org.example.c.demo01_user.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("tb_user")
public class TbUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String address;
}
说明:
@TableName("tb_user")指定对应数据库表。@TableId(type = IdType.AUTO)表示主键由 MySQL 自增。- MyBatis Plus 默认会把 Java 的
username映射到不区分大小写的 MySQL 字段userName。
5.5 Mapper、Service 和实现类
创建 mapper/TbUserMapper.java:
java
package org.example.c.demo01_user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.c.demo01_user.entity.TbUser;
@Mapper
public interface TbUserMapper extends BaseMapper<TbUser> {
}
创建 service/TbUserService.java:
java
package org.example.c.demo01_user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.example.c.demo01_user.entity.TbUser;
public interface TbUserService extends IService<TbUser> {
}
创建 service/impl/TbUserServiceImpl.java:
java
package org.example.c.demo01_user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.c.demo01_user.entity.TbUser;
import org.example.c.demo01_user.mapper.TbUserMapper;
import org.example.c.demo01_user.service.TbUserService;
import org.springframework.stereotype.Service;
@Service
public class TbUserServiceImpl
extends ServiceImpl<TbUserMapper, TbUser>
implements TbUserService {
}
说明:继承 MyBatis Plus 提供的接口和实现后,无需手写 SQL,就能使用 list()、getById()、save() 等常用方法。
5.6 用户接口
创建 controller/TbUserController.java:
java
package org.example.c.demo01_user.controller;
import org.example.c.demo01_user.entity.TbUser;
import org.example.c.demo01_user.service.TbUserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class TbUserController {
private final TbUserService tbUserService;
public TbUserController(TbUserService tbUserService) {
this.tbUserService = tbUserService;
}
@GetMapping("/list")
public List<TbUser> list() {
return tbUserService.list();
}
@GetMapping("/{id}")
public TbUser getOne(@PathVariable Long id) {
return tbUserService.getById(id);
}
}
完成后提供两个接口:
GET /user/list:查询所有用户。GET /user/{id}:按照用户 ID 查询。
6. 创建订单服务
订单服务的 Mapper、Service 和实体类与用户服务结构相同,只是操作 tb_order 表。这里重点说明它和用户服务不同的部分。
6.1 订单服务 pom.xml
创建 demo01_order/pom.xml,基础依赖与用户服务相同,并额外加入 OpenFeign 和 LoadBalancer:
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example.c</groupId>
<artifactId>demo01_parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>demo01_order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
额外依赖的作用:
spring-cloud-starter-openfeign:通过 Java 接口声明远程 HTTP 调用。spring-cloud-starter-loadbalancer:根据 Nacos 返回的服务实例选择一个实例;以后增加多个用户服务实例时也能负载均衡。
6.2 订单服务启动类
java
package org.example.c.demo01_order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Demo01OrderApplication {
public static void main(String[] args) {
SpringApplication.run(Demo01OrderApplication.class, args);
}
}
@EnableFeignClients 会扫描带有 @FeignClient 的接口并生成调用实现。
6.3 订单实体、Mapper 和 Service
实体类的核心结构:
java
package org.example.c.demo01_order.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("tb_order")
public class TbOrder {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String name;
private Long price;
private Integer num;
}
Mapper:
java
@Mapper
public interface TbOrderMapper extends BaseMapper<TbOrder> {
}
Service:
java
public interface TbOrderService extends IService<TbOrder> {
}
实现类:
java
@Service
public class TbOrderServiceImpl
extends ServiceImpl<TbOrderMapper, TbOrder>
implements TbOrderService {
}
实际创建文件时,需要补齐对应的 package 和 import,写法与用户服务一致。
6.4 创建 OpenFeign 客户端
创建 feignclient/UserFeignClient.java:
java
package org.example.c.demo01_order.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("user-service")
public interface UserFeignClient {
@GetMapping("/user/{id}")
String getOne(@PathVariable("id") Long id);
}
说明:
user-service必须和用户服务的spring.application.name完全一致。- 这里没有写 IP 和端口。
- OpenFeign 会先向 Nacos 查询
user-service的可用实例,再发送 HTTP 请求。 - 推荐在
@PathVariable中明确写参数名,避免某些编译配置下无法推断参数名。
6.5 创建订单接口
java
package org.example.c.demo01_order.controller;
import org.example.c.demo01_order.entity.TbOrder;
import org.example.c.demo01_order.feignclient.UserFeignClient;
import org.example.c.demo01_order.service.TbOrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/tbOrder")
public class TbOrderController {
private final TbOrderService tbOrderService;
private final UserFeignClient userFeignClient;
public TbOrderController(TbOrderService tbOrderService,
UserFeignClient userFeignClient) {
this.tbOrderService = tbOrderService;
this.userFeignClient = userFeignClient;
}
@GetMapping("/list")
public List<TbOrder> list() {
return tbOrderService.list();
}
@GetMapping("/{id}")
public String getOne(@PathVariable Long id) {
TbOrder order = tbOrderService.getById(id);
String user = userFeignClient.getOne(order.getUserId());
return order + System.lineSeparator() + user;
}
}
访问 /tbOrder/101 时将发生以下过程:
- 浏览器请求订单服务。
- 订单服务从订单数据库查询订单 101。
- 从订单中取得
userId。 - OpenFeign 通过 Nacos 找到用户服务。
- 调用用户服务的
/user/{id}。 - 订单服务组合订单和用户结果后返回浏览器。
7. 配置本地环境和 Compose 环境
同一个程序有两种运行位置:
- 在 IDEA 中运行:程序在宿主机上。
- 在 Docker 中运行:程序在 Compose 网络的容器中。
这两种环境访问 MySQL 和 Nacos 的地址不同,所以需要区分配置。
7.1 用户服务 application.yml
demo01_user/src/main/resources/application.yml:
yaml
server:
port: 8081
spring:
application:
name: user-service
datasource:
url: jdbc:mysql://localhost:3307/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123
cloud:
nacos:
discovery:
server-addr: localhost:8848
这是本地默认配置。在 IDEA 中直接运行时,Java 程序通过宿主机映射端口访问容器。
7.2 用户服务 application-compose.yml
demo01_user/src/main/resources/application-compose.yml:
yaml
spring:
datasource:
url: jdbc:mysql://mysql:3306/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
cloud:
nacos:
discovery:
server-addr: nacos:8848
7.3 订单服务 application.yml
demo01_order/src/main/resources/application.yml:
yaml
server:
port: 8082
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://localhost:3307/spring-cloud-order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123
cloud:
nacos:
discovery:
server-addr: localhost:8848
7.4 订单服务 application-compose.yml
demo01_order/src/main/resources/application-compose.yml:
yaml
spring:
datasource:
url: jdbc:mysql://mysql:3306/spring-cloud-order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
cloud:
nacos:
discovery:
server-addr: nacos:8848
7.5 为什么地址不同
text
宿主机访问 MySQL:localhost:3307
容器访问 MySQL:mysql:3306
宿主机访问 Nacos:localhost:8848
容器访问 Nacos:nacos:8848
Compose 会为项目创建一个内部网络,并为每个服务提供 DNS 名称。因此,在容器内部:
mysql会解析到 MySQL 容器。nacos会解析到 Nacos 容器。user-service会解析到用户服务容器。
不能在容器中使用 localhost 访问其他容器,因为容器中的 localhost 只代表当前容器自己。
application-compose.yml 只有在激活 compose profile 时才加载。Compose 中通过下面的环境变量激活:
yaml
environment:
SPRING_PROFILES_ACTIVE: compose
Spring Boot 会先读取 application.yml,再用 application-compose.yml 中的同名配置覆盖它。
8. 准备 MySQL 初始化脚本
8.1 用户数据库
创建 init/01-user.sql:
sql
CREATE DATABASE IF NOT EXISTS `spring-cloud-user`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `spring-cloud-user`;
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`userName` VARCHAR(100) DEFAULT NULL COMMENT '用户名',
`address` VARCHAR(255) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_name` (`userName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_user` (`id`, `userName`, `address`) VALUES
(1, '柳岩', '湖南省衡阳市'),
(2, '粒子', '陕西省西安市'),
(3, '华沉鱼', '湖北省十堰市');
8.2 订单数据库
创建 init/02-order.sql:
sql
CREATE DATABASE IF NOT EXISTS `spring-cloud-order`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `spring-cloud-order`;
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单 ID',
`user_id` BIGINT NOT NULL COMMENT '用户 ID',
`name` VARCHAR(100) DEFAULT NULL COMMENT '商品名称',
`price` BIGINT NOT NULL COMMENT '商品价格,单位为分',
`num` INT DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_order` (`id`, `user_id`, `name`, `price`, `num`) VALUES
(101, 1, 'Apple iPhone', 699900, 1),
(102, 2, '电动车', 209900, 1),
(103, 3, '运动鞋', 43900, 1);
8.3 Nacos 数据库脚本
Nacos 使用 MySQL 保存配置、命名空间等数据。需要把与所用 Nacos 版本对应的 MySQL 初始化脚本保存为:
text
init/nacos.sql
本项目使用 Nacos v2.3.2,应使用对应版本的 mysql-schema.sql 内容。Compose 中的 MYSQL_DATABASE: spring-cloud 会先创建 spring-cloud 数据库,随后 nacos.sql 会在这个数据库中创建 Nacos 所需表。
重要规则:/docker-entrypoint-initdb.d 中的脚本只会在 MySQL 数据目录第一次创建时执行。
如果 MySQL 数据卷已经存在,后来新增或修改 SQL,简单重启容器不会重新执行它。开发环境需要完全重新初始化时,可以使用:
powershell
docker compose down -v
docker compose up -d
警告:down -v 会删除 Compose 数据卷,其中的 MySQL 数据会丢失。只有确认数据可以删除时才能执行。
9. 为两个后端编写 Dockerfile
9.1 用户服务 Dockerfile
创建 demo01_user/Dockerfile:
dockerfile
FROM eclipse-temurin:17-jre
COPY target/demo01_user-0.0.1-SNAPSHOT.jar /app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "/app.jar"]
9.2 订单服务 Dockerfile
创建 demo01_order/Dockerfile:
dockerfile
FROM eclipse-temurin:17-jre
COPY target/demo01_order-0.0.1-SNAPSHOT.jar /app.jar
EXPOSE 8082
ENTRYPOINT ["java", "-jar", "/app.jar"]
每一行的作用:
FROM:以包含 Java 17 运行环境的镜像为基础。COPY:把 Maven 生成的 JAR 复制进镜像。EXPOSE:声明应用监听的端口,主要用于说明;真正映射端口仍由 Compose 的ports决定。ENTRYPOINT:容器启动时执行java -jar /app.jar。
注意:Dockerfile 不会自动执行 Maven 打包。如果 target 中没有 JAR,镜像构建会失败;如果 JAR 是旧的,镜像里运行的也会是旧代码。
10. 编写 Docker Compose
在父工程根目录创建 docker-compose.yml:
yaml
services:
mysql:
container_name: mysql
image: mysql:8.0.30
command: --skip-character-set-client-handshake
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: 123
MYSQL_DATABASE: spring-cloud
restart: always
nacos:
container_name: nacos
image: nacos/nacos-server:v2.3.2
ports:
- "8848:8848"
- "9848:9848"
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: spring-cloud
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: 123
NACOS_AUTH_ENABLE: false
MYSQL_SERVICE_DB_PARAM: characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
depends_on:
- mysql
restart: always
user-service:
container_name: user-service
build: ./demo01_user
ports:
- "8081:8081"
environment:
SPRING_PROFILES_ACTIVE: compose
depends_on:
- mysql
- nacos
restart: always
order-service:
container_name: order-service
build: ./demo01_order
ports:
- "8082:8082"
environment:
SPRING_PROFILES_ACTIVE: compose
depends_on:
- mysql
- nacos
restart: always
volumes:
mysql_data: {}
10.1 ports 的含义
格式是:
text
宿主机端口:容器端口
例如:
yaml
ports:
- "3307:3306"
表示:
- 在 Windows 上访问 MySQL 使用
localhost:3307。 - 其他容器访问 MySQL 使用
mysql:3306。
10.2 volumes 的含义
yaml
- mysql_data:/var/lib/mysql
把数据库数据放进 Docker 命名卷。即使容器被重新创建,数据仍然保留。
yaml
- ./init:/docker-entrypoint-initdb.d
把项目中的 init 目录挂载到 MySQL 初始化脚本目录。
10.3 depends_on 的限制
depends_on 只保证容器启动顺序,不保证 MySQL 和 Nacos 已经完全可以接受请求。
本项目给服务设置了 restart: always。如果后端第一次启动时 Nacos 尚未准备好,后端退出后会自动重启,直到连接成功。
更严格的生产配置应给 MySQL 和 Nacos增加 healthcheck,再使用健康状态作为启动条件。学习阶段可以先使用当前配置。
10.4 为什么不需要手动声明 network
Compose 会自动创建一个默认网络,并把当前文件中的所有服务加入该网络,因此容器可以直接用服务名互相访问。
11. 第一次构建和启动
所有命令都在父工程根目录执行。
第一步:打包 Java 项目
powershell
mvn clean package -DskipTests
作用:
clean:删除之前的target,避免旧产物干扰。package:编译并生成两个可运行 JAR。-DskipTests:跳过测试执行,适合首次验证环境;正式交付前不应长期跳过测试。
成功后应存在:
text
demo01_user/target/demo01_user-0.0.1-SNAPSHOT.jar
demo01_order/target/demo01_order-0.0.1-SNAPSHOT.jar
第二步:检查 Compose 配置
powershell
docker compose config
作用:解析 Compose 文件,可以提前发现 YAML 缩进、字段拼写和变量错误。
第三步:构建镜像并启动
powershell
docker compose up -d --build
作用:
up:创建并启动服务。-d:后台运行。--build:根据最新 JAR 重新构建两个后端镜像。
第一次启动需要下载 MySQL、Nacos 和 Java 基础镜像,耗时会更长。
第四步:查看容器状态
powershell
docker compose ps -a
正常结果中,4 个服务都应该是 Up。如果后端显示 Restarting 或 Exited,说明应用启动失败,必须查看日志。
第五步:查看日志
powershell
docker compose logs -f user-service order-service
看到类似下面的内容表示启动成功:
text
Started Demo01UserApplication
Started Demo01OrderApplication
register finished
按 Ctrl + C 只会退出日志查看,不会停止后台容器。
12. 验证是否搭建成功
12.1 验证 Nacos
浏览器访问:
text
http://localhost:8848/nacos/
进入"服务管理 → 服务列表",应该能看到:
text
user-service
order-service
12.2 验证用户服务
text
http://localhost:8081/user/list
http://localhost:8081/user/1
应该返回用户数据。
12.3 验证订单服务
text
http://localhost:8082/tbOrder/list
http://localhost:8082/tbOrder/101
第二个地址不仅查询订单,还会通过 OpenFeign 调用用户服务。如果同时返回订单和用户内容,说明以下链路全部正常:
text
浏览器 → 订单服务 → 订单数据库
→ Nacos → 用户服务 → 用户数据库
注意:访问 http://localhost:8081/ 或 http://localhost:8082/ 可能返回 404,这是正常的,因为示例没有定义根路径 /。
13. 修改代码后的标准操作流程
只修改 Java 代码或 application*.yml 后:
powershell
mvn package -DskipTests
docker compose up -d --build
docker compose ps
原因:
- 源代码和配置文件需要先进入新的 JAR。
- Dockerfile 再把新 JAR 放进新镜像。
- Compose 使用新镜像重新创建后端容器。
只执行 docker compose restart 不会重新打包,也不会重新构建镜像,所以代码修改不会生效。
如果只修改 docker-compose.yml:
powershell
docker compose up -d
如果修改 Dockerfile、JAR 或构建上下文中的内容:
powershell
docker compose up -d --build
14. 常用维护命令
查看服务状态:
powershell
docker compose ps -a
查看全部日志:
powershell
docker compose logs -f
只查看最近 100 行:
powershell
docker compose logs --tail=100 user-service
重启某个服务:
powershell
docker compose restart user-service
停止并删除容器,但保留数据库数据:
powershell
docker compose down
再次启动:
powershell
docker compose up -d
停止并删除容器及数据卷:
powershell
docker compose down -v
最后一个命令会删除数据库数据,只用于确认可以清空数据的开发环境。
15. 常见故障排查
15.1 浏览器显示"无法访问此网站"
先执行:
powershell
docker compose ps -a
检查后端是否为 Up。如果是 Restarting,继续执行:
powershell
docker compose logs --tail=200 user-service
docker compose logs --tail=200 order-service
浏览器访问失败通常只是最终现象,真正原因在应用启动日志中。
15.2 UnknownHostException: host.docker.internal
容器中的配置不应该绕到宿主机访问同一个 Compose 中的服务。应使用:
yaml
mysql:3306
nacos:8848
并确认 Compose 激活了:
yaml
SPRING_PROFILES_ACTIVE: compose
15.3 Connection refused
常见原因:
- MySQL 或 Nacos 尚未启动完成。
- 端口写错。
- 容器内部错误使用了宿主机映射端口。
容器内部必须使用 MySQL 的 3306,不能使用宿主机的 3307。
15.4 Unknown database
说明数据库没有创建。检查:
init是否正确挂载。- SQL 文件是否存在。
- MySQL 数据卷是不是在添加 SQL 之前就已经创建。
如果允许清空数据,可执行:
powershell
docker compose down -v
docker compose up -d
15.5 修改配置后容器仍使用旧配置
最常见原因是只修改了源码,没有重新生成 JAR 和镜像。执行:
powershell
mvn package -DskipTests
docker compose up -d --build
15.6 Nacos 中看不到服务
检查:
- 后端是否正常运行。
spring.application.name是否存在。server-addr在 Compose 环境是否为nacos:8848。- Nacos 是否已正常启动。
- 日志中是否出现
register finished。
15.7 Feign 报"找不到 user-service 实例"
检查:
- 用户服务和订单服务是否连接到同一个 Nacos。
@FeignClient("user-service")是否和用户服务名称完全一致。- Nacos 服务列表中的用户服务是否健康。
- 订单服务是否加入了 OpenFeign 和 LoadBalancer 依赖。
- 启动类是否有
@EnableFeignClients。
15.8 访问根路径返回 404
这是接口路径不正确,不代表服务没启动。当前接口是:
text
/user/list
/user/{id}
/tbOrder/list
/tbOrder/{id}
15.9 MySQL 中文乱码
建议同时做到:
- 数据库和表使用
utf8mb4。 - JDBC URL 带
useUnicode=true&characterEncoding=utf8。 - SQL 文件保存为 UTF-8。
- 不要使用编辑器错误地以 ANSI 或 GBK 保存 SQL。
15.10 端口被占用
如果 8081、8082、8848、9848 或 3307 已被其他程序占用,可以修改 ports 左侧的宿主机端口。例如:
yaml
ports:
- "18081:8081"
此时浏览器访问 http://localhost:18081,但容器内部端口和 Spring Boot 的 server.port 仍然是 8081。
16. 本地运行与全容器运行的区别
方案 A:MySQL、Nacos 在 Docker,后端在 IDEA
先启动基础设施:
powershell
docker compose up -d mysql nacos
然后分别在 IDEA 中运行两个启动类。此时使用默认 application.yml:
text
MySQL:localhost:3307
Nacos:localhost:8848
方案 B:所有服务都在 Docker
powershell
mvn package -DskipTests
docker compose up -d --build
此时 Compose 设置 SPRING_PROFILES_ACTIVE=compose,应用读取 application-compose.yml:
text
MySQL:mysql:3306
Nacos:nacos:8848
不要同时在 IDEA 和 Docker 中启动相同端口的后端,否则会发生端口冲突。
17. 从零搭建检查清单
按顺序逐项确认:
- 已安装 JDK 17、Maven 和 Docker Desktop。
- Docker Desktop 已启动。
- 父工程
pom.xml使用pom打包,并声明两个模块。 - 父工程统一管理 Spring Cloud、Nacos、MySQL 和 MyBatis Plus 版本。
- 用户服务端口为 8081,服务名为
user-service。 - 订单服务端口为 8082,服务名为
order-service。 - 两个服务都加入 Nacos discovery 依赖。
- 订单服务加入 OpenFeign 和 LoadBalancer 依赖。
- 订单启动类加入
@EnableFeignClients。 - Feign 服务名与
spring.application.name完全一致。 - 本地配置使用
localhost:3307和localhost:8848。 - Compose 配置使用
mysql:3306和nacos:8848。 - Compose 激活
SPRING_PROFILES_ACTIVE=compose。 -
init目录中包含用户、订单和 Nacos SQL。 - 两个 Dockerfile 中的 JAR 名称与 Maven 产物一致。
- 已运行
mvn clean package -DskipTests。 - 已运行
docker compose config。 - 已运行
docker compose up -d --build。 -
docker compose ps -a显示 4 个容器都是Up。 - Nacos 中能看到两个服务。
- 用户列表接口返回 200。
- 订单详情接口可以同时获得订单和用户信息。
18. 推荐的学习顺序
为了真正理解这套项目,建议按下面顺序练习:
- 先只用 Spring Boot 写一个返回字符串的接口。
- 加入 MySQL 和 MyBatis Plus,让接口能查询数据库。
- 复制出第二个独立 Spring Boot 服务。
- 启动 Nacos,让两个服务完成注册。
- 在订单服务中加入 OpenFeign,通过服务名调用用户服务。
- 分别为两个后端构建 Docker 镜像。
- 用 Compose 统一启动 MySQL、Nacos 和两个后端。
- 理解本地地址与容器服务名的区别。
- 最后再增加网关、配置中心、熔断限流和认证等能力。
完成本文后,你已经搭建的是一个最小但完整的微服务闭环:独立服务、独立数据、注册发现、服务调用、容器化和统一编排都已具备。
第二部分:加入服务集群、Gateway 与 Sentinel
前面的项目已经具备 MySQL、Nacos、用户服务、订单服务、OpenFeign 和 Docker Compose。本部分继续将它升级为:
text
浏览器
│
▼
Gateway:localhost:8222
├─ /tbUser/** ──> user-service 集群
│ ├─ user-1:8081
│ └─ user-2:8083
│
└─ /tbOrder/** ──> order-service:8082
│
└─ OpenFeign ──> user-service 集群
19. 升级后的服务和端口
| Compose 服务 | 应用名称 | 宿主机端口 | 容器端口 | 作用 |
|---|---|---|---|---|
mysql |
MySQL | 3307 | 3306 | 保存业务数据和 Nacos 数据 |
nacos |
Nacos | 8848、9848 | 8848、9848 | 服务注册与发现 |
sentinel |
Sentinel Dashboard | 8858 | 8858 | 流量监控和规则管理 |
user-1 |
user-service |
8081 | 8081 | 用户服务实例 1 |
user-2 |
user-service |
8083 | 8083 | 用户服务实例 2 |
order-service |
order-service |
8082 | 8082 | 订单服务 |
gateway-service |
gateway-service |
8222 | 8222 | 系统统一入口 |
这里的"集群"是业务服务集群:两个用户服务实例使用同一个服务名 user-service。当前 Nacos 仍使用:
yaml
MODE: standalone
所以 Nacos 本身不是集群。真正的 Nacos 集群需要部署多个 Nacos 节点。
20. 创建 Gateway 模块
20.1 把 Gateway 加入父工程
在父工程 pom.xml 中加入模块:
xml
<modules>
<module>demo01_user</module>
<module>demo01_order</module>
<module>demo01_gateway</module>
</modules>
目录结构:
text
demo01_gateway/
├─ pom.xml
├─ Dockerfile
└─ src/main/
├─ java/org/example/c/demo01_gateway/
│ └─ Demo01GatewayApplication.java
└─ resources/
├─ application.yml
└─ application-compose.yml
20.2 Gateway 依赖
demo01_gateway/pom.xml 的关键依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
各依赖的作用:
spring-cloud-starter-gateway:提供路由、谓词和过滤器能力。nacos-discovery:让 Gateway 注册到 Nacos,并查询后端实例。loadbalancer:处理lb://服务名并选择一个实例。sentinel:监控 Gateway 流量并执行限流规则。
Gateway 使用 WebFlux 响应式模型,不要再加入 spring-boot-starter-web,否则可能产生 MVC 与 WebFlux 冲突。Gateway 不直接调用 Feign 接口,通常也不需要 OpenFeign 依赖。
构建插件:
xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
20.3 Gateway 启动类
java
package org.example.c.demo01_gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Demo01GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(Demo01GatewayApplication.class, args);
}
}
当前版本引入 Nacos Discovery starter 后,会自动启用服务注册与发现,因此 @EnableDiscoveryClient 可以省略。
21. 配置 Gateway 路由
21.1 本地配置
demo01_gateway/src/main/resources/application.yml:
yaml
server:
port: 8222
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/tbUser/**
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/tbOrder/**
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8858
port: 8719
核心配置解释:
id是路由的唯一名称。Path决定什么请求匹配这条路由。lb://user-service表示从 Nacos 获取user-service的实例,并进行负载均衡。- Gateway 默认保留原始路径,因此
/tbUser/list转发后仍是/tbUser/list。 - Gateway 的 Path 必须与后端 Controller 的
@RequestMapping一致。
例如用户 Controller 为:
java
@RequestMapping("/tbUser")
那么 Gateway 应匹配:
yaml
- Path=/tbUser/**
可以配置自动路由:
yaml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
但自动路由会形成带服务名的访问路径,控制范围也更宽。初学和正式项目推荐优先使用显式 routes。
21.2 Compose 专用配置
demo01_gateway/src/main/resources/application-compose.yml:
yaml
spring:
cloud:
nacos:
discovery:
server-addr: nacos:8848
sentinel:
transport:
dashboard: sentinel:8858
port: 8719
本地运行使用 localhost,容器运行使用 Compose 服务名:
text
本地:localhost:8848、localhost:8858
容器:nacos:8848、sentinel:8858
22. 创建两个用户服务实例
22.1 让应用端口可覆盖
用户服务 application-compose.yml:
yaml
server:
port: ${SERVER_PORT:8081}
spring:
datasource:
url: jdbc:mysql://mysql:3306/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
cloud:
nacos:
discovery:
server-addr: nacos:8848
sentinel:
transport:
dashboard: sentinel:8858
port: 8719
${SERVER_PORT:8081} 表示:存在 SERVER_PORT 环境变量时使用该值,否则使用 8081。
22.2 在 Compose 中复用同一个镜像
yaml
user-1:
container_name: user-1
build: ./demo01_user
ports:
- "8081:8081"
environment:
SPRING_PROFILES_ACTIVE: compose
SERVER_PORT: 8081
depends_on:
- mysql
- nacos
restart: always
user-2:
container_name: user-2
build: ./demo01_user
ports:
- "8083:8083"
environment:
SPRING_PROFILES_ACTIVE: compose
SERVER_PORT: 8083
depends_on:
- mysql
- nacos
restart: always
两个容器使用同一份代码和镜像,但端口不同。因为基础配置都是:
yaml
spring:
application:
name: user-service
所以 Nacos 中表现为一个服务、两个实例。
22.3 显示当前实例端口
为了验证负载均衡,可以让接口返回当前端口:
java
@Value("${server.port}")
private String port;
@GetMapping("/{id}")
public String getOne(@PathVariable Long id) {
TbUser user = tbUserService.getById(id);
return user + System.lineSeparator() + "port=" + port;
}
连续访问:
text
http://localhost:8222/tbUser/1
响应中的端口会出现 8081 或 8083,说明 Gateway 从两个健康实例中选择了一个。
23. OpenFeign 调用用户集群
订单服务仍然只声明服务名,不需要知道有几个用户实例:
java
@FeignClient("user-service")
public interface UserFeignClient {
@GetMapping("/tbUser/{id}")
String getOne(@PathVariable("id") Long id);
}
调用过程:
text
order-service
├─ 向 Nacos 查询 user-service
├─ 获得 user-1:8081 和 user-2:8083
├─ LoadBalancer 选择一个健康实例
└─ OpenFeign 请求 /tbUser/{id}
不要在 Feign 中写固定的 url=http://user-1:8081,否则会绕过 Nacos 和负载均衡。
24. Sentinel 的组成与接入
Sentinel 在本项目中包含两部分:
- Sentinel 客户端:位于 Gateway、用户服务和订单服务中,真正执行流量统计、限流和熔断。
- Sentinel Dashboard:独立控制台,用于查看应用和下发规则。
给三个应用加入:
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
Compose 环境配置:
yaml
spring:
cloud:
sentinel:
transport:
dashboard: sentinel:8858
port: 8719
说明:
dashboard是控制台地址。port是 Sentinel 客户端命令通信端口,不是业务 HTTP 端口。- 每个容器有独立网络空间,多个容器使用 8719 通常不会冲突。
- Dashboard 不是业务请求的代理,流量不会先经过 Dashboard。
Compose 中加入 Sentinel:
yaml
sentinel:
container_name: sentinel
image: registry.cn-beijing.aliyuncs.com/all100/sentinel-dashboard:1.8.6
ports:
- "8858:8858"
environment:
JAVA_OPTS: "-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858"
restart: always
浏览器访问:
text
http://localhost:8858
如果该镜像保留默认登录信息,通常为 sentinel/sentinel;如果不能登录,应以所用镜像的实际说明为准。
25. 升级后的完整 Compose 配置
yaml
services:
mysql:
container_name: mysql
image: mysql:8.0.30
command: --skip-character-set-client-handshake
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: 123
MYSQL_DATABASE: spring-cloud
restart: always
nacos:
container_name: nacos
image: nacos/nacos-server:v2.3.2
ports:
- "8848:8848"
- "9848:9848"
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: spring-cloud
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: 123
NACOS_AUTH_ENABLE: false
MYSQL_SERVICE_DB_PARAM: characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
depends_on:
- mysql
restart: always
sentinel:
container_name: sentinel
image: registry.cn-beijing.aliyuncs.com/all100/sentinel-dashboard:1.8.6
ports:
- "8858:8858"
environment:
JAVA_OPTS: "-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858"
restart: always
user-1:
container_name: user-1
build: ./demo01_user
ports:
- "8081:8081"
environment:
SPRING_PROFILES_ACTIVE: compose
SERVER_PORT: 8081
depends_on:
- mysql
- nacos
- sentinel
restart: always
user-2:
container_name: user-2
build: ./demo01_user
ports:
- "8083:8083"
environment:
SPRING_PROFILES_ACTIVE: compose
SERVER_PORT: 8083
depends_on:
- mysql
- nacos
- sentinel
restart: always
order-service:
container_name: order-service
build: ./demo01_order
ports:
- "8082:8082"
environment:
SPRING_PROFILES_ACTIVE: compose
depends_on:
- mysql
- nacos
- sentinel
restart: always
gateway-service:
container_name: gateway-service
build: ./demo01_gateway
ports:
- "8222:8222"
environment:
SPRING_PROFILES_ACTIVE: compose
depends_on:
- nacos
- sentinel
restart: always
volumes:
mysql_data: {}
新版 Compose 不再需要顶层 version: '3.8'。depends_on 只保证启动顺序,不保证依赖已经健康,因此开发环境可配合 restart: always,生产环境应增加 healthcheck。
26. Gateway Dockerfile
demo01_gateway/Dockerfile:
dockerfile
FROM eclipse-temurin:17-jre
COPY target/demo01_gateway-0.0.1-SNAPSHOT.jar /app.jar
EXPOSE 8222
ENTRYPOINT ["java", "-jar", "/app.jar"]
27. 重新构建与启动
在父工程根目录执行:
powershell
mvn clean package -DskipTests
docker compose config
docker compose up -d --build
docker compose ps -a
三个 JAR 应分别生成在:
text
demo01_user/target/demo01_user-0.0.1-SNAPSHOT.jar
demo01_order/target/demo01_order-0.0.1-SNAPSHOT.jar
demo01_gateway/target/demo01_gateway-0.0.1-SNAPSHOT.jar
查看日志:
powershell
docker compose logs --tail=100 gateway-service user-1 user-2 order-service
正常情况下应看到 7 个容器为 Up:
text
mysql
nacos
sentinel
user-1
user-2
order-service
gateway-service
28. 按顺序验证
28.1 验证 Nacos
访问:
text
http://localhost:8848/nacos/
应看到:
text
gateway-service:1 个实例
order-service:1 个实例
user-service:2 个实例
28.2 直接验证后端
text
http://localhost:8081/tbUser/1
http://localhost:8083/tbUser/1
http://localhost:8082/tbOrder/list
28.3 通过 Gateway 验证
text
http://localhost:8222/tbUser/list
http://localhost:8222/tbUser/1
http://localhost:8222/tbOrder/list
http://localhost:8222/tbOrder/101
28.4 验证集群容错
先停止一个用户实例:
powershell
docker compose stop user-1
等待 Nacos 更新实例状态后,再访问 Gateway。请求应由 user-2 处理。测试完成后恢复:
powershell
docker compose start user-1
29. 使用 Sentinel 配置限流
Sentinel 通常要在应用产生请求后才会上报资源。因此先访问几次:
text
http://localhost:8222/tbUser/list
http://localhost:8222/tbOrder/list
http://localhost:8081/tbUser/list
http://localhost:8082/tbOrder/list
再打开:
text
http://localhost:8858
给用户接口添加 QPS 限流:
- 左侧选择
user-service。 - 打开"簇点链路"。
- 找到实际显示的资源,例如
/tbUser/list。 - 点击"流控"。
- 阈值类型选择 QPS。
- 单机阈值设置为 1。
- 流控模式选择"直接"。
- 流控效果选择"快速失败"。
- 保存并快速刷新接口验证。
Gateway 限流时,应选择 gateway-service,再根据控制台实际显示的路由资源添加规则。建议路由使用清晰、固定的 ID:
yaml
id: user-service-route
id: order-service-route
两个 user-service 实例归属于同一个应用,但单机 QPS 通常按每个实例分别计算。两个实例各限制 QPS 1,不等于整个集群总 QPS 1。集群共享总阈值属于 Sentinel 集群流控。
Sentinel 规则是否永久保存
Dashboard 中直接创建的规则通常保存在客户端内存中:
- 应用重启后可能丢失。
- 适合学习和测试。
- 不等同于规则持久化。
生产项目通常将 Sentinel 规则持久化到 Nacos,需要加入 Sentinel Nacos 数据源依赖,并在 Nacos 中保存规则。
30. 限流、熔断与降级的区别
- 限流:请求过多时拒绝一部分请求,保护系统容量。
- 熔断:下游持续慢调用或错误时,暂时停止继续调用,避免故障扩散。
- 降级:主逻辑不可用时返回简化或友好的替代结果。
Sentinel 能触发限流和熔断,但要返回友好业务内容,通常还需要编写 BlockHandler、Feign fallback 或 Gateway 异常响应处理。
31. 升级后常见故障
Gateway 返回 404
检查:
- 请求路径是否匹配
Path。 - Gateway 转发路径是否与 Controller 的
@RequestMapping一致。 - 修改配置后是否重新打包并重建镜像。
Gateway 返回 503
通常表示找不到可用服务实例。检查:
- Nacos 是否存在目标服务。
- 实例是否健康。
lb://user-service是否与spring.application.name完全一致。- Gateway 是否能访问
nacos:8848。
Sentinel 中看不到应用
检查:
- 应用是否加入 Sentinel starter。
- Compose 配置是否使用
sentinel:8858。 - 应用启动后是否真正收到过请求。
- Sentinel 容器是否为
Up。
Sentinel 规则没有效果
检查:
- 是否选对应用和资源。
- 测试请求是否经过配置规则的组件。
- 阈值是否足够低。
- Gateway 规则和后端规则是否混淆。
Feign 调用返回 404
Feign 路径必须和用户 Controller 一致:
java
@GetMapping("/tbUser/{id}")
修改配置后没有生效
powershell
mvn package -DskipTests
docker compose up -d --build
只执行 docker compose restart 不会生成新 JAR,也不会构建新镜像。
32. 第二阶段验收清单
- 父工程包含
demo01_gateway模块。 - Gateway 加入 Gateway、Nacos、LoadBalancer 和 Sentinel 依赖。
- Gateway 没有混用
spring-boot-starter-web。 - 用户路由指向
lb://user-service。 - 订单路由指向
lb://order-service。 - 路由 Path 与后端 Controller 路径一致。
-
user-1和user-2使用同一份镜像。 - 两个实例分别监听 8081 和 8083。
- 两个实例都以
user-service注册到 Nacos。 - Nacos 中
user-service显示两个健康实例。 - OpenFeign 使用服务名调用用户集群。
- Gateway、用户服务和订单服务已加入 Sentinel starter。
- Compose 中使用
sentinel:8858。 - Sentinel 控制台可以通过 8858 打开。
- 通过 8222 可以访问用户和订单接口。
- 响应中的端口可以证明负载均衡生效。
- 停止一个用户实例后,另一个实例仍能响应。
- 产生流量后,Sentinel 能显示应用和资源。
- 低 QPS 规则能够触发限流。
- 已理解 Dashboard 临时规则默认不会持久保存。
33. 下一阶段建议
- 将 Sentinel 规则持久化到 Nacos。
- 为 OpenFeign 添加 fallback。
- 给 Gateway 添加统一异常响应和跨域配置。
- 添加 JWT 登录认证与 Gateway 鉴权过滤器。
- 使用 Nacos 配置中心管理公共配置。
- 给 Compose 服务增加 healthcheck。
- 引入 Prometheus 和 Grafana。
- 学习真正的 Nacos 多节点集群。
34. Spring Cloud 完整工作流程

本项目可以用下面这条链路概括:
text
客户端
↓
Gateway 统一入口
↓
Nacos 查询服务实例
↓
LoadBalancer 选择实例
↓
用户服务或订单服务
↓
MyBatis Plus 查询 MySQL
↓
结果沿原路径返回客户端
整个过程中 Sentinel 持续统计流量,并执行限流、熔断等规则。
34.1 系统启动阶段
执行:
powershell
mvn package -DskipTests
docker compose up -d --build
系统按以下过程启动:
- Maven 将用户、订单和 Gateway 打包成 JAR。
- Docker 根据各模块的 Dockerfile 构建镜像。
- Compose 创建内部网络并启动 MySQL、Nacos、Sentinel、两个用户实例、订单服务和 Gateway。
- MySQL 第一次创建数据卷时执行
init目录中的 SQL。 - 每个 Spring Boot 应用读取
application.yml,再由application-compose.yml覆盖容器专用配置。 - 应用通过
nacos:8848连接 Nacos。 - Gateway、订单服务和两个用户实例把服务名、容器 IP、端口、健康状态注册到 Nacos。
- 应用中的 Sentinel 客户端连接
sentinel:8858,准备上报资源和接收规则。
Nacos 中最终大致形成:
text
gateway-service
└─ 172.x.x.x:8222
order-service
└─ 172.x.x.x:8082
user-service
├─ 172.x.x.x:8081
└─ 172.x.x.x:8083
两个用户容器的名称分别是 user-1 和 user-2,但它们的 spring.application.name 都是 user-service,因此在 Nacos 中属于同一个服务的两个实例。
34.2 客户端访问用户接口
客户端请求:
text
GET http://localhost:8222/tbUser/1
完整过程:
- 请求先到达 Gateway 的 8222 端口。
- Gateway 使用
Path=/tbUser/**匹配用户路由。 - 路由目标
lb://user-service表示不能使用固定地址,需要查询服务实例。 - Gateway 从 Nacos 获取两个健康的
user-service实例。 - LoadBalancer 从 8081 和 8083 实例中选择一个。
- Gateway 把
/tbUser/1转发给选中的用户服务。 - 用户服务的 Controller 接收请求并调用 Service。
- Service 通过 MyBatis Plus 和 Mapper 查询
spring-cloud-user.tb_user。 - 用户服务把查询结果返回 Gateway。
- Gateway 再把结果返回客户端。
流程图:
text
浏览器 :8222
↓
Gateway 匹配 /tbUser/**
↓
Nacos 返回 user-service 实例列表
↓
LoadBalancer 选择 user-1 或 user-2
↓
TbUserController
↓
TbUserService
↓
TbUserMapper
↓
MySQL spring-cloud-user
34.3 客户端访问订单接口
客户端请求:
text
GET http://localhost:8222/tbOrder/101
完整过程:
- Gateway 使用
Path=/tbOrder/**匹配订单路由。 - Gateway 根据
lb://order-service从 Nacos 找到订单服务。 - 请求被转发到
order-service:8082。 - 订单服务从
spring-cloud-order.tb_order查询订单 101。 - 订单数据中包含
userId。 - 订单服务调用
UserFeignClient.getOne(userId)。 - OpenFeign 根据
@FeignClient("user-service")查找目标服务。 - Nacos 返回两个用户服务实例。
- LoadBalancer 选择其中一个实例。
- OpenFeign 向该实例发送
/tbUser/{id}请求。 - 用户服务查询用户数据库并返回用户信息。
- 订单服务组合订单和用户结果,再经过 Gateway 返回客户端。
流程图:
text
浏览器
↓
Gateway
↓
order-service
├─ MyBatis Plus ──> 订单数据库
│
└─ OpenFeign
↓
Nacos 查询 user-service
↓
LoadBalancer 选择用户实例
↓
user-service ──> 用户数据库
↓
组合订单和用户信息
↓
Gateway 返回客户端
34.4 各组件分别负责什么
| 组件 | 在项目中的职责 |
|---|---|
| Spring Boot | 创建并运行每个独立后端应用 |
| Spring Cloud Gateway | 提供统一入口,根据路径转发请求 |
| Nacos Discovery | 保存服务名与实例地址,提供注册和发现 |
| Spring Cloud LoadBalancer | 从同一服务的多个健康实例中选择一个 |
| OpenFeign | 让订单服务通过 Java 接口调用用户服务 |
| Sentinel Client | 统计接口和路由流量,执行限流与熔断规则 |
| Sentinel Dashboard | 查看应用和资源,下发流控规则 |
| MyBatis Plus | 简化实体、Mapper、Service 和数据库 CRUD |
| MySQL | 保存用户、订单和 Nacos 数据 |
| Dockerfile | 把每个 Java 应用制作成镜像 |
| Docker Compose | 创建网络并统一启动、停止所有容器 |
34.5 Sentinel 在调用链中的位置
Sentinel 不负责路由,也不是所有请求都必须经过的代理。Sentinel 客户端运行在应用内部:
text
请求进入 Gateway
↓
Gateway Sentinel 检查路由规则
↓
请求进入后端
↓
后端 Sentinel 检查接口规则
↓
继续执行或立即拒绝
如果请求量超过规则阈值,Sentinel 会快速拒绝部分请求,避免请求继续占用线程、连接和数据库资源。Dashboard 主要用于观察和配置规则,真正执行规则的是各应用中的 Sentinel 客户端。
34.6 一个实例停止后会怎样
例如停止 user-1:
powershell
docker compose stop user-1
随后发生:
user-1不再向 Nacos维持健康状态。- Nacos 将该实例从健康实例列表中移除或标记为不健康。
- Gateway 和订单服务更新本地服务列表。
- LoadBalancer 不再选择
user-1。 - 后续请求继续由
user-2处理。
这就是多实例部署提供的基本容错能力。但如果所有用户实例都停止,Gateway 或 Feign 将无法找到可用实例;要返回友好结果,还需要继续配置 Gateway 异常处理或 Feign fallback。
34.7 配置和地址的工作方式
同一个应用在不同运行环境中使用不同地址:
text
IDEA 本地运行:localhost:3307、localhost:8848、localhost:8858
Compose 容器运行:mysql:3306、nacos:8848、sentinel:8858
Compose 通过下面的环境变量激活容器配置:
yaml
SPRING_PROFILES_ACTIVE: compose
Spring Boot 先加载公共的 application.yml,再加载 application-compose.yml 覆盖同名配置。容器通过 Compose 内部 DNS 使用服务名互相访问,不能使用 localhost 访问其他容器。
34.8 完整流程总结
text
启动阶段:
Compose 启动容器
→ Spring Boot 加载 compose 配置
→ 服务注册到 Nacos
→ Sentinel 客户端连接 Dashboard
请求阶段:
客户端请求 Gateway
→ Gateway 根据 Path 匹配路由
→ Nacos 提供目标服务实例
→ LoadBalancer 选择实例
→ 后端执行业务并查询 MySQL
→ 如果需要其他服务,OpenFeign 再次经过服务发现和负载均衡
→ 响应经 Gateway 返回客户端
治理阶段:
Sentinel 统计 Gateway 和后端资源流量
→ Dashboard 展示调用情况
→ 管理员添加限流或熔断规则
→ Sentinel 客户端在应用内部执行规则
因此,Spring Cloud 并不是一个单独运行的软件,而是一组协作组件:Nacos 解决"服务在哪里",Gateway 解决"请求从哪里进入并转发到哪里",LoadBalancer 解决"多个实例选择哪个",OpenFeign 解决"服务之间如何方便地调用",Sentinel 解决"流量过大或下游异常时如何保护系统"。
35. 使用 Nacos 远端配置中心
前面使用 Nacos 完成了服务注册与发现。现在用户服务又使用 Nacos Config 作为远端配置中心。
这两个功能虽然由同一个 Nacos 提供,但用途不同:
| 功能 | 解决的问题 | 当前项目示例 |
|---|---|---|
| Nacos Discovery | 服务在哪里、有几个健康实例 | user-service 注册 8081 和 8083 两个实例 |
| Nacos Config | 应用应该使用什么配置 | 保存数据库地址和 record.num |
远端配置中心的价值是:公共配置不必全部写死在 JAR 中,修改部分配置时也不必重新打包镜像。
35.1 添加 Nacos Config 依赖
在 demo01_user/pom.xml 中加入:
xml
<!-- Nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
两个 Nacos starter 的职责不要混淆:
xml
<!-- 服务注册与发现 -->
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<!-- 读取远端配置 -->
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
只添加 Discovery 依赖不能读取远端配置;只添加 Config 依赖也不能完成服务注册。
35.2 在 Nacos 中创建配置
打开 Nacos 控制台:
text
http://localhost:8848/nacos/
进入:
text
配置管理 → 配置列表 → 新建配置
按照当前项目填写:
text
Data ID:spring-cloud-user-dev.yml
Group:DEFAULT_GROUP
配置格式:YAML
配置内容:
yaml
spring:
datasource:
url: jdbc:mysql://mysql:3306/spring-cloud-user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
record:
num: 100
字段含义:
spring.datasource.url:用户服务在 Compose 网络中使用的数据库地址。record.num:用于演示远端配置读取和动态刷新的自定义配置。- Data ID 相当于远端配置文件名,导入时必须完全一致。
- Group 用于对配置进行分组;没有特别指定时通常使用
DEFAULT_GROUP。
配置中心适合保存连接地址、功能开关、业务阈值等配置。数据库密码、令牌等敏感内容在正式环境中还需要结合权限、加密或专门的密钥管理系统,不应直接公开保存。
35.3 用户服务导入远端配置
demo01_user/src/main/resources/application-compose.yml 的核心配置:
yaml
server:
port: ${SERVER_PORT:8082}
spring:
cloud:
nacos:
discovery:
server-addr: nacos:8848
config:
server-addr: nacos:8848
config:
import:
- nacos:spring-cloud-user-dev.yml
sentinel:
transport:
dashboard: sentinel:8858
port: 8719
关键配置解释:
nacos.discovery.server-addr:服务注册中心地址。nacos.config.server-addr:配置中心地址。- 当前两个地址都是
nacos:8848,但分别服务于注册发现和配置读取。 spring.config.import:告诉 Spring Boot 在启动期间导入指定的 Nacos 配置。nacos:spring-cloud-user-dev.yml:导入 Data ID 为spring-cloud-user-dev.yml的配置。- Compose 中的
SERVER_PORT=8081或SERVER_PORT=8083会分别决定两个用户实例的端口。
为了减少混淆,可以把默认端口写成 8081:
yaml
server:
port: ${SERVER_PORT:8081}
当前 Compose 已为两个用户容器明确设置 SERVER_PORT,因此即使默认值是 8082,容器实际仍分别监听 8081 和 8083。但默认值改成 8081 更符合用户服务的语义。
35.4 为什么使用 spring.config.import
较新的 Spring Boot 和 Spring Cloud 使用 Config Data 机制加载外部配置:
yaml
spring:
config:
import:
- nacos:spring-cloud-user-dev.yml
应用启动时的过程是:
text
读取本地 application.yml
↓
激活 compose profile
↓
读取 application-compose.yml
↓
发现 spring.config.import
↓
连接 nacos:8848
↓
读取 DEFAULT_GROUP/spring-cloud-user-dev.yml
↓
把远端属性加入 Spring Environment
↓
创建 DataSource、Controller 等 Bean
因此,配置必须在相关 Bean 初始化之前成功加载。假如 Nacos 不可用或 Data ID 不存在,应用可能在启动阶段直接失败。
如果远端配置是可选的,可以使用:
yaml
spring:
config:
import:
- optional:nacos:spring-cloud-user-dev.yml
optional: 表示远端配置不存在或暂时不可用时,应用仍可以尝试使用本地配置启动。是否使用可选模式,应根据该配置是不是应用启动必需项决定。
35.5 本地运行时关闭导入检查
当前默认 application.yml 中配置了:
yaml
spring:
cloud:
nacos:
config:
import-check:
enabled: false
原因是:
- 用户服务已经加入 Nacos Config starter。
- Compose profile 中配置了
spring.config.import。 - 本地或测试环境暂时不强制依赖远端配置。
- 关闭导入检查可以避免本地环境因为没有声明 Nacos import 而直接报错。
这只是关闭"必须声明 import"的校验,不代表 Compose 环境不会读取远端配置。Compose 激活后,application-compose.yml 仍会显式导入 Nacos 配置。
35.6 在代码中读取远端配置
用户 Controller 中加入:
java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
@RestController
@RequestMapping("/tbUser")
@RefreshScope
public class TbUserController {
@Value("${server.port}")
private String port;
@Value("${record.num}")
private int recordNum;
// 省略其他代码
}
在接口响应中输出配置:
java
@GetMapping("/{id}")
public String getOne(@PathVariable Long id) {
TbUser user = tbUserService.getById(id);
return user + System.lineSeparator()
+ port + System.lineSeparator()
+ recordNum;
}
作用说明:
@Value("${record.num}")从 Spring Environment 中读取record.num。- 该属性来自 Nacos 中的
spring-cloud-user-dev.yml。 @RefreshScope允许这个 Bean 在远端配置变化后重新创建,从而读取新值。- 没有
@RefreshScope时,普通@Value字段通常只在 Bean 创建时赋值,远端修改后不一定动态更新。
如果配置项较多,推荐使用 @ConfigurationProperties 将同一前缀的配置集中映射到一个类,而不是声明大量 @Value。
35.7 构建并启动
修改依赖和配置后必须重新打包、重建镜像:
powershell
mvn package -DskipTests
docker compose up -d --build user-1 user-2
docker compose ps -a
查看用户服务日志:
powershell
docker compose logs --tail=150 user-1 user-2
重点检查:
- 两个应用是否成功启动。
- 是否成功连接
nacos:8848。 - 是否存在 Data ID 找不到、YAML 格式错误或
record.num无法绑定等异常。
35.8 验证远端配置读取
分别直接访问两个实例:
text
http://localhost:8081/tbUser/1
http://localhost:8083/tbUser/1
也可以通过 Gateway:
text
http://localhost:8222/tbUser/1
响应中应该包含:
text
用户信息
8081 或 8083
100
其中:
- 8081 或 8083 证明请求落到了哪个用户实例。
- 100 证明
record.num已从 Nacos 远端配置读取成功。
35.9 验证动态刷新
在 Nacos 控制台打开:
text
spring-cloud-user-dev.yml
把:
yaml
record:
num: 100
修改为:
yaml
record:
num: 200
点击发布,然后重新访问:
text
http://localhost:8222/tbUser/1
如果响应从 100 变成 200,并且没有重新打包或重启容器,说明:
text
Nacos 发布配置
→ 用户服务收到配置变更
→ @RefreshScope Bean 重新创建
→ @Value 读取新值
→ 接口返回 200
由于 user-1 和 user-2 都导入同一个 Data ID,一次发布会作用于两个用户服务实例。通过 Gateway 多次访问时,两个实例都应该逐步返回新值。
35.10 配置优先级应如何理解
本项目中的配置来源包括:
text
application.yml
application-compose.yml
Nacos 远端配置
Compose 环境变量
命令行参数
如果同一个属性在多个位置重复出现,最终值由 Spring Boot 配置优先级决定。实践中建议减少重复定义:
- 本地公共配置放
application.yml。 - 容器连接地址和 Nacos import 放
application-compose.yml。 - 需要集中管理或动态调整的业务配置放 Nacos。
- 每个实例不同的配置,例如
SERVER_PORT,放 Compose 环境变量。 - 密码等敏感信息不要直接写进公开仓库。
当前项目的合理分工是:
text
application.yml:服务名、本地开发地址
application-compose.yml:Nacos 地址、Sentinel 地址、配置导入声明
Nacos:数据源地址、record.num 等集中配置
Compose 环境变量:两个实例各自的 SERVER_PORT
35.11 常见错误
启动时报缺少 spring.config.import
检查是否配置:
yaml
spring:
config:
import:
- nacos:spring-cloud-user-dev.yml
如果某个环境明确不使用配置中心,可以关闭 import 检查或使用 optional:nacos:。
找不到 Data ID
检查以下内容是否完全一致:
text
Nacos Data ID:spring-cloud-user-dev.yml
导入名称:nacos:spring-cloud-user-dev.yml
Group:DEFAULT_GROUP
Data ID 的大小写、后缀和字符必须一致。
容器无法连接配置中心
容器内必须使用:
yaml
server-addr: nacos:8848
不能使用 localhost:8848,因为容器中的 localhost 是当前用户服务容器自己。
Could not resolve placeholder record.num
表示 Spring Environment 中不存在 record.num。检查:
- 远端配置是否已经发布。
- Data ID 和 Group 是否正确。
- YAML 缩进是否正确。
- 用户服务是否成功导入远端配置。
如果希望属性缺失时使用默认值,可以写:
java
@Value("${record.num:100}")
private int recordNum;
修改 Nacos 后接口没有变化
检查:
- 使用该属性的 Bean 是否标注
@RefreshScope。 - 修改后是否点击了"发布",而不只是保存在编辑框。
- 用户服务日志中是否收到配置变更。
- 请求是否落到另一个尚未更新的实例。
- 修改的是否是正确命名空间和 Group 下的 Data ID。
远端数据库地址导致本地运行失败
远端配置当前使用:
text
mysql:3306
这个地址只适用于 Compose 网络。IDEA 本地运行应使用:
text
localhost:3307
因此,应通过 profile、不同 Data ID 或环境变量区分本地与容器环境,不要让本地运行错误地加载容器专用数据库地址。
35.12 加入配置中心后的完整启动流程
text
user-service 容器启动
↓
读取 application.yml
↓
激活 compose profile
↓
读取 application-compose.yml
↓
连接 Nacos Config
↓
导入 spring-cloud-user-dev.yml
↓
创建数据源和 @RefreshScope Bean
↓
把 user-service 实例注册到 Nacos Discovery
↓
连接 Sentinel Dashboard
↓
开始处理 Gateway 和 Feign 请求
运行期间的动态更新流程:
text
在 Nacos 修改 record.num 并发布
↓
Nacos 通知 user-1 和 user-2
↓
两个实例更新 Spring Environment
↓
@RefreshScope 重新创建 Controller Bean
↓
新请求读取新的 record.num
至此,Nacos 在项目中同时承担了两种职责:用 Discovery 管理服务实例,用 Config 集中管理应用配置。