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核心概念
- 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 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.查询路由
2.新增路由
3.删除路由
3.刷新路由缓存
动态路由
通过上面的actuator的restful接口可以实现一部分动态路由功能, 如果我们需要拓展的话可以参考actuator的代码实现GatewayControllerEndpoint中的代码,实现我们自己的动态路由功能.
代码中有我们在actuator所看到的接口方法.
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();
});
}
}