? 在微服务的架构中,每一个服务都是在独立的运行的,而一个完整的微服务系统,都是由这些一个个独立运行的服务组成的。每个服务各施其职。各个微服务之间的联系通过REST API或者RPC完成通信。 比如一个场景是: 用户要查看一个商品信息,我们知道一个商品的页面会有: 商品的信息,广告,评论,库存等等。到这里就会涉及到有4个服务了,如果我们没有网关的话,可能就要调用多个服务去获取信息,但是可能会出现一些问题,问题如下:
? spring-cloud-Gateway
是spring-cloud
的一个子项目。而zuul
则是netflix
公司的项目,只是spring将zuul
集成在spring-cloud中使用而已。因为zuul2.0
连续跳票和zuul1
的性能表现不是很理想,所以催生了spring团队开发了Gateway
项目。
zuul1.x和spring gateway对比:
? 注意:现在zuul2.x已经开发出来了。但是spring cloud没有将zuul2.x集成到spring cloud当中,现在的spring cloud zuul组件还是1.x的。所以用网关还是优先使用spirng cloud gateway把
? spring cloud 的路由信息可以通过RouteDefinition
这个类去查看。 这个类里面包含了 id, predicates断言, filters过滤器,uri 转发的地址等等这几个的成员变量,当请求到达网关时 ,首先会基于predicates断言去判断该请求满不满足需求,满足的话就进行下面一系列的filter , 然后再转发到目标uri去。
pom.xml
spring-cloud-dependencies
注意:Hoxton.SR4这个版本是需要添加spring-boot-starter-validation的,不然会报错的。其他版本不清楚会不会
yml配置:
spring:
application:
name: gateway-demo
cloud:
gateway:
enabled: true
routes:
#路径的匹配,StripPrefix代表信
- id: path_route
predicates:
- Path=/baidu
filters:
- StripPrefix=1
uri: https://www.bilibili.com
#COOKIE的匹配
- id: COOKIE_route
predicates:
- COOKIE=chocolate,ch.p
uri: https://www.bilibili.com
#请求头匹配
- id: header_route
predicates:
- Header=X-Request-Id, \d+
uri: https://www.bilibili.com
# 组合匹配
- id: compose
predicates:
- Path=/compose
- Header=name, cong
uri: https://www.bilibili.com
filters:
- StripPrefix=1
? 可以看到有许多的路由信息,大概都是会有:id,predicates,filters,uri这几个参数。
注意点:
uri真的是只是取uri而已,比如你设置uri:http://127.0.0.1:8071/orders,他还是只是取http://127.0.0.1:8071这一段而已
请求网关的地址,会把网关的uri后面那段地址添加到,uri后面上去。比如 请求网关地址: http://127.0.0.1:8080/abc ,
uri是https://www.bilibili.com, 最后访问的地址就是 https://www.bilibili.com/abc了, 至于如果想去掉/abc,可以使用StripPrefix=1这个过滤器去掉。
路由的优先级级别是按照你配置的顺序来的,如果前面的断言已经匹配上了。后面的断言就不会走了
例如下面这个例子:
- id: path_route1
predicates:
- Header=name, cong
uri: https://www.bilibili.com
- id: path_route2
predicates:
- Path=/abc
filters:
- StripPrefix=1
uri: https://baidu.com
? 至于为什么为什么没跳到bilbili,是因为最终访问的是https://www.bilibili.com/abc 所以肯定是返回出错的.这里也验证了注意点的第二点了
? 断言和过滤配置方式分成两种:一种是Shortcut Configuration,一种是Fully Expanded Arguments,以下的配置方式功能是相同的,都是如果COOKIE存在myCOOKIE=myCOOKIEvalue的话,断言判断成功的。
- id: Fully_Expanded
predicates:
- name: COOKIE
args:
name: myCOOKIE
regexp: myCOOKIEvalue
uri: https://www.bilibili.com
- id: short_cut
predicates:
- COOKIE=myCOOKIE,myCOOKIEvalue
uri: https://www.bilibili.com
至于args的信息在哪里找。因为是通过COOKIE断言的,所以可在COOKIERoutePredicateFactory.Config类上面看到相应的参数。
? 常用的断言有:
其他的可以去到官网上面看。
官网地址:https://docs.spring.io/spring-cloud-gateway/docs/3.0.0-SNAPSHOT/reference/html/#gateway-request-predicates-factories
? 自定义一个自己的断言,其实这个断言跟Header头部匹配差不多一样的。功能就是判断头部有没有key为Authorization,value为token
仿照HeaderRoutePredicateFactory,写了一个自己的自定义类
代码实现:
* project name : cloud-demo
* Date:2020/10/3
* Author: yc.guo
* DESC: 需要头部带上authentication才能让其通过
*/
@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory {
public AuthRoutePredicateFactory() {
super(AuthRoutePredicateFactory.Config.class);
}
public List
return Arrays.asList("name", "value");
}
@Override
public Predicate
return serverWebExchange -> {
List
if(list!=null && list.size() > 0 && list.contains(config.value)){
return true;
}
return false;
};
}
@Validated
public static class Config {
@NotEmpty
private String name;
private String value;
public String getName() {
return name;
}
public AuthRoutePredicateFactory.Config setName(String name) {
this.name = name;
return this;
}
public String getValue() {
return value;
}
public AuthRoutePredicateFactory.Config setValue(String value) {
this.value = value;
return this;
}
public Config() {
}
}
}
yml:
- id: define
predicates:
- Auth=Authorization,token
uri: https://www.bilibili.com
注意点:
? Filter分为全局过滤器和路由过滤器。路由过滤器只是针对单个路由的,全局过滤器是针对于所有的路由的,优先级应该是路由过滤器先执行,再到全局过滤器执行
RouteFilter路由过滤器基本有:
StripPrefix - 实现类:StripPrefixGatewayFilterFactory 。
列子:路径是http://127.0.0.1/a/b/c/d ,StripPrefix=2的话 ,得到的结果http://127.0.0.1/c/d
限流过滤器RequestRateLimiter-实现类:RequestRateLimiterGatewayFilterFactory,注意:这个类不能使用shortCut方式因为他没有实现shortcutFieldOrder
? 限流过滤器通过redis还有令牌桶算法去实现的。 令牌桶算法简单的可以把他认为是: 一个很有特色的奶茶店,但是他是每天只能供应50杯奶茶。 那每天只卖50杯了,想喝的话明天请早。令牌桶的意思就是比如,每秒钟可以补充10个令牌桶(),总令牌桶容量可以达到30个。
第一秒内拿走了25个 30-25=5; 第一秒之后补充10个:15个
第二秒没人拿走: 15 第二秒后补充10个: 15+10=25
第三秒没人拿走: 25 第三秒后补充10个: 这里因为容量是30,所以就是30
第四秒需要拿走35: 0,还有5个吃闭门羹了 第四秒后补充10个: 10个
pom.xml文件:
yml配置:
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
deny-empty-key: true
keyResolver: ‘#{@ipAddressKeyResolver}‘ #通过ip作为key值
redis-rate-limiter.replenishRate: 1 #每秒补充的令牌数
redis-rate-limiter.burstCapacity: 2 #令牌容量大小
#redis的配置
spinrg:
redis:
host: 127.0.0.1
port: 6379
? 注意点: redis-rate-limiter.replenishRate:2 , redis-rate-limiter.burstCapacity: 1 这样子虽然每秒补充2个,但是容量只有1个的话,也是只允许一个请求,所以这里还是一秒钟只能访问一个请求。
ipAddressKeyResolver:
@Component
public class ipAddressKeyResolver implements KeyResolver {
@Override
public Mono
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
现象如下:
可以看到如果不允许访问的时候会返回一个429的状态码。HTTP 429 - Too Many Requests
实现的功能是路由的时候打印一下日志:
/**
* project name : cloud-demo
* Date:2020/10/4
* Author: yc.guo
* DESC:
*/
@Component
public class LogsGatewayFilterFactory extends AbstractGatewayFilterFactory
Logger logger= LoggerFactory.getLogger(LogsGatewayFilterFactory.class);
@Override
public List
return Arrays.asList("name");
}
public LogsGatewayFilterFactory() {
super(LogsGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) ->{
logger.info("pre: 执行前的日志!" + config.name);
chain.filter(exchange); //继续走下去的方法
logger.info("post: 执行后的日志" + config.name);
return Mono.empty();
};
}
public static class Config {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
yml
- id: logs_filter
predicates:
- Path=/logs/orders
filters:
- StripPrefix=1
uri: http://127.0.0.1:8071
这个注意的点和上面的自定义断言的一样的。要按照上面自定义断言的注意点去做。
全局过滤器常用到的有:
? spirng cloud集成的负载均衡器有ribbon,所以gateway应该是可以无缝对接ribbon的。ribbon可以从yml配置文件上得到负载均衡的地址,也可以读取eureka上面得到负载均衡的地址,
pom.xml
application.yml:
#结合ribbon的负载均衡
- id: lb_fiter
predicates:
- Path=/lb/**
filters:
- StripPrefix=1
uri: lb://service1
service1:
ribbon:
listOfServers: 127.0.0.1:8071
提醒:uri是lb://serviceId ,负债均衡需要lb://开头才能识别得鸟
官网上建议使用ReactiveLoadBalancerClientFilter,只需要这样spring.cloud.loadbalancer.ribbon.enabledto
false`就行了
注意点:
? 如果通过文件配置的方式实现负载均衡,这个不能注册上eureka。一注册上了服务地址就会读取eureka上面了。不会读取本地配置文件了。之前我实现ribbon成功了,然后去实验去读取eureka的时候,然后回过头去测试ribbon就不行了
pom.xml
@SpringBootApplication
@EnableEurekaClient //开启注册到eureka上
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class,args);
}
}
spring:
application:
name: gateway-demo
cloud:
gateway:
enabled: true
routes:
#结合eureka的负载均衡
- id: discovery_filter
predicates:
- Path=/eureka/lb/**
filters:
- StripPrefix=2
uri: lb://eureka-client
#设置发现读取远端地址为true
discovery:
locator:
enabled: true
这里也是需要lb://开头的
默认情况下,当一个服务实例在LoadBalancer中没有找到时,将返回503。你可以通过配spring.cloud.gateway.loadbalancer.use404=true来让它返回404。
? 如果使用正常的配置方式,我们都是通过配置application.yml,来配置路由的,但是这样不太好的就是如果我们需要改变路由信息的话,就得需要重新修改application.yml并且重新启动项目才能让路由生效(我现在的公司用的zuul,也是这样,这样做的话缺点很明显的,因为你修改一次路由就得重启一次项目)。
? 按照正常的设定我的想法是:spring cloud通过读取配置文件的路由信息,创建了一些路由对象放到内存中,然后当请求网关时,再通过这些路由信息进行一个处理。当项目启动的时候,那我们也可以直接修改他内存里的路由信息,不就可以完成动态路由了吗?
? spring cloud真的提供了一个接口出来给我们做路由信息的管理,接口名字就是:RouteDefinitionRepository,而默认的实现类就只有一个:InMemoryRouteDefinitionRepository(利用内存管理路由信息)。下面这个图是RouteDefinitionRepository的结构图:
? 动态路由实际上运行的流程:
RouteDefinitionRepository
的路由信息,加载到内存中使用redis来存储路由信息的类:(通过仿照InMemoryRouteDefinitionRepository来实现的)
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private final static String GATEWAY_ROUTE_KEY="gateway_dynamic_route";
Logger logger= LoggerFactory.getLogger(RedisRouteDefinitionRepository.class);
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisTemplate
@Override
public Flux
List
redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route ->{
try {
list.add(objectMapper.readValue((String) route,RouteDefinition.class));
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new RuntimeException("解析失败");
}
});
return Flux.fromIterable(list);
}
@Override
public Mono
return route.flatMap(routeDefinition -> {
try {
System.out.println(objectMapper.writeValueAsString(route));
redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,
routeDefinition.getId(),
objectMapper.writeValueAsString(routeDefinition));
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new RuntimeException("保存失败");
}
return Mono.empty();
}
);
}
@Override
public Mono
return routeId.flatMap( id -> {
redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
return Mono.empty();
});
}
}
? 存储redis的数据结构,使用hash来存储。value我是直接使用json格式来存储路由信息,路由信息的json格式可以参考一下:actuator下面添加路由信息的body里面的结构。
存储redis的数据结构:
使用nacos来存储路由信息的类:(等我学习完nacos时补上)
提示:如果我们没有引入actuator的话,我们可以直接操作存储的媒介来达到目的,比如redis,nacos。
按照官网的说法,可以通过Restful Api来管理动态路由的信息,不过需要引入spring-boot-starter-actuator。
xml:
配置文件:
management.endpoint.gateway.enabled=true # default value
management.endpoints.web.exposure.include=gateway
查看所有的路由信息:
http://127.0.0.1:8080/actuator/gateway/routes
添加路由信息:
post请求,url: http://127.0.0.1:8080/actuator/gateway/routes/first_route
body
{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [{
"name" : "StripPrefix",
"args":{"parts":"1"}
}],
"uri": "https://www.bilibili.com",
"order": 0
}
删除一个路由信息(delete请求)
http://127.0.0.1:8080/actuator/gateway/routes/first_route
刷新加载RouteDefinitionRepository的路由信息到缓存中(post请求):
http://127.0.0.1:8080/actuator/gateway/refresh
一些小发现:查看路由信息:
? 这两个路由信息我是没有没有配的,但他却出现在路由信息上面,按照我的想法,应该是如果配置了读取eureka上的地址列表实现负载均衡的话,网关就会读取eureka上面的apps信息,并且把apps信息转换成相应的路由信息,保存到内存上去。
Spring Cloud Gateway入门demo