feat: refactor Feign eager load, add LoadBalancer warm-up, and fix gateway trailing slash compatibility (#1800)

Signed-off-by: Haotian Zhang <928016560@qq.com>
2024
shedfreewu 2 days ago committed by Haotian Zhang
parent 0f2d7073a4
commit fc8a4dbcd1

@ -6,3 +6,4 @@
- [feat: Support Polaris config env value and add related tests](https://github.com/Tencent/spring-cloud-tencent/pull/1797)
- [refactor: modify the initialization of ApplicationContextAwareUtils.](https://github.com/Tencent/spring-cloud-tencent/pull/1798)
- [feat: support enable/disable cloud location provider via configuration.](https://github.com/Tencent/spring-cloud-tencent/pull/1799)
- [feat: refactor Feign eager load, add LoadBalancer warm-up, and fix gateway trailing slash compatibility](https://github.com/Tencent/spring-cloud-tencent/pull/1800)

@ -62,5 +62,11 @@
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -19,27 +19,33 @@ package com.tencent.cloud.polaris.eager.config;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadSmartLifecycle;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadContextInitializer;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.PolarisLoadBalancerEagerContextInitializer;
import com.tencent.cloud.polaris.eager.instrument.services.ServicesEagerLoadSmartLifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(LoadBalancerEagerLoadProperties.class)
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.enabled", havingValue = "true", matchIfMissing = true)
public class PolarisEagerLoadAutoConfiguration {
@Bean
@ConditionalOnClass(name = "feign.Feign")
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.feign.enabled", havingValue = "true", matchIfMissing = true)
public FeignEagerLoadSmartLifecycle feignEagerLoadSmartLifecycle(
ApplicationContext applicationContext, @Autowired(required = false) PolarisDiscoveryClient polarisDiscoveryClient,
@Autowired(required = false) PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
return new FeignEagerLoadSmartLifecycle(applicationContext, polarisDiscoveryClient, polarisReactiveDiscoveryClient);
public FeignEagerLoadContextInitializer feignEagerLoadContextInitializer(
ApplicationContext applicationContext,
LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties) {
return new FeignEagerLoadContextInitializer(applicationContext, loadBalancerClientFactory, loadBalancerEagerLoadProperties);
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.services.enabled", havingValue = "true", matchIfMissing = true)
@ -48,5 +54,12 @@ public class PolarisEagerLoadAutoConfiguration {
@Autowired(required = false) PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
return new ServicesEagerLoadSmartLifecycle(polarisDiscoveryClient, polarisReactiveDiscoveryClient);
}
@Bean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.eager-load.enabled", matchIfMissing = true)
public PolarisLoadBalancerEagerContextInitializer polarisLoadBalancerEagerContextInitializer(
LoadBalancerClientFactory loadBalancerClientFactory, LoadBalancerEagerLoadProperties properties) {
return new PolarisLoadBalancerEagerContextInitializer(loadBalancerClientFactory, properties.getClients());
}
}

@ -0,0 +1,164 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.feign;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerWarmUpUtils;
import com.tencent.polaris.api.utils.StringUtils;
import feign.Target;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.framework.JdkDynamicAopProxyUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
/**
* Feign eager load context initializer.
* Implements ApplicationListener&lt;ApplicationReadyEvent&gt; to warm up FeignClient services
* after the application is ready.
*
* @author Yuwei Fu
*/
public class FeignEagerLoadContextInitializer implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger LOG = LoggerFactory.getLogger(FeignEagerLoadContextInitializer.class);
private final ApplicationContext applicationContext;
private final LoadBalancerClientFactory loadBalancerClientFactory;
private final LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties;
public FeignEagerLoadContextInitializer(ApplicationContext applicationContext,
LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties) {
this.applicationContext = applicationContext;
this.loadBalancerClientFactory = loadBalancerClientFactory;
this.loadBalancerEagerLoadProperties = loadBalancerEagerLoadProperties;
}
public static Target.HardCodedTarget<?> getHardCodedTarget(Object proxy) {
try {
int count = 0;
Object invocationHandler = proxy;
// Avoid infinite loop
while (count++ < 100) {
invocationHandler = Proxy.getInvocationHandler(invocationHandler);
if (invocationHandler instanceof AopProxy) {
invocationHandler = JdkDynamicAopProxyUtils.getTarget(invocationHandler);
continue;
}
break;
}
for (Field field : invocationHandler.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object fieldValue = field.get(invocationHandler);
if (fieldValue instanceof Target.HardCodedTarget) {
return (Target.HardCodedTarget<?>) fieldValue;
}
}
}
catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("proxy:{}, getTarget failed.", proxy, e);
}
}
return null;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("feign eager-load start");
// Get services that are already warmed by PolarisLoadBalancerEagerContextInitializer
Set<String> skipServices = getLoadBalancerEagerLoadServices();
// Set to track already warmed services
Set<String> warmedServices = new HashSet<>();
// Warm up FeignClient services
for (Object bean : applicationContext.getBeansWithAnnotation(FeignClient.class).values()) {
try {
if (Proxy.isProxyClass(bean.getClass())) {
Target.HardCodedTarget<?> hardCodedTarget = getHardCodedTarget(bean);
if (hardCodedTarget != null) {
FeignClient feignClient = hardCodedTarget.type().getAnnotation(FeignClient.class);
// if feignClient contains url, it doesn't need to eager load.
if (StringUtils.isEmpty(feignClient.url())) {
// support variables and default values.
String url = hardCodedTarget.name();
// refer to FeignClientFactoryBean, convert to URL, then take the host as the service name.
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "http://" + url;
}
String serviceName = URI.create(url).getHost();
// Skip if already warmed by PolarisLoadBalancerEagerContextInitializer
if (skipServices.contains(serviceName)) {
LOG.debug("[{}] skip eager-load, already configured in LoadBalancerEagerLoadProperties.clients", serviceName);
continue;
}
// Skip if already warmed in this round
if (warmedServices.contains(serviceName)) {
LOG.debug("[{}] already warmed, skip.", serviceName);
continue;
}
LOG.info("[{}] eager-load start, feign name: {}", serviceName, hardCodedTarget.name());
LoadBalancerWarmUpUtils.warmUp(loadBalancerClientFactory, serviceName);
warmedServices.add(serviceName);
}
}
}
}
catch (Exception e) {
LOG.debug("[{}] eager-load failed.", bean, e);
}
}
LOG.info("feign eager-load end");
}
/**
* Get services configured in LoadBalancerEagerLoadProperties.
* These services are warmed by PolarisLoadBalancerEagerContextInitializer.
* @return set of service names to skip
*/
private Set<String> getLoadBalancerEagerLoadServices() {
Set<String> services = new HashSet<>();
if (loadBalancerEagerLoadProperties != null
&& loadBalancerEagerLoadProperties.isEnabled()
&& loadBalancerEagerLoadProperties.getClients() != null) {
services.addAll(loadBalancerEagerLoadProperties.getClients());
}
return services;
}
}

@ -1,128 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.feign;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.URI;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import com.tencent.polaris.api.utils.StringUtils;
import feign.Target;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
private static final Logger LOG = LoggerFactory.getLogger(FeignEagerLoadSmartLifecycle.class);
private final ApplicationContext applicationContext;
private final PolarisDiscoveryClient polarisDiscoveryClient;
private final PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient;
public FeignEagerLoadSmartLifecycle(ApplicationContext applicationContext, PolarisDiscoveryClient polarisDiscoveryClient,
PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
this.applicationContext = applicationContext;
this.polarisDiscoveryClient = polarisDiscoveryClient;
this.polarisReactiveDiscoveryClient = polarisReactiveDiscoveryClient;
}
@Override
public void start() {
LOG.info("feign eager-load start");
for (Object bean : applicationContext.getBeansWithAnnotation(FeignClient.class).values()) {
try {
if (Proxy.isProxyClass(bean.getClass())) {
Target.HardCodedTarget<?> hardCodedTarget = getHardCodedTarget(bean);
if (hardCodedTarget != null) {
FeignClient feignClient = hardCodedTarget.type().getAnnotation(FeignClient.class);
// if feignClient contains url, it doesn't need to eager load.
if (StringUtils.isEmpty(feignClient.url())) {
// support variables and default values.
String url = hardCodedTarget.name();
// refer to FeignClientFactoryBean, convert to URL, then take the host as the service name.
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "http://" + url;
}
String serviceName = URI.create(url).getHost();
LOG.info("[{}] eager-load start", serviceName);
if (polarisDiscoveryClient != null) {
polarisDiscoveryClient.getInstances(serviceName);
}
else if (polarisReactiveDiscoveryClient != null) {
polarisReactiveDiscoveryClient.getInstances(serviceName).subscribe();
}
else {
LOG.warn("[{}] no discovery client found.", serviceName);
}
LOG.info("[{}] eager-load end", serviceName);
}
}
}
}
catch (Exception e) {
LOG.debug("[{}] eager-load failed.", bean, e);
}
}
LOG.info("feign eager-load end");
}
public static Target.HardCodedTarget<?> getHardCodedTarget(Object proxy) {
try {
Object invocationHandler = Proxy.getInvocationHandler(proxy);
for (Field field : invocationHandler.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object fieldValue = field.get(invocationHandler);
if (fieldValue instanceof Target.HardCodedTarget) {
return (Target.HardCodedTarget<?>) fieldValue;
}
}
}
catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("proxy:{}, getTarget failed.", proxy, e);
}
}
return null;
}
@Override
public void stop() {
}
@Override
public boolean isRunning() {
return false;
}
@Override
public int getPhase() {
return 10;
}
}

@ -0,0 +1,47 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.loadbalancer;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("spring.cloud.loadbalancer.eager-load")
public class LoadBalancerEagerLoadProperties {
private List<String> clients;
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
}

@ -0,0 +1,64 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.loadbalancer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
/**
* Utility class for load balancer warm-up operations.
* Provides common warm-up logic for eager loading services.
*
* @author Yuwei Fu
*/
public final class LoadBalancerWarmUpUtils {
private static final Logger LOG = LoggerFactory.getLogger(LoadBalancerWarmUpUtils.class);
private LoadBalancerWarmUpUtils() {
}
/**
* Warm up a service by triggering load balancer initialization.
* @param factory the LoadBalancerClientFactory
* @param serviceName the service name to warm up
* @return true if warm-up succeeded, false otherwise
*/
public static boolean warmUp(LoadBalancerClientFactory factory, String serviceName) {
try {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = factory.getInstance(serviceName);
if (loadBalancer != null) {
loadBalancer.choose();
LOG.info("[{}] eager-load end", serviceName);
return true;
}
else {
LOG.warn("[{}] no loadBalancer found.", serviceName);
return false;
}
}
catch (Exception e) {
LOG.debug("[{}] eager-load failed.", serviceName, e);
return false;
}
}
}

@ -0,0 +1,66 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.loadbalancer;
import java.util.List;
import com.tencent.polaris.api.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationListener;
/**
* @author Yuwei Fu
*/
public class PolarisLoadBalancerEagerContextInitializer implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger LOG = LoggerFactory.getLogger(PolarisLoadBalancerEagerContextInitializer.class);
private final LoadBalancerClientFactory factory;
private final List<String> serviceNames;
public PolarisLoadBalancerEagerContextInitializer(LoadBalancerClientFactory factory, List<String> serviceNames) {
this.factory = factory;
this.serviceNames = serviceNames;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("spring cloud eager-load start");
try {
if (!CollectionUtils.isEmpty(serviceNames)) {
for (String serviceName : serviceNames) {
LoadBalancerWarmUpUtils.warmUp(factory, serviceName);
}
}
LOG.info("spring cloud eager-load end");
}
catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("spring cloud eager-load failed.", e);
}
}
}
}

@ -0,0 +1,50 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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 org.springframework.aop.framework;
import java.lang.reflect.Field;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class JdkDynamicAopProxyUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JdkDynamicAopProxyUtils.class);
private JdkDynamicAopProxyUtils() {
}
public static Object getTarget(Object invocationHandler) {
if (invocationHandler instanceof JdkDynamicAopProxy) {
try {
JdkDynamicAopProxy jdkDynamicAopProxy = (JdkDynamicAopProxy) invocationHandler;
Field advisedField = JdkDynamicAopProxy.class.getDeclaredField("advised");
advisedField.setAccessible(true);
AdvisedSupport advisedSupport = (AdvisedSupport) advisedField.get(jdkDynamicAopProxy);
if (advisedSupport != null && advisedSupport.getTargetSource() != null) {
return advisedSupport.getTargetSource().getTarget();
}
}
catch (Exception e) {
LOGGER.error("Unexpected error occurred while getting target from JdkDynamicAopProxy", e);
}
}
return null;
}
}

@ -0,0 +1,249 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.feign;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties;
import com.tencent.cloud.polaris.registry.PolarisAutoServiceRegistration;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* Test for {@link FeignEagerLoadContextInitializer}.
*
* @author Test Author
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = DEFINED_PORT,
classes = FeignEagerLoadContextInitializerTest.TestApplication.class,
properties = {
"server.port=48085",
"spring.config.location = classpath:application-test.yml",
"spring.main.web-application-type = servlet",
"spring.cloud.gateway.enabled = false",
"spring.cloud.polaris.discovery.eager-load.enabled = true",
"spring.cloud.polaris.discovery.eager-load.feign.enabled = true",
"spring.cloud.loadbalancer.eager-load.enabled = true",
"spring.cloud.loadbalancer.eager-load.clients = test-service-1,test-service-2,test-feign-service-skip"
})
public class FeignEagerLoadContextInitializerTest {
@MockBean
private LoadBalancerClientFactory loadBalancerClientFactory;
@Autowired
private FeignEagerLoadContextInitializer feignEagerLoadContextInitializer;
@Autowired
private LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties;
@Autowired
private ConfigurableApplicationContext applicationContext;
@BeforeEach
public void setUp() {
reset(loadBalancerClientFactory);
}
@Test
public void testLoadBalancerEagerLoadPropertiesLoaded() {
// Verify LoadBalancerEagerLoadProperties is loaded correctly
assertThat(loadBalancerEagerLoadProperties.getClients())
.isNotNull()
.containsExactlyInAnyOrder("test-service-1", "test-service-2", "test-feign-service-skip");
assertThat(loadBalancerEagerLoadProperties.isEnabled()).isTrue();
}
@Test
public void testFeignClient() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify services in LoadBalancerEagerLoadProperties.clients are NOT warmed (skipped)
// because they are handled by PolarisLoadBalancerEagerContextInitializer
verify(loadBalancerClientFactory, never()).getInstance("test-service-1");
verify(loadBalancerClientFactory, never()).getInstance("test-service-2");
// Verify FeignClient services are warmed (without url)
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service");
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-http");
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-https");
// Verify FeignClient with url is NOT warmed
verify(loadBalancerClientFactory, never()).getInstance("localhost:48085");
// Verify choose method is called
verify(mockLoadBalancer, times(3)).choose();
}
@Test
public void testSkipServicesInLoadBalancerEagerLoadProperties() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify services configured in LoadBalancerEagerLoadProperties.clients are skipped
// test-feign-service-skip is configured in LoadBalancerEagerLoadProperties.clients
verify(loadBalancerClientFactory, never()).getInstance("test-feign-service-skip");
}
@Test
public void testFeignClientWithHttpPrefix() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify service name is correctly extracted from http:// prefix
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-http");
}
@Test
public void testFeignClientWithHttpsPrefix() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify service name is correctly extracted from https:// prefix
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-https");
}
@SpringBootApplication
@EnableFeignClients
@RestController
protected static class TestApplication {
@Bean
public TestBeanPostProcessor testBeanPostProcessor() {
return new TestBeanPostProcessor();
}
@Bean
public FeignAspect feignAspect() {
return new FeignAspect();
}
@RequestMapping("/test")
public String test() {
return "test";
}
// Normal FeignClient (without url)
@FeignClient(name = "test-feign-service")
public interface TestFeignClient {
@RequestMapping("/test")
String test();
}
// FeignClient with http:// prefix
@FeignClient(name = "http://test-feign-service-http")
public interface TestFeignClientWithHttp {
@RequestMapping("/test")
String test();
}
// FeignClient with https:// prefix
@FeignClient(name = "https://test-feign-service-https")
public interface TestFeignClientWithHttps {
@RequestMapping("/test")
String test();
}
// FeignClient with url (should skip warm-up)
@FeignClient(name = "test-feign-with-url", url = "http://localhost:48085")
public interface TestFeignClientWithUrl {
@RequestMapping("/test")
String test();
}
// FeignClient that is also in LoadBalancerEagerLoadProperties.clients
// This service should be skipped in FeignEagerLoadContextInitializer
@FeignClient(name = "test-feign-service-skip")
public interface TestFeignClientSkip {
@RequestMapping("/test")
String test();
}
}
static class TestBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof PolarisAutoServiceRegistration) {
return org.mockito.Mockito.mock(PolarisAutoServiceRegistration.class);
}
return bean;
}
}
@Aspect
static class FeignAspect {
private static final Logger LOG = LoggerFactory.getLogger(FeignAspect.class);
@Around("@within(org.springframework.cloud.openfeign.FeignClient)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
LOG.info("FeignAspect around");
return joinPoint.proceed();
}
}
}

@ -0,0 +1,164 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.eager.instrument.loadbalancer;
import com.tencent.cloud.polaris.registry.PolarisAutoServiceRegistration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* Test for {@link PolarisLoadBalancerEagerContextInitializer}.
*
* @author Test Author
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = DEFINED_PORT,
classes = PolarisLoadBalancerEagerContextInitializerTest.TestApplication.class,
properties = {
"server.port=48086",
"spring.config.location = classpath:application-test.yml",
"spring.main.web-application-type = servlet",
"spring.cloud.gateway.enabled = false",
"spring.cloud.polaris.discovery.eager-load.enabled = true",
"spring.cloud.loadbalancer.eager-load.enabled = true",
"spring.cloud.loadbalancer.eager-load.clients = test-service-1,test-service-2,test-service-3"
})
public class PolarisLoadBalancerEagerContextInitializerTest {
@MockBean
private LoadBalancerClientFactory loadBalancerClientFactory;
@Autowired
private PolarisLoadBalancerEagerContextInitializer polarisLoadBalancerEagerContextInitializer;
@Autowired
private LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties;
@BeforeEach
public void setUp() {
reset(loadBalancerClientFactory);
}
@Test
public void testLoadBalancerEagerLoadPropertiesLoaded() {
// Verify LoadBalancerEagerLoadProperties is loaded correctly
assertThat(loadBalancerEagerLoadProperties.getClients())
.isNotNull()
.containsExactlyInAnyOrder("test-service-1", "test-service-2", "test-service-3");
assertThat(loadBalancerEagerLoadProperties.isEnabled()).isTrue();
}
@Test
public void testWarmUpServices() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
polarisLoadBalancerEagerContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify all services in LoadBalancerEagerLoadProperties.clients are warmed
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-1");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-2");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-3");
// Verify choose method is called for each service
verify(mockLoadBalancer, times(3)).choose();
}
@Test
public void testWarmUpWithNullLoadBalancer() {
// Prepare mock - return null for some services
when(loadBalancerClientFactory.getInstance("test-service-1")).thenReturn(null);
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance("test-service-2")).thenReturn(mockLoadBalancer);
when(loadBalancerClientFactory.getInstance("test-service-3")).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
polarisLoadBalancerEagerContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify all services are attempted
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-1");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-2");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-3");
// Verify choose method is only called for non-null load balancers
verify(mockLoadBalancer, times(2)).choose();
}
@Test
public void testWarmUpWithException() {
// Prepare mock - throw exception for some services
when(loadBalancerClientFactory.getInstance("test-service-1"))
.thenThrow(new RuntimeException("Test exception"));
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance("test-service-2")).thenReturn(mockLoadBalancer);
when(loadBalancerClientFactory.getInstance("test-service-3")).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
polarisLoadBalancerEagerContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify all services are attempted (exception should not stop the loop)
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-1");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-2");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-3");
}
@SpringBootApplication
protected static class TestApplication {
@Bean
public TestBeanPostProcessor testBeanPostProcessor() {
return new TestBeanPostProcessor();
}
}
static class TestBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof PolarisAutoServiceRegistration) {
return org.mockito.Mockito.mock(PolarisAutoServiceRegistration.class);
}
return bean;
}
}
}

@ -250,6 +250,14 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
ServerHttpRequest request = exchange.getRequest();
String[] apis = rebuildExternalApi(request, path.value());
GroupContext.ContextRoute contextRoute = manager.getGroupPathRoute(config.getGroup(), apis[0]);
// Before Spring 6.0, trailing slashes were matched by default (e.g. "/api/test/" could match "/api/test").
// Retry without trailing slashes to maintain backward compatibility.
if (contextRoute == null) {
String trimmedMatchPath = stripTrailingSlashes(apis[0]);
if (!trimmedMatchPath.equals(apis[0])) {
contextRoute = manager.getGroupPathRoute(config.getGroup(), trimmedMatchPath);
}
}
if (contextRoute == null) {
String msg = String.format("[externalFilter] Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], path.value());
logger.warn(msg);
@ -272,6 +280,14 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
logger.debug("[msFilter] path:{}, apis: {}", path, apis);
// check api
GroupContext.ContextRoute contextRoute = manager.getGroupPathRoute(config.getGroup(), apis[0]);
// Before Spring 6.0, trailing slashes were matched by default (e.g. "/api/test/" could match "/api/test").
// Retry without trailing slashes to maintain backward compatibility.
if (contextRoute == null) {
String trimmedMatchPath = stripTrailingSlashes(apis[0]);
if (!trimmedMatchPath.equals(apis[0])) {
contextRoute = manager.getGroupPathRoute(config.getGroup(), trimmedMatchPath);
}
}
if (contextRoute == null) {
String msg = String.format("[msFilter] Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], path.value());
logger.warn(msg);
@ -327,6 +343,14 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
logger.debug("[unitFilter] path:{}, apis: {}", path, apis);
// check api
GroupContext.ContextRoute contextRoute = manager.getGroupUnitPathRoute(config.getGroup(), apis[0]);
// Before Spring 6.0, trailing slashes were matched by default (e.g. "/api/test/" could match "/api/test").
// Retry without trailing slashes to maintain backward compatibility.
if (contextRoute == null) {
String trimmedMatchPath = stripTrailingSlashes(apis[0]);
if (!trimmedMatchPath.equals(apis[0])) {
contextRoute = manager.getGroupUnitPathRoute(config.getGroup(), trimmedMatchPath);
}
}
if (contextRoute == null) {
String msg = String.format("[unitFilter] Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], path.value());
logger.warn(msg);
@ -367,9 +391,10 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
matchPath.append("/").append(pathSegments[i]);
realPath.append("/").append(pathSegments[i]);
}
if (path.endsWith("/")) {
matchPath.append("/");
realPath.append("/");
String trailingSlashes = getTrailingSlashes(path);
if (StringUtils.isNotEmpty(trailingSlashes)) {
matchPath.append(trailingSlashes);
realPath.append(trailingSlashes);
}
return new String[] {matchPath.toString(), realPath.toString()};
}
@ -387,9 +412,10 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
matchPath.append("/").append(pathSegments[i]);
realPath.append("/").append(pathSegments[i]);
}
if (path.endsWith("/")) {
matchPath.append("/");
realPath.append("/");
String trailingSlashes = getTrailingSlashes(path);
if (StringUtils.isNotEmpty(trailingSlashes)) {
matchPath.append(trailingSlashes);
realPath.append(trailingSlashes);
}
return new String[] {matchPath.toString(), realPath.toString()};
}
@ -443,9 +469,10 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
matchPath.append("/").append(pathSegments[i]);
realPath.append("/").append(pathSegments[i]);
}
if (path.endsWith("/")) {
matchPath.append("/");
realPath.append("/");
String trailingSlashes = getTrailingSlashes(path);
if (StringUtils.isNotEmpty(trailingSlashes)) {
matchPath.append(trailingSlashes);
realPath.append(trailingSlashes);
}
return new String[] {matchPath.toString(), realPath.toString()};
@ -601,4 +628,31 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered {
MetadataContextUtils.putMetadataObjectValue(ContextConstant.Trace.EXTRA_TRACE_ATTRIBUTES, traceAttributes);
}
/**
* Returns all trailing slashes of the given path, e.g. "/api/test///" "///"
*/
private String getTrailingSlashes(String path) {
int end = path.length();
int start = end;
while (start > 0 && path.charAt(start - 1) == '/') {
start--;
}
return path.substring(start, end);
}
/**
* Strips trailing slashes from the given path, e.g. "GET|/api/test/" "GET|/api/test".
* Returns the original string if no trailing slashes are present.
* <p>
* Used to maintain backward compatibility with pre-Spring-6.0 behavior, where trailing slashes
* were matched by default (e.g. "/api/test/" could match a route configured as "/api/test").
*/
private String stripTrailingSlashes(String path) {
int end = path.length();
while (end > 0 && path.charAt(end - 1) == '/') {
end--;
}
return path.substring(0, end);
}
}

@ -17,6 +17,7 @@
package com.tencent.cloud.plugin.gateway.context;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
@ -119,6 +120,139 @@ class ContextGatewayFilterTest {
}
// Test getTrailingSlashes via reflection
@Test
void shouldGetTrailingSlashesCorrectly() throws Exception {
Method method = ContextGatewayFilter.class.getDeclaredMethod("getTrailingSlashes", String.class);
method.setAccessible(true);
assertThat(method.invoke(filter, "/api/test///")).isEqualTo("///");
assertThat(method.invoke(filter, "/api/test/")).isEqualTo("/");
assertThat(method.invoke(filter, "/api/test")).isEqualTo("");
assertThat(method.invoke(filter, "/")).isEqualTo("/");
assertThat(method.invoke(filter, "///")).isEqualTo("///");
assertThat(method.invoke(filter, "")).isEqualTo("");
}
// Test stripTrailingSlashes via reflection
@Test
void shouldStripTrailingSlashesCorrectly() throws Exception {
Method method = ContextGatewayFilter.class.getDeclaredMethod("stripTrailingSlashes", String.class);
method.setAccessible(true);
assertThat(method.invoke(filter, "GET|/api/test/")).isEqualTo("GET|/api/test");
assertThat(method.invoke(filter, "GET|/api/test///")).isEqualTo("GET|/api/test");
assertThat(method.invoke(filter, "GET|/api/test")).isEqualTo("GET|/api/test");
assertThat(method.invoke(filter, "/api/test/")).isEqualTo("/api/test");
assertThat(method.invoke(filter, "/")).isEqualTo("");
assertThat(method.invoke(filter, "///")).isEqualTo("");
assertThat(method.invoke(filter, "")).isEqualTo("");
}
// Test trailing slash backward compatibility: request "/api/test/" should match route configured as "/api/test"
@Test
void shouldMatchRouteWithTrailingSlashCompatibility() {
// Setup group context with route configured WITHOUT trailing slash
GroupContext.ContextPredicate predicate = createPredicate(ApiType.EXTERNAL, Position.PATH, Position.PATH);
groupContext.setPredicate(predicate);
groupContext.setRoutes(Collections.singletonList(
createContextRoute("GET|/external/api", "GET", "testNS", "testSvc")
));
// Create test request WITH trailing slash
MockServerHttpRequest request = MockServerHttpRequest.get("/context/external/api/").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
// Exact match (with trailing slash) returns null; stripped match returns the route
GroupContext.ContextRoute route = createContextRoute("GET|/external/api", "GET", "http://test.com");
when(mockManager.getGroupPathRoute("testGroup", "GET|/external/api/")).thenReturn(null);
when(mockManager.getGroupPathRoute("testGroup", "GET|/external/api")).thenReturn(route);
// Execute filter — should not throw NotFoundException
Mono<Void> result = filter.filter(exchange, mockChain);
// Verify the request URL was resolved correctly (path preserved with trailing slash)
URI finalUri = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(finalUri.toString()).isEqualTo("http://test.com/external/api/");
}
// Test that exact match (without trailing slash) still works normally
@Test
void shouldMatchRouteExactlyWithoutTrailingSlash() {
// Setup group context
GroupContext.ContextPredicate predicate = createPredicate(ApiType.EXTERNAL, Position.PATH, Position.PATH);
groupContext.setPredicate(predicate);
groupContext.setRoutes(Collections.singletonList(
createContextRoute("GET|/external/api", "GET", "testNS", "testSvc")
));
// Create test request WITHOUT trailing slash
MockServerHttpRequest request = MockServerHttpRequest.get("/context/external/api").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
GroupContext.ContextRoute route = createContextRoute("GET|/external/api", "GET", "http://test.com");
when(mockManager.getGroupPathRoute("testGroup", "GET|/external/api")).thenReturn(route);
// Execute filter
filter.filter(exchange, mockChain);
// Verify the request URL was resolved correctly (path preserved without trailing slash)
URI finalUri = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(finalUri.toString()).isEqualTo("http://test.com/external/api");
}
// Test MS API trailing slash backward compatibility: request "/context/ns/svc/api/test/" should match route configured as "GET|/ns/svc/api/test"
@Test
void shouldMatchMsRouteWithTrailingSlashCompatibility() {
// Setup group context with MS API predicate and route configured WITHOUT trailing slash
GroupContext.ContextPredicate predicate = createPredicate(ApiType.MS, Position.PATH, Position.PATH);
groupContext.setPredicate(predicate);
groupContext.setRoutes(Collections.singletonList(
createContextRoute("GET|/testNS/testSvc/api/test", "GET", "testNS", "testSvc")
));
// Create test request WITH trailing slash: /context/namespace/service/api/path/
MockServerHttpRequest request = MockServerHttpRequest.get("/context/testNS/testSvc/api/test/").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
// Exact match (with trailing slash) returns null; stripped match returns the route
GroupContext.ContextRoute route = createContextRoute("GET|/testNS/testSvc/api/test", "GET", "testNS", "testSvc");
when(mockManager.getGroupPathRoute("testGroup", "GET|/testNS/testSvc/api/test/")).thenReturn(null);
when(mockManager.getGroupPathRoute("testGroup", "GET|/testNS/testSvc/api/test")).thenReturn(route);
// Execute filter — should not throw NotFoundException
Mono<Void> result = filter.filter(exchange, mockChain);
// Verify the request URL was resolved correctly (path preserved with trailing slash)
URI finalUri = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(finalUri.toString()).isEqualTo("lb://testSvc/api/test/");
}
// Test that exact match for MS API (without trailing slash) still works normally
@Test
void shouldMatchMsRouteExactlyWithoutTrailingSlash() {
// Setup group context with MS API predicate
GroupContext.ContextPredicate predicate = createPredicate(ApiType.MS, Position.PATH, Position.PATH);
groupContext.setPredicate(predicate);
groupContext.setRoutes(Collections.singletonList(
createContextRoute("GET|/testNS/testSvc/api/test", "GET", "testNS", "testSvc")
));
// Create test request WITHOUT trailing slash
MockServerHttpRequest request = MockServerHttpRequest.get("/context/testNS/testSvc/api/test").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
GroupContext.ContextRoute route = createContextRoute("GET|/testNS/testSvc/api/test", "GET", "testNS", "testSvc");
when(mockManager.getGroupPathRoute("testGroup", "GET|/testNS/testSvc/api/test")).thenReturn(route);
// Execute filter
filter.filter(exchange, mockChain);
// Verify the request URL was resolved correctly (path preserved without trailing slash)
URI finalUri = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(finalUri.toString()).isEqualTo("lb://testSvc/api/test");
}
// Helper method to create test predicate
private GroupContext.ContextPredicate createPredicate(ApiType apiType, Position nsPos, Position svcPos) {
GroupContext.ContextPredicate predicate = new GroupContext.ContextPredicate();

@ -17,10 +17,12 @@
package com.tencent.cloud.plugin.unit.config;
import com.tencent.cloud.plugin.unit.discovery.UnitFeignEagerLoadSmartLifecycle;
import com.tencent.cloud.plugin.unit.discovery.UnitFeignEagerLoadContextInitializer;
import com.tencent.cloud.plugin.unit.discovery.UnitLoadBalancerEagerContextInitializer;
import com.tencent.cloud.plugin.unit.discovery.UnitPolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadSmartLifecycle;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadContextInitializer;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.PolarisLoadBalancerEagerContextInitializer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
@ -34,8 +36,12 @@ public class UnitBeanPostProcessor implements BeanPostProcessor {
return new UnitPolarisDiscoveryClient(discoveryClient);
}
if (bean instanceof FeignEagerLoadSmartLifecycle) {
return new UnitFeignEagerLoadSmartLifecycle();
if (bean instanceof FeignEagerLoadContextInitializer) {
return new UnitFeignEagerLoadContextInitializer();
}
if (bean instanceof PolarisLoadBalancerEagerContextInitializer) {
return new UnitLoadBalancerEagerContextInitializer();
}
return bean;

@ -17,21 +17,27 @@
package com.tencent.cloud.plugin.unit.discovery;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadSmartLifecycle;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadContextInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UnitFeignEagerLoadSmartLifecycle extends FeignEagerLoadSmartLifecycle {
import org.springframework.boot.context.event.ApplicationReadyEvent;
private static final Logger LOG = LoggerFactory.getLogger(UnitFeignEagerLoadSmartLifecycle.class);
/**
* Unit Feign eager load context initializer.
* Ignores feign eager load in unit mode.
*/
public class UnitFeignEagerLoadContextInitializer extends FeignEagerLoadContextInitializer {
private static final Logger LOG = LoggerFactory.getLogger(UnitFeignEagerLoadContextInitializer.class);
public UnitFeignEagerLoadSmartLifecycle() {
public UnitFeignEagerLoadContextInitializer() {
super(null, null, null);
}
@Override
public void start() {
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("ignore feign eager load in unit mode");
}
}

@ -0,0 +1,42 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.plugin.unit.discovery;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.PolarisLoadBalancerEagerContextInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
/**
* Unit LoadBalancer eager load context initializer.
* Ignores loadbalancer eager load in unit mode.
*/
public class UnitLoadBalancerEagerContextInitializer extends PolarisLoadBalancerEagerContextInitializer {
private static final Logger LOG = LoggerFactory.getLogger(UnitLoadBalancerEagerContextInitializer.class);
public UnitLoadBalancerEagerContextInitializer() {
super(null, null);
}
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("ignore loadbalancer eager load in unit mode");
}
}
Loading…
Cancel
Save