diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0c39f31e..b848c8542 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,4 +2,5 @@
---
- [Add metadata transfer example.](https://github.com/Tencent/spring-cloud-tencent/pull/210)
-- [feat:merge features from 1.5.2-Hoxton.SR9 except router.](https://github.com/Tencent/spring-cloud-tencent/pull/226)
\ No newline at end of file
+- [feat:merge features from 1.5.2-Hoxton.SR9 except router.](https://github.com/Tencent/spring-cloud-tencent/pull/226)
+- [feat:merge router features from 1.5.2-Hoxton.SR9.](https://github.com/Tencent/spring-cloud-tencent/pull/232)
\ No newline at end of file
diff --git a/spring-cloud-starter-tencent-polaris-router/pom.xml b/spring-cloud-starter-tencent-polaris-router/pom.xml
index d623da7b1..147451b5f 100644
--- a/spring-cloud-starter-tencent-polaris-router/pom.xml
+++ b/spring-cloud-starter-tencent-polaris-router/pom.xml
@@ -19,6 +19,10 @@
com.tencent.cloud
spring-cloud-tencent-polaris-loadbalancer
+
+ com.tencent.cloud
+ spring-cloud-starter-tencent-metadata-transfer
+
@@ -26,7 +30,51 @@
com.tencent.polaris
router-rule
+
+ com.tencent.polaris
+ router-metadata
+
+
+ com.tencent.polaris
+ router-nearby
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-inline
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ net.bytebuddy
+ byte-buddy
+ test
+
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java
new file mode 100644
index 000000000..114548722
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java
@@ -0,0 +1,62 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.util.CollectionUtils;
+
+/**
+ * the context for router.
+ *
+ *@author lepdou 2022-05-17
+ */
+public class PolarisRouterContext {
+
+ /**
+ * the label for rule router.
+ */
+ public static final String RULE_ROUTER_LABELS = "ruleRouter";
+ /**
+ * transitive labels.
+ */
+ public static final String TRANSITIVE_LABELS = "transitive";
+
+ private Map> labels;
+
+ public Map getLabels(String labelType) {
+ if (CollectionUtils.isEmpty(labels)) {
+ return Collections.emptyMap();
+ }
+ Map subLabels = labels.get(labelType);
+ if (CollectionUtils.isEmpty(subLabels)) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(subLabels);
+ }
+
+ public void setLabels(String labelType, Map subLabels) {
+ if (this.labels == null) {
+ this.labels = new HashMap<>();
+ }
+ labels.put(labelType, subLabels);
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterServiceInstanceListSupplier.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterServiceInstanceListSupplier.java
new file mode 100644
index 000000000..13b4afc17
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterServiceInstanceListSupplier.java
@@ -0,0 +1,205 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.tencent.cloud.common.metadata.MetadataContext;
+import com.tencent.cloud.common.metadata.MetadataContextHolder;
+import com.tencent.cloud.common.pojo.PolarisServiceInstance;
+import com.tencent.cloud.common.util.ExpressionLabelUtils;
+import com.tencent.cloud.common.util.JacksonUtils;
+import com.tencent.cloud.polaris.loadbalancer.LoadBalancerUtils;
+import com.tencent.cloud.polaris.router.config.PolarisMetadataRouterProperties;
+import com.tencent.cloud.polaris.router.config.PolarisNearByRouterProperties;
+import com.tencent.cloud.polaris.router.config.PolarisRuleBasedRouterProperties;
+import com.tencent.cloud.polaris.router.resttemplate.PolarisLoadBalancerRequest;
+import com.tencent.polaris.api.exception.ErrorCode;
+import com.tencent.polaris.api.exception.PolarisException;
+import com.tencent.polaris.api.pojo.Instance;
+import com.tencent.polaris.api.pojo.ServiceInfo;
+import com.tencent.polaris.api.pojo.ServiceInstances;
+import com.tencent.polaris.plugins.router.metadata.MetadataRouter;
+import com.tencent.polaris.plugins.router.nearby.NearbyRouter;
+import com.tencent.polaris.plugins.router.rule.RuleBasedRouter;
+import com.tencent.polaris.router.api.core.RouterAPI;
+import com.tencent.polaris.router.api.rpc.ProcessRoutersRequest;
+import com.tencent.polaris.router.api.rpc.ProcessRoutersResponse;
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
+import org.springframework.cloud.client.loadbalancer.Request;
+import org.springframework.cloud.client.loadbalancer.RequestDataContext;
+import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Service routing entrance.
+ *
+ * Rule routing needs to rely on request parameters for server filtering.
+ * The interface cannot obtain the context object of the request granularity,
+ * so the routing capability cannot be achieved through ServerListFilter.
+ *
+ * And {@link PolarisRouterServiceInstanceListSupplier#get(Request)} provides the ability to pass in http headers,
+ * so routing capabilities are implemented through IRule.
+ *
+ * @author Haotian Zhang, lepdou
+ */
+public class PolarisRouterServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
+
+ private final PolarisNearByRouterProperties polarisNearByRouterProperties;
+ private final PolarisMetadataRouterProperties polarisMetadataRouterProperties;
+ private final PolarisRuleBasedRouterProperties polarisRuleBasedRouterProperties;
+ private final RouterAPI routerAPI;
+
+ public PolarisRouterServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
+ RouterAPI routerAPI,
+ PolarisNearByRouterProperties polarisNearByRouterProperties,
+ PolarisMetadataRouterProperties polarisMetadataRouterProperties,
+ PolarisRuleBasedRouterProperties polarisRuleBasedRouterProperties) {
+ super(delegate);
+ this.routerAPI = routerAPI;
+ this.polarisNearByRouterProperties = polarisNearByRouterProperties;
+ this.polarisMetadataRouterProperties = polarisMetadataRouterProperties;
+ this.polarisRuleBasedRouterProperties = polarisRuleBasedRouterProperties;
+ }
+
+ @Override
+ public Flux> get() {
+ throw new PolarisException(ErrorCode.INTERNAL_ERROR, "Unsupported method.");
+ }
+
+ @Override
+ public Flux> get(Request request) {
+ // 1. get all servers
+ Flux> allServers = getDelegate().get();
+
+ // 2. filter by router
+ DefaultRequestContext requestContext = (DefaultRequestContext) request.getContext();
+ PolarisRouterContext key = null;
+ if (requestContext instanceof RequestDataContext) {
+ key = buildRouterContext(((RequestDataContext) requestContext).getClientRequest().getHeaders());
+ }
+ else if (requestContext.getClientRequest() instanceof PolarisLoadBalancerRequest) {
+ key = buildRouterContext(((PolarisLoadBalancerRequest>) requestContext.getClientRequest()).getRequest()
+ .getHeaders());
+ }
+ return doRouter(allServers, key);
+ }
+
+ //set method to public for unit test
+ PolarisRouterContext buildRouterContext(HttpHeaders headers) {
+ Collection labelHeaderValues = headers.get(RouterConstants.ROUTER_LABEL_HEADER);
+
+ if (CollectionUtils.isEmpty(labelHeaderValues)) {
+ return null;
+ }
+
+ PolarisRouterContext routerContext = new PolarisRouterContext();
+
+ routerContext.setLabels(PolarisRouterContext.TRANSITIVE_LABELS, MetadataContextHolder.get()
+ .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE));
+
+ labelHeaderValues.forEach(labelHeaderValue -> {
+ Map labels = JacksonUtils.deserialize2Map(labelHeaderValue);
+ if (!CollectionUtils.isEmpty(labels)) {
+ Map unescapeLabels = new HashMap<>(labels.size());
+ for (Map.Entry entry : labels.entrySet()) {
+ String escapedKey = ExpressionLabelUtils.unescape(entry.getKey());
+ String escapedValue = ExpressionLabelUtils.unescape(entry.getValue());
+ unescapeLabels.put(escapedKey, escapedValue);
+ }
+ routerContext.setLabels(PolarisRouterContext.RULE_ROUTER_LABELS, unescapeLabels);
+ }
+ });
+
+ return routerContext;
+ }
+
+ Flux> doRouter(Flux> allServers, PolarisRouterContext key) {
+ ServiceInstances serviceInstances = LoadBalancerUtils.transferServersToServiceInstances(allServers);
+
+ // filter instance by routers
+ ProcessRoutersRequest processRoutersRequest = buildProcessRoutersRequest(serviceInstances, key);
+
+ ProcessRoutersResponse processRoutersResponse = routerAPI.processRouters(processRoutersRequest);
+
+ List filteredInstances = new ArrayList<>();
+ ServiceInstances filteredServiceInstances = processRoutersResponse.getServiceInstances();
+ for (Instance instance : filteredServiceInstances.getInstances()) {
+ filteredInstances.add(new PolarisServiceInstance(instance));
+ }
+ return Flux.fromIterable(Collections.singletonList(filteredInstances));
+ }
+
+ ProcessRoutersRequest buildProcessRoutersRequest(ServiceInstances serviceInstances, PolarisRouterContext key) {
+ ProcessRoutersRequest processRoutersRequest = new ProcessRoutersRequest();
+ processRoutersRequest.setDstInstances(serviceInstances);
+
+ // metadata router
+ if (polarisMetadataRouterProperties.isEnabled()) {
+ Map transitiveLabels = getRouterLabels(key, PolarisRouterContext.TRANSITIVE_LABELS);
+ processRoutersRequest.putRouterMetadata(MetadataRouter.ROUTER_TYPE_METADATA, transitiveLabels);
+ }
+
+ // nearby router
+ if (polarisNearByRouterProperties.isEnabled()) {
+ Map nearbyRouterMetadata = new HashMap<>();
+ nearbyRouterMetadata.put(NearbyRouter.ROUTER_ENABLED, "true");
+ processRoutersRequest.putRouterMetadata(NearbyRouter.ROUTER_TYPE_NEAR_BY, nearbyRouterMetadata);
+ }
+
+ // rule based router
+ // set dynamic switch for rule based router
+ boolean ruleBasedRouterEnabled = polarisRuleBasedRouterProperties.isEnabled();
+ Map ruleRouterMetadata = new HashMap<>();
+ ruleRouterMetadata.put(RuleBasedRouter.ROUTER_ENABLED, String.valueOf(ruleBasedRouterEnabled));
+ processRoutersRequest.putRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED, ruleRouterMetadata);
+
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.setNamespace(MetadataContext.LOCAL_NAMESPACE);
+ serviceInfo.setService(MetadataContext.LOCAL_SERVICE);
+
+ if (ruleBasedRouterEnabled) {
+ Map ruleRouterLabels = getRouterLabels(key, PolarisRouterContext.RULE_ROUTER_LABELS);
+ // The label information that the rule based routing depends on
+ // is placed in the metadata of the source service for transmission.
+ // Later, can consider putting it in routerMetadata like other routers.
+ serviceInfo.setMetadata(ruleRouterLabels);
+ }
+
+ processRoutersRequest.setSourceService(serviceInfo);
+
+ return processRoutersRequest;
+ }
+
+ private Map getRouterLabels(PolarisRouterContext key, String type) {
+ if (key != null) {
+ return key.getLabels(type);
+ }
+ return Collections.emptyMap();
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/package-info.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/RouterConstants.java
similarity index 79%
rename from spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/package-info.java
rename to spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/RouterConstants.java
index 3eb18e376..6437c83e8 100644
--- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/package-info.java
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/RouterConstants.java
@@ -13,11 +13,20 @@
* 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 com.tencent.cloud.polaris.router;
+
/**
- * Package info of router.
+ * Router constants.
*
- * @author Haotian Zhang
+ *@author lepdou 2022-05-17
*/
-package com.tencent.cloud.polaris.router;
+public class RouterConstants {
+
+ /**
+ * the header of router label.
+ */
+ public static final String ROUTER_LABEL_HEADER = "internal-router-label";
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/RouterRuleLabelResolver.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/RouterRuleLabelResolver.java
new file mode 100644
index 000000000..5ab6e549f
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/RouterRuleLabelResolver.java
@@ -0,0 +1,75 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.tencent.cloud.common.util.ExpressionLabelUtils;
+import com.tencent.cloud.polaris.context.ServiceRuleManager;
+import com.tencent.polaris.client.pb.ModelProto;
+import com.tencent.polaris.client.pb.RoutingProto;
+
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Resolve label expressions from routing rules.
+ * @author lepdou 2022-05-19
+ */
+public class RouterRuleLabelResolver {
+
+ private final ServiceRuleManager serviceRuleManager;
+
+ public RouterRuleLabelResolver(ServiceRuleManager serviceRuleManager) {
+ this.serviceRuleManager = serviceRuleManager;
+ }
+
+ public Set getExpressionLabelKeys(String namespace, String sourceService, String dstService) {
+ List rules = serviceRuleManager.getServiceRouterRule(namespace, sourceService, dstService);
+
+ if (CollectionUtils.isEmpty(rules)) {
+ return Collections.emptySet();
+ }
+
+ Set expressionLabels = new HashSet<>();
+
+ for (RoutingProto.Route rule : rules) {
+ List sources = rule.getSourcesList();
+ if (CollectionUtils.isEmpty(sources)) {
+ continue;
+ }
+ for (RoutingProto.Source source : sources) {
+ Map labels = source.getMetadataMap();
+ if (CollectionUtils.isEmpty(labels)) {
+ continue;
+ }
+ for (String labelKey : labels.keySet()) {
+ if (ExpressionLabelUtils.isExpressionLabel(labelKey)) {
+ expressionLabels.add(labelKey);
+ }
+ }
+ }
+ }
+
+ return expressionLabels;
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/LoadBalancerConfiguration.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/LoadBalancerConfiguration.java
new file mode 100644
index 000000000..a77a8588c
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/LoadBalancerConfiguration.java
@@ -0,0 +1,96 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.config;
+
+import com.tencent.cloud.polaris.router.PolarisRouterServiceInstanceListSupplier;
+import com.tencent.polaris.router.api.core.RouterAPI;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
+import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
+import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+
+/**
+ * Auto configuration for ribbon components.
+ * @author lepdou 2022-05-17
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnDiscoveryEnabled
+public class LoadBalancerConfiguration {
+
+ /**
+ * Order of reactive discovery service instance supplier.
+ */
+ private static final int REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER = 193827465;
+
+ @Configuration
+ @ConditionalOnReactiveDiscoveryEnabled
+ @Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER)
+ static class PolarisReactiveSupportConfiguration {
+
+ @Bean
+ @ConditionalOnBean(ReactiveDiscoveryClient.class)
+ @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "polaris")
+ public ServiceInstanceListSupplier polarisRouterDiscoveryClientServiceInstanceListSupplier(
+ ConfigurableApplicationContext context,
+ RouterAPI routerAPI,
+ PolarisNearByRouterProperties polarisNearByRouterProperties,
+ PolarisMetadataRouterProperties polarisMetadataRouterProperties,
+ PolarisRuleBasedRouterProperties polarisRuleBasedRouterProperties) {
+ return new PolarisRouterServiceInstanceListSupplier(
+ ServiceInstanceListSupplier.builder().withDiscoveryClient().build(context),
+ routerAPI,
+ polarisNearByRouterProperties,
+ polarisMetadataRouterProperties,
+ polarisRuleBasedRouterProperties);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnBlockingDiscoveryEnabled
+ @Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER + 1)
+ static class PolarisBlockingSupportConfiguration {
+
+ @Bean
+ @ConditionalOnBean(DiscoveryClient.class)
+ @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "polaris")
+ public ServiceInstanceListSupplier polarisRouterDiscoveryClientServiceInstanceListSupplier(
+ ConfigurableApplicationContext context,
+ RouterAPI routerAPI,
+ PolarisNearByRouterProperties polarisNearByRouterProperties,
+ PolarisMetadataRouterProperties polarisMetadataRouterProperties,
+ PolarisRuleBasedRouterProperties polarisRuleBasedRouterProperties) {
+ return new PolarisRouterServiceInstanceListSupplier(
+ ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().build(context),
+ routerAPI,
+ polarisNearByRouterProperties,
+ polarisMetadataRouterProperties,
+ polarisRuleBasedRouterProperties);
+ }
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisMetadataRouterProperties.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisMetadataRouterProperties.java
new file mode 100644
index 000000000..4a20e53f6
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisMetadataRouterProperties.java
@@ -0,0 +1,46 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * the configuration for metadata router.
+ * @author lepdou 2022-05-23
+ */
+@ConfigurationProperties(prefix = "spring.cloud.polaris.router.metadata-router")
+public class PolarisMetadataRouterProperties {
+
+ private boolean enabled = true;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisMetadataRouterProperties{" +
+ "enabled=" + enabled +
+ '}';
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisNearByRouterProperties.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisNearByRouterProperties.java
new file mode 100644
index 000000000..3467b0587
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisNearByRouterProperties.java
@@ -0,0 +1,47 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * the configuration for nearby router.
+ *
+ * @author lepdou 2022-05-23
+ */
+@ConfigurationProperties(prefix = "spring.cloud.polaris.router.nearby-router")
+public class PolarisNearByRouterProperties {
+
+ private boolean enabled = true;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisNearByRouterProperties{" +
+ "enabled=" + enabled +
+ '}';
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisRuleBasedRouterProperties.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisRuleBasedRouterProperties.java
new file mode 100644
index 000000000..89b4bead5
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/PolarisRuleBasedRouterProperties.java
@@ -0,0 +1,48 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * the configuration for rule based router.
+ *
+ * @author lepdou 2022-05-23
+ */
+@ConfigurationProperties(prefix = "spring.cloud.polaris.router.rule-router")
+public class PolarisRuleBasedRouterProperties {
+
+ private boolean enabled = true;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisNearByRouterProperties{" +
+ "enabled=" + enabled +
+ '}';
+ }
+
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java
new file mode 100644
index 000000000..a9e1a54df
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.config;
+
+import java.util.List;
+
+import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
+import com.tencent.cloud.polaris.context.ServiceRuleManager;
+import com.tencent.cloud.polaris.router.RouterRuleLabelResolver;
+import com.tencent.cloud.polaris.router.feign.RouterLabelFeignInterceptor;
+import com.tencent.cloud.polaris.router.resttemplate.PolarisLoadBalancerBeanPostProcessor;
+import com.tencent.cloud.polaris.router.spi.RouterLabelResolver;
+
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.annotation.Order;
+import org.springframework.lang.Nullable;
+
+import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
+
+/**
+ * router module auto configuration.
+ *
+ *@author lepdou 2022-05-11
+ */
+@Configuration
+@LoadBalancerClients(defaultConfiguration = LoadBalancerConfiguration.class)
+@Import({PolarisNearByRouterProperties.class, PolarisMetadataRouterProperties.class, PolarisRuleBasedRouterProperties.class})
+public class RouterAutoConfiguration {
+
+ @Bean
+ public RouterLabelFeignInterceptor routerLabelInterceptor(@Nullable List routerLabelResolvers,
+ MetadataLocalProperties metadataLocalProperties,
+ RouterRuleLabelResolver routerRuleLabelResolver) {
+ return new RouterLabelFeignInterceptor(routerLabelResolvers, metadataLocalProperties, routerRuleLabelResolver);
+ }
+
+ @Bean
+ @Order(HIGHEST_PRECEDENCE)
+ public PolarisLoadBalancerBeanPostProcessor polarisLoadBalancerBeanPostProcessor() {
+ return new PolarisLoadBalancerBeanPostProcessor();
+ }
+
+ @Bean
+ public RouterRuleLabelResolver routerRuleLabelResolver(ServiceRuleManager serviceRuleManager) {
+ return new RouterRuleLabelResolver(serviceRuleManager);
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/feign/FeignExpressionLabelUtils.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/feign/FeignExpressionLabelUtils.java
new file mode 100644
index 000000000..92b8a6608
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/feign/FeignExpressionLabelUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.feign;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.tencent.cloud.common.util.ExpressionLabelUtils;
+import feign.RequestTemplate;
+import org.apache.commons.lang.StringUtils;
+
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Resolve rule expression label from feign request.
+ * @author lepdou 2022-05-20
+ */
+public class FeignExpressionLabelUtils {
+
+ public static Map resolve(RequestTemplate request, Set labelKeys) {
+ if (CollectionUtils.isEmpty(labelKeys)) {
+ return Collections.emptyMap();
+ }
+
+ Map labels = new HashMap<>();
+
+ for (String labelKey : labelKeys) {
+ if (StringUtils.startsWithIgnoreCase(labelKey, ExpressionLabelUtils.LABEL_HEADER_PREFIX)) {
+ String headerKey = ExpressionLabelUtils.parseHeaderKey(labelKey);
+ if (StringUtils.isBlank(headerKey)) {
+ continue;
+ }
+ labels.put(labelKey, getHeaderValue(request, headerKey));
+ }
+ else if (StringUtils.startsWithIgnoreCase(labelKey, ExpressionLabelUtils.LABEL_QUERY_PREFIX)) {
+ String queryKey = ExpressionLabelUtils.parseQueryKey(labelKey);
+ if (StringUtils.isBlank(queryKey)) {
+ continue;
+ }
+ labels.put(labelKey, getQueryValue(request, queryKey));
+ }
+ else if (StringUtils.equalsIgnoreCase(ExpressionLabelUtils.LABEL_METHOD, labelKey)) {
+ labels.put(labelKey, request.method());
+ }
+ else if (StringUtils.equalsIgnoreCase(ExpressionLabelUtils.LABEL_URI, labelKey)) {
+ URI uri = URI.create(request.request().url());
+ labels.put(labelKey, uri.getPath());
+ }
+ }
+
+ return labels;
+ }
+
+ public static String getHeaderValue(RequestTemplate request, String key) {
+ Map> headers = request.headers();
+ return ExpressionLabelUtils.getFirstValue(headers, key);
+
+ }
+
+ public static String getQueryValue(RequestTemplate request, String key) {
+ return ExpressionLabelUtils.getFirstValue(request.queries(), key);
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/feign/RouterLabelFeignInterceptor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/feign/RouterLabelFeignInterceptor.java
new file mode 100644
index 000000000..fac0879b4
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/feign/RouterLabelFeignInterceptor.java
@@ -0,0 +1,134 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.feign;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.tencent.cloud.common.metadata.MetadataContext;
+import com.tencent.cloud.common.metadata.MetadataContextHolder;
+import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
+import com.tencent.cloud.common.util.ExpressionLabelUtils;
+import com.tencent.cloud.common.util.JacksonUtils;
+import com.tencent.cloud.polaris.router.RouterConstants;
+import com.tencent.cloud.polaris.router.RouterRuleLabelResolver;
+import com.tencent.cloud.polaris.router.spi.RouterLabelResolver;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.core.Ordered;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Resolver labels from request.
+ *
+ *@author lepdou 2022-05-12
+ */
+public class RouterLabelFeignInterceptor implements RequestInterceptor, Ordered {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RouterLabelFeignInterceptor.class);
+
+ private final List routerLabelResolvers;
+ private final MetadataLocalProperties metadataLocalProperties;
+ private final RouterRuleLabelResolver routerRuleLabelResolver;
+
+ public RouterLabelFeignInterceptor(List routerLabelResolvers,
+ MetadataLocalProperties metadataLocalProperties,
+ RouterRuleLabelResolver routerRuleLabelResolver) {
+ if (!CollectionUtils.isEmpty(routerLabelResolvers)) {
+ routerLabelResolvers.sort(Comparator.comparingInt(Ordered::getOrder));
+ this.routerLabelResolvers = routerLabelResolvers;
+ }
+ else {
+ this.routerLabelResolvers = null;
+ }
+ this.metadataLocalProperties = metadataLocalProperties;
+ this.routerRuleLabelResolver = routerRuleLabelResolver;
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+
+ @Override
+ public void apply(RequestTemplate requestTemplate) {
+ // local service labels
+ Map labels = new HashMap<>(metadataLocalProperties.getContent());
+
+ // labels from rule expression
+ String peerServiceName = requestTemplate.feignTarget().name();
+ Map ruleExpressionLabels = getRuleExpressionLabels(requestTemplate, peerServiceName);
+ labels.putAll(ruleExpressionLabels);
+
+ // labels from request
+ if (!CollectionUtils.isEmpty(routerLabelResolvers)) {
+ routerLabelResolvers.forEach(resolver -> {
+ try {
+ Map customResolvedLabels = resolver.resolve(requestTemplate);
+ if (!CollectionUtils.isEmpty(customResolvedLabels)) {
+ labels.putAll(customResolvedLabels);
+ }
+ }
+ catch (Throwable t) {
+ LOGGER.error("[SCT][Router] revoke RouterLabelResolver occur some exception. ", t);
+ }
+ });
+ }
+
+ // labels from downstream
+ Map transitiveLabels = MetadataContextHolder.get()
+ .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE);
+ labels.putAll(transitiveLabels);
+
+ // Because when the label is placed in RequestTemplate.header,
+ // RequestTemplate will parse the header according to the regular, which conflicts with the expression.
+ // Avoid conflicts by escaping.
+ Map escapeLabels = new HashMap<>(labels.size());
+ for (Map.Entry entry : labels.entrySet()) {
+ String escapedKey = ExpressionLabelUtils.escape(entry.getKey());
+ String escapedValue = ExpressionLabelUtils.escape(entry.getValue());
+ escapeLabels.put(escapedKey, escapedValue);
+ }
+
+ // pass label by header
+ if (escapeLabels.size() == 0) {
+ requestTemplate.header(RouterConstants.ROUTER_LABEL_HEADER);
+ return;
+ }
+ requestTemplate.header(RouterConstants.ROUTER_LABEL_HEADER, JacksonUtils.serialize2Json(escapeLabels));
+ }
+
+ private Map getRuleExpressionLabels(RequestTemplate requestTemplate, String peerService) {
+ Set labelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE,
+ MetadataContext.LOCAL_SERVICE, peerService);
+
+ if (CollectionUtils.isEmpty(labelKeys)) {
+ return Collections.emptyMap();
+ }
+
+ return FeignExpressionLabelUtils.resolve(requestTemplate, labelKeys);
+ }
+
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessor.java
new file mode 100644
index 000000000..5bafbd9fb
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessor.java
@@ -0,0 +1,65 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.resttemplate;
+
+import java.util.List;
+
+import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
+import com.tencent.cloud.common.util.BeanFactoryUtils;
+import com.tencent.cloud.polaris.router.RouterRuleLabelResolver;
+import com.tencent.cloud.polaris.router.spi.RouterLabelResolver;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory;
+
+/**
+ * Replace LoadBalancerInterceptor with PolarisLoadBalancerInterceptor.
+ * PolarisLoadBalancerInterceptor can pass routing context information.
+ *
+ *@author lepdou 2022-05-18
+ */
+public class PolarisLoadBalancerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
+
+ private BeanFactory factory;
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.factory = beanFactory;
+ }
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof LoadBalancerInterceptor) {
+ LoadBalancerRequestFactory requestFactory = this.factory.getBean(LoadBalancerRequestFactory.class);
+ LoadBalancerClient loadBalancerClient = this.factory.getBean(LoadBalancerClient.class);
+ List routerLabelResolvers = BeanFactoryUtils.getBeans(factory, RouterLabelResolver.class);
+ MetadataLocalProperties metadataLocalProperties = this.factory.getBean(MetadataLocalProperties.class);
+ RouterRuleLabelResolver routerRuleLabelResolver = this.factory.getBean(RouterRuleLabelResolver.class);
+
+ return new PolarisLoadBalancerInterceptor(loadBalancerClient, requestFactory,
+ routerLabelResolvers, metadataLocalProperties, routerRuleLabelResolver);
+ }
+ return bean;
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java
new file mode 100644
index 000000000..eb7c90ce0
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java
@@ -0,0 +1,157 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.resttemplate;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.tencent.cloud.common.metadata.MetadataContext;
+import com.tencent.cloud.common.metadata.MetadataContextHolder;
+import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
+import com.tencent.cloud.common.util.ExpressionLabelUtils;
+import com.tencent.cloud.common.util.JacksonUtils;
+import com.tencent.cloud.polaris.router.RouterConstants;
+import com.tencent.cloud.polaris.router.RouterRuleLabelResolver;
+import com.tencent.cloud.polaris.router.spi.RouterLabelResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor capabilities.
+ * Parses the label from the request and puts it into the RouterContext for routing.
+ *
+ *@author lepdou 2022-05-18
+ */
+public class PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PolarisLoadBalancerInterceptor.class);
+
+ private final LoadBalancerClient loadBalancer;
+ private final LoadBalancerRequestFactory requestFactory;
+ private final List routerLabelResolvers;
+ private final MetadataLocalProperties metadataLocalProperties;
+ private final RouterRuleLabelResolver routerRuleLabelResolver;
+
+ public PolarisLoadBalancerInterceptor(LoadBalancerClient loadBalancer,
+ LoadBalancerRequestFactory requestFactory,
+ List routerLabelResolvers,
+ MetadataLocalProperties metadataLocalProperties,
+ RouterRuleLabelResolver routerRuleLabelResolver) {
+ super(loadBalancer, requestFactory);
+ this.loadBalancer = loadBalancer;
+ this.requestFactory = requestFactory;
+ this.metadataLocalProperties = metadataLocalProperties;
+ this.routerRuleLabelResolver = routerRuleLabelResolver;
+
+ if (!CollectionUtils.isEmpty(routerLabelResolvers)) {
+ routerLabelResolvers.sort(Comparator.comparingInt(Ordered::getOrder));
+ this.routerLabelResolvers = routerLabelResolvers;
+ }
+ else {
+ this.routerLabelResolvers = null;
+ }
+ }
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
+ final URI originalUri = request.getURI();
+ String peerServiceName = originalUri.getHost();
+ Assert.state(peerServiceName != null,
+ "Request URI does not contain a valid hostname: " + originalUri);
+
+ setLabelsToHeaders(request, body, peerServiceName);
+
+ return this.loadBalancer.execute(peerServiceName,
+ new PolarisLoadBalancerRequest<>(request, this.requestFactory.createRequest(request, body, execution)));
+ }
+
+ void setLabelsToHeaders(HttpRequest request, byte[] body, String peerServiceName) {
+ // local service labels
+ Map labels = new HashMap<>(metadataLocalProperties.getContent());
+
+ // labels from rule expression
+ Map ruleExpressionLabels = getExpressionLabels(request, peerServiceName);
+ if (!CollectionUtils.isEmpty(ruleExpressionLabels)) {
+ labels.putAll(ruleExpressionLabels);
+ }
+
+ // labels from request
+ if (!CollectionUtils.isEmpty(routerLabelResolvers)) {
+ routerLabelResolvers.forEach(resolver -> {
+ try {
+ Map customResolvedLabels = resolver.resolve(request, body);
+ if (!CollectionUtils.isEmpty(customResolvedLabels)) {
+ labels.putAll(customResolvedLabels);
+ }
+ }
+ catch (Throwable t) {
+ LOGGER.error("[SCT][Router] revoke RouterLabelResolver occur some exception. ", t);
+ }
+ });
+ }
+
+ // labels from downstream
+ Map transitiveLabels = MetadataContextHolder.get()
+ .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE);
+ labels.putAll(transitiveLabels);
+
+ // Because when the label is placed in RequestTemplate.header,
+ // RequestTemplate will parse the header according to the regular, which conflicts with the expression.
+ // Avoid conflicts by escaping.
+ Map escapeLabels = new HashMap<>(labels.size());
+ for (Map.Entry entry : labels.entrySet()) {
+ String escapedKey = ExpressionLabelUtils.escape(entry.getKey());
+ String escapedValue = ExpressionLabelUtils.escape(entry.getValue());
+ escapeLabels.put(escapedKey, escapedValue);
+ }
+
+ // pass label by header
+ if (escapeLabels.size() == 0) {
+ request.getHeaders().set(RouterConstants.ROUTER_LABEL_HEADER, null);
+ return;
+ }
+ request.getHeaders().set(RouterConstants.ROUTER_LABEL_HEADER, JacksonUtils.serialize2Json(escapeLabels));
+ }
+
+ private Map getExpressionLabels(HttpRequest request, String peerServiceName) {
+ Set labelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE,
+ MetadataContext.LOCAL_SERVICE, peerServiceName);
+
+ if (CollectionUtils.isEmpty(labelKeys)) {
+ return Collections.emptyMap();
+ }
+
+ return ExpressionLabelUtils.resolve(request, labelKeys);
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerRequest.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerRequest.java
new file mode 100644
index 000000000..ad4d47295
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerRequest.java
@@ -0,0 +1,52 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.resttemplate;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
+import org.springframework.http.HttpRequest;
+
+/**
+ * Wrapper of {@link LoadBalancerRequest}.
+ *
+ * @author Haotian Zhang
+ */
+public class PolarisLoadBalancerRequest implements LoadBalancerRequest {
+
+ private HttpRequest request;
+
+ private LoadBalancerRequest delegate;
+
+ public PolarisLoadBalancerRequest(HttpRequest request, LoadBalancerRequest delegate) {
+ this.request = request;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public T apply(ServiceInstance instance) throws Exception {
+ return delegate.apply(instance);
+ }
+
+ public HttpRequest getRequest() {
+ return request;
+ }
+
+ public LoadBalancerRequest getDelegate() {
+ return delegate;
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java
new file mode 100644
index 000000000..49b9ccf4c
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java
@@ -0,0 +1,49 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.spi;
+
+import java.util.Map;
+
+import feign.RequestTemplate;
+
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpRequest;
+
+/**
+ * The spi for resolving labels from request.
+ *
+ * @author lepdou 2022-05-11
+ */
+public interface RouterLabelResolver extends Ordered {
+
+ /**
+ * resolve labels from feign request.
+ * @param requestTemplate the feign request.
+ * @return resolved labels
+ */
+ Map resolve(RequestTemplate requestTemplate);
+
+ /**
+ * resolve labels from rest template request.
+ * @param request the rest template request.
+ * @param body the rest template request body.
+ * @return resolved labels
+ */
+ Map resolve(HttpRequest request, byte[] body);
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 000000000..4f248b727
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,22 @@
+{
+ "properties": [
+ {
+ "name": "spring.cloud.polaris.router.metadata-router.enabled",
+ "type": "java.lang.Boolean",
+ "defaultValue": true,
+ "description": "the switch for metadata router."
+ },
+ {
+ "name": "spring.cloud.polaris.router.nearby-router.enabled",
+ "type": "java.lang.Boolean",
+ "defaultValue": true,
+ "description": "the switch for near by router."
+ },
+ {
+ "name": "spring.cloud.polaris.router.rule-router.enabled",
+ "type": "java.lang.Boolean",
+ "defaultValue": true,
+ "description": "the switch for rule based router."
+ }
+ ]
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..d33dcea7f
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ com.tencent.cloud.polaris.router.config.RouterAutoConfiguration
diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/PolarisRouterContextTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/PolarisRouterContextTest.java
new file mode 100644
index 000000000..2a8d38752
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/PolarisRouterContextTest.java
@@ -0,0 +1,64 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * test for {@link PolarisRouterContext}
+ *
+ *@author lepdou 2022-05-26
+ */
+public class PolarisRouterContextTest {
+
+ @Test
+ public void testNormalGetterSetter() {
+ Map labels = new HashMap<>();
+ labels.put("k1", "v1");
+ labels.put("k2", "v2");
+
+ PolarisRouterContext routerContext = new PolarisRouterContext();
+ routerContext.setLabels(PolarisRouterContext.RULE_ROUTER_LABELS, labels);
+
+ Assert.assertEquals(0, routerContext.getLabels(PolarisRouterContext.TRANSITIVE_LABELS).size());
+ Assert.assertEquals(2, routerContext.getLabels(PolarisRouterContext.RULE_ROUTER_LABELS).size());
+ Assert.assertEquals("v1", routerContext.getLabels(PolarisRouterContext.RULE_ROUTER_LABELS).get("k1"));
+ Assert.assertEquals("v2", routerContext.getLabels(PolarisRouterContext.RULE_ROUTER_LABELS).get("k2"));
+ Assert.assertNull(routerContext.getLabels(PolarisRouterContext.RULE_ROUTER_LABELS).get("k3"));
+ }
+
+ @Test
+ public void testSetNull() {
+ PolarisRouterContext routerContext = new PolarisRouterContext();
+ routerContext.setLabels(PolarisRouterContext.RULE_ROUTER_LABELS, null);
+ Assert.assertEquals(0, routerContext.getLabels(PolarisRouterContext.TRANSITIVE_LABELS).size());
+ Assert.assertEquals(0, routerContext.getLabels(PolarisRouterContext.RULE_ROUTER_LABELS).size());
+ }
+
+ @Test
+ public void testGetEmptyRouterContext() {
+ PolarisRouterContext routerContext = new PolarisRouterContext();
+ Assert.assertEquals(0, routerContext.getLabels(PolarisRouterContext.TRANSITIVE_LABELS).size());
+ Assert.assertEquals(0, routerContext.getLabels(PolarisRouterContext.RULE_ROUTER_LABELS).size());
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/PolarisRouterServiceInstanceListSupplierTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/PolarisRouterServiceInstanceListSupplierTest.java
new file mode 100644
index 000000000..51a2bab30
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/PolarisRouterServiceInstanceListSupplierTest.java
@@ -0,0 +1,248 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.tencent.cloud.common.metadata.MetadataContext;
+import com.tencent.cloud.common.metadata.MetadataContextHolder;
+import com.tencent.cloud.common.pojo.PolarisServiceInstance;
+import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
+import com.tencent.cloud.polaris.loadbalancer.config.PolarisLoadBalancerProperties;
+import com.tencent.cloud.polaris.router.config.PolarisMetadataRouterProperties;
+import com.tencent.cloud.polaris.router.config.PolarisNearByRouterProperties;
+import com.tencent.cloud.polaris.router.config.PolarisRuleBasedRouterProperties;
+import com.tencent.polaris.api.pojo.DefaultInstance;
+import com.tencent.polaris.api.pojo.DefaultServiceInstances;
+import com.tencent.polaris.api.pojo.Instance;
+import com.tencent.polaris.api.pojo.ServiceInstances;
+import com.tencent.polaris.api.pojo.ServiceKey;
+import com.tencent.polaris.plugins.router.metadata.MetadataRouter;
+import com.tencent.polaris.plugins.router.nearby.NearbyRouter;
+import com.tencent.polaris.plugins.router.rule.RuleBasedRouter;
+import com.tencent.polaris.router.api.core.RouterAPI;
+import com.tencent.polaris.router.api.rpc.ProcessRoutersRequest;
+import com.tencent.polaris.router.api.rpc.ProcessRoutersResponse;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * test for {@link PolarisRouterServiceInstanceListSupplier}
+ *@author lepdou 2022-05-26
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class PolarisRouterServiceInstanceListSupplierTest {
+
+ private static AtomicBoolean initTransitiveMetadata = new AtomicBoolean(false);
+ @Mock
+ private ServiceInstanceListSupplier delegate;
+ @Mock
+ private PolarisLoadBalancerProperties polarisLoadBalancerProperties;
+ @Mock
+ private PolarisNearByRouterProperties polarisNearByRouterProperties;
+ @Mock
+ private PolarisMetadataRouterProperties polarisMetadataRouterProperties;
+ @Mock
+ private PolarisRuleBasedRouterProperties polarisRuleBasedRouterProperties;
+ @Mock
+ private RouterAPI routerAPI;
+ private String testNamespace = "testNamespace";
+ private String testCallerService = "testCallerService";
+ private String testCalleeService = "testCalleeService";
+
+ @Test
+ public void testBuildMetadataRouteRequest() {
+ when(polarisMetadataRouterProperties.isEnabled()).thenReturn(true);
+
+ try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) {
+ mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
+ .thenReturn(testCallerService);
+
+ setTransitiveMetadata();
+
+ PolarisRouterServiceInstanceListSupplier compositeRule = new PolarisRouterServiceInstanceListSupplier(
+ delegate, routerAPI, polarisNearByRouterProperties,
+ polarisMetadataRouterProperties, polarisRuleBasedRouterProperties);
+
+ ServiceInstances serviceInstances = assembleServiceInstances();
+ PolarisRouterContext routerContext = assembleRouterContext();
+
+ ProcessRoutersRequest request = compositeRule.buildProcessRoutersRequest(serviceInstances, routerContext);
+
+ Map routerMetadata = request.getRouterMetadata(MetadataRouter.ROUTER_TYPE_METADATA);
+
+ Assert.assertEquals(1, routerMetadata.size());
+ Assert.assertEquals(0, request.getRouterMetadata(NearbyRouter.ROUTER_TYPE_NEAR_BY).size());
+ Assert.assertEquals(1, request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED).size());
+ Assert.assertEquals("false", request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED)
+ .get(RuleBasedRouter.ROUTER_ENABLED));
+ }
+ }
+
+ @Test
+ public void testBuildNearbyRouteRequest() {
+ when(polarisNearByRouterProperties.isEnabled()).thenReturn(true);
+
+ try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) {
+ mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
+ .thenReturn(testCallerService);
+
+ setTransitiveMetadata();
+
+ PolarisRouterServiceInstanceListSupplier compositeRule = new PolarisRouterServiceInstanceListSupplier(
+ delegate, routerAPI, polarisNearByRouterProperties,
+ polarisMetadataRouterProperties, polarisRuleBasedRouterProperties);
+
+ ServiceInstances serviceInstances = assembleServiceInstances();
+ PolarisRouterContext routerContext = assembleRouterContext();
+
+ ProcessRoutersRequest request = compositeRule.buildProcessRoutersRequest(serviceInstances, routerContext);
+
+ Map routerMetadata = request.getRouterMetadata(NearbyRouter.ROUTER_TYPE_NEAR_BY);
+
+ Assert.assertEquals(0, request.getRouterMetadata(MetadataRouter.ROUTER_TYPE_METADATA).size());
+ Assert.assertEquals(1, routerMetadata.size());
+ Assert.assertEquals("true", routerMetadata.get(NearbyRouter.ROUTER_ENABLED));
+ Assert.assertEquals(1, request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED).size());
+ Assert.assertEquals("false", request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED)
+ .get(RuleBasedRouter.ROUTER_ENABLED));
+ }
+ }
+
+ @Test
+ public void testBuildRuleBasedRouteRequest() {
+ when(polarisRuleBasedRouterProperties.isEnabled()).thenReturn(true);
+
+ try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) {
+ mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())).
+ thenReturn(testCallerService);
+
+ setTransitiveMetadata();
+
+ PolarisRouterServiceInstanceListSupplier compositeRule = new PolarisRouterServiceInstanceListSupplier(
+ delegate, routerAPI, polarisNearByRouterProperties,
+ polarisMetadataRouterProperties, polarisRuleBasedRouterProperties);
+
+ ServiceInstances serviceInstances = assembleServiceInstances();
+ PolarisRouterContext routerContext = assembleRouterContext();
+
+ ProcessRoutersRequest request = compositeRule.buildProcessRoutersRequest(serviceInstances, routerContext);
+
+ Map routerMetadata = request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED);
+
+ Assert.assertEquals(1, routerMetadata.size());
+ Assert.assertEquals(0, request.getRouterMetadata(MetadataRouter.ROUTER_TYPE_METADATA).size());
+ Assert.assertEquals(0, request.getRouterMetadata(NearbyRouter.ROUTER_TYPE_NEAR_BY).size());
+ Assert.assertEquals(1, request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED).size());
+ Assert.assertEquals("true", request.getRouterMetadata(RuleBasedRouter.ROUTER_TYPE_RULE_BASED)
+ .get(RuleBasedRouter.ROUTER_ENABLED));
+ }
+ }
+
+ @Test
+ public void testRouter() {
+ when(polarisRuleBasedRouterProperties.isEnabled()).thenReturn(true);
+
+ try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) {
+ mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
+ .thenReturn(testCallerService);
+
+ setTransitiveMetadata();
+
+ PolarisRouterServiceInstanceListSupplier compositeRule = new PolarisRouterServiceInstanceListSupplier(
+ delegate, routerAPI, polarisNearByRouterProperties,
+ polarisMetadataRouterProperties, polarisRuleBasedRouterProperties);
+
+ ProcessRoutersResponse assembleResponse = assembleProcessRoutersResponse();
+ when(routerAPI.processRouters(any())).thenReturn(assembleResponse);
+
+ Flux> servers = compositeRule.doRouter(assembleServers(), assembleRouterContext());
+
+
+ Assert.assertEquals(assembleResponse.getServiceInstances().getInstances().size(),
+ servers.toStream().mapToLong(List::size).sum());
+ }
+ }
+
+ private void setTransitiveMetadata() {
+ if (initTransitiveMetadata.compareAndSet(false, true)) {
+ // mock transitive metadata
+ MetadataContext metadataContext = Mockito.mock(MetadataContext.class);
+ try (MockedStatic mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class)) {
+ mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext);
+ }
+ }
+ }
+
+ private ServiceInstances assembleServiceInstances() {
+ ServiceKey serviceKey = new ServiceKey(testNamespace, testCalleeService);
+ List instances = new LinkedList<>();
+ instances.add(new DefaultInstance());
+ instances.add(new DefaultInstance());
+ instances.add(new DefaultInstance());
+ instances.add(new DefaultInstance());
+ instances.add(new DefaultInstance());
+
+ return new DefaultServiceInstances(serviceKey, instances);
+ }
+
+ private PolarisRouterContext assembleRouterContext() {
+ PolarisRouterContext routerContext = new PolarisRouterContext();
+ Map transitiveLabels = new HashMap<>();
+ transitiveLabels.put("k1", "v1");
+ Map routerLabels = new HashMap<>();
+ routerLabels.put("k2", "v2");
+ routerLabels.put("k3", "v3");
+ routerContext.setLabels(PolarisRouterContext.TRANSITIVE_LABELS, transitiveLabels);
+ routerContext.setLabels(PolarisRouterContext.RULE_ROUTER_LABELS, routerLabels);
+ return routerContext;
+ }
+
+ private ProcessRoutersResponse assembleProcessRoutersResponse() {
+ return new ProcessRoutersResponse(assembleServiceInstances());
+ }
+
+ private Flux> assembleServers() {
+ ServiceInstances serviceInstances = assembleServiceInstances();
+ List servers = new ArrayList<>();
+ for (Instance instance : serviceInstances.getInstances()) {
+ servers.add(new PolarisServiceInstance(instance));
+ }
+ return Flux.fromIterable(Collections.singletonList(servers));
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/RouterRuleLabelResolverTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/RouterRuleLabelResolverTest.java
new file mode 100644
index 000000000..26c1dfb13
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/RouterRuleLabelResolverTest.java
@@ -0,0 +1,93 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router;
+
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.Lists;
+import com.tencent.cloud.polaris.context.ServiceRuleManager;
+import com.tencent.polaris.client.pb.ModelProto;
+import com.tencent.polaris.client.pb.RoutingProto;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.Mockito.when;
+
+/**
+ * test for {@link RouterRuleLabelResolver}
+ *@author lepdou 2022-05-26
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class RouterRuleLabelResolverTest {
+
+ @Mock
+ private ServiceRuleManager serviceRuleManager;
+
+ private final String testNamespace = "testNamespace";
+ private final String testSourceService = "sourceService";
+ private final String testDstService = "dstService";
+
+ @Test
+ public void test() {
+ Map labels = new HashMap<>();
+ ModelProto.MatchString matchString = ModelProto.MatchString.getDefaultInstance();
+ String validKey1 = "${http.header.uid}";
+ String validKey2 = "${http.query.name}";
+ String validKey3 = "${http.method}";
+ String validKey4 = "${http.uri}";
+ String invalidKey = "${http.expression.wrong}";
+ labels.put(validKey1, matchString);
+ labels.put(validKey2, matchString);
+ labels.put(validKey3, matchString);
+ labels.put(validKey4, matchString);
+ labels.put(invalidKey, matchString);
+
+ RoutingProto.Source source1 = RoutingProto.Source.newBuilder().putAllMetadata(labels).build();
+ RoutingProto.Source source2 = RoutingProto.Source.newBuilder().putAllMetadata(labels).build();
+ RoutingProto.Source source3 = RoutingProto.Source.newBuilder().putAllMetadata(new HashMap<>()).build();
+
+ List routes = new LinkedList<>();
+ RoutingProto.Route route = RoutingProto.Route.newBuilder()
+ .addAllSources(Lists.newArrayList(source1, source2, source3))
+ .build();
+ routes.add(route);
+
+ when(serviceRuleManager.getServiceRouterRule(testNamespace, testSourceService, testDstService)).thenReturn(routes);
+
+ RouterRuleLabelResolver resolver = new RouterRuleLabelResolver(serviceRuleManager);
+
+ Set resolvedExpressionLabelKeys = resolver.getExpressionLabelKeys(testNamespace, testSourceService, testDstService);
+
+ Assert.assertNotNull(resolvedExpressionLabelKeys);
+ Assert.assertEquals(4, resolvedExpressionLabelKeys.size());
+ Assert.assertTrue(resolvedExpressionLabelKeys.contains(validKey1));
+ Assert.assertTrue(resolvedExpressionLabelKeys.contains(validKey2));
+ Assert.assertTrue(resolvedExpressionLabelKeys.contains(validKey3));
+ Assert.assertTrue(resolvedExpressionLabelKeys.contains(validKey4));
+ Assert.assertFalse(resolvedExpressionLabelKeys.contains(invalidKey));
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/feign/FeignExpressionLabelUtilsTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/feign/FeignExpressionLabelUtilsTest.java
new file mode 100644
index 000000000..6078be75c
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/feign/FeignExpressionLabelUtilsTest.java
@@ -0,0 +1,140 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.feign;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.common.collect.Sets;
+import feign.Request;
+import feign.RequestTemplate;
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Test for {@link FeignExpressionLabelUtils}
+ *@author lepdou 2022-05-26
+ */
+public class FeignExpressionLabelUtilsTest {
+
+ @Test
+ public void testGetHeaderLabel() {
+ String headerKey = "uid";
+ String headerValue = "1000";
+ String headerKey2 = "teacher.age";
+ String headerValue2 = "1000";
+
+ RequestTemplate requestTemplate = new RequestTemplate();
+ requestTemplate.header(headerKey, headerValue);
+ requestTemplate.header(headerKey2, headerValue2);
+
+ String labelKey1 = "${http.header.uid}";
+ String labelKey2 = "${http.header.name}";
+ String labelKey3 = "${http.headername}";
+ String labelKey4 = "${http.header.}";
+ String labelKey5 = "${http.header.teacher.age}";
+ Map result = FeignExpressionLabelUtils.resolve(requestTemplate,
+ Sets.newHashSet(labelKey1, labelKey2, labelKey3, labelKey4, labelKey5));
+
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertEquals(headerValue, result.get(labelKey1));
+ Assert.assertEquals(headerValue2, result.get(labelKey5));
+ Assert.assertTrue(StringUtils.isEmpty(result.get(labelKey2)));
+ Assert.assertTrue(StringUtils.isEmpty(result.get(labelKey3)));
+ Assert.assertTrue(StringUtils.isEmpty(result.get(labelKey4)));
+ }
+
+ @Test
+ public void testGetQueryLabel() {
+ String headerKey = "uid";
+ String headerValue = "1000";
+ String headerKey2 = "teacher.age";
+ String headerValue2 = "1000";
+
+ RequestTemplate requestTemplate = new RequestTemplate();
+ requestTemplate.query(headerKey, headerValue);
+ requestTemplate.query(headerKey2, headerValue2);
+
+ String labelKey1 = "${http.query.uid}";
+ String labelKey2 = "${http.query.name}";
+ String labelKey3 = "${http.queryname}";
+ String labelKey4 = "${http.query.}";
+ String labelKey5 = "${http.query.teacher.age}";
+ Map result = FeignExpressionLabelUtils.resolve(requestTemplate,
+ Sets.newHashSet(labelKey1, labelKey2, labelKey3, labelKey4, labelKey5));
+
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertEquals(headerValue, result.get(labelKey1));
+ Assert.assertEquals(headerValue2, result.get(labelKey5));
+ Assert.assertTrue(StringUtils.isEmpty(result.get(labelKey2)));
+ Assert.assertTrue(StringUtils.isEmpty(result.get(labelKey3)));
+ Assert.assertTrue(StringUtils.isEmpty(result.get(labelKey4)));
+ }
+
+ @Test
+ public void testGetMethod() {
+ RequestTemplate requestTemplate = new RequestTemplate();
+ requestTemplate.method(Request.HttpMethod.GET);
+
+ String labelKey1 = "${http.method}";
+ Map result = FeignExpressionLabelUtils.resolve(requestTemplate,
+ Sets.newHashSet(labelKey1));
+
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertEquals("GET", result.get(labelKey1));
+ }
+
+ @Test
+ public void testGetUri() {
+ String uri = "/user/get";
+
+ RequestTemplate requestTemplate = new RequestTemplate();
+ requestTemplate.uri(uri);
+ requestTemplate.method(Request.HttpMethod.GET);
+ requestTemplate.target("http://localhost");
+ requestTemplate = requestTemplate.resolve(new HashMap<>());
+
+ String labelKey1 = "${http.uri}";
+ Map result = FeignExpressionLabelUtils.resolve(requestTemplate,
+ Sets.newHashSet(labelKey1));
+
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertEquals(uri, result.get(labelKey1));
+ }
+
+ @Test
+ public void testGetUri2() {
+ String uri = "/";
+
+ RequestTemplate requestTemplate = new RequestTemplate();
+ requestTemplate.uri(uri);
+ requestTemplate.method(Request.HttpMethod.GET);
+ requestTemplate.target("http://localhost");
+ requestTemplate = requestTemplate.resolve(new HashMap<>());
+
+ String labelKey1 = "${http.uri}";
+ Map result = FeignExpressionLabelUtils.resolve(requestTemplate,
+ Sets.newHashSet(labelKey1));
+
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertEquals(uri, result.get(labelKey1));
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/feign/RouterLabelFeignInterceptorTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/feign/RouterLabelFeignInterceptorTest.java
new file mode 100644
index 000000000..d5f7076fc
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/feign/RouterLabelFeignInterceptorTest.java
@@ -0,0 +1,142 @@
+/*
+ * Tencent is pleased to support the open source community by making Spring Cloud Tencent available.
+ *
+ * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * 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 com.tencent.cloud.polaris.router.feign;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.tencent.cloud.common.metadata.MetadataContext;
+import com.tencent.cloud.common.metadata.MetadataContextHolder;
+import com.tencent.cloud.common.metadata.config.MetadataLocalProperties;
+import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
+import com.tencent.cloud.common.util.ExpressionLabelUtils;
+import com.tencent.cloud.common.util.JacksonUtils;
+import com.tencent.cloud.polaris.router.RouterConstants;
+import com.tencent.cloud.polaris.router.RouterRuleLabelResolver;
+import com.tencent.cloud.polaris.router.spi.RouterLabelResolver;
+import feign.RequestTemplate;
+import feign.Target;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * test for {@link RouterLabelFeignInterceptor}
+ * @author lepdou 2022-05-26
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class RouterLabelFeignInterceptorTest {
+
+ @Mock
+ private MetadataLocalProperties metadataLocalProperties;
+ @Mock
+ private RouterRuleLabelResolver routerRuleLabelResolver;
+ @Mock
+ private RouterLabelResolver routerLabelResolver;
+
+ @Test
+ public void testResolveRouterLabel() {
+ RouterLabelFeignInterceptor routerLabelFeignInterceptor = new RouterLabelFeignInterceptor(
+ Collections.singletonList(routerLabelResolver),
+ metadataLocalProperties, routerRuleLabelResolver);
+
+ // mock request template
+ RequestTemplate requestTemplate = new RequestTemplate();
+ String headerUidKey = "uid";
+ String headerUidValue = "1000";
+ requestTemplate.header(headerUidKey, headerUidValue);
+ String peerService = "peerService";
+ Target.EmptyTarget