本文为 https://windmt.com/2018/04/15/spring-cloud-3-service-producer-and-consumer/ 的学习笔记。
本文中,介绍如何使用 Eureka 服务注册中心,搭建一个简单的服务端注册服务,客户端去调用服务使用的案例。
案例中有三个角色:服务注册中心、服务提供者、服务消费者,其中服务注册中心的建立可参考上一篇学习笔记 :Spring Cloud:服务注册与发现 的 Eureka 单节点启动即可。
流程如下:
本例中,服务提供者有一个 hello()
方法,根据传入参数,提供输出 “hello xxx + 当前时间” 的服务。
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>eureka-producerartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>eureka-producername>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
其中比较重要的配置为:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
spring:
application:
name: eureka-producer
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
instance:
hostname: localhost
server:
port: 8000
spring.application.name
:指定微服务的名称,后续在调用的时候只需要使用改名称就可以进行服务的访问。eureka.client.service-url
:指定服务注册中心的位置。eureka.instance.hostname
:指定 host,因为是本机测试,所以设置为 localhost,否则默认为计算机名。server.port
:为了区别服务提供方和服务注册中心,使用不同的端口。package com.example.demo.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EurekaProducerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaProducerApplication.class, args);
}
}
package com.example.demo.controller;
import java.util.Date;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/")
public String hello(@RequestParam String name) {
return "Hello, " + name + " " + new Date();
}
}
Hello, windmt Tue Feb 18 14:27:55 CST 2020
至此,服务提供者配置已经完成。
创建服务消费者,根据使用 API 的不同,大致分为三种:LoadBalancerClient、Spring Cloud Ribbon、Spring Cloud Feign。
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>eureka-consumerartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>eureka-consumername>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
spring:
application:
name: eureka-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/ # 指定 Eureka 注册中心的地址
instance:
hostname: localhost
server:
port: 9000 # 分别为 9000、9001、9002
RestTemplate
,用来发起 REST 请求。package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class EurekaConsumerApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(EurekaConsumerApplication.class, args);
}
}
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
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.client.RestTemplate;
@RequestMapping("/hello")
@RestController
public class HelloController {
@Autowired
private LoadBalancerClient client;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/")
public String hello(@RequestParam String name) {
name += "!";
ServiceInstance instance = client.choose("eureka-producer");
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/hello/?name=" + name;
return restTemplate.getForObject(url, String.class);
}
}
可以看到这里,我们注入了 LoadBalancerClient 和 RestTemplate,并在 hello 方法中,先通过 loadBalancerClient 的 choose 方法来负载均衡的选出一个 eureka-producer 的服务实例,这个服务实例的基本信息存储在 ServiceInstance 中,然后通过这些对象中的信息拼接出访问服务调用者的 /hello/ 接口的详细地址,最后再利用 RestTemplate 对象实现对服务提供者接口的调用。
另外,为了在调用时能从返回结果上与服务提供者有个区分,在这里我简单处理了一下,name+="!",即服务调用者的 response 中会比服务提供者的多一个感叹号(!)。
启动工程后,就可以在注册中心 http://localhost:7000/的页面看到 EUREKA-CONSUMER 服务:
访问 http://localhost:9000/hello/?name=windmt,验证是否调用成功:
Hello, windmt! Thu Feb 20 16:47:50 CST 2020
Spring Cloud Ribborn 是一个基于 HTTP 和 TCP 的客户端负载均衡器。它可以通过在客户端中配置 ribbonServerList 来设置服务端列表去轮询访问以达到均衡负载的作用。
当 Ribbon 与 Eureka 联合使用时,ribbonServerList 会被 DiscoveryEnabledNIWSServerList 重写,扩展成从 Eureka 注册中心中获取服务实例列表。同时它也会用 NIWSDiscoveryPing 来取代 IPing,它将职责委托给 Eureka 来确定服务端是否已经启动。
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>eureka-consumerartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>eureka-consumername>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
至于spring-cloud-starter-ribbon,因为我使用的 Spring Cloud 版本是 Hoxton.SR1,spring-cloud-starter-netflix-eureka-client 里边已经包含了 spring-cloud-starter-netflix-ribbon 了,无需在 pom.xml 文件中额外添加依赖。
spring:
application:
name: eureka-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/ # 指定 Eureka 注册中心的地址
instance:
hostname: localhost
server:
port: 9001
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
@SpringBootApplication
public class EurekaConsumerRibbonApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(EurekaConsumerRibbonApplication.class, args);
}
}
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
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.client.RestTemplate;
@RequestMapping("/hello")
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/")
public String hello(@RequestParam String name) {
name += "!";
String url = "http://eureka-producer/hello/?name=" + name;
return restTemplate.getForObject(url, String.class);
}
}
这里直接用服务名 eureka-producer
取代之前的具体的 host:port
。
Spring Cloud Ribbon 有一个拦截器,它能够在这里进行实际调用的时候,自动的去选取服务实例,并将这里的服务名替换成实际要请求的 IP 地址和端口,从而完成服务接口的调用。
访问 http://localhost:9001/hello/?name=windmt 以验证是否调用成功:
Hello, windmt! Fri Feb 21 15:16:52 CST 2020
也可以通过启动多个 eureka-producer 服务来观察其负载均衡的效果。
创建一个 Spring boot 工程
打开 File -> New -> Project,选择 Spring Starter Project,点击 Next。
name 设置为 eureka-consumer-feign,点击 Next。
选择 Eureka Discovery Client、Spring Web 和 OpenFeign,点击 Finish。
自动生成的 pom.xml 文件内容如下:
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>eureka-consumer-feignartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>eureka-consumer-feignname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
与前面的几个工程项目相比,额外添加了以下依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
spring:
application:
name: eureka-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/ # 指定 Eureka 注册中心的地址
instance:
hostname: localhost
server:
port: 9002
@EnableFeignClients
:package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class EurekaConsumerFeignApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaConsumerFeignApplication.class, args);
}
}
@FeighClient
注解来指定这个接口所要调用的服务名称:package com.example.demo.controller;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "eureka-producer")
public interface HelloRemote {
@GetMapping("/hello/")
String hello(@RequestParam(value = "name") String name);
}
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/hello")
@RestController
public class HelloController {
@Autowired
HelloRemote helloRemote;
@GetMapping("/{name}")
public String index(@PathVariable("name") String name) {
return helloRemote.hello(name + "!");
}
}
访问 http://localhost:9002/hello/windmt 以验证是否调用成功
Hello, windmt! Fri Feb 21 23:44:09 CST 2020
以上三种方式都能实现负载均衡,都是以轮询访问的方式实现的。这个以大家常用的 Feign 的方式做一个测试。
以上面 eureka-producer 为例子修改,将其中的 controller 改动如下:
package com.example.demo.controller;
import java.util.Date;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;
@RestController
@RequestMapping("/hello")
public class HelloController {
@Value("${config.producer.instance:0}")
private int instance;
@GetMapping("/")
public String hello(@RequestParam String name) {
return "[" + instance + "]" + "Hello, " + name + " " + new Date();
}
}
右键项目,依次执行 Run As -> Maven clean 和 Run As -> Maven install,生成 jar 包。
打开 cmd,切换到工程目录下,如 D:\eclipse-workspace\eureka-producer,依次执行下面命令:
// 分别以 8000 和 8001 启动实例 1 和 实例 2
java -jar target/eureka-producer-0.0.1-SNAPSHOT.jar --config.producer.instance=1 --server.port=8000
java -jar target/eureka-producer-0.0.1-SNAPSHOT.jar --config.producer.instance=2 --server.port=8001
也可以无需导出工程,直接在 Eclipse 中启动:
右键工程,选择 Run As -> Run Configurations。
点击新建,设置 Name 为 eureka-producer - 1,Project 为 eureka-producer,Main Type 为 com.example.demo.EurekaProducerApplication。
选择 Arguments 标签,Program arguments 设置为--config.producer.instance=1 --server.port=8000
,点击 Run。
eureka-producer - 2 同样设置,最后 Program arguments 设置为--config.producer.instance=2 --server.port=8001
,点击 Run。
此时两实例已启动完成,然后启动 eureka-consumer-feign,访问 http://localhost:9002/hello/windmt,不断测试,发现以下结果交替出现:
[1]Hello, windmt! Sat Feb 22 10:46:18 CST 2020
[2]Hello, windmt! Sat Feb 22 10:46:35 CST 2020
[1]Hello, windmt! Sat Feb 22 10:46:42 CST 2020
[1]Hello, windmt! Sat Feb 22 10:46:50 CST 2020
这说明两个服务中心自动提供了服务均衡负载的功能。如果我们将服务提供者的数量在提高为 N 个,测试结果一样,请求会自动轮询到每个服务端来处理。
本文的示例代码:下载