上一篇:Spring Cloud微服务基础一 (Eureka和Ribbon)
本文使用的开发环境:
在微服务架构中,每个服务单元都是单独部署,单独运行,服务之间通过远程调用实现信息交互,那么当某个服务的响应太慢或者故障,又或者因为网络波动或故障,则会造成调用者延迟或调用失败,当大量请求到达,则会造成请求的堆积,导致调用者的线程挂起,从而引发调用者也无法响应,调用者也发生故障。
所以在微服务架构中,很容易造成服务故障的蔓延,引发整个微服务系统瘫痪不可用。为了解决此问题,微服务架构中引入了一种叫熔断器的服务保护机制。
熔断器也叫断路器,就是当被调用方没有响应,调用方直接返回一个错误响应即可,而不是长时间的等待,这样避免调用时因为等待而线程一直得不到释放,避免故障在分布式系统间蔓延。
Spring Cloud Hystrix
实现了熔断器、线程隔离等一系列服务保护功能。该功能也是基于Netflix的开源框架Hystrix实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。
在SpringCloud
的服务消费者中使用熔断器Hystrix
是非常简单和方便的,只需要简单三步即可:
在创建服务消费者时,勾选hystrix
依赖:Spring Cloud Circuit Breaker --> Hystrix [Maintenance]
也可以手动往pom.xml文件中添加
Hystrix
的依赖:<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-hystrixartifactId> dependency>
如果手动添加依赖,必须添加Maven的依赖管理器
<properties> <spring-cloud.version>Hoxton.SR1spring-cloud.version> properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-dependenciesartifactId> <version>${spring-cloud.version}version> <type>pomtype> <scope>importscope> dependency> dependencies> dependencyManagement>
注意:如果通过SpringBoot的开发工具创建Web工程那么这个依赖以及依赖管理是自动添加的
在入口类中使用@EnableCircuitBreaker
注解开启断路器功能,也可以使用一个@SpringCloudApplication
注解代替主类上的三个注解。
注解
@SpringCloudApplication
是三个注解的集合,包括:@SpringBootApplication
、@EnableDiscoveryClient
、@EnableCircuitBreaker
,分别是SpringBoot注解、注册服务中心Eureka注解、断路器注解。对于SpringCloud来说,这是每一微服务必须应有的三个注解,所以才推出了@SpringCloudApplication这一注解集合。
在调用远程服务的方法上添加注解:@HystrixCommand
@HystrixCommand
注解用于标记当前方法使用了 Hystrix
的服务熔断机制,当服务的提供者出现异常或超时都会进行熔断。
fallbackMethod
:fallbackMethod
参数用于指定当服务熔断之后,使用哪个方法的返回值来替代服务提供者的返回信息,取值为当前类中的某个方法名,该方法又叫异常熔断方法。定义的异常熔断方法中,可以通过指定形参 Throwable
来获取程序抛出的异常,该异常可能是服务提供者抛出的,也可能是服务消费者抛出的。
ignoreExceptions
:指定一个数组,该数组中是需要忽略的异常类型,当服务的提供者抛出了某个异常以后,如果这个异常我们不需要进行服务的降级,那么我们就可以指定这个参数来忽略这个异常,异常将会抛给页面。取值为Throwable
类的子类class
,例如NullPointerException.class
或RuntimeException.class
注意:通常情况下我们不能将程序的异常直接抛到用户面前,所以通常情况我们不需要指定ignoreExceptions
参数
commandProperties
:指定Hystrix
的一些属性,取值为一个数组,其中@HystrixProperty
注解用于指定Hystrix
的某个属性
timeoutInMilliseconds
:表示超时时间,单位为毫秒,默认为1000毫秒,如果你后端的响应超过此时间,就会触发断路器;一个小例子:
@RestController
public class TestController {
@Resource
private RestTemplate restTemplate;
@RequestMapping("/test")
@HystrixCommand(fallbackMethod="error",
//ignoreExceptions = NullPointerException.class,
commandProperties={
/*
timeoutInMilliseconds属性表示超时时间 5000表示5秒的等待超时,
如果指定服务提供者是超过5秒钟还没有做出响应,则熔断当前服务的调用
*/
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="5000")
}
)
public String test(){
//注意:这里使用了服务名从Eureka中获取服务地址因此必须修改RestTemplateConfig中的内容
ResponseEntity<String> result = restTemplate.getForEntity("http://SERVER-PROVIDER/test", String.class);
return "Hystrix的服务消费者-----" + result.getBody();
}
//自定义的异常熔断方法,方法名可以任意,主要用于当服务熔断以后来替代服务提供者的响应数据
//我们可以在熔断方法中添加一个Throwable的参数用来获取因程序抛出异常后熔断服务的异常原因
//注意:
// 1、这个异常通常来讲需要利用日志插件(log4j等)将异常信息记录到日志中, 这里只做简单的输出
// 2、这个异常可能来自服务的提供者也能来自我们服务消费本身
public String error(Throwable throwable) {
//通常要打印到日志文件中,这里只做简单的输出处理
System.out.println(throwable.getMessage());
return "服务繁忙请稍后再试";
}
}
有了服务的熔断,随之就会有服务的降级,所谓服务降级,就是当某个服务熔断之后,服务端提供的服务将不再被调用,此时由客户端自己准备一个本地的 fallback 回调,返回一个默认值来代表服务端的返回;
这种做法,虽然不能得到正确的返回结果,但至少保证了服务的可用,比直接抛出错误或服务不可用要好很多,当然这需要根据具体的业务场景来选择;
在HystrixCommand实现的run()方法(后面会讲)抛出异常时,除了HystrixBadRequestException之外,其他异常均会被Hystrix认为命令执行失败并处罚服务降级的处理逻辑。
失败类型 | 抛出的异常 | 异常原因 | 是否会被fallback |
---|---|---|---|
FAILURE | HystrixRuntimeException | 执行失败 | 是 |
TIMEOUT | HystrixRuntimeException | 执行超时 | 是 |
SHORT_CIRCUITED | HystrixRuntimeException | 断路器打开 | 是 |
THREAD_POOL_REJECTED | HystrixRuntimeException | 线程池拒绝 | 是 |
SEMAPHORE_REJECTED | HystrixRuntimeException | 信号量拒绝 | 是 |
BAD_REQUEST | HystrixBadRequestException | 一般是由非法参数或者一些非系统异常引起的 | 否 |
我们也可以自定义类继承 HystrixCommand
来实现自定义的 Hystrix
请求,在 getFallback
方法中调用getExecutionException
方法来获取服务抛出的异常;
自定义熔断器类:
public class MyHystrixCommand extends HystrixCommand {
private RestTemplate restTemplate;
private String url;
public MyHystrixCommand(Setter setter, RestTemplate restTemplate, String url) {
super(setter);
this.restTemplate = restTemplate;
this.url = url;
}
//这个方法不需要手动调用也不能手动调用,如果手动调用了以后就不能进行服务熔断
protected Object run() throws Exception {
return restTemplate.getForEntity(url, String.class).getBody();
}
//异常熔断方法,用于返回熔断后的响应数据来替代服务提供者的响应信息
protected Object getFallback() {
//如果服务因为异常而熔断的则用于获取错误的异常信息对象
Throwable throwable = super.getExecutionException();
//返回熔断的具体数据内容
return "服务被熔断了";
}
}
Controller中调用自定义熔断类:
@RestController
public class TestController {
@Resource
private RestTemplate restTemplate;
@RequestMapping("/test1")
public Object test1() {
String url = "http://10-EUREKA-CLIENT-HYSTRIX-PROVIDER/test";
MyHystrixCommand command = new MyHystrixCommand(com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")), restTemplate, url);
Object result = command.execute();
return result;
}
}
Hystrix
仪表盘(Hystrix Dashboard
),就像汽车的仪表盘实时显示汽车的各项数据一样,Hystrix
仪表盘主要用来监控Hystrix
的实时运行状态,通过它我们可以看到Hystrix
的各项指标信息,从而快速发现系统中存在的问题进而解决它。
要使用Hystrix
仪表盘功能,我们首先需要有一个Hystrix Dashboard
,这个功能我们可以在原来的消费者应用上添加,让原来的消费者应用具备Hystrix仪表盘功能,但一般地,微服务架构思想是推崇服务的拆分,Hystrix Dashboard也是一个服务,所以通常会单独创建一个新的工程专门用做Hystrix Dashboard服务;
创建一个Spring Boot模块,勾选相关依赖:
Spring Cloud Circuit Breaker --> Hystrix Dashboard [Maintenance]
Web --> Spring Web
也可以直接添加Hystrix Dashboard的依赖:
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId> dependency>
如果手动添加依赖,必须添加Maven的依赖管理器
<properties> <spring-cloud.version>Hoxton.SR1spring-cloud.version> properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-dependenciesartifactId> <version>${spring-cloud.version}version> <type>pomtype> <scope>importscope> dependency> dependencies> dependencyManagement>
注意:如果通过SpringBoot的开发工具创建Web工程那么这个依赖以及依赖管理是自动添加的
在入口类上添加@EnableHystrixDashboard
注解开启仪表盘功能,如下:
@SpringBootApplication
@EnableHystrixDashboard
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置application.properties文件:只需要修改一下端口号即可:
server.port=3721
至此,Hystrix监控环境就搭建好了。启动服务,访问地址(ip + 端口 + /hystrix
):localhost:3721/hystrix
,进入如下页面:
通过Hystrix Dashboard主页面的文字介绍,我们可以知道,Hystrix Dashboard共支持三种不同的监控方式
默认的集群监控:通过URL:http://turbine-hostname:port/turbine.stream
开启,实现对默认集群的监控。
指定的集群监控:通过URL:http://turbine-hostname:port/turbine.stream?cluster=[clusterName]
开启,实现对clusterName集群的监控。
单体应用的监控:通过URL:http://hystrix-app:port/hystrix.stream
开启,实现对具体某个服务实例的监控。
首先,要有一个Eureka的服务注册中心和一个服务提供者,可以参考上一篇博客:Spring Cloud微服务基本使用(Eureka和Ribbon)
然后重新搭建服务消费者或者是修改服务消费者:
在构建服务的时候(Spring Boot),需要另外勾选 spring boot 的服务监控依赖和 hystrix
依赖:
Spring Cloud Circuit Breaker --> Hystrix Dashboard [Maintenance]
Ops --> Spring Boot Actuator
也可以直接在现有项目上添加这两个依赖(注意版本):
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-actuatorartifactId> dependency> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-hystrixartifactId> dependency>
如果手动添加依赖,必须添加Maven的依赖管理器
<properties> <spring-cloud.version>Hoxton.SR1spring-cloud.version> properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-dependenciesartifactId> <version>${spring-cloud.version}version> <type>pomtype> <scope>importscope> dependency> dependencies> dependencyManagement>
注意:如果通过SpringBoot的开发工具创建Web工程那么这个依赖以及依赖管理是自动添加的
配置文件需要配置spring boot监控端点的访问权限:
management.endpoints.web.exposure.include=*
这个是用来暴露 endpoints 的,由于 endpoints 中会包含很多敏感信息,除了 health 和 info 两个支持直接访问外,其他的默认不能直接访问,所以我们让它都能访问。或者指定:
management.endpoints.web.exposure.include=hystrix.stream
访问入口:http://ip地址:端口号/actuator/hystrix.stream
,这个地址是填在仪表盘页面第一个输入框中的,剩下两个输入框随意指定。
注意:如果出现一直Loading...
的情况,则先访问服务中的任意一个其他接口,再访问 /actuator/hystrix.stream
接口。
Feign是Netflix公司开发的一个声明式的REST调用客户端;
Ribbon负载均衡、Hystrix服务熔断是我们Spring Cloud中进行微服务开发非常基础的组件,在使用的过程中我们也发现它们一般都是同时出现的,而且配置也都非常相似,每次开发都有很多相同的代码,因此 Spring Cloud 基于Netflix Feign
整合了 Ribbon
和 Hystrix
两个组件,让我们的开发工作变得更加简单,就像 Spring Boot 是对 Spring+SpringMVC 的简化一样,Spring Cloud Feign 对 Ribbon
负载均衡、Hystrix
服务熔断进行简化,在其基础上进行了进一步的封装,不仅在配置上大大简化了开发工作,同时还提供了一种声明式的Web服务客户端定义方式;
创建一个Spring Boot模块,勾选相关依赖:
Spring Cloud Routing --> OpenFeign
Spring Cloud Discovery --> Eureka Server
Spring Cloud Circuit Breaker --> Hystrix [Maintenance]
Web --> Spring Web
也可以直接添加相关的依赖,这里只列出了 Feign 的依赖,其它的依赖之前都有列举过,之后就不再赘述了:
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-openfeignartifactId> dependency>
在项目入口类上添加 @EnableFeignClients
注解表示开启 Spring Cloud Feign 的支持功能;
定义一个接口用来声明服务
@FeignClient
注解来标记当前接口是 Feign
的一个客户端接口,并通过 name
属性或者 value
属性指定服务提供者的服务名称(相当于是当前接口与一个服务提供者绑定,接口中的一个方法对应着服务提供者中的一个方法,调用接口中的方法就是调用服务提供者中对应的方法)。@RequestMapping
注解的 value
属性指定访问的服务提供者中服务的具体请求名称spring.application.name=server-provider
):@RestController
public class Controller {
@RequestMapping("/test")
public String test() {
return "Hello Spring Cloud!,This is Feign";
}
}
//@FeignClient 注解作用是标记当前接口是Feign的一个客户端接口
//属性name 用于指定服务提供者的服务名称
@FeignClient(name ="server-provider")
public interface TestService {
//标记当前方法用于请求远程服务提供者
//属性 /test 用于指定需要访问的服务的具体请求名称
@RequestMapping("/test")
String test();
}
接下来,就可以在 Controller 中调用服务提供者中的服务了:
@RestController
public class MyController {
@Autowired
private TestService testService;
@RequestMapping("/web")
public String web() {
return testService.test();
}
}
实际上 @FeignClient
注解会对这个接口生成动态代理,从eureka的readonly中拿到其他服务信息,进行http请求调用。
Feign
默认集成了 Ribbon
,并和 Eureka
结合,默认实现了负载均衡的效果。
默认的负载均衡策略是轮询,若要修改负载均衡策略,详见上一篇博客中的 Ribbon 的配置
在 application.properties
文件开启 feign
对 hystrix
功能支持(支持熔断),默认不支持
feign.hystrix.enabled=true
自定义一个服务熔断器类,该类实现了Feign
的客户端接口,并实现了接口中的方法。当接口在调用服务提供者时被熔断了,则会使用该类中实现的方法来替代服务提供的者的返回信息。
//自定义一个服务熔断器类,并实现Feign的客户端接口
@Component //将当前类定义到Spring上下文容器中
public class MyFallBack implements TestService {
//服务熔断方法,当TestService接口中的这个方法在执行时被熔断了则使用这个方法的返回值
//来替代服务提供的者的返回信息
public String test() {
return "服务被异常熔断";
}
}
在对应的Feign的客户端接口的 @FeignClient
注解中,通过 fallback
属性指定使用的回调类,例如上面写的Feign客户端接口:
//通过 fallback 属性指定熔断之后的回调类
@FeignClient(name = "server-provider", fallback = MyFallback.class)
public interface TestService {
@RequestMapping("/test")
String test();
}
刚才已经实现了远程服务发生异常后可以进行服务的熔断,但是不能获取到远程服务的异常信息,如果要获取远程服务的异常信息,就要使用fallbackFactory
FallbackFactory
接口,范型
为 Feign
的客户端接口FallbackFactory
接口中的 create
方法,该方法的形参为 Throwable
,返回值类型为指定的范型 T
Feign
客户端接口的 @FeignClient
注解中,使用 fallbackFactory
属性指定异常熔断工厂类。自定义熔断工厂类:
//自定义异常熔断的熔断器工厂类
//需要实现父接口FallbackFactory 并指定范型为某个Feign的客户端接口
//表示为某个接口进行服务熔断
@Component
public class MyFallbackFactory implements FallbackFactory<TestService> {
//create来自父接口用于创建具体的服务熔断对象参数为异常的对象,可以利用这个对象
//来实现异常信息的获取
public TestService create(Throwable throwable) {
return new TestService() {
@Override
public String test() {
//对异常信息进行处理……
return "服务异常熔断" + throwable.getMessage();
}
};
}
}
Feign
客户端接口:
//通过 fallbackFactory 属性指定熔断之后的回调工厂类
@FeignClient(name = "server-provider", fallbackFactory = MyFallbackFactory.class)
public interface TestService {
@RequestMapping("/test")
String test();
}
通过掌握 Eureka、Ribbon、Hystrix、Feign,我们已经可以基本搭建出一套简略版的微服务架构了,如下图:
在上面的架构图中,我们的服务包括:内部服务Service A
和内部服务Service B
,这两个服务都是集群部署,每个服务部署了3个实例,他们都会通过Eureka Server
注册中心注册与订阅服务,而Open Service
是一个对外的服务,也是集群部署,外部调用方通过负载均衡设备调用Open Service
服务,比如负载均衡使用Nginx,这样的实现是否合理,或者是否有更好的实现方式呢?接下来我们主要围绕该问题展开讨论。
1、如果我们的微服务中有很多个独立服务都要对外提供服务,那么我们要如何去管理这些接口?特别是当项目非常庞大的情况下要如何管理?
2、在微服务中,一个独立的系统被拆分成了很多个独立的服务,为了确保安全,权限管理也是一个不可回避的问题,如果在每一个服务上都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也就太大了,而且维护也非常不方便。
为了解决上述问题,微服务架构中提出了API
网关的概念,它就像一个安检站一样,所有外部的请求都需要经过它的调度与过滤,然后API
网关来实现请求路由、负载均衡、权限验证等功能;
那么Spring Cloud这个一站式的微服务开发框架基于Netflix Zuul
实现了Spring Cloud Zuul
,采用Spring Cloud Zuul
即可实现一套API
网关服务。
创建一个普通的 Spring Boot 模块,在依赖项处勾选相关依赖:
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-zuulartifactId> dependency>
在入口类上添加 @EnableZuulProxy
注解,开启Zuul
的API
网关服务功能。
配置 application.properties
主要是配置 Eureka 和 Zuul 的路由规则,关于 Zuul 的路由规则,后面会细讲。
#配置服务内嵌的Tomcat端口
server.port=8083
#配置服务的名称
spring.application.name=zuul
#配置路由规则 zuul.routes.api-zuul.path,其中 api-zuul 可以任意填写
# /api-zuul/** 表示请求的拦截规则为:以/api-zuul开头的任意目录以及子孙目录中所有请求都会被拦截
zuul.routes.api-zuul.path=/api-zuul/**
#指向服务名字 用于对这个服务下的某个写特定请求进行拦截
# zuul.routes.api-zuul.serviceId,其中 api-zuul 要与上面配置的路由规则对应
zuul.routes.api-zuul.serviceId=zuul-consumer-api-zuul
#配置API网关到注册中心上,API网关也将作为一个服务注册到eureka-server上
eureka.client.service-url.defaultZone=http://localhost:9100/eureka
注意:zuul.routes.api-zuul.path
和 zuul.routes.api-zuul.serviceId
中,配置的 api-zuul
是路由的名字,可以任意定义,但是一组 path
和serviceId
映射关系的路由名要相同。例如,也可以写成:zuul.routes.zuul-url.path
和 zuul.routes.zuul-url.serviceId
等。
以上配置,我们的路由规则就是匹配所有符合/api-zuul/**
的请求,只要路径中带有/api- zuul /
都将被转发到 zuul-consumer-api-zuul
服务上,至于 zuul-consumer-api-zuul
服务的地址到底是什么,则由eureka-server
注册中心去分析,我们只需要写上服务名即可。
测试,从浏览器访问网关 http://localhost:8083/api-zuul/test
,则相当于访问了 http://zuul-consumer-api-zuul/test
(zuul-consumer-api-zuul
是服务名)。
前面讲到的路由配置如下:
#配置路由规则
zuul.routes.api-zuul.path=/api-zuul/**
zuul.routes.api-zuul.serviceId=zuul-consumer-api-zuul
当访问地址符合 /api-zuul/**
规则的时候,会被自动定位到 zuul-consumer-api-zuul
服务上,不过两行代码有点麻烦,还可以简化为:
zuul.routes.zuul-consumer-api-zuul=/api-zuul/**
zuul.routes
后面跟着的是服务名,服务名后面跟着的是路径规则,这种配置方式更简单。
如果我们不希望拦截某些请求,比如说我不需要拦截/hello
接口路由,那么可以按如下方式配置:
#过滤某些请求路径,过滤掉之后,API网关不会对这些请求进行任何的拦截处理
zuul.ignored-patterns=/**/hello/**
我们可以统一的为路由规则增加前缀,设置方式如下:
#为API网关添加统一访问前缀
zuul.prefix=/myapi
此时我们的访问路径就变成了http://localhost:8080/myapi/api-zuul/test
通配符 | 含义 | 举例 | 说明 |
---|---|---|---|
? | 匹配任意单个字符 | /xxx/? | 匹配 /xxx/a, /xxx/b, /xxx/c等 |
* | 匹配任意数量的字符 | /xxx/* | 匹配 /xxx/aaa,/xxx/bbb,/xxx/ccc等,无法匹配 /xxx/a/b/c |
** | 匹配任意数量的字符 | /xxx/** | 匹配 /xxx/aaa,/xxx/bbb,/xxx/ccc等,也可以匹配 /xxx/a/b/c |
API
网关后,再转发给自己本身一般情况下API
网关只是作为各个微服务的统一入口,但是有时候我们可能也需要在API
网关服务上做一些特殊的业务逻辑处理,那么我们可以让请求到达API
网关后,再转发给自己本身,由API
网关自己来处理,那么我们可以进行如下的操作:
首先在 Zuul 中创建一个 Controller:
@RestController
public class ApiController {
@RequestMapping("/apitest")
public String apiTest() {
return "这是api网关处理的请求";
}
}
在 application.properties 中进行配置:
# 拦截以 /geteway 开头的任意请求并转发到当前Api网关自身中的某个Controller中,而不是转发给其他的服务
zuul.routes.gateway.path=/gateway/**
# Api自身处理请求的Controller路径。
# 路径http://localhost:8083/gateway/apitest相当于http://localhost:8083/apitest
# 如果zuul.routes.gateway.url设置成了:forward:/apitest,则请求路径为http://localhost:8083/gateway,相当于http://localhost:8083/apitest
zuul.routes.gateway.url=forward:/
测试:通过访问 http://localhost:8083/gateway/apitest
即可访问到API内部的业务逻辑处理
Spring cloud Zuul就像一个安检站,所有请求都会经过这个安检站,所以我们可以在该安检站内实现对请求的过滤。
ZuulFilter
,并将该Filter
作为一个Bean
,ZuulFilter
类中重要的四个方法:
filterType
方法的返回值为 String
类型,表示过滤器的类型,过滤器的类型决定了过滤器在哪个生命周期执行,pre
表示在路由之前执行过滤器,其他值还有post
、error
、route
和static
,当然也可以自定义。filterOrder
方法表示过滤器的执行顺序,当过滤器很多时,我们可以通过该方法的返回值来指定过滤器的执行顺序。filterType
相同时,数字越小,越先执行,可以是负数。shouldFilter
方法用来判断过滤器是否执行,true
表示执行,false
表示不执行。run
方法则表示过滤的具体逻辑,如果请求地址中携带了token
参数的话,则认为是合法请求,否则为非法请求,如果是非法请求的话,首先设置 setSendZuulResponse(false)
,表示不对该请求进行路由,然后设置响应码和响应值。这个run方法的返回值目前暂时没有任何意义,可以返回任意值。//自定义请求的网关过滤器
@Component
public class AuthFilter extends ZuulFilter {
//定义当前过滤器的类型 pre表示在执行请求之前拦截过滤
public String filterType() {
return "pre";
}
//过滤器的排序,如果有多个按照返回值大小依次执行
public int filterOrder() {
return 0;
}
//是否启动当前过滤器 返回true表示启用
public boolean shouldFilter() {
return true;
}
//具体的请求拦截方法 返回值没有意义
public Object run() throws ZuulException {
//定义并获取请求上下文对象, 这个对象来自Zuul的依赖包
RequestContext requestContext = RequestContext.getCurrentContext();
//获取用户请求对象
HttpServletRequest request = requestContext.getRequest();
//获取请求身份令牌
String token = request.getParameter("token");
if (!"123".equals(token)) {//进入if表示当前请求没有身份令牌或令牌错误,表示请求非法
//设置false表示请求非法,不会继续执行请求,而是立即返回
requestContext.setSendZuulResponse(false);
//设置响应状态码为401,表示请求非法
requestContext.setResponseStatusCode(401);
//设置为用户返回响应数据的编码格式
requestContext.addZuulResponseHeader("content-type", "text/html;charset=utf-8");
//设置为用户返回的具体响应信息
requestContext.setResponseBody("用户请求非法");
} else {
System.out.println("请求合法可以访问---------------继续访问服务提供者");
}
return null;
}
}
Spring Cloud Zuul 对异常的处理是非常方便的,但是由于Spring Cloud处于迅速发展中,各个版本之间有所差异,本案例是以Hoxton.RC2
版本为例,来说明Spring Cloud Zuul中的异常处理问题。
首先我们来看一张官方给出的Zuul请求的生命周期图:
pre filter
:在请求路由到目标之前执行。一般用于请求认证、负载均衡和日志记录。routing filter
:处理目标请求。这里使用Apache HttpClient或Netflix Ribbon构造对目标的HTTP请求。post filter
:在目标请求返回后执行。一般会在此步骤添加响应头、收集统计和性能数据等。errot filter
:整个流程某块出错时执行。他们之间的关系:
pre
、route
、post
的顺序来执行,然后由post
返回response
pre
阶段,如果有自定义的过滤器则执行自定义的过滤器pre
、routing
、post
的任意一个阶段如果抛异常了,则执行error
过滤器对于 error
过滤器,我们可以有两种方式统一处理异常:
zuul
默认的异常处理SendErrorFilter
过滤器,然后自定义我们自己的error filter
过滤器error
错误页面注意:自定义异常的过滤器和全局error
错误页面有冲突,二选一即可
禁用 Zuul 的默认error
过滤器,然后才能自定义异常过滤器类
zuul.SendErrorFilter.error.disable=true
自定义Error过滤器
@Component
public class ErrorFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
//指定过滤器类型
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException)context.getThrowable();
logger.error("进入系统异常拦截", exception);
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
//exception.nStatusCode 获取状态码
response.setStatus(exception.nStatusCode);
PrintWriter writer = null;
try {
writer = response.getWriter();
//exception.getMessage获取异常信息,exception.errorCause获取异常原因
writer.print("{code:"+ exception.nStatusCode +",message:\""+ exception.getMessage() +"\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer!=null){
writer.close();
}
}
} catch (Exception var5) {
ReflectionUtils.rethrowRuntimeException(var5);
}
return null;
}
}
测试,当之前自定义的 pre
过滤器(AuthFilter )出现异常时,便会执行 error
过滤器
自定义一个 Controller
实现异常控制器的父接口 ErrorController
,并实现getErrorPath()
方法,该方法的返回值是一个String
类型的请求地址,在这个请求地址中进行异常处理。当程序抛出异常之后,将会执行对应的请求路径。
注意:Zuul 的默认error
过滤器需要打开(上一个方法是需要关闭的),默认是打开的,或者使用下面的设置
zuul.SendErrorFilter.error.disable=false
小例子:
//自定义异常控制器,并实现异常控制器的父接口,这个接口是SpringBoot提供的
@RestController
public class MyErrorController implements ErrorController {
//如果当前工程抛出异常以后,则利用这个方法的返回值所对应的请求来显示错误信息
@Override
public String getErrorPath() {
return "/error";
}
//在这个请求地址中进行异常处理
@RequestMapping("/error")
public String error() {
//全局异常页面可以使用Json的方式显示错误信息,也可以利用Thymeleaf显示一个真正的html页面
return "这是一个全局异常页面";
}
}