parent
55e5a7e795
commit
219f944162
@ -0,0 +1,29 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class NacosConfigs { |
||||
|
||||
public static final String GROUP_CHANNEL = "sop:channel"; |
||||
|
||||
public static final String GROUP_ROUTE = "sop:route"; |
||||
|
||||
public static final String DATA_ID_GRAY = "com.gitee.sop.channel.gray"; |
||||
|
||||
public static final String DATA_ID_IP_BLACKLIST = "com.gitee.sop.channel.ipblacklist"; |
||||
|
||||
public static final String DATA_ID_ISV = "com.gitee.sop.channel.isv"; |
||||
|
||||
public static final String DATA_ID_ROUTE_PERMISSION = "com.gitee.sop.channel.routepermission"; |
||||
|
||||
public static final String DATA_ID_LIMIT_CONFIG = "com.gitee.sop.channel.limitconfig"; |
||||
|
||||
public static final String DATA_ID_ROUTE_CONFIG = "com.gitee.sop.channel.routeconfig"; |
||||
|
||||
private static final String DATA_ID_TPL = "com.gitee.sop.route.%s"; |
||||
|
||||
public static String getRouteDataId(String serviceId) { |
||||
return String.format(DATA_ID_TPL, serviceId); |
||||
} |
||||
} |
@ -1,344 +0,0 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
import com.gitee.sop.adminserver.common.ZookeeperOperationException; |
||||
import com.gitee.sop.adminserver.common.ZookeeperPathExistException; |
||||
import com.gitee.sop.adminserver.common.ZookeeperPathNotExistException; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.io.IOUtils; |
||||
import org.apache.commons.lang.StringUtils; |
||||
import org.apache.commons.lang.math.NumberUtils; |
||||
import org.apache.curator.framework.CuratorFramework; |
||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||
import org.apache.curator.framework.recipes.cache.ChildData; |
||||
import org.apache.curator.framework.recipes.cache.NodeCache; |
||||
import org.apache.curator.framework.recipes.cache.NodeCacheListener; |
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCache; |
||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||
import org.apache.zookeeper.CreateMode; |
||||
import org.apache.zookeeper.data.Stat; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.io.Closeable; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
import static com.gitee.sop.adminserver.bean.SopAdminConstants.SOP_MSG_CHANNEL_PATH; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ZookeeperContext { |
||||
|
||||
private static CuratorFramework client; |
||||
|
||||
private static Environment environment; |
||||
|
||||
public static void setEnvironment(Environment environment) { |
||||
Assert.notNull(environment, "environment不能为null"); |
||||
ZookeeperContext.environment = environment; |
||||
initZookeeperClient(); |
||||
} |
||||
|
||||
public synchronized static void initZookeeperClient() { |
||||
if (client != null) { |
||||
return; |
||||
} |
||||
setClient(createClient()); |
||||
} |
||||
|
||||
public static CuratorFramework createClient() { |
||||
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string"); |
||||
if (StringUtils.isBlank(zookeeperServerAddr)) { |
||||
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数"); |
||||
} |
||||
String baseSleepTimeMs = environment.getProperty("spring.cloud.zookeeper.baseSleepTimeMs"); |
||||
String maxRetries = environment.getProperty("spring.cloud.zookeeper.maxRetries"); |
||||
log.info("初始化zookeeper客户端,zookeeperServerAddr:{}, baseSleepTimeMs:{}, maxRetries:{}", |
||||
zookeeperServerAddr, baseSleepTimeMs, maxRetries); |
||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(NumberUtils.toInt(baseSleepTimeMs, 3000), NumberUtils.toInt(maxRetries, 3))) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
return client; |
||||
} |
||||
|
||||
public static String getSopRouteRootPath() { |
||||
return SopAdminConstants.SOP_SERVICE_ROUTE_PATH; |
||||
} |
||||
|
||||
public static String buildServiceIdPath(String serviceId) { |
||||
if (StringUtils.isBlank(serviceId)) { |
||||
throw new NullPointerException("serviceId不能为空"); |
||||
} |
||||
return getSopRouteRootPath() + "/" + serviceId; |
||||
} |
||||
|
||||
public static String buildRoutePath(String serviceId, String routeId) { |
||||
if (StringUtils.isBlank(serviceId)) { |
||||
throw new NullPointerException("serviceId不能为空"); |
||||
} |
||||
if (StringUtils.isBlank(routeId)) { |
||||
throw new NullPointerException("routeId不能为空"); |
||||
} |
||||
String serviceIdPath = getSopRouteRootPath() + "/" + serviceId; |
||||
return serviceIdPath + "/" + routeId; |
||||
} |
||||
|
||||
public static String getServiceGrayChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/gray"; |
||||
} |
||||
|
||||
public static String getIsvInfoChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/isvinfo"; |
||||
} |
||||
|
||||
public static String getIsvRoutePermissionChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/isv-route-permission"; |
||||
} |
||||
|
||||
public static String getRouteConfigChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/route-conf"; |
||||
} |
||||
|
||||
public static String getLimitConfigChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/limit-conf"; |
||||
} |
||||
|
||||
public static String getIpBlacklistChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/ipblacklist-conf"; |
||||
} |
||||
|
||||
public static CuratorFramework getClient() { |
||||
return client; |
||||
} |
||||
|
||||
public static void setClient(CuratorFramework client) { |
||||
ZookeeperContext.client = client; |
||||
} |
||||
|
||||
public static boolean isPathExist(String path) { |
||||
try { |
||||
return client.checkExists().forPath(path) != null; |
||||
} catch (Exception e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 对已存在的path赋值。如果path不存在抛异常 |
||||
* |
||||
* @param path 已存在的 |
||||
* @param data 数据 |
||||
* @return |
||||
* @throws ZookeeperPathNotExistException |
||||
*/ |
||||
public static Stat updatePathData(String path, String data) throws ZookeeperPathNotExistException { |
||||
if (!isPathExist(path)) { |
||||
throw new ZookeeperPathNotExistException("path " + path + " 不存在"); |
||||
} |
||||
try { |
||||
return getClient().setData().forPath(path, data.getBytes()); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("updatePathData error, path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 创建新的path,并赋值。如果path已存在抛异常 |
||||
* |
||||
* @param path 待创建的path |
||||
* @param data 值 |
||||
* @throws ZookeeperPathExistException |
||||
*/ |
||||
public static String addPath(String path, String data) throws ZookeeperPathExistException { |
||||
if (isPathExist(path)) { |
||||
throw new ZookeeperPathExistException("path " + path + " 已存在"); |
||||
} |
||||
try { |
||||
return addPath(path, CreateMode.PERSISTENT, data); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("addPath error path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 添加节点 |
||||
* |
||||
* @param path 待创建的path |
||||
* @param createMode 节点类型 |
||||
* @param data 节点数据 |
||||
* @return |
||||
*/ |
||||
public static String addPath(String path, CreateMode createMode, String data) { |
||||
try { |
||||
return getClient().create() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.withMode(createMode) |
||||
.forPath(path, data.getBytes()); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("addPath error path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 删除节点及子节点 |
||||
* |
||||
* @param path |
||||
*/ |
||||
public static void deletePathDeep(String path) { |
||||
try { |
||||
getClient().delete() |
||||
.deletingChildrenIfNeeded() |
||||
.forPath(path); |
||||
} catch (Exception e) { |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 创建新的path,并赋值。如果path已存在则不创建 |
||||
* |
||||
* @param path 待创建的path |
||||
* @param data 值 |
||||
*/ |
||||
public static String addPathQuietly(String path, String data) { |
||||
if (isPathExist(path)) { |
||||
return path; |
||||
} |
||||
try { |
||||
return addPath(path, data); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("addPathQuietly error path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 新建或保存节点 |
||||
* |
||||
* @param path |
||||
* @param data |
||||
* @return |
||||
*/ |
||||
public static String createOrUpdateData(String path, String data) { |
||||
try { |
||||
return getClient().create() |
||||
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
|
||||
.orSetData() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.forPath(path, data.getBytes()); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("createOrUpdateData error path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取节点内容 |
||||
* |
||||
* @param path |
||||
* @return |
||||
* @throws ZookeeperPathNotExistException |
||||
*/ |
||||
public static String getData(String path) throws ZookeeperPathNotExistException { |
||||
if (!isPathExist(path)) { |
||||
throw new ZookeeperPathNotExistException("path 不存在, path=" + path); |
||||
} |
||||
try { |
||||
byte[] data = getClient().getData().forPath(path); |
||||
return new String(data); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("getData error path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取子节点数据 |
||||
* |
||||
* @param parentPath 父节点 |
||||
* @return |
||||
*/ |
||||
public static List<ChildData> getChildrenData(String parentPath) { |
||||
PathChildrenCache pathChildrenCache = buildPathChildrenCache(parentPath); |
||||
if (pathChildrenCache == null) { |
||||
return Collections.emptyList(); |
||||
} |
||||
return pathChildrenCache.getCurrentData(); |
||||
} |
||||
|
||||
public static PathChildrenCache buildPathChildrenCache(String path) { |
||||
if (!isPathExist(path)) { |
||||
return null; |
||||
} |
||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||
// 且第三个参数要设置为true,不然ChildData对象中的getData返回null
|
||||
PathChildrenCache childrenCache = new PathChildrenCache(client, path, true); |
||||
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
|
||||
try { |
||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("buildPathChildrenCache error path=" + path, e); |
||||
} |
||||
return childrenCache; |
||||
} |
||||
|
||||
/** |
||||
* 监听一个临时节点 |
||||
* |
||||
* @param path |
||||
* @param listenCallback 回调 |
||||
* @return 返回path |
||||
* @throws Exception |
||||
*/ |
||||
public static void listenTempPath(String path, ListenCallback listenCallback) throws Exception { |
||||
String initData = "{}"; |
||||
CuratorFramework client = createClient(); |
||||
client.create() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.withMode(CreateMode.EPHEMERAL) |
||||
.forPath(path, initData.getBytes()); |
||||
|
||||
final NodeCache cache = new NodeCache(client, path, false); |
||||
cache.getListenable().addListener(new NodeCacheListener() { |
||||
@Override |
||||
public void nodeChanged() throws Exception { |
||||
byte[] nodeData = cache.getCurrentData().getData(); |
||||
String data = new String(nodeData); |
||||
if (StringUtils.isNotBlank(data) && !initData.equals(data)) { |
||||
listenCallback.onError(data); |
||||
Executors.newSingleThreadExecutor().execute(() -> new ZKClose(cache, client)); |
||||
} |
||||
} |
||||
}); |
||||
cache.start(); |
||||
} |
||||
|
||||
public interface ListenCallback { |
||||
void onError(String errorMsg); |
||||
} |
||||
|
||||
static class ZKClose implements Runnable { |
||||
Closeable[] closes; |
||||
|
||||
public ZKClose(Closeable ...closes) { |
||||
this.closes = closes; |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
try { |
||||
Thread.sleep(2000); |
||||
IOUtils.closeQuietly(closes); |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,26 +0,0 @@ |
||||
package com.gitee.sop.adminserver.common; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZookeeperOperationException extends RuntimeException { |
||||
|
||||
public ZookeeperOperationException() { |
||||
} |
||||
|
||||
public ZookeeperOperationException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public ZookeeperOperationException(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
public ZookeeperOperationException(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
public ZookeeperOperationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { |
||||
super(message, cause, enableSuppression, writableStackTrace); |
||||
} |
||||
} |
@ -1,10 +0,0 @@ |
||||
package com.gitee.sop.adminserver.common; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZookeeperPathExistException extends Exception { |
||||
public ZookeeperPathExistException(String message) { |
||||
super(message); |
||||
} |
||||
} |
@ -1,10 +0,0 @@ |
||||
package com.gitee.sop.adminserver.common; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZookeeperPathNotExistException extends Exception { |
||||
public ZookeeperPathNotExistException(String message) { |
||||
super(message); |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
package com.gitee.sop.adminserver.service; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.nacos.api.annotation.NacosInjected; |
||||
import com.alibaba.nacos.api.config.ConfigService; |
||||
import com.alibaba.nacos.api.exception.NacosException; |
||||
import com.gitee.sop.adminserver.bean.ChannelMsg; |
||||
import com.gitee.sop.adminserver.common.BizException; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
public class ConfigPushService { |
||||
|
||||
@NacosInjected |
||||
private ConfigService configService; |
||||
|
||||
public void publishConfig(String dataId, String groupId, ChannelMsg channelMsg) { |
||||
try { |
||||
log.info("nacos配置, dataId:{}, groupId:{}, operation:{}", dataId, groupId, channelMsg.getOperation()); |
||||
configService.publishConfig(dataId, groupId, JSON.toJSONString(channelMsg)); |
||||
} catch (NacosException e) { |
||||
log.error("nacos配置失败, dataId:{}, groupId:{}, operation:{}", dataId, groupId, channelMsg.getOperation(), e); |
||||
throw new BizException("nacos配置失败"); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,45 +0,0 @@ |
||||
package com.gitee.sop.adminserver; |
||||
|
||||
import com.gitee.sop.adminserver.bean.SopAdminConstants; |
||||
import junit.framework.TestCase; |
||||
import org.apache.curator.framework.CuratorFramework; |
||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||
import org.apache.zookeeper.data.Stat; |
||||
|
||||
public class CuratorTest extends TestCase { |
||||
|
||||
private static String zookeeperServerAddr = "localhost:2181"; |
||||
|
||||
static CuratorFramework client; |
||||
|
||||
public CuratorTest() { |
||||
client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
} |
||||
|
||||
/** |
||||
* 递归删除节点,只能在测试环境用。 |
||||
* |
||||
* @throws Exception |
||||
*/ |
||||
public void testDel() { |
||||
try { |
||||
client.delete() |
||||
.deletingChildrenIfNeeded() |
||||
.forPath(SopAdminConstants.RELOAD_ROUTE_PERMISSION_PATH); |
||||
} catch (Exception e) { |
||||
} |
||||
} |
||||
|
||||
public void testCheck() throws Exception { |
||||
String path = SopAdminConstants.RELOAD_ROUTE_PERMISSION_PATH + "/1562231019332"; |
||||
Stat stat = client.checkExists().forPath(path); |
||||
System.out.println(path + (stat == null ? "不存在" : "存在")); |
||||
} |
||||
|
||||
} |
@ -1,4 +1,4 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.route; |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import lombok.Data; |
||||
|
@ -0,0 +1,42 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import lombok.Data; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import javax.validation.ValidationException; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class GatewayPredicateDefinition { |
||||
public static final String GEN_KEY = "_genkey_"; |
||||
/** 断言对应的Name */ |
||||
private String name; |
||||
/** 配置的断言规则 */ |
||||
private Map<String, String> args = new LinkedHashMap<>(); |
||||
|
||||
public GatewayPredicateDefinition() { |
||||
} |
||||
|
||||
public GatewayPredicateDefinition(String text) { |
||||
int eqIdx = text.indexOf(61); |
||||
if (eqIdx <= 0) { |
||||
throw new ValidationException("Unable to parse GatewayPredicateDefinition text '" + text + "', must be of the form name=value"); |
||||
} else { |
||||
this.setName(text.substring(0, eqIdx)); |
||||
String[] params = StringUtils.tokenizeToStringArray(text.substring(eqIdx + 1), ","); |
||||
|
||||
for(int i = 0; i < params.length; ++i) { |
||||
this.args.put(generateName(i), params[i]); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
public static String generateName(int i) { |
||||
return GEN_KEY + i; |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class GatewayRouteDefinition { |
||||
/** |
||||
* 路由的Id(接口名+版本号),确保此id全局唯一 |
||||
*/ |
||||
private String id; |
||||
|
||||
/** |
||||
* 接口名 |
||||
*/ |
||||
private String name; |
||||
|
||||
/** |
||||
* 版本号 |
||||
*/ |
||||
private String version; |
||||
|
||||
/** |
||||
* 路由断言集合配置 |
||||
*/ |
||||
private List<GatewayPredicateDefinition> predicates = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 路由过滤器集合配置 |
||||
*/ |
||||
private List<GatewayFilterDefinition> filters = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 路由规则转发的目标uri |
||||
*/ |
||||
private String uri; |
||||
|
||||
/** |
||||
* uri后面跟的path |
||||
*/ |
||||
private String path; |
||||
|
||||
/** |
||||
* 路由执行的顺序 |
||||
*/ |
||||
private int order = 0; |
||||
|
||||
/** |
||||
* 是否忽略验证,业务参数验证除外 |
||||
*/ |
||||
private int ignoreValidate; |
||||
|
||||
/** |
||||
* 状态,0:待审核,1:启用,2:禁用 |
||||
*/ |
||||
private int status = 1; |
||||
|
||||
/** |
||||
* 是否合并结果 |
||||
*/ |
||||
private int mergeResult; |
||||
|
||||
/** |
||||
* 是否需要授权才能访问 |
||||
*/ |
||||
private int permission; |
||||
} |
@ -0,0 +1,29 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class NacosConfigs { |
||||
|
||||
public static final String GROUP_CHANNEL = "sop:channel"; |
||||
|
||||
public static final String GROUP_ROUTE = "sop:route"; |
||||
|
||||
public static final String DATA_ID_GRAY = "com.gitee.sop.channel.gray"; |
||||
|
||||
public static final String DATA_ID_IP_BLACKLIST = "com.gitee.sop.channel.ipblacklist"; |
||||
|
||||
public static final String DATA_ID_ISV = "com.gitee.sop.channel.isv"; |
||||
|
||||
public static final String DATA_ID_ROUTE_PERMISSION = "com.gitee.sop.channel.routepermission"; |
||||
|
||||
public static final String DATA_ID_LIMIT_CONFIG = "com.gitee.sop.channel.limitconfig"; |
||||
|
||||
public static final String DATA_ID_ROUTE_CONFIG = "com.gitee.sop.channel.routeconfig"; |
||||
|
||||
private static final String DATA_ID_TPL = "com.gitee.sop.route.%s"; |
||||
|
||||
public static String getRouteDataId(String serviceId) { |
||||
return String.format(DATA_ID_TPL, serviceId); |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField; |
||||
import lombok.Data; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class ServiceRouteInfo { |
||||
|
||||
/** |
||||
* 服务名称,对应spring.application.name |
||||
*/ |
||||
private String serviceId; |
||||
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss") |
||||
private Date createTime = new Date(); |
||||
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss") |
||||
private Date updateTime = new Date(); |
||||
|
||||
private String description; |
||||
|
||||
/** |
||||
* 路由信息md5,md5(sort(routeIdList)) |
||||
*/ |
||||
private String md5; |
||||
|
||||
private List<GatewayRouteDefinition> routeDefinitionList; |
||||
|
||||
public String fetchServiceIdLowerCase() { |
||||
return serviceId.toLowerCase(); |
||||
} |
||||
} |
@ -1,17 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.route; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class GatewayPredicateDefinition { |
||||
/** 断言对应的Name */ |
||||
private String name; |
||||
/** 配置的断言规则 */ |
||||
private Map<String, String> args = new LinkedHashMap<>(); |
||||
} |
@ -1,21 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition; |
||||
import lombok.Data; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Getter |
||||
@Setter |
||||
public class GatewayRouteDefinition extends BaseRouteDefinition { |
||||
/** 路由断言集合配置 */ |
||||
private List<GatewayPredicateDefinition> predicates = new ArrayList<>(); |
||||
/** 路由过滤器集合配置 */ |
||||
private List<GatewayFilterDefinition> filters = new ArrayList<>(); |
||||
} |
@ -1,9 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class GatewayServiceRouteInfo extends BaseServiceRouteInfo<GatewayRouteDefinition> { |
||||
} |
@ -1,13 +1,16 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.AbstractTargetRoute; |
||||
import com.gitee.sop.gatewaycommon.bean.GatewayRouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import org.springframework.cloud.gateway.route.RouteDefinition; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class GatewayTargetRoute extends AbstractTargetRoute<GatewayServiceRouteInfo, GatewayRouteDefinition, RouteDefinition> { |
||||
public GatewayTargetRoute(GatewayServiceRouteInfo baseServiceRouteInfo, GatewayRouteDefinition baseRouteDefinition, RouteDefinition targetRoute) { |
||||
super(baseServiceRouteInfo, baseRouteDefinition, targetRoute); |
||||
public class GatewayTargetRoute extends AbstractTargetRoute<RouteDefinition> { |
||||
|
||||
public GatewayTargetRoute(ServiceRouteInfo serviceRouteInfo, GatewayRouteDefinition routeDefinition, RouteDefinition targetRoute) { |
||||
super(serviceRouteInfo, routeDefinition, targetRoute); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,62 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.GatewayRouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public abstract class BaseRouteCache<T extends TargetRoute> implements RouteLoader { |
||||
|
||||
/** |
||||
* KEY:serviceId, value: md5 |
||||
*/ |
||||
private Map<String, String> serviceIdMd5Map = new HashMap<>(); |
||||
|
||||
private RouteRepository<T> routeRepository; |
||||
|
||||
/** |
||||
* 构建目标路由对象,zuul和gateway定义的路由对象 |
||||
* |
||||
* @param serviceRouteInfo 路由服务对象 |
||||
* @param gatewayRouteDefinition 路由对象 |
||||
* @return 返回目标路由对象 |
||||
*/ |
||||
protected abstract T buildRouteDefinition(ServiceRouteInfo serviceRouteInfo, GatewayRouteDefinition gatewayRouteDefinition); |
||||
|
||||
public BaseRouteCache(RouteRepository<T> routeRepository) { |
||||
this.routeRepository = routeRepository; |
||||
} |
||||
|
||||
@Override |
||||
public void load(ServiceRouteInfo serviceRouteInfo) { |
||||
try { |
||||
String newMd5 = serviceRouteInfo.getMd5(); |
||||
String md5 = serviceIdMd5Map.putIfAbsent(serviceRouteInfo.getServiceId(), newMd5); |
||||
if (Objects.equals(newMd5, md5)) { |
||||
return; |
||||
} |
||||
List<GatewayRouteDefinition> routeDefinitionList = serviceRouteInfo.getRouteDefinitionList(); |
||||
for (GatewayRouteDefinition gatewayRouteDefinition : routeDefinitionList) { |
||||
T routeDefinition = this.buildRouteDefinition(serviceRouteInfo, gatewayRouteDefinition); |
||||
routeRepository.add(routeDefinition); |
||||
} |
||||
} catch (Exception e) { |
||||
log.error("加载路由信息失败,serviceRouteInfo:{}", serviceRouteInfo, e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void remove(String serviceId) { |
||||
serviceIdMd5Map.remove(serviceId); |
||||
routeRepository.deleteAll(serviceId); |
||||
} |
||||
} |
@ -1,220 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.curator.framework.recipes.cache.ChildData; |
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; |
||||
import org.springframework.core.env.Environment; |
||||
|
||||
|
||||
/** |
||||
* 路由管理,采用zookeeper实现,监听路由的增删改,并适时更新到本地。路由的存储格式为: |
||||
* <pre> |
||||
* /com.gitee.sop.route 根节点 |
||||
* /serviceId 服务节点,名字为服务名 |
||||
* /route1 路由节点,名字为:name+version,存放路由信息 |
||||
* /route2 |
||||
* /... |
||||
* </pre> |
||||
* |
||||
* @param <R> 路由根对象,可以理解为最外面的大json:{....,routeDefinitionList:[]} |
||||
* @param <E> 路由Item对象,对应大json里面的具体路由信息,routeDefinitionList:[] |
||||
* @param <T> 目标路由对象 |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public abstract class BaseRouteManager<R extends BaseServiceRouteInfo<E>, E extends BaseRouteDefinition, T extends TargetRoute> implements RouteManager { |
||||
|
||||
protected Environment environment; |
||||
|
||||
protected RouteRepository<T> routeRepository; |
||||
|
||||
protected String routeRootPath; |
||||
|
||||
/** |
||||
* 返回路由根对象class |
||||
* |
||||
* @return 返回R.class |
||||
*/ |
||||
protected abstract Class<R> getServiceRouteInfoClass(); |
||||
|
||||
/** |
||||
* 返回路由Item对象class |
||||
* |
||||
* @return 返回E.class |
||||
*/ |
||||
protected abstract Class<E> getRouteDefinitionClass(); |
||||
|
||||
/** |
||||
* 构建目标路由对象,zuul和gateway定义的路由对象 |
||||
* |
||||
* @param serviceRouteInfo |
||||
* @param routeDefinition |
||||
* @return 返回目标路由对象 |
||||
*/ |
||||
protected abstract T buildRouteDefinition(R serviceRouteInfo, E routeDefinition); |
||||
|
||||
public BaseRouteManager(Environment environment, RouteRepository<T> routeRepository) { |
||||
this.environment = environment; |
||||
ZookeeperContext.setEnvironment(environment); |
||||
this.routeRepository = routeRepository; |
||||
this.routeRootPath = ZookeeperContext.getRouteRootPath(); |
||||
this.createRouteRootPath(this.routeRootPath); |
||||
} |
||||
|
||||
@Override |
||||
public void refresh() { |
||||
this.refreshRouteInfo(); |
||||
} |
||||
|
||||
protected void refreshRouteInfo() { |
||||
try { |
||||
this.watchServiceChange(routeRootPath); |
||||
} catch (Exception e) { |
||||
log.error("刷新路由配置失败", e); |
||||
throw new IllegalStateException("刷新路由配置失败"); |
||||
} |
||||
} |
||||
|
||||
protected void createRouteRootPath(String routeRootPath) { |
||||
try { |
||||
ZookeeperContext.createPath(routeRootPath, ""); |
||||
} catch (Exception e) { |
||||
log.error("创建路由根节点失败", e); |
||||
throw new IllegalStateException("创建路由根节点失败"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 监听微服务更改 |
||||
* |
||||
* @param rootPath |
||||
* @throws Exception |
||||
*/ |
||||
protected void watchServiceChange(String rootPath) throws Exception { |
||||
ZookeeperContext.getChildrenAndListen(rootPath, childDataList -> { |
||||
log.info("========== 加载路由信息 =========="); |
||||
log.info("{} # 路由根节点", rootPath); |
||||
for (ChildData childData : childDataList) { |
||||
String serviceNodeData = new String(childData.getData()); |
||||
R serviceRouteInfo = JSON.parseObject(serviceNodeData, getServiceRouteInfoClass()); |
||||
String servicePath = childData.getPath(); |
||||
log.info("\t{} # service节点,节点数据:{}", servicePath, serviceNodeData); |
||||
try { |
||||
this.loadServiceRouteItem(serviceRouteInfo, servicePath); |
||||
} catch (Exception e) { |
||||
log.error("加载路由信息失败,servicePath:{}", servicePath, e); |
||||
} |
||||
} |
||||
}, (client, event) -> { |
||||
PathChildrenCacheEvent.Type type = event.getType(); |
||||
synchronized (type) { |
||||
// 通过判断event type的方式来实现不同事件的触发
|
||||
if (PathChildrenCacheEvent.Type.CHILD_ADDED.equals(type)) { |
||||
String serviceNodeData = new String(event.getData().getData()); |
||||
R serviceRouteInfo = JSON.parseObject(serviceNodeData, getServiceRouteInfoClass()); |
||||
// 添加子节点时触发
|
||||
String servicePath = event.getData().getPath(); |
||||
log.info("新增serviceId节点:{},节点数据:{}", servicePath, serviceNodeData); |
||||
this.watchRouteItems(serviceRouteInfo, servicePath); |
||||
} else if (PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(type)) { |
||||
// 修改子节点数据时触发,暂时没有什么操作
|
||||
String nodeData = new String(event.getData().getData()); |
||||
log.info("修改serviceId节点:{},节点数据:{}", event.getData().getPath(), nodeData); |
||||
} else if (PathChildrenCacheEvent.Type.CHILD_REMOVED.equals(type)) { |
||||
// 删除service节点
|
||||
String nodeData = new String(event.getData().getData()); |
||||
log.info("删除serviceId节点:{},节点数据:{}", event.getData().getPath(), nodeData); |
||||
R serviceRouteInfo = JSON.parseObject(nodeData, getServiceRouteInfoClass()); |
||||
routeRepository.deleteAll(serviceRouteInfo.getServiceId()); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 加载service下的路由信息 |
||||
* |
||||
* @param servicePath |
||||
*/ |
||||
protected void loadServiceRouteItem(R serviceRouteInfo, String servicePath) throws Exception { |
||||
ZookeeperContext.getChildrenData(servicePath, childDataList -> { |
||||
for (ChildData childData : childDataList) { |
||||
String routeItemPath = childData.getPath(); |
||||
byte[] routeItemData = childData.getData(); |
||||
String routeDataJson = buildZookeeperData(routeItemData); |
||||
log.info("\t\t{} # 路由节点,节点数据:{}", routeItemPath, routeDataJson); |
||||
this.saveRouteItem(serviceRouteInfo, routeDataJson); |
||||
} |
||||
}); |
||||
this.watchRouteItems(serviceRouteInfo, servicePath); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 监听serviceId目录下面的子节点 |
||||
* |
||||
* @param serviceRouteInfo |
||||
* @param servicePath serviceId节点 |
||||
*/ |
||||
protected void watchRouteItems(R serviceRouteInfo, String servicePath) throws Exception { |
||||
log.info("监听{}下子节点增删改", servicePath); |
||||
// 添加事件监听器
|
||||
ZookeeperContext.listenChildren(servicePath, (client, event) -> { |
||||
PathChildrenCacheEvent.Type type = event.getType(); |
||||
synchronized (type) { |
||||
// 通过判断event type的方式来实现不同事件的触发
|
||||
if (PathChildrenCacheEvent.Type.CHILD_ADDED.equals(type)) { |
||||
// 新增单个路由
|
||||
String routeDataJson = buildZookeeperData(event.getData().getData()); |
||||
log.info("新增单个路由,serviceId:{}, 路由数据:{}", serviceRouteInfo.getServiceId(), routeDataJson); |
||||
saveRouteItem(serviceRouteInfo, routeDataJson); |
||||
} else if (PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(type)) { |
||||
// 修改单个路由
|
||||
String routeDataJson = buildZookeeperData(event.getData().getData()); |
||||
log.info("修改单个路由,serviceId:{}, 路由数据:{}", serviceRouteInfo.getServiceId(), routeDataJson); |
||||
updateRouteItem(serviceRouteInfo, routeDataJson); |
||||
} else if (PathChildrenCacheEvent.Type.CHILD_REMOVED.equals(type)) { |
||||
// 删除单个路由
|
||||
String routeDataJson = buildZookeeperData(event.getData().getData()); |
||||
log.info("删除单个路由,serviceId:{}, 路由数据:{}", serviceRouteInfo.getServiceId(), routeDataJson); |
||||
deleteRouteItem(serviceRouteInfo, routeDataJson); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
protected void saveRouteItem(R serviceRouteInfo, String nodeDataJson) { |
||||
T routeDefinition = getRouteDefinition(serviceRouteInfo, nodeDataJson); |
||||
routeRepository.add(routeDefinition); |
||||
} |
||||
|
||||
protected void updateRouteItem(R serviceRouteInfo, String nodeDataJson) { |
||||
T routeDefinition = getRouteDefinition(serviceRouteInfo, nodeDataJson); |
||||
routeRepository.update(routeDefinition); |
||||
} |
||||
|
||||
protected void deleteRouteItem(R serviceRouteInfo, String nodeDataJson) { |
||||
E routeDefinitionItem = getRouteDefinitionItem(nodeDataJson); |
||||
routeRepository.delete(routeDefinitionItem.getId()); |
||||
} |
||||
|
||||
protected T getRouteDefinition(R serviceRouteInfo, String nodeDataJson) { |
||||
E routeDefinitionItem = getRouteDefinitionItem(nodeDataJson); |
||||
T routeDefinition = buildRouteDefinition(serviceRouteInfo, routeDefinitionItem); |
||||
return routeDefinition; |
||||
} |
||||
|
||||
protected E getRouteDefinitionItem(String nodeDataJson) { |
||||
return JSON.parseObject(nodeDataJson, getRouteDefinitionClass()); |
||||
} |
||||
|
||||
protected String buildZookeeperData(byte[] data) { |
||||
return new String(data); |
||||
} |
||||
|
||||
} |
@ -1,24 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import org.springframework.beans.BeansException; |
||||
import org.springframework.context.ApplicationContext; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ManagerContext { |
||||
|
||||
private static ApplicationContext ctx; |
||||
|
||||
public static void setApplicationContext(ApplicationContext applicationContext) throws BeansException { |
||||
ctx = applicationContext; |
||||
} |
||||
|
||||
public static <T> T getManager(Class<T> clazz) { |
||||
return ctx.getBean(clazz); |
||||
} |
||||
|
||||
public static ApplicationContext getApplicationContext() { |
||||
return ctx; |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface RouteLoader { |
||||
void load(ServiceRouteInfo serviceRouteInfo); |
||||
|
||||
void remove(String serviceId); |
||||
} |
@ -1,20 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.IsvRoutePermission; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface RoutePermissionManager { |
||||
/** |
||||
* 加载路由授权 |
||||
*/ |
||||
void load(); |
||||
|
||||
/** |
||||
* 更新路由授权信息 |
||||
* |
||||
* @param isvRoutePermission 授权信息 |
||||
*/ |
||||
void update(IsvRoutePermission isvRoutePermission); |
||||
} |
@ -0,0 +1,118 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.nacos.api.config.ConfigService; |
||||
import com.alibaba.nacos.api.exception.NacosException; |
||||
import com.alibaba.nacos.api.naming.NamingService; |
||||
import com.alibaba.nacos.api.naming.pojo.Instance; |
||||
import com.alibaba.nacos.api.naming.pojo.ServiceInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.NacosConfigs; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties; |
||||
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties; |
||||
import org.springframework.context.ApplicationEvent; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.util.DigestUtils; |
||||
import org.springframework.web.client.RestTemplate; |
||||
|
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* 发现新服务,更新路由信息 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ServiceRoutesLoader<T extends TargetRoute> { |
||||
|
||||
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq"; |
||||
|
||||
private static final int FIVE_SECONDS = 1000 * 5; |
||||
|
||||
@Autowired |
||||
private NacosDiscoveryProperties nacosDiscoveryProperties; |
||||
|
||||
@Autowired |
||||
private NacosConfigProperties nacosConfigProperties; |
||||
|
||||
@Autowired |
||||
private BaseRouteCache<T> baseRouteCache; |
||||
|
||||
private RestTemplate restTemplate = new RestTemplate(); |
||||
|
||||
private volatile long lastUpdateTime; |
||||
|
||||
public synchronized void load(ApplicationEvent event) { |
||||
long now = System.currentTimeMillis(); |
||||
// 5秒内只能执行一次,解决重启应用连续加载4次问题
|
||||
if (now - lastUpdateTime < FIVE_SECONDS) { |
||||
return; |
||||
} |
||||
lastUpdateTime = now; |
||||
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); |
||||
List<ServiceInfo> subscribes = null; |
||||
try { |
||||
subscribes = namingService.getSubscribeServices(); |
||||
} catch (NacosException e) { |
||||
log.error("namingService.getSubscribeServices()错误", e); |
||||
} |
||||
if (CollectionUtils.isEmpty(subscribes)) { |
||||
return; |
||||
} |
||||
// subscribe
|
||||
String thisServiceId = nacosDiscoveryProperties.getService(); |
||||
ConfigService configService = nacosConfigProperties.configServiceInstance(); |
||||
for (ServiceInfo serviceInfo : subscribes) { |
||||
String serviceName = serviceInfo.getName(); |
||||
// 如果是本机服务,跳过
|
||||
if (Objects.equals(thisServiceId, serviceName)) { |
||||
continue; |
||||
} |
||||
try { |
||||
String dataId = NacosConfigs.getRouteDataId(serviceName); |
||||
String groupId = NacosConfigs.GROUP_ROUTE; |
||||
List<Instance> allInstances = namingService.getAllInstances(serviceName); |
||||
if (CollectionUtils.isEmpty(allInstances)) { |
||||
log.info("{}服务下线,删除路由信息", serviceName); |
||||
// 如果没有服务列表,则删除所有路由信息
|
||||
baseRouteCache.remove(serviceName); |
||||
configService.removeConfig(dataId, groupId); |
||||
} else { |
||||
for (Instance instance : allInstances) { |
||||
log.info("加载服务路由,instance:{}", instance); |
||||
String url = getRouteRequestUrl(instance); |
||||
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class); |
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) { |
||||
String body = responseEntity.getBody(); |
||||
log.debug("加载{}路由,路由信息:{}", serviceName, body); |
||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(body, ServiceRouteInfo.class); |
||||
baseRouteCache.load(serviceRouteInfo); |
||||
configService.publishConfig(dataId, groupId, body); |
||||
} |
||||
} |
||||
} |
||||
} catch (NacosException e) { |
||||
log.error("选择服务实例失败,serviceName:{}", serviceName, e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static String getRouteRequestUrl(Instance instance) { |
||||
String query = buildQuery(SECRET); |
||||
return "http://" + instance.getIp() + ":" + instance.getPort() + "/sop/routes" + query; |
||||
} |
||||
|
||||
private static String buildQuery(String secret) { |
||||
String time = String.valueOf(System.currentTimeMillis()); |
||||
String source = secret + time + secret; |
||||
String sign = DigestUtils.md5DigestAsHex(source.getBytes()); |
||||
return "?time=" + time + "&sign=" + sign; |
||||
} |
||||
|
||||
} |
@ -1,252 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.lang.StringUtils; |
||||
import org.apache.commons.lang.math.NumberUtils; |
||||
import org.apache.curator.framework.CuratorFramework; |
||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||
import org.apache.curator.framework.recipes.cache.ChildData; |
||||
import org.apache.curator.framework.recipes.cache.NodeCache; |
||||
import org.apache.curator.framework.recipes.cache.NodeCacheListener; |
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCache; |
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; |
||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_MSG_CHANNEL_PATH; |
||||
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_SERVICE_ROUTE_PATH; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ZookeeperContext { |
||||
|
||||
private static CuratorFramework client; |
||||
|
||||
private static Environment environment; |
||||
|
||||
public static void setEnvironment(Environment environment) { |
||||
Assert.notNull(environment, "environment不能为null"); |
||||
ZookeeperContext.environment = environment; |
||||
initZookeeperClient(); |
||||
} |
||||
|
||||
public synchronized static void initZookeeperClient() { |
||||
if (client != null) { |
||||
return; |
||||
} |
||||
setClient(createClient()); |
||||
} |
||||
|
||||
public static CuratorFramework createClient() { |
||||
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string"); |
||||
if (StringUtils.isBlank(zookeeperServerAddr)) { |
||||
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数"); |
||||
} |
||||
String baseSleepTimeMs = environment.getProperty("spring.cloud.zookeeper.baseSleepTimeMs"); |
||||
String maxRetries = environment.getProperty("spring.cloud.zookeeper.maxRetries"); |
||||
log.info("初始化zookeeper客户端,zookeeperServerAddr:{}, baseSleepTimeMs:{}, maxRetries:{}", |
||||
zookeeperServerAddr, baseSleepTimeMs, maxRetries); |
||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(NumberUtils.toInt(baseSleepTimeMs, 3000), NumberUtils.toInt(maxRetries, 3))) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
return client; |
||||
} |
||||
|
||||
public static String getRouteRootPath() { |
||||
return SOP_SERVICE_ROUTE_PATH; |
||||
} |
||||
|
||||
public static String getIsvInfoChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/isvinfo"; |
||||
} |
||||
|
||||
public static String getServiceGrayChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/gray"; |
||||
} |
||||
|
||||
public static String getIsvRoutePermissionChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/isv-route-permission"; |
||||
} |
||||
|
||||
public static String getRouteConfigChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/route-conf"; |
||||
} |
||||
|
||||
public static String getLimitConfigChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/limit-conf"; |
||||
} |
||||
|
||||
public static String getIpBlacklistChannelPath() { |
||||
return SOP_MSG_CHANNEL_PATH + "/ipblacklist-conf"; |
||||
} |
||||
|
||||
public static CuratorFramework getClient() { |
||||
return client; |
||||
} |
||||
|
||||
public static void setClient(CuratorFramework client) { |
||||
ZookeeperContext.client = client; |
||||
} |
||||
|
||||
/** |
||||
* 新建或保存节点 |
||||
* |
||||
* @param path |
||||
* @param data |
||||
* @return 返回path |
||||
* @throws Exception |
||||
*/ |
||||
public static String createOrUpdateData(String path, String data) throws Exception { |
||||
return getClient().create() |
||||
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
|
||||
.orSetData() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
/** |
||||
* 更新节点 |
||||
* @param path |
||||
* @param data |
||||
* @throws Exception |
||||
*/ |
||||
public static void updatePath(String path, String data) throws Exception { |
||||
getClient().setData().forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
/** |
||||
* 创建path,如果path存在不报错,静默返回path名称 |
||||
* |
||||
* @param path |
||||
* @param data |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public static String createPath(String path, String data) throws Exception { |
||||
if (isPathExist(path)) { |
||||
return path; |
||||
} |
||||
return getClient().create() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
public static boolean isPathExist(String path) { |
||||
try { |
||||
return client.checkExists().forPath(path) != null; |
||||
} catch (Exception e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 监听一个节点 |
||||
* |
||||
* @param path |
||||
* @param onChange 节点修改后触发 |
||||
* @return 返回path |
||||
* @throws Exception |
||||
*/ |
||||
public static String listenPath(String path, Consumer<NodeCache> onChange) throws Exception { |
||||
String ret = createOrUpdateData(path, "{}"); |
||||
final NodeCache cache = new NodeCache(client, path, false); |
||||
cache.getListenable().addListener(new NodeCacheListener() { |
||||
@Override |
||||
public void nodeChanged() throws Exception { |
||||
onChange.accept(cache); |
||||
} |
||||
}); |
||||
cache.start(); |
||||
return ret; |
||||
} |
||||
|
||||
/** |
||||
* 获取子节点信息并监听子节点 |
||||
* |
||||
* @param parentPath 父节点路径 |
||||
* @param listConsumer 子节点数据 |
||||
* @param listener 监听事件 |
||||
* @throws Exception |
||||
*/ |
||||
public static void getChildrenAndListen(String parentPath, Consumer<List<ChildData>> listConsumer, PathChildrenCacheListener listener) throws Exception { |
||||
// 为子节点添加watcher
|
||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||
PathChildrenCache childrenCache = new PathChildrenCache(client, parentPath, true); |
||||
|
||||
/** |
||||
* StartMode: 初始化方式 |
||||
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
||||
* NORMAL:异步初始化 |
||||
* BUILD_INITIAL_CACHE:同步初始化 |
||||
*/ |
||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||
|
||||
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
|
||||
List<ChildData> childDataList = childrenCache.getCurrentData(); |
||||
listConsumer.accept(childDataList); |
||||
log.info("监听子节点增删改,监听路径:{}", parentPath); |
||||
// 监听根节点下面的子节点
|
||||
childrenCache.getListenable().addListener(listener); |
||||
} |
||||
|
||||
/** |
||||
* 获取子节点信息 |
||||
* |
||||
* @param parentPath 父节点路径 |
||||
* @param listConsumer 子节点数据 |
||||
* @throws Exception |
||||
*/ |
||||
public static void getChildrenData(String parentPath, Consumer<List<ChildData>> listConsumer) throws Exception { |
||||
// 为子节点添加watcher
|
||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||
PathChildrenCache childrenCache = new PathChildrenCache(client, parentPath, true); |
||||
|
||||
/** |
||||
* StartMode: 初始化方式 |
||||
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
||||
* NORMAL:异步初始化 |
||||
* BUILD_INITIAL_CACHE:同步初始化 |
||||
*/ |
||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||
|
||||
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
|
||||
List<ChildData> childDataList = childrenCache.getCurrentData(); |
||||
listConsumer.accept(childDataList); |
||||
childrenCache.close(); |
||||
} |
||||
|
||||
/** |
||||
* 监听子节点的增删改 |
||||
* |
||||
* @param parentPath 父节点路径 |
||||
* @param listener |
||||
* @throws Exception |
||||
*/ |
||||
public static void listenChildren(String parentPath, PathChildrenCacheListener listener) throws Exception { |
||||
// 为子节点添加watcher
|
||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||
PathChildrenCache childrenCache = new PathChildrenCache(client, parentPath, true); |
||||
|
||||
/** |
||||
* StartMode: 初始化方式 |
||||
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
||||
* NORMAL:异步初始化 |
||||
* BUILD_INITIAL_CACHE:同步初始化 |
||||
*/ |
||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||
// 监听根节点下面的子节点
|
||||
childrenCache.getListenable().addListener(listener); |
||||
} |
||||
|
||||
} |
@ -1,54 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext; |
||||
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||
import com.netflix.zuul.context.RequestContext; |
||||
import com.netflix.zuul.exception.ZuulException; |
||||
import org.apache.commons.lang3.BooleanUtils; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
||||
/** |
||||
* 路由权限校验,有些接口需要配置权限才能访问。 |
||||
* @author tanghc |
||||
* @deprecated 已经整合到ApiValidator中,见ApiValidator.checkPermission() |
||||
*/ |
||||
@Deprecated |
||||
public class PreRoutePermissionFilter extends BaseZuulFilter { |
||||
|
||||
@Autowired |
||||
private IsvRoutePermissionManager isvRoutePermissionManager; |
||||
|
||||
@Override |
||||
protected FilterType getFilterType() { |
||||
return FilterType.PRE; |
||||
} |
||||
|
||||
@Override |
||||
protected int getFilterOrder() { |
||||
// 放在签名验证后面
|
||||
return PRE_ROUTE_PERMISSION_FILTER_ORDER; |
||||
} |
||||
|
||||
@Override |
||||
protected Object doRun(RequestContext requestContext) throws ZuulException { |
||||
ApiParam apiParam = ZuulContext.getApiParam(); |
||||
String routeId = apiParam.fetchNameVersion(); |
||||
TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(routeId); |
||||
BaseRouteDefinition routeDefinition = targetRoute.getRouteDefinition(); |
||||
boolean needCheckPermission = BooleanUtils.toBoolean(routeDefinition.getPermission()); |
||||
if (needCheckPermission) { |
||||
String appKey = apiParam.fetchAppKey(); |
||||
boolean hasPermission = isvRoutePermissionManager.hasPermission(appKey, routeId); |
||||
if (!hasPermission) { |
||||
throw ErrorEnum.ISV_ROUTE_NO_PERMISSIONS.getErrorMeta().getException(); |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,34 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.GatewayRouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.manager.BaseRouteCache; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||
import com.gitee.sop.gatewaycommon.util.RouteUtil; |
||||
import org.springframework.cloud.netflix.zuul.filters.Route; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZuulRouteCache extends BaseRouteCache<ZuulTargetRoute> { |
||||
|
||||
/** 路由重试 */ |
||||
public static final boolean RETRYABLE = true; |
||||
|
||||
public ZuulRouteCache(RouteRepository<ZuulTargetRoute> routeRepository) { |
||||
super(routeRepository); |
||||
} |
||||
|
||||
@Override |
||||
protected ZuulTargetRoute buildRouteDefinition(ServiceRouteInfo serviceRouteInfo, GatewayRouteDefinition gatewayRouteDefinition) { |
||||
Route route = new Route( |
||||
gatewayRouteDefinition.getId() |
||||
, gatewayRouteDefinition.getPath() |
||||
, RouteUtil.getZuulLocation(gatewayRouteDefinition.getUri()) |
||||
, "" |
||||
, RETRYABLE |
||||
, null |
||||
); |
||||
return new ZuulTargetRoute(serviceRouteInfo, gatewayRouteDefinition, route); |
||||
} |
||||
} |
@ -1,13 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Getter |
||||
@Setter |
||||
public class ZuulRouteDefinition extends BaseRouteDefinition { |
||||
} |
@ -1,9 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZuulServiceRouteInfo extends BaseServiceRouteInfo<ZuulRouteDefinition> { |
||||
} |
@ -1,40 +0,0 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.manager.BaseRouteManager; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||
import com.gitee.sop.gatewaycommon.util.RouteUtil; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.cloud.netflix.zuul.filters.Route; |
||||
import org.springframework.core.env.Environment; |
||||
|
||||
/** |
||||
* 路由内容管理,新增活修改路由 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ZuulZookeeperRouteManager extends BaseRouteManager<ZuulServiceRouteInfo, ZuulRouteDefinition, ZuulTargetRoute> { |
||||
|
||||
/** 路由重试 */ |
||||
public static final boolean RETRYABLE = true; |
||||
|
||||
public ZuulZookeeperRouteManager(Environment environment, RouteRepository<ZuulTargetRoute> routeRepository) { |
||||
super(environment, routeRepository); |
||||
} |
||||
|
||||
@Override |
||||
protected Class<ZuulServiceRouteInfo> getServiceRouteInfoClass() { |
||||
return ZuulServiceRouteInfo.class; |
||||
} |
||||
|
||||
@Override |
||||
protected Class<ZuulRouteDefinition> getRouteDefinitionClass() { |
||||
return ZuulRouteDefinition.class; |
||||
} |
||||
|
||||
@Override |
||||
protected ZuulTargetRoute buildRouteDefinition(ZuulServiceRouteInfo serviceRouteInfo, ZuulRouteDefinition routeDefinition) { |
||||
Route route = new Route(routeDefinition.getId(), routeDefinition.getPath(), RouteUtil.getZuulLocation(routeDefinition.getUri()), "", RETRYABLE, null); |
||||
return new ZuulTargetRoute(serviceRouteInfo, routeDefinition, route); |
||||
} |
||||
} |
@ -1,138 +0,0 @@ |
||||
package com.gitee.sop.servercommon.bean; |
||||
|
||||
import com.gitee.sop.servercommon.exception.ZookeeperOperationException; |
||||
import com.gitee.sop.servercommon.exception.ZookeeperPathNotExistException; |
||||
import lombok.Getter; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.lang.StringUtils; |
||||
import org.apache.commons.lang.math.NumberUtils; |
||||
import org.apache.curator.framework.CuratorFramework; |
||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||
import org.apache.zookeeper.CreateMode; |
||||
import org.springframework.core.env.Environment; |
||||
|
||||
import java.io.Closeable; |
||||
import java.io.IOException; |
||||
|
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@Getter |
||||
public class ZookeeperTool implements Closeable { |
||||
|
||||
private CuratorFramework client; |
||||
private Environment environment; |
||||
|
||||
public ZookeeperTool(Environment environment) { |
||||
this.environment = environment; |
||||
initZookeeperClient(environment); |
||||
} |
||||
|
||||
public void initZookeeperClient(Environment environment) { |
||||
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string"); |
||||
if (StringUtils.isBlank(zookeeperServerAddr)) { |
||||
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数"); |
||||
} |
||||
String baseSleepTimeMs = environment.getProperty("spring.cloud.zookeeper.baseSleepTimeMs"); |
||||
String maxRetries = environment.getProperty("spring.cloud.zookeeper.maxRetries"); |
||||
log.info("初始化zookeeper客户端,zookeeperServerAddr:{}, baseSleepTimeMs:{}, maxRetries:{}", |
||||
zookeeperServerAddr, baseSleepTimeMs, maxRetries); |
||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(NumberUtils.toInt(baseSleepTimeMs, 3000), NumberUtils.toInt(maxRetries, 3))) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
|
||||
this.client = client; |
||||
} |
||||
|
||||
/** |
||||
* 获取节点内容 |
||||
* |
||||
* @param path |
||||
* @return 返回节点内容 |
||||
* @throws ZookeeperPathNotExistException 节点不存在,抛出异常 |
||||
*/ |
||||
public String getData(String path) throws ZookeeperPathNotExistException { |
||||
if (!isPathExist(path)) { |
||||
throw new ZookeeperPathNotExistException("path 不存在, path=" + path); |
||||
} |
||||
try { |
||||
byte[] data = getClient().getData().forPath(path); |
||||
return new String(data); |
||||
} catch (Exception e) { |
||||
throw new ZookeeperOperationException("getData error path=" + path, e); |
||||
} |
||||
} |
||||
|
||||
public boolean isPathExist(String path) { |
||||
try { |
||||
return client.checkExists().forPath(path) != null; |
||||
} catch (Exception e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 创建path,如果path存在不报错,静默返回path名称 |
||||
* |
||||
* @param path |
||||
* @param data |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public String createPath(String path, String data) throws Exception { |
||||
if (isPathExist(path)) { |
||||
return path; |
||||
} |
||||
return getClient().create() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
/** |
||||
* 创建临时序列节点 |
||||
* @param path 节点路径 |
||||
* @param data 数据 |
||||
* @return 返回节点路径 |
||||
* @throws Exception |
||||
*/ |
||||
public String createOrUpdateEphemeralSequentialPath(String path, String data) throws Exception { |
||||
return getClient().create() |
||||
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
|
||||
.orSetData() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL) |
||||
.forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
/** |
||||
* 新建或保存节点 |
||||
* |
||||
* @param path |
||||
* @param data |
||||
* @return 返回path |
||||
* @throws Exception |
||||
*/ |
||||
public String createOrUpdateData(String path, String data) throws Exception { |
||||
return getClient().create() |
||||
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
|
||||
.orSetData() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
@Override |
||||
public void close() throws IOException { |
||||
if (this.client != null) { |
||||
this.client.close(); |
||||
} |
||||
} |
||||
} |
@ -1,26 +0,0 @@ |
||||
package com.gitee.sop.servercommon.exception; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZookeeperOperationException extends RuntimeException { |
||||
|
||||
public ZookeeperOperationException() { |
||||
} |
||||
|
||||
public ZookeeperOperationException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public ZookeeperOperationException(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
public ZookeeperOperationException(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
public ZookeeperOperationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { |
||||
super(message, cause, enableSuppression, writableStackTrace); |
||||
} |
||||
} |
@ -1,10 +0,0 @@ |
||||
package com.gitee.sop.servercommon.exception; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZookeeperPathNotExistException extends Exception { |
||||
public ZookeeperPathNotExistException(String message) { |
||||
super(message); |
||||
} |
||||
} |
@ -0,0 +1,100 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.bean.ServiceConfig; |
||||
import com.gitee.sop.servercommon.mapping.ApiMappingInfo; |
||||
import com.gitee.sop.servercommon.mapping.ApiMappingRequestCondition; |
||||
import com.gitee.sop.servercommon.mapping.RouteUtil; |
||||
import org.apache.commons.lang3.BooleanUtils; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
import org.springframework.web.servlet.mvc.condition.RequestCondition; |
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ApiMetaBuilder { |
||||
|
||||
/** 接口名规则:允许字母、数字、点、下划线 */ |
||||
private static final String REGEX_API_NAME = "^[a-zA-Z0-9\\.\\_\\-]+$"; |
||||
|
||||
public ServiceApiInfo getServiceApiInfo(String serviceId, RequestMappingHandlerMapping requestMappingHandlerMapping) { |
||||
if (serviceId == null) { |
||||
throw new IllegalArgumentException("请在application.properties中指定spring.application.name属性"); |
||||
} |
||||
List<ServiceApiInfo.ApiMeta> apis = this.buildApiMetaList(requestMappingHandlerMapping); |
||||
// 排序
|
||||
apis.sort(Comparator.comparing(ServiceApiInfo.ApiMeta::fetchNameVersion)); |
||||
|
||||
ServiceApiInfo serviceApiInfo = new ServiceApiInfo(); |
||||
serviceApiInfo.setServiceId(serviceId); |
||||
serviceApiInfo.setApis(apis); |
||||
|
||||
return serviceApiInfo; |
||||
} |
||||
|
||||
protected List<ServiceApiInfo.ApiMeta> buildApiMetaList(RequestMappingHandlerMapping requestMappingHandlerMapping) { |
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods(); |
||||
Set<RequestMappingInfo> requestMappingInfos = handlerMethods.keySet(); |
||||
List<String> store = new ArrayList<>(); |
||||
List<ServiceApiInfo.ApiMeta> apis = new ArrayList<>(requestMappingInfos.size()); |
||||
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethodEntry : handlerMethods.entrySet()) { |
||||
ServiceApiInfo.ApiMeta apiMeta = this.buildApiMeta(handlerMethodEntry); |
||||
if (apiMeta == null) { |
||||
continue; |
||||
} |
||||
String key = apiMeta.fetchNameVersion(); |
||||
if (store.contains(key)) { |
||||
throw new IllegalArgumentException("重复申明接口,请检查path和version,path:" + apiMeta.getPath() + ", version:" + apiMeta.getVersion()); |
||||
} else { |
||||
store.add(key); |
||||
} |
||||
apis.add(apiMeta); |
||||
} |
||||
return apis; |
||||
} |
||||
|
||||
protected ServiceApiInfo.ApiMeta buildApiMeta(Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethodEntry) { |
||||
RequestMappingInfo requestMappingInfo = handlerMethodEntry.getKey(); |
||||
Set<String> patterns = requestMappingInfo.getPatternsCondition().getPatterns(); |
||||
RequestCondition<?> customCondition = requestMappingInfo.getCustomCondition(); |
||||
if (customCondition instanceof ApiMappingRequestCondition) { |
||||
ApiMappingRequestCondition condition = (ApiMappingRequestCondition) customCondition; |
||||
ApiMappingInfo apiMappingInfo = condition.getApiMappingInfo(); |
||||
String name = apiMappingInfo.getName(); |
||||
String version = apiMappingInfo.getVersion(); |
||||
// 方法完整的path,如: /goods/listGoods,/users/user/get
|
||||
String path = patterns.iterator().next(); |
||||
// 不是ApiMapping注解的接口,name属性是null
|
||||
if (name == null || ServiceConfig.getInstance().isWebappMode()) { |
||||
name = buildName(path); |
||||
} |
||||
this.checkApiName(name); |
||||
ServiceApiInfo.ApiMeta apiMeta = new ServiceApiInfo.ApiMeta(name, path, version); |
||||
apiMeta.setIgnoreValidate(BooleanUtils.toInteger(apiMappingInfo.isIgnoreValidate())); |
||||
apiMeta.setMergeResult(BooleanUtils.toInteger(apiMappingInfo.isMergeResult())); |
||||
apiMeta.setPermission(BooleanUtils.toInteger(apiMappingInfo.isPermission())); |
||||
return apiMeta; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
protected void checkApiName(String name) { |
||||
if (!name.matches(REGEX_API_NAME)) { |
||||
throw new IllegalArgumentException("接口名称只允许【字母、数字、点(.)、下划线(_)、减号(-)】,错误接口:" + name); |
||||
} |
||||
} |
||||
|
||||
|
||||
protected String buildName(String path) { |
||||
return RouteUtil.buildApiName(path); |
||||
} |
||||
} |
@ -0,0 +1,46 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.route.ServiceRouteInfo; |
||||
import com.gitee.sop.servercommon.util.OpenUtil; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@RestController |
||||
public class ServiceRouteController { |
||||
|
||||
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq"; |
||||
|
||||
@Autowired |
||||
private RequestMappingHandlerMapping requestMappingHandlerMapping; |
||||
|
||||
@Autowired |
||||
private Environment environment; |
||||
|
||||
|
||||
@RequestMapping("/sop/routes") |
||||
public ServiceRouteInfo listRoutes(HttpServletRequest request, HttpServletResponse response) { |
||||
if (!OpenUtil.validateSimpleSign(request, SECRET)) { |
||||
log.error("签名验证失败, params:{}", request.getQueryString()); |
||||
return null; |
||||
} |
||||
String serviceId = environment.getProperty("spring.application.name"); |
||||
ApiMetaBuilder apiMetaBuilder = new ApiMetaBuilder(); |
||||
ServiceApiInfo serviceApiInfo = apiMetaBuilder.getServiceApiInfo(serviceId, requestMappingHandlerMapping); |
||||
ServiceRouteInfoBuilder serviceRouteInfoBuilder = new ServiceRouteInfoBuilder(environment); |
||||
return serviceRouteInfoBuilder.build(serviceApiInfo); |
||||
} |
||||
|
||||
|
||||
} |
@ -0,0 +1,150 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.route.GatewayPredicateDefinition; |
||||
import com.gitee.sop.servercommon.route.GatewayRouteDefinition; |
||||
import com.gitee.sop.servercommon.route.ServiceRouteInfo; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.DigestUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ServiceRouteInfoBuilder { |
||||
|
||||
/** |
||||
* 网关对应的LoadBalance协议 |
||||
*/ |
||||
private static final String PROTOCOL_LOAD_BALANCE = "lb://"; |
||||
|
||||
private static final String PATH_SPLIT = "/"; |
||||
|
||||
private static final String DEFAULT_CONTEXT_PATH = "/"; |
||||
|
||||
/** |
||||
* NameVersion=alipay.story.get1.0 |
||||
* see com.gitee.sop.gatewaycommon.routeDefinition.NameVersionRoutePredicateFactory |
||||
*/ |
||||
private static String QUERY_PREDICATE_DEFINITION_TPL = "NameVersion=%s"; |
||||
|
||||
private static ServiceApiInfo.ApiMeta FIRST_API_META = new ServiceApiInfo.ApiMeta("_first.route_", "/", "v_000"); |
||||
|
||||
private Environment environment; |
||||
|
||||
public ServiceRouteInfoBuilder(Environment environment) { |
||||
this.environment = environment; |
||||
} |
||||
|
||||
public ServiceRouteInfo build(ServiceApiInfo serviceApiInfo) { |
||||
return this.buildServiceGatewayInfo(serviceApiInfo); |
||||
} |
||||
|
||||
/** |
||||
* 构建接口信息,符合spring cloud gateway的格式 |
||||
* |
||||
* @param serviceApiInfo 服务接口信息 |
||||
* @return 返回服务路由信息 |
||||
*/ |
||||
protected ServiceRouteInfo buildServiceGatewayInfo(ServiceApiInfo serviceApiInfo) { |
||||
List<ServiceApiInfo.ApiMeta> apis = serviceApiInfo.getApis(); |
||||
List<GatewayRouteDefinition> routeDefinitionList = new ArrayList<>(apis.size()); |
||||
routeDefinitionList.add(this.buildReadBodyRouteDefinition(serviceApiInfo)); |
||||
for (ServiceApiInfo.ApiMeta apiMeta : apis) { |
||||
GatewayRouteDefinition gatewayRouteDefinition = this.buildGatewayRouteDefinition(serviceApiInfo, apiMeta); |
||||
routeDefinitionList.add(gatewayRouteDefinition); |
||||
} |
||||
ServiceRouteInfo serviceRouteInfo = new ServiceRouteInfo(); |
||||
serviceRouteInfo.setServiceId(serviceApiInfo.getServiceId()); |
||||
serviceRouteInfo.setRouteDefinitionList(routeDefinitionList); |
||||
String md5 = buildMd5(routeDefinitionList); |
||||
serviceRouteInfo.setMd5(md5); |
||||
return serviceRouteInfo; |
||||
} |
||||
|
||||
/** |
||||
* 构建路由id MD5 |
||||
* |
||||
* @param routeDefinitionList 路由列表 |
||||
* @return 返回MD5 |
||||
*/ |
||||
protected String buildMd5(List<GatewayRouteDefinition> routeDefinitionList) { |
||||
List<String> routeIdList = routeDefinitionList.stream() |
||||
.map(JSON::toJSONString) |
||||
.sorted() |
||||
.collect(Collectors.toList()); |
||||
String md5Source = org.apache.commons.lang3.StringUtils.join(routeIdList, ""); |
||||
return DigestUtils.md5DigestAsHex(md5Source.getBytes(StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
protected GatewayRouteDefinition buildGatewayRouteDefinition(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
GatewayRouteDefinition gatewayRouteDefinition = new GatewayRouteDefinition(); |
||||
// 唯一id规则:接口名 + 版本号
|
||||
String routeId = apiMeta.fetchNameVersion(); |
||||
this.checkPath(routeId, "接口定义(" + routeId + ")不能有斜杠字符'/'"); |
||||
BeanUtils.copyProperties(apiMeta, gatewayRouteDefinition); |
||||
gatewayRouteDefinition.setId(routeId); |
||||
gatewayRouteDefinition.setFilters(Collections.emptyList()); |
||||
gatewayRouteDefinition.setPredicates(this.buildPredicates(apiMeta)); |
||||
String uri = this.buildUri(serviceApiInfo, apiMeta); |
||||
String path = this.buildServletPath(serviceApiInfo, apiMeta); |
||||
gatewayRouteDefinition.setUri(uri); |
||||
gatewayRouteDefinition.setPath(path); |
||||
return gatewayRouteDefinition; |
||||
} |
||||
|
||||
protected List<GatewayPredicateDefinition> buildPredicates(ServiceApiInfo.ApiMeta apiMeta) { |
||||
GatewayPredicateDefinition gatewayPredicateDefinition = new GatewayPredicateDefinition(); |
||||
gatewayPredicateDefinition.setName("ReadBody"); |
||||
return Arrays.asList(gatewayPredicateDefinition, this.buildNameVersionPredicateDefinition(apiMeta)); |
||||
} |
||||
|
||||
protected GatewayPredicateDefinition buildNameVersionPredicateDefinition(ServiceApiInfo.ApiMeta apiMeta) { |
||||
return new GatewayPredicateDefinition(String.format(QUERY_PREDICATE_DEFINITION_TPL, apiMeta.fetchNameVersion())); |
||||
} |
||||
|
||||
/** |
||||
* 添加com.gitee.sop.gatewaycommon.routeDefinition.ReadBodyRoutePredicateFactory,解决form表单获取不到问题 |
||||
* |
||||
* @return 返回路由定义 |
||||
*/ |
||||
protected GatewayRouteDefinition buildReadBodyRouteDefinition(ServiceApiInfo serviceApiInfo) { |
||||
GatewayRouteDefinition readBodyRouteDefinition = this.buildGatewayRouteDefinition(serviceApiInfo, FIRST_API_META); |
||||
readBodyRouteDefinition.setOrder(Integer.MIN_VALUE); |
||||
|
||||
readBodyRouteDefinition.setPredicates(this.buildPredicates(FIRST_API_META)); |
||||
|
||||
return readBodyRouteDefinition; |
||||
} |
||||
|
||||
protected String buildUri(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
return PROTOCOL_LOAD_BALANCE + serviceApiInfo.getServiceId(); |
||||
} |
||||
|
||||
protected String buildServletPath(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
String contextPath = environment.getProperty("server.servlet.context-path", DEFAULT_CONTEXT_PATH); |
||||
String servletPath = apiMeta.getPath(); |
||||
if (servletPath == null) { |
||||
servletPath = ""; |
||||
} |
||||
servletPath = StringUtils.trimLeadingCharacter(servletPath, '/'); |
||||
return contextPath + servletPath; |
||||
} |
||||
|
||||
private void checkPath(String path, String errorMsg) { |
||||
if (path.contains(PATH_SPLIT)) { |
||||
throw new IllegalArgumentException(errorMsg); |
||||
} |
||||
} |
||||
} |
@ -1,271 +0,0 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.bean.ZookeeperTool; |
||||
import com.gitee.sop.servercommon.exception.ZookeeperPathNotExistException; |
||||
import com.gitee.sop.servercommon.route.GatewayPredicateDefinition; |
||||
import com.gitee.sop.servercommon.route.GatewayRouteDefinition; |
||||
import com.gitee.sop.servercommon.route.ServiceRouteInfo; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.DigestUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* 上传路由到zookeeper |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@Getter |
||||
@Setter |
||||
public class ServiceZookeeperApiMetaManager implements ApiMetaManager { |
||||
|
||||
/** 网关对应的LoadBalance协议 */ |
||||
private static final String PROTOCOL_LOAD_BALANCE = "lb://"; |
||||
|
||||
private static final String PATH_SPLIT = "/"; |
||||
|
||||
private static final String DEFAULT_CONTEXT_PATH = "/"; |
||||
|
||||
/** |
||||
* NameVersion=alipay.story.get1.0 |
||||
* see com.gitee.sop.gatewaycommon.routeDefinition.NameVersionRoutePredicateFactory |
||||
*/ |
||||
private static String QUERY_PREDICATE_DEFINITION_TPL = "NameVersion=%s"; |
||||
|
||||
private static ServiceApiInfo.ApiMeta FIRST_API_META = new ServiceApiInfo.ApiMeta("_first.route_", "/", "v_000"); |
||||
|
||||
|
||||
private Environment environment; |
||||
|
||||
private ZookeeperTool zookeeperTool; |
||||
|
||||
private String serviceId; |
||||
|
||||
public ServiceZookeeperApiMetaManager(Environment environment) { |
||||
this.environment = environment; |
||||
serviceId = environment.getProperty("spring.application.name"); |
||||
if (StringUtils.isEmpty(serviceId)) { |
||||
throw new IllegalArgumentException("请在application.properties中指定spring.application.name属性"); |
||||
} |
||||
this.zookeeperTool = new ZookeeperTool(environment); |
||||
|
||||
this.uploadServiceId(environment); |
||||
} |
||||
|
||||
/** |
||||
* 上传serviceId目录 |
||||
* |
||||
* @param environment environment |
||||
*/ |
||||
protected void uploadServiceId(Environment environment) { |
||||
try { |
||||
this.checkZookeeperNode(serviceId, "serviceId(" + serviceId + ")不能有斜杠字符'/'"); |
||||
ServiceRouteInfo serviceRouteInfo = this.buildServiceRouteInfo(); |
||||
// 保存路径
|
||||
String savePath = serviceRouteInfo.getZookeeperPath(); |
||||
String nodeData = JSON.toJSONString(serviceRouteInfo); |
||||
log.info("serviceId:{}, zookeeper保存路径:{}", serviceId, savePath); |
||||
this.zookeeperTool.createPath(savePath, nodeData); |
||||
this.zookeeperTool.createPath(serviceRouteInfo.getZookeeperTempServiceIdPath(), ""); |
||||
} catch (Exception e) { |
||||
throw new IllegalStateException("zookeeper操作失败"); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void uploadApi(ServiceApiInfo serviceApiInfo) { |
||||
try { |
||||
ServiceRouteInfo serviceRouteInfo = this.buildServiceGatewayInfo(serviceApiInfo); |
||||
this.uploadServiceRouteInfoToZookeeper(serviceRouteInfo); |
||||
// 同时上传一个临时节点
|
||||
String tempPath = serviceRouteInfo.getZookeeperTempServiceIdChildPath(); |
||||
this.zookeeperTool.createOrUpdateEphemeralSequentialPath(tempPath, JSON.toJSONString(serviceRouteInfo)); |
||||
} catch (Exception e) { |
||||
log.error("上传一个临时节点失败, serviceApiInfo:{}", serviceApiInfo, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 构建接口信息,符合spring cloud gateway的格式 |
||||
* |
||||
* @param serviceApiInfo 服务接口信息 |
||||
* @return 返回服务路由信息 |
||||
*/ |
||||
protected ServiceRouteInfo buildServiceGatewayInfo(ServiceApiInfo serviceApiInfo) { |
||||
List<ServiceApiInfo.ApiMeta> apis = serviceApiInfo.getApis(); |
||||
List<GatewayRouteDefinition> routeDefinitionList = new ArrayList<>(apis.size()); |
||||
routeDefinitionList.add(this.buildReadBodyRouteDefinition(serviceApiInfo)); |
||||
for (ServiceApiInfo.ApiMeta apiMeta : apis) { |
||||
GatewayRouteDefinition gatewayRouteDefinition = this.buildGatewayRouteDefinition(serviceApiInfo, apiMeta); |
||||
routeDefinitionList.add(gatewayRouteDefinition); |
||||
} |
||||
ServiceRouteInfo serviceRouteInfo = this.buildServiceRouteInfo(); |
||||
serviceRouteInfo.setRouteDefinitionList(routeDefinitionList); |
||||
String md5 = buildMd5(routeDefinitionList); |
||||
serviceRouteInfo.setMd5(md5); |
||||
return serviceRouteInfo; |
||||
} |
||||
|
||||
/** |
||||
* 构建路由id MD5 |
||||
* |
||||
* @param routeDefinitionList 路由列表 |
||||
* @return 返回MD5 |
||||
*/ |
||||
protected String buildMd5(List<GatewayRouteDefinition> routeDefinitionList) { |
||||
List<String> routeIdList = routeDefinitionList.stream() |
||||
.map(JSON::toJSONString) |
||||
.sorted() |
||||
.collect(Collectors.toList()); |
||||
String md5Source = org.apache.commons.lang3.StringUtils.join(routeIdList, ""); |
||||
return DigestUtils.md5DigestAsHex(md5Source.getBytes(StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
protected ServiceRouteInfo buildServiceRouteInfo() { |
||||
ServiceRouteInfo serviceRouteInfo = new ServiceRouteInfo(); |
||||
serviceRouteInfo.setServiceId(serviceId); |
||||
String description = environment.getProperty("spring.application.description"); |
||||
serviceRouteInfo.setDescription(description); |
||||
return serviceRouteInfo; |
||||
} |
||||
|
||||
protected GatewayRouteDefinition buildGatewayRouteDefinition(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
GatewayRouteDefinition gatewayRouteDefinition = new GatewayRouteDefinition(); |
||||
// 唯一id规则:接口名 + 版本号
|
||||
String routeId = apiMeta.fetchNameVersion(); |
||||
this.checkZookeeperNode(routeId, "接口定义(" + routeId + ")不能有斜杠字符'/'"); |
||||
BeanUtils.copyProperties(apiMeta, gatewayRouteDefinition); |
||||
gatewayRouteDefinition.setId(routeId); |
||||
gatewayRouteDefinition.setFilters(Collections.emptyList()); |
||||
gatewayRouteDefinition.setPredicates(this.buildPredicates(apiMeta)); |
||||
String uri = this.buildUri(serviceApiInfo, apiMeta); |
||||
String path = this.buildServletPath(serviceApiInfo, apiMeta); |
||||
gatewayRouteDefinition.setUri(uri); |
||||
gatewayRouteDefinition.setPath(path); |
||||
return gatewayRouteDefinition; |
||||
} |
||||
|
||||
protected List<GatewayPredicateDefinition> buildPredicates(ServiceApiInfo.ApiMeta apiMeta) { |
||||
GatewayPredicateDefinition gatewayPredicateDefinition = new GatewayPredicateDefinition(); |
||||
gatewayPredicateDefinition.setName("ReadBody"); |
||||
return Arrays.asList(gatewayPredicateDefinition, this.buildNameVersionPredicateDefinition(apiMeta)); |
||||
} |
||||
|
||||
protected GatewayPredicateDefinition buildNameVersionPredicateDefinition(ServiceApiInfo.ApiMeta apiMeta) { |
||||
return new GatewayPredicateDefinition(String.format(QUERY_PREDICATE_DEFINITION_TPL, apiMeta.fetchNameVersion())); |
||||
} |
||||
|
||||
/** |
||||
* 添加com.gitee.sop.gatewaycommon.routeDefinition.ReadBodyRoutePredicateFactory,解决form表单获取不到问题 |
||||
* |
||||
* @return 返回路由定义 |
||||
*/ |
||||
protected GatewayRouteDefinition buildReadBodyRouteDefinition(ServiceApiInfo serviceApiInfo) { |
||||
GatewayRouteDefinition readBodyRouteDefinition = this.buildGatewayRouteDefinition(serviceApiInfo, FIRST_API_META); |
||||
readBodyRouteDefinition.setOrder(Integer.MIN_VALUE); |
||||
|
||||
readBodyRouteDefinition.setPredicates(this.buildPredicates(FIRST_API_META)); |
||||
|
||||
return readBodyRouteDefinition; |
||||
} |
||||
|
||||
protected String buildUri(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
return PROTOCOL_LOAD_BALANCE + serviceApiInfo.getServiceId(); |
||||
} |
||||
|
||||
protected String buildServletPath(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
String contextPath = environment.getProperty("server.servlet.context-path", DEFAULT_CONTEXT_PATH); |
||||
String servletPath = apiMeta.getPath(); |
||||
if (servletPath == null) { |
||||
servletPath = ""; |
||||
} |
||||
servletPath = StringUtils.trimLeadingCharacter(servletPath, '/'); |
||||
return contextPath + servletPath; |
||||
} |
||||
|
||||
/** |
||||
* 上传接口信息到zookeeper |
||||
* |
||||
* @param serviceRouteInfo 路由服务信息 |
||||
*/ |
||||
protected void uploadServiceRouteInfoToZookeeper(ServiceRouteInfo serviceRouteInfo) { |
||||
String savePath = serviceRouteInfo.getZookeeperPath(); |
||||
try { |
||||
String existServiceRouteInfoData = zookeeperTool.getData(savePath); |
||||
ServiceRouteInfo serviceRouteInfoExist = JSON.parseObject(existServiceRouteInfoData, ServiceRouteInfo.class); |
||||
String oldMD5 = serviceRouteInfoExist.getMd5(); |
||||
String newMD5 = serviceRouteInfo.getMd5(); |
||||
if (Objects.equals(oldMD5, newMD5)) { |
||||
log.info("接口没有改变,无需上传路由信息"); |
||||
return; |
||||
} |
||||
} catch (ZookeeperPathNotExistException e) { |
||||
log.warn("服务路径不存在,path:{}", savePath); |
||||
} |
||||
try { |
||||
log.info("上传接口信息到zookeeper,path:{}, serviceId:{}, 接口数量:{}", |
||||
savePath, |
||||
serviceRouteInfo.getServiceId(), |
||||
serviceRouteInfo.getRouteDefinitionList().size()); |
||||
|
||||
String parentPath = this.uploadFolder(serviceRouteInfo); |
||||
this.uploadRouteItems(serviceRouteInfo, parentPath); |
||||
} catch (Exception e) { |
||||
log.error("更新接口信息到zookeeper失败, serviceId:{}", serviceRouteInfo.getServiceId(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 上传文件夹内容 |
||||
* |
||||
* @param serviceRouteInfo 路由服务信息 |
||||
* @return 返回文件夹路径 |
||||
*/ |
||||
protected String uploadFolder(ServiceRouteInfo serviceRouteInfo) throws Exception { |
||||
// 保存路径
|
||||
String savePath = serviceRouteInfo.getZookeeperPath(); |
||||
String serviceRouteInfoJson = JSON.toJSONString(serviceRouteInfo); |
||||
log.info("上传service目录到zookeeper,路径:{},内容:{}", savePath, serviceRouteInfoJson); |
||||
this.zookeeperTool.createOrUpdateData(savePath, serviceRouteInfoJson); |
||||
return savePath; |
||||
} |
||||
|
||||
/** |
||||
* 上传路由信息 |
||||
* |
||||
* @param serviceRouteInfo 路由服务 |
||||
* @param parentPath 父节点 |
||||
* @throws Exception |
||||
*/ |
||||
protected void uploadRouteItems(ServiceRouteInfo serviceRouteInfo, String parentPath) throws Exception { |
||||
List<GatewayRouteDefinition> routeDefinitionList = serviceRouteInfo.getRouteDefinitionList(); |
||||
for (GatewayRouteDefinition routeDefinition : routeDefinitionList) { |
||||
// 父目录/子目录
|
||||
String savePath = parentPath + PATH_SPLIT + routeDefinition.getId(); |
||||
String routeDefinitionJson = JSON.toJSONString(routeDefinition); |
||||
log.info("上传路由配置到zookeeper,路径:{},路由数据:{}", savePath, routeDefinitionJson); |
||||
this.zookeeperTool.createOrUpdateData(savePath, routeDefinitionJson); |
||||
} |
||||
} |
||||
|
||||
private void checkZookeeperNode(String path, String errorMsg) { |
||||
if (path.contains(PATH_SPLIT)) { |
||||
throw new IllegalArgumentException(errorMsg); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,19 +0,0 @@ |
||||
package com.gitee.sop.gateway.manager; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.ApiConfig; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ManagerInitializer { |
||||
|
||||
public ManagerInitializer() { |
||||
ApiConfig apiConfig = ApiConfig.getInstance(); |
||||
apiConfig.setIsvManager(new DbIsvManager()); |
||||
apiConfig.setIsvRoutePermissionManager(new DbIsvRoutePermissionManager()); |
||||
apiConfig.setRouteConfigManager(new DbRouteConfigManager()); |
||||
apiConfig.setLimitConfigManager(new DbLimitConfigManager()); |
||||
apiConfig.setIpBlacklistManager(new DbIPBlacklistManager()); |
||||
apiConfig.setUserKeyManager(new DbEnvGrayManager()); |
||||
} |
||||
} |
@ -0,0 +1,3 @@ |
||||
spring.application.name=api-gateway |
||||
# nacos的config需要放在bootstrap.properties中 |
||||
spring.cloud.nacos.config.server-addr=127.0.0.1:8848 |
@ -1,52 +0,0 @@ |
||||
package com.gitee.sop.gateway; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||
import junit.framework.TestCase; |
||||
import org.apache.curator.framework.CuratorFramework; |
||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class CuratorTest extends TestCase { |
||||
|
||||
private String zookeeperServerAddr = "localhost:2181"; |
||||
|
||||
/** |
||||
* 递归删除节点,只能在测试环境用。 |
||||
* |
||||
* @throws Exception |
||||
*/ |
||||
public void testDel() throws Exception { |
||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
|
||||
try { |
||||
client.delete().deletingChildrenIfNeeded().forPath(SopConstants.SOP_SERVICE_ROUTE_PATH); |
||||
} catch (Exception e) { |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 删除临时节点 |
||||
*/ |
||||
public void testDelTemp() { |
||||
String tempRoot = "/com.gitee.sop.service.tmp"; |
||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
|
||||
try { |
||||
client.delete().deletingChildrenIfNeeded().forPath(tempRoot); |
||||
} catch (Exception e) { |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,381 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.Data; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
import okhttp3.Cookie; |
||||
import okhttp3.CookieJar; |
||||
import okhttp3.FormBody; |
||||
import okhttp3.HttpUrl; |
||||
import okhttp3.MediaType; |
||||
import okhttp3.MultipartBody; |
||||
import okhttp3.OkHttpClient; |
||||
import okhttp3.Request; |
||||
import okhttp3.RequestBody; |
||||
import okhttp3.Response; |
||||
import org.apache.commons.codec.digest.DigestUtils; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.Serializable; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* http请求工具,基于OKHTTP3 |
||||
* @author tanghc |
||||
*/ |
||||
public class HttpTool { |
||||
private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); |
||||
|
||||
private Map<String, List<Cookie>> cookieStore = new HashMap<String, List<Cookie>>(); |
||||
|
||||
private OkHttpClient httpClient; |
||||
|
||||
public HttpTool() { |
||||
this(new HttpToolConfig()); |
||||
} |
||||
|
||||
public HttpTool(HttpToolConfig httpToolConfig) { |
||||
this.initHttpClient(httpToolConfig); |
||||
} |
||||
|
||||
protected void initHttpClient(HttpToolConfig httpToolConfig) { |
||||
httpClient = new OkHttpClient.Builder() |
||||
.connectTimeout(httpToolConfig.connectTimeoutSeconds, TimeUnit.SECONDS) // 设置链接超时时间,默认10秒
|
||||
.readTimeout(httpToolConfig.readTimeoutSeconds, TimeUnit.SECONDS) |
||||
.writeTimeout(httpToolConfig.writeTimeoutSeconds, TimeUnit.SECONDS) |
||||
.cookieJar(new CookieJar() { |
||||
public void saveFromResponse(HttpUrl httpUrl, List<Cookie> list) { |
||||
cookieStore.put(httpUrl.host(), list); |
||||
} |
||||
|
||||
public List<Cookie> loadForRequest(HttpUrl httpUrl) { |
||||
List<Cookie> cookies = cookieStore.get(httpUrl.host()); |
||||
return cookies != null ? cookies : new ArrayList<Cookie>(); |
||||
} |
||||
}).build(); |
||||
} |
||||
|
||||
@Data |
||||
public static class HttpToolConfig { |
||||
/** |
||||
* 请求超时时间 |
||||
*/ |
||||
private int connectTimeoutSeconds = 10; |
||||
/** |
||||
* http读取超时时间 |
||||
*/ |
||||
private int readTimeoutSeconds = 10; |
||||
/** |
||||
* http写超时时间 |
||||
*/ |
||||
private int writeTimeoutSeconds = 10; |
||||
} |
||||
|
||||
/** |
||||
* get请求 |
||||
* |
||||
* @param url |
||||
* @param header |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public String get(String url, Map<String, String> header) throws IOException { |
||||
Request.Builder builder = new Request.Builder().url(url).get(); |
||||
// 添加header
|
||||
addHeader(builder, header); |
||||
|
||||
Request request = builder.build(); |
||||
Response response = httpClient.newCall(request).execute(); |
||||
return response.body().string(); |
||||
} |
||||
|
||||
/** |
||||
* 提交表单 |
||||
* |
||||
* @param url url |
||||
* @param form 参数 |
||||
* @param header header |
||||
* @param method 请求方式,post,get等 |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public String request(String url, Map<String, ?> form, Map<String, String> header, HTTPMethod method) throws IOException { |
||||
Request.Builder requestBuilder = buildRequestBuilder(url, form, method); |
||||
// 添加header
|
||||
addHeader(requestBuilder, header); |
||||
|
||||
Request request = requestBuilder.build(); |
||||
Response response = httpClient |
||||
.newCall(request) |
||||
.execute(); |
||||
try { |
||||
return response.body().string(); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 请求json数据,contentType=application/json |
||||
* @param url 请求路径 |
||||
* @param json json数据 |
||||
* @param header header |
||||
* @return 返回响应结果 |
||||
* @throws IOException |
||||
*/ |
||||
public String requestJson(String url, String json, Map<String, String> header) throws IOException { |
||||
RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, json); |
||||
Request.Builder requestBuilder = new Request.Builder() |
||||
.url(url) |
||||
.post(body); |
||||
// 添加header
|
||||
addHeader(requestBuilder, header); |
||||
|
||||
Request request = requestBuilder.build(); |
||||
Response response = httpClient |
||||
.newCall(request) |
||||
.execute(); |
||||
try { |
||||
return response.body().string(); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
} |
||||
|
||||
public static Request.Builder buildRequestBuilder(String url, Map<String, ?> form, HTTPMethod method) { |
||||
switch (method) { |
||||
case GET: |
||||
return new Request.Builder() |
||||
.url(buildHttpUrl(url, form)) |
||||
.get(); |
||||
case HEAD: |
||||
return new Request.Builder() |
||||
.url(buildHttpUrl(url, form)) |
||||
.head(); |
||||
case PUT: |
||||
return new Request.Builder() |
||||
.url(url) |
||||
.put(buildFormBody(form)); |
||||
case DELETE: |
||||
return new Request.Builder() |
||||
.url(url) |
||||
.delete(buildFormBody(form)); |
||||
default: |
||||
return new Request.Builder() |
||||
.url(url) |
||||
.post(buildFormBody(form)); |
||||
} |
||||
} |
||||
|
||||
public static HttpUrl buildHttpUrl(String url, Map<String, ?> form) { |
||||
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); |
||||
for (Map.Entry<String, ?> entry : form.entrySet()) { |
||||
urlBuilder.addQueryParameter(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
return urlBuilder.build(); |
||||
} |
||||
|
||||
public static FormBody buildFormBody(Map<String, ?> form) { |
||||
FormBody.Builder paramBuilder = new FormBody.Builder(StandardCharsets.UTF_8); |
||||
for (Map.Entry<String, ?> entry : form.entrySet()) { |
||||
paramBuilder.add(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
return paramBuilder.build(); |
||||
} |
||||
|
||||
/** |
||||
* 提交表单,并且上传文件 |
||||
* |
||||
* @param url |
||||
* @param form |
||||
* @param header |
||||
* @param files |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public String requestFile(String url, Map<String, String> form, Map<String, String> header, List<UploadFile> files) |
||||
throws IOException { |
||||
// 创建MultipartBody.Builder,用于添加请求的数据
|
||||
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder(); |
||||
bodyBuilder.setType(MultipartBody.FORM); |
||||
|
||||
for (UploadFile uploadFile : files) { |
||||
bodyBuilder.addFormDataPart(uploadFile.getName(), // 请求的名字
|
||||
uploadFile.getFileName(), // 文件的文字,服务器端用来解析的
|
||||
RequestBody.create(null, uploadFile.getFileData()) // 创建RequestBody,把上传的文件放入
|
||||
); |
||||
} |
||||
|
||||
Set<Map.Entry<String, String>> entrySet = form.entrySet(); |
||||
for (Map.Entry<String, String> entry : entrySet) { |
||||
bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue()); |
||||
} |
||||
|
||||
RequestBody requestBody = bodyBuilder.build(); |
||||
|
||||
Request.Builder builder = new Request.Builder().url(url).post(requestBody); |
||||
|
||||
// 添加header
|
||||
addHeader(builder, header); |
||||
|
||||
Request request = builder.build(); |
||||
Response response = httpClient.newCall(request).execute(); |
||||
try { |
||||
return response.body().string(); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
} |
||||
|
||||
private void addHeader(Request.Builder builder, Map<String, String> header) { |
||||
if (header != null) { |
||||
Set<Map.Entry<String, String>> entrySet = header.entrySet(); |
||||
for (Map.Entry<String, String> entry : entrySet) { |
||||
builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void setCookieStore(Map<String, List<Cookie>> cookieStore) { |
||||
this.cookieStore = cookieStore; |
||||
} |
||||
|
||||
public void setHttpClient(OkHttpClient httpClient) { |
||||
this.httpClient = httpClient; |
||||
} |
||||
|
||||
public enum HTTPMethod { |
||||
GET, |
||||
POST, |
||||
PUT, |
||||
HEAD, |
||||
DELETE; |
||||
|
||||
private HTTPMethod() { |
||||
} |
||||
|
||||
public String value() { |
||||
return this.name(); |
||||
} |
||||
|
||||
public static HTTPMethod fromValue(String v) { |
||||
return valueOf(v.toUpperCase()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 文件上传类 |
||||
* @author tanghc |
||||
*/ |
||||
@Getter |
||||
@Setter |
||||
public static class UploadFile implements Serializable { |
||||
private static final long serialVersionUID = -1100614660944996398L; |
||||
|
||||
/** |
||||
* @param name 表单名称,不能重复 |
||||
* @param file 文件 |
||||
* @throws IOException |
||||
*/ |
||||
public UploadFile(String name, File file) throws IOException { |
||||
this(name, file.getName(), FileUtil.toBytes(file)); |
||||
} |
||||
|
||||
/** |
||||
* @param name 表单名称,不能重复 |
||||
* @param fileName 文件名 |
||||
* @param input 文件流 |
||||
* @throws IOException |
||||
*/ |
||||
public UploadFile(String name, String fileName, InputStream input) throws IOException { |
||||
this(name, fileName, FileUtil.toBytes(input)); |
||||
} |
||||
|
||||
/** |
||||
* @param name 表单名称,不能重复 |
||||
* @param fileName 文件名 |
||||
* @param fileData 文件数据 |
||||
*/ |
||||
public UploadFile(String name, String fileName, byte[] fileData) { |
||||
super(); |
||||
this.name = name; |
||||
this.fileName = fileName; |
||||
this.fileData = fileData; |
||||
this.md5 = DigestUtils.md5Hex(fileData); |
||||
} |
||||
|
||||
private String name; |
||||
private String fileName; |
||||
private byte[] fileData; |
||||
private String md5; |
||||
|
||||
} |
||||
|
||||
public static class FileUtil { |
||||
|
||||
/** |
||||
* The default buffer size to use. |
||||
*/ |
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; |
||||
private static final int EOF = -1; |
||||
|
||||
/** |
||||
* 将文件流转换成byte[] |
||||
* @param input |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public static byte[] toBytes(InputStream input) throws IOException { |
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(); |
||||
int n = 0; |
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; |
||||
|
||||
while (EOF != (n = input.read(buffer))) { |
||||
output.write(buffer, 0, n); |
||||
} |
||||
return output.toByteArray(); |
||||
} |
||||
|
||||
/** |
||||
* 将文件转换成数据流 |
||||
* @param file 文件 |
||||
* @return 返回数据流 |
||||
* @throws IOException |
||||
*/ |
||||
public static byte[] toBytes(File file) throws IOException { |
||||
if (file.exists()) { |
||||
if (file.isDirectory()) { |
||||
throw new IOException("File '" + file + "' exists but is a directory"); |
||||
} |
||||
if (file.canRead() == false) { |
||||
throw new IOException("File '" + file + "' cannot be read"); |
||||
} |
||||
} else { |
||||
throw new FileNotFoundException("File '" + file + "' does not exist"); |
||||
} |
||||
InputStream input = null; |
||||
try { |
||||
input = new FileInputStream(file); |
||||
return toBytes(input); |
||||
} finally { |
||||
try { |
||||
if (input != null) { |
||||
input.close(); |
||||
} |
||||
} catch (IOException ioe) { |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,179 +0,0 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.lang.StringUtils; |
||||
import org.apache.commons.lang.math.NumberUtils; |
||||
import org.apache.curator.framework.CuratorFramework; |
||||
import org.apache.curator.framework.CuratorFrameworkFactory; |
||||
import org.apache.curator.framework.recipes.cache.ChildData; |
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCache; |
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; |
||||
import org.apache.curator.framework.recipes.cache.TreeCache; |
||||
import org.apache.curator.framework.recipes.cache.TreeCacheListener; |
||||
import org.apache.curator.retry.ExponentialBackoffRetry; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
|
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ZookeeperContext { |
||||
|
||||
private static CuratorFramework client; |
||||
|
||||
public static void setEnvironment(Environment environment) { |
||||
Assert.notNull(environment, "environment不能为null"); |
||||
initZookeeperClient(environment); |
||||
} |
||||
|
||||
public synchronized static void initZookeeperClient(Environment environment) { |
||||
if (client != null) { |
||||
return; |
||||
} |
||||
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string"); |
||||
if (StringUtils.isBlank(zookeeperServerAddr)) { |
||||
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数"); |
||||
} |
||||
String baseSleepTimeMs = environment.getProperty("spring.cloud.zookeeper.baseSleepTimeMs"); |
||||
String maxRetries = environment.getProperty("spring.cloud.zookeeper.maxRetries"); |
||||
log.info("初始化zookeeper客户端,zookeeperServerAddr:{}, baseSleepTimeMs:{}, maxRetries:{}", |
||||
zookeeperServerAddr, baseSleepTimeMs, maxRetries); |
||||
CuratorFramework client = CuratorFrameworkFactory.builder() |
||||
.connectString(zookeeperServerAddr) |
||||
.retryPolicy(new ExponentialBackoffRetry(NumberUtils.toInt(baseSleepTimeMs, 3000), NumberUtils.toInt(maxRetries, 3))) |
||||
.build(); |
||||
|
||||
client.start(); |
||||
|
||||
setClient(client); |
||||
} |
||||
|
||||
public static String getRouteRootPath() { |
||||
return WebsiteConstants.SOP_SERVICE_ROUTE_PATH; |
||||
} |
||||
|
||||
public static String getServiceTempRootPath() { |
||||
return WebsiteConstants.SOP_SERVICE_TEMP_PATH; |
||||
} |
||||
|
||||
public static CuratorFramework getClient() { |
||||
return client; |
||||
} |
||||
|
||||
public static void setClient(CuratorFramework client) { |
||||
ZookeeperContext.client = client; |
||||
} |
||||
|
||||
public static boolean isPathExist(String path) { |
||||
try { |
||||
return client.checkExists().forPath(path) != null; |
||||
} catch (Exception e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 是否有子节点 |
||||
* @param parentPath |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public static boolean hasChildren(String parentPath) throws Exception { |
||||
List<String> children = client.getChildren().forPath(parentPath); |
||||
return !CollectionUtils.isEmpty(children); |
||||
} |
||||
|
||||
/** |
||||
* 创建path,如果path存在不报错,静默返回path名称 |
||||
* |
||||
* @param path |
||||
* @param data |
||||
* @return |
||||
* @throws Exception |
||||
*/ |
||||
public static String createPath(String path, String data) throws Exception { |
||||
if (isPathExist(path)) { |
||||
return path; |
||||
} |
||||
return getClient().create() |
||||
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
|
||||
.creatingParentContainersIfNeeded() |
||||
.forPath(path, data.getBytes()); |
||||
} |
||||
|
||||
/** |
||||
* 获取子节点信息并监听子节点 |
||||
* |
||||
* @param parentPath 父节点路径 |
||||
* @param listConsumer 子节点数据 |
||||
* @param listener 监听事件 |
||||
* @throws Exception |
||||
*/ |
||||
public static void getChildrenAndListen(String parentPath, Consumer<List<ChildData>> listConsumer, PathChildrenCacheListener listener) throws Exception { |
||||
// 为子节点添加watcher
|
||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||
PathChildrenCache childrenCache = new PathChildrenCache(client, parentPath, true); |
||||
|
||||
/** |
||||
* StartMode: 初始化方式 |
||||
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
||||
* NORMAL:异步初始化 |
||||
* BUILD_INITIAL_CACHE:同步初始化 |
||||
*/ |
||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||
|
||||
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
|
||||
List<ChildData> childDataList = childrenCache.getCurrentData(); |
||||
listConsumer.accept(childDataList); |
||||
log.info("监听子节点增删改,监听路径:{}", parentPath); |
||||
// 监听根节点下面的子节点
|
||||
childrenCache.getListenable().addListener(listener); |
||||
} |
||||
|
||||
/** |
||||
* 监听子节点的增删改 |
||||
* |
||||
* @param parentPath 父节点路径 |
||||
* @param listener |
||||
* @throws Exception |
||||
*/ |
||||
public static void listenChildren(String parentPath, PathChildrenCacheListener listener) throws Exception { |
||||
// 为子节点添加watcher
|
||||
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
|
||||
PathChildrenCache childrenCache = new PathChildrenCache(client, parentPath, true); |
||||
|
||||
/** |
||||
* StartMode: 初始化方式 |
||||
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件 |
||||
* NORMAL:异步初始化 |
||||
* BUILD_INITIAL_CACHE:同步初始化 |
||||
*/ |
||||
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); |
||||
// 监听根节点下面的子节点
|
||||
childrenCache.getListenable().addListener(listener); |
||||
} |
||||
|
||||
/** |
||||
* 监听子节点,可以自定义层级 |
||||
* @param parentPath 父节点路径 |
||||
* @param maxDepth 层级,从1开始。比如当前监听节点/t1,目录最深为/t1/t2/t3/t4,则maxDepth=3,说明下面3级子目录全部监听 |
||||
* @param listener |
||||
* @throws Exception |
||||
*/ |
||||
public static void listenChildren(String parentPath, int maxDepth, TreeCacheListener listener) throws Exception { |
||||
final TreeCache treeCache = TreeCache |
||||
.newBuilder(client, parentPath) |
||||
.setCacheData(true) |
||||
.setMaxDepth(maxDepth) |
||||
.build(); |
||||
|
||||
treeCache.getListenable().addListener(listener); |
||||
treeCache.start(); |
||||
} |
||||
|
||||
} |
@ -1,12 +0,0 @@ |
||||
package com.gitee.sop.websiteserver.config; |
||||
|
||||
import com.gitee.sop.registryapi.config.BaseRegistryConfig; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Configuration |
||||
public class RegistryConfig extends BaseRegistryConfig { |
||||
|
||||
} |
@ -0,0 +1,99 @@ |
||||
package com.gitee.sop.websiteserver.manager; |
||||
|
||||
import com.alibaba.nacos.api.exception.NacosException; |
||||
import com.alibaba.nacos.api.naming.NamingService; |
||||
import com.alibaba.nacos.api.naming.pojo.Instance; |
||||
import com.alibaba.nacos.api.naming.pojo.ServiceInfo; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.util.DigestUtils; |
||||
import org.springframework.web.client.RestTemplate; |
||||
|
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
public class DocDiscovery { |
||||
|
||||
private static final String SECRET = "b749a2ec000f4f29"; |
||||
|
||||
private static final int FIVE_SECONDS = 1000 * 5; |
||||
|
||||
@Autowired |
||||
private NacosDiscoveryProperties nacosDiscoveryProperties; |
||||
|
||||
private RestTemplate restTemplate = new RestTemplate(); |
||||
|
||||
private volatile long lastUpdateTime; |
||||
|
||||
public synchronized void refresh(DocManager docManager) { |
||||
long now = System.currentTimeMillis(); |
||||
// 5秒内只能执行一次,解决重启应用连续加载4次问题
|
||||
if (now - lastUpdateTime < FIVE_SECONDS) { |
||||
return; |
||||
} |
||||
lastUpdateTime = now; |
||||
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); |
||||
List<ServiceInfo> subscribes = null; |
||||
try { |
||||
subscribes = namingService.getSubscribeServices(); |
||||
} catch (NacosException e) { |
||||
log.error("namingService.getSubscribeServices()错误", e); |
||||
} |
||||
if (CollectionUtils.isEmpty(subscribes)) { |
||||
return; |
||||
} |
||||
// subscribe
|
||||
String thisServiceId = nacosDiscoveryProperties.getService(); |
||||
for (ServiceInfo serviceInfo : subscribes) { |
||||
String serviceName = serviceInfo.getName(); |
||||
// 如果是本机服务,跳过
|
||||
if (Objects.equals(thisServiceId, serviceName) || "api-gateway".equalsIgnoreCase(serviceName)) { |
||||
continue; |
||||
} |
||||
try { |
||||
List<Instance> allInstances = namingService.getAllInstances(serviceName); |
||||
if (CollectionUtils.isEmpty(allInstances)) { |
||||
log.info("{}服务下线,删除文档信息", serviceName); |
||||
// 如果没有服务列表,则删除所有路由信息
|
||||
docManager.remove(serviceName); |
||||
} else { |
||||
for (Instance instance : allInstances) { |
||||
log.info("加载服务文档,instance:{}", instance); |
||||
String url = getRouteRequestUrl(instance); |
||||
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class); |
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) { |
||||
String body = responseEntity.getBody(); |
||||
log.debug("加载{}文档,文档信息:{}", serviceName, body); |
||||
docManager.addDocInfo(serviceName, body); |
||||
} |
||||
} |
||||
} |
||||
} catch (NacosException e) { |
||||
log.error("选择服务实例失败,serviceName:{}", serviceName, e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static String getRouteRequestUrl(Instance instance) { |
||||
String query = buildQuery(SECRET); |
||||
return "http://" + instance.getIp() + ":" + instance.getPort() + "/v2/api-docs" + query; |
||||
} |
||||
|
||||
private static String buildQuery(String secret) { |
||||
String time = String.valueOf(System.currentTimeMillis()); |
||||
String source = secret + time + secret; |
||||
String sign = DigestUtils.md5DigestAsHex(source.getBytes()); |
||||
return "?time=" + time + "&sign=" + sign; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue