diff --git a/CHANGELOG.md b/CHANGELOG.md index ee7c14dae..1b1ef31f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,3 +41,4 @@ - [feat:support smooth upgrade from tsf.](https://github.com/Tencent/spring-cloud-tencent/pull/1472) - [fix:fix caller disposable metadata handle when using tracing.](https://github.com/Tencent/spring-cloud-tencent/pull/1476) - [refactor:update registry status.](https://github.com/Tencent/spring-cloud-tencent/pull/1486) +- [feat: implement circuit breaker in enhance plugin, support listen config group, support refresh single config in refresh_context mode.](https://github.com/Tencent/spring-cloud-tencent/pull/1490) diff --git a/pom.xml b/pom.xml index 962e7af3a..d28ab8ae4 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ 2.0.1.0-2021.0.9-SNAPSHOT - 5.3.31 + 5.3.39 2.7.18 diff --git a/spring-cloud-starter-tencent-metadata-transfer/src/test/java/com/tencent/cloud/metadata/core/EncodeTransferMedataRestTemplateInterceptorTest.java b/spring-cloud-starter-tencent-metadata-transfer/src/test/java/com/tencent/cloud/metadata/core/EncodeTransferMedataRestTemplateInterceptorTest.java index c24d3d4ec..9e7d2a1eb 100644 --- a/spring-cloud-starter-tencent-metadata-transfer/src/test/java/com/tencent/cloud/metadata/core/EncodeTransferMedataRestTemplateInterceptorTest.java +++ b/spring-cloud-starter-tencent-metadata-transfer/src/test/java/com/tencent/cloud/metadata/core/EncodeTransferMedataRestTemplateInterceptorTest.java @@ -18,13 +18,10 @@ package com.tencent.cloud.metadata.core; import java.net.URI; -import java.util.Arrays; import java.util.Map; import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.metadata.MetadataContextHolder; -import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateInterceptor; -import com.tencent.cloud.rpc.enhancement.plugin.DefaultEnhancedPluginRunner; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -79,13 +76,7 @@ protected static class TestApplication { @Bean public RestTemplate restTemplate() { - - EncodeTransferMedataRestTemplateEnhancedPlugin plugin = new EncodeTransferMedataRestTemplateEnhancedPlugin(); - EnhancedRestTemplateInterceptor interceptor = new EnhancedRestTemplateInterceptor( - new DefaultEnhancedPluginRunner(Arrays.asList(plugin), new MockRegistration(), null)); - RestTemplate template = new RestTemplate(); - template.setInterceptors(Arrays.asList(interceptor)); - return template; + return new RestTemplate(); } @RequestMapping("/test") diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreaker.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreaker.java index a34b2c476..0f4c62f81 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreaker.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreaker.java @@ -27,7 +27,9 @@ import com.tencent.polaris.api.pojo.ServiceKey; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.circuitbreak.api.FunctionalDecorator; +import com.tencent.polaris.circuitbreak.api.InvokeHandler; import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest; +import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext; import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +41,7 @@ * * @author seanyu 2023-02-27 */ -public class PolarisCircuitBreaker implements CircuitBreaker { +public class PolarisCircuitBreaker implements CircuitBreaker, InvokeHandler { private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreaker.class); @@ -49,6 +51,8 @@ public class PolarisCircuitBreaker implements CircuitBreaker { private final ConsumerAPI consumerAPI; + private final InvokeHandler invokeHandler; + public PolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf, ConsumerAPI consumerAPI, CircuitBreakAPI circuitBreakAPI) { @@ -59,6 +63,7 @@ public PolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBr this.consumerAPI = consumerAPI; this.conf = conf; this.decorator = circuitBreakAPI.makeFunctionalDecorator(makeDecoratorRequest); + this.invokeHandler = circuitBreakAPI.makeInvokeHandler(makeDecoratorRequest); } @Override @@ -77,4 +82,18 @@ public T run(Supplier toRun, Function fallback) { } } + @Override + public void acquirePermission() { + invokeHandler.acquirePermission(); + } + + @Override + public void onSuccess(InvokeContext.ResponseContext responseContext) { + invokeHandler.onSuccess(responseContext); + } + + @Override + public void onError(InvokeContext.ResponseContext responseContext) { + invokeHandler.onError(responseContext); + } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreaker.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreaker.java deleted file mode 100644 index d7e171b0e..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreaker.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker; - -import java.util.function.Function; - -import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder; -import com.tencent.cloud.polaris.circuitbreaker.common.PolarisResultToErrorCode; -import com.tencent.cloud.polaris.circuitbreaker.instrument.reactor.PolarisCircuitBreakerReactorTransformer; -import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils; -import com.tencent.polaris.api.core.ConsumerAPI; -import com.tencent.polaris.api.pojo.ServiceKey; -import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; -import com.tencent.polaris.circuitbreak.api.InvokeHandler; -import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest; -import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext; -import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; - -/** - * ReactivePolarisCircuitBreaker. - * - * @author seanyu 2023-02-27 - */ -public class ReactivePolarisCircuitBreaker implements ReactiveCircuitBreaker { - - private final InvokeHandler invokeHandler; - - private final ConsumerAPI consumerAPI; - - private final PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf; - - public ReactivePolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf, - ConsumerAPI consumerAPI, - CircuitBreakAPI circuitBreakAPI) { - InvokeContext.RequestContext requestContext = new FunctionalDecoratorRequest( - new ServiceKey(conf.getNamespace(), conf.getService()), conf.getProtocol(), conf.getMethod(), conf.getPath()); - requestContext.setSourceService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService())); - requestContext.setResultToErrorCode(new PolarisResultToErrorCode()); - this.consumerAPI = consumerAPI; - this.conf = conf; - this.invokeHandler = circuitBreakAPI.makeInvokeHandler(requestContext); - } - - - @Override - public Mono run(Mono toRun, Function> fallback) { - Mono toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler)); - if (fallback != null) { - toReturn = toReturn.onErrorResume(throwable -> { - if (throwable instanceof CallAbortedException) { - PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable); - } - return fallback.apply(throwable); - }); - } - return toReturn; - } - - @Override - public Flux run(Flux toRun, Function> fallback) { - Flux toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler)); - if (fallback != null) { - toReturn = toReturn.onErrorResume(throwable -> { - if (throwable instanceof CallAbortedException) { - PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable); - } - return fallback.apply(throwable); - }); - } - return toReturn; - } - -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreakerFactory.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreakerFactory.java deleted file mode 100644 index 56c61f9bf..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreakerFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder; -import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerProperties; -import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils; -import com.tencent.polaris.api.core.ConsumerAPI; -import com.tencent.polaris.api.utils.ThreadPoolUtils; -import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; -import com.tencent.polaris.client.util.NamedThreadFactory; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; - -/** - * ReactivePolarisCircuitBreakerFactory. - * - * @author seanyu 2023-02-27 - */ -public class ReactivePolarisCircuitBreakerFactory extends - ReactiveCircuitBreakerFactory implements DisposableBean { - - private final CircuitBreakAPI circuitBreakAPI; - - private final ConsumerAPI consumerAPI; - - private final ScheduledExecutorService cleanupService = Executors.newSingleThreadScheduledExecutor( - new NamedThreadFactory("sct-reactive-circuitbreaker-cleanup", true)); - - private Function defaultConfiguration = - id -> { - String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id); - return new PolarisCircuitBreakerConfigBuilder() - .namespace(metadata[0]) - .service(metadata[1]) - .path(metadata[2]) - .protocol(metadata[3]) - .method(metadata[4]) - .build(); - }; - - public ReactivePolarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI, - PolarisCircuitBreakerProperties polarisCircuitBreakerProperties) { - this.circuitBreakAPI = circuitBreakAPI; - this.consumerAPI = consumerAPI; - cleanupService.scheduleWithFixedDelay( - () -> { - getConfigurations().clear(); - }, - polarisCircuitBreakerProperties.getConfigurationCleanupInterval(), - polarisCircuitBreakerProperties.getConfigurationCleanupInterval(), TimeUnit.MILLISECONDS); - } - - @Override - public ReactiveCircuitBreaker create(String id) { - PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = getConfigurations() - .computeIfAbsent(id, defaultConfiguration); - return new ReactivePolarisCircuitBreaker(conf, consumerAPI, circuitBreakAPI); - } - - @Override - protected PolarisCircuitBreakerConfigBuilder configBuilder(String id) { - String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id); - return new PolarisCircuitBreakerConfigBuilder(metadata[0], metadata[1], metadata[2], metadata[3], metadata[4]); - } - - @Override - public void configureDefault( - Function defaultConfiguration) { - this.defaultConfiguration = defaultConfiguration; - } - - @Override - public void destroy() { - ThreadPoolUtils.waitAndStopThreadPools(new ExecutorService[] {cleanupService}); - } -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java new file mode 100644 index 000000000..724a949af --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.beanprocessor; + +import com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate.PolarisLoadBalancerInterceptor; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; +import org.springframework.core.Ordered; +import org.springframework.lang.NonNull; + +/** + * LoadBalancerInterceptorBeanPostProcessor is used to wrap the default LoadBalancerInterceptor implementation and returns a custom PolarisLoadBalancerInterceptor. + * + * @author Shedfree Wu + */ +public class LoadBalancerInterceptorBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware, Ordered { + /** + * The order of the bean post processor. if user want to wrap it(CustomLoadBalancerInterceptor -> PolarisLoadBalancerInterceptor), CustomLoadBalancerInterceptorBeanPostProcessor's order should be bigger than ${@link POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER}. + */ + public static final int POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER = 0; + + private BeanFactory factory; + + @Override + public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException { + this.factory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + if (bean instanceof LoadBalancerInterceptor) { + // Support rest template router. + // Replaces the default LoadBalancerInterceptor implementation and returns a custom PolarisLoadBalancerInterceptor + LoadBalancerRequestFactory requestFactory = this.factory.getBean(LoadBalancerRequestFactory.class); + LoadBalancerClient loadBalancerClient = this.factory.getBean(LoadBalancerClient.class); + EnhancedPluginRunner pluginRunner = this.factory.getBean(EnhancedPluginRunner.class); + return new PolarisLoadBalancerInterceptor(loadBalancerClient, requestFactory, pluginRunner); + } + return bean; + } + + @Override + public int getOrder() { + return POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER; + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCode.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCode.java index 338ede19f..946f9a6ae 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCode.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCode.java @@ -17,6 +17,7 @@ package com.tencent.cloud.polaris.circuitbreaker.common; +import com.tencent.cloud.polaris.context.CircuitBreakerStatusCodeException; import com.tencent.polaris.circuitbreak.api.pojo.ResultToErrorCode; import feign.FeignException; @@ -49,6 +50,9 @@ else if (checkClassExist("org.springframework.web.reactive.function.client.WebCl && e instanceof WebClientResponseException) { return ((WebClientResponseException) e).getRawStatusCode(); } + else if (e instanceof CircuitBreakerStatusCodeException) { + return ((CircuitBreakerStatusCodeException) e).getRawStatusCode(); + } return -1; } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/GatewayPolarisCircuitBreakerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/GatewayPolarisCircuitBreakerAutoConfiguration.java index 27cbd0183..b4b7dfa94 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/GatewayPolarisCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/GatewayPolarisCircuitBreakerAutoConfiguration.java @@ -17,21 +17,11 @@ package com.tencent.cloud.polaris.circuitbreaker.config; -import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory; -import com.tencent.cloud.polaris.circuitbreaker.instrument.gateway.PolarisCircuitBreakerFilterFactory; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; -import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter; -import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties; import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,23 +34,9 @@ */ @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true) -@AutoConfigureAfter({ReactivePolarisCircuitBreakerAutoConfiguration.class}) -@ConditionalOnClass({DispatcherHandler.class, ReactivePolarisCircuitBreakerAutoConfiguration.class, - ReactiveCircuitBreakerFactory.class, ReactivePolarisCircuitBreakerFactory.class, GatewayAutoConfiguration.class}) +@ConditionalOnClass({ DispatcherHandler.class, GatewayAutoConfiguration.class}) public class GatewayPolarisCircuitBreakerAutoConfiguration { - @Bean - @ConditionalOnBean(ReactiveCircuitBreakerFactory.class) - @ConditionalOnEnabledFilter - public PolarisCircuitBreakerFilterFactory polarisCircuitBreakerFilterFactory( - ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, - ObjectProvider dispatcherHandler, - @Autowired(required = false) ReactiveDiscoveryClient discoveryClient, - @Autowired(required = false) DiscoveryLocatorProperties properties - ) { - return new PolarisCircuitBreakerFilterFactory(reactiveCircuitBreakerFactory, dispatcherHandler, discoveryClient, properties); - } - @Bean @ConditionalOnMissingBean @ConditionalOnEnabledFilter diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java index e4d45bab8..5d0f8c965 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java @@ -21,8 +21,9 @@ import java.util.List; import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory; +import com.tencent.cloud.polaris.circuitbreaker.beanprocessor.LoadBalancerInterceptorBeanPostProcessor; import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier; -import com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate.PolarisCircuitBreakerRestTemplateBeanPostProcessor; +import com.tencent.cloud.polaris.circuitbreaker.reporter.CircuitBreakerPlugin; import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter; import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter; import com.tencent.cloud.polaris.context.PolarisSDKContextManager; @@ -36,7 +37,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.Customizer; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -56,10 +56,9 @@ public class PolarisCircuitBreakerAutoConfiguration { private List> customizers = new ArrayList<>(); @Bean - @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") - public static PolarisCircuitBreakerRestTemplateBeanPostProcessor polarisCircuitBreakerRestTemplateBeanPostProcessor( - ApplicationContext applicationContext) { - return new PolarisCircuitBreakerRestTemplateBeanPostProcessor(applicationContext); + @ConditionalOnMissingBean(CircuitBreakerPlugin.class) + public CircuitBreakerPlugin circuitBreakerPlugin(CircuitBreakerFactory circuitBreakerFactory) { + return new CircuitBreakerPlugin(circuitBreakerFactory); } @Bean @@ -92,4 +91,10 @@ public CircuitBreakerConfigModifier circuitBreakerConfigModifier(RpcEnhancementR return new CircuitBreakerConfigModifier(properties); } + @Bean + @ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor") + public LoadBalancerInterceptorBeanPostProcessor loadBalancerInterceptorBeanPostProcessor() { + return new LoadBalancerInterceptorBeanPostProcessor(); + } + } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java index c6b601286..2abc5099f 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfiguration.java @@ -30,7 +30,6 @@ @ConditionalOnProperty("spring.cloud.polaris.enabled") @Import({ PolarisCircuitBreakerAutoConfiguration.class, - ReactivePolarisCircuitBreakerAutoConfiguration.class, PolarisCircuitBreakerFeignClientAutoConfiguration.class, GatewayPolarisCircuitBreakerAutoConfiguration.class }) diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java index 160ac400a..c8edc7eb8 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerFeignClientAutoConfiguration.java @@ -52,8 +52,8 @@ public CircuitBreakerNameResolver polarisCircuitBreakerNameResolver() { @Bean @ConditionalOnBean(CircuitBreakerFactory.class) @ConditionalOnMissingBean(Targeter.class) - public Targeter polarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, CircuitBreakerNameResolver circuitBreakerNameResolver) { - return new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); + public Targeter polarisFeignCircuitBreakerTargeter() { + return new PolarisFeignCircuitBreakerTargeter(); } @Bean diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java deleted file mode 100644 index bc77d90e0..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/ReactivePolarisCircuitBreakerAutoConfiguration.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.config; - -import java.util.ArrayList; -import java.util.List; - -import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory; -import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier; -import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter; -import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter; -import com.tencent.cloud.polaris.context.PolarisSDKContextManager; -import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration; -import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.client.circuitbreaker.Customizer; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * AutoConfiguration for ReactivePolarisCircuitBreaker. - * - * @author seanyu 2023-02-27 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = {"reactor.core.publisher.Mono", "reactor.core.publisher.Flux"}) -@ConditionalOnPolarisCircuitBreakerEnabled -@EnableConfigurationProperties(PolarisCircuitBreakerProperties.class) -@AutoConfigureAfter(RpcEnhancementAutoConfiguration.class) -public class ReactivePolarisCircuitBreakerAutoConfiguration { - - @Autowired(required = false) - private List> customizers = new ArrayList<>(); - - @Bean - @ConditionalOnMissingBean(SuccessCircuitBreakerReporter.class) - public SuccessCircuitBreakerReporter successCircuitBreakerReporter(RpcEnhancementReporterProperties properties, - PolarisSDKContextManager polarisSDKContextManager) { - return new SuccessCircuitBreakerReporter(properties, polarisSDKContextManager.getCircuitBreakAPI()); - } - - @Bean - @ConditionalOnMissingBean(ExceptionCircuitBreakerReporter.class) - public ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter(RpcEnhancementReporterProperties properties, - PolarisSDKContextManager polarisSDKContextManager) { - return new ExceptionCircuitBreakerReporter(properties, polarisSDKContextManager.getCircuitBreakAPI()); - } - - @Bean - @ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class) - public ReactiveCircuitBreakerFactory polarisReactiveCircuitBreakerFactory(PolarisSDKContextManager polarisSDKContextManager, - PolarisCircuitBreakerProperties polarisCircuitBreakerProperties) { - ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory( - polarisSDKContextManager.getCircuitBreakAPI(), polarisSDKContextManager.getConsumerAPI(), polarisCircuitBreakerProperties); - customizers.forEach(customizer -> customizer.customize(factory)); - return factory; - } - - @Bean - @ConditionalOnMissingBean(CircuitBreakerConfigModifier.class) - public CircuitBreakerConfigModifier reactiveCircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) { - return new CircuitBreakerConfigModifier(properties); - } - -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreaker.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreaker.java index b0ef6b41c..796b85e11 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreaker.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreaker.java @@ -20,8 +20,6 @@ import feign.Feign; import feign.Target; -import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.FallbackFactory; /** @@ -46,29 +44,9 @@ public static PolarisFeignCircuitBreaker.Builder builder() { * Builder for Feign CircuitBreaker integration. */ public static final class Builder extends Feign.Builder { - - private CircuitBreakerFactory circuitBreakerFactory; - private String feignClientName; - private CircuitBreakerNameResolver circuitBreakerNameResolver; - public Builder() { } - public PolarisFeignCircuitBreaker.Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) { - this.circuitBreakerFactory = circuitBreakerFactory; - return this; - } - - public PolarisFeignCircuitBreaker.Builder feignClientName(String feignClientName) { - this.feignClientName = feignClientName; - return this; - } - - public PolarisFeignCircuitBreaker.Builder circuitBreakerNameResolver(CircuitBreakerNameResolver circuitBreakerNameResolver) { - this.circuitBreakerNameResolver = circuitBreakerNameResolver; - return this; - } - public T target(Target target, T fallback) { return build(fallback != null ? new FallbackFactory.Default(fallback) : null).newInstance(target); } @@ -84,7 +62,7 @@ public T target(Target target) { public Feign build(final FallbackFactory nullableFallbackFactory) { this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler( - circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory, circuitBreakerNameResolver, this.decoder)); + target, dispatch, nullableFallbackFactory, decoder)); return this.build(); } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandler.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandler.java index 067dd10f1..f9c42330e 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandler.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandler.java @@ -23,21 +23,14 @@ import java.lang.reflect.Proxy; import java.util.LinkedHashMap; import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; import com.tencent.cloud.polaris.circuitbreaker.exception.FallbackWrapperException; import feign.InvocationHandlerFactory; import feign.Target; import feign.codec.Decoder; -import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; -import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException; -import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.FallbackFactory; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; import static feign.Util.checkNotNull; @@ -47,11 +40,6 @@ * @author sean yu */ public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHandler { - - private final CircuitBreakerFactory factory; - - private final String feignClientName; - private final Target target; private final Map dispatch; @@ -60,20 +48,16 @@ public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHa private final Map fallbackMethodMap; - private final CircuitBreakerNameResolver circuitBreakerNameResolver; - private final Decoder decoder; - public PolarisFeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target target, - Map dispatch, FallbackFactory nullableFallbackFactory, - CircuitBreakerNameResolver circuitBreakerNameResolver, Decoder decoder) { - this.factory = factory; - this.feignClientName = feignClientName; + public PolarisFeignCircuitBreakerInvocationHandler(Target target, + Map dispatch, + FallbackFactory nullableFallbackFactory, Decoder decoder) { + this.target = checkNotNull(target, "target"); this.dispatch = checkNotNull(dispatch, "dispatch"); this.fallbackMethodMap = toFallbackMethod(dispatch); this.nullableFallbackFactory = nullableFallbackFactory; - this.circuitBreakerNameResolver = circuitBreakerNameResolver; this.decoder = decoder; } @@ -115,13 +99,12 @@ else if ("toString".equals(method.getName())) { return toString(); } - String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method); - CircuitBreaker circuitBreaker = factory.create(circuitName); - Supplier supplier = asSupplier(method, args); - Function fallbackFunction; - if (this.nullableFallbackFactory != null) { - fallbackFunction = throwable -> { - Object fallback = this.nullableFallbackFactory.create(throwable); + try { + return this.dispatch.get(method).invoke(args); + } + catch (Exception e) { + if (this.nullableFallbackFactory != null) { + Object fallback = this.nullableFallbackFactory.create(e); try { return this.fallbackMethodMap.get(method).invoke(fallback, args); } @@ -129,21 +112,12 @@ else if ("toString".equals(method.getName())) { unwrapAndRethrow(exception); } return null; - }; - } - else { - fallbackFunction = throwable -> { + } + else { PolarisCircuitBreakerFallbackFactory.DefaultFallback fallback = - (PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(throwable); + (PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(e); return fallback.fallback(method); - }; - } - try { - return circuitBreaker.run(supplier, fallbackFunction); - } - catch (FallbackWrapperException e) { - // unwrap And Rethrow - throw e.getCause(); + } } } @@ -160,31 +134,6 @@ private void unwrapAndRethrow(Exception exception) { } } - private Supplier asSupplier(final Method method, final Object[] args) { - final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); - final Thread caller = Thread.currentThread(); - return () -> { - boolean isAsync = caller != Thread.currentThread(); - try { - if (isAsync) { - RequestContextHolder.setRequestAttributes(requestAttributes); - } - return dispatch.get(method).invoke(args); - } - catch (RuntimeException throwable) { - throw throwable; - } - catch (Throwable throwable) { - throw new RuntimeException(throwable); - } - finally { - if (isAsync) { - RequestContextHolder.resetRequestAttributes(); - } - } - }; - } - @Override public boolean equals(Object obj) { if (obj instanceof PolarisFeignCircuitBreakerInvocationHandler) { diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeter.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeter.java index fe7c799de..585b48321 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeter.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeter.java @@ -20,8 +20,6 @@ import feign.Feign; import feign.Target; -import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.cloud.openfeign.FeignContext; @@ -35,15 +33,6 @@ */ public class PolarisFeignCircuitBreakerTargeter implements Targeter { - private final CircuitBreakerFactory circuitBreakerFactory; - - private final CircuitBreakerNameResolver circuitBreakerNameResolver; - - public PolarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, CircuitBreakerNameResolver circuitBreakerNameResolver) { - this.circuitBreakerFactory = circuitBreakerFactory; - this.circuitBreakerNameResolver = circuitBreakerNameResolver; - } - @Override public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget target) { @@ -60,20 +49,20 @@ public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignCo if (fallbackFactory != void.class) { return targetWithFallbackFactory(name, context, target, builder, fallbackFactory); } - return builder(name, builder).target(target); + return builder.target(target); } private T targetWithFallbackFactory(String feignClientName, FeignContext context, Target.HardCodedTarget target, PolarisFeignCircuitBreaker.Builder builder, Class fallbackFactoryClass) { FallbackFactory fallbackFactory = (FallbackFactory) getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class); - return builder(feignClientName, builder).target(target, fallbackFactory); + return builder.target(target, fallbackFactory); } private T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget target, PolarisFeignCircuitBreaker.Builder builder, Class fallback) { T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); - return builder(feignClientName, builder).target(target, fallbackInstance); + return builder.target(target, fallbackInstance); } private T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, @@ -92,12 +81,4 @@ private T getFromContext(String fallbackMechanism, String feignClientName, F } return (T) fallbackInstance; } - - private PolarisFeignCircuitBreaker.Builder builder(String feignClientName, PolarisFeignCircuitBreaker.Builder builder) { - return builder - .circuitBreakerFactory(circuitBreakerFactory) - .feignClientName(feignClientName) - .circuitBreakerNameResolver(circuitBreakerNameResolver); - } - } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/gateway/PolarisCircuitBreakerFilterFactory.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/gateway/PolarisCircuitBreakerFilterFactory.java deleted file mode 100644 index 5a17c4043..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/gateway/PolarisCircuitBreakerFilterFactory.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.instrument.gateway; - - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import com.tencent.cloud.common.metadata.MetadataContext; -import com.tencent.polaris.api.pojo.CircuitBreakerStatus; -import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.beans.InvalidPropertyException; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; -import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; -import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties; -import org.springframework.cloud.gateway.filter.GatewayFilter; -import org.springframework.cloud.gateway.filter.GatewayFilterChain; -import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; -import org.springframework.cloud.gateway.route.Route; -import org.springframework.cloud.gateway.support.HttpStatusHolder; -import org.springframework.cloud.gateway.support.ServiceUnavailableException; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.util.UriComponentsBuilder; - -import static java.util.Optional.ofNullable; -import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset; - -/** - * PolarisCircuitBreakerFilterFactory. - * mostly copy from SpringCloudCircuitBreakerFilterFactory, but create ReactiveCircuitBreaker per request to build method level CircuitBreaker. - * - * @author seanyu 2023-02-27 - */ -public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory { - - private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory; - private final ObjectProvider dispatcherHandlerProvider; - private String routeIdPrefix; - // do not use this dispatcherHandler directly, use getDispatcherHandler() instead. - private volatile DispatcherHandler dispatcherHandler; - - public PolarisCircuitBreakerFilterFactory( - ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, - ObjectProvider dispatcherHandlerProvider, - ReactiveDiscoveryClient discoveryClient, - DiscoveryLocatorProperties properties - ) { - super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider); - this.reactiveCircuitBreakerFactory = reactiveCircuitBreakerFactory; - this.dispatcherHandlerProvider = dispatcherHandlerProvider; - if (discoveryClient != null && properties != null) { - if (StringUtils.hasText(properties.getRouteIdPrefix())) { - routeIdPrefix = properties.getRouteIdPrefix(); - } - else { - routeIdPrefix = discoveryClient.getClass().getSimpleName() + "_"; - } - } - } - - private void addExceptionDetails(Throwable t, ServerWebExchange exchange) { - ofNullable(t).ifPresent( - exception -> exchange.getAttributes().put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception)); - } - - private DispatcherHandler getDispatcherHandler() { - if (dispatcherHandler == null) { - dispatcherHandler = dispatcherHandlerProvider.getIfAvailable(); - } - return dispatcherHandler; - } - - private String getCircuitBreakerId(Config config) { - if (!StringUtils.hasText(config.getName()) && StringUtils.hasText(config.getRouteId())) { - if (routeIdPrefix != null && config.getRouteId().startsWith(routeIdPrefix)) { - return config.getRouteId().replace(routeIdPrefix, ""); - } - return config.getRouteId(); - } - return config.getName(); - } - - private boolean isNumeric(String statusString) { - try { - Integer.parseInt(statusString); - return true; - } - catch (NumberFormatException e) { - return false; - } - } - - private List getSeriesStatus(String series) { - if (!Arrays.asList("1**", "2**", "3**", "4**", "5**").contains(series)) { - throw new InvalidPropertyException(Config.class, "statusCodes", "polaris circuit breaker status code can only be a numeric http status, or a http series pattern, e.g. [\"1**\",\"2**\",\"3**\",\"4**\",\"5**\"]"); - } - HttpStatus[] allHttpStatus = HttpStatus.values(); - if (series.startsWith("1")) { - return Arrays.stream(allHttpStatus).filter(HttpStatus::is1xxInformational).collect(Collectors.toList()); - } - else if (series.startsWith("2")) { - return Arrays.stream(allHttpStatus).filter(HttpStatus::is2xxSuccessful).collect(Collectors.toList()); - } - else if (series.startsWith("3")) { - return Arrays.stream(allHttpStatus).filter(HttpStatus::is3xxRedirection).collect(Collectors.toList()); - } - else if (series.startsWith("4")) { - return Arrays.stream(allHttpStatus).filter(HttpStatus::is4xxClientError).collect(Collectors.toList()); - } - else if (series.startsWith("5")) { - return Arrays.stream(allHttpStatus).filter(HttpStatus::is5xxServerError).collect(Collectors.toList()); - } - return Arrays.asList(allHttpStatus); - } - - private Set getDefaultStatus() { - return Arrays.stream(HttpStatus.values()) - .filter(HttpStatus::is5xxServerError) - .collect(Collectors.toSet()); - } - - @Override - public GatewayFilter apply(Config config) { - Set statuses = config.getStatusCodes().stream() - .flatMap(statusCode -> { - List httpStatuses = new ArrayList<>(); - if (isNumeric(statusCode)) { - httpStatuses.add(HttpStatusHolder.parse(statusCode).getHttpStatus()); - } - else { - httpStatuses.addAll(getSeriesStatus(statusCode)); - } - return httpStatuses.stream(); - }) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - if (CollectionUtils.isEmpty(statuses)) { - statuses.addAll(getDefaultStatus()); - } - String circuitBreakerId = getCircuitBreakerId(config); - return new GatewayFilter() { - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); - String serviceName = circuitBreakerId; - if (route != null) { - serviceName = route.getUri().getHost(); - } - String path = exchange.getRequest().getPath().value(); - String method = exchange.getRequest().getMethod() == null ? - "GET" : exchange.getRequest().getMethod().name(); - ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(MetadataContext.LOCAL_NAMESPACE + "#" + serviceName + "#" + path + "#http#" + method); - return cb.run( - chain.filter(exchange).doOnSuccess(v -> { - // throw CircuitBreakerStatusCodeException by default for all need checking status - // so polaris can report right error status - Set statusNeedToCheck = new HashSet<>(); - statusNeedToCheck.addAll(statuses); - statusNeedToCheck.addAll(getDefaultStatus()); - HttpStatus status = exchange.getResponse().getStatusCode(); - if (statusNeedToCheck.contains(status)) { - throw new CircuitBreakerStatusCodeException(status); - } - }), - t -> { - // pre-check CircuitBreakerStatusCodeException's status matches input status - if (t instanceof CircuitBreakerStatusCodeException) { - HttpStatus status = ((CircuitBreakerStatusCodeException) t).getStatusCode(); - // no need to fallback - if (!statuses.contains(status)) { - return Mono.error(t); - } - } - // do fallback - if (config.getFallbackUri() == null) { - // polaris checking - if (t instanceof CallAbortedException) { - CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo(); - if (fallbackInfo != null) { - ServerHttpResponse response = exchange.getResponse(); - response.setRawStatusCode(fallbackInfo.getCode()); - if (fallbackInfo.getHeaders() != null) { - fallbackInfo.getHeaders() - .forEach((k, v) -> response.getHeaders().add(k, v)); - } - DataBuffer bodyBuffer = null; - if (fallbackInfo.getBody() != null) { - byte[] bytes = fallbackInfo.getBody().getBytes(StandardCharsets.UTF_8); - bodyBuffer = response.bufferFactory().wrap(bytes); - } - return bodyBuffer != null ? response.writeWith(Flux.just(bodyBuffer)) : response.setComplete(); - } - } - return Mono.error(t); - } - exchange.getResponse().setStatusCode(null); - reset(exchange); - - // TODO: copied from RouteToRequestUrlFilter - URI uri = exchange.getRequest().getURI(); - // TODO: assume always? - boolean encoded = containsEncodedParts(uri); - URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null) - .uri(config.getFallbackUri()).scheme(null).build(encoded).toUri(); - exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); - addExceptionDetails(t, exchange); - - // Reset the exchange - reset(exchange); - - ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build(); - return getDispatcherHandler().handle(exchange.mutate().request(request).build()); - }) - .onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError())); - } - - @Override - public String toString() { - return filterToStringCreator(PolarisCircuitBreakerFilterFactory.this) - .append("name", config.getName()).append("fallback", config.getFallbackUri()).toString(); - } - }; - - } - - @Override - protected Mono handleErrorWithoutFallback(Throwable t, boolean resumeWithoutError) { - if (t instanceof java.util.concurrent.TimeoutException) { - return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t)); - } - if (t instanceof CallAbortedException) { - return Mono.error(new ServiceUnavailableException()); - } - if (t instanceof CircuitBreakerStatusCodeException) { - return Mono.empty(); - } - if (resumeWithoutError) { - return Mono.empty(); - } - return Mono.error(t); - } - -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponse.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponse.java index 6e6f1ccde..9b8bb2283 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponse.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponse.java @@ -53,7 +53,7 @@ public PolarisCircuitBreakerHttpResponse(int code, Map headers, this(new CircuitBreakerStatus.FallbackInfo(code, headers, body)); } - PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) { + public PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) { this.fallbackInfo = fallbackInfo; if (fallbackInfo.getHeaders() != null) { fallbackInfo.getHeaders().forEach(headers::add); diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java deleted file mode 100644 index 2c05c3600..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateBeanPostProcessor.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.context.ApplicationContext; -import org.springframework.core.type.MethodMetadata; -import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; - -/** - * PolarisCircuitBreakerRestTemplateBeanPostProcessor. - * - * @author sean yu - */ -public class PolarisCircuitBreakerRestTemplateBeanPostProcessor implements MergedBeanDefinitionPostProcessor { - - private final ApplicationContext applicationContext; - private final Set cache = Collections.synchronizedSet(new HashSet<>()); - - public PolarisCircuitBreakerRestTemplateBeanPostProcessor(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - @Override - public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { - if (checkAnnotated(beanDefinition, beanType, beanName)) { - cache.add(beanName); - } - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (cache.contains(beanName)) { - String interceptorBeanNamePrefix = StringUtils.uncapitalize("PolarisCircuitBreaker"); - RestTemplate restTemplate = (RestTemplate) bean; - String interceptorBeanName = interceptorBeanNamePrefix + "@" + bean; - CircuitBreakerFactory circuitBreakerFactory = this.applicationContext.getBean(CircuitBreakerFactory.class); - registerBean(interceptorBeanName, applicationContext, circuitBreakerFactory, restTemplate); - PolarisCircuitBreakerRestTemplateInterceptor polarisCircuitBreakerRestTemplateInterceptor = applicationContext - .getBean(interceptorBeanName, PolarisCircuitBreakerRestTemplateInterceptor.class); - restTemplate.getInterceptors().add(0, polarisCircuitBreakerRestTemplateInterceptor); - } - return bean; - } - - private boolean checkAnnotated(RootBeanDefinition beanDefinition, - Class beanType, String beanName) { - return beanName != null && beanType == RestTemplate.class - && beanDefinition.getSource() instanceof MethodMetadata - && ((MethodMetadata) beanDefinition.getSource()) - .isAnnotated(LoadBalanced.class.getName()); - } - - private void registerBean(String interceptorBeanName, ApplicationContext applicationContext, - CircuitBreakerFactory circuitBreakerFactory, RestTemplate restTemplate) { - // register PolarisCircuitBreakerRestTemplateInterceptor bean - DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext - .getAutowireCapableBeanFactory(); - BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder - .genericBeanDefinition(PolarisCircuitBreakerRestTemplateInterceptor.class); - beanDefinitionBuilder.addConstructorArgValue(circuitBreakerFactory); - beanDefinitionBuilder.addConstructorArgValue(restTemplate); - BeanDefinition interceptorBeanDefinition = beanDefinitionBuilder - .getRawBeanDefinition(); - beanFactory.registerBeanDefinition(interceptorBeanName, - interceptorBeanDefinition); - } - -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java deleted file mode 100644 index d81c9c253..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateInterceptor.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate; - -import java.io.IOException; - -import com.tencent.cloud.common.metadata.MetadataContext; -import com.tencent.cloud.polaris.circuitbreaker.exception.FallbackWrapperException; -import com.tencent.polaris.api.pojo.CircuitBreakerStatus; -import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; - -import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestTemplate; - -/** - * PolarisCircuitBreakerRestTemplateInterceptor. - * - * @author sean yu - */ -public class PolarisCircuitBreakerRestTemplateInterceptor implements ClientHttpRequestInterceptor { - - private final CircuitBreakerFactory circuitBreakerFactory; - - private final RestTemplate restTemplate; - - public PolarisCircuitBreakerRestTemplateInterceptor( - CircuitBreakerFactory circuitBreakerFactory, - RestTemplate restTemplate - ) { - this.circuitBreakerFactory = circuitBreakerFactory; - this.restTemplate = restTemplate; - } - - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - try { - String httpMethod = "GET"; - if (request.getMethod() != null) { - httpMethod = request.getMethod().name(); - } - return circuitBreakerFactory.create(MetadataContext.LOCAL_NAMESPACE + "#" + request.getURI() - .getHost() + "#" + request.getURI().getPath() + "#http#" + httpMethod).run( - () -> { - try { - ClientHttpResponse response = execution.execute(request, body); - ResponseErrorHandler errorHandler = restTemplate.getErrorHandler(); - if (errorHandler.hasError(response)) { - errorHandler.handleError(request.getURI(), request.getMethod(), response); - } - return response; - } - catch (IOException e) { - throw new IllegalStateException(e); - } - }, - t -> { - if (t instanceof CallAbortedException) { - CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo(); - if (fallbackInfo != null) { - return new PolarisCircuitBreakerHttpResponse(fallbackInfo); - } - } - throw new FallbackWrapperException(t); - } - ); - } - catch (FallbackWrapperException e) { - // unwrap And Rethrow - Throwable underlyingException = e.getCause(); - if (underlyingException instanceof RuntimeException) { - throw (RuntimeException) underlyingException; - } - throw new IllegalStateException(underlyingException); - } - - } - -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptor.java new file mode 100644 index 000000000..2e57fdf22 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptor.java @@ -0,0 +1,69 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate; + +import java.io.IOException; +import java.net.URI; + +import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateWrapInterceptor; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; + +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; + +/** + * PolarisLoadBalancerInterceptor is a wrapper of LoadBalancerInterceptor. + * + * @author Shedfree Wu + */ +public class PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor { + + private final LoadBalancerClient loadBalancer; + private final LoadBalancerRequestFactory requestFactory; + + private final EnhancedPluginRunner enhancedPluginRunner; + + public PolarisLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory, + EnhancedPluginRunner enhancedPluginRunner) { + super(loadBalancer, requestFactory); + this.loadBalancer = loadBalancer; + this.requestFactory = requestFactory; + this.enhancedPluginRunner = enhancedPluginRunner; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + final URI originalUri = request.getURI(); + String peerServiceName = originalUri.getHost(); + Assert.state(peerServiceName != null, + "Request URI does not contain a valid hostname: " + originalUri); + + if (enhancedPluginRunner != null) { + EnhancedRestTemplateWrapInterceptor enhancedRestTemplateWrapInterceptor = new EnhancedRestTemplateWrapInterceptor(enhancedPluginRunner, loadBalancer); + return enhancedRestTemplateWrapInterceptor.intercept(request, peerServiceName, this.requestFactory.createRequest(request, body, execution)); + } + else { + return super.intercept(request, body, execution); + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/CircuitBreakerPlugin.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/CircuitBreakerPlugin.java new file mode 100644 index 000000000..23588515f --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/CircuitBreakerPlugin.java @@ -0,0 +1,113 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.reporter; + +import com.tencent.cloud.common.constant.ContextConstant; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker; +import com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate.PolarisCircuitBreakerHttpResponse; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; +import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; +import com.tencent.polaris.metadata.core.MetadataType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; +import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; + +import static com.tencent.cloud.rpc.enhancement.plugin.PluginOrderConstant.ClientPluginOrder.CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; + +/** + * CircuitBreakerPlugin, do circuit breaker in enhance plugin and record info into metadata. + * + * @author Shedfree Wu + */ +public class CircuitBreakerPlugin implements EnhancedPlugin { + + private static final Logger LOG = LoggerFactory.getLogger(SuccessPolarisReporter.class); + + private CircuitBreakerFactory circuitBreakerFactory; + + public CircuitBreakerPlugin(CircuitBreakerFactory circuitBreakerFactory) { + this.circuitBreakerFactory = circuitBreakerFactory; + } + + @Override + public String getName() { + return CircuitBreakerPlugin.class.getName(); + } + + @Override + public EnhancedPluginType getType() { + return EnhancedPluginType.Client.PRE; + } + + @Override + public void run(EnhancedPluginContext context) throws Throwable { + + EnhancedRequestContext request = context.getRequest(); + EnhancedResponseContext response = context.getResponse(); + + String governanceNamespace = MetadataContext.LOCAL_NAMESPACE; + + String host = request.getServiceUrl() != null ? request.getServiceUrl().getHost() : request.getUrl().getHost(); + String path = request.getServiceUrl() != null ? request.getServiceUrl().getPath() : request.getUrl().getPath(); + String httpMethod = request.getHttpMethod().name(); + + CircuitBreaker circuitBreaker = circuitBreakerFactory.create(governanceNamespace + "#" + host + "#" + path + "#http#" + httpMethod); + if (circuitBreaker instanceof PolarisCircuitBreaker) { + PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) circuitBreaker; + putMetadataObjectValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER, polarisCircuitBreaker); + putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_START_TIME, System.currentTimeMillis()); + + try { + polarisCircuitBreaker.acquirePermission(); + } + catch (CallAbortedException e) { + LOG.debug("[CircuitBreakerPlugin] request is aborted. request service url=[{}]", request.getServiceUrl()); + if (e.getFallbackInfo() != null) { + Object fallbackResponse = new PolarisCircuitBreakerHttpResponse(e.getFallbackInfo()); + putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE, fallbackResponse); + } + throw e; + } + } + } + + @Override + public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) { + LOG.error("SuccessCircuitBreakerReporter runs failed. context=[{}].", + context, throwable); + } + + @Override + public int getOrder() { + return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; + } + + private void putMetadataObjectValue(String key, Object value) { + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(key, value); + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporter.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporter.java index 2a8f49519..e4082417e 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporter.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporter.java @@ -18,7 +18,11 @@ package com.tencent.cloud.polaris.circuitbreaker.reporter; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import com.tencent.cloud.common.constant.ContextConstant; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; @@ -27,6 +31,9 @@ import com.tencent.cloud.rpc.enhancement.plugin.PolarisEnhancedPluginUtils; import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; +import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext; +import com.tencent.polaris.metadata.core.MetadataObjectValue; +import com.tencent.polaris.metadata.core.MetadataType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,6 +97,35 @@ public void run(EnhancedPluginContext context) throws Throwable { circuitBreakAPI.report(resourceStat); + MetadataObjectValue circuitBreakerObject = MetadataContextHolder.get(). + getMetadataContainer(MetadataType.APPLICATION, true). + getMetadataValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER); + + MetadataObjectValue startTimeMilliObject = MetadataContextHolder.get(). + getMetadataContainer(MetadataType.APPLICATION, true). + getMetadataValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_START_TIME); + + boolean existCircuitBreaker = existMetadataValue(circuitBreakerObject); + boolean existStartTime = existMetadataValue(startTimeMilliObject); + + if (existCircuitBreaker && existStartTime) { + PolarisCircuitBreaker polarisCircuitBreaker = circuitBreakerObject.getObjectValue().get(); + Long startTimeMillis = startTimeMilliObject.getObjectValue().get(); + + long delay = System.currentTimeMillis() - startTimeMillis; + InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext(); + responseContext.setDuration(delay); + responseContext.setDurationUnit(TimeUnit.MILLISECONDS); + responseContext.setError(context.getThrowable()); + + if (responseContext.getError() == null) { + polarisCircuitBreaker.onSuccess(responseContext); + } + else { + polarisCircuitBreaker.onError(responseContext); + } + } + } @Override @@ -102,4 +138,9 @@ public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) public int getOrder() { return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; } + + private static boolean existMetadataValue(MetadataObjectValue metadataObjectValue) { + return Optional.ofNullable(metadataObjectValue).map(MetadataObjectValue::getObjectValue). + map(Optional::isPresent).orElse(false); + } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporter.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporter.java index bbe5ba52f..3e1daf4ec 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporter.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporter.java @@ -18,7 +18,12 @@ package com.tencent.cloud.polaris.circuitbreaker.reporter; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import com.tencent.cloud.common.constant.ContextConstant; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker; +import com.tencent.cloud.polaris.context.CircuitBreakerStatusCodeException; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; @@ -29,11 +34,15 @@ import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter; import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; +import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext; +import com.tencent.polaris.metadata.core.MetadataObjectValue; +import com.tencent.polaris.metadata.core.MetadataType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.http.HttpStatus; import static com.tencent.cloud.rpc.enhancement.plugin.PluginOrderConstant.ClientPluginOrder.CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; @@ -91,6 +100,37 @@ public void run(EnhancedPluginContext context) throws Throwable { .getPath(), response.getHttpStatus(), context.getDelay()); circuitBreakAPI.report(resourceStat); + + MetadataObjectValue circuitBreakerObject = MetadataContextHolder.get(). + getMetadataContainer(MetadataType.APPLICATION, true). + getMetadataValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER); + + MetadataObjectValue startTimeMilliObject = MetadataContextHolder.get(). + getMetadataContainer(MetadataType.APPLICATION, true). + getMetadataValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_START_TIME); + + boolean existCircuitBreaker = existMetadataValue(circuitBreakerObject); + boolean existStartTime = existMetadataValue(startTimeMilliObject); + + if (existCircuitBreaker && existStartTime) { + PolarisCircuitBreaker polarisCircuitBreaker = circuitBreakerObject.getObjectValue().get(); + Long startTimeMillis = startTimeMilliObject.getObjectValue().get(); + long delay = System.currentTimeMillis() - startTimeMillis; + InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext(); + responseContext.setDuration(delay); + responseContext.setDurationUnit(TimeUnit.MILLISECONDS); + HttpStatus status = HttpStatus.resolve(response.getHttpStatus()); + if (status != null && (status.is5xxServerError() || status.is4xxClientError())) { + Throwable throwable = new CircuitBreakerStatusCodeException(status); + responseContext.setError(throwable); + } + if (responseContext.getError() == null) { + polarisCircuitBreaker.onSuccess(responseContext); + } + else { + polarisCircuitBreaker.onError(responseContext); + } + } } @Override @@ -103,4 +143,9 @@ public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) public int getOrder() { return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; } + + private static boolean existMetadataValue(MetadataObjectValue metadataObjectValue) { + return Optional.ofNullable(metadataObjectValue).map(MetadataObjectValue::getObjectValue). + map(Optional::isPresent).orElse(false); + } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/resources/META-INF/spring.factories index 07972a65c..05c88507a 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/resources/META-INF/spring.factories @@ -1,6 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration,\ - com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration,\ com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration,\ com.tencent.cloud.polaris.circuitbreaker.config.GatewayPolarisCircuitBreakerAutoConfiguration,\ com.tencent.cloud.polaris.circuitbreaker.endpoint.PolarisCircuitBreakerEndpointAutoConfiguration diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java index ec73339ce..95c34a578 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerMockServerTest.java @@ -24,7 +24,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -50,11 +49,8 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER; @@ -62,7 +58,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Test for {@link PolarisCircuitBreaker} and {@link ReactivePolarisCircuitBreaker} using real mock server. + * Test for {@link PolarisCircuitBreaker} using real mock server. * * @author sean yu */ @@ -138,26 +134,6 @@ public void testCircuitBreaker() { Utils.sleepUninterrupted(2000); } assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback")); - - // always fallback - ReactivePolarisCircuitBreakerFactory reactivePolarisCircuitBreakerFactory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI, polarisCircuitBreakerProperties); - ReactiveCircuitBreaker rcb = reactivePolarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER); - - assertThat(Mono.just("foobar").transform(it -> rcb.run(it, t -> Mono.just("fallback"))) - .block()).isEqualTo("fallback"); - - assertThat(Mono.error(new RuntimeException("boom")).transform(it -> rcb.run(it, t -> Mono.just("fallback"))) - .block()).isEqualTo("fallback"); - - assertThat(Flux.just("foobar", "hello world") - .transform(it -> rcb.run(it, t -> Flux.just("fallback", "fallback"))) - .collectList().block()) - .isEqualTo(Arrays.asList("fallback", "fallback")); - - assertThat(Flux.error(new RuntimeException("boom")).transform(it -> rcb.run(it, t -> Flux.just("fallback"))) - .collectList().block()) - .isEqualTo(Collections.singletonList("fallback")); - } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreakerTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreakerTest.java deleted file mode 100644 index da680c300..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/ReactivePolarisCircuitBreakerTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -import com.tencent.cloud.common.util.ApplicationContextAwareUtils; -import com.tencent.cloud.common.util.ReflectionUtils; -import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder; -import com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration; -import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; -import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration; -import com.tencent.polaris.client.util.Utils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; -import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; - -import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; -import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Test for {@link ReactivePolarisCircuitBreaker}. - * - * @author sean yu - */ -@ExtendWith(MockitoExtension.class) -public class ReactivePolarisCircuitBreakerTest { - - private static MockedStatic mockedApplicationContextAwareUtils; - private final ApplicationContextRunner reactiveContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of( - PolarisContextAutoConfiguration.class, - RpcEnhancementAutoConfiguration.class, - LoadBalancerAutoConfiguration.class, - ReactivePolarisCircuitBreakerAutoConfiguration.class)) - .withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true") - .withPropertyValues("spring.cloud.polaris.circuitbreaker.configuration-cleanup-interval=5000"); - - @BeforeAll - public static void beforeClass() { - mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); - mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace")) - .thenReturn(NAMESPACE_TEST); - mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service")) - .thenReturn(SERVICE_CIRCUIT_BREAKER); - } - - @AfterAll - public static void afterAll() { - mockedApplicationContextAwareUtils.close(); - } - - @Test - public void run() { - this.reactiveContextRunner.run(context -> { - ReactivePolarisCircuitBreakerFactory polarisCircuitBreakerFactory = context.getBean(ReactivePolarisCircuitBreakerFactory.class); - ReactiveCircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER); - - PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration configuration = - polarisCircuitBreakerFactory.configBuilder(SERVICE_CIRCUIT_BREAKER).build(); - - polarisCircuitBreakerFactory.configureDefault(id -> configuration); - - assertThat(Mono.just("foobar").transform(cb::run).block()).isEqualTo("foobar"); - - assertThat(Mono.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Mono.just("fallback"))) - .block()).isEqualTo("fallback"); - - assertThat(Flux.just("foobar", "hello world").transform(cb::run).collectList().block()) - .isEqualTo(Arrays.asList("foobar", "hello world")); - - assertThat(Flux.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Flux.just("fallback"))) - .collectList().block()).isEqualTo(Collections.singletonList("fallback")); - - Method getConfigurationsMethod = ReflectionUtils.findMethod(PolarisCircuitBreakerFactory.class, - "getConfigurations"); - Assertions.assertNotNull(getConfigurationsMethod); - ReflectionUtils.makeAccessible(getConfigurationsMethod); - Map values = (Map) ReflectionUtils.invokeMethod(getConfigurationsMethod, polarisCircuitBreakerFactory); - Assertions.assertNotNull(values); - Assertions.assertEquals(1, values.size()); - - Utils.sleepUninterrupted(10 * 1000); - - Assertions.assertEquals(0, values.size()); - }); - } - -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessorTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessorTest.java new file mode 100644 index 000000000..7d42fd052 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessorTest.java @@ -0,0 +1,127 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.beanprocessor; + +import com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate.PolarisLoadBalancerInterceptor; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test for ${@link LoadBalancerInterceptorBeanPostProcessor}. + * + * @author Shedfree Wu + */ +class LoadBalancerInterceptorBeanPostProcessorTest { + + @Mock + private BeanFactory beanFactory; + + @Mock + private LoadBalancerRequestFactory requestFactory; + + @Mock + private LoadBalancerClient loadBalancerClient; + + @Mock + private EnhancedPluginRunner pluginRunner; + + private LoadBalancerInterceptorBeanPostProcessor processor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + processor = new LoadBalancerInterceptorBeanPostProcessor(); + processor.setBeanFactory(beanFactory); + + // Setup mock behavior + when(beanFactory.getBean(LoadBalancerRequestFactory.class)).thenReturn(requestFactory); + when(beanFactory.getBean(LoadBalancerClient.class)).thenReturn(loadBalancerClient); + when(beanFactory.getBean(EnhancedPluginRunner.class)).thenReturn(pluginRunner); + } + + @Test + void testPostProcessBeforeInitializationWithLoadBalancerInterceptor() { + // Arrange + LoadBalancerInterceptor originalInterceptor = mock(LoadBalancerInterceptor.class); + String beanName = "testBean"; + + // Act + Object result = processor.postProcessBeforeInitialization(originalInterceptor, beanName); + + // Assert + Assertions.assertInstanceOf(PolarisLoadBalancerInterceptor.class, result); + verify(beanFactory).getBean(LoadBalancerRequestFactory.class); + verify(beanFactory).getBean(LoadBalancerClient.class); + verify(beanFactory).getBean(EnhancedPluginRunner.class); + } + + @Test + void testPostProcessBeforeInitializationWithNonLoadBalancerInterceptor() { + // Arrange + Object originalBean = new Object(); + String beanName = "testBean"; + + // Act + Object result = processor.postProcessBeforeInitialization(originalBean, beanName); + + // Assert + Assertions.assertSame(originalBean, result); + } + + @Test + void testGetOrder() { + // Act + int order = processor.getOrder(); + + // Assert + Assertions.assertEquals(LoadBalancerInterceptorBeanPostProcessor.POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER, order); + } + + @Test + void testSetBeanFactory() { + // Arrange + BeanFactory newBeanFactory = mock(BeanFactory.class); + LoadBalancerInterceptorBeanPostProcessor newProcessor = new LoadBalancerInterceptorBeanPostProcessor(); + + // Act + newProcessor.setBeanFactory(newBeanFactory); + + // Assert + // Verify the bean factory is set by trying to process a bean + LoadBalancerInterceptor interceptor = mock(LoadBalancerInterceptor.class); + when(newBeanFactory.getBean(LoadBalancerRequestFactory.class)).thenReturn(requestFactory); + when(newBeanFactory.getBean(LoadBalancerClient.class)).thenReturn(loadBalancerClient); + when(newBeanFactory.getBean(EnhancedPluginRunner.class)).thenReturn(pluginRunner); + + Object result = newProcessor.postProcessBeforeInitialization(interceptor, "testBean"); + Assertions.assertInstanceOf(PolarisLoadBalancerInterceptor.class, result); + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCodeTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCodeTest.java new file mode 100644 index 000000000..23b974d14 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/common/PolarisResultToErrorCodeTest.java @@ -0,0 +1,94 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.common; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** + * Test for ${@link PolarisResultToErrorCode}. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +class PolarisResultToErrorCodeTest { + + private final PolarisResultToErrorCode converter = new PolarisResultToErrorCode(); + + @Test + void testOnSuccess() { + Assertions.assertEquals(200, converter.onSuccess("any value")); + } + + @Test + void testOnErrorWithWebClientResponseException() { + // Given + WebClientResponseException exception = WebClientResponseException.create( + 404, "Not Found", null, null, null); + + // When + int errorCode = converter.onError(exception); + + // Then + Assertions.assertEquals(404, errorCode); + } + + @Test + void testOnErrorWithCircuitBreakerStatusCodeException() { + // When + int errorCode = converter.onError(new RuntimeException("test")); + + // Then + Assertions.assertEquals(-1, errorCode); + } + + @Test + void testOnErrorWithUnknownException() { + // Given + RuntimeException exception = new RuntimeException("Unknown error"); + + // When + int errorCode = converter.onError(exception); + + // Then + Assertions.assertEquals(-1, errorCode); + } + + @Test + void testCheckClassExist() throws Exception { + // Given + Method checkClassExist = PolarisResultToErrorCode.class.getDeclaredMethod("checkClassExist", String.class); + checkClassExist.setAccessible(true); + + PolarisResultToErrorCode converter = new PolarisResultToErrorCode(); + + // test exist class + boolean result1 = (boolean) checkClassExist.invoke(converter, "java.lang.String"); + Assertions.assertTrue(result1); + + // test not exist class + boolean result2 = (boolean) checkClassExist.invoke(converter, "com.nonexistent.Class"); + Assertions.assertFalse(result2); + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfigurationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfigurationTest.java index fc08e4c27..517ad9e86 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfigurationTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfigurationTest.java @@ -25,7 +25,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; @@ -46,14 +45,6 @@ public class PolarisCircuitBreakerAutoConfigurationTest { PolarisCircuitBreakerAutoConfiguration.class)) .withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true"); - private final ApplicationContextRunner reactiveContextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of( - PolarisContextAutoConfiguration.class, - RpcEnhancementAutoConfiguration.class, - LoadBalancerAutoConfiguration.class, - ReactivePolarisCircuitBreakerAutoConfiguration.class)) - .withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true"); - @Test public void testDefaultInitialization() { this.contextRunner.run(context -> { @@ -64,13 +55,4 @@ public void testDefaultInitialization() { }); } - @Test - public void testReactiveInitialization() { - this.reactiveContextRunner.run(context -> { - assertThat(context).hasSingleBean(ReactivePolarisCircuitBreakerAutoConfiguration.class); - assertThat(context).hasSingleBean(ReactiveCircuitBreakerFactory.class); - assertThat(context).hasSingleBean(CircuitBreakerConfigModifier.class); - }); - } - } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfigurationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfigurationTest.java index 1b0e900ff..8b704de81 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfigurationTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfigurationTest.java @@ -54,7 +54,6 @@ public void testDefaultInitialization() { this.contextRunner.run(context -> { assertThat(context).hasSingleBean(PolarisCircuitBreakerAutoConfiguration.class); assertThat(context).hasSingleBean(PolarisCircuitBreakerFeignClientAutoConfiguration.class); - assertThat(context).hasSingleBean(ReactivePolarisCircuitBreakerAutoConfiguration.class); }); } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisCircuitBreakerFeignIntegrationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisCircuitBreakerFeignIntegrationTest.java deleted file mode 100644 index ff6a25e0b..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisCircuitBreakerFeignIntegrationTest.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.instrument.feign; - - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.InvocationTargetException; -import java.nio.charset.StandardCharsets; -import java.util.stream.Collectors; - -import com.google.protobuf.util.JsonFormat; -import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration; -import com.tencent.cloud.polaris.context.PolarisSDKContextManager; -import com.tencent.polaris.api.pojo.ServiceKey; -import com.tencent.polaris.client.util.Utils; -import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto; -import com.tencent.polaris.test.mock.discovery.NamingServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException; -import org.springframework.cloud.openfeign.EnableFeignClients; -import org.springframework.cloud.openfeign.FallbackFactory; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; - -/** - * @author sean yu - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = RANDOM_PORT, - classes = PolarisCircuitBreakerFeignIntegrationTest.TestConfig.class, - properties = { - "spring.cloud.gateway.enabled=false", - "feign.circuitbreaker.enabled=true", - "spring.cloud.polaris.address=grpc://127.0.0.1:10081", - "spring.cloud.openfeign.circuitbreaker.enabled=true", - "spring.cloud.polaris.namespace=" + NAMESPACE_TEST, - "spring.cloud.polaris.service=test" - }) -public class PolarisCircuitBreakerFeignIntegrationTest { - - private static final String TEST_SERVICE_NAME = "test-service-callee"; - - private static NamingServer namingServer; - - @Autowired - private EchoService echoService; - - @Autowired - private FooService fooService; - - @Autowired - private BarService barService; - - @Autowired - private BazService bazService; - - @BeforeAll - static void beforeAll() throws Exception { - PolarisSDKContextManager.innerDestroy(); - namingServer = NamingServer.startNamingServer(10081); - ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, TEST_SERVICE_NAME); - - CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder(); - InputStream inputStream = PolarisCircuitBreakerFeignIntegrationTest.class.getClassLoader() - .getResourceAsStream("circuitBreakerRule.json"); - String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines() - .collect(Collectors.joining("")); - JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder); - CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build(); - CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder() - .addRules(circuitBreakerRule).build(); - namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker); - } - - @AfterAll - static void afterAll() { - if (null != namingServer) { - namingServer.terminate(); - } - } - - @Test - public void contextLoads() { - assertThat(echoService).isNotNull(); - assertThat(fooService).isNotNull(); - } - - @Test - public void testFeignClient() throws InvocationTargetException { - assertThat(echoService.echo("test")).isEqualTo("echo fallback"); - Utils.sleepUninterrupted(2000); - assertThatThrownBy(() -> { - echoService.echo(null); - }).isInstanceOf(InvocationTargetException.class); - assertThatThrownBy(() -> { - fooService.echo("test"); - }).isInstanceOf(NoFallbackAvailableException.class); - Utils.sleepUninterrupted(2000); - assertThat(barService.bar()).isEqualTo("\"fallback from polaris server\""); - Utils.sleepUninterrupted(2000); - assertThat(bazService.baz()).isEqualTo("\"fallback from polaris server\""); - assertThat(fooService.toString()).isNotEqualTo(echoService.toString()); - assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode()); - assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE); - } - - @FeignClient(value = TEST_SERVICE_NAME, contextId = "1", fallback = EchoServiceFallback.class) - @Primary - public interface EchoService { - - @RequestMapping(path = "echo/{str}") - String echo(@RequestParam("str") String param) throws InvocationTargetException; - - } - - @FeignClient(value = TEST_SERVICE_NAME, contextId = "2", fallbackFactory = CustomFallbackFactory.class) - public interface FooService { - - @RequestMapping("echo/{str}") - String echo(@RequestParam("str") String param); - - } - - @FeignClient(value = TEST_SERVICE_NAME, contextId = "3") - public interface BarService { - - @RequestMapping(path = "bar") - String bar(); - - } - - public interface BazService { - - @RequestMapping(path = "baz") - String baz(); - - } - - @FeignClient(value = TEST_SERVICE_NAME, contextId = "4") - public interface BazClient extends BazService { - - } - - @Configuration - @EnableAutoConfiguration - @ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class}) - @EnableFeignClients - public static class TestConfig { - - @Bean - public EchoServiceFallback echoServiceFallback() { - return new EchoServiceFallback(); - } - - @Bean - public CustomFallbackFactory customFallbackFactory() { - return new CustomFallbackFactory(); - } - } - - public static class EchoServiceFallback implements EchoService { - - @Override - public String echo(@RequestParam("str") String param) throws InvocationTargetException { - if (param == null) { - throw new InvocationTargetException(new Exception(), "test InvocationTargetException"); - } - return "echo fallback"; - } - - } - - public static class FooServiceFallback implements FooService { - - @Override - public String echo(@RequestParam("str") String param) { - throw new NoFallbackAvailableException("fallback", new RuntimeException()); - } - - } - - public static class CustomFallbackFactory - implements FallbackFactory { - - private final FooService fooService = new FooServiceFallback(); - - @Override - public FooService create(Throwable throwable) { - return fooService; - } - - } -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandlerTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandlerTest.java new file mode 100644 index 000000000..e10030302 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerInvocationHandlerTest.java @@ -0,0 +1,216 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.instrument.feign; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import com.tencent.cloud.polaris.circuitbreaker.exception.FallbackWrapperException; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; +import feign.InvocationHandlerFactory; +import feign.Target; +import feign.codec.Decoder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.openfeign.FallbackFactory; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for ${@link PolarisFeignCircuitBreakerInvocationHandler}. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +class PolarisFeignCircuitBreakerInvocationHandlerTest { + + @Mock + private Target target; + + @Mock + private InvocationHandlerFactory.MethodHandler methodHandler; + + @Mock + private FallbackFactory fallbackFactory; + + @Mock + private Decoder decoder; + + private Map dispatch; + private PolarisFeignCircuitBreakerInvocationHandler handler; + + private Method testMethod; + + @BeforeEach + void setUp() throws Exception { + dispatch = new HashMap<>(); + + testMethod = TestInterface.class.getDeclaredMethod("testMethod"); + dispatch.put(testMethod, methodHandler); + + handler = new PolarisFeignCircuitBreakerInvocationHandler( + target, + dispatch, + fallbackFactory, + decoder + ); + } + + @Test + void testConstructorWithNullTarget() { + Assertions.assertThrows(NullPointerException.class, () -> + new PolarisFeignCircuitBreakerInvocationHandler( + null, dispatch, fallbackFactory, decoder + ) + ); + } + + @Test + void testConstructorWithNullDispatch() { + Assertions.assertThrows(NullPointerException.class, () -> + new PolarisFeignCircuitBreakerInvocationHandler( + target, null, fallbackFactory, decoder + ) + ); + } + + @Test + void testToFallbackMethod() throws Exception { + Method method = TestInterface.class.getMethod("testMethod"); + Map testDispatch = new HashMap<>(); + testDispatch.put(method, methodHandler); + + Map result = PolarisFeignCircuitBreakerInvocationHandler.toFallbackMethod(testDispatch); + + Assertions.assertNotNull(result); + Assertions.assertTrue(result.containsKey(method)); + Assertions.assertEquals(method, result.get(method)); + } + + @Test + void testEqualsMethod() throws Throwable { + Method equalsMethod = Object.class.getMethod("equals", Object.class); + Object mockProxy = mock(Object.class); + + // Test equals with null + Assertions.assertFalse((Boolean) handler.invoke(mockProxy, equalsMethod, new Object[] {null})); + + // Test equals with non-proxy object + Assertions.assertFalse((Boolean) handler.invoke(mockProxy, equalsMethod, new Object[] {new Object()})); + } + + @Test + void testToStringMethod() throws Throwable { + Method toStringMethod = Object.class.getMethod("toString"); + Object mockProxy = mock(Object.class); + when(target.toString()).thenReturn("TestTarget"); + + Assertions.assertEquals("TestTarget", handler.invoke(mockProxy, toStringMethod, null)); + } + + @Test + void testCustomFallbackFactoryWithFallbackError() throws Throwable { + // Arrange + handler = new PolarisFeignCircuitBreakerInvocationHandler(target, dispatch, fallbackFactory, decoder); + Exception originalException = new RuntimeException("Original error"); + + when(methodHandler.invoke(any())).thenThrow(originalException); + TestImpl testImpl = new TestImpl(); + when(fallbackFactory.create(any())).thenReturn(testImpl); + + // Act + assertThatThrownBy(() -> handler.invoke(null, testMethod, new Object[] {})).isInstanceOf(FallbackWrapperException.class); + } + + @Test + void testCustomFallbackFactoryWithFallbackError2() throws Throwable { + // Arrange + handler = new PolarisFeignCircuitBreakerInvocationHandler(target, dispatch, fallbackFactory, decoder); + Exception originalException = new RuntimeException("Original error"); + + when(methodHandler.invoke(any())).thenThrow(originalException); + TestImpl2 testImpl = new TestImpl2(); + when(fallbackFactory.create(any())).thenReturn(testImpl); + + // Act + assertThatThrownBy(() -> handler.invoke(null, testMethod, new Object[] {})). + isInstanceOf(RuntimeException.class).hasMessage("test"); + } + + @Test + void testDefaultFallbackCreation() throws Throwable { + // Arrange + handler = new PolarisFeignCircuitBreakerInvocationHandler(target, dispatch, null, decoder); + CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(200, new HashMap<>(), "mock body"); + CallAbortedException originalException = new CallAbortedException("test rule", fallbackInfo); + + Object expected = new Object(); + when(methodHandler.invoke(any())).thenThrow(originalException); + when(decoder.decode(any(), any())).thenReturn(expected); + + // Act + Object result = handler.invoke(null, testMethod, new Object[] {}); + + // Verify + Assertions.assertEquals(expected, result); + } + + @Test + void testEquals() { + PolarisFeignCircuitBreakerInvocationHandler testHandler = new PolarisFeignCircuitBreakerInvocationHandler( + target, + dispatch, + fallbackFactory, + decoder + ); + + Assertions.assertEquals(handler, testHandler); + Assertions.assertEquals(handler.hashCode(), testHandler.hashCode()); + } + + interface TestInterface { + String testMethod() throws InvocationTargetException; + } + + static class TestImpl implements TestInterface { + + @Override + public String testMethod() throws InvocationTargetException { + throw new InvocationTargetException(new RuntimeException("test")); + } + } + + static class TestImpl2 implements TestInterface { + + @Override + public String testMethod() throws InvocationTargetException { + throw new RuntimeException("test"); + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeterTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeterTest.java index 55acbce5a..1f5067607 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeterTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTargeterTest.java @@ -27,6 +27,7 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; +import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.cloud.openfeign.FeignContext; @@ -48,15 +49,21 @@ public class PolarisFeignCircuitBreakerTargeterTest { @Mock CircuitBreakerNameResolver circuitBreakerNameResolver; + @Mock + FeignContext feignContext; + + @Mock + private FeignClientFactoryBean factory; + @Test public void testTarget() { - PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); + PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(); targeter.target(new FeignClientFactoryBean(), new Feign.Builder(), new FeignContext(), new Target.HardCodedTarget<>(TestApi.class, "/test")); } @Test public void testTarget2() { - PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); + PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(); FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); doReturn(TestApi.class).when(feignClientFactoryBean).getFallback(); doReturn("test").when(feignClientFactoryBean).getName(); @@ -69,7 +76,7 @@ public void testTarget2() { @Test public void testTarget3() { - PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); + PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(); FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); doReturn(void.class).when(feignClientFactoryBean).getFallback(); doReturn(TestApi.class).when(feignClientFactoryBean).getFallbackFactory(); @@ -81,9 +88,63 @@ public void testTarget3() { }).isInstanceOf(IllegalStateException.class); } + @Test + public void testTarget4() { + PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(); + FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); + // no fallback and no fallback factory + doReturn(void.class).when(feignClientFactoryBean).getFallback(); + doReturn(void.class).when(feignClientFactoryBean).getFallbackFactory(); + doReturn("test").when(feignClientFactoryBean).getName(); + + targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignContext, new Target.HardCodedTarget<>(TestApi.class, "/test")); + } + + @Test + public void testTargetWithFallbackFactory() { + PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(); + FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); + + doReturn(void.class).when(feignClientFactoryBean).getFallback(); + doReturn(PolarisCircuitBreakerFallbackFactory.class).when(feignClientFactoryBean).getFallbackFactory(); + doReturn("test").when(feignClientFactoryBean).getName(); + + doReturn(new PolarisCircuitBreakerFallbackFactory()).when(feignContext).getInstance("test", PolarisCircuitBreakerFallbackFactory.class); + + targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignContext, new Target.HardCodedTarget<>(TestApi.class, "/test")); + } + + @Test + public void testTargetWithFallback() { + PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(); + FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); + + doReturn(TestApiFallback.class).when(feignClientFactoryBean).getFallback(); + doReturn("test").when(feignClientFactoryBean).getName(); + + + doReturn(new PolarisCircuitBreakerFallbackFactory()).when(feignContext).getInstance("test", TestApiFallback.class); + + targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignContext, new Target.HardCodedTarget<>(TestApi.class, "/test")); + } + interface TestApi { @RequestLine("GET /test") void test(); } + static class TestApiFallback implements TestApi { + @Override + public void test() { + // fallback implementation + } + } + + public static class PolarisCircuitBreakerFallbackFactory implements FallbackFactory { + @Override + public TestApi create(Throwable cause) { + return new TestApiFallback(); + } + } + } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTest.java new file mode 100644 index 000000000..881eea705 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/feign/PolarisFeignCircuitBreakerTest.java @@ -0,0 +1,133 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.instrument.feign; + +import feign.Feign; +import feign.RequestLine; +import feign.Target; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.openfeign.FallbackFactory; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +/** + * Tests for {@link PolarisFeignCircuitBreaker}. + */ +public class PolarisFeignCircuitBreakerTest { + + private PolarisFeignCircuitBreaker.Builder builder; + + @BeforeEach + public void setUp() { + builder = PolarisFeignCircuitBreaker.builder(); + } + + @Test + public void testBuilderNotNull() { + Assertions.assertNotNull(builder); + } + + @Test + public void testTargetWithFallback() { + // Mock the target + Class targetType = MyService.class; + String name = "myService"; + Target target = mock(Target.class); + + // mock return value + when(target.type()).thenReturn(targetType); + when(target.name()).thenReturn(name); + + // Mock the fallback + MyService fallback = mock(MyService.class); + when(fallback.sayHello()).thenReturn("Fallback Hello"); + + // Call the target method + MyService result = builder.target(target, fallback); + + // Verify that the result is not null and the fallback factory is used + Assertions.assertNotNull(result); + Assertions.assertEquals("Fallback Hello", result.sayHello()); + } + + @Test + public void testTargetWithFallbackFactory() { + // Mock the target and fallback factory + Class targetType = MyService.class; + String name = "myService"; + Target target = mock(Target.class); + + // mock return value + when(target.type()).thenReturn(targetType); + when(target.name()).thenReturn(name); + + FallbackFactory fallbackFactory = mock(FallbackFactory.class); + + // Mock the fallback from the factory + MyService fallback = mock(MyService.class); + when(fallback.sayHello()).thenReturn("Fallback Hello"); + when(fallbackFactory.create(any())).thenReturn(fallback); + + // Call the target method + MyService result = builder.target(target, fallbackFactory); + + // Verify that the result is not null and the fallback factory is used + Assertions.assertNotNull(result); + Assertions.assertEquals("Fallback Hello", result.sayHello()); + } + + @Test + public void testTargetWithoutFallback() { + // Mock the target + Class targetType = MyService.class; + String name = "myService"; + Target target = mock(Target.class); + + // mock return value + when(target.type()).thenReturn(targetType); + when(target.name()).thenReturn(name); + + // Call the target method + MyService result = builder.target(target); + + // Verify that the result is not null + Assertions.assertNotNull(result); + // Additional verifications can be added here based on the implementation + } + + @Test + public void testBuildWithNullableFallbackFactory() { + // Call the build method with a null fallback factory + Feign feign = builder.build(null); + + // Verify that the Feign instance is not null + Assertions.assertNotNull(feign); + // Additional verifications can be added here based on the implementation + } + + public interface MyService { + @RequestLine("GET /hello") + String sayHello(); + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/gateway/PolarisCircuitBreakerGatewayIntegrationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/gateway/PolarisCircuitBreakerGatewayIntegrationTest.java deleted file mode 100644 index 6fbad2e16..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/gateway/PolarisCircuitBreakerGatewayIntegrationTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.instrument.gateway; - - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - -import com.google.protobuf.util.JsonFormat; -import com.tencent.cloud.polaris.context.PolarisSDKContextManager; -import com.tencent.polaris.api.pojo.ServiceKey; -import com.tencent.polaris.client.util.Utils; -import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto; -import com.tencent.polaris.test.mock.discovery.NamingServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; -import org.springframework.cloud.gateway.route.RouteLocator; -import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; -import static org.assertj.core.api.Assertions.assertThat; - - -/** - * @author sean yu - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest( - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = PolarisCircuitBreakerGatewayIntegrationTest.TestApplication.class -) -@ActiveProfiles("test-gateway") -@AutoConfigureWebTestClient(timeout = "1000000") -public class PolarisCircuitBreakerGatewayIntegrationTest { - - private static final String TEST_SERVICE_NAME = "test-service-callee"; - - private static NamingServer namingServer; - - @Autowired - private WebTestClient webClient; - - @Autowired - private ApplicationContext applicationContext; - - @BeforeAll - static void beforeAll() throws Exception { - PolarisSDKContextManager.innerDestroy(); - namingServer = NamingServer.startNamingServer(10081); - ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, TEST_SERVICE_NAME); - - CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder(); - InputStream inputStream = PolarisCircuitBreakerGatewayIntegrationTest.class.getClassLoader() - .getResourceAsStream("circuitBreakerRule.json"); - String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines() - .collect(Collectors.joining("")); - JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder); - CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build(); - CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder() - .addRules(circuitBreakerRule).build(); - namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker); - } - - @AfterAll - static void afterAll() { - if (null != namingServer) { - namingServer.terminate(); - } - } - - @Test - public void fallback() throws Exception { - SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config(); - applicationContext.getBean(PolarisCircuitBreakerFilterFactory.class).apply(config).toString(); - - webClient - .get().uri("/err") - .header("Host", "www.circuitbreaker.com") - .exchange() - .expectStatus().isOk() - .expectBody() - .consumeWith( - response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes())); - - Utils.sleepUninterrupted(2000); - - webClient - .get().uri("/err-skip-fallback") - .header("Host", "www.circuitbreaker-skip-fallback.com") - .exchange() - .expectStatus(); - - Utils.sleepUninterrupted(2000); - - // this should be 200, but for some unknown reason, GitHub action run failed in windows, so we skip this check - webClient - .get().uri("/err-skip-fallback") - .header("Host", "www.circuitbreaker-skip-fallback.com") - .exchange() - .expectStatus(); - - Utils.sleepUninterrupted(2000); - - webClient - .get().uri("/err-no-fallback") - .header("Host", "www.circuitbreaker-no-fallback.com") - .exchange() - .expectStatus(); - - Utils.sleepUninterrupted(2000); - - webClient - .get().uri("/err-no-fallback") - .header("Host", "www.circuitbreaker-no-fallback.com") - .exchange() - .expectStatus(); - - Utils.sleepUninterrupted(2000); - - webClient - .get().uri("/err-no-fallback") - .header("Host", "www.circuitbreaker-no-fallback.com") - .exchange() - .expectStatus(); - } - - - @Configuration - @EnableAutoConfiguration - public static class TestApplication { - - @Bean - public RouteLocator myRoutes(RouteLocatorBuilder builder) { - Set codeSets = new HashSet<>(); - codeSets.add("4**"); - codeSets.add("5**"); - return builder.routes() - .route(p -> p - .host("*.circuitbreaker.com") - .filters(f -> f - .circuitBreaker(config -> config - .setStatusCodes(codeSets) - .setFallbackUri("forward:/fallback") - .setName(TEST_SERVICE_NAME) - )) - .uri("http://httpbin.org:80")) - .route(p -> p - .host("*.circuitbreaker-skip-fallback.com") - .filters(f -> f - .circuitBreaker(config -> config - .setStatusCodes(Collections.singleton("5**")) - .setName(TEST_SERVICE_NAME) - )) - .uri("http://httpbin.org:80")) - .route(p -> p - .host("*.circuitbreaker-no-fallback.com") - .filters(f -> f - .circuitBreaker(config -> config - .setName(TEST_SERVICE_NAME) - )) - .uri("lb://" + TEST_SERVICE_NAME)) - .build(); - } - - @RestController - static class Controller { - @RequestMapping("/fallback") - public Mono fallback() { - return Mono.just("fallback"); - } - } - - } -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponseTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponseTest.java new file mode 100644 index 000000000..f0d00219a --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerHttpResponseTest.java @@ -0,0 +1,122 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests for {@link PolarisCircuitBreakerHttpResponse}. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +public class PolarisCircuitBreakerHttpResponseTest { + @Test + void testConstructorWithCodeOnly() { + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(200); + + Assertions.assertEquals(200, response.getRawStatusCode()); + Assertions.assertNotNull(response.getHeaders()); + Assertions.assertTrue(response.getHeaders().isEmpty()); + Assertions.assertNull(response.getBody()); + } + + @Test + void testConstructorWithCodeAndBody() { + String body = "test body"; + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(200, body); + + Assertions.assertEquals(200, response.getRawStatusCode()); + Assertions.assertNotNull(response.getHeaders()); + Assertions.assertTrue(response.getHeaders().isEmpty()); + Assertions.assertNotNull(response.getBody()); + } + + @Test + void testConstructorWithCodeHeadersAndBody() { + String body = "test body"; + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("Authorization", "Bearer token"); + + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(200, headers, body); + + Assertions.assertEquals(200, response.getRawStatusCode()); + Assertions.assertNotNull(response.getHeaders()); + Assertions.assertEquals(2, response.getHeaders().size()); + Assertions.assertTrue(response.getHeaders().containsKey("Content-Type")); + Assertions.assertTrue(response.getHeaders().containsKey("Authorization")); + Assertions.assertNotNull(response.getBody()); + } + + @Test + void testConstructorWithFallbackInfo() { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(200, headers, "test body"); + + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(fallbackInfo); + + Assertions.assertEquals(200, response.getRawStatusCode()); + Assertions.assertEquals(fallbackInfo, response.getFallbackInfo()); + Assertions.assertNotNull(response.getHeaders()); + Assertions.assertTrue(response.getHeaders().containsKey("Content-Type")); + Assertions.assertNotNull(response.getBody()); + } + + @Test + void testGetStatusTextWithValidHttpStatus() { + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(200); + Assertions.assertEquals("OK", response.getStatusText()); + } + + @Test + void testGetStatusTextWithInvalidHttpStatus() { + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(999); + Assertions.assertEquals("", response.getStatusText()); + } + + @Test + void testClose() { + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(200, "test body"); + InputStream body = response.getBody(); + Assertions.assertNotNull(body); + + response.close(); + + // Verify that reading from closed stream throws exception + Assertions.assertDoesNotThrow(() -> body.read()); + } + + @Test + void testCloseWithNullBody() { + PolarisCircuitBreakerHttpResponse response = new PolarisCircuitBreakerHttpResponse(200); + Assertions.assertNull(response.getBody()); + + // Should not throw exception when closing null body + Assertions.assertDoesNotThrow(() -> response.close()); + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateIntegrationTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateIntegrationTest.java deleted file mode 100644 index f21fc754a..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisCircuitBreakerRestTemplateIntegrationTest.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.stream.Collectors; - -import com.google.protobuf.util.JsonFormat; -import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration; -import com.tencent.cloud.polaris.context.PolarisSDKContextManager; -import com.tencent.polaris.api.pojo.ServiceKey; -import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto; -import com.tencent.polaris.test.mock.discovery.NamingServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.cloud.openfeign.EnableFeignClients; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.client.ExpectedCount; -import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.DefaultUriBuilderFactory; - -import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; - -/** - * @author sean yu - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = RANDOM_PORT, - classes = PolarisCircuitBreakerRestTemplateIntegrationTest.TestConfig.class, - properties = { - "spring.cloud.gateway.enabled=false", - "spring.cloud.polaris.address=grpc://127.0.0.1:10081", - "feign.circuitbreaker.enabled=true", - "spring.cloud.polaris.namespace=" + NAMESPACE_TEST, - "spring.cloud.polaris.service=test" - }) -public class PolarisCircuitBreakerRestTemplateIntegrationTest { - - private static final String TEST_SERVICE_NAME = "test-service-callee"; - - private static NamingServer namingServer; - - @Autowired - @Qualifier("defaultRestTemplate") - private RestTemplate defaultRestTemplate; - - @Autowired - @Qualifier("restTemplateFallbackFromPolaris") - private RestTemplate restTemplateFallbackFromPolaris; - - @BeforeAll - static void beforeAll() throws Exception { - PolarisSDKContextManager.innerDestroy(); - namingServer = NamingServer.startNamingServer(10081); - ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, TEST_SERVICE_NAME); - CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder(); - InputStream inputStream = PolarisCircuitBreakerRestTemplateIntegrationTest.class.getClassLoader() - .getResourceAsStream("circuitBreakerRule.json"); - String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines() - .collect(Collectors.joining("")); - JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder); - CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build(); - CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder() - .addRules(circuitBreakerRule).build(); - namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker); - } - - @AfterAll - static void afterAll() { - if (null != namingServer) { - namingServer.terminate(); - } - } - - @Test - public void testRestTemplate() throws URISyntaxException { - MockRestServiceServer mockServer = MockRestServiceServer.createServer(defaultRestTemplate); - mockServer - .expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info"))) - .andExpect(method(HttpMethod.GET)) - .andRespond(withStatus(HttpStatus.OK).body("OK")); - assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("OK"); - mockServer.verify(); - mockServer.reset(); - HttpHeaders headers = new HttpHeaders(); - mockServer - .expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info"))) - .andExpect(method(HttpMethod.GET)) - .andRespond(withStatus(HttpStatus.BAD_GATEWAY).headers(headers).body("BAD_GATEWAY")); - assertThatThrownBy(() -> { - defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class); - }).isInstanceOf(HttpServerErrorException.class); - mockServer.verify(); - mockServer.reset(); - assertThatThrownBy(() -> { - restTemplateFallbackFromPolaris.getForObject("/example/service/b/info", String.class); - }).isInstanceOf(IllegalStateException.class); - assertThat(restTemplateFallbackFromPolaris.getForObject("/example/service/b/info", String.class)).isEqualTo("\"fallback from polaris server\""); - } - - @Configuration - @EnableAutoConfiguration - @ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class}) - @EnableFeignClients - public static class TestConfig { - - @Bean - public RestTemplate defaultRestTemplate() { - return new RestTemplate(); - } - - @Bean - @LoadBalanced - public RestTemplate restTemplateFallbackFromPolaris() { - DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME); - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setUriTemplateHandler(uriBuilderFactory); - return restTemplate; - } - - @RestController - @RequestMapping("/example/service/b") - public class ServiceBController { - - /** - * Get service information. - * - * @return service information - */ - @GetMapping("/info") - public String info() { - return "hello world ! I'm a service B1"; - } - - } - - } -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptorTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptorTest.java new file mode 100644 index 000000000..5e6e608ef --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptorTest.java @@ -0,0 +1,126 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate; + +import java.io.IOException; +import java.net.URI; +import java.util.Objects; + +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for ${@link PolarisLoadBalancerInterceptor}. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +class PolarisLoadBalancerInterceptorTest { + + @Mock + private LoadBalancerClient loadBalancer; + + @Mock + private LoadBalancerRequestFactory requestFactory; + + @Mock + private EnhancedPluginRunner enhancedPluginRunner; + + @Mock + private HttpRequest request; + + @Mock + private ClientHttpRequestExecution execution; + + private PolarisLoadBalancerInterceptor interceptor; + private byte[] body; + + @BeforeEach + void setUp() { + body = "test body".getBytes(); + } + + @Test + void testInterceptWithEnhancedPlugin() throws IOException { + // Arrange + ClientHttpResponse mockResponse = mock(ClientHttpResponse.class); + interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, enhancedPluginRunner); + URI uri = URI.create("http://test-service/path"); + when(request.getURI()).thenReturn(uri); + when(loadBalancer.execute(any(), any())).thenReturn(mockResponse); + + // Act + ClientHttpResponse response = interceptor.intercept(request, body, execution); + + // Assert + Assertions.assertTrue(Objects.equals(mockResponse, response) || response instanceof PolarisCircuitBreakerHttpResponse); + } + + @Test + void testInterceptWithoutEnhancedPlugin() throws IOException { + // Arrange + ClientHttpResponse mockResponse = mock(ClientHttpResponse.class); + interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, null); + URI uri = URI.create("http://test-service/path"); + when(request.getURI()).thenReturn(uri); + when(loadBalancer.execute(any(), any())).thenReturn(mockResponse); + + // Act + ClientHttpResponse response = interceptor.intercept(request, body, execution); + + // Assert + Assertions.assertEquals(mockResponse, response); + } + + @Test + void testInterceptWithInvalidUri() { + // Arrange + interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, enhancedPluginRunner); + when(request.getURI()).thenReturn(URI.create("http:///path")); // Invalid URI without host + + // Act & Assert + Exception exception = Assertions.assertThrows(IllegalStateException.class, () -> { + interceptor.intercept(request, body, execution); + }); + Assertions.assertTrue(exception.getMessage().contains("Request URI does not contain a valid hostname")); + } + + @Test + void testConstructor() { + // Act + interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, enhancedPluginRunner); + + // Assert + Assertions.assertNotNull(interceptor); + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/CircuitBreakerPluginTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/CircuitBreakerPluginTest.java new file mode 100644 index 000000000..9e8600d4e --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/CircuitBreakerPluginTest.java @@ -0,0 +1,217 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.circuitbreaker.reporter; + +import java.net.URI; +import java.util.HashMap; + +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker; +import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; +import com.tencent.polaris.api.core.ConsumerAPI; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; +import com.tencent.polaris.circuitbreak.api.InvokeHandler; +import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; +import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * CircuitBreakerPluginTest. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +public class CircuitBreakerPluginTest { + + private static MockedStatic mockedApplicationContextAwareUtils; + @InjectMocks + private CircuitBreakerPlugin circuitBreakerPlugin; + @Mock + private CircuitBreakAPI circuitBreakAPI; + @Mock + private CircuitBreakerFactory circuitBreakerFactory; + + @Mock + private ConsumerAPI consumerAPI; + + + @BeforeAll + static void beforeAll() { + mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); + mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) + .thenReturn("unit-test"); + } + + @AfterAll + static void afterAll() { + mockedApplicationContextAwareUtils.close(); + } + + @BeforeEach + void setUp() { + MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST; + MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER; + } + + @Test + public void testGetName() { + assertThat(circuitBreakerPlugin.getName()).isEqualTo(CircuitBreakerPlugin.class.getName()); + } + + @Test + public void testType() { + assertThat(circuitBreakerPlugin.getType()).isEqualTo(EnhancedPluginType.Client.PRE); + } + + @Test + public void testRun() throws Throwable { + when(circuitBreakAPI.makeInvokeHandler(any())).thenReturn(new MockInvokeHandler()); + + PolarisCircuitBreakerConfigBuilder polarisCircuitBreakerConfigBuilder = new PolarisCircuitBreakerConfigBuilder(); + PolarisCircuitBreaker polarisCircuitBreaker = new PolarisCircuitBreaker(polarisCircuitBreakerConfigBuilder.build(), consumerAPI, circuitBreakAPI); + when(circuitBreakerFactory.create(anyString())).thenReturn(polarisCircuitBreaker); + + + EnhancedPluginContext pluginContext = new EnhancedPluginContext(); + EnhancedRequestContext request = EnhancedRequestContext.builder() + .httpMethod(HttpMethod.GET) + .url(URI.create("http://0.0.0.0/")) + .httpHeaders(new HttpHeaders()) + .build(); + EnhancedResponseContext response = EnhancedResponseContext.builder() + .httpStatus(200) + .build(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setServiceId(SERVICE_PROVIDER); + + pluginContext.setRequest(request); + pluginContext.setResponse(response); + pluginContext.setTargetServiceInstance(serviceInstance, null); + pluginContext.setThrowable(new RuntimeException()); + + assertThatThrownBy(() -> circuitBreakerPlugin.run(pluginContext)).isExactlyInstanceOf(CallAbortedException.class); + circuitBreakerPlugin.getOrder(); + circuitBreakerPlugin.getName(); + circuitBreakerPlugin.getType(); + } + + @Test + public void testRun2() throws Throwable { + when(circuitBreakAPI.makeInvokeHandler(any())).thenReturn(new MockInvokeHandler2()); + + PolarisCircuitBreakerConfigBuilder polarisCircuitBreakerConfigBuilder = new PolarisCircuitBreakerConfigBuilder(); + PolarisCircuitBreaker polarisCircuitBreaker = new PolarisCircuitBreaker(polarisCircuitBreakerConfigBuilder.build(), consumerAPI, circuitBreakAPI); + when(circuitBreakerFactory.create(anyString())).thenReturn(polarisCircuitBreaker); + + + EnhancedPluginContext pluginContext = new EnhancedPluginContext(); + EnhancedRequestContext request = EnhancedRequestContext.builder() + .httpMethod(HttpMethod.GET) + .url(URI.create("http://0.0.0.0/")) + .httpHeaders(new HttpHeaders()) + .build(); + EnhancedResponseContext response = EnhancedResponseContext.builder() + .httpStatus(200) + .build(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setServiceId(SERVICE_PROVIDER); + + pluginContext.setRequest(request); + pluginContext.setResponse(response); + pluginContext.setTargetServiceInstance(serviceInstance, null); + pluginContext.setThrowable(new RuntimeException()); + // no exception + circuitBreakerPlugin.run(pluginContext); + } + + @Test + public void testHandlerThrowable() { + // mock request + EnhancedRequestContext request = mock(EnhancedRequestContext.class); + // mock response + EnhancedResponseContext response = mock(EnhancedResponseContext.class); + + EnhancedPluginContext context = new EnhancedPluginContext(); + context.setRequest(request); + context.setResponse(response); + circuitBreakerPlugin.handlerThrowable(context, new RuntimeException("Mock exception.")); + } + + static class MockInvokeHandler implements InvokeHandler { + @Override + public void acquirePermission() { + throw new CallAbortedException("mock", new CircuitBreakerStatus.FallbackInfo(0, new HashMap<>(), "")); + } + + @Override + public void onSuccess(InvokeContext.ResponseContext responseContext) { + + } + + @Override + public void onError(InvokeContext.ResponseContext responseContext) { + + } + } + + static class MockInvokeHandler2 implements InvokeHandler { + @Override + public void acquirePermission() { + return; + } + + @Override + public void onSuccess(InvokeContext.ResponseContext responseContext) { + + } + + @Override + public void onError(InvokeContext.ResponseContext responseContext) { + + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporterTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporterTest.java index 4f2382c9f..487978f8f 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporterTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/ExceptionCircuitBreakerReporterTest.java @@ -19,8 +19,11 @@ import java.net.URI; +import com.tencent.cloud.common.constant.ContextConstant; import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; @@ -28,6 +31,7 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.client.api.SDKContext; +import com.tencent.polaris.metadata.core.MetadataType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -69,6 +73,8 @@ public class ExceptionCircuitBreakerReporterTest { private ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter; @Mock private CircuitBreakAPI circuitBreakAPI; + @Mock + private PolarisCircuitBreaker polarisCircuitBreaker; @BeforeAll static void beforeAll() { @@ -142,4 +148,66 @@ public void testHandlerThrowable() { context.setResponse(response); exceptionCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception.")); } + + @Test + public void testExistCircuitBreaker() throws Throwable { + + doReturn(true).when(reporterProperties).isEnabled(); + + EnhancedPluginContext pluginContext = new EnhancedPluginContext(); + EnhancedRequestContext request = EnhancedRequestContext.builder() + .httpMethod(HttpMethod.GET) + .url(URI.create("http://0.0.0.0/")) + .build(); + EnhancedResponseContext response = EnhancedResponseContext.builder() + .httpStatus(300) + .build(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setServiceId(SERVICE_PROVIDER); + + pluginContext.setRequest(request); + pluginContext.setResponse(response); + pluginContext.setTargetServiceInstance(serviceInstance, null); + pluginContext.setThrowable(new RuntimeException()); + + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER, polarisCircuitBreaker); + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_START_TIME, System.currentTimeMillis()); + + exceptionCircuitBreakerReporter.run(pluginContext); + + response = EnhancedResponseContext.builder() + .httpStatus(500) + .build(); + pluginContext.setResponse(response); + exceptionCircuitBreakerReporter.run(pluginContext); + } + + @Test + public void testExistCircuitBreaker2() throws Throwable { + + doReturn(true).when(reporterProperties).isEnabled(); + + EnhancedPluginContext pluginContext = new EnhancedPluginContext(); + EnhancedRequestContext request = EnhancedRequestContext.builder() + .httpMethod(HttpMethod.GET) + .url(URI.create("http://0.0.0.0/")) + .build(); + EnhancedResponseContext response = EnhancedResponseContext.builder() + .httpStatus(300) + .build(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setServiceId(SERVICE_PROVIDER); + + pluginContext.setRequest(request); + pluginContext.setResponse(response); + pluginContext.setTargetServiceInstance(serviceInstance, null); + pluginContext.setThrowable(new RuntimeException()); + // not exist circuit CIRCUIT_BREAKER_START_TIME + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER, polarisCircuitBreaker); + + exceptionCircuitBreakerReporter.run(pluginContext); + } } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporterTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporterTest.java index e6a227bee..4545e7cf1 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporterTest.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/reporter/SuccessCircuitBreakerReporterTest.java @@ -17,10 +17,16 @@ package com.tencent.cloud.polaris.circuitbreaker.reporter; +import java.lang.reflect.Field; import java.net.URI; +import com.tencent.cloud.common.constant.ContextConstant; import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.StaticMetadataManager; +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; @@ -28,6 +34,7 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.client.api.SDKContext; +import com.tencent.polaris.metadata.core.MetadataType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -62,16 +69,18 @@ public class SuccessCircuitBreakerReporterTest { private static MockedStatic mockedApplicationContextAwareUtils; @Mock - private SDKContext sdkContext; - @Mock private RpcEnhancementReporterProperties reporterProperties; + @Mock + private SDKContext sdkContext; @InjectMocks private SuccessCircuitBreakerReporter successCircuitBreakerReporter; @Mock private CircuitBreakAPI circuitBreakAPI; + @Mock + private PolarisCircuitBreaker polarisCircuitBreaker; @BeforeAll - static void beforeAll() { + static void beforeAll() throws Exception { mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) .thenReturn("unit-test"); @@ -81,6 +90,12 @@ static void beforeAll() { .when(applicationContext).getBean(RpcEnhancementReporterProperties.class); mockedApplicationContextAwareUtils.when(ApplicationContextAwareUtils::getApplicationContext) .thenReturn(applicationContext); + + StaticMetadataManager metadataManager = new StaticMetadataManager(new MetadataLocalProperties(), null); + + Field field = MetadataContextHolder.class.getDeclaredField("staticMetadataManager"); + field.setAccessible(true); + field.set(null, metadataManager); } @AfterAll @@ -147,4 +162,64 @@ public void testHandlerThrowable() { context.setResponse(response); successCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception.")); } + + @Test + public void testExistCircuitBreaker() throws Throwable { + + doReturn(true).when(reporterProperties).isEnabled(); + + EnhancedPluginContext pluginContext = new EnhancedPluginContext(); + EnhancedRequestContext request = EnhancedRequestContext.builder() + .httpMethod(HttpMethod.GET) + .url(URI.create("http://0.0.0.0/")) + .build(); + EnhancedResponseContext response = EnhancedResponseContext.builder() + .httpStatus(300) + .build(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setServiceId(SERVICE_PROVIDER); + + pluginContext.setRequest(request); + pluginContext.setResponse(response); + pluginContext.setTargetServiceInstance(serviceInstance, null); + + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER, polarisCircuitBreaker); + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_START_TIME, System.currentTimeMillis()); + + successCircuitBreakerReporter.run(pluginContext); + + response = EnhancedResponseContext.builder() + .httpStatus(500) + .build(); + pluginContext.setResponse(response); + successCircuitBreakerReporter.run(pluginContext); + } + + @Test + public void testExistCircuitBreaker2() throws Throwable { + + doReturn(true).when(reporterProperties).isEnabled(); + + EnhancedPluginContext pluginContext = new EnhancedPluginContext(); + EnhancedRequestContext request = EnhancedRequestContext.builder() + .httpMethod(HttpMethod.GET) + .url(URI.create("http://0.0.0.0/")) + .build(); + EnhancedResponseContext response = EnhancedResponseContext.builder() + .httpStatus(300) + .build(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setServiceId(SERVICE_PROVIDER); + + pluginContext.setRequest(request); + pluginContext.setResponse(response); + pluginContext.setTargetServiceInstance(serviceInstance, null); + // not exist circuit CIRCUIT_BREAKER_START_TIME + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER, polarisCircuitBreaker); + + successCircuitBreakerReporter.run(pluginContext); + } } diff --git a/spring-cloud-starter-tencent-polaris-config/pom.xml b/spring-cloud-starter-tencent-polaris-config/pom.xml index dde98dbae..4e7c6d46a 100644 --- a/spring-cloud-starter-tencent-polaris-config/pom.xml +++ b/spring-cloud-starter-tencent-polaris-config/pom.xml @@ -18,6 +18,12 @@ com.tencent.cloud spring-cloud-tencent-polaris-context + + + spring-security-crypto + org.springframework.security + + @@ -66,6 +72,12 @@ org.springframework.cloud spring-cloud-context + + + spring-security-crypto + org.springframework.security + + diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java index 7a3aada30..d0f696ca6 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java @@ -105,7 +105,10 @@ private void initDataSource(ConfigurationImpl configuration) { + " with spring.cloud.polaris.address or spring.cloud.polaris.config.address"); } - checkAddressAccessible(configAddresses); + // enable close check address for unit tests + if (polarisConfigProperties.isCheckAddress()) { + checkAddressAccessible(configAddresses); + } configuration.getConfigFile().getServerConnector().setAddresses(configAddresses); diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java index 1e69443a1..3fee72002 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java @@ -19,18 +19,17 @@ import com.tencent.cloud.polaris.config.adapter.AffectedConfigurationPropertiesRebinder; import com.tencent.cloud.polaris.config.adapter.PolarisConfigPropertyRefresher; -import com.tencent.cloud.polaris.config.adapter.PolarisConfigRefreshScopeAnnotationDetector; import com.tencent.cloud.polaris.config.adapter.PolarisRefreshAffectedContextRefresher; import com.tencent.cloud.polaris.config.adapter.PolarisRefreshEntireContextRefresher; import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor; import com.tencent.cloud.polaris.config.condition.ConditionalOnReflectRefreshType; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; import com.tencent.cloud.polaris.config.listener.PolarisConfigChangeEventListener; -import com.tencent.cloud.polaris.config.listener.PolarisConfigRefreshOptimizationListener; import com.tencent.cloud.polaris.config.logger.PolarisConfigLoggerApplicationListener; import com.tencent.cloud.polaris.config.spring.annotation.SpringValueProcessor; import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import com.tencent.polaris.configuration.api.core.ConfigFileService; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -77,46 +76,38 @@ public ConfigurationPropertiesRebinder affectedConfigurationPropertiesRebinder( @Bean @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) public PolarisConfigPropertyRefresher polarisRefreshContextPropertySourceAutoRefresher( - PolarisConfigProperties polarisConfigProperties, ContextRefresher contextRefresher) { - return new PolarisRefreshEntireContextRefresher(polarisConfigProperties, contextRefresher); + PolarisConfigProperties polarisConfigProperties, SpringValueRegistry springValueRegistry, + ConfigFileService configFileService, ContextRefresher contextRefresher) { + return new PolarisRefreshEntireContextRefresher(polarisConfigProperties, + springValueRegistry, configFileService, contextRefresher); + } + + @Bean + public SpringValueRegistry springValueRegistry() { + return new SpringValueRegistry(); + } + + @Bean + public PlaceholderHelper placeholderHelper() { + return new PlaceholderHelper(); + } + + @Bean + public SpringValueProcessor springValueProcessor(PlaceholderHelper placeholderHelper, + SpringValueRegistry springValueRegistry, PolarisConfigProperties polarisConfigProperties) { + return new SpringValueProcessor(placeholderHelper, springValueRegistry, polarisConfigProperties); } @Configuration(proxyBeanMethods = false) @ConditionalOnReflectRefreshType @AutoConfigureBefore(PolarisConfigAutoConfiguration.class) public static class PolarisReflectRefresherAutoConfiguration { - @Bean - public SpringValueRegistry springValueRegistry() { - return new SpringValueRegistry(); - } - - @Bean - public PlaceholderHelper placeholderHelper() { - return new PlaceholderHelper(); - } - - @Bean - public SpringValueProcessor springValueProcessor(PlaceholderHelper placeholderHelper, - SpringValueRegistry springValueRegistry, PolarisConfigProperties polarisConfigProperties) { - return new SpringValueProcessor(placeholderHelper, springValueRegistry, polarisConfigProperties); - } - @Bean public PolarisConfigPropertyRefresher polarisReflectPropertySourceAutoRefresher( PolarisConfigProperties polarisConfigProperties, SpringValueRegistry springValueRegistry, - PlaceholderHelper placeholderHelper) { + PlaceholderHelper placeholderHelper, ConfigFileService configFileService, ContextRefresher contextRefresher) { return new PolarisRefreshAffectedContextRefresher(polarisConfigProperties, - springValueRegistry, placeholderHelper); - } - - @Bean - public PolarisConfigRefreshScopeAnnotationDetector polarisConfigRefreshScopeAnnotationDetector() { - return new PolarisConfigRefreshScopeAnnotationDetector(); - } - - @Bean - public PolarisConfigRefreshOptimizationListener polarisConfigRefreshOptimizationListener() { - return new PolarisConfigRefreshOptimizationListener(); + springValueRegistry, placeholderHelper, configFileService, contextRefresher); } } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigCustomExtensionLayer.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigCustomExtensionLayer.java index fe42df1d5..65ed06b3d 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigCustomExtensionLayer.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigCustomExtensionLayer.java @@ -36,5 +36,5 @@ public interface PolarisConfigCustomExtensionLayer { void executeAfterLocateConfigReturning(CompositePropertySource compositePropertySource); - boolean executeRegisterPublishChangeListener(PolarisPropertySource polarisPropertySource); + boolean executeRegisterPublishChangeListener(PolarisPropertySource polarisPropertySource, PolarisPropertySource effectPolarisPropertySource); } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocator.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocator.java index 4cbae71aa..a903d1f99 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocator.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocator.java @@ -31,6 +31,7 @@ import com.tencent.polaris.configuration.api.core.ConfigFileMetadata; import com.tencent.polaris.configuration.api.core.ConfigFileService; import com.tencent.polaris.configuration.api.core.ConfigKVFile; +import com.tencent.polaris.configuration.client.internal.CompositeConfigFile; import com.tencent.polaris.configuration.client.internal.DefaultConfigFileMetadata; import org.apache.commons.lang.ArrayUtils; import org.slf4j.Logger; @@ -68,6 +69,8 @@ public class PolarisConfigFileLocator implements PropertySourceLocator { // this class provides customized logic for some customers to configure special business group files private final PolarisConfigCustomExtensionLayer polarisConfigCustomExtensionLayer = PolarisServiceLoaderUtil.getPolarisConfigCustomExtensionLayer(); + private volatile static CompositePropertySource compositePropertySourceCache = null; + public PolarisConfigFileLocator(PolarisConfigProperties polarisConfigProperties, PolarisContextProperties polarisContextProperties, ConfigFileService configFileService, Environment environment) { this.polarisConfigProperties = polarisConfigProperties; @@ -76,10 +79,21 @@ public PolarisConfigFileLocator(PolarisConfigProperties polarisConfigProperties, this.environment = environment; } + /** + * order: spring boot default config files > custom config files > tsf default config group. + * @param environment The current Environment. + * @return The PropertySource to be added to the Environment. + */ @Override public PropertySource locate(Environment environment) { if (polarisConfigProperties.isEnabled()) { + // use cache when refreshing context + if (compositePropertySourceCache != null) { + return compositePropertySourceCache; + } + CompositePropertySource compositePropertySource = new CompositePropertySource(POLARIS_CONFIG_PROPERTY_SOURCE_NAME); + compositePropertySourceCache = compositePropertySource; try { // load custom config extension files initCustomPolarisConfigExtensionFiles(compositePropertySource); @@ -87,10 +101,11 @@ public PropertySource locate(Environment environment) { initInternalConfigFiles(compositePropertySource); // load custom config files List configFileGroups = polarisConfigProperties.getGroups(); - if (CollectionUtils.isEmpty(configFileGroups)) { - return compositePropertySource; + if (!CollectionUtils.isEmpty(configFileGroups)) { + initCustomPolarisConfigFiles(compositePropertySource, configFileGroups); } - initCustomPolarisConfigFiles(compositePropertySource, configFileGroups); + // load tsf default config group + initTsfConfigGroups(compositePropertySource); return compositePropertySource; } finally { @@ -123,7 +138,10 @@ private void initInternalConfigFiles(CompositePropertySource compositePropertySo List internalConfigFiles = getInternalConfigFiles(); for (ConfigFileMetadata configFile : internalConfigFiles) { - PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(configFile.getNamespace(), configFile.getFileGroup(), configFile.getFileName()); + if (StringUtils.isEmpty(configFile.getFileGroup())) { + continue; + } + PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(configFileService, configFile.getNamespace(), configFile.getFileGroup(), configFile.getFileName()); compositePropertySource.addPropertySource(polarisPropertySource); @@ -190,6 +208,29 @@ private void buildInternalBootstrapConfigFiles(List internal internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap.yaml")); } + void initTsfConfigGroups(CompositePropertySource compositePropertySource) { + String tsfId = environment.getProperty("tsf_id"); + String tsfNamespaceName = environment.getProperty("tsf_namespace_name"); + String tsfGroupName = environment.getProperty("tsf_group_name"); + + if (StringUtils.isEmpty(tsfId) || StringUtils.isEmpty(tsfNamespaceName) || StringUtils.isEmpty(tsfGroupName)) { + return; + } + String namespace = polarisContextProperties.getNamespace(); + List tsfConfigGroups = Arrays.asList( + tsfId + "." + tsfGroupName + ".application_config_group", + tsfId + "." + tsfNamespaceName + ".global_config_group"); + for (String tsfConfigGroup : tsfConfigGroups) { + PolarisPropertySource polarisPropertySource = loadGroupPolarisPropertySource(configFileService, namespace, tsfConfigGroup); + if (polarisPropertySource == null) { + // not register to polaris + continue; + } + compositePropertySource.addPropertySource(polarisPropertySource); + PolarisPropertySourceManager.addPropertySource(polarisPropertySource); + } + } + private void initCustomPolarisConfigFiles(CompositePropertySource compositePropertySource, List configFileGroups) { String namespace = polarisContextProperties.getNamespace(); @@ -201,46 +242,90 @@ private void initCustomPolarisConfigFiles(CompositePropertySource compositePrope String group = configFileGroup.getName(); if (!StringUtils.hasText(group)) { - throw new IllegalArgumentException("polaris config group name cannot be empty."); + continue; } List files = configFileGroup.getFiles(); + if (CollectionUtils.isEmpty(files)) { - return; + PolarisPropertySource polarisPropertySource = loadGroupPolarisPropertySource(configFileService, namespace, group); + if (polarisPropertySource == null) { + continue; + } + compositePropertySource.addPropertySource(polarisPropertySource); + PolarisPropertySourceManager.addPropertySource(polarisPropertySource); + LOGGER.info("[SCT Config] Load and inject polaris config file success. namespace = {}, group = {}", namespace, group); } + else { + for (String fileName : files) { + PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(configFileService, groupNamespace, group, fileName); - for (String fileName : files) { - PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(groupNamespace, group, fileName); - - compositePropertySource.addPropertySource(polarisPropertySource); + compositePropertySource.addPropertySource(polarisPropertySource); - PolarisPropertySourceManager.addPropertySource(polarisPropertySource); + PolarisPropertySourceManager.addPropertySource(polarisPropertySource); - LOGGER.info("[SCT Config] Load and inject polaris config file success. namespace = {}, group = {}, fileName = {}", groupNamespace, group, fileName); + LOGGER.info("[SCT Config] Load and inject polaris config file success. namespace = {}, group = {}, fileName = {}", groupNamespace, group, fileName); + } } } } - private PolarisPropertySource loadPolarisPropertySource(String namespace, String group, String fileName) { - ConfigKVFile configKVFile; - // unknown extension is resolved as yaml file - if (ConfigFileFormat.isYamlFile(fileName) || ConfigFileFormat.isUnknownFile(fileName)) { - configKVFile = configFileService.getConfigYamlFile(namespace, group, fileName); + public static PolarisPropertySource loadPolarisPropertySource(ConfigFileService configFileService, String namespace, String group, String fileName) { + ConfigKVFile configKVFile = loadConfigKVFile(configFileService, namespace, group, fileName); + + Map map = new ConcurrentHashMap<>(); + for (String key : configKVFile.getPropertyNames()) { + map.put(key, configKVFile.getProperty(key, null)); } - else if (ConfigFileFormat.isPropertyFile(fileName)) { - configKVFile = configFileService.getConfigPropertiesFile(namespace, group, fileName); + + return new PolarisPropertySource(namespace, group, fileName, configKVFile, map); + } + + public static PolarisPropertySource loadGroupPolarisPropertySource(ConfigFileService configFileService, String namespace, String group) { + List configKVFiles = new ArrayList<>(); + + com.tencent.polaris.configuration.api.core.ConfigFileGroup remoteGroup = configFileService.getConfigFileGroup(namespace, group); + if (remoteGroup == null) { + return null; } - else { - LOGGER.warn("[SCT Config] Unsupported config file. namespace = {}, group = {}, fileName = {}", namespace, group, fileName); - throw new IllegalStateException("Only configuration files in the format of properties / yaml / yaml" + " can be injected into the spring context"); + for (ConfigFileMetadata configFile : remoteGroup.getConfigFileMetadataList()) { + String fileName = configFile.getFileName(); + ConfigKVFile configKVFile = loadConfigKVFile(configFileService, namespace, group, fileName); + configKVFiles.add(configKVFile); } + CompositeConfigFile compositeConfigFile = new CompositeConfigFile(configKVFiles); + Map map = new ConcurrentHashMap<>(); - for (String key : configKVFile.getPropertyNames()) { - map.put(key, configKVFile.getProperty(key, null)); + for (String key : compositeConfigFile.getPropertyNames()) { + String value = compositeConfigFile.getProperty(key, null); + map.put(key, value); } - return new PolarisPropertySource(namespace, group, fileName, configKVFile, map); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("namespace='" + namespace + '\'' + + ", group='" + group + '\'' + ", fileName='" + compositeConfigFile + '\'' + + ", map='" + map + '\''); + } + + return new PolarisPropertySource(namespace, group, "", compositeConfigFile, map); + } + + public static ConfigKVFile loadConfigKVFile(ConfigFileService configFileService, String namespace, String group, String fileName) { + ConfigKVFile configKVFile; + // unknown extension is resolved as properties file + if (ConfigFileFormat.isPropertyFile(fileName) || ConfigFileFormat.isUnknownFile(fileName)) { + configKVFile = configFileService.getConfigPropertiesFile(namespace, group, fileName); + } + else if (ConfigFileFormat.isYamlFile(fileName)) { + configKVFile = configFileService.getConfigYamlFile(namespace, group, fileName); + } + else { + LOGGER.warn("[SCT Config] Unsupported config file. namespace = {}, group = {}, fileName = {}", namespace, group, fileName); + + throw new IllegalStateException("Only configuration files in the format of properties / yaml / yaml" + " can be injected into the spring context"); + } + return configKVFile; } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigPropertyAutoRefresher.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigPropertyAutoRefresher.java index 8e4c52934..5e66182f0 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigPropertyAutoRefresher.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigPropertyAutoRefresher.java @@ -18,12 +18,21 @@ package com.tencent.cloud.polaris.config.adapter; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import com.google.common.collect.Sets; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; import com.tencent.cloud.polaris.config.logger.PolarisConfigLoggerContext; +import com.tencent.cloud.polaris.config.utils.PolarisPropertySourceUtils; +import com.tencent.polaris.configuration.api.core.ConfigFileGroup; +import com.tencent.polaris.configuration.api.core.ConfigFileMetadata; +import com.tencent.polaris.configuration.api.core.ConfigFileService; import com.tencent.polaris.configuration.api.core.ConfigKVFile; import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener; import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; @@ -33,6 +42,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; +import org.springframework.core.env.PropertySource; import org.springframework.lang.NonNull; import org.springframework.util.CollectionUtils; @@ -53,8 +63,14 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL // this class provides customized logic for some customers to configure special business group files private final PolarisConfigCustomExtensionLayer polarisConfigCustomExtensionLayer = PolarisServiceLoaderUtil.getPolarisConfigCustomExtensionLayer(); - public PolarisConfigPropertyAutoRefresher(PolarisConfigProperties polarisConfigProperties) { + private static final Set registeredPolarisPropertySets = Sets.newConcurrentHashSet(); + + private final ConfigFileService configFileService; + + public PolarisConfigPropertyAutoRefresher(PolarisConfigProperties polarisConfigProperties, + ConfigFileService configFileService) { this.polarisConfigProperties = polarisConfigProperties; + this.configFileService = configFileService; } @Override @@ -81,13 +97,16 @@ private void registerPolarisConfigPublishEvent() { // register polaris config publish event for (PolarisPropertySource polarisPropertySource : polarisPropertySources) { + // group property source if (polarisPropertySource.getConfigKVFile() instanceof CompositeConfigFile) { CompositeConfigFile configKVFile = (CompositeConfigFile) polarisPropertySource.getConfigKVFile(); for (ConfigKVFile cf : configKVFile.getConfigKVFiles()) { PolarisPropertySource p = new PolarisPropertySource(cf.getNamespace(), cf.getFileGroup(), cf.getFileName(), cf, new HashMap<>()); - registerPolarisConfigPublishChangeListener(p); - customRegisterPolarisConfigPublishChangeListener(p); + registerPolarisConfigPublishChangeListener(p, polarisPropertySource); + customRegisterPolarisConfigPublishChangeListener(p, polarisPropertySource); + registeredPolarisPropertySets.add(p.getPropertySourceName()); } + registerPolarisConfigGroupChangeListener(polarisPropertySource); } else { registerPolarisConfigPublishChangeListener(polarisPropertySource); @@ -104,14 +123,74 @@ private void customInitRegisterPolarisConfig(PolarisConfigPropertyAutoRefresher polarisConfigCustomExtensionLayer.initRegisterConfig(polarisConfigPropertyAutoRefresher); } + private void registerPolarisConfigGroupChangeListener(PolarisPropertySource polarisPropertySource) { + ConfigFileGroup configFileGroup = configFileService.getConfigFileGroup( + polarisPropertySource.getNamespace(), polarisPropertySource.getGroup()); + + if (configFileGroup == null) { + return; + } + configFileGroup.addChangeListener(event -> { + try { + LOGGER.debug("ConfigFileGroup receive onChange event:{}", event); + List oldConfigFileMetadataList = event.getOldConfigFileMetadataList(); + List newConfigFileMetadataList = event.getNewConfigFileMetadataList(); + + Map added = calculateUnregister(oldConfigFileMetadataList, newConfigFileMetadataList); + if (added.isEmpty()) { + return; + } + Set changedKeys = new HashSet<>(); + + for (Map.Entry entry : added.entrySet()) { + if (registeredPolarisPropertySets.contains(entry.getKey())) { + continue; + } + registeredPolarisPropertySets.add(entry.getKey()); + LOGGER.info("[SCT Config] add polaris config file:{}", entry.getKey()); + ConfigFileMetadata configFileMetadata = entry.getValue(); + PolarisPropertySource p = PolarisConfigFileLocator.loadPolarisPropertySource( + configFileService, configFileMetadata.getNamespace(), + configFileMetadata.getFileGroup(), configFileMetadata.getFileName()); + LOGGER.info("[SCT Config] changed property = {}", p.getSource().keySet()); + changedKeys.addAll(p.getSource().keySet()); + this.registerPolarisConfigPublishChangeListener(p, polarisPropertySource); + PolarisPropertySourceManager.addPropertySource(p); + for (String changedKey : p.getSource().keySet()) { + polarisPropertySource.getSource().put(changedKey, p.getSource().get(changedKey)); + refreshSpringValue(changedKey); + } + } + refreshConfigurationProperties(changedKeys); + } + catch (Exception e) { + LOGGER.error("[SCT Config] receive onChange exception,", e); + } + }); + } + public void registerPolarisConfigPublishChangeListener(PolarisPropertySource polarisPropertySource) { - LOGGER.info("{} will register polaris config publish listener", polarisPropertySource.getPropertySourceName()); - polarisPropertySource.getConfigKVFile() + registerPolarisConfigPublishChangeListener(polarisPropertySource, polarisPropertySource); + } + + public void registerPolarisConfigPublishChangeListener(PolarisPropertySource listenPolarisPropertySource, PolarisPropertySource effectPolarisPropertySource) { + LOGGER.info("{} will register polaris config publish listener, effect source:{}", + listenPolarisPropertySource.getPropertySourceName(), effectPolarisPropertySource.getPropertySourceName()); + listenPolarisPropertySource.getConfigKVFile() .addChangeListener((ConfigKVFileChangeListener) configKVFileChangeEvent -> { - LOGGER.info("[SCT Config] received polaris config change event and will refresh spring context." + " namespace = {}, group = {}, fileName = {}", polarisPropertySource.getNamespace(), polarisPropertySource.getGroup(), polarisPropertySource.getFileName()); + LOGGER.info("[SCT Config] received polaris config change event and will refresh spring context." + " namespace = {}, group = {}, fileName = {}", + listenPolarisPropertySource.getNamespace(), listenPolarisPropertySource.getGroup(), listenPolarisPropertySource.getFileName()); - Map source = polarisPropertySource.getSource(); + Map effectSource = effectPolarisPropertySource.getSource(); + Map listenSource = listenPolarisPropertySource.getSource(); + boolean isGroupRefresh = !listenPolarisPropertySource.equals(effectPolarisPropertySource); + + PolarisPropertySource newGroupSource = null; + if (isGroupRefresh) { + newGroupSource = PolarisConfigFileLocator.loadGroupPolarisPropertySource(configFileService, + effectPolarisPropertySource.getNamespace(), effectPolarisPropertySource.getGroup()); + } for (String changedKey : configKVFileChangeEvent.changedKeys()) { ConfigPropertyChangeInfo configPropertyChangeInfo = configKVFileChangeEvent.getChangeInfo(changedKey); @@ -133,10 +212,27 @@ public void registerPolarisConfigPublishChangeListener(PolarisPropertySource pol switch (configPropertyChangeInfo.getChangeType()) { case MODIFIED: case ADDED: - source.put(changedKey, configPropertyChangeInfo.getNewValue()); + effectSource.put(changedKey, configPropertyChangeInfo.getNewValue()); + if (isGroupRefresh) { + listenSource.put(changedKey, configPropertyChangeInfo.getNewValue()); + } break; case DELETED: - source.remove(changedKey); + if (isGroupRefresh) { + // when the key is deleted, the value should load from group source + Object newValue = Optional.ofNullable(newGroupSource).map(PropertySource::getSource). + map(source -> source.get(changedKey)).orElse(null); + if (newValue != null) { + effectSource.put(changedKey, newValue); + } + else { + effectSource.remove(changedKey); + } + listenSource.remove(changedKey); + } + else { + effectSource.remove(changedKey); + } break; } // update the attribute with @Value annotation @@ -148,11 +244,43 @@ public void registerPolarisConfigPublishChangeListener(PolarisPropertySource pol } private void customRegisterPolarisConfigPublishChangeListener(PolarisPropertySource polarisPropertySource) { + customRegisterPolarisConfigPublishChangeListener(polarisPropertySource, polarisPropertySource); + } + + private void customRegisterPolarisConfigPublishChangeListener(PolarisPropertySource listenPolarisPropertySource, PolarisPropertySource effectPolarisPropertySource) { if (polarisConfigCustomExtensionLayer == null) { LOGGER.debug("[SCT Config] PolarisConfigCustomExtensionLayer is not init, ignore the following execution steps"); return; } - polarisConfigCustomExtensionLayer.executeRegisterPublishChangeListener(polarisPropertySource); + polarisConfigCustomExtensionLayer.executeRegisterPublishChangeListener(listenPolarisPropertySource, effectPolarisPropertySource); + } + + private Map calculateUnregister(List oldConfigFileMetadataList, + List newConfigFileMetadataList) { + + + Map oldConfigFileMetadataMap = oldConfigFileMetadataList.stream() + .collect(Collectors.toMap( + configFileMetadata -> PolarisPropertySourceUtils.generateName( + configFileMetadata.getNamespace(), + configFileMetadata.getFileGroup(), + configFileMetadata.getFileName()), + configFileMetadata -> configFileMetadata)); + + Map newConfigFileMetadataMap = newConfigFileMetadataList.stream() + .collect(Collectors.toMap( + configFileMetadata -> PolarisPropertySourceUtils.generateName( + configFileMetadata.getNamespace(), + configFileMetadata.getFileGroup(), + configFileMetadata.getFileName()), + configFileMetadata -> configFileMetadata)); + Map added = new HashMap<>(); + for (Map.Entry entry : newConfigFileMetadataMap.entrySet()) { + if (!oldConfigFileMetadataMap.containsKey(entry.getKey())) { + added.put(entry.getKey(), entry.getValue()); + } + } + return added; } /** diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigRefreshScopeAnnotationDetector.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigRefreshScopeAnnotationDetector.java deleted file mode 100644 index 20381a49e..000000000 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigRefreshScopeAnnotationDetector.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.config.adapter; - -import java.lang.annotation.Annotation; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.core.Ordered; -import org.springframework.core.PriorityOrdered; -import org.springframework.lang.NonNull; - -/** - * Mainly used to detect whether the annotation class {@link org.springframework.cloud.context.config.annotation.RefreshScope} - * exists, and whether the user has configured beans using this annotation in their business system. - * If the annotation {@code @RefreshScope} exists and is used, the auto-optimization will be triggered - * in listener {@link com.tencent.cloud.polaris.config.listener.PolarisConfigRefreshOptimizationListener}. - * - *

This bean will only be created and initialized when the config refresh type is {@code RefreshType.REFLECT}. - * - * @author jarvisxiong - */ -@SuppressWarnings({"unchecked", "rawtypes"}) -public class PolarisConfigRefreshScopeAnnotationDetector implements BeanPostProcessor, InitializingBean, PriorityOrdered { - - private final AtomicBoolean isRefreshScopeAnnotationUsed = new AtomicBoolean(false); - - private Class refreshScopeAnnotationClass; - - private String annotatedRefreshScopeBeanName; - - @Override - public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) - throws BeansException { - return bean; - } - - @Override - public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) - throws BeansException { - if (isRefreshScopeAnnotationUsed() || refreshScopeAnnotationClass == null) { - return bean; - } - Annotation[] refreshScopeAnnotations = bean.getClass().getAnnotationsByType(refreshScopeAnnotationClass); - if (refreshScopeAnnotations.length > 0) { - if (isRefreshScopeAnnotationUsed.compareAndSet(false, true)) { - annotatedRefreshScopeBeanName = beanName; - } - } - return bean; - } - - @Override - public void afterPropertiesSet() { - try { - refreshScopeAnnotationClass = Class.forName( - "org.springframework.cloud.context.config.annotation.RefreshScope", - false, - getClass().getClassLoader()); - } - catch (ClassNotFoundException ignored) { - } - } - - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE; - } - - public boolean isRefreshScopeAnnotationUsed() { - return isRefreshScopeAnnotationUsed.get(); - } - - public String getAnnotatedRefreshScopeBeanName() { - return annotatedRefreshScopeBeanName; - } -} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshAffectedContextRefresher.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshAffectedContextRefresher.java index 193285bf0..421cd41db 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshAffectedContextRefresher.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshAffectedContextRefresher.java @@ -24,6 +24,7 @@ import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; import com.tencent.cloud.polaris.config.spring.property.SpringValue; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import com.tencent.polaris.configuration.api.core.ConfigFileService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +32,7 @@ import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; @@ -57,11 +59,15 @@ public class PolarisRefreshAffectedContextRefresher extends PolarisConfigPropert private TypeConverter typeConverter; + private ContextRefresher contextRefresher; + public PolarisRefreshAffectedContextRefresher(PolarisConfigProperties polarisConfigProperties, - SpringValueRegistry springValueRegistry, PlaceholderHelper placeholderHelper) { - super(polarisConfigProperties); + SpringValueRegistry springValueRegistry, PlaceholderHelper placeholderHelper, + ConfigFileService configFileService, ContextRefresher contextRefresher) { + super(polarisConfigProperties, configFileService); this.springValueRegistry = springValueRegistry; this.placeholderHelper = placeholderHelper; + this.contextRefresher = contextRefresher; } @Override @@ -78,7 +84,20 @@ public void refreshSpringValue(String changedKey) { @Override public void refreshConfigurationProperties(Set changeKeys) { - context.publishEvent(new EnvironmentChangeEvent(context, changeKeys)); + boolean needRefreshContext = false; + for (String changedKey : changeKeys) { + boolean inRefreshScope = springValueRegistry.isRefreshScopeKey(changedKey); + if (inRefreshScope) { + needRefreshContext = true; + break; + } + } + if (needRefreshContext) { + contextRefresher.refresh(); + } + else { + context.publishEvent(new EnvironmentChangeEvent(context, changeKeys)); + } } private void updateSpringValue(SpringValue springValue) { diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresher.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresher.java index 1f51885c3..ea2f55087 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresher.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresher.java @@ -13,6 +13,7 @@ * 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.config.adapter; @@ -20,8 +21,15 @@ import java.util.Set; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import com.tencent.polaris.configuration.api.core.ConfigFileService; +import org.springframework.beans.BeansException; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; /** * The default implement of Spring Cloud refreshes the entire Spring Context. @@ -29,13 +37,19 @@ * * @author lingxiao.wlx */ -public class PolarisRefreshEntireContextRefresher extends PolarisConfigPropertyAutoRefresher { +public class PolarisRefreshEntireContextRefresher extends PolarisConfigPropertyAutoRefresher implements ApplicationContextAware { private final ContextRefresher contextRefresher; + private final SpringValueRegistry springValueRegistry; + + private ConfigurableApplicationContext context; + public PolarisRefreshEntireContextRefresher(PolarisConfigProperties polarisConfigProperties, - ContextRefresher contextRefresher) { - super(polarisConfigProperties); + SpringValueRegistry springValueRegistry, ConfigFileService configFileService, ContextRefresher contextRefresher) { + + super(polarisConfigProperties, configFileService); + this.springValueRegistry = springValueRegistry; this.contextRefresher = contextRefresher; } @@ -46,6 +60,24 @@ public void refreshSpringValue(String changedKey) { @Override public void refreshConfigurationProperties(Set changeKeys) { - contextRefresher.refresh(); + boolean needRefreshContext = false; + for (String changedKey : changeKeys) { + boolean inRefreshScope = springValueRegistry.isRefreshScopeKey(changedKey); + if (inRefreshScope) { + needRefreshContext = true; + break; + } + } + if (needRefreshContext) { + contextRefresher.refresh(); + } + else { + context.publishEvent(new EnvironmentChangeEvent(context, changeKeys)); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = (ConfigurableApplicationContext) applicationContext; } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/condition/ReflectRefreshTypeCondition.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/condition/ReflectRefreshTypeCondition.java index 15186d32e..39c8442c9 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/condition/ReflectRefreshTypeCondition.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/condition/ReflectRefreshTypeCondition.java @@ -36,7 +36,7 @@ public class ReflectRefreshTypeCondition extends SpringBootCondition { */ public static final String POLARIS_CONFIG_REFRESH_TYPE = "spring.cloud.polaris.config.refresh-type"; - private static final RefreshType DEFAULT_REFRESH_TYPE = RefreshType.REFLECT; + private static final RefreshType DEFAULT_REFRESH_TYPE = RefreshType.REFRESH_CONTEXT; @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java index 3dc62ed12..2b330ea19 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java @@ -69,7 +69,7 @@ public class PolarisConfigProperties { /** * Attribute refresh type. */ - private RefreshType refreshType = RefreshType.REFLECT; + private RefreshType refreshType = RefreshType.REFRESH_CONTEXT; /** * List of injected configuration files. @@ -95,6 +95,11 @@ public class PolarisConfigProperties { */ private boolean internalEnabled = true; + /** + * if check address is enabled. + */ + private boolean checkAddress = true; + public boolean isEnabled() { return enabled; } @@ -191,6 +196,14 @@ public void setInternalEnabled(boolean internalEnabled) { this.internalEnabled = internalEnabled; } + public boolean isCheckAddress() { + return checkAddress; + } + + public void setCheckAddress(boolean checkAddress) { + this.checkAddress = checkAddress; + } + @Override public String toString() { return "PolarisConfigProperties{" + @@ -206,6 +219,7 @@ public String toString() { ", dataSource='" + dataSource + '\'' + ", localFileRootPath='" + localFileRootPath + '\'' + ", internalEnabled=" + internalEnabled + + ", checkAddress=" + checkAddress + '}'; } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListener.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListener.java deleted file mode 100644 index 4badbc157..000000000 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListener.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.config.listener; - -import java.util.Collections; - -import com.tencent.cloud.polaris.config.adapter.PolarisConfigRefreshScopeAnnotationDetector; -import com.tencent.cloud.polaris.config.adapter.PolarisRefreshEntireContextRefresher; -import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; -import com.tencent.cloud.polaris.config.enums.RefreshType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.cloud.context.refresh.ContextRefresher; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.lang.NonNull; - -import static com.tencent.cloud.polaris.config.condition.ReflectRefreshTypeCondition.POLARIS_CONFIG_REFRESH_TYPE; - -/** - * When {@link com.tencent.cloud.polaris.config.adapter.PolarisConfigRefreshScopeAnnotationDetector} detects that - * the annotation {@code @RefreshScope} exists and is used, but the config refresh type - * {@code spring.cloud.polaris.config.refresh-type} is still {@code RefreshType.REFLECT}, then the framework will - * automatically switch the config refresh type to {@code RefreshType.REFRESH_CONTEXT}. - * - *

The purpose of this optimization is to omit additional configuration, and facilitate for users to use the - * dynamic configuration refresh strategy of Spring Cloud Context.

- * - * @author jarvisxiong - */ -public class PolarisConfigRefreshOptimizationListener implements ApplicationListener { - - private static final Logger LOGGER = LoggerFactory.getLogger(PolarisConfigRefreshOptimizationListener.class); - - private static final String CONFIG_REFRESH_TYPE_PROPERTY = "configRefreshTypeProperty"; - - private static final String REFLECT_REBINDER_BEAN_NAME = "affectedConfigurationPropertiesRebinder"; - - private static final String REFLECT_REFRESHER_BEAN_NAME = "polarisReflectPropertySourceAutoRefresher"; - - private static final String REFRESH_CONTEXT_REFRESHER_BEAN_NAME = "polarisRefreshContextPropertySourceAutoRefresher"; - - - @Override - public void onApplicationEvent(@NonNull ContextRefreshedEvent event) { - ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) event.getApplicationContext(); - PolarisConfigRefreshScopeAnnotationDetector detector = applicationContext.getBean(PolarisConfigRefreshScopeAnnotationDetector.class); - boolean isRefreshScopeAnnotationUsed = detector.isRefreshScopeAnnotationUsed(); - String annotatedRefreshScopeBeanName = detector.getAnnotatedRefreshScopeBeanName(); - // using System.setProperty to set spring.cloud.polaris.config.refresh-type - String value = System.getProperty("spring.cloud.polaris.config.refresh-type"); - boolean isSystemSetRefreshType = RefreshType.REFRESH_CONTEXT.toString().equalsIgnoreCase(value); - // a bean is using @RefreshScope, but the config refresh type is still [reflect], switch automatically - if (isRefreshScopeAnnotationUsed || isSystemSetRefreshType) { - if (isRefreshScopeAnnotationUsed) { - LOGGER.warn("Detected that the bean [{}] is using @RefreshScope annotation, but the config refresh type is still [reflect]. " + "[SCT] will automatically switch to [refresh_context].", annotatedRefreshScopeBeanName); - } - if (isSystemSetRefreshType) { - LOGGER.warn("Detected that using System.setProperty to set spring.cloud.polaris.config.refresh-type = refresh_context, but the config refresh type is still [reflect]. " + "[SCT] will automatically switch to [refresh_context]."); - } - switchConfigRefreshTypeProperty(applicationContext); - modifyPolarisConfigPropertiesBean(applicationContext); - // remove related bean of type [reflect] - removeRelatedBeansOfReflect(applicationContext); - // register a new refresher bean of type [refresh_context] - registerRefresherBeanOfRefreshContext(applicationContext); - // add the new refresher to context as a listener - addRefresherBeanAsListener(applicationContext); - } - } - - private void switchConfigRefreshTypeProperty(ConfigurableApplicationContext applicationContext) { - MutablePropertySources propertySources = applicationContext.getEnvironment().getPropertySources(); - propertySources.addFirst(new MapPropertySource(CONFIG_REFRESH_TYPE_PROPERTY, Collections.singletonMap(POLARIS_CONFIG_REFRESH_TYPE, RefreshType.REFRESH_CONTEXT))); - } - - private void modifyPolarisConfigPropertiesBean(ConfigurableApplicationContext applicationContext) { - PolarisConfigProperties polarisConfigProperties = applicationContext.getBean(PolarisConfigProperties.class); - polarisConfigProperties.setRefreshType(RefreshType.REFRESH_CONTEXT); - } - - private void removeRelatedBeansOfReflect(ConfigurableApplicationContext applicationContext) { - try { - DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory(); - beanFactory.removeBeanDefinition(REFLECT_REFRESHER_BEAN_NAME); - beanFactory.removeBeanDefinition(REFLECT_REBINDER_BEAN_NAME); - } - catch (BeansException e) { - // If there is a removeBean exception in this code, do not affect the main process startup. Some user usage may cause the polarisReflectPropertySourceAutoRefresher to not load, and the removeBeanDefinition will report an error - LOGGER.debug("removeRelatedBeansOfReflect occur error:", e); - } - } - - private void registerRefresherBeanOfRefreshContext(ConfigurableApplicationContext applicationContext) { - DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory(); - AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); - beanDefinition.setBeanClass(PolarisRefreshEntireContextRefresher.class); - PolarisConfigProperties polarisConfigProperties = beanFactory.getBean(PolarisConfigProperties.class); - ContextRefresher contextRefresher = beanFactory.getBean(ContextRefresher.class); - ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues(); - constructorArgumentValues.addIndexedArgumentValue(0, polarisConfigProperties); - constructorArgumentValues.addIndexedArgumentValue(1, contextRefresher); - beanFactory.registerBeanDefinition(REFRESH_CONTEXT_REFRESHER_BEAN_NAME, beanDefinition); - } - - - private void addRefresherBeanAsListener(ConfigurableApplicationContext applicationContext) { - PolarisRefreshEntireContextRefresher refresher = (PolarisRefreshEntireContextRefresher) applicationContext.getBean(REFRESH_CONTEXT_REFRESHER_BEAN_NAME); - applicationContext.addApplicationListener(refresher); - } -} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java index 08898608d..1ba9eb02d 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java @@ -23,7 +23,11 @@ import java.util.List; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.lang.NonNull; @@ -34,17 +38,32 @@ * * @author weihubeats 2022-7-10 */ -public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrdered { +public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware { + + private ConfigurableListableBeanFactory beanFactory; @Override public Object postProcessBeforeInitialization(Object bean, @NonNull String beanName) throws BeansException { Class clazz = bean.getClass(); + + boolean isRefreshScope = false; + + try { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + if ("refresh".equals(beanDefinition.getScope())) { + isRefreshScope = true; + } + } + catch (Exception ignored) { + // ignore + } + for (Field field : findAllField(clazz)) { - processField(bean, beanName, field); + processField(bean, beanName, field, isRefreshScope); } for (Method method : findAllMethod(clazz)) { - processMethod(bean, beanName, method); + processMethod(bean, beanName, method, isRefreshScope); } return bean; } @@ -60,7 +79,7 @@ public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull Stri * @param beanName beanName * @param field field */ - protected abstract void processField(Object bean, String beanName, Field field); + protected abstract void processField(Object bean, String beanName, Field field, boolean isRefreshScope); /** * subclass should implement this method to process method. @@ -68,7 +87,7 @@ public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull Stri * @param beanName beanName * @param method method */ - protected abstract void processMethod(Object bean, String beanName, Method method); + protected abstract void processMethod(Object bean, String beanName, Method method, boolean isRefreshScope); @Override @@ -77,15 +96,20 @@ public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } - private List findAllField(Class clazz) { + protected List findAllField(Class clazz) { final List res = new LinkedList<>(); ReflectionUtils.doWithFields(clazz, res::add); return res; } - private List findAllMethod(Class clazz) { + protected List findAllMethod(Class clazz) { final List res = new LinkedList<>(); ReflectionUtils.doWithMethods(clazz, res::add); return res; } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.beanFactory = (ConfigurableListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java index 0d79b1cc1..d925d7cfe 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java @@ -21,11 +21,13 @@ import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import com.google.common.base.CaseFormat; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; @@ -35,6 +37,7 @@ import com.tencent.cloud.polaris.config.spring.property.SpringValue; import com.tencent.cloud.polaris.config.spring.property.SpringValueDefinition; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import com.tencent.polaris.api.utils.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +53,8 @@ import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.Bean; import org.springframework.lang.NonNull; @@ -105,23 +110,31 @@ public Object postProcessBeforeInitialization(Object bean, @NonNull String beanN @Override - protected void processField(Object bean, String beanName, Field field) { + protected void processField(Object bean, String beanName, Field field, boolean isRefreshScope) { // register @Value on field Value value = field.getAnnotation(Value.class); if (value == null) { return; } - doRegister(bean, beanName, field, value); + doRegister(bean, beanName, field, value, isRefreshScope); } @Override - protected void processMethod(Object bean, String beanName, Method method) { + protected void processMethod(Object bean, String beanName, Method method, boolean isRefreshScope) { //register @Value on method Value value = method.getAnnotation(Value.class); - if (value == null) { + if (value != null) { + processMethodValue(bean, beanName, method, value, isRefreshScope); return; } + + if (method.getAnnotation(RefreshScope.class) != null) { + processMethodRefreshScope(bean, method); + } + } + + private void processMethodValue(Object bean, String beanName, Method method, Value value, boolean isRefreshScope) { //skip Configuration bean methods if (method.getAnnotation(Bean.class) != null) { return; @@ -131,8 +144,128 @@ protected void processMethod(Object bean, String beanName, Method method) { bean.getClass().getName(), method.getName(), method.getParameterTypes().length); return; } + doRegister(bean, beanName, method, value, isRefreshScope); + } + + /** + * @RefreshScope on method. + * method parameter with @Value ${@link com.tencent.cloud.polaris.config.spring.annotation.RefreshScopeSpringProcessorTest.TestConfig3#testBean3}. + * method parameter class with @ConfigurationProperties ${@link com.tencent.cloud.polaris.config.spring.annotation.RefreshScopeSpringProcessorTest.TestConfig4#testBean4}. + * @ConfigurationProperties outside method may effect @RefreshScope bean${@link com.tencent.cloud.polaris.config.spring.annotation.RefreshScopeSpringProcessorTest.TestConfig5#testBean5()}. + * @param bean spring bean. + * @param method method. + */ + private void processMethodRefreshScope(Object bean, Method method) { + // must have @Bean annotation + if (method.getAnnotation(Bean.class) == null) { + return; + } + + for (Parameter parameter : method.getParameters()) { + Value value = parameter.getAnnotation(Value.class); + if (value != null) { + // method parameter with @Value + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + springValueRegistry.putRefreshScopeKeys(keys); + } + // method parameter class with @ConfigurationProperties + ConfigurationProperties configurationProperties = parameter.getType().getAnnotation(ConfigurationProperties.class); + parseConfigurationPropertiesKeys(configurationProperties, parameter.getType()); + } + + // analyze all fields of the class containing the method. + for (Field field : findAllField(bean.getClass())) { + Value value = field.getAnnotation(Value.class); + if (value != null) { + // field with @Value + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + springValueRegistry.putRefreshScopeKeys(keys); + continue; + } + // field class with @ConfigurationProperties + ConfigurationProperties configurationProperties = field.getType().getAnnotation(ConfigurationProperties.class); + parseConfigurationPropertiesKeys(configurationProperties, field.getType()); + } + } + + /** + * parse refresh scope keys from @ConfigurationProperties. + * @param configurationProperties @ConfigurationProperties annotation object. + * @param clazz class of @ConfigurationProperties bean. + */ + private void parseConfigurationPropertiesKeys(ConfigurationProperties configurationProperties, Class clazz) { + if (configurationProperties != null) { + // get prefix from @ConfigurationProperties prefix or value. + String prefix = configurationProperties.value(); + if (StringUtils.isEmpty(prefix)) { + prefix = configurationProperties.prefix(); + } + if (StringUtils.isNotEmpty(prefix)) { + prefix += "."; + } + parseConfigKeys(clazz, prefix); + } + } + + /** + * parse all fields of the configClazz. + * if the field is primitive or wrapper, add it to refresh scope key map. + * ${@link com.tencent.cloud.polaris.config.spring.annotation.RefreshScopeSpringProcessorTest.TestBeanProperties2#name} + * if the field is collection, add it to refresh scope prefix trie node. + * ${@link com.tencent.cloud.polaris.config.spring.annotation.RefreshScopeSpringProcessorTest.TestBeanProperties2#list} + * if the field is complex type, recursive parse. + * ${@link com.tencent.cloud.polaris.config.spring.annotation.RefreshScopeSpringProcessorTest.TestBeanProperties2#inner} + * @param configClazz class or subclass of @ConfigurationProperties bean. + * @param prefix prefix or subclass's prefix of @ConfigurationProperties bean. + */ + private void parseConfigKeys(Class configClazz, String prefix) { + for (Field field : findAllField(configClazz)) { + if (isPrimitiveOrWrapper(field.getType())) { + // lowerCamel format + springValueRegistry.putRefreshScopeKey(prefix + field.getName()); + // lower-hyphen format + springValueRegistry.putRefreshScopeKey( + prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName())); + } + else if (isCollection(field.getType())) { + springValueRegistry.putRefreshScopePrefixKey(prefix + field.getName()); + springValueRegistry.putRefreshScopePrefixKey( + prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName())); + } + else { + // complex type, recursive parse + parseConfigKeys(field.getType(), prefix + field.getName() + "."); + parseConfigKeys(field.getType(), + prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName()) + "."); + } + } + } - doRegister(bean, beanName, method, value); + /** + * whether the class is primitive or wrapper. + * @param clazz the class under analysis. + * @return true if the class is primitive or wrapper, otherwise false. + */ + private static boolean isPrimitiveOrWrapper(Class clazz) { + return clazz.isPrimitive() || + clazz == String.class || + clazz == Boolean.class || + clazz == Character.class || + clazz == Byte.class || + clazz == Short.class || + clazz == Integer.class || + clazz == Long.class || + clazz == Float.class || + clazz == Double.class; + } + + /** + * whether the class is collection(array, collection, map). + * @param clazz the class under analysis. + * @return true if the class is collection(array, collection, map), otherwise false. + */ + private static boolean isCollection(Class clazz) { + return clazz.isArray() || Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz); } @Override @@ -147,7 +280,7 @@ public void postProcessBeanDefinitionRegistry(@NonNull BeanDefinitionRegistry be } } - private void doRegister(Object bean, String beanName, Member member, Value value) { + private void doRegister(Object bean, String beanName, Member member, Value value, boolean isRefreshScope) { Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); if (keys.isEmpty()) { return; @@ -158,10 +291,16 @@ private void doRegister(Object bean, String beanName, Member member, Value value if (member instanceof Field) { Field field = (Field) member; springValue = new SpringValue(key, value.value(), bean, beanName, field); + if (isRefreshScope) { + springValueRegistry.putRefreshScopeKey(key); + } } else if (member instanceof Method) { Method method = (Method) member; springValue = new SpringValue(key, value.value(), bean, beanName, method); + if (isRefreshScope) { + springValueRegistry.putRefreshScopeKey(key); + } } else { LOGGER.error("Polaris @Value annotation currently only support to be used on methods and fields, " diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java index 8718bc264..3ac250960 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -29,6 +30,9 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; +import com.google.common.collect.Sets; +import com.tencent.polaris.api.pojo.TrieNode; +import com.tencent.polaris.api.utils.TrieUtil; import com.tencent.polaris.client.util.NamedThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +58,10 @@ public class SpringValueRegistry implements DisposableBean { private final Object LOCK = new Object(); private ScheduledExecutorService executor; + private final TrieNode refreshScopePrefixRoot = new TrieNode<>(TrieNode.ROOT_PATH); + + private final Set refreshScopeKeys = Sets.newConcurrentHashSet(); + public void register(BeanFactory beanFactory, String key, SpringValue springValue) { if (!registry.containsKey(beanFactory)) { synchronized (LOCK) { @@ -63,7 +71,16 @@ public void register(BeanFactory beanFactory, String key, SpringValue springValu } } - registry.get(beanFactory).put(key, springValue); + Multimap multimap = registry.get(beanFactory); + for (SpringValue existingValue : multimap.get(key)) { + // if the spring value is already registered, remove it + if (existingValue.getBeanName().equals(springValue.getBeanName())) { + multimap.remove(key, existingValue); + break; + } + + } + multimap.put(key, springValue); // lazy initialize if (initialized.compareAndSet(false, true)) { @@ -102,6 +119,30 @@ private void scanAndClean() { } } + public void putRefreshScopePrefixKey(String key) { + TrieUtil.buildConfigTrieNode(key, refreshScopePrefixRoot); + } + + public void putRefreshScopeKey(String key) { + refreshScopeKeys.add(key); + } + + public void putRefreshScopeKeys(Set keys) { + refreshScopeKeys.addAll(keys); + } + + /** + * first check if the key is in refreshScopeKeys, if not, check the key by TrieUtil. + * @param key changed key. + * @return true if the key is refresh scope key, otherwise false. + */ + public boolean isRefreshScopeKey(String key) { + if (refreshScopeKeys.contains(key)) { + return true; + } + return TrieUtil.checkConfig(refreshScopePrefixRoot, key); + } + @Override public void destroy() throws Exception { executor.shutdown(); diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigKVFile.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigKVFile.java index f423ea86b..5d19faec4 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigKVFile.java +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigKVFile.java @@ -36,12 +36,32 @@ public class MockedConfigKVFile implements ConfigKVFile { private final Map properties; + + private String fileName; + + private String fileGroup; + + private String namespace; + private final List listeners = new ArrayList<>(); public MockedConfigKVFile(Map properties) { this.properties = properties; } + public MockedConfigKVFile(Map properties, String fileName) { + this.properties = properties; + this.fileName = fileName; + } + + public MockedConfigKVFile(Map properties, String fileName, String fileGroup, String namespace) { + this.properties = properties; + this.fileName = fileName; + this.fileGroup = fileGroup; + this.namespace = namespace; + + } + @Override public String getProperty(String s, String s1) { return String.valueOf(properties.get(s)); @@ -160,16 +180,16 @@ public void fireChangeListener(ConfigKVFileChangeEvent event) { @Override public String getNamespace() { - return null; + return namespace; } @Override public String getFileGroup() { - return null; + return fileGroup; } @Override public String getFileName() { - return null; + return fileName; } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocatorTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocatorTest.java index d9a215b70..3e8e5fb09 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocatorTest.java +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigFileLocatorTest.java @@ -17,6 +17,8 @@ package com.tencent.cloud.polaris.config.adapter; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -25,24 +27,34 @@ import com.google.common.collect.Lists; import com.tencent.cloud.polaris.config.config.ConfigFileGroup; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.enums.RefreshType; import com.tencent.cloud.polaris.context.config.PolarisContextProperties; import com.tencent.polaris.configuration.api.core.ConfigFileService; import com.tencent.polaris.configuration.api.core.ConfigKVFile; +import com.tencent.polaris.configuration.client.internal.RevisableConfigFileGroup; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; /** * test for {@link PolarisConfigFileLocator}. - *@author lepdou 2022-06-11 + * + * @author lepdou 2022-06-11 */ @ExtendWith(MockitoExtension.class) public class PolarisConfigFileLocatorTest { @@ -65,6 +77,7 @@ public void setUp() { @Test public void testLoadApplicationPropertiesFile() { + clearCompositePropertySourceCache(); PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, configFileService, environment); @@ -102,6 +115,7 @@ public void testLoadApplicationPropertiesFile() { @Test public void testActiveProfileFilesPriorityBiggerThanDefault() { + clearCompositePropertySourceCache(); PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, configFileService, environment); @@ -151,6 +165,7 @@ public void testActiveProfileFilesPriorityBiggerThanDefault() { @Test public void testGetCustomFiles() { + clearCompositePropertySourceCache(); PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, configFileService, environment); @@ -201,4 +216,166 @@ public void testGetCustomFiles() { assertThat(propertySource.getProperty("k2")).isEqualTo("v2"); assertThat(propertySource.getProperty("k3")).isEqualTo("v3"); } + + + @Test + public void testGetCustomGroupFiles() { + clearCompositePropertySourceCache(); + PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, + configFileService, environment); + + when(polarisContextProperties.getNamespace()).thenReturn(testNamespace); + when(polarisContextProperties.getService()).thenReturn(testServiceName); + + Map emptyMap = new HashMap<>(); + ConfigKVFile emptyConfigFile = new MockedConfigKVFile(emptyMap); + + when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "application.properties")).thenReturn(emptyConfigFile); + when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "application.yml")).thenReturn(emptyConfigFile); + when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "application.yaml")).thenReturn(emptyConfigFile); + when(configFileService.getConfigPropertiesFile(testNamespace, testServiceName, "bootstrap.properties")).thenReturn(emptyConfigFile); + when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "bootstrap.yml")).thenReturn(emptyConfigFile); + when(configFileService.getConfigYamlFile(testNamespace, testServiceName, "bootstrap.yaml")).thenReturn(emptyConfigFile); + + List customFiles = new LinkedList<>(); + ConfigFileGroup configFileGroup = new ConfigFileGroup(); + String customGroup = "group2"; + configFileGroup.setName(customGroup); + String customFile1 = "file1.properties"; + String customFile2 = "file2.yaml"; + customFiles.add(configFileGroup); + + when(polarisConfigProperties.isEnabled()).thenReturn(true); + when(polarisConfigProperties.getGroups()).thenReturn(customFiles); + when(polarisConfigProperties.isInternalEnabled()).thenReturn(true); + when(environment.getActiveProfiles()).thenReturn(new String[] {}); + + // file1.properties + Map file1Map = new HashMap<>(); + file1Map.put("k1", "v1"); + file1Map.put("k2", "v2"); + ConfigKVFile file1 = new MockedConfigKVFile(file1Map, customFile1); + when(configFileService.getConfigPropertiesFile(testNamespace, customGroup, customFile1)).thenReturn(file1); + + // file2.yaml + Map file2Map = new HashMap<>(); + file2Map.put("k1", "v11"); + file2Map.put("k3", "v3"); + ConfigKVFile file2 = new MockedConfigKVFile(file2Map, customFile2); + when(configFileService.getConfigYamlFile(testNamespace, customGroup, customFile2)).thenReturn(file2); + + RevisableConfigFileGroup revisableConfigFileGroup = new RevisableConfigFileGroup(testNamespace, customGroup, Arrays.asList(file1, file2), "v1"); + when(configFileService.getConfigFileGroup(testNamespace, customGroup)).thenReturn(revisableConfigFileGroup); + + PropertySource propertySource = locator.locate(environment); + + assertThat(propertySource.getProperty("k1")).isEqualTo("v1"); + assertThat(propertySource.getProperty("k2")).isEqualTo("v2"); + assertThat(propertySource.getProperty("k3")).isEqualTo("v3"); + } + + @Test + void testInitTsfConfigGroupsSuccessfulLoad() { + clearCompositePropertySourceCache(); + + // Arrange + String tsfId = "test-id"; + String tsfNamespace = "test-namespace"; + String tsfGroup = "test-group"; + String polarisNamespace = "polaris-namespace"; + + when(environment.getProperty("tsf_id")).thenReturn(tsfId); + when(environment.getProperty("tsf_namespace_name")).thenReturn(tsfNamespace); + when(environment.getProperty("tsf_group_name")).thenReturn(tsfGroup); + when(polarisContextProperties.getNamespace()).thenReturn(polarisNamespace); + + String expectedAppConfigGroup = tsfId + "." + tsfGroup + ".application_config_group"; + + // mock polaris config properties + PolarisPropertySource mockPropertySource = mock(PolarisPropertySource.class); + when(mockPropertySource.getPropertySourceName()).thenReturn(expectedAppConfigGroup); + + CompositePropertySource compositePropertySource = mock(CompositePropertySource.class); + try (MockedStatic mockedStatic = mockStatic(PolarisConfigFileLocator.class)) { + mockedStatic.when(() -> PolarisConfigFileLocator.loadGroupPolarisPropertySource( + eq(configFileService), + eq(polarisNamespace), + any() + )).thenReturn(mockPropertySource); + + PolarisConfigFileLocator locator = new PolarisConfigFileLocator( + polarisConfigProperties, + polarisContextProperties, + configFileService, + environment + ); + // Act + locator.initTsfConfigGroups(compositePropertySource); + + // Verify + List polarisPropertySources = PolarisPropertySourceManager.getAllPropertySources(); + assertThat(polarisPropertySources.stream().map(PolarisPropertySource::getPropertySourceName). + filter(name -> name.equals(expectedAppConfigGroup)).count() == 1); + } + } + + @Test + void testPolarisConfigProperties() { + PolarisConfigProperties testProperties = new PolarisConfigProperties(); + boolean enabled = true; + String address = "127.0.0.1"; + int port = 1234; + String token = ""; + boolean autoRefresh = true; + RefreshType refreshType = RefreshType.REFRESH_CONTEXT; + List groups = new LinkedList<>(); + boolean preference = true; + String dataSource = "test-data-source"; + String localFileRootPath = "test-local-file-root-path"; + boolean internalEnabled = true; + boolean checkAddress = true; + boolean shutdownIfConnectToConfigServerFailed = true; + + testProperties.setEnabled(enabled); + testProperties.setAddress(address); + testProperties.setPort(port); + testProperties.setToken(token); + testProperties.setAutoRefresh(autoRefresh); + testProperties.setRefreshType(refreshType); + testProperties.setGroups(groups); + testProperties.setPreference(preference); + testProperties.setDataSource(dataSource); + testProperties.setLocalFileRootPath(localFileRootPath); + testProperties.setInternalEnabled(internalEnabled); + testProperties.setCheckAddress(checkAddress); + testProperties.setShutdownIfConnectToConfigServerFailed(shutdownIfConnectToConfigServerFailed); + + Assertions.assertEquals(enabled, testProperties.isEnabled()); + Assertions.assertEquals(address, testProperties.getAddress()); + Assertions.assertEquals(port, testProperties.getPort()); + Assertions.assertEquals(token, testProperties.getToken()); + Assertions.assertEquals(autoRefresh, testProperties.isAutoRefresh()); + Assertions.assertEquals(refreshType, testProperties.getRefreshType()); + Assertions.assertEquals(groups, testProperties.getGroups()); + Assertions.assertEquals(preference, testProperties.isPreference()); + Assertions.assertEquals(dataSource, testProperties.getDataSource()); + Assertions.assertEquals(localFileRootPath, testProperties.getLocalFileRootPath()); + Assertions.assertEquals(internalEnabled, testProperties.isInternalEnabled()); + Assertions.assertEquals(checkAddress, testProperties.isCheckAddress()); + Assertions.assertEquals(shutdownIfConnectToConfigServerFailed, testProperties.isShutdownIfConnectToConfigServerFailed()); + + Assertions.assertNotNull(testProperties.toString()); + } + + private void clearCompositePropertySourceCache() { + try { + Class clazz = PolarisConfigFileLocator.class; + Field field = clazz.getDeclaredField("compositePropertySourceCache"); + field.setAccessible(true); + field.set(null, null); + } + catch (Exception e) { + // ignore + } + } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigRefreshScopeAnnotationDetectorTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigRefreshScopeAnnotationDetectorTest.java deleted file mode 100644 index 10f1bd7d8..000000000 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisConfigRefreshScopeAnnotationDetectorTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.config.adapter; - -import com.tencent.cloud.polaris.config.PolarisConfigAutoConfiguration; -import com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration; -import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; -import org.springframework.cloud.context.config.annotation.RefreshScope; - -import static org.assertj.core.api.Assertions.as; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * test for {@link PolarisConfigRefreshScopeAnnotationDetector}. - */ -@SuppressWarnings("rawtypes") -public class PolarisConfigRefreshScopeAnnotationDetectorTest { - - private static Class refreshScopeAnnotationClass = null; - - static { - try { - refreshScopeAnnotationClass = Class.forName( - "org.springframework.cloud.context.config.annotation.RefreshScope", - false, - PolarisConfigRefreshScopeAnnotationDetectorTest.class.getClassLoader()); - } - catch (ClassNotFoundException ignored) { - } - } - - @Test - public void testUseRefreshScope() { - ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(PolarisConfigBootstrapAutoConfiguration.class)) - .withConfiguration(AutoConfigurations.of(PolarisConfigAutoConfiguration.class)) - .withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class)) - .withConfiguration(AutoConfigurations.of(ConfigurationPropertiesRebinderAutoConfiguration.class)) - .withBean("testBeanWithRefreshScope", TestBeanWithRefreshScope.class) - .withPropertyValues("spring.application.name=" + "polarisConfigRefreshScopeAnnotationDetectorTest") - .withPropertyValues("server.port=" + 8080) - .withPropertyValues("spring.cloud.polaris.address=grpc://127.0.0.1:10081") - .withPropertyValues("spring.cloud.polaris.config.connect-remote-server=false"); - contextRunner.run(context -> { - assertThat(context).hasSingleBean(PolarisConfigRefreshScopeAnnotationDetector.class); - PolarisConfigRefreshScopeAnnotationDetector detector = context.getBean(PolarisConfigRefreshScopeAnnotationDetector.class); - assertThat(detector.isRefreshScopeAnnotationUsed()).isTrue(); - assertThat(detector.getAnnotatedRefreshScopeBeanName()).isEqualTo("scopedTarget.testBeanWithRefreshScope"); - assertThat(detector).extracting("refreshScopeAnnotationClass", as(InstanceOfAssertFactories.type(Class.class))) - .isEqualTo(refreshScopeAnnotationClass); - }); - } - - @Test - public void testNotUseRefreshScope() { - ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(PolarisConfigBootstrapAutoConfiguration.class)) - .withConfiguration(AutoConfigurations.of(PolarisConfigAutoConfiguration.class)) - .withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class)) - .withConfiguration(AutoConfigurations.of(ConfigurationPropertiesRebinderAutoConfiguration.class)) - .withPropertyValues("spring.application.name=" + "polarisConfigRefreshScopeAnnotationDetectorTest") - .withPropertyValues("server.port=" + 8080) - .withPropertyValues("spring.cloud.polaris.address=grpc://127.0.0.1:10081") - .withPropertyValues("spring.cloud.polaris.config.connect-remote-server=false"); - contextRunner.run(context -> { - assertThat(context).hasSingleBean(PolarisConfigRefreshScopeAnnotationDetector.class); - PolarisConfigRefreshScopeAnnotationDetector detector = context.getBean(PolarisConfigRefreshScopeAnnotationDetector.class); - assertThat(detector.isRefreshScopeAnnotationUsed()).isFalse(); - assertThat(detector.getAnnotatedRefreshScopeBeanName()).isNull(); - assertThat(detector).extracting("refreshScopeAnnotationClass", as(InstanceOfAssertFactories.type(Class.class))) - .isEqualTo(refreshScopeAnnotationClass); - }); - } - - @RefreshScope - protected static class TestBeanWithRefreshScope { - - } -} diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java index 7bbdd5fc5..24b5fea1b 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java @@ -19,17 +19,24 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; import com.tencent.cloud.polaris.config.spring.property.SpringValue; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.polaris.configuration.api.core.ChangeType; +import com.tencent.polaris.configuration.api.core.ConfigFileService; import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent; import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; +import com.tencent.polaris.configuration.client.internal.CompositeConfigFile; +import com.tencent.polaris.configuration.client.internal.RevisableConfigFileGroup; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,6 +45,7 @@ import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.ConfigurableApplicationContext; import static org.assertj.core.api.Assertions.assertThat; @@ -54,6 +62,7 @@ public class PolarisPropertiesSourceAutoRefresherTest { private final String testNamespace = "testNamespace"; + private final String testFileGroup = "testFileGroup"; private final String testServiceName = "testServiceName"; private final String testFileName = "application.properties"; @Mock @@ -65,6 +74,12 @@ public class PolarisPropertiesSourceAutoRefresherTest { @Mock private PlaceholderHelper placeholderHelper; + @Mock + private ConfigFileService configFileService; + + @Mock + private ContextRefresher contextRefresher; + @BeforeEach public void setUp() { PolarisPropertySourceManager.clearPropertySources(); @@ -72,7 +87,8 @@ public void setUp() { @Test public void testConfigFileChanged() throws Exception { - PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher(polarisConfigProperties, springValueRegistry, placeholderHelper); + PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher( + polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService, contextRefresher); ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class); TypeConverter typeConverter = mock(TypeConverter.class); @@ -105,10 +121,13 @@ public void testConfigFileChanged() throws Exception { ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1", "v11", ChangeType.MODIFIED); ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED); ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k2", "v2", null, ChangeType.DELETED); + ConfigPropertyChangeInfo changeInfoLogger = new ConfigPropertyChangeInfo("logging.level.root", null, "info", ChangeType.ADDED); + Map changeInfos = new HashMap<>(); changeInfos.put("k1", changeInfo); changeInfos.put("k2", changeInfo3); changeInfos.put("k4", changeInfo2); + changeInfos.put("logging.level.root", changeInfoLogger); ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos); refresher.onApplicationEvent(null); @@ -120,4 +139,88 @@ public void testConfigFileChanged() throws Exception { assertThat(polarisPropertySource.getProperty("k2")).isNull(); assertThat(polarisPropertySource.getProperty("k4")).isEqualTo("v4"); } + + @Test + public void testConfigFileGroupChanged() throws Exception { + PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher( + polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService, contextRefresher); + ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); + ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class); + TypeConverter typeConverter = mock(TypeConverter.class); + when(beanFactory.getTypeConverter()).thenReturn(typeConverter); + when(applicationContext.getBeanFactory()).thenReturn(beanFactory); + refresher.setApplicationContext(applicationContext); + when(typeConverter.convertIfNecessary(any(), any(), (Field) any())).thenReturn("v11"); + Collection springValues = new ArrayList<>(); + MockedConfigChange mockedConfigChange = new MockedConfigChange(); + mockedConfigChange.setK1("v1"); + Field field = mockedConfigChange.getClass().getDeclaredField("k1"); + SpringValue springValue = new SpringValue("v1", "placeholder", mockedConfigChange, "mockedConfigChange", field); + + springValues.add(springValue); + + when(springValueRegistry.get(any(), any())).thenReturn(springValues); + + when(polarisConfigProperties.isAutoRefresh()).thenReturn(true); + + Map content = new ConcurrentHashMap<>(); + content.put("k1", "v1"); + content.put("k2", "v2"); + content.put("k3", "v3"); + MockedConfigKVFile file = new MockedConfigKVFile(content, testFileName, testFileGroup, testNamespace); + when(configFileService.getConfigPropertiesFile(testNamespace, testFileGroup, testFileName)) + .thenReturn(file); + + CompositeConfigFile compositeConfigFile = new CompositeConfigFile(Collections.singletonList(file)); + PolarisPropertySource polarisPropertySource = new PolarisPropertySource(testNamespace, testFileGroup, testFileName, + compositeConfigFile, new ConcurrentHashMap<>(content)); + + PolarisPropertySourceManager.addPropertySource(polarisPropertySource); + + RevisableConfigFileGroup revisableConfigFileGroup = new RevisableConfigFileGroup(testNamespace, testFileGroup, Collections.singletonList(file), "v1"); + when(configFileService.getConfigFileGroup(testNamespace, testFileGroup)).thenReturn(revisableConfigFileGroup); + + refresher.onApplicationEvent(null); + + Map content2 = new ConcurrentHashMap<>(); + content2.put("k1", "v1.1"); + content2.put("k3.1", "v3.1"); + MockedConfigKVFile file2 = new MockedConfigKVFile(content2, "file2.properties", testFileGroup, testNamespace); + + when(configFileService.getConfigPropertiesFile(testNamespace, testFileGroup, "file2.properties")) + .thenReturn(file2); + + revisableConfigFileGroup.updateConfigFileList(Arrays.asList(file, file2), "v2"); + Thread.sleep(5000); + assertThat(polarisPropertySource.getProperty("k1")).isEqualTo("v1.1"); + assertThat(polarisPropertySource.getProperty("k2")).isEqualTo("v2"); + assertThat(polarisPropertySource.getProperty("k3")).isEqualTo("v3"); + assertThat(polarisPropertySource.getProperty("k3.1")).isEqualTo("v3.1"); + + // delete event + revisableConfigFileGroup.updateConfigFileList(Collections.singletonList(file), "v3"); + ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1.1", null, ChangeType.DELETED); + ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k3.1", "v3.1", null, ChangeType.DELETED); + ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED); + Map changeInfos = new TreeMap<>(); + changeInfos.put("k1", changeInfo); + changeInfos.put("k3.1", changeInfo2); + changeInfos.put("k4", changeInfo3); + ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos); + file2.fireChangeListener(event); + Thread.sleep(5000); + + assertThat(polarisPropertySource.getProperty("k1")).isEqualTo("v1"); + assertThat(polarisPropertySource.getProperty("k3.1")).isNull(); + assertThat(polarisPropertySource.getProperty("k4")).isEqualTo("v4"); + + revisableConfigFileGroup.updateConfigFileList(Arrays.asList(file, file2), "v4"); + Thread.sleep(5000); + + // no exception + MockedConfigKVFile file3 = new MockedConfigKVFile(Collections.emptyMap(), "file3.properties", testFileGroup, testNamespace); + revisableConfigFileGroup.updateConfigFileList(Arrays.asList(file, file2, file3), "v5"); + Thread.sleep(5000); + + } } diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresherTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresherTest.java new file mode 100644 index 000000000..dc90f2e37 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisRefreshEntireContextRefresherTest.java @@ -0,0 +1,209 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.config.adapter; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import com.tencent.polaris.configuration.api.core.ConfigFileService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link PolarisRefreshEntireContextRefresher}. + * + * @author Shedfree Wu + */ +public class PolarisRefreshEntireContextRefresherTest { + @Mock + private PolarisConfigProperties polarisConfigProperties; + + @Mock + private SpringValueRegistry springValueRegistry; + + @Mock + private ConfigFileService configFileService; + + @Mock + private ContextRefresher contextRefresher; + + @Mock + private ConfigurableApplicationContext applicationContext; + + @Mock + private PolarisConfigCustomExtensionLayer mockExtensionLayer; + + private PolarisRefreshEntireContextRefresher refresher; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + refresher = new PolarisRefreshEntireContextRefresher( + polarisConfigProperties, + springValueRegistry, + configFileService, + contextRefresher + ); + refresher.setApplicationContext(applicationContext); + } + + @Test + void testRefreshSpringValue() { + // test refreshSpringValue method, it should do nothing + refresher.refreshSpringValue("test.key"); + + // Verify + verifyNoInteractions(contextRefresher); + verifyNoInteractions(springValueRegistry); + } + + @Test + void testRefreshConfigurationPropertiesWithRefreshScope() { + // Arrange + Set changeKeys = new HashSet<>(); + changeKeys.add("test.key1"); + changeKeys.add("test.key2"); + + // mock test.key1 in refresh scope + when(springValueRegistry.isRefreshScopeKey("test.key1")).thenReturn(true); + + // Act + refresher.refreshConfigurationProperties(changeKeys); + + // Verify + verify(contextRefresher, times(1)).refresh(); + verifyNoInteractions(applicationContext); + } + + @Test + void testRefreshConfigurationPropertiesWithoutRefreshScope() { + // Arrange + Set changeKeys = new HashSet<>(); + changeKeys.add("test.key1"); + changeKeys.add("test.key2"); + + // mock a key not in refresh scope + when(springValueRegistry.isRefreshScopeKey(anyString())).thenReturn(false); + + // Act + refresher.refreshConfigurationProperties(changeKeys); + + // Verify + verify(contextRefresher, never()).refresh(); + verify(applicationContext, times(1)) + .publishEvent(any(EnvironmentChangeEvent.class)); + } + + @Test + void testSetApplicationContext() { + // Arrange + ConfigurableApplicationContext newContext = mock(ConfigurableApplicationContext.class); + + // Act + refresher.setApplicationContext(newContext); + + // Verify + Set changeKeys = new HashSet<>(); + changeKeys.add("test.key"); + when(springValueRegistry.isRefreshScopeKey(anyString())).thenReturn(false); + + refresher.refreshConfigurationProperties(changeKeys); + verify(newContext, times(1)).publishEvent(any(EnvironmentChangeEvent.class)); + } + + @Test + void testRefreshConfigurationPropertiesWithEmptyChangeKeys() { + // Arrange + Set changeKeys = new HashSet<>(); + + // Act + refresher.refreshConfigurationProperties(changeKeys); + + // Verify + verify(contextRefresher, never()).refresh(); + verify(applicationContext, times(1)) + .publishEvent(any(EnvironmentChangeEvent.class)); + } + + @Test + void testRefreshConfigurationPropertiesWithMultipleRefreshScopeKeys() { + // Arrange + Set changeKeys = new HashSet<>(); + changeKeys.add("test.key1"); + changeKeys.add("test.key2"); + changeKeys.add("test.key3"); + + // mock multiple keys in refresh scope + when(springValueRegistry.isRefreshScopeKey(anyString())).thenReturn(true); + + // Act + refresher.refreshConfigurationProperties(changeKeys); + + // Verify + verify(contextRefresher, times(1)).refresh(); + verifyNoInteractions(applicationContext); + } + + @Test + void testPolarisConfigCustomExtensionLayer() throws Exception { + refresher.setRegistered(true); + + Field field = PolarisConfigPropertyAutoRefresher.class + .getDeclaredField("polarisConfigCustomExtensionLayer"); + field.setAccessible(true); + field.set(refresher, mockExtensionLayer); + + Method method = PolarisConfigPropertyAutoRefresher.class + .getDeclaredMethod("customInitRegisterPolarisConfig", PolarisConfigPropertyAutoRefresher.class); + method.setAccessible(true); + method.invoke(refresher, refresher); + + + method = PolarisConfigPropertyAutoRefresher.class.getDeclaredMethod( + "customRegisterPolarisConfigPublishChangeListener", + PolarisPropertySource.class, PolarisPropertySource.class); + + method.setAccessible(true); + method.invoke(refresher, null, null); + + // Verify + verify(mockExtensionLayer, times(1)).initRegisterConfig(refresher); + + } + +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListenerTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListenerTest.java index d4d627db7..77ce8e892 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListenerTest.java +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListenerTest.java @@ -17,13 +17,16 @@ package com.tencent.cloud.polaris.config.listener; +import java.lang.reflect.Field; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import com.google.common.collect.Sets; +import com.tencent.cloud.polaris.config.adapter.PolarisConfigFileLocator; import com.tencent.cloud.polaris.config.annotation.PolarisConfigKVFileChangeListener; import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,6 +37,7 @@ import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.CompositePropertySource; import org.springframework.stereotype.Component; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -46,7 +50,9 @@ */ @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = DEFINED_PORT, classes = ConfigChangeListenerTest.TestApplication.class, - properties = {"server.port=48081", "spring.config.location = classpath:application-test.yml"}) + properties = {"server.port=48081", "spring.config.location = classpath:application-test.yml", + "spring.cloud.polaris.config.connect-remote-server=true", "spring.cloud.polaris.config.check-address=false" + }) public class ConfigChangeListenerTest { private static final CountDownLatch hits = new CountDownLatch(2); @@ -57,6 +63,19 @@ public class ConfigChangeListenerTest { @Autowired private TestApplication.TestConfig testConfig; + @BeforeAll + public static void setUp() { + try { + Class clazz = PolarisConfigFileLocator.class; + Field field = clazz.getDeclaredField("compositePropertySourceCache"); + field.setAccessible(true); + field.set(null, new CompositePropertySource("mock")); + } + catch (Exception e) { + // ignore + } + } + @Test public void test() throws InterruptedException { //before change @@ -70,7 +89,7 @@ public void test() throws InterruptedException { applicationEventPublisher.publishEvent(event); //after change - boolean ret = hits.await(5, TimeUnit.SECONDS); + boolean ret = hits.await(2, TimeUnit.SECONDS); Assertions.assertThat(ret).isEqualTo(true); Assertions.assertThat(testConfig.getChangeCnt()).isEqualTo(2); diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListenerNotTriggeredTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListenerNotTriggeredTest.java deleted file mode 100644 index 1e258682d..000000000 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListenerNotTriggeredTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.config.listener; - -import java.util.HashMap; -import java.util.Map; - -import com.tencent.cloud.polaris.config.adapter.MockedConfigKVFile; -import com.tencent.cloud.polaris.config.adapter.PolarisPropertySource; -import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager; -import com.tencent.cloud.polaris.config.adapter.PolarisRefreshAffectedContextRefresher; -import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; -import com.tencent.cloud.polaris.config.enums.RefreshType; -import com.tencent.polaris.configuration.api.core.ChangeType; -import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent; -import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.context.refresh.ContextRefresher; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.stereotype.Component; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static com.tencent.cloud.polaris.config.condition.ReflectRefreshTypeCondition.POLARIS_CONFIG_REFRESH_TYPE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; - -/** - * test for {@link PolarisConfigRefreshOptimizationListener}. - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = DEFINED_PORT, classes = PolarisConfigRefreshOptimizationListenerNotTriggeredTest.TestApplication.class, - properties = { - "server.port=48081", - "spring.cloud.polaris.address=grpc://127.0.0.1:10081", - "spring.cloud.polaris.config.connect-remote-server=false", - "spring.cloud.polaris.config.refresh-type=reflect", - "spring.config.location = classpath:application-test.yml" - }) -public class PolarisConfigRefreshOptimizationListenerNotTriggeredTest { - - private static final String REFLECT_REFRESHER_BEAN_NAME = "polarisReflectPropertySourceAutoRefresher"; - - private static final String TEST_NAMESPACE = "testNamespace"; - - private static final String TEST_SERVICE_NAME = "testServiceName"; - - private static final String TEST_FILE_NAME = "application.properties"; - - @Autowired - private ConfigurableApplicationContext context; - - @BeforeAll - static void beforeAll() { - PolarisPropertySourceManager.clearPropertySources(); - } - - @Test - public void testNotSwitchConfigRefreshType() { - RefreshType actualRefreshType = context.getEnvironment() - .getProperty(POLARIS_CONFIG_REFRESH_TYPE, RefreshType.class); - assertThat(actualRefreshType).isEqualTo(RefreshType.REFLECT); - PolarisConfigProperties polarisConfigProperties = context.getBean(PolarisConfigProperties.class); - assertThat(polarisConfigProperties.getRefreshType()).isEqualTo(RefreshType.REFLECT); - assertThat(context.containsBean(REFLECT_REFRESHER_BEAN_NAME)).isTrue(); - PolarisRefreshAffectedContextRefresher refresher = context - .getBean(REFLECT_REFRESHER_BEAN_NAME, PolarisRefreshAffectedContextRefresher.class); - assertThat(((AbstractApplicationContext) context).getApplicationListeners().contains(refresher)).isTrue(); - } - - @Test - public void testConfigFileChanged() { - Map content = new HashMap<>(); - content.put("k1", "v1"); - content.put("k2", "v2"); - content.put("k3", "v3"); - MockedConfigKVFile file = new MockedConfigKVFile(content); - - PolarisPropertySource polarisPropertySource = new PolarisPropertySource(TEST_NAMESPACE, TEST_SERVICE_NAME, TEST_FILE_NAME, - file, content); - PolarisPropertySourceManager.addPropertySource(polarisPropertySource); - - PolarisRefreshAffectedContextRefresher refresher = context.getBean(PolarisRefreshAffectedContextRefresher.class); - PolarisRefreshAffectedContextRefresher spyRefresher = Mockito.spy(refresher); - - refresher.setRegistered(false); - spyRefresher.onApplicationEvent(null); - - ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1", "v11", ChangeType.MODIFIED); - ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED); - ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k2", "v2", null, ChangeType.DELETED); - Map changeInfos = new HashMap<>(); - changeInfos.put("k1", changeInfo); - changeInfos.put("k2", changeInfo3); - changeInfos.put("k4", changeInfo2); - ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos); - file.fireChangeListener(event); - - ContextRefresher mockContextRefresher = context.getBean(ContextRefresher.class); - when(mockContextRefresher.refresh()).thenReturn(event.changedKeys()); - - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshSpringValue("k1"); - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshSpringValue("k2"); - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshSpringValue("k4"); - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshConfigurationProperties(event.changedKeys()); - } - - @SpringBootApplication - protected static class TestApplication { - - @Primary - @Bean - public ContextRefresher contextRefresher() { - return mock(ContextRefresher.class); - } - - @Component - protected static class TestBeanWithoutRefreshScope { - - } - } -} diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListenerTriggeredTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListenerTriggeredTest.java deleted file mode 100644 index 5ffa22664..000000000 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/listener/PolarisConfigRefreshOptimizationListenerTriggeredTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.polaris.config.listener; - -import java.util.HashMap; -import java.util.Map; - -import com.tencent.cloud.polaris.config.adapter.MockedConfigKVFile; -import com.tencent.cloud.polaris.config.adapter.PolarisPropertySource; -import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager; -import com.tencent.cloud.polaris.config.adapter.PolarisRefreshEntireContextRefresher; -import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; -import com.tencent.cloud.polaris.config.enums.RefreshType; -import com.tencent.polaris.configuration.api.core.ChangeType; -import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent; -import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.context.config.annotation.RefreshScope; -import org.springframework.cloud.context.refresh.ContextRefresher; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.stereotype.Component; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static com.tencent.cloud.polaris.config.condition.ReflectRefreshTypeCondition.POLARIS_CONFIG_REFRESH_TYPE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; - -/** - * test for {@link PolarisConfigRefreshOptimizationListener}. - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = DEFINED_PORT, classes = PolarisConfigRefreshOptimizationListenerTriggeredTest.TestApplication.class, - properties = { - "server.port=48081", - "spring.cloud.polaris.address=grpc://127.0.0.1:10081", - "spring.cloud.polaris.config.connect-remote-server=false", - "spring.cloud.polaris.config.refresh-type=reflect", - "spring.config.location = classpath:application-test.yml" - }) -public class PolarisConfigRefreshOptimizationListenerTriggeredTest { - - private static final String REFRESH_CONTEXT_REFRESHER_BEAN_NAME = "polarisRefreshContextPropertySourceAutoRefresher"; - - private static final String TEST_NAMESPACE = "testNamespace"; - - private static final String TEST_SERVICE_NAME = "testServiceName"; - - private static final String TEST_FILE_NAME = "application.properties"; - - @Autowired - private ConfigurableApplicationContext context; - - @BeforeEach - public void setUp() { - PolarisPropertySourceManager.clearPropertySources(); - } - - @Test - public void testSwitchConfigRefreshType() { - RefreshType actualRefreshType = context.getEnvironment() - .getProperty(POLARIS_CONFIG_REFRESH_TYPE, RefreshType.class); - assertThat(actualRefreshType).isEqualTo(RefreshType.REFRESH_CONTEXT); - PolarisConfigProperties polarisConfigProperties = context.getBean(PolarisConfigProperties.class); - assertThat(polarisConfigProperties.getRefreshType()).isEqualTo(RefreshType.REFRESH_CONTEXT); - assertThat(context.containsBean(REFRESH_CONTEXT_REFRESHER_BEAN_NAME)).isTrue(); - PolarisRefreshEntireContextRefresher refresher = context - .getBean(REFRESH_CONTEXT_REFRESHER_BEAN_NAME, PolarisRefreshEntireContextRefresher.class); - assertThat(((AbstractApplicationContext) context).getApplicationListeners().contains(refresher)).isTrue(); - } - - @Test - public void testConfigFileChanged() { - Map content = new HashMap<>(); - content.put("k1", "v1"); - content.put("k2", "v2"); - content.put("k3", "v3"); - MockedConfigKVFile file = new MockedConfigKVFile(content); - - PolarisPropertySource polarisPropertySource = new PolarisPropertySource(TEST_NAMESPACE, TEST_SERVICE_NAME, TEST_FILE_NAME, - file, content); - PolarisPropertySourceManager.addPropertySource(polarisPropertySource); - - PolarisRefreshEntireContextRefresher refresher = context.getBean(PolarisRefreshEntireContextRefresher.class); - PolarisRefreshEntireContextRefresher spyRefresher = Mockito.spy(refresher); - - refresher.setRegistered(false); - spyRefresher.onApplicationEvent(null); - - ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1", "v11", ChangeType.MODIFIED); - ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED); - ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k2", "v2", null, ChangeType.DELETED); - Map changeInfos = new HashMap<>(); - changeInfos.put("k1", changeInfo); - changeInfos.put("k2", changeInfo3); - changeInfos.put("k4", changeInfo2); - ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos); - file.fireChangeListener(event); - - ContextRefresher mockContextRefresher = context.getBean(ContextRefresher.class); - when(mockContextRefresher.refresh()).thenReturn(event.changedKeys()); - - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshSpringValue("k1"); - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshSpringValue("k2"); - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshSpringValue("k4"); - Mockito.verify(spyRefresher, Mockito.times(1)) - .refreshConfigurationProperties(event.changedKeys()); - } - - @SpringBootApplication - protected static class TestApplication { - - @Primary - @Bean - public ContextRefresher contextRefresher() { - return mock(ContextRefresher.class); - } - - @Component - @RefreshScope - protected static class TestBeanWithRefreshScope { - - } - } -} diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/spring/annotation/RefreshScopeSpringProcessorTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/spring/annotation/RefreshScopeSpringProcessorTest.java new file mode 100644 index 000000000..11b17db59 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/spring/annotation/RefreshScopeSpringProcessorTest.java @@ -0,0 +1,334 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.config.spring.annotation; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Objects; + +import com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration; +import com.tencent.cloud.polaris.config.enums.RefreshType; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link SpringValueProcessor}. + * + * @author Shedfree Wu + */ +public class RefreshScopeSpringProcessorTest { + + private static ServerSocket serverSocket; + + @BeforeAll + static void beforeAll() { + new Thread(() -> { + try { + serverSocket = new ServerSocket(8093); + serverSocket.accept(); + } + catch (IOException e) { + e.printStackTrace(); + } + }).start(); + } + + @AfterAll + static void afterAll() throws IOException { + if (Objects.nonNull(serverSocket)) { + serverSocket.close(); + } + } + + @Test + public void springValueFiledProcessorTest() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PolarisConfigBootstrapAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(ValueTest.class)) + .withConfiguration(AutoConfigurations.of(TestConfig2.class)) + .withConfiguration(AutoConfigurations.of(TestConfig3.class)) + .withConfiguration(AutoConfigurations.of(TestConfig4.class)) + .withConfiguration(AutoConfigurations.of(TestConfig5.class)) + .withConfiguration(AutoConfigurations.of(TestBeanProperties1.class)) + .withConfiguration(AutoConfigurations.of(TestBeanProperties2.class)) + .withConfiguration(AutoConfigurations.of(PolarisConfigAutoConfiguration.class)) + .withAllowBeanDefinitionOverriding(true) + .withPropertyValues("spring.application.name=" + "conditionalOnConfigReflectEnabledTest") + .withPropertyValues("spring.cloud.polaris.address=grpc://127.0.0.1:10081") + .withPropertyValues("spring.cloud.polaris.config.refresh-type=" + RefreshType.REFLECT) + .withPropertyValues("spring.cloud.polaris.config.enabled=true") + .withPropertyValues("timeout=10000"); + contextRunner.run(context -> { + SpringValueRegistry springValueRegistry = context.getBean(SpringValueRegistry.class); + + assertThat(springValueRegistry.isRefreshScopeKey("key.not.exist")).isFalse(); + // @RefreshScope on @Component bean, @Value on field + assertThat(springValueRegistry.isRefreshScopeKey("timeout")).isTrue(); + // not exact match + assertThat(springValueRegistry.isRefreshScopeKey("timeout.test")).isFalse(); + // @RefreshScope on @Component bean, @Value on method + assertThat(springValueRegistry.isRefreshScopeKey("name")).isTrue(); + // @RefreshScope and @Bean on method, @Value on field + assertThat(springValueRegistry.isRefreshScopeKey("test.bean.name")).isTrue(); + // @RefreshScope and @Bean on method, @Value on method + assertThat(springValueRegistry.isRefreshScopeKey("test.bean.timeout")).isTrue(); + // @RefreshScope and @Bean on method, @Value on parameter + assertThat(springValueRegistry.isRefreshScopeKey("test.param.name")).isTrue(); + // @RefreshScope and @Bean on method, @ConfigurationProperties bean on method parameter + assertThat(springValueRegistry.isRefreshScopeKey("test.properties1.name")).isTrue(); + // @RefreshScope and @Bean on method, @ConfigurationProperties bean in class + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.name")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.inner.name")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.set")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.list")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.list[0]")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.array")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.array[0]")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.map")).isTrue(); + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.map.key")).isTrue(); + + assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.notExist")).isFalse(); + // @RefreshScope and @Bean on method, @Value bean in class + assertThat(springValueRegistry.isRefreshScopeKey("test.bean5.name")).isTrue(); + }); + } + + + @Configuration + @EnableAutoConfiguration + static class PolarisConfigAutoConfiguration { + + @Autowired + private BeanFactory beanFactory; + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + } + + @Component + @RefreshScope + private static class ValueTest { + + ValueTest() { + } + + private static String name; + @Value("${timeout:1000}") + private int timeout; + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + @Value("${name:1000}") + public void setName(String name) { + ValueTest.name = name; + } + } + + @Configuration + static class TestConfig2 { + @Bean + @RefreshScope + public TestBean testBean2() { + return new TestBean(); + } + } + + @Configuration + static class TestConfig3 { + @Bean + @RefreshScope + public TestBean testBean3(@Value("${test.param.name:}") String name) { + return new TestBean(); + } + } + + @Configuration + static class TestConfig4 { + @Bean + @RefreshScope + public TestBean testBean4(TestBeanProperties1 testBeanProperties1) { + return new TestBean(); + } + } + + @Configuration + static class TestConfig5 { + + @Autowired + private TestBeanProperties2 testBeanProperties2; + + @Value("${test.bean5.name:}") + private String name; + + @Bean + @RefreshScope + public TestBean testBean5() { + TestBean testBean = new TestBean(); + testBean.setName(testBeanProperties2.getName()); + return testBean; + } + } + + static class TestBean { + + @Value("${test.bean.name:}") + private String name; + + private int timeout; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getTimeout() { + return timeout; + } + + @Value("${test.bean.timeout:0}") + public void setTimeout(int timeout) { + this.timeout = timeout; + } + } + + @Component + @ConfigurationProperties(prefix = "test.properties1") + static class TestBeanProperties1 { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Component + @ConfigurationProperties("test.properties2") + static class TestBeanProperties2 { + private String name; + + private HashSet set; + + private ArrayList list; + + private String[] array; + + private HashMap map; + + private InnerProperties inner; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public HashSet getSet() { + return set; + } + + public void setSet(HashSet set) { + this.set = set; + } + + public ArrayList getList() { + return list; + } + + public void setList(ArrayList list) { + this.list = list; + } + + public String[] getArray() { + return array; + } + + public void setArray(String[] array) { + this.array = array; + } + + public HashMap getMap() { + return map; + } + + public void setMap(HashMap map) { + this.map = map; + } + + public InnerProperties getInner() { + return inner; + } + + public void setInner(InnerProperties inner) { + this.inner = inner; + } + } + + static class InnerProperties { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/PolarisContractReporter.java b/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/PolarisContractReporter.java index f6682b2f7..66e2e01bb 100644 --- a/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/PolarisContractReporter.java +++ b/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/PolarisContractReporter.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; +import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.util.GzipUtil; import com.tencent.cloud.polaris.PolarisDiscoveryProperties; import com.tencent.cloud.polaris.contract.config.PolarisContractProperties; @@ -100,7 +101,7 @@ else if (multipleOpenApiWebFluxResource != null) { ReportServiceContractRequest request = new ReportServiceContractRequest(); String name = polarisContractProperties.getName(); if (StringUtils.isBlank(name)) { - name = polarisDiscoveryProperties.getService(); + name = MetadataContext.LOCAL_SERVICE; } request.setName(name); request.setNamespace(polarisDiscoveryProperties.getNamespace()); diff --git a/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/filter/FilterConstant.java b/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/filter/FilterConstant.java index c97faee2c..87161748e 100644 --- a/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/filter/FilterConstant.java +++ b/spring-cloud-starter-tencent-polaris-contract/src/main/java/com/tencent/cloud/polaris/contract/filter/FilterConstant.java @@ -47,7 +47,7 @@ public final class FilterConstant { /** * Swagger resource url prefix. */ - public static final String SWAGGER_RESOURCE_PREFIX = "/swagger-resource/"; + public static final String SWAGGER_RESOURCE_PREFIX = "/swagger-resource"; /** * Swagger webjars V2 url prefix. diff --git a/spring-cloud-starter-tencent-polaris-discovery/pom.xml b/spring-cloud-starter-tencent-polaris-discovery/pom.xml index 6e0ec3146..35c87ef0d 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/pom.xml +++ b/spring-cloud-starter-tencent-polaris-discovery/pom.xml @@ -18,6 +18,12 @@ com.tencent.cloud spring-cloud-tencent-rpc-enhancement + + + spring-security-crypto + org.springframework.security + + diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/discovery/PolarisServiceDiscovery.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/discovery/PolarisServiceDiscovery.java index ee03a7d83..6ffdbe52c 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/discovery/PolarisServiceDiscovery.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/discovery/PolarisServiceDiscovery.java @@ -82,7 +82,7 @@ public List getServices() throws PolarisException { return Collections.emptyList(); } return polarisDiscoveryHandler.getServices().getServices().stream() - .map(ServiceInfo::getService).collect(Collectors.toList()); + .map(ServiceInfo::getService).distinct().collect(Collectors.toList()); } } diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/loadbalancer/PolarisLoadBalancerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/loadbalancer/PolarisLoadBalancerAutoConfiguration.java index b0cc4ebf6..33a17360d 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/loadbalancer/PolarisLoadBalancerAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/loadbalancer/PolarisLoadBalancerAutoConfiguration.java @@ -17,26 +17,15 @@ package com.tencent.cloud.polaris.loadbalancer; -import java.util.ArrayList; -import java.util.List; - import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; -import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateInterceptor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; -import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; -import org.springframework.cloud.client.loadbalancer.RestTemplateCustomizer; -import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.util.CollectionUtils; /** * Auto-configuration of loadbalancer for Polaris. @@ -52,42 +41,4 @@ @LoadBalancerClients(defaultConfiguration = PolarisLoadBalancerClientConfiguration.class) public class PolarisLoadBalancerAutoConfiguration { - @Bean - public RestTemplateCustomizer polarisRestTemplateCustomizer( - @Autowired(required = false) RetryLoadBalancerInterceptor retryLoadBalancerInterceptor, - @Autowired(required = false) LoadBalancerInterceptor loadBalancerInterceptor) { - return restTemplate -> { - List list = new ArrayList<>(restTemplate.getInterceptors()); - // LoadBalancerInterceptor must invoke before EnhancedRestTemplateInterceptor - int addIndex = list.size(); - if (CollectionUtils.containsInstance(list, retryLoadBalancerInterceptor) || CollectionUtils.containsInstance(list, loadBalancerInterceptor)) { - ClientHttpRequestInterceptor enhancedRestTemplateInterceptor = null; - for (int i = 0; i < list.size(); i++) { - if (list.get(i) instanceof EnhancedRestTemplateInterceptor) { - enhancedRestTemplateInterceptor = list.get(i); - addIndex = i; - } - } - if (enhancedRestTemplateInterceptor != null) { - list.remove(addIndex); - list.add(enhancedRestTemplateInterceptor); - } - } - else { - if (retryLoadBalancerInterceptor != null || loadBalancerInterceptor != null) { - for (int i = 0; i < list.size(); i++) { - if (list.get(i) instanceof EnhancedRestTemplateInterceptor) { - addIndex = i; - } - } - list.add(addIndex, - retryLoadBalancerInterceptor != null - ? retryLoadBalancerInterceptor - : loadBalancerInterceptor); - } - } - restTemplate.setInterceptors(list); - }; - } - } diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisRegistration.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisRegistration.java index 8a0c471fd..9a0f99936 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisRegistration.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisRegistration.java @@ -57,7 +57,7 @@ public class PolarisRegistration implements Registration { private final StaticMetadataManager staticMetadataManager; - private final String serviceId; + private String serviceId; private final String host; private final boolean isSecure; private final ServletWebServerApplicationContext servletWebServerApplicationContext; @@ -164,6 +164,10 @@ public void customize() { } } + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + @Override public String getServiceId() { return serviceId; diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisServiceRegistry.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisServiceRegistry.java index f98591bcd..71de4be33 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisServiceRegistry.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/registry/PolarisServiceRegistry.java @@ -23,6 +23,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.metadata.StaticMetadataManager; import com.tencent.cloud.common.util.OkHttpUtil; import com.tencent.cloud.common.util.OtUtils; @@ -106,6 +107,7 @@ public void register(PolarisRegistration registration) { } registration.customize(); String serviceId = registration.getServiceId(); + MetadataContext.setLocalService(serviceId); // Register instance. InstanceRegisterRequest instanceRegisterRequest = new InstanceRegisterRequest(); diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/plugin/lossless/LosslessRegistryAspectTest.java b/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/plugin/lossless/LosslessRegistryAspectTest.java index f8e1c2e98..f9a6eb77a 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/plugin/lossless/LosslessRegistryAspectTest.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/test/java/com/tencent/cloud/plugin/lossless/LosslessRegistryAspectTest.java @@ -17,7 +17,10 @@ package com.tencent.cloud.plugin.lossless; +import java.lang.reflect.Field; +import java.net.URI; import java.util.Collections; +import java.util.Map; import com.tencent.cloud.common.util.OkHttpUtil; import com.tencent.cloud.plugin.lossless.config.LosslessAutoConfiguration; @@ -28,6 +31,7 @@ import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClientConfiguration; import com.tencent.cloud.polaris.registry.PolarisRegistration; import com.tencent.cloud.polaris.registry.PolarisServiceRegistry; +import com.tencent.cloud.rpc.enhancement.transformer.RegistrationTransformer; import com.tencent.polaris.api.pojo.ServiceKey; import com.tencent.polaris.test.mock.discovery.NamingServer; import org.junit.jupiter.api.AfterAll; @@ -40,6 +44,9 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationUtils; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -106,6 +113,31 @@ public class LosslessRegistryAspectTest { .withPropertyValues("spring.cloud.polaris.discovery.namespace=" + NAMESPACE_TEST) .withPropertyValues("spring.cloud.polaris.discovery.token=xxxxxx"); + private final WebApplicationContextRunner contextRunner3 = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + MockDiscoveryConfiguration.class, + LosslessAutoConfiguration.class, + LosslessPropertiesBootstrapConfiguration.class, + PolarisContextAutoConfiguration.class, + PolarisPropertiesConfiguration.class, + PolarisDiscoveryClientConfiguration.class, + PolarisDiscoveryAutoConfiguration.class)) + .withPropertyValues("spring.cloud.nacos.discovery.enabled=false") + .withPropertyValues("spring.cloud.polaris.lossless.enabled=true") + .withPropertyValues("spring.cloud.polaris.lossless.healthCheckInterval=1000") + .withPropertyValues("spring.cloud.polaris.lossless.healthCheckPath=/test") + .withPropertyValues("spring.cloud.polaris.admin.port=28082") + .withPropertyValues("spring.application.name=" + SERVICE_PROVIDER) + .withPropertyValues("server.port=" + APPLICATION_PORT) + .withPropertyValues("spring.cloud.polaris.localIpAddress=" + HOST) + .withPropertyValues("spring.cloud.polaris.localPort=" + APPLICATION_PORT) + .withPropertyValues("spring.cloud.polaris.address=grpc://127.0.0.1:10081") + .withPropertyValues("spring.cloud.polaris.discovery.namespace=" + NAMESPACE_TEST) + .withPropertyValues("spring.cloud.polaris.discovery.token=xxxxxx") + .withPropertyValues("spring.autoconfigure.exclude=" + + "org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration"); + + @BeforeAll static void beforeAll() throws Exception { namingServer = NamingServer.startNamingServer(10081); @@ -185,9 +217,121 @@ public void testRegister2() { }); } + @Test + public void testRegister3() { + this.contextRunner3.run(context -> { + + AbstractAutoServiceRegistration autoServiceRegistration = context.getBean(AbstractAutoServiceRegistration.class); + + assertThatCode(() -> { + AutoServiceRegistrationUtils.register(autoServiceRegistration); + }).doesNotThrowAnyException(); + + Thread.sleep(2000); + + assertThatCode(() -> { + AutoServiceRegistrationUtils.deRegister(autoServiceRegistration); + }).doesNotThrowAnyException(); + + LosslessRegistryAspect losslessRegistryAspect = context.getBean(LosslessRegistryAspect.class); + Field field = LosslessRegistryAspect.class.getDeclaredField("registrationTransformer"); + field.setAccessible(true); + RegistrationTransformer registrationTransformer = (RegistrationTransformer) field.get(losslessRegistryAspect); + assertThat(registrationTransformer.getClass().getName().contains("PolarisRegistrationTransformer")); + + field = LosslessRegistryAspect.class.getDeclaredField("registration"); + field.setAccessible(true); + Registration registration = (Registration) field.get(losslessRegistryAspect); + assertThat(registration.getClass().getName().contains("PolarisRegistration")); + + field = LosslessRegistryAspect.class.getDeclaredField("serviceRegistry"); + field.setAccessible(true); + ServiceRegistry serviceRegistry = (ServiceRegistry) field.get(losslessRegistryAspect); + assertThat(serviceRegistry.getClass().getName().contains("PolarisServiceRegistry")); + }); + } + @Configuration @EnableAutoConfiguration static class PolarisPropertiesConfiguration { } + + @Configuration + static class MockDiscoveryConfiguration { + @Bean + public ServiceRegistry mockServiceRegistry() { + return new ServiceRegistry() { + @Override + public void register(Registration registration) { + + } + + @Override + public void deregister(Registration registration) { + + } + + @Override + public void close() { + + } + + @Override + public void setStatus(Registration registration, String status) { + + } + + @Override + public Object getStatus(Registration registration) { + return null; + } + }; + } + + @Bean + public Registration mockRegistration() { + return new Registration() { + @Override + public String getServiceId() { + return null; + } + + @Override + public String getHost() { + return null; + } + + @Override + public int getPort() { + return 0; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public URI getUri() { + return null; + } + + @Override + public Map getMetadata() { + return null; + } + }; + } + + @Bean + public RegistrationTransformer mockRegistrationTransformer() { + return new RegistrationTransformer() { + @Override + public String getRegistry() { + return null; + } + }; + } + } } diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java index 02a77ed76..07a13e6e2 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckReactiveFilter.java @@ -104,6 +104,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { quotaResponse = QuotaCheckUtils.getQuota(limitAPI, localNamespace, localService, 1, path); if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { + LOG.info("block by ratelimit rule, uri:{}", exchange.getRequest().getURI()); ServerHttpResponse response = exchange.getResponse(); DataBuffer dataBuffer; if (Objects.nonNull(quotaResponse.getActiveRule()) diff --git a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java index a4a312dc4..70c68dbda 100644 --- a/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java +++ b/spring-cloud-starter-tencent-polaris-ratelimit/src/main/java/com/tencent/cloud/polaris/ratelimit/filter/QuotaCheckServletFilter.java @@ -99,6 +99,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht try { quotaResponse = QuotaCheckUtils.getQuota(limitAPI, localNamespace, localService, 1, request.getRequestURI()); if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { + LOG.info("block by ratelimit rule, uri:{}", request.getRequestURI()); if (Objects.nonNull(quotaResponse.getActiveRule()) && StringUtils.isNotBlank(quotaResponse.getActiveRule().getCustomResponse().getBody())) { response.setStatus(polarisRateLimitProperties.getRejectHttpCode()); diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java index 48857ad46..370cbf519 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java @@ -31,7 +31,6 @@ import com.tencent.cloud.polaris.router.interceptor.NamespaceRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.NearbyRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.RuleBasedRouterRequestInterceptor; -import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateInterceptor; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; @@ -112,13 +111,7 @@ public RouterLabelRestTemplateInterceptor routerLabelRestTemplateInterceptor() { public SmartInitializingSingleton addRouterLabelInterceptorForRestTemplate(RouterLabelRestTemplateInterceptor interceptor) { return () -> restTemplates.forEach(restTemplate -> { List list = new ArrayList<>(restTemplate.getInterceptors()); - int addIndex = list.size(); - for (int i = 0; i < list.size(); i++) { - if (list.get(i) instanceof EnhancedRestTemplateInterceptor) { - addIndex = i; - } - } - list.add(addIndex, interceptor); + list.add(interceptor); restTemplate.setInterceptors(list); }); } diff --git a/spring-cloud-tencent-commons/pom.xml b/spring-cloud-tencent-commons/pom.xml index 59daa7efe..3c103e3ac 100644 --- a/spring-cloud-tencent-commons/pom.xml +++ b/spring-cloud-tencent-commons/pom.xml @@ -15,7 +15,7 @@ 2.6 - 2.11.0 + 2.14.0 @@ -53,11 +53,23 @@ org.springframework.boot spring-boot-starter-validation + + + tomcat-embed-el + org.apache.tomcat.embed + + org.springframework.cloud spring-cloud-starter + + + spring-security-rsa + org.springframework.security + + diff --git a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/ContextConstant.java b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/ContextConstant.java index 2de8a2c75..ee32161b0 100644 --- a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/ContextConstant.java +++ b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/ContextConstant.java @@ -43,4 +43,23 @@ public final class ContextConstant { private ContextConstant() { } + + public static final class CircuitBreaker { + /** + * polaris circuit breaker. + */ + public static final String POLARIS_CIRCUIT_BREAKER = "PolarisCircuitBreaker"; + /** + * circuit breaker start time. + */ + public static final String CIRCUIT_BREAKER_START_TIME = "CIRCUIT_BREAKER_START_TIME"; + /** + * circuit breaker fallback http response. + */ + public static final String CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE = "CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE"; + + private CircuitBreaker() { + + } + } } diff --git a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/metadata/MetadataContext.java b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/metadata/MetadataContext.java index e01e41afb..733d69057 100644 --- a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/metadata/MetadataContext.java +++ b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/metadata/MetadataContext.java @@ -319,4 +319,8 @@ public void putFragmentContext(String fragment, Map context) { break; } } + + public static void setLocalService(String service) { + LOCAL_SERVICE = service; + } } diff --git a/spring-cloud-tencent-dependencies/pom.xml b/spring-cloud-tencent-dependencies/pom.xml index 8d04a1816..da6c8c3fe 100644 --- a/spring-cloud-tencent-dependencies/pom.xml +++ b/spring-cloud-tencent-dependencies/pom.xml @@ -74,18 +74,20 @@ 2.0.1.0-2021.0.9-SNAPSHOT - 2.0.0.0 + 2.0.0.1 - 32.0.1-jre - 1.7.0 - 4.9.0 + 1.78.1 1.12.19 - 2.13.5 - 3.21.7 - 3.9.0 + 1.17.1 + 32.1.3-jre + 2.15.3 + 4.9.0 2.9.9 + 3.21.7 2.0.2 + 2.0 + 1.7.0 3.3.0 @@ -220,6 +222,12 @@ ${revision} + + com.tencent.cloud + spring-cloud-tencent-security-protection-plugin + ${revision} + + com.google.guava @@ -265,18 +273,36 @@ ${byte-buddy.version} - - com.squareup.okio - okio - ${okio.version} - - joda-time joda-time ${joda-time.version} + + org.bouncycastle + bcpkix-jdk18on + ${bcpkix-jdk18on.version} + + + + org.bouncycastle + bcprov-jdk15to18 + ${bcpkix-jdk18on.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + commons-codec + commons-codec + ${commons-codec.version} + + org.mockito mockito-inline diff --git a/spring-cloud-tencent-plugin-starters/pom.xml b/spring-cloud-tencent-plugin-starters/pom.xml index b80d06b7f..4159c7843 100644 --- a/spring-cloud-tencent-plugin-starters/pom.xml +++ b/spring-cloud-tencent-plugin-starters/pom.xml @@ -22,6 +22,7 @@ spring-cloud-starter-tencent-threadlocal-plugin spring-cloud-starter-tencent-trace-plugin spring-cloud-starter-tencent-fault-tolerance + spring-cloud-tencent-security-protection-plugin diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-discovery-adapter-plugin/src/main/java/com/tencent/cloud/plugin/discovery/adapter/config/NacosDiscoveryAdapterAutoConfiguration.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-discovery-adapter-plugin/src/main/java/com/tencent/cloud/plugin/discovery/adapter/config/NacosDiscoveryAdapterAutoConfiguration.java index 7bcfeeab1..bdd454c91 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-discovery-adapter-plugin/src/main/java/com/tencent/cloud/plugin/discovery/adapter/config/NacosDiscoveryAdapterAutoConfiguration.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-discovery-adapter-plugin/src/main/java/com/tencent/cloud/plugin/discovery/adapter/config/NacosDiscoveryAdapterAutoConfiguration.java @@ -49,7 +49,6 @@ public InstanceTransformer instanceTransformer() { } @Bean - @ConditionalOnMissingBean @ConditionalOnClass(name = "com.alibaba.cloud.nacos.registry.NacosRegistration") public RegistrationTransformer registrationTransformer() { return new NacosRegistrationTransformer(); diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-lossless-plugin/src/main/java/com/tencent/cloud/plugin/lossless/config/LosslessAutoConfiguration.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-lossless-plugin/src/main/java/com/tencent/cloud/plugin/lossless/config/LosslessAutoConfiguration.java index 4c902dd2a..37e4272d1 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-lossless-plugin/src/main/java/com/tencent/cloud/plugin/lossless/config/LosslessAutoConfiguration.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-lossless-plugin/src/main/java/com/tencent/cloud/plugin/lossless/config/LosslessAutoConfiguration.java @@ -17,6 +17,8 @@ package com.tencent.cloud.plugin.lossless.config; +import java.util.List; + import com.tencent.cloud.plugin.lossless.LosslessRegistryAspect; import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; import com.tencent.cloud.polaris.context.PolarisSDKContextManager; @@ -43,11 +45,36 @@ public class LosslessAutoConfiguration { @Bean @ConditionalOnMissingBean public LosslessRegistryAspect losslessRegistryAspect( - ServiceRegistry serviceRegistry, Registration registration, PolarisContextProperties properties, - LosslessProperties losslessProperties, PolarisSDKContextManager polarisSDKContextManager, - RegistrationTransformer registrationTransformer) { - return new LosslessRegistryAspect(serviceRegistry, registration, properties, losslessProperties, - polarisSDKContextManager, registrationTransformer); + List serviceRegistryList, List registrationList, List registrationTransformerList, + PolarisContextProperties properties, LosslessProperties losslessProperties, PolarisSDKContextManager polarisSDKContextManager) { + + ServiceRegistry targetServiceRegistry = serviceRegistryList.size() > 0 ? serviceRegistryList.get(0) : null; + Registration targetRegistration = registrationList.size() > 0 ? registrationList.get(0) : null; + RegistrationTransformer targetRegistrationTransformer = registrationTransformerList.size() > 0 ? registrationTransformerList.get(0) : null; + // if contains multiple service registry, find the polaris service registr + if (serviceRegistryList.size() > 1) { + for (ServiceRegistry serviceRegistry : serviceRegistryList) { + if (serviceRegistry.getClass().getName().contains("PolarisServiceRegistry")) { + targetServiceRegistry = serviceRegistry; + } + } + } + if (registrationList.size() > 1) { + for (Registration registration : registrationList) { + if (registration.getClass().getName().contains("PolarisRegistration")) { + targetRegistration = registration; + } + } + } + if (registrationTransformerList.size() > 1) { + for (RegistrationTransformer registrationTransformer : registrationTransformerList) { + if (registrationTransformer.getClass().getName().contains("PolarisRegistrationTransformer")) { + targetRegistrationTransformer = registrationTransformer; + } + } + } + return new LosslessRegistryAspect(targetServiceRegistry, targetRegistration, properties, losslessProperties, + polarisSDKContextManager, targetRegistrationTransformer); } } diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/pom.xml b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/pom.xml new file mode 100644 index 000000000..1674db4e8 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/pom.xml @@ -0,0 +1,59 @@ + + + + spring-cloud-tencent-plugin-starters + com.tencent.cloud + ${revision} + ../pom.xml + + 4.0.0 + + spring-cloud-tencent-security-protection-plugin + Spring Cloud Tencent Lossless Plugin + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework + spring-beans + + + + org.slf4j + slf4j-api + + + + org.springframework + spring-webmvc + true + + + + org.springframework + spring-webflux + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mockito + mockito-inline + test + + + + + diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/java/com/tencent/cloud/plugin/protection/ExitUtils.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/java/com/tencent/cloud/plugin/protection/ExitUtils.java new file mode 100644 index 000000000..2bf12db99 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/java/com/tencent/cloud/plugin/protection/ExitUtils.java @@ -0,0 +1,52 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.plugin.protection; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * ExitUtils. + * + * @author Shedfree Wu + */ +public final class ExitUtils { + + private ExitUtils() { + + } + + public static void exit(ApplicationContext context) { + exit(context, 3000); + } + + public static void exit(ApplicationContext context, int delay) { + if (context instanceof ConfigurableApplicationContext) { + ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) context; + configurableContext.close(); + } + + try { + Thread.sleep(delay); + } + catch (InterruptedException e) { + // do nothing + } + System.exit(0); + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/java/com/tencent/cloud/plugin/protection/SecurityProtectionAutoConfiguration.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/java/com/tencent/cloud/plugin/protection/SecurityProtectionAutoConfiguration.java new file mode 100644 index 000000000..589240c63 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/java/com/tencent/cloud/plugin/protection/SecurityProtectionAutoConfiguration.java @@ -0,0 +1,87 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.plugin.protection; + + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.function.RouterFunction; + +/** + * SecurityProtectionAutoConfiguration. + * + * @author Shedfree Wu + */ +@Configuration +public class SecurityProtectionAutoConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(SecurityProtectionAutoConfiguration.class); + + @Configuration + @ConditionalOnProperty(name = "spring.cloud.tencent.security.protection.servlet.enabled", matchIfMissing = true) + @ConditionalOnClass(name = {"org.springframework.web.servlet.function.RouterFunction"}) + static class ServletProtectionConfiguration implements InitializingBean { + + @Autowired(required = false) + List routerFunctions; + + @Autowired + ApplicationContext applicationContext; + + @Override + public void afterPropertiesSet() { + if (routerFunctions != null && !routerFunctions.isEmpty()) { + LOGGER.error("Detected the presence of webmvc RouterFunction-related beans, which may trigger the CVE-2024-38819 vulnerability. The program will soon exit."); + LOGGER.error("routerFunctions:{}: ", routerFunctions); + + ExitUtils.exit(applicationContext); + } + } + } + + @Configuration + @ConditionalOnProperty(name = "spring.cloud.tencent.security.protection.reactive.enabled", matchIfMissing = true) + @ConditionalOnClass(name = {"org.springframework.web.reactive.function.server.RouterFunction"}) + static class ReactiveProtectionConfiguration implements InitializingBean { + + @Autowired(required = false) + List routerFunctions; + + @Autowired + ApplicationContext applicationContext; + + @Override + public void afterPropertiesSet() { + if (routerFunctions != null && !routerFunctions.isEmpty()) { + LOGGER.error("Detected the presence of webflux RouterFunction-related beans, which may trigger the CVE-2024-38819 vulnerability. The program will soon exit."); + LOGGER.error("routerFunctions:{}: ", routerFunctions); + ExitUtils.exit(applicationContext); + } + } + } + +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/resources/META-INF/spring.factories b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..3e0f0ee08 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.tencent.cloud.plugin.protection.SecurityProtectionAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/test/java/com/tencent/cloud/plugin/protection/SecurityProtectionAutoConfigurationTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/test/java/com/tencent/cloud/plugin/protection/SecurityProtectionAutoConfigurationTest.java new file mode 100644 index 000000000..cb93c89ea --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-tencent-security-protection-plugin/src/test/java/com/tencent/cloud/plugin/protection/SecurityProtectionAutoConfigurationTest.java @@ -0,0 +1,189 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.plugin.protection; + +import java.security.Permission; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.web.servlet.function.RouterFunction; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link SecurityProtectionAutoConfiguration}. + */ +@ExtendWith(MockitoExtension.class) +class SecurityProtectionAutoConfigurationTest { + + @Mock + private ConfigurableApplicationContext applicationContext; + + @Test + void testServletProtectionNoRouterFunctions() { + // Arrange + SecurityProtectionAutoConfiguration.ServletProtectionConfiguration config = + new SecurityProtectionAutoConfiguration.ServletProtectionConfiguration(); + config.applicationContext = applicationContext; + config.routerFunctions = null; + + // Act + config.afterPropertiesSet(); + + // Verify + // Should not call exit when no RouterFunctions present + verify(applicationContext, never()).close(); + } + + @Test + void testServletProtectionEmptyRouterFunctions() { + // Arrange + SecurityProtectionAutoConfiguration.ServletProtectionConfiguration config = + new SecurityProtectionAutoConfiguration.ServletProtectionConfiguration(); + config.applicationContext = applicationContext; + config.routerFunctions = new ArrayList<>(); + + // Act + config.afterPropertiesSet(); + + // Verify + // Should not call exit when RouterFunctions list is empty + verify(applicationContext, never()).close(); + } + + @Test + void testServletProtectionWithRouterFunctions() { + // Arrange + SecurityProtectionAutoConfiguration.ServletProtectionConfiguration config = + new SecurityProtectionAutoConfiguration.ServletProtectionConfiguration(); + config.applicationContext = mock(ConfigurableApplicationContext.class); + List routerFunctions = new ArrayList<>(); + routerFunctions.add(mock(RouterFunction.class)); + config.routerFunctions = routerFunctions; + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + config.afterPropertiesSet(); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + @Test + void testReactiveProtectionNoRouterFunctions() { + // Arrange + SecurityProtectionAutoConfiguration.ReactiveProtectionConfiguration config = + new SecurityProtectionAutoConfiguration.ReactiveProtectionConfiguration(); + config.applicationContext = applicationContext; + config.routerFunctions = null; + + // Act + config.afterPropertiesSet(); + + // Verify + verify(applicationContext, never()).close(); + } + + @Test + void testReactiveProtectionEmptyRouterFunctions() { + // Arrange + SecurityProtectionAutoConfiguration.ReactiveProtectionConfiguration config = + new SecurityProtectionAutoConfiguration.ReactiveProtectionConfiguration(); + config.applicationContext = applicationContext; + config.routerFunctions = new ArrayList<>(); + + // Act + config.afterPropertiesSet(); + + // Verify + verify(applicationContext, never()).close(); + } + + @Test + void testReactiveProtectionWithRouterFunctions() { + // Arrange + SecurityProtectionAutoConfiguration.ReactiveProtectionConfiguration config = + new SecurityProtectionAutoConfiguration.ReactiveProtectionConfiguration(); + config.applicationContext = mock(ConfigurableApplicationContext.class); + List routerFunctions = new ArrayList<>(); + routerFunctions.add(mock(org.springframework.web.reactive.function.server.RouterFunction.class)); + config.routerFunctions = routerFunctions; + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + config.afterPropertiesSet(); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + @Test + void testInterruptedExceptionHandling() throws InterruptedException { + // Arrange + ConfigurableApplicationContext mockContext = mock(ConfigurableApplicationContext.class); + Thread testThread = new Thread(() -> ExitUtils.exit(mockContext, 3000)); + + + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + // Act + testThread.start(); + testThread.interrupt(); + Thread.sleep(6000); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + public static class ExitSecurityManager extends SecurityManager { + + @Override + public void checkPermission(Permission perm) { + if (perm.getName().contains("exitVM")) { + throw new SecurityException("System.exit is not allowed"); + } + } + } +} diff --git a/spring-cloud-tencent-polaris-context/pom.xml b/spring-cloud-tencent-polaris-context/pom.xml index 491ef31b9..4d43b7008 100644 --- a/spring-cloud-tencent-polaris-context/pom.xml +++ b/spring-cloud-tencent-polaris-context/pom.xml @@ -174,6 +174,16 @@ com.tencent.polaris registry-memory + + + error_prone_annotations + com.google.errorprone + + + j2objc-annotations + com.google.j2objc + + diff --git a/spring-cloud-tencent-polaris-context/src/main/java/com/tencent/cloud/polaris/context/CircuitBreakerStatusCodeException.java b/spring-cloud-tencent-polaris-context/src/main/java/com/tencent/cloud/polaris/context/CircuitBreakerStatusCodeException.java new file mode 100644 index 000000000..d756714e4 --- /dev/null +++ b/spring-cloud-tencent-polaris-context/src/main/java/com/tencent/cloud/polaris/context/CircuitBreakerStatusCodeException.java @@ -0,0 +1,34 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.context; + +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpStatusCodeException; + +/** + * CircuitBreaker HttpStatusCodeException. + * + * @author Shedfree Wu + */ +public class CircuitBreakerStatusCodeException extends HttpStatusCodeException { + + public CircuitBreakerStatusCodeException(HttpStatus statusCode) { + super(statusCode); + } + +} diff --git a/spring-cloud-tencent-polaris-context/src/main/java/com/tencent/cloud/polaris/context/listener/FailedEventApplicationListener.java b/spring-cloud-tencent-polaris-context/src/main/java/com/tencent/cloud/polaris/context/listener/FailedEventApplicationListener.java new file mode 100644 index 000000000..b963141e3 --- /dev/null +++ b/spring-cloud-tencent-polaris-context/src/main/java/com/tencent/cloud/polaris/context/listener/FailedEventApplicationListener.java @@ -0,0 +1,69 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.context.listener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeansException; +import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; + +/** + * Failed event listener. + * + * @author skyehtzhang + */ +@Configuration +public class FailedEventApplicationListener implements ApplicationListener, ApplicationContextAware { + + private static final Logger logger = LoggerFactory.getLogger(FailedEventApplicationListener.class); + + private ApplicationContext applicationContext; + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ApplicationFailedEvent) { + ApplicationFailedEvent failedEvent = (ApplicationFailedEvent) event; + if (failedEvent.getException() != null) { + logger.error("[onApplicationEvent] exception in failed event", failedEvent.getException()); + } + + if (applicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) applicationContext).close(); + } + try { + Thread.sleep(3000); + } + catch (InterruptedException e) { + // do nothing + } + System.exit(0); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/spring-cloud-tencent-polaris-context/src/main/resources/META-INF/spring.factories b/spring-cloud-tencent-polaris-context/src/main/resources/META-INF/spring.factories index ac093c297..7751f48cc 100644 --- a/spring-cloud-tencent-polaris-context/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-tencent-polaris-context/src/main/resources/META-INF/spring.factories @@ -6,7 +6,8 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.tencent.cloud.polaris.context.config.PolarisContextBootstrapAutoConfiguration,\ com.tencent.cloud.polaris.context.config.extend.tsf.TsfContextBootstrapConfiguration org.springframework.context.ApplicationListener=\ - com.tencent.cloud.polaris.context.logging.PolarisLoggingApplicationListener + com.tencent.cloud.polaris.context.logging.PolarisLoggingApplicationListener,\ + com.tencent.cloud.polaris.context.listener.FailedEventApplicationListener org.springframework.boot.env.EnvironmentPostProcessor=\ com.tencent.cloud.polaris.context.config.PolarisContextEnvironmentPostProcessor,\ com.tencent.cloud.polaris.context.config.extend.tsf.TsfCoreEnvironmentPostProcessor diff --git a/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/listener/FailedEventApplicationListenerTest.java b/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/listener/FailedEventApplicationListenerTest.java new file mode 100644 index 000000000..8e38f769f --- /dev/null +++ b/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/listener/FailedEventApplicationListenerTest.java @@ -0,0 +1,154 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.context.listener; + +import java.security.Permission; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.mockito.Mockito.when; + + +class FailedEventApplicationListenerTest { + + @Mock + private ConfigurableApplicationContext mockConfigurableContext; + + @Mock + private ApplicationContext mockApplicationContext; + + @Mock + private ApplicationFailedEvent mockFailedEvent; + + private FailedEventApplicationListener listener; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + listener = new FailedEventApplicationListener(); + } + + @Test + void testSetApplicationContext() { + // Test setting application context + listener.setApplicationContext(mockApplicationContext); + } + + @Test + void testOnApplicationEventWithConfigurableContext() { + // Arrange + listener.setApplicationContext(mockConfigurableContext); + when(mockFailedEvent.getException()).thenReturn(new RuntimeException("Test Exception")); + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + // Act + listener.onApplicationEvent(mockFailedEvent); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + @Test + void testOnApplicationEventWithNonConfigurableContext() { + // Arrange + listener.setApplicationContext(mockApplicationContext); + when(mockFailedEvent.getException()).thenReturn(new RuntimeException("Test Exception")); + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + // Act + listener.onApplicationEvent(mockFailedEvent); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + @Test + void testOnApplicationEventWithInterruptedException() { + // Arrange + listener.setApplicationContext(mockConfigurableContext); + Thread.currentThread().interrupt(); // Simulate interruption + + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + // Act + listener.onApplicationEvent(mockFailedEvent); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + @Test + void testOnApplicationEventWithNullException() { + // Arrange + listener.setApplicationContext(mockConfigurableContext); + when(mockFailedEvent.getException()).thenReturn(null); + + + SecurityManager originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new ExitSecurityManager()); + + try { + // Act + listener.onApplicationEvent(mockFailedEvent); + } + catch (SecurityException e) { + // Ignore + } + finally { + System.setSecurityManager(originalSecurityManager); + } + } + + public static class ExitSecurityManager extends SecurityManager { + + @Override + public void checkPermission(Permission perm) { + if (perm.getName().contains("exitVM")) { + throw new SecurityException("System.exit is not allowed"); + } + } + } +} diff --git a/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/logging/PolarisLoggingPathSystemPropertyTest.java b/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/logging/PolarisLoggingPathSystemPropertyTest.java index a7539e6f7..44069f621 100644 --- a/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/logging/PolarisLoggingPathSystemPropertyTest.java +++ b/spring-cloud-tencent-polaris-context/src/test/java/com/tencent/cloud/polaris/context/logging/PolarisLoggingPathSystemPropertyTest.java @@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Test for {@link PolarisLoggingApplicationListener} + * Test for {@link PolarisLoggingApplicationListener}. * * @author wenxuan70 */ diff --git a/spring-cloud-tencent-rpc-enhancement/pom.xml b/spring-cloud-tencent-rpc-enhancement/pom.xml index 4cc4ab831..b21f18c56 100644 --- a/spring-cloud-tencent-rpc-enhancement/pom.xml +++ b/spring-cloud-tencent-rpc-enhancement/pom.xml @@ -37,6 +37,12 @@ org.springframework.boot spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-logging + + @@ -80,6 +86,12 @@ test + + io.projectreactor + reactor-test + test + + org.mockito mockito-inline diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java index 52ad1d56f..7b4c7013f 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java @@ -27,7 +27,6 @@ import com.tencent.cloud.rpc.enhancement.instrument.feign.EnhancedLoadBalancerClientAspect; import com.tencent.cloud.rpc.enhancement.instrument.filter.EnhancedReactiveFilter; import com.tencent.cloud.rpc.enhancement.instrument.filter.EnhancedServletFilter; -import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateInterceptor; import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisLoadBalancerRequestTransformer; import com.tencent.cloud.rpc.enhancement.instrument.scg.EnhancedGatewayGlobalFilter; import com.tencent.cloud.rpc.enhancement.instrument.webclient.EnhancedWebClientExchangeFilterFunction; @@ -89,7 +88,6 @@ public InstanceTransformer instanceTransformer() { } @Bean - @ConditionalOnMissingBean @ConditionalOnClass(name = "com.tencent.cloud.polaris.registry.PolarisRegistration") public RegistrationTransformer registrationTransformer() { return new PolarisRegistrationTransformer(); @@ -99,9 +97,9 @@ public RegistrationTransformer registrationTransformer() { @Lazy public EnhancedPluginRunner enhancedFeignPluginRunner( @Autowired(required = false) List enhancedPlugins, - @Autowired(required = false) Registration registration, + @Autowired(required = false) List registrations, PolarisSDKContextManager polarisSDKContextManager) { - return new DefaultEnhancedPluginRunner(enhancedPlugins, registration, polarisSDKContextManager.getSDKContext()); + return new DefaultEnhancedPluginRunner(enhancedPlugins, registrations, polarisSDKContextManager.getSDKContext()); } @Bean @@ -183,23 +181,6 @@ public EnhancedLoadBalancerClientAspect enhancedLoadBalancerClientAspect() { @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") protected static class PolarisRestTemplateAutoConfiguration { - @Autowired(required = false) - private List restTemplates = Collections.emptyList(); - - @Bean - public EnhancedRestTemplateInterceptor enhancedPolarisRestTemplateReporter(@Lazy EnhancedPluginRunner pluginRunner) { - return new EnhancedRestTemplateInterceptor(pluginRunner); - } - - @Bean - public SmartInitializingSingleton setPolarisReporterForRestTemplate(EnhancedRestTemplateInterceptor reporter) { - return () -> { - for (RestTemplate restTemplate : restTemplates) { - restTemplate.getInterceptors().add(reporter); - } - }; - } - @Bean @ConditionalOnMissingBean @ConditionalOnClass(name = {"org.springframework.cloud.client.loadbalancer.LoadBalancerRequestTransformer"}) diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClient.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClient.java index e4c2d1a5b..ad60f858d 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClient.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClient.java @@ -19,16 +19,24 @@ import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import feign.Client; import feign.Request; import feign.Request.Options; +import feign.RequestTemplate; import feign.Response; import org.springframework.cloud.client.DefaultServiceInstance; @@ -61,16 +69,19 @@ public Response execute(Request request, Options options) throws IOException { request.headers().forEach((s, strings) -> requestHeaders.addAll(s, new ArrayList<>(strings))); URI url = URI.create(request.url()); + URI serviceUrl = url.resolve(request.requestTemplate().url()); + EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder() .httpHeaders(requestHeaders) .httpMethod(HttpMethod.valueOf(request.httpMethod().name())) .url(url) + .serviceUrl(serviceUrl) .build(); enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setOriginRequest(request); enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance()); - String svcName = request.requestTemplate().feignTarget().name(); + String svcName = serviceUrl.getHost(); DefaultServiceInstance serviceInstance = new DefaultServiceInstance( String.format("%s-%s-%d", svcName, url.getHost(), url.getPort()), svcName, url.getHost(), url.getPort(), url.getScheme().equals("https")); @@ -82,11 +93,11 @@ public Response execute(Request request, Options options) throws IOException { enhancedPluginContext.setTargetServiceInstance(serviceInstance, url); } - // Run pre enhanced plugins. - pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); - long startMillis = System.currentTimeMillis(); try { + // Run pre enhanced plugins. + pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + Response response = delegate.execute(request, options); enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis); @@ -103,6 +114,15 @@ public Response execute(Request request, Options options) throws IOException { pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext); return response; } + catch (CallAbortedException callAbortedException) { + // circuit breaker fallback, not need to run post/exception enhanced plugins. + if (callAbortedException.getFallbackInfo() != null) { + return getFallbackResponse(callAbortedException.getFallbackInfo()); + } + else { + throw callAbortedException; + } + } catch (IOException origin) { enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis); enhancedPluginContext.setThrowable(origin); @@ -115,4 +135,25 @@ public Response execute(Request request, Options options) throws IOException { pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext); } } + + private Response getFallbackResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) { + + Response.Builder responseBuilder = Response.builder() + .status(fallbackInfo.getCode()); + if (fallbackInfo.getHeaders() != null) { + Map> headers = new HashMap<>(); + fallbackInfo.getHeaders().forEach((k, v) -> headers.put(k, Collections.singleton(v))); + responseBuilder.headers(headers); + } + if (fallbackInfo.getBody() != null) { + responseBuilder.body(fallbackInfo.getBody(), StandardCharsets.UTF_8); + } + // Feign Response need a nonnull Request, + // which is not important in fallback response (no real request), + // so we create a fake one + Request fakeRequest = Request.create(Request.HttpMethod.GET, "/", new HashMap<>(), Request.Body.empty(), new RequestTemplate()); + responseBuilder.request(fakeRequest); + + return responseBuilder.build(); + } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptor.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java similarity index 58% rename from spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptor.java rename to spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java index 5f739118c..b30c7835a 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptor.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java @@ -18,18 +18,25 @@ package com.tencent.cloud.rpc.enhancement.instrument.resttemplate; import java.io.IOException; +import java.net.URI; +import java.util.Optional; +import com.tencent.cloud.common.constant.ContextConstant; import com.tencent.cloud.common.metadata.MetadataContextHolder; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; +import com.tencent.polaris.metadata.core.MetadataObjectValue; +import com.tencent.polaris.metadata.core.MetadataType; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; +import org.springframework.cloud.client.loadbalancer.ServiceRequestWrapper; import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import static com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisLoadBalancerRequestTransformer.LOAD_BALANCER_SERVICE_INSTANCE; @@ -39,37 +46,67 @@ * * @author sean yu */ -public class EnhancedRestTemplateInterceptor implements ClientHttpRequestInterceptor { +public class EnhancedRestTemplateWrapInterceptor { private final EnhancedPluginRunner pluginRunner; - public EnhancedRestTemplateInterceptor(EnhancedPluginRunner pluginRunner) { + private final LoadBalancerClient delegate; + + public EnhancedRestTemplateWrapInterceptor(EnhancedPluginRunner pluginRunner, LoadBalancerClient delegate) { this.pluginRunner = pluginRunner; + this.delegate = delegate; } - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + + public ClientHttpResponse intercept(HttpRequest request, String serviceId, + LoadBalancerRequest loadBalancerRequest) throws IOException { EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext(); + URI serviceUrl = request.getURI(); + if (request instanceof ServiceRequestWrapper) { + serviceUrl = ((ServiceRequestWrapper) request).getRequest().getURI(); + } + EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder() .httpHeaders(request.getHeaders()) .httpMethod(request.getMethod()) .url(request.getURI()) + .serviceUrl(serviceUrl) .build(); enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setOriginRequest(request); enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance()); - enhancedPluginContext.setTargetServiceInstance((ServiceInstance) MetadataContextHolder.get() - .getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), request.getURI()); + // Run pre enhanced plugins. - pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + try { + pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + } + catch (CallAbortedException callAbortedException) { + MetadataObjectValue fallbackResponseValue = MetadataContextHolder.get(). + getMetadataContainer(MetadataType.APPLICATION, true). + getMetadataValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE); + + boolean existFallback = Optional.ofNullable(fallbackResponseValue). + map(MetadataObjectValue::getObjectValue).map(Optional::isPresent).orElse(false); + + if (existFallback) { + Object fallbackResponse = fallbackResponseValue.getObjectValue().orElse(null); + if (fallbackResponse instanceof ClientHttpResponse) { + return (ClientHttpResponse) fallbackResponse; + } + } + throw callAbortedException; + } long startMillis = System.currentTimeMillis(); try { - ClientHttpResponse response = execution.execute(request, body); + ClientHttpResponse response = delegate.execute(serviceId, loadBalancerRequest); + // get target instance after execute + enhancedPluginContext.setTargetServiceInstance((ServiceInstance) MetadataContextHolder.get() + .getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), request.getURI()); enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis); EnhancedResponseContext enhancedResponseContext = EnhancedResponseContext.builder() @@ -80,6 +117,8 @@ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttp // Run post enhanced plugins. pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext); + + return response; } catch (IOException e) { diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java index 1cd2748b5..e26ccc1e0 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java @@ -18,6 +18,8 @@ package com.tencent.cloud.rpc.enhancement.instrument.scg; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Optional; import com.tencent.cloud.common.constant.OrderConstant; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; @@ -25,15 +27,24 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; +import com.tencent.polaris.api.utils.CollectionUtils; +import com.tencent.polaris.api.utils.StringUtils; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import reactor.core.publisher.Mono; import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.Response; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.route.Route; import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.server.ServerWebExchange; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; @@ -52,18 +63,40 @@ public EnhancedGatewayGlobalFilter(EnhancedPluginRunner pluginRunner) { @Override public Mono filter(ServerWebExchange originExchange, GatewayFilterChain chain) { + Response serviceInstanceResponse = originExchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR); + String serviceId = Optional.ofNullable(serviceInstanceResponse).map(Response::getServer). + map(ServiceInstance::getServiceId).orElse(null); + EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext(); EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder() .httpHeaders(originExchange.getRequest().getHeaders()) .httpMethod(originExchange.getRequest().getMethod()) .url(originExchange.getRequest().getURI()) + .serviceUrl(getServiceUri(originExchange, serviceId)) .build(); enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setOriginRequest(originExchange); // Run pre enhanced plugins. - pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + try { + pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + } + catch (CallAbortedException e) { + if (e.getFallbackInfo() == null) { + throw e; + } + // circuit breaker fallback, not need to run post/exception enhanced plugins. + ServerHttpResponse response = originExchange.getResponse(); + HttpStatus httpStatus = HttpStatus.resolve(e.getFallbackInfo().getCode()); + response.setStatusCode(httpStatus != null ? httpStatus : HttpStatus.INTERNAL_SERVER_ERROR); + if (CollectionUtils.isNotEmpty(e.getFallbackInfo().getHeaders())) { + e.getFallbackInfo().getHeaders().forEach(response.getHeaders()::set); + } + String body = Optional.of(e.getFallbackInfo().getBody()).orElse(""); + DataBuffer dataBuffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(dataBuffer)); + } // Exchange may be changed in plugin ServerWebExchange exchange = (ServerWebExchange) enhancedPluginContext.getOriginRequest(); long startTime = System.currentTimeMillis(); @@ -73,9 +106,9 @@ public Mono filter(ServerWebExchange originExchange, GatewayFilterChain ch URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); enhancedPluginContext.getRequest().setUrl(uri); if (uri != null) { - if (route != null && route.getUri().getScheme().contains("lb")) { + if (route != null && route.getUri().getScheme().contains("lb") && StringUtils.isNotEmpty(serviceId)) { DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); - serviceInstance.setServiceId(route.getUri().getHost()); + serviceInstance.setServiceId(serviceId); serviceInstance.setHost(uri.getHost()); serviceInstance.setPort(uri.getPort()); enhancedPluginContext.setTargetServiceInstance(serviceInstance, null); @@ -113,4 +146,19 @@ public Mono filter(ServerWebExchange originExchange, GatewayFilterChain ch public int getOrder() { return OrderConstant.Client.Scg.ENHANCED_FILTER_ORDER; } + + private URI getServiceUri(ServerWebExchange originExchange, String serviceId) { + URI uri = originExchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); + if (StringUtils.isEmpty(serviceId) || uri == null) { + return null; + } + try { + return new URI(originExchange.getRequest().getURI().getScheme(), + serviceId, uri.getPath(), + originExchange.getRequest().getURI().getRawQuery()); + } + catch (Exception e) { + return null; + } + } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunction.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunction.java index e8d8f1231..73008f51b 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunction.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunction.java @@ -17,15 +17,22 @@ package com.tencent.cloud.rpc.enhancement.instrument.webclient; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + import com.tencent.cloud.common.metadata.MetadataContextHolder; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; +import com.tencent.polaris.api.utils.CollectionUtils; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import reactor.core.publisher.Mono; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; @@ -53,6 +60,7 @@ public Mono filter(ClientRequest originRequest, ExchangeFunction .httpHeaders(originRequest.headers()) .httpMethod(originRequest.method()) .url(originRequest.url()) + .serviceUrl(getServiceUri(originRequest)) .build(); enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setOriginRequest(originRequest); @@ -62,7 +70,21 @@ public Mono filter(ClientRequest originRequest, ExchangeFunction .getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), originRequest.url()); // Run post enhanced plugins. - pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + try { + pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); + } + catch (CallAbortedException e) { + if (e.getFallbackInfo() == null) { + throw e; + } + HttpStatus httpStatus = HttpStatus.resolve(e.getFallbackInfo().getCode()); + ClientResponse.Builder responseBuilder = ClientResponse.create(httpStatus != null ? httpStatus : HttpStatus.INTERNAL_SERVER_ERROR) + .body(Optional.of(e.getFallbackInfo().getBody()).orElse("")); + if (CollectionUtils.isNotEmpty(e.getFallbackInfo().getHeaders())) { + e.getFallbackInfo().getHeaders().forEach(responseBuilder::header); + } + return Mono.just(responseBuilder.build()); + } // request may be changed by plugin ClientRequest request = (ClientRequest) enhancedPluginContext.getOriginRequest(); long startTime = System.currentTimeMillis(); @@ -91,4 +113,21 @@ public Mono filter(ClientRequest originRequest, ExchangeFunction pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext); }); } + + private URI getServiceUri(ClientRequest clientRequest) { + Object instance = MetadataContextHolder.get() + .getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE); + if (!(instance instanceof ServiceInstance)) { + return null; + } + ServiceInstance serviceInstance = (ServiceInstance) instance; + URI uri = clientRequest.url(); + + try { + return new URI(uri.getScheme(), serviceInstance.getServiceId(), uri.getPath(), uri.getRawQuery()); + } + catch (URISyntaxException e) { + return null; + } + } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunner.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunner.java index f46596991..64538a10f 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunner.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunner.java @@ -23,6 +23,7 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import com.tencent.polaris.client.api.SDKContext; import org.springframework.cloud.client.DefaultServiceInstance; @@ -41,6 +42,16 @@ public class DefaultEnhancedPluginRunner implements EnhancedPluginRunner { private final ServiceInstance localServiceInstance; + public DefaultEnhancedPluginRunner( + List enhancedPlugins, + List registration, + SDKContext sdkContext + ) { + this(enhancedPlugins, + getPolarisRegistration(registration), + sdkContext); + } + public DefaultEnhancedPluginRunner( List enhancedPlugins, Registration registration, @@ -74,6 +85,9 @@ public void run(EnhancedPluginType pluginType, EnhancedPluginContext context) { try { plugin.run(context); } + catch (CallAbortedException callAbortedException) { + throw callAbortedException; + } catch (Throwable throwable) { plugin.handlerThrowable(context, throwable); } @@ -85,4 +99,16 @@ public ServiceInstance getLocalServiceInstance() { return this.localServiceInstance; } + static Registration getPolarisRegistration(List registration) { + + if (CollectionUtils.isEmpty(registration)) { + return null; + } + for (Registration reg : registration) { + if ("com.tencent.cloud.polaris.registry.PolarisRegistration".equals(reg.getClass().getName())) { + return reg; + } + } + return registration.get(0); + } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedRequestContext.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedRequestContext.java index 086c14fcc..e17656487 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedRequestContext.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedRequestContext.java @@ -35,6 +35,8 @@ public class EnhancedRequestContext { private URI url; + private URI serviceUrl; + public HttpMethod getHttpMethod() { return httpMethod; } @@ -59,6 +61,14 @@ public void setUrl(URI url) { this.url = url; } + public URI getServiceUrl() { + return serviceUrl; + } + + public void setServiceUrl(URI serviceUrl) { + this.serviceUrl = serviceUrl; + } + public static EnhancedContextRequestBuilder builder() { return new EnhancedContextRequestBuilder(); } @@ -69,6 +79,7 @@ public String toString() { "httpMethod=" + httpMethod + ", httpHeaders=" + httpHeaders + ", url=" + url + + ", serviceUrl=" + serviceUrl + '}'; } @@ -77,6 +88,8 @@ public static final class EnhancedContextRequestBuilder { private HttpHeaders httpHeaders; private URI url; + private URI serviceUrl; + private EnhancedContextRequestBuilder() { } @@ -95,11 +108,17 @@ public EnhancedContextRequestBuilder url(URI url) { return this; } + public EnhancedContextRequestBuilder serviceUrl(URI serviceUrl) { + this.serviceUrl = serviceUrl; + return this; + } + public EnhancedRequestContext build() { EnhancedRequestContext enhancedRequestContext = new EnhancedRequestContext(); enhancedRequestContext.httpMethod = this.httpMethod; enhancedRequestContext.url = this.url; enhancedRequestContext.httpHeaders = this.httpHeaders; + enhancedRequestContext.serviceUrl = this.serviceUrl; return enhancedRequestContext; } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/transformer/InstanceTransformer.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/transformer/InstanceTransformer.java index 500664d93..77fa49edc 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/transformer/InstanceTransformer.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/transformer/InstanceTransformer.java @@ -20,6 +20,7 @@ import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.polaris.api.pojo.DefaultInstance; import com.tencent.polaris.api.pojo.Instance; +import com.tencent.polaris.api.utils.StringUtils; import org.springframework.cloud.client.ServiceInstance; @@ -41,10 +42,18 @@ default void transformDefault(DefaultInstance instance, ServiceInstance serviceI instance.setNamespace(MetadataContext.LOCAL_NAMESPACE); instance.setService(serviceInstance.getServiceId()); instance.setProtocol(serviceInstance.getScheme()); - instance.setId(serviceInstance.getInstanceId()); instance.setHost(serviceInstance.getHost()); instance.setPort(serviceInstance.getPort()); instance.setMetadata(serviceInstance.getMetadata()); + instance.setWeight(100); + instance.setHealthy(true); + + if (StringUtils.isNotEmpty(serviceInstance.getInstanceId())) { + instance.setId(serviceInstance.getInstanceId()); + } + else { + instance.setId(serviceInstance.getHost() + ":" + serviceInstance.getPort()); + } } void transformCustom(DefaultInstance instance, ServiceInstance serviceInstance); diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfigurationTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfigurationTest.java index dd3911967..30eb27b88 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfigurationTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfigurationTest.java @@ -19,7 +19,6 @@ import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; import com.tencent.cloud.rpc.enhancement.instrument.feign.EnhancedFeignBeanPostProcessor; -import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateInterceptor; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.reporter.ExceptionPolarisReporter; import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter; @@ -58,7 +57,6 @@ public void testDefaultInitialization() { assertThat(context).hasSingleBean(EnhancedFeignBeanPostProcessor.class); assertThat(context).hasSingleBean(SuccessPolarisReporter.class); assertThat(context).hasSingleBean(ExceptionPolarisReporter.class); - assertThat(context).hasSingleBean(EnhancedRestTemplateInterceptor.class); assertThat(context).hasSingleBean(RestTemplate.class); }); } diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClientTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClientTest.java index 8cf85b46a..ca4bab8d9 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClientTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/feign/EnhancedFeignClientTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import com.tencent.cloud.polaris.context.PolarisSDKContextManager; @@ -27,6 +28,8 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import feign.Client; import feign.Request; import feign.RequestTemplate; @@ -81,7 +84,7 @@ public void testConstructor() { List enhancedPlugins = getMockEnhancedFeignPlugins(); try { new EnhancedFeignClient(mock(Client.class), - new DefaultEnhancedPluginRunner(enhancedPlugins, null, polarisSDKContextManager.getSDKContext())); + new DefaultEnhancedPluginRunner(enhancedPlugins, new ArrayList<>(), polarisSDKContextManager.getSDKContext())); } catch (Throwable e) { fail("Exception encountered.", e); @@ -111,7 +114,7 @@ else if (request.httpMethod().equals(Request.HttpMethod.POST)) { requestTemplate.feignTarget(target); EnhancedFeignClient polarisFeignClient = new EnhancedFeignClient(delegate, - new DefaultEnhancedPluginRunner(getMockEnhancedFeignPlugins(), null, polarisSDKContextManager.getSDKContext())); + new DefaultEnhancedPluginRunner(getMockEnhancedFeignPlugins(), new ArrayList<>(), polarisSDKContextManager.getSDKContext())); // 200 Response response = polarisFeignClient.execute(Request.create(Request.HttpMethod.GET, "http://localhost:8080/test", @@ -135,6 +138,52 @@ else if (request.httpMethod().equals(Request.HttpMethod.POST)) { } } + @Test + public void testExecuteCallAbortedException() throws IOException { + // mock Client.class + Client delegate = mock(Client.class); + doAnswer(invocation -> { + Request request = invocation.getArgument(0); + if (request.httpMethod().equals(Request.HttpMethod.GET)) { + return Response.builder().request(request).status(200).build(); + } + else if (request.httpMethod().equals(Request.HttpMethod.POST)) { + return Response.builder().request(request).status(502).build(); + } + throw new IOException("Mock exception."); + }).when(delegate).execute(any(Request.class), nullable(Request.Options.class)); + + // mock target + Target target = Target.EmptyTarget.create(Object.class); + + // mock RequestTemplate.class + RequestTemplate requestTemplate = new RequestTemplate(); + requestTemplate.feignTarget(target); + + EnhancedFeignClient polarisFeignClient = new EnhancedFeignClient(delegate, + new DefaultEnhancedPluginRunner(getMockCallAbortedExceptionEnhancedFeignPlugins(null), new ArrayList<>(), polarisSDKContextManager.getSDKContext())); + + // Exception + try { + polarisFeignClient.execute(Request.create(Request.HttpMethod.GET, "http://localhost:8080/test", + Collections.emptyMap(), null, requestTemplate), null); + fail("CallAbortedException should be thrown."); + } + catch (CallAbortedException t) { + assertThat(t).isInstanceOf(CallAbortedException.class); + assertThat(t.getMessage()).contains("Mock CallAbortedException"); + } + + polarisFeignClient = new EnhancedFeignClient(delegate, + new DefaultEnhancedPluginRunner(getMockCallAbortedExceptionEnhancedFeignPlugins( + new CircuitBreakerStatus.FallbackInfo(200, new HashMap<>(), "mock ok")), new ArrayList<>(), polarisSDKContextManager.getSDKContext())); + + // fallback 200 + Response response = polarisFeignClient.execute(Request.create(Request.HttpMethod.GET, "http://localhost:8080/test", + Collections.emptyMap(), null, requestTemplate), null); + assertThat(response.status()).isEqualTo(200); + } + private List getMockEnhancedFeignPlugins() { List enhancedPlugins = new ArrayList<>(); @@ -230,6 +279,30 @@ public int getOrder() { } + private List getMockCallAbortedExceptionEnhancedFeignPlugins(CircuitBreakerStatus.FallbackInfo fallbackInfo) { + List enhancedPlugins = new ArrayList<>(); + + enhancedPlugins.add(new EnhancedPlugin() { + + @Override + public int getOrder() { + return 0; + } + + @Override + public EnhancedPluginType getType() { + return EnhancedPluginType.Client.PRE; + } + + @Override + public void run(EnhancedPluginContext context) throws Throwable { + throw new CallAbortedException("Mock CallAbortedException", fallbackInfo); + } + }); + + return enhancedPlugins; + } + @SpringBootApplication protected static class TestApplication { diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptorTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptorTest.java deleted file mode 100644 index 7da97785b..000000000 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptorTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making spring-cloud-tencent available. - * - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * Unless required by applicable law or agreed to in writing, software distributed - * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.tencent.cloud.rpc.enhancement.instrument.resttemplate; - -import java.io.IOException; -import java.net.SocketTimeoutException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; - -import com.tencent.cloud.common.metadata.MetadataContext; -import com.tencent.cloud.common.metadata.StaticMetadataManager; -import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; -import com.tencent.cloud.common.util.ApplicationContextAwareUtils; -import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; -import com.tencent.cloud.rpc.enhancement.plugin.DefaultEnhancedPluginRunner; -import com.tencent.polaris.client.api.SDKContext; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.cloud.client.serviceregistry.Registration; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpResponse; - -import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; -import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; - -@ExtendWith(MockitoExtension.class) -public class EnhancedRestTemplateInterceptorTest { - - private static MockedStatic mockedApplicationContextAwareUtils; - @Mock - Registration registration; - @Mock - private RpcEnhancementReporterProperties reporterProperties; - @Mock - private SDKContext sdkContext; - @Mock - private ClientHttpRequestExecution mockClientHttpRequestExecution; - @Mock - private ClientHttpResponse mockClientHttpResponse; - @Mock - private HttpRequest mockHttpRequest; - @Mock - private HttpHeaders mockHttpHeaders; - - @BeforeAll - static void beforeAll() { - mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); - mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) - .thenReturn("unit-test"); - ApplicationContext applicationContext = mock(ApplicationContext.class); - MetadataLocalProperties metadataLocalProperties = mock(MetadataLocalProperties.class); - StaticMetadataManager staticMetadataManager = mock(StaticMetadataManager.class); - doReturn(metadataLocalProperties).when(applicationContext).getBean(MetadataLocalProperties.class); - doReturn(staticMetadataManager).when(applicationContext).getBean(StaticMetadataManager.class); - mockedApplicationContextAwareUtils.when(ApplicationContextAwareUtils::getApplicationContext) - .thenReturn(applicationContext); - } - - @AfterAll - static void afterAll() { - mockedApplicationContextAwareUtils.close(); - } - - @BeforeEach - void setUp() { - MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST; - MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER; - } - - @Test - public void testRun() throws IOException, URISyntaxException { - - ClientHttpResponse actualResult; - final byte[] inputBody = null; - - URI uri = new URI("http://0.0.0.0/"); - doReturn(uri).when(mockHttpRequest).getURI(); - doReturn(HttpMethod.GET).when(mockHttpRequest).getMethod(); - doReturn(mockHttpHeaders).when(mockHttpRequest).getHeaders(); - doReturn(mockClientHttpResponse).when(mockClientHttpRequestExecution).execute(mockHttpRequest, inputBody); - - EnhancedRestTemplateInterceptor reporter = new EnhancedRestTemplateInterceptor(new DefaultEnhancedPluginRunner(new ArrayList<>(), registration, null)); - actualResult = reporter.intercept(mockHttpRequest, inputBody, mockClientHttpRequestExecution); - assertThat(actualResult).isEqualTo(mockClientHttpResponse); - - actualResult = reporter.intercept(mockHttpRequest, inputBody, mockClientHttpRequestExecution); - assertThat(actualResult).isEqualTo(mockClientHttpResponse); - - doThrow(new SocketTimeoutException()).when(mockClientHttpRequestExecution).execute(mockHttpRequest, inputBody); - assertThatThrownBy(() -> reporter.intercept(mockHttpRequest, inputBody, mockClientHttpRequestExecution)).isInstanceOf(SocketTimeoutException.class); - } - -} diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptorTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptorTest.java new file mode 100644 index 000000000..dcd1a8a48 --- /dev/null +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptorTest.java @@ -0,0 +1,302 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.rpc.enhancement.instrument.resttemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +import com.tencent.cloud.common.constant.ContextConstant; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; +import com.tencent.polaris.metadata.core.MetadataType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; +import org.springframework.cloud.client.loadbalancer.ServiceRequestWrapper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test for {@link EnhancedRestTemplateWrapInterceptor}. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +class EnhancedRestTemplateWrapInterceptorTest { + + @Mock + private EnhancedPluginRunner pluginRunner; + + @Mock + private LoadBalancerClient delegate; + + @Mock + private HttpRequest request; + + @Mock + private ClientHttpResponse response; + + @Mock + private ServiceRequestWrapper serviceRequestWrapper; + + @Mock + private ServiceInstance localServiceInstance; + + private EnhancedRestTemplateWrapInterceptor interceptor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + interceptor = new EnhancedRestTemplateWrapInterceptor(pluginRunner, delegate); + } + + @Test + void testInterceptWithNormalRequest() throws IOException { + // Arrange + URI uri = URI.create("http://test-service/api"); + HttpHeaders headers = new HttpHeaders(); + headers.add("test-header", "test-value"); + + when(request.getURI()).thenReturn(uri); + when(request.getHeaders()).thenReturn(headers); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance); + when(delegate.execute(any(), any())).thenReturn(response); + when(response.getRawStatusCode()).thenReturn(200); + when(response.getHeaders()).thenReturn(new HttpHeaders()); + + // Act + interceptor.intercept(request, "test-service", mock(LoadBalancerRequest.class)); + + // Assert + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class)); + + // Verify context setup + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class); + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture()); + + EnhancedPluginContext capturedContext = contextCaptor.getValue(); + Assertions.assertEquals(uri, capturedContext.getRequest().getUrl()); + Assertions.assertEquals(uri, capturedContext.getRequest().getServiceUrl()); + Assertions.assertEquals(headers, capturedContext.getRequest().getHttpHeaders()); + Assertions.assertEquals(HttpMethod.GET, capturedContext.getRequest().getHttpMethod()); + Assertions.assertEquals(localServiceInstance, capturedContext.getLocalServiceInstance()); + } + + @Test + void testInterceptWithServiceRequestWrapper() throws IOException { + // Arrange + URI originalUri = URI.create("http://original-service/api"); + URI wrappedUri = URI.create("http://wrapped-service/api"); + HttpHeaders headers = new HttpHeaders(); + + when(serviceRequestWrapper.getURI()).thenReturn(wrappedUri); + when(serviceRequestWrapper.getRequest()).thenReturn(request); + when(serviceRequestWrapper.getHeaders()).thenReturn(headers); + when(serviceRequestWrapper.getMethod()).thenReturn(HttpMethod.POST); + when(request.getURI()).thenReturn(originalUri); + when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance); + when(delegate.execute(any(), any())).thenReturn(response); + when(response.getRawStatusCode()).thenReturn(200); + when(response.getHeaders()).thenReturn(new HttpHeaders()); + + // Act + interceptor.intercept(serviceRequestWrapper, "test-service", mock(LoadBalancerRequest.class)); + + // Assert + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class)); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class); + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture()); + + EnhancedPluginContext capturedContext = contextCaptor.getValue(); + Assertions.assertEquals(wrappedUri, capturedContext.getRequest().getUrl()); + Assertions.assertEquals(originalUri, capturedContext.getRequest().getServiceUrl()); + Assertions.assertEquals(serviceRequestWrapper, capturedContext.getOriginRequest()); + } + + @Test + void testInterceptWithFallback() throws IOException { + // Arrange + URI originalUri = URI.create("http://original-service/api"); + URI wrappedUri = URI.create("http://wrapped-service/api"); + HttpHeaders headers = new HttpHeaders(); + + CallAbortedException abortedException = new CallAbortedException("test-error", null); + + when(serviceRequestWrapper.getURI()).thenReturn(wrappedUri); + when(serviceRequestWrapper.getRequest()).thenReturn(request); + when(serviceRequestWrapper.getHeaders()).thenReturn(headers); + when(serviceRequestWrapper.getMethod()).thenReturn(HttpMethod.POST); + when(request.getURI()).thenReturn(originalUri); + when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance); + doThrow(abortedException) + .when(pluginRunner) + .run(any(), any()); + + Object fallbackResponse = new MockClientHttpResponse(); + MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true). + putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE, fallbackResponse); + + // Act + interceptor.intercept(serviceRequestWrapper, "test-service", mock(LoadBalancerRequest.class)); + + // Assert + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class)); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class); + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture()); + + EnhancedPluginContext capturedContext = contextCaptor.getValue(); + Assertions.assertEquals(wrappedUri, capturedContext.getRequest().getUrl()); + Assertions.assertEquals(originalUri, capturedContext.getRequest().getServiceUrl()); + Assertions.assertEquals(serviceRequestWrapper, capturedContext.getOriginRequest()); + } + + @Test + void testInterceptWithNoFallback() { + // Arrange + URI originalUri = URI.create("http://original-service/api"); + URI wrappedUri = URI.create("http://wrapped-service/api"); + HttpHeaders headers = new HttpHeaders(); + + CallAbortedException abortedException = new CallAbortedException("test-error", null); + + when(serviceRequestWrapper.getURI()).thenReturn(wrappedUri); + when(serviceRequestWrapper.getRequest()).thenReturn(request); + when(serviceRequestWrapper.getHeaders()).thenReturn(headers); + when(serviceRequestWrapper.getMethod()).thenReturn(HttpMethod.POST); + when(request.getURI()).thenReturn(originalUri); + when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance); + doThrow(abortedException) + .when(pluginRunner) + .run(any(), any()); + // Act + Assertions.assertThrows(CallAbortedException.class, () -> { + interceptor.intercept(serviceRequestWrapper, "test-service", mock(LoadBalancerRequest.class)); + }); + } + + @Test + void testInterceptWithNullLocalServiceInstance() throws IOException { + // Arrange + URI uri = URI.create("http://test-service/api"); + when(request.getURI()).thenReturn(uri); + when(request.getHeaders()).thenReturn(new HttpHeaders()); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(pluginRunner.getLocalServiceInstance()).thenReturn(null); + when(delegate.execute(any(), any())).thenReturn(response); + when(response.getRawStatusCode()).thenReturn(200); + when(response.getHeaders()).thenReturn(new HttpHeaders()); + + // Act + interceptor.intercept(request, "test-service", mock(LoadBalancerRequest.class)); + + // Assert + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class)); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class); + verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture()); + + EnhancedPluginContext capturedContext = contextCaptor.getValue(); + assertThat(capturedContext.getLocalServiceInstance()).isNull(); + } + + @Test + void testExceptionHandling() throws IOException { + // Arrange + LoadBalancerRequest loadBalancerRequest = mock(LoadBalancerRequest.class); + IOException expectedException = new IOException("Test exception"); + when(delegate.execute(anyString(), any(LoadBalancerRequest.class))) + .thenThrow(expectedException); + + // Act & Assert + Exception actualException = Assertions.assertThrows(IOException.class, () -> { + interceptor.intercept(request, "test-service", loadBalancerRequest); + }); + + // Verify exception handling + verify(pluginRunner, times(1)) + .run(eq(EnhancedPluginType.Client.EXCEPTION), any(EnhancedPluginContext.class)); + + // Verify finally block is executed + verify(pluginRunner, times(1)) + .run(eq(EnhancedPluginType.Client.FINALLY), any(EnhancedPluginContext.class)); + + // Verify the thrown exception is the same + Assertions.assertEquals(expectedException, actualException); + } + + static class MockClientHttpResponse implements ClientHttpResponse { + @Override + public HttpStatus getStatusCode() throws IOException { + return null; + } + + @Override + public int getRawStatusCode() throws IOException { + return 0; + } + + @Override + public String getStatusText() throws IOException { + return null; + } + + @Override + public void close() { + + } + + @Override + public InputStream getBody() throws IOException { + return null; + } + + @Override + public HttpHeaders getHeaders() { + return null; + } + } +} diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilterTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilterTest.java index 21174b5a6..9458400c3 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilterTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilterTest.java @@ -20,6 +20,12 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.metadata.StaticMetadataManager; @@ -27,8 +33,16 @@ import com.tencent.cloud.common.util.ApplicationContextAwareUtils; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.plugin.DefaultEnhancedPluginRunner; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import com.tencent.polaris.client.api.SDKContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,22 +53,33 @@ import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.Response; import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.route.Route; import org.springframework.context.ApplicationContext; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; @@ -76,6 +101,13 @@ public class EnhancedGatewayGlobalFilterTest { private RpcEnhancementReporterProperties reporterProperties; @Mock private SDKContext sdkContext; + @Mock + private Response serviceInstanceResponse; + @Mock + private ServiceInstance serviceInstance; + @Mock + private EnhancedPluginRunner pluginRunner; + @BeforeAll static void beforeAll() { @@ -117,6 +149,9 @@ public void testRun() throws URISyntaxException { doReturn(new URI("http://0.0.0.0/")).when(exchange).getAttribute(GATEWAY_REQUEST_URL_ATTR); doReturn(request).when(exchange).getRequest(); doReturn(response).when(exchange).getResponse(); + doReturn(serviceInstanceResponse).when(exchange).getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR); + doReturn(serviceInstance).when(serviceInstanceResponse).getServer(); + doReturn("test-service").when(serviceInstance).getServiceId(); EnhancedGatewayGlobalFilter reporter = new EnhancedGatewayGlobalFilter(new DefaultEnhancedPluginRunner(new ArrayList<>(), registration, null)); reporter.getOrder(); @@ -128,4 +163,167 @@ public void testRun() throws URISyntaxException { assertThatThrownBy(() -> reporter.filter(exchange, chain).block()).isInstanceOf(RuntimeException.class); } + + @Test + void testFilterWithCallAbortedException() { + // Arrange + HttpHeaders headers = new HttpHeaders(); + CallAbortedException exception = new CallAbortedException("Test abort", null); + + when(exchange.getRequest()).thenReturn(request); + when(request.getHeaders()).thenReturn(headers); + when(request.getMethod()).thenReturn(HttpMethod.GET); + doThrow(exception).when(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class)); + + EnhancedGatewayGlobalFilter filter = new EnhancedGatewayGlobalFilter(pluginRunner); + // Act & Assert + Assertions.assertThrows(CallAbortedException.class, () -> { + filter.filter(exchange, chain); + }); + } + + @Test + void testFilterWithCallAbortedExceptionAndFallback() { + // Arrange + MultiValueMap headers = new TestMultiValueMap(); + headers.put("header-key", Collections.singletonList("header-value")); + + Map headersMap = new HashMap<>(); + headersMap.put("header-key", "header-value"); + + CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(HttpStatus.INTERNAL_SERVER_ERROR.value(), headersMap, "Fallback Response"); + + CallAbortedException abortedException = new CallAbortedException("test rule", fallbackInfo); + ServerHttpResponse response = mock(ServerHttpResponse.class); + HttpHeaders responseHeaders = new HttpHeaders(); + + when(exchange.getRequest()).thenReturn(request); + when(request.getHeaders()).thenReturn(new HttpHeaders(headers)); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(exchange.getResponse()).thenReturn(response); + when(response.getHeaders()).thenReturn(responseHeaders); + when(response.bufferFactory()).thenReturn(new DefaultDataBufferFactory()); + + doThrow(abortedException).when(pluginRunner) + .run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class)); + EnhancedGatewayGlobalFilter filter = new EnhancedGatewayGlobalFilter(pluginRunner); + + // Act + Mono result = filter.filter(exchange, chain); + + // Assert + verify(response).setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + } + + public static class TestMultiValueMap implements MultiValueMap { + + @Override + public String getFirst(String key) { + return null; + } + + @Override + public void add(String key, String value) { + + } + + @Override + public void addAll(String key, List values) { + + } + + @Override + public void addAll(MultiValueMap values) { + + } + + @Override + public void set(String key, String value) { + + } + + @Override + public void setAll(Map values) { + + } + + @Override + public Map toSingleValueMap() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(Object key) { + return false; + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Override + public List get(Object key) { + return null; + } + + @Nullable + @Override + public List put(String key, List value) { + return null; + } + + @Override + public List remove(Object key) { + return null; + } + + @Override + public void putAll(@NotNull Map> m) { + + } + + @Override + public void clear() { + + } + + @NotNull + @Override + public Set keySet() { + return null; + } + + @NotNull + @Override + public Collection> values() { + return null; + } + + @NotNull + @Override + public Set>> entrySet() { + return null; + } + + @Override + public boolean equals(Object o) { + return false; + } + + @Override + public int hashCode() { + return 0; + } + } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunctionTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunctionTest.java index ef1f319fa..9a487e8e6 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunctionTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/webclient/EnhancedWebClientExchangeFilterFunctionTest.java @@ -20,15 +20,23 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; import com.tencent.cloud.common.metadata.StaticMetadataManager; import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; import com.tencent.cloud.common.util.ApplicationContextAwareUtils; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.plugin.DefaultEnhancedPluginRunner; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; +import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; +import com.tencent.polaris.api.pojo.CircuitBreakerStatus; +import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import com.tencent.polaris.client.api.SDKContext; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +46,9 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpHeaders; @@ -48,14 +58,18 @@ import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; +import static com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisLoadBalancerRequestTransformer.LOAD_BALANCER_SERVICE_INSTANCE; import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST; import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class EnhancedWebClientExchangeFilterFunctionTest { @@ -73,6 +87,10 @@ public class EnhancedWebClientExchangeFilterFunctionTest { private ExchangeFunction exchangeFunction; @Mock private ClientResponse clientResponse; + @Mock + private EnhancedPluginRunner pluginRunner; + @Mock + private ServiceInstance serviceInstance; @BeforeAll static void beforeAll() { @@ -97,6 +115,7 @@ static void afterAll() { void setUp() { MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST; MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER; + MetadataContextHolder.get().setLoadbalancer(LOAD_BALANCER_SERVICE_INSTANCE, serviceInstance); } @Test @@ -124,4 +143,57 @@ public void testRun() throws URISyntaxException { } + @Test + void testCallAbortedWithFallback() { + // Arrange + URI uri = URI.create("http://127.0.0.1:8080/api"); + ClientRequest request = ClientRequest.create(HttpMethod.GET, uri).build(); + + Map headers = new HashMap<>(); + headers.put("header-key", "header-value"); + + CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(HttpStatus.SERVICE_UNAVAILABLE.value(), headers, "Fallback Response"); + + CallAbortedException abortedException = new CallAbortedException("test rule", fallbackInfo); + + doThrow(abortedException) + .when(pluginRunner) + .run(eq(EnhancedPluginType.Client.PRE), any()); + when(serviceInstance.getServiceId()).thenReturn("test-service"); + + EnhancedWebClientExchangeFilterFunction filterFunction = new EnhancedWebClientExchangeFilterFunction(pluginRunner); + + // Act + Mono responseMono = filterFunction.filter(request, exchangeFunction); + + // Assert + StepVerifier.create(responseMono) + .expectNextMatches(response -> { + Assertions.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.statusCode()); + assertThat(response.headers().asHttpHeaders().containsKey("header-key")).isTrue(); + return true; + }) + .verifyComplete(); + } + + @Test + void testCallAborted() { + // Arrange + URI uri = URI.create("http://127.0.0.1:8080/api"); + ClientRequest request = ClientRequest.create(HttpMethod.GET, uri).build(); + + CallAbortedException abortedException = new CallAbortedException("test rule", null); + + doThrow(abortedException) + .when(pluginRunner) + .run(eq(EnhancedPluginType.Client.PRE), any()); + when(serviceInstance.getServiceId()).thenReturn("test-service"); + + EnhancedWebClientExchangeFilterFunction filterFunction = new EnhancedWebClientExchangeFilterFunction(pluginRunner); + + // Act + assertThatThrownBy(() -> filterFunction.filter(request, exchangeFunction)). + isInstanceOf(CallAbortedException.class); + } + } diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunnerTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunnerTest.java new file mode 100644 index 000000000..abf773a5b --- /dev/null +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/DefaultEnhancedPluginRunnerTest.java @@ -0,0 +1,89 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.rpc.enhancement.plugin; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.client.serviceregistry.Registration; + +/** + * Tests for {@link DefaultEnhancedPluginRunner}. + * + * @author Shedfree Wu + */ +@ExtendWith(MockitoExtension.class) +public class DefaultEnhancedPluginRunnerTest { + + @Test + void getPolarisRegistration_WithPolarisRegistration_ReturnsPolarisRegistration() { + // Arrange + List registrations = new ArrayList<>(); + Registration normalReg = createMockRegistration(); + Registration normalReg2 = createMockRegistration(); + registrations.add(normalReg); + registrations.add(normalReg2); + + // Act + Registration result = DefaultEnhancedPluginRunner.getPolarisRegistration(registrations); + + // Assert + Assertions.assertSame(normalReg, result); + } + + // Helper method to create mock Registration objects + private Registration createMockRegistration() { + return new Registration() { + @Override + public String getServiceId() { + return null; + } + + @Override + public String getHost() { + return null; + } + + @Override + public int getPort() { + return 0; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public java.net.URI getUri() { + return null; + } + + @Override + public java.util.Map getMetadata() { + return null; + } + }; + } + +} diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedPluginContextTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedPluginContextTest.java index 4c2aa5ed5..59b2a0d23 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedPluginContextTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/EnhancedPluginContextTest.java @@ -19,6 +19,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -97,6 +98,7 @@ public void testGetAndSet() throws Throwable { EnhancedRequestContext requestContext = new EnhancedRequestContext(); requestContext.setHttpHeaders(new HttpHeaders()); requestContext.setUrl(new URI("/")); + requestContext.setServiceUrl(new URI("http://test-service/path")); requestContext.setHttpMethod(HttpMethod.GET); EnhancedRequestContext requestContext1 = EnhancedRequestContext.builder() @@ -150,7 +152,7 @@ public void testGetAndSet() throws Throwable { doReturn(configuration).when(sdkContext).getConfig(); - enhancedPluginRunner = new DefaultEnhancedPluginRunner(Collections.singletonList(enhancedPlugin2), null, sdkContext); + enhancedPluginRunner = new DefaultEnhancedPluginRunner(Collections.singletonList(enhancedPlugin2), new ArrayList<>(), sdkContext); enhancedPluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext); } diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/PolarisEnhancedPluginUtilsTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/PolarisEnhancedPluginUtilsTest.java index be296c26f..26e7187d1 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/PolarisEnhancedPluginUtilsTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/plugin/PolarisEnhancedPluginUtilsTest.java @@ -18,6 +18,7 @@ package com.tencent.cloud.rpc.enhancement.plugin; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; @@ -28,6 +29,9 @@ import com.tencent.cloud.common.constant.HeaderConstant; import com.tencent.cloud.common.constant.RouterConstant; import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.StaticMetadataManager; +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; import com.tencent.cloud.common.util.ApplicationContextAwareUtils; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat; @@ -70,7 +74,7 @@ public class PolarisEnhancedPluginUtilsTest { private SDKContext sdkContext; @BeforeAll - static void beforeAll() { + static void beforeAll() throws Exception { mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) .thenReturn("unit-test"); @@ -80,6 +84,12 @@ static void beforeAll() { .when(applicationContext).getBean(RpcEnhancementReporterProperties.class); mockedApplicationContextAwareUtils.when(ApplicationContextAwareUtils::getApplicationContext) .thenReturn(applicationContext); + + StaticMetadataManager metadataManager = new StaticMetadataManager(new MetadataLocalProperties(), null); + + Field field = MetadataContextHolder.class.getDeclaredField("staticMetadataManager"); + field.setAccessible(true); + field.set(null, metadataManager); } @AfterAll