1. 程式人生 > 其它 >SpringCloud元件--Zuul入門解析

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來對請求的不同時間短進行功能擴充套件。