Merge remote-tracking branch 'origin/2020.0.0' into support_spring_config_import
commit
6779603fb4
@ -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
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue