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