什么是服務網(wǎng)關
前文我們已經(jīng)了解了構建微服務的基礎springboot,同時也能使用springboot構建服務。接下來我們就基于springboot聊一下springcloud。這個springcloud并不是一個特定的技術,它指的是微服務中一個生態(tài)體系。比如包括網(wǎng)關,注冊中心,配置中心等。今天我們就先了解一下微服務網(wǎng)關,微服務網(wǎng)關有很多種我們這次采用現(xiàn)在主流的spring cloud gateway來講解說明。 在微服務體系中,每個服務都是一個獨立的模塊都是一個獨立運行的組件,一個完整的微服務體系是由若干個獨立的服務組成,每個服務完成自己業(yè)務模塊功能。比如用戶服務提供用戶信息相關的服務和功能,支付模塊提供支付相關的功能。各個服務之間通過REST API或者RPC(以后講)進行通信,并且一般我們微服務要做到無狀態(tài)的通信。 我們實現(xiàn)微服務之后在一些方面也會帶來不方便的地方,如果網(wǎng)頁端或者app端需要請求修改送貨地址,還有購物之后要付款在這個場景下:


如上圖會出現(xiàn)一些問題:
- 客戶端要發(fā)起多次請求,請求不同域名對應的服務,增加了通信成本以及對客戶端代碼的維護增加了復雜性。
- 服務驗證會在每個服務里面單獨做,如果每個服務驗證鑒權邏輯不同就會導致客戶端反復驗證。
- 另外如果各個服務采用的協(xié)議不同那么對于客戶端來講那就是災難性的。
基于上面所以我們就需要一個中間層,讓客戶端去請求中間層,至于需要請求那個服務由中間件去請求,最后將結果匯總返回給客戶端,這個中間層就是網(wǎng)關。


為什么要使用網(wǎng)關
使用網(wǎng)關有幾個作用:
統(tǒng)一鑒權
一般我們在網(wǎng)關上進行鑒權有兩種:1,是對于請求的客戶端身份的認證。2,訪問權限控制就是當確認用戶身份之后判斷是否有某個資源的訪問權限。 曾經(jīng)我們在單體應用中,客戶端請求驗證身份和對于資源權限的約束比較簡單,通過請求的session就可以獲取對應的用戶以及權限信息,但是在微服務架構下,所有的服務都被拆成單個微服務而且還是集群部署這種情況就會變得復雜,因為如果還是使用session的話在分布式情況每次請求不一定會落在同一臺機器上,這樣就會導致session無效。就需要我們進行額外的工作保證集群中的session是一致的。所以我們在網(wǎng)關層進行統(tǒng)一的處理認證:


日志記錄
當客戶端請求進來之后我們需要記錄當前請求的時間依賴來源地址,ip等信息,這樣我們就可以統(tǒng)一的在網(wǎng)關層面上進行攔截獲取,之后輸出到日志文件中通過ELK組件進行輸出,記錄內(nèi)容可以多維度多信息統(tǒng)一記錄而不需要到具體每個服務中進行分別記錄。
請求分發(fā)和過濾
對于網(wǎng)關來講這個請求匹配分發(fā)是最重要的功能,我們常見的nginx其實他這個組件就有請求轉發(fā)和過濾的功能,對于網(wǎng)關來講可以對請求進行前置和后置的過濾。
- 請求分發(fā):接收客戶端的請求,將請求對應到后面的各個微服務上并請求微服務,因為微服務粒度比較細,所以這個網(wǎng)關就可以對各個微服務進行功能性的整合最終給回客戶端。
- 過濾:網(wǎng)關會攔截所有請求,相當于spring中AOP一個橫向的切面,在這個切面上進行鑒權,限流,認證等操作。
灰度發(fā)布
一般公司的互聯(lián)網(wǎng)產(chǎn)品都是迭代非??斓模径际切〔娇炫?。基本是一個周發(fā)布一個版本迭代。在這種情況下就會出現(xiàn)風險,比如兼容性,功能完整度,時間比較短會存在bug最終產(chǎn)生事故等問題。這樣一般我們發(fā)布的時候會將新的功能發(fā)布到指定的機器上分過去一小部分流量來觀察具體情況。所以網(wǎng)關作為請求的入口就正好可以完成這個功能。


常用網(wǎng)關解決方案
一般我們常用的網(wǎng)關有幾種比如:OpenResty、Zuul、Gateway、Kong、Tyk等。我們主要是用spring體系的框架,所以我們本文針對Gateway進行講解,其它幾種網(wǎng)關實現(xiàn)不做重點說明,OpenResty是有nginx+lua集成的web服務器,集成了許多三方庫和模塊。Zuul其實springcloud前期也是在集成使用,到那時由于他的線程模型策略可能導致的性能問題最終spring選擇了自己研發(fā)的spring cloud gateway。
環(huán)境準備
本文我們使用一個簡單的案例來演示一下spring cloud gateway的使用方法,首先我們需要住呢比2個spring boot的應用,具體創(chuàng)建方式請參考我們本專題第二篇文章。
- spring-cloud-gateway-service1 這個是一個微服務
- Spring-cloud-gateway-wangguan 網(wǎng)關微服務
我們根據(jù)以前專題創(chuàng)建了2個服務第一個服務我們添加一個controller
@RequestMapping(value = {"/getUser"}, method = RequestMethod.GET)
public String getUser() {
Map<String ,String> user = new HashMap<>();
user.put("name", "張三");
user.put("age", "45");
String s = JSONObject.toJSONString(user);
return s;
}
復制代碼

第二個網(wǎng)關服務我們增加pom依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
spring-cloud-starter-gateway
<version>2.0.4.RELEASE</version>
</dependency>
復制代碼

在application.yml中添加gateway路由
spring:
cloud:
gateway:
routes:
- predicates:
- Path =/gateway/** #匹配規(guī)則
uri: http://localhost:8099/getUser #服務1的訪問地址
filters:
- StripRrefix: 1 #去掉前綴
server:
port: 8077
復制代碼

針對上面配置含義說明:
- uri:目標服務地址,可配置uri和lb://應用服務名稱
- predicates:匹配條件,根據(jù)規(guī)則匹配是否請求該路由
- filters: 過濾規(guī)則,這個過濾包含前置過濾和后置過濾,
- StripPrefix=1,表示去掉前綴,即在轉發(fā)目標url的時候去掉’gateway'
這個時候我們啟動服務之后發(fā)現(xiàn)服務啟動日志:Netty started on port(s): 8077.說明我們服務成功了,并且網(wǎng)關依賴的是nettyserver啟動幾個服務監(jiān)聽。 我們訪問:
在配置正確的情況下將會返回服務返回的結果。
spring cloud gateway原理


上圖是gateway官方給出的原理圖,可能不太好理解,我們自己畫個圖輔助理解一下: 如上圖有幾個概念先說明一下:
- 路由(Route):是網(wǎng)關的組件之一,由id ,uri ,predicate ,filter組成。
- 斷言(Predicate):匹配http請求中的內(nèi)容。如果返回結果是true則就按當前的router進行轉發(fā)。
- 過濾器(Filter):為請求提供前置和后置的過濾。
當客戶端發(fā)送請求到網(wǎng)關時,網(wǎng)關會根據(jù)一系列的Predicate的匹配結果來決定訪問哪個route路由,然后根據(jù)過濾器進行請求處理,過濾器可以在請求發(fā)送到后端服務之前和之后執(zhí)行。
路由規(guī)則
spring cloud gateway中提供了路由匹配機制,比如我們前文配置的Path=/gateway/** . 意思就是通過Path的屬性來匹配URL前綴是/gateway/的請求。其實spring cloud gateway給我們提供了很多規(guī)則供我們使用。 每一個Predicate的使用,你可以理解為:當滿足這種條件后才會被轉發(fā),如果是多個,那就是都滿足的情況下被轉發(fā)。這些Predict的源碼在org.springframework.cloud.gateway.handler.predicate包中我們簡單看一下:


動態(tài)路由
gateway配置路由主要有兩種方式,1.用yml配置文件,2.寫在代碼里。而無論是 yml,還是代碼配置,啟動網(wǎng)關后將無法修改路由配置,如有新服務要上線,則需要先把網(wǎng)關下線,修改 yml 配置后,再重啟網(wǎng)關。這種方式如果在網(wǎng)關上沒有優(yōu)雅停機就會出現(xiàn)服務間斷,這無疑是不能被接受的。gateway網(wǎng)關啟動時,路由信息默認會加載內(nèi)存中,路由信息被封裝到 RouteDefinition 對象中,配置多個RouteDefinition組成gateway的路由系統(tǒng),RouteDefinition中的屬性與上面代碼配置的屬性一一對應:


那么就需要我們的動態(tài)路由來解決這個問題了。Spring Cloud Gateway 提供了 Endpoint 端點,暴露路由信息,有獲取所有路由、刷新路由、查看單個路由、刪除路由等方法,具體實現(xiàn)類org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint ,想訪問端點中的方法需要添加 spring-boot-starter-actuator 注解,并在配置文件中暴露所有端點。編寫動態(tài)路由實現(xiàn)類,需實現(xiàn)ApplicationEventPublisherAware接口。
/**
* 動態(tài)路由服務
*/
@Service
public class GoRouteServiceImpl implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
//增加路由
public String add(RouteDefinition definition) {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
//更新路由
public String update(RouteDefinition definition) {
try {
delete(definition.getId());
} catch (Exception e) {
return "update fail,not find route routeId: "+definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception e) {
return "update route fail";
}
}
//刪除路由
public Mono<ResponseEntity<Object>> delete(String id) {
return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {
return Mono.just(ResponseEntity.ok().build());
})).onErrorResume((t) -> {
return t instanceof NotFoundException;
}, (t) -> {
return Mono.just(ResponseEntity.notFound().build());
});
}
}
復制代碼

編寫 Rest接口,通過這些接口實現(xiàn)動態(tài)路由功能.
@RestController
@RequestMapping("/changeRoute")
public class ChangeRouteController {
@Autowired
private GoRouteServiceImpl goRouteServiceImpl;
//增加路由
@PostMapping("/add")
public String add(@RequestBody GatewayRouteDefinition gwdefinition) {
String flag = "fail";
try {
RouteDefinition definition = assembleRouteDefinition(gwdefinition);
flag = this.goRouteService.add(definition);
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
//刪除路由
@DeleteMapping("/routes/{id}")
public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
try {
return this.goRouteService.delete(id);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
//更新路由
@PostMapping("/update")
public String update(@RequestBody GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = assembleRouteDefinition(gwdefinition);
return this.goRouteService.update(definition);
}
//把傳遞進來的參數(shù)轉換成路由對象
private RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = new RouteDefinition();
definition.setId(gwdefinition.getId());
definition.setOrder(gwdefinition.getOrder());
//設置斷言
List<PredicateDefinition> pdList=new ArrayList<>();
List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();
for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setArgs(gpDefinition.getArgs());
predicate.setName(gpDefinition.getName());
pdList.add(predicate);
}
definition.setPredicates(pdList);
//設置過濾器
List<FilterDefinition> filters = new ArrayList();
List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();
for(GatewayFilterDefinition filterDefinition : gatewayFilters){
FilterDefinition filter = new FilterDefinition();
filter.setName(filterDefinition.getName());
filter.setArgs(filterDefinition.getArgs());
filters.add(filter);
}
definition.setFilters(filters);
URI uri = null;
if(gwdefinition.getUri().startsWith("http")){
uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
}else{
// uri為 lb://consumer-service 時使用下面的方法
uri = URI.create(gwdefinition.getUri());
}
definition.setUri(uri);
return definition;
}
}
復制代碼

其實一般我們很少通過API去調用rest服務去增刪路由信息,一般我們主流都是通過集成nacos的config功能動態(tài)增添路由。與nacos整合我們后面在講。
過濾器
網(wǎng)關過濾器Filter分為Pre和Post即前置過濾和后置過濾器。分別為在具體請求轉發(fā)到后端微服務之前執(zhí)行和將結果返回給客戶端之前執(zhí)行。 內(nèi)置的GatewayFilter比較多大概有19種,如:
- AddRequestHeader GatewayFilter Factory ,
- AddRequestParameter GatewayFilter Factory ,
- AddResponseHeader GatewayFilter Factory
就不過多舉例了,使用起來也比較簡單,我們著重看一下如何自定義過濾器:
- 全局過濾器:全局過濾器,對所有的路由都有效,所有不用在配置文件中配置,主要實現(xiàn)了GlobalFilter 和 Ordered接口,并將過濾器注冊到spring 容器。
@Service
@Slf4j
public class AllDefineFilter implements GlobalFilter,Ordered{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[pre]-Enter AllDefineFilter");
return chain.filter(exchange).then(Mono.fromRunnable(()->{
log.info("[post]-Return Result");
}));
}
@Override
public int getOrder() {
return 0;
}
}
復制代碼

- 局部過濾器:需要在配置文件中配置,如果配置,則該過濾器才會生效。主要實現(xiàn)GatewayFilter, Ordered接口,并通過AbstractGatewayFilterFactory的子類注冊到spring容器中,當然也可以直接繼承AbstractGatewayFilterFactory,在里面寫過濾器邏輯,還可以從配置文件中讀取外部數(shù)據(jù)。
@Component
@Slf4j
public class UserDefineGatewayFilter extends AbstractGatewayFilterFactory<UserDefineGatewayFilter.GpConfig>{
public UserDefineGatewayFilter(){
super(GpConfig.class);
}
@Override
public GatewayFilter apply(GpConfig config) {
return ((exchange, chain) -> {
log.info("[Pre] Filter Request,name:"+config.getName());
return chain.filter(exchange).then(Mono.fromRunnable(()->{
log.info("[Post] Response Filter");
}));
});
}
public static class UserConfig{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
復制代碼

這塊需要有注意的地方:
- 類名必須要統(tǒng)一以GatewayFiterFactory結尾,因為默認情況下過濾器的name會采用該自定義類的前綴。這里的name=UserDefine,也就是在yml中filters中的name值。
- 在apply方法中,同時包含Pre和Post過濾。在then方法中是請求執(zhí)行結束之后的后置處理。
- UserConfig是一個配置類,該類中只有一個屬性name。這個屬性可以在ym文件中使用。
- 該類需要裝載到Spring IoC容器,此處使用@Component注解實現(xiàn)。
其實整個spring cloud gateway 與spring cloud alibaba整合的很好,可以與nacos整合可以與sentinel整合進行限流,這個后期我們單獨進行講解。
作者:我是大明哥
鏈接:https://juejin.cn/post/6923100060913926157
來源:稀土掘金
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
微信公眾號【程序員黃小斜】作者是前螞蟻金服Java工程師,專注分享Java技術干貨和求職成長心得,不限于BAT面試,算法、計算機基礎、數(shù)據(jù)庫、分布式、spring全家桶、微服務、高并發(fā)、JVM、Docker容器,ELK、大數(shù)據(jù)等。關注后回復【book】領取精選20本Java面試必備精品電子書。