【SpringCloud】Eureka的使用

发布于:2025-03-29 ⋅ 阅读:(29) ⋅ 点赞:(0)

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 关键部分解析:

  1. <modules>
    定义子模块(当前为空),用于多模块项目管理。所有子模块需在此声明,Maven 会按顺序构建。
  2. <parent>
    继承 Spring Boot 的默认配置(如插件、依赖管理),版本为 3.1.6,简化项目配置。
  3. <properties>
    统一定义变量,便于维护版本:
    • Java 版本:17
    • Spring Cloud:2022.0.3
  4. <dependencies>
    当前仅引入 Lombok(代码简化工具),<optional>true</optional> 表示不传递依赖给子模块。
  5. <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 目录下创建厨师子模块和服务员子模块。

### 3.3.1 cook-service 模块配置代码

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 错误。所以现在你理解了服务名的重要性了吗?