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: localhostclient:# 表示是否從Eureka Server獲取注冊信息,默認為true.因為這是一個單點的Eureka Server,# 不需要同步其他的Eureka Server節點的數據,這里設置為falsefetch-registry: false# 表示是否將自己注冊到Eureka Server,默認為true.由于當前應用就是Eureka Server,故而設置為false.register-with-eureka: falseservice-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 {@Autowiredprivate 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 {@Beanpublic 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 {@Autowiredprivate DiscoveryClient discoveryClient;@Autowiredprivate 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 {@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate 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 錯誤。所以現在你理解了服務名的重要性了嗎?