我们知道现在项目一般都会使用springboot作为基础框架,如果项目需要基于springboot实现自动导入的话,一般都会实现一个xxx-starter,我们看看spring-cloud-starter-alibaba-sentinel
是怎么实现的。
本篇有些内容需要基于之前一篇的分析Sentinel限流原理(基于Sentinel1.8.1),限流、熔断、热点参数限流、授权实现原理
springboot自动装配原理,底层源码分析,条件注解实现机制,EnableAutoConfiguration,AutoConfigurationImportSelector
我们看下spring-cloud-starter-alibaba-sentinel
中spring.factoryies
内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\
com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\
com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\
com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration
org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\
com.alibaba.cloud.sentinel.custom.SentinelCircuitBreakerConfiguration
其中比较重要的一个是SentinelAutoConfiguration
在这个自动注入配置里面,会注入SentinelResourceAspect
这是springboot中实现基于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 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();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
throw ex;
}
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
traceException(ex);
return handleFallback(pjp, annotation, ex);
}
throw ex;
} finally {
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
}
}
可以看到SentinelResourceAspect
上有Aspect
注解,基于spring的AOP功能,在有SentinelResource
的方法上会进行切面处理。
这里在执行实际方法之前,会通过SphU.entry(resourceName, resourceType, entryType, pjp.getArgs())
去获取entry,如果获取到就执行方法,获取不到,按照之前的分析,会抛出BlockException
型异常。
而对于资源名称的获取,首先是直接获取SentinelResource
注解有没有值,如果有值则用该值作为资源名称,否则的话,资源名称为:类全名称+:+方法名称+(+参数类型列表+)
EntryType默认为EntryType.OUT
BlockException异常处理
如果访问出现了BlockException
,那么会调用handleBlockException
,调用blockHandler
或者blockHandlerClass
进行处理。
protected Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
throws Throwable {
Method blockHandlerMethod = extractBlockHandlerMethod(pjp, annotation.blockHandler(),
annotation.blockHandlerClass());
if (blockHandlerMethod != null) {
Object[] originArgs = pjp.getArgs();
// Construct args.
Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
args[args.length - 1] = ex;
try {
if (isStatic(blockHandlerMethod)) {
return blockHandlerMethod.invoke(null, args);
}
return blockHandlerMethod.invoke(pjp.getTarget(), args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
return handleFallback(pjp, annotation, ex);
}
对于是否应该调用哪个方法,Sentinel中是这样判断的:
首先必须blockHandler必须有值,如果没配置的化,那么不执行block处理,如果配置blockHandler也配置了blockHandlerClass,那么会从blockHandlerClass数组的第一个元素作为待执行的类且待执行的block方法必须是静态方法方法名与blockHandler配置一致;如果配置blockHandler但是没有配置blockHandlerClass,那么取当前执行方法的类为待执行类,block方法名与blockHandler配置一致。如果待执行类不是当前类,那么方法必须是一个静态方法,且方法名与blockHandler配置一致;如果待执行类的是当前类,那么方法静态或非静态方法都可以;而方法的返回值必须与SentinelResource注解的方法返回值一样,参数方面,必须比SentinelResource注解的方法的参数多一个,最后一个参数必须是BlockException类型,前面的参数必须与SentinelResource注解的方法参数完全一致。
比如有如下方法:
@SentinelResource(blockHandler="handleBolckForTest")
public String test(String name,int age){
xxxxx
}
如果是当前类处理,那么该方法与上述方法必须在一个类,且方法定义如下:
public String handleBolckForTest(String name,int age,BlockException exception){
xxxxx
}
如果是放在其他类中处理,那么如下:
// Test.java
@SentinelResource(blockHandler="handleBolckForTest",blockHandlerClas={BlockHandlerClassTest})
public String test(String name,int age){
xxxxx
}
// BlockHandlerClassTest.java
public static String handleBolckForTest(String name,int age,BlockException exception){
xxxxx
}
如果说没有配置任何blockHandler
处理逻辑,那么将走handleFallback
逻辑。
非BlockException异常处理
如果方法执行过程中抛出了非BlockException
异常,
- 如果配置
exceptionsToIgnore
属性,那么判断抛出的异常是否是该类型异常,如果是,表明不用处理该类型异常,直接对外抛出该异常 - 如果配置了
exceptionsToTrace
,那么判断抛出的异常是否是该类型异常,如果是的话且需要被trace,那么会调用entry.setError
,默认exceptionsToTrace=T{Throwable.class}
,而这里哪些类需要被Trace则是由Tracer
去判断:
protected static boolean shouldTrace(Throwable t) {
if (t == null || t instanceof BlockException) {
return false;
}
if (exceptionPredicate != null) {
return exceptionPredicate.test(t);
}
if (ignoreClasses != null) {
for (Class<? extends Throwable> clazz : ignoreClasses) {
if (clazz != null && clazz.isAssignableFrom(t.getClass())) {
return false;
}
}
}
if (traceClasses != null) {
for (Class<? extends Throwable> clazz : traceClasses) {
if (clazz != null && clazz.isAssignableFrom(t.getClass())) {
return true;
}
}
return false;
}
return true;
}
如果有需要,大家可以研究下这个来定义哪些异常需要追踪。
然后执行handleFallback
逻辑
- 如果上面两个逻辑都没有配置,那么抛出该异常,
加下来看看handleFallback
处理:
protected Object handleFallback(ProceedingJoinPoint pjp, SentinelResource annotation, Throwable ex)
throws Throwable {
return handleFallback(pjp, annotation.fallback(), annotation.defaultFallback(), annotation.fallbackClass(), ex);
}
protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, String defaultFallback,
Class<?>[] fallbackClass, Throwable ex) throws Throwable {
Object[] originArgs = pjp.getArgs();
// Execute fallback function if configured.
Method fallbackMethod = extractFallbackMethod(pjp, fallback, fallbackClass);
if (fallbackMethod != null) {
// Construct args.
int paramCount = fallbackMethod.getParameterTypes().length;
Object[] args;
if (paramCount == originArgs.length) {
args = originArgs;
} else {
args = Arrays.copyOf(originArgs, originArgs.length + 1);
args[args.length - 1] = ex;
}
try {
if (isStatic(fallbackMethod)) {
return fallbackMethod.invoke(null, args);
}
return fallbackMethod.invoke(pjp.getTarget(), args);
} catch (InvocationTargetException e) {
// throw the actual exception
throw e.getTargetException();
}
}
// If fallback is absent, we'll try the defaultFallback if provided.
return handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex);
}
handleFallback
查找需要执行的falback方法逻辑与blockHandler基本一致,只不过方法的最后一个参数必须是Throwablele类型
比如:
@SentinelResource(fallback="fallbackHandle")
public String test(String name,int age){
xxxxx
}
在本类中实现fallback方法如下:
@SentinelResource(fallback="fallbackHandle")
public String fallbackHandle(String name,int age,Throwable ex){
xxxxx
}
如果在其他类中处理:
// Test.java
@SentinelResource(fallback="fallbackHandle",fallbackClass={FallBack.class})
public String test(String name,int age){
xxxxx
}
//FallBack.java
public String fallbackHandle(String name,int age,Throwable ex){
xxxxx
}
如果没有配置fallback逻辑,那么会走defaultFallback
逻辑,
对于defaultFallback
,sentinel中与上面两个处理类似,defaultFallback方法的返回值必须与被注解的方法一样,但是参数不一样,不同点如下:
defaultFallback的方法参数可以有两种,一种是参数列表为空,另外一种参数列表只有一个参数,且该参数类型必须是Throwable类型
在本类中实现fallback方法如下:
@SentinelResource(defaultFallback="defaultFallbackHandle")
public String fallbackHandle(String name,int age,Throwable ex){
xxxxx
}
在本类中实现defaultFallback方法如下:
public String defaultFallbackHandle(Throwable ex){
xxxxx
}
// 或者
public String defaultFallbackHandle(){
xxxxx
}
如果在其他类实现:
// Test.java
@SentinelResource(defaultFallback="defaultFallbackHandle",fallbackClass={FallBack.class})
public String test(String name,int age){
xxxxx
}
//FallBack.java
public String defaultFallbackHandle(Throwable ex){
xxxxx
}
//或者
public String defaultFallbackHandle(){
xxxxx
}
如果异常没有配置处理方法,那么会将该异常抛出,不进行任何处理,由上层业务处理。
最后,需要执行entry.exit
方法
总结
最后我们总结一下,在sentinel-springboot-starter基于SentinelResource
注解和SentinelResourceAspect
来实现熔断限流。
在方法执行前,首先会通Sentinel获取Entry
,如果能够获取到,则表明能够访问,执行方法逻辑,如果不能,那么会判断抛出的异常类型。
首先是对于BlockException类型异常,这个就是限流引发的异常,优先处理,处理通过SentinelResource的blockHandler和blockHandlerClass两个属性去处理,具体逻辑为
:
如果配置了blockHandlerClass
,那么就会去blockHandlerClass配置的类
中找到blockHandler
属性对应的方法,如果没有那么就从当前方法所属类中去找blockHandler
属性对应的方法名称,而限流触发执行的方法除了名称要与
blockHandler配置的一样外还必须满足:1. 返回值与被注解的方法一样 2. 参数列表与被注解方法列表前面完全一致,但是参数列表要多一个,在最后以为必须为BlockException的参数
当前类中的方法可以为静态方法或实例方法,但是blockHandlerClass中方法必须为静态方法,如果配置了blockHandlerClass且当前类中也有对应的blockHandler方法,那么执行blockHandlerClass中方法不会执行当前类中方法。
如果捕获的异常不是BlockException异常,那么会走fallback处理逻辑,与BlockException异常处理基本差不多,方法最后一位必须是Throwable参数,不同的是,如果没有配置fallback,那么还会走defaultFallback处理逻辑
对于defaultFallback,默认会查找defaultFallback
,该方法的返回值必须与被注解的方法一致,但是参数必须为空或者只有一个Throwable
参数,如果两个都配置了,优先选择空参数列表的方法,如果配置fallbackClass
属性,那么会从fallbackClass列表的第一个查找defaultFallback
配置的方法。
以上就是springboot中实现sentinel starter的原理以及相关配置使用。