上一篇:《Spring Cloud入門教程(五):API服務(wù)網(wǎng)關(guān)(Zuul) 上》
本人和同事撰寫的《Spring Cloud微服務(wù)架構(gòu)開發(fā)實戰(zhàn)》一書也在京東矿辽、當當?shù)葧晟霞芮镉荆蠹铱梢渣c擊這里前往購買,多謝大家支持和捧場恍风!
Zuul給我們的第一印象通常是這樣:它包含了對請求的路由和過濾兩個功能,其中路由功能負責將外部請求轉(zhuǎn)發(fā)到具體的微服務(wù)實例上,是實現(xiàn)外部訪問統(tǒng)一入口的基礎(chǔ)熟菲。過濾器功能則負責對請求的處理過程進行干預(yù),是實現(xiàn)請求校驗朴恳、服務(wù)聚合等功能的基礎(chǔ)抄罕。然而實際上,路由功能在真正運行時于颖,它的路由映射和請求轉(zhuǎn)發(fā)都是由幾個不同的過濾器完成的呆贿。其中,路由映射主要是通過PRE類型的過濾器完成森渐,它將請求路徑與配置的路由規(guī)則進行匹配做入,以找到需要轉(zhuǎn)發(fā)的目標地址。而請求轉(zhuǎn)發(fā)的部分則是由Route類型的過濾器來完成同衣,對PRE類型過濾器獲得的路由地址進行轉(zhuǎn)發(fā)竟块。所以,過濾器可以說是Zuul實現(xiàn)API網(wǎng)關(guān)功能最重要的核心部件耐齐,每一個進入Zuul的請求都會經(jīng)過一系列的過濾器處理鏈得到請求響應(yīng)并返回給客戶端浪秘。
1. 過濾器簡介
1.1 過濾器特性
Zuul過濾器的關(guān)鍵特性有:
- Type: 定義在請求執(zhí)行過程中何時被執(zhí)行;
- Execution Order: 當存在多個過濾器時蒋情,用來指示執(zhí)行的順序,值越小就會越早執(zhí)行;
- Criteria: 執(zhí)行的條件耸携,即該過濾器何時會被觸發(fā);
- Action: 具體的動作棵癣。
過濾器之間并不會直接進行通信,而是通過RequestContext
來共享信息夺衍,RequestContext
是線程安全的狈谊。
對應(yīng)上面Zuul過濾器的特性,我們在實現(xiàn)一個自定義過濾器時需要實現(xiàn)的方法有:
/**
* 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()方法返回的是執(zhí)行順序;
- shouldFilter()方法則是判斷是否需要執(zhí)行該過濾器;
- run()則是所要執(zhí)行的具體過濾動作沟沙。
1.2 過濾器類型
Zuul中定義了四種標準的過濾器類型河劝,這些過濾器類型對應(yīng)于請求的典型生命周期。
-
PRE
過濾器: 在請求被路由之前調(diào)用, 可用來實現(xiàn)身份驗證尝胆、在集群中選擇請求的微服務(wù)丧裁、記錄調(diào)試信息等; -
ROUTING
過濾器: 在路由請求時候被調(diào)用; -
POST
過濾器: 在路由到微服務(wù)以后執(zhí)行, 可用來為響應(yīng)添加標準的HTTP Header、收集統(tǒng)計信息和指標含衔、將響應(yīng)從微服務(wù)發(fā)送給客戶端等; -
ERROR
過濾器: 在處理請求過程時發(fā)生錯誤時被調(diào)用煎娇。
Zuul過濾器的類型其實也是Zuul過濾器的生命周期,通過下面這張圖來了解它們的執(zhí)行過程贪染。
除了上面給出的四種默認的過濾器類型之外缓呛,Zuul還允許我們創(chuàng)建自定義的過濾器類型。例如杭隙,我們可以定制一種STATIC類型的過濾器哟绊,直接在Zuul中生成響應(yīng),而不將請求轉(zhuǎn)發(fā)到后端的微服務(wù)痰憎。
1.3 自定義過濾器示例代碼
筆者自己沒有單獨構(gòu)建一個過濾器示例的場景票髓,我們看一下官方給出的幾個示例。
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;
}
}
這個是官方給出的一個示例铣耘,從請求的參數(shù)foo
中獲取需要轉(zhuǎn)發(fā)到的服務(wù)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請求轉(zhuǎn)換為使用OkHttp3
進行請求裆操,并將服務(wù)端的返回轉(zhuǎn)換成Servlet的響應(yīng)。
注意: 官方說這僅僅是一個示例炉媒,功能不一定正確踪区。
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 關(guān)于Zuul過濾器Error的一點補充
當Zuul在執(zhí)行過程中拋出一個異常時,error
過濾器就會被執(zhí)行白粉。而SendErrorFilter
只有在RequestContext.getThrowable()
不為空的時候才會執(zhí)行密强。它將錯誤信息設(shè)置到請求的javax.servlet.error.*
屬性中茅郎,并轉(zhuǎn)發(fā)Spring Boot的錯誤頁面。
Zuul過濾器實現(xiàn)的具體類是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
可以在所有階段捕獲異常后執(zhí)行奕扣,但是如果post
階段中出現(xiàn)異常被error
處理后則不再回到post
階段執(zhí)行薪鹦,也就是說需要保證在post
階段不要有異常,因為一旦有異常后就會造成該過濾器后面其它post
過濾器將不再被執(zhí)行惯豆。
一個簡單的全局異常處理的方法是: 添加一個類型為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為我們提供了兩個主應(yīng)用注解: @EnableZuulServer
和@EnableZuulProxy
地熄,其中@EnableZuulProxy
包含@EnableZuulServer
的功能,而且還加入了@EnableCircuitBreaker
和@EnableDiscoveryClient
芯杀。當我們需要運行一個沒有代理功能的Zuul服務(wù)端考,或者有選擇的開關(guān)部分代理功能時,那么需要使用 @EnableZuulServer
替代 @EnableZuulProxy
揭厚。 這時候我們可以添加任何 ZuulFilter
類型實體類都會被自動加載却特,這和上一篇使用@EnableZuulProxy
是一樣,但不會自動加載任何代理過濾器筛圆。
2.1 @EnableZuulServer默認過濾器
當我們使用@EnableZuulServer
時裂明,默認所加載的過濾器有:
2.1.1 PRE類型過濾器
- ServletDetectionFilter
該過濾器是最先被執(zhí)行的。其主要用來檢查當前請求是通過Spring的DispatcherServlet
處理運行的太援,還是通過ZuulServlet
來處理運行的闽晦。判斷結(jié)果會保存在isDispatcherServletRequest
中,值類型為布爾型提岔。
- FormBodyWrapperFilter
該過濾器的目的是將符合要求的請求體包裝成FormBodyRequestWrapper
對象仙蛉,以供后續(xù)處理使用。
- DebugFilter
PRE類型過濾器唧垦。當請求參數(shù)中設(shè)置了debug
參數(shù)時捅儒,該過濾器會將當前請求上下文中的RequestContext.setDebugRouting()
和RequestContext.setDebugRequest()
設(shè)置為true
,這樣后續(xù)的過濾器可以根據(jù)這兩個參數(shù)信息定義一些debug信息振亮,當生產(chǎn)環(huán)境出現(xiàn)問題時巧还,我們就可以通過增加該參數(shù)讓后臺打印出debug信息,以幫助我們進行問題分析坊秸。對于請求中的debug
參數(shù)的名稱麸祷,我們可以通過zuul.debug.parameter
進行自定義。
2.1.2 ROUTE類型過濾器
- SendForwardFilter
該過濾器只對請求上下文中存在forward.to
(FilterConstants.FORWARD_TO_KEY
)參數(shù)的請求進行處理褒搔。即處理之前我們路由規(guī)則中forward
的本地跳轉(zhuǎn)阶牍。
2.1.3 POST類型過濾器
- SendResponseFilter
該過濾器就是對代理請求所返回的響應(yīng)進行封裝喷面,然后作為本次請求的相應(yīng)發(fā)送回給請求者。
2.1.4 Error類型過濾器
- SendErrorFilter
該過濾器就是判斷當前請求上下文中是否有異常信息(RequestContext.getThrowable()
不為空)走孽,如果有則默認轉(zhuǎn)發(fā)到/error
頁面鞋拟,我們也可以通過設(shè)置error.path
來自定義錯誤頁面颅和。
2.2 @EnableZuulProxy默認過濾器
@EnableZuulProxy
則在上面的基礎(chǔ)上增加以下過濾器:
2.2.1 PRE類型過濾器
- PreDecorationFilter
該過濾器根據(jù)提供的RouteLocator確定路由到的地址,以及怎樣去路由。該路由器也可為后端請求設(shè)置各種代理相關(guān)的header乖杠。
2.2.2 ROUTE類型過濾器
- RibbonRoutingFilter
該過濾器會針對上下文中存在serviceId(可以通過RequestContext.getCurrentContext().get(“serviceId”)
獲取)的請求進行處理候衍,使用Ribbon荆残、Hystrix和可插拔的HTTP客戶端發(fā)送請求啼肩,并將服務(wù)實例的請求結(jié)果返回。也就是之前所說的只有當我們使用serviceId配置路由規(guī)則時Ribbon和Hystrix方才生效硕盹。
- SimpleHostRoutingFilter
該過濾器檢測到routeHost
參數(shù)(可通過RequestContext.getRouteHost()
獲取)設(shè)置時符匾,就會通過Apache HttpClient向指定的URL發(fā)送請求。此時瘩例,請求不會使用Hystrix命令進行包裝啊胶,所以這類請求也就沒有線程隔離和斷路器保護。
你可以到這里下載本篇的代碼仰剿。