SpringCloud元件--Zuul入門解析
一、Zuul簡介
1、閘道器的作用
分散式架構中,服務節點數量較多,而對於客戶端而言,多個服務節點暴露出來的API應該是統一的,否則每個節點地址不同,客戶端就需要維護所有服務節點地址然後再選擇一個訪問,很明顯客戶端的維護成本就很高。
此時就需要有一個暴露統一API的閘道器服務將多服務節點封裝,客戶端訪問閘道器,閘道器再將客戶端的請求分發給被封裝的服務節點,這樣就避免了客戶端和服務端的直接互動。
另外閘道器除了可以實現動態路由功能外,還可以實現引數校驗、鑑權、日誌、快取、負載均衡、監控、限流等多種功能。總之對於非業務需求的統一功能都可以交給閘道器來實現,設計思想有點類似於Spring的面向切面程式設計。
2、Zuul是什麼
Zuul是Netfilx開源的微服務系統的閘道器元件。
Zuul的作用有很多,如下幾個方面
1.配合Ribbon、Eureka可以實現智慧動態路由和負載均衡功能,將請求按策略分發到叢集中合適的服務例項;
2.將服務和客戶端之間進行解耦,客戶端訪問的是閘道器暴露出來的統一API,客戶端不需要關係服務的執行情況;
3.統一鑑權,監控,日誌,快取,限流,引數校驗等功能;
4.通過流量監控,可以根據流量進行服務降級;
3、Zuul工作流程
Zuul本質是一個Servlet來對請求進行控制,核心是建立了一系列的過濾器。Zuul包括以下四種過濾器:
1、PRE過濾器:請求路由到具體的服務之前執行,可以用作安全校驗、身份校驗、引數校驗等前置工作;
2、ROUTING過濾器:用於將請求路由到具體的服務例項,預設使用Http Client進行網路請求;
3、POST過濾器:在請求已經被路由到微服務後執行,通常用於收集統計資訊、指標並將響應返回給客戶端;
4、ERROR過濾器:在其他過濾器出現異常時執行;
各個型別的過濾器執行順序依次為PRE過濾器->ROUTING過濾器->POST過濾器,如果出現異常就執行ERROR過濾器
二、Zuul實踐
Zuul也是一個服務,所以需要構建Zuul-Server服務
新增Zuul相關依賴
1 <dependencies> 2 <dependency> 3<groupId>org.springframework.cloud</groupId> 4 <artifactId>spring-cloud-starter-eureka</artifactId> 5 <version>1.4.7.RELEASE</version> 6 </dependency> 7 <dependency> 8 <groupId>org.springframework.cloud</groupId> 9 <artifactId>spring-cloud-starter-zuul</artifactId> 10 <version>1.4.7.RELEASE</version> 11 </dependency> 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-web</artifactId> 15 <version>2.5.0</version> 16 </dependency> 17 <dependency> 18 <groupId>org.springframework.boot</groupId> 19 <artifactId>spring-boot-starter-test</artifactId> 20 <scope>test</scope> 21 </dependency> 22 </dependencies>
新增Zuul相關配置application.yml
1 server: 2 port: 5000 3 spring: 4 application: 5 name: zuul-server 6 eureka: 7 client: 8 serviceUrl: 9 defaultZone: http://localhost:8761/eureka/ 10 zuul: 11 routes: 12 goods: 13 path: /goods/** 14 serverId: goods 15 order: 16 path: /order/** 17 serverId: order
其中server表示Zuul服務配置,eureka表示Zuul也是一個Eureka客戶端需要配置Eureka,最後就是閘道器配置zuul,routes是路由配置,需要配置服務的serverId表示路由的服務名稱,path表示路由的服務地址,配置之後zuul可以將指定serverId的請求分發到指定的服務。
新增啟動類,並新增相關注解
1 @SpringBootApplication 2 @EnableEurekaClient 3 @EnableZuulProxy 4 public class ZuulApplication { 5 6 public static void main(String[] args){ 7 SpringApplication.run(ZuulApplication.class); 8 System.out.println("Zuul starting..."); 9 } 10 }
@EnableEurekaClient註解表示ZuulServer是一個Eureka客戶端,@EnableZuulProxy註解表示當前是一個Zuul伺服器,開啟Zuul功能。
啟動ZuulApplication類,此時就可以完成請求路由功能了,訪問http://localhost:5000/goods/xxxx就可以將請求轉發到goods服務的API
新增不同邏輯的過濾器,如進行引數校驗,自定義CheckFilter過濾器繼承父類ZuulFilter,實現ZuulFilter的抽象方法。
@Component public class CheckFilter extends ZuulFilter{ @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { /**處理路由過濾等邏輯,根據filterType在不同的時機執行具體的邏輯 */ //1.獲取請求上下文 RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); Iterator<Map.Entry<String,String[]>> iterator = request.getParameterMap().entrySet().iterator(); while (iterator.hasNext()){ Map.Entry<String ,String[]> entry = iterator.next(); if(entry.getValue()==null || entry.getValue().length==0){ System.out.println("引數校驗失敗:引數為空:" + entry.getKey()); return null; } } return null; } }
實現filterType方法表示當前是一個PRE過濾器,filterOrder方法返回過濾器順序,值越小優先順序越高, run方法就是過濾器的具體處理邏輯。
三、Zuul實現原理
3.1、@EnableZuulProxy註解
Zuul使用時比較簡單,只需要在啟動類新增@EnableZuulProxy註解即可,所以Zuul的所有功能都圍繞該註解進行,@EnableZuulProxy的作用是載入ZuulProxyMarkerConfiguration的例項,@EnableZuulProxy定義如下:
1 @EnableCircuitBreaker 2 @Target(ElementType.TYPE) 3 @Retention(RetentionPolicy.RUNTIME) 4 @Import(ZuulProxyMarkerConfiguration.class) 5 public @interface EnableZuulProxy { 6 }
而ZuulProxyMarkerConfiguration的作用是注入一個ZuulProxy的標記例項,定義如下:
1 @Configuration 2 public class ZuulProxyMarkerConfiguration { 3 @Bean 4 public Marker zuulProxyMarkerBean() { 5 return new Marker(); 6 } 7 8 class Marker { 9 } 10 }
內部類Marker唯一的作用就是用於標記,所以需要看哪裡需要用到該標記,通過引用搜索發現引用的地方是ZuulProxyAutoConfiguration,父類是ZuulServerAutoConfiguration,定義如下:
@Configuration @EnableConfigurationProperties({ ZuulProperties.class }) @ConditionalOnClass(ZuulServlet.class) @ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class) // Make sure to get the ServerProperties from the same place as a normal web app would // FIXME @Import(ServerPropertiesAutoConfiguration.class) public class ZuulServerAutoConfiguration {
父類ZuulServerAutoConfiguration中會向Spring容器中注入ServletRegistrationBean例項,該例項中建立了一個ZuulServlet。而Zuul的核心功能就在於這個ZuulServlet,Zuul接收到的所有請求最終都會由於ZuulServlet的service方法來完成
@Bean @ConditionalOnMissingBean(name = "zuulServlet") public ServletRegistrationBean zuulServlet() { ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(new ZuulServlet(), this.zuulProperties.getServletPattern()); // The whole point of exposing this servlet is to provide a route that doesn't // buffer requests. servlet.addInitParameter("buffer-requests", "false"); return servlet; }
3.2、ZuulServlet
ZuulServlet繼承之HttpServlet,初始化init方法建立了ZuulRunner物件,而核心功能在於service方法,所有請求都會先執行到service方法,原始碼如下:
@Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { /** 1.初始化ZuulRunner 物件*/ init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); /** 2.獲取HttpRequest請求上下文*/ RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { /** 3.執行pre過濾器*/ preRoute(); } catch (ZuulException e) { /** 異常時執行error過濾器和post過濾器*/ error(e); postRoute(); return; } try { /** 4.執行route過濾器*/ route(); } catch (ZuulException e) { /** 異常時執行error過濾器和post過濾器*/ error(e); postRoute(); return; } try { /** 5.執行post過濾器*/ postRoute(); } catch (ZuulException e) { /** 異常時執行error過濾器*/ error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); } finally { /** 清理本地快取RequestContext*/ RequestContext.getCurrentContext().unset(); } }
整體流程比較清晰,依次執行 PRE過濾器 -> ROUTE過濾器 -> POST過濾器, 如果有異常就執行 ERROR過濾器, 另外如果POST過濾器執行之前異常同樣也會執行POST過濾器, 所以可以保證POST過濾器肯定會被執行,
而ERROR過濾器只會在其他過濾器異常時才會執行。另外在執行過濾器邏輯之前會初始化上下文RequestContext,過濾器執行完之後清理RequestContext。
ZuulServlet的preRoute、route、postRoute、error方法都沒有具體實現,都委託給ZuulRunner執行對應的方法,而ZuulRunner也不是具體的執行者,而是交給了FilterProcessor來執行。
3.3、FilterProcessor
FilterProcessor是過濾器的具體執行者,是一個單例物件。ZuulServlet的各種過濾器最終分別執行了FilterProcessor的preRoute()、route()、postRoute()、error()方法,而這四個方法最終都是執行了FilterProcessor的內部方法runFilters(String filterType)方法。
runFilter原始碼如下:
1 public Object runFilters(String sType) throws Throwable { 2 if (RequestContext.getCurrentContext().debugRouting()) { 3 Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); 4 } 5 boolean bResult = false; 6 /** 1.獲取指定型別的過濾器列表*/ 7 List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); 8 if (list != null) { 9 for (int i = 0; i < list.size(); i++) { 10 ZuulFilter zuulFilter = list.get(i); 11 /** 2.遍歷執行過濾器*/ 12 Object result = processZuulFilter(zuulFilter); 13 if (result != null && result instanceof Boolean) { 14 bResult |= ((Boolean) result); 15 } 16 } 17 } 18 return bResult; 19 }
首先呼叫FilterLoader物件的getFilterByType方法根據過濾器型別查詢過濾器列表,然後執行processZuulFilter方法執行過濾器邏輯
3.3.1、獲取過濾器
所有Zuul的過濾器都需要實現ZuulFilter介面,並且需要被@Component註解修飾從而注入到Spring容器中,而ZuulServerAutoConfiguration類中有一個靜態內部類ZuulFilterConfiguration類,該類的作用是將Spring容器中所有的ZuulFilter例項取出來,程式碼如下:
/** ZuulServerAutoConfiguration內部類 */ @Configuration protected static class ZuulFilterConfiguration { @Autowired private Map<String, ZuulFilter> filters; @Bean public ZuulFilterInitializer zuulFilterInitializer( CounterFactory counterFactory, TracerFactory tracerFactory) { FilterLoader filterLoader = FilterLoader.getInstance(); FilterRegistry filterRegistry = FilterRegistry.instance(); return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry); } }
ZuulFilterConfigruation首先注入所有的ZuulFilter物件,採用Map儲存,而ZuulFilterInitiailizer的作用就是初始化過濾器註冊器,程式碼如下:
@PostConstruct public void contextInitialized() { log.info("Starting filter initializer"); TracerFactory.initialize(tracerFactory); CounterFactory.initialize(counterFactory); for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) { filterRegistry.put(entry.getKey(), entry.getValue()); } }
FilterRegistry是過濾器註冊器,內部採用Map<String, ZuulFilter> filters 儲存所有ZuulFilter過濾器例項。
再回到ZuulRunner類的runFilters方法,通過FilterLoader的getFiltersByType方法獲取指定型別過濾器,所以FilterLoader的作用就是將FilterRegistry中過濾器按不同型別進行歸類,採用Map<String, List<ZuulFilter>> hashFiltersByType 儲存不同型別的過濾器列表。
所以獲取過濾器的邏輯比較簡單,就是從Spring容器中獲取所有ZuulFilter例項,然後按不同的型別進行分組存在Map中快取起來即可。
3.3.2、執行過濾器
1 /** 執行過濾器 */ 2 public Object processZuulFilter(ZuulFilter filter) throws ZuulException { 3 /** 1.獲取請求上下文 */ 4 RequestContext ctx = RequestContext.getCurrentContext(); 5 boolean bDebug = ctx.debugRouting(); 6 final String metricPrefix = "zuul.filter-"; 7 long execTime = 0; 8 String filterName = ""; 9 try { 10 long ltime = System.currentTimeMillis(); 11 filterName = filter.getClass().getSimpleName(); 12 13 RequestContext copy = null; 14 Object o = null; 15 Throwable t = null; 16 17 if (bDebug) { 18 Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName); 19 copy = ctx.copy(); 20 } 21 /** 2.執行過濾器的runFilter方法 */ 22 ZuulFilterResult result = filter.runFilter(); 23 ExecutionStatus s = result.getStatus(); 24 execTime = System.currentTimeMillis() - ltime; 25 26 switch (s) { 27 /** 3.將執行結果存在RequestContext上下文中 */ 28 case FAILED: 29 t = result.getException(); 30 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); 31 break; 32 case SUCCESS: 33 o = result.getResult(); 34 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime); 35 if (bDebug) { 36 Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms"); 37 Debug.compareContextState(filterName, copy); 38 } 39 break; 40 default: 41 break; 42 } 43 44 if (t != null) throw t; 45 46 usageNotifier.notify(filter, s); 47 /** 4.返回執行結果 */ 48 return o; 49 50 } catch (Throwable e) { 51 if (bDebug) { 52 Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage()); 53 } 54 usageNotifier.notify(filter, ExecutionStatus.FAILED); 55 if (e instanceof ZuulException) { 56 throw (ZuulException) e; 57 } else { 58 ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName); 59 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); 60 throw ex; 61 } 62 } 63 }
整體邏輯就是先獲取全域性請求上下文RequestContext,然後呼叫ZuulFilter的runFilter方法執行過濾器邏輯,最後將zuulFilter執行的結果存入上下文RequestContext中並返回執行結果即可。
3.4、Zuul內建過濾器
Zuul過濾器可以通過自定義ZuulFilter實現類來新增,同時Zuul還提供了內建的眾多過濾器,分別如下圖示,比較核心的就是RibbonRoutingFilter,這個過濾器負載路由轉發並且集成了Ribbon的負載均衡功能.
總結:
Zuul本質上就是一個Servlet,並且通過Spring容器管理了一系列的過濾器ZuulFilter例項,各個ZuulFilter有一個型別,包括preRoute、route、postRoute、error四種類型,不同的型別執行的順序和時機不同。當請求進入Zuul時,由ZuulServlet接收並通過service方法處理。service方法邏輯就是從Spring容器中找到各種型別的ZuulFilter過濾器例項,然後遍歷按順序和時機來執行過濾器的處理邏輯。使用時可以自定義ZuulFilter來對請求的不同時間短進行功能擴充套件。