Spring Cloud Gateway从入门到实战

  • API网关的作用
  • Spring Cloud Gateway特性
  • 与Zuul的对比
  • Spring Cloud Gateway核心概念
  • Spring Cloud Gateway工作原理
  • Spring Cloud Gateway入门案例
  • 内置的路由断言工厂
  • 内置的Filter
  • 基于路由发现的路由规则
  • Gateway Filter 与 Global Filter
  • 自定义Gateway Filter
  • 自定义Global Filter
  • Filter的执行顺序
  • Actuator Api
  • 动态路由


先上官方文档: https://spring.io/projects/spring-cloud-gateway

API网关的作用

众所周知, Java的API网关通常基于Filter Chain(过滤器链)构成, 可以实现单点登录、鉴权、路由转发、熔断、限流、监控、日志、加密解密、灰度发布等功能。在下游业务系统中就不必再处理这些重复的工作, 也可以优化我们的服务架构。

Spring Cloud Gateway特性

  • 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
  • 动态路由
  • 作用于特定路由的 Predicates 和 Filters
  • 集成 Hystrix 断路器
  • 集成 Spring Cloud DiscoveryClient
  • 易于编写的 Predicates 和 Filters
  • 限流
  • 路径重写

通过这些特性我们可以看出, Spring Cloud Gateway 是一个基于Spring5与SpringBoot、响应式的、并且提供大量实用功能的API网关组件。

与Zuul的对比

主要是与zuul1.0的对比

  • Zuul基于Servlet 阻塞IO, Gateway基于Netty 非阻塞IO(NIO)
  • Zuul2.0也是基于Netty, 但Spring不再集成
  • 性能对比-基准测试 Gateway大约是zuul1.0的1.5-1.6倍 (https://github.com/spencergibb/spring-cloud-gateway-bench)
  • Spring Cloud Netflix项目进入维护模式(不再添加新特性,仅提供bug与安全问题修复), 包括Zuul Hystrix Ribbon等模块。Spring建议的替代品

Spring Cloud Gateway核心概念

spring gateway 跟服务直接 大量closewait状态_Cloud

  • Route 路由。网关的最基础的部分,由一个ID、一个目的URL、一组断言工厂与一组Filter组成,如果断言为真,则请求的url与指定路由匹配
  • Predicate 断言/谓词。 Java 8的断言函数。Spring cloud gateway 中的断言函数输入类型为ServerWebExchange ,可以用它来匹配 Http Request中的任何信息
  • Filter 过滤器。一个标准的Spring webFilter ,分为两种类型 Gateway Filter 与 Global Filter。 Filter会对请求和响应进行处理

Spring Cloud Gateway工作原理

客户端通过网络请求服务器→HttpWebHandlerAdapter适配器将请求的request封装为Gateway所需的ServerWebExchange对象→DispatcherHandler遍历Mapping 获取Handler →RoutePredicateHandlerMapping处理断言匹配路由→断言成功获取路由,FilteringWebHandler构建Filter责任链并执行

spring gateway 跟服务直接 大量closewait状态_gateway_02

Spring Cloud Gateway入门案例

1.创建SpringCloud项目并添加getaway依赖

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

2.配置路由
方式一: Java Config

@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder
				.routes()
				.route("_server_baidu", r ->
						r.path("/server-baidu/**")
								.uri("http://www.baidu.com"))
				.build();
	}

方式一: yml配置文件

# yml配置方式
  cloud:
    gateway:
      routes:
        - id: server_baidu
          uri: http://www.baidu.com
          predicates:
            - Path=/server-baidu/**

启动项目 ,访问127.0.0.1:8080//server-baidu ,会返回百度首页

内置的路由断言工厂

断言工厂

介绍

使用

After路由断言工厂

对请求时间进行断言处理

请求时间在断言指定的时间之后则成立

Before路由断言工厂

对请求时间进行断言处理

请求时间在断言指定的时间之前则成立

Between路由断言工厂

对请求时间进行断言处理

请求时间在断言指定的时间区间内则成立

Cookie路由断言工厂

对cookie进行断言处理

请求cookie中有指定参数则成立

Header路由断言工厂

对请求头进行断言处理

请求头中有指定的key-value则成立

Host路由断言工厂

对请求中的Host进行断言处理

请求头中host为指定的值则成立

Method路由断言工厂

对请求方式进行断言处理

请求方式为指定方式(GET/POST等)则成立

Query路由断言工厂

对请求参数进行断言处理

请求参数满足指定的key-value则成立

RemoteAddr路由断言工厂

对请求IP进行断言处理

请求IP为指定的IP或指定IP段则成立

@Bean
	public RouteLocator customRouteLocator1(RouteLocatorBuilder builder) {
		// 获取当前时间后一分钟 & 前一分钟
		ZonedDateTime dateTimeBefore = LocalDateTime.now().plusMinutes(1).atZone(ZoneId.systemDefault());
		ZonedDateTime dateTimeAfter = LocalDateTime.now().minusMinutes(1).atZone(ZoneId.systemDefault());

		return builder
				.routes()
				// after断言  请求时间在dateTimeAfter之后则断言为真
				.route("_server_taobao_after_id", r -> r.after(dateTimeAfter).uri("https://www.taobao.com"))
				// before断言  请求时间在dateTimeBefore之前则断言为真
				.route("_server_taobao_before_id", r -> r.before(dateTimeBefore).uri("https://www.taobao.com"))
				// between断言  请求时间在dateTimeAfter与dateTimeBefore之间则断言为真
				.route("_server_taobao_between_id", r -> r.between(dateTimeAfter, dateTimeBefore).uri("https://www.taobao.com"))
				// cookie断言  请求cookie中有sessionId并且为指定的值(ABCDEFG)则断言为真
				.route("_server_taobao_cookie_id", r -> r.cookie("sessionId", "ABCDEFG").uri("https://www.taobao.com"))
				// Header断言  请求头中有token并且为指定的值(ABCDEFG)则断言为真
				.route("_server_taobao_header_id", r -> r.header("token", "ABCDEFG").uri("https://www.taobao.com"))
				// Host断言  请求头中有host并且为指定的值(127.0.0.2)则断言为真
				.route("_server_taobao_host_id", r -> r.host("127.0.0.2").uri("https://www.taobao.com"))
				// Method断言  请求方式为POST则断言为真
				.route("_server_taobao_method_id", r -> r.method("POST").uri("https://www.taobao.com"))
				// Query断言  请求参数中有userId参数并且为指定的值(199999)则断言为真
				.route("_server_taobao_query_id", r -> r.query("userId", "199999").uri("https://www.taobao.com"))
				// RemoteAddr断言  请求IP为指定的值(127.0.0.1)则断言为真
				.route("_server_taobao_remote_addr_id", r -> r.remoteAddr("127.0.0.1").uri("https://www.taobao.com"))
				.build();
	}

内置的Filter

内置Filter

介绍

使用

AddRequestHeader 过滤器

增加请求头

需要增加请求头数据时

AddRequestParameter 过滤器

增加请求参数

需要增加请求参数时

AddResponseHeader 过滤器

增加响应头

需要增加响应头数据时

RewritePath 过滤器

重写请求路径

需要对请求路径进行修改时

StripPrefix 过滤器

路径截取

需要对请求路径进行截取时

Retry 过滤器

请求重试

出现网络抖动或服务异常需要调用重试时

Hystrix 过滤器

断路器

需要配置断路器Hystrix时

@Bean
	public RouteLocator customRouteLocator2(RouteLocatorBuilder builder) {
		return builder
				.routes()
				// AddRequestHeader过滤器
				.route("_add_request_header_id",
						r -> r.path("/testHeader/**")
								.filters(f -> f.addRequestHeader("userId", "199999"))
								.uri("http://127.0.0.1:8081"))
				// AddRequestParameter过滤器
				.route("_add_request_parameter_id",
						r -> r.path("/testParameter/**")
								.filters(f -> f.addRequestParameter("id", "123456789"))
								.uri("http://127.0.0.1:8081"))
				// RewritePath过滤器
				.route("_rewrite_path_id",
						r -> r.path("/server-a/**")
								.filters(f -> f.rewritePath("/server-a/(?<segment>.*)", "/$\\{segment}"))
								.uri("http://127.0.0.1:8081"))
				// AddResponseHeader过滤器
				.route("_add_response_header_id",
						r -> r.path("/testResponseHeader/**")
								.filters(f -> f.addResponseHeader("sessionId", "13579"))
								.uri("http://127.0.0.1:8081"))
				// StripPrefix过滤器
				.route("_strip_prefix_id",
						r -> r.path("/server-aaa/**")
								.filters(f -> f.stripPrefix(1))
								.uri("http://127.0.0.1:8081"))
				/**
				 * Retry过滤器  可以指定请求方式、异常类型、返回的HTTP状态码(默认get请求重试)
				 */
				.route("_retry_id",
						r -> r.path("/testRetry/**")
								.filters(f -> f.retry(config ->
										config.setRetries(2)
												.setStatuses(HttpStatus.INTERNAL_SERVER_ERROR)
												.setMethods(HttpMethod.GET, HttpMethod.POST)))
								.uri("http://127.0.0.1:8081"))
				// Hystrix过滤器
				.route("_hystrix_id",
						r -> r.path("/testHystrix/**")
								.filters(f -> f.hystrix(config -> config.setFallbackUri("forward:/fallback")))
								.uri("http://127.0.0.1:8081"))
				.build();
	}

基于路由发现的路由规则

1.创建SpringCloud项目并添加getaway与eureka依赖

<dependency>
   <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>${eureka.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

2.编辑配置文件打开服务实例发现
方式一: Java Config

@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder
				.routes()
				.route("_server_baidu", r ->
						r.path("/server-baidu/**")
								.uri("http://www.baidu.com"))
				.build();
	}

方式一: yml配置文件

# yml配置方式
server:
  port: 7779
# eureka server
eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://127.0.0.1:9999/eureka/
  instance:
    prefer-ip-address: true

spring:
  application:
    name: gateway-server
  # 为eureka上注册的服务自动创建路由
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          # 服务名称转小写 此时调用接口时 服务名称传小写 否则需要传大写的名称
          # (consul与zookeeper不需要配置此选项)
          lower-case-service-id: true

此时启动项目可以通过gateway/注册到eureka上的实例服务名称/具体接口URL访问接口
http://{gateway_ip}:{gateway_port}/{service_instance_name}/interface

Gateway Filter 与 Global Filter

  • Gateway Filter 一个Filter过滤器, 对指定url进行过滤/处理
  • Global Filter 一个全局的Filter过滤器, 作用于所有路由

自定义Gateway Filter

实现Gateway Filter与Order接口

/**
 *  代码示例为接口响应时间输出
 */
public class GatewayFilerDemo implements GatewayFilter, Ordered {

	private static final String START_TIME = "SERVER_EXEC_START_TIME";

	/**
	 * 过滤器逻辑
	 */
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
		return chain.filter(exchange).then(
				Mono.fromRunnable(() -> {
					Long startTime = exchange.getAttribute(START_TIME);
					if (Objects.nonNull(startTime)) {
						Long execTime = System.currentTimeMillis() - startTime;
						System.out.println("接口执行时间 :  ==> " + execTime + "ms");
					}
				})
		);
	}

	/**
	 * 排序号
	 */
	@Override
	public int getOrder() {
		return 100;
	}
}

Gateway Filter的使用: 对请求Path为/server-a/**的请求,应用GatewayFilerDemo过滤器

@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder
				.routes()
				.route("_server_a_id",
						r -> r.path("/server-a/**")
								.filters(f ->
										f.rewritePath("/server-a/(?<segment>.*)", "/$\\{segment}")
												.filter(new GatewayFilerDemo()))
								.uri("lb://server-a"))
				.build();
	}

自定义Global Filter

实现Global Filter与Ordered接口,并交付给Spring容器管理即可,所有通过gateway的请求都会被此过滤器处理

/**
 *  代码示例为token验证Demo
 */
@Component
public class GlobalFilterDemo implements GlobalFilter, Ordered {

	/**
	 * 过滤器逻辑
	 */
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		String token = exchange.getRequest().getHeaders().getFirst("token");
		if (token != null && token.equals("abc")) {
			System.out.println("token验证成功");
			return chain.filter(exchange);
		}
		// 未通过返回401
		exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
		return exchange.getResponse().setComplete();
	}

	/**
	 * 排序号
	 */
	@Override
	public int getOrder() {
		return 200;
	}
}

Filter的执行顺序

执行Route的时候,Spring Cloud Gateway会将
Global Filter和Route Filter结合起来并排序执行

  • 没有给order的Global Filter则保持order为null 去排序
  • 没有给order的Route Filter的order则从1开始, 根据顺序给值
  • 对于Pre Filter,执行顺序与排序相同
  • 对于Post Filter,执行顺序与排序相反

Actuator Api

Actuator 组件提供了功能丰富的restful接口可以对gateway中的路由进行操作.

请求方式

url

功能

GET

/actuator/gateway/routes

查看全部路由

GET

/actuator/gateway/globalfilters

查看全局过滤器列表

GET

/actuator/gateway/routefilters

查看网关成功加载的过滤器工厂

POST

/actuator/gateway/refresh

刷新路由缓存

GET

/actuator/gateway/routes/{id}

获取指定路由信息

DEL

/actuator/gateway/routes/{id}

删除指定路由

POST

/actuator/gateway/routes /{id}

添加路由

使用前提:
1.项目集成端点依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.配置文件打开端点访问

# 端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
    gateway:
      enabled: true

示例:

1.查询路由

spring gateway 跟服务直接 大量closewait状态_gateway_03


2.新增路由

spring gateway 跟服务直接 大量closewait状态_java_04

3.删除路由

spring gateway 跟服务直接 大量closewait状态_spring_05

3.刷新路由缓存

spring gateway 跟服务直接 大量closewait状态_网关_06

动态路由

通过上面的actuator的restful接口可以实现一部分动态路由功能, 如果我们需要拓展的话可以参考actuator的代码实现GatewayControllerEndpoint中的代码,实现我们自己的动态路由功能.
代码中有我们在actuator所看到的接口方法.

spring gateway 跟服务直接 大量closewait状态_Cloud_07

public class GatewayControllerEndpoint implements ApplicationEventPublisherAware {
    private static final Log log = LogFactory.getLog(GatewayControllerEndpoint.class);
    private RouteDefinitionLocator routeDefinitionLocator;
    private List<GlobalFilter> globalFilters;
    private List<GatewayFilterFactory> GatewayFilters;
    private RouteDefinitionWriter routeDefinitionWriter;
    private RouteLocator routeLocator;
    private ApplicationEventPublisher publisher;

    public GatewayControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List<GlobalFilter> globalFilters, List<GatewayFilterFactory> GatewayFilters, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
        this.routeDefinitionLocator = routeDefinitionLocator;
        this.globalFilters = globalFilters;
        this.GatewayFilters = GatewayFilters;
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeLocator = routeLocator;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @PostMapping({"/refresh"})
    public Mono<Void> refresh() {
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return Mono.empty();
    }

    @GetMapping({"/globalfilters"})
    public Mono<HashMap<String, Object>> globalfilters() {
        return this.getNamesToOrders(this.globalFilters);
    }

    @GetMapping({"/routefilters"})
    public Mono<HashMap<String, Object>> routefilers() {
        return this.getNamesToOrders(this.GatewayFilters);
    }

    private <T> Mono<HashMap<String, Object>> getNamesToOrders(List<T> list) {
        return Flux.fromIterable(list).reduce(new HashMap(), this::putItem);
    }

    private HashMap<String, Object> putItem(HashMap<String, Object> map, Object o) {
        Integer order = null;
        if (o instanceof Ordered) {
            order = ((Ordered)o).getOrder();
        }

        map.put(o.toString(), order);
        return map;
    }

    @GetMapping({"/routes"})
    public Mono<List<Map<String, Object>>> routes() {
        Mono<Map<String, RouteDefinition>> routeDefs = this.routeDefinitionLocator.getRouteDefinitions().collectMap(RouteDefinition::getId);
        Mono<List<Route>> routes = this.routeLocator.getRoutes().collectList();
        return Mono.zip(routeDefs, routes).map((tuple) -> {
            Map<String, RouteDefinition> defs = (Map)tuple.getT1();
            List<Route> routeList = (List)tuple.getT2();
            List<Map<String, Object>> allRoutes = new ArrayList();
            routeList.forEach((route) -> {
                HashMap<String, Object> r = new HashMap();
                r.put("route_id", route.getId());
                r.put("order", route.getOrder());
                if (defs.containsKey(route.getId())) {
                    r.put("route_definition", defs.get(route.getId()));
                } else {
                    HashMap<String, Object> obj = new HashMap();
                    obj.put("predicate", route.getPredicate().toString());
                    if (!route.getFilters().isEmpty()) {
                        ArrayList<String> filters = new ArrayList();
                        Iterator var6 = route.getFilters().iterator();

                        while(var6.hasNext()) {
                            GatewayFilter filter = (GatewayFilter)var6.next();
                            filters.add(filter.toString());
                        }

                        obj.put("filters", filters);
                    }

                    if (!obj.isEmpty()) {
                        r.put("route_object", obj);
                    }
                }

                allRoutes.add(r);
            });
            return allRoutes;
        });
    }

    @PostMapping({"/routes/{id}"})
    public Mono<ResponseEntity<Void>> save(@PathVariable String id, @RequestBody Mono<RouteDefinition> route) {
        return this.routeDefinitionWriter.save(route.map((r) -> {
            r.setId(id);
            log.debug("Saving route: " + route);
            return r;
        })).then(Mono.defer(() -> {
            return Mono.just(ResponseEntity.created(URI.create("/routes/" + id)).build());
        }));
    }

    @DeleteMapping({"/routes/{id}"})
    public Mono<ResponseEntity<Object>> delete(@PathVariable 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());
        });
    }

    @GetMapping({"/routes/{id}"})
    public Mono<ResponseEntity<RouteDefinition>> route(@PathVariable String id) {
        return this.routeDefinitionLocator.getRouteDefinitions().filter((route) -> {
            return route.getId().equals(id);
        }).singleOrEmpty().map(ResponseEntity::ok).switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
    }

    @GetMapping({"/routes/{id}/combinedfilters"})
    public Mono<HashMap<String, Object>> combinedfilters(@PathVariable String id) {
        return this.routeLocator.getRoutes().filter((route) -> {
            return route.getId().equals(id);
        }).reduce(new HashMap(), this::putItem);
    }
}

实现我们自己的动态路由
1.定义路由bean 断言bean 过滤器bean 参考actuator接口返回值中的字段

public class GatewayRouteDefinition {
	/**
	 * 路由ID
	 */
	private String id;
	/**
	 * 一组断言
	 */
	private List<GatewayPredicateDefinition> predicates = new ArrayList<>();
	/**
	 * 一组过滤器
	 */
	private List<GatewayFilterDefinition> filters = new ArrayList<>();
	/**
	 * 转发的目标URI
	 */
	private String uri;
	/**
	 * 执行顺序号
	 */
	private int order;
public class GatewayFilterDefinition {
	/**
	 * 过滤器名称
	 */
	private String name;
	/**
	 * 路由规则
	 */
	private Map<String, String> args = new LinkedHashMap<>();
public class GatewayPredicateDefinition {
	/**
	 * 断言名称
	 */
	private String name;
	/**
	 * 断言规则
	 */
	private Map<String, String> args = new LinkedHashMap<>();

2.参考GatewayControllerEndpoint实现ApplicationEventPublisherAware实现对应的功能

@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware, IDynamicRouteService {

	@Autowired
	private RouteDefinitionWriter routeDefinitionWriter;
	@Autowired
	private ApplicationEventPublisher publisher;
	@Autowired
	private RouteDefinitionLocator routeDefinitionLocator;


	@Override
	public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
		this.publisher = applicationEventPublisher;
	}

	@Override
	public Flux<RouteDefinition> query(RouteDefinition routeDefinition) {
		return routeDefinitionLocator.getRouteDefinitions();
	}

	@Override
	public boolean queryById(RouteDefinition routeDefinition) {
		return false;
	}

	@Override
	public boolean add(RouteDefinition routeDefinition) {
		routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
		this.publisher.publishEvent(new RefreshRoutesEvent(this));
		return true;
	}

	@Override
	public boolean update(RouteDefinition routeDefinition) {
		this.routeDefinitionWriter.delete(Mono.just(routeDefinition.getId())).subscribe();

		routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
		this.publisher.publishEvent(new RefreshRoutesEvent(this));
		return true;
	}

	@Override
	public Mono<ResponseEntity<Object>> delete(String id) {
		this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
		this.publisher.publishEvent(new RefreshRoutesEvent(this));
		return null;
	}
}

3.如需再拓展, 例如将路由存储到redis中实现分布式路由,可以实现我们自己的RouteDefinitionRepository.
它的唯一实现类为InMemoryRouteDefinitionRepository,其中使用了Collections.synchronizedMap来存储我们创建的路由.
我们也可以在此进行拓展.

@Component
@Qualifier("MyRouteDefinitionRepository")
public class MyRouteDefinitionRepository implements RouteDefinitionRepository {

	private static Map<String, RouteDefinition> routes = new ConcurrentHashMap<>();

	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		return Flux.fromIterable(routes.values());
	}

	@Override
	public Mono<Void> save(Mono<RouteDefinition> route) {
		return route.flatMap((r) -> {
			routes.put(r.getId(), r);
			return Mono.empty();
		});
	}

	@Override
	public Mono<Void> delete(Mono<String> routeId) {
		return routeId.flatMap((id) -> {
			routes.remove(id);
			return Mono.empty();
		});
	}
}