Sentinel API
這裏介紹三個重要的API。
- ContextUtil
- Tracer
- SphU
@GetMapping("/test-sentinel-api")
public String testSentinelApi(@RequestParam(required = false) String a) {
//定義一個sentinel受保護的資源,名稱是test-sentinel-api
String resourceName = "test-sentinel-api";
//
ContextUtil.enter(resourceName, "test-wfw");
Entry entry = null;
try {
entry = SphU.entry(resourceName);
//被保護的邏輯
if (StringUtils.isEmpty(a)) {
throw new IllegalArgumentException("a is not null");
}
return a;
} catch (BlockException be) {
//如果受保護的資源被限流或者降級了 就會拋BlockException
log.warn("限流或者降級了", be);
return "限流或者降級了";
} catch (IllegalArgumentException ie) {
//統計 IllegalArgumentException 發生的次數、佔比。。。
Tracer.trace(ie);
return "a is not null";
} finally {
if(entry != null) {
//退出entry
entry.exit();
}
ContextUtil.exit();
}
}
Sentinel 註解
屬性 | 作用 | 是否必須 |
---|---|---|
value | 資源名稱 | 是 |
entryType | entry類型,標記流量的方向,取值IN/OUT,默認是OUT | 否 |
blockHandler | 處理BlockException的函數名稱。函數要求:1.必須是 public 2.返回類型與原方法一致 3.參數類型需要和原方法相匹配,並在最後加 BlockException 類型的參數。4.默認需和原方法在同一個類中。若希望使用其他類的函數,可配置 blockHandlerClass ,並指定blockHandlerClass裏面的方法。 | 否 |
blockHandlerClass | 存放blockHandler的類。對應的處理函數必須static修飾,否則無法解析,其他要求:同blockHandler。 | 否 |
fallback | 用於在拋出異常的時候提供fallback處理邏輯。fallback函數可以針對所有類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。函數要求:1. 返回類型與原方法一致 2. 參數類型需要和原方法相匹配,Sentinel 1.6開始,也可在方法最後加 Throwable 類型的參數。3.默認需和原方法在同一個類中。若希望使用其他類的函數,可配置 fallbackClass ,並指定fallbackClass裏面的方法。 | 否 |
fallbackClass【1.6】 | 存放fallback的類。對應的處理函數必須static修飾,否則無法解析,其他要求:同fallback。 | 否 |
defaultFallback【1.6】 | 用於通用的 fallback 邏輯。默認fallback函數可以針對所有類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,以fallback爲準。函數要求:1. 返回類型與原方法一致 2. 方法參數列表爲空,或者有一個 Throwable 類型的參數。3. 默認需要和原方法在同一個類中。若希望使用其他類的函數,可配置 fallbackClass ,並指定 fallbackClass 裏面的方法。 | 否 |
exceptionsToIgnore【1.6】 | 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出。 | 否 |
exceptionsToTrace | 需要trace的異常 | Throwable |
限流處理的方法。
@GetMapping("/test-sentinel-resource")
@SentinelResource(value = "test-sentinel-resource",
blockHandlerClass = TestBlock.class,
blockHandler = "block",
fallbackClass = TestFallBack.class,
fallback = "fallBack"
)
public String testSentinelResource(@RequestParam(required = false) String a) {
if (StringUtils.isEmpty(a)) {
throw new IllegalArgumentException("a is not null");
}
return a;
}
限流處理類。
@Slf4j
@Component
public class TestBlock {
public static String block(String a, BlockException e) {
log.warn("限流 或者 降級 block a:{}", a, e);
return "限流 或者 降級 block";
}
}
降級處理類。
@Slf4j
@Component
public class TestFallBack {
public static String fallBack(String a, Throwable e) {
log.warn("限流 或者 降級 fall a:{}", a, e);
return "限流 或者 降級 fall";
}
}
TIPS
- 1.6.0 之前的版本 fallback 函數只針對降級異常(DegradeException)進行處理,不能針對業務異常進行處理。
- 若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出 BlockException 時只會進入 blockHandler 處理邏輯。若未配置 blockHandler、fallback 和 defaultFallback,則被限流降級時會將 BlockException 直接拋出。
- 從 1.4.0 版本開始,註解方式定義資源支持自動統計業務異常,無需手動調用 Tracer.trace(ex) 來記錄業務異常。Sentinel 1.4.0 以前的版本需要自行調用 Tracer.trace(ex) 來記錄業務異常。
源碼分析
SentinelResourceAspect
是對 @SentinelResource
的處理類
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
// 獲取當前訪問的方法
Method originMethod = resolveMethod(pjp);
// 獲取方法上的SentinelResource註解
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
// Should not go through here.
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
// 獲取資源名
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
Entry entry = null;
try {
entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs());
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
// 處理被限制的異常,回調事先配置的異常處理方法
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
Tracer.trace(ex);
throw ex;
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
- 使用aspect的around攔截,攔截標註有SentinelResource的註解
- 進入方法之前調用SphU.entry(resourceName, entryType),結束之後調用entry.exit();
- 異常的時候調用handleBlockException方法
handleBlockException
通過反射獲取到註解上配置的fallback
方法
private Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
throws Exception {
// Execute fallback for degrading if configured.
Object[] originArgs = pjp.getArgs();
if (isDegradeFailure(ex)) {
Method method = extractFallbackMethod(pjp, annotation.fallback());
if (method != null) {
return method.invoke(pjp.getTarget(), originArgs);
}
}
// Execute block handler if configured.
Method blockHandler = extractBlockHandlerMethod(pjp, annotation.blockHandler(), annotation.blockHandlerClass());
if (blockHandler != null) {
// Construct args.
Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
args[args.length - 1] = ex;
if (isStatic(blockHandler)) {
return blockHandler.invoke(null, args);
}
return blockHandler.invoke(pjp.getTarget(), args);
}
// If no block handler is present, then directly throw the exception.
throw ex;
}
整合 Feign
-
增加配置
feign: sentinel: enabled: true
-
創建限流處理類
@Slf4j @Component public class UserFallBackFactoryFeign implements FallbackFactory<UserFeign> { @Override public UserFeign create(Throwable throwable) { return id -> { log.warn("限流或降級 id:{}", id, throwable); User user = new User(); user.setName("默認用戶"); return user; }; } } @Slf4j @Component public class UserFallBackFeign implements UserFeign { @Override public User getUserById(Long id) { User user = new User(); user.setName("默認用戶"); return user; } }
-
feign 註解修改
需要注意的是 fallbackFactory 和 fallback 只能存在一個。
@FeignClient(value = "user", //fallbackFactory = UserFallBackFactoryFeign.class, fallback = UserFallBackFeign.class ) public interface UserFeign { @GetMapping("/user/{id}") User getUserById(@PathVariable Long id); }
源碼分析
我們先分析下Feign的整個構造流程。
-
從
@EnableFeignClients
註解可以看到,入口在該註解上的FeignClientsRegistrar
類上。 -
FeignClientsRegistrar
實現了ImportBeanDefinitionRegistrar
接口,用來了動態加載@FeignClient
註解的接口並最終會被轉換成FeignClientFactoryBean
這個FactoryBean
,FactoryBean
內部的getObject
方法最終會返回一個 Proxy。 -
在構造 Proxy 的過程中會根據
org.springframework.cloud.openfeign.Targeter
接口的target
方法去構造。如果啓動了hystrix
開關(feign.hystrix.enabled=true
),會使用HystrixTargeter
,否則使用默認的DefaultTargeter
。 -
Targeter
內部構造 Proxy 的過程中會使用feign.Feign.Builder
去調用它的build
方法構造feign.Feign
實例(默認只有一個子類ReflectiveFeign
)。如果啓動了hystrix
開關(feign.hystrix.enabled=true
),會使用feign.hystrix.HystrixFeign.Builder
,否則使用默認的feign.Feign.Builder
-
構造出
feign.Feign
實例之後,調用newInstance
方法返回一個 Proxy -
簡單看下這個 newInstance 方法內部的邏輯:
public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList(); Method[] var5 = target.type().getMethods(); int var6 = var5.length; for(int var7 = 0; var7 < var6; ++var7) { Method method = var5[var7]; if (method.getDeclaringClass() != Object.class) { if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } } // 使用 InvocationHandlerFactory 根據接口的方法信息和 target 對象構造 InvocationHandler InvocationHandler handler = this.factory.create(target, methodToHandler); // 構造代理 T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); Iterator var12 = defaultMethodHandlers.iterator(); while(var12.hasNext()) { DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next(); defaultMethodHandler.bindTo(proxy); } return proxy; }
這裏的
InvocationHandlerFactory
是通過構造Feign
的時候傳入的:1)使用原生的
DefaultTargeter
: 那麼會使用feign.InvocationHandlerFactory.Default
這個factory
,並且構造出來的InvocationHandler
是feign.ReflectiveFeign.FeignInvocationHandler
。2)使用
hystrix
的HystrixTargeter
: 那麼會在feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory)
方法中調用父類的invocationHandlerFactory
方法傳入一個匿名的InvocationHandlerFactory
實現類,該類內部構造出的InvocationHandler
爲HystrixInvocationHandler
。
理解了 Feign 的執行過程之後,Sentinel 想要整合 Feign,可以參考 Hystrix 的實現:
- 實現
Targeter
接口SentinelTargeter
。 很不幸,Targeter
這個接口屬於包級別的接口,在外部包中無法使用,這個Targeter
無法使用。沒關係,我們可以沿用默認的HystrixTargeter
(實際上會用DefaultTargeter
)。 FeignClientFactoryBean
內部構造Targeter
、feign.Feign.Builder
的時候,都會從FeignContext
中獲取。所以我們沿用默認的DefaultTargeter
的時候,內部使用的feign.Feign.Builder
可控,而且這個Builder
不是包級別的類,可在外部使用。- 創建
SentinelFeign.Builder
繼承feign.Feign.Builder
,用來構造Feign
。 SentinelFeign.Builder
內部需要獲取FeignClientFactoryBean
中的屬性進行處理,比如獲取fallback
,name
,fallbackFactory
。很不幸,FeignClientFactoryBean
這個類也是包級別的類。沒關係,我們知道它存在在ApplicationContext
中的 beanName, 拿到 bean 之後根據反射獲取屬性就行(該過程在初始化的時候進行,不會在調用的時候進行,所以不會影響性能)。SentinelFeign.Builder
調用build
方法構造Feign
的過程中,我們不需要實現一個新的Feign
,跟hystrix
一樣沿用ReflectiveFeign
即可,在沿用的過程中調用父類feign.Feign.Builder
的一些方法進行改造即可,比如invocationHandlerFactory
方法設置InvocationHandlerFactory
,contract
的調用。- 跟 hystrix 一樣實現自定義的
InvocationHandler
接口SentinelInvocationHandler
用來處理方法的調用。 SentinelInvocationHandler
內部使用 Sentinel 進行保護,這個時候涉及到資源名的獲取。SentinelInvocationHandler
內部的feign.Target
能獲取服務名信息,feign.InvocationHandlerFactory.MethodHandler
的實現類feign.SynchronousMethodHandler
能拿到對應的請求路徑信息。很不幸,feign.SynchronousMethodHandler
這個類也是包級別的類。沒關係,我們可以自定義一個feign.Contract
的實現類SentinelContractHolder
在處理MethodMetadata
的過程把這些 metadata 保存下來(feign.Contract
這個接口在Builder
構造Feign
的過程中會對方法進行解析並驗證)。- 在
SentinelFeign.Builder
中調用contract
進行設置,SentinelContractHolder
內部保存一個Contract
使用委託方式不影響原先的Contract
過程。
- 創建
總結
-
Feign 的內部很多類都是 package 級別的,外部 package 無法引用某些類,這個時候只能想辦法繞過去,比如使用反射
-
目前這種實現有風險,萬一哪天 starter 內部使用的 Feign 相關類變成了 package 級別,那麼會改造代碼。所以把 Sentinel 的實現放到 Feign 裏並給 Feign 官方提 pr 可能更加合適
-
Feign的處理流程還是比較清晰的,只要能夠理解其設計原理,我們就能容易地整合進去