Spring Cloud的Zuul是什么
通過前面內(nèi)容的學(xué)習(xí),我們已經(jīng)可以基本搭建出一套簡略版的微服務(wù)架構(gòu)了,我們有注冊中心 Eureka,可以將服務(wù)注冊到該注冊中心中,我們有 Ribbon 或Feign 可以實現(xiàn)對服務(wù)負(fù)載均衡地調(diào)用,我們有 Hystrix 可以實現(xiàn)服務(wù)的熔斷,但是我們還缺少什么呢?
我們首先來看一個微服務(wù)架構(gòu)圖:
在上面的架構(gòu)圖中,我們的服務(wù)包括:內(nèi)部服務(wù) Service A 和內(nèi)部服務(wù) ServiceB,這兩個服務(wù)都是集群部署,每個服務(wù)部署了 3 個實例,他們都會通過 EurekaServer 注冊中心注冊與訂閱服務(wù),而 Open Service 是一個對外的服務(wù),也是集群部署,外部調(diào)用方通過負(fù)載均衡設(shè)備調(diào)用 Open Service 服務(wù),比如負(fù)載均衡使用 Nginx,這樣的實現(xiàn)是否合理,或者是否有更好的實現(xiàn)方式呢?接下來我們主要圍繞該問題展開討論。
1、如果我們的微服務(wù)中有很多個獨立服務(wù)都要對外提供服務(wù),那么我們要如何去管理這些接口?特別是當(dāng)項目非常龐大的情況下要如何管理?
2、在微服務(wù)中,一個獨立的系統(tǒng)被拆分成了很多個獨立的服務(wù),為了確保安全,權(quán)限管理也是一個不可回避的問題,如果在每一個服務(wù)上都添加上相同的權(quán)限驗證代碼來確保系統(tǒng)不被非法訪問,那么工作量也就太大了,而且維護(hù)也非常不方便。
為了解決上述問題,微服務(wù)架構(gòu)中提出了 API 網(wǎng)關(guān)的概念,它就像一個安檢站一樣,所有外部的請求都需要經(jīng)過它的調(diào)度與過濾,然后 API 網(wǎng)關(guān)來實現(xiàn)請求路由、負(fù)載均衡、權(quán)限驗證等功能;
那么 Spring Cloud 這個一站式的微服務(wù)開發(fā)框架基于 Netflix Zuul 實現(xiàn)了Spring Cloud Zuul,采用 Spring Cloud Zuul 即可實現(xiàn)一套 API 網(wǎng)關(guān)服務(wù)。
1、創(chuàng)建一個普通的 Spring Boot 工程名為 06-springcloud-api-gateway,然后添加相關(guān)依賴,這里我們主要添加兩個依賴 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 網(wǎng)關(guān)服務(wù)功能:
@EnableZuulProxy //開啟Zuul的API網(wǎng)關(guān)服務(wù)功能
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3、在 application.properties 文件中配置路由規(guī)則:
#配置服務(wù)內(nèi)嵌的Tomcat端口
server.port=8080
#配置服務(wù)的名稱
spring.application.name=06-springcloud-api-gateway
#配置路由規(guī)則
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
#配置API網(wǎng)關(guān)到注冊中心上,API網(wǎng)關(guān)也將作為一個服務(wù)注冊到eureka-server上
eureka.client.service-url.defaultZone=http://eureka8761:8761/eureka/,http:/
/eureka8762:8762/eureka/
以上配置,我們的路由規(guī)則就是匹配所有符合/api-wkcto/**的請求,只要路徑中帶有/api-wkcto/都將被轉(zhuǎn)發(fā)到 05-springcloud-service-feign 服務(wù)上,至于05-springcloud-service-feign 服務(wù)的地址到底是什么則由 eureka-server 注冊中心去分析,我們只需要寫上服務(wù)名即可。
以我們目前搭建的項目為例,請求 http://localhost:8080/api-wkcto/web/hello 接口則相當(dāng)于請求 http://localhost:8082/web/hello(05-springcloud-service-feign 服務(wù)的地址為 http://localhost:8082/web/hello),路由規(guī)則中配置的 api-wkcto 是路由的名字,可以任意定義,但是一組 path 和serviceId 映射關(guān)系的路由名要相同。
如果以上測試成功,則表示們的 API 網(wǎng)關(guān)服務(wù)已經(jīng)構(gòu)建成功了,我們發(fā)送的符合路由規(guī)則的請求將自動被轉(zhuǎn)發(fā)到相應(yīng)的服務(wù)上去處理。
我們知道 Spring cloud Zuul 就像一個安檢站,所有請求都會經(jīng)過這個安檢站,所以我們可以在該安檢站內(nèi)實現(xiàn)對請求的過濾,下面我們以一個權(quán)限驗證案例說這一點:
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 方法的返回值為過濾器的類型,過濾器的類型決定了過濾器在哪個生命周期執(zhí)行,pre 表示在路由之前執(zhí)行過濾器,其他值還有 post、error、route 和 static,當(dāng)然也可以自定義。
(2)filterOrder 方法表示過濾器的執(zhí)行順序,當(dāng)過濾器很多時,我們可以通過該方法的返回值來指定過濾器的執(zhí)行順序。
(3)shouldFilter 方法用來判斷過濾器是否執(zhí)行,true 表示執(zhí)行,false 表示不執(zhí)行。
(4)run 方法則表示過濾的具體邏輯,如果請求地址中攜帶了 token 參數(shù)的話,則認(rèn)為是合法請求,否則為非法請求,如果是非法請求的話,首先設(shè)置ctx.setSendZuulResponse(false); 表示不對該請求進(jìn)行路由,然后設(shè)置響應(yīng)碼和響應(yīng)值。這個 run 方法的返回值目前暫時沒有任何意義,可以返回任意值。
2、通過 http://localhost:8080/api-wkcto/web/hello 地址訪問,就會被過濾器過濾。
1、 在前面的例子中:
#配置路由規(guī)則
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
當(dāng)訪問地址符合/api-wkcto/**規(guī)則的時候,會被自動定位到05-springcloud-service-feign 服務(wù)上,不過兩行代碼有點麻煩,還可以簡化為:
zuul.routes.05-springcloud-service-feign=/api-wkcto/**
zuul.routes 后面跟著的是服務(wù)名,服務(wù)名后面跟著的是路徑規(guī)則,這種配置方式更簡單。
2、 如果映射規(guī)則我們什么都不寫,系統(tǒng)也給我們提供了一套默認(rèn)的配置規(guī)則默認(rèn)的配置規(guī)則如下:
#默認(rèn)的規(guī)則
zuul.routes.05-springcloud-service-feign.path=/05-springcloud-service-feign/**
zuul.routes.05-springcloud-service-feign.serviceId=05-springcloud-service-feign
3、默認(rèn)情況下,Eureka 上所有注冊的服務(wù)都會被 Zuul 創(chuàng)建映射關(guān)系來進(jìn)行路由。
但是對于我這里的例子來說,我希望:05-springcloud-service-feign 提供服務(wù);而01-springcloud-service-provider 作為服務(wù)提供者只對服務(wù)消費者提供服務(wù),不對外提供服務(wù)。
如果使用默認(rèn)的路由規(guī)則,則 Zuul 也會自動為01-springcloud-service-provider 創(chuàng)建映射規(guī)則,這個時候我們可以采用如下方式來讓 Zuul 跳過 01-springcloud-service-provider 服務(wù),不為其創(chuàng)建路由規(guī)則:
#忽略掉服務(wù)提供者的默認(rèn)規(guī)則
zuul.ignored-services=01-springcloud-service-provider
不給某個服務(wù)設(shè)置映射規(guī)則,這個配置我們可以進(jìn)一步細(xì)化,比如說我不想給/hello 接口路由,那我們可以按如下方式配置:
#忽略掉某一些接口路徑
zuul.ignored-patterns=/**/hello/**
此外,我們也可以統(tǒng)一的為路由規(guī)則增加前綴,設(shè)置方式如下:
#配置網(wǎng)關(guān)路由的前綴
zuul.prefix=/myapi
此時我們的訪問路徑就變成了 http://localhost:8080/myapi/web/hello
4、 路由規(guī)則通配符的含義:
通配符 | 含義 | 舉例 | 說明 |
---|---|---|---|
? |
匹配任意單個字符 |
/05-springcloud-service-feign/? |
匹配 /05-springcloud-service-feign/a, /05-springcloud-service-feign/b, /05-springcloud-service-feign/c 等 |
* |
匹配任意數(shù)量的字符 |
/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 |
** |
匹配任意數(shù)量的字符 |
/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 網(wǎng)關(guān)只是作為各個微服務(wù)的統(tǒng)一入口,但是有時候我們可能也需要在 API 網(wǎng)關(guān)服務(wù)上做一些特殊的業(yè)務(wù)邏輯處理,那么我們可以讓請求到達(dá) API 網(wǎng)關(guān)后,再轉(zhuǎn)發(fā)給自己本身,由 API 網(wǎng)關(guān)自己來處理,那么我們可以進(jìn)行如下的操作:
在 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 處于迅速發(fā)展中,各個版本之間有所差異,本案例是以 Finchley.RELEASE 版本為例,來說明 Spring Cloud Zuul 中的異常處理問題。
首先我們來看一張官方給出的 Zuul 請求的生命周期圖:
1、正常情況下所有的請求都是按照 pre、route、post 的順序來執(zhí)行,然后由 post返回 response。
2、在 pre 階段,如果有自定義的過濾器則執(zhí)行自定義的過濾器。
3、pre、routing、post 的任意一個階段如果拋異常了,則執(zhí)行 error 過濾器。
我們可以有兩種方式統(tǒng)一處理異常:
(1)禁用 zuul 默認(rèn)的異常處理 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("進(jìn)入系統(tǒng)異常攔截", 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頁
蛙課網(wǎng)【動力節(jié)點旗下品牌】
http://www.wkcto.com
}
return null;
}
}
(2)自定義全局 error 錯誤頁面
@RestController
public class ErrorHandlerController implements ErrorController {
/**
* 出異常后進(jìn)入該方法,交由下面的方法處理
*/
@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();
}
}