Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您完成创建负载均衡微服务的过程。

你将构建的项目

您将构建一个微服务应用程序,该应用程序使用 Spring Cloud LoadBalancer 来在调用另一个微服务时提供客户端负载均衡。

你将需要什么

  • 大约 15 分钟
  • 一个喜欢的文本编辑器或 IDE
  • JDK 1.8 或更高版本
  • Gradle 6+ 或 Maven 3.5+
  • 您也可以直接将代码导入到您的 IDE 中:
  • Spring Tool Suite (STS) 或 IntelliJ IDEA

创建一个根项目

本指南将带您构建两个项目,其中一个项目是另一个项目的依赖项。因此,您需要在根项目下创建两个子项目。首先,在顶层创建构建配置。对于 Maven,您需要一个包含 <modules>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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-spring-cloud-loadbalancer</artifactId>
    <version>0.1.0</version>
    <packaging>pom</packaging>

    <modules>
      <module>say-hello</module>
      <module>user</module>
    </modules>
</project>

对于 Gradle 来说,您需要一个包含相同目录的 settings.gradle 文件:

rootProject.name = 'gs-spring-cloud-loadbalancer'

include 'say-hello'
include 'user'

可选地,您可以包含一个空的 build.gradle(以帮助 IDE 识别根目录)。

创建目录结构

在您希望作为根目录的目录中,创建以下子目录结构(例如,在 *nix 系统上使用 mkdir say-hello user 命令):

└── say-hello
└── user

在项目的根目录中,您需要设置一个构建系统,本指南将向您展示如何使用 Maven 或 Gradle。

从 Spring Initializr 开始

如果您在 Say Hello 项目中使用 Maven,请访问 Spring Initializr 生成一个包含所需依赖(Spring Web)的新项目。

以下代码展示了选择 Maven 时生成的 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 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.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-loadbalancer-say-hello</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-loadbalancer-say-hello</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <spring-boot.repackage.skip>true</spring-boot.repackage.skip>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>

如果您在 Say Hello 项目中使用 Gradle,请访问 Spring Initializr 来生成一个包含所需依赖(Spring Web)的新项目。

以下清单显示了当您选择 Gradle 时生成的 build.gradle 文件:

plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

bootJar {
    enabled = false
}

如果您使用 Maven 来构建 User 项目,请访问 Spring Initializr 以生成一个包含所需依赖项(Cloud Loadbalancer 和 Spring Reactive Web)的新项目。

下面的清单展示了当您选择 Maven 时生成的 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 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.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-loadbalancer-user</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-loadbalancer-user</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <spring-boot.repackage.skip>true</spring-boot.repackage.skip>
        <java.version>17</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</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>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </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>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

</project>

如果您在 User 项目中使用 Gradle,请访问 Spring Initializr 生成一个包含所需依赖项(Cloud Loadbalancer 和 Spring Reactive Web)的新项目。

以下清单展示了在选择 Gradle 时创建的 build.gradle 文件:

plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
}

ext {
    set('springCloudVersion', "2023.0.0")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

test {
    useJUnitPlatform()
}

bootJar {
    enabled = false
}

手动初始化 (可选)

如果您想手动初始化项目,而不是使用前面显示的链接,请按照以下步骤操作:

  1. 访问 https://start.spring.io。该服务会为您拉取应用程序所需的所有依赖项,并完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Spring Web(适用于 Say Hello 项目)或 Cloud LoadbalancerSpring Reactive Web(适用于 User 项目)。

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您选择的配置打包好的 Web 应用程序。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此过程。

实现 "Say Hello" 服务

我们的“server”服务名为Say Hello。它从一个可通过/greeting访问的端点返回随机问候语(从三个静态列表中选出)。

src/main/java/hello目录下,创建文件SayHelloApplication.java

以下清单展示了say-hello/src/main/java/hello/SayHelloApplication.java的内容:

package hello;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class SayHelloApplication {

  private static Logger log = LoggerFactory.getLogger(SayHelloApplication.class);

  public static void main(String[] args) {
    SpringApplication.run(SayHelloApplication.class, args);
  }

  @GetMapping("/greeting")
  public String greet() {
  log.info("Access /greeting");

  List<String> greetings = Arrays.asList("Hi there", "Greetings", "Salutations");
  Random rand = new Random();

  int randomNum = rand.nextInt(greetings.size());
  return greetings.get(randomNum);
  }

  @GetMapping("/")
  public String home() {
  log.info("Access /");
  return "Hi!";
  }
}

这是一个简单的@RestController,其中我们为/greeting路径定义了一个@RequestMapping方法,另一个为根路径/

我们将在本地运行此应用程序的多个实例,并与一个客户端服务应用程序一起运行。开始步骤如下:

  1. 创建一个 src/main/resources 目录。

  2. 在该目录中创建一个 application.yml 文件。

  3. 在该文件中,为 server.port 设置一个默认值。

(我们将指示应用程序的其他实例在其他端口上运行,以便在我们启动客户端时,没有任何 Say Hello 实例与客户端发生冲突)。当我们在这个文件中时,我们还可以为我们的服务设置 spring.application.name

以下清单展示了 say-hello/src/main/resources/application.yml 的内容:

spring:
  application:
    name: say-hello

server:
  port: 8090

从客户端服务访问

我们的用户会看到 User 应用程序。它会调用 Say Hello 应用程序来获取问候语,然后在用户访问 /hi/hello 端点时将该问候语发送给用户。

在 User 应用程序目录下的 src/main/java/hello 中,添加 UserApplication.java 文件:

以下清单展示了 user/src/main/java/hello/UserApplication.java 的内容。

package hello;

import reactor.core.publisher.Mono;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;

/**
 * @author Olga Maciaszek-Sharma
 */
@SpringBootApplication
@RestController
public class UserApplication {

  private final WebClient.Builder loadBalancedWebClientBuilder;
  private final ReactorLoadBalancerExchangeFilterFunction lbFunction;

  public UserApplication(WebClient.Builder webClientBuilder,
      ReactorLoadBalancerExchangeFilterFunction lbFunction) {
    this.loadBalancedWebClientBuilder = webClientBuilder;
    this.lbFunction = lbFunction;
  }

  public static void main(String[] args) {
    SpringApplication.run(UserApplication.class, args);
  }

  @RequestMapping("/hi")
  public Mono<String> hi(@RequestParam(value = "name", defaultValue = "Mary") String name) {
    return loadBalancedWebClientBuilder.build().get().uri("http://say-hello/greeting")
        .retrieve().bodyToMono(String.class)
        .map(greeting -> String.format("%s, %s!", greeting, name));
  }

  @RequestMapping("/hello")
  public Mono<String> hello(@RequestParam(value = "name", defaultValue = "John") String name) {
    return WebClient.builder()
        .filter(lbFunction)
        .build().get().uri("http://say-hello/greeting")
        .retrieve().bodyToMono(String.class)
        .map(greeting -> String.format("%s, %s!", greeting, name));
  }
}

我们还需要一个 @Configuration 类,用于设置一个负载均衡的 WebClient.Builder 实例:

以下清单展示了 user/src/main/java/hello/WebClientConfig.java 文件的内容:

package hello;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class)
public class WebClientConfig {

  @LoadBalanced
  @Bean
  WebClient.Builder webClientBuilder() {
    return WebClient.builder();
  }

}

配置提供了一个@LoadBalanced WebClient.Builder实例,当有人访问UserApplication.java中的hi端点时,我们会使用它。一旦访问了hi端点,我们就使用这个构建器创建一个WebClient实例,该实例向Say Hello服务的URL发出HTTP GET请求,并将结果作为String返回。

UserApplication.java中,我们还添加了一个执行相同操作的/hello端点。然而,我们没有使用@LoadBalanced注解,而是使用了一个通过@Autowired注入的负载均衡交换过滤器函数(lbFunction),我们通过filter()方法将其传递给一个通过编程方式构建的WebClient实例。

尽管我们为两个端点设置的负载均衡 WebClient 实例略有不同,但两者的最终行为完全相同。Spring Cloud LoadBalancer 用于选择 Say Hello 服务的合适实例。

spring.application.nameserver.port 属性添加到 src/main/resources/application.propertiessrc/main/resources/application.yml 中:

以下清单展示了 user/src/main/resources/application.yml 的内容

spring:
  application:
    name: user

server:
  port: 8888

跨服务器实例的负载均衡

现在我们可以访问 User 服务上的 /hihello,并看到友好的问候:

$ curl http://localhost:8888/hi
Greetings, Mary!

$ curl http://localhost:8888/hi?name=Orontes
Salutations, Orontes!

WebClientConfig.java 中,我们通过使用 @LoadBalancerClient 注解传递了 LoadBalancer 的自定义配置:

@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class)

这意味着,每当访问名为 say-hello 的服务时,Spring Cloud LoadBalancer 不会使用默认配置,而是使用 SayHelloConfiguration.java 中提供的配置。

以下清单展示了 user/src/main/java/hello/SayHelloConfiguration.java 的内容:

package hello;

import java.util.Arrays;
import java.util.List;

import reactor.core.publisher.Flux;

import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

/**
 * @author Olga Maciaszek-Sharma
 */
public class SayHelloConfiguration {

  @Bean
  @Primary
  ServiceInstanceListSupplier serviceInstanceListSupplier() {
    return new DemoServiceInstanceListSuppler("say-hello");
  }

}

class DemoServiceInstanceListSuppler implements ServiceInstanceListSupplier {

  private final String serviceId;

  DemoServiceInstanceListSuppler(String serviceId) {
    this.serviceId = serviceId;
  }

  @Override
  public String getServiceId() {
    return serviceId;
  }

  @Override
  public Flux<List<ServiceInstance>> get() {
    return Flux.just(Arrays
        .asList(new DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 8090, false),
            new DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 9092, false),
            new DefaultServiceInstance(serviceId + "3", serviceId, "localhost", 9999, false)));
  }
}

在该类中,我们提供了一个自定义的 ServiceInstanceListSupplier,其中包含三个硬编码的实例,Spring Cloud LoadBalancer 在调用 Say Hello 服务时会从中进行选择。

此步骤已添加,以解释如何将您自己的自定义配置传递给 Spring Cloud LoadBalancer。然而,您不需要使用 @LoadBalancerClient 注解并为 LoadBalancer 创建自己的配置。最典型的方式是将 Spring Cloud LoadBalancer 与服务发现一起使用。如果您的类路径上有任何 DiscoveryClient,默认的 Spring Cloud LoadBalancer 配置将使用它来检查服务实例。因此,您只需从正在运行的实例中进行选择。您可以通过此指南了解如何使用 ServiceDiscovery

我们还添加了一个带有默认 server.portspring.application.nameapplication.yml 文件。

以下清单展示了 user/src/main/resources/application.yml 的内容:

spring:
  application:
    name: user

server:
  port: 8888

测试负载均衡器

以下列表展示了如何使用 Gradle 运行 Say Hello 服务:

$ ./gradlew bootRun

以下清单展示了如何使用 Maven 运行 Say Hello 服务:

$ mvn spring-boot:run

为了实现负载均衡,您需要两台服务器运行相同应用程序的独立实例。您可以通过在不同的端口上运行Say Hello服务的第二个实例来实现这一点。在这个例子中,我们使用端口9999。

要使用Gradle实现这一点,请打开一个新的终端并运行以下命令:

export SERVER_PORT=9092
./gradlew bootRun

要在 Maven 中执行此操作,请打开一个新的终端并运行以下命令:

export SERVER_PORT=9999
mvn spring-boot:run

然后,您可以启动 User 服务。此时,您应该有三个终端:两个用于两个 Say Hello 实例,一个用于 User。接着,您可以访问 localhost:8888/hi 并观察 Say Hello 服务实例的运行情况。

您对 User 服务的请求应该会以轮询方式分散到正在运行的 Say Hello 实例中:

2016-03-09 21:15:28.915  INFO 90046 --- [nio-8090-exec-7] hello.SayHelloApplication                : Access /greeting

摘要

恭喜!您刚刚开发了一个 Spring Loadbalancer 应用程序!

本页目录