Spring Cloud的Zuul是什么
通過前面內容的學習,我們已經可以基本搭建出一套簡略版的微服務架構了,我們有注冊中心 Eureka,可以將服務注冊到該注冊中心中,我們有 Ribbon 或Feign 可以實現對服務負載均衡地調用,我們有 Hystrix 可以實現服務的熔斷,但是我們還缺少什么呢?
我們首先來看一個微服務架構圖:
在上面的架構圖中,我們的服務包括:內部服務 Service A 和內部服務 ServiceB,這兩個服務都是集群部署,每個服務部署了 3 個實例,他們都會通過 EurekaServer 注冊中心注冊與訂閱服務,而 Open Service 是一個對外的服務,也是集群部署,外部調用方通過負載均衡設備調用 Open Service 服務,比如負載均衡使用 Nginx,這樣的實現是否合理,或者是否有更好的實現方式呢?接下來我們主要圍繞該問題展開討論。
1、如果我們的微服務中有很多個獨立服務都要對外提供服務,那么我們要如何去管理這些接口?特別是當項目非常龐大的情況下要如何管理?
2、在微服務中,一個獨立的系統被拆分成了很多個獨立的服務,為了確保安全,權限管理也是一個不可回避的問題,如果在每一個服務上都添加上相同的權限驗證代碼來確保系統不被非法訪問,那么工作量也就太大了,而且維護也非常不方便。
為了解決上述問題,微服務架構中提出了 API 網關的概念,它就像一個安檢站一樣,所有外部的請求都需要經過它的調度與過濾,然后 API 網關來實現請求路由、負載均衡、權限驗證等功能;
那么 Spring Cloud 這個一站式的微服務開發框架基于 Netflix Zuul 實現了Spring Cloud Zuul,采用 Spring Cloud Zuul 即可實現一套 API 網關服務。
1、創建一個普通的 Spring Boot 工程名為 06-springcloud-api-gateway,然后添加相關依賴,這里我們主要添加兩個依賴 zuul 和 eureka 依賴:
<!--添加spring cloud的zuul的起步依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--添加spring cloud的eureka的客戶端依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、在入口類上添加@EnableZuulProxy 注解,開啟 Zuul 的 API 網關服務功能:
@EnableZuulProxy //開啟Zuul的API網關服務功能
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3、在 application.properties 文件中配置路由規則:
#配置服務內嵌的Tomcat端口
server.port=8080
#配置服務的名稱
spring.application.name=06-springcloud-api-gateway
#配置路由規則
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
#配置API網關到注冊中心上,API網關也將作為一個服務注冊到eureka-server上
eureka.client.service-url.defaultZone=http://eureka8761:8761/eureka/,http:/
/eureka8762:8762/eureka/
以上配置,我們的路由規則就是匹配所有符合/api-wkcto/**的請求,只要路徑中帶有/api-wkcto/都將被轉發到 05-springcloud-service-feign 服務上,至于05-springcloud-service-feign 服務的地址到底是什么則由 eureka-server 注冊中心去分析,我們只需要寫上服務名即可。
以我們目前搭建的項目為例,請求 http://localhost:8080/api-wkcto/web/hello 接口則相當于請求 http://localhost:8082/web/hello(05-springcloud-service-feign 服務的地址為 http://localhost:8082/web/hello),路由規則中配置的 api-wkcto 是路由的名字,可以任意定義,但是一組 path 和serviceId 映射關系的路由名要相同。
如果以上測試成功,則表示們的 API 網關服務已經構建成功了,我們發送的符合路由規則的請求將自動被轉發到相應的服務上去處理。
我們知道 Spring cloud Zuul 就像一個安檢站,所有請求都會經過這個安檢站,所以我們可以在該安檢站內實現對請求的過濾,下面我們以一個權限驗證案例說這一點:
1、我們定義一個過濾器類并繼承自 ZuulFilter,并將該 Filter 作為一個 Bean:
@Component
public class AuthFilter 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 {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8");
ctx.setResponseBody("非法訪問");
}
return null;
}
}
(1)filterType 方法的返回值為過濾器的類型,過濾器的類型決定了過濾器在哪個生命周期執行,pre 表示在路由之前執行過濾器,其他值還有 post、error、route 和 static,當然也可以自定義。
(2)filterOrder 方法表示過濾器的執行順序,當過濾器很多時,我們可以通過該方法的返回值來指定過濾器的執行順序。
(3)shouldFilter 方法用來判斷過濾器是否執行,true 表示執行,false 表示不執行。
(4)run 方法則表示過濾的具體邏輯,如果請求地址中攜帶了 token 參數的話,則認為是合法請求,否則為非法請求,如果是非法請求的話,首先設置ctx.setSendZuulResponse(false); 表示不對該請求進行路由,然后設置響應碼和響應值。這個 run 方法的返回值目前暫時沒有任何意義,可以返回任意值。
2、通過 http://localhost:8080/api-wkcto/web/hello 地址訪問,就會被過濾器過濾。
1、 在前面的例子中:
#配置路由規則
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
當訪問地址符合/api-wkcto/**規則的時候,會被自動定位到05-springcloud-service-feign 服務上,不過兩行代碼有點麻煩,還可以簡化為:
zuul.routes.05-springcloud-service-feign=/api-wkcto/**
zuul.routes 后面跟著的是服務名,服務名后面跟著的是路徑規則,這種配置方式更簡單。
2、 如果映射規則我們什么都不寫,系統也給我們提供了一套默認的配置規則默認的配置規則如下:
#默認的規則
zuul.routes.05-springcloud-service-feign.path=/05-springcloud-service-feign/**
zuul.routes.05-springcloud-service-feign.serviceId=05-springcloud-service-feign
3、默認情況下,Eureka 上所有注冊的服務都會被 Zuul 創建映射關系來進行路由。
但是對于我這里的例子來說,我希望:05-springcloud-service-feign 提供服務;而01-springcloud-service-provider 作為服務提供者只對服務消費者提供服務,不對外提供服務。
如果使用默認的路由規則,則 Zuul 也會自動為01-springcloud-service-provider 創建映射規則,這個時候我們可以采用如下方式來讓 Zuul 跳過 01-springcloud-service-provider 服務,不為其創建路由規則:
#忽略掉服務提供者的默認規則
zuul.ignored-services=01-springcloud-service-provider
不給某個服務設置映射規則,這個配置我們可以進一步細化,比如說我不想給/hello 接口路由,那我們可以按如下方式配置:
#忽略掉某一些接口路徑
zuul.ignored-patterns=/**/hello/**
此外,我們也可以統一的為路由規則增加前綴,設置方式如下:
#配置網關路由的前綴
zuul.prefix=/myapi
此時我們的訪問路徑就變成了 http://localhost:8080/myapi/web/hello
4、 路由規則通配符的含義:
通配符 | 含義 | 舉例 | 說明 |
---|---|---|---|
? |
匹配任意單個字符 |
/05-springcloud-service-feign/? |
匹配 /05-springcloud-service-feign/a, /05-springcloud-service-feign/b, /05-springcloud-service-feign/c 等 |
* |
匹配任意數量的字符 |
/05-springcloud-service-feign/* |
匹配 /05-springcloud-service-feign/aaa, /05-springcloud-service-feign/bbb, /05-springcloud-service-feign/ccc 等, 無法匹配 /05-springcloud-service-feign/a/b/c |
** |
匹配任意數量的字符 |
/05-springcloud-service-feign/** |
匹配 /05-springcloud-service-feign/aaa, /05-springcloud-service-feign/bbb, /05-springcloud-service-feign/ccc 等, 也可以匹配 /05-springcloud-service-feign/a/b/c |
5、一般情況下 API 網關只是作為各個微服務的統一入口,但是有時候我們可能也需要在 API 網關服務上做一些特殊的業務邏輯處理,那么我們可以讓請求到達 API 網關后,再轉發給自己本身,由 API 網關自己來處理,那么我們可以進行如下的操作:
在 06-springcloud-api-gateway 項目中新建如下 Controller:
@RestController
public class GateWayController {
@RequestMapping("/api/local")
public String hello() {
return "exec the api gateway.";
}
}
然后在 application.properties 文件中配置:
zuul.routes.gateway.path=/gateway/**
zuul.routes.gateway.url=forward:/api/local
Zuul的異常處理
Spring Cloud Zuul 對異常的處理是非常方便的,但是由于 Spring Cloud 處于迅速發展中,各個版本之間有所差異,本案例是以 Finchley.RELEASE 版本為例,來說明 Spring Cloud Zuul 中的異常處理問題。
首先我們來看一張官方給出的 Zuul 請求的生命周期圖:
1、正常情況下所有的請求都是按照 pre、route、post 的順序來執行,然后由 post返回 response。
2、在 pre 階段,如果有自定義的過濾器則執行自定義的過濾器。
3、pre、routing、post 的任意一個階段如果拋異常了,則執行 error 過濾器。
我們可以有兩種方式統一處理異常:
(1)禁用 zuul 默認的異常處理 SendErrorFilter 過濾器,然后自定義我們自己的Errorfilter 過濾器
zuul.SendErrorFilter.error.disable=true
@Component
public class ErrorFilter extends ZuulFilter {
private static final Logger logger =
LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException)context.getThrowable();
logger.error("進入系統異常攔截", exception);
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
response.setStatus(exception.nStatusCode);
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print("{code:"+ exception.nStatusCode +",message:\""+
exception.getMessage() +"\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer!=null){
writer.close();
}
}
} catch (Exception var5) {
ReflectionUtils.rethrowRuntimeException(var5);
第 44頁共 52頁
蛙課網【動力節點旗下品牌】
http://www.wkcto.com
}
return null;
}
}
(2)自定義全局 error 錯誤頁面
@RestController
public class ErrorHandlerController implements ErrorController {
/**
* 出異常后進入該方法,交由下面的方法處理
*/
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping("/error")
public Object error(){
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException)ctx.getThrowable();
return exception.nStatusCode + "--" + exception.getMessage();
}
}