feat: implement circuit breaker in enhance plugin, support listen config group, support refresh single config in refresh_context mode. (#1501)

Co-authored-by: shedfreewu <49236872+shedfreewu@users.noreply.github.com>
pull/1503/head
Haotian Zhang 6 months ago committed by GitHub
parent 0cf3d21f12
commit 021eeb47f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -45,3 +45,4 @@
- [fix:fix caller disposable metadata handle when using tracing.](https://github.com/Tencent/spring-cloud-tencent/pull/1483) - [fix:fix caller disposable metadata handle when using tracing.](https://github.com/Tencent/spring-cloud-tencent/pull/1483)
- [refactor:update registry status.](https://github.com/Tencent/spring-cloud-tencent/pull/1484) - [refactor:update registry status.](https://github.com/Tencent/spring-cloud-tencent/pull/1484)
- [feat:support polaris route for gateway mvc.](https://github.com/Tencent/spring-cloud-tencent/pull/1493) - [feat:support polaris route for gateway mvc.](https://github.com/Tencent/spring-cloud-tencent/pull/1493)
- [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/1501)

@ -90,7 +90,7 @@
<properties> <properties>
<!-- Project revision --> <!-- Project revision -->
<revision>2.0.0.0-2023.0.3-SNAPSHOT</revision> <revision>2.0.1.0-2023.0.3-SNAPSHOT</revision>
<!-- Spring Framework --> <!-- Spring Framework -->
<spring.framework.version>6.1.14</spring.framework.version> <spring.framework.version>6.1.14</spring.framework.version>

@ -18,13 +18,10 @@
package com.tencent.cloud.metadata.core; package com.tencent.cloud.metadata.core;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder; 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.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -79,13 +76,7 @@ public class EncodeTransferMedataRestTemplateInterceptorTest {
@Bean @Bean
public RestTemplate restTemplate() { public RestTemplate restTemplate() {
return new 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;
} }
@RequestMapping("/test") @RequestMapping("/test")

@ -27,7 +27,9 @@ import com.tencent.polaris.api.core.ConsumerAPI;
import com.tencent.polaris.api.pojo.ServiceKey; import com.tencent.polaris.api.pojo.ServiceKey;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.circuitbreak.api.FunctionalDecorator; 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.FunctionalDecoratorRequest;
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException; import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -39,7 +41,7 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
* *
* @author seanyu 2023-02-27 * @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); private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreaker.class);
@ -50,6 +52,8 @@ public class PolarisCircuitBreaker implements CircuitBreaker {
private final ConsumerAPI consumerAPI; private final ConsumerAPI consumerAPI;
private final InvokeHandler invokeHandler;
public PolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf, public PolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf,
ConsumerAPI consumerAPI, ConsumerAPI consumerAPI,
CircuitBreakAPI circuitBreakAPI) { CircuitBreakAPI circuitBreakAPI) {
@ -60,6 +64,7 @@ public class PolarisCircuitBreaker implements CircuitBreaker {
this.consumerAPI = consumerAPI; this.consumerAPI = consumerAPI;
this.conf = conf; this.conf = conf;
this.decorator = circuitBreakAPI.makeFunctionalDecorator(makeDecoratorRequest); this.decorator = circuitBreakAPI.makeFunctionalDecorator(makeDecoratorRequest);
this.invokeHandler = circuitBreakAPI.makeInvokeHandler(makeDecoratorRequest);
} }
@Override @Override
@ -78,4 +83,18 @@ public class PolarisCircuitBreaker implements CircuitBreaker {
} }
} }
@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);
}
} }

@ -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 <T> Mono<T> run(Mono<T> toRun, Function<Throwable, Mono<T>> fallback) {
Mono<T> 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 <T> Flux<T> run(Flux<T> toRun, Function<Throwable, Flux<T>> fallback) {
Flux<T> 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;
}
}

@ -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<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration, PolarisCircuitBreakerConfigBuilder> 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<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> 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<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration) {
this.defaultConfiguration = defaultConfiguration;
}
@Override
public void destroy() {
ThreadPoolUtils.waitAndStopThreadPools(new ExecutorService[] {cleanupService});
}
}

@ -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;
}
}

@ -17,6 +17,7 @@
package com.tencent.cloud.polaris.circuitbreaker.common; package com.tencent.cloud.polaris.circuitbreaker.common;
import com.tencent.cloud.polaris.context.CircuitBreakerStatusCodeException;
import com.tencent.polaris.circuitbreak.api.pojo.ResultToErrorCode; import com.tencent.polaris.circuitbreak.api.pojo.ResultToErrorCode;
import feign.FeignException; import feign.FeignException;
@ -49,6 +50,9 @@ public class PolarisResultToErrorCode implements ResultToErrorCode {
&& e instanceof WebClientResponseException) { && e instanceof WebClientResponseException) {
return ((WebClientResponseException) e).getRawStatusCode(); return ((WebClientResponseException) e).getRawStatusCode();
} }
else if (e instanceof CircuitBreakerStatusCodeException) {
return ((CircuitBreakerStatusCodeException) e).getRawStatusCode();
}
return -1; return -1;
} }

@ -17,21 +17,11 @@
package com.tencent.cloud.polaris.circuitbreaker.config; 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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter; 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.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -44,23 +34,9 @@ import org.springframework.web.reactive.DispatcherHandler;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true) @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@AutoConfigureAfter({ReactivePolarisCircuitBreakerAutoConfiguration.class}) @ConditionalOnClass({ DispatcherHandler.class, GatewayAutoConfiguration.class})
@ConditionalOnClass({DispatcherHandler.class, ReactivePolarisCircuitBreakerAutoConfiguration.class,
ReactiveCircuitBreakerFactory.class, ReactivePolarisCircuitBreakerFactory.class, GatewayAutoConfiguration.class})
public class GatewayPolarisCircuitBreakerAutoConfiguration { public class GatewayPolarisCircuitBreakerAutoConfiguration {
@Bean
@ConditionalOnBean(ReactiveCircuitBreakerFactory.class)
@ConditionalOnEnabledFilter
public PolarisCircuitBreakerFilterFactory polarisCircuitBreakerFilterFactory(
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
ObjectProvider<DispatcherHandler> dispatcherHandler,
@Autowired(required = false) ReactiveDiscoveryClient discoveryClient,
@Autowired(required = false) DiscoveryLocatorProperties properties
) {
return new PolarisCircuitBreakerFilterFactory(reactiveCircuitBreakerFactory, dispatcherHandler, discoveryClient, properties);
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnEnabledFilter @ConditionalOnEnabledFilter

@ -21,8 +21,9 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory; 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.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.ExceptionCircuitBreakerReporter;
import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter; import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter;
import com.tencent.cloud.polaris.context.PolarisSDKContextManager; import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
@ -36,7 +37,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.Customizer; import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -56,10 +56,9 @@ public class PolarisCircuitBreakerAutoConfiguration {
private List<Customizer<PolarisCircuitBreakerFactory>> customizers = new ArrayList<>(); private List<Customizer<PolarisCircuitBreakerFactory>> customizers = new ArrayList<>();
@Bean @Bean
@ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") @ConditionalOnMissingBean(CircuitBreakerPlugin.class)
public static PolarisCircuitBreakerRestTemplateBeanPostProcessor polarisCircuitBreakerRestTemplateBeanPostProcessor( public CircuitBreakerPlugin circuitBreakerPlugin(CircuitBreakerFactory circuitBreakerFactory) {
ApplicationContext applicationContext) { return new CircuitBreakerPlugin(circuitBreakerFactory);
return new PolarisCircuitBreakerRestTemplateBeanPostProcessor(applicationContext);
} }
@Bean @Bean
@ -92,4 +91,10 @@ public class PolarisCircuitBreakerAutoConfiguration {
return new CircuitBreakerConfigModifier(properties); return new CircuitBreakerConfigModifier(properties);
} }
@Bean
@ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor")
public LoadBalancerInterceptorBeanPostProcessor loadBalancerInterceptorBeanPostProcessor() {
return new LoadBalancerInterceptorBeanPostProcessor();
}
} }

@ -30,7 +30,6 @@ import org.springframework.context.annotation.Import;
@ConditionalOnProperty("spring.cloud.polaris.enabled") @ConditionalOnProperty("spring.cloud.polaris.enabled")
@Import({ @Import({
PolarisCircuitBreakerAutoConfiguration.class, PolarisCircuitBreakerAutoConfiguration.class,
ReactivePolarisCircuitBreakerAutoConfiguration.class,
PolarisCircuitBreakerFeignClientAutoConfiguration.class, PolarisCircuitBreakerFeignClientAutoConfiguration.class,
GatewayPolarisCircuitBreakerAutoConfiguration.class GatewayPolarisCircuitBreakerAutoConfiguration.class
}) })

@ -52,8 +52,8 @@ public class PolarisCircuitBreakerFeignClientAutoConfiguration {
@Bean @Bean
@ConditionalOnBean(CircuitBreakerFactory.class) @ConditionalOnBean(CircuitBreakerFactory.class)
@ConditionalOnMissingBean(Targeter.class) @ConditionalOnMissingBean(Targeter.class)
public Targeter polarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, CircuitBreakerNameResolver circuitBreakerNameResolver) { public Targeter polarisFeignCircuitBreakerTargeter() {
return new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); return new PolarisFeignCircuitBreakerTargeter();
} }
@Bean @Bean

@ -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<Customizer<ReactivePolarisCircuitBreakerFactory>> 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);
}
}

@ -20,8 +20,6 @@ package com.tencent.cloud.polaris.circuitbreaker.instrument.feign;
import feign.Feign; import feign.Feign;
import feign.Target; 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.FallbackFactory;
/** /**
@ -46,29 +44,9 @@ public final class PolarisFeignCircuitBreaker {
* Builder for Feign CircuitBreaker integration. * Builder for Feign CircuitBreaker integration.
*/ */
public static final class Builder extends Feign.Builder { public static final class Builder extends Feign.Builder {
private CircuitBreakerFactory circuitBreakerFactory;
private String feignClientName;
private CircuitBreakerNameResolver circuitBreakerNameResolver;
public Builder() { 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> T target(Target<T> target, T fallback) { public <T> T target(Target<T> target, T fallback) {
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target); return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
} }
@ -84,7 +62,7 @@ public final class PolarisFeignCircuitBreaker {
public Feign build(final FallbackFactory<?> nullableFallbackFactory) { public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler( this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler(
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory, circuitBreakerNameResolver, this.decoder)); target, dispatch, nullableFallbackFactory, decoder));
return this.build(); return this.build();
} }

@ -23,21 +23,14 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import com.tencent.cloud.polaris.circuitbreaker.exception.FallbackWrapperException; import com.tencent.cloud.polaris.circuitbreaker.exception.FallbackWrapperException;
import feign.InvocationHandlerFactory; import feign.InvocationHandlerFactory;
import feign.Target; import feign.Target;
import feign.codec.Decoder; 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.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory; 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; import static feign.Util.checkNotNull;
@ -47,11 +40,6 @@ import static feign.Util.checkNotNull;
* @author sean yu * @author sean yu
*/ */
public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHandler { public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHandler {
private final CircuitBreakerFactory factory;
private final String feignClientName;
private final Target<?> target; private final Target<?> target;
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch; private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
@ -60,20 +48,16 @@ public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHa
private final Map<Method, Method> fallbackMethodMap; private final Map<Method, Method> fallbackMethodMap;
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
private final Decoder decoder; private final Decoder decoder;
public PolarisFeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target, public PolarisFeignCircuitBreakerInvocationHandler(Target<?> target,
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch,
CircuitBreakerNameResolver circuitBreakerNameResolver, Decoder decoder) { FallbackFactory<?> nullableFallbackFactory, Decoder decoder) {
this.factory = factory;
this.feignClientName = feignClientName;
this.target = checkNotNull(target, "target"); this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch"); this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackMethodMap = toFallbackMethod(dispatch); this.fallbackMethodMap = toFallbackMethod(dispatch);
this.nullableFallbackFactory = nullableFallbackFactory; this.nullableFallbackFactory = nullableFallbackFactory;
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
this.decoder = decoder; this.decoder = decoder;
} }
@ -115,13 +99,12 @@ public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHa
return toString(); return toString();
} }
String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method); try {
CircuitBreaker circuitBreaker = factory.create(circuitName); return this.dispatch.get(method).invoke(args);
Supplier<Object> supplier = asSupplier(method, args); }
Function<Throwable, Object> fallbackFunction; catch (Exception e) {
if (this.nullableFallbackFactory != null) { if (this.nullableFallbackFactory != null) {
fallbackFunction = throwable -> { Object fallback = this.nullableFallbackFactory.create(e);
Object fallback = this.nullableFallbackFactory.create(throwable);
try { try {
return this.fallbackMethodMap.get(method).invoke(fallback, args); return this.fallbackMethodMap.get(method).invoke(fallback, args);
} }
@ -129,21 +112,12 @@ public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHa
unwrapAndRethrow(exception); unwrapAndRethrow(exception);
} }
return null; return null;
};
} }
else { else {
fallbackFunction = throwable -> {
PolarisCircuitBreakerFallbackFactory.DefaultFallback fallback = PolarisCircuitBreakerFallbackFactory.DefaultFallback fallback =
(PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(throwable); (PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(e);
return fallback.fallback(method); return fallback.fallback(method);
};
}
try {
return circuitBreaker.run(supplier, fallbackFunction);
} }
catch (FallbackWrapperException e) {
// unwrap And Rethrow
throw e.getCause();
} }
} }
@ -160,31 +134,6 @@ public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHa
} }
} }
private Supplier<Object> 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 @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof PolarisFeignCircuitBreakerInvocationHandler) { if (obj instanceof PolarisFeignCircuitBreakerInvocationHandler) {

@ -20,8 +20,6 @@ package com.tencent.cloud.polaris.circuitbreaker.instrument.feign;
import feign.Feign; import feign.Feign;
import feign.Target; 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.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClientFactory; import org.springframework.cloud.openfeign.FeignClientFactory;
import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.cloud.openfeign.FeignClientFactoryBean;
@ -35,15 +33,6 @@ import org.springframework.util.StringUtils;
*/ */
public class PolarisFeignCircuitBreakerTargeter implements Targeter { 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 @Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context,
Target.HardCodedTarget<T> target) { Target.HardCodedTarget<T> target) {
@ -60,20 +49,20 @@ public class PolarisFeignCircuitBreakerTargeter implements Targeter {
if (fallbackFactory != void.class) { if (fallbackFactory != void.class) {
return targetWithFallbackFactory(name, context, target, builder, fallbackFactory); return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
} }
return builder(name, builder).target(target); return builder.target(target);
} }
private <T> T targetWithFallbackFactory(String feignClientName, FeignClientFactory context, private <T> T targetWithFallbackFactory(String feignClientName, FeignClientFactory context,
Target.HardCodedTarget<T> target, PolarisFeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) { Target.HardCodedTarget<T> target, PolarisFeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) {
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory", FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory",
feignClientName, context, fallbackFactoryClass, FallbackFactory.class); feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
return builder(feignClientName, builder).target(target, fallbackFactory); return builder.target(target, fallbackFactory);
} }
private <T> T targetWithFallback(String feignClientName, FeignClientFactory context, Target.HardCodedTarget<T> target, private <T> T targetWithFallback(String feignClientName, FeignClientFactory context, Target.HardCodedTarget<T> target,
PolarisFeignCircuitBreaker.Builder builder, Class<?> fallback) { PolarisFeignCircuitBreaker.Builder builder, Class<?> fallback) {
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
return builder(feignClientName, builder).target(target, fallbackInstance); return builder.target(target, fallbackInstance);
} }
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignClientFactory context, private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignClientFactory context,
@ -92,12 +81,4 @@ public class PolarisFeignCircuitBreakerTargeter implements Targeter {
} }
return (T) fallbackInstance; return (T) fallbackInstance;
} }
private PolarisFeignCircuitBreaker.Builder builder(String feignClientName, PolarisFeignCircuitBreaker.Builder builder) {
return builder
.circuitBreakerFactory(circuitBreakerFactory)
.feignClientName(feignClientName)
.circuitBreakerNameResolver(circuitBreakerNameResolver);
}
} }

@ -1,287 +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.HttpStatusCode;
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<DispatcherHandler> dispatcherHandlerProvider;
private String routeIdPrefix;
// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
private volatile DispatcherHandler dispatcherHandler;
public PolarisCircuitBreakerFilterFactory(
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
ObjectProvider<DispatcherHandler> 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<HttpStatus> 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<HttpStatus> getDefaultStatus() {
return Arrays.stream(HttpStatus.values())
.filter(HttpStatus::is5xxServerError)
.collect(Collectors.toSet());
}
@Override
public GatewayFilter apply(Config config) {
Set<HttpStatus> statuses = config.getStatusCodes().stream()
.flatMap(statusCode -> {
List<HttpStatus> 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<Void> 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().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<HttpStatus> statusNeedToCheck = new HashSet<>();
statusNeedToCheck.addAll(statuses);
statusNeedToCheck.addAll(getDefaultStatus());
HttpStatusCode status = exchange.getResponse().getStatusCode();
if (status == null) {
throw new CircuitBreakerStatusCodeException(HttpStatus.INTERNAL_SERVER_ERROR);
}
if (statusNeedToCheck.contains(HttpStatus.resolve(status.value()))) {
throw new CircuitBreakerStatusCodeException(status);
}
}),
t -> {
// pre-check CircuitBreakerStatusCodeException's status matches input status
if (t instanceof CircuitBreakerStatusCodeException) {
HttpStatusCode status = ((CircuitBreakerStatusCodeException) t).getStatusCode();
// no need to fallback
if (!statuses.contains(HttpStatus.resolve(status.value()))) {
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<Void> 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);
}
}

@ -53,7 +53,7 @@ public class PolarisCircuitBreakerHttpResponse extends AbstractClientHttpRespons
this(new CircuitBreakerStatus.FallbackInfo(code, headers, body)); this(new CircuitBreakerStatus.FallbackInfo(code, headers, body));
} }
PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) { public PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) {
this.fallbackInfo = fallbackInfo; this.fallbackInfo = fallbackInfo;
if (fallbackInfo.getHeaders() != null) { if (fallbackInfo.getHeaders() != null) {
fallbackInfo.getHeaders().forEach(headers::add); fallbackInfo.getHeaders().forEach(headers::add);

@ -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<String> 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);
}
}

@ -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.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 {
return circuitBreakerFactory.create(MetadataContext.LOCAL_NAMESPACE + "#" + request.getURI()
.getHost() + "#" + request.getURI().getPath() + "#http#" + request.getMethod().name()).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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}

@ -18,7 +18,11 @@
package com.tencent.cloud.polaris.circuitbreaker.reporter; package com.tencent.cloud.polaris.circuitbreaker.reporter;
import java.util.Optional; 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.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
@ -27,6 +31,9 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.PolarisEnhancedPluginUtils; import com.tencent.cloud.rpc.enhancement.plugin.PolarisEnhancedPluginUtils;
import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat; import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -88,6 +95,35 @@ public class ExceptionCircuitBreakerReporter implements EnhancedPlugin {
circuitBreakAPI.report(resourceStat); circuitBreakAPI.report(resourceStat);
MetadataObjectValue<PolarisCircuitBreaker> circuitBreakerObject = MetadataContextHolder.get().
getMetadataContainer(MetadataType.APPLICATION, true).
getMetadataValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER);
MetadataObjectValue<Long> 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 @Override
@ -100,4 +136,9 @@ public class ExceptionCircuitBreakerReporter implements EnhancedPlugin {
public int getOrder() { public int getOrder() {
return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER;
} }
private static boolean existMetadataValue(MetadataObjectValue<?> metadataObjectValue) {
return Optional.ofNullable(metadataObjectValue).map(MetadataObjectValue::getObjectValue).
map(Optional::isPresent).orElse(false);
}
} }

@ -18,7 +18,12 @@
package com.tencent.cloud.polaris.circuitbreaker.reporter; package com.tencent.cloud.polaris.circuitbreaker.reporter;
import java.util.Optional; 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.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
@ -29,11 +34,15 @@ import com.tencent.cloud.rpc.enhancement.plugin.PolarisEnhancedPluginUtils;
import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter; import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter;
import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat; import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance; 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; import static com.tencent.cloud.rpc.enhancement.plugin.PluginOrderConstant.ClientPluginOrder.CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER;
@ -89,6 +98,37 @@ public class SuccessCircuitBreakerReporter implements EnhancedPlugin {
resourceStat.getRetStatus().name(), request.getHttpMethod().name(), request.getUrl().getPath(), response.getHttpStatus(), context.getDelay()); resourceStat.getRetStatus().name(), request.getHttpMethod().name(), request.getUrl().getPath(), response.getHttpStatus(), context.getDelay());
circuitBreakAPI.report(resourceStat); circuitBreakAPI.report(resourceStat);
MetadataObjectValue<PolarisCircuitBreaker> circuitBreakerObject = MetadataContextHolder.get().
getMetadataContainer(MetadataType.APPLICATION, true).
getMetadataValue(ContextConstant.CircuitBreaker.POLARIS_CIRCUIT_BREAKER);
MetadataObjectValue<Long> 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 @Override
@ -101,4 +141,9 @@ public class SuccessCircuitBreakerReporter implements EnhancedPlugin {
public int getOrder() { public int getOrder() {
return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER; return CIRCUIT_BREAKER_REPORTER_PLUGIN_ORDER;
} }
private static boolean existMetadataValue(MetadataObjectValue<?> metadataObjectValue) {
return Optional.ofNullable(metadataObjectValue).map(MetadataObjectValue::getObjectValue).
map(Optional::isPresent).orElse(false);
}
} }

@ -1,5 +1,4 @@
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration 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.PolarisCircuitBreakerFeignClientAutoConfiguration
com.tencent.cloud.polaris.circuitbreaker.config.GatewayPolarisCircuitBreakerAutoConfiguration com.tencent.cloud.polaris.circuitbreaker.config.GatewayPolarisCircuitBreakerAutoConfiguration
com.tencent.cloud.polaris.circuitbreaker.endpoint.PolarisCircuitBreakerEndpointAutoConfiguration com.tencent.cloud.polaris.circuitbreaker.endpoint.PolarisCircuitBreakerEndpointAutoConfiguration

@ -24,7 +24,6 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -50,11 +49,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; 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.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.NAMESPACE_TEST;
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER; import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
@ -62,7 +58,7 @@ import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
import static org.assertj.core.api.Assertions.assertThat; 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 * @author sean yu
*/ */
@ -138,26 +134,6 @@ public class PolarisCircuitBreakerMockServerTest {
Utils.sleepUninterrupted(2000); Utils.sleepUninterrupted(2000);
} }
assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback")); 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"));
} }
} }

@ -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<ApplicationContextAwareUtils> 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());
});
}
}

@ -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);
}
}

@ -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);
}
}

@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; 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.client.loadbalancer.LoadBalancerAutoConfiguration;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
@ -46,14 +45,6 @@ public class PolarisCircuitBreakerAutoConfigurationTest {
PolarisCircuitBreakerAutoConfiguration.class)) PolarisCircuitBreakerAutoConfiguration.class))
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true"); .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 @Test
public void testDefaultInitialization() { public void testDefaultInitialization() {
this.contextRunner.run(context -> { this.contextRunner.run(context -> {
@ -64,13 +55,4 @@ public class PolarisCircuitBreakerAutoConfigurationTest {
}); });
} }
@Test
public void testReactiveInitialization() {
this.reactiveContextRunner.run(context -> {
assertThat(context).hasSingleBean(ReactivePolarisCircuitBreakerAutoConfiguration.class);
assertThat(context).hasSingleBean(ReactiveCircuitBreakerFactory.class);
assertThat(context).hasSingleBean(CircuitBreakerConfigModifier.class);
});
}
} }

@ -54,7 +54,6 @@ public class PolarisCircuitBreakerBootstrapConfigurationTest {
this.contextRunner.run(context -> { this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(PolarisCircuitBreakerAutoConfiguration.class); assertThat(context).hasSingleBean(PolarisCircuitBreakerAutoConfiguration.class);
assertThat(context).hasSingleBean(PolarisCircuitBreakerFeignClientAutoConfiguration.class); assertThat(context).hasSingleBean(PolarisCircuitBreakerFeignClientAutoConfiguration.class);
assertThat(context).hasSingleBean(ReactivePolarisCircuitBreakerAutoConfiguration.class);
}); });
} }
} }

@ -1,227 +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",
"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<FooService> {
private final FooService fooService = new FooServiceFallback();
@Override
public FooService create(Throwable throwable) {
return fooService;
}
}
}

@ -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<TestInterface> fallbackFactory;
@Mock
private Decoder decoder;
private Map<Method, InvocationHandlerFactory.MethodHandler> 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<Method, InvocationHandlerFactory.MethodHandler> testDispatch = new HashMap<>();
testDispatch.put(method, methodHandler);
Map<Method, Method> 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");
}
}
}

@ -27,6 +27,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClientFactory; import org.springframework.cloud.openfeign.FeignClientFactory;
import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.cloud.openfeign.FeignClientFactoryBean;
@ -48,19 +49,24 @@ public class PolarisFeignCircuitBreakerTargeterTest {
@Mock @Mock
CircuitBreakerNameResolver circuitBreakerNameResolver; CircuitBreakerNameResolver circuitBreakerNameResolver;
@Mock
FeignClientFactory feignClientFactory;
@Mock
private FeignClientFactoryBean factory;
@Test @Test
public void testTarget() { public void testTarget() {
PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter();
targeter.target(new FeignClientFactoryBean(), new Feign.Builder(), new FeignClientFactory(), new Target.HardCodedTarget<>(TestApi.class, "/test")); targeter.target(new FeignClientFactoryBean(), new Feign.Builder(), new FeignClientFactory(), new Target.HardCodedTarget<>(TestApi.class, "/test"));
} }
@Test @Test
public void testTarget2() { public void testTarget2() {
PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter();
FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class);
doReturn(TestApi.class).when(feignClientFactoryBean).getFallback(); doReturn(TestApi.class).when(feignClientFactoryBean).getFallback();
doReturn("test").when(feignClientFactoryBean).getName(); doReturn("test").when(feignClientFactoryBean).getName();
FeignClientFactory feignClientFactory = mock(FeignClientFactory.class);
doReturn(null).when(feignClientFactory).getInstance("test", TestApi.class); doReturn(null).when(feignClientFactory).getInstance("test", TestApi.class);
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignClientFactory, new Target.HardCodedTarget<>(TestApi.class, "/test")); targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignClientFactory, new Target.HardCodedTarget<>(TestApi.class, "/test"));
@ -69,21 +75,76 @@ public class PolarisFeignCircuitBreakerTargeterTest {
@Test @Test
public void testTarget3() { public void testTarget3() {
PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver); PolarisFeignCircuitBreakerTargeter targeter = new PolarisFeignCircuitBreakerTargeter();
FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class); FeignClientFactoryBean feignClientFactoryBean = mock(FeignClientFactoryBean.class);
doReturn(void.class).when(feignClientFactoryBean).getFallback(); doReturn(void.class).when(feignClientFactoryBean).getFallback();
doReturn(TestApi.class).when(feignClientFactoryBean).getFallbackFactory(); doReturn(TestApi.class).when(feignClientFactoryBean).getFallbackFactory();
doReturn("test").when(feignClientFactoryBean).getName(); doReturn("test").when(feignClientFactoryBean).getName();
FeignClientFactory feignClientFactory = mock(FeignClientFactory.class);
doReturn(Object.class).when(feignClientFactory).getInstance("test", TestApi.class); doReturn(Object.class).when(feignClientFactory).getInstance("test", TestApi.class);
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignClientFactory, new Target.HardCodedTarget<>(TestApi.class, "/test")); targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignClientFactory, new Target.HardCodedTarget<>(TestApi.class, "/test"));
}).isInstanceOf(IllegalStateException.class); }).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(), feignClientFactory, 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(feignClientFactory)
.getInstance("test", PolarisCircuitBreakerFallbackFactory.class);
targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignClientFactory, 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(feignClientFactory)
.getInstance("test", TestApiFallback.class);
targeter.target(feignClientFactoryBean, new PolarisFeignCircuitBreaker.Builder(), feignClientFactory, new Target.HardCodedTarget<>(TestApi.class, "/test"));
}
interface TestApi { interface TestApi {
@RequestLine("GET /test") @RequestLine("GET /test")
void test(); void test();
} }
static class TestApiFallback implements TestApi {
@Override
public void test() {
// fallback implementation
}
}
public static class PolarisCircuitBreakerFallbackFactory implements FallbackFactory<TestApi> {
@Override
public TestApi create(Throwable cause) {
return new TestApiFallback();
}
}
} }

@ -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<MyService> targetType = MyService.class;
String name = "myService";
Target<MyService> 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<MyService> targetType = MyService.class;
String name = "myService";
Target<MyService> target = mock(Target.class);
// mock return value
when(target.type()).thenReturn(targetType);
when(target.name()).thenReturn(name);
FallbackFactory<MyService> 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<MyService> targetType = MyService.class;
String name = "myService";
Target<MyService> 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();
}
}

@ -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<String> 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<String> fallback() {
return Mono.just("fallback");
}
}
}
}

@ -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<String, String> 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<String, String> 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());
}
}

@ -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";
}
}
}
}

@ -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);
}
}

@ -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<ApplicationContextAwareUtils> 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) {
}
}
}

@ -19,8 +19,11 @@ package com.tencent.cloud.polaris.circuitbreaker.reporter;
import java.net.URI; import java.net.URI;
import com.tencent.cloud.common.constant.ContextConstant;
import com.tencent.cloud.common.metadata.MetadataContext; 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.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
@ -28,6 +31,7 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.client.api.SDKContext; 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.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -69,6 +73,8 @@ public class ExceptionCircuitBreakerReporterTest {
private ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter; private ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter;
@Mock @Mock
private CircuitBreakAPI circuitBreakAPI; private CircuitBreakAPI circuitBreakAPI;
@Mock
private PolarisCircuitBreaker polarisCircuitBreaker;
@BeforeAll @BeforeAll
static void beforeAll() { static void beforeAll() {
@ -142,4 +148,66 @@ public class ExceptionCircuitBreakerReporterTest {
context.setResponse(response); context.setResponse(response);
exceptionCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception.")); 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);
}
} }

@ -17,10 +17,16 @@
package com.tencent.cloud.polaris.circuitbreaker.reporter; package com.tencent.cloud.polaris.circuitbreaker.reporter;
import java.lang.reflect.Field;
import java.net.URI; import java.net.URI;
import com.tencent.cloud.common.constant.ContextConstant;
import com.tencent.cloud.common.metadata.MetadataContext; 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.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties; import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
@ -28,6 +34,7 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI; import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
import com.tencent.polaris.client.api.SDKContext; 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.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -62,16 +69,18 @@ public class SuccessCircuitBreakerReporterTest {
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils; private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
@Mock @Mock
private SDKContext sdkContext;
@Mock
private RpcEnhancementReporterProperties reporterProperties; private RpcEnhancementReporterProperties reporterProperties;
@Mock
private SDKContext sdkContext;
@InjectMocks @InjectMocks
private SuccessCircuitBreakerReporter successCircuitBreakerReporter; private SuccessCircuitBreakerReporter successCircuitBreakerReporter;
@Mock @Mock
private CircuitBreakAPI circuitBreakAPI; private CircuitBreakAPI circuitBreakAPI;
@Mock
private PolarisCircuitBreaker polarisCircuitBreaker;
@BeforeAll @BeforeAll
static void beforeAll() { static void beforeAll() throws Exception {
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
.thenReturn("unit-test"); .thenReturn("unit-test");
@ -81,6 +90,12 @@ public class SuccessCircuitBreakerReporterTest {
.when(applicationContext).getBean(RpcEnhancementReporterProperties.class); .when(applicationContext).getBean(RpcEnhancementReporterProperties.class);
mockedApplicationContextAwareUtils.when(ApplicationContextAwareUtils::getApplicationContext) mockedApplicationContextAwareUtils.when(ApplicationContextAwareUtils::getApplicationContext)
.thenReturn(applicationContext); .thenReturn(applicationContext);
StaticMetadataManager metadataManager = new StaticMetadataManager(new MetadataLocalProperties(), null);
Field field = MetadataContextHolder.class.getDeclaredField("staticMetadataManager");
field.setAccessible(true);
field.set(null, metadataManager);
} }
@AfterAll @AfterAll
@ -147,4 +162,64 @@ public class SuccessCircuitBreakerReporterTest {
context.setResponse(response); context.setResponse(response);
successCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception.")); 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);
}
} }

@ -18,6 +18,12 @@
<dependency> <dependency>
<groupId>com.tencent.cloud</groupId> <groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-polaris-context</artifactId> <artifactId>spring-cloud-tencent-polaris-context</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-security-crypto</artifactId>
<groupId>org.springframework.security</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<!-- Spring Cloud Tencent dependencies end --> <!-- Spring Cloud Tencent dependencies end -->
@ -66,6 +72,12 @@
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId> <artifactId>spring-cloud-context</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-security-crypto</artifactId>
<groupId>org.springframework.security</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<!-- Spring cloud dependencies start --> <!-- Spring cloud dependencies start -->

@ -105,7 +105,10 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
+ " with spring.cloud.polaris.address or spring.cloud.polaris.config.address"); + " with spring.cloud.polaris.address or spring.cloud.polaris.config.address");
} }
// enable close check address for unit tests
if (polarisConfigProperties.isCheckAddress()) {
checkAddressAccessible(configAddresses); checkAddressAccessible(configAddresses);
}
configuration.getConfigFile().getServerConnector().setAddresses(configAddresses); configuration.getConfigFile().getServerConnector().setAddresses(configAddresses);

@ -19,18 +19,17 @@ package com.tencent.cloud.polaris.config;
import com.tencent.cloud.polaris.config.adapter.AffectedConfigurationPropertiesRebinder; import com.tencent.cloud.polaris.config.adapter.AffectedConfigurationPropertiesRebinder;
import com.tencent.cloud.polaris.config.adapter.PolarisConfigPropertyRefresher; 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.PolarisRefreshAffectedContextRefresher;
import com.tencent.cloud.polaris.config.adapter.PolarisRefreshEntireContextRefresher; import com.tencent.cloud.polaris.config.adapter.PolarisRefreshEntireContextRefresher;
import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor; import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor;
import com.tencent.cloud.polaris.config.condition.ConditionalOnReflectRefreshType; import com.tencent.cloud.polaris.config.condition.ConditionalOnReflectRefreshType;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.listener.PolarisConfigChangeEventListener; 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.logger.PolarisConfigLoggerApplicationListener;
import com.tencent.cloud.polaris.config.spring.annotation.SpringValueProcessor; 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.PlaceholderHelper;
import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; 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.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -77,14 +76,12 @@ public class PolarisConfigAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT) @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public PolarisConfigPropertyRefresher polarisRefreshContextPropertySourceAutoRefresher( public PolarisConfigPropertyRefresher polarisRefreshContextPropertySourceAutoRefresher(
PolarisConfigProperties polarisConfigProperties, ContextRefresher contextRefresher) { PolarisConfigProperties polarisConfigProperties, SpringValueRegistry springValueRegistry,
return new PolarisRefreshEntireContextRefresher(polarisConfigProperties, contextRefresher); ConfigFileService configFileService, ContextRefresher contextRefresher) {
return new PolarisRefreshEntireContextRefresher(polarisConfigProperties,
springValueRegistry, configFileService, contextRefresher);
} }
@Configuration(proxyBeanMethods = false)
@ConditionalOnReflectRefreshType
@AutoConfigureBefore(PolarisConfigAutoConfiguration.class)
public static class PolarisReflectRefresherAutoConfiguration {
@Bean @Bean
public SpringValueRegistry springValueRegistry() { public SpringValueRegistry springValueRegistry() {
return new SpringValueRegistry(); return new SpringValueRegistry();
@ -101,22 +98,16 @@ public class PolarisConfigAutoConfiguration {
return new SpringValueProcessor(placeholderHelper, springValueRegistry, polarisConfigProperties); return new SpringValueProcessor(placeholderHelper, springValueRegistry, polarisConfigProperties);
} }
@Configuration(proxyBeanMethods = false)
@ConditionalOnReflectRefreshType
@AutoConfigureBefore(PolarisConfigAutoConfiguration.class)
public static class PolarisReflectRefresherAutoConfiguration {
@Bean @Bean
public PolarisConfigPropertyRefresher polarisReflectPropertySourceAutoRefresher( public PolarisConfigPropertyRefresher polarisReflectPropertySourceAutoRefresher(
PolarisConfigProperties polarisConfigProperties, SpringValueRegistry springValueRegistry, PolarisConfigProperties polarisConfigProperties, SpringValueRegistry springValueRegistry,
PlaceholderHelper placeholderHelper) { PlaceholderHelper placeholderHelper, ConfigFileService configFileService, ContextRefresher contextRefresher) {
return new PolarisRefreshAffectedContextRefresher(polarisConfigProperties, return new PolarisRefreshAffectedContextRefresher(polarisConfigProperties,
springValueRegistry, placeholderHelper); springValueRegistry, placeholderHelper, configFileService, contextRefresher);
}
@Bean
public PolarisConfigRefreshScopeAnnotationDetector polarisConfigRefreshScopeAnnotationDetector() {
return new PolarisConfigRefreshScopeAnnotationDetector();
}
@Bean
public PolarisConfigRefreshOptimizationListener polarisConfigRefreshOptimizationListener() {
return new PolarisConfigRefreshOptimizationListener();
} }
} }
} }

@ -36,5 +36,5 @@ public interface PolarisConfigCustomExtensionLayer {
void executeAfterLocateConfigReturning(CompositePropertySource compositePropertySource); void executeAfterLocateConfigReturning(CompositePropertySource compositePropertySource);
boolean executeRegisterPublishChangeListener(PolarisPropertySource polarisPropertySource); boolean executeRegisterPublishChangeListener(PolarisPropertySource polarisPropertySource, PolarisPropertySource effectPolarisPropertySource);
} }

@ -31,6 +31,7 @@ import com.tencent.cloud.polaris.context.config.PolarisContextProperties;
import com.tencent.polaris.configuration.api.core.ConfigFileMetadata; import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
import com.tencent.polaris.configuration.api.core.ConfigFileService; import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.api.core.ConfigKVFile; 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 com.tencent.polaris.configuration.client.internal.DefaultConfigFileMetadata;
import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger; 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 // this class provides customized logic for some customers to configure special business group files
private final PolarisConfigCustomExtensionLayer polarisConfigCustomExtensionLayer = PolarisServiceLoaderUtil.getPolarisConfigCustomExtensionLayer(); private final PolarisConfigCustomExtensionLayer polarisConfigCustomExtensionLayer = PolarisServiceLoaderUtil.getPolarisConfigCustomExtensionLayer();
private volatile static CompositePropertySource compositePropertySourceCache = null;
public PolarisConfigFileLocator(PolarisConfigProperties polarisConfigProperties, public PolarisConfigFileLocator(PolarisConfigProperties polarisConfigProperties,
PolarisContextProperties polarisContextProperties, ConfigFileService configFileService, Environment environment) { PolarisContextProperties polarisContextProperties, ConfigFileService configFileService, Environment environment) {
this.polarisConfigProperties = polarisConfigProperties; this.polarisConfigProperties = polarisConfigProperties;
@ -76,10 +79,21 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
this.environment = environment; 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 @Override
public PropertySource<?> locate(Environment environment) { public PropertySource<?> locate(Environment environment) {
if (polarisConfigProperties.isEnabled()) { if (polarisConfigProperties.isEnabled()) {
// use cache when refreshing context
if (compositePropertySourceCache != null) {
return compositePropertySourceCache;
}
CompositePropertySource compositePropertySource = new CompositePropertySource(POLARIS_CONFIG_PROPERTY_SOURCE_NAME); CompositePropertySource compositePropertySource = new CompositePropertySource(POLARIS_CONFIG_PROPERTY_SOURCE_NAME);
compositePropertySourceCache = compositePropertySource;
try { try {
// load custom config extension files // load custom config extension files
initCustomPolarisConfigExtensionFiles(compositePropertySource); initCustomPolarisConfigExtensionFiles(compositePropertySource);
@ -87,10 +101,11 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
initInternalConfigFiles(compositePropertySource); initInternalConfigFiles(compositePropertySource);
// load custom config files // load custom config files
List<ConfigFileGroup> configFileGroups = polarisConfigProperties.getGroups(); List<ConfigFileGroup> configFileGroups = polarisConfigProperties.getGroups();
if (CollectionUtils.isEmpty(configFileGroups)) { if (!CollectionUtils.isEmpty(configFileGroups)) {
return compositePropertySource;
}
initCustomPolarisConfigFiles(compositePropertySource, configFileGroups); initCustomPolarisConfigFiles(compositePropertySource, configFileGroups);
}
// load tsf default config group
initTsfConfigGroups(compositePropertySource);
return compositePropertySource; return compositePropertySource;
} }
finally { finally {
@ -123,7 +138,10 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
List<ConfigFileMetadata> internalConfigFiles = getInternalConfigFiles(); List<ConfigFileMetadata> internalConfigFiles = getInternalConfigFiles();
for (ConfigFileMetadata configFile : internalConfigFiles) { 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); compositePropertySource.addPropertySource(polarisPropertySource);
@ -190,6 +208,29 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap.yaml")); 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<String> 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<ConfigFileGroup> configFileGroups) { private void initCustomPolarisConfigFiles(CompositePropertySource compositePropertySource, List<ConfigFileGroup> configFileGroups) {
String namespace = polarisContextProperties.getNamespace(); String namespace = polarisContextProperties.getNamespace();
@ -201,16 +242,23 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
String group = configFileGroup.getName(); String group = configFileGroup.getName();
if (!StringUtils.hasText(group)) { if (!StringUtils.hasText(group)) {
throw new IllegalArgumentException("polaris config group name cannot be empty."); continue;
} }
List<String> files = configFileGroup.getFiles(); List<String> files = configFileGroup.getFiles();
if (CollectionUtils.isEmpty(files)) { 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) { for (String fileName : files) {
PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(groupNamespace, group, fileName); PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(configFileService, groupNamespace, group, fileName);
compositePropertySource.addPropertySource(polarisPropertySource); compositePropertySource.addPropertySource(polarisPropertySource);
@ -220,21 +268,10 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
} }
} }
} }
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);
}
else if (ConfigFileFormat.isPropertyFile(fileName)) {
configKVFile = configFileService.getConfigPropertiesFile(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"); public static PolarisPropertySource loadPolarisPropertySource(ConfigFileService configFileService, String namespace, String group, String fileName) {
} ConfigKVFile configKVFile = loadConfigKVFile(configFileService, namespace, group, fileName);
Map<String, Object> map = new ConcurrentHashMap<>(); Map<String, Object> map = new ConcurrentHashMap<>();
for (String key : configKVFile.getPropertyNames()) { for (String key : configKVFile.getPropertyNames()) {
@ -243,4 +280,52 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
return new PolarisPropertySource(namespace, group, fileName, configKVFile, map); return new PolarisPropertySource(namespace, group, fileName, configKVFile, map);
} }
public static PolarisPropertySource loadGroupPolarisPropertySource(ConfigFileService configFileService, String namespace, String group) {
List<ConfigKVFile> configKVFiles = new ArrayList<>();
com.tencent.polaris.configuration.api.core.ConfigFileGroup remoteGroup = configFileService.getConfigFileGroup(namespace, group);
if (remoteGroup == null) {
return null;
}
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<String, Object> map = new ConcurrentHashMap<>();
for (String key : compositeConfigFile.getPropertyNames()) {
String value = compositeConfigFile.getProperty(key, null);
map.put(key, value);
}
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;
}
} }

@ -18,12 +18,21 @@
package com.tencent.cloud.polaris.config.adapter; package com.tencent.cloud.polaris.config.adapter;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; 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.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.logger.PolarisConfigLoggerContext; 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.ConfigKVFile;
import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener; import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
@ -33,6 +42,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.core.env.PropertySource;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.util.CollectionUtils; 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 // this class provides customized logic for some customers to configure special business group files
private final PolarisConfigCustomExtensionLayer polarisConfigCustomExtensionLayer = PolarisServiceLoaderUtil.getPolarisConfigCustomExtensionLayer(); private final PolarisConfigCustomExtensionLayer polarisConfigCustomExtensionLayer = PolarisServiceLoaderUtil.getPolarisConfigCustomExtensionLayer();
public PolarisConfigPropertyAutoRefresher(PolarisConfigProperties polarisConfigProperties) { private static final Set<String> registeredPolarisPropertySets = Sets.newConcurrentHashSet();
private final ConfigFileService configFileService;
public PolarisConfigPropertyAutoRefresher(PolarisConfigProperties polarisConfigProperties,
ConfigFileService configFileService) {
this.polarisConfigProperties = polarisConfigProperties; this.polarisConfigProperties = polarisConfigProperties;
this.configFileService = configFileService;
} }
@Override @Override
@ -81,13 +97,16 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
// register polaris config publish event // register polaris config publish event
for (PolarisPropertySource polarisPropertySource : polarisPropertySources) { for (PolarisPropertySource polarisPropertySource : polarisPropertySources) {
// group property source
if (polarisPropertySource.getConfigKVFile() instanceof CompositeConfigFile) { if (polarisPropertySource.getConfigKVFile() instanceof CompositeConfigFile) {
CompositeConfigFile configKVFile = (CompositeConfigFile) polarisPropertySource.getConfigKVFile(); CompositeConfigFile configKVFile = (CompositeConfigFile) polarisPropertySource.getConfigKVFile();
for (ConfigKVFile cf : configKVFile.getConfigKVFiles()) { for (ConfigKVFile cf : configKVFile.getConfigKVFiles()) {
PolarisPropertySource p = new PolarisPropertySource(cf.getNamespace(), cf.getFileGroup(), cf.getFileName(), cf, new HashMap<>()); PolarisPropertySource p = new PolarisPropertySource(cf.getNamespace(), cf.getFileGroup(), cf.getFileName(), cf, new HashMap<>());
registerPolarisConfigPublishChangeListener(p); registerPolarisConfigPublishChangeListener(p, polarisPropertySource);
customRegisterPolarisConfigPublishChangeListener(p); customRegisterPolarisConfigPublishChangeListener(p, polarisPropertySource);
registeredPolarisPropertySets.add(p.getPropertySourceName());
} }
registerPolarisConfigGroupChangeListener(polarisPropertySource);
} }
else { else {
registerPolarisConfigPublishChangeListener(polarisPropertySource); registerPolarisConfigPublishChangeListener(polarisPropertySource);
@ -104,14 +123,74 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
polarisConfigCustomExtensionLayer.initRegisterConfig(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<ConfigFileMetadata> oldConfigFileMetadataList = event.getOldConfigFileMetadataList();
List<ConfigFileMetadata> newConfigFileMetadataList = event.getNewConfigFileMetadataList();
Map<String, ConfigFileMetadata> added = calculateUnregister(oldConfigFileMetadataList, newConfigFileMetadataList);
if (added.isEmpty()) {
return;
}
Set<String> changedKeys = new HashSet<>();
for (Map.Entry<String, ConfigFileMetadata> 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) { public void registerPolarisConfigPublishChangeListener(PolarisPropertySource polarisPropertySource) {
LOGGER.info("{} will register polaris config publish listener", polarisPropertySource.getPropertySourceName()); registerPolarisConfigPublishChangeListener(polarisPropertySource, polarisPropertySource);
polarisPropertySource.getConfigKVFile() }
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 -> { .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<String, Object> source = polarisPropertySource.getSource(); Map<String, Object> effectSource = effectPolarisPropertySource.getSource();
Map<String, Object> 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()) { for (String changedKey : configKVFileChangeEvent.changedKeys()) {
ConfigPropertyChangeInfo configPropertyChangeInfo = configKVFileChangeEvent.getChangeInfo(changedKey); ConfigPropertyChangeInfo configPropertyChangeInfo = configKVFileChangeEvent.getChangeInfo(changedKey);
@ -133,10 +212,27 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
switch (configPropertyChangeInfo.getChangeType()) { switch (configPropertyChangeInfo.getChangeType()) {
case MODIFIED: case MODIFIED:
case ADDED: case ADDED:
source.put(changedKey, configPropertyChangeInfo.getNewValue()); effectSource.put(changedKey, configPropertyChangeInfo.getNewValue());
if (isGroupRefresh) {
listenSource.put(changedKey, configPropertyChangeInfo.getNewValue());
}
break; break;
case DELETED: 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; break;
} }
// update the attribute with @Value annotation // update the attribute with @Value annotation
@ -148,11 +244,43 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
} }
private void customRegisterPolarisConfigPublishChangeListener(PolarisPropertySource polarisPropertySource) { private void customRegisterPolarisConfigPublishChangeListener(PolarisPropertySource polarisPropertySource) {
customRegisterPolarisConfigPublishChangeListener(polarisPropertySource, polarisPropertySource);
}
private void customRegisterPolarisConfigPublishChangeListener(PolarisPropertySource listenPolarisPropertySource, PolarisPropertySource effectPolarisPropertySource) {
if (polarisConfigCustomExtensionLayer == null) { if (polarisConfigCustomExtensionLayer == null) {
LOGGER.debug("[SCT Config] PolarisConfigCustomExtensionLayer is not init, ignore the following execution steps"); LOGGER.debug("[SCT Config] PolarisConfigCustomExtensionLayer is not init, ignore the following execution steps");
return; return;
} }
polarisConfigCustomExtensionLayer.executeRegisterPublishChangeListener(polarisPropertySource); polarisConfigCustomExtensionLayer.executeRegisterPublishChangeListener(listenPolarisPropertySource, effectPolarisPropertySource);
}
private Map<String, ConfigFileMetadata> calculateUnregister(List<ConfigFileMetadata> oldConfigFileMetadataList,
List<ConfigFileMetadata> newConfigFileMetadataList) {
Map<String, ConfigFileMetadata> oldConfigFileMetadataMap = oldConfigFileMetadataList.stream()
.collect(Collectors.toMap(
configFileMetadata -> PolarisPropertySourceUtils.generateName(
configFileMetadata.getNamespace(),
configFileMetadata.getFileGroup(),
configFileMetadata.getFileName()),
configFileMetadata -> configFileMetadata));
Map<String, ConfigFileMetadata> newConfigFileMetadataMap = newConfigFileMetadataList.stream()
.collect(Collectors.toMap(
configFileMetadata -> PolarisPropertySourceUtils.generateName(
configFileMetadata.getNamespace(),
configFileMetadata.getFileGroup(),
configFileMetadata.getFileName()),
configFileMetadata -> configFileMetadata));
Map<String, ConfigFileMetadata> added = new HashMap<>();
for (Map.Entry<String, ConfigFileMetadata> entry : newConfigFileMetadataMap.entrySet()) {
if (!oldConfigFileMetadataMap.containsKey(entry.getKey())) {
added.put(entry.getKey(), entry.getValue());
}
}
return added;
} }
/** /**

@ -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}.
*
* <p>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;
}
}

@ -24,6 +24,7 @@ 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.PlaceholderHelper;
import com.tencent.cloud.polaris.config.spring.property.SpringValue; import com.tencent.cloud.polaris.config.spring.property.SpringValue;
import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -31,6 +32,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
@ -57,11 +59,15 @@ public class PolarisRefreshAffectedContextRefresher extends PolarisConfigPropert
private TypeConverter typeConverter; private TypeConverter typeConverter;
private ContextRefresher contextRefresher;
public PolarisRefreshAffectedContextRefresher(PolarisConfigProperties polarisConfigProperties, public PolarisRefreshAffectedContextRefresher(PolarisConfigProperties polarisConfigProperties,
SpringValueRegistry springValueRegistry, PlaceholderHelper placeholderHelper) { SpringValueRegistry springValueRegistry, PlaceholderHelper placeholderHelper,
super(polarisConfigProperties); ConfigFileService configFileService, ContextRefresher contextRefresher) {
super(polarisConfigProperties, configFileService);
this.springValueRegistry = springValueRegistry; this.springValueRegistry = springValueRegistry;
this.placeholderHelper = placeholderHelper; this.placeholderHelper = placeholderHelper;
this.contextRefresher = contextRefresher;
} }
@Override @Override
@ -78,8 +84,21 @@ public class PolarisRefreshAffectedContextRefresher extends PolarisConfigPropert
@Override @Override
public void refreshConfigurationProperties(Set<String> changeKeys) { public void refreshConfigurationProperties(Set<String> 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)); context.publishEvent(new EnvironmentChangeEvent(context, changeKeys));
} }
}
private void updateSpringValue(SpringValue springValue) { private void updateSpringValue(SpringValue springValue) {
try { try {

@ -13,6 +13,7 @@
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * 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 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. * specific language governing permissions and limitations under the License.
*
*/ */
package com.tencent.cloud.polaris.config.adapter; package com.tencent.cloud.polaris.config.adapter;
@ -20,8 +21,15 @@ package com.tencent.cloud.polaris.config.adapter;
import java.util.Set; import java.util.Set;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; 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.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. * The default implement of Spring Cloud refreshes the entire Spring Context.
@ -29,13 +37,19 @@ import org.springframework.cloud.context.refresh.ContextRefresher;
* *
* @author lingxiao.wlx * @author lingxiao.wlx
*/ */
public class PolarisRefreshEntireContextRefresher extends PolarisConfigPropertyAutoRefresher { public class PolarisRefreshEntireContextRefresher extends PolarisConfigPropertyAutoRefresher implements ApplicationContextAware {
private final ContextRefresher contextRefresher; private final ContextRefresher contextRefresher;
private final SpringValueRegistry springValueRegistry;
private ConfigurableApplicationContext context;
public PolarisRefreshEntireContextRefresher(PolarisConfigProperties polarisConfigProperties, public PolarisRefreshEntireContextRefresher(PolarisConfigProperties polarisConfigProperties,
ContextRefresher contextRefresher) { SpringValueRegistry springValueRegistry, ConfigFileService configFileService, ContextRefresher contextRefresher) {
super(polarisConfigProperties);
super(polarisConfigProperties, configFileService);
this.springValueRegistry = springValueRegistry;
this.contextRefresher = contextRefresher; this.contextRefresher = contextRefresher;
} }
@ -46,6 +60,24 @@ public class PolarisRefreshEntireContextRefresher extends PolarisConfigPropertyA
@Override @Override
public void refreshConfigurationProperties(Set<String> changeKeys) { public void refreshConfigurationProperties(Set<String> changeKeys) {
boolean needRefreshContext = false;
for (String changedKey : changeKeys) {
boolean inRefreshScope = springValueRegistry.isRefreshScopeKey(changedKey);
if (inRefreshScope) {
needRefreshContext = true;
break;
}
}
if (needRefreshContext) {
contextRefresher.refresh(); contextRefresher.refresh();
} }
else {
context.publishEvent(new EnvironmentChangeEvent(context, changeKeys));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = (ConfigurableApplicationContext) applicationContext;
}
} }

@ -36,7 +36,7 @@ public class ReflectRefreshTypeCondition extends SpringBootCondition {
*/ */
public static final String POLARIS_CONFIG_REFRESH_TYPE = "spring.cloud.polaris.config.refresh-type"; 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 @Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {

@ -69,7 +69,7 @@ public class PolarisConfigProperties {
/** /**
* Attribute refresh type. * Attribute refresh type.
*/ */
private RefreshType refreshType = RefreshType.REFLECT; private RefreshType refreshType = RefreshType.REFRESH_CONTEXT;
/** /**
* List of injected configuration files. * List of injected configuration files.
@ -95,6 +95,11 @@ public class PolarisConfigProperties {
*/ */
private boolean internalEnabled = true; private boolean internalEnabled = true;
/**
* if check address is enabled.
*/
private boolean checkAddress = true;
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
@ -191,6 +196,14 @@ public class PolarisConfigProperties {
this.internalEnabled = internalEnabled; this.internalEnabled = internalEnabled;
} }
public boolean isCheckAddress() {
return checkAddress;
}
public void setCheckAddress(boolean checkAddress) {
this.checkAddress = checkAddress;
}
@Override @Override
public String toString() { public String toString() {
return "PolarisConfigProperties{" + return "PolarisConfigProperties{" +
@ -206,6 +219,7 @@ public class PolarisConfigProperties {
", dataSource='" + dataSource + '\'' + ", dataSource='" + dataSource + '\'' +
", localFileRootPath='" + localFileRootPath + '\'' + ", localFileRootPath='" + localFileRootPath + '\'' +
", internalEnabled=" + internalEnabled + ", internalEnabled=" + internalEnabled +
", checkAddress=" + checkAddress +
'}'; '}';
} }
} }

@ -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}.
*
* <p>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.</p>
*
* @author jarvisxiong
*/
public class PolarisConfigRefreshOptimizationListener implements ApplicationListener<ContextRefreshedEvent> {
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);
}
}

@ -23,7 +23,11 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.springframework.beans.BeansException; 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.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.Ordered;
import org.springframework.core.PriorityOrdered; import org.springframework.core.PriorityOrdered;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
@ -34,17 +38,32 @@ import org.springframework.util.ReflectionUtils;
* *
* @author weihubeats 2022-7-10 * @author weihubeats 2022-7-10
*/ */
public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrdered { public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware {
private ConfigurableListableBeanFactory beanFactory;
@Override @Override
public Object postProcessBeforeInitialization(Object bean, @NonNull String beanName) public Object postProcessBeforeInitialization(Object bean, @NonNull String beanName)
throws BeansException { throws BeansException {
Class<?> clazz = bean.getClass(); 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)) { for (Field field : findAllField(clazz)) {
processField(bean, beanName, field); processField(bean, beanName, field, isRefreshScope);
} }
for (Method method : findAllMethod(clazz)) { for (Method method : findAllMethod(clazz)) {
processMethod(bean, beanName, method); processMethod(bean, beanName, method, isRefreshScope);
} }
return bean; return bean;
} }
@ -60,7 +79,7 @@ public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrd
* @param beanName beanName * @param beanName beanName
* @param field field * @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. * subclass should implement this method to process method.
@ -68,7 +87,7 @@ public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrd
* @param beanName beanName * @param beanName beanName
* @param method method * @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 @Override
@ -77,15 +96,20 @@ public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrd
return Ordered.LOWEST_PRECEDENCE; return Ordered.LOWEST_PRECEDENCE;
} }
private List<Field> findAllField(Class<?> clazz) { protected List<Field> findAllField(Class<?> clazz) {
final List<Field> res = new LinkedList<>(); final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, res::add); ReflectionUtils.doWithFields(clazz, res::add);
return res; return res;
} }
private List<Method> findAllMethod(Class<?> clazz) { protected List<Method> findAllMethod(Class<?> clazz) {
final List<Method> res = new LinkedList<>(); final List<Method> res = new LinkedList<>();
ReflectionUtils.doWithMethods(clazz, res::add); ReflectionUtils.doWithMethods(clazz, res::add);
return res; return res;
} }
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.beanFactory = (ConfigurableListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
}
} }

@ -21,11 +21,13 @@ import java.beans.PropertyDescriptor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Member; import java.lang.reflect.Member;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.google.common.base.CaseFormat;
import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
@ -35,6 +37,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.SpringValue;
import com.tencent.cloud.polaris.config.spring.property.SpringValueDefinition; import com.tencent.cloud.polaris.config.spring.property.SpringValueDefinition;
import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry;
import com.tencent.polaris.api.utils.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -50,6 +53,8 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; 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.context.annotation.Bean;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
@ -105,23 +110,31 @@ public class SpringValueProcessor extends PolarisProcessor implements BeanDefini
@Override @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 // register @Value on field
Value value = field.getAnnotation(Value.class); Value value = field.getAnnotation(Value.class);
if (value == null) { if (value == null) {
return; return;
} }
doRegister(bean, beanName, field, value); doRegister(bean, beanName, field, value, isRefreshScope);
} }
@Override @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 //register @Value on method
Value value = method.getAnnotation(Value.class); Value value = method.getAnnotation(Value.class);
if (value == null) { if (value != null) {
processMethodValue(bean, beanName, method, value, isRefreshScope);
return; 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 //skip Configuration bean methods
if (method.getAnnotation(Bean.class) != null) { if (method.getAnnotation(Bean.class) != null) {
return; return;
@ -131,8 +144,128 @@ public class SpringValueProcessor extends PolarisProcessor implements BeanDefini
bean.getClass().getName(), method.getName(), method.getParameterTypes().length); bean.getClass().getName(), method.getName(), method.getParameterTypes().length);
return; 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<String> 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<String> 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()) + ".");
}
}
}
/**
* 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;
}
doRegister(bean, beanName, method, value); /**
* 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 @Override
@ -147,7 +280,7 @@ public class SpringValueProcessor extends PolarisProcessor implements BeanDefini
} }
} }
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<String> keys = placeholderHelper.extractPlaceholderKeys(value.value()); Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
if (keys.isEmpty()) { if (keys.isEmpty()) {
return; return;
@ -158,10 +291,16 @@ public class SpringValueProcessor extends PolarisProcessor implements BeanDefini
if (member instanceof Field) { if (member instanceof Field) {
Field field = (Field) member; Field field = (Field) member;
springValue = new SpringValue(key, value.value(), bean, beanName, field); springValue = new SpringValue(key, value.value(), bean, beanName, field);
if (isRefreshScope) {
springValueRegistry.putRefreshScopeKey(key);
}
} }
else if (member instanceof Method) { else if (member instanceof Method) {
Method method = (Method) member; Method method = (Method) member;
springValue = new SpringValue(key, value.value(), bean, beanName, method); springValue = new SpringValue(key, value.value(), bean, beanName, method);
if (isRefreshScope) {
springValueRegistry.putRefreshScopeKey(key);
}
} }
else { else {
LOGGER.error("Polaris @Value annotation currently only support to be used on methods and fields, " LOGGER.error("Polaris @Value annotation currently only support to be used on methods and fields, "

@ -20,6 +20,7 @@ package com.tencent.cloud.polaris.config.spring.property;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -29,6 +30,9 @@ import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps; 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 com.tencent.polaris.client.util.NamedThreadFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -54,6 +58,10 @@ public class SpringValueRegistry implements DisposableBean {
private final Object LOCK = new Object(); private final Object LOCK = new Object();
private ScheduledExecutorService executor; private ScheduledExecutorService executor;
private final TrieNode<String> refreshScopePrefixRoot = new TrieNode<>(TrieNode.ROOT_PATH);
private final Set<String> refreshScopeKeys = Sets.newConcurrentHashSet();
public void register(BeanFactory beanFactory, String key, SpringValue springValue) { public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
if (!registry.containsKey(beanFactory)) { if (!registry.containsKey(beanFactory)) {
synchronized (LOCK) { synchronized (LOCK) {
@ -63,7 +71,16 @@ public class SpringValueRegistry implements DisposableBean {
} }
} }
registry.get(beanFactory).put(key, springValue); Multimap<String, SpringValue> 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 // lazy initialize
if (initialized.compareAndSet(false, true)) { if (initialized.compareAndSet(false, true)) {
@ -102,6 +119,30 @@ public class SpringValueRegistry implements DisposableBean {
} }
} }
public void putRefreshScopePrefixKey(String key) {
TrieUtil.buildConfigTrieNode(key, refreshScopePrefixRoot);
}
public void putRefreshScopeKey(String key) {
refreshScopeKeys.add(key);
}
public void putRefreshScopeKeys(Set<String> 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 @Override
public void destroy() throws Exception { public void destroy() throws Exception {
executor.shutdown(); executor.shutdown();

@ -36,12 +36,32 @@ import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener;
public class MockedConfigKVFile implements ConfigKVFile { public class MockedConfigKVFile implements ConfigKVFile {
private final Map<String, Object> properties; private final Map<String, Object> properties;
private String fileName;
private String fileGroup;
private String namespace;
private final List<ConfigKVFileChangeListener> listeners = new ArrayList<>(); private final List<ConfigKVFileChangeListener> listeners = new ArrayList<>();
public MockedConfigKVFile(Map<String, Object> properties) { public MockedConfigKVFile(Map<String, Object> properties) {
this.properties = properties; this.properties = properties;
} }
public MockedConfigKVFile(Map<String, Object> properties, String fileName) {
this.properties = properties;
this.fileName = fileName;
}
public MockedConfigKVFile(Map<String, Object> properties, String fileName, String fileGroup, String namespace) {
this.properties = properties;
this.fileName = fileName;
this.fileGroup = fileGroup;
this.namespace = namespace;
}
@Override @Override
public String getProperty(String s, String s1) { public String getProperty(String s, String s1) {
return String.valueOf(properties.get(s)); return String.valueOf(properties.get(s));
@ -160,16 +180,16 @@ public class MockedConfigKVFile implements ConfigKVFile {
@Override @Override
public String getNamespace() { public String getNamespace() {
return null; return namespace;
} }
@Override @Override
public String getFileGroup() { public String getFileGroup() {
return null; return fileGroup;
} }
@Override @Override
public String getFileName() { public String getFileName() {
return null; return fileName;
} }
} }

@ -17,6 +17,8 @@
package com.tencent.cloud.polaris.config.adapter; package com.tencent.cloud.polaris.config.adapter;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -25,24 +27,34 @@ import java.util.Map;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.tencent.cloud.polaris.config.config.ConfigFileGroup; import com.tencent.cloud.polaris.config.config.ConfigFileGroup;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; 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.cloud.polaris.context.config.PolarisContextProperties;
import com.tencent.polaris.configuration.api.core.ConfigFileService; import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.api.core.ConfigKVFile; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import static org.assertj.core.api.Assertions.assertThat; 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; import static org.mockito.Mockito.when;
/** /**
* test for {@link PolarisConfigFileLocator}. * test for {@link PolarisConfigFileLocator}.
*@author lepdou 2022-06-11 *
* @author lepdou 2022-06-11
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class PolarisConfigFileLocatorTest { public class PolarisConfigFileLocatorTest {
@ -65,6 +77,7 @@ public class PolarisConfigFileLocatorTest {
@Test @Test
public void testLoadApplicationPropertiesFile() { public void testLoadApplicationPropertiesFile() {
clearCompositePropertySourceCache();
PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties,
configFileService, environment); configFileService, environment);
@ -102,6 +115,7 @@ public class PolarisConfigFileLocatorTest {
@Test @Test
public void testActiveProfileFilesPriorityBiggerThanDefault() { public void testActiveProfileFilesPriorityBiggerThanDefault() {
clearCompositePropertySourceCache();
PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties,
configFileService, environment); configFileService, environment);
@ -151,6 +165,7 @@ public class PolarisConfigFileLocatorTest {
@Test @Test
public void testGetCustomFiles() { public void testGetCustomFiles() {
clearCompositePropertySourceCache();
PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties, PolarisConfigFileLocator locator = new PolarisConfigFileLocator(polarisConfigProperties, polarisContextProperties,
configFileService, environment); configFileService, environment);
@ -201,4 +216,166 @@ public class PolarisConfigFileLocatorTest {
assertThat(propertySource.getProperty("k2")).isEqualTo("v2"); assertThat(propertySource.getProperty("k2")).isEqualTo("v2");
assertThat(propertySource.getProperty("k3")).isEqualTo("v3"); 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<String, Object> 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<ConfigFileGroup> 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<String, Object> 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<String, Object> 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<PolarisConfigFileLocator> 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<PolarisPropertySource> 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 = "<PASSWORD>";
boolean autoRefresh = true;
RefreshType refreshType = RefreshType.REFRESH_CONTEXT;
List<ConfigFileGroup> 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
}
}
} }

@ -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 {
}
}

@ -19,17 +19,24 @@ package com.tencent.cloud.polaris.config.adapter;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; 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.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; 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.SpringValue;
import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry;
import com.tencent.polaris.configuration.api.core.ChangeType; 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.ConfigKVFileChangeEvent;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -38,6 +45,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -54,6 +62,7 @@ import static org.mockito.Mockito.when;
public class PolarisPropertiesSourceAutoRefresherTest { public class PolarisPropertiesSourceAutoRefresherTest {
private final String testNamespace = "testNamespace"; private final String testNamespace = "testNamespace";
private final String testFileGroup = "testFileGroup";
private final String testServiceName = "testServiceName"; private final String testServiceName = "testServiceName";
private final String testFileName = "application.properties"; private final String testFileName = "application.properties";
@Mock @Mock
@ -65,6 +74,12 @@ public class PolarisPropertiesSourceAutoRefresherTest {
@Mock @Mock
private PlaceholderHelper placeholderHelper; private PlaceholderHelper placeholderHelper;
@Mock
private ConfigFileService configFileService;
@Mock
private ContextRefresher contextRefresher;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
PolarisPropertySourceManager.clearPropertySources(); PolarisPropertySourceManager.clearPropertySources();
@ -72,7 +87,8 @@ public class PolarisPropertiesSourceAutoRefresherTest {
@Test @Test
public void testConfigFileChanged() throws Exception { 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); ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class);
ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class); ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class);
TypeConverter typeConverter = mock(TypeConverter.class); TypeConverter typeConverter = mock(TypeConverter.class);
@ -105,10 +121,13 @@ public class PolarisPropertiesSourceAutoRefresherTest {
ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1", "v11", ChangeType.MODIFIED); ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", "v1", "v11", ChangeType.MODIFIED);
ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED); ConfigPropertyChangeInfo changeInfo2 = new ConfigPropertyChangeInfo("k4", null, "v4", ChangeType.ADDED);
ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k2", "v2", null, ChangeType.DELETED); ConfigPropertyChangeInfo changeInfo3 = new ConfigPropertyChangeInfo("k2", "v2", null, ChangeType.DELETED);
ConfigPropertyChangeInfo changeInfoLogger = new ConfigPropertyChangeInfo("logging.level.root", null, "info", ChangeType.ADDED);
Map<String, ConfigPropertyChangeInfo> changeInfos = new HashMap<>(); Map<String, ConfigPropertyChangeInfo> changeInfos = new HashMap<>();
changeInfos.put("k1", changeInfo); changeInfos.put("k1", changeInfo);
changeInfos.put("k2", changeInfo3); changeInfos.put("k2", changeInfo3);
changeInfos.put("k4", changeInfo2); changeInfos.put("k4", changeInfo2);
changeInfos.put("logging.level.root", changeInfoLogger);
ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos); ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos);
refresher.onApplicationEvent(null); refresher.onApplicationEvent(null);
@ -120,4 +139,88 @@ public class PolarisPropertiesSourceAutoRefresherTest {
assertThat(polarisPropertySource.getProperty("k2")).isNull(); assertThat(polarisPropertySource.getProperty("k2")).isNull();
assertThat(polarisPropertySource.getProperty("k4")).isEqualTo("v4"); 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<SpringValue> 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<String, Object> 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<String, Object> 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<String, ConfigPropertyChangeInfo> 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);
}
} }

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}

@ -17,13 +17,16 @@
package com.tencent.cloud.polaris.config.listener; package com.tencent.cloud.polaris.config.listener;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.google.common.collect.Sets; 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.cloud.polaris.config.annotation.PolarisConfigKVFileChangeListener;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -34,6 +37,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
@ -46,7 +50,9 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = DEFINED_PORT, classes = ConfigChangeListenerTest.TestApplication.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 { public class ConfigChangeListenerTest {
private static final CountDownLatch hits = new CountDownLatch(2); private static final CountDownLatch hits = new CountDownLatch(2);
@ -57,6 +63,19 @@ public class ConfigChangeListenerTest {
@Autowired @Autowired
private TestApplication.TestConfig testConfig; 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 @Test
public void test() throws InterruptedException { public void test() throws InterruptedException {
//before change //before change

@ -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<String, Object> 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<String, ConfigPropertyChangeInfo> 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 {
}
}
}

@ -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<String, Object> 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<String, ConfigPropertyChangeInfo> 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 {
}
}
}

@ -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<String> set;
private ArrayList<String> list;
private String[] array;
private HashMap<String, String> map;
private InnerProperties inner;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HashSet<String> getSet() {
return set;
}
public void setSet(HashSet<String> set) {
this.set = set;
}
public ArrayList<String> getList() {
return list;
}
public void setList(ArrayList<String> list) {
this.list = list;
}
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
public HashMap<String, String> getMap() {
return map;
}
public void setMap(HashMap<String, String> 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;
}
}
}

@ -25,6 +25,7 @@ import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.util.GzipUtil; import com.tencent.cloud.common.util.GzipUtil;
import com.tencent.cloud.polaris.PolarisDiscoveryProperties; import com.tencent.cloud.polaris.PolarisDiscoveryProperties;
import com.tencent.cloud.polaris.contract.config.PolarisContractProperties; import com.tencent.cloud.polaris.contract.config.PolarisContractProperties;
@ -100,7 +101,7 @@ public class PolarisContractReporter implements ApplicationListener<ApplicationR
ReportServiceContractRequest request = new ReportServiceContractRequest(); ReportServiceContractRequest request = new ReportServiceContractRequest();
String name = polarisContractProperties.getName(); String name = polarisContractProperties.getName();
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
name = polarisDiscoveryProperties.getService(); name = MetadataContext.LOCAL_SERVICE;
} }
request.setName(name); request.setName(name);
request.setNamespace(polarisDiscoveryProperties.getNamespace()); request.setNamespace(polarisDiscoveryProperties.getNamespace());

@ -47,7 +47,7 @@ public final class FilterConstant {
/** /**
* Swagger resource url prefix. * 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. * Swagger webjars V2 url prefix.

@ -18,6 +18,12 @@
<dependency> <dependency>
<groupId>com.tencent.cloud</groupId> <groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-rpc-enhancement</artifactId> <artifactId>spring-cloud-tencent-rpc-enhancement</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-security-crypto</artifactId>
<groupId>org.springframework.security</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>

@ -82,7 +82,7 @@ public class PolarisServiceDiscovery {
return Collections.emptyList(); return Collections.emptyList();
} }
return polarisDiscoveryHandler.getServices().getServices().stream() return polarisDiscoveryHandler.getServices().getServices().stream()
.map(ServiceInfo::getService).collect(Collectors.toList()); .map(ServiceInfo::getService).distinct().collect(Collectors.toList());
} }
} }

@ -17,26 +17,15 @@
package com.tencent.cloud.polaris.loadbalancer; 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.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.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; 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.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration; import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.util.CollectionUtils;
/** /**
* Auto-configuration of loadbalancer for Polaris. * Auto-configuration of loadbalancer for Polaris.
@ -52,42 +41,4 @@ import org.springframework.util.CollectionUtils;
@LoadBalancerClients(defaultConfiguration = PolarisLoadBalancerClientConfiguration.class) @LoadBalancerClients(defaultConfiguration = PolarisLoadBalancerClientConfiguration.class)
public class PolarisLoadBalancerAutoConfiguration { public class PolarisLoadBalancerAutoConfiguration {
@Bean
public RestTemplateCustomizer polarisRestTemplateCustomizer(
@Autowired(required = false) RetryLoadBalancerInterceptor retryLoadBalancerInterceptor,
@Autowired(required = false) LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> 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);
};
}
} }

@ -57,7 +57,7 @@ public class PolarisRegistration implements Registration {
private final StaticMetadataManager staticMetadataManager; private final StaticMetadataManager staticMetadataManager;
private final String serviceId; private String serviceId;
private final String host; private final String host;
private final boolean isSecure; private final boolean isSecure;
private final ServletWebServerApplicationContext servletWebServerApplicationContext; private final ServletWebServerApplicationContext servletWebServerApplicationContext;
@ -164,6 +164,10 @@ public class PolarisRegistration implements Registration {
} }
} }
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
@Override @Override
public String getServiceId() { public String getServiceId() {
return serviceId; return serviceId;

@ -23,6 +23,7 @@ import java.util.Objects;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.StaticMetadataManager; import com.tencent.cloud.common.metadata.StaticMetadataManager;
import com.tencent.cloud.common.util.OkHttpUtil; import com.tencent.cloud.common.util.OkHttpUtil;
import com.tencent.cloud.common.util.OtUtils; import com.tencent.cloud.common.util.OtUtils;
@ -106,6 +107,7 @@ public class PolarisServiceRegistry implements ServiceRegistry<PolarisRegistrati
} }
registration.customize(); registration.customize();
String serviceId = registration.getServiceId(); String serviceId = registration.getServiceId();
MetadataContext.setLocalService(serviceId);
// Register instance. // Register instance.
InstanceRegisterRequest instanceRegisterRequest = new InstanceRegisterRequest(); InstanceRegisterRequest instanceRegisterRequest = new InstanceRegisterRequest();

@ -17,7 +17,10 @@
package com.tencent.cloud.plugin.lossless; package com.tencent.cloud.plugin.lossless;
import java.lang.reflect.Field;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import com.tencent.cloud.common.util.OkHttpUtil; import com.tencent.cloud.common.util.OkHttpUtil;
import com.tencent.cloud.plugin.lossless.config.LosslessAutoConfiguration; import com.tencent.cloud.plugin.lossless.config.LosslessAutoConfiguration;
@ -28,6 +31,7 @@ import com.tencent.cloud.polaris.discovery.PolarisDiscoveryAutoConfiguration;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClientConfiguration; import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClientConfiguration;
import com.tencent.cloud.polaris.registry.PolarisRegistration; import com.tencent.cloud.polaris.registry.PolarisRegistration;
import com.tencent.cloud.polaris.registry.PolarisServiceRegistry; 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.api.pojo.ServiceKey;
import com.tencent.polaris.test.mock.discovery.NamingServer; import com.tencent.polaris.test.mock.discovery.NamingServer;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
@ -40,6 +44,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration;
import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationUtils; 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 org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat; 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.namespace=" + NAMESPACE_TEST)
.withPropertyValues("spring.cloud.polaris.discovery.token=xxxxxx"); .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 @BeforeAll
static void beforeAll() throws Exception { static void beforeAll() throws Exception {
namingServer = NamingServer.startNamingServer(10081); namingServer = NamingServer.startNamingServer(10081);
@ -185,9 +217,121 @@ public class LosslessRegistryAspectTest {
}); });
} }
@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 @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
static class PolarisPropertiesConfiguration { 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<String, String> getMetadata() {
return null;
}
};
}
@Bean
public RegistrationTransformer mockRegistrationTransformer() {
return new RegistrationTransformer() {
@Override
public String getRegistry() {
return null;
}
};
}
}
} }

@ -103,6 +103,7 @@ public class QuotaCheckReactiveFilter implements WebFilter, Ordered {
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, localNamespace, localService, 1, path); quotaResponse = QuotaCheckUtils.getQuota(limitAPI, localNamespace, localService, 1, path);
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) {
LOG.info("block by ratelimit rule, uri:{}", exchange.getRequest().getURI());
ServerHttpResponse response = exchange.getResponse(); ServerHttpResponse response = exchange.getResponse();
DataBuffer dataBuffer; DataBuffer dataBuffer;
if (Objects.nonNull(quotaResponse.getActiveRule()) if (Objects.nonNull(quotaResponse.getActiveRule())

@ -97,6 +97,7 @@ public class QuotaCheckServletFilter extends OncePerRequestFilter {
try { try {
quotaResponse = QuotaCheckUtils.getQuota(limitAPI, localNamespace, localService, 1, request.getRequestURI()); quotaResponse = QuotaCheckUtils.getQuota(limitAPI, localNamespace, localService, 1, request.getRequestURI());
if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) { if (quotaResponse.getCode() == QuotaResultCode.QuotaResultLimited) {
LOG.info("block by ratelimit rule, uri:{}", request.getRequestURI());
if (Objects.nonNull(quotaResponse.getActiveRule()) if (Objects.nonNull(quotaResponse.getActiveRule())
&& StringUtils.isNotBlank(quotaResponse.getActiveRule().getCustomResponse().getBody())) { && StringUtils.isNotBlank(quotaResponse.getActiveRule().getCustomResponse().getBody())) {
response.setStatus(polarisRateLimitProperties.getRejectHttpCode()); response.setStatus(polarisRateLimitProperties.getRejectHttpCode());

@ -32,7 +32,6 @@ import com.tencent.cloud.polaris.router.interceptor.MetadataRouterRequestInterce
import com.tencent.cloud.polaris.router.interceptor.NamespaceRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.NamespaceRouterRequestInterceptor;
import com.tencent.cloud.polaris.router.interceptor.NearbyRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.NearbyRouterRequestInterceptor;
import com.tencent.cloud.polaris.router.interceptor.RuleBasedRouterRequestInterceptor; 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.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -127,13 +126,7 @@ public class RouterAutoConfiguration {
public SmartInitializingSingleton addRouterLabelInterceptorForRestTemplate(RouterLabelRestTemplateInterceptor interceptor) { public SmartInitializingSingleton addRouterLabelInterceptorForRestTemplate(RouterLabelRestTemplateInterceptor interceptor) {
return () -> restTemplates.forEach(restTemplate -> { return () -> restTemplates.forEach(restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors()); List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
int addIndex = list.size(); list.add(interceptor);
for (int i = 0; i < list.size(); i++) {
if (list.get(i) instanceof EnhancedRestTemplateInterceptor) {
addIndex = i;
}
}
list.add(addIndex, interceptor);
restTemplate.setInterceptors(list); restTemplate.setInterceptors(list);
}); });
} }

@ -15,7 +15,7 @@
<properties> <properties>
<commons.lang.version>2.6</commons.lang.version> <commons.lang.version>2.6</commons.lang.version>
<commons.io.version>2.11.0</commons.io.version> <commons.io.version>2.14.0</commons.io.version>
</properties> </properties>
<dependencies> <dependencies>
@ -53,11 +53,23 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
<exclusions>
<exclusion>
<artifactId>tomcat-embed-el</artifactId>
<groupId>org.apache.tomcat.embed</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId> <artifactId>spring-cloud-starter</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-security-rsa</artifactId>
<groupId>org.springframework.security</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>

@ -43,4 +43,23 @@ public final class ContextConstant {
private 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() {
}
}
} }

@ -319,4 +319,8 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
break; break;
} }
} }
public static void setLocalService(String service) {
LOCAL_SERVICE = service;
}
} }

@ -71,10 +71,10 @@
<properties> <properties>
<!-- Project revision --> <!-- Project revision -->
<revision>2.0.0.0-2023.0.3-SNAPSHOT</revision> <revision>2.0.1.0-2023.0.3-SNAPSHOT</revision>
<!-- Polaris SDK version --> <!-- Polaris SDK version -->
<polaris.version>2.0.0.0</polaris.version> <polaris.version>2.0.0.1</polaris.version>
<!-- Dependencies --> <!-- Dependencies -->
<guava.version>32.0.1-jre</guava.version> <guava.version>32.0.1-jre</guava.version>
@ -217,6 +217,12 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-security-protection-plugin</artifactId>
<version>${revision}</version>
</dependency>
<!-- third part framework dependencies --> <!-- third part framework dependencies -->
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>

@ -22,6 +22,7 @@
<module>spring-cloud-starter-tencent-threadlocal-plugin</module> <module>spring-cloud-starter-tencent-threadlocal-plugin</module>
<module>spring-cloud-starter-tencent-trace-plugin</module> <module>spring-cloud-starter-tencent-trace-plugin</module>
<module>spring-cloud-starter-tencent-fault-tolerance</module> <module>spring-cloud-starter-tencent-fault-tolerance</module>
<module>spring-cloud-tencent-security-protection-plugin</module>
</modules> </modules>
</project> </project>

@ -49,7 +49,6 @@ public class NacosDiscoveryAdapterAutoConfiguration {
} }
@Bean @Bean
@ConditionalOnMissingBean
@ConditionalOnClass(name = "com.alibaba.cloud.nacos.registry.NacosRegistration") @ConditionalOnClass(name = "com.alibaba.cloud.nacos.registry.NacosRegistration")
public RegistrationTransformer registrationTransformer() { public RegistrationTransformer registrationTransformer() {
return new NacosRegistrationTransformer(); return new NacosRegistrationTransformer();

@ -17,6 +17,8 @@
package com.tencent.cloud.plugin.lossless.config; package com.tencent.cloud.plugin.lossless.config;
import java.util.List;
import com.tencent.cloud.plugin.lossless.LosslessRegistryAspect; import com.tencent.cloud.plugin.lossless.LosslessRegistryAspect;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import com.tencent.cloud.polaris.context.PolarisSDKContextManager; import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
@ -43,11 +45,36 @@ public class LosslessAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public LosslessRegistryAspect losslessRegistryAspect( public LosslessRegistryAspect losslessRegistryAspect(
ServiceRegistry serviceRegistry, Registration registration, PolarisContextProperties properties, List<ServiceRegistry> serviceRegistryList, List<Registration> registrationList, List<RegistrationTransformer> registrationTransformerList,
LosslessProperties losslessProperties, PolarisSDKContextManager polarisSDKContextManager, PolarisContextProperties properties, LosslessProperties losslessProperties, PolarisSDKContextManager polarisSDKContextManager) {
RegistrationTransformer registrationTransformer) {
return new LosslessRegistryAspect(serviceRegistry, registration, properties, losslessProperties, ServiceRegistry targetServiceRegistry = serviceRegistryList.size() > 0 ? serviceRegistryList.get(0) : null;
polarisSDKContextManager, registrationTransformer); 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);
} }
} }

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-tencent-plugin-starters</artifactId>
<groupId>com.tencent.cloud</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-tencent-security-protection-plugin</artifactId>
<name>Spring Cloud Tencent Lossless Plugin</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -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);
}
}

@ -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<RouterFunction> 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<org.springframework.web.reactive.function.server.RouterFunction> 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);
}
}
}
}

@ -0,0 +1,3 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tencent.cloud.plugin.protection.SecurityProtectionAutoConfiguration

@ -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<RouterFunction> 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<org.springframework.web.reactive.function.server.RouterFunction> 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");
}
}
}
}

@ -174,6 +174,16 @@
<dependency> <dependency>
<groupId>com.tencent.polaris</groupId> <groupId>com.tencent.polaris</groupId>
<artifactId>registry-memory</artifactId> <artifactId>registry-memory</artifactId>
<exclusions>
<exclusion>
<artifactId>error_prone_annotations</artifactId>
<groupId>com.google.errorprone</groupId>
</exclusion>
<exclusion>
<artifactId>j2objc-annotations</artifactId>
<groupId>com.google.j2objc</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>

@ -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);
}
}

@ -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<ApplicationEvent>, 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;
}
}

@ -2,7 +2,8 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.tencent.cloud.polaris.context.config.PolarisContextBootstrapAutoConfiguration,\ com.tencent.cloud.polaris.context.config.PolarisContextBootstrapAutoConfiguration,\
com.tencent.cloud.polaris.context.config.extend.tsf.TsfContextBootstrapConfiguration com.tencent.cloud.polaris.context.config.extend.tsf.TsfContextBootstrapConfiguration
org.springframework.context.ApplicationListener=\ 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=\ org.springframework.boot.env.EnvironmentPostProcessor=\
com.tencent.cloud.polaris.context.config.PolarisContextEnvironmentPostProcessor,\ com.tencent.cloud.polaris.context.config.PolarisContextEnvironmentPostProcessor,\
com.tencent.cloud.polaris.context.config.extend.tsf.TsfCoreEnvironmentPostProcessor com.tencent.cloud.polaris.context.config.extend.tsf.TsfCoreEnvironmentPostProcessor

@ -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");
}
}
}
}

@ -37,6 +37,12 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -80,6 +86,12 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId> <artifactId>mockito-inline</artifactId>

@ -27,7 +27,6 @@ import com.tencent.cloud.rpc.enhancement.instrument.feign.EnhancedFeignBeanPostP
import com.tencent.cloud.rpc.enhancement.instrument.feign.PolarisLoadBalancerFeignRequestTransformer; import com.tencent.cloud.rpc.enhancement.instrument.feign.PolarisLoadBalancerFeignRequestTransformer;
import com.tencent.cloud.rpc.enhancement.instrument.filter.EnhancedReactiveFilter; 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.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.resttemplate.PolarisLoadBalancerRequestTransformer;
import com.tencent.cloud.rpc.enhancement.instrument.scg.EnhancedGatewayGlobalFilter; import com.tencent.cloud.rpc.enhancement.instrument.scg.EnhancedGatewayGlobalFilter;
import com.tencent.cloud.rpc.enhancement.instrument.webclient.EnhancedWebClientExchangeFilterFunction; import com.tencent.cloud.rpc.enhancement.instrument.webclient.EnhancedWebClientExchangeFilterFunction;
@ -89,7 +88,6 @@ public class RpcEnhancementAutoConfiguration {
} }
@Bean @Bean
@ConditionalOnMissingBean
@ConditionalOnClass(name = "com.tencent.cloud.polaris.registry.PolarisRegistration") @ConditionalOnClass(name = "com.tencent.cloud.polaris.registry.PolarisRegistration")
public RegistrationTransformer registrationTransformer() { public RegistrationTransformer registrationTransformer() {
return new PolarisRegistrationTransformer(); return new PolarisRegistrationTransformer();
@ -99,9 +97,9 @@ public class RpcEnhancementAutoConfiguration {
@Lazy @Lazy
public EnhancedPluginRunner enhancedFeignPluginRunner( public EnhancedPluginRunner enhancedFeignPluginRunner(
@Autowired(required = false) List<EnhancedPlugin> enhancedPlugins, @Autowired(required = false) List<EnhancedPlugin> enhancedPlugins,
@Autowired(required = false) Registration registration, @Autowired(required = false) List<Registration> registrations,
PolarisSDKContextManager polarisSDKContextManager) { PolarisSDKContextManager polarisSDKContextManager) {
return new DefaultEnhancedPluginRunner(enhancedPlugins, registration, polarisSDKContextManager.getSDKContext()); return new DefaultEnhancedPluginRunner(enhancedPlugins, registrations, polarisSDKContextManager.getSDKContext());
} }
@Bean @Bean
@ -183,23 +181,6 @@ public class RpcEnhancementAutoConfiguration {
@ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate")
protected static class PolarisRestTemplateAutoConfiguration { protected static class PolarisRestTemplateAutoConfiguration {
@Autowired(required = false)
private List<RestTemplate> 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 @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnClass(name = {"org.springframework.cloud.client.loadbalancer.LoadBalancerRequestTransformer"}) @ConditionalOnClass(name = {"org.springframework.cloud.client.loadbalancer.LoadBalancerRequestTransformer"})

@ -19,16 +19,24 @@ package com.tencent.cloud.rpc.enhancement.instrument.feign;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; 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.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; 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.Client;
import feign.Request; import feign.Request;
import feign.Request.Options; import feign.Request.Options;
import feign.RequestTemplate;
import feign.Response; import feign.Response;
import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.DefaultServiceInstance;
@ -61,16 +69,19 @@ public class EnhancedFeignClient implements Client {
request.headers().forEach((s, strings) -> requestHeaders.addAll(s, new ArrayList<>(strings))); request.headers().forEach((s, strings) -> requestHeaders.addAll(s, new ArrayList<>(strings)));
URI url = URI.create(request.url()); URI url = URI.create(request.url());
URI serviceUrl = url.resolve(request.requestTemplate().url());
EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder() EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder()
.httpHeaders(requestHeaders) .httpHeaders(requestHeaders)
.httpMethod(HttpMethod.valueOf(request.httpMethod().name())) .httpMethod(HttpMethod.valueOf(request.httpMethod().name()))
.url(url) .url(url)
.serviceUrl(serviceUrl)
.build(); .build();
enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setRequest(enhancedRequestContext);
enhancedPluginContext.setOriginRequest(request); enhancedPluginContext.setOriginRequest(request);
enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance()); enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance());
String svcName = request.requestTemplate().feignTarget().name(); String svcName = serviceUrl.getHost();
DefaultServiceInstance serviceInstance = new DefaultServiceInstance( DefaultServiceInstance serviceInstance = new DefaultServiceInstance(
String.format("%s-%s-%d", svcName, url.getHost(), url.getPort()), String.format("%s-%s-%d", svcName, url.getHost(), url.getPort()),
svcName, url.getHost(), url.getPort(), url.getScheme().equals("https")); svcName, url.getHost(), url.getPort(), url.getScheme().equals("https"));
@ -82,11 +93,11 @@ public class EnhancedFeignClient implements Client {
enhancedPluginContext.setTargetServiceInstance(serviceInstance, url); enhancedPluginContext.setTargetServiceInstance(serviceInstance, url);
} }
long startMillis = System.currentTimeMillis();
try {
// Run pre enhanced plugins. // Run pre enhanced plugins.
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
long startMillis = System.currentTimeMillis();
try {
Response response = delegate.execute(request, options); Response response = delegate.execute(request, options);
enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis); enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis);
@ -103,6 +114,15 @@ public class EnhancedFeignClient implements Client {
pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext); pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext);
return response; 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) { catch (IOException origin) {
enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis); enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis);
enhancedPluginContext.setThrowable(origin); enhancedPluginContext.setThrowable(origin);
@ -115,4 +135,25 @@ public class EnhancedFeignClient implements Client {
pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext); 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<String, Collection<String>> 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();
}
} }

@ -18,18 +18,25 @@
package com.tencent.cloud.rpc.enhancement.instrument.resttemplate; package com.tencent.cloud.rpc.enhancement.instrument.resttemplate;
import java.io.IOException; 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.common.metadata.MetadataContextHolder;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; 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.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.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
import static com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisLoadBalancerRequestTransformer.LOAD_BALANCER_SERVICE_INSTANCE; import static com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisLoadBalancerRequestTransformer.LOAD_BALANCER_SERVICE_INSTANCE;
@ -39,37 +46,67 @@ import static com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisL
* *
* @author sean yu * @author sean yu
*/ */
public class EnhancedRestTemplateInterceptor implements ClientHttpRequestInterceptor { public class EnhancedRestTemplateWrapInterceptor {
private final EnhancedPluginRunner pluginRunner; private final EnhancedPluginRunner pluginRunner;
public EnhancedRestTemplateInterceptor(EnhancedPluginRunner pluginRunner) { private final LoadBalancerClient delegate;
public EnhancedRestTemplateWrapInterceptor(EnhancedPluginRunner pluginRunner, LoadBalancerClient delegate) {
this.pluginRunner = pluginRunner; 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<ClientHttpResponse> loadBalancerRequest) throws IOException {
EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext(); EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext();
URI serviceUrl = request.getURI();
if (request instanceof ServiceRequestWrapper) {
serviceUrl = ((ServiceRequestWrapper) request).getRequest().getURI();
}
EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder() EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder()
.httpHeaders(request.getHeaders()) .httpHeaders(request.getHeaders())
.httpMethod(request.getMethod()) .httpMethod(request.getMethod())
.url(request.getURI()) .url(request.getURI())
.serviceUrl(serviceUrl)
.build(); .build();
enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setRequest(enhancedRequestContext);
enhancedPluginContext.setOriginRequest(request); enhancedPluginContext.setOriginRequest(request);
enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance()); enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance());
enhancedPluginContext.setTargetServiceInstance((ServiceInstance) MetadataContextHolder.get()
.getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), request.getURI());
// Run pre enhanced plugins. // Run pre enhanced plugins.
try {
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
}
catch (CallAbortedException callAbortedException) {
MetadataObjectValue<Object> 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(); long startMillis = System.currentTimeMillis();
try { 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); enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis);
EnhancedResponseContext enhancedResponseContext = EnhancedResponseContext.builder() EnhancedResponseContext enhancedResponseContext = EnhancedResponseContext.builder()
@ -80,6 +117,8 @@ public class EnhancedRestTemplateInterceptor implements ClientHttpRequestInterce
// Run post enhanced plugins. // Run post enhanced plugins.
pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext); pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext);
return response; return response;
} }
catch (IOException e) { catch (IOException e) {

@ -18,6 +18,8 @@
package com.tencent.cloud.rpc.enhancement.instrument.scg; package com.tencent.cloud.rpc.enhancement.instrument.scg;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import com.tencent.cloud.common.constant.OrderConstant; import com.tencent.cloud.common.constant.OrderConstant;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
@ -25,15 +27,24 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; 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 reactor.core.publisher.Mono;
import org.springframework.cloud.client.DefaultServiceInstance; 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.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered; 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 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_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
@ -52,18 +63,40 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
@Override @Override
public Mono<Void> filter(ServerWebExchange originExchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange originExchange, GatewayFilterChain chain) {
Response<ServiceInstance> serviceInstanceResponse = originExchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR);
String serviceId = Optional.ofNullable(serviceInstanceResponse).map(Response::getServer).
map(ServiceInstance::getServiceId).orElse(null);
EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext(); EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext();
EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder() EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder()
.httpHeaders(originExchange.getRequest().getHeaders()) .httpHeaders(originExchange.getRequest().getHeaders())
.httpMethod(originExchange.getRequest().getMethod()) .httpMethod(originExchange.getRequest().getMethod())
.url(originExchange.getRequest().getURI()) .url(originExchange.getRequest().getURI())
.serviceUrl(getServiceUri(originExchange, serviceId))
.build(); .build();
enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setRequest(enhancedRequestContext);
enhancedPluginContext.setOriginRequest(originExchange); enhancedPluginContext.setOriginRequest(originExchange);
// Run pre enhanced plugins. // Run pre enhanced plugins.
try {
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); 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 // Exchange may be changed in plugin
ServerWebExchange exchange = (ServerWebExchange) enhancedPluginContext.getOriginRequest(); ServerWebExchange exchange = (ServerWebExchange) enhancedPluginContext.getOriginRequest();
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@ -73,9 +106,9 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
enhancedPluginContext.getRequest().setUrl(uri); enhancedPluginContext.getRequest().setUrl(uri);
if (uri != null) { 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(); DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
serviceInstance.setServiceId(route.getUri().getHost()); serviceInstance.setServiceId(serviceId);
serviceInstance.setHost(uri.getHost()); serviceInstance.setHost(uri.getHost());
serviceInstance.setPort(uri.getPort()); serviceInstance.setPort(uri.getPort());
enhancedPluginContext.setTargetServiceInstance(serviceInstance, null); enhancedPluginContext.setTargetServiceInstance(serviceInstance, null);
@ -113,4 +146,19 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
public int getOrder() { public int getOrder() {
return OrderConstant.Client.Scg.ENHANCED_FILTER_ORDER; 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;
}
}
} }

@ -17,15 +17,22 @@
package com.tencent.cloud.rpc.enhancement.instrument.webclient; 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.common.metadata.MetadataContextHolder;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext; import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext; 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 reactor.core.publisher.Mono;
import org.springframework.cloud.client.ServiceInstance; 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.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
@ -53,6 +60,7 @@ public class EnhancedWebClientExchangeFilterFunction implements ExchangeFilterFu
.httpHeaders(originRequest.headers()) .httpHeaders(originRequest.headers())
.httpMethod(originRequest.method()) .httpMethod(originRequest.method())
.url(originRequest.url()) .url(originRequest.url())
.serviceUrl(getServiceUri(originRequest))
.build(); .build();
enhancedPluginContext.setRequest(enhancedRequestContext); enhancedPluginContext.setRequest(enhancedRequestContext);
enhancedPluginContext.setOriginRequest(originRequest); enhancedPluginContext.setOriginRequest(originRequest);
@ -62,7 +70,21 @@ public class EnhancedWebClientExchangeFilterFunction implements ExchangeFilterFu
.getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), originRequest.url()); .getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), originRequest.url());
// Run post enhanced plugins. // Run post enhanced plugins.
try {
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext); 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 // request may be changed by plugin
ClientRequest request = (ClientRequest) enhancedPluginContext.getOriginRequest(); ClientRequest request = (ClientRequest) enhancedPluginContext.getOriginRequest();
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@ -91,4 +113,21 @@ public class EnhancedWebClientExchangeFilterFunction implements ExchangeFilterFu
pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext); 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;
}
}
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save