From 96f16231cd38ff964d944a70eee21a253ccf5b22 Mon Sep 17 00:00:00 2001 From: shedfreewu Date: Thu, 5 Mar 2026 18:59:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E9=A2=84=E7=83=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pom.xml | 6 + .../PolarisEagerLoadAutoConfiguration.java | 23 +- ... => FeignEagerLoadContextInitializer.java} | 100 ++++--- .../LoadBalancerEagerContextInitializer.java | 48 ++++ .../LoadBalancerEagerLoadProperties.java | 30 +++ .../loadbalancer/LoadBalancerWarmUpUtils.java | 64 +++++ .../FeignEagerLoadContextInitializerTest.java | 249 ++++++++++++++++++ ...adBalancerEagerContextInitializerTest.java | 164 ++++++++++++ .../unit/config/UnitBeanPostProcessor.java | 14 +- ...UnitFeignEagerLoadContextInitializer.java} | 16 +- ...itLoadBalancerEagerContextInitializer.java | 42 +++ 11 files changed, 704 insertions(+), 52 deletions(-) rename spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/{FeignEagerLoadSmartLifecycle.java => FeignEagerLoadContextInitializer.java} (54%) create mode 100644 spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializer.java create mode 100644 spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerLoadProperties.java create mode 100644 spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerWarmUpUtils.java create mode 100644 spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializerTest.java create mode 100644 spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializerTest.java rename spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/{UnitFeignEagerLoadSmartLifecycle.java => UnitFeignEagerLoadContextInitializer.java} (69%) create mode 100644 spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitLoadBalancerEagerContextInitializer.java diff --git a/spring-cloud-starter-tencent-polaris-discovery/pom.xml b/spring-cloud-starter-tencent-polaris-discovery/pom.xml index 67bdae3ea..c03de3430 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/pom.xml +++ b/spring-cloud-starter-tencent-polaris-discovery/pom.xml @@ -62,5 +62,11 @@ spring-boot-actuator-autoconfigure true + + + io.github.openfeign + feign-slf4j + test + diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/config/PolarisEagerLoadAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/config/PolarisEagerLoadAutoConfiguration.java index f0a900e29..28726e712 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/config/PolarisEagerLoadAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/config/PolarisEagerLoadAutoConfiguration.java @@ -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.LoadBalancerEagerContextInitializer; +import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties; 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 LoadBalancerEagerContextInitializer loadBalancerEagerContextInitializer( + LoadBalancerClientFactory loadBalancerClientFactory, LoadBalancerEagerLoadProperties properties) { + return new LoadBalancerEagerContextInitializer(loadBalancerClientFactory, properties.getClients()); + } } diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializer.java similarity index 54% rename from spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java rename to spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializer.java index 87ce27905..ad8f03333 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializer.java @@ -20,9 +20,11 @@ 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.discovery.PolarisDiscoveryClient; -import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient; +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; @@ -30,30 +32,48 @@ 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.SmartLifecycle; +import org.springframework.context.ApplicationListener; -public class FeignEagerLoadSmartLifecycle implements SmartLifecycle { +/** + * Feign eager load context initializer. + * Implements ApplicationListener to warm up FeignClient services + * after the application is ready. + * + * @author Yuwei Fu + */ +public class FeignEagerLoadContextInitializer implements ApplicationListener { - private static final Logger LOG = LoggerFactory.getLogger(FeignEagerLoadSmartLifecycle.class); + private static final Logger LOG = LoggerFactory.getLogger(FeignEagerLoadContextInitializer.class); - private final ApplicationContext applicationContext; + private ApplicationContext applicationContext; - private final PolarisDiscoveryClient polarisDiscoveryClient; + private LoadBalancerClientFactory loadBalancerClientFactory; - private final PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient; + private LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties; - public FeignEagerLoadSmartLifecycle(ApplicationContext applicationContext, PolarisDiscoveryClient polarisDiscoveryClient, - PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) { + public FeignEagerLoadContextInitializer(ApplicationContext applicationContext, + LoadBalancerClientFactory loadBalancerClientFactory, + LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties) { this.applicationContext = applicationContext; - this.polarisDiscoveryClient = polarisDiscoveryClient; - this.polarisReactiveDiscoveryClient = polarisReactiveDiscoveryClient; + this.loadBalancerClientFactory = loadBalancerClientFactory; + this.loadBalancerEagerLoadProperties = loadBalancerEagerLoadProperties; } @Override - public void start() { + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { LOG.info("feign eager-load start"); + + // Get services that are already warmed by LoadBalancerEagerContextInitializer + Set skipServices = getLoadBalancerEagerLoadServices(); + + // Set to track already warmed services + Set warmedServices = new HashSet<>(); + + // Warm up FeignClient services for (Object bean : applicationContext.getBeansWithAnnotation(FeignClient.class).values()) { try { if (Proxy.isProxyClass(bean.getClass())) { @@ -70,17 +90,22 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle { } String serviceName = URI.create(url).getHost(); - LOG.info("[{}] eager-load start, feign name: {}", serviceName, hardCodedTarget.name()); - if (polarisDiscoveryClient != null) { - polarisDiscoveryClient.getInstances(serviceName); + // Skip if already warmed by LoadBalancerEagerContextInitializer + if (skipServices.contains(serviceName)) { + LOG.debug("[{}] skip eager-load, already configured in LoadBalancerEagerLoadProperties.clients", serviceName); + continue; } - else if (polarisReactiveDiscoveryClient != null) { - polarisReactiveDiscoveryClient.getInstances(serviceName).subscribe(); + + // Skip if already warmed in this round + if (warmedServices.contains(serviceName)) { + LOG.debug("[{}] already warmed, skip.", serviceName); + continue; } - else { - LOG.warn("[{}] no discovery client found.", serviceName); - } - LOG.info("[{}] eager-load end", serviceName); + + LOG.info("[{}] eager-load start, feign name: {}", serviceName, hardCodedTarget.name()); + LoadBalancerWarmUpUtils.warmUp(loadBalancerClientFactory, serviceName); + + warmedServices.add(serviceName); } } } @@ -90,14 +115,28 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle { } } LOG.info("feign eager-load end"); + } + /** + * Get services configured in LoadBalancerEagerLoadProperties. + * These services are warmed by LoadBalancerEagerContextInitializer. + * @return set of service names to skip + */ + private Set getLoadBalancerEagerLoadServices() { + Set services = new HashSet<>(); + if (loadBalancerEagerLoadProperties != null + && loadBalancerEagerLoadProperties.isEnabled() + && loadBalancerEagerLoadProperties.getClients() != null) { + services.addAll(loadBalancerEagerLoadProperties.getClients()); + } + return services; } 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) { @@ -122,19 +161,4 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle { } return null; } - - @Override - public void stop() { - - } - - @Override - public boolean isRunning() { - return false; - } - - @Override - public int getPhase() { - return 10; - } } diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializer.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializer.java new file mode 100644 index 000000000..f9d0e5dfc --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializer.java @@ -0,0 +1,48 @@ +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 LoadBalancerEagerContextInitializer implements ApplicationListener { + + + private static final Logger LOG = LoggerFactory.getLogger(LoadBalancerEagerContextInitializer.class); + + private final LoadBalancerClientFactory factory; + + private final List serviceNames; + + public LoadBalancerEagerContextInitializer(LoadBalancerClientFactory factory, List 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); + } + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerLoadProperties.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerLoadProperties.java new file mode 100644 index 000000000..3ddaa2084 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerLoadProperties.java @@ -0,0 +1,30 @@ +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 clients; + + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getClients() { + return clients; + } + + public void setClients(List clients) { + this.clients = clients; + } + +} diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerWarmUpUtils.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerWarmUpUtils.java new file mode 100644 index 000000000..ed289b26d --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerWarmUpUtils.java @@ -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 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; + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializerTest.java b/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializerTest.java new file mode 100644 index 000000000..c273a1728 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadContextInitializerTest.java @@ -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 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 LoadBalancerEagerContextInitializer + 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 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 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 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(); + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializerTest.java b/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializerTest.java new file mode 100644 index 000000000..5be985860 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/polaris/eager/instrument/loadbalancer/LoadBalancerEagerContextInitializerTest.java @@ -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 LoadBalancerEagerContextInitializer}. + * + * @author Test Author + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = DEFINED_PORT, + classes = LoadBalancerEagerContextInitializerTest.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 LoadBalancerEagerContextInitializerTest { + + @MockBean + private LoadBalancerClientFactory loadBalancerClientFactory; + + @Autowired + private LoadBalancerEagerContextInitializer loadBalancerEagerContextInitializer; + + @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 mockLoadBalancer = mock(ReactiveLoadBalancer.class); + when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer); + + // Execute warm-up by triggering ApplicationReadyEvent + loadBalancerEagerContextInitializer.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 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 + loadBalancerEagerContextInitializer.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 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 + loadBalancerEagerContextInitializer.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; + } + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/config/UnitBeanPostProcessor.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/config/UnitBeanPostProcessor.java index e14689a70..e61c1e843 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/config/UnitBeanPostProcessor.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/config/UnitBeanPostProcessor.java @@ -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.LoadBalancerEagerContextInitializer; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -35,8 +37,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 LoadBalancerEagerContextInitializer) { + return new UnitLoadBalancerEagerContextInitializer(); } return bean; diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitFeignEagerLoadSmartLifecycle.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitFeignEagerLoadContextInitializer.java similarity index 69% rename from spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitFeignEagerLoadSmartLifecycle.java rename to spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitFeignEagerLoadContextInitializer.java index 60cf1f5d6..c53e4dcb5 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitFeignEagerLoadSmartLifecycle.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitFeignEagerLoadContextInitializer.java @@ -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"); } } diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitLoadBalancerEagerContextInitializer.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitLoadBalancerEagerContextInitializer.java new file mode 100644 index 000000000..10e65d0e5 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-unit-plugin/src/main/java/com/tencent/cloud/plugin/unit/discovery/UnitLoadBalancerEagerContextInitializer.java @@ -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.LoadBalancerEagerContextInitializer; +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 LoadBalancerEagerContextInitializer { + + 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"); + } +}