在前面幾節,我給大家介紹了當一個系統拆分成微服務後,會產生的問題與解決方案:服務如何發現與管理(Nacos註冊中心實戰),服務與服務如何通訊(Ribbon, Feign實戰)
今天我們就來聊一聊另一個問題:客戶端如何訪問?
在單體架構時,我們的系統只有一個入口,前端人員呼叫起來十分的簡單。
但是當我們拆分為一個微服務系統後,每個服務都有屬於自己ip和埠號,我們不可能跟前端說:誒,呼叫這個介面的時候你就使用這個地址哈。
前端:
既然這樣不行的話,那我們能不能利用已有的知識想一個解決方案呢?
不是真的能用的解決方案
其實我們很容易的就能想到,我們的服務是具備互相發現及通訊的能力的,那麼,我們是不是可以搞一個類似統一入口(閘道器)樣的服務,前端只請求這個服務,由這個服務去呼叫真實服務的Feign介面。
舉個例子:
- 商品服務的獲取商品介面:localhost:8080/get/goods
- 訂單服務的下訂單介面:localhost:8081/order
現在有個閘道器服務, 裡面有兩個介面:localhost:5555/get/goods, localhost:5555/order
前端呼叫獲取商品介面時,訪問:localhost:5555/get/goods,然後閘道器服務呼叫商品服務的Feign介面
下單時:訪問: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給我們內建了哪些斷言工廠呢?
以下展示我覺得常用的斷言工廠,更多的內容還請小夥伴自己檢視文件
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分後的請求
- After=2021-08-08T23:05:13.605+08:00[Asia/Shanghai]
我們在23:03進行測試
訪問失敗了
Before
匹配在某個時間(ZonedDateTime)前的請求
與After相似,不再演示
Between
匹配在某個時間段(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
匹配某個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進行呼叫
RemoteAddr
匹配指定的遠端源地址
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
舉一個用的比較多的過濾器:
StripPrefix
顧名思義,除去字首的過濾器,將匹配的請求的字首去除,將去除後的請求轉發給下游服務
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的全域性過濾器,以及如何實現自定義異常處理。
以上
看完之後想必有所收穫吧~ 想要了解更多精彩內容,歡迎關注公眾號:程式設計師阿鑑,阿鑑在公眾號歡迎你的到來~
個人部落格空間:https://zijiancode.cn/archives/gateway
gittee: https://gitee.com/lzj960515/my-micro-service-demo