Spring Boot : 使用 Zuul 實現 API Gateway 的路由和過濾 ( Routing and Filtering )
本節通過使用 Netflix Zuul 實現微服應用中的路由(簡單代理轉發)和過濾功能。
API Gateway 的搭建工作,技術選型是 Netflix Zuul
API Gateway 是隨著微服務(Microservice)這個概念一起興起的一種架構模式,它用于解決微服務過于分散,沒有一個統一的出入口進行流量管理的問題。
Netflix Zuul 中提供了 Service Discovery (Eureka), Circuit Breaker (Hystrix), Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon) 等功能。
Spring Boot 是構建單個微服務應用的理想選擇,但是我們還需要以某種方式將它們互相聯系起來。這就是 Spring Cloud Netflix 所要解決的問題。Netflix 它提供了各種組件,比如:Eureka服務發現與Ribbon客戶端負載均衡的結合,為內部“微服務”提供通信支持。但是,如果你想要與外界通信時(你提供外部API,或只是從你的頁面使用AJAX),將各種服務隱藏在一個代理之后是一個明智的選擇。
常規的選擇我們會使用Nginx作為代理。但是Netflix帶來了它自己的解決方案——智能路由Zuul。它帶有許多有趣的功能,它可以用于身份驗證、服務遷移、分級卸載以及各種動態路由選項。同時,它是使用Java編寫的。
和大部分基于Java的Web應用類似,Zuul也采用了servlet架構,因此Zuul處理每個請求的方式是針對每個請求是用一個線程來處理。通常情況下,為了提高性能,所有請求會被放到處理隊列中,從線程池中選取空閑線程來處理該請求。這樣的設計方式,足以應付一般的高并發場景。
Zuul 是在云平臺上提供動態路由,監控,彈性,安全等邊緣服務的框架。Zuul 相當于是設備和 Netflix 流應用的 Web 網站后端所有請求的前門。Zuul 可以適當的對多個 Amazon Auto Scaling Groups 進行路由請求。
其架構如下圖所示:
Zuul提供了一個框架,可以對過濾器進行動態的加載,編譯,運行。過濾器之間沒有直接的相互通信。他們是通過一個RequestContext的靜態類來進行數據傳遞的。RequestContext類中有ThreadLocal變量來記錄每個Request所需要傳遞的數據。
過濾器是由Groovy寫成。這些過濾器文件被放在Zuul Server上的特定目錄下面。Zuul會定期輪詢這些目錄。修改過的過濾器會動態的加載到Zuul Server中以便于request使用。
下面有幾種標準的過濾器類型:
- PRE:這種過濾器在請求到達Origin Server之前調用。比如身份驗證,在集群中選擇請求的Origin Server,記log等。
- ROUTING:在這種過濾器中把用戶請求發送給Origin Server。發送給Origin Server的用戶請求在這類過濾器中build。并使用Apache HttpClient或者Netfilx Ribbon發送給Origin Server。
- POST:這種過濾器在用戶請求從Origin Server返回以后執行。比如在返回的response上面加response header,做各種統計等。并在該過濾器中把response返回給客戶。
- ERROR:在其他階段發生錯誤時執行該過濾器。
- 客戶定制:比如我們可以定制一種STATIC類型的過濾器,用來模擬生成返回給客戶的response。
過濾器的生命周期如下所示:
就像上圖中所描述的一樣,Zuul 提供了四種過濾器的 API,分別為前置(Pre)、后置(Post)、路由(Route)和錯誤(Error)四種處理方式。
一個請求會先按順序通過所有的前置過濾器,之后在路由過濾器中轉發給后端應用,得到響應后又會通過所有的后置過濾器,最后響應給客戶端。在整個流程中如果發生了異常則會跳轉到錯誤過濾器中。
一般來說,如果需要在請求到達后端應用前就進行處理的話,會選擇前置過濾器,例如鑒權、請求轉發、增加請求參數等行為。在請求完成后需要處理的操作放在后置過濾器中完成,例如統計返回值和調用時間、記錄日志、增加跨域頭等行為。路由過濾器一般只需要選擇 Zuul 中內置的即可,錯誤過濾器一般只需要一個,這樣可以在 Gateway 遇到錯誤邏輯時直接拋出異常中斷流程,并直接統一處理返回結果。
Zuul可以通過加載動態過濾機制,從而實現以下各項功能:
- 驗證與安全保障: 識別面向各類資源的驗證要求并拒絕那些與要求不符的請求。
- 審查與監控: 在邊緣位置追蹤有意義數據及統計結果,從而為我們帶來準確的生產狀態結論。
- 動態路由: 以動態方式根據需要將請求路由至不同后端集群處。
- 壓力測試: 逐漸增加指向集群的負載流量,從而計算性能水平。
- 負載分配: 為每一種負載類型分配對應容量,并棄用超出限定值的請求。
- 靜態響應處理: 在邊緣位置直接建立部分響應,從而避免其流入內部集群。
- 多區域彈性: 跨越AWS區域進行請求路由,旨在實現ELB使用多樣化并保證邊緣位置與使用者盡可能接近。
除此之外,Netflix公司還利用Zuul的功能通過金絲雀版本實現精確路由與壓力測試。
Features
Service Discovery
Eureka instances can be registered and clients can discover the instances using Spring-managed beans
An embedded Eureka server can be created with declarative Java configuration
Circuit Breaker
Hystrix clients can be built with a simple annotation-driven method decorator
Embedded Hystrix dashboard with declarative Java configuration
Declarative REST Client
Feign creates a dynamic implementation of an interface decorated with JAX-RS or Spring MVC annotations
Client Side Load Balancer
Ribbon
External Configuration
A bridge from the Spring Environment to Archaius (enables native configuration of Netflix components using Spring Boot conventions)
Router and Filter
automatic registration of Zuul filters, and a simple convention over configuration approach to reverse proxy creation
安裝 Zuul 服務。它的應用本身很簡單:
@SpringBootApplication
@Controller
@EnableZuulProxy
public class DemoApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DemoApplication.class)
.web(true).run(args);
}
}
我們還需要在 application.yml中定義固定的路由規則:
zuul:
routes:
sodik:
path: /sodik/**
url: http://target
現在我們試試運行測試:
$ curl http://proxy:8080/sodik/sample.html
應用場景
以下介紹一些 Zuul 中不同過濾器的應用場景。
前置過濾器
鑒權
一般來說整個服務的鑒權邏輯可以很復雜。
- 客戶端:App、Web、Backend
- 權限組:用戶、后臺人員、其他開發者
- 實現:OAuth、JWT
- 使用方式:Token、Cookie、SSO
而對于后端應用來說,它們其實只需要知道請求屬于誰,而不需要知道為什么,所以 Gateway 可以友善的幫助后端應用完成鑒權這個行為,并將用戶的唯一標示透傳到后端,而不需要、甚至不應該將身份信息也傳遞給后端,防止某些應用利用這些敏感信息做錯誤的事情。
Zuul 默認情況下在處理后會刪除請求的 Authorization
頭和 Set-Cookie
頭,也算是貫徹了這個原則。
流量轉發
流量轉發的含義就是將指向 /a/xxx.json
的請求轉發到指向 /b/xxx.json
的請求。這個功能可能在一些項目遷移、或是灰度發布上會有一些用處。
在 Zuul 中并沒有一個很好的辦法去修改 Request URI。在某些 Issue 中開發者會建議設置 requestURI
這個屬性,但是實際在 Zuul 自身的 PreDecorationFilter
流程中又會被覆蓋一遍。
不過對于一個基于 Servlet 的應用,使用 HttpServletRequestWrapper
基本可以解決一切問題,在這個場景中只需要重寫其 getRequestURI
方法即可。
class RewriteURIRequestWrapper extends HttpServletRequestWrapper {
private String rewriteURI;
public RewriteURIRequestWrapper(HttpServletRequest request, String rewriteURI) {
super(request);
this.rewriteURI = rewriteURI;
}
@Override
public String getRequestURI() {
return rewriteURI;
}
}
后置過濾器
跨域
使用 Gateway 做跨域相比應用本身或是 Nginx 的好處是規則可以配置的更加靈活。例如一個常見的規則。
對于任意的 AJAX 請求,返回
Access-Control-Allow-Origin
為*
,且Access-Control-Allow-Credentials
為true
,這是一個常用的允許任意源跨域的配置,但是不允許請求攜帶任何 Cookie如果一個被信任的請求者需要攜帶 Cookie,那么將它的
Origin
增加到白名單中。對于白名單中的請求,返回Access-Control-Allow-Origin
為該域名,且Access-Control-Allow-Credentials
為true
,這樣請求者可以正常的請求接口,同時可以在請求接口時攜帶 Cookie對于 302 的請求,即使在白名單內也必須要設置
Access-Control-Allow-Origin
為*
,否則重定向后的請求攜帶的Origin
會為null
,有可能會導致 iOS 低版本的某些兼容問題
統計
Gateway 可以統一收集所有應用請求的記錄,并寫入日志文件或是發到監控系統,相比 Nginx 的 access log,好處主要也是二次開發比較方便,比如可以關注一些業務相關的 HTTP 頭,或是將請求參數和返回值都保存為日志打入消息隊列中,便于線上故障調試。也可以收集一些性能指標發送到類似 Statsd 這樣的監控平臺。
錯誤過濾器
錯誤過濾器的主要用法就像是 Jersey 中的 ExceptionMapper
或是 Spring MVC 中的 @ExceptionHandler
一樣,在處理流程中認為有問題時,直接拋出統一的異常,錯誤過濾器捕獲到這個異常后,就可以統一的進行返回值的封裝,并直接結束該請求。
配置管理
雖然將這些邏輯都切換到了 Gateway,省去了很多維護和迭代的成本,但是也面臨著一個很大的問題,就是 Gateway 只有邏輯卻沒有配置,它并不知道一個請求要走哪些流程。
例如同樣是后端服務 API,有的可能是給網頁版用的、有的是給客戶端用的,亦或是有的給用戶用、有的給管理人員用,那么 Gateway 如何知道到底這些 API 是否需要登錄、流控以及緩存呢?
理論上我們可以為 Gateway 編寫一個管理后臺,里面有當前服務的所有 API,每一個開發者都可以在里面創建新的 API,以及為它增加鑒權、緩存、跨域等功能。為了簡化使用,也許我們會額外的增加一個權限組,例如 /admin/*
下的所有 API 都應該為后臺接口,它只允許內部來源的鑒權訪問。
但是這樣做依舊太復雜了,而且非常硬編碼,當開發者開發了一個新的 API 之后,即使這個應用已經能正常接收特定 URI 的請求并處理之后,卻還要通過人工的方式去一個管理后臺進行額外的配置,而且可能會因為不謹慎打錯了路徑中的某個單詞而造成不必要的事故,這都是不合理的。
我個人推薦的做法是,在后端應用中依舊保持配置的能力,即使應用里已經沒有真實處理的邏輯了。例如在 Java 中通過注解聲明式的編寫 API,且在應用啟動時自動注冊 Gateway 就是一種比較好的選擇。
/**
* 這個接口需要鑒權,鑒權方式是 OAuth
*/
@Authorization(OAuth)
@RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
public void del(@PathVariable int id) {
//...
}
/**
* 這個接口可以緩存,并且每個 IP/User 每秒最多請求 10 次
*/
@Cacheable
@RateLimiting(limit = "10/1s", scope = {IP, USER})
@RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
public void info(@PathVariable int id) {
//...
}
這樣 API 的編寫者就會根據業務場景考慮該 API 需要哪些功能,也減少了管理的復雜度。
除此之外還會有一些后端應用無關的配置,有些是自動化的,例如惡意請求攔截,Gateway 會將所有請求的信息通過消息隊列發送給一些實時數據分析的應用,這些應用會對請求分析,發現惡意請求的特征,并通過 Gateway 提供的接口將這些特征上報給 Gateway,Gateway 就可以實時的對這些惡意請求進行攔截。
穩定性
在 Nginx 和后端應用之間又建立了一個 Java 應用作為流量入口,很多人會去擔心它的穩定性,亦或是擔心它能否像 Nginx 一樣和后端的多個 upstream 進行交互,以下主要介紹一下 Zuul 的隔離機制以及重試機制。
隔離機制
在微服務的模式下,應用之間的聯系變得沒那么強烈,理想中任何一個應用超過負載或是掛掉了,都不應該去影響到其他應用。但是在 Gateway 這個層面,有沒有可能出現一個應用負載過重,導致將整個 Gateway 都壓垮了,已致所有應用的流量入口都被切斷?
這當然是有可能的,想象一個每秒會接受很多請求的應用,在正常情況下這些請求可能在 10 毫秒之內就能正常響應,但是如果有一天它出了問題,所有請求都會 Block 到 30 秒超時才會斷開(例如頻繁 Full GC 無法有效釋放內存)。那么在這個時候,Gateway 中也會有大量的線程在等待請求的響應,最終會吃光所有線程,導致其他正常應用的請求也受到影響。
在 Zuul 中,每一個后端應用都稱為一個 Route,為了避免一個 Route 搶占了太多資源影響到其他 Route 的情況出現,Zuul 使用 Hystrix 對每一個 Route 都做了隔離和限流。
Hystrix 的隔離策略有兩種,基于線程或是基于信號量。Zuul 默認的是基于線程的隔離機制,這意味著每一個 Route 的請求都會在一個固定大小且獨立的線程池中執行,這樣即使其中一個 Route 出現了問題,也只會是某一個線程池發生了阻塞,其他 Route 不會受到影響。
一般使用 Hystrix 時,只有調用量巨大會受到線程開銷影響時才會使用信號量進行隔離策略,對于 Zuul 這種網絡請求的用途使用線程隔離更加穩妥。
重試機制
一般來說,后端應用的健康狀態是不穩定的,應用列表隨時會有修改,所以 Gateway 必須有足夠好的容錯機制,能夠減少后端應用變更時造成的影響。
Zuul 的路由主要有 Eureka 和 Ribbon 兩種方式,下面簡單介紹下 Ribbon 支持哪些容錯配置。
重試的場景分為三種:
-
okToRetryOnConnectErrors
:只重試網絡錯誤 -
okToRetryOnAllErrors
:重試所有錯誤 -
OkToRetryOnAllOperations
:重試所有操作(這里不太理解,猜測是 GET/POST 等請求都會重試)
重試的次數有兩種:
-
MaxAutoRetries
:每個節點的最大重試次數 -
MaxAutoRetriesNextServer
:更換節點重試的最大次數
一般來說我們希望只在網絡連接失敗時進行重試、或是對 5XX 的 GET 請求進行重試(不推薦對 POST 請求進行重試,無法保證冪等性會造成數據不一致)。單臺的重試次數可以盡量小一些,重試的節點數盡量多一些,整體效果會更好。
如果有更加復雜的重試場景,例如需要對特定的某些 API、特定的返回值進行重試,那么也可以通過實現 RequestSpecificRetryHandler
定制邏輯(不建議直接使用 RetryHandler
,因為這個子類可以使用很多已有的功能)。
一個典型的多線程阻塞型架構的運行方式:對于每個請求,由一個專門的線程來進行處理,整個處理流程在線程內是阻塞的。由圖可見,當一個請求處理速度很慢(如遇到響應很慢的后段應用),可能會影響整個系統的響應。為了應對這種情況,Netflix也有針對的解決方案:Hystrix。
上面介紹了同步系統的設計,和同步系統設計方式不同,異步系統通常設計成事件驅動。
如上圖所示,當請求到達時,異步系統會將其包裝成一個事件,提交到事件循環中。事件循環中會維護一系列的監聽器、處理器,針對事件做出一系列的處理,最終將結果返回給用戶。這種設計模式通常被稱作“反應堆模式(Reactor pattern)”相比于同步多線程系統,異步事件系統可以以較少的線程(甚至是單線程)來處理所有的請求。
完整示例
網關服務應用( 實現了 API 的代理轉發 ):
application.properties
zuul.routes.api.url=http://localhost:8090
ribbon.eureka.enabled=false
server.port=8080
Spring Cloud Zuul will automatically set the path to the application name. In this sample because we set zuul.routes.books.url, so Zuul will proxy requests to /books to this URL.
Notice the second-to-last property in our file: Spring Cloud Netflix Zuul uses Netflix’s Ribbon to perform client-side load balancing, and by default, Ribbon would use Netflix Eureka for service discovery. For this simple example, we’re skipping service discovery, so we’ve set ribbon.eureka.enabled to false. Since Ribbon now can’t use Eureka to look up services, we must specify a url for the Book service.
SimpleFilter extends ZuulFilter
package hello.filters.pre;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SimpleFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(SimpleFilter.class);
@Override
public String filterType() {
return "前置過濾";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
log.info(String.format("response Status : %s, ContentType: %s", response.getStatus(), response.getContentType()));
return null;
}
}
GatewayApplication
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import hello.filters.pre.SimpleFilter;
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public SimpleFilter simpleFilter() {
return new SimpleFilter();
}
}
注意到: @EnableZuulProxy
真正的服務應用
application.properties
spring.application.name=book
server.port=8090
BookApplication
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class BookApplication {
public static void main(String[] args) {
SpringApplication.run(BookApplication.class, args);
}
@RequestMapping(value = "/title")
public String title() {
return "Spring Boot 2.0 極簡教程";
}
@RequestMapping(value = "/author")
public String author() {
return " 陳光劍";
}
}
直接訪問真實服務(server.port=8090):
http://127.0.0.1:8090/book/info
通過網關 Gateway 代理訪問:
http://localhost:8080/api/book/info
這個對于的配置是 gateway 工程里面的 application.properties 配置:
zuul.routes.api.url=http://localhost:8090
ribbon.eureka.enabled=false
server.port=8080
代理 Filter 日志:
2018-01-03 02:09:07.685 INFO 50363 --- [nio-8080-exec-1] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped URL path [/api/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2018-01-03 02:09:07.726 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : GET request to http://localhost:8080/api/book/info
2018-01-03 02:09:07.726 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : LocalAddr: 0:0:0:0:0:0:0:1
2018-01-03 02:09:07.727 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : LocalName: localhost
2018-01-03 02:09:07.727 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : LocalPort: 8080
2018-01-03 02:09:07.727 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : RemoteAddr: 0:0:0:0:0:0:0:1
2018-01-03 02:09:07.727 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : RemoteHost: 0:0:0:0:0:0:0:1
2018-01-03 02:09:07.727 INFO 50363 --- [nio-8080-exec-1] hello.filters.pre.SimpleFilter : RemotePort: 49917
其中的核心 Controller 是 ZuulController
package org.springframework.cloud.netflix.zuul.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.http.ZuulServlet;
/**
* @author Spencer Gibb
*/
public class ZuulController extends ServletWrappingController {
public ZuulController() {
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null); // Allow all
}
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request,
HttpServletResponse response) throws Exception {
try {
return super.handleRequestInternal(request, response);
}
finally {
// @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
RequestContext.getCurrentContext().unset();
}
}
}
其中的核心 ZuulServlet 類代碼如下
Core Zuul servlet which intializes and orchestrates zuulFilter execution
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.zuul.http;
import com.netflix.zuul.FilterProcessor;
import com.netflix.zuul.ZuulRunner;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.runners.MockitoJUnitRunner;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;
/**
* Core Zuul servlet which intializes and orchestrates zuulFilter execution
*
* @author Mikey Cohen
* Date: 12/23/11
* Time: 10:44 AM
*/
public class ZuulServlet extends HttpServlet {
private static final long serialVersionUID = -3374242278843351500L;
private ZuulRunner zuulRunner;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
String bufferReqsStr = config.getInitParameter("buffer-requests");
boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;
zuulRunner = new ZuulRunner(bufferReqs);
}
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
/**
* executes "post" ZuulFilters
*
* @throws ZuulException
*/
void postRoute() throws ZuulException {
zuulRunner.postRoute();
}
/**
* executes "route" filters
*
* @throws ZuulException
*/
void route() throws ZuulException {
zuulRunner.route();
}
/**
* executes "pre" filters
*
* @throws ZuulException
*/
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
/**
* initializes request
*
* @param servletRequest
* @param servletResponse
*/
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
zuulRunner.init(servletRequest, servletResponse);
}
/**
* sets error context info and executes "error" filters
*
* @param e
*/
void error(ZuulException e) {
RequestContext.getCurrentContext().setThrowable(e);
zuulRunner.error();
}
@RunWith(MockitoJUnitRunner.class)
public static class UnitTest {
@Mock
HttpServletRequest servletRequest;
@Mock
HttpServletResponseWrapper servletResponse;
@Mock
FilterProcessor processor;
@Mock
PrintWriter writer;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testProcessZuulFilter() {
ZuulServlet zuulServlet = new ZuulServlet();
zuulServlet = spy(zuulServlet);
RequestContext context = spy(RequestContext.getCurrentContext());
try {
FilterProcessor.setProcessor(processor);
RequestContext.testSetCurrentContext(context);
when(servletResponse.getWriter()).thenReturn(writer);
zuulServlet.init(servletRequest, servletResponse);
verify(zuulServlet, times(1)).init(servletRequest, servletResponse);
assertTrue(RequestContext.getCurrentContext().getRequest() instanceof HttpServletRequestWrapper);
assertTrue(RequestContext.getCurrentContext().getResponse() instanceof HttpServletResponseWrapper);
zuulServlet.preRoute();
verify(processor, times(1)).preRoute();
zuulServlet.postRoute();
verify(processor, times(1)).postRoute();
// verify(context, times(1)).unset();
zuulServlet.route();
verify(processor, times(1)).route();
RequestContext.testSetCurrentContext(null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
從下面的源代碼中,可以看出,前置過濾器的名稱是硬編碼關鍵字"pre" :
也就是我們在重寫 filterType 這個方法時返回的值:
@Override
public String filterType() {
return "pre";
}
我們也可以看到 ZuulServlet 里面的核心實現邏輯:
除了“pre” 路由之外,還有下面的兩個 route:
/**
* Runs all "route" filters. These filters route calls to an origin.
*
* @throws ZuulException if an exception occurs.
*/
public void route() throws ZuulException {
try {
runFilters("route");
} catch (Throwable e) {
if (e instanceof ZuulException) {
throw (ZuulException) e;
}
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
}
}
/**
* runs "post" filters which are called after "route" filters. ZuulExceptions from ZuulFilters are thrown.
* Any other Throwables are caught and a ZuulException is thrown out with a 500 status code
*
* @throws ZuulException
*/
public void postRoute() throws ZuulException {
try {
runFilters("post");
} catch (Throwable e) {
if (e instanceof ZuulException) {
throw (ZuulException) e;
}
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
}
}
本節示例工程源代碼:
API Gateway 網關工程: https://github.com/KotlinSpringBoot/demo3_zuul_api_gateway
微服務提供者工程:
https://github.com/KotlinSpringBoot/demo3_zuul_microservice_provider
參考文章:
Zuul 介紹:http://blog.csdn.net/liaokailin/article/details/51525908
服務網關(路由配置):http://www.lxweimin.com/p/bcf31021a67f
服務網關(過濾器): http://www.lxweimin.com/p/8a8c79591380