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


    阅读之前,希望你已经了解过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;
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    调用的代码明显是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);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    首先自定义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);
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    然后获取容器中名字为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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    确点: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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

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

    缺点

    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);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    虽然代码中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;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    但是目前的实现只是判断如果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("${ddmc.feign.dynamic.timeout.enabled:true}")
        private Boolean enabled;
    
        public FeignDynamicTimeoutAop() {
            log.info("[ddmc-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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

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

  • 相关阅读:
    【vue2第十二章】ref和$refs获取dom元素 和 vue异步更新与$nextTick使用
    Power Apps使用oData访问表数据并赋值前端
    .Net依赖注入神器Scrutor(下)
    1小时掌握Python操作Mysql数据库之pymysql模块技术
    BP神经网络中的BP是指,bp神经网络属于什么
    详解Python Tornado框架写一个Web应用全过程
    48.Java Lambda表达式
    【Unity】2D角色跳跃控制器
    生成器版和查看器版有什么区别?
    数组第 k 大子序列
  • 原文地址:https://blog.csdn.net/Ethan_199402/article/details/126076791