微服务可能分布在不同的主机上,这样有许多缺点:前端需要硬编码调用不同地址的微服务很麻烦;存在跨域访问的问题;微服务地址直接暴露是不安全的。还有所以需要为前端提供一个统一的访问入口。Gateway 就是用于解决以上问题的框架。Gateway 本身是一个微服务,要注册到 Eureka 中。
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency></dependencies>server: port: 10010spring: application: name: api-gateway cloud: gateway: routes: - id: user-service-route # 路由的标识 ID,任意名称都可以 uri: http://127.0.0.1:9092 # 路由的转发地址 predicates: - Path=/user/** # 路由的映射范围eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka instance: prefer-ip-address: true启动实例后能在 Eureka 注册中心看到注册,http://localhost:10010/user/** 的请求会被转发到 http://localhost:9092/user/**。
入门案例中使用的是静态IP 地址,下面是用服务名称代替具体 IP,这样避免直接使用 IP,并可以添加负载均衡。
只需要将配置中的 uri 更换为「loadbalance 协议」
spring: application: name: api-gateway cloud: gateway: routes: - id: user-service-route # 路由的标识 ID,任意名称都可以 uri: lb://USER-SERVICE # 路由的转发地址 predicates: - Path=/user/** # 路由的映射范围http://localhost:10010/**-->lb://USER-SERVICE/user/**spring: application: name: api-gateway cloud: gateway: routes: - id: user-service-route # 路由的标识 ID,任意名称都可以 uri: lb://USER-SERVICE # 路由的转发地址 predicates: - Path=/** # 路由的映射范围 filters: - PrefixPath=/userhttp://localhost:10010/api/api/user/**-->lb://USER-SERVICE/user/**spring: application: name: api-gateway cloud: gateway: routes: - id: user-service-route # 路由的标识 ID,任意名称都可以 uri: lb://USER-SERVICE # 路由的转发地址 predicates: - Path=/api/api/user/** # 路由的映射范围 filters: - StripPrefix=2 # 去除两层前缀前面修改请求其实都是通过 Gateway 的过滤器实现的,上面用到的过滤器为 StripPrefixGatewayFilterFactory 产生的过滤器。Gateway 提供了几十种过滤器,过滤器可以过滤指定规则的请求,也可以过滤所有请求。
spring: cloud: gateway: default-filters: - AddResponseHeader=this-is-a-header, Have-Fun这个过滤器可以为所有请求添加一个响应头,this-is-a-header: Have-Fun。
路由器分为局部路由和全局路由,局部路由分为两种(上面展示的两种):
全局过滤器:不在配置文件中配置,需要实现 GlobalFilter 接口。
过滤器类似于拦截器,但是只在微服务处理前后运作,请求转发不会触发过滤器。
自定义过滤器需要实现 AbstractGatewayFilterFactory 抽象类。该类使用内部类接受配置文件中的参数,需要给这个内部类的提供 Getter/Setter 方法。
package com.example.gateway.filter;import java.util.Arrays;import java.util.List;import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.stereotype.Component;@Componentpublic class MyMyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyMyGatewayFilterFactory.Config> { public MyMyGatewayFilterFactory() { super(Config.class); } // 这个方法指定配置文件注入 Config 的哪一个字段 public List<String> shortcutFieldOrder() { return Arrays.asList("param"); } // 由于接受配置文件参数 public static class Config { private String param; public String getParam() { return param; } public void setParam(String param) { this.param = param; } } // 此为过滤器逻辑,返回值是一个过滤方法,可以使用 Lambda 表达式。 // exchange 参数可以获取请求内容。chain 用于执行请求。 @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { // 前置处理 ServerHttpRequest request = exchange.getRequest(); if (request.getQueryParams().containsKey(config.param)) { request.getQueryParams().get(config.param) .forEach(o -> System.out.printf("局部过滤器:%s = %s \n", config.param, o)); } return chain.filter(exchange); // 执行请求 }; }}这里我们使用配置文件将 "name" 注入到 Config.param 字段上。注意配置中使用该过滤器类的前缀来注入。比如 MyMyGatewayFilterFactory 的前缀为 MyMy。过滤器类的命名格式是固定的,为 XxxxGatewayFilterFactory,需要使用 @Component 注入到 Spring 容器中,泛型使用 Config 内部类。
spring: cloud: gateway: routes: - id: user-service-route # 路由的标识 ID,任意名称都可以 uri: lb://USER-SERVICE # 路由的转发地址 predicates: - Path=/api/api/user/** # 路由的映射范围 filters: - StripPrefix=2 # 去除两层前缀 - MyMy=name default-filters: - AddResponseHeader=this-is-a-header, Have-Fun实现 GlobalFilter, Ordered 接口并注入到 Spring 容器中。
package com.example.gateway.filter;import org.apache.commons.lang.StringUtils;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;@Componentpublic class MyGlobalFilter implements GlobalFilter, Ordered { // 设置执行的优先级,数字越小优先级越高 @Override public int getOrder() { return 1; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("自定义全局过滤器 MyGlobalFilter 执行"); String token = exchange.getRequest().getHeaders().getFirst("token"); // 如果没有 token 将响应状态设置为「未验证 401」,并直接返回,不执行后面的操作。 if (StringUtils.isBlank(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }}Gateway 默认集成了 Ribbon 和 Hystrix。
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2000 #服务降级超时时间 circuitBreaker: errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50% sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒 requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20ribbon: ConnectTimeout: 1000 # 连接超时时长 ReadTimeout: 2000 # 数据通信超时时长 MaxAutoRetries: 0 # 当前服务器的重试次数 MaxAutoRetriesNextServer: 0 # 重试多少次服务 OkToRetryOnAllOperations: false # 是否对所有的请求方式都重试微服务会面临跨域访问(比如端口不同)的问题,默认情况下服务不允许别的域访问,我们需要在配置中设置「白名单」告知从哪些域来的请求是被允许的。
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': # 哪些请求可以跨域 allowedOrigins: # 跨域白名单 - "http://docs.spring.io" allowedMethods: # 允许的请求方式 - GET 一个常见的情况是前后端分离,前端后端部署在不同端口。比如上面配置中 http://docs.spring.io 为前端的域,完成这个配置之后,前端就可以正常使用 Ajax 访问其他域的所有微服务。
启动多个 Gateway 实例,实现微服务内部访问的高可用。但是 Gateway 主要适用于直接暴露给客户端,仅内部高可用并不是想要的,这时候需要使用 Nginx 代理 Gateway。
Gateway 是作为整体微服务暴露给客户端的访问入口,通常用作权限鉴定和流量控制。Feign 是微服务内部之间互相调用的入口。一句话说就是 Gateway、Feign 一外一内。
尊重原创,转载请标明出处。