3. Eureka
3.1 Eureka 介绍
Eureka主要分为两个部分:
EurekaServer:
作为注册中心Server端,向微服务应用程序提供服务注册,发现,健康检查等能力。
EurekaClient:
服务提供者,服务启动时,会向 EurekaServer 注册自己的信息 (IP,端口,服务信息
等),Eureka Server 会存储这些信息。
3.2 搭建 Eureka 服务
Eureka 是一个单独的服务,所以咱们要手动搭建出 Eureka 服务器。
这里使用父子项目来搭建 Eureka 服务,先创建一个 spring-cloud-demo 的父项目:
后续所有的项目创建都在这个父项目中创建子项目。
直接复制下面 spring-cloud-demo 的 pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<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>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>${mybatis.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
pom.xml 关键部分解析:
<modules>
定义子模块(当前为空),用于多模块项目管理。所有子模块需在此声明,Maven 会按顺序构建。<parent>
继承 Spring Boot 的默认配置(如插件、依赖管理),版本为3.1.6
,简化项目配置。<properties>
统一定义变量,便于维护版本:- Java 版本:17
- Spring Cloud:
2022.0.3
<dependencies>
当前仅引入 Lombok(代码简化工具),<optional>true</optional>
表示不传递依赖给子模块。<dependencyManagement>
统一管理依赖版本,子模块引用时无需指定版本:- 引入 Spring Cloud 全家桶的版本控制
- 锁定 MyBatis 和 MySQL 驱动版本
- 测试用 MyBatis 依赖(仅测试范围生效)
记得删除掉父工程的 src 目录,因为后续不会在父工程中写代码。
有了上述操作,咱们的父工程就创建好了,接下来开始创建子工程。
下面开始在父工程底下创建一个子模块,也就是用这个子模块去搭建 Eureka 服务:
创建子模块项目名为 eureka-server:
此时点击 Create 后,就会发现 spring-cloud-demo 目录下出现了 eureka-server 子项目,同时在父项目的 pom.xml 中的 modules 里出现了咱们子模块的名字
在子项目 eureka-server 的 pom.xml <dependencies> </dependencies>
中引入 eureka-server 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
在子项目 eureka-server 的 pom.xml <project> </project>
加入项目构件插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
记得点击右上角 maven 的刷新,或者打开 maven 面板点击 Reload ALL Maven Project 按钮重新加载下依赖。
由于创建的子项目是一个空的项目,所以需要手动创建好对应的包和子模块的启动类:

EurekaServerApplication.class 代码如下:
package com.zlcode.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
编写配置文件,这里需要手动在 resoureces 中创建该项目的配置文件 papplication.yml
# Eureka相关配置
# Eureka 服务
server:
port: 10010
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# 表示是否从Eureka Server获取注册信息,默认为true.因为这是一个单点的Eureka Server,
# 不需要同步其他的Eureka Server节点的数据,这里设置为false
fetch-registry: false
# 表示是否将自己注册到Eureka Server,默认为true.由于当前应用就是Eureka Server,故而设置为false.
register-with-eureka: false
service-url:
# 设置Eureka Server的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
有了上述的操作,就能执行 eureka-server 项目中的 main 方法了。
在浏览器中输入 http://127.0.0.01:10010 就能发现此时 eureka-server 已经成功启动了。
3.3 创建cook服务和waiter服务
接下来再来创建两个子项目,分别是 厨师(cook) 和 服务员(waiter),厨师需要让服务员上菜。
创建厨师服务和服务员服务跟创建 Eureka 项目一致,创建的流程这里就省略了,也就是在 spring-cloud-demo 目录下创建厨师子模块和服务员子模块。

cook-service 的 pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cook-service</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
cook-service 的 application.yml 文件如下:
server:
port: 8080
spring:
application:
name: cook-service # 添加服务器名称
#Eureka Client
eureka:
client:
service-url:
# eureka 地址
defaultZone: http://127.0.0.1:10010/eureka/
cook-service 的 启动类 文件如下:
package com.zlcode.cook;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CookServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CookServiceApplication.class, args);
}
}
3.3.2 waiter-service 模块配置代码
waiter-service 的 pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>waiter-service</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
waiter-service 的 application.yml 文件如下:
server:
port: 9090
spring:
application:
name: waiter-service # 添加服务器名称
#Eureka Client
eureka:
client:
service-url:
# eureka 地址
defaultZone: http://127.0.0.1:10010/eureka/
waiter-service 的 启动类 文件如下:
package com.zlcode.waiter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WaiterServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
}
3.3.3 cook 实体类
将 CookInfo 创建在 cookie-service/*/cook/model/目录下:
package com.zlcode.cook.model;
import lombok.Data;
@Data
public class CookInfo {
private Integer cookId;
private String cookieName;
}
3.3.4 waiter 实体类
将 WaiterInfo 创建在 waiter-service/*/waiter/model/目录下:
package com.zlcode.waiter.model;
import lombok.Data;
@Data
public class WaiterInfo {
private Integer waiterId;
private String waiterName;
}
3.4 服务注册
启动咱们的 waiter-service 和 cook-service 项目,就能自动的进行 eureka 的服务注册了,因为咱们在 .yml 已经配置好了 eureka 的地址。
启动 waiter 和 cook 后,刷新 http://127.0.0.1:10010 就能发现:
通过上面可以看到,已经把 cook-service 和 waiter-service 注册到咱们部搭建的 eureka-server 服务中了。
上述这样的操作,咱们就称作为服务注册。
由于 cook-service 想通过 eureka-server 去发现 waiter-service 所以,也要让 cook-service 自己在 eureka 中注册,这样一来 cook-service 就可以从 eureka-server 中去发现 waiter-service 服务。
3.5 服务发现
前面咱们说,厨师想通过喊服务员名字来让服务员上菜,但是呢它并不知道当前哪些服务员是能提供服务的,于是便需要向 eureka-server 去获取可用服务列表,于是厨师就不需要关注服务员叫什么了。
咱们对 cook-service 和 waiter-service 分别创建一个 controller 包,在 cook 的 controller 包中新建 CookController类,在 waiter 的 controller 中新建 WaiterController 类,此处咱们就不新建 Service 层 和 Mapper 层了,这里只是为了学习服务发现,所以使用一层演示就足够了。
对于 waiter 服务来说,就提供一个接口,这个接口就是执行一个模拟的任务:
package com.zlcode.waiter.controller;
import jakarta.websocket.server.PathParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/waiter")
@Slf4j
public class WaiterController {
@RequestMapping("/up")
public String up(@PathParam("content") String content) {
log.info("正在执行: " + content);
return "执行: " + content + "成功!";
}
}
上述 waiter 就提供了 /waiter/up 这个接口,比如传递的 content 为 “给55桌上红烧肉”,这个请求就会打印一下这个content,然后再模拟返回结果。下面咱们来使用 postman 测试这个接口的可用性:
这里可用发现 waiter 服务的 up 接口成功返回了预期的值,同时在 waiter 的控制台也能看到:
调用 waiter 的 up 方法后,成功的模拟执行了上菜操作!
接下来咱们看一下 cook 提供了哪个接口:
package com.zlcode.cook.controller;
import jakarta.websocket.server.PathParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/cook")
public class CookController {
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/ok")
public String ok(@PathParam("content")String content) {
// 通过 discoveryClient 从 eureka-server 获取服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("waiter-service");
String uri = instances.get(0).getUri().toString(); // 拿到服务列表的第一个 ip:端口号
// ----------------------------------------------------------
// 这里咱们拿到了 waiter 的 ip 地址和端口号
// 也就是知道了服务员的名字,那么要如何告诉服务员你需要让他上菜呢?
// 当然现实中可用喊一嗓子,但在代码中呢?
// ---------------------------------------------------------
return "";
}
}
此时这里就可用通过 discoveryClient.getInstances 实例列表,此处是获取名字为 waiter-service 的服务列表,获取后,通过 instances.get(0).getUri().toString(); 获取这个实例的 ip 和 端口号。
为什么这里可能会有多个实例呢?别忘记了,把同一个项目换成不同的端口号,分别运行,这就是两个服务,同时这两个服务都进行服务注册,此时 eureka 上就会有两个这样的服务了,只是对应的端口号不同,如果端口号相同,但是ip不同,也就是在不同的主机上,这样也是 ok 的。
但是这里只启动了一个 waiter-service 服务,所以咱们取第 0 个就ok了。
ip 和端口号是拿到了,可是如何调用 waiter-service 服务提供的 up 接口呢?
其实也很简单,既然都拿到ip端口号了,直接使用 ip+端口号/waiter/up?content=“xxx” 这个 url 给 waiter 发一个 http 请求就ok了,但是使用 js 发 http 请求相信都会,但是在 Java 中如何给发送 http 请求呢?
这里可用使用 Spring 提供的 RestTemplate 类,通过这个可用发送一个 http 请求,但是注意了!!!
想认识 RestTemplate 就得知道什么是 Rest。
REST(Representational State Transfer),表现层资源状态转移。
简单来说:REST描述的是在网络中Client和Server的⼀种交互形式,REST本身不实用,实用的是如何设计RESTfulAPI(REST风格的网络接口)接口类似于:
GET/blog/{blogId}:查询博客
DELETE/blog/{blogId}:删除博客
而 RestTemplate 是Spring提供,封装HTTP调⽤,并强制使用RESTful风格。它会处理HTTP连接和关闭,只需要使用者提供资源的地地和参数即可。
所以要想使用这个 RestTemplate 进行 http 请求,先得把咱们 waiter 服务的 up 接口改成 Rest 风格的接口:
package com.zlcode.waiter.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/waiter")
@Slf4j
public class WaiterController {
@RequestMapping("/up/{content}")
public String up(@PathVariable("content") String content) {
log.info("正在执行: " + content);
return "执行: " + content + "成功!";
}
}
接下来在 cook-service 项目中先定义 RestTemplate,把这个对象交给 Spring 容器管理,在 cook 目录下创建 config 目录,在这个目录下创建一个 BeanConfig 类,在这个类中定义好咱们要用的 RestTemplate 就 OK 了。
package com.zlcode.cook.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class BeanConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
接下来修改 CookConroller 的 ok 接口:
package com.zlcode.cook.controller;
import jakarta.websocket.server.PathParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@RequestMapping("/cook")
public class CookController {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
// 咱们的 cook 里面的接口可以不遵循 Rest 规范,
// 只要保证使用 restTemplate 调用的接口遵守 Rest 规范就ok了
@RequestMapping("/ok")
public String ok(@PathParam("content")String content) {
// 通过 discoveryClient 从 eureka-server 获取服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("waiter-service");
String uri = instances.get(0).getUri().toString(); // 拿到服务列表的第一个 ip:端口号
// 通过 restTemplate 给 waiter-service 发送 http 请求
String url = uri + "/waiter/up/{content}";
// 第一个参数是请求 url,
// 第二个参数是这个请求的返回值类型的class对象,
// 第三个参数是占位符对应的值
String resp = restTemplate.getForObject(url, String.class, content);
return "调用成功, 已收到 waiter 的响应: " + resp;
}
}
此时咱们此处的代码,就是 cook 厨师,通过 eureka 注册中心获取到服务列表,拿到第一个服务的 ip和端口,通过这个 ip 和 端口 拼接上完整的接口路由,带上参数,就实现了远程方法调用了。
此处如果服务员离职了,来了个新的服务员,对于厨师来说,没有任何影响,厨师只需要关注注册中心有哪些服务列表就行了。
3.6 服务注册和发现的注意点
List<ServiceInstance> instances = discoveryClient.getInstances("waiter-service");
String uri = instances.get(0).getUri().toString(); // 拿到服务列表的第一个 ip:端口号
String url = uri + "/waiter/up/{content}";
String resp = restTemplate.getForObject(url, String.class, content);
上面代码是服务发现和远程调用的重要代码。
上面的 discoveryClient.getInstances(“waiter-service”) 这里方法的形参,waiter-service 就像是给这个服务取了一个名字,比如传菜的都叫做服务员,只要名字叫 waiter-service 的服务,都能获取到,那么注册中心是如何知道每个服务的名字呢?
观察 waiter-service 的 spring.application.name 的值是 waiter-server,注册中心就是通过这个 name 来区分的,所以也就是有可能出现如下的情况:
服务A 的名字:waiter-service 这个服务只提供了 test 接口
服务B 的名字:waiter-service 这个服务只提供了 hello 接口
由于服务 AB 启动时都会进行服务注册,那么问题来了,通过discoveryClient.getInstances(“waiter-service”) 会拿到两个实例,也就是可以获取两个服务的 ip 和 端口号,假设 服务C 想调用 test 接口,而服务C 只知道 name 为 waiter-service 的服务提供了 test 接口,但是服务C不知道的是:AB都叫 test-service,但是服务B没有提供 test 接口。
如果此时 C 通过 instances.get(0).getUri().toString();,拿到的是服务B的ip和端口,此时再通过 restTemplate 进行调用 test 接口,那问题就大了!!
也就是说,如果统称为服务员,那么必须都得具备传菜的接口服务。
所以对于这里的 spring.application.name 的值,要保证相同的 name 它所提供的服务(接口)要是一致的!
现在咱们就来复现上面这种情况,先创建 B/hello 和 A/test 接口,并且这两个服务都叫做 waiter-service。
AController:
package com.zlcode.waiter.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AController {
@RequestMapping("test")
public String test () {
return "成功调用了 服务A 的 test 接口";
}
}
BController:
package com.zlcode.waiter.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BController {
@RequestMapping("/hello")
public String hello() {
return "成功调用了 服务B 的 hello 接口";
}
}
启动这两个服务A和服务B,服务A的端口为9091,服务B的端口为9092,此时可以发现 服务A 只提供了 test 接口,服务B 只提供了 hello 接口。
此时启动服务A和服务B,都发现已经成功使用 waiter-service 这个名字进行服务注册了。
然后使用服务C去获取 waiter-service 对应的服务列表,调用下这两个服务的 test 接口:
package com.zlcode.cook.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/C")
public class CController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/run")
public void run() {
List<ServiceInstance> instances = discoveryClient.getInstances("waiter-service");
for (ServiceInstance instance : instances) {
String uri = instance.getUri().toString();
log.info("获取到服务器地址:" + uri);
String url = uri + "/test";
try {
String resp = restTemplate.getForObject(url, String.class);
log.info(uri + " 调用服务器 test 接口成功! resp: " + resp);
} catch (Exception e) {
e.printStackTrace();
log.error("服务器: " + uri + "没有提供 test 接口.");
}
}
}
}
上述就是服务C,咱们给它运行在 8080 端口上,然后调用 127.0.0.1/C/run 这个接口后,他就会从注册中心获取 waiter-service 服务信息,此时能拿到两个,9091 端口是提供了 test 接口服务的,而 9092 则没有提供 test 接口服务。
在 postman 调用 127.0.0.1/C/run 观察项目打印日志:
果然不出所料,没有提供 test 接口的服务B就会报 404 错误。所以现在你理解了服务名的重要性了吗?