持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情
哈喽,大家好,我是一条。
nacos 和 admin 搭建完之后,我们还需要一个网关作为整个系统的门户,所以的访问都必须经过网关才能到我们的具体服务。
所以就要求我们的网关具有,请求分发,拦截过滤的功能,同时还需要做到负载均衡。
那就需要去注册中心拿到各服务信息,不能将服务地址硬编码到代码里。
这些是比较简单的,难点在于如何动态的更新网关配置。
首先,nacos是支持配置动态刷新的,那我就只需要实现gateway收到nacos的配置更新事件后更新自己的路由配置,更新的方式选择全量刷新即可。
那整体就可以分为两块:
- 监听nacos
- 更新配置
同时,服务初次启动时应该全量拉取配置并新增到gateway。
按照这个思路,我们来实现其核心类。
定义配置文件的位置
首先路由配置我们是放在nacos的,那如何从众多配合文件中确定我们的路由配置呢,nacos给我们提供了data-id和group。
所以建好配置之后就需要在gateway中配置好路由配置文件的data-id 和 group ,有点绕嘴哈。
spring:
cloud:
nacos:
custom:
gateway:
router:
data-id: cloud-gateway-router
group: dev
还需要一个配置类来读取到javabean里。
@Configuration
public class GatewayConfig {
/** 读取配置的超时时间 */
public static final long DEFAULT_TIMEOUT = 30000;
/** Nacos 服务器地址 */
public static String NACOS_SERVER_ADDR;
/** 命名空间 */
public static String NACOS_NAMESPACE;
/** data-id */
public static String NACOS_ROUTE_DATA_ID;
/** 分组 id */
public static String NACOS_ROUTE_GROUP;
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNACOS_SERVER_ADDR(String NACOS_SERVER_ADDR) {
this.NACOS_SERVER_ADDR = NACOS_SERVER_ADDR;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
public void setNACOS_NAMESPACE(String NACOS_NAMESPACE) {
this.NACOS_NAMESPACE = NACOS_NAMESPACE;
}
@Value("${spring.cloud.nacos.custom.gateway.router.data-id}")
public void setNACOS_ROUTE_DATA_ID(String NACOS_ROUTE_DATA_ID) {
this.NACOS_ROUTE_DATA_ID = NACOS_ROUTE_DATA_ID;
}
@Value("${spring.cloud.nacos.custom.gateway.router.group}")
public void setNACOS_ROUTE_GROUP(String NACOS_ROUTE_GROUP) {
this.NACOS_ROUTE_GROUP = NACOS_ROUTE_GROUP;
}
}
添加监听
通过 server-addr、namespace、data-id、group,我们就可以连接到nacos,再读取其配置文件的json,初始化我们的gateway配置,再对nacos添加监听即可。
看代码:
@Component
@Slf4j
@DependsOn({"gatewayConfig"})
public class GatewayRouteNacosListener {
private ConfigService nacosConfig;
private GatewayRouteHandler updateRouteService;
// private GateWayConfig GateWayConfig;
public GatewayRouteNacosListener(GatewayRouteHandler updateRouteService) {
this.updateRouteService = updateRouteService;
}
/**
* <h2>Bean 在容器中构造完成之后会执行 init 方法</h2>
* */
@PostConstruct
public void init() {
log.info("gateway route init....");
try {
// 初始化 Nacos 配置客户端
initNacosConfig();
if (null == nacosConfig) {
log.error("init config service fail");
return;
}
// 通过 Nacos Config 并指定路由配置路径去获取路由配置
String configInfo = nacosConfig.getConfig(
GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP,
GatewayConfig.DEFAULT_TIMEOUT
);
log.info("get current gateway config: [{}]", configInfo);
List<RouteDefinition> definitionList =
JSON.parseArray(configInfo, RouteDefinition.class);
if (CollectionUtils.isNotEmpty(definitionList)) {
updateRouteService.BatchAddRoute(definitionList);
}
} catch (Exception ex) {
log.error("gateway route init has some error: [{}]", ex.getMessage(), ex);
}
// 设置监听器
addNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP);
}
public void initNacosConfig(){
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
this.nacosConfig = NacosFactory.createConfigService(properties);
} catch (Exception ex) {
log.error("init gateway nacos config error: [{}]", ex.getMessage(), ex);
}
}
/**
* <h2>监听 Nacos 下发的动态路由配置</h2>
* */
private void addNacosListener(String dataId, String group) {
try {
// 给 Nacos Config 客户端增加一个监听器
nacosConfig.addListener(dataId, group, new Listener() {
/**
* 自己提供线程池执行操作
* */
@Override
public Executor getExecutor() {
return Executors.newFixedThreadPool(5);
}
/**
* <h2>监听器收到配置更新</h2>
* @param configInfo Nacos 中最新的配置定义
* */
@Override
public void receiveConfigInfo(String configInfo) {
log.info("start to update config: [{}]", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("transfer to route definition: [{}]", definitionList.toString());
updateRouteService.fullUpdateRoute(definitionList);
}
});
} catch (NacosException ex) {
log.error("dynamic update gateway config error: [{}]", ex.getMessage(), ex);
}
}
}
这里要特别注意一下,@DependsOn({"gatewayConfig"})的配置里面配置类名的首字母小写之后的名字。即容器中的名字。
gateway更新配置
接下来就要实现updateRouteService.fullUpdateRoute(definitionList);这个方法。
需要两个工具来实现
// 对路由的写操作 - 新增、删除
private final RouteDefinitionWriter writer;
// 对路由的读操作 - 查询
private final RouteDefinitionLocator locator;
直接看代码,有注释:
/**
* 注册监听器,监听 nacos 的路由变化。
*/
@Service
@Slf4j
public class GatewayRouteHandler implements ApplicationEventPublisherAware {
// 对路由的写操作 - 新增、删除
private final RouteDefinitionWriter writer;
// 对路由的读操作 - 查询
private final RouteDefinitionLocator locator;
public GatewayRouteHandler(RouteDefinitionWriter writer, RouteDefinitionLocator locator) {
this.writer = writer;
this.locator = locator;
}
private ApplicationEventPublisher eventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}
/**
* <h2>增加路由定义</h2>
*/
public void addRoute(RouteDefinition definition) {
log.info("gateway add route: [{}]", definition);
// 保存路由配置并发布
writer.save(Mono.just(definition)).subscribe();
// 发布事件通知给 Gateway, 同步新增的路由定义
this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
public void BatchAddRoute(List<RouteDefinition> definitions) {
definitions.forEach(this::addRoute);
}
/**
* <h2>全量更新路由</h2>
*/
public void fullUpdateRoute(List<RouteDefinition> definitions) {
log.info("gateway full update route: [{}]", definitions);
// 先拿到当前 Gateway 中存储的路由定义
List<RouteDefinition> routeDefinitionsExits = locator.getRouteDefinitions().buffer().blockFirst();
if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
// 清除掉之前所有的 "旧的" 路由定义
routeDefinitionsExits.forEach(rd -> {
log.info("delete route definition: [{}]", rd);
deleteRouteById(rd.getId());
});
}
// 把更新的路由定义同步到 gateway 中
definitions.forEach(this::addRoute);
}
/**
* <h2>根据路由 id 删除路由配置</h2>
*/
private void deleteRouteById(String id) {
try {
writer.delete(Mono.just(id)).subscribe();
// 发布事件通知给 gateway 更新路由定义
this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("gateway delete route success - id: [{}]", id);
} catch (Exception ex) {
log.error("gateway delete route fail: [{}]", ex.getMessage(), ex);
}
}
/**
* <h2>单个更新路由</h2>
*/
private void updateRouteById(RouteDefinition definition) {
try {
deleteRouteById(definition.getId());
addRoute(definition);
} catch (Exception ex) {
log.error("gateway update route fail: [{}]", ex.getMessage(), ex);
}
}
private void batchUpdateRoute(List<RouteDefinition> definitions) {
definitions.forEach(this::updateRouteById);
}
}
最后
这样我们就更新路由配置文件,gateway就会受到更新事件,从而触发更新。
这个企业级的实现方式一定要学会。
下期见!