- 實現自定義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上面