SpringCloud系列--6网关

  |   0 评论   |   0 浏览

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

过滤器运行机制

image.png
如上图: @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 时候的一个结构图:
image.png

加入 Zuul 之后的一个结构图:
image.png

集群搭建

需要一个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成功了。

调用过程如下图:
image.png

默认情况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值决定,提供的值越小,优先级越高,下图是默认过滤器及优先级
image.png

自定义过滤器

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等过滤器不会被启用,如下黑色的过滤器不会启用:

image.png

如果我们不想使用自带的一些过滤器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中讲解。


标题:SpringCloud系列--6网关
作者:码农路上
地址:http://wujingjian.club/articles/2020/03/23/1584935173954.html