切面编程(5):api网关-spring cloud gateway

1 简述

spring cloud 早期api网关组件是zuul,底层基于tomcat,采用传统编程方式,曾广泛应用,性能也不错,目前最新版本是2021年4月,基本不再迭代,新项目就别用了。

spring cloud gateway 采用响应式编程风格,是spring cloud当前主推api网关解决方案,它的框架、流程、类库的设计,都继承了spring一贯的直观、简单的风格。概念理解起来,还是很清晰的。它采用新的异步非阻塞的响应式编程风格,之所以颠覆原来已有成熟方案,个人理解,api网关的上游、下游都采用异步非阻塞方式处理,可以大大降低对线程依赖,提高CPU利用率,比较适合IO处理密集,功能相对单一的场合。

了解响应式编程过程中,刚开始可能会不适应,甚至对它产生怀疑,这个可参考一些它的经典应用场景,响应式编程从推出、到推广,已有很长一段时间,不管是spring,还是redis,mongodb都在积极跟进,足见该框架的优秀。

抛开具体技术,对于api网关框架,路由是它最基本、最核心的能力,也因它的路由网关性质,使他具备统一拦截处理的能力,比较适合处理一些全局、统一性的问题。

api网关除路由外,最常具备的功能,就是认证、鉴权,以及流量保护、重试策略、防刷token、统计日志等,另外像人机识别、灰度发布等机制也比较适合。

另外,既然作为API网关角色,就需要对API制定规则,不管是外部请求的header,url,还是后端服务的响应码、返回值结构,需要有一个统一的外观,可依据不同来源pc、app、h5灵活定义。

链接:Spring Cloud Gateway文档

2 概念

2.1 术语

以下是spring cloud gateway框架,抽象的核心概念,主要包含3种:

Route(路由): 网关的基本构件。它由一个ID、一个目的地URI、一个谓词(Predicate)集合和一个过滤器(Filter)集合定义。

Predicate(谓词、断言): http请求的匹配规则,以此来匹配唯一的路由节点,已内置丰富配置规则,比如请求url、header、请求时间等。

Filter(过滤器): 它是 GatewayFilter 实例,已由特定工厂构建。在这里,你可以在发送下游请求之前或之后,对请求和响应进行修改。

提示:三者不是并列概念,存在从属关系,Predicate、Filter从属于Route。

2.2 流程

以下是官网Spring Cloud Gateway 工作概述图

从图可看出,跟java中servelt容器处理流程很相似,保留了传统的命名规范,不同的是servelt由进程内Controller处理,而Gateway服务则由外部远程服务处理。

Filter过滤器可以在代理请求发送之前和之后运行,在所有的 "pre" (前)过滤器逻辑都被执行后,才会向后端服务提交请求,然后"post" (后)逻辑被执行。

Filter跟早期的zuul网关框架有所不同,zuul中Filter只能在pre(前)、post(后)阶段二选一。

3 准备

3.1 注册中心

注册中心选择nacos,下载、它的安装部署,这里就不介绍,nacos官网文档描述的很详细,配置如下:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.0.106:8848

3.2 maven依赖

	<dependencies>
		<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>
		    <version>2.2.5.RELEASE</version>
		</dependency>		
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>2.3.2.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>		
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Hoxton.SR9</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

3.3 编程风格

Spring Cloud Gateway 建立在 Spring Boot 2.x、 Spring WebFlux 和 Project Reactor之上。
默认基于 Netty,采用响应式编程风格。

其中,Reactor 框架是 Pivotal 公司(开发 Spring 等技术的公司)开发,它符合Reactive Streams 规范(Reactive Streams 是由 Netflix、TypeSafe、Pivotal 等公司发起)。

4 配置

4.1 一般配置

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: hello1
#          uri: http://localhost:8081
          uri: lb://mtr-cloud-client1
          predicates:
            - Path=/aaa/demo/**,/ccc/demo/**
            - After=2022-05-26T15:15:00.789+08:00
            - Header=X-Request-Id, \d+
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Request-Type, pc  

上面是一个完整的route配置,它包含4个属性:id,uri,predicates,filters

id

唯一标记一个路由配置项。

uri

描述后端服务地址,通常按lb://server_id方式配置(server_id,后端服务在注册中心的Id标识 ) ,也可按http://host:port 风格配置 。

predicates(匹配)

负责路由匹配, 就是将一个http请求与一个具体的路由配置项关联。框架内已提供丰富的匹配功能,可以对请求的时间、url、head、method等维度进行匹配,除参考官网的配置介绍,如果有兴趣,可了解下源码,逻辑都比较简单,包路径org.springframework.cloud.gateway.handler.predicate,每个维度的predicate都对应一个单独的类,例如:PathRoutePredicateFactory等。

filters(过滤)

对已匹配的请求,进行请求、响应结果的修改,或增加拦截处理逻辑。基本上,api网关的主要处理逻辑,都在Filter实现。通常都会包含StripPrefix配置,指明后端服务url在路径的起始位置。

框架已内置、实现了丰富的过滤器,不仅包含AddRequestHeader, AddResponseHeader, ModifyRequestBody, ModifyResponseBody等对请求、响应处理的过滤器,还有CircuitBreaker、Retry、SecureHeaders等实用过滤器,同样很有必要了解下源码,包路径org.springframework.cloud.gateway.filter.factory下都有对应的工厂类。

4.2 超时配置

为保证接口可用性,一般都会对后端服务响应时间限制:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
# 全局超时
      httpclient:
        connect-timeout: 2000
        response-timeout: 2s
      routes:
        - id: hello1
          uri: lb://mtr-cloud-client1
          predicates:
            - Path=/aaa/demo/**,/ccc/demo/**
            - After=2022-05-26T15:15:00.789+08:00
          filters:
            - StripPrefix=1
          metadata:
# 局部超时
            response-timeout: 2000
            connect-timeout: 2s    

4.3 重试配置

为保证接口可用性,一般也会对后端服务响应超时、IO异常,进行重试处理。

      routes:
        - id: hello1
          uri: lb://mtr-cloud-client1
          predicates:
            - Path=/aaa/demo/**,/ccc/demo/**
            - After=2022-05-26T15:15:00.789+08:00
          filters:
            - StripPrefix=1
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY
                methods: GET,POST
                backoff:
                  firstBackoff: 1000ms
                  maxBackoff: 10000ms
                  factor: 2
                  basedOnPreviousValue: false

注:以上仅是常用配置,spring cloud gateway 提供了丰富的内置过滤器,可参考官网了解。

5 自定义

5.1 predicate工厂

新建MyHeader断言工厂,参考内置Header断言工厂实现,注意命名规范,需要以RoutePredicateFactory结尾。

@Component
public class MyHeaderRoutePredicateFactory
		extends AbstractRoutePredicateFactory<MyHeaderRoutePredicateFactory.Config> {

	public static final String HEADER_KEY = "header";
	public static final String REGEXP_KEY = "regexp";

	public MyHeaderRoutePredicateFactory() {
		super(Config.class);
	}

	@Override
	public List<String> shortcutFieldOrder() {
		return Arrays.asList(HEADER_KEY, REGEXP_KEY);
	}

	@Override
	public Predicate<ServerWebExchange> apply(Config config) {
		boolean hasRegex = !StringUtils.isEmpty(config.regexp);

		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;
				}
				
				if (hasRegex) {
					return values.stream().anyMatch(value -> value.matches(config.regexp));
				}

				return true;
			}

			@Override
			public String toString() {
				return String.format("Header: %s regexp=%s", config.header,
						config.regexp);
			}
		};
	}

	@Validated
	public static class Config {

		@NotEmpty
		private String header;
		private String regexp;

		public String getHeader() {
			return header;
		}

		public Config setHeader(String header) {
			this.header = header;
			return this;
		}

		public String getRegexp() {
			return regexp;
		}

		public Config setRegexp(String regexp) {
			this.regexp = regexp;
			return this;
		}
	}
}

配置:

      routes:
        - id: hello1
#          uri: http://localhost:8081
          uri: lb://mtr-cloud-client1
          predicates:
            - Path=/aaa/demo/**,/ccc/demo/**
            - After=2022-05-26T15:15:00.789+08:00
            - Header=X-Request-Id1, \d+
            - MyHeader=X-Request-Id2, \d+
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Request-Type, pc  

5.2 filter工厂

新建HelloParameter过滤器工厂,参考内置AddRequestParameter过滤器工厂,注意命名规范,需要以GatewayFilterFactory结尾。

@Component
public class HelloParameterGatewayFilterFactory extends AbstractGatewayFilterFactory<HelloParameterGatewayFilterFactory.Config> {
	
	private final Logger logger = LoggerFactory.getLogger(getClass());

	public HelloParameterGatewayFilterFactory() {
		super(HelloParameterGatewayFilterFactory.Config.class);
		logger.info("Loaded HelloParameterGatewayFilterFactory [HelloParameter]");
	}

	@Override
	public List<String> shortcutFieldOrder() {
		return Arrays.asList("key", "value");
	}

	@Override
	public GatewayFilter apply(HelloParameterGatewayFilterFactory.Config config) {
		return (exchange, chain) -> {
			ServerHttpRequest request = exchange.getRequest().mutate()
					.header(config.key, config.value).build();
			
			return chain.filter(exchange.mutate().request(request).build());
		};
	}

	public static class Config {

		private String key;
		private String value;
		
		public String getKey() {
			return key;
		}
		public void setKey(String key) {
			this.key = key;
		}
		public String getValue() {
			return value;
		}
		public void setValue(String value) {
			this.value = value;
		}
	}
}

配置:

      routes:
        - id: hello1
#          uri: http://localhost:8081
          uri: lb://mtr-cloud-client1
          predicates:
            - Path=/aaa/demo/**,/ccc/demo/**
            - After=2022-05-26T15:15:00.789+08:00
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Request-Type, pc  
            - HelloParameter=wang, morning

5.3 全局filter

下面是一个模拟验证token的过滤器,如果请求Header中没有传递有效token,就会被拦截。

@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {
	
	private final Logger logger = LoggerFactory.getLogger(getClass());
	
	private static final String HEADER_KEY_TOKEN = "token";

	@Override
	public int getOrder() {
		return 0;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		
		logger.info("TokenGlobalFilter begin, uri:{}", exchange.getRequest().getURI());
		
		HttpHeaders headers = exchange.getRequest().getHeaders();
		String token = headers.getFirst(HEADER_KEY_TOKEN);
		
		if (!this.checkToken(token)) {			
			ServerHttpResponse response = exchange.getResponse();
			response.setStatusCode(HttpStatus.UNAUTHORIZED);
			response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
			
			//return response.writeWith(Flux.just(response.bufferFactory().wrap("token is invalid".getBytes())));
			return response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap("token is invalid".getBytes()))));				
		}
		return chain.filter(exchange);
	}
	
	private boolean checkToken(String value) {		
		return "wind".equals(value);
	}

}

提示:不管是过滤器工厂,还是全局过滤器,都可以通过Ordered接口来调整处理顺序,都是在GatewayFilter实现类添加。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值