最近在微服务项目中使用服务降级功能,采用FallbackFactory方式来实现(服务降级方式请参考:feign常用俩种降级方式Fallback和FallbackFactory),但是发现非常繁琐:针对每一个使用@FeignClient注释的远程服务都需要实现一个FallbackFactory,并将实现的FallbackFactory配置为@FeignClient注解fallbackFactory属性的值。
举个例子:如果项目有10个RemoteXXService远程接口服务,每个服务包含5个方法。为了实现所有服务及其方法调用时的服务降级,需要自定义实现10个RemoteXXServiceFallbackFactory接口,总共需要实现50个方法。
那能不能实现Feign接口调用的统一服务降级呢?即,在没有配置自定义的FallbackFactory情况下,默认使用该统一的服务降级逻辑。这样子,如果要自定义降级服务也可以,不定义则统一使用默认服务降级逻辑。
以上是本文要解决的问题,先看看与之相关的类:
SentinelFeignAutoConfiguration类:根据配置feign.sentinel.enabled属性启动Sentinel降级服务。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
return SentinelFeign.builder();
}
}
SentinelFeign类:build方法中构建了代理类SentinelInvocationHandler
@Override
public Feign build() {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target,
Map dispatch) {
...
Object fallbackInstance;
FallbackFactory fallbackFactoryInstance;
// check fallback and fallbackFactory properties
if (void.class != fallback) {
fallbackInstance = getFromContext(beanName, "fallback", fallback,
target.type());
return new SentinelInvocationHandler(target, dispatch,
new FallbackFactory.Default(fallbackInstance));
}
if (void.class != fallbackFactory) {
fallbackFactoryInstance = (FallbackFactory) getFromContext(
beanName, "fallbackFactory", fallbackFactory,
FallbackFactory.class);
return new SentinelInvocationHandler(target, dispatch,
fallbackFactoryInstance);
}
return new SentinelInvocationHandler(target, dispatch);
}
...
});
super.contract(new SentinelContractHolder(contract));
return super.build();
}
SentinelInvocationHandler类:invoke方法实现代理逻辑。下面的代码第60-63行,如果fallbackFactory为null,则直接抛出异常。这就是在没有配置降级服务的情况下,被调用服务Down掉,导致调用服务本身对应接口不可用的根本原因。
SentinelInvocationHandler(Target> target, Map dispatch,
FallbackFactory fallbackFactory) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackFactory = fallbackFactory;
this.fallbackMethodMap = toFallbackMethod(dispatch);
}
SentinelInvocationHandler(Target> target, Map dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
...
Object result;
MethodHandler methodHandler = this.dispatch.get(method);
// only handle by HardCodedTarget
if (target instanceof Target.HardCodedTarget) {
Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
.get(hardCodedTarget.type().getName()
+ Feign.configKey(hardCodedTarget.type(), method));
// resource default is HttpMethod:protocol://url
if (methodMetadata == null) {
result = methodHandler.invoke(args);
}
else {
String resourceName = methodMetadata.template().method().toUpperCase()
+ ":" + hardCodedTarget.url() + methodMetadata.template().path();
Entry entry = null;
try {
ContextUtil.enter(resourceName);
entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
result = methodHandler.invoke(args);
}
catch (Throwable ex) {
// fallback handle
if (!BlockException.isBlockException(ex)) {
Tracer.traceEntry(ex, entry);
}
if (fallbackFactory != null) {
try {
Object fallbackResult = fallbackMethodMap.get(method)
.invoke(fallbackFactory.create(ex), args);
return fallbackResult;
}
catch (IllegalAccessException e) {
// shouldn't happen as method is public due to being an
// interface
throw new AssertionError(e);
}
catch (InvocationTargetException e) {
throw new AssertionError(e.getCause());
}
}
else {
// throw exception if fallbackFactory is null
throw ex;
}
}
finally {
if (entry != null) {
entry.exit(1, args);
}
ContextUtil.exit();
}
}
}
else {
// other target type using default strategy
result = methodHandler.invoke(args);
}
return result;
}
解决方案采用逆向思维:
(1)要解决没有配置自定义fallbackFactory导致的问题,需要调整SentinelInvocationHandler类中invoke方法中fallbackFactory为null的处理逻辑。具体调整思路:新增boolean类型配置参数feign.sentinel.autoFallback,如果True,则增加统一降级默认逻辑。根据开闭原则,参考SentinelInvocationHandler的实现,我们重新创建一个类:XxxSentinelInvocationHandler,具体修改内容有三处:
// 新增类字段
private boolean autoFallback;
// 构造方法增加新增的字段autoFallback
XxxSentinelInvocationHandler(Target> target, Map dispatch,
FallbackFactory fallbackFactory, boolean autoFallback) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackFactory = fallbackFactory;
this.fallbackMethodMap = toFallbackMethod(dispatch);
this.autoFallback = autoFallback;
}
XxxSentinelInvocationHandler(Target> target, Map dispatch, boolean autoFallback) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.autoFallback = autoFallback;
}
// 修改invoke方法中的逻辑,自定义fallbackFactory的处理逻辑不变,修改没有定义fallbackFactory的逻辑。
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
...
else {
//没有自定义降级处理,如果自动降级且返回格式是R(R是笔者项目统一的接口返回体类型),则进行统一降级处理
if (autoFallback && method.getReturnType().equals(R.class)) {
String failMsg = "远程调用" + method.getDeclaringClass().getName() + "." + method.getName() + "异常!";
log.error(failMsg);
log.error(ex.getMessage(), ex);
return R.failed(failMsg);
}
throw ex;
}
...
}
(2)上述步骤中修改的构造器会在SentinelFeign.Builder.build()方法中调用,需要修改该方法的部分逻辑。同样,新建类XxxSentinelFeign,对照SentinelFeign有三处修改:
# 增加一个带参数构造方法
public static XxxSentinelFeign.Builder builder(boolean autoFallback) {
return new XxxSentinelFeign.Builder(autoFallback);
}
# XxxSentinelFeign.Builder类增加属性字段autoFallback和构造函数
private boolean autoFallback = Boolean.FALSE;
public Builder(boolean autoFallback) {this.autoFallback = autoFallback;}
public boolean isAutoFallback() {
return autoFallback;
}
# 修改build方法中使用XxxSentinelInvocationHandler的地方,增加一个调用参数autoFallback
if (void.class != fallback) {
fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type());
return new XxxSentinelInvocationHandler(target, dispatch,
new FallbackFactory.Default(fallbackInstance), isAutoFallback());
}
if (void.class != fallbackFactory) {
fallbackFactoryInstance = (FallbackFactory) getFromContext(beanName, "fallbackFactory",
fallbackFactory, FallbackFactory.class);
return new XxxSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance, isAutoFallback());
}
return new XxxSentinelInvocationHandler(target, dispatch, isAutoFallback());
(3)最后一步,参考SentinelFeignAutoConfiguration类,新建自动配置类SentinelAutoConfiguration,将上一步新建的类调用起来。@AutoConfigureBefore保证该自动配置在原自动配置之前生效。
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class)
public class SentinelAutoConfiguration {
@Value(value = "${feign.sentinel.autoFallback:false}")
private boolean autoFallback;
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
return XxxSentinelFeign.builder(autoFallback);
}
}
(4)如果上述自动配置类需要能被第三方项目加载注入容器,需要在当前项目的resources/META-INF目录下增加spring.factories文件,并在文件中加入如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.maya.framework.common.sentinel.SentinelAutoConfiguration
完成上述四步之后,就可以在nacos中配置feign.sentinel.autoFallback参数来控制是否启动统一自动服务降级。由于降级服务采用JDK动态代理实现,预先生成代理类,因此feign.sentinel.autoFallback参数值变更,需要重启服务才能生效。
本文源码下载:基于openfeign+sentinel的统一降级服务代码