赞
踩
关于Sentinel和Hystrix之间对比以及Sentinel原理在官方文档有详细文档,这里就不再做多余赘述,Sentinel常规集成通常是借助Sentinel Dashboard服务端整合实现服务的限流、熔断降级以及多维护的监控。但是项目当下已经集成promethus监控、aws云原生服务自带流量监控等,因此Sentinel Dashboard服务端提供的多维监控模项目需求优先级并不高。综合项目实际情况以及节约成本的理念我们提出:Spring Cloud + Sentinel + nacos 动态数据源模式(无Dashboard服务端)实现微服务的服务降级功能。
本文主要包含围Sentinel绕微服务的服务降级功能实现、自定义slot实现熔断降级预警功能以及基于-sentinel-实现-feign-全局异常兜底。
从官方提供Sentinel整体架构可以看出Dashboard服务端在Sentinel整体架构中仅负责规则配置、实时监控、机器发现等辅助模块。
实际处理流控、熔断降级是Sentinel-core完成。因此剥离Dashboard服务端,独立实现服务的熔断降级功能是可行的。
调研官方文档不难发现,Sentinel针对Spring Cloud微服务提供了依赖:
如上图,基于nacos配置中心实现Sentinel规则动态数据源管理,微服务启动时拉取熔断降级规则并维持心跳动态更新数据源配置。
根据源码分析,可以看出nacos动态数据源实现如下:
从入口程序SentinelAutoConfiguration开始,应用程序从环境配置Properties获取指定的数据源配置,最终通过静态规则管理类DegradeRuleManager注册到数据源,从而实现动态刷新规则配置。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring-cloud-starter-alibaba-sentinel 默认开启sentinel功能,引入依赖便可以使用sentinel,源码片段如下:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
name = {"spring.cloud.sentinel.enabled"},
matchIfMissing = true
)
@EnableConfigurationProperties({SentinelProperties.class})
public class SentinelAutoConfiguration {
spring-cloud-alibaba-sentinel-datasource依赖会从数据源中动态加载sentinel规则,源码片段如下:
# AbstractDataSourceProperties public void postRegister(AbstractDataSource dataSource) { switch(this.getRuleType()) { case FLOW: FlowRuleManager.register2Property(dataSource.getProperty()); break; case DEGRADE: DegradeRuleManager.register2Property(dataSource.getProperty()); break; case PARAM_FLOW: ParamFlowRuleManager.register2Property(dataSource.getProperty()); break; case SYSTEM: SystemRuleManager.register2Property(dataSource.getProperty()); break; case AUTHORITY: AuthorityRuleManager.register2Property(dataSource.getProperty()); break; case GW_FLOW: GatewayRuleManager.register2Property(dataSource.getProperty()); break; case GW_API_GROUP: GatewayApiDefinitionManager.register2Property(dataSource.getProperty()); } }
@SentinelResource 可以作用于方法上的熔断降级保护,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:
这里补充说明下blockHandler和fallback触发机制
- fallback如上述所讲,是异常降级兜底函数,当资源函数出现异常将会进入fallback如上述所讲。
- blockHandler是当资源函数某项指标超过设定的规则时触发
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
代码示例
下面代码示例通过@SentinelResource 注解在方法上进行埋点,标记getBaseUserInfo1函数为Sentinel资源,并指定了兜底函数和降级函数。
public class DsUserBaseQueryApplicationImpl implements DsUserBaseQueryApplication { @Override @SentinelResource(value = "baseUserInfo", entryType = EntryType.IN, fallback = "defaultFallback", blockHandler = "exceptionHandler") public String getBaseUserInfo1(String userId) { if (StringUtils.isEmpty(userId)) { throw new IllegalArgumentException("userId is empty."); } return System.currentTimeMillis() + userId; } //默认的 fallback 函数名称 public String defaultFallback(String userId) { log.info("Go to default fallback"); return "defaultFallback降级了"; } // Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致. public String exceptionHandler(String userId, BlockException ex) { log.error("blockHandler服务降级了", ex); // Do some log here. return "Oops,blockHandler, error occurred at " + userId; } }
增加动态数据源配置
动态数据源配置直接在SpringCloud配置模块增加sentinel.datasource数据源,支持flow限流规则和degrade降级规则。在flow/degrade层下添加具体的数据源配置介质,下面为基于nacos配置中心介质的动态数据源配置
#sentinel配置相关 spring: cloud: sentinel: datasource: flow: nacos: server-addr: ${spring.cloud.nacos.config.server-addr} dataId: ${spring.application.name}-flow-rules groupId: SENTINEL_GROUP # 规则类型,取值见: # org.springframework.cloud.alibaba.sentinel.datasource.RuleType rule-type: flow namespace: ${xxxx.sentinel.nacos.namespace} degrade: nacos: server-addr: ${spring.cloud.nacos.config.server-addr} dataId: global-sentinel-degrade-rules groupId: SENTINEL_GROUP rule-type: degrade data-type: json namespace: ${spring.cloud.nacos.discovery.namespace}
指定资源熔断规则
上文通过动态数据源配置指定了nacos降级规则配置文件,配置文件采用json格式的数组配置,详细配置如下:
[
{
"resource":"baseUserInfo", # 资源名称
"grade":2, # 规则编号,2代表异常次数降级规则
"count":5, # 阈值
"timeWindow":10, # 降级窗口时间,单位s
"MinRequestAmount": 2 # 最小触发请求数
}
]
降级规则结果验证测试
配置完降级规则启动服务,首次访问接口,参数传递为空,服务端资源出现异常,直接进入兜底函数。
http://192.168.132.49:7041/user/base?userId=
下图为fallback兜底函数降级结果:
此后一秒内连续5次访问后,资源异常次数达到阈值,服务进入
blockHandler规则降级函数,并且在此后10秒内都会进入规则降级流程。下图为异常次数达到阈值后,进行blockHandler规则降级结果:
开启sentinel feign支持
要启用sentinel feign降级功能需要在应用配置中显示关闭Spring Cloud 默认Hystrix降级开关 和 启用 feign sentinel 开启:
#打开sentinel对feign的支持
feign:
sentinel:
enabled: true
hystrix:
enabled: false
开启feign支持后,应用启动将初始化sentinel feign 资源:
@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({SphU.class, Feign.class}) public class SentinelFeignAutoConfiguration { public SentinelFeignAutoConfiguration() { } @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty( name = {"feign.sentinel.enabled"} ) public Builder feignSentinelBuilder() { return SentinelFeign.builder(); } }
sentinel 对@FeignClient 注解中的所有属性,Sentinel 都做了兼容,查看源码片段:
if (Void.TYPE != fallback) {
Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type());
return new SentinelInvocationHandler(target, dispatch, new feign.hystrix.FallbackFactory.Default(fallbackInstance));
} else if (Void.TYPE != fallbackFactory) {
FallbackFactory fallbackFactoryInstance = (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class);
return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
} else {
return new SentinelInvocationHandler(target, dispatch);
}
sentinel会根据定义的feing接口构建相应的资源,资源名策略定义:httpmethod:protocol://requesturl。
编码,定义feign调用服务端和调用方
# api 定义 @GetMapping(value = "/inner/user/base") String getBaseUserInfo(@RequestParam("userId") String userId); # feign api 定义 @FeignClient(contextId = "dsUserBaseApiClient", name = "xxxx", fallback = DsUserBaseApiClientFallback.class, configuration = FeignFallbackConfiguration.class) public interface DsUserBaseApiClient extends DsUserBaseApi { } # feign 接口调用 @GetMapping(value = "/user/info") public String getBaseUserInfo1(String userId) { return client.getBaseUserInfo(userId); } # feign server 定义 @Override public String getBaseUserInfo(String userId) { log.info("降级测试start..."); try { log.info("降级测试, 我开始休眠了..."); Thread.sleep(3 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("降级测试, 我睡醒了..."); return baseQueryApplication.getBaseUserInfo(userId); }
配置feign超时规则
0-超时异常降级策略,阈值2000ms
[
{
"resource": "GET:http://demo-xxxx-server/inner/user/base",
"grade": 0,
"count": 2000,
"timeWindow":10
}
]
项目默认超时时间10 * 1000MS,Server接口设置睡眠时间3 * 1000MS,阈值2 * 1000 ms,访问接口feign会正常返回。
使用Jmster进行压力测试,在50 * 50 循环调用过程,当feign调用平均响应时间超过设定的阈值后,将会提前进行熔断降级,调用feign接口定义的fallback函数,而不是一直等待服务端响应。
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain):
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
- AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
- DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
ProcessorSlotChain(核心骨架):将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。
系统会为每个资源创建一套SlotChain。
Sentinel框架对feign适配自动为feign创建Entry,源码片段如下:
# SentinelInvocationHandler.invoke(...) String resourceName = methodMetadata.template().method().toUpperCase() + ":" + hardCodedTarget.url() + methodMetadata.template().path(); Entry entry = null; Object var12; try { Throwable ex; try { ContextUtil.enter(resourceName); entry = SphU.entry(resourceName, EntryType.OUT, 1, args); result = methodHandler.invoke(args); return result; } catch (Throwable var22) { ex = var22; if (!BlockException.isBlockException(var22)) { Tracer.trace(var22); } } if (this.fallbackFactory == null) { throw var22; }
Sentinel框架通过AOP 切莫入口SentinelResourceAspect为@SentinelResource注解标记的资源自动创建Entry对象,源码片段如下:
@Around("sentinelResourceAnnotationPointcut()") public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable { Method originMethod = this.resolveMethod(pjp); SentinelResource annotation = (SentinelResource)originMethod.getAnnotation(SentinelResource.class); if (annotation == null) { throw new IllegalStateException("Wrong state for SentinelResource annotation"); } else { String resourceName = this.getResourceName(annotation.value(), originMethod); EntryType entryType = annotation.entryType(); int resourceType = annotation.resourceType(); Entry entry = null; Object var10; try { Object var18; try { # 为资源构建 entry对象 entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs()); Object result = pjp.proceed(); var18 = result; return var18; } catch (BlockException var15) { var18 = this.handleBlockException(pjp, annotation, var15); return var18; } catch (Throwable var16) { Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore(); if (exceptionsToIgnore.length > 0 && this.exceptionBelongsTo(var16, exceptionsToIgnore)) { throw var16; } } if (!this.exceptionBelongsTo(var16, annotation.exceptionsToTrace())) { throw var16; } this.traceException(var16); var10 = this.handleFallback(pjp, annotation, var16); } finally { if (entry != null) { entry.exit(1, pjp.getArgs()); } } return var10; } }
Sentinel槽链中Slot执行顺序是固定的,但并不是绝对的。Sentinel将ProcessorSlot作为SPI接口进行扩展,使得SlotChain具备了扩展能力。用户可以自定义Slot并编排Slot间的顺序。
下图为Sentinel默认Slot链路实现:
熔断降级是保障微服务稳定性的重要手段,而在服务降级前提前预警,以便开发人员提前处理导致请求响应超时、接口异常等问题能够更加有效保障微服务的稳定性。
自定义Slot实现降级提前预警功能
熔断降级提前预警实现思路是分析了Sentinel默认ProcessorSlotChain构建思路并结合SPI机制,自定义熔断降级提前预警Slot并重新构建ProcessorSlotChain。代码实现如下:
# 降级预警实现 @Slf4j public class DegradeEarlyWarningSlot extends AbstractLinkedProcessorSlot<DefaultNode> { /** * 从熔断降级规则管理器中提取降级规则并构建预警阈值规则 * @param resource * @return */ private List<DegradeRule> getRuleProvider(String resource) { List<DegradeRule> rules = DegradeRuleManager.getRules(); List<DegradeRule> earlyWarningRuleList = Lists.newArrayList(); for (DegradeRule rule : rules) { DegradeRule earlyWarningRule = new DegradeRule(); BeanUtils.copyProperties(rule, earlyWarningRule); double earlyWarningRuleCount; if (rule.getGrade() == 2) { // 异常数取异常阈值-1 earlyWarningRuleCount = rule.getCount() - 1; } else { // 异常比例 和 平均超时时间取阈值的80%作为提前预警阈值 earlyWarningRuleCount = rule.getCount() * 0.8; } earlyWarningRule.setCount(earlyWarningRuleCount); earlyWarningRuleList.add(earlyWarningRule); } return earlyWarningRuleList.stream().filter(rule -> resource.equals(rule.getResource())).collect(Collectors.toList()); } /** * get origin rule * * @param resource * @return */ private DegradeRule getOriginRule(String resource) { List<DegradeRule> originRule = DegradeRuleManager.getRules() .stream() .filter(rule -> rule.getResource().equals(resource)) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(originRule)) { return null; } return originRule.get(0); } @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode defaultNode, int count, boolean prioritized, Object... args) throws Throwable { String resource = context.getCurEntry().getResourceWrapper().getName(); List<DegradeRule> rules = getRuleProvider(resource); // 这里日志打印只是为了演示,后期计划集成disputhcer内存队列 + 飞书预警 if (rules != null) { for (DegradeRule rule : rules) { if (!rule.passCheck(context, defaultNode, count)) { DegradeRule originRule = getOriginRule(resource); String originRuleCount = originRule == null ? "未知" : String.valueOf(originRule.getCount()); log.info("DegradeEarlyWarning: 服务{} 资源{} 目前的熔断指标已经超过{},接近配置的熔断阈值:{},", rule.getLimitApp(), resource, rule.getCount(), originRuleCount); break; } } } fireEntry(context, resourceWrapper, defaultNode, count, prioritized, args); } @Override public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { this.fireExit(context, resourceWrapper, count, args); } }
实现SlotChainBuilder,重新定义ProcessorSlotChain。
public class CustomerSlotChainBuilder implements SlotChainBuilder { public CustomerSlotChainBuilder() { } public ProcessorSlotChain build() { ProcessorSlotChain chain = new DefaultProcessorSlotChain(); chain.addLast(new NodeSelectorSlot()); chain.addLast(new ClusterBuilderSlot()); chain.addLast(new LogSlot()); chain.addLast(new StatisticSlot()); chain.addLast(new SystemSlot()); chain.addLast(new AuthoritySlot()); chain.addLast(new FlowSlot()); chain.addLast(new DegradeSlot()); # 在默认调用链基础上添加预警功能 chain.addLast(new DegradeEarlyWarningSlot()); return chain; } }
添加SPI机制配置文件,在META-INF/services目录下定义一个名字为接口全限定名的文件,文件命名如下:
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
com.xxxx.xx.common.sentinel.slot.CustomerSlotChainBuilder
应用集成依赖添加预警功能
应用在pom文件中引入依赖,这里的依赖根据项目实际定义的基础包
<dependency>
<groupId>com.xxxx.framework</groupId>
<artifactId>sentinel-spring-boot-starter</artifactId>
<version>3.0.0-SNAPSHOT</version>
</dependency>
启动项目验证预警功能
2022-05-16 09:53:42.016 INFO [http-nio-7041-exec-1]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:45.902 INFO [http-nio-7041-exec-2]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:47.709 INFO [http-nio-7041-exec-3]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:49.001 INFO [http-nio-7041-exec-4]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:50.471 INFO [http-nio-7041-exec-5]c.m.saas.common.sentinel.slot.DegradeEarlyWarningSlot.entry:78 -DegradeEarlyWarning: 服务default 资源baseUserInfo 目前的熔断指标已经超过4.0,接近配置的熔断阈值:5.0,
2022-05-16 09:53:50.472 INFO [http-nio-7041-exec-5]c.m.s.d.application.impl.DsUserBaseQueryApplicationImpl.defaultFallback:46 -Go to default fallback
2022-05-16 09:53:51.923 ERROR[http-nio-7041-exec-6]c.m.s.d.appli
根据上述日志可以看出当异常次数达到4时会提前预警。
Spring CLoud微服务间交互使用Feign技术框架,在网络请求时,可能会出现异常请求,如果还想再异常情况下使系统可用,那么就需要容错处理,使用FeignClient时可对fallback进行配置,但随着接口数不断增加,配置也越来越重复繁琐,且大多容错逻辑均一致,因此需要对容错配置进行代理,提供全局统一容错处理。
通过官方文档我们知道feign支持基于Hystrix fallbackFactory 和 fallback模式的,但是两者均需要定义相应的fallbackFactory 和 fallback处理类。参考官方示例:
@FeignClient(name = "hello", fallback = HystrixClientFallback.class) protected interface HystrixClient { @RequestMapping(method = RequestMethod.GET, value = "/hello") Hello iFailSometimes(); } static class HystrixClientFallback implements HystrixClient { @Override public Hello iFailSometimes() { return new Hello("fallback"); } } @FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class) protected interface HystrixClient { @RequestMapping(method = RequestMethod.GET, value = "/hello") Hello iFailSometimes(); } @Component static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> { @Override public HystrixClient create(Throwable cause) { return new HystrixClient() { @Override public Hello iFailSometimes() { return new Hello("fallback; reason was: " + cause.getMessage()); } }; } }
如上述示例,随着接口的增加势必会产生大量类似的模板代码。
通过对 spring-cloud-starter-alibaba-sentinel包源码分析,该包仅简单使用了四个类就实现对feign的支持。核心原理是通过自定义SentinelFeign构建器重新实现了feign对象初始化,添加了对Sentinel熔断限流的支持。查看核心源码如下:
public Feign build() { super.invocationHandlerFactory(new InvocationHandlerFactory() { public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { Object feignClientFactoryBean = Builder.this.applicationContext.getBean("&" + target.type().getName()); Class fallback = (Class)Builder.this.getFieldValue(feignClientFactoryBean, "fallback"); Class fallbackFactory = (Class)Builder.this.getFieldValue(feignClientFactoryBean, "fallbackFactory"); String beanName = (String)Builder.this.getFieldValue(feignClientFactoryBean, "contextId"); if (!StringUtils.hasText(beanName)) { beanName = (String)Builder.this.getFieldValue(feignClientFactoryBean, "name"); } if (Void.TYPE != fallback) { Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type()); return new SentinelInvocationHandler(target, dispatch, new feign.hystrix.FallbackFactory.Default(fallbackInstance)); } else if (Void.TYPE != fallbackFactory) { FallbackFactory fallbackFactoryInstance = (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class); return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); } else { return new SentinelInvocationHandler(target, dispatch); } }
可以看出当未设置fallback 或者 fallbackFactory时,不会传递fallbackFactory到SentinelInvocationHandler。因此解决思路是:
自定义全局异常兜底处理函数CustomCommonFallbackFactory,具体实现如下:
@Slf4j @AllArgsConstructor public class CustomCommonFallback<T> implements MethodInterceptor { private final Class<T> targetType; private final String targetName; private final Throwable cause; @Nullable @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) { String errorMessage = cause.getMessage(); log.error("Feign API Fallback:[{}.{}] serviceId:[{}] message:[{}]", targetType.getName(), method.getName(), targetName, errorMessage); // BusinessException,直接返回 if (cause instanceof BusinessException) { BusinessException be = (BusinessException) cause; return Result.of(false, null, be.getCode(), be.getMsg(), null); } else if (cause instanceof FeignException) { FeignException exception = (FeignException) cause; // 提取业务异常 return Result.of(false, null, exception.status(), exception.contentUTF8(), null); } else { // 提取原始异常 Throwable causeA = cause.getCause(); if (causeA != null && causeA instanceof ClientException) { return Result.of(false, null, -1, String.format("%s服务已下线&服务状态不正常.", method.getName()), null); } else { return Result.of(false, null, -1, "系统未知异常.", null); } } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } CustomCommonFallback<?> that = (CustomCommonFallback<?>) o; return targetType.equals(that.targetType); } @Override public int hashCode() { return Objects.hash(targetType); } } @AllArgsConstructor public class CustomCommonFallbackFactory<T> implements FallbackFactory<T> { private final Target<T> target; @Override @SuppressWarnings("unchecked") public T create(Throwable cause) { final Class<T> targetType = target.type(); final String targetName = target.name(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(targetType); enhancer.setUseCache(true); enhancer.setCallback(new CustomCommonFallback<>(targetType, targetName, cause)); return (T) enhancer.create(); } }
改写Feign build()逻辑,当未定义fallback 或者 fallbackFactory时,传入公共的CustomCommonFallbackFactory到SentinelInvocationHandler。这里需要注意,由于SentinelInvocationHandler访问权限限制包内访问,因此将新建的类放com.alibaba.cloud.sentinel.feign目录下。具体代码如下:
public final class CustomSentinelFeign { private CustomSentinelFeign() { } public static CustomSentinelFeign.Builder builder() { return new CustomSentinelFeign.Builder(); } public static final class Builder extends feign.Feign.Builder implements ApplicationContextAware { private Contract contract = new Contract.Default(); private ApplicationContext applicationContext; private FeignContext feignContext; @Override public feign.Feign.Builder invocationHandlerFactory(feign.InvocationHandlerFactory invocationHandlerFactory) { throw new UnsupportedOperationException(); } @Override public CustomSentinelFeign.Builder contract(Contract contract) { this.contract = contract; return this; } /** * 自定义feign构建器,在模式SentinelFeign基础增加 CustomCommonFallbackFactory, * 当feign配置不指定兜底函数将使用默认CustomCommonFallbackFactory * @return */ @Override public Feign build() { super.invocationHandlerFactory(new InvocationHandlerFactory() { public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { Object feignClientFactoryBean = CustomSentinelFeign.Builder.this.applicationContext.getBean("&" + target.type().getName()); Class fallback = (Class) getFieldValue(feignClientFactoryBean, "fallback"); Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean, "fallbackFactory"); String beanName = (String) CustomSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "contextId"); if (!StringUtils.hasText(beanName)) { beanName = (String) CustomSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "name"); } if (Void.TYPE != fallback) { Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type()); return new SentinelInvocationHandler(target, dispatch, new FallbackFactory.Default(fallbackInstance)); } else if (Void.TYPE != fallbackFactory) { FallbackFactory fallbackFactoryInstance = (FallbackFactory) this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class); return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); } else { // 默认的 fallbackFactory CustomCommonFallbackFactory customFallbackFactory = new CustomCommonFallbackFactory(target); return new SentinelInvocationHandler(target, dispatch, customFallbackFactory); } } private Object getFromContext(String name, String type, Class fallbackType, Class targetType) { Object fallbackInstance = feignContext.getInstance(name, fallbackType); if (fallbackInstance == null) { throw new IllegalStateException(String.format( "No %s instance of type %s found for feign client %s", type, fallbackType, name)); } if (!targetType.isAssignableFrom(fallbackType)) { throw new IllegalStateException(String.format( "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", type, fallbackType, targetType, name)); } return fallbackInstance; } }); super.contract(new SentinelContractHolder(contract)); return super.build(); } private Object getFieldValue(Object instance, String fieldName) { Field field = ReflectionUtils.findField(instance.getClass(), fieldName); field.setAccessible(true); try { return field.get(instance); } catch (IllegalAccessException e) { // ignore } return null; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; feignContext = this.applicationContext.getBean(FeignContext.class); } } }
最后注入新定义的Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({SphU.class, Feign.class})
public class CustomFeignAutoConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnClass({SphU.class, Feign.class})
@ConditionalOnProperty(name = "feign.sentinel.enabled")
@Primary
public Feign.Builder feignSentinelBuilder() {
return CustomSentinelFeign.builder();
}
}
要使用Sentinel全局异常兜底需要引入基础依赖包并且在配置文件中配置feign.sentinel.enabled=true,注释掉feign.hystrix.enabled=true
feign.sentinel.enabled=true
feign.hystrix.enabled=false
<dependency>
<groupId>com.xxxx.framework</groupId>
<artifactId>sentinel-spring-boot-starter</artifactId>
<version>3.0.0-SNAPSHOT</version>
</dependency>
定义feign不配置fullback
@FeignClient(contextId = "dsUserBaseApiClient", name = "demo-xxxx-server")
public interface DsUserBaseApiClient extends DsUserBaseApi {
}
调用feign接口,当出现异常打印日志,可以看出定义的公共CustomCommonFallback触发生效。
2022-06-13 20:23:52.626 ERROR[http-nio-7042-exec-1]c.m.s.c.sentinel.feign.fallback.CustomCommonFallback.intercept:32 -Feign API Fallback:[com.xxxxx.saas.demoapi.feign.DsUserBaseApiClient.getBaseUserInfo] serviceId:[demo-xxxxx-server] message:[com.netflix.client.ClientException: Load balancer does not have available server for client: demo-xxxx-server]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。