Zuul的核心java
Filter是Zuul的核心,用來實現對外服務的控制。Filter的生命週期有4個,分別是「PRE」、「ROUTING」、「POST」、「ERROR」,整個生命週期能夠用下圖來表示。web
Zuul大部分功能都是經過過濾器來實現的,這些過濾器類型對應於請求的典型生命週期。正則表達式
Zuul中默認實現的Filterspring
類型 | 順序 | 過濾器 | 功能 |
---|---|---|---|
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 | 處理正常的請求響應 |
自定義Filtersql
實現自定義Filter,須要繼承ZuulFilter的類,並覆蓋其中的4個方法。apache
咱們假設有這樣一個場景,由於服務網關應對的是外部的全部請求,爲了不產生安全隱患,咱們須要對請求作必定的限制,好比請求中含有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次請求。