Feign针对每个接口单独设置超时

背景

Feign可以通过配置文件来定义调用服务器端REST API的超时设置,可以指定全局超时,也可以指定声明@FeignClient的单个Client,但是不可以设置Client中的每个接口方法的超时。由于项目中部分接口需要较大的超时,如果通过单独定义一个额外的Client,则需要调整大量代码,那能否通过底层框架解决呢。

Feign原理

Spring Cloud Open Feign 针对每个@FeignClient声明的每个接口,通过动态代理生成一个代理类,InvocationHandler的实现类为feign.ReflectiveFeign.FeignInvocationHandler

static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }

dispatchMethodHandler具体实现为SynchronousMethodHandler

final class SynchronousMethodHandler implements MethodHandler {
    public Object invoke(Object[] argv) throws Throwable {
        RequestTemplate template = buildTemplateFromArgs.create(argv);
            //每次调用,如果Feign的接口方法,有Options 参数,则单独设置超时。
        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;
          }
        }
  }
}

Sentinel的扩展

Sentinel对Feign进行了扩展,以便进行埋点。

public class SentinelInvocationHandler implements InvocationHandler {
    private final Target<?> target;

	private final Map<Method, MethodHandler> dispatch;

	private FallbackFactory fallbackFactory;

	private Map<Method, Method> fallbackMethodMap;
    
@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);
                    //调用的是  SynchronousMethodHandler 方法。
					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;
	}    
}

启用Sentinel Feign

feign:
  sentinel:
    enabled: true

解决方案

既然底层是通过额外的参数进行了超时设置,那么是否可以通过对bean实现代理解决呢?

可以通过BeanPostProcessor的实现进行操作。

源码

DynamicTimeoutFeginInvocationHandler

代理接口实现:

package com.jurassic.cloud.sample.config;

import feign.Request;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lihz
 * @date 2024/11/10
 */
@RequiredArgsConstructor
public class DynamicTimeoutFeginInvocationHandler implements InvocationHandler {

    final private Object targetObject;

    final private DynamicTimeoutFeignConfiguration.FeignTimeoutOption timeoutOption;

    private Map<Method, Holder> holders = new HashMap<>(16);

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(targetObject, args);
        }
        if (timeoutOption.getConfig().isEmpty()) {
            return method.invoke(targetObject, args);
        }

        if (holders.containsKey(method)) {
            Holder holder = holders.get(method);
            Object[] newArgs = additionalArgs(args, holder.getOptions());
            return holder.getHandler().invoke(targetObject, method, newArgs);
        }

        RequestMapping annotation = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);

        if (annotation != null) {
            String[] value = annotation.value();
            for (String s : value) {
                if (timeoutOption.getConfig().containsKey(s)) {
                    DynamicTimeoutFeignConfiguration.FeignTimeoutOptionInfo feignTimeoutOptionInfo = timeoutOption.getConfig().get(s);
                    Request.Options options = new Request.Options(
                            feignTimeoutOptionInfo.getConnectTimeout()
                            , feignTimeoutOptionInfo.getReadTimeout()
                    );

                    InvocationHandler handler = Proxy.getInvocationHandler(targetObject);
                    Object[] newArgs = additionalArgs(args, options);

                    holders.put(method, new Holder(handler, options));

                    return handler.invoke(targetObject, method, newArgs);
                }
            }
        }

        return method.invoke(targetObject, args);
    }

    private Object[] additionalArgs(Object[] args, Request.Options options) {

        int length = args == null ? 0 : args.length;
        Object[] newArgs = new Object[length + 1];
        for (int i = 0; i < length; i++) {
            newArgs[i] = args[i];
        }

        newArgs[newArgs.length - 1] = options;
        return newArgs;
    }

    @AllArgsConstructor
    @Getter
    public static class Holder {
        private InvocationHandler handler;
        private Request.Options options;
    }
}

BeanPostProcessor实现

package com.jurassic.cloud.sample.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.openfeign.FeignClient;

import java.lang.reflect.Proxy;

/**
 * @author lihz
 * @date 2024/11/10
 */
@Slf4j
public class DynamicTimeoutFeignBeanProcessor implements BeanPostProcessor {
    private DynamicTimeoutFeignConfiguration.FeignTimeoutOption timeoutOption;

    public DynamicTimeoutFeignBeanProcessor(DynamicTimeoutFeignConfiguration.FeignTimeoutOption timeoutOption) {
        this.timeoutOption = timeoutOption;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (hasFeign(bean)) {
            DynamicTimeoutFeginInvocationHandler handler = new DynamicTimeoutFeginInvocationHandler(
                    bean, timeoutOption
            );

            Object o = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), handler);
            log.info(beanName + " ... postProcessAfterInitialization ");
            return o;
        }

        return bean;
    }

    private boolean hasFeign(Object bean) {
        for (Class<?> anInterface : bean.getClass().getInterfaces()) {
            if (anInterface.getAnnotation(FeignClient.class) != null) {
                return true;
            }
        }

        return false;
    }
}

自动配置

package com.jurassic.cloud.sample.config;

import feign.Feign;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@ConditionalOnClass(Feign.class)
@Configuration
@EnableConfigurationProperties(DynamicTimeoutFeignConfiguration.FeignTimeoutOption.class)
@AutoConfigureAfter(FeignAutoConfiguration.class)
public class DynamicTimeoutFeignConfiguration {

    @Bean
    public DynamicTimeoutFeignBeanProcessor beanProcessor(FeignTimeoutOption timeoutOption) {
        return new DynamicTimeoutFeignBeanProcessor(timeoutOption);
    }

    @ConfigurationProperties(prefix = "feign.timeout")
    @Getter
    @Setter
    public static class FeignTimeoutOption {
          private Map<String, FeignTimeoutOptionInfo> config = new HashMap<>();
    }

    @Getter
    @Setter
    public static class FeignTimeoutOptionInfo {
        private int connectTimeout;
        private int readTimeout;
    }
}

yaml配置

feign:
  timeout:
    config: 
      "[/T1/timeout]":    # API PATH
        connectTimeout: 1000
        readTimeout: 8000

测试

image-20241111135332827

参考

Sentinel与OpenFeign整合实现熔断降级源码分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值