diff --git a/dubbo-cluster/pom.xml b/dubbo-cluster/pom.xml
index f7235cf7d6e..ca560b84b45 100644
--- a/dubbo-cluster/pom.xml
+++ b/dubbo-cluster/pom.xml
@@ -40,6 +40,10 @@
org.yaml
snakeyaml
+
+ org.apache.commons
+ commons-jexl3
+
org.apache.curator
curator-framework
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java
index c4936102f9d..febb480e089 100644
--- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java
@@ -120,6 +120,17 @@ public interface Constants {
*/
String EXPORT_KEY = "export";
+
+ /**
+ * Specify the expression context builder in order for evaluating the expression
+ */
+ String CONTEXT_BUILDER_KEY = "context.builder";
+
+ /**
+ * The default strategy name for context builder
+ */
+ String DEFAULT_CONTEXT_BUILDER = "default";
+
String PEER_KEY = "peer";
String CONSUMER_URL_KEY = "CONSUMER_URL";
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouter.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouter.java
new file mode 100644
index 00000000000..5ae597b4a53
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouter.java
@@ -0,0 +1,99 @@
+package org.apache.dubbo.rpc.cluster.router.expression;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.config.configcenter.ConfigChangeType;
+import org.apache.dubbo.common.config.configcenter.ConfigChangedEvent;
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.ExtensionLoader;
+import org.apache.dubbo.common.logger.Logger;
+import org.apache.dubbo.common.logger.LoggerFactory;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.config.ConfigurationUtils;
+import org.apache.dubbo.common.utils.Holder;
+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.expression.context.ContextBuilder;
+import org.apache.dubbo.rpc.cluster.router.expression.model.Rule;
+import org.apache.dubbo.rpc.cluster.router.expression.model.RuleSet;
+import org.apache.dubbo.rpc.cluster.router.expression.model.ExpressionRuleConstructor;
+
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.MapContext;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+import org.yaml.snakeyaml.Yaml;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+public class ExpressionRouter extends ObserverRouter {
+
+ public static final String NAME = "expression";
+
+ private static final Logger logger = LoggerFactory.getLogger(ExpressionRouter.class);
+
+ /**
+ * Store the mapping relations of provider/ruleSet.
+ */
+ private static final Map ruleSets = new ConcurrentHashMap<>();
+
+ private static final JexlEngine engine = new JexlBuilder().create();
+
+ private ContextBuilder contextBuilder;
+
+ public ExpressionRouter(URL url) {
+ super(url, url.getParameter(CommonConstants.APPLICATION_KEY));
+ contextBuilder = ExtensionLoader.getExtensionLoader(ContextBuilder.class)
+ .getExtension(url.getParameter(Constants.CONTEXT_BUILDER_KEY, Constants.DEFAULT_CONTEXT_BUILDER));
+ }
+
+ @Override
+ protected BitList> doRoute(BitList> invokers, URL url, Invocation invocation, boolean needToPrintMessage, Holder> nodeHolder, Holder messageHolder) throws RpcException {
+ String application = url.getParameter(CommonConstants.REMOTE_APPLICATION_KEY);
+ if(application == null){
+ return invokers;
+ }
+ RuleSet ruleSet = ruleSets.get(application);
+ if (logger.isTraceEnabled()) {
+ logger.trace(ruleSet.toString());
+ }
+ if (ruleSet != null && ruleSet.isEnabled()) {
+ JexlContext clientContext = new MapContext();
+ contextBuilder.buildClientContext(url, invocation).forEach(clientContext::set);
+ for (Rule rule : ruleSet.getRules()) {
+ Object clientQualified = engine.createExpression(rule.getClientCondition()).evaluate(clientContext);
+ if (clientQualified instanceof Boolean && (Boolean) clientQualified) {
+ List> result = invokers
+ .stream()
+ .filter(invoker -> matches(contextBuilder.buildServerContext(invoker, url, invocation), rule.getServerQuery()))
+ .collect(Collectors.toList());
+ if (CollectionUtils.isNotEmpty(result)) {
+ return result;
+ }
+ }
+ }
+ if (ruleSet.isDefaultRuleEnabled()) {
+ return invokers;
+ } else {
+ return new BitList>(new ArrayList<>());
+ }
+ }
+ return invokers;
+ }
+
+ public boolean matches(Map objects, String expression) {
+ JexlContext context = new MapContext();
+ objects.forEach(context::set);
+ Object qualified = engine.createExpression(expression).evaluate(context);
+ return qualified instanceof Boolean && (Boolean) qualified;
+ }
+
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouterFactory.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouterFactory.java
new file mode 100644
index 00000000000..92ee2e16c11
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouterFactory.java
@@ -0,0 +1,19 @@
+package org.apache.dubbo.rpc.cluster.router.expression;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.cluster.Router;
+import org.apache.dubbo.rpc.cluster.RouterFactory;
+import org.apache.dubbo.rpc.cluster.router.state.StateRouter;
+import org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory;
+
+@Activate
+public class ExpressionRouterFactory implements StateRouterFactory {
+
+ public static final String NAME = "expression";
+
+ @Override
+ public StateRouter getRouter(Class interfaceClass, URL url) {
+ return new ExpressionRouter(url);
+ }
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ObserverRouter.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ObserverRouter.java
new file mode 100644
index 00000000000..9fa8917b8a3
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/ObserverRouter.java
@@ -0,0 +1,29 @@
+package org.apache.dubbo.rpc.cluster.router.expression;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.config.configcenter.ConfigChangedEvent;
+import org.apache.dubbo.common.config.configcenter.ConfigurationListener;
+import org.apache.dubbo.common.config.configcenter.DynamicConfiguration;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter;
+
+public abstract class ObserverRouter extends AbstractStateRouter implements ConfigurationListener {
+ public static final String NAME = "OBSERVER_ROUTER";
+ private static final String RULE_SUFFIX = ".observer-router";
+
+ public ObserverRouter(URL url, String ruleKey) {
+ super(url);
+ this.init(ruleKey);
+ }
+
+ private synchronized void init(String ruleKey) {
+ if (StringUtils.isNotEmpty(ruleKey)) {
+ String routerKey = ruleKey + RULE_SUFFIX;
+ ruleRepository.addListener(routerKey, this);
+ String rule = ruleRepository.getRule(routerKey, DynamicConfiguration.DEFAULT_GROUP);
+ if (StringUtils.isNotEmpty(rule)) {
+ this.process(new ConfigChangedEvent(routerKey, DynamicConfiguration.DEFAULT_GROUP, rule));
+ }
+ }
+ }
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/context/ContextBuilder.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/context/ContextBuilder.java
new file mode 100644
index 00000000000..3b58577128f
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/context/ContextBuilder.java
@@ -0,0 +1,18 @@
+package org.apache.dubbo.rpc.cluster.router.expression.context;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.extension.SPI;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@SPI
+public interface ContextBuilder {
+
+ Map buildClientContext(URL url, Invocation invocation);
+
+ Map buildServerContext(Invoker invoker, URL url, Invocation invocation);
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/context/DefaultContextBuilder.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/context/DefaultContextBuilder.java
new file mode 100644
index 00000000000..1cb002b0f83
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/context/DefaultContextBuilder.java
@@ -0,0 +1,44 @@
+package org.apache.dubbo.rpc.cluster.router.expression.context;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The default context builder used for evaluating the expressions.
+ */
+@Activate
+public class DefaultContextBuilder implements ContextBuilder {
+
+ /**
+ * The object name of Client-Side
+ */
+ private static final String CLIENT_NAME = "c";
+ /**
+ * The object name of Request
+ */
+ private static final String REQUEST_NAME = "r";
+ /**
+ * The object name of Server-Side
+ */
+ private static final String SERVER_NAME = "s";
+
+ @Override
+ public Map buildClientContext(URL url, Invocation invocation) {
+ return CollectionUtils.toMap(REQUEST_NAME, invocation.getAttachments(), CLIENT_NAME, url.getParameters());
+ }
+
+ @Override
+ public Map buildServerContext(Invoker invoker, URL url, Invocation invocation) {
+ Map params = new HashMap<>(invoker.getUrl().getParameters());
+ params.put("port", invoker.getUrl().getPort());
+ params.put("address", invoker.getUrl().getAddress());
+ return Collections.singletonMap(SERVER_NAME, params);
+ }
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/ExpressionRuleConstructor.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/ExpressionRuleConstructor.java
new file mode 100644
index 00000000000..b9de1fd4009
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/ExpressionRuleConstructor.java
@@ -0,0 +1,43 @@
+package org.apache.dubbo.rpc.cluster.router.expression.model;
+
+import org.yaml.snakeyaml.TypeDescription;
+import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.Tag;
+
+import java.util.stream.Collectors;
+
+/**
+ * A yaml constructor for parsing RuleSets which should be a map.
+ */
+public class ExpressionRuleConstructor extends Constructor {
+
+ private TypeDescription itemType = new TypeDescription(RuleSet.class);
+
+ private static final String ROOT_NAME = "ruleSetRoot";
+
+ public ExpressionRuleConstructor() {
+ this.rootTag = new Tag(ROOT_NAME);
+ this.addTypeDescription(itemType);
+ }
+
+ @Override
+ protected Object constructObject(Node node) {
+ if (ROOT_NAME.equals(node.getTag().getValue()) && node instanceof MappingNode) {
+ MappingNode mNode = (MappingNode) node;
+ return mNode.getValue().stream().collect(
+ Collectors.toMap(
+ t -> super.constructObject(t.getKeyNode()),
+ t -> {
+ Node child = t.getValueNode();
+ child.setType(itemType.getType());
+ return super.constructObject(child);
+ }
+ )
+ );
+ } else {
+ return super.constructObject(node);
+ }
+ }
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/Rule.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/Rule.java
new file mode 100644
index 00000000000..9dbb31cb3b8
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/Rule.java
@@ -0,0 +1,39 @@
+package org.apache.dubbo.rpc.cluster.router.expression.model;
+
+/**
+ * A single rule which client prerequisite and server filter.
+ */
+public class Rule {
+
+ /**
+ * Somewhat like whenCondition in ConditionRouter.
+ * This is acted on client and the result should be true/false after evaluation.
+ */
+ private String clientCondition;
+ /**
+ * Somewhat like thenCondition in ConditionRouter.
+ * This is acted on server and the result should be server list after evaluation.
+ */
+ private String serverQuery;
+
+ public String getClientCondition() {
+ return clientCondition;
+ }
+
+ public void setClientCondition(String clientCondition) {
+ this.clientCondition = clientCondition;
+ }
+
+ public String getServerQuery() {
+ return serverQuery;
+ }
+
+ public void setServerQuery(String serverQuery) {
+ this.serverQuery = serverQuery;
+ }
+
+ public String toString(){
+ return "Rule(clientCondition=" + clientCondition
+ + ", serverQuery=" + serverQuery + ")";
+ }
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/RuleSet.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/RuleSet.java
new file mode 100644
index 00000000000..10aea57c474
--- /dev/null
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/expression/model/RuleSet.java
@@ -0,0 +1,56 @@
+package org.apache.dubbo.rpc.cluster.router.expression.model;
+
+import java.util.List;
+
+public class RuleSet {
+
+ /**
+ * Whether the ruleSet is enabled or not, set true as its default value.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Whether default rule is enabled, set false as its default value.
+ * This is useful when none of the provider is found after evaluating all the rules.
+ * If this is set to false, exception of no provider will be thrown.
+ * If this is set to true, all the left providers will be chosen, just like the rule of following:
+ * clientCondition: true
+ * serverQuery: true
+ */
+ private boolean defaultRuleEnabled;
+
+ /**
+ * The rules are in order. The top one will be evaluated in top priority.
+ */
+ private List rules;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isDefaultRuleEnabled() {
+ return defaultRuleEnabled;
+ }
+
+ public void setDefaultRuleEnabled(boolean defaultRuleEnabled) {
+ this.defaultRuleEnabled = defaultRuleEnabled;
+ }
+
+ public List getRules() {
+ return rules;
+ }
+
+ public void setRules(List rules) {
+ this.rules = rules;
+ }
+
+ public String toString(){
+ return "RuleSet(enabled=" + enabled
+ + ", defaultRuleEnabled=" + defaultRuleEnabled
+ + ",rules=" + rules + ")";
+ }
+}
diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java
index 2da97cb2e1a..a2fbcfb2fd9 100644
--- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java
+++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java
@@ -36,7 +36,7 @@ public abstract class AbstractStateRouter implements StateRouter {
private volatile URL url;
private volatile StateRouter nextRouter = null;
- private final GovernanceRuleRepository ruleRepository;
+ protected final GovernanceRuleRepository ruleRepository;
/**
* Should continue route if current router's result is empty
diff --git a/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.expression.context.ContextBuilder b/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.expression.context.ContextBuilder
new file mode 100644
index 00000000000..4d03eca1b52
--- /dev/null
+++ b/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.expression.context.ContextBuilder
@@ -0,0 +1 @@
+default=org.apache.dubbo.rpc.cluster.router.expression.context.DefaultContextBuilder
diff --git a/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory b/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
index 90fbccde211..d93c8ff5939 100644
--- a/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
+++ b/dubbo-cluster/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
@@ -4,3 +4,4 @@ service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceStateRouterF
app=org.apache.dubbo.rpc.cluster.router.condition.config.AppStateRouterFactory
mock=org.apache.dubbo.rpc.cluster.router.mock.MockStateRouterFactory
standard-mesh-rule=org.apache.dubbo.rpc.cluster.router.mesh.route.StandardMeshRuleRouterFactory
+expression=org.apache.dubbo.rpc.cluster.router.expression.ExpressionRouterFactory
diff --git a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouterTest.java b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouterTest.java
new file mode 100644
index 00000000000..b7659448d5d
--- /dev/null
+++ b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/expression/ExpressionRouterTest.java
@@ -0,0 +1,60 @@
+package org.apache.dubbo.rpc.cluster.router.expression;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.config.configcenter.ConfigChangedEvent;
+import org.apache.dubbo.common.config.configcenter.DynamicConfiguration;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.RpcInvocation;
+import org.apache.dubbo.rpc.cluster.router.MockInvoker;
+
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+import org.apache.dubbo.common.utils.Holder;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ExpressionRouterTest {
+
+ private static final String LOCAL_HOST = "127.0.0.1";
+ private static final String SERVICE = "/org.apache.dubbo.demo.DemoService";
+
+ @BeforeAll
+ public static void setUpBeforeClass() throws Exception {
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ }
+
+ @Test
+ public void testRoute(){
+ Invocation invocation = new RpcInvocation();
+ List> invokers = new ArrayList<>();
+ Invoker invoker1 = new MockInvoker(URL.valueOf("dubbo://" + LOCAL_HOST + ":20880/" + SERVICE));
+ Invoker invoker2 = new MockInvoker(URL.valueOf("dubbo://" + LOCAL_HOST + ":20881/" + SERVICE));
+ invokers.add(invoker1);
+ invokers.add(invoker2);
+ BitList> bitInvokers = new BitList<>(invokers);
+
+
+ String params = "?remote.application=dubbo-demo-annotation-provider&application=dubbo-demo-annotation-consumer";
+ String consumer = "consumer://" + LOCAL_HOST + SERVICE + params;
+
+ ObserverRouter router = (ObserverRouter)new ExpressionRouterFactory().getRouter(String.class, URL.valueOf(consumer));
+
+ BitList fileredInvokers = router.route(bitInvokers.clone(), URL.valueOf(consumer), invocation, false, new Holder<>());
+
+ //WHY the following line throws NullPointerException, Hard to understand, it runs well in my local env @Fixme
+// List> result = router.route(invokers, URL.valueOf(consumer), invocation);
+//
+// Assertions.assertEquals(1, result.size());
+// Assertions.assertEquals("20880", result.get(0).getUrl().getPort() + "");
+ //un-comment the above lines when fixed.
+ Assertions.assertEquals(2, fileredInvokers.size()); //Since the above error, add this un-useful line
+ }
+}