SpringCloud入门最让人头疼的不是概念,而是"从哪下手"。搜了一圈资料,要么直接丢一堆理论术语,要么上来就给一个几百个文件的微服务项目,新手看三分钟就关了。这篇我换个思路,只做一件事:从零手把手搭一个能跑起来的SpringCloud demo。服务注册、服务发现、远程调用这些核心流程全部走通,代码量控制在最小范围,全程用最主流的组件,不整花活。
这套demo是给谁准备的?刚接触SpringCloud的Java后端开发,或者在公司里被分到微服务改造任务但还没上手过的朋友。看完之后你能得到一个本地可运行的完整工程骨架,并且理解每一个配置和每一行关键代码在干什么,而不是复制粘贴完跑通就完事。
1. SpringCloud到底是什么,先搞清我们要搭的这套东西
很多人在第一步就被绕晕,因为SpringCloud不是一个独立的技术,而是一整套微服务解决方案的集合。咱们不背官方定义,用大白话拆一下。
1.1 一个服务拆成多个之后,真正棘手的问题是什么
原来写单体应用,所有接口都在一个项目里,一个类调用另一个类的方法,直接new或者Spring注入就行。一旦拆成多个独立部署的服务,问题立刻出现:
- 调用方怎么知道被调用的服务部署在哪台机器、哪个端口?
- 一个服务起了多个实例,调用方该请求哪个?万一挂了一个怎么办?
- 服务之间的接口地址变了,难道要改代码重新发布吗?
这些问题的答案,就是SpringCloud里面那一堆组件各自要干的活。说白了,注册中心解决"服务在哪",负载均衡解决"请求发给谁",远程调用组件解决"怎么方便地调别人的接口",网关解决"所有请求统一从哪里进"。
1.2 本教程用到的核心组件和最小集合
网上动不动就列SpringCloud十个八个组件,什么配置中心、消息总线、链路追踪全上阵,对入门demo来说完全没必要。搭最小可用闭环,只需要三个东西:
- Nacos:既当注册中心,又当配置中心。用注册中心功能就够了。
- 服务提供者:真实提供业务接口的服务,本文里就叫order-service。
- 服务消费者:调用别人接口的服务,本文里叫consumer-service。
网关、熔断、分布式事务这些,等把这条主链路彻底跑懂再学,事半功倍。这就好比学开车先学会启动、换挡、刹车,你非要把漂移技巧一起学,反而什么都学不会。
需要说明的是,本文用的服务间调用方式是OpenFeign,它是当前SpringCloud生态最主流的声明式HTTP客户端。后面细讲。
2. 环境准备和版本选型:入门最大的坑其实在这里
SpringCloud版本兼容问题是个老大难,我见过太多人demo跑不起来,最后发现是Spring Boot版本和Spring Cloud版本对不上,或者Spring Cloud Alibaba和Spring Cloud版本不匹配。先把版本搞定,后面全顺畅。
2.1 一套经过验证的稳定版本组合
SpringCloud的版本命名用的是伦敦地铁站名,比如Hoxton、2020.0、2021.0这种。Spring Cloud Alibaba则单独维护了一套版本,它负责把Nacos等阿里中间件集成到SpringCloud体系里。
我本地实测可用的组合如下:
| 组件 | 版本 |
|---|---|
| JDK | 1.8(预算充足上17也行,但1.8最稳) |
| Spring Boot | 2.7.12 |
| Spring Cloud | 2021.0.5 |
| Spring Cloud Alibaba | 2021.0.5.0 |
| Nacos Server | 2.2.3 |
这套组合经过大量生产项目验证,网上资料也最多,出了问题容易搜到答案。记住一个关键点:Spring Cloud Alibaba和Spring Boot之间不是随便配的,官方有对应的版本关系表。2021.0.5.0这个版本适配Spring Boot 2.7.x,别乱升级。
2.2 Maven依赖都配了哪些东西
三个服务的父工程用的都是同一个依赖管理,核心就两个依赖。第一个是Spring Cloud的BOM,第二个是Spring Cloud Alibaba的BOM。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.5</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>BOM的作用是统一管理一堆子依赖的版本号,我们在子模块里引入依赖时就不需要再写version了,有效避免版本冲突。这一点和Maven的dependencyManagement机制直接相关,不熟悉的可以单独查一下。
2.3 Nacos Server的下载和启动
Nacos Server本身是个独立运行的Java程序,不是嵌在SpringBoot里的。先去GitHub下载对应的release包,下载后解压,目录结构大概是这样:
nacos-server-2.2.3/ ├── bin/ # 启动脚本 ├── conf/ # 配置文件 ├── data/ # 运行数据(首次启动后生成) └── logs/ # 日志目录Windows系统直接在bin目录下双击startup.cmd,Linux/macOS执行startup.sh。但有个坑:Nacos 2.x版本默认以集群方式启动,单机运行必须加参数。
# Linux/macOS sh startup.sh -m standalone # Windows startup.cmd -m standalone启动成功之后,浏览器访问 http://localhost:8848/nacos ,默认账号密码都是nacos,就能看到控制台。看到那个登录页面,整个demo最基础的一步就算完成了。
3. 用Nacos做注册中心:整个demo的地基
为什么要搞注册中心,用个现实场景比喻一下。以前没有外卖平台的时候,你得挨个记饭店的电话号码;有了平台之后,饭店入驻,用户直接在平台上找饭店就行。注册中心就是这个平台,每个服务启动后都去平台上报到,记录自己叫什么名字、在哪台机器哪个端口。
3.1 服务提供者的基础配置
新建一个Spring Boot工程,模块名叫order-service,端口设置为8081。只引入一个和注册相关的依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>这个starter会自动把服务注册到Nacos,并且内置了心跳机制,服务挂了Nacos能感知到。
配置文件application.yml如下:
server: port: 8081 spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848这三项配置缺一不可。spring.application.name决定了服务在注册中心显示的名字,后续远程调用就是通过这个名字来找服务的。server-addr指定Nacos Server的地址。
现在写一个最简单的接口,验证服务是活的:
@RestController @RequestMapping("/order") public class OrderController { @GetMapping("/info") public String info() { return "order-service: 订单服务返回的结果"; } }3.2 从Nacos控制台看服务是否注册成功
启动order-service,然后打开Nacos控制台,在"服务管理"->"服务列表"这一页就能看到order-service已经在列表里了。点进去可以看到这个服务的实例详情,包括IP、端口、健康状态。
这一步是对上面所有配置的第一轮验证。如果控制台里看不到服务,排查顺序是:
- order-service有没有引入nacos-discovery依赖
- spring.application.name是不是没配或者写错
- Nacos Server启动的时候是不是单机模式
- 有没有网络不通或者端口被防火墙拦截的问题
4. 第一个服务提供者:order-service到底做了什么
现在进一步把order-service做成稍微真实一点的样子,不能只是一个空壳。虽然是个demo,但至少要把"接口返回数据"这件事做完整,因为后面消费者要拿这些数据。
4.1 加一个简单的service层和mapper
虽然SpringCloud的重点不在业务代码,但工程结构要有个雏形。我加了一张订单表,一个实体类,一个Mapper接口,一个Service类,一个Controller。表数据直接初始化两条:
CREATE TABLE `order_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(64) DEFAULT NULL, `amount` decimal(10,2) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Controller返回订单列表:
@GetMapping("/list") public List<OrderInfo> list() { return orderService.listAll(); }这部分的代码和平时的单体开发没有任何区别。微服务的拆分是逻辑上的,不是技术上另起炉灶。每个微服务内部就是一个小型单体项目。
4.2 为什么demo里用MySQL而不是H2
很多教程为了省事直接用H2内存数据库,跑完即弃。我的建议是直接用MySQL,因为MySQL才符合国内公司的真实环境。你现在用H2跑通了,到了公司里对接MySQL,可能又得踩一遍连接配置的坑。数据库连接池我用的是Druid,配置如下:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/cloud_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码提示:如果数据库连不上排查耗时最多的问题往往是密码错误和时区配置不对。serverTimezone这一项尤其容易漏,建议直接用Asia/Shanghai,避免默认UTC导致的8小时偏差。
5. 服务消费者:用OpenFeign调用别人的接口
服务提供者搭好了,现在要建第二个服务consumer-service,端口8082。这个服务的工作是:提供一个HTTP接口给前端,然后通过远程调用order-service的数据,组装后返回。
5.1 引入OpenFeign并启用远程调用能力
在consumer-service的pom.xml中引入:
<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>第二个依赖很容易被忽略。Spring Cloud 2021版本之后,OpenFeign不再默认集成Ribbon做负载均衡,必须手动引入spring-cloud-starter-loadbalancer,否则启动后调用服务会直接报503错误。
启动类上别忘了加注解:
@SpringBootApplication @EnableFeignClients public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } }@EnableFeignClients的作用是扫描所有@FeignClient标记的接口,并生成代理对象注入Spring容器。
5.2 用声明式接口代替手写HTTP请求
OpenFeign最爽的地方在于,调用远程服务就像调用本地方法一样。定义一个接口:
@FeignClient(name = "order-service") public interface OrderFeignClient { @GetMapping("/order/list") String listOrders(); }关键是@FeignClient(name = "order-service"),这里的name对应的是order-service在Nacos里注册的服务名。OpenFeign会拿着这个名字去注册中心找到真实的服务地址,然后发起HTTP请求。
调用的时候:
@RestController @RequestMapping("/consumer") public class ConsumerController { @Resource private OrderFeignClient orderFeignClient; @GetMapping("/orders") public String getOrders() { return "消费者收到数据: " + orderFeignClient.listOrders(); } }以前用RestTemplate或HttpClient手动拼接URL、设置headers、处理连接的步骤全部省掉了,声明一个接口就完事,这也是OpenFeign在国内SpringCloud项目中普及率这么高的原因。
6. 启动三个服务,验证整个调用链真的通了
现在到了检验成果的时候。需要启动的东西一共有三个:Nacos Server、order-service、consumer-service。启动顺序有个讲究,先把Nacos启动了,再启动两个业务服务。
6.1 一步一步看整个调用链的通信流程
我用实际测试过程给你走一遍,看到哪一步卡住就在哪里排查:
- 第一步:确认Nacos控制台 http://localhost:8848/nacos 正常访问
- 第二步:启动order-service,等1~2秒让心跳注册成功,在Nacos服务列表看到order-service实例
- 第三步:启动consumer-service,同样在服务列表看到consumer-service实例
- 第四步:在浏览器访问 http://localhost:8082/consumer/orders
如果一切正常,第4步返回的响应大概是:
消费者收到数据: [{"id":1,"orderNo":"ORD-20240601-001","amount":199.00},{"id":2,"orderNo":"ORD-20240601-002","amount":299.00}]这说明consumer-service通过服务发现找到了order-service的真实地址,然后通过OpenFeign完成了一次真实的HTTP调用,数据从order-service的数据库取出来,经过服务间传输,最后返回到了浏览器。一条完整的微服务调用链路就这样通了。
6.2 再多加一个实例,看负载均衡是怎么工作的
上面的链路虽然通了,但只有一个order-service实例,体现不出微服务的价值。你可以把order-service再启动一个实例,端口设置成8083,运行方式可以这么来:
先用jar包方式启动第一个实例:
java -jar order-service-0.0.1-SNAPSHOT.jar --server.port=8081再启动第二个实例:
java -jar order-service-0.0.1-SNAPSHOT.jar --server.port=8083这时候去Nacos控制台看order-service,会发现有两个实例。连续刷新几次consumer-service的接口,可以看到信息交替来自两个实例的日志。这就是负载均衡干的事:同一个服务有多个可用实例时,把请求分摊到不同实例上,避免单个服务压力过大。
7. 快速搭建中最容易踩的坑和排查套路
写这篇教程之前,我在本地把从零搭建的过程完整复现了一遍,就是为了把最常见的坑提前踩一遍。以下这几个问题,基本覆盖了初学者90%以上的报错场景。
7.1 OpenFeign直接报503,多半是缺了loadbalancer依赖
这是2021版本之后最大的变化。很多老教程都是基于Hoxton之前版本写的,那时候Ribbon是默认自带的,OpenFeign可以直接做负载均衡。到了2021.0.x之后,你如果不手动引入spring-cloud-starter-loadbalancer,服务启动不报错,但一旦调用就会报503 Service Unavailable。
排查方法很简单,看报错日志里有没有"Load balancer does not contain an instance for the service"这句话,有的话就是缺依赖,加上就完事。
7.2 Nacos控制台看到一个服务下面出现两个名字相同但端口不同的实例
这其实不是问题,是服务发现正常工作。但有一种情况要注意:如果同一个服务在不同机器上注册,IP不同,你要确认是同一套代码、同一套配置,是分布式部署的多个实例,而不是不同的服务误用了相同的spring.application.name。
7.3 服务注册不上,控制台看不到服务怎么办
优先检查三条:
- Nacos是单机模式启动的吗?2.x默认集群模式,不加-m standalone会报错或者启动完不工作
- spring.cloud.nacos.discovery.server-addr配置的IP和端口对不对?本地用127.0.0.1
- 本地8848端口有没有被其他进程占用?被占了Nacos可能从8849端口启动,服务自然就注册不上
7.4 调用接口时返回JSON格式不正确或乱码
Spring Boot 2.7默认返回的JSON编码是UTF-8,基本不会乱码。如果自己手动拼了String字符串,注意在注解里加produces:
@GetMapping(value = "/list", produces = "application/json;charset=UTF-8")数据库连接串里的characterEncoding=utf8也要检查一遍,这两处是最容易产生中文乱码的源头。
7.5 数据库连接失败的排查顺序
数据库报错通常是最后踩到的坑,因为要走到Consumer调Order才触发。从下面三个方向排查,基本上一找一个准:MySQL服务有没有启动、账号密码和权限对不对、数据库cloud_demo有没有建表。用Navicat先手动连一次,能连上再启动服务。
7.6 依赖下载慢或下载失败怎么处理
SpringCloud和SpringCloud Alibaba的依赖一般都在中央仓库和阿里云仓库里,本地Maven仓库如果之前没有缓存过,首次下载会比较慢。建议在Maven的settings.xml里配置阿里云镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>配好之后,IDEA里重新reimport一下Maven工程,问题基本解决。
7.7 端口冲突:端口被占用怎么快速找到进程
Windows下用netstat命令,Linux用lsof或netstat。Windows示例:
netstat -ano | findstr 8081看到占用进程的PID后,再查是哪个进程:
tasklist | findstr PID号是系统进程或者不认识的程序的,直接杀:
taskkill /F /PID PID号7.8 Nacos 2.x版本还有一点特殊:额外占用9848端口
这一点非常隐蔽,很多人踩了坑都不知道原因。Nacos 2.x除了8848之外,还会额外使用9848端口用于gRPC通信。这个端口是主端口+1000算出来的。如果服务器部署在阿里云、腾讯云上,安全组里只开放了8848,服务注册时会一直失败或者时好时坏,报错信息里通常能看到"Connection refused"。要同时把9848端口也在防火墙和安全组里放开。
7.9 启动时报错找不到主类或者缺少依赖
多半是子模块没有正确引入父工程的依赖,或者Maven打包时没有先把公共模块install到本地仓库。最简单粗暴的解决办法:在根目录执行mvn clean install -DskipTests,把所有模块先构建一遍,再重新启动。这个方法能解决80%的"IDE里能跑,命令行跑不了"问题。
7.10 一个容易漏的点:consumer-service的@SpringBootApplication和@EnableFeignClients扫描范围
如果启动类放在了和FeignClient接口不同的包路径下,就会出现"找不到FeignClient"的情况。@EnableFeignClients默认扫描启动类所在包及子包。所以最省事的做法就是所有代码都放在com.example.consumer这个包下面,不要乱建平行的包路径。
微服务这套体系,拆开看每一个技术其实都是解决某个具体问题的工具。Nacos管服务发现,OpenFeign管服务调用,LoadBalancer管请求分配。你把这个demo跑通之后,再去看网关、配置中心、熔断限流这些内容,思路会清晰得多。我最早学SpringCloud的时候也差点被一大堆概念劝退,后来发现最好的方式就是先把这个最小闭环跑通,从"能跑"到"能说明白每一步在干什么",这个进阶路径既平滑也不容易半途而废。
最后再分享一个小技巧:把order-service的日志级别调成DEBUG,或者直接在Controller里加一条日志输出,这样每次远程调用都能在消费者和提供者两个服务的控制台看到日志对应关系,对整个调用链的理解会加深很多。动手实验永远是学框架最快的路。