Zuul的核心
Filter是Zuul的核心,用來實現對外服務的控制。Filter的生命週期有4個,分別是“PRE”、“ROUTING”、“POST”、“ERROR”,整個生命週期可以用下圖來表示。
Zuul大部分功能都是通過過濾器來實現的,這些過濾器類型對應於請求的典型生命週期。
- PRE: 這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試信息等。
- ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
- POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來爲響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
- ERROR:在其他階段發生錯誤時執行該過濾器。 除了默認的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定製一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。
Zuul中默認實現的Filter
類型 | 順序 | 過濾器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 標記處理Servlet的類型 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
route | 1 | DebugFilter | 標記調試標誌 |
route | 5 | PreDecorationFilter | 處理請求上下文供後續使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發 |
route | 100 | SimpleHostRoutingFilter | url請求轉發 |
route | 500 | SendForwardFilter | forward請求轉發 |
post | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
自定義Filter
實現自定義Filter,需要繼承ZuulFilter的類,並覆蓋其中的4個方法。
我們假設有這樣一個場景,因爲服務網關應對的是外部的所有請求,爲了避免產生安全隱患,我們需要對請求做一定的限制,比如請求中含有Token便讓請求繼續往下走,如果請求不帶Token就直接返回並給出提示。
首先自定義一個Filter,在run()方法中驗證參數是否含有Token。
package com.example.demo; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; public class CusFilter extends ZuulFilter { @Override public String filterType() { return "pre"; //定義filter的類型,有pre、route、post、error四種 } @Override public int filterOrder() { return 10; //定義filter的順序,數字越小表示順序越高,越先執行 } @Override public boolean shouldFilter() { return true; //表示是否需要執行該filter,true表示執行,false表示不執行 } @Override public Object run() { // return null; //filter需要執行的具體操作 RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); // logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString()); String token = request.getParameter("token");// 獲取請求的參數 if (StringUtils.isNotBlank(token)) { ctx.setSendZuulResponse(true); //對請求進行路由 ctx.setResponseStatusCode(200); ctx.set("isSuccess", true); return null; } else { ctx.setSendZuulResponse(false); //不對其進行路由 ctx.setResponseStatusCode(400); ctx.setResponseBody("token is empty"); ctx.set("isSuccess", false); return null; } } }
將CusFilter加入到請求攔截隊列,在啓動類中添加以下代碼:
@Bean public CusFilter tokenFilter() { return new CusFilter(); }
測試
分別啓動EurekaServer、EurekaClient、SpringColudZuulSimple。輸入http://localhost:8890/producer/hello?name=cuiyw時頁面顯示如下。
輸入url帶token時輸出正常。
路由熔斷
當我們的後端服務出現異常的時候,我們不希望將異常拋出給最外層,期望服務可以自動進行一降級。Zuul給我們提供了這樣的支持。當某個服務出現異常時,直接返回我們預設的信息。
我們通過自定義的fallback方法,並且將其指定給某個route來實現該route訪問出問題的熔斷處理。主要繼承ZuulFallbackProvider接口來實現,ZuulFallbackProvider默認有兩個方法,一個用來指明熔斷攔截哪個服務,一個定製返回內容。
實現類通過實現getRoute方法,告訴Zuul它是負責哪個route定義的熔斷。而fallbackResponse方法則是告訴 Zuul 斷路出現時,它會提供一個什麼返回值來處理請求。
後來Spring又擴展了此類,豐富了返回方式,在返回的內容中添加了異常信息,因此最新版本建議直接繼承類FallbackProvider 。
測試
依次啓動EurekaServer、EurekaClient(設置不同端口啓動)、SpringColudZuulSimple,之後需要將EurekaClient斷開一個,如果不知道斷開哪個時可以使用進程id。
package com.example.demo; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; @Component public class ProducerFallback implements FallbackProvider{ public ClientHttpResponse fallbackResponse() { // TODO Auto-generated method stub return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "OK"; } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); // logger.info("Excption {}",reason); } return fallbackResponse(); } @Override public String getRoute() { return "spring-cloud-producer"; } }
當服務出現異常時,打印相關異常信息,並返回”The service is unavailable.”。
路由重試
有時候因爲網絡或者其它原因,服務可能會暫時的不可用,這個時候我們希望可以再次對服務進行重試,Zuul也幫我們實現了此功能,需要結合Spring Retry 一起來實現。下面我們以上面的項目爲例做演示。
添加Spring Retry依賴
首先在spring-cloud-zuul項目中添加Spring Retry依賴。
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.2.4.RELEASE</version> </dependency>
開啓Zuul Retry
再配置文件中配置啓用Zuul Retry,在main方法中添加@EnableRetry註解
#是否開啓重試功能
zuul.retryable=true
#對當前服務的重試次數
ribbon.MaxAutoRetries=2
#切換相同Server的次數
ribbon.MaxAutoRetriesNextServer=0
測試
在EurekaClient中增加支持日誌功能,引入spring-boot-starter-log4j2,同時排除start-web中默認的日誌。
</dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-logging</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency>
在application.properties同級目錄下增加log4j2.xml配置文件。
<?xml version="1.0" encoding="UTF-8"?> <!--Configuration後面的status,這個用於設置log4j2自身內部的信息輸出,可以不設置,當設置成trace時,你會看到log4j2內部各種詳細輸出;可以設置成OFF(關閉)或Error(只輸出錯誤信息) --> <!--monitorInterval:Log4j2能夠自動檢測修改配置 文件和重新配置本身,設置間隔秒數 --> <Configuration status="WARN" monitorInterval="30"> <Properties> <!-- 缺省配置(用於開發環境),配置日誌文件輸出目錄和動態參數。其他環境需要在VM參數中指定; “sys:”表示:如果VM參數中沒指定這個變量值,則使用本文件中定義的缺省全局變量值 --> <Property name="instance">EurekaClient</Property> <Property name="log.dir">D:\log\logs</Property> </Properties> <!-- 定義所有的appender --> <Appenders> <!-- 優先級從高到低分別是 OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL --> <!-- 單詞解釋: Match:匹配 DENY:拒絕 Mismatch:不匹配 ACCEPT:接受 --> <!-- DENY,日誌將立即被拋棄不再經過其他過濾器; NEUTRAL,有序列表裏的下個過濾器過接着處理日誌; ACCEPT,日誌會被立即處理,不再經過剩餘過濾器。 --> <!--輸出日誌的格式 %d{yyyy-MM-dd HH:mm:ss, SSS} : 日誌生產時間 %p : 日誌輸出格式 %c : logger的名稱 %m : 日誌內容,即 logger.info("message") %n : 換行符 %C : Java類名 %L : 日誌輸出所在行數 %M : 日誌輸出所在方法名 hostName : 本地機器名 hostAddress : 本地ip地址 --> <!--這個輸出控制檯的配置 --> <Console name="Console" target="SYSTEM_OUT"> <!--控制檯只輸出level及以上級別的信息(onMatch),其他的直接拒絕(onMismatch)--> <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="NEUTRAL"/> <!--輸出日誌的格式 --> <PatternLayout pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/> </Console> <!-- info及以上級別的信息,每次大小超過size,則這size大小的日誌會自動存入按年份-月份建立的文件夾下面並進行壓縮,作爲存檔 <RollingRandomAccessFile> filepattern 中的日期格式精確位數決定了生成日誌的日期單位, 如果按月生成日誌,那麼 filePath 修改爲 "${LOG_HOME}/app-%d{yyyy-MM}.log"; 按小時生成日誌,filePath = "${LOG_HOME}/app-%d{yyyy-MM-dd-HH-mm}.log"; --> <RollingRandomAccessFile name="infoLog" fileName="${log.dir}/${instance}-info.log" filePattern="${log.dir}/%d{yyyy-MM}/${instance}-info-%d{yyyy-MM-dd}-%i.log.gz" append="true"> <!--filePattern="${log.dir}/%d{yyyy-MM}/${instance}-info-%d{mm-ss}-%i.log.gz"--> <PatternLayout pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/> <!--控制檯只輸出level及以上級別的信息(onMatch),其他的直接拒絕(onMismatch) --> <Filters> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> </Filters> <Policies> <!-- 基於時間的滾動策略,interval屬性用來指定多久滾動一次,默認是1 hour --> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <!-- 基於指定文件大小的滾動策略,size屬性用來定義每個日誌文件的大小 --> <SizeBasedTriggeringPolicy size="200MB"/> <!-- DefaultRolloverStrategy:用來指定同一個文件夾下最多有幾個日誌文件時開始刪除最舊的,創建新的(通過max屬性) --> </Policies> <!-- <CronTriggeringPolicy schedule="* * * * * ?"/>觸發策略 --> <!-- <DirectWriteRolloverStrategy maxFiles="10" /> --> <!-- 最多備份30天以內的日誌,此處爲策略限制,Delete中可以按自己需要用正則表達式編寫 --> <!-- DefaultRolloverStrategy 加屬性:max="30" 保留近30天的日誌文件 --> <DefaultRolloverStrategy> <!-- 在rollover時間內匹配刪除基本目錄下所有滿足參數glob等於*/wyait-*.log.gz和超過3天或更早的文件。 --> <!-- 1.maxDepth:要訪問的目錄的最大級別數。值爲0表示僅訪問起始文件(基本路徑本身),除非被安全管理者拒絕。Integer.MAX_VALUE的值 表示應該訪問所有級別。默認爲1,意思是指定基本目錄中的文件。 2. age的單位:D、H、M、S,分別表示天、小時、分鐘、秒 3. basePath表示日誌存儲的基目錄,maxDepth=“1”表示當前目錄。因爲我們封存的歷史日誌在basePath裏面的backup目錄,所以maxDepth設置爲2。 --> <Delete basePath="${log.dir}" maxDepth="2"> <!-- IfFileName - glob: 如果regex沒有指定的話,則必須。使用類似於正則表達式但是又具有更簡單的有限模式語言來匹配相對路徑(相對於基本路徑) --> <IfFileName glob="*/EurekaClient-*.log.gz"/> <!-- IfLastModified - age: 必須。指定持續時間duration。該條件接受比指定持續時間更早或更舊的文件。 --> <IfLastModified age="90D"/> </Delete> </DefaultRolloverStrategy> </RollingRandomAccessFile> <!-- warn級別的日誌信息 --> <RollingRandomAccessFile name="warnLog" fileName="${log.dir}/${instance}-warn.log" filePattern="${log.dir}/%d{yyyy-MM}/${instance}-warn-%d{yyyy-MM-dd}-%i.log.gz" append="true"> <Filters> <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> </Filters> <PatternLayout pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <SizeBasedTriggeringPolicy size="200MB"/> </Policies> </RollingRandomAccessFile> <!-- error級別的日誌信息 --> <RollingRandomAccessFile name="errorLog" fileName="${log.dir}/${instance}-error.log" filePattern="${log.dir}/%d{yyyy-MM}/${instance}-error-%d{yyyy-MM-dd}-%i.log.gz" append="true"> <Filters> <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/> </Filters> <PatternLayout pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <SizeBasedTriggeringPolicy size="200MB"/> </Policies> </RollingRandomAccessFile> </Appenders> <!-- 全局配置,默認所有的Logger都繼承此配置 --> <!-- 用來配置LoggerConfig,包含一個root logger和若干個普通logger。 additivity指定是否同時輸出log到父類的appender,缺省爲true。 一個Logger可以綁定多個不同的Appender。只有定義了logger並引入的appender,appender纔會生效。 --> <Loggers> <!-- 第三方的軟件日誌級別 --> <logger name="org.springframework" level="info" additivity="true"> <AppenderRef ref="Console"/> <AppenderRef ref="infoLog"/> <AppenderRef ref="warnLog"/> <AppenderRef ref="errorLog"/> </logger> <logger name="java.sql.PreparedStatement" level="debug" additivity="true"> <AppenderRef ref="Console"/> <AppenderRef ref="infoLog"/> </logger> <!-- root logger 配置 --> <Root level="debug" includeLocation="true"> <AppenderRef ref="infoLog"/> <AppenderRef ref="Console"/> <AppenderRef ref="errorLog"/> </Root> <!-- AsyncRoot - 異步記錄日誌 - 需要LMAX Disruptor的支持 --> <!-- <AsyncRoot level="info" additivity="false"> <AppenderRef ref="Console" /> <AppenderRef ref="infoLog" /> <AppenderRef ref="errorLog" /> </AsyncRoot> --> </Loggers> </Configuration>
修改HelloController方法。
@RestController public class HelloController { private static final Logger logger = LoggerFactory .getLogger(HelloController.class); @RequestMapping("/hello") public String index(@RequestParam String name) { logger.info("request two name is "+name); try{ Thread.sleep(1000000); }catch ( Exception e){ logger.error(" hello two error",e); } return "hello "+name+",this is two messge"; //return "hello "+name+",this is first messge"; } }
修改端口啓動EurekaClient,在瀏覽器刷新http://localhost:8890/spring-cloud-producer/hello?name=cuiyw&token=123。
在頁面輸出The service is unavailable時,可發現下面日誌,說明進行了3次請求。