如何自定义feign方法级别的超时时间

17 篇文章 2 订阅
2 篇文章 0 订阅

阅读之前,希望你已经了解过feign的调用流程

问题:feign暴露出来的config默认只支持接口contextId级别的配置,也就是如果我们项目中一些二方依赖接口比较慢,但是他们被包在一个方法较多的client中,那么该如何对这一个单独的接口进行超时配置呢?

如果你已经了解过feign源码,应该SynchronousMethodHandler不陌生了,回顾一下他的invoke方法

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

调用的代码明显是executeAndDecode(template, options),而超时时间相关的参数就在options中,知道了这一点,我们可以

通过反射修改options

options的获取就在findOptions中

Options options = findOptions(argv);

 Options findOptions(Object[] argv) {
    if (argv == null || argv.length == 0) {
      return this.options;
    }
    return (Options) Stream.of(argv)
        .filter(o -> o instanceof Options)
        .findFirst()
        .orElse(this.options);
  }

首先自定义XXXTargetAwareFeignClient继承TargetAwareFeignClient方法

public class XXXTargetAwareFeignClient extends TargetAwareFeignClient {

    public XXXTargetAwareFeignClient(Client delegate) {
        super(delegate);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        Integer timeout = FeignTimeoutUtil.get();
        if (Objects.nonNull(timeout)) {
            Request.Options currentOptions = new Options(timeout,timeout);
            return super.execute(request, currentOptions);
        }
        return super.execute(request, options);
    }


}

然后获取容器中名字为feignClient的bean,将它的clientDelegate替换为我么自定义的XXXTargetAwareFeignClient,这样每次调用client.execute(request, options)方法时,就会使用重写过的execute方法来改写options的值

@Component
public class FeignClientBeanPostProcessor implements BeanPostProcessor {


    @Autowired
    private okhttp3.OkHttpClient okHttpClient;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("feignClient")) {
            //通过反射来设置ClusterAwareLoadBalancerFeignClient的client属性
            try {
                OkHttpClient delegate = new OkHttpClient(okHttpClient);
                XXXTargetAwareFeignClient engineTargetAwareFeignClient = new XXXTargetAwareFeignClient(delegate);
                Class clazz = AopUtils.getTargetClass(bean);
                Class superClass = clazz.getSuperclass();
                Field clientDelegateField = clazz.getDeclaredField("clientDelegate");
                clientDelegateField.setAccessible(true);
                Field delegateField = superClass.getDeclaredField("delegate");
                delegateField.setAccessible(true);
                try {
                    clientDelegateField.set(bean, engineTargetAwareFeignClient);
                    delegateField.set(bean, engineTargetAwareFeignClient);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
        return bean;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

确点:feignClient这个bean会随着内部中间件的升级而改变继承结构,这种写法不够稳定

修改字节码

思路就是直接修改findOptions方法的字节码,比如使用Instrumenter来需改

public class FeignClientInstrumenter implements Instrumenter {

    // feign do http invoke class
    private static final String ENHANCE_CLASS = "feign.SynchronousMethodHandler";
    private static final String ENHANCE_METHOD = "findOptions";

    // both connect and read are use same timeout config
    private static final int MAX_TIMEOUT = 30_000;


    @Override
    public AgentBuilder instrument(AgentBuilder agentBuilder) {
        return agentBuilder.type(named(ENHANCE_CLASS)).transform(new FeignClientInstrumenter.Transformer());
    }

    private static class Transformer implements AgentBuilder.Transformer {

        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
            TypeDescription typeDescription,
            ClassLoader classLoader,
            JavaModule module) {
            return builder.visit(Advice.to(Interceptor.class).on(named(ENHANCE_METHOD)));
        }
    }
    
    public static class Interceptor {

        @Advice.OnMethodEnter
        public static Options onEnter(@Advice.Origin Method method,
            @Argument(0) Object[] args,
            @Advice.This Object obj) {
            //超时逻辑
            Integer customizedTimeout = getCustomizedTimeout(method,args)

            if (customizedTimeout != null) {
                // safety check, make sure the final Option is not
                if (customizedTimeout < 0 || customizedTimeout > MAX_TIMEOUT) {
                    customizedTimeout = MAX_TIMEOUT;
                }

                return new Options(customizedTimeout, customizedTimeout);
            }

            return null;
        }

        @Advice.OnMethodExit
        public static void onExit(
            @Advice.Enter Options enter,
            @Advice.Return(readOnly = false, typing = Typing.DYNAMIC) Options returned) {
            
        }
    }

}

优点是直接修改字节码性能更高

缺点

  1. 依赖来feign的版本,不是每个版本feign.SynchronousMethodHandler中都有findOptions给你拿来做修改的
  2. 不能区分具体接口,适合统一路由接口,根据租户的标识来分发具体的超时时间(可以判断args[0]的类型区分是否是路由接口)

aop切面

切面的切入时间应该在findOptions之后,那么明显LoadBalancerFeignClient的public Response execute(Request request, Request.Options options) throws IOException 更合适

public Response execute(Request request, Request.Options options) throws IOException {
		try {
			URI asUri = URI.create(request.url());
			String clientName = asUri.getHost();
			URI uriWithoutHost = cleanUrl(request.url(), clientName);
			FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
					this.delegate, request, uriWithoutHost);

			IClientConfig requestConfig = getClientConfig(options, clientName);
			return lbClient(clientName)
					.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
		}
		catch (ClientException e) {
			IOException io = findIOException(e);
			if (io != null) {
				throw io;
			}
			throw new RuntimeException(e);
		}
	}

虽然代码中IClientConfig requestConfig = getClientConfig(options, clientName);方法也可能修改到options的值

IClientConfig getClientConfig(Request.Options options, String clientName) {
		IClientConfig requestConfig;
		if (options == DEFAULT_OPTIONS) {
			requestConfig = this.clientFactory.getClientConfig(clientName);
		}
		else {
			requestConfig = new FeignOptionsClientConfig(options);
		}
		return requestConfig;
	}

但是目前的实现只是判断如果options为DEFAULT_OPTIONS,则获取对应ribbon的config,否则直接以传入的options构建FeignOptionsClientConfig,因为在这之前我们就已经修改过options,所以options不可能为DEFAULT_OPTIONS。

AOP代码如下:

@Aspect
@Component
@Slf4j
@Order(0)
@ConditionalOnProperty(prefix = "feign.dynamic.timeout", value = "enabled", matchIfMissing = true)
public class FeignDynamicTimeoutAop {

    @Resource
    private FeignTimeoutProperties feignTimeoutProperties;

    @Value("${feign.dynamic.timeout.enabled:true}")
    private Boolean enabled;

    public FeignDynamicTimeoutAop() {
        log.info("[feign-ext]feign加载超时时间动态拦截启用加载成功...");
    }

    @Pointcut("execution(execution(public * org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(..))")
    public void pointcut() {//
    }


    private Request.Options getOptions(Map<String, TimeOutVO> configs, URI asUri) {
        TimeOutVO timeOutVO = configs.get(asUri.getHost() + asUri.getPath());
        if (timeOutVO == null) {
            timeOutVO = configs.get(asUri.getHost());
        }
        if (timeOutVO == null) {
            return null;
        }

        if (timeOutVO.getConnectTimeout() == null) {
            timeOutVO.setConnectTimeout(timeOutVO.getReadTimeout());
        }
        if (timeOutVO.getReadTimeout() == null) {
            timeOutVO.setReadTimeout(timeOutVO.getConnectTimeout());
        }
        Request.Options options = new Request.Options(timeOutVO.getConnectTimeout(), timeOutVO.getReadTimeout());
        return options;
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
       
        Object[] args = joinPoint.getArgs();
        if (enabled && MapUtils.isNotEmpty(feignTimeoutProperties.getConfig())) {
            try {
                Map<String, TimeOutVO> configs = feignTimeoutProperties.getConfig();
                Request.Options options = null;
                if (args.length == FeignKeyConstants.ARGS_LEN) {
                    Request request = (Request) args[FeignKeyConstants.FEIGN_REQUEST_ARGS];
                    URI asUri = URI.create(request.url());
                    options = getOptions(configs, asUri);
                    Cat.logEvent("FeignDepend", new StringBuilder().append(asUri.getHost()).append(":").append(asUri.getPath()).append("@").append(options == null ? -1 : options.connectTimeoutMillis()).toString());
                }
                if (options != null) {
                    args[FeignKeyConstants.FEIGN_REQUEST_OPTION_ARGS] = options;
                }
            } catch (Exception e) {
                log.error("feign超时设置异常.", e);
            }
            return joinPoint.proceed(args);
        }
        return joinPoint.proceed();
    }

}

推荐使用这种方式,比较灵活

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值