在后面几节,我给大家介绍了当一个零碎拆分成微服务后,会产生的问题与解决方案:服务如何发现与治理(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...
概念
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/g...
当初,开始编写网关服务
引入依赖
<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: 5555spring: application: name: my-gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: public username: nacos password: nacoslogging: 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 */@SpringBootApplicationpublic class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); }}
测试
启动服务,并拜访:http://localhost:5555/goods/g...
能够看到,服务胜利被路由了
一个简略的网关服务就这样实现了,小伙伴看完过有没有对网关的概念更加粗浅呢?
断言
在下面的例子中,咱们就用到了一个断言工厂:Path
在Spring Cloud Gateway中,所有的断言工厂都是继承于AbstractRoutePredicateFactory
, 并且命名规定为:XxxRoutePredicateFactory
, 比方Path的类名为:PathRoutePredicateFactory
那么,Spring Cloud Gateway给咱们内置了哪些断言工厂呢?
文档:https://docs.spring.io/spring...
以下展现我感觉罕用的断言工厂,更多的内容还请小伙伴本人查看文档
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...
举一个用的比拟多的过滤器:
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/goo..., 匹配该路由,而后将第一个前缀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 */@Componentpublic 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@Componentpublic 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 */@Slf4jpublic 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 */@Datapublic 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 */@Configurationpublic 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/archive...
gittee: https://gitee.com/lzj960515/m...