上一篇:《Spring Cloud入門教程(五):API服務網關(Zuul) 上》
本人和同事撰寫的《Spring Cloud微服務架構開發實戰》一書也在京東、當當等書店上架,大家可以點擊這里前往購買,多謝大家支持和捧場!
Zuul給我們的第一印象通常是這樣:它包含了對請求的路由和過濾兩個功能,其中路由功能負責將外部請求轉發到具體的微服務實例上,是實現外部訪問統一入口的基礎。過濾器功能則負責對請求的處理過程進行干預,是實現請求校驗、服務聚合等功能的基礎。然而實際上,路由功能在真正運行時,它的路由映射和請求轉發都是由幾個不同的過濾器完成的。其中,路由映射主要是通過PRE類型的過濾器完成,它將請求路徑與配置的路由規則進行匹配,以找到需要轉發的目標地址。而請求轉發的部分則是由Route類型的過濾器來完成,對PRE類型過濾器獲得的路由地址進行轉發。所以,過濾器可以說是Zuul實現API網關功能最重要的核心部件,每一個進入Zuul的請求都會經過一系列的過濾器處理鏈得到請求響應并返回給客戶端。
1. 過濾器簡介
1.1 過濾器特性
Zuul過濾器的關鍵特性有:
- Type: 定義在請求執行過程中何時被執行;
- Execution Order: 當存在多個過濾器時,用來指示執行的順序,值越小就會越早執行;
- Criteria: 執行的條件,即該過濾器何時會被觸發;
- Action: 具體的動作。
過濾器之間并不會直接進行通信,而是通過RequestContext
來共享信息,RequestContext
是線程安全的。
對應上面Zuul過濾器的特性,我們在實現一個自定義過濾器時需要實現的方法有:
/**
* Zuul Pre-Type Filter
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
public class PreTypeZuulFilter extends ZuulFilter {
protected Logger logger = LoggerFactory.getLogger(PreTypeZuulFilter.class);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
this.logger.info("This is pre-type zuul filter.");
return null;
}
}
其中:
- filterType()方法是該過濾器的類型;
- filterOrder()方法返回的是執行順序;
- shouldFilter()方法則是判斷是否需要執行該過濾器;
- run()則是所要執行的具體過濾動作。
1.2 過濾器類型
Zuul中定義了四種標準的過濾器類型,這些過濾器類型對應于請求的典型生命周期。
-
PRE
過濾器: 在請求被路由之前調用, 可用來實現身份驗證、在集群中選擇請求的微服務、記錄調試信息等; -
ROUTING
過濾器: 在路由請求時候被調用; -
POST
過濾器: 在路由到微服務以后執行, 可用來為響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等; -
ERROR
過濾器: 在處理請求過程時發生錯誤時被調用。
Zuul過濾器的類型其實也是Zuul過濾器的生命周期,通過下面這張圖來了解它們的執行過程。
除了上面給出的四種默認的過濾器類型之外,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定制一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到后端的微服務。
1.3 自定義過濾器示例代碼
筆者自己沒有單獨構建一個過濾器示例的場景,我們看一下官方給出的幾個示例。
PRE類型示例
public class QueryParamPreFilter extends ZuulFilter {
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
}
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request.getParameter("foo") != null) {
// put the serviceId in `RequestContext`
ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
}
return null;
}
}
這個是官方給出的一個示例,從請求的參數foo
中獲取需要轉發到的服務Id。當然官方并不建議我們這么做,這里只是方便給出一個示例而已。
ROUTE類型示例
public class OkHttpRoutingFilter extends ZuulFilter {
@Autowired
private ProxyRequestHelper helper;
@Override
public String filterType() {
return ROUTE_TYPE;
}
@Override
public int filterOrder() {
return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse();
}
@Override
public Object run() {
OkHttpClient httpClient = new OkHttpClient.Builder()
// customize
.build();
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String uri = this.helper.buildZuulRequestURI(request);
Headers.Builder headers = new Headers.Builder();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
InputStream inputStream = request.getInputStream();
RequestBody requestBody = null;
if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
MediaType mediaType = null;
if (headers.get("Content-Type") != null) {
mediaType = MediaType.parse(headers.get("Content-Type"));
}
requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
}
Request.Builder builder = new Request.Builder()
.headers(headers.build())
.url(uri)
.method(method, requestBody);
Response response = httpClient.newCall(builder.build()).execute();
LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
responseHeaders.put(entry.getKey(), entry.getValue());
}
this.helper.setResponse(response.code(), response.body().byteStream(), responseHeaders);
context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
return null;
}
}
這個示例是將HTTP請求轉換為使用OkHttp3
進行請求,并將服務端的返回轉換成Servlet的響應。
注意: 官方說這僅僅是一個示例,功能不一定正確。
POST類型示例
public class AddResponseHeaderFilter extends ZuulFilter {
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse(); servletResponse.addHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}
這個示例很簡單就是返回的頭中增加一個隨機生成X-Foo
。
1.4 禁用過濾器
只需要在application.properties(或yml)中配置需要禁用的filter,格式為:zuul.[filter-name].[filter-type].disable=true
。如:
zuul.FormBodyWrapperFilter.pre.disable=true
1.5 關于Zuul過濾器Error的一點補充
當Zuul在執行過程中拋出一個異常時,error
過濾器就會被執行。而SendErrorFilter
只有在RequestContext.getThrowable()
不為空的時候才會執行。它將錯誤信息設置到請求的javax.servlet.error.*
屬性中,并轉發Spring Boot的錯誤頁面。
Zuul過濾器實現的具體類是ZuulServletFilter
,其核心代碼如下:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
try {
preRouting();
} catch (ZuulException e) {
error(e);
postRouting();
return;
}
// Only forward onto to the chain if a zuul response is not being sent
if (!RequestContext.getCurrentContext().sendZuulResponse()) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
try {
routing();
} catch (ZuulException e) {
error(e);
postRouting();
return;
}
try {
postRouting();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
從這段代碼中可以看出,error
可以在所有階段捕獲異常后執行,但是如果post
階段中出現異常被error
處理后則不再回到post
階段執行,也就是說需要保證在post
階段不要有異常,因為一旦有異常后就會造成該過濾器后面其它post
過濾器將不再被執行。
一個簡單的全局異常處理的方法是: 添加一個類型為error
的過濾器,將錯誤信息寫入RequestContext
,這樣SendErrorFilter
就可以獲取錯誤信息了。代碼如下:
public class GlobalErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
Throwable throwable = context.getThrowable();
this.logger.error("[ErrorFilter] error message: {}", throwable.getCause().getMessage());
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", throwable.getCause());
return null;
}
}
2. @EnableZuulServer VS. @EnableZuulProxy
Zuul為我們提供了兩個主應用注解: @EnableZuulServer
和@EnableZuulProxy
,其中@EnableZuulProxy
包含@EnableZuulServer
的功能,而且還加入了@EnableCircuitBreaker
和@EnableDiscoveryClient
。當我們需要運行一個沒有代理功能的Zuul服務,或者有選擇的開關部分代理功能時,那么需要使用 @EnableZuulServer
替代 @EnableZuulProxy
。 這時候我們可以添加任何 ZuulFilter
類型實體類都會被自動加載,這和上一篇使用@EnableZuulProxy
是一樣,但不會自動加載任何代理過濾器。
2.1 @EnableZuulServer默認過濾器
當我們使用@EnableZuulServer
時,默認所加載的過濾器有:
2.1.1 PRE類型過濾器
- ServletDetectionFilter
該過濾器是最先被執行的。其主要用來檢查當前請求是通過Spring的DispatcherServlet
處理運行的,還是通過ZuulServlet
來處理運行的。判斷結果會保存在isDispatcherServletRequest
中,值類型為布爾型。
- FormBodyWrapperFilter
該過濾器的目的是將符合要求的請求體包裝成FormBodyRequestWrapper
對象,以供后續處理使用。
- DebugFilter
PRE類型過濾器。當請求參數中設置了debug
參數時,該過濾器會將當前請求上下文中的RequestContext.setDebugRouting()
和RequestContext.setDebugRequest()
設置為true
,這樣后續的過濾器可以根據這兩個參數信息定義一些debug信息,當生產環境出現問題時,我們就可以通過增加該參數讓后臺打印出debug信息,以幫助我們進行問題分析。對于請求中的debug
參數的名稱,我們可以通過zuul.debug.parameter
進行自定義。
2.1.2 ROUTE類型過濾器
- SendForwardFilter
該過濾器只對請求上下文中存在forward.to
(FilterConstants.FORWARD_TO_KEY
)參數的請求進行處理。即處理之前我們路由規則中forward
的本地跳轉。
2.1.3 POST類型過濾器
- SendResponseFilter
該過濾器就是對代理請求所返回的響應進行封裝,然后作為本次請求的相應發送回給請求者。
2.1.4 Error類型過濾器
- SendErrorFilter
該過濾器就是判斷當前請求上下文中是否有異常信息(RequestContext.getThrowable()
不為空),如果有則默認轉發到/error
頁面,我們也可以通過設置error.path
來自定義錯誤頁面。
2.2 @EnableZuulProxy默認過濾器
@EnableZuulProxy
則在上面的基礎上增加以下過濾器:
2.2.1 PRE類型過濾器
- PreDecorationFilter
該過濾器根據提供的RouteLocator確定路由到的地址,以及怎樣去路由。該路由器也可為后端請求設置各種代理相關的header。
2.2.2 ROUTE類型過濾器
- RibbonRoutingFilter
該過濾器會針對上下文中存在serviceId(可以通過RequestContext.getCurrentContext().get(“serviceId”)
獲取)的請求進行處理,使用Ribbon、Hystrix和可插拔的HTTP客戶端發送請求,并將服務實例的請求結果返回。也就是之前所說的只有當我們使用serviceId配置路由規則時Ribbon和Hystrix方才生效。
- SimpleHostRoutingFilter
該過濾器檢測到routeHost
參數(可通過RequestContext.getRouteHost()
獲取)設置時,就會通過Apache HttpClient向指定的URL發送請求。此時,請求不會使用Hystrix命令進行包裝,所以這類請求也就沒有線程隔離和斷路器保護。
你可以到這里下載本篇的代碼。