目录
3.2.1、AddRequestParameterGatewayFilterFactory:添加请求头
3.2.2、RequestRateLimiterGatewayFilterFactory:限流
3.2.3、RetryGatewayFilterFactory:重试
1、网关作用
在微服务场景下,前端访问不同服务的时候,需要做重复的工作,如鉴权,日志等功能,为了解决统一问题,就增加微服务网关进行整合,如限流,鉴权,缓存,熔断,日志,协议转换。
2、基本使用
依照之前的项目进行构建,依旧采用spring脚手架,但需要注意的是,spring cloud gateway和web-starter的jar包是冲突的,主要因为gateway采用的框架和mvc有冲突,因此需要在父pom中移除web-starter。
构建完成后,记得删除父依赖中的web-starter。
之后进行环境配置,这次采用yml的方式进行配置。
你问我为什么不用properties?
我不会(微笑)
依旧要配置服务名称,端口号和eureka地址,除此之外,需要配置gateway。
在这里有一个大致的说明,详细配置会在后面进行讲解。
这个配置的意思是,所有localhost:8087/gateway/**的访都会被转接到localhost:8083/**上,StripPrefix的作用是忽略path后的几个路由字段,这里的1,就是忽略/gateway。
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
routes:
- predicates:
- Path=/gateway/**
filters:
- StripPrefix=1
uri: http://localhost:8083/
server:
port: 8087
eureka:
client:
service-url:
defaultZone: http://localhost:8081/eureka
启动完成后,尝试访问:http://localhost:8087/gateway/default,即可得到和访问http://localhost:8083/default一样的结果。
3、配置说明
可以看到,上文中有几个很重要的配置
- Route:路由,包含predicate,
- Predicate:断言
- Filter:过滤器
3.1、Predicate 断言
Predicate的断言和java8的并无区别,简单来说,就是和配置好的字符进行匹配,如果符合,就会进入到Filter中进行过滤。
3.1.1、系统断言
除了path,还有很多其他的断言实现,这些断言实现只要一起配置,就可以同时生效。
- Path:进行路径匹配,如- Path=/gateway/**
- Query:进行正则匹配,或者参数与参数值匹配,如- Query=/name,lily,需要http://localhost:8087/gateway/default?name=lily,增加参数name=lily之后,就可以正常访问了。
- Method:访问方式,post或get,如:-Method=Get
- Header:访问请求头限制,形式类似Query,也是key,value形式的,正则表达式同样适用
- Cookie:访问Cookie限制,形式类似Query,也是key,value形式的,正则表达式同样适用
3.1.2、自定义断言
通过查看gateway包内的文件,可以看到,想要自定义断言,只需要继承AbstractRoutePredicateFactory抽象类,但需要注意的是,类的名称一定是xxx+RoutePredicateFactory,xxx是任意一个名字,这将作为你的自定义断言名称,后半部分不能更改,因为系统会默认加载后面为RoutePredicateFactory的类作为断言。
AuthRoutePredicateFactory 类必须被IOC托管,所以增加@Component注解。
继承AbstractRoutePredicateFactory类,实现对应方法。
添加内部类Config作为传递参数的类,并加入到AbstractRoutePredicateFactory的泛型定义中,super实现去掉参数,将Config内部类作为指定参数。
shortcutFieldOrder方法,是作为简化配置的方式实现的,意思是将配置后的参数名称直接作为name,不需要额外声明。
apply是核心断言方法,返回值为Boolean类型,这里的方法是需要在Header中添加设置好的参数名,如果没有得到,就返回错误。
@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {
public AuthRoutePredicateFactory() {
super(Config.class);
}
private static final String NAME_KEY = "name";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
String name = config.getName();
return serverWebExchange -> {
HttpHeaders httpHeaders = serverWebExchange.getRequest().getHeaders();
List<String> strings = httpHeaders.get(name);
return !(strings==null) && (strings.size() > 0);
};
}
public static class Config{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
在配置中,增加Auth=Authorization
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
routes:
- id: route1
predicates:
- Path=/gateway/**
- Method=Get
- Auth=Authorization
filters:
- StripPrefix=1
uri: http://localhost:8083/
server:
port: 8087
eureka:
client:
service-url:
defaultZone: http://localhost:8081/eureka
增加完成后,需要在Postman中进行测试
可以看到,如果没有添加对应的参数Authorization,就无法进入断言。
3.2、Filter 过滤
下面列举一些常见的过滤器
3.2.1、AddRequestParameterGatewayFilterFactory:添加请求头
这个来自于类AddRequestParameterGatewayFilterFactory,其中GatewayFilterFactory是固定后缀,AddRequestParameter作为name来简化配置,作用是限定header中要有一对键值对。
- id: add_request_paraemter
uri: http://localhost:8083/
filter:
#在Header中增加一对键值对,name和lily
- AddRequestParameter=name,lily
3.2.2、RequestRateLimiterGatewayFilterFactory:限流
这个来自于类RequestRateLimiterGatewayFilterFactory,其中GatewayFilterFactory是固定后缀。
限流器使用了redis作为计数器,使用令牌桶进行限流。
- burstCapacity ,令牌桶上限 。
- replenishRate ,令牌桶填充平均速率,单位:秒。
- keyResolver ,限流键解析器 Bean 对象名字 ,使用 SpEL 表达式,从 Spring 容器中获取 Bean 对象
- id: request_rate_limiter
uri: http://localhost:8083/
predicates:
- Path=/limiter/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
keyResolver: '#{@ipAddressKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 2
增加配置redis地址:
spring:
application:
name: spring-cloud-gateway
redis:
host: 127.0.0.1
增加继承KeyResolver的类,这个类的作用就是参数KeyResolver的限流地址
@Component
public class IpAddressKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
在pom文件中添加redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
3.2.3、RetryGatewayFilterFactory:重试
这个来自于类RetryGatewayFilterFactory,其中GatewayFilterFactory是固定后缀,其常见参数有四个:
private int retries = 3;
private List<Series> series;
private List<HttpStatus> statuses;
private List<HttpMethod> methods;
- retries,重试次数,默认为3
- status,重试状态码,取值来自HttpStatus
- series,重试状态码范围,取值来自Series
- methods,重试方法类型,取值来自HttpMethod
如果不用复数形式,可以直接使用数值进行设置
- id: retry_route
uri: http://localhost:8083/
predicates:
- Path=/retry/**
filters:
- StripPrefix=1
- name: Retry
args:
retries: 3
statuses: SERVICE_UNAVAILABLE
methods: GET
series: SERVER_ERROR
3.2.4、自定义过滤器
可以通过类似定义断言的方式自定义过滤器。
继承AbstractGatewayFilterFactory,实现类MyDefineGatewayFilterFactory。
打印的两个日志意味着前置拦截和后置拦截。
@Component
public class MyDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<MyDefineGatewayFilterFactory.Config> {
Logger logger = LoggerFactory.getLogger(MyDefineGatewayFilterFactory.class);
public MyDefineGatewayFilterFactory() {
super(Config.class);
}
private static final String NAME_KEY = "name";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
logger.info("pre My Filter Request, Name:" + config.getName());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
String statusCode = exchange.getResponse().getStatusCode().toString();
logger.info("post statusCode is, code" + statusCode);
}));
});
}
public static class Config {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
设置配置文件环境。
- id: my_define
uri: http://localhost:8083/
predicates:
- Path=/define/**
filters:
- StripPrefix=1
- MyDefine=lily
访问路径:http://localhost:8087/define/default
ReactiveLoadBalancerClientFilter不同于自定义的前置后置过滤器,它是全局过滤器,无需配置,自动生效,它实现的是GlobalFilter, Ordered,因此自定义全局过滤器时也可以这样。
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
Logger logger = LoggerFactory.getLogger(MyGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
logger.info("MyGlobalFilter is starting");
}));
}
@Override
public int getOrder() {
return 0;
}
}
无需配置,直接执行任意一个请求,即可发现打印出的日志
4、实现负载均衡
想要实现负载均衡,只需要特殊的配置:
将uri的地址配置成lb(load balance)+服务名称即可,即:lb://spring-cloud-xxx
discovery配置的lower-case-service-id是服务ID是否小写,enable是否启动
配置完成后,既可达成负载均衡。
spring:
application:
name: spring-cloud-gateway
redis:
host: 127.0.0.1
cloud:
gateway:
routes:
- id: gateway_route
uri: lb://spring-cloud-user-provide
predicates:
- Path=/gateway/**
filters:
- StripPrefix=1
discovery:
locator:
lower-case-service-id: true
enabled: true
5、动态路由实现
5.1、内存中的路由缓存
gateway组件本身就提供了一些能改变路由配置的接口,但是需要添加配置:
management:
endpoints:
web:
exposure:
include: "*"
这里的*表示任何路由都可以访问,但是这样的访问很不安全,不过这里只是演示。
根据官方文档,根据访问类型不同,访问/actuator/gateway/routes达成的效果是不同的。
get方式可以得到当前全部的gateway配置:http://localhost:8087/actuator/gateway/routes
post方式可以根据routeid增加或修改路由配置:
这里是body的内容:
{
"uri":"https://www.baidu.com",
"predicates":[{
"name":"Path",
"args":{
"pattern":"/baidu/**"
}
}],
"filters":[{
"name":"StripPrefix",
"args":{
"_genkey_0":1
}
}]
}
执行完成后,还需要执行方法进行刷新,方式也是post:http://localhost:8087/actuator/gateway/refresh
这时新配置才会生效。
访问:http://localhost:8087/baidu/s
就可以发现,已经跳转到百度网页上了。
但是这样的方式是将新的配置缓存到内存上,只要重启服务,就会失效,我们需要新的持久化的配置方式。
5.2、持久化路由
其实上文中提到的全部可执行url,都存在于类GatewayControllerEndpoint之中,这个类起到的作用就是controller的作用。
在它的父类之中,有修改和保存路由的方法:
@PostMapping({"/routes/{id}"})
public Mono<ResponseEntity<Object>> save(@PathVariable String id, @RequestBody RouteDefinition route) {
return Mono.just(route).doOnNext(this::validateRouteDefinition).flatMap((routeDefinition) -> {
return this.routeDefinitionWriter.save(Mono.just(routeDefinition).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());
}));
}).switchIfEmpty(Mono.defer(() -> {
return Mono.just(ResponseEntity.badRequest().build());
}));
}
可以看到,这个保存路由的基本方法,就是routeDefinitionWriter.save,这是一个接口,有一个实现类,那么其实只要自定义一个实现类,就可以实现动态路由的持久化。
protected RouteDefinitionWriter routeDefinitionWriter;
使用redis作为持久化的数据存储方式,加入对应的jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
增加新的自定义路由访问方式。
将类托管到IOC中,自动注入RedisTemplate,重写方法,包括获取,保存和删除,都依靠redis进行。
@Component
public class MyRedisRouteDefinitionRepository implements RouteDefinitionRepository {
private final static String GATEWAY_KEY = "GATEWAY_ROUTE";
@Autowired
RedisTemplate<String, String> redisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
redisTemplate.opsForHash().values(GATEWAY_KEY).stream().forEach(route -> {
routeDefinitionList.add(JSON.parseObject(route.toString(), RouteDefinition.class));
});
return Flux.fromIterable(routeDefinitionList);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
redisTemplate.opsForHash().put(GATEWAY_KEY, routeDefinition.getId(), JSON.toJSONString(routeDefinition));
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
if (redisTemplate.opsForHash().hasKey(GATEWAY_KEY, id)) {
redisTemplate.opsForHash().delete(GATEWAY_KEY, id);
return Mono.empty();
}
return Mono.defer(() -> (Mono.error(new Exception())));
});
}
}
RouteDefinition是用来保存路由信息的类,此处展示部分内容
@Validated
public class RouteDefinition {
private String id;
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList();
@Valid
private List<FilterDefinition> filters = new ArrayList();
@NotNull
private URI uri;
private Map<String, Object> metadata = new HashMap();
private int order = 0;
public RouteDefinition() {
}
}
GatewayAutoConfiguration起到自动装配的作用。
@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) {
return new PropertiesRouteDefinitionLocator(properties);
}