| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.dubbo.rpc.cluster.router.condition; |
| |
| import org.apache.dubbo.common.URL; |
| import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; |
| import org.apache.dubbo.common.logger.LoggerFactory; |
| import org.apache.dubbo.common.utils.CollectionUtils; |
| import org.apache.dubbo.common.utils.Holder; |
| import org.apache.dubbo.common.utils.NetUtils; |
| import org.apache.dubbo.common.utils.StringUtils; |
| import org.apache.dubbo.common.utils.UrlUtils; |
| import org.apache.dubbo.rpc.Invocation; |
| import org.apache.dubbo.rpc.Invoker; |
| import org.apache.dubbo.rpc.RpcException; |
| import org.apache.dubbo.rpc.cluster.Constants; |
| import org.apache.dubbo.rpc.cluster.router.RouterSnapshotNode; |
| import org.apache.dubbo.rpc.cluster.router.condition.config.AppStateRouter; |
| import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter; |
| import org.apache.dubbo.rpc.cluster.router.state.BitList; |
| |
| import java.text.ParseException; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static org.apache.dubbo.common.constants.CommonConstants.ENABLED_KEY; |
| import static org.apache.dubbo.common.constants.CommonConstants.HOST_KEY; |
| import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY; |
| import static org.apache.dubbo.common.constants.CommonConstants.METHOD_KEY; |
| import static org.apache.dubbo.common.constants.LoggerCodeConstants.CLUSTER_CONDITIONAL_ROUTE_LIST_EMPTY; |
| import static org.apache.dubbo.common.constants.LoggerCodeConstants.CLUSTER_FAILED_EXEC_CONDITION_ROUTER; |
| import static org.apache.dubbo.rpc.cluster.Constants.ADDRESS_KEY; |
| import static org.apache.dubbo.rpc.cluster.Constants.FORCE_KEY; |
| import static org.apache.dubbo.rpc.cluster.Constants.RULE_KEY; |
| import static org.apache.dubbo.rpc.cluster.Constants.RUNTIME_KEY; |
| |
| /** |
| * ConditionRouter |
| * It supports the conditional routing configured by "override://", in 2.6.x, |
| * refer to https://dubbo.apache.org/en/docs/v2.7/user/examples/routing-rule/ . |
| * For 2.7.x and later, please refer to {@link org.apache.dubbo.rpc.cluster.router.condition.config.ServiceStateRouter} |
| * and {@link AppStateRouter} |
| * refer to https://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/ . |
| */ |
| public class ConditionStateRouter<T> extends AbstractStateRouter<T> { |
| public static final String NAME = "condition"; |
| |
| private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(AbstractStateRouter.class); |
| protected static final Pattern ROUTE_PATTERN = Pattern.compile("([&!=,]*)\\s*([^&!=,\\s]+)"); |
| protected static Pattern ARGUMENTS_PATTERN = Pattern.compile("arguments\\[([0-9]+)\\]"); |
| protected Map<String, MatchPair> whenCondition; |
| protected Map<String, MatchPair> thenCondition; |
| |
| private boolean enabled; |
| |
| public ConditionStateRouter(URL url, String rule, boolean force, boolean enabled) { |
| super(url); |
| this.setForce(force); |
| this.enabled = enabled; |
| if (enabled) { |
| this.init(rule); |
| } |
| } |
| |
| public ConditionStateRouter(URL url) { |
| super(url); |
| this.setUrl(url); |
| this.setForce(url.getParameter(FORCE_KEY, false)); |
| this.enabled = url.getParameter(ENABLED_KEY, true); |
| if (enabled) { |
| init(url.getParameterAndDecoded(RULE_KEY)); |
| } |
| } |
| |
| 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; |
| } |
| |
| @Override |
| protected BitList<Invoker<T>> doRoute(BitList<Invoker<T>> invokers, URL url, Invocation invocation, |
| boolean needToPrintMessage, Holder<RouterSnapshotNode<T>> nodeHolder, |
| Holder<String> messageHolder) throws RpcException { |
| if (!enabled) { |
| if (needToPrintMessage) { |
| messageHolder.set("Directly return. Reason: ConditionRouter disabled."); |
| } |
| return invokers; |
| } |
| |
| if (CollectionUtils.isEmpty(invokers)) { |
| if (needToPrintMessage) { |
| messageHolder.set("Directly return. Reason: Invokers from previous router is empty."); |
| } |
| return invokers; |
| } |
| try { |
| if (!matchWhen(url, invocation)) { |
| if (needToPrintMessage) { |
| messageHolder.set("Directly return. Reason: WhenCondition not match."); |
| } |
| return invokers; |
| } |
| if (thenCondition == null) { |
| logger.warn(CLUSTER_CONDITIONAL_ROUTE_LIST_EMPTY,"condition state router thenCondition is empty","","The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey()); if (needToPrintMessage) { |
| messageHolder.set("Empty return. Reason: ThenCondition is empty."); |
| } |
| return BitList.emptyList(); |
| } |
| BitList<Invoker<T>> result = invokers.clone(); |
| result.removeIf(invoker -> !matchThen(invoker.getUrl(), url)); |
| |
| if (!result.isEmpty()) { |
| if (needToPrintMessage) { |
| messageHolder.set("Match return."); |
| } |
| return result; |
| } else if (this.isForce()) { |
| logger.warn(CLUSTER_CONDITIONAL_ROUTE_LIST_EMPTY,"execute condition state router result list is empty. and force=true","","The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(RULE_KEY)); |
| if (needToPrintMessage) { |
| messageHolder.set("Empty return. Reason: Empty result from condition and condition is force."); |
| } |
| return result; |
| } |
| } catch (Throwable t) { |
| logger.error(CLUSTER_FAILED_EXEC_CONDITION_ROUTER,"execute condition state router exception","","Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(),t); |
| } |
| if (needToPrintMessage) { |
| messageHolder.set("Directly return. Reason: Error occurred ( or result is empty )."); |
| } |
| return invokers; |
| } |
| |
| @Override |
| public boolean isRuntime() { |
| // We always return true for previously defined Router, that is, old Router doesn't support cache anymore. |
| // return true; |
| return this.getUrl().getParameter(RUNTIME_KEY, false); |
| } |
| |
| boolean matchWhen(URL url, Invocation invocation) { |
| return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation); |
| } |
| |
| private boolean matchThen(URL url, URL param) { |
| return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null); |
| } |
| |
| private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) { |
| Map<String, String> sample = url.toMap(); |
| boolean result = false; |
| for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) { |
| String key = matchPair.getKey(); |
| |
| if (key.startsWith(Constants.ARGUMENTS)) { |
| if (!matchArguments(matchPair, invocation)) { |
| return false; |
| } else { |
| result = true; |
| continue; |
| } |
| } |
| |
| String sampleValue; |
| //get real invoked method name from invocation |
| if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) { |
| sampleValue = invocation.getMethodName(); |
| } else if (ADDRESS_KEY.equals(key)) { |
| sampleValue = url.getAddress(); |
| } else if (HOST_KEY.equals(key)) { |
| sampleValue = url.getHost(); |
| } else { |
| sampleValue = sample.get(key); |
| } |
| if (sampleValue != null) { |
| if (!matchPair.getValue().isMatch(sampleValue, param)) { |
| return false; |
| } else { |
| result = true; |
| } |
| } else { |
| //not pass the condition |
| if (!matchPair.getValue().matches.isEmpty()) { |
| return false; |
| } else { |
| result = true; |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * analysis the arguments in the rule. |
| * Examples would be like this: |
| * "arguments[0]=1", whenCondition is that the first argument is equal to '1'. |
| * "arguments[1]=a", whenCondition is that the second argument is equal to 'a'. |
| * @param matchPair |
| * @param invocation |
| * @return |
| */ |
| public boolean matchArguments(Map.Entry<String, MatchPair> matchPair, Invocation invocation) { |
| try { |
| // split the rule |
| String key = matchPair.getKey(); |
| String[] expressArray = key.split("\\."); |
| String argumentExpress = expressArray[0]; |
| final Matcher matcher = ARGUMENTS_PATTERN.matcher(argumentExpress); |
| if (!matcher.find()) { |
| return false; |
| } |
| |
| //extract the argument index |
| int index = Integer.parseInt(matcher.group(1)); |
| if (index < 0 || index > invocation.getArguments().length) { |
| return false; |
| } |
| |
| //extract the argument value |
| Object object = invocation.getArguments()[index]; |
| |
| if (matchPair.getValue().isMatch(String.valueOf(object), null)) { |
| return true; |
| } |
| } catch (Exception e) { |
| logger.warn(CLUSTER_FAILED_EXEC_CONDITION_ROUTER,"condition state router arguments match failed","","Arguments match failed, matchPair[]" + matchPair + "] invocation[" + invocation + "]",e); |
| } |
| |
| return false; |
| } |
| |
| protected static final class MatchPair { |
| final Set<String> matches = new HashSet<String>(); |
| final Set<String> mismatches = new HashSet<String>(); |
| |
| private boolean isMatch(String value, URL param) { |
| if (!matches.isEmpty() && mismatches.isEmpty()) { |
| for (String match : matches) { |
| if (UrlUtils.isMatchGlobPattern(match, value, param)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| if (!mismatches.isEmpty() && matches.isEmpty()) { |
| for (String mismatch : mismatches) { |
| if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| if (!matches.isEmpty() && !mismatches.isEmpty()) { |
| //when both mismatches and matches contain the same value, then using mismatches first |
| for (String mismatch : mismatches) { |
| if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) { |
| return false; |
| } |
| } |
| for (String match : matches) { |
| if (UrlUtils.isMatchGlobPattern(match, value, param)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| return false; |
| } |
| } |
| } |