Spring Cloud学习笔记26——Spring Cloud 微服务实战

微服务构建:Spring Boot

构建Maven项目

  1. 通过官方的Spring Initializr工具来产生基础项目。
    在这里插入图片描述
  2. 下载并解压生成的项目压缩包,并用IDEMaven项目导入,以Intellij IDEA为例。
    在这里插入图片描述
    在这里插入图片描述
  3. 单击Import project from external model并选择Maven,一直单击Next按钮。
    在这里插入图片描述

实现RESTful API

Spring Boot中创建一个RESTful API的实现代码同Spring MVC应用一样,只是不需要像Spring MVC那样先做很多配置,而是像下面这样直接开始编写Controller内容:

  1. 新建packagecom.study.springcloud.hello.controller
    在这里插入图片描述
  2. 新建HelloController类,内容如下所示。
@RestController
public class HelloController {

	@RequestMapping("/hello")
	public String index(){
		return "Hello World";
	}

}
  1. 启动该应用,通过浏览器访问http;//localhost:8080/hello
    在这里插入图片描述

引入actuator

pom.xmldependency节点中,新增spring-boot-starter-actuator的依赖,如下。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

重新启动应用,有一批端点定义,这些端点并非我们自己在程序中创建的,而是由spring-boot-starter-actuator模块根据应用依赖和配置自动创建出来的监控和管理端点。通过这些端点,我们可以实时获取应用的各项监控指标。这些内容将帮助我们制定更为个性化的监控策略。
在这里插入图片描述

服务治理:Spring Cloud Eureka

搭建服务注册中心

  1. 首先,创建一个基础的Spring Boot工程,命名为eureka-server,并在pom.xml中引入必要的依赖内容,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>com.study.springcloud</groupId>
	<artifactId>eureka-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<name>eureka-server</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>

        <!--Eureka Server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>

        <!--通用测试模块,包含JUnit、Hamcrest、Mockito-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

	</dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</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-snapshot</id>
            <name>Spring Snapshot</name>
            <url>https://repo.spring.io/snapshot</url>
        </repository>
        <repository>
            <id>spring-milestone</id>
            <name>Spring Milestone</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
        <repository>
            <id>aliyun</id>
            <name>Aliyun</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
    </repositories>

</project>
  1. 通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用进行对话。只需在一个普通的Spring Boot应用中添加这个注解即可:
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(EurekaServerApplication.class, args);
	}
}
  1. 在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为,在application.properties中增加如下配置:
#指定服务注册中心的端口,与后续要进行注册的服务区分
server.port=1111

#实例的主机名称
eureka.instance.hostname=localhost

#由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己
eureka.client.register-with-eureka=false

#由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置为false
eureka.client.fetch-registry=false

#服务的URL
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
  1. 启动应用并访问http://localhost:1111/,可以看到如下页面,其中Instances currently registered with Eureka栏是空的,说明该注册中心还没有注册任何服务。
    在这里插入图片描述

注册服务提供者

修改前面的Spring Boot入门项目,将其作为一个微服务应用向服务注册中心发布自己。

  1. 首先,修改pom.xml,增加Spring Cloud Eureka模块的依赖,具体代码如下所示:
<dependencies>

       <!--Eureka-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-eureka</artifactId>
       </dependency>

       <!--全栈Web开发模块,包含嵌入式Tomcat、Spring MVC-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <!--通用测试模块,包含JUnit、Hamcrest、Mockito-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.RC1</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-snapshot</id>
        <name>Spring Snapshot</name>
        <url>https://repo.spring.io/snapshot</url>
    </repository>
    <repository>
        <id>spring-milestone</id>
        <name>Spring Milestone</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
    <repository>
        <id>aliyun</id>
        <name>Aliyun</name>
        <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    </repository>
</repositories>
  1. 接着,改造/hello请求处理接口,通过注入DiscoveryClient对象,在日志中打印出服务的相关内容。
@RestController
public class HelloController {

	private final Logger logger= Logger.getLogger(getClass());

	@Autowired
	private DiscoveryClient client;

	@RequestMapping(value = "/hello",method = RequestMethod.GET)
	public String index(){
		ServiceInstance instance=client.getLocalServiceInstance();
		logger.info("hello, host:"+instance.getHost()+", service_id:"+instance.getServiceId());
		return "Hello World";
	}

}
  1. 然后,在主类中通过加上@EnableDiscoveryClient注解,激活Eureka中的DiscoveryClient实现(自动化配置,创建DiscoveryClient接口针对Eureka客户端的EurekaDiscoveryClient实例),才能实现上述Controller中对服务信息的输出。
@EnableDiscoveryClient
@SpringBootApplication
public class HelloEurekaClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloEurekaClientApplication.class, args);
	}
}
  1. 最后,我们需要在application.properties配置文件中,通过spring.application.name属性来为服务命名。再通过eureka.client.service-url.defaultZone属性来指定服务注册中心的地址,这里我们指定为之前构建的服务注册中心地址,完整配置如下所示:
#为服务命名
spring.application.name=hello-eureka-client

#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
  1. 分别启动服务注册中心以及这里改造后的hello-eureka-client服务。访问Eureka的信息面板。
    在这里插入图片描述
  2. 通过访问http://localhost:8080/hello,直接向该服务发起请求,在控制台中可以看到如下输出:
    在这里插入图片描述
    这些输出内容就是之前我们在HelloController中注入的DiscoveryClient接口对象,从服务注册中心获取的服务相关信息。

高可用注册中心

在前面的服务注册中心的基础之上进行扩展,构建一个双节点的服务注册中心集群。

  1. 创建application-peer1.properties,作为peer1服务中心的配置,并将serviceUrl指向peer2
#为服务命名
spring.application.name=eureka-server

#指定服务注册中心的端口,与后续要进行注册的服务区分
server.port=1111

#实例的主机名称
eureka.instance.hostname=peer1

spring.profiles.active=peer1

#相互注册要开启
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true

#服务的URL
eureka.client.service-url.defaultZone=http://peer2:1112/eureka/
  1. 创建application-peer2.properties,作为peer2服务中心的配置,并将serviceUrl指向peer1
#为服务命名
spring.application.name=eureka-server

#指定服务注册中心的端口,与后续要进行注册的服务区分
server.port=1112

#实例的主机名称
eureka.instance.hostname=peer2

spring.profiles.active=peer2

#相互注册要开启
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true

#服务的URL
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/
  1. /etc/hosts文件中添加对peer1peer2的转换,让上面配置的host形式的serviceUrl能在本地正确访问到;Windows系统路径为:C:\Windows\System32\drivers\etc\hosts
127.0.0.1       peer1
127.0.0.1       peer2
  1. 通过spring.profiles.active属性来分别启动peer1peer2,先用mvn install命令将应用打包成jar包,再通过以下命令来启动应用:
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1

java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
  1. 访问peer1的注册中心http://peer1:1111/,可以看到registered-replicas中已经有peer2节点的eureka-server了,且节点在可用分片(available-replicas)中:
    在这里插入图片描述
  2. 同样地,访问peer2的注册中心http://peer2:1112/
    在这里插入图片描述
  3. peer1关闭,刷新http://peer2:1112/,可以看到peer1的节点变为了不可用分片(unavailable-replicas):
    在这里插入图片描述
  4. 在设置了多节点的服务注册中心之后,服务提供方还需要做一些简单的配置才能将服务注册到Eureka Server集群中。以hello-eureka-client为例,修改application.properties配置文件,将注册中心指向前面搭建的peer1peer2,如下所示:
#为服务命名
spring.application.name=hello-eureka-client

#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
  1. 启动hello-eureka-client,访问http://peer1:1111/http://peer2:1112/,可以看到hello-eureka-client服务同时被注册到了peer1peer2上。若此时断开peer1,由于hello-eureka-client服务同时也向peer2注册,因此在peer2上的其他服务依然能访问到hello-eureka-client服务,从而实现了服务注册中心的高可用:
    在这里插入图片描述
    在这里插入图片描述
  2. 如不想使用主机名来定义注册中心的地址,也可以使用IP地址的形式,但是需要在配置文件中增加配置参数eureka.instance.prefer-ip-address=true,该值默认为false

服务发现与消费

下面来尝试构建一个服务消费者,它主要完成两个目标,发现服务以及消费服务。其中,服务发现的任务由Eureka的客户端完成,而服务消费的任务由Ribbon完成。

下面我们通过构建一个简单的示例,看看在Eureka的服务治理体系下如何实现服务的发现与消费。

  1. 首先,我们做一些准备工作。启动之前实现的服务注册中心eureka-server以及hello-eureka-client服务,为了实验Ribbon的客户端负载均衡功能,我们通过java -jar命令行的方式来启动两个不同端口的hello-eureka-client,具体如下:
java -jar hello-eureka-client-0.0.1-SNAPSHOT.jar --server.port=8081

java -jar hello-eureka-client-0.0.1-SNAPSHOT.jar --server.port=8082
  1. 访问http://localhost:1111/页面:
    在这里插入图片描述
  2. 创建一个Spring Boot的基础工程来实现服务消费者,取名为ribbon-consumer,并在pom.xml中引入如下的依赖内容,即新增了Ribbon模块的依赖spring-cloud-starter-ribbon
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.5.3.RELEASE</version>
	<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>1.8</java.version>
</properties>

<dependencies>

	<!--Eureka-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-eureka</artifactId>
       </dependency>

       <!--Ribbon-->
       <dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-ribbon</artifactId>
	</dependency>

       <!--全栈Web开发模块,包含嵌入式Tomcat、Spring MVC-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

	<!--通用测试模块,包含JUnit、Hamcrest、Mockito-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.RC1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  1. 修改应用主类RibbonConsumerApplication,通过@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获得服务发现的能力。同时,在该主类中创建RestTemplateSpring Bean实例,并通过@LoadBalanced注解开启客户端负载均衡。
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {

	@Bean
	@LoadBalanced
	RestTemplate restTemplate(){
		return new RestTemplate();
	}

	public static void main(String[] args) {
		SpringApplication.run(RibbonConsumerApplication.class, args);
	}
}
  1. 创建ConsumerController类并实现/ribbon-consumer接口。在该接口中,通过在上面创建的RestTemplate来实现对hello-eureka-client服务提供的/hello接口进行调用。注意这里访问的地址是服务名hello-eureka-client,而不是一个具体的地址,在服务治理框架中,这是一个非常重要的特性。
@RestController
public class ConsumerController {

	@Autowired
	RestTemplate restTemplate;

	@RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET)
	public String helloConsumer(){
		return restTemplate.getForEntity("http://hello-eureka-client/hello", String.class).getBody();
	}

}
  1. application.properties中配置Eureka服务注册中心的位置,需要与之前的hello-eureka-client一样,不然是发现不了该服务的,同时设置该消费者的端口为9000,不能与之前启动的应用端口冲突。
#为服务命名
spring.application.name=ribbon-consumer

#指定端口
server.port=9000

#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
  1. 启动ribbon-consumer应用后,访问http://localhost:1111/eureka/页面,可以看到当前除了hello-eureka-client之外,还多了我们实现的ribbon-consumer服务。
    在这里插入图片描述
  2. 通过向http://localhost:9000/ribbon-consumer发起GET请求,成功返回了“Hello World”。此时,我们可以在ribbon-consumer应用的控制台中看到如下信息,Ribbon输出了当前客户端维护的hello-eureka-client的服务列表情况。其中包含了各个实例的位置,Ribbon就是按照此信息进行轮询访问,以实现基于客户端的负载均衡。另外还输出了一些其他非常有用的信息,如对各个实例的请求总数量、第一次连接信息、上一次连接信息、总的请求失败数量等。
    在这里插入图片描述

服务容错保护:Spring Cloud Hystrix

入门工程

在开始使用Spring Cloud Hystrix实现断路器之前,我们先用之前实现的一些内容作为基础,构建一个如下图架构所示的服务调用关系:
在这里插入图片描述
我们在这里需要启动的工程有如下一些:

  • eureka-server工程:服务注册中心,端口为1111
  • hello-eureka-client工程:hello-eureka-client的服务单元,两个实例启动端口分别为80818082
  • ribbon-consumer工程:使用Ribbon实现的服务消费者,端口为9000

在未加入断路器之前,关闭8081的实例,发送GET请求到http://localhost:9000/ribbon-consumer,页面显示如下:
在这里插入图片描述

下面我们开始引入Spring Cloud Hystrix

  1. ribbon-consumer工程的pom.xmldependency节点中引入spring-cloud-starter-hystrix依赖:
<!--Hystrix-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
  1. ribbon-consumer工程的主类RibbonConsumerApplication中使用@EnableCircuitBreaker注解开启断路器功能:
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {

	@Bean
	@LoadBalanced
	RestTemplate restTemplate(){
		return new RestTemplate();
	}

	public static void main(String[] args) {
		SpringApplication.run(RibbonConsumerApplication.class, args);
	}
}
  1. 改造服务消费方式,新增HelloService类,注入RestTemplate实例。然后,将在ConsumerController中对RestTemplate的使用迁移到helloService函数中,最后,在helloService函数上增加@HystrixCommand注解来指定回调方法:
@Service
public class HelloService {

	@Autowired
	RestTemplate restTemplate;

	@HystrixCommand(fallbackMethod = "helloFallback")
	public String helloService(){
		return restTemplate.getForEntity("http://hello-eureka-client/hello", String.class).getBody();
	}

	public String helloFallback(){
		return "error";
	}

}
  1. 修改ConsumerController类,注入上面实现的HelloService实例,并在helloConsumer中进行调用:
@RestController
public class ConsumerController {

	@Autowired
	HelloService helloService;

	@RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET)
	public String helloConsumer(){
		return helloService.helloService();
	}

}
  1. 下面来验证一下通过断路器实现的服务回调逻辑,确保此时服务注册中心、两个hello-eureka-client以及ribbon-consumer均已启动,访问http://localhost:9000/ribbon-consumer可以轮询两个hello-eureka-client并返回一些文字信息。此时我们断开8081hello-eureka-client,然后访问http://localhost:9000/ribbon-consumer,当轮询到8081服务端时,输出内容为error,不再是之前的错误内容,Hystrix的服务回调生效。除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,我们还可以模拟一下服务阻塞(长时间未响应)的情况。我们对hello-eureka-client的/hello接口做一些修改,具体如下:
@RequestMapping(value = "/hello",method = RequestMethod.GET)
	public String hello() throws Exception{
		ServiceInstance instance=client.getLocalServiceInstance();
		//让处理线程等待几秒钟
		int sleepTime=new Random().nextInt(3000);
		logger.info("sleepTime"+sleepTime);
		Thread.sleep(sleepTime);

		logger.info("hello, host:"+instance.getHost()+", service_id:"+instance.getServiceId());
		return "Hello World";
	}

通过Thread.sleep()函数可让/hello接口的处理线程不是马上返回内容,而是在阻塞几秒之后才返回内容。由于Hystrix默认超时时间为2000毫秒,所以这里采用了03000的随机数以让处理过程有一定概率发生超时来触发断路器。

Hystrix仪表盘

现在在前面Hystrix入门工程的基础上构建一个Hystrix Dashboard来对ribbon-consumer实现监控,完成后的架构如下图所示:
在这里插入图片描述
Spring Cloud中构建一个Hystrix Dashboard非常简单,只需要下面4步:

  1. 创建一个标准的Spring Boot工程,命名为hystrix-dashboard
  2. 编辑pom.xml,具体依赖内容如下:
<!--Hystrix-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

<!--Hystrix Dashboard-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>

<!--Actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 为应用主类加上@EnableHystrixDashboard,启用Hystrix Dashboard功能。
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {

	public static void main(String[] args) {
		SpringApplication.run(HystrixDashboardApplication.class, args);
	}
}
  1. 根据实际情况修改application.properties配置文件,比如选择一个未被占用的端口等,此步不是必需的。
#为服务命名
spring.application.name=hystrix-dashboard

#指定端口
server.port=2001

到这里我们已经完成了基本配置,接下来可以启动该应用,并访问http://localhost:2001/hystrix。可以看到如下页面:
在这里插入图片描述
这是Hystrix Dashboard的监控首页,该页面中并没有具体的监控信息。

接下来我们实现单个服务实例的监控。

Hystrix Dashboard监控单实例节点需要通过访问实例的/hystrix.stream接口来实现,我们需要为服务实例添加这个端点,而添加该功能的步骤也同样简单,只需要下面两步:

  1. 在服务实例pom.xmldependencies节点中新增spring-boot-starter-actuator监控模块以开启监控相关的端点,并确保已经引入断路器的依赖spring-cloud-starter-hystrix
<!--Hystrix-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

<!--Actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 确保在服务实例的主类中已经使用@EnableCircuitBreaker注解,开启了断路器功能。

在为ribbon-consumer加入上面的配置之后,重启它的实例,此时我们可以在控制台中看到打印了大量的监控端点,其中/hystrix.stream就是用于Hystrix Dashboard来展现监控信息的接口。
在这里插入图片描述
到这里已经完成了所有的配置,在Hystrix Dashboard的首页输入http://localhost:9000/hystrix.stream,可以看到已启动对ribbon-consumer的监控,单击Monitor Stream按钮:
在这里插入图片描述
访问http://localhost:9000/ribbon-consumer并刷新多次,可看到如下页面:
在这里插入图片描述

Turbine集群监控

下面我们将在前面的基础上做一些扩展,通过引入Turbine来聚合ribbon-consumer服务的监控信息,并输出给Hystrix Dashboard来进行展示,最后完成如下图所示的结构:
在这里插入图片描述
具体实现步骤如下:

  1. 创建一个标准的Spring Boot工程,命名为turbine
  2. 编辑pom.xml,具体依赖内容如下所示:
<!--Turbine-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>

<!--Actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 创建应用主类TurbineApplication,并使用@EnableTurbine注解开启Turbine。
@EnableTurbine
@SpringBootApplication
public class TurbineApplication {

	public static void main(String[] args) {
		SpringApplication.run(TurbineApplication.class, args);
	}
}
  1. application.properties中加入EurekaTurbine的相关配置,具体如下:
# 为服务命名
spring.application.name=turbine

# 指定端口
server.port=8989

management.port=8990

# 指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

# 指定需要收集监控信息的服务名
turbine.app-config=ribbon-consumer

# 指定集群名称
# 当服务数量非常多的时候,可以启动多个Turbine服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,
# 同时该参数值可以在Hystrix仪表盘中用来定位不同的聚合集群,只需在Hystrix Stream的URL中通过cluster参数来指定
turbine.cluster-name-expression="default"

# 该参数设为true可以让同一主机上的服务通过主机名与端口号的组合来进行区分,
# 默认情况下会以host来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计
turbine.combine-host-port=true
  1. 分别启动eureka-serverhello-eureka-clientribbon-consumerturbine以及hystrix-dashboard。访问Hystrix Dashboard,并开启对http://localhost:8989/turbine.stream的监控,可以看到如下页面:
    在这里插入图片描述
    虽然我们如之前的架构那样启动了两个hello-eureka-client,但是在监控页面中依然只是展示了一个监控图,是由于这两个实例是同一个服务,而对于集群来说我们关注的是集群服务的高可用性,所以Turbine会将相同服务作为整体来看待,并汇总成一个监控图。

也可以为hello-eureka-client设置一个新的spring.application.name,比如ribbon-consumer-2,并启动它,此时我们就有两个服务会将监控信息输出给Turbine汇总了。刷新之前的监控页面,可以看到有两个监控图。

与消息代理结合

Spring Cloud在封装Turbine的时候,还封装了基于消息代理的收集实现。所以,我们可以将所有需要收集的监控信息都输出到消息代理中,然后Turbine服务再从消息代理中异步获取这些监控信息,最后将这些监控信息聚合并输出到Hystrix Dashboard中。通过引入消息代理,我们的TurbineHystrix Dashboard实现的监控架构可以改成如下图所示的结构:
在这里插入图片描述
下面,我们来构建一个新的应用以实现基于消息代理的Turbine聚合服务,具体步骤如下所示:

  1. 创建一个标准的Spring Boot工程,命名为turbine-amqp
  2. 编辑pom.xml,具体依赖内容如下所示:
<!--Turbine Amqp-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-turbine-amqp</artifactId>
</dependency>

<!--Actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

spring-cloud-starter-turbine-amqp实际上包装了spring-cloud-starter-turbine-streamspring-cloud-starter-stream-rabbit
3. 在应用主类中使用@EnableTurbineStream注解来启用Turbine Stream的配置。

@EnableTurbineStream
@EnableDiscoveryClient
@SpringBootApplication
public class TurbineAmqpApplication {

	public static void main(String[] args) {
		SpringApplication.run(TurbineAmqpApplication.class, args);
	}
}
  1. 配置application.properties文件。
# 为服务命名
spring.application.name=turbine

# 指定端口
server.port=8989

management.port=8990

# 指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
  1. 对于Turbine的配置已经完成了,下面需要对服务消费者ribbon-consumer做一些修改,使其监控信息能够输出到RabbitMQ上。这个修改也非常简单,只需在pom.xml中增加对spring-cloud-netflix-hystrix-amqp的依赖,具体如下:
<!--Hystrix Amqp-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-hystrix-amqp</artifactId>
</dependency>
  1. 启动eureka-serverhello-eureka-clientribbon-consumerturbine以及hystrix-dashboard,同时确保RabbitMQ已在正常运行。访问Hystrix Dashboard,并开启对http://localhost:8989/turbine.stream的监控,我们可以获得如之前实现的同样结果,只是这里的监控信息收集是通过消息代理异步实现的。

声明式服务调用:Spring Cloud Feign

入门工程

下面的示例继续使用之前我们实现的hello-eureka-client服务,这里我们会通过Spring Cloud Feign提供的声明式服务绑定功能来实现对该服务接口的调用。

  1. 首先,创建一个Spring Boot基础工程,取名为feign-consumer,并在pom.xml中引入spring-cloud-starter-eurekaspring-cloud-starter-feign依赖,具体内容如下所示:
<!--Eureka-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<!--Feign-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

<!--全栈Web开发模块,包含嵌入式Tomcat、Spring MVC-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. 创建应用主类FeignConsumerApplication,并通过@EnableFeignClients注解开启Spring Cloud Feign的支持功能。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignConsumerApplication {

	public static void main(String[] args) {
		SpringApplication.run(FeignConsumerApplication.class, args);
	}
}
  1. 定义HelloService接口,通过@FeignClient注解指定服务名来绑定服务,然后再使用Spring MVC的注解来绑定具体该服务提供的REST接口。
@FeignClient("hello-eureka-client")
public interface HelloService {

	@RequestMapping("/hello")
	String hello();

}
  1. 接着,创建一个ConsumerController来实现对Feign客户端的调用。使用@Autowired直接注入上面定义的HelloService实例,并在helloConsumer函数中调用这个绑定了hello-service服务接口的客户端来向该服务发起/hello接口的调用。
@RestController
public class ConsumerController {

	@Autowired
	HelloService helloService;

	@RequestMapping(value = "/feign-consumer",method = RequestMethod.GET)
	public String helloConsumer(){
		return helloService.hello();
	}

}
  1. 最后,同Ribbon实现的服务消费者一样,需要在application.properties中指定服务注册中心,并定义自身的服务名为feign-consumer,为了方便本地调试与之前的Ribbon消费者区分,端口使用9090
#为服务命名
spring.application.name=feign-consumer

#指定端口
server.port=9090

#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
  1. 我们先启动服务注册中心以及两个hello-eureka-client,然后启动feign-consumer,此时我们在Eureka信息面板中可以看到如下内容:
    在这里插入图片描述
  2. 发送几次GET请求到http://localhost:9090/feign-consumer,可以得到如之前Ribbon实现时一样的效果,正确返回了“Hello World”。并且根据控制台的输出,我们可以看到Feign实现的消费者,依然是利用Ribbon维护了针对hello-eureka-client的服务列表信息,并且通过轮询实现了客户端负载均衡。而与Ribbon不同的是,通过Feign我们只需定义服务绑定接口,以声明式的方法,优雅而简单地实现了服务调用。
    在这里插入图片描述
    在这里插入图片描述

参数绑定

在开始介绍Spring Cloud Feign的参数绑定之前,我们先扩展一下服务提供方hello-eureka-client。增加下面这些接口定义,其中包含带有Request参数的请求、带有Header信息的请求、带有RequestBody的请求以及请求响应体中是一个对象的请求。

@RequestMapping(value = "/hello1",method = RequestMethod.GET)
public String hello(@RequestParam String name){
	return "Hello "+name;
}

@RequestMapping(value = "/hello2",method = RequestMethod.GET)
public User hello(@RequestHeader String name,@RequestHeader Integer age){
	return new User(name,age);
}

@RequestMapping(value = "/hello3",method = RequestMethod.POST)
public String hello(@RequestBody User user){
	return "Hello"+user.getName()+","+user.getAge();
}

User对象的定义如下,需要注意的是,这里必须要有User的默认构造函数,否则Spring Cloud Feign根据JSON字符串转换User对象时会抛出异常。

public class User {

	private String name;
	private Integer age;

	public User() {
	}

	public User(String name, Integer age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
	}

}

改造完hello-eureka-client之后,我们开始在前面的feign-consumer应用中实现这些新增的请求的绑定。

  1. 首先,在feign-consumer中创建与上面一样的User类。
  2. 然后,在HelloService接口中增加对上述三个新增接口的绑定声明,修改后,完整的HelloService接口如下所示:
@FeignClient("hello-eureka-client")
public interface HelloService {

	@RequestMapping("/hello")
	String hello();

	@RequestMapping(value = "/hello1",method = RequestMethod.GET)
	String hello(@RequestParam("name") String name);

	@RequestMapping(value = "/hello2",method = RequestMethod.GET)
	User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age);

	@RequestMapping(value = "/hello3", method = RequestMethod.POST)
	String hello(@RequestBody User user);

}

这里一定要注意,在定义各参数绑定时,@RequestParam@RequestHeader等可以指定参数名称的注解,它们的value千万不能少。在Spring MVC程序中,这些注解会根据参数名来作为默认值,但是在Feign中绑定参数必须通过value属性来指明具体的参数名,不然会抛出IllegalStateException异常,value属性不能为空。

  1. 最后,在Controller中新增一个/feign-consumer2接口,来对新增的接口进行调用,修改后的完整代码如下所示:
@RestController
public class ConsumerController {

	@Autowired
	HelloService helloService;

	@RequestMapping(value = "/feign-consumer",method = RequestMethod.GET)
	public String helloConsumer(){
		return helloService.hello();
	}

	@RequestMapping(value = "/feign-consumer2",method = RequestMethod.GET)
	public String helloConsumer2(){
		StringBuilder sb=new StringBuilder();
		sb.append(helloService.hello()).append("\n");
		sb.append(helloService.hello("DIDI")).append("\n");
		sb.append(helloService.hello("DIDI",30)).append("\n");
		sb.append(helloService.hello(new User("DIDI",30))).append("\n");
		return sb.toString();
	}

}
  1. 在完成上述改造之后,启动服务注册中心、两个hello-eureka-client服务以及我们改造过的feign-consumer。通过发送GET请求到http://localhost:9090/feign-consumer2,触发HelloService对新增接口的调用。最终,我们会获得如下输出,代表接口绑定和调用成功。
    在这里插入图片描述

继承特性

当使用Spring MVC的注解来绑定服务接口时,我们几乎完全可以从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。

Spring Cloud Feign中,提供了继承特性来帮助我们解决这些复制操作,以进一步减少代码量。下面看一下如何通过Spring Cloud Feign的继承特性来实现REST接口定义的复用。

  1. 为了能够复用DTO与接口定义,我们先创建一个基础的Maven工程,命名为hello-service-api
  2. 由于在hello-service-api中需要定义可同时复用于服务端与客户端的接口,我们要使用到Spring MVC的注解,所以在pom.xml中引入spring-boot-starter-web依赖,具体内容如下所示。
<groupId>com.study.springcloud</groupId>
<artifactId>hello-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>hello-service-api</name>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>

    <!--全栈Web开发模块,包含嵌入式Tomcat、Spring MVC-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

</dependencies>
  1. 将前面实现的User对象复制到hello-service-api工程的dto包下。
  2. 创建service包,在其下创建HelloService接口,内容如下,接口中的User对象为本项目dto包中的User
@RequestMapping("/refactor")
public interface HelloService {

	@RequestMapping(value = "/hello4",method = RequestMethod.GET)
	String hello(@RequestParam("name") String name);

	@RequestMapping(value = "/hello5",method = RequestMethod.GET)
	User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age);

	@RequestMapping(value = "/hello6", method = RequestMethod.POST)
	String hello(@RequestBody User user);

}

因为后续还会通过之前的hello-eureka-clientfeign-consumer来重构,所以为了避免接口混淆,在这里定义HelloService时,除了头部定义了/refactor前缀之外,同时将提供服务的三个接口更名为/hello4/hello5/hello6

  1. 下面对hello-eureka-client进行重构,在pom.xmldependency节点中,新增对hello-service-api的依赖。
<dependency>
    <groupId>com.study.springcloud</groupId>
    <artifactId>hello-service-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  1. 创建RefactorHelloController类继承hello-service-spi中定义的HelloService接口,并参考之前的HelloController来实现这三个接口,具体内容如下所示:
@RestController
public class RefactorHelloController implements HelloService {

	@Override
	public String hello(@RequestParam("name")String name) {
		return "Hello "+name;
	}

	@Override
	public User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age) {
		return new User(name,age);
	}

	@Override
	public String hello(@RequestBody User user) {
		return "Hello"+user.getName()+","+user.getAge();
	}

}
  1. 我们可以看到通过继承的方式,在Controller中不再包含以往会定义的请求映射注解@RequestMapping,而参数的注解定义在重写的时候会自动带过来。在这个类中,除了要实现接口逻辑之外,只需再增加@RestController注解使该类成为一个REST接口类就大功告成了。
  2. 完成了服务提供者的重构,接下来在服务消费者feign-consumerpom.xml文件中,如在服务提供者中一样,新增对hello-service-api的依赖。
  3. 创建RefactorHelloService接口,并继承hello-service-api包中的HelloService接口,然后添加@FeignClient注解来绑定服务。
@FeignClient("hello-eureka-client")
public interface RefactorHelloService extends HelloService {
}
  1. 最后,在ConsumerController中,注入RefactorHelloService的实例,并新增一个请求/feign-consumer3来触发对RefactorHelloService的实例的调用。
@Autowired
RefactorHelloService refactorHelloService;

@RequestMapping(value = "/feign-consumer3",method = RequestMethod.GET)
public String helloConsumer3(){
	StringBuilder sb=new StringBuilder();
	sb.append(refactorHelloService.hello("MIMI")).append("\n");
	sb.append(refactorHelloService.hello("MIMI",20)).append("\n");
	sb.append(refactorHelloService.hello(new User("MIMI",20))).append("\n");
	return sb.toString();
}
  1. 注意必须先构建hello-service-api工程,然后再构建hello-eureka-clientfeign-consumer。接着我们分别启动服务注册中心,hello-eureka-clientfeign-consumer,并访问http://localhost:9090/feign-consumer3,调用成功后可以获得如下输出:
    在这里插入图片描述

使用Spring Cloud Feign继承特性的优点很明显,可以将接口的定义从Controller中剥离,同时配合Maven私有仓库就可以轻易地实现接口定义的共享,实现在构建期的接口绑定,从而有效减少服务客户端的绑定配置。

这么做虽然可以可以很方便地实现接口定义和依赖的共享,不用再复制粘贴接口进行绑定,但是这样的做法使用不当的话会带来副作用。

由于接口在构建期间就建立起了依赖,那么接口变动就会对项目构建造成影响,可能服务提供方修改了一个接口定义,那么会直接导致客户端工程的构建失败。所以,如果开发团队通过此方法来实现接口共享的话,建议在开发评审期间严格遵守面向对象的开闭原则,尽可能地做好前后版本的兼容,防止牵一发而动全身的后果,增加团队不必要的维护工作量。

重试机制

Spring Cloud Feign中默认实现了请求的重试机制。我们可以通过修改之前的示例做一些验证。

  1. hello-eureka-client应用的/hello接口实现中,增加一些随机延迟,比如:
@RequestMapping(value = "/hello",method = RequestMethod.GET)
	public String hello() throws Exception{
		ServiceInstance instance=client.getLocalServiceInstance();
		//测试超时
		int sleepTime=new Random().nextInt(3000);
		logger.info("sleepTime"+sleepTime);
		Thread.sleep(sleepTime);

		logger.info("hello, host:"+instance.getHost()+", service_id:"+instance.getServiceId());
		return "Hello World";
	}
  1. feign-consumer应用中增加如下重试配置参数。
# 开启重试机制
spring.cloud.loadbalancer.retry.enabled=true

# 请求连接的超时时间
hello-eureka-client.ribbon.ConnectTimeout=500

# 请求处理的超时时间
hello-eureka-client.ribbon.ReadTimeout=2000

# 对所有操作请求都进行重试
hello-eureka-client.ribbon.OkToRetryOnAllOperations=true

# 切换实例的重试次数,根据此处设置会尝试更换两次实例进行重试
hello-eureka-client.ribbon.MaxAutoRetriesNextServer=2

# 对当前实例的重试次数,根据此处设置重试策略先尝试访问首选实例一次,失败后才更换实例访问,
# 更换实例访问的次数由hello-eureka-client.ribbon.MaxAutoRetriesNextServer参数设置
hello-eureka-client.ribbon.MaxAutoRetries=1
  1. 最后,启动这些应用,并尝试访问几次http://localhost:9090/feign-consumer接口。当请求发生超时的时候,我们在hello-eureka-client的控制台中可能会获得如下输出内容(由于sleepTime的随机性,并不一定每次相同):
    在这里插入图片描述
    从控制台输出中,我们可以看到这次访问的第一次请求延迟时间为2155毫秒,由于超时时间设置为2000毫秒,Feign客户端发起了重试,第二次请求的延迟为796毫秒,没有超时。Feign客户端在进行服务调用时,虽然经历了一次失败,但是通过重试机制,最终还是获得了请求结果。所以,对于重试机制的实现,对于构建高可用的服务集群来说非常重要,而Spring Cloud Feign也为其提供了足够的支持。需要注意Ribbon的超时和Hystrix的超时是两个概念,为了让上述实现有效,我们需要让Hystrix的超时时间大于Ribbon的超时时间,否则Hystrix命令超时后,该命令直接熔断,重试机制就没有任何意义了。

禁用Hystrix

Spring Cloud Feign中,可以通过feign.hystrix.enabled=false来关闭Hystrix功能。另外,如果不想全局地关闭Hystrix支持,而只想针对某个服务客户端关闭Hystrix支持时,需要通过使用@Scope("prototype")注解为指定的客户端配置Feign.Builder实例,详细实现步骤如下所示:

  1. 构建一个关闭Hystrix的配置类。
@Configuration
public class DisableHystrixConfiguration {

	@Bean
	@Scope("prototype")
	public Feign.Builder feignBuilder(){
		return Feign.builder();
	}

}
  1. HelloService@FeignClient注解中,通过configuration参数引入上面实现的配置。
@FeignClient(name = "hello-eureka-client",configuration = DisableHystrixConfiguration.class)
public interface HelloService {

	@RequestMapping("/hello")
	String hello();

	@RequestMapping(value = "/hello1",method = RequestMethod.GET)
	String hello(@RequestParam("name") String name);

	@RequestMapping(value = "/hello2",method = RequestMethod.GET)
	User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age);

	@RequestMapping(value = "/hello3", method = RequestMethod.POST)
	String hello(@RequestBody User user);

}

服务降级配置

Hystrix提供的服务降级是服务容错的重要功能,由于Spring Cloud Feign在定义服务客户端的时候与Spring Cloud Ribbon有很大差别,HystrixCommand定义被封装了起来,我们无法通过@HystrixCommand注解的fallback参数那样来指定具体的服务降级处理方法。但是,Spring Cloud Feign提供了另外一种简单的定义方式,下面我们在之前创建的feign-consumer工程中进行改造。

  1. 编辑pom.xml,加入Hystrix依赖:
<!--Hystrix-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
  1. 修改application.properties配置文件,加入如下内容:
# 开启Feign客户端的Hystrix支持
feign.hystrix.enabled=true
  1. 服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类。比如为HelloService接口实现一个服务降级类HelloServiceFallback,其中每个重写方法的实现逻辑都可以用来定义相应的服务降级逻辑,具体如下:服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类。比如为HelloService接口实现一个服务降级类HelloServiceFallback,其中每个重写方法的实现逻辑都可以用来定义相应的服务降级逻辑,具体如下:服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类。比如为HelloService接口实现一个服务降级类HelloServiceFallback,其中每个重写方法的实现逻辑都可以用来定义相应的服务降级逻辑,具体如下:服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类。比如为HelloService接口实现一个服务降级类HelloServiceFallback,其中每个重写方法的实现逻辑都可以用来定义相应的服务降级逻辑,具体如下:
@Component
public class HelloServiceFallback implements HelloService {

	@Override
	public String hello() {
		return "error";
	}

	@Override
	public String hello(@RequestParam("name") String name) {
		return "error";
	}

	@Override
	public User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age) {
		return new User("未知",0);
	}

	@Override
	public String hello(@RequestBody User user) {
		return "error";
	}
}
  1. 在服务绑定接口HelloService中,通过@FeignClient注解的fallback属性来指定对应的服务降级实现类。
@FeignClient(name = "hello-eureka-client",fallback = HelloServiceFallback.class)
public interface HelloService {

	@RequestMapping("/hello")
	String hello();

	@RequestMapping(value = "/hello1",method = RequestMethod.GET)
	String hello(@RequestParam("name") String name);

	@RequestMapping(value = "/hello2",method = RequestMethod.GET)
	User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age);

	@RequestMapping(value = "/hello3", method = RequestMethod.POST)
	String hello(@RequestBody User user);

}
  1. 如果你的项目中有前面的DisableHystrixConfiguration类,全部记得注掉,否则会禁用Hystrix
  2. 启动服务注册中心和feign-consumer,但是不启动hello-eureka-client服务。发送GET请求到http://localhost:9090/feign-consumer2,该接口会分别调用HelloService中的4个绑定接口,但因为hello-eureka-client服务没有启动,会直接触发服务降级,并获得下面的输出内容:启动服务注册中心和feign-consumer,但是不启动hello-eureka-client服务。发送GET请求到http://localhost:9090/feign-consumer2,该接口会分别调用HelloService中的4个绑定接口,但因为hello-eureka-client服务没有启动,会直接触发服务降级,并获得下面的输出内容:
    在这里插入图片描述

正如我们在HelloServiceFallback类中实现的内容,每一个服务接口的断路器实际就是实现类中的重写函数的实现。

日志配置

Spring Cloud Feign在构建被@FeignClient注解修饰的服务客户端时,会为每一个客户端都创建一个feign.Logger实例,我们可以利用该日志对象的DEBUG模式来帮助分析Feign的请求细节。可以在application.properties文件中使用logging.level.<FeignClient>的参数配置格式来开启指定Feign客户端的DEBUG日志,其中<FeignClient>Feign客户端定义接口的完整路径,比如针对前面实现的HelloService可以按如下配置开启:

# 开启指定Feign客户端的DEBUG日志
logging.level.com.study.springcloud.feignconsumer.service.HelloService=DEBUG

但是,只是添加了如上配置,还无法实现对DEBUG日志的输出。这时由于Feign客户端默认的Logger.Level对象定义为NONE级别,该级别不会记录任何Feign调用过程中的信息,所以我们需要调整它的级别,针对全局的日志级别,可以在应用主类中直接加入Logger.LevelBean创建,具体如下:

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignConsumerApplication {

	@Bean
	Logger.Level feignLoggerLevel(){
		return Logger.Level.FULL;
	}

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

当然也可以通过实现配置类,然后在具体的Feign客户端来指定配置类以实现是否要调整不同的日志级别,比如下面的实现:

@Configuration
public class FullLogConfiguration {

	@Bean
	Logger.Level feignLoggerLevel(){
		return Logger.Level.FULL;
	}

}
@FeignClient(name = "hello-eureka-client",configuration = FullLogConfiguration.classpublic interface HelloService {

	@RequestMapping("/hello")
	String hello();

	@RequestMapping(value = "/hello1",method = RequestMethod.GET)
	String hello(@RequestParam("name") String name);

	@RequestMapping(value = "/hello2",method = RequestMethod.GET)
	User hello(@RequestHeader("name") String name,@RequestHeader("age") Integer age);

	@RequestMapping(value = "/hello3", method = RequestMethod.POST)
	String hello(@RequestBody User user);

}

在调整日志级别为FULL之后,我们可以再访问一下之前的http://localhost:9090/feign-consumer接口,这时我们在feign-consumer的控制台中就可以看到类似下面的请求详细日志:
在这里插入图片描述
在这里插入图片描述

对于FeignLogger级别主要有下面4类,可根据实际需要进行调整使用。

  • NONE:不记录任何信息。
  • BASIC:仅记录请求方法、URL以及响应状态码和执行时间。
  • HEADERS:除了记录BASIC级别的信息之外,还会记录请求和响应的头信息。
  • FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等。

API网关服务:Spring Cloud Zuul

构建网关

首先,在实现各种API网关服务的高级功能之前,我们需要做一些准备工作,比如,构建起最基本的API网关服务,并且搭建几个用于路由和过滤使用的微服务应用等。对于微服务应用,我们可以直接使用之前实现的hello-eureka-client和feign-consumer。虽然之前我们一直将feign-consumer视为消费者,但是在Eureka的服务注册与发现体系中,每个服务既是提供者也是消费者,所以feign-consumer实质上也是一个服务提供者。之前我们访问的http://localhost:9090/feign-consumer等一系列接口就是它提供的服务。

接下来,我们详细介绍一下API网关服务的构建过程。

  1. 创建一个基础的Spring Boot工程,命名为api-gateway,并在pom.xml中引入spring-cloud-starter-zuul依赖,具体如下:
<!--Zuul-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

spring-cloud-starter-zuul模块中不仅包含了Netflix Zuul的核心依赖zuul-core,它还包含了下面这些网关服务需要的重要依赖:spring-cloud-starter-hystrix(用来在网关服务中实现对微服务转发时候的保护机制,通过线程隔离和断路器,防止微服务的故障引发API网关资源无法释放,从而影响其他应用的对外服务)、spring-cloud-starter-ribbon(用来实现在网关服务进行路由转发时候的客户端负载均衡以及请求重试)、spring-boot-starter-actuator(用来提供常规的微服务管理端点)。另外,在spring-cloud-starter-zuul中还特别提供了/routes端点来返回当前的所有路由规则。

  1. 创建应用主类,使用@EnableZuulProxy注解开启ZuulAPI网关服务功能。
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(ApiGatewayApplication.class, args);
	}
}
  1. application.properties中配置Zuul应用的基础信息,如应用名、服务端口号等,具体内容如下:
# 为服务命名
spring.application.name=api-gateway

# 指定端口
server.port=5555

完成上面的工作后,通过Zuul实现的API网关服务就构建完毕了。

请求路由

下面来为上面构建的网关服务增加请求路由的功能。为了演示请求路由的功能,我们先将之前准备的Eureka服务注册中心和微服务应用都启动起来。此时,我们在Eureka信息面板中可以看到如下图所示的两个微服务应用已经被注册成功了。
在这里插入图片描述

传统路由方式

使用Spring Cloud Zuul实现路由功能非常简单,只需要对api-gateway服务增加一些关于路由规则的配置,就能实现传统的路由转发功能,比如:

# 发往API网关服务的请求中,所有符合/api-a-url/**规则的访问都将被路由转发到http://localhost:8080/地址上,
# 也就是说,当我们访问http://localhost:5555/api-a-url/hello的时候,API网关服务会将该请求路由到http://localhost:8080/提供的微服务接口上
# 配置属性zuul.routes.api-a-url.path中的api-a-url部分为路由的名字,可以任意定义,但是一组path和url映射关系的路由名要相同,面向服务的映射方式也是如此
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8080/

面向服务的路由

很显然,传统路由的配置方式对于我们来说并不友好,它同样需要运维人员花费大量的时间来维护各个路由pathurl的关系。为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝整合,我们可以让路由的path不是映射具体的url,而是让它映射到某个具体的服务,而具体的url则交给Eureka的服务发现机制去自动维护,我们称这类路由为面向服务的路由。在Zuul中使用服务路由也同样简单,只需做下面这些配置。

  1. 为了与Eureka整合,我们需要在api-gatewaypom.xml中引入spring-cloud-starter-eureka依赖,具体如下:
<!--Eureka-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
  1. api-gatewayapplication.properties配置文件中指定Eureka注册中心的位置,并且配置服务路由。具体如下:
# 定义名为api-a的路由来映射hello-eureka-client
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.service-id=hello-eureka-client

# 定义名为api-b的路由来映射feign-consumer
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.service-id=feign-consumer

# 指定服务注册中心的地址,
# 除了将自己注册成服务之外,同时也让Zuul能够获取hello-eureka-client和feign-consumer服务的实例清单,
# 以实现path映射服务,再从服务中挑选实例来进行请求转发的完整路由机制
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
  1. eureka-serverhello-eureka-clientfeign-consumer以及api-gateway都启动起来。在Eureka信息面板中,我们可以看到,除了hello-eureka-clientfeign-consumer之外,多了一个网关服务api-gateway
    在这里插入图片描述
  2. 此时我们已经可以通过服务网关来访问hello-eureka-clientfeign-consumer这两个服务了。根据配置的映射关系,向网关发起http://localhost:5555/api-a/hello请求,该url符合/api-a/**规则,由api-a路由负责转发,该路由映射的serviceIdhello-eureka-client,所以最终/hello请求会被发送到hello-eureka-client服务的某个实例上去,发起http://localhost:5555/api-b/feign-consumer请求也类似。

通过面向服务的路由配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的pathserviceId的映射组合,使得维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关服务可以自动化完成服务实例清单的维护,完美地解决了对路由映射实例的维护问题。

请求过滤

通过前置的网关服务来完成非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那么可以在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度也得到了相应降低。

Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。

下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。

public class AccessFilter extends ZuulFilter {

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

	//过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。
	//这里定义为pre,代表会在请求被路由之前执行。
	@Override
	public String filterType() {
		return "pre";
	}

	//过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
	@Override
	public int filterOrder() {
		return 0;
	}

	//判断该过滤器是否需要被执行。
	//这里我们直接返回了true,因此该过滤器对所有请求都会生效。
	//实际运用中我们可以利用该函数来指定过滤器的有效范围。
	@Override
	public boolean shouldFilter() {
		return true;
	}

	//过滤器的具体逻辑。
	//这里我们通过ctx.setSendZuulResponse(false)令Zuul过滤该请求,不对其进行路由,
	//然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然也可以进一步优化我们的返回,
	//比如,通过ctx.setResponseBody(body)对返回的body内容进行编辑等。
	@Override
	public Object run() {
		RequestContext ctx=RequestContext.getCurrentContext();
		HttpServletRequest request=ctx.getRequest();

		log.info("send {} request to {}",request.getMethod(),request.getRequestURL().toString());

		Object accessToken=request.getParameter("accessToken");

		if(accessToken==null){
			log.warn("access token is empty");
			ctx.setSendZuulResponse(false);
			ctx.setResponseStatusCode(401);
			return null;
		}

		log.info("access token ok");
		return null;
	}
}

在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器,比如,在应用主类中增加如下内容:

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

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

	@Bean
	public AccessFilter accessFilter(){
		return new AccessFilter();
	}

}

重写启动api-gateway服务,并发起http://localhost:5555/api-a/hello请求,返回401错误:
在这里插入图片描述
发起http://localhost:5555/api-a/hello?accessToken=token请求,正确路由到hello-eureka-client/hello接口,并返回Hello World
在这里插入图片描述

分布式配置中心:Spring Cloud Config

构建配置中心

通过Spring Cloud Config构建一个分布式配置中心非常简单,只需要以下三步:

  1. 创建一个基础的Spring Boot工程,命名为config-server,并在pom.xml中引入下面的依赖:
<!--Config Server-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
  1. 创建Spring Boot的程序主类,并添加@EnableConfigServer注解,开启Spring Cloud Config的服务端功能。
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigServerApplication.class, args);
	}
}
  1. application.properties中添加配置服务的基本信息以及Git仓库的相关信息,如下所示:
# 为服务命名
spring.application.name=config-server

# 指定端口
server.port=7001

# 配置Git仓库位置
spring.cloud.config.server.git.uri=https://github.com/yehongliu/config-server-study.git

# 配置仓库路径下的相对搜索位置,可以配置多个
spring.cloud.config.server.git.search-paths=config-repo
  1. 启动config-server

客户端配置映射

  1. 在上面配置的Git仓库下创建一个config-repo目录作为配置仓库,并根据不同环境新建下面4个配置文件:
    在这里插入图片描述
  2. 在这4个配置文件中均设置一个from属性,并为每个配置文件分别设置不同的值,如下所示:
from=git-dev-1.0

from=git-prod-1.0

from=git-test-1.0

from=git-default-1.0
  1. 此时我们已经可以通过浏览器、POSTMANCURL等工具直接来访问我们的配置内容了。访问配置信息的URL与配置文件的映射关系如下所示:
/{application}/{profile}[/{label}]

/{application}-{profile}.yml

/{label}/{application}-{profile}.yml

/{application}-{profile}.properties

/{label}/{application}-{profile}.properties
  1. 下面尝试在微服务应用中获取上述配置信息。首先,创建一个Spring Boot应用,命名为config-client,并在pom.xml中引入下述依赖:
<!--Config-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<!--全栈Web开发模块,包含嵌入式Tomcat、Spring MVC-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. 创建Spring Boot的应用主类,具体如下:
@SpringBootApplication
public class ConfigClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigClientApplication.class, args);
	}
}
  1. 创建bootstrap.properties配置,来指定获取配置文件的config-server位置,例如:
# 对应配置文件规则中的{application}部分
spring.application.name=configstudy

# 对应配置文件规则中的{profile}部分
spring.cloud.config.profile=dev

# 对应配置文件规则中的{label}部分
spring.cloud.config.label=master

# 配置中心config-server的地址
spring.cloud.config.uri=http://localhost:7001/

# 指定端口
server.port=7002

这里需要格外注意,只有当我们配置spring.cloud.config.uri的时候,客户端应用才会尝试连接Spring Cloud Config的服务端来获取远程配置信息并初始化Spring环境配置,否则Spring Cloud Config的客户端会默认尝试连接http://localhost:8888。同时,上面这些属性必须配置在bootstrap.properties、环境变量或是其他优先级高于应用Jar包内的配置信息中,这样config-server中的配置信息才能被正确加载。
7. 创建一个RESTful接口来返回配置中心的from属性,通过@Value("${from}")绑定配置服务中心配置的from属性,具体实现如下:

@RefreshScope
@RestController
public class TestController {

	@Value("${from}")
	private String from;

	@RequestMapping("/from")
	public String from(){
		return this.from;
	}
}
  1. 除了通过@Value注解绑定注入之外,也可以通过Environment对象来获取配置属性,比如:
@RefreshScope
@RestController
public class TestController {
	@Autowired
	private Environment env;

	@RequestMapping("/from")
	public String from(){
		return env.getProperty("from", "undefined");
	}

}
  1. 启动config-client应用,并访问http://localhost:7002/from,我们就可以根据配置内容输出对应环境的from内容了。可以继续通过修改bootstrap.properties中的配置内容获取不同的配置信息来熟悉配置服务中的配置规则。
    在这里插入图片描述

服务化配置中心

下面将基于前面实现的config-serverconfig-client工程来进行改造实现,将Config Server注册到服务中心,并通过服务发现来访问Config Server并获取Git仓库中的配置信息。

  1. config-serverpom.xml中增加spring-cloud-starter-eureka依赖,以实现将分布式配置中心加入Eureka的服务治理体系。
<!--Eureka-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
  1. application.properties中配置参数eureka.client.service-url.defaultZone以指定服务注册中心的位置,详细内容如下:
# 为服务命名
spring.application.name=config-server

# 指定端口
server.port=7001

# 配置服务注册中心
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

# 配置Git仓库位置
spring.cloud.config.server.git.uri=https://github.com/yehongliu/config-server-study.git

# 配置仓库路径下的相对搜索位置,可以配置多个
spring.cloud.config.server.git.search-paths=config-repo
  1. 在应用主类中,新增@EnableDiscoveryClient注解,用来将config-server注册到上面配置的服务注册中心上去。
@EnableDiscoveryClient
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigServerApplication.class, args);
	}
}
  1. 启动该应用,并访问http://localhost:1111/,可以在Eureka Server的信息面板中看到config-server已经被注册了。
    在这里插入图片描述
  2. config-clientpom.xml中增加spring-cloud-starter-eureka依赖,以实现客户端发现config-server服务,具体配置如下:
<!--Eureka-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<!--Config-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<!--全栈Web开发模块,包含嵌入式Tomcat、Spring MVC-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. bootstrap.properties中,按如下配置:
# 对应配置文件规则中的{application}部分,定位配置信息
spring.application.name=configstudy

# 指定端口
server.port=7002

# 指定服务注册中心,用于服务的注册与发现
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

# 开启通过服务来访问Config Server的功能
spring.cloud.config.discovery.enabled=true

# 指定Config Server注册的服务名
spring.cloud.config.discovery.service-id=config-server

# 对应配置文件规则中的{profile}部分,定位配置信息
spring.cloud.config.profile=dev
  1. 在应用主类中,新增@EnableDiscoveryClient注解,用来发现config-server服务,利用其来加载应用配置:
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigClientApplication.class, args);
	}
}
  1. 沿用之前我们创建的Controller来加载Git中的配置信息:
@RefreshScope
@RestController
public class TestController {

	@Value("${from}")
	private String from;

	@RequestMapping("/from")
	public String from(){
		return this.from;
	}
}
  1. 启动该客户端应用,访问http://localhost:1111/,可以在Eureka Server的信息面板中看到该应用已经被注册成功。
    在这里插入图片描述
  2. 访问客户端应用提供的服务http://localhost:7002/from,此时,我们会返回在Git仓库中configstudy-dev.properties文件中配置的from属性内容:
    from=git-dev-1.0
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_43415405/article/details/84655545
今日推荐