Zuul的简介
什么是Zuul?
概述: Zuul是Netflix的一个开源组件,它是通过Servlet实现的。
作用:通过把网关和服务治理整合到一起,Spring Cloud Zuul可以获取到服务注册信息,结合Ribbon,Hystrix等更好地实现路由转发、负载均衡等功能。
为什么用Hystrix?
我们可以搭建简单的微服务架构系统并实现各服务之间的调用,但是不同的微服务一般会有不同的网络地址,而外部客户端(例如手机APP)可能需要调用多个服务的接口才能完成一个业务需求。而客户端直接与各个微服务通信,这样会有许多问题出现。
客户端直接与微服务通信
例如,一个电商的手机APP,可能会调用多个微服务的接口,才能完成一次购票的业务流程。如图所示:
客户端与微服务直接通信,就会产生许多问题,具体如下:
1.客户端会多次请求不同的微服务,使客户端变得更为复杂。
2. 存在跨域请求,在一定场景下处理相对复杂。例如,在重定向或js发起的 ajax请求时,会因为域名不同、二级域名不同、子域名不同或端口号不同等因素,处理变得相对复杂。
3. 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个,这种情况下,如果是客户端与微服务直接通信的架构,那么用户访问每一个接口的请求路径都需要改变,这就使得重构变得难以实现。
4. 某些微服务可能会设置防火墙等不友好的协议,难以做到直接访问。
客户通过Zuul网关与微服务通信
针对客户端与服务直接通信产生的问题,可以使用服务网关解决。服务网关相当于介于客户端和服务端之间的中间层,所有的外部请求都会先经过服务网关进行调度和过滤。
服务网关除了要实现请求路由、负载均衡、过滤等功能之外,还要实现更多功能,例如与服务相关框架整合、服务请求的熔断等。下图描述了加入服务网关的微服务调度过程。
快速搭建Zuul工程
简单搭建Zuul工程
1.创建eureka-server项目(之前创建过,可看我之前写的)
2.创建服务提供者eureka-provider项目(之前创建过,可看我之前写的,同上)
3.创建服务消费者eureka-consumer项目
依赖(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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiaofeng</groupId>
<artifactId>eureka-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-consumer</name>
<description>Demo project for Spring Boot</description>
<url/>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.7.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</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>
</project>
application.yml
spring:
application:
name: eureka-consumer
server:
port: 8764
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
instance:
hostname: localhost
开启Eureka Client功能:在启动类@EnableEurekaClient
创建config,使 RestTemplate实例对象处理请求时拥有客户端负载均衡的能力
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
创建一个service包,并添加@Service注解,service类中注入RestTemplate实例对象,并添加一个hi()方法,在hi()方法中对eureka-provider提供者服务进行调用
@Service
public class ConsumerService {
@Autowired
private RestTemplate restTemplate;
public String hi(@RequestParam("id") String id) {
return restTemplate.getForObject("http://eureka-provider/hi?id=" + id, String.class);
}
}
创建一个controller包,并添加@RestController注解,注入我们写的service对象,并创建一个hi()方法
@RestController
public class ConsumerController {
@Autowired
private ConsumerService consumerService;
@GetMapping("/hi")
public String hi(String id){
return consumerService.hi(id);
}
}
4.创建网关服务gateway-zuul项目
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
application.yml:
server:
port: 8835
spring:
application:
name: gateway-zuul
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
instance:
hostname: localhost
zuul:
routes:
eureka-consumer:
path: /eureka-consumer/**
在启动类,添加@EnableZuulProxy注解开启服务网关Zuul功能
5.项目测试
查看服务注册是否成功:
不用网关进行访问:
用Zuul网关进行访问:
Zuul的路由映射规则配置
1.服务路由配置
服务路由是Zuul通过与Spring Cloud Eureka的整合,实现了对服务实例的自动化维护,我们在使用服务路由配置的时候,无需通过serviceId指定具体服务实例地址,只需要通过zuul.routes.<路由名>.path与zuul.routes.<路由名>. serviceId的方式成对配置即可。
代码的作用是将符合/eureka-consumer/**规则的请求路径转发到名为eureka-consumer的服务实例上。
对于面向服务的路由配置,除了使用path与serviceId映射的配置方式之外,还有一种更简洁的配置方式,即zuul.routes.<serviceId>=<path>,其中<serviceId>用来指定路由的具体服务名,<path>用来配置匹配的请求映射地址。
2.服务路由默认规则
默认情况下,Zuul会自动为Eureka服务注册中心的所有服务创建映射关系进行路由,这会使得一些我们不希望对外开放的服务也可能被外部访问到。当使用服务名称作为前缀路径时,实际上会匹配类似下面的默认路由配置。
使用关闭默认的路由配置后,此时我们需要在配置文件中逐个为需要路由的服务添加映射规则。当然可以使用path与serviceId 组合的配置方式,也可以使用更简洁的zuul.routes.<serviceId>=<path>配置方式,只有在配置文件中出现的映射规则会被创建路由,而从Eureka中获取的其他服务,Zuul将不会再为它们创建路由规则。
3.自定义路由映射规则
如果我们的各个微服务应用都遵循了类似userprovider-v1这样的命名规则,通过“-”分隔的规范来定义服务名和服务版本标识的话,那么,我们可以使用Zuul中自定义服务与路由映射关系的功能,实现符合上述规则的微服务自动化地创建类似/v1/userprovider/**的路由匹配规则。实现步骤非常简单,引用官方文档,只需在网关项目中,增加Bean实例
4.路径匹配
在使用服务路由的配置方式时,我们需要为每个路由规则定义路由表达式,也就是path参数。在Zuul中,路由表达式采用了Ant风格定义。Ant风格的路由表达式共有三种通配符:
为了更灵活的使用路由配置规则,Zuul还提供了一个忽略表达式参数zuul.ignored-patterns,该参数用来设置不被网关进行路由的URL表达式。例如,不希望/hi接口被路由,配置信息如下。
当配置了忽略表达式参数时,通过网关访问eureka-consumer的/hi接口时,会提示该接口不存在,在控制台可以看到没有匹配路由的输出信息。
5.路由前缀
Zuul提供了zuul.prefix参数设置路由前缀。
prefix属性将路由前缀设置为/api,访问eureka-consumer服务的/api/eureka-consumer/1路径,请求将会被转发到eureka-consumer的/api/1。说明设置prefix参数后,Zuul会把代理前缀从默认路径中移除掉,为避免这种情况,可以使用zuul.stripPrefix=false来关闭移除代理前缀的动作,也可以通过zuul.routes.<路由名>.strip-prefix=false来指定服务关闭移除代理前缀的动作。
Zuul和Hystrix结合实现熔断
实现网关处理类
Zuul和Hystrix结合使用实现熔断功能时,需要实现FallbackProvider接口,在网关项目gateway-zuul中创建网关处理类MyFallbackProvider类,用于处理回退逻辑。并实现 FallbackProvider中的两个抽象方法,包括getRoute ()、 fallbackResponse ()。
Zuul和Hystrix结合使用实现熔断功能时,实现FallbackProvider接口,实现getRoute()方法
@Component
public class MyFallbackProvider implements FallbackProvider {
/*
* 指定当前服务哪些是需要回退的
* */
@Override
public String getRoute() {
return "eureka-consumer";
}
实现fallbackResponse()方法,重写getStatusCode()、getRawStatusCode() getStatusText()方法。
/*
* 执行回退操作的具体逻辑
* */
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("连接异常,我是fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
MediaType mt = new MediaType("application","json", Charset.forName("UTF-8"));
httpHeaders.setContentType(mt);
return httpHeaders;
}
};
}
测试
自己制造异常,将eureka-provider服务停止,因为写了MyFallbackProvider类,使用会跳转这个错误:
这样就说明Zuul和Hystrix结合实现熔断整合成功!
Zuul中的Eager Load配置
Spring Cloud Zuul的路由转发也是通过Ribbon实现负载均衡的,默认情况下,客户端相关的Bean会延迟加载,在第一次调用集群服务时,才会初始化这些对象。所以Zuul无法在第一时间加载到Ribbon的负载均衡,如果想提前加载Ribbon客户端,可以在配置文件中进行以下配置
如果我们使用默认路由,而没有通过配置的方式指定具体路由规则,那么zuul.ribbon.eager-load.enabled=true的配置就没有什么作用了。
因此,在真正使用的时候,我们可以通过zuul.ignored-services=*来忽略所有的默认路由,让所有路由配置均在配置文件中维护,以达到网关zuul启动时就默认初始化好了各个路由所要转发的负载均衡对象。
具体编写如下:
Zuul的过滤器
Zuul的过滤器介绍
Spring Cloud Zuul作为网关组件将客户端请求路由到业务处理过中,大部分功能都是通过过滤器实现的。Zuul定义了四种标准的过滤器类型。
(1)pre:该过滤器会在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
(2)route:负责请求转发到服务。 原始请求在此构建,并使用ApacheHttpClient或Netflix Ribbon发送原始请求。
(3)post:在route和error过滤器之后被调用,可以在响应消息中添加标准HTTPHeader、收集统计信息和指标,以及将响应发送给客户端等。
(4)error:处理请求发送错误时被调用。
Zuul请求的生命周期
Zuul请求的生命周期描述了各种类型过滤器的执行顺序。
HTTP请求到达Zuul,最先来到pre过滤器,在这里会去映射url patern到目标地址上,然后将请求与找到的地址交给route类型(routing在filterType方法返回的类型是“route”,不是routing)的过滤器进行求转发,请求服务实例获取响应,通过post类型过滤器对处理结果进行加工、转换等操作并返回。error类型的过滤器比较特殊,它在整个请求过程中,只在有异常的情况下才会触发,将异常结果交给post类型过滤器加工并返回。
自定义Zuul过滤器
手编写一个Zuul过滤器,来实现打印用户请求的Http方法以及请求地址。编写Zuul过滤器只需要继承ZuulFilter,并实现 ZuulFilter中的四个抽象方法,包括filterType()、 filterOrder()、shouldFilter()和run()。
在gateway-zuul项目中定义表示Zuul过滤器的PreRequestLogFilter类,继承ZuulFilter。实现ZuulFilter的filterType抽象方法。
package com.xiaofeng.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
@Component
public class PreRequestLogFilter extends ZuulFilter {
/*声明日志*/
private static final Logger logger = LoggerFactory.getLogger(PreRequestLogFilter.class);
/*当前编写的自定义类型是什么? 有四种:pre、route、post、error*/
@Override
public String filterType() {
return "pre";
}
/*当前编写的自定义过滤器的执行顺序是什么*/
@Override
public int filterOrder() {
return 1;
}
/*是否执行当前过滤器,默认为false,要改为true*/
@Override
public boolean shouldFilter() {
return true;
}
/*写业务逻辑*/
@Override
public Object run() throws ZuulException {
//获取当前请求的上下文对象
RequestContext currentContext = RequestContext.getCurrentContext();
//获取当前请求的对象
HttpServletRequest request = currentContext.getRequest();
logger.info("进入访问过滤器,访问的url:{},访问的方法:{}",request.getRequestURL(),request.getMethod());
String accessToken = request.getHeader("accessToken");
if (StringUtils.isEmpty(accessToken)){
logger.info("当前的请求需要传递access Token");
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(401);
return null;
}
logger.info("请求通过过滤器");
return null;
}
}
启动测试。依次启动eureka-server,eureka-consumer和gateway-zuul。使用浏览器访问http://localhost:8835/eureka-consumer/hi,会在在控制台看到下列信息,如图所示。
在浏览器会返回401:
在PostMan中需写accessToken和一个value值就可以请求到:
禁用Zuul过滤器
Spring Cloud默认为Zuul编写并启动了一些过滤器,例如DebugFilter、FormBodyWrapperilter、PreDecorationFilter等,这些过滤器都会存放在spring- cloud-netflix-core这个Jar包的org.springframework.cloud.netflix.zuul.filters包中。
在一些情况下,我们需要禁用掉部分过滤器,只需设置zuul.<SimpleClassName>.<filterType>.disable=true,就可以禁用SimpleClassName所对应的过滤器。以过滤器org.springframework.cloud.netflix.zuul.filters.post.SentResponseFilter为例,只需要设置zuul.SendResponseFilter.pre.disable=true就可以了。