spring-cloud-Zuul學習(四)【中級】--自定義zuul Filter詳解【重新定義spring cloud實踐】


 

  • 實現自定義zuul Filter

方法很簡單,只要繼承ZuulFilter跟加入到spring IOC容器即可,zuulFilter是一個抽象類,里面包含以下方法需要我們實現:

String filterType():使用返回值設置Filter類型,可設置pre、route、post、error
int filterOrder():使用返回值設置filter執行次序
boolean shouldFilter():使用返回值設值改Filter是否執行
Object run() throws ZuulException:核心執行邏輯

說起這個,為了方便把上一節說的四種Filter類型粘上:
  •  Zuul總共有四種不同的生命周期類型的Filter:

pre:  在路由下級服務之前執行;比如鑒權、限流都是需要在此類Filter執行。

route:這類Filter是Zuul路由動作的執行者,是Apache HTTPClient或Netflix Ribbon構建和發送原始Http請求的地方,目前已支持OKHTTP。

post:這類Filter是源服務返回結果或發生異常信息發生后執行的;需要對返回信息做一些處理,可以在此類Filter處理。

error:在整個生命周期內如果發生異常,則會進入error Filter,可做全局異常處理。

  前面有說過了關於zuul-server的搭建,這里不說了,以表形式簡單說明下:

項目名 端口 說明
eureka-server 8761 eureka注冊中心
zuul-filter-server 6666 zuul服務端,里面將/client/**的請求路由到client-a去
client-a 7070 一個基本的接口提供者,有一個/student//getStudent接口

  以下為自定義pre類型的Filter測試類:

 1 /**
 2  * 第一個zuul-Filter
 3  * pre類型:路由到下游服務之前執行,可做限流、鑒權等這些
 4  * @author libo
 5  * @date 2019/4/28 17:47
 6  */ 
8
@Component 9 @Slf4j 10 public class FirstPreZuulFilter extends ZuulFilter { 11 12 /** 13 * 14 * @return 使用返回值設置Filter類型,可設置pre、route、post、error 15 */ 16 @Override 17 public String filterType() { 18 return PRE_TYPE; 19 } 20 21 /** 22 * 23 * @return 使用返回值設置filter執行次序 24 */ 25 @Override 26 public int filterOrder() { 27 return 0; 28 } 29 30 /** 31 * 32 * @return 使用返回值設值改Filter是否執行 33 */ 34 @Override 35 public boolean shouldFilter() { 36 return true; 37 } 38 39 /** 40 * 41 * @return 核心執行邏輯 42 * @throws ZuulException 43 */ 44 @Override 45 public Object run() throws ZuulException { 46 log.info("========================這是第一個自定義的Zuul-filter"); 47 return null; 48 } 49 }

 補充說明:可以靜態導入FilterConstants類,里面提供了很多常量信息。(import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;)

我個人喜歡使用postman測試接口,訪問鏈接:localhost:6666/client//student//getStudent,可以看到控制台日志:

 

  • 多層Filter處理案例:

上面是一個最簡單的Filter,實際開發中經常會根據需求,然后自定義實現以上類型的FIlter。在Filter之中,通過com.netflix.zuul.context。RequestContext類進行通訊,內部采用ThreadLocal保存每個請求的一些信息,包括請求路由、錯誤信息、HttpServletRequest、HTTPServletResponse,它還擴展了ConcurrentHashMap,目的是為了在處理過程中保存任何形式的信息。

模擬業務需求:先使用NamePreZuulFilter判斷是否傳入了name,接着使用AgePreZuulFilter判斷是否傳入age,最后在PostZuulFilter里統一處理返回內容。

先以表格方式展示有幾個服務,服務介紹:

項目 端口 說明
eureka-server 8761 本地eureka注冊中心
client-a 7070 接口提供者,提供一個接收name跟age參數的接口,/student//addStudent
zuul-filter-server 6666 zuul服務端,里面存在兩個pre:NamePreZuulFilter(1)、AgePreZuulFilter(2),一個post:PostZuulFilter(3)

整個項目圖:

zuul-filter-server:

依賴說明:三個子項目父級zuul-filter-server的pom.xml這里我只引入了eureka客戶端依賴、gson工具依賴,eureka-server那里就一個eureka服務依賴(spring-boot那些我在zuul-Filter的父級spring-cloud-Zuul那里引入)。

zuul-filter-server的pom.xml:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5     <parent>
 6         <artifactId>spring-cloud-Zuul</artifactId>
 7         <groupId>lb.study</groupId>
 8         <version>1.0-SNAPSHOT</version>
 9     </parent>
10     <modelVersion>4.0.0</modelVersion>
11     <version>1.0-SNAPSHOT</version>
12 
13     <artifactId>zuul-Filter</artifactId>
14 
15     <dependencies>
16         <!-- eureka客戶端依賴開始 -->
17         <dependency>
18             <groupId>org.springframework.cloud</groupId>
19             <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
20         </dependency>
21         <!-- eureka客戶端依賴結束 -->
22 
23         <!-- ribbon依賴開始 -->
24         <!--<dependency>
25             <groupId>org.springframework.cloud</groupId>
26             <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
27         </dependency>-->
28         <!-- ribbon依賴結束 -->
29 
30         <dependency>
31             <groupId>com.google.code.gson</groupId>
32             <artifactId>gson</artifactId>
33         </dependency>
34     </dependencies>
35 
36     <modules>
37         <module>zuul-filter-server</module>
38         <module>eureka-server</module>
39         <module>client-a</module>
40     </modules>
41 </project>

zuul-filter-server的pom:

    <dependencies>
        <!-- zuul服務導包開始 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- zuul服務導包結束 -->
    </dependencies>

zuul-filter-server啟動類ZuulFilterServerApplication:

 1 package lb.study.zuul.zuulfilterserver;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 6 import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
 7 
 8 @SpringBootApplication
 9 @EnableZuulProxy //@EnableZuulProxy區別@EnableZuulServer:@EnableZuulServer缺少幾個zuul內置的Filter,不舉出來了
10 @EnableDiscoveryClient
11 public class ZuulFilterServerApplication {
12 
13     public static void main(String[] args) {
14         SpringApplication.run(ZuulFilterServerApplication.class, args);
15     }
16 
17 }

zuul-filter-server配置文件:

spring:
  application:
    name: zuul-filter-server
server:
  port: 6666

zuul:
  routes:
    client-a: /client/**
NamePreZuulFilter用來驗證name是否存在,(int filterOrder())順序為1。如果沒傳則使用setSendZuulResponse(false)禁止route Filter路由到源服務client-a,使用setResponseBody("信息")定制返回結果(在postZuulFilter有定制這個返回結果)。通過在上下文中設置參數
requestContext.set("logic-is-success",false)來作為下個Filter是否執行的標識:
 1 /**
 2  * 用來判斷name是否存在的Filter
 3  * @author libo
 4  * @date 2019/4/29 11:38
 5  */
 6 @Component
 7 @Slf4j
 8 public class NamePreZuulFilter extends ZuulFilter {
 9 
10     private static Gson gson = new Gson();
11     private Map params = new LinkedHashMap();
12 
13     @Override
14     public String filterType() {
15         return PRE_TYPE;
16     }
17 
18     @Override
19     public int filterOrder() {
20         return 1;
21     }
22 
23     @Override
24     public boolean shouldFilter() {
25         return true;
26     }
27 
28     @Override
29     public Object run() throws ZuulException {
30         log.info("進入NamePreZuulFilter-----------");
31         //取得Request對象並取name參數
32         RequestContext requestContext = RequestContext.getCurrentContext();
33         HttpServletRequest request = requestContext.getRequest();
34         System.out.println("NamePreZuulFilter--request-----------"+request);//這里的request跟源服務那里的request是不同的,所以底層是一個新的請求
35         String name = request.getParameter("name");
36         if(name == null){//如果不存在name
37             //則禁止路由下級服務,但還是會被下個Filter攔截哦,別理解錯了
38             requestContext.setSendZuulResponse(false);
39             //設置返回body,也作為下級判斷
40             params.put("msg","姓名不能為空");
41             params.put("lastDate",new Date());
42             requestContext.setResponseBody(gson.toJson(params));
43             //logic-is-success執行標識保存在上下文中,可作為同類型下游Filter的開關
44             requestContext.set("logic-is-success",false);
45             //結束
46             return null;
47         }
48         //成功,設置logic-is-success執行標識
49         requestContext.set("logic-is-success",true);
50         return null;
51     }
52 }
AgePreZuulFilter執行順序為2,用來驗證age是否傳入,也根據NamePreZuulFilter返回的logic-is-success標識是否執行此Filter。其他的跟前面一樣,定制信息、設置是否路由源服務:
 1 /**
 2  * 接着判斷age是否傳過來
 3  * @author libo
 4  * @date 2019/4/29 13:58
 5  */
 6 @Component
 7 @Slf4j
 8 public class AgePreZuulFilter extends ZuulFilter {
 9     private static Gson gson = new Gson();
10     private Map params = new LinkedHashMap();
11 
12     @Override
13     public String filterType() {
14         return PRE_TYPE;
15     }
16 
17     @Override
18     public int filterOrder() {
19         return 2;
20     }
21 
22     @Override
23     public boolean shouldFilter() {
24         //這里從上下文中取執行標識,並設置是否執行此Filter
25         RequestContext requestContext = RequestContext.getCurrentContext();
26         boolean bool = (boolean)requestContext.get("logic-is-success");
27         return bool;
28     }
29 
30     @Override
31     public Object run() throws ZuulException {
32         log.info("進入AgePreZuulFilter-----------");
33         //取得Request對象並取name參數
34         RequestContext requestContext = RequestContext.getCurrentContext();
35         HttpServletRequest request = requestContext.getRequest();
36         String age = request.getParameter("age");
37         if(age==null){
38             //則禁止路由下級服務,但還是會被下個Filter攔截哦,別理解錯了
39             requestContext.setSendZuulResponse(false);
40             //設置返回body,也作為下級判斷
41             params.put("msg","年齡不能為空");
42             params.put("lastDate",new Date());
43             requestContext.setResponseBody(gson.toJson(params));
44             //logic-is-success執行標識保存在上下文中,可作為同類型下游Filter的開關
45             requestContext.set("logic-is-success",false);
46             //結束
47             return null;
48         }
49         //前面優先的FIlter設置默認logic-is-success=true了 這里就不設置了
50         return null;
51     }
52 }
PostZuulFilter是post類型FIlter,在源服務返回結果后執行,我這里設置順序為3。在這里檢查有無定制的ResponseBody,以及設置字符集,此外還設置了HTTP響應碼:
 1 /**
 2  * 源服務返回結果類型
 3  * @author libo
 4  * @date 2019/4/29 14:19
 5  */
 6 @Component
 7 @Slf4j
 8 public class PostZuulFilter extends ZuulFilter {
 9 
10     @Override
11     public String filterType() {
12         return POST_TYPE;
13     }
14 
15     @Override
16     public int filterOrder() {
17         return 3;
18     }
19 
20     @Override
21     public boolean shouldFilter() {
22         return true;
23     }
24 
25     @Override
26     public Object run() throws ZuulException {
27         log.info("進入PostZuulFilter-----------");
28         //上下文
29         RequestContext requestContext = RequestContext.getCurrentContext();
30         //取response對象
31         HttpServletResponse response = requestContext.getResponse();
32         //響應字符類型
33         response.setCharacterEncoding("utf-8");
34         //獲取上下文中保存的responseBody
35         String responseBody = requestContext.getResponseBody();
36         if(responseBody != null){//如果不為空,則說明存在異常流程(上面只有在異常的時候設置這個)
37             //設置響應信息
38             requestContext.setResponseBody(responseBody);
39             requestContext.setResponseStatusCode(500);
40         }
41         return null;
42     }
43 }

 

  • 測試

使用postman訪問localhost:6666/client//student//addStudent:

  • 測試案例一:name跟age都為空:

狀態碼500(在postZuulFilter那里設置的HTTP響應狀態碼)

可以看到跳過了AgeZuulFilter直接進入PostZuulFilter。

  • 測試案例二:name不為空,age為空:

  • 測試案例三:name不為空,age不為空:

 

requestContext通訊分析:在使用requestContext進行通訊的時候,其實我想過:這個通訊不就是在上下文中存東西,然后用來通訊的嗎?那我是不是可以直接用ServletRequest進行通訊?這個其實也是可以的,因為在各層Filter中那是同一個request,路由到源服務然后再回來也是同一個,畢竟沒有去改存於ConcurrentHashMap內的request對象。這個可以通過RequestContext源碼看清,它有繼承ConcurrentHashMap,將一些request、response這些東西都存那里。

 1     /**
 2      * sets a key value to Boolen.TRUE
 3      *
 4      * @param key
 5      */
 6     public void set(String key) {
 7         put(key, Boolean.TRUE);
 8     }
 9 
10     /**
11      * puts the key, value into the map. a null value will remove the key from the map
12      *
13      * @param key
14      * @param value
15      */
16     public void set(String key, Object value) {
17         if (value != null) put(key, value);
18         else remove(key);
19     }
 1     /**
 2      * @return the HttpServletRequest from the "request" key
 3      */
 4     public HttpServletRequest getRequest() {
 5         return (HttpServletRequest) get("request");
 6     }
 7 
 8     /**
 9      * sets the HttpServletRequest into the "request" key
10      *
11      * @param request
12      */
13     public void setRequest(HttpServletRequest request) {
14         put("request", request);
15     }
16 
17     /**
18      * @return the HttpServletResponse from the "response" key
19      */
20     public HttpServletResponse getResponse() {
21         return (HttpServletResponse) get("response");
22     }
23 
24     /**
25      * sets the "response" key to the HttpServletResponse passed in
26      *
27      * @param response
28      */
29     public void setResponse(HttpServletResponse response) {
30         set("response", response);
31     }

 

到此一個多層Filter業務處理完成。RequestContext中還有很多的API,比如通過InputStream responseDataStream = requestContext.getResponseDataStream();獲取源服務返回的數據流,再做些統一處理。

 本人所有的源碼都存在github.com上面


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM