This is an automated email from the ASF dual-hosted git repository.

albumenj pushed a commit to branch 3.2
in repository https://gitbox.apache.org/repos/asf/dubbo.git


The following commit(s) were added to refs/heads/3.2 by this push:
     new f95e659eef Feat xdsroute parse (#10956)
f95e659eef is described below

commit f95e659eeff52eeba16a94ccf792535acc530c02
Author: haoyann <[email protected]>
AuthorDate: Fri Dec 9 16:31:40 2022 +0800

    Feat xdsroute parse (#10956)
    
    Co-authored-by: Albumen Kevin <[email protected]>
---
 .../cluster/router/xds/EdsEndpointListener.java    |  28 ++
 .../rpc/cluster/router/xds/EdsEndpointManager.java | 114 +++++++
 .../cluster/router/xds/RdsRouteRuleManager.java    | 116 +++++++
 .../cluster/router/xds/RdsVirtualHostListener.java | 177 ++++++++++
 .../cluster/router/xds/XdsRouteRuleListener.java   |  30 ++
 .../dubbo/rpc/cluster/router/xds/XdsRouter.java    | 366 +++++++++++++++++++++
 .../rpc/cluster/router/xds/XdsRouterFactory.java   |  31 ++
 .../router/xds/XdsScopeModelInitializer.java       |  42 +++
 .../rpc/cluster/router/xds/rule/ClusterWeight.java |  38 +++
 .../cluster/router/xds/rule/DestinationSubset.java |  58 ++++
 .../router/xds/rule/HTTPRouteDestination.java      |  43 +++
 .../rpc/cluster/router/xds/rule/HeaderMatcher.java | 123 +++++++
 .../cluster/router/xds/rule/HttpRequestMatch.java  |  41 +++
 .../cluster/router/xds/rule/LongRangeMatch.java    |  49 +++
 .../rpc/cluster/router/xds/rule/PathMatcher.java   |  74 +++++
 .../rpc/cluster/router/xds/rule/XdsRouteRule.java  |  41 +++
 ...bbo.rpc.cluster.router.state.StateRouterFactory |   1 +
 ...rg.apache.dubbo.rpc.model.ScopeModelInitializer |   1 +
 .../cluster/router/xds/EdsEndpointManagerTest.java | 116 +++++++
 .../router/xds/RdsRouteRuleManagerTest.java        | 139 ++++++++
 .../router/xds/RdsVirtualHostListenerTest.java     | 231 +++++++++++++
 .../dubbo/rpc/cluster/router/xds/XdsRouteTest.java | 363 ++++++++++++++++++++
 .../cluster/router/xds/rule/HeaderMatcherTest.java |  92 ++++++
 .../cluster/router/xds/rule/PathMatcherTest.java   |  60 ++++
 24 files changed, 2374 insertions(+)

diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java
new file mode 100644
index 0000000000..0416236166
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java
@@ -0,0 +1,28 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+
+import java.util.Set;
+
+
+public interface EdsEndpointListener {
+
+    void onEndPointChange(String cluster, Set<Endpoint> endpoints);
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java
new file mode 100644
index 0000000000..e29f9becc8
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java
@@ -0,0 +1,114 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+public class EdsEndpointManager {
+
+    private static final ConcurrentHashMap<String, Set<EdsEndpointListener>> 
ENDPOINT_LISTENERS = new ConcurrentHashMap<>();
+
+    private static final ConcurrentHashMap<String, Set<Endpoint>> 
ENDPOINT_DATA_CACHE = new ConcurrentHashMap<>();
+
+    private static final ConcurrentHashMap<String, Consumer<Set<Endpoint>>> 
EDS_LISTENERS = new ConcurrentHashMap<>();
+
+
+    public EdsEndpointManager() {
+    }
+
+    public synchronized void subscribeEds(String cluster, EdsEndpointListener 
listener) {
+
+        Set<EdsEndpointListener> listeners = 
ENDPOINT_LISTENERS.computeIfAbsent(cluster, key ->
+            new ConcurrentHashSet<>()
+        );
+        if (CollectionUtils.isEmpty(listeners)) {
+            doSubscribeEds(cluster);
+        }
+        listeners.add(listener);
+
+        if (ENDPOINT_DATA_CACHE.containsKey(cluster)) {
+            listener.onEndPointChange(cluster, 
ENDPOINT_DATA_CACHE.get(cluster));
+        }
+    }
+
+    private void doSubscribeEds(String cluster) {
+        EDS_LISTENERS.computeIfAbsent(cluster, key -> endpoints -> {
+            notifyEndpointChange(cluster, endpoints);
+        });
+        Consumer<Set<Endpoint>> consumer = EDS_LISTENERS.get(cluster);
+
+        //todo control plane subscribe eds
+    }
+
+    public synchronized void unSubscribeEds(String cluster, 
EdsEndpointListener listener) {
+        Set<EdsEndpointListener> listeners = ENDPOINT_LISTENERS.get(cluster);
+        if (CollectionUtils.isEmpty(listeners)) {
+            return;
+        }
+        listeners.remove(listener);
+        if (listeners.isEmpty()) {
+            ENDPOINT_LISTENERS.remove(cluster);
+            doUnsubscribeEds(cluster);
+        }
+    }
+
+    private void doUnsubscribeEds(String cluster) {
+        Consumer<Set<Endpoint>> consumer = EDS_LISTENERS.remove(cluster);
+
+        if (consumer != null) {
+
+            //todo control plane unsubscribe eds
+        }
+        ENDPOINT_DATA_CACHE.remove(cluster);
+    }
+
+
+    public void notifyEndpointChange(String cluster, Set<Endpoint> endpoints) {
+
+        ENDPOINT_DATA_CACHE.put(cluster, endpoints);
+
+        Set<EdsEndpointListener> listeners = ENDPOINT_LISTENERS.get(cluster);
+        if (CollectionUtils.isEmpty(listeners)) {
+            return;
+        }
+        for (EdsEndpointListener listener : listeners) {
+            listener.onEndPointChange(cluster, endpoints);
+        }
+    }
+
+    // for test
+    static ConcurrentHashMap<String, Set<EdsEndpointListener>> 
getEndpointListeners() {
+        return ENDPOINT_LISTENERS;
+    }
+
+    // for test
+    static ConcurrentHashMap<String, Set<Endpoint>> getEndpointDataCache() {
+        return ENDPOINT_DATA_CACHE;
+    }
+
+    // for test
+    static ConcurrentHashMap<String, Consumer<Set<Endpoint>>> 
getEdsListeners() {
+        return EDS_LISTENERS;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java
new file mode 100644
index 0000000000..0d60c20c0f
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java
@@ -0,0 +1,116 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class RdsRouteRuleManager {
+
+
+    private static final ConcurrentHashMap<String, Set<XdsRouteRuleListener>> 
RULE_LISTENERS = new ConcurrentHashMap<>();
+
+    private static final ConcurrentHashMap<String, List<XdsRouteRule>> 
ROUTE_DATA_CACHE = new ConcurrentHashMap<>();
+
+    private static final ConcurrentHashMap<String, RdsVirtualHostListener> 
RDS_LISTENERS = new ConcurrentHashMap<>();
+
+    public RdsRouteRuleManager() {
+    }
+
+    public synchronized void subscribeRds(String domain, XdsRouteRuleListener 
listener) {
+
+        Set<XdsRouteRuleListener> listeners = 
RULE_LISTENERS.computeIfAbsent(domain, key ->
+            new ConcurrentHashSet<>()
+        );
+        if (CollectionUtils.isEmpty(listeners)) {
+            doSubscribeRds(domain);
+        }
+        listeners.add(listener);
+
+        if (ROUTE_DATA_CACHE.containsKey(domain)) {
+            listener.onRuleChange(domain, ROUTE_DATA_CACHE.get(domain));
+        }
+    }
+
+    private void doSubscribeRds(String domain) {
+        RDS_LISTENERS.computeIfAbsent(domain, key -> new 
RdsVirtualHostListener(domain, this));
+        RdsVirtualHostListener rdsVirtualHostListener = 
RDS_LISTENERS.get(domain);
+        // todo request control plane subscribe rds
+    }
+
+    public synchronized void unSubscribeRds(String domain, 
XdsRouteRuleListener listener) {
+        Set<XdsRouteRuleListener> listeners = RULE_LISTENERS.get(domain);
+        if (CollectionUtils.isEmpty(listeners)) {
+            return;
+        }
+        listeners.remove(listener);
+        if (listeners.isEmpty()) {
+            RULE_LISTENERS.remove(domain);
+            doUnsubscribeRds(domain);
+        }
+    }
+
+    private void doUnsubscribeRds(String domain) {
+        RdsVirtualHostListener rdsVirtualHostListener = 
RDS_LISTENERS.remove(domain);
+
+        if (rdsVirtualHostListener != null) {
+
+            // todo request control plane unsubscribe rds
+        }
+        ROUTE_DATA_CACHE.remove(domain);
+    }
+
+
+    public void notifyRuleChange(String domain, List<XdsRouteRule> 
xdsRouteRules) {
+
+        ROUTE_DATA_CACHE.put(domain, xdsRouteRules);
+
+        Set<XdsRouteRuleListener> listeners = RULE_LISTENERS.get(domain);
+        if (CollectionUtils.isEmpty(listeners)) {
+            return;
+        }
+        boolean empty = CollectionUtils.isEmpty(xdsRouteRules);
+        for (XdsRouteRuleListener listener : listeners) {
+            if (empty) {
+                listener.clearRule(domain);
+            } else {
+                listener.onRuleChange(domain, xdsRouteRules);
+            }
+        }
+    }
+
+    // for test
+    static ConcurrentHashMap<String, Set<XdsRouteRuleListener>> 
getRuleListeners() {
+        return RULE_LISTENERS;
+    }
+
+    // for test
+    static ConcurrentHashMap<String, List<XdsRouteRule>> getRouteDataCache() {
+        return ROUTE_DATA_CACHE;
+    }
+
+    // for test
+    static ConcurrentHashMap<String, RdsVirtualHostListener> getRdsListeners() 
{
+        return RDS_LISTENERS;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java
new file mode 100644
index 0000000000..b29e21626a
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java
@@ -0,0 +1,177 @@
+/*
+ * 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.xds;
+
+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.rpc.cluster.router.xds.rule.ClusterWeight;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.LongRangeMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.PathMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import io.envoyproxy.envoy.config.route.v3.Route;
+import io.envoyproxy.envoy.config.route.v3.RouteAction;
+import io.envoyproxy.envoy.config.route.v3.RouteMatch;
+import io.envoyproxy.envoy.config.route.v3.VirtualHost;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class RdsVirtualHostListener {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(RdsVirtualHostListener.class);
+
+    private final String domain;
+
+    private final RdsRouteRuleManager routeRuleManager;
+
+
+    public RdsVirtualHostListener(String domain, RdsRouteRuleManager 
routeRuleManager) {
+        this.domain = domain;
+        this.routeRuleManager = routeRuleManager;
+    }
+
+    public void parseVirtualHost(VirtualHost virtualHost) {
+        if (virtualHost == null || 
CollectionUtils.isEmpty(virtualHost.getRoutesList())) {
+            // post empty
+            routeRuleManager.notifyRuleChange(domain, new ArrayList<>());
+            return;
+        }
+        try {
+            List<XdsRouteRule> xdsRouteRules = 
virtualHost.getRoutesList().stream().map(route -> {
+                if (route.getMatch().getQueryParametersCount() != 0) {
+                    return null;
+                }
+                HttpRequestMatch match = parseMatch(route.getMatch());
+                HTTPRouteDestination action = parseAction(route);
+                return new XdsRouteRule(match, action);
+            }).filter(Objects::nonNull).collect(Collectors.toList());
+            // post rules
+            routeRuleManager.notifyRuleChange(domain, xdsRouteRules);
+        } catch (Exception e) {
+            LOGGER.error("parse domain: " + domain + " xds VirtualHost error", 
e);
+        }
+
+    }
+
+    private HttpRequestMatch parseMatch(RouteMatch match) {
+        PathMatcher pathMatcher = parsePathMatch(match);
+        List<HeaderMatcher> headerMatchers = parseHeadMatch(match);
+        return new HttpRequestMatch(pathMatcher, headerMatchers);
+    }
+
+    private PathMatcher parsePathMatch(RouteMatch match) {
+        boolean caseSensitive = match.getCaseSensitive().getValue();
+        PathMatcher pathMatcher = new PathMatcher();
+        pathMatcher.setCaseSensitive(caseSensitive);
+        switch (match.getPathSpecifierCase()) {
+            case PREFIX:
+                pathMatcher.setPrefix(match.getPrefix());
+                return pathMatcher;
+            case PATH:
+                pathMatcher.setPath(match.getPath());
+                return pathMatcher;
+            case SAFE_REGEX:
+                String regex = match.getSafeRegex().getRegex();
+                pathMatcher.setRegex(regex);
+                return pathMatcher;
+            case PATHSPECIFIER_NOT_SET:
+                return null;
+            default:
+                throw new IllegalArgumentException("Path specifier is not 
expect");
+        }
+    }
+
+    private List<HeaderMatcher> parseHeadMatch(RouteMatch routeMatch) {
+        List<HeaderMatcher> headerMatchers = new ArrayList<>();
+        List<io.envoyproxy.envoy.config.route.v3.HeaderMatcher> headersList = 
routeMatch.getHeadersList();
+        for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher headerMatcher : 
headersList) {
+            HeaderMatcher matcher = new HeaderMatcher();
+            matcher.setName(headerMatcher.getName());
+            matcher.setInverted(headerMatcher.getInvertMatch());
+            switch (headerMatcher.getHeaderMatchSpecifierCase()) {
+                case EXACT_MATCH:
+                    matcher.setExactValue(headerMatcher.getExactMatch());
+                    headerMatchers.add(matcher);
+                    break;
+                case SAFE_REGEX_MATCH:
+                    
matcher.setRegex(headerMatcher.getSafeRegexMatch().getRegex());
+                    headerMatchers.add(matcher);
+                    break;
+                case RANGE_MATCH:
+                    LongRangeMatch rang = new LongRangeMatch();
+                    rang.setStart(headerMatcher.getRangeMatch().getStart());
+                    rang.setEnd(headerMatcher.getRangeMatch().getEnd());
+                    matcher.setRange(rang);
+                    headerMatchers.add(matcher);
+                    break;
+                case PRESENT_MATCH:
+                    matcher.setPresent(headerMatcher.getPresentMatch());
+                    headerMatchers.add(matcher);
+                    break;
+                case PREFIX_MATCH:
+                    matcher.setPrefix(headerMatcher.getPrefixMatch());
+                    headerMatchers.add(matcher);
+                    break;
+                case SUFFIX_MATCH:
+                    matcher.setSuffix(headerMatcher.getSuffixMatch());
+                    headerMatchers.add(matcher);
+                    break;
+                case HEADERMATCHSPECIFIER_NOT_SET:
+                default:
+                    throw new IllegalArgumentException("Header specifier is 
not expect");
+            }
+        }
+        return headerMatchers;
+    }
+
+    private HTTPRouteDestination parseAction(Route route) {
+        switch (route.getActionCase()) {
+            case ROUTE:
+                HTTPRouteDestination httpRouteDestination = new 
HTTPRouteDestination();
+                // only support cluster and weight cluster
+                RouteAction routeAction = route.getRoute();
+                RouteAction.ClusterSpecifierCase clusterSpecifierCase = 
routeAction.getClusterSpecifierCase();
+                if (clusterSpecifierCase == 
RouteAction.ClusterSpecifierCase.CLUSTER) {
+                    httpRouteDestination.setCluster(routeAction.getCluster());
+                    return httpRouteDestination;
+                } else if (clusterSpecifierCase == 
RouteAction.ClusterSpecifierCase.WEIGHTED_CLUSTERS) {
+                    List<ClusterWeight> clusterWeights = 
routeAction.getWeightedClusters().
+                        getClustersList().stream()
+                        .map(c -> new ClusterWeight(c.getName(), 
c.getWeight().getValue()))
+                        .sorted(Comparator.comparing(ClusterWeight::getWeight))
+                        .collect(Collectors.toList());
+                    httpRouteDestination.setWeightedClusters(clusterWeights);
+                    return httpRouteDestination;
+                }
+            case REDIRECT:
+            case DIRECT_RESPONSE:
+            case FILTER_ACTION:
+            case ACTION_NOT_SET:
+            default:
+                throw new IllegalArgumentException("Cluster specifier is not 
expect");
+        }
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java
new file mode 100644
index 0000000000..bc2c5262ca
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java
@@ -0,0 +1,30 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import java.util.List;
+
+public interface XdsRouteRuleListener {
+
+
+    void onRuleChange(String appName, List<XdsRouteRule> xdsRouteRules);
+
+    void clearRule(String appName);
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java
new file mode 100644
index 0000000000..17a4af0f85
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java
@@ -0,0 +1,366 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.common.utils.Holder;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.RpcException;
+import org.apache.dubbo.rpc.cluster.router.RouterSnapshotNode;
+import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.DestinationSubset;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.PathMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+public class XdsRouter<T> extends AbstractStateRouter<T> implements 
XdsRouteRuleListener, EdsEndpointListener {
+
+    private Set<String> subscribeApplications;
+
+    private final ConcurrentHashMap<String, List<XdsRouteRule>> 
xdsRouteRuleMap;
+
+    private final ConcurrentHashMap<String, DestinationSubset<T>> 
destinationSubsetMap;
+
+    private final RdsRouteRuleManager rdsRouteRuleManager;
+
+    private final EdsEndpointManager edsEndpointManager;
+
+    private volatile BitList<Invoker<T>> currentInvokeList;
+
+    private static final String BINARY_HEADER_SUFFIX = "-bin";
+
+    public XdsRouter(URL url) {
+        super(url);
+        rdsRouteRuleManager = 
url.getOrDefaultApplicationModel().getBeanFactory().getBean(RdsRouteRuleManager.class);
+        edsEndpointManager = 
url.getOrDefaultApplicationModel().getBeanFactory().getBean(EdsEndpointManager.class);
+        subscribeApplications = new ConcurrentHashSet<>();
+        destinationSubsetMap = new ConcurrentHashMap<>();
+        xdsRouteRuleMap = new ConcurrentHashMap<>();
+        currentInvokeList = new BitList<>(new ArrayList<>());
+    }
+
+    /**
+     * @deprecated only for uts
+     */
+    protected XdsRouter(URL url, RdsRouteRuleManager rdsRouteRuleManager, 
EdsEndpointManager edsEndpointManager) {
+        super(url);
+        this.rdsRouteRuleManager = rdsRouteRuleManager;
+        this.edsEndpointManager = edsEndpointManager;
+        subscribeApplications = new ConcurrentHashSet<>();
+        destinationSubsetMap = new ConcurrentHashMap<>();
+        xdsRouteRuleMap = new ConcurrentHashMap<>();
+        currentInvokeList = new BitList<>(new ArrayList<>());
+    }
+
+    @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 (CollectionUtils.isEmpty(invokers)) {
+            if (needToPrintMessage) {
+                messageHolder.set("Directly Return. Reason: Invokers from 
previous router is empty.");
+            }
+            return invokers;
+        }
+
+        if (CollectionUtils.isEmptyMap(xdsRouteRuleMap)) {
+            if (needToPrintMessage) {
+                messageHolder.set("Directly Return. Reason: xds route rule is 
empty.");
+            }
+            return invokers;
+        }
+
+        StringBuilder stringBuilder = needToPrintMessage ? new StringBuilder() 
: null;
+
+        // find match cluster
+        String matchCluster = null;
+        Set<String> appNames = subscribeApplications;
+        for (String subscribeApplication : appNames) {
+            List<XdsRouteRule> rules = 
xdsRouteRuleMap.get(subscribeApplication);
+            if (CollectionUtils.isEmpty(rules)) {
+                continue;
+            }
+            for (XdsRouteRule rule : rules) {
+                String cluster = computeMatchCluster(invocation, rule);
+                if (cluster != null) {
+                    matchCluster = cluster;
+                    break;
+                }
+            }
+            if (matchCluster != null) {
+                if (stringBuilder != null) {
+                    stringBuilder.append("Match App: 
").append(subscribeApplication).append(" Cluster: 
").append(matchCluster).append(' ');
+                }
+                break;
+            }
+        }
+        // not match request just return
+        if (matchCluster == null) {
+            if (needToPrintMessage) {
+                messageHolder.set("Directly Return. Reason: xds rule not 
match.");
+            }
+            return invokers;
+        }
+        DestinationSubset<T> destinationSubset = 
destinationSubsetMap.get(matchCluster);
+        // cluster no target provider
+        if (destinationSubset == null) {
+            if (needToPrintMessage) {
+                messageHolder.set(stringBuilder.append("no target 
subset").toString());
+            }
+            return BitList.emptyList();
+        }
+        if (needToPrintMessage) {
+            messageHolder.set(stringBuilder.toString());
+        }
+        if (destinationSubset.getInvokers() == null) {
+            return BitList.emptyList();
+        }
+
+        return destinationSubset.getInvokers().and(invokers);
+    }
+
+    private String computeMatchCluster(Invocation invocation, XdsRouteRule 
rule) {
+        // compute request match cluster
+        HttpRequestMatch requestMatch = rule.getMatch();
+        if (requestMatch.getPathMatcher() == null && 
CollectionUtils.isEmpty(requestMatch.getHeaderMatcherList())) {
+            return null;
+        }
+        PathMatcher pathMatcher = requestMatch.getPathMatcher();
+        if (pathMatcher != null) {
+            String path = "/" + invocation.getInvoker().getUrl().getPath() + 
"/" + invocation.getMethodName();
+            if (!pathMatcher.isMatch(path)) {
+                return null;
+            }
+        }
+        List<HeaderMatcher> headerMatchers = 
requestMatch.getHeaderMatcherList();
+        for (HeaderMatcher headerMatcher : headerMatchers) {
+            String headerName = headerMatcher.getName();
+            // not support byte
+            if (headerName.endsWith(BINARY_HEADER_SUFFIX)) {
+                return null;
+            }
+            String headValue = invocation.getAttachment(headerName);
+            if (!headerMatcher.match(headValue)) {
+                return null;
+            }
+        }
+        HTTPRouteDestination route = rule.getRoute();
+        if (route.getCluster() != null) {
+            return route.getCluster();
+        }
+        return computeWeightCluster(route.getWeightedClusters());
+    }
+
+    private String computeWeightCluster(List<ClusterWeight> weightedClusters) {
+        int totalWeight = 
Math.max(weightedClusters.stream().mapToInt(ClusterWeight::getWeight).sum(), 1);
+        // target must greater than 0
+        // if weight is 0, the destination will not receive any traffic.
+        int target = ThreadLocalRandom.current().nextInt(1, totalWeight + 1);
+        for (ClusterWeight weightedCluster : weightedClusters) {
+            int weight = weightedCluster.getWeight();
+            target -= weight;
+            if (target <= 0) {
+                return weightedCluster.getName();
+            }
+        }
+        return null;
+    }
+
+    public void notify(BitList<Invoker<T>> invokers) {
+        BitList<Invoker<T>> invokerList = invokers == null ? 
BitList.emptyList() : invokers;
+        currentInvokeList = invokerList.clone();
+
+        // compute need subscribe/unsubscribe rds application
+        Set<String> currentApplications = new HashSet<>();
+        for (Invoker<T> invoker : invokerList) {
+            String applicationName = invoker.getUrl().getRemoteApplication();
+            if (StringUtils.isNotEmpty(applicationName)) {
+                currentApplications.add(applicationName);
+            }
+        }
+
+        if (!subscribeApplications.equals(currentApplications)) {
+            synchronized (this) {
+                for (String currentApplication : currentApplications) {
+                    if (!subscribeApplications.contains(currentApplication)) {
+                        rdsRouteRuleManager.subscribeRds(currentApplication, 
this);
+                    }
+                }
+                for (String preApplication : subscribeApplications) {
+                    if (!currentApplications.contains(preApplication)) {
+                        rdsRouteRuleManager.unSubscribeRds(preApplication, 
this);
+                    }
+                }
+                subscribeApplications = currentApplications;
+            }
+        }
+
+        // update subset
+        synchronized (this) {
+            BitList<Invoker<T>> allInvokers = currentInvokeList.clone();
+            for (DestinationSubset<T> subset : destinationSubsetMap.values()) {
+                computeSubset(subset, allInvokers);
+            }
+        }
+
+    }
+
+    private void computeSubset(DestinationSubset<T> subset, 
BitList<Invoker<T>> invokers) {
+        Set<Endpoint> endpoints = subset.getEndpoints();
+        List<Invoker<T>> filterInvokers = invokers.stream().filter(inv -> {
+            String host = inv.getUrl().getHost();
+            int port = inv.getUrl().getPort();
+            Optional<Endpoint> any = endpoints.stream()
+                .filter(end -> host.equals(end.getAddress()) && port == 
end.getPortValue())
+                .findAny();
+            return any.isPresent();
+        }).collect(Collectors.toList());
+        subset.setInvokers(new BitList<>(filterInvokers));
+    }
+
+    @Override
+    public synchronized void onRuleChange(String appName, List<XdsRouteRule> 
xdsRouteRules) {
+        if (CollectionUtils.isEmpty(xdsRouteRules)) {
+            clearRule(appName);
+            return;
+        }
+        Set<String> oldCluster = getAllCluster();
+        xdsRouteRuleMap.put(appName, xdsRouteRules);
+        Set<String> newCluster = getAllCluster();
+        changeClusterSubscribe(oldCluster, newCluster);
+    }
+
+    private Set<String> getAllCluster() {
+        if (CollectionUtils.isEmptyMap(xdsRouteRuleMap)) {
+            return new HashSet<>();
+        }
+        Set<String> clusters = new HashSet<>();
+        xdsRouteRuleMap.forEach((appName, rules) -> {
+            for (XdsRouteRule rule : rules) {
+                HTTPRouteDestination action = rule.getRoute();
+                if (action.getCluster() != null) {
+                    clusters.add(action.getCluster());
+                } else if 
(CollectionUtils.isNotEmpty(action.getWeightedClusters())) {
+                    for (ClusterWeight weightedCluster : 
action.getWeightedClusters()) {
+                        clusters.add(weightedCluster.getName());
+                    }
+                }
+            }
+        });
+        return clusters;
+    }
+
+    private void changeClusterSubscribe(Set<String> oldCluster, Set<String> 
newCluster) {
+        Set<String> removeSubscribe = new HashSet<>(oldCluster);
+        Set<String> addSubscribe = new HashSet<>(newCluster);
+
+        removeSubscribe.removeAll(newCluster);
+        addSubscribe.removeAll(oldCluster);
+        // remove subscribe cluster
+        for (String cluster : removeSubscribe) {
+            edsEndpointManager.unSubscribeEds(cluster, this);
+            destinationSubsetMap.remove(cluster);
+        }
+        // add subscribe cluster
+        for (String cluster : addSubscribe) {
+            destinationSubsetMap.put(cluster, new 
DestinationSubset<>(cluster));
+            edsEndpointManager.subscribeEds(cluster, this);
+        }
+    }
+
+    @Override
+    public synchronized void clearRule(String appName) {
+        Set<String> oldCluster = getAllCluster();
+        List<XdsRouteRule> oldRules = xdsRouteRuleMap.remove(appName);
+        if (CollectionUtils.isEmpty(oldRules)) {
+            return;
+        }
+        Set<String> newCluster = getAllCluster();
+        changeClusterSubscribe(oldCluster, newCluster);
+    }
+
+    @Override
+    public synchronized void onEndPointChange(String cluster, Set<Endpoint> 
endpoints) {
+        // find and update subset
+        DestinationSubset<T> subset = destinationSubsetMap.get(cluster);
+        if (subset == null) {
+            return;
+        }
+        subset.setEndpoints(endpoints);
+        computeSubset(subset, currentInvokeList.clone());
+    }
+
+    @Override
+    public void stop() {
+        for (String app : subscribeApplications) {
+            rdsRouteRuleManager.unSubscribeRds(app, this);
+        }
+        for (String cluster : getAllCluster()) {
+            edsEndpointManager.unSubscribeEds(cluster, this);
+        }
+    }
+
+
+    @Deprecated
+    Set<String> getSubscribeApplications() {
+        return subscribeApplications;
+    }
+
+    /**
+     * for ut only
+     */
+    @Deprecated
+    BitList<Invoker<T>> getInvokerList() {
+        return currentInvokeList;
+    }
+
+    /**
+     * for ut only
+     */
+    @Deprecated
+    ConcurrentHashMap<String, List<XdsRouteRule>> getXdsRouteRuleMap() {
+        return xdsRouteRuleMap;
+    }
+
+
+    /**
+     * for ut only
+     */
+    @Deprecated
+    ConcurrentHashMap<String, DestinationSubset<T>> getDestinationSubsetMap() {
+        return destinationSubsetMap;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java
new file mode 100644
index 0000000000..0e9a0c5750
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.cluster.router.state.StateRouter;
+import org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory;
+
+@Activate(order = 100)
+public class XdsRouterFactory implements StateRouterFactory {
+
+    @Override
+    public <T> StateRouter<T> getRouter(Class<T> interfaceClass, URL url) {
+        return new XdsRouter<>(url);
+    }
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java
new file mode 100644
index 0000000000..e94d4be4a9
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java
@@ -0,0 +1,42 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.common.beans.factory.ScopeBeanFactory;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.model.ModuleModel;
+import org.apache.dubbo.rpc.model.ScopeModelInitializer;
+
+public class XdsScopeModelInitializer implements ScopeModelInitializer {
+
+    @Override
+    public void initializeFrameworkModel(FrameworkModel frameworkModel) {
+
+    }
+
+    @Override
+    public void initializeApplicationModel(ApplicationModel applicationModel) {
+        ScopeBeanFactory beanFactory = applicationModel.getBeanFactory();
+        beanFactory.registerBean(RdsRouteRuleManager.class);
+        beanFactory.registerBean(EdsEndpointManager.class);
+    }
+
+    @Override
+    public void initializeModuleModel(ModuleModel moduleModel) {
+    }
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java
new file mode 100644
index 0000000000..a48857cea4
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java
@@ -0,0 +1,38 @@
+/*
+ * 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.xds.rule;
+
+public class ClusterWeight {
+
+    private final String name;
+
+    private final int weight;
+
+    public ClusterWeight(String name, int weight) {
+        this.name = name;
+        this.weight = weight;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getWeight() {
+        return weight;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java
new file mode 100644
index 0000000000..9644a687b0
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java
@@ -0,0 +1,58 @@
+/*
+ * 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.xds.rule;
+
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DestinationSubset<T> {
+
+    public DestinationSubset(String clusterName) {
+        this.clusterName = clusterName;
+    }
+
+    private final String clusterName;
+
+    private Set<Endpoint> endpoints = new HashSet<>();
+
+    private BitList<Invoker<T>> invokers;
+
+    public String getClusterName() {
+        return clusterName;
+    }
+
+    public Set<Endpoint> getEndpoints() {
+        return endpoints;
+    }
+
+    public void setEndpoints(Set<Endpoint> endpoints) {
+        this.endpoints = endpoints;
+
+    }
+
+    public BitList<Invoker<T>> getInvokers() {
+        return invokers;
+    }
+
+    public void setInvokers(BitList<Invoker<T>> invokers) {
+        this.invokers = invokers;
+    }
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java
new file mode 100644
index 0000000000..471e2a215a
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java
@@ -0,0 +1,43 @@
+/*
+ * 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.xds.rule;
+
+import java.util.List;
+
+public class HTTPRouteDestination {
+
+    private String cluster;
+
+    private List<ClusterWeight> weightedClusters;
+
+    public String getCluster() {
+        return cluster;
+    }
+
+    public void setCluster(String cluster) {
+        this.cluster = cluster;
+    }
+
+    public List<ClusterWeight> getWeightedClusters() {
+        return weightedClusters;
+    }
+
+    public void setWeightedClusters(List<ClusterWeight> weightedClusters) {
+        this.weightedClusters = weightedClusters;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java
new file mode 100644
index 0000000000..68bc38a9d5
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java
@@ -0,0 +1,123 @@
+/*
+ * 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.xds.rule;
+
+public class HeaderMatcher {
+
+
+    public String name;
+
+    public String exactValue;
+
+    private String regex;
+
+    public LongRangeMatch range;
+
+    public Boolean present;
+
+    public String prefix;
+
+    public String suffix;
+
+    public boolean inverted;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getExactValue() {
+        return exactValue;
+    }
+
+    public void setExactValue(String exactValue) {
+        this.exactValue = exactValue;
+    }
+
+    public String getRegex() {
+        return regex;
+    }
+
+    public void setRegex(String regex) {
+        this.regex = regex;
+    }
+
+    public LongRangeMatch getRange() {
+        return range;
+    }
+
+    public void setRange(LongRangeMatch range) {
+        this.range = range;
+    }
+
+    public Boolean getPresent() {
+        return present;
+    }
+
+    public void setPresent(Boolean present) {
+        this.present = present;
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(String prefix) {
+        this.prefix = prefix;
+    }
+
+    public String getSuffix() {
+        return suffix;
+    }
+
+    public void setSuffix(String suffix) {
+        this.suffix = suffix;
+    }
+
+    public boolean isInverted() {
+        return inverted;
+    }
+
+    public void setInverted(boolean inverted) {
+        this.inverted = inverted;
+    }
+
+    public boolean match(String input) {
+        if (getPresent() != null) {
+            return (input == null) == getPresent().equals(isInverted());
+        }
+        if (input == null) {
+            return false;
+        }
+        if (getExactValue() != null) {
+            return getExactValue().equals(input) != isInverted();
+        } else if (getRegex() != null) {
+            return input.matches(getRegex()) != isInverted();
+        } else if (getRange() != null) {
+            return getRange().isMatch(input) != isInverted();
+        } else if (getPrefix() != null) {
+            return input.startsWith(getPrefix()) != isInverted();
+        } else if (getSuffix() != null) {
+            return input.endsWith(getSuffix()) != isInverted();
+        }
+        return false;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java
new file mode 100644
index 0000000000..aff10ba890
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java
@@ -0,0 +1,41 @@
+/*
+ * 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.xds.rule;
+
+import java.util.List;
+
+public class HttpRequestMatch {
+
+    private final PathMatcher pathMatcher;
+
+    private final List<HeaderMatcher> headerMatcherList;
+
+    public HttpRequestMatch(PathMatcher pathMatcher, List<HeaderMatcher> 
headerMatcherList) {
+        this.pathMatcher = pathMatcher;
+        this.headerMatcherList = headerMatcherList;
+    }
+
+    public PathMatcher getPathMatcher() {
+        return pathMatcher;
+    }
+
+    public List<HeaderMatcher> getHeaderMatcherList() {
+        return headerMatcherList;
+    }
+
+
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java
new file mode 100644
index 0000000000..d52123f03b
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java
@@ -0,0 +1,49 @@
+/*
+ * 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.xds.rule;
+
+public class LongRangeMatch {
+    private long start;
+    private long end;
+
+    public long getStart() {
+        return start;
+    }
+
+    public void setStart(long start) {
+        this.start = start;
+    }
+
+    public long getEnd() {
+        return end;
+    }
+
+    public void setEnd(long end) {
+        this.end = end;
+    }
+
+
+    public boolean isMatch(String input) {
+        try {
+            long num = Long.parseLong(input);
+            return num >= getStart() && num <= getEnd();
+        } catch (NumberFormatException ignore) {
+            return false;
+        }
+    }
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java
new file mode 100644
index 0000000000..00931fb0a5
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java
@@ -0,0 +1,74 @@
+/*
+ * 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.xds.rule;
+
+
+public class PathMatcher {
+
+    private String path;
+
+    private String prefix;
+
+    private String regex;
+
+    private boolean caseSensitive;
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(String prefix) {
+        this.prefix = prefix;
+    }
+
+    public String getRegex() {
+        return regex;
+    }
+
+    public void setRegex(String regex) {
+        this.regex = regex;
+    }
+
+    public boolean isCaseSensitive() {
+        return caseSensitive;
+    }
+
+    public void setCaseSensitive(boolean caseSensitive) {
+        this.caseSensitive = caseSensitive;
+    }
+
+    public boolean isMatch(String input) {
+        if (getPath() != null) {
+            return isCaseSensitive()
+                ? getPath().equals(input)
+                : getPath().equalsIgnoreCase(input);
+        } else if (getPrefix() != null) {
+            return isCaseSensitive()
+                ? input.startsWith(getPrefix())
+                : input.toLowerCase().startsWith(getPrefix());
+        }
+        return input.matches(getRegex());
+    }
+}
diff --git 
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java
 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java
new file mode 100644
index 0000000000..2ca613bdd1
--- /dev/null
+++ 
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java
@@ -0,0 +1,41 @@
+/*
+ * 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.xds.rule;
+
+
+
+public class XdsRouteRule {
+
+    private final HttpRequestMatch match;
+
+    private final HTTPRouteDestination route;
+
+    public XdsRouteRule(HttpRequestMatch match, HTTPRouteDestination route) {
+        this.match = match;
+        this.route = route;
+    }
+
+
+    public HttpRequestMatch getMatch() {
+        return match;
+    }
+
+    public HTTPRouteDestination getRoute() {
+        return route;
+    }
+
+}
diff --git 
a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
 
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
new file mode 100644
index 0000000000..ca9b94ea84
--- /dev/null
+++ 
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
@@ -0,0 +1 @@
+xds=org.apache.dubbo.rpc.cluster.router.xds.XdsRouterFactory
\ No newline at end of file
diff --git 
a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
 
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
new file mode 100644
index 0000000000..3005831c66
--- /dev/null
+++ 
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
@@ -0,0 +1 @@
+xds-route=org.apache.dubbo.rpc.cluster.router.xds.XdsScopeModelInitializer
diff --git 
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java
 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java
new file mode 100644
index 0000000000..7da4950c55
--- /dev/null
+++ 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class EdsEndpointManagerTest {
+
+
+    @BeforeEach
+    public void before() {
+        EdsEndpointManager.getEdsListeners().clear();
+        EdsEndpointManager.getEndpointListeners().clear();
+        EdsEndpointManager.getEndpointDataCache().clear();
+    }
+
+    @Test
+    public void subscribeEdsTest() {
+        EdsEndpointManager manager = new EdsEndpointManager();
+        String cluster = "testApp";
+        int subscribeNum = 3;
+        for (int i = 0; i < subscribeNum; i++) {
+            manager.subscribeEds(cluster, new EdsEndpointListener() {
+                @Override
+                public void onEndPointChange(String cluster, Set<Endpoint> 
endpoints) {
+
+                }
+            });
+        }
+        assertNotNull(EdsEndpointManager.getEdsListeners().get(cluster));
+        
assertEquals(EdsEndpointManager.getEndpointListeners().get(cluster).size(), 
subscribeNum);
+    }
+
+
+    @Test
+    public void unsubscribeRdsTest() {
+        EdsEndpointManager manager = new EdsEndpointManager();
+        String domain = "testApp";
+        EdsEndpointListener listener = new EdsEndpointListener() {
+            @Override
+            public void onEndPointChange(String cluster, Set<Endpoint> 
endpoints) {
+
+            }
+        };
+        manager.subscribeEds(domain, listener);
+        assertNotNull(EdsEndpointManager.getEdsListeners().get(domain));
+        
assertEquals(EdsEndpointManager.getEndpointListeners().get(domain).size(), 1);
+
+        manager.unSubscribeEds(domain, listener);
+        assertNull(EdsEndpointManager.getEdsListeners().get(domain));
+        assertNull(EdsEndpointManager.getEndpointListeners().get(domain));
+    }
+
+
+    @Test
+    public void notifyRuleChangeTest() {
+
+        Map<String, Set<Endpoint>> cacheData = new HashMap<>();
+        String domain = "testApp";
+        Set<Endpoint> endpoints = new HashSet<>();
+        Endpoint endpoint = new Endpoint();
+        endpoints.add(endpoint);
+
+        EdsEndpointListener listener = new EdsEndpointListener() {
+            @Override
+            public void onEndPointChange(String cluster, Set<Endpoint> 
endpoints) {
+                cacheData.put(cluster, endpoints);
+            }
+        };
+
+        EdsEndpointManager manager = new EdsEndpointManager();
+        manager.subscribeEds(domain, listener);
+        manager.notifyEndpointChange(domain, endpoints);
+        assertEquals(cacheData.get(domain), endpoints);
+
+        Map<String, Set<Endpoint>> cacheData2 = new HashMap<>();
+        EdsEndpointListener listener2 = new EdsEndpointListener() {
+            @Override
+            public void onEndPointChange(String cluster, Set<Endpoint> 
endpoints) {
+                cacheData2.put(cluster, endpoints);
+            }
+        };
+        manager.subscribeEds(domain, listener2);
+        assertEquals(cacheData2.get(domain), endpoints);
+        // clear
+        manager.notifyEndpointChange(domain, new HashSet<>());
+        assertEquals(cacheData.get(domain).size(), 0);
+    }
+
+}
diff --git 
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java
 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java
new file mode 100644
index 0000000000..44e2ad16e5
--- /dev/null
+++ 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class RdsRouteRuleManagerTest {
+
+    @BeforeEach
+    public void before() {
+        RdsRouteRuleManager.getRuleListeners().clear();
+        RdsRouteRuleManager.getRouteDataCache().clear();
+        RdsRouteRuleManager.getRdsListeners().clear();
+    }
+
+
+    @Test
+    public void subscribeRdsTest() {
+        RdsRouteRuleManager manager = new RdsRouteRuleManager();
+        String domain = "testApp";
+        int subscribeNum = 3;
+        for (int i = 0; i < subscribeNum; i++) {
+            manager.subscribeRds(domain, new XdsRouteRuleListener() {
+                @Override
+                public void onRuleChange(String appName, List<XdsRouteRule> 
xdsRouteRules) {
+
+                }
+
+                @Override
+                public void clearRule(String appName) {
+
+                }
+            });
+        }
+        assertNotNull(RdsRouteRuleManager.getRdsListeners().get(domain));
+        
assertEquals(RdsRouteRuleManager.getRuleListeners().get(domain).size(), 
subscribeNum);
+    }
+
+
+    @Test
+    public void unsubscribeRdsTest() {
+        RdsRouteRuleManager manager = new RdsRouteRuleManager();
+        String domain = "testApp";
+        XdsRouteRuleListener listener = new XdsRouteRuleListener() {
+            @Override
+            public void onRuleChange(String appName, List<XdsRouteRule> 
xdsRouteRules) {
+
+            }
+
+            @Override
+            public void clearRule(String appName) {
+
+            }
+        };
+        manager.subscribeRds(domain, listener);
+        assertNotNull(RdsRouteRuleManager.getRdsListeners().get(domain));
+        
assertEquals(RdsRouteRuleManager.getRuleListeners().get(domain).size(), 1);
+
+        manager.unSubscribeRds(domain, listener);
+        assertNull(RdsRouteRuleManager.getRdsListeners().get(domain));
+        assertNull(RdsRouteRuleManager.getRuleListeners().get(domain));
+    }
+
+
+    @Test
+    public void notifyRuleChangeTest() {
+
+        Map<String, List<XdsRouteRule>> cacheData = new HashMap<>();
+        String domain = "testApp";
+        List<XdsRouteRule> xdsRouteRules = new ArrayList<>();
+        XdsRouteRule rule = new XdsRouteRule(new HttpRequestMatch(null, null),
+            new HTTPRouteDestination());
+        xdsRouteRules.add(rule);
+
+        XdsRouteRuleListener listener = new XdsRouteRuleListener() {
+            @Override
+            public void onRuleChange(String appName, List<XdsRouteRule> 
xdsRouteRules) {
+                cacheData.put(appName, xdsRouteRules);
+            }
+
+            @Override
+            public void clearRule(String appName) {
+                cacheData.remove(appName);
+            }
+        };
+
+        RdsRouteRuleManager manager = new RdsRouteRuleManager();
+        manager.subscribeRds(domain, listener);
+        manager.notifyRuleChange(domain, xdsRouteRules);
+        assertEquals(cacheData.get(domain), xdsRouteRules);
+
+        Map<String, List<XdsRouteRule>> cacheData2 = new HashMap<>();
+        XdsRouteRuleListener listener2 = new XdsRouteRuleListener() {
+            @Override
+            public void onRuleChange(String appName, List<XdsRouteRule> 
xdsRouteRules) {
+                cacheData2.put(appName, xdsRouteRules);
+            }
+
+            @Override
+            public void clearRule(String appName) {
+                cacheData2.remove(appName);
+            }
+        };
+        manager.subscribeRds(domain, listener2);
+        assertEquals(cacheData2.get(domain), xdsRouteRules);
+        // clear
+        manager.notifyRuleChange(domain, new ArrayList<>());
+        assertNull(cacheData.get(domain));
+    }
+
+}
diff --git 
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java
 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java
new file mode 100644
index 0000000000..2899e6f3b8
--- /dev/null
+++ 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.UInt32Value;
+import io.envoyproxy.envoy.config.route.v3.HeaderMatcher;
+import io.envoyproxy.envoy.config.route.v3.Route;
+import io.envoyproxy.envoy.config.route.v3.RouteAction;
+import io.envoyproxy.envoy.config.route.v3.RouteMatch;
+import io.envoyproxy.envoy.config.route.v3.VirtualHost;
+import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
+import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher;
+import io.envoyproxy.envoy.type.v3.Int64Range;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RdsVirtualHostListenerTest {
+
+    private final String domain = "testApp";
+
+    private final Map<String, List<XdsRouteRule>> dataCache = new HashMap<>();
+
+    private final XdsRouteRuleListener listener = new XdsRouteRuleListener() {
+        @Override
+        public void onRuleChange(String appName, List<XdsRouteRule> 
xdsRouteRules) {
+            dataCache.put(appName, xdsRouteRules);
+        }
+
+        @Override
+        public void clearRule(String appName) {
+            dataCache.remove(appName);
+        }
+    };
+
+    private final RdsRouteRuleManager manager = new RdsRouteRuleManager();
+
+    private final RdsVirtualHostListener rdsVirtualHostListener = new 
RdsVirtualHostListener("testApp", manager);
+
+    @BeforeEach
+    public void init() {
+        dataCache.clear();
+        manager.subscribeRds(domain, listener);
+    }
+
+
+    @Test
+    public void parsePathPathMatcherTest() {
+        String path = "/test/name";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(domain)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                
.setMatch(RouteMatch.newBuilder().setPath(path).setCaseSensitive(BoolValue.newBuilder().setValue(true).build()).build())
+                
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+                .build()
+            ).build();
+        rdsVirtualHostListener.parseVirtualHost(virtualHost);
+        List<XdsRouteRule> rules = dataCache.get(domain);
+        assertNotNull(rules);
+        assertEquals(rules.get(0).getMatch().getPathMatcher().getPath(), path);
+        assertTrue(rules.get(0).getMatch().getPathMatcher().isCaseSensitive());
+    }
+
+    @Test
+    public void parsePrefixPathMatcherTest() {
+        String prefix = "/test";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(domain)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPrefix(prefix).build())
+                
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+                .build()
+            )
+            .build();
+        rdsVirtualHostListener.parseVirtualHost(virtualHost);
+        List<XdsRouteRule> rules = dataCache.get(domain);
+        assertNotNull(rules);
+        assertEquals(rules.get(0).getMatch().getPathMatcher().getPrefix(), 
prefix);
+    }
+
+    @Test
+    public void parseRegexPathMatcherTest() {
+        String regex = "/test/.*";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(domain)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setSafeRegex(
+                    RegexMatcher.newBuilder().setRegex(regex).build()
+                ).build())
+                
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+                .build()
+            )
+            .build();
+        rdsVirtualHostListener.parseVirtualHost(virtualHost);
+        List<XdsRouteRule> rules = dataCache.get(domain);
+        assertNotNull(rules);
+        assertEquals(rules.get(0).getMatch().getPathMatcher().getRegex(), 
regex);
+    }
+
+    @Test
+    public void parseHeadMatcherTest() {
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(domain)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder()
+                    .addHeaders(HeaderMatcher.newBuilder()
+                        .setName("head-exactValue")
+                        .setExactMatch("exactValue")
+                        .setInvertMatch(true)
+                        .build())
+                    .addHeaders(HeaderMatcher.newBuilder()
+                        .setName("head-regex")
+                        
.setSafeRegexMatch(RegexMatcher.newBuilder().setRegex("regex").build())
+                        .build())
+                    .addHeaders(HeaderMatcher.newBuilder()
+                        .setName("head-range")
+                        
.setRangeMatch(Int64Range.newBuilder().setStart(1).setEnd(100).build())
+                        .build())
+                    .addHeaders(HeaderMatcher.newBuilder()
+                        .setName("head-present")
+                        .setPresentMatch(true)
+                        .build())
+                    .addHeaders(HeaderMatcher.newBuilder()
+                        .setName("head-prefix")
+                        .setPrefixMatch("prefix")
+                        .build())
+                    .addHeaders(HeaderMatcher.newBuilder()
+                        .setName("head-suffix")
+                        .setSuffixMatch("suffix")
+                        .build())
+                    .build()
+                )
+                
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+                .build()
+            )
+            .build();
+        rdsVirtualHostListener.parseVirtualHost(virtualHost);
+        List<XdsRouteRule> rules = dataCache.get(domain);
+        assertNotNull(rules);
+        List<org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher> 
headerMatcherList = rules.get(0).getMatch().getHeaderMatcherList();
+        for (org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher 
matcher : headerMatcherList) {
+            if (matcher.getName().equals("head-exactValue")) {
+                assertEquals(matcher.getExactValue(), "exactValue");
+            } else if (matcher.getName().equals("head-regex")) {
+                assertEquals(matcher.getRegex(), "regex");
+            } else if (matcher.getName().equals("head-range")) {
+                assertEquals(matcher.getRange().getStart(), 1);
+                assertEquals(matcher.getRange().getEnd(), 100);
+            } else if (matcher.getName().equals("head-present")) {
+                assertTrue(matcher.getPresent());
+            } else if (matcher.getName().equals("head-prefix")) {
+                assertEquals(matcher.getPrefix(), "prefix");
+            } else if (matcher.getName().equals("head-suffix")) {
+                assertEquals(matcher.getSuffix(), "suffix");
+            }
+        }
+    }
+
+    @Test
+    public void parseRouteClusterTest() {
+        String cluster = "cluster-test";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(domain)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPrefix("/test").build())
+                .setRoute(RouteAction.newBuilder().setCluster(cluster).build())
+                .build()
+            )
+            .build();
+        rdsVirtualHostListener.parseVirtualHost(virtualHost);
+        List<XdsRouteRule> rules = dataCache.get(domain);
+        assertNotNull(rules);
+        assertEquals(rules.get(0).getRoute().getCluster(), cluster);
+    }
+
+    @Test
+    public void parseRouteWeightClusterTest() {
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(domain)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPrefix("/test").build())
+                .setRoute(RouteAction.newBuilder().setWeightedClusters(
+                    WeightedCluster.newBuilder()
+                        
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName("cluster-test1")
+                            
.setWeight(UInt32Value.newBuilder().setValue(40).build()).build())
+                        
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName("cluster-test2")
+                            
.setWeight(UInt32Value.newBuilder().setValue(60).build()).build())
+                        .build()
+                ).build())
+                .build())
+            .build();
+        rdsVirtualHostListener.parseVirtualHost(virtualHost);
+        List<XdsRouteRule> rules = dataCache.get(domain);
+        assertNotNull(rules);
+        List<ClusterWeight> weightedClusters = 
rules.get(0).getRoute().getWeightedClusters();
+        assertEquals(weightedClusters.size(), 2);
+        for (ClusterWeight weightedCluster : weightedClusters) {
+            if (weightedCluster.getName().equals("cluster-test1")) {
+                assertEquals(weightedCluster.getWeight(), 40);
+            } else if (weightedCluster.getName().equals("cluster-test2")) {
+                assertEquals(weightedCluster.getWeight(), 60);
+            }
+        }
+    }
+
+}
diff --git 
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java
 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java
new file mode 100644
index 0000000000..e48ae10ace
--- /dev/null
+++ 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.xds;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.utils.Holder;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.cluster.router.mesh.util.TracingContextProvider;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.DestinationSubset;
+
+import com.google.protobuf.UInt32Value;
+import io.envoyproxy.envoy.config.route.v3.HeaderMatcher;
+import io.envoyproxy.envoy.config.route.v3.Route;
+import io.envoyproxy.envoy.config.route.v3.RouteAction;
+import io.envoyproxy.envoy.config.route.v3.RouteMatch;
+import io.envoyproxy.envoy.config.route.v3.VirtualHost;
+import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class XdsRouteTest {
+
+    private EdsEndpointManager edsEndpointManager;
+
+    private RdsRouteRuleManager rdsRouteRuleManager;
+    private Set<TracingContextProvider> tracingContextProviders;
+    private URL url;
+
+    @BeforeEach
+    public void setup() {
+        edsEndpointManager = Mockito.spy(EdsEndpointManager.class);
+        rdsRouteRuleManager = Mockito.spy(RdsRouteRuleManager.class);
+        tracingContextProviders = new HashSet<>();
+
+        url = URL.valueOf("test://localhost/DemoInterface");
+    }
+
+    private Invoker<Object> createInvoker(String app) {
+        URL url = URL.valueOf("dubbo://localhost/DemoInterface?" + 
(StringUtils.isEmpty(app) ? "" : "remote.application=" + app));
+        Invoker invoker = Mockito.mock(Invoker.class);
+        when(invoker.getUrl()).thenReturn(url);
+        return invoker;
+    }
+
+    private Invoker<Object> createInvoker(String app, String address) {
+        URL url = URL.valueOf("dubbo://" + address + "/DemoInterface?" + 
(StringUtils.isEmpty(app) ? "" : "remote.application=" + app));
+        Invoker invoker = Mockito.mock(Invoker.class);
+        when(invoker.getUrl()).thenReturn(url);
+        return invoker;
+    }
+
+    @Test
+    public void testNotifyInvoker() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        xdsRouter.notify(null);
+        assertEquals(0, xdsRouter.getSubscribeApplications().size());
+
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(createInvoker(""), createInvoker("app1")));
+
+        xdsRouter.notify(invokers);
+
+        assertEquals(1, xdsRouter.getSubscribeApplications().size());
+        assertTrue(xdsRouter.getSubscribeApplications().contains("app1"));
+        assertEquals(invokers, xdsRouter.getInvokerList());
+
+        verify(rdsRouteRuleManager, times(1)).subscribeRds("app1", xdsRouter);
+
+        invokers = new BitList<>(Arrays.asList(createInvoker("app2")));
+        xdsRouter.notify(invokers);
+        verify(rdsRouteRuleManager, times(1)).subscribeRds("app2", xdsRouter);
+        verify(rdsRouteRuleManager, times(1)).unSubscribeRds("app1", 
xdsRouter);
+        assertEquals(invokers, xdsRouter.getInvokerList());
+
+        xdsRouter.stop();
+        verify(rdsRouteRuleManager, times(1)).unSubscribeRds("app2", 
xdsRouter);
+    }
+
+    @Test
+    public void testRuleChange() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName = "app1";
+        String cluster1 = "cluster-test1";
+        String cluster2 = "cluster-test2";
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(createInvoker(appName)));
+        xdsRouter.notify(invokers);
+        String path = "/DemoInterface/call";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(appName)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPath(path).build())
+                
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+                .build()
+            ).build();
+        RdsVirtualHostListener hostListener = new 
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+        hostListener.parseVirtualHost(virtualHost);
+        assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1);
+        verify(edsEndpointManager, times(1)).subscribeEds(cluster1, xdsRouter);
+
+        VirtualHost virtualHost2 = VirtualHost.newBuilder()
+            .addDomains(appName)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPath(path).build())
+                
.setRoute(RouteAction.newBuilder().setCluster("cluster-test2").build())
+                .build()
+            ).build();
+        hostListener.parseVirtualHost(virtualHost2);
+        assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1);
+        verify(edsEndpointManager, times(1)).subscribeEds(cluster2, xdsRouter);
+        verify(edsEndpointManager, times(1)).unSubscribeEds(cluster1, 
xdsRouter);
+    }
+
+
+    @Test
+    public void testEndpointChange() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName = "app1";
+        String cluster1 = "cluster-test1";
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(createInvoker(appName, "1.1.1.1:20880")
+            , createInvoker(appName, "2.2.2.2:20880")));
+        xdsRouter.notify(invokers);
+        String path = "/DemoInterface/call";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(appName)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPath(path).build())
+                
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+                .build()
+            ).build();
+        RdsVirtualHostListener hostListener = new 
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+        hostListener.parseVirtualHost(virtualHost);
+        assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1);
+        verify(edsEndpointManager, times(1)).subscribeEds(cluster1, xdsRouter);
+
+        Set<Endpoint> endpoints = new HashSet<>();
+        Endpoint endpoint1 = new Endpoint();
+        endpoint1.setAddress("1.1.1.1");
+        endpoint1.setPortValue(20880);
+        Endpoint endpoint2 = new Endpoint();
+        endpoint2.setAddress("2.2.2.2");
+        endpoint2.setPortValue(20880);
+        endpoints.add(endpoint1);
+        endpoints.add(endpoint2);
+        edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+
+        DestinationSubset<Object> objectDestinationSubset = 
xdsRouter.getDestinationSubsetMap().get(cluster1);
+        assertEquals(invokers, objectDestinationSubset.getInvokers());
+    }
+
+    @Test
+    public void testRouteNotMatch() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName = "app1";
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(createInvoker(appName, "1.1.1.1:20880")
+            , createInvoker(appName, "2.2.2.2:20880")));
+        assertEquals(invokers, xdsRouter.route(invokers.clone(), null, null, 
false, null));
+        Holder<String> message = new Holder<>();
+        xdsRouter.doRoute(invokers.clone(), null, null, true, null, message);
+        assertEquals("Directly Return. Reason: xds route rule is empty.", 
message.get());
+    }
+
+    @Test
+    public void testRoutePathMatch() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName = "app1";
+        String cluster1 = "cluster-test1";
+        Invoker<Object> invoker1 = createInvoker(appName, "1.1.1.1:20880");
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(invoker1
+            , createInvoker(appName, "2.2.2.2:20880")));
+        xdsRouter.notify(invokers);
+        String path = "/DemoInterface/call";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(appName)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPath(path).build())
+                
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+                .build()
+            ).build();
+        RdsVirtualHostListener hostListener = new 
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+        hostListener.parseVirtualHost(virtualHost);
+        Invocation invocation = Mockito.mock(Invocation.class);
+        Invoker invoker = Mockito.mock(Invoker.class);
+        URL url1 = Mockito.mock(URL.class);
+        when(invoker.getUrl()).thenReturn(url1);
+        when(url1.getPath()).thenReturn("DemoInterface");
+        when(invocation.getInvoker()).thenReturn(invoker);
+        when(invocation.getMethodName()).thenReturn("call");
+
+        Set<Endpoint> endpoints = new HashSet<>();
+        Endpoint endpoint1 = new Endpoint();
+        endpoint1.setAddress("1.1.1.1");
+        endpoint1.setPortValue(20880);
+        endpoints.add(endpoint1);
+        edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+        BitList<Invoker<Object>> routes = xdsRouter.route(invokers.clone(), 
null, invocation, false, null);
+        assertEquals(1, routes.size());
+        assertEquals(invoker1, routes.get(0));
+
+    }
+
+
+    @Test
+    public void testRouteHeadMatch() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName = "app1";
+        String cluster1 = "cluster-test1";
+        Invoker<Object> invoker1 = createInvoker(appName, "1.1.1.1:20880");
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(invoker1
+            , createInvoker(appName, "2.2.2.2:20880")));
+        xdsRouter.notify(invokers);
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(appName)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().addHeaders(
+                        HeaderMatcher.newBuilder()
+                            .setName("userId")
+                            .setExactMatch("123")
+                            .build()
+                    ).build()
+                )
+                
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+                .build()
+            ).build();
+        RdsVirtualHostListener hostListener = new 
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+        hostListener.parseVirtualHost(virtualHost);
+        Invocation invocation = Mockito.mock(Invocation.class);
+        when(invocation.getAttachment("userId")).thenReturn("123");
+        Set<Endpoint> endpoints = new HashSet<>();
+        Endpoint endpoint1 = new Endpoint();
+        endpoint1.setAddress("1.1.1.1");
+        endpoint1.setPortValue(20880);
+        endpoints.add(endpoint1);
+        edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+        BitList<Invoker<Object>> routes = xdsRouter.route(invokers.clone(), 
null, invocation, false, null);
+        assertEquals(1, routes.size());
+        assertEquals(invoker1, routes.get(0));
+    }
+
+
+    @Test
+    public void testRouteWeightCluster() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName = "app1";
+        String cluster1 = "cluster-test1";
+        String cluster2 = "cluster-test2";
+        Invoker<Object> invoker1 = createInvoker(appName, "1.1.1.1:20880");
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(invoker1
+            , createInvoker(appName, "2.2.2.2:20880")));
+        xdsRouter.notify(invokers);
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(appName)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().addHeaders(
+                        HeaderMatcher.newBuilder()
+                            .setName("userId")
+                            .setExactMatch("123")
+                            .build()
+                    ).build()
+                )
+                .setRoute(RouteAction.newBuilder().setWeightedClusters(
+                        WeightedCluster.newBuilder()
+                            
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName(cluster1)
+                                
.setWeight(UInt32Value.newBuilder().setValue(100).build()).build())
+                            
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName(cluster2)
+                                
.setWeight(UInt32Value.newBuilder().setValue(0).build()).build())
+                            .build())
+                    .build()
+                ).build()).build();
+        RdsVirtualHostListener hostListener = new 
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+        hostListener.parseVirtualHost(virtualHost);
+        Invocation invocation = Mockito.mock(Invocation.class);
+        when(invocation.getAttachment("userId")).thenReturn("123");
+        Set<Endpoint> endpoints = new HashSet<>();
+        Endpoint endpoint1 = new Endpoint();
+        endpoint1.setAddress("1.1.1.1");
+        endpoint1.setPortValue(20880);
+        endpoints.add(endpoint1);
+        edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+
+        endpoints = new HashSet<>();
+        Endpoint endpoint2 = new Endpoint();
+        endpoint2.setAddress("2.2.2.2");
+        endpoint2.setPortValue(20880);
+        endpoints.add(endpoint2);
+        edsEndpointManager.notifyEndpointChange(cluster2, endpoints);
+
+        for (int i = 0; i < 10; i++) {
+            BitList<Invoker<Object>> routes = 
xdsRouter.route(invokers.clone(), null, invocation, false, null);
+            assertEquals(1, routes.size());
+            assertEquals(invoker1, routes.get(0));
+        }
+    }
+
+    @Test
+    public void testRouteMultiApp() {
+        XdsRouter<Object> xdsRouter = new XdsRouter<>(url, 
rdsRouteRuleManager, edsEndpointManager);
+        String appName1 = "app1";
+        String appName2 = "app2";
+        String cluster1 = "cluster-test1";
+        Invoker<Object> invoker1 = createInvoker(appName2, "1.1.1.1:20880");
+        Invoker<Object> invoker2 = createInvoker(appName1, "2.2.2.2:20880");
+        BitList<Invoker<Object>> invokers = new 
BitList<>(Arrays.asList(invoker1
+            , invoker2));
+        xdsRouter.notify(invokers);
+        assertEquals(xdsRouter.getSubscribeApplications().size(), 2);
+        String path = "/DemoInterface/call";
+        VirtualHost virtualHost = VirtualHost.newBuilder()
+            .addDomains(appName2)
+            .addRoutes(Route.newBuilder().setName("route-test")
+                .setMatch(RouteMatch.newBuilder().setPath(path).build())
+                
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+                .build()
+            ).build();
+        RdsVirtualHostListener hostListener = new 
RdsVirtualHostListener(appName2, rdsRouteRuleManager);
+        hostListener.parseVirtualHost(virtualHost);
+        Invocation invocation = Mockito.mock(Invocation.class);
+        Invoker invoker = Mockito.mock(Invoker.class);
+        URL url1 = Mockito.mock(URL.class);
+        when(invoker.getUrl()).thenReturn(url1);
+        when(url1.getPath()).thenReturn("DemoInterface");
+        when(invocation.getInvoker()).thenReturn(invoker);
+        when(invocation.getMethodName()).thenReturn("call");
+
+        Set<Endpoint> endpoints = new HashSet<>();
+        Endpoint endpoint1 = new Endpoint();
+        endpoint1.setAddress("1.1.1.1");
+        endpoint1.setPortValue(20880);
+        endpoints.add(endpoint1);
+        edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+        BitList<Invoker<Object>> routes = xdsRouter.route(invokers.clone(), 
null, invocation, false, null);
+        assertEquals(1, routes.size());
+        assertEquals(invoker1, routes.get(0));
+    }
+
+}
diff --git 
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java
 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java
new file mode 100644
index 0000000000..dc8e240482
--- /dev/null
+++ 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.xds.rule;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HeaderMatcherTest {
+
+    @Test
+    public void exactValueMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        headMatcher.setName("testHead");
+        String value = "testValue";
+        headMatcher.setExactValue(value);
+        assertTrue(headMatcher.match(value));
+    }
+
+
+    @Test
+    public void regexMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        headMatcher.setRegex("test.*");
+        String value = "testValue";
+        headMatcher.setExactValue(value);
+        assertTrue(headMatcher.match(value));
+    }
+
+    @Test
+    public void rangMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        LongRangeMatch range = new LongRangeMatch();
+        range.setStart(100);
+        range.setEnd(500);
+        headMatcher.setRange(range);
+        assertTrue(headMatcher.match("300"));
+    }
+
+
+    @Test
+    public void presentMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        headMatcher.setName("testHead");
+        headMatcher.setPresent(true);
+        assertTrue(headMatcher.match("value"));
+        headMatcher.setPresent(false);
+        assertTrue(headMatcher.match(null));
+    }
+
+    @Test
+    public void prefixMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        headMatcher.setName("testHead");
+        headMatcher.setPrefix("test");
+        assertTrue(headMatcher.match("testValue"));
+    }
+
+
+    @Test
+    public void suffixMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        headMatcher.setName("testHead");
+        headMatcher.setSuffix("Value");
+        assertTrue(headMatcher.match("testValue"));
+    }
+
+    @Test
+    public void invertedMatcherTest() {
+        HeaderMatcher headMatcher = new HeaderMatcher();
+        headMatcher.setName("testHead");
+        String value = "testValue";
+        headMatcher.setExactValue(value);
+        headMatcher.setInverted(true);
+        assertFalse(headMatcher.match("testValue"));
+    }
+}
diff --git 
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java
 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java
new file mode 100644
index 0000000000..51bb920a6c
--- /dev/null
+++ 
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.xds.rule;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PathMatcherTest {
+
+
+    @Test
+    public void pathMatcherTest() {
+        PathMatcher pathMatcher = new PathMatcher();
+        String path = "/testService/test";
+        pathMatcher.setPath(path);
+        assertTrue(pathMatcher.isMatch(path));
+        assertTrue(pathMatcher.isMatch(path.toUpperCase()));
+        pathMatcher.setCaseSensitive(true);
+        assertFalse(pathMatcher.isMatch(path.toUpperCase()));
+
+    }
+
+    @Test
+    public void prefixMatcherTest() {
+        PathMatcher pathMatcher = new PathMatcher();
+        String prefix = "/test";
+        String path = "/testService/test";
+        pathMatcher.setPrefix(prefix);
+        assertTrue(pathMatcher.isMatch(path));
+        assertTrue(pathMatcher.isMatch(path.toUpperCase()));
+        pathMatcher.setCaseSensitive(true);
+        assertFalse(pathMatcher.isMatch(path.toUpperCase()));
+    }
+
+    @Test
+    public void regexMatcherTest() {
+        PathMatcher pathMatcher = new PathMatcher();
+        String regex = "/testService/.*";
+        String path = "/testService/test";
+        pathMatcher.setRegex(regex);
+        assertTrue(pathMatcher.isMatch(path));
+    }
+
+}

Reply via email to