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
parent
0cf3d21f12
commit
021eeb47f2
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.GatewayPolarisCircuitBreakerAutoConfiguration
|
||||
com.tencent.cloud.polaris.circuitbreaker.endpoint.PolarisCircuitBreakerEndpointAutoConfiguration
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptor.java → spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java
57
spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateInterceptor.java → spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue