SpringCloud系列--6网关
SpringCloud 提供 Zuul 对外部的请求过程划分为不同的阶段,每个阶段提供一系列的过滤器,这些过滤器帮助我们实现以下功能:
- 身份验证和安全性: 对需要身份认证的资源进行过滤,拒绝处理不符合身份认证的请求
- 观察和监控: 跟踪重要的数据,为我们展示准确的请求状态
- 动态路由: 将请求动态路由到不同的服务集群
- 负载分配: 设置每种请求的处理能力,删除那些超出限制的请求
- 静态响应处理:提供一些静态的过滤器,直接响应一些请求,而不将它们转发到集群内部
- 路由的多样化: 除了可以将请求路由到 SpringCloud 集群外,还可以将请求路由到其他服务
以下为示例:
先搭建一个简单的 Web 服务
pom.xml
<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>org.crazyi.cloud</groupId>
<artifactId>book-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.4.RELEASE</version>
</dependency>
</dependencies>
</project>
启动类
package org.crazyit.cloud;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class BookApplication {
@RequestMapping(value = "/hello/{name}", method = RequestMethod.GET)
@ResponseBody
public String hello(@PathVariable String name) {
return "hello " + name;
}
public static void main(String[] args) {
new SpringApplicationBuilder(BookApplication.class).properties(
"server.port=8090").run(args);
}
}
新建一个 zuul 项目
pom.xml
引入 zuul 包,因为 zuul 内部使用了 httpclient 所有也一起引入
<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>org.crazyit.cloud</groupId>
<artifactId>first-router</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
</dependencies>
</project>
启动类开启 @EnableZuulProxy
代理
package org.crazyit.cloud;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(GatewayApplication.class).properties(
"server.port=8080").run(args);
}
}
配置路由 application.yml
将发送给http://localhost:8080/books的所有请求转发到 8090 端口
zuul:
routes:
books:
url: http://localhost:8090
浏览器访问 http://localhost:8080/books/hello/world 请求转发到 8090 并返回 hello world
过滤器运行机制
如上图: @EnableZuulProxy 注解后,Spring 容器初始化时,会将 Zuul 的相关配置初始化,其中包含一个 SpringBoot 的 Bean: ServletRegistrationBean,该类主要用于注册 Servlet。Zuul 提供了一个 ZuulServlet 类,在 Servlet 的 service 方法中,执行各种 zuul 过滤器(ZuulFilter).
ZuulServlet 的 service 方法接收到请求后,会执行 pre 阶段过滤器,再执行 routing 过滤器,最后 post 过滤器,其中 routing 阶段会将请求转发到“源服务”,源服务可以是第三方服务也可以是 SpringCloud 集群服务. 在 pre 和 routing 阶段如果出现异常,会执行 Error 过滤器,整个过程的 HTTP 请求、HTTP 响应、状态等数据,都会被封装到一个 RequestContext 对象中。
集群中使用 Zuul
之前没有 Zuul 时候的一个结构图:
加入 Zuul 之后的一个结构图:
集群搭建
需要一个eurekaserver服务
<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>org.crazyit.cloud</groupId>
<artifactId>zuul-eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
server:
enable-self-preservation: false
package org.crazyit.cloud;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class ServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ServerApplication.class).run(args);
}
}
需要一个web服务提供者
<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>org.crazyit.cloud</groupId>
<artifactId>zuul-book-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
</project>
spring:
application:
name: zuul-book-service
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
package org.crazyit.cloud.web;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BookController {
@RequestMapping(value = "/book/{bookId}", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public Book findBook(@PathVariable Integer bookId) {
Book book = new Book();
book.setId(bookId);
book.setName("Workflow讲义");
book.setAuthor("杨恩雄");
return book;
}
}
package org.crazyit.cloud.web;
public class Book {
private Integer id;
private String name;
private String author;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
package org.crazyit.cloud;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class BookApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(BookApplication.class).properties(
"server.port=9000").run(args);
}
}
需要一个服务调用者
<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>org.crazyit.cloud</groupId>
<artifactId>zuul-sale-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
</project>
server:
port: 9100
spring:
application:
name: zuul-sale-service
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
package org.crazyit.cloud.feign;
public class Book {
private Integer id;
private String name;
private String author;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
package org.crazyit.cloud.feign;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient("zuul-book-service") // 声明调用书本服务
public interface BookService {
/**
* 调用书本服务的接口,获取一个Book实例
*/
@RequestMapping(method = RequestMethod.GET, value = "/book/{bookId}")
Book getBook(@PathVariable("bookId") Integer bookId);
}
package org.crazyit.cloud.web;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.crazyit.cloud.feign.Book;
import org.crazyit.cloud.feign.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SaleController {
@Autowired
private BookService bookService;
/**
* 进行图书销售
*/
@RequestMapping(value = "/sale-book/{bookId}", method = RequestMethod.GET)
@ResponseBody
public String saleBook(@PathVariable Integer bookId) {
// 调用book服务查找
Book book = bookService.getBook(bookId);
// 控制台输入,模拟进行销售
System.out.println("销售模块处理销售,要销售的图书id: " + book.getId() + ", 书名:"
+ book.getName());
// 销售成功
return "SUCCESS";
}
@RequestMapping(value = "/testHeader", method = RequestMethod.GET)
@ResponseBody
public String testHeader(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaderNames();
while(headers.hasMoreElements()) {
String headerName = headers.nextElement();
System.out.println("#############" + headerName);
}
return "testHeader";
}
@RequestMapping(value = "/errorTest", method = RequestMethod.GET)
@ResponseBody
public String errorTest() throws Exception {
Thread.sleep(3000);
return "errorTest";
}
}
package org.crazyit.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class SaleApplication {
public static void main(String[] args) {
SpringApplication.run(SaleApplication.class, args);
}
}
重点是这个网关项目
<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>org.crazyit.cloud</groupId>
<artifactId>zuul-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
<version>1.5.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency>
</dependencies>
</project>
spring:
application:
name: zuul-gateway
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
management:
security:
enabled: false
zuul:
ignoredPatterns: /sale/noRoute
# sensitiveHeaders: cookie
# ignoredHeaders: accept-language
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
routeTest:
path: /routeTest/163
url: http://www.163.com
route163:
url: http://www.163.com
noRoute163:
url: www.163.com
helloRoute:
path: /test/**
url: forward:/source/hello
restTestRoute:
path: /rest-tpl-sale/**
serviceId: zuul-sale-service
exceptionTest:
path: /exceptionTest/**
ribbon:
eager-load:
enabled: true
#SendForwardFilter:
#route:
#disable: true
需要@EnableZuulProxy注解,并配置该服务加入到eureka中,上述application.yml配置声明所有的/sale/**请求路由到zuul-sale-service进行处理.(一般情况下,配置了serviceId后,处理请求的routing阶段,将会使用一个名称为RibbonRoutingFilter的过滤器,该过滤器会调用Ribbon的API来实现负载均衡,默认用HTTPClient)
我们一步一步来走:
先启动zuul服务
public static void main(String[] args) {
new SpringApplicationBuilder(GatewayApplication.class).properties(
"server.port=8080").run(args);
}
访问 zuul http://localhost:8080/sale/sale-book/1 返回 SUCCESS字符串,并且查看服务调用者的控制台有输出:
销售模块处理销售,要销售的图书id: 1, 书名:Workflow讲义
可见我们配置的/sale/**转发到 zuul-sale-service成功了。
调用过程如下图:
默认情况ribbon负载均衡调用使用HttpClient,替换为okhttpClient
需要添加配置
ribbon.okhttp.enabled=true
对应添加okhttp的jar包依赖
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
路由配置
简单路由
在routing阶段有几个过滤器,最基本的是SimpleHostRoutingFilter,该过滤器运行后,会将请求全部转发到“源服务”,所以称为简单路由,例如:
zuul:
routes:
routeTest:
path: /routeTest/163
url: http://www.163.com
则访问http://localhost:8080/routeTest/163将跳转到163网站,为了配置简便,看一看省略path,默认情况下使用routeId作为path,以下省略了path的配置:
zuul:
routes:
route163:
url: http://www.163.com
如上,访问http://localhost:8080/route163 同样路由到163网站,(注意:简单路由中url地址要携带http:或者https开头)
在此过程中,为了保证转发的性能使用了HTTPClient的连接池,可以如下配置连接池属性
zuul.host.maxTotalConnections: 目标主机的最大连接数,默认值200,相当于调用PoolingHttpClientConnectionManager的setMaxTotal方法
zuul.host.maxPerRouteConnections: 每个主机的初始连接数,默认值20,相当于调用PoolingHttpClientConnectionManager的 setDefaultMaxPerRoute方法
跳转路由
外部访问网管A地址要跳转到B地址,过滤器为SendForwardFilter,例子:
我们在网关项目中写一个简单的Controller
@RequestMapping(value = "/source/hello/{name}", method = RequestMethod.GET)
@ResponseBody
public String hello(@PathVariable("name") String name) {
return "hello " + name;
}
然后进行跳转路由配置如下:
zuul:
routes:
helloRoute:
path: /test/**
url: forward:/source/hello
访问 http://localhost:8080/test/xxx,可以看到返回hello xxx,可见请求被跳转了,实际上是调用了RequestDispatcher的forward方法进行跳转。
Ribbon路由
当网管作为Eureka客户端注册到Eureka服务器时,可以通过配置serviceId将请求转发到集群服务中,例如:
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
与 简单路由类似,serviceId可以省略,当省略时,将会使用routeId作为serviceId,如下与上面效果相同:
zuul:
routes:
zuul-sale-service:
path: /sale/**
注意点: 如果url不是http[s]:开头,也不是forward:开头,将会执行Ribbon路由过滤器,将url看做一个serviceId,以下配置与上述效果也等同:
zuul:
routes:
sale:
path: /sale/**
url: zuul-sale-service
自定义路由规则
自定义路由规则只需要创建一个PatternServiceRouteMapper即可:
package org.crazyit.cloud;
import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration
public class MyConfig {
/**
* 访问网关的 /xxxxx/**,将会被路由到 zuul-xxxxx-service 服务进行处理,
构造器的第一个参数为serviceId的正则表达式,第二个参数为路由的path.
*/
@Bean
public PatternServiceRouteMapper patternServiceRouteMapper() {
return new PatternServiceRouteMapper(
"(zuul)-(?<module>.+)-(service)", "${module}/**");
}
}
以上规则,如果想让一个或多个服务不被路由,可以使用zuul.ignoredServices属性配置
,例如:
zuul.ignoredServices: zuul-sale-service,zuul-book-service
忽略路由
除了使用zuul.ignoredServices:
忽略外,还可以使用zuul.igmoredPatterns:
;
zuul:
ignoredPatterns: /sale/noRoute
# sensitiveHeaders: cookie
# ignoredHeaders: accept-language
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
访问 /sale路径的请求都会被路由到zuul-sale-service进行处理,但/sale/noRoute除外;
我们在网关服务中再添加一个测试接口
@RequestMapping(value = "/sale/noRoute", method = RequestMethod.GET)
@ResponseBody
public String saleNoRoute(HttpServletRequest request) {
System.out.println("设置了ignoredPatterns,该方法将会执行");
return "/sale/noRoute";
}
访问 http://localhost:8080/sale/noRoute 发现请求并没有进行路由走,而是执行了上面的代码,返回/sale/noRoute
Zuul的其他配置
请求头
在集群服务间共享请求头没有问题,但是请求转发到他系统,对于敏感的请求头信息需要进行处理,默认情况下,HTTP请求头的Cookie、Set-Cookie、Authorization属性不会传递到"源服务",可以使用sensitiveHeaders属性来配置敏感头,下面配置对全局生效:
zuul:
sensitiveHeaders: accept-language, cookie
以下配置仅对一个路由生效:
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
sensitiveHeaders: cookie
除了使用sensitiveHeaders属性外,还可以使用ignoredHeaders属性来配置全局忽略的请求头。使用该配置后,请求与响应中所配置的头信息均被忽略:
zuul:
ignoredHeaders: accept-language
路由端点
网关项目中提供了一个/routes服务,可以查看路由映射信息,如果想开启需要以下条件:
- 网关项目中引入Spring Boot Actuator
- 项目中使用了@EnableZuulProxy注解
一般情况下,Actuator开启了端点的安全认证,即使符合以上两个条件,也无法访问routes服务.要解决该问题,可以在配置文件中将management.security.enabled属性设置为false
关闭安全认证。
按照上述操作配置后, 访问 http://localhost:8080/routes
输出:
{
"/sale/**": "zuul-sale-service",
"/routeTest/163": "http://www.163.com",
"/route163/**": "http://www.163.com",
"/noRoute163/**": "www.163.com",
"/test/**": "forward:/source/hello",
"/rest-tpl-sale/**": "zuul-sale-service",
"/exceptionTest/**": "exceptionTest",
"/zuul-sale-service/**": "zuul-sale-service",
"/zuul-book-service/**": "zuul-book-service"
}
Zuul 与 Hystrix
当我们对网关进行配置让其调用集群服务时,将会执行Ribbon路由过滤器(RibbonRoutingFilter).该过滤器在进行转发时会封装一个Hystrix命令予以执行.换言之,它具有容错功能.如果“源服务”出现问题(超时等),那么所执行的Hystrix命令将会触发回退,下面进行测试:
我们在服务调用者中添加一个接口:
@RequestMapping(value = "/errorTest", method = RequestMethod.GET)
@ResponseBody
public String errorTest() throws Exception {
Thread.sleep(3000);
return "errorTest";
}
在网关项目中建立一个网关处理类,处理回退逻辑:
package org.crazyit.cloud.hy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
/**
* 回退的处理类
* @author 杨恩雄
*
*/
public class MyFallbackProvider implements ZuulFallbackProvider {
// 返回路由的名称
public String getRoute() {
return "zuul-sale-service";
}
// 回退触发时,返回默认的响应
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return headers;
}
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
public int getRawStatusCode() throws IOException {
return 200;
}
public String getStatusText() throws IOException {
return "OK";
}
public void close() {
}
};
}
}
同时将此类作为spring的bean
package org.crazyit.cloud.hy;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FallbackConfig {
@Bean
public ZuulFallbackProvider saleFallbackProvider() {
return new MyFallbackProvider();
}
}
回退处理类需要实现ZuulFallbackProvider接口,实现getRoute方法返回路由的名称,该方法将于配置中的路由进行对应,本例配置的路由如下:
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
简单来说就是: zuul-sale-service路由出现问题导致触发回退时,由MyFallbackProvider处理。
访问 http://localhost:8080/sale/errorTest 浏览器返回fallback字符串,可见回退被触发,以上实现的MyFallbackProvider仅对zuul-sale-service路由有效,如果想对全局有效,可以使用以下实现:
public String getRoute(){
return "*";
}
在Zuul中预加载Ribbon
调用集群服务时,会使用Ribbon的客户端,默认情况下,客户端相关的bean会延迟加载,在第一次调用集群服务时才会初始化这些对象,在第一次调用时,控制台会输出类似于:DynamicServerListLoadBalancer for client zuul-sale-service initialized:
如果想提前加载Ribbon客户端,可以在配置文件中进行以下配置:
zuul:
ribbon:
eager-load:
true
以上的配置在spring容器初始化时,就会创建Ribbon客户端的相关实例.
Zuul功能进阶(涉及原理)
过滤器优先级
过滤器的优先级执行顺序由各自提供的一个int值决定,提供的值越小,优先级越高,下图是默认过滤器及优先级
自定义过滤器
package org.crazyit.cloud.filter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import com.netflix.zuul.ZuulFilter;
public class MyFilter extends ZuulFilter {
// 过滤器执行条件
public boolean shouldFilter() {
return true;
}
// 执行方法
public Object run() {
System.out.println("执行 MyFilter 过滤器");
return null;
}
// 表示将在路由阶段执行
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
// 返回1,路由阶段,该过滤将会最先执行
public int filterOrder() {
return 1;
}
}
该过滤器会在routing阶段执行,优先级设置为1,也就是routing阶段最先执行。另外shouldFilter方法确定最终是否执行该方法,本例返回true代表任何路由规则都会执行该过滤器,然后将其加入spring管理
@Configuration
public class FilterConfig {
@Bean
public MyFilter myFilter() {
return new MyFilter();
}
}
访问 http://localhost:8080/test/1 返回hello 1
并且在网关服务的控制台输出 执行 MyFilter 过滤器
说明我们的自定义过滤器执行了,由于上面shouldFilter返回true,所以访问任何一个配置好的路由都会z执行上面的自定义过滤器。
动态加载过滤器
网管不能随便重启,相对于集群中其他节点,网关更需要长期、稳定提供服务,如果要增加过滤器重启网关代价太大,为了解决此问题,Zuul提供过滤器的动态加载功能,可以使用Groovy来编写过滤器,然后添加到加载目录,让Zuul去动态加载:
加入Groovy依赖:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency>
在网关项目中构造器里面调用Zuul的API来实现动态加载:
package org.crazyit.cloud;
import java.io.File;
import javax.annotation.PostConstruct;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import com.netflix.zuul.FilterFileManager;
import com.netflix.zuul.FilterLoader;
import com.netflix.zuul.groovy.GroovyCompiler;
import com.netflix.zuul.groovy.GroovyFileFilter;
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
@PostConstruct
public void zuulInit() {
FilterLoader.getInstance().setCompiler(new GroovyCompiler());
// 读取配置,获取脚本根目录
String scriptRoot = System.getProperty("zuul.filter.root", "groovy/filters");
System.out.println("scriptRoot:" + scriptRoot);
// 获取刷新间隔
String refreshInterval = System.getProperty("zuul.filter.refreshInterval", "5");
if (scriptRoot.length() > 0) scriptRoot = scriptRoot + File.separator;
try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
FilterFileManager.init(Integer.parseInt(refreshInterval), scriptRoot + "pre",
scriptRoot + "route", scriptRoot + "post");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
new SpringApplicationBuilder(GatewayApplication.class).properties(
"server.port=8080").run(args);
}
}
上述方法先读取zuul.filter.root 和 zuul.filter.refreshInterval两个属性,分别表示动态过滤器的根目录以及刷新间隔(秒),这两个属性优先读取配置文件的值,如果没有则使用默认值,可以在配置文件中配置如下(这里只描述,实际运行时没有加如下配置,我们读取的默认值):
zuul:
filter:
root: "groovy/filters"
refreshInterval: 5
接着调用FilterFileManager的init方法,初始化3个过滤器目录: pre、route和post.
这是我们还没有编写要动态加载的过滤器,先启动项目,让上述zuulInit()方法执行,这样会每隔固定时间刷新我们配置的groovy/filter下面的文件,如果有过滤器则会动态加载进去。后续我们编写好过滤器放到此目录下,隔配置的间隔时间后,再调用方法,测试过滤器是否动态加载成功。
启动项目我们先访问 http://localhost:8080/test/loadfilter 看到控制台只输出执行 MyFilter 过滤器
, 当我们把写好的过滤器:
package groovy.filters;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import com.netflix.zuul.ZuulFilter;
class DynamicFilter extends ZuulFilter {
public boolean shouldFilter() {
return true;
}
public Object run() {
System.out.println("========= 这一个是动态加载的过滤器:DynamicFilter");
return null;
}
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
public int filterOrder() {
return 3;
}
}
放到 groovy/filters/post里面的时候, 再去请求 http://localhost:8080/test/loadfilter 看到控制台输出
执行 MyFilter 过滤器
========= 这一个是动态加载的过滤器:DynamicFilter
说明我们的动态过滤器已经加载并运行。
但是实际运行的时候发现,上述代码需要适当改造,需要将脚本根目录指定为全路径,不然会找到其他项目里面(这点在实际应用中还需要观察,可能我本地的intellj工具有点问题)
// 读取配置,获取脚本根目录
String scriptRoot = System.getProperty("zuul.filter.root", "groovy/filters");
System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%scriptRoot:" + scriptRoot);
scriptRoot = "/Users/apple/coding/springcloud/codes/07/03/zuul-gateway/src/main/java/" + scriptRoot;
禁用过滤器
如果想禁用其中一个过滤器,可以使用配置:
zuul:
SendForwardFilter:
route:
disable: true
以上禁用了SendForwadFilter(处理跳转路由的过滤器),如果再为url属性使用forward:进行配置则不会生效。同样禁用其他过滤器也会导致失去响应的功能。
请求上下文
HTTP 请求的全部信息都封装在一个RequestContext对象中,该对象集成ConcurrentHashMap。可将RequestContext看做一个Map, RequestContext维护着当前线程的全部请求变量,例如请求的URI、serviceId、主机信息等等。本小节将以RequestContext为基础,编写一个自定义的过滤器,使用RestTemplate来调用集群服务。
package org.crazyit.cloud.filter;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.web.client.RestTemplate;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
public class RestTemplateFilter extends ZuulFilter {
private RestTemplate restTemplate;
public RestTemplateFilter(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 获取请求uri
String uri = request.getRequestURI();
// 为了不影响其他路由,uri中含有 rest-tpl-sale 才执行本路由器
if(uri.indexOf("rest-tpl-sale") != -1) {
return true;
} else {
return false;
}
}
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// 获取需要调用的服务id
String serviceId = (String)ctx.get("serviceId");
// 获取请求的uri
String uri = (String)ctx.get("requestURI");
// 组合成url给RestTemplate调用
String url = "http://" + serviceId + uri;
System.out.println("执行RestTemplateFilter, 调用的url:" + url);
// 调用并获取结果
String result = this.restTemplate.getForObject(url, String.class);
// 设置路由状态,表示已经进行路由
ctx.setResponseBody(result);
// 设置响应标识,
//划重点: 设置为响应标识ctx.sendZuulResponse()后,SpringCloud自带的Ribbon路由过滤器(RibbonRoutingFilter)、
// 简单路由过滤器(SimpleHostRoutingFilter)将不会执行
ctx.sendZuulResponse();
return null;
}
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
// 比自带的过滤器要先执行
return 2;
}
}
然后将RestTemplateFilter加入配置:
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
@Bean
public RestTemplateFilter restTemplateFilter(RestTemplate restTemplate) {
return new RestTemplateFilter(restTemplate); // 注入RestTemplate
}
网关配置中配置路由规则:
zuul:
routes:
restTestRoute:
path: /rest-tpl-sale/**
serviceId: zuul-sale-service
当访问 http://localhost:8080/rest-tpl-sale/sale-book/1 浏览器返回SUCCESS,控制台输出:
执行 MyFilter 过滤器
执行RestTemplateFilter, 调用的url:http://zuul-sale-service/sale-book/1
========= 这一个是动态加载的过滤器:DynamicFilter
然后我们发现服务提供方的接口被调用了两次,一次是我们自定义拦截器RestTemplateFilter
中run方法中调用的,另一次是我们请求的地址转发过去之后调用的,
区别如下:
http://localhost:8080/rest-tpl-sale/sale-book/1 会调用两次服务方接口.
http://localhost:8080/sale/sale-book/1 因为没有restRemplateFilter所有只调用一次服务方接口.
本例中也简单介绍了RequestContext所维护的信息。
@EnableZuulServer注解
之前使用了@EnableZuulProxy来开启Zuul功能,除了该注解外,还可以使用@EnableZuulServer,这个注解更像一个"低配版"的@EnableZuulProxy. 使用@EnableZuulServer后,SimpleHostRoutingFilter、RibbonRoutingFilter等过滤器不会被启用,如下黑色的过滤器不会启用:
如果我们不想使用自带的一些过滤器RibbonRoutingFilter和SimpleHostRoutingFilter,想自定义过滤器调用服务,可以考虑用@EnableZuulServer注解。
error过滤器
各阶段过滤器执行异常抛出异常会被捕获,然后调用RequestContext的setThrowable方法设置异常.error阶段的SendErrorFilter过滤器会判断RequestContext中是否存在异常(getThrowable是否为null),如果存在,才会执行SendErrorFilter过滤器,
SendErrorFilter过滤器执行时,将异常设置到HttpServletRequest中,再调用RequestDispatcher的forward方法,默认跳转/error页面,我们自顶一个一个过滤器故意抛出异常:
package org.crazyit.cloud.filter;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class ExceptionFilter extends ZuulFilter {
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 获取请求的uri
String uri = request.getRequestURI();
// 为不影响其他例子,uri含有exceptionTest才执行本过滤器
if(uri.indexOf("exceptionTest") != -1) {
return true;
}
return false;
}
public Object run() {
System.out.println("执行 ExceptionFilter,将抛出异常");
throw new ZuulRuntimeException(new ZuulException("exception msg", 201, "my cause"));
}
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
public int filterOrder() {
return 3;
}
}
新建一个控制器输出这些错误信息:
package org.crazyit.cloud.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.BasicErrorController;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class MyErrorController extends BasicErrorController {
public MyErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
@Override
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
System.out.println("=== 输出异常信息 ===");
System.out.println(request.getAttribute("javax.servlet.error.status_code"));
System.out.println(request.getAttribute("javax.servlet.error.exception"));
System.out.println(request.getAttribute("javax.servlet.error.message"));
return super.errorHtml(request, response);
}
}
并配置到springBean中
@Bean
public ExceptionFilter exceptionFilter() {
return new ExceptionFilter();
}
超类BasicErrorController,是SpringBoot中用于处理错误的控制器基类.在过滤器抛出异常后,SendErrorFilter会跳转到/error路径,然后就会执行MyErrorController的errorHtml方法返回到错误页面。我们在方法中输出错误信息。
spring:
application:
name: zuul-gateway
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
management:
security:
enabled: false
zuul:
ignoredPatterns: /sale/noRoute
# sensitiveHeaders: cookie
# ignoredHeaders: accept-language
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
routeTest:
path: /routeTest/163
url: http://www.163.com
route163:
url: http://www.163.com
noRoute163:
url: www.163.com
helloRoute:
path: /test/**
url: forward:/source/hello
restTestRoute:
path: /rest-tpl-sale/**
serviceId: zuul-sale-service
exceptionTest:
path: /exceptionTest/**
ribbon:
eager-load:
enabled: true
#SendForwardFilter:
#route:
#disable: true
访问 http://localhost:8080/exceptionTest/test, 可以看到控制台输出:
执行 ExceptionFilter,将抛出异常
2020-03-24 19:04:52.965 WARN 19201 --- [nio-8080-exec-1] o.s.c.n.z.filters.post.SendErrorFilter : Error during filtering
com.netflix.zuul.exception.ZuulException: exception msg
at org.crazyit.cloud.filter.ExceptionFilter.run(ExceptionFilter.java:28) ~[classes/:na]
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...........
=== 输出异常信息 ===
201
com.netflix.zuul.exception.ZuulException: exception msg
my cause
根据输出结果可知,过滤器抛出的异常信息可以在错误处理的控制器中获取。
动态路由
之前路由规则都配置在application.yml中,实际中我们可能规则在不停变化,需要路由规则的动态更新,需要以配置文件的更新、配置项刷新为前提,这部分将在spring Cloud config中讲解。