Merge remote-tracking branch 'origin/2020.0.0' into support_spring_config_import

pull/2349/head
Freeman Lau 3 years ago
commit 6779603fb4

@ -80,7 +80,7 @@
<properties>
<!-- Project revision -->
<revision>2021.1-SNAPSHOT</revision>
<revision>2022.0-SNAPSHOT</revision>
<!-- Spring Cloud -->
<spring.cloud.version>2020.0.1</spring.cloud.version>

@ -18,7 +18,7 @@
<description>Spring Cloud Alibaba Dependencies</description>
<properties>
<revision>2021.1-SNAPSHOT</revision>
<revision>2022.0-SNAPSHOT</revision>
<sentinel.version>1.8.0</sentinel.version>
<seata.version>1.3.0</seata.version>
<nacos.client.version>1.4.1</nacos.client.version>

@ -23,6 +23,7 @@
<module>sentinel-example/sentinel-dubbo-example/sentinel-dubbo-api</module>
<module>sentinel-example/sentinel-feign-example/sentinel-feign-consumer-example</module>
<module>sentinel-example/sentinel-feign-example/sentinel-feign-provider-example</module>
<module>sentinel-example/sentinel-circuitbreaker-example</module>
<module>sentinel-example/sentinel-webflux-example</module>
<module>sentinel-example/sentinel-spring-cloud-gateway-example</module>
<module>nacos-example/nacos-discovery-example</module>

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<version>${revision}</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-circuitbreaker-example</artifactId>
<name>Spring Cloud Starter Alibaba Sentinel - Feign With Sentinel CircuitBreaker Example</name>
<description>Example demonstrating how to use sentinel circuit breaker with feign</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<version>3.0.4</version> <!-- version >= 3.0.4 -->
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-circuitbreaker-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>${maven-deploy-plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,82 @@
# Sentinel Feign Circuit Breaker Example
## 项目说明
OpenFeign 整合 Sentinel 断路器实现
## 示例
1. 添加依赖
```xml
<!-- spring cloud alibaba 2021.0 暂时使用 spring cloud 2020.0.1 -->
<!-- 如果需要支持 Feign client 的配置, 需要升级 spring-cloud-openfeign-core 到 3.0.4 -->
<!-- 或者升级 spring cloud 到 2020.0.4 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<version>3.0.4</version> <!-- version >= 3.0.4 -->
</dependency>
```
2. 添加配置到配置中心
```yaml
feign:
circuitbreaker:
enabled: true # 开启 feign 断路器支持
sentinel:
default-rule: default # 默认规则名称
rules:
# 默认规则, 对所有 feign client 生效
default:
- grade: 2 # 根据异常数目降级
count: 1
timeWindow: 15 # 降级后到半开状态的时间
statIntervalMs: 1000
minRequestAmount: 1
# 只对 feign client user 生效
user:
- grade: 2
count: 1
timeWindow: 15
statIntervalMs: 1000
minRequestAmount: 1
# 只对 feign client user 的方法 feignMethod 生效
# 括号里是参数类型, 多个逗号分割, 比如 user#method(boolean,String,Map)
"[user#feignMethod(boolean)]":
- grade: 2
count: 1
timeWindow: 10
statIntervalMs: 1000
minRequestAmount: 1
```
## 验证配置生效
启动项目
验证默认 feign client 生效
先访问 http://localhost/test/default/false 2 次
再访问 http://localhost/test/default/true 断路器处于打开状态
验证指定 feign client 生效
先访问 http://localhost/test/feign/true 2 次
再访问 http://localhost/test/feign/false 断路器处于打开状态
验证 feign client 指定方法生效
先访问 http://localhost/test/feignMethod/false 2次
再访问 http://localhost/test/feignMethod/true 断路器处于打开状态
## 规则动态刷新
修改配置中心的规则, 再访问上述接口

@ -0,0 +1,34 @@
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.alibaba.cloud.examples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author freeman
*/
@SpringBootApplication
@EnableFeignClients
public class FeignCircuitBreakerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignCircuitBreakerApplication.class, args);
}
}

@ -0,0 +1,39 @@
package com.alibaba.cloud.examples.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
*
*
* @author freeman
*/
@RestController
public class ApiController {
@GetMapping("/default/{ok}")
public String defaultConfig(@PathVariable boolean ok) {
if (ok) {
return "ok";
}
throw new RuntimeException("fail");
}
@GetMapping("/feign/{ok}")
public String feignConfig(@PathVariable boolean ok) {
if (ok) {
return "ok";
}
throw new RuntimeException("fail");
}
@GetMapping("/feignMethod/{ok}")
public String feignMethodConfig(@PathVariable boolean ok) {
if (ok) {
return "ok";
}
throw new RuntimeException("fail");
}
}

@ -0,0 +1,39 @@
package com.alibaba.cloud.examples.controller;
import com.alibaba.cloud.examples.feign.OrderClient;
import com.alibaba.cloud.examples.feign.UserClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
*
*
* @author freeman
*/
@RestController
public class TestController {
@Autowired
private UserClient userClient;
@Autowired
private OrderClient orderClient;
@GetMapping("/test/default/{ok}")
public String testDefault(@PathVariable boolean ok) {
return orderClient.defaultConfig(ok);
}
@GetMapping("/test/feign/{ok}")
public String testFeign(@PathVariable boolean ok) {
return userClient.feign(ok);
}
@GetMapping("/test/feignMethod/{ok}")
public String testFeignMethod(@PathVariable boolean ok) {
return userClient.feignMethod(ok);
}
}

@ -0,0 +1,18 @@
package com.alibaba.cloud.examples.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
*
*
* @author freeman
*/
@FeignClient(value = "order", url = "http://localhost:${server.port}", fallback = OrderClientFallBack.class)
public interface OrderClient {
@GetMapping("/default/{ok}")
String defaultConfig(@PathVariable boolean ok);
}

@ -0,0 +1,16 @@
package com.alibaba.cloud.examples.feign;
import org.springframework.stereotype.Component;
/**
*
*
* @author freeman
*/
@Component
public class OrderClientFallBack implements OrderClient {
@Override
public String defaultConfig(boolean ok) {
return "order fallback";
}
}

@ -0,0 +1,21 @@
package com.alibaba.cloud.examples.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
*
*
* @author freeman
*/
@FeignClient(value = "user", url = "http://localhost:${server.port}", fallback = UserClientFallBack.class)
public interface UserClient {
@GetMapping("/feignMethod/{ok}")
String feignMethod(@PathVariable boolean ok);
@GetMapping("/feign/{ok}")
String feign(@PathVariable boolean ok);
}

@ -0,0 +1,21 @@
package com.alibaba.cloud.examples.feign;
import org.springframework.stereotype.Component;
/**
*
*
* @author freeman
*/
@Component
public class UserClientFallBack implements UserClient {
@Override
public String feignMethod(boolean ok) {
return "user fallback";
}
@Override
public String feign(boolean ok) {
return "user fallback";
}
}

@ -0,0 +1,11 @@
server:
port: 80
spring:
application:
name: circuit-breaker-app
cloud:
nacos:
config:
server-addr: localhost:8848
name: sentinel-circuitbreaker-rules.yml
file-extension: yml

@ -0,0 +1,27 @@
feign:
circuitbreaker:
enabled: true
sentinel:
default-rule: default
rules:
# global feign client
default:
- grade: 2
count: 1
timeWindow: 15
statIntervalMs: 1000
minRequestAmount: 1
# specific feign client
user:
- grade: 2
count: 1
timeWindow: 15
statIntervalMs: 1000
minRequestAmount: 1
# specific feign client single method
"[user#feignMethod(boolean)]":
- grade: 2
count: 1
timeWindow: 10
statIntervalMs: 1000
minRequestAmount: 1

@ -21,6 +21,13 @@
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<!-- Dynamic refresh from config center -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
@ -43,6 +50,33 @@
<optional>true</optional>
</dependency>
<!-- support Feign client configuration start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<version>3.0.4</version> <!-- need greater than 3.0.4, support CircuitBreakerNameResolver -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<optional>true</optional>
</dependency>
<!-- support Feign client configuration end -->
<!-- datasource rules -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -68,6 +102,22 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -19,8 +19,6 @@ package com.alibaba.cloud.circuitbreaker.sentinel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -32,6 +30,7 @@ import org.springframework.context.annotation.Configuration;
/**
* @author Eric Zhao
* @author freeman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(
@ -40,28 +39,15 @@ import org.springframework.context.annotation.Configuration;
havingValue = "true", matchIfMissing = true)
public class ReactiveSentinelCircuitBreakerAutoConfiguration {
@Autowired(required = false)
private List<Customizer<ReactiveSentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Bean
@ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class)
public ReactiveCircuitBreakerFactory reactiveSentinelCircuitBreakerFactory() {
return new ReactiveSentinelCircuitBreakerFactory();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(
name = { "reactor.core.publisher.Mono", "reactor.core.publisher.Flux" })
public static class ReactiveSentinelCustomizerConfiguration {
@Autowired(required = false)
private List<Customizer<ReactiveSentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Autowired(required = false)
private ReactiveSentinelCircuitBreakerFactory factory;
@PostConstruct
public void init() {
customizers.forEach(customizer -> customizer.customize(factory));
}
ReactiveSentinelCircuitBreakerFactory factory = new ReactiveSentinelCircuitBreakerFactory();
customizers.forEach(customizer -> customizer.customize(factory));
return factory;
}
}

@ -19,7 +19,6 @@ package com.alibaba.cloud.circuitbreaker.sentinel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import com.alibaba.csp.sentinel.SphU;
@ -36,6 +35,7 @@ import org.springframework.context.annotation.Configuration;
* Auto configuration for {@link SentinelCircuitBreaker}.
*
* @author Eric Zhao
* @author freeman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class })
@ -43,26 +43,15 @@ import org.springframework.context.annotation.Configuration;
havingValue = "true", matchIfMissing = true)
public class SentinelCircuitBreakerAutoConfiguration {
@Autowired(required = false)
private List<Customizer<SentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Bean
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
public CircuitBreakerFactory sentinelCircuitBreakerFactory() {
return new SentinelCircuitBreakerFactory();
}
@Configuration(proxyBeanMethods = false)
public static class SentinelCustomizerConfiguration {
@Autowired(required = false)
private List<Customizer<SentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Autowired(required = false)
private SentinelCircuitBreakerFactory factory;
@PostConstruct
public void init() {
customizers.forEach(customizer -> customizer.customize(factory));
}
SentinelCircuitBreakerFactory factory = new SentinelCircuitBreakerFactory();
customizers.forEach(customizer -> customizer.customize(factory));
return factory;
}
}

@ -0,0 +1,175 @@
package com.alibaba.cloud.circuitbreaker.sentinel.feign;
import java.lang.reflect.Method;
import java.util.*;
import com.alibaba.cloud.circuitbreaker.sentinel.SentinelConfigBuilder;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.cloud.client.circuitbreaker.AbstractCircuitBreakerFactory;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.AnnotationUtils;
/**
* Sentinel circuit breaker config change listener.
*
* @author freeman
*/
public class CircuitBreakerRuleChangeListener implements ApplicationContextAware,
ApplicationListener<RefreshScopeRefreshedEvent>, SmartInitializingSingleton {
private static final Logger LOGGER = LoggerFactory.getLogger(CircuitBreakerRuleChangeListener.class);
private SentinelFeignClientProperties properties;
/**
* properties backup, prevent rules from being updated every time the container is refreshed
*/
private SentinelFeignClientProperties propertiesBackup;
private AbstractCircuitBreakerFactory circuitBreakerFactory;
private ApplicationContext applicationContext;
@Override
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
ensureReady();
// No need to update the rules
if (Objects.equals(properties, propertiesBackup)) {
return;
}
clearRules();
// rebind
configureDefault();
configureCustom();
updateBackup();
LOGGER.info("sentinel circuit beaker rules refreshed.");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterSingletonsInstantiated() {
this.propertiesBackup = applicationContext
.getBean(SentinelFeignClientProperties.class).copy();
}
private void ensureReady() {
// Do not inject these beans directly,
// as it will cause the bean to be initialized prematurely,
// and we don't want to change the initialization order of the beans
if (circuitBreakerFactory == null) {
String[] names = applicationContext.getBeanNamesForType(AbstractCircuitBreakerFactory.class);
if (names.length >= 1) {
this.circuitBreakerFactory = applicationContext.getBean(names[0], AbstractCircuitBreakerFactory.class);
}
}
if (properties == null) {
this.properties = applicationContext.getBean(SentinelFeignClientProperties.class);
}
}
private void clearRules() {
clearCircuitBreakerFactory();
clearFeignClientRulesInDegradeManager();
}
private void configureDefault() {
configureDefault(properties, circuitBreakerFactory);
}
private void configureCustom() {
configureCustom(properties, circuitBreakerFactory);
}
private void clearCircuitBreakerFactory() {
Optional.ofNullable(getConfigurations(circuitBreakerFactory))
.ifPresent(Map::clear);
}
private void clearFeignClientRulesInDegradeManager() {
// first, clear all manually configured feign clients and methods.
propertiesBackup.getRules().keySet().stream()
.filter(key -> !Objects.equals(key, propertiesBackup.getDefaultRule()))
.forEach(resource -> Optional.ofNullable(DegradeRuleManager.getRulesOfResource(resource)).ifPresent(Set::clear));
// Find all feign clients, clear the corresponding rules
// NOTE: feign client name cannot be the same as the general resource name !!!
Arrays.stream(applicationContext.getBeanNamesForAnnotation(FeignClient.class))
// A little trick, FeignClient bean name is full class name.
// Simple exclusions, such as its subclass.
.filter(beanName -> beanName.contains("."))
.map(beanName -> {
try {
return Class.forName(beanName);
} catch (ClassNotFoundException ignore) {
// definitely not a feign client, just ignore
return null;
}
})
.filter(Objects::nonNull)
.forEach(clazz -> {
FeignClient anno = clazz.getAnnotation(FeignClient.class);
if (anno == null || AnnotationUtils.getValue(anno) == null) {
return;
}
String feignClientName = AnnotationUtils.getValue(anno).toString();
Optional.ofNullable(DegradeRuleManager.getRulesOfResource(feignClientName)).ifPresent(Set::clear);
});
}
private void updateBackup() {
this.propertiesBackup = this.properties.copy();
}
// static method
public static void configureCustom(SentinelFeignClientProperties properties,
AbstractCircuitBreakerFactory factory) {
properties.getRules().forEach((resourceName, degradeRules) -> {
if (!Objects.equals(properties.getDefaultRule(), resourceName)) {
factory.configure(builder -> ((SentinelConfigBuilder) builder)
.rules(properties.getRules().getOrDefault(resourceName,
new ArrayList<>())),
resourceName);
}
});
}
public static void configureDefault(SentinelFeignClientProperties properties,
AbstractCircuitBreakerFactory factory) {
List<DegradeRule> defaultConfigurations = properties.getRules()
.getOrDefault(properties.getDefaultRule(), new ArrayList<>());
factory.configureDefault(
resourceName -> new SentinelConfigBuilder(resourceName.toString())
.entryType(EntryType.OUT).rules(defaultConfigurations).build());
}
public static Map getConfigurations(
AbstractCircuitBreakerFactory circuitBreakerFactory) {
try {
Method method = AbstractCircuitBreakerFactory.class
.getDeclaredMethod("getConfigurations");
method.setAccessible(true);
return (Map) method.invoke(circuitBreakerFactory);
}
catch (Exception ignored) {
}
return Collections.emptyMap();
}
}

@ -0,0 +1,48 @@
package com.alibaba.cloud.circuitbreaker.sentinel.feign;
import java.lang.reflect.Method;
import java.util.Map;
import feign.Feign;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.AbstractCircuitBreakerFactory;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import static com.alibaba.cloud.circuitbreaker.sentinel.feign.CircuitBreakerRuleChangeListener.*;
/**
* Feign client circuit breaker name resolver.
*
* <p>
* <strong>note:</strong> spring cloud openfeign version need greater than 3.0.4.
*
* @author freeman
* @see CircuitBreakerNameResolver
*/
public class FeignClientCircuitNameResolver implements CircuitBreakerNameResolver {
private final Map configurations;
public FeignClientCircuitNameResolver(AbstractCircuitBreakerFactory factory) {
configurations = getConfigurations(factory);
}
@Override
public String resolveCircuitBreakerName(String feignClientName, Target<?> target,
Method method) {
String key = getKey(feignClientName, target, method);
if (configurations != null && configurations.containsKey(key)) {
return key;
}
return feignClientName;
}
private String getKey(String feignClientName, Target<?> target, Method method) {
String key = Feign.configKey(target.type(), method);
return feignClientName + key.substring(key.indexOf('#'));
}
}

@ -0,0 +1,115 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.alibaba.cloud.circuitbreaker.sentinel.feign;
import java.util.Collections;
import java.util.List;
import com.alibaba.cloud.circuitbreaker.sentinel.ReactiveSentinelCircuitBreakerFactory;
import com.alibaba.cloud.circuitbreaker.sentinel.SentinelCircuitBreakerFactory;
import feign.Feign;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.circuitbreaker.AbstractCircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.cloud.openfeign.FeignClientFactoryBean;
import org.springframework.cloud.util.ConditionalOnBootstrapEnabled;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static com.alibaba.cloud.circuitbreaker.sentinel.feign.CircuitBreakerRuleChangeListener.configureCustom;
import static com.alibaba.cloud.circuitbreaker.sentinel.feign.CircuitBreakerRuleChangeListener.configureDefault;
/**
* Auto configuration for feign client circuit breaker rules.
*
* @author freeman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Feign.class, FeignClientFactoryBean.class })
@ConditionalOnProperty(name = "spring.cloud.circuitbreaker.sentinel.enabled",
havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(SentinelFeignClientProperties.class)
public class SentinelFeignClientAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "feign.sentinel.refresh-rules",
havingValue = "true", matchIfMissing = true)
public static class CircuitBreakerListenerConfiguration {
@Bean
public CircuitBreakerRuleChangeListener circuitBreakerRuleChangeListener() {
return new CircuitBreakerRuleChangeListener();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CircuitBreakerNameResolver.class)
public static class CircuitBreakerNameResolverConfiguration {
@Bean
@ConditionalOnMissingBean(CircuitBreakerNameResolver.class)
public CircuitBreakerNameResolver feignClientCircuitNameResolver(
ObjectProvider<List<AbstractCircuitBreakerFactory>> provider) {
List<AbstractCircuitBreakerFactory> factories = provider
.getIfAvailable(Collections::emptyList);
if (factories.size() >= 1) {
return new FeignClientCircuitNameResolver(factories.get(0));
}
throw new IllegalArgumentException(
"need one CircuitBreakerFactory/ReactiveCircuitBreakerFactory, but 0 found.");
}
}
@Configuration(proxyBeanMethods = false)
public static class SentinelCustomizerConfiguration {
@Bean
public Customizer<SentinelCircuitBreakerFactory> configureRulesCustomizer(
SentinelFeignClientProperties properties) {
return factory -> {
configureDefault(properties, factory);
configureCustom(properties, factory);
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = { "reactor.core.publisher.Mono",
"reactor.core.publisher.Flux" })
public static class ReactiveSentinelCustomizerConfiguration {
@Bean
public Customizer<ReactiveSentinelCircuitBreakerFactory> reactiveConfigureRulesCustomizer(
SentinelFeignClientProperties properties) {
return factory -> {
configureDefault(properties, factory);
configureCustom(properties, factory);
};
}
}
}

@ -0,0 +1,87 @@
package com.alibaba.cloud.circuitbreaker.sentinel.feign;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Sentinel feign client properties.
*
* @author freeman
*/
@ConfigurationProperties("feign.sentinel")
public class SentinelFeignClientProperties {
/**
* default rule name.
*/
private String defaultRule = "default";
/**
* enable refresh circuit breaker rules from config center.
*/
private boolean refreshRules = true;
private Map<String, List<DegradeRule>> rules = new HashMap<>();
public String getDefaultRule() {
return defaultRule;
}
public void setDefaultRule(String defaultRule) {
this.defaultRule = defaultRule;
}
public boolean isRefreshRules() {
return refreshRules;
}
public void setRefreshRules(boolean refreshRules) {
this.refreshRules = refreshRules;
}
public Map<String, List<DegradeRule>> getRules() {
return rules;
}
public void setRules(Map<String, List<DegradeRule>> rules) {
this.rules = rules;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SentinelFeignClientProperties that = (SentinelFeignClientProperties) o;
return refreshRules == that.refreshRules
&& Objects.equals(defaultRule, that.defaultRule)
&& Objects.equals(rules, that.rules);
}
@Override
public int hashCode() {
return Objects.hash(defaultRule, refreshRules, rules);
}
public SentinelFeignClientProperties copy() {
try {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(this);
return objectMapper.readValue(json, this.getClass());
} catch (JsonProcessingException ignored) {
}
return new SentinelFeignClientProperties();
}
}

@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.circuitbreaker.sentinel.SentinelCircuitBreakerAutoConfiguration,\
com.alibaba.cloud.circuitbreaker.sentinel.ReactiveSentinelCircuitBreakerAutoConfiguration
com.alibaba.cloud.circuitbreaker.sentinel.ReactiveSentinelCircuitBreakerAutoConfiguration,\
com.alibaba.cloud.circuitbreaker.sentinel.feign.SentinelFeignClientAutoConfiguration

@ -0,0 +1,241 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.alibaba.cloud.circuitbreaker.sentinel.feign;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import static com.alibaba.cloud.circuitbreaker.sentinel.feign.FeignClientCircuitBreakerRuleIntegrationTest.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* @author freeman
*/
@SpringBootTest(webEnvironment = DEFINED_PORT, classes = Application.class, properties = {
"server.port=10101",
"feign.circuitbreaker.enabled=true",
"feign.sentinel.default-rule=default",
"feign.sentinel.rules.default[0].grade=2",
"feign.sentinel.rules.default[0].count=2",
"feign.sentinel.rules.default[0].timeWindow=2",
"feign.sentinel.rules.default[0].statIntervalMs=1000",
"feign.sentinel.rules.default[0].minRequestAmount=5",
"feign.sentinel.rules.user[0].grade=2",
"feign.sentinel.rules.user[0].count=2",
"feign.sentinel.rules.user[0].timeWindow=2",
"feign.sentinel.rules.user[0].statIntervalMs=1000",
"feign.sentinel.rules.user[0].minRequestAmount=5",
"feign.sentinel.rules.[user#specificFeignMethod(boolean)][0].grade=2",
"feign.sentinel.rules.[user#specificFeignMethod(boolean)][0].count=1",
"feign.sentinel.rules.[user#specificFeignMethod(boolean)][0].timeWindow=2",
"feign.sentinel.rules.[user#specificFeignMethod(boolean)][0].statIntervalMs=1000",
"feign.sentinel.rules.[user#specificFeignMethod(boolean)][0].minRequestAmount=5"
})
public class FeignClientCircuitBreakerRuleIntegrationTest {
@Autowired
private Application.UserClient userClient;
@Autowired
private Application.OrderClient orderClient;
@Test
public void testDefaultRule() throws Exception {
// test default configuration is working
// ok
assertThat(orderClient.defaultConfig(true)).isEqualTo("ok");
assertThat(orderClient.defaultConfig(true)).isEqualTo("ok");
assertThat(orderClient.defaultConfig(true)).isEqualTo("ok");
// occur exception
assertThat(orderClient.defaultConfig(false)).isEqualTo("fallback");
assertThat(orderClient.defaultConfig(false)).isEqualTo("fallback");
// test circuit breaker close
assertThat(orderClient.defaultConfig(true)).isEqualTo("ok");
// the 3rd exception, circuit breaker open
assertThat(orderClient.defaultConfig(false)).isEqualTo("fallback");
// test circuit breaker open
assertThat(orderClient.defaultConfig(true)).isEqualTo("fallback");
assertThat(orderClient.defaultConfig(true)).isEqualTo("fallback");
// longer than timeWindow, circuit breaker half open
Thread.sleep(2100L);
// let circuit breaker close
assertThat(orderClient.defaultConfig(true)).isEqualTo("ok");
assertThat(orderClient.defaultConfig(true)).isEqualTo("ok");
}
@Test
public void testSpecificFeignRule() throws Exception {
// test specific Feign client configuration is working
// ok
assertThat(userClient.specificFeign(true)).isEqualTo("ok");
assertThat(userClient.specificFeign(true)).isEqualTo("ok");
assertThat(userClient.specificFeign(true)).isEqualTo("ok");
// occur exception
assertThat(userClient.specificFeign(false)).isEqualTo("fallback");
assertThat(userClient.specificFeign(false)).isEqualTo("fallback");
// test circuit breaker close
assertThat(userClient.specificFeign(true)).isEqualTo("ok");
// the 3rd exception, circuit breaker open
assertThat(userClient.specificFeign(false)).isEqualTo("fallback");
// test circuit breaker open
assertThat(userClient.specificFeign(true)).isEqualTo("fallback");
assertThat(userClient.specificFeign(true)).isEqualTo("fallback");
// longer than timeWindow, circuit breaker half open
Thread.sleep(2100L);
// let circuit breaker close
assertThat(userClient.specificFeign(true)).isEqualTo("ok");
assertThat(userClient.specificFeign(true)).isEqualTo("ok");
}
@Test
public void testSpecificFeignMethodRule() throws Exception {
// test specific Feign client method configuration is working
// ok
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
// occur exception
assertThat(userClient.specificFeignMethod(false)).isEqualTo("fallback");
// 1 time exception, circuit breaker is closed(configuration is 1, but we need 2
// to make it open)
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
// occur the 2nd exception, circuit breaker open
assertThat(userClient.specificFeignMethod(false)).isEqualTo("fallback");
// test circuit breaker is open
assertThat(userClient.specificFeignMethod(true)).isEqualTo("fallback");
assertThat(userClient.specificFeignMethod(true)).isEqualTo("fallback");
// longer than timeWindow, circuit breaker half open
Thread.sleep(2100L);
// let circuit breaker close
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok");
}
@Configuration
@EnableAutoConfiguration
@RestController
@EnableFeignClients
protected static class Application {
@FeignClient(value = "user", url = "http://localhost:${server.port}", fallback = UserClientFallback.class)
interface UserClient {
@GetMapping("/specificFeign/{success}")
String specificFeign(@PathVariable boolean success);
@GetMapping("/specificFeignMethod/{success}")
String specificFeignMethod(@PathVariable boolean success);
}
@FeignClient(value = "order", url = "http://localhost:${server.port}", fallback = OrderClientFallback.class)
interface OrderClient {
@GetMapping("/defaultConfig/{success}")
String defaultConfig(@PathVariable boolean success);
}
@Component
static class UserClientFallback implements UserClient {
@Override
public String specificFeign(boolean success) {
return "fallback";
}
@Override
public String specificFeignMethod(boolean success) {
return "fallback";
}
}
@Component
static class OrderClientFallback implements OrderClient {
@Override
public String defaultConfig(boolean success) {
return "fallback";
}
}
@RestController
static class TestController {
@GetMapping("/specificFeign/{success}")
public String specificFeign(@PathVariable boolean success) {
if (success) {
return "ok";
}
throw new RuntimeException("failed");
}
@GetMapping("/defaultConfig/{success}")
String defaultConfig(@PathVariable boolean success) {
if (success) {
return "ok";
}
throw new RuntimeException("failed");
}
@GetMapping("/specificFeignMethod/{success}")
String specificFeignMethod(@PathVariable boolean success) {
if (success) {
return "ok";
}
throw new RuntimeException("failed");
}
}
}
}

@ -97,7 +97,7 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Loading…
Cancel
Save