一、dubbo路由规则的读取详解
要想解决dubbo-admin路由规则设置的bug,那么就必须要搞懂dubbo是怎么读取admin设置的规则,源码必须要读。。。。
1、RegistryDirectory和RouterChain
RegistryDirectory是dubbo服务注册与发现机制,主要来看这里的代码(RouterChain就不写了):
private Optional<List<Router>> toRouters(List<URL> urls) {
if (urls == null || urls.isEmpty()) {
return Optional.empty();
}
List<Router> routers = new ArrayList<>();
for (URL url : urls) {
if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
continue;
}
String routerType = url.getParameter(ROUTER_KEY);
if (routerType != null && routerType.length() > 0) {
url = url.setProtocol(routerType);
}
try {
//从这里开始看
Router router = ROUTER_FACTORY.getRouter(url);
if (!routers.contains(router)) {
routers.add(router);
}
} catch (Throwable t) {
logger.error("convert router url to router error, url: " + url, t);
}
}
return Optional.of(routers);
}
这里会把zk上面所有的服务url都去遍历是否有路由规则的存在。
从这段代码一直往下延申
Router router = ROUTER_FACTORY.getRouter(url);
来到CacheableRouterFactory(其他的支线,我就不一一说了)
@Override
public Router getRouter(URL url) {
routerMap.computeIfAbsent(url.getServiceKey(), k -> createRouter(url));
return routerMap.get(url.getServiceKey());
}
再来到ServiceRouterFactory
@Override
protected Router createRouter(URL url) {
return new ServiceRouter(DynamicConfiguration.getDynamicConfiguration(), url);
}
紧接着来到服务路由的类ServiceRouter
public ServiceRouter(DynamicConfiguration configuration, URL url) {
super(configuration, url, DynamicConfiguration.getRuleKey(url));
this.priority = SERVICE_ROUTER_DEFAULT_PRIORITY;
}
最重要的就是DynamicConfiguration.getRuleKey(url)这段代码,这是决定了dubbo去zk读取路由的key,这也是跟dubbo-admin设置路由路径不一致的地方。
我们顺着往下走,来到URL.java
/**
* The format is "{interface}:[version]:[group]"
* @return
*/
public String getColonSeparatedKey() {
StringBuilder serviceNameBuilder = new StringBuilder();
append(serviceNameBuilder, INTERFACE_KEY, true);
append(serviceNameBuilder, VERSION_KEY, false);
append(serviceNameBuilder, GROUP_KEY, false);
return serviceNameBuilder.toString();
}
这里就很清晰了,dubbo是会拿出三个参数来拼接成ruleKey的,也就是说拼接的形式是interface:version:group(还不清楚的话可以看看append方法),即使version和group为空也会存在":",举例:com.zkd.baseInterface.route.RouteService::或者com.zkd.baseInterface.route.RouteService:😗
看一下继承的父类ListenableRouter情况
public ListenableRouter(DynamicConfiguration configuration, URL url, String ruleKey) {
super(configuration, url);
this.force = false;
this.init(ruleKey);
}
private synchronized void init(String ruleKey) {
if (StringUtils.isEmpty(ruleKey)) {
return;
}
String routerKey = ruleKey + RULE_SUFFIX;
configuration.addListener(routerKey, this);
//进入这getRule的方法
String rule = configuration.getRule(routerKey, DynamicConfiguration.DEFAULT_GROUP);
if (StringUtils.isNotEmpty(rule)) {
this.process(new ConfigChangeEvent(routerKey, rule));
}
}
这边就会根据上面拼接的ruleKey去dubbo/config/dubbo下找到相应配置文件名的路由配置
ConditionRouter.java
public void init(String rule) {
try {
if (rule == null || rule.trim().length() == 0) {
throw new IllegalArgumentException("Illegal route rule!");
}
rule = rule.replace("consumer.", "").replace("provider.", "");
int i = rule.indexOf("=>");
String whenRule = i < 0 ? null : rule.substring(0, i).trim();
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
// NOTE: It should be determined on the business level whether the `When condition` can be empty or not.
this.whenCondition = when;
this.thenCondition = then;
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
private static Map<String, MatchPair> parseRule(String rule)
throws ParseException {
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
if (StringUtils.isBlank(rule)) {
return condition;
}
// Key-Value pair, stores both match and mismatch conditions
MatchPair pair = null;
// Multiple values
Set<String> values = null;
final Matcher matcher = ROUTE_PATTERN.matcher(rule);
while (matcher.find()) { // Try to match one by one
String separator = matcher.group(1);
String content = matcher.group(2);
// Start part of the condition expression.
if (StringUtils.isEmpty(separator)) {
pair = new MatchPair();
condition.put(content, pair);
}
// The KV part of the condition expression
else if ("&".equals(separator)) {
if (condition.get(content) == null) {
pair = new MatchPair();
condition.put(content, pair);
} else {
pair = condition.get(content);
}
}
// The Value in the KV part.
else if ("=".equals(separator)) {
if (pair == null) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
values = pair.matches;
values.add(content);
}
// The Value in the KV part.
else if ("!=".equals(separator)) {
if (pair == null) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
values = pair.mismatches;
values.add(content);
}
// The Value in the KV part, if Value have more than one items.
else if (",".equals(separator)) { // Should be separated by ','
if (values == null || values.isEmpty()) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
values.add(content);
} else {
throw new ParseException("Illegal route rule \"" + rule
+ "\", The error char '" + separator + "' at index "
+ matcher.start() + " before \"" + content + "\".", matcher.start());
}
}
return condition;
}
至此,路由配置规则的读取完成
二、dubbo-admin路由规则的写入
这是一个非常大的bug,总之一句话,dubbo-admin开发者根本就没有对照着dubbo读取路由规则的ruleKey拼接方式来生成路径,因此导致dubbo根本就读不到规则。
本人重写了dubbo-admin写入规则的代码
1、ConditionRoutesController.java
@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public boolean createRule(@RequestBody ConditionRouteDTO routeDTO, @PathVariable String env) {
String serviceName = routeDTO.getService();
String app = routeDTO.getApplication();
if (StringUtils.isEmpty(serviceName) && StringUtils.isEmpty(app)) {
throw new ParamValidationException("serviceName and app is Empty!");
}
if (StringUtils.isNotEmpty(app) && providerService.findVersionInApplication(app).equals("2.6")) {
throw new VersionValidationException("dubbo 2.6 does not support application scope routing rule");
}
// routeService.createConditionRoute(routeDTO);
//按照上面的配置写入方法,dubbo根本就读不到相应的规则配置
//因此我写了下面的方法
routeService.createConditionRouteByServices(routeDTO);
return true;
}
请接着看
RouteServiceImpl.java
/**
* 这个方法有BUG,自写了一个
* @param conditionRoute
*/
@Override
public void createConditionRoute(ConditionRouteDTO conditionRoute) {
String id = ConvertUtil.getIdFromDTO(conditionRoute);
String path = getPath(id, Constants.CONDITION_ROUTE);
setRouterConfig(path,conditionRoute);
}
/**
* 这个方法是替代上面的
* 主要是生成路径名称的规则要跟dubbo保持一致,不然dubbo读不到
* @param conditionRoute
*/
@Override
public void createConditionRouteByServices(ConditionRouteDTO conditionRoute) {
//由于dubbo那边读取路由路径的规则是根据:
// 1、interface
// 2、version
// 3、group
//因此这里生成路由规则时,serviceId的拼接规则应该是interface:version:group
String serviceId = ConvertUtil.getIdFromDTO(conditionRoute);
String[] params = serviceId.split(":",3);
String serviceName = params[0];
String version = params[1];
String group = params[2];
List<Provider> services = providerService.findByService(serviceName);
conditionRoute.setService(serviceName);
for (Provider service : services) {
String path = getPathWithProvider(service,version,group,Constants.CONDITION_ROUTE);
if (StringUtils.isBlank(path)) {
continue;
}
System.out.println("path:" + path);
setRouterConfig(path,conditionRoute);
}
}
/**
* 由于dubbo获取路由的key是从provider的url上根据参数拼接的
* 所以dubbo-admin生成路由文件的路径名也得这样肝
* @param service
* @param version
* @param group
* @param type
* @return
*/
private String getPathWithProvider(Provider service,String version
, String group ,String type) {
URL url = service.toUrl();
if (!StringUtils.isBlank(version)
&& !version.equalsIgnoreCase(url.getParameter("version"))) {
//设置的version不为空并跟service的version不匹配,则跳过
return "";
}
if (!StringUtils.isBlank(group)
&& !group.equalsIgnoreCase(url.getParameter("group"))) {
//设置的group不为空并跟service的group不匹配,则跳过
return "";
}
String pathKey = url.getColonSeparatedKey();
if (type.equals(Constants.CONDITION_ROUTE)) {
return prefix + Constants.PATH_SEPARATOR + pathKey + Constants.CONDITION_RULE_SUFFIX;
} else {
return prefix + Constants.PATH_SEPARATOR + pathKey + Constants.TAG_RULE_SUFFIX;
}
}
/**
* 设置路由规则
* @param path
* @param conditionRoute
*/
private void setRouterConfig(String path,ConditionRouteDTO conditionRoute) {
String existConfig = dynamicConfiguration.getConfig(path);
RoutingRule existRule = null;
if (existConfig != null) {
existRule = YamlParser.loadObject(existConfig, RoutingRule.class);
}
existRule = RouteUtils.insertConditionRule(existRule, conditionRoute);
//register2.7
dynamicConfiguration.setConfig(path, YamlParser.dumpObject(existRule));
//register2.6
if (StringUtils.isNotEmpty(conditionRoute.getService())) {
for (Route old : convertRouteToOldRoute(conditionRoute)) {
registry.register(old.toUrl().addParameter(Constants.COMPATIBLE_CONFIG, true));
}
}
}
至此,bug修改结束
另外需要说明的时,按照这个修改方法,那么在dubbo-admin中编写规则的serviceId时,请按照interface:version:group的形式拼接