掘金 后端 ( ) • 2024-04-25 14:24

最近在微服务项目中使用服务降级功能,采用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<Method, MethodHandler> 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<Method, MethodHandler> dispatch,
      FallbackFactory fallbackFactory) {
   this.target = checkNotNull(target, "target");
   this.dispatch = checkNotNull(dispatch, "dispatch");
   this.fallbackFactory = fallbackFactory;
   this.fallbackMethodMap = toFallbackMethod(dispatch);
}

SentinelInvocationHandler(Target<?> target, Map<Method, MethodHandler> 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<Method, InvocationHandlerFactory.MethodHandler> 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<Method, InvocationHandlerFactory.MethodHandler> 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的统一降级服务代码

三、参考文档

  1. 《springboot openfeign Sentinel统一降级处理》