在前面幾節,我給你們介紹了當一個系統拆分紅微服務後,會產生的問題與解決方案:服務如何發現與管理(Nacos註冊中心實戰),服務與服務如何通訊(Ribbon, Feign實戰)html
今天咱們就來聊一聊另外一個問題:客戶端如何訪問?前端
在單體架構時,咱們的系統只有一個入口,前端人員調用起來十分的簡單。java
可是當咱們拆分爲一個微服務系統後,每一個服務都有屬於本身ip和端口號,咱們不可能跟前端說:誒,調用這個接口的時候你就使用這個地址哈。react
前端:git
既然這樣不行的話,那咱們能不能利用已有的知識想一個解決方案呢?程序員
其實咱們很容易的就能想到,咱們的服務是具有互相發現及通訊的能力的,那麼,咱們是否是能夠搞一個相似統一入口(網關)樣的服務,前端只請求這個服務,由這個服務去調用真實服務的Feign接口。web
舉個例子:spring
如今有個網關服務, 裏面有兩個接口:localhost:5555/get/goods, localhost:5555/orderbootstrap
前端調用獲取商品接口時,訪問:localhost:5555/get/goods,而後網關服務調用商品服務的Feign接口api
下單時:訪問:localhost:5555/order,而後網關服務調用訂單服務的Feign接口
小結一下:
這個方案是否解決了服務入口統一的問題:解決了
能用嗎:能用,但不是徹底能用
由於這樣會有一個問題,服務寫的每個接口,都須要給出一個Feign接口,給咱們的網關服務調用。
Spring Cloud爲咱們提供了一個解決方案:Spring Cloud Gateway
Spring Cloud Gateway提供了一個創建在Spring生態系統之上的API網關,可以簡單而有效的方式來路由到API,並基於 Filter 的方式提供一些功能,如:安全、監控。
Spring Cloud Gateway是由Spring Boot 2.x、Spring WebFlux和Reactor實現的,須要Spring Boot和Spring Webflux提供的Netty運行環境。它不能在傳統的Servlet容器中工做,也不能在以WAR形式構建時工做。
官方文檔:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
Route(路由):網關的基本構件,它由一個ID、一個目的地URI、一個斷言集合和一個過濾器集合定義。若是集合斷言爲真,則路由被匹配。
Predicate(斷言):Java 8斷言函數。參數類型是Spring Framework ServerWebExchange。可讓開發者在HTTP請求中的任何內容上進行匹配,好比頭文件或參數。
Filter(過濾):由特定的工廠構建的GatewayFilter的實例,與傳統的Filter同樣,可以請求先後對請求就行處理。
客戶端向Spring Cloud Gateway發出請求。若是Gateway處理程序映射肯定一個請求與路由相匹配,它將被髮送到Gateway Web處理程序。這個處理程序經過一個特定於該請求的過濾器鏈來運行該請求。
過濾器能夠在代理請求發送以前和以後運行pre和post邏輯。
預先準備一個服務,用來測試路由
我這裏準備了個一個商品服務,並提供了一個接口:http://localhost:8082/goods/get-goods
如今,開始編寫網關服務
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
bootstrap.yaml
server: port: 5555 spring: application: name: my-gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: public username: nacos password: nacos logging: level: org.springframework.cloud.gateway: info com.alibaba.nacos.client.naming: warn
application.yaml
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/goods/**
package com.my.micro.service.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author Zijian Liao * @since 1.0.0 */ @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
啓動服務,並訪問:http://localhost:5555/goods/get-goods
能夠看到,服務成功被路由了
一個簡單的網關服務就這樣完成了,小夥伴看完過有沒有對網關的概念更加深入呢?
在上面的例子中,咱們就用到了一個斷言工廠:Path
在Spring Cloud Gateway中,全部的斷言工廠都是繼承於AbstractRoutePredicateFactory
, 而且命名規則爲:XxxRoutePredicateFactory
, 好比Path的類名爲:PathRoutePredicateFactory
那麼,Spring Cloud Gateway給咱們內置了哪些斷言工廠呢?
如下展現我以爲經常使用的斷言工廠,更多的內容還請小夥伴本身查看文檔
匹配在某個時間(ZonedDateTime)後的請求
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/goods/** # 匹配23:05分後的請求 - After=2021-08-08T23:05:13.605+08:00[Asia/Shanghai]
咱們在23:03進行測試
訪問失敗了
匹配在某個時間(ZonedDateTime)前的請求
與After類似,再也不演示
匹配在某個時間段(ZonedDateTime)的請求
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/goods/** # 匹配23:05-23:10的請求 - Between=2021-08-08T23:05:13.605+08:00[Asia/Shanghai],2021-08-08T23:10:13.605+08:00[Asia/Shanghai]
匹配某個Host的請求
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/goods/** #配置host爲192.168.1.105請求 - Host=192.168.1.105
注意,測試時須要將端口號改成80
嘗試使用127.0.0.1發起調用
改成192.168.1.105進行調用
匹配指定的遠程源地址
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/goods/** #配置RemoteAddr爲192.168.1網段的地址 - RemoteAddr=192.168.1.1/24
測試
啓用內網穿透測試
訪問失敗了
關於過濾器這塊我舉個例子,更多的內容請小夥伴本身查閱文檔
官方文檔:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
舉一個用的比較多的過濾器:
顧名思義,除去前綴的過濾器,將匹配的請求的前綴去除,將去除後的請求轉發給下游服務
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/api/goods/** filters: # 1表示去除一個前綴 - StripPrefix=1
組合來看,意思是當客戶端發起請求:http://localhost:5555/api/goods/get-goods, 匹配該路由,而後將第一個前綴
api
去除,而後轉發給商品服務,轉發的路徑爲:/goods/get-goods
測試
上面提到過:全部的斷言工廠都是繼承於AbstractRoutePredicateFactory
, 而且命名規則爲:XxxRoutePredicateFactory
, 好比Path的類名爲:PathRoutePredicateFactory
咱們如今就來嘗試實現一個自定義的請求頭斷言工廠吧
編寫代碼
package com.my.micro.service.gateway.filter; import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.Predicate; /** * @author Zijian Liao * @since 1.0.0 */ @Component public class MyHeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<MyHeaderRoutePredicateFactory.Config> { /** * Header key. */ public static final String HEADER_KEY = "header"; /** * Regexp key. */ public static final String REGEXP_KEY = "regexp"; public MyHeaderRoutePredicateFactory() { super(MyHeaderRoutePredicateFactory.Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList(HEADER_KEY, REGEXP_KEY); } @Override public Predicate<ServerWebExchange> apply(MyHeaderRoutePredicateFactory.Config config) { return new GatewayPredicate() { @Override public boolean test(ServerWebExchange exchange) { // 獲取請求頭 List<String> values = exchange.getRequest().getHeaders() .getOrDefault(config.header, Collections.emptyList()); if (values.isEmpty()) { return false; } // 判斷請求頭中的值是否與配置匹配 return values.stream() .anyMatch(value -> value.matches(config.regexp)); } @Override public String toString() { return String.format("Header: %s=%s ", config.header, config.regexp); } }; } public static class Config { private String header; private String regexp; public String getHeader() { return header; } public void setHeader(String header) { this.header = header; } public String getRegexp() { return regexp; } public void setRegexp(String regexp) { this.regexp = regexp; } } }
編寫配置
spring: cloud: gateway: # 路由配置 routes: # 路由id, 保證惟一性 - id: my-goods # 路由的地址,格式:協議://服務名 lb: load balance,my-goods: 商品服務名 uri: lb://my-goods # 斷言 predicates: # 匹配goods開頭的請求 - Path=/api/goods/** # 匹配header爲name=aljian的請求 - MyHeader=name,ajian filters: # 1表示去除一個前綴 - StripPrefix=1
測試
直接在瀏覽器中訪問
改用postman訪問
自定義過濾器的方式與自定義斷言工廠的方式大體相同,因此過濾器繼承於AbstractGatewayFilterFactory
或者AbstractNameValueGatewayFilterFactory
, 命名規則爲XxxGatewayFilterFactory
好比內置的添加請求頭過濾器
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory { @Override public GatewayFilter apply(NameValueConfig config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 獲取到須要添加的header value String value = ServerWebExchangeUtils.expand(exchange, config.getValue()); // 將header添加到request中 ServerHttpRequest request = exchange.getRequest().mutate() .header(config.getName(), value).build(); // 從新構建出一個exchange return chain.filter(exchange.mutate().request(request).build()); } @Override public String toString() { return filterToStringCreator(AddRequestHeaderGatewayFilterFactory.this) .append(config.getName(), config.getValue()).toString(); } }; } }
以上內容都是針對於每個router,Spring Cloud Gateway提供了一個針對全部router的全局過濾器
實現方式以下
package com.my.micro.service.gateway.filter; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * @author Zijian Liao * @since 1.0.0 */ @Slf4j @Component public class MyGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); log.info("進入全局過濾器,請求路徑爲:{}", path); // 編寫任何你想要實現的邏輯,好比權限校驗 return chain.filter(exchange); } }
測試
小夥伴應該發現了,在遇到錯誤時,Spring Cloud Gateway返回給客戶端的異常並不優雅,因此咱們須要自定義異常處理
package com.my.micro.service.gateway.exception; import com.my.micro.service.gateway.result.BaseResult; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.lang.NonNull; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; /** * @author Zijian Liao */ @Slf4j public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler { /** * Create a new {@code DefaultErrorWebExceptionHandler} instance. * * @param errorAttributes the error attributes * @param resourceProperties the resources configuration properties * @param errorProperties the error configuration properties * @param applicationContext the current application context */ public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } @NonNull @Override protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { Throwable throwable = getError(request); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(this.handleError(throwable))); } private BaseResult<Void> handleError(Throwable throwable){ return BaseResult.failure(throwable.getMessage()); } }
BaseResult
package com.my.micro.service.gateway.result; import lombok.Data; /** * @author Zijian Liao * @since 1.0.0 */ @Data public class BaseResult<T> { private Integer code; private String message; public BaseResult(Integer code, String message){ this.code = code; this.message = message; } public static <T> BaseResult<T> failure(String message){ return new BaseResult<>(-1, message); } }
package com.my.micro.service.gateway.exception; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; import java.util.stream.Collectors; /** * @author Zijian Liao * @since 1.0.0 */ @Configuration public class ExceptionConfiguration { @Primary @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { DefaultErrorWebExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes, resourceProperties, serverProperties.getError(), applicationContext); exceptionHandler.setViewResolvers(viewResolversProvider.orderedStream().collect(Collectors.toList())); exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders()); return exceptionHandler; } }
本編介紹了關於微服務架構中——客戶端如何訪問的解決方案:Spring Cloud Gateway
其中介紹了Gateway的三個核心概念:Route,Predicate,Filter。並演示瞭如何配置及使用他們,還講解了如何自定義Predicate和Filter。
最後介紹了Spring Cloud Gateway的全局過濾器,以及如何實現自定義異常處理。
以上
看完以後想必有所收穫吧~ 想要了解更多精彩內容,歡迎關注公衆號:程序員阿鑑,阿鑑在公衆號歡迎你的到來~