之前springcloud gateway项目是的路由配置都是静态配置在项目的application.yml文件中,不能实现路由的热更新。前期业务发展也比较缓慢,新增路由的场景频率不是很高,最近业务越来越广,新增项目频率明显升高,所以想着把路由配置提到apollo中,本想着很简单的一个事情,结果谁知坑一个比一个深,接下来讲一讲都有啥坑,又该如何填这些坑。
如何接入apollo
springcloud gateway支持通过properties
和yml
进行配置,下文简称gateway。
properties
示例
spring.cloud.gateway.routes[0].id = geektao
spring.cloud.gateway.routes[0].uri = http://www.geektao.ai
spring.cloud.gateway.routes[0].predicates[0] = Path=/geektao/**
spring.cloud.gateway.routes[0].filters[0] = StripPrefix=1
yml
示例
spring:
cloud:
gateway:
routes:
- id: geektao
uri: http://www.geektao.ai
predicates:
- Path=/geektao/**
filters:
- StripPrefix=1
使用properties
进行配置的话,为了遵循springboot项目的规范需要指定routes的索引,还得有序递增,太麻烦,这时候yml
格式就很香,所以建议使用yml
格式进行配置。
首先,创建namesapce
。
apollo默认的namespace
是application
,properties
格式的,不符合我们的使用场景,所以新建一个yml
格式的namespace
。
gateway项目引入apollo客户端依赖。
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>${apollo.version}</version>
</dependency>
在gateway项目resources
目录下新建META_INF
目录,创建app.properties
配置文件,这里有个坑,在apollo中,properties格式的namespace在引入时只需要用名称就可以,但是其它格式的namespace在引入时需要按照「名称.格式」的规则来,否则无法正常加载
。
# apollo
apollo.bootstrap.enabled=true
apollo.meta=http://xxxx
apollo.accesskey.secret=xxxx
apollo.bootstrap.namespaces=application,base-yml.yml
app.id=xxxx
开启apollo注册。这里也需要指定value的值,不指定默认只加载application。
@EnableApolloConfig(value = {"application","base-yml.yml"})
启动项目,访问被代理服务的地址,成功响应表示完成。
实现动态路由配置
完成上述步骤,路由配置还是静态的,我们修改完apollo后,除非再次从apollo获取配置文件,这时候才能获取到最新的值,但是无法同步更新运行中的springboot项目中的上下文配置数据,所以我们需要监听apollo中配置的变化,然后手动更新springboot和getaway的配置数据。
核心代码贴出来了,这里在配置@ApolloConfigChangeListener(value = ROUTE_NAMESPACE)
记着也得使用base-yml.yml
的namspace,不然无法正常监听。
@Configuration
@Slf4j
public class RuteDynamicRefresher implements ApplicationContextAware, ApplicationEventPublisherAware {
private static final String ROUTE_NAMESPACE = "base-yml.yml";
private ApplicationContext applicationContext;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
@ApolloConfigChangeListener(value = ROUTE_NAMESPACE)
public void routeChange(ConfigChangeEvent changeEvent) {
refreshGatewayProperties(changeEvent);
}
private void refreshGatewayProperties(ConfigChangeEvent changeEvent) {
try {
// 打印变更日志
log.info("[RuteDynamicRefresher] update {}", JSONObject.toJSONString(changeEvent));
// 更新 项目环境配置
EnvironmentChangeEvent environmentChangeEvent = new EnvironmentChangeEvent(changeEvent.changedKeys());
this.applicationContext.publishEvent(environmentChangeEvent);
log.info("[RuteDynamicRefresher] publish environmentChangeEvent {}", environmentChangeEvent);
// 更新路由配置
RefreshRoutesEvent refreshRoutesEvent = new RefreshRoutesEvent(this);
log.info("[RuteDynamicRefresher] publish refreshRoutesEvent {}", refreshRoutesEvent);
this.publisher.publishEvent(refreshRoutesEvent);
} catch (Exception e) {
log.error("[refreshGatewayProperties] 更新失败!", e);
}
}
}
这时候可以尝试新增、删除、修改路由的配置,就可以正常使用了。
还有坑,上边我们只是配置了必要的路由配置,实际生产使用过程中,不同的服务之间接口响应的时长阈值也是不一样的,耗时长的可能需要好几秒(当然这种建议改成异步或轮训的方式),短的可能几十毫秒,那就需要为每个路由单独设置超时时间。
gateway支持配置全局超时时间,所有路由都生效。
# 全局的响应超时时间,网络链接后,后端服务多久不返回网关就报错
#response-timeout: PT5S
# 全局的TCP连接超时时间,多长时间获取不到超时就报错
#connect-timeout: 5000
还支持配置单个路由超时时间,单位时间毫秒。
spring:
cloud:
gateway:
routes:
- id: geektao
uri: http://www.geektao.ai
predicates:
- Path=/geektao/**
filters:
- StripPrefix=1
metadata:
response-timeout: 2000
connect-timeout: 2000
但是在apollo中配置之后进行测试,发现未生效。走的还是全局的超时时间5秒。这里我直接说原因了,大家下来可以自己debug看一看。
问题就出在org.springframework.cloud.gateway.filter.NettyRoutingFilter
这个类的getResponseTimeout(Route route)
方法。
private Duration getResponseTimeout(Route route) {
Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
if (responseTimeoutAttr != null && responseTimeoutAttr instanceof Number) {
Long routeResponseTimeout = ((Number) responseTimeoutAttr).longValue();
if (routeResponseTimeout >= 0) {
return Duration.ofMillis(routeResponseTimeout);
}
else {
return null;
}
}
return properties.getResponseTimeout();
}
其实直接使用项目中的yml配置文件是没有问题的,因为yml配置文件中的value值是有类型的,Integer就是Integer,String就是String。所以在项目中配置的yml文件中读取到的response-timeout
值就是Number
类型,这个方法就可以正常返回值。
但是在apollo配置中,底层使用的是Properties
进行配置数据的存储,value都被转成了String
类型,那走到if (responseTimeoutAttr != null && responseTimeoutAttr instanceof Number)
时必然就是false
了,那就返回了全局的超时时间。
那既然知道了这里有问题,我搜了搜从apollo下手有些困难,所以我们从gateway下手,因为这个类不是jvm中原生类,那就意味着是通过应用类加载器加载的,那我们可以试着在自己项目中创建org.springframework.cloud.gateway.filter.NettyRoutingFilter
类,然后重写getResponseTimeout
方法,当然我这里直接暴力改了,默认这个配置只会配置为Long
类型,大家实际用时最好加上异常处理。
private Duration getResponseTimeout(Route route) {
Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
Long routeResponseTimeout;
if (responseTimeoutAttr != null) {
Long routeResponseTimeout = Long.parseLong((String)responseTimeoutAttr)
if (routeResponseTimeout >= 0) {
return Duration.ofMillis(routeResponseTimeout);
} else {
return null;
}
}
return properties.getResponseTimeout();
}
再次测试单路由的超时配置,问题解决。
关注公众号「极客涛」,同步更新文章。