微服务构建:Spring Boot
构建Maven项目
- 通过官方的
Spring Initializr
工具来产生基础项目。 - 下载并解压生成的项目压缩包,并用
IDE
以Maven
项目导入,以Intellij IDEA
为例。 - 单击
Import project from external model
并选择Maven
,一直单击Next
按钮。
实现RESTful API
在Spring Boot
中创建一个RESTful API的实现代码同Spring MVC
应用一样,只是不需要像Spring MVC
那样先做很多配置,而是像下面这样直接开始编写Controller
内容:
- 新建
package
:com.study.springcloud.hello.controller
。 - 新建
HelloController
类,内容如下所示。
@RestController
public class HelloController {
@RequestMapping("/hello")
public String index(){
return "Hello World";
}
}
- 启动该应用,通过浏览器访问
http;//localhost:8080/hello
。
引入actuator
在pom.xml
的dependency
节点中,新增spring-boot-starter-actuator
的依赖,如下。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
重新启动应用,有一批端点定义,这些端点并非我们自己在程序中创建的,而是由spring-boot-starter-actuator
模块根据应用依赖和配置自动创建出来的监控和管理端点。通过这些端点,我们可以实时获取应用的各项监控指标。这些内容将帮助我们制定更为个性化的监控策略。
搭建服务注册中心
- 首先,创建一个基础的
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>
- 通过
@EnableEurekaServer
注解启动一个服务注册中心提供给其他应用进行对话。只需在一个普通的Spring Boot
应用中添加这个注解即可:
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
- 在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为,在
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/
- 启动应用并访问
http://localhost:1111/
,可以看到如下页面,其中Instances currently registered with Eureka
栏是空的,说明该注册中心还没有注册任何服务。
注册服务提供者
修改前面的Spring Boot
入门项目,将其作为一个微服务应用向服务注册中心发布自己。
- 首先,修改
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>
- 接着,改造
/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";
}
}
- 然后,在主类中通过加上
@EnableDiscoveryClient
注解,激活Eureka
中的DiscoveryClient
实现(自动化配置,创建DiscoveryClient
接口针对Eureka
客户端的EurekaDiscoveryClient
实例),才能实现上述Controller
中对服务信息的输出。
@EnableDiscoveryClient
@SpringBootApplication
public class HelloEurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(HelloEurekaClientApplication.class, args);
}
}
- 最后,我们需要在
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/
- 分别启动服务注册中心以及这里改造后的
hello-eureka-client
服务。访问Eureka
的信息面板。 - 通过访问
http://localhost:8080/hello
,直接向该服务发起请求,在控制台中可以看到如下输出: - 这些输出内容就是之前我们在
HelloController
中注入的DiscoveryClient
接口对象,从服务注册中心获取的服务相关信息。
高可用注册中心
在前面的服务注册中心的基础之上进行扩展,构建一个双节点的服务注册中心集群。
- 创建
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/
- 创建
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/
- 在
/etc/hosts
文件中添加对peer1
和peer2
的转换,让上面配置的host
形式的serviceUrl
能在本地正确访问到;Windows
系统路径为:C:\Windows\System32\drivers\etc\hosts
。
127.0.0.1 peer1
127.0.0.1 peer2
- 通过
spring.profiles.active
属性来分别启动peer1
和peer2
,先用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
- 访问
peer1
的注册中心http://peer1:1111/
,可以看到registered-replicas
中已经有peer2
节点的eureka-server
了,且节点在可用分片(available-replicas
)中: - 同样地,访问
peer2
的注册中心http://peer2:1112/
: - 将
peer1
关闭,刷新http://peer2:1112/
,可以看到peer1
的节点变为了不可用分片(unavailable-replicas
): - 在设置了多节点的服务注册中心之后,服务提供方还需要做一些简单的配置才能将服务注册到
Eureka Server
集群中。以hello-eureka-client
为例,修改application.properties
配置文件,将注册中心指向前面搭建的peer1
和peer2
,如下所示:
#为服务命名
spring.application.name=hello-eureka-client
#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
- 启动
hello-eureka-client
,访问http://peer1:1111/
和http://peer2:1112/
,可以看到hello-eureka-client
服务同时被注册到了peer1
和peer2
上。若此时断开peer1
,由于hello-eureka-client
服务同时也向peer2
注册,因此在peer2
上的其他服务依然能访问到hello-eureka-client
服务,从而实现了服务注册中心的高可用: - 如不想使用主机名来定义注册中心的地址,也可以使用
IP
地址的形式,但是需要在配置文件中增加配置参数eureka.instance.prefer-ip-address=true
,该值默认为false
。
服务发现与消费
下面来尝试构建一个服务消费者,它主要完成两个目标,发现服务以及消费服务。其中,服务发现的任务由Eureka
的客户端完成,而服务消费的任务由Ribbon
完成。
下面我们通过构建一个简单的示例,看看在Eureka
的服务治理体系下如何实现服务的发现与消费。
- 首先,我们做一些准备工作。启动之前实现的服务注册中心
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
- 访问
http://localhost:1111/
页面: - 创建一个
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>
- 修改应用主类
RibbonConsumerApplication
,通过@EnableDiscoveryClient
注解让该应用注册为Eureka
客户端应用,以获得服务发现的能力。同时,在该主类中创建RestTemplate
的Spring 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);
}
}
- 创建
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();
}
}
- 在
application.properties
中配置Eureka
服务注册中心的位置,需要与之前的hello-eureka-client
一样,不然是发现不了该服务的,同时设置该消费者的端口为9000
,不能与之前启动的应用端口冲突。
#为服务命名
spring.application.name=ribbon-consumer
#指定端口
server.port=9000
#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
- 启动
ribbon-consumer
应用后,访问http://localhost:1111/eureka/
页面,可以看到当前除了hello-eureka-client
之外,还多了我们实现的ribbon-consumer
服务。 - 通过向
http://localhost:9000/ribbon-consumer
发起GET
请求,成功返回了“Hello World
”。此时,我们可以在ribbon-consumer
应用的控制台中看到如下信息,Ribbon
输出了当前客户端维护的hello-eureka-client
的服务列表情况。其中包含了各个实例的位置,Ribbon
就是按照此信息进行轮询访问,以实现基于客户端的负载均衡。另外还输出了一些其他非常有用的信息,如对各个实例的请求总数量、第一次连接信息、上一次连接信息、总的请求失败数量等。
入门工程
在开始使用Spring Cloud Hystrix
实现断路器之前,我们先用之前实现的一些内容作为基础,构建一个如下图架构所示的服务调用关系:
我们在这里需要启动的工程有如下一些:
eureka-server
工程:服务注册中心,端口为1111
。hello-eureka-client
工程:hello-eureka-client
的服务单元,两个实例启动端口分别为8081
和8082
。ribbon-consumer
工程:使用Ribbon
实现的服务消费者,端口为9000
。
在未加入断路器之前,关闭8081
的实例,发送GET
请求到http://localhost:9000/ribbon-consumer
,页面显示如下:
下面我们开始引入Spring Cloud Hystrix
。
- 在
ribbon-consumer
工程的pom.xml
的dependency
节点中引入spring-cloud-starter-hystrix
依赖:
<!--Hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
- 在
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);
}
}
- 改造服务消费方式,新增
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";
}
}
- 修改
ConsumerController
类,注入上面实现的HelloService
实例,并在helloConsumer
中进行调用:
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET)
public String helloConsumer(){
return helloService.helloService();
}
}
- 下面来验证一下通过断路器实现的服务回调逻辑,确保此时服务注册中心、两个
hello-eureka-client
以及ribbon-consumer
均已启动,访问http://localhost:9000/ribbon-consumer
可以轮询两个hello-eureka-client
并返回一些文字信息。此时我们断开8081
的hello-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
毫秒,所以这里采用了0
至3000
的随机数以让处理过程有一定概率发生超时来触发断路器。
Hystrix仪表盘
现在在前面Hystrix
入门工程的基础上构建一个Hystrix Dashboard
来对ribbon-consumer
实现监控,完成后的架构如下图所示:
在Spring Cloud
中构建一个Hystrix Dashboard
非常简单,只需要下面4
步:
- 创建一个标准的
Spring Boot
工程,命名为hystrix-dashboard
。 - 编辑
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>
- 为应用主类加上
@EnableHystrixDashboard
,启用Hystrix Dashboard
功能。
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
- 根据实际情况修改
application.properties
配置文件,比如选择一个未被占用的端口等,此步不是必需的。
#为服务命名
spring.application.name=hystrix-dashboard
#指定端口
server.port=2001
到这里我们已经完成了基本配置,接下来可以启动该应用,并访问http://localhost:2001/hystrix
。可以看到如下页面:
这是Hystrix Dashboard
的监控首页,该页面中并没有具体的监控信息。
接下来我们实现单个服务实例的监控。
Hystrix Dashboard
监控单实例节点需要通过访问实例的/hystrix.stream
接口来实现,我们需要为服务实例添加这个端点,而添加该功能的步骤也同样简单,只需要下面两步:
- 在服务实例
pom.xml
的dependencies
节点中新增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>
- 确保在服务实例的主类中已经使用
@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
来进行展示,最后完成如下图所示的结构:
具体实现步骤如下:
- 创建一个标准的
Spring Boot
工程,命名为turbine
。 - 编辑
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>
- 创建应用主类
TurbineApplication
,并使用@EnableTurbine注解开启Turbine。
@EnableTurbine
@SpringBootApplication
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
- 在
application.properties
中加入Eureka
和Turbine
的相关配置,具体如下:
# 为服务命名
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
- 分别启动
eureka-server
、hello-eureka-client
、ribbon-consumer
、turbine
以及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
中。通过引入消息代理,我们的Turbine
和Hystrix Dashboard
实现的监控架构可以改成如下图所示的结构:
下面,我们来构建一个新的应用以实现基于消息代理的Turbine
聚合服务,具体步骤如下所示:
- 创建一个标准的
Spring Boot
工程,命名为turbine-amqp
。 - 编辑
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-stream
和spring-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);
}
}
- 配置
application.properties
文件。
# 为服务命名
spring.application.name=turbine
# 指定端口
server.port=8989
management.port=8990
# 指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
- 对于
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>
- 启动
eureka-server
、hello-eureka-client
、ribbon-consumer
、turbine
以及hystrix-dashboard
,同时确保RabbitMQ
已在正常运行。访问Hystrix Dashboard
,并开启对http://localhost:8989/turbine.stream
的监控,我们可以获得如之前实现的同样结果,只是这里的监控信息收集是通过消息代理异步实现的。
入门工程
下面的示例继续使用之前我们实现的hello-eureka-client
服务,这里我们会通过Spring Cloud Feign
提供的声明式服务绑定功能来实现对该服务接口的调用。
- 首先,创建一个
Spring Boot
基础工程,取名为feign-consumer
,并在pom.xml
中引入spring-cloud-starter-eureka
和spring-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>
- 创建应用主类
FeignConsumerApplication
,并通过@EnableFeignClients
注解开启Spring Cloud Feign
的支持功能。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class, args);
}
}
- 定义
HelloService
接口,通过@FeignClient
注解指定服务名来绑定服务,然后再使用Spring MVC
的注解来绑定具体该服务提供的REST
接口。
@FeignClient("hello-eureka-client")
public interface HelloService {
@RequestMapping("/hello")
String hello();
}
- 接着,创建一个
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();
}
}
- 最后,同
Ribbon
实现的服务消费者一样,需要在application.properties
中指定服务注册中心,并定义自身的服务名为feign-consumer
,为了方便本地调试与之前的Ribbon
消费者区分,端口使用9090
。
#为服务命名
spring.application.name=feign-consumer
#指定端口
server.port=9090
#指定服务注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
- 我们先启动服务注册中心以及两个
hello-eureka-client
,然后启动feign-consumer
,此时我们在Eureka
信息面板中可以看到如下内容: - 发送几次
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
应用中实现这些新增的请求的绑定。
- 首先,在
feign-consumer
中创建与上面一样的User
类。 - 然后,在
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
属性不能为空。
- 最后,在
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();
}
}
- 在完成上述改造之后,启动服务注册中心、两个
hello-eureka-client
服务以及我们改造过的feign-consumer
。通过发送GET
请求到http://localhost:9090/feign-consumer2
,触发HelloService
对新增接口的调用。最终,我们会获得如下输出,代表接口绑定和调用成功。
继承特性
当使用Spring MVC
的注解来绑定服务接口时,我们几乎完全可以从服务提供方的Controller
中依靠复制操作,构建出相应的服务客户端绑定接口。
在Spring Cloud Feign
中,提供了继承特性来帮助我们解决这些复制操作,以进一步减少代码量。下面看一下如何通过Spring Cloud Feign
的继承特性来实现REST
接口定义的复用。
- 为了能够复用
DTO
与接口定义,我们先创建一个基础的Maven
工程,命名为hello-service-api
。 - 由于在
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>
- 将前面实现的
User
对象复制到hello-service-api
工程的dto
包下。 - 创建
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-client
和feign-consumer
来重构,所以为了避免接口混淆,在这里定义HelloService
时,除了头部定义了/refactor
前缀之外,同时将提供服务的三个接口更名为/hello4
、/hello5
、/hello6
。
- 下面对
hello-eureka-client
进行重构,在pom.xml
的dependency
节点中,新增对hello-service-api
的依赖。
<dependency>
<groupId>com.study.springcloud</groupId>
<artifactId>hello-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 创建
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();
}
}
- 我们可以看到通过继承的方式,在
Controller
中不再包含以往会定义的请求映射注解@RequestMapping
,而参数的注解定义在重写的时候会自动带过来。在这个类中,除了要实现接口逻辑之外,只需再增加@RestController
注解使该类成为一个REST
接口类就大功告成了。 - 完成了服务提供者的重构,接下来在服务消费者
feign-consumer
的pom.xml
文件中,如在服务提供者中一样,新增对hello-service-api
的依赖。 - 创建
RefactorHelloService
接口,并继承hello-service-api
包中的HelloService
接口,然后添加@FeignClient
注解来绑定服务。
@FeignClient("hello-eureka-client")
public interface RefactorHelloService extends HelloService {
}
- 最后,在
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();
}
- 注意必须先构建
hello-service-api
工程,然后再构建hello-eureka-client
和feign-consumer
。接着我们分别启动服务注册中心,hello-eureka-client
和feign-consumer
,并访问http://localhost:9090/feign-consumer3
,调用成功后可以获得如下输出:
使用Spring Cloud Feign
继承特性的优点很明显,可以将接口的定义从Controller
中剥离,同时配合Maven
私有仓库就可以轻易地实现接口定义的共享,实现在构建期的接口绑定,从而有效减少服务客户端的绑定配置。
这么做虽然可以可以很方便地实现接口定义和依赖的共享,不用再复制粘贴接口进行绑定,但是这样的做法使用不当的话会带来副作用。
由于接口在构建期间就建立起了依赖,那么接口变动就会对项目构建造成影响,可能服务提供方修改了一个接口定义,那么会直接导致客户端工程的构建失败。所以,如果开发团队通过此方法来实现接口共享的话,建议在开发评审期间严格遵守面向对象的开闭原则,尽可能地做好前后版本的兼容,防止牵一发而动全身的后果,增加团队不必要的维护工作量。
重试机制
在Spring Cloud Feign
中默认实现了请求的重试机制。我们可以通过修改之前的示例做一些验证。
- 在
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";
}
- 在
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
- 最后,启动这些应用,并尝试访问几次
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
实例,详细实现步骤如下所示:
- 构建一个关闭
Hystrix
的配置类。
@Configuration
public class DisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder(){
return Feign.builder();
}
}
- 在
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
工程中进行改造。
- 编辑
pom.xml
,加入Hystrix
依赖:
<!--Hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
- 修改
application.properties
配置文件,加入如下内容:
# 开启Feign客户端的Hystrix支持
feign.hystrix.enabled=true
- 服务降级逻辑的实现只需要为
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";
}
}
- 在服务绑定接口
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);
}
- 如果你的项目中有前面的
DisableHystrixConfiguration
类,全部记得注掉,否则会禁用Hystrix
。 - 启动服务注册中心和
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.Level
的Bean
创建,具体如下:
@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.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);
}
在调整日志级别为FULL
之后,我们可以再访问一下之前的http://localhost:9090/feign-consumer
接口,这时我们在feign-consumer
的控制台中就可以看到类似下面的请求详细日志:
对于Feign
的Logger
级别主要有下面4
类,可根据实际需要进行调整使用。
NONE
:不记录任何信息。BASIC
:仅记录请求方法、URL
以及响应状态码和执行时间。HEADERS
:除了记录BASIC
级别的信息之外,还会记录请求和响应的头信息。FULL
:记录所有请求与响应的明细,包括头信息、请求体、元数据等。
构建网关
首先,在实现各种API
网关服务的高级功能之前,我们需要做一些准备工作,比如,构建起最基本的API
网关服务,并且搭建几个用于路由和过滤使用的微服务应用等。对于微服务应用,我们可以直接使用之前实现的hello-eureka-client和feign-consumer
。虽然之前我们一直将feign-consumer
视为消费者,但是在Eureka
的服务注册与发现体系中,每个服务既是提供者也是消费者,所以feign-consumer
实质上也是一个服务提供者。之前我们访问的http://localhost:9090/feign-consumer
等一系列接口就是它提供的服务。
接下来,我们详细介绍一下API
网关服务的构建过程。
- 创建一个基础的
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
端点来返回当前的所有路由规则。
- 创建应用主类,使用
@EnableZuulProxy
注解开启Zuul
的API
网关服务功能。
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
- 在
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/
面向服务的路由
很显然,传统路由的配置方式对于我们来说并不友好,它同样需要运维人员花费大量的时间来维护各个路由path
与url
的关系。为了解决这个问题,Spring Cloud Zuul
实现了与Spring Cloud Eureka
的无缝整合,我们可以让路由的path
不是映射具体的url
,而是让它映射到某个具体的服务,而具体的url
则交给Eureka
的服务发现机制去自动维护,我们称这类路由为面向服务的路由。在Zuul
中使用服务路由也同样简单,只需做下面这些配置。
- 为了与
Eureka
整合,我们需要在api-gateway
的pom.xml
中引入spring-cloud-starter-eureka
依赖,具体如下:
<!--Eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
- 在
api-gateway
的application.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/
- 将
eureka-server
、hello-eureka-client
、feign-consumer
以及api-gateway
都启动起来。在Eureka
信息面板中,我们可以看到,除了hello-eureka-client
和feign-consumer
之外,多了一个网关服务api-gateway
。 - 此时我们已经可以通过服务网关来访问
hello-eureka-client
和feign-consumer
这两个服务了。根据配置的映射关系,向网关发起http://localhost:5555/api-a/hello
请求,该url
符合/api-a/**
规则,由api-a
路由负责转发,该路由映射的serviceId
为hello-eureka-client
,所以最终/hello
请求会被发送到hello-eureka-client
服务的某个实例上去,发起http://localhost:5555/api-b/feign-consumer
请求也类似。
通过面向服务的路由配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的path
与serviceId
的映射组合,使得维护工作变得非常简单。这完全归功于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 Boot
工程,命名为config-server
,并在pom.xml
中引入下面的依赖:
<!--Config Server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
- 创建
Spring Boot
的程序主类,并添加@EnableConfigServer
注解,开启Spring Cloud Config
的服务端功能。
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
- 在
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
- 启动
config-server
。
客户端配置映射
- 在上面配置的
Git
仓库下创建一个config-repo
目录作为配置仓库,并根据不同环境新建下面4
个配置文件: - 在这
4
个配置文件中均设置一个from
属性,并为每个配置文件分别设置不同的值,如下所示:
from=git-dev-1.0
from=git-prod-1.0
from=git-test-1.0
from=git-default-1.0
- 此时我们已经可以通过浏览器、
POSTMAN
或CURL
等工具直接来访问我们的配置内容了。访问配置信息的URL
与配置文件的映射关系如下所示:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
- 下面尝试在微服务应用中获取上述配置信息。首先,创建一个
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>
- 创建
Spring Boot
的应用主类,具体如下:
@SpringBootApplication
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
}
- 创建
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;
}
}
- 除了通过
@Value
注解绑定注入之外,也可以通过Environment
对象来获取配置属性,比如:
@RefreshScope
@RestController
public class TestController {
@Autowired
private Environment env;
@RequestMapping("/from")
public String from(){
return env.getProperty("from", "undefined");
}
}
- 启动
config-client
应用,并访问http://localhost:7002/from
,我们就可以根据配置内容输出对应环境的from
内容了。可以继续通过修改bootstrap.properties
中的配置内容获取不同的配置信息来熟悉配置服务中的配置规则。
服务化配置中心
下面将基于前面实现的config-server
和config-client
工程来进行改造实现,将Config Server
注册到服务中心,并通过服务发现来访问Config Server
并获取Git
仓库中的配置信息。
- 在
config-server
的pom.xml
中增加spring-cloud-starter-eureka
依赖,以实现将分布式配置中心加入Eureka
的服务治理体系。
<!--Eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
- 在
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
- 在应用主类中,新增
@EnableDiscoveryClient
注解,用来将config-server
注册到上面配置的服务注册中心上去。
@EnableDiscoveryClient
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
- 启动该应用,并访问
http://localhost:1111/
,可以在Eureka Server
的信息面板中看到config-server
已经被注册了。 - 在
config-client
的pom.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>
- 在
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
- 在应用主类中,新增
@EnableDiscoveryClient
注解,用来发现config-server
服务,利用其来加载应用配置:
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
}
- 沿用之前我们创建的
Controller
来加载Git
中的配置信息:
@RefreshScope
@RestController
public class TestController {
@Value("${from}")
private String from;
@RequestMapping("/from")
public String from(){
return this.from;
}
}
- 启动该客户端应用,访问
http://localhost:1111/
,可以在Eureka Server
的信息面板中看到该应用已经被注册成功。 - 访问客户端应用提供的服务
http://localhost:7002/from
,此时,我们会返回在Git
仓库中configstudy-dev.properties
文件中配置的from
属性内容:from=git-dev-1.0
。