1 Hystix
1.1 简介
Hystix,即熔断器。
主页:https://github.com/Netflix/Hystrix/ 如图所示:
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。如图所示:
1.2. 熔断器的工作机制
如图所示:
正常工作的情况下,客户端请求调用服务API接口。如图所示:
当有服务出现异常时,直接进行失败回滚,服务降级处理。如图所示:
1.3 动手实践
1.3.1.引入依赖
1.首先在user-consumer中引入Hystix依赖的代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
如图所示:
2.在主启动类添加如图所示的注解;
3.编写UserDao的代码如下:
package com.txw.consumerdemo.dao;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.txw.consumerdemo.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* 用户持久层
* @author Adair
* E-mail: 1578533828@qq.com
*/
@SuppressWarnings("all") // 注解警告信息
@Component
public class UserDao {
@Autowired
private RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
@HystrixCommand(fallbackMethod = "queryUserByIdFallback")
public User queryUserById(Long id){
long begin = System.currentTimeMillis();
String url = "http://user-service/user/" + id;
User user = this.restTemplate.getForObject(url, User.class);
long end = System.currentTimeMillis();
// 记录访问用时:
logger.info("访问用时:{}", end - begin);
return user;
}
public User queryUserByIdFallback(Long id){
User user = new User();
user.setId(id);
user.setName("用户信息查询出现异常!");
return user;
}
}
如图所示:
@Component
public class UserDao {
@Autowired
private RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
@HystrixCommand(fallbackMethod = "queryUserByIdFallback")
public User queryUserById(Long id){
long begin = System.currentTimeMillis();
String url = "http://user-service/user/" + id;
User user = this.restTemplate.getForObject(url, User.class);
long end = System.currentTimeMillis();
// 记录访问用时:
logger.info("访问用时:{}", end - begin);
return user;
}
public User queryUserByIdFallback(Long id){
User user = new User();
user.setId(id);
user.setName("用户信息查询出现异常!");
return user;
}
}
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
:声明一个失败回滚处理函数queryUserByIdFallback,当queryUserById执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。- 为了方便查看熔断的触发时机,我们记录请求访问时间。
在原来的业务逻辑中调用这个DAO的代码如下:
package com.txw.consumerdemo.service;
import com.txw.consumerdemo.dao.UserDao;
import com.txw.consumerdemo.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 用户业务层
* @author Adair
* E-mail: 1578533828@qq.com
*/
@SuppressWarnings("all") // 注解警告信息
@Service
public class UserService {
@Autowired
private UserDao userDao;
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
ids.forEach(id -> {
// 我们测试多次查询,
users.add(this.userDao.queryUserById(id));
});
return users;
}
}
如图所示:
1.3.2 改造服务提供者
改造服务提供者,随机休眠一段时间,以触发熔断的代码如下:
package com.txw.userservice.service;
import com.txw.userservice.entity.User;
import com.txw.userservice.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Random;
/**
* 用户业务层
* @author Adair
* E-mail: 1578533828@qq.com
*/
@Service
@SuppressWarnings("all") // 注解警告信息
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) throws InterruptedException {
// 为了演示超时现象,我们在这里然线程休眠,时间随机 0~2000毫秒
Thread.sleep(new Random().nextInt(2000));
return this.userMapper.selectByPrimaryKey(id);
}
}
如图所示:
启动项目,测试,如图所示:
1.3.3 优化
虽然熔断实现了,但是我们的重试机制似乎没有生效,是这样吗?
其实这里是因为我们的Ribbon超时时间设置的是1000ms代码如图所示:
而Hystix的超时时间默认也是1000ms,因此重试机制没有被触发,而是先触发了熔断。
所以,Ribbon的超时时间一定要小于Hystix的超时时间。
我们可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
来设置Hystrix超时时间。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 3000 # 设置hystrix的超时时间为3000ms
如图所示:
2 Feign
在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码:
String baseUrl = "http://user-service/user/";
User user = this.restTemplate.getForObject(baseUrl + id, User.class)
如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?
这就是我们接下来要学的Feign的功能了。
2.1 简介
有道词典的英文解释:
为什么叫伪装?
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
项目主页:https://github.com/OpenFeign/feign,如图所示:
2.2 快速入门
1.引入相应依赖的代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
如图所示:
2.编写UserFeignClient的代码如下:
package com.txw.consumerdemo.feign;
import com.txw.consumerdemo.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign的客户端
* @author Adair
* E-mail: 1578533828@qq.com
*/
@FeignClient("user-service")
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
如图所示:
- 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像。
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称。- 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果。
3.编写UserService的代码如下:
package com.txw.consumerdemo.service;
import com.txw.consumerdemo.entity.User;
import com.txw.consumerdemo.feign.UserFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 用户业务层
* @author Adair
* E-mail: 1578533828@qq.com
*/
@SuppressWarnings("all") // 注解警告信息
@Service
public class UserService {
@Autowired
private UserFeignClient userFeignClient;
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
ids.forEach(id -> {
// 我们测试多次查询,
users.add(this.userFeignClient.queryUserById(id));
});
return users;
}
}
如图所示:
我们在启动类上,添加注解,开启Feign功能的代码如下:
package com.txw.consumerdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableHystrix
@EnableFeignClients // 开启Feign功能
@EnableDiscoveryClient // 开启EurekaClient功能
@SpringBootApplication
public class ConsumerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerDemoApplication.class, args);
}
}
如图所示:
你会发现RestTemplate的注册被我删除了。Feign中已经自动集成了Ribbon负载均衡,因此我们不需要自己定义RestTemplate了。
启动项目,测试结果如图所示:
正常获取到了结果。
2.3 负载均衡
Feign中本身已经集成了Ribbon依赖和自动配置,如图所示:
因此我们不需要额外引入依赖,也不需要再注册RestTemplate
对象。
另外,我们可以,可以通过ribbon.xx
来进行全局配置。也可以通过服务名.ribbon.xx
来对指定服务配置的代码如下:
user-service:
ribbon:
ConnectTimeout: 250 # 连接超时时间(ms)
ReadTimeout: 1000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 1 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
2.4 Hystix支持
只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
但是,Feign中的Fallback配置不像Ribbon中那样简单了。
1)首先,我们要定义一个类,实现刚才编写的UserFeignClient,作为fallback的处理类的代码如下:
package com.txw.consumerdemo.feign;
import com.txw.consumerdemo.entity.User;
import org.springframework.stereotype.Component;
/**
* @author Adair
* E-mail: 1578533828@qq.com
*/
@Component
public class UserFeignClientFallback implements UserFeignClient {
@Override
public User queryUserById(Long id) {
User user = new User();
user.setId(id);
user.setName("用户查询出现异常!");
return user;
}
}
如图所示:
2)然后在UserFeignClient中,指定刚才编写的实现类的代码如下:
package com.txw.consumerdemo.feign;
import com.txw.consumerdemo.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign的客户端
* @author Adair
* E-mail: 1578533828@qq.com
*/
@FeignClient(value = "user-service",fallback = UserFeignClientFallback.class)
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
如图所示:
3)重启测试:
我们关闭user-service服务,然后在页面访问:
2.5 请求压缩(了解)
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:
feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩
同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限
注:上面的数据类型、压缩大小下限均为默认值。
2.6 日志级别(了解)
前面讲过,通过logging.level.xx=debug
来设置日志级别。然而这个对Fegin客户端而言不会产生效果。因为@FeignClient
注解修改的客户端在被代理时,都会创建一个新的Fegin.Logger实例。我们需要额外指定这个日志的级别才可以。
1)设置com.leyou包下的日志级别都为debug的代码如下:
logging:
level:
com.txw: debug
2)编写配置类,定义日志级别的代码如下:
package com.txw.consumerdemo.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Adair
* E-mail: 1578533828@qq.com
*/
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
如图所示:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间。
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息。
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
3)在FeignClient中指定配置类的代码如下:
package com.txw.consumerdemo.feign;
import com.txw.consumerdemo.config.FeignConfig;
import com.txw.consumerdemo.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign的客户端
* @author Adair
* E-mail: 1578533828@qq.com
*/
@FeignClient(value = "user-service",fallback = UserFeignClientFallback.class,configuration = FeignConfig.class)
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
如图所示:
4)重启项目,即可看到每次访问的日志。如图所示:
3 Zuul网关
通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?
先来说说这样架构需要做的一些事儿以及存在的不足:
- 首先,破坏了服务无状态特点。
- 为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。
- 从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外考虑对接口访问的控制处理。
- 其次,无法直接复用既有接口。
- 当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
面对类似上面的问题,我们要如何解决呢?答案是:服务网关!
- 当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制
等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
3.1 简介
官网:https://github.com/Netflix/zuul,如图所示:
Zuul:维基百科:
电影《捉鬼敢死队》中的怪兽,Zuul,在纽约引发了巨大骚乱。
事实上,在微服务架构中,Zuul就是守门的大Boss!一夫当关,万夫莫开!
3.2 Zuul加入后的架构
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
3.3 快速入门
1…选择项目右键点击New–Module,如图所示:
2.选择Spring Initializr,点击Next。如图所示:
3.填写项目,点击Next。如图所示:
4.点击Next即可,如图所示:
5.点击Finish,如图所示:
6.修改pom.xml的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.txw</groupId>
<artifactId>eureka-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>zuui-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuui-demo</name>
<description>zuul网关</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
如图所示:
7. 编写启动类,通过@EnableZuulProxy
注解开启Zuul的功能的代码如下:
package com.txw.zuuidemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy // 开启Zuul的网关功能
@SpringBootApplication
@SuppressWarnings("all") // 注解警告信息
public class ZuuiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuuiDemoApplication.class, args);
}
}
如图所示:
8.编写配置application.yml的代码如下:
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
如图所示:
我们需要用Zuul来代理user-service服务,先看一下控制面板中的服务状态:
映射规则如下:
zuul:
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
url: http://127.0.0.1:8082 # 映射路径对应的实际url地址
我们将符合path
规则的一切请求,都代理到 url
参数指定的地址
本例中,我们将 /user-service/**
开头的请求,代理到http://127.0.0.1:8082
9.启动测试:
访问的路径中需要加上配置规则的映射路径,我们访问:http://127.0.0.1:10010/user-service/user/1,如图所示:
3.4 面向服务的路由
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。
我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!
1.添加Eureka客户端依赖的代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
如图所示:
2.开启Eureka客户端发现功能的代码如下:
package com.txw.zuuidemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy // 开启Zuul的网关功能
@SpringBootApplication
@EnableDiscoveryClient
@SuppressWarnings("all") // 注解警告信息
public class ZuuiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuuiDemoApplication.class, args);
}
}
如图所示:
3.修改映射配置,通过服务名称获取
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://127.0.0.1:1086/eureka
如图所示:
4.修改映射配置,通过服务名称获取
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
zuul:
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
serviceId: user-service # 指定服务名称
如图所示:
5.启动测试
再次启动,这次Zuul进行代理时,会利用Ribbon进行负载均衡访问:
日志中可以看到使用了负载均衡器,如图所示:
3.5 简化的路由配置
在刚才的配置中,我们的规则是这样的:
zuul.routes.<route>.path=/xxx/**
: 来指定映射路径。<route>
是自定义的路由名zuul.routes.<route>.serviceId=/user-service
:来指定服务名。
而大多数情况下,我们的<route>
路由名称往往和 服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.<serviceId>=<path>
比方说上面我们关于user-service的配置可以简化为一条:
zuul:
routes:
user-service: /user-service/** # 这里是映射路径
如图所示:
采取项目,访问:http://localhost:10010/user-service/user/1,如图所示:
省去了对服务名称的配置。
3.6 默认的路由规则
在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
- 默认情况下,一切服务的映射路径就是服务名本身。
- 例如服务名为:
user-service
,则默认的映射路径就是:/user-service/**
- 例如服务名为:
也就是说,刚才的映射规则我们完全不配置也是OK的,不信就试试看。
3.7 路由前缀
配置示例:
zuul:
prefix: /api # 添加路由前缀
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
service-id: user-service # 指定服务名称
如图所示:
我们通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。
路径/api/user-service/user/1
将会被代理到`/user-service/user/1,如图所示:
3.8 过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
3.8.1 ZuulFilter
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法的代码如下:
public abstract ZuulFilter implements IZuulFilter{
// 过滤器类型
abstract public String filterType();
// 过滤器顺序
abstract public int filterOrder();
// 来自IZuulFilter 要不要过滤
boolean shouldFilter();
// IZuulFilter 过滤逻辑
Object run() throws ZuulException;
}
如图所示:
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用
filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
3.8.2 过滤器执行生命周期:
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
- 正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
- 异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
所有内置过滤器列表:
3.8.3 使用场景
场景非常多:
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了。
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
3.9 自定义过滤器
接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。
1.定义过滤器类的代码如下:
package com.txw.zuuidemo.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义过滤器类
* @author Adair
* E-mail: 1578533828@qq.com
*/
@SuppressWarnings("all") // 注解警告信息
@Component
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
// 登录校验,肯定是在前置拦截
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 顺序设置
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
// 返回true,代表过滤器生效。
return true;
}
@Override
public Object run() throws ZuulException {
// 登录校验逻辑。
// 1.获取Zuul提供的请求上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
// 2.从上下文中获取request对象
HttpServletRequest req = ctx.getRequest();
// 3.从请求中获取token
String token = req.getParameter("access-token");
// 4.判断
if(token == null || "".equals(token.trim())){
// 没有token,登录校验失败,拦截
ctx.setSendZuulResponse(false);
// 返回401状态码。也可以考虑重定向到登录页。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校验通过,可以考虑把用户信息放入上下文,继续向后执行
return null;
}
}
如图所示:
启动项目,访问http://localhost:10010/api/user-service/user/1?access-token=12345,如图所示:
3.10 负载均衡和熔断
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置的代码如下:
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 连接超时时间(ms)
ReadTimeout: 2000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔断超时时长:6000ms
如图所示:
注意:Ribbon的超时时长,真实值是(read + connect)*2,必须小于Hystrix时长。