首页 > 其他分享 >Feign Method Timeout 配置

Feign Method Timeout 配置

时间:2023-05-06 17:35:26浏览次数:39  
标签:Feign feign method project public Options Timeout archetype Method

项目背景

这个应用是微服务架构,使用Springboot+Springcloud,其中Springcloud部分使用了openfeign来实现通讯交互。

项目结构层次,我们将一个微服务暴力的拆分成两个模块:xxx-api/xxx-server,xxx-api是用来发布交互的接口,xxx-server模块是用来运行web服务。对于xxx-api模块,我们根据业务场景,将不同的api聚合在不同的接口里,如:

-DemoClient
 -getDemos()
 -reportDemos()

现在客户要求,涉及到定时任务的调用都要改成同步调用,返回给调度服务真实的处理结果。但有的任务执行时间相对长,我们针对性的设置某些api的超时等待时间?

现有实现

FeignContext是个子容器,不同的ContextId间配置隔离。默认情况下,FeignClient的Name/Value等于ContextId,你也可以自定义ContextId。所以,ContextId最低也是FeignClient级别的,不能对方法级别进行配置。

默认全局配置

feign.client.config.default.connect-timeout=10000  
feign.client.config.default.read-timeout=60000

微服务级别配置

@FeignClient(value = "project-archetype", url = "${project-archetype-server-url-prefix}")  
@Tag(name = "ArchetypeDemoClient", description = "ArchetypeDemoClient")  
public interface ArchetypeDemoClient {}

@FeignClient(value = "project-archetype1", url = "${project-archetype-server-url-prefix}")  
@Tag(name = "ArchetypeDemoClient", description = "ArchetypeDemoClient")  
public interface ArchetypeDemoClient2 {}
feign.client.config.project-archetype.connect-timeout=10000  
feign.client.config.project-archetype.read-timeout=60000

接口级别配置

@FeignClient(value = "project-archetype", url = "${project-archetype-server-url-prefix}", contextId = "project-archetype-longtime")  
@Tag(name = "ArchetypeDemoClient", description = "ArchetypeDemoClient")  
public interface ArchetypeDemoClient3 {}
feign.client.config.project-archetype-longtime.connect-timeout=10000  
feign.client.config.project-archetype-longtime.read-timeout=1800000

方法级别配置

@FeignClient(value = "project-archetype", url = "${project-archetype-server-url-prefix}")  
@Tag(name = "ArchetypeDemoClient", description = "ArchetypeDemoClient")  
public interface ArchetypeDemoClient2 {  
  
    @Operation(tags = "save", description = "A sample interface of saving data")  
    @PostMapping(value = "/save", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)  
    ResponseResult<ArchetypeDemoVO> save(Request.Options options, @RequestBody @Valid ArchetypeDemoDTO dto);
}
// 这里有灵活了,可以通过读取配置
// sample是硬编码测试
archetypeDemoClient2.save(new Request.Options(10000, 30000), dto);

根据现有的实现,我们的可选项是

  1. 使用[[#接口级别配置]],把长时间的api拆出来成独立的接口;
  2. 使用[[#方法级别配置]],增加api的参数改造调用方式;

方案上,我更倾向于后者,但我不想调用方式改变。

源码

下面FeignClient初始化的代码路径中的一小节(全部的我也没仔细看):
FeignClientsRegistrar#registerFeignClient->
FeignClientFactoryBean#getTarget->Feign#targetbuild得到ReflectiveFeignReflectiveFeign#newInstance

public <T> T newInstance(Target<T> target) {  
    // 下面这行代码给当前Feign的每个方法生成了feign.InvocationHandlerFactory$MethodHandler,它实际上就是feign.SynchronousMethodHandler,
    // 而SynchronousMethodHandler里的options就是上面配置文件预先配置的,因此每个Method都一样。
    // result.put(md.configKey(), this.factory.create(target, md, (Factory)buildTemplate, this.options, this.decoder, this.errorDecoder));
    Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);  
	
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();  
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();  
    Method[] var5 = target.type().getMethods();  
    int var6 = var5.length;  
  
    for(int var7 = 0; var7 < var6; ++var7) {  
        Method method = var5[var7];  
        if (method.getDeclaringClass() != Object.class) {  
            if (Util.isDefault(method)) {  
                DefaultMethodHandler handler = new DefaultMethodHandler(method);  
                defaultMethodHandlers.add(handler);  
                methodToHandler.put(method, handler);  
            } else {  
                methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));  
            }  
        }  
    }  
  
    InvocationHandler handler = this.factory.create(target, methodToHandler);  
    T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);  
    Iterator var12 = defaultMethodHandlers.iterator();  
  
    while(var12.hasNext()) {  
        DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();  
        defaultMethodHandler.bindTo(proxy);  
    }  
  
    return proxy;  
}

可以看到FeignClient最终就是一个proxy,proxy里的java.lang.reflect.InvocationHandler就是feign.ReflectiveFeign$FeignInvocationHandlerfeign.ReflectiveFeign$FeignInvocationHandlerdispatch里记录了FeignClient方法的处理feign.InvocationHandlerFactory$MethodHandlerfeign.SynchronousMethodHandler

feign.SynchronousMethodHandler代码处理请求过程中有一个逻辑,如果method请求参数里设置了Option的话,可以覆盖原有配置,否则就是用预先配置的。

public Object invoke(Object[] argv) throws Throwable {  
    RequestTemplate template = this.buildTemplateFromArgs.create(argv);  
    // 从参数里筛选options,如果没有的,返回预先配置的
    Options options = this.findOptions(argv);  
    Retryer retryer = this.retryer.clone();  
  
    while(true) {  
        try {  
            return this.executeAndDecode(template, options);  
        } catch (RetryableException var9) {  
          // retry handle
        }  
    }  
}

Options findOptions(Object[] argv) {  
    if (argv != null && argv.length != 0) {  
        Stream var10000 = Stream.of(argv);  
        Options.class.getClass();  
        var10000 = var10000.filter(Options.class::isInstance);  
        Options.class.getClass();  
        return (Options)var10000.map(Options.class::cast).findFirst().orElse(this.options);  
    } else {  
        return this.options;  
    }  
}

那我能不能对feign.SynchronousMethodHandler使用包装模式增加下”责任“呢,即在invoke前,把这个Object[] argv改下?

答案:可以。

新方法级别配置

核心代码

获取全部的FeignClients

private Map<String, Object> getAllFeignClients() {  
    return SpringUtil.getApplicationContext().getBeansWithAnnotation(FeignClient.class)  
            .entrySet().stream()  
            .filter(e -> e.getValue() instanceof Proxy)  
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));  
}

获取全部的Methodhanlders

public Optional<Map<Method, InvocationHandlerFactory.MethodHandler>> getMethodHandlers() {  
    for (Map.Entry<String, Object> entry : allFeignClients.entrySet()) {  
        InvocationHandler invocationHandler = Proxy.getInvocationHandler(entry.getValue());  
        if (invocationHandler instanceof ReflectiveFeign.FeignInvocationHandler feignInvocationHandler) {  
            try {  
                Field field = ReflectiveFeign.FeignInvocationHandler.class.getDeclaredField("dispatch");  
                field.setAccessible(true);  
                //noinspection unchecked  
                Optional<Map<Method, InvocationHandlerFactory.MethodHandler>> methodMethodHandlerMap = Optional.ofNullable((Map<Method, InvocationHandlerFactory.MethodHandler>) field.get(feignInvocationHandler));  
                field.setAccessible(false);  
                return methodMethodHandlerMap;  
            } catch (NoSuchFieldException e) {  
                throw new InteriorException(InteriorErrorStatus.ofServerError("No dispatch field in ReflectiveFeign.FeignInvocationHandler"));  
            } catch (IllegalAccessException e) {  
                throw new InteriorException(InteriorErrorStatus.ofServerError("Get dispatch error of ReflectiveFeign.FeignInvocationHandler"));  
            }  
        }  
    }  
    return Optional.empty();  
}

替换现有的Methodhandler为包装类

private void enhanceAsTimeoutMethodHandler(Map<Method, InvocationHandlerFactory.MethodHandler> methodMethodHandlerMap) {  
    Map<Method, InvocationHandlerFactory.MethodHandler> changedMethodHandlerMap = new HashMap<>();  
  
    for (Map.Entry<Method, InvocationHandlerFactory.MethodHandler> methodHandlerEntry : methodMethodHandlerMap.entrySet()) {  
        Method method = methodHandlerEntry.getKey(); 
        // 自定义的注解,参见使用方式 
        TimeoutOptions timeoutOptions = method.getAnnotation(TimeoutOptions.class);
        // 如果有自定义的注解,且参数里不含Reqeust.Options
        // 就替换现有的MethodHandler为包装类  
        if (timeoutOptions != null && Arrays.stream(method.getParameterTypes()).noneMatch(e -> e.equals(Request.Options.class))) {  
            changedMethodHandlerMap.put(method, new TimeoutMethodHandler(method, timeoutOptions, methodHandlerEntry.getValue()));  
        }  
    }  
  
    String enhancedInfo = changedMethodHandlerMap.keySet().stream().map(Method::toGenericString)  
            .collect(Collectors.joining("\n"));  
  
    if (!StringUtils.isEmpty(enhancedInfo)) {  
        log.info("Enhanced for {} feign method timeout: {}", enhanceFor().toGenericString(), enhancedInfo);  
    }  
  
    methodMethodHandlerMap.putAll(changedMethodHandlerMap);  
}

包装类

@Slf4j  
public class TimeoutMethodHandler implements InvocationHandlerFactory.MethodHandler {  
    @Getter  
    @Setter    
    private Request.Options requestOptions;  
    private final InvocationHandlerFactory.MethodHandler delegate;  
    @Getter  
    private final String name;  
  
    public TimeoutMethodHandler(Method method, TimeoutOptions timeoutOptions, InvocationHandlerFactory.MethodHandler origin) {  
        this.name = method.toGenericString();  
        this.requestOptions = new Request.Options(timeoutOptions.connectTimeout(), timeoutOptions.connectTimeoutUnit(),  
                timeoutOptions.readTimeout(), timeoutOptions.readTimeoutUnit(), timeoutOptions.followRedirects());  
        this.delegate = origin;  
        // Fuck the fields of requestOptions are all final.  
        // Field optionsField = origin.getClass().getDeclaredField("options");        // this.requestOptions = optionsField.get(this.delegate);        
        // 增加参数后,原有的metadata里的index都增加1
        // 否则序列化就有问题了
        try {  
            Field methodMetadataField = origin.getClass().getDeclaredField("metadata");  
            methodMetadataField.setAccessible(true);  
            MethodMetadata methodMetadata = (MethodMetadata) methodMetadataField.get(this.delegate);  
            if (methodMetadata.urlIndex() != null) {  
                methodMetadata.urlIndex(methodMetadata.urlIndex() + 1);  
            }  
            if (methodMetadata.bodyIndex() != null) {  
                methodMetadata.bodyIndex(methodMetadata.bodyIndex() + 1);  
            }  
            if (methodMetadata.headerMapIndex() != null) {  
                methodMetadata.headerMapIndex(methodMetadata.headerMapIndex() + 1);  
            }  
            if (methodMetadata.queryMapIndex() != null) {  
                methodMetadata.queryMapIndex(methodMetadata.queryMapIndex() + 1);  
            }  
            methodMetadataField.setAccessible(false);  
        } catch (NoSuchFieldException | IllegalAccessException e) {  
            e.printStackTrace();  
            throw new InteriorException(InteriorErrorStatus.ofServerError("Modify method metadata error"));  
        }  
    }  
  
    @Override  
    public Object invoke(Object[] objects) throws Throwable { 
	    // 这里就是增加的“责任”了 
        if (objects != null && objects.length != 0) {  
            List<Object> originObjects = Arrays.stream(objects).toList();  
            ArrayList<Object> newObjects = new ArrayList<>(originObjects);  
            newObjects.add(0, requestOptions);  
            return this.delegate.invoke(newObjects.toArray());  
        }  
        return this.delegate.invoke(new Object[]{requestOptions});  
    }  
  
    @Override  
    public String toString() {  
        return "[%s]->(%s,%s,%s,%s,%s)".formatted(name, requestOptions.connectTimeout(), requestOptions.connectTimeoutUnit(),  
                requestOptions.readTimeout(), requestOptions.readTimeoutUnit(), requestOptions.isFollowRedirects());  
    }  
}

使用

@FeignClient(value = "project-archetype", url = "${project-archetype-server-url-prefix}")  
@Tag(name = "ArchetypeDemoClient", description = "ArchetypeDemoClient")  
public interface ArchetypeDemoClient2 {  
  
    @Operation(tags = "save", description = "A sample interface of saving data")  
    @PostMapping(value = "/save", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)  
    @TimeoutOptions(connectTimeout = 5000, readTimeout = 120000)  
    ResponseResult<ArchetypeDemoVO> save2(@RequestBody @Valid ArchetypeDemoDTO dto);  
  
}

扩展

这里硬编码了对应的时间戳,那如果我想修改呢?重启?

答案:不需要。

既然我们包装了MethodHandler,那我们就动态修改MethodHandler里的Request.Options

细节不说了,大致上是提供一个spring actuator的endpoint来读取全部包装过的FeignClient方法和修改指定FeignClient方法。

效果如下:
查询

curl --location 'http://127.0.0.1:8888/actuator/feignMethodTimeout' \

--data ''

查询结果

[
"[public abstract com.xxx.project.common.protocol.communication.ResponseResult<com.xxx.project.archetype.api.vo.ArchetypeDemoVO> com.xxx.project.archetype.api.ArchetypeDemoClient2.save2(com.xxx.project.archetype.api.dto.ArchetypeDemoDTO)]->(5000,MILLISECONDS,120000,MILLISECONDS,true)"

]

修改

curl --location 'http://127.0.0.1:8888/actuator/feignMethodTimeout' \
--header 'Content-Type: application/json' \
--data '{
    "method":"public abstract com.xxx.project.common.protocol.communication.ResponseResult<com.xxx.project.archetype.api.vo.ArchetypeDemoVO> com.xxx.project.archetype.api.ArchetypeDemoClient2.save2(com.xxx.project.archetype.api.dto.ArchetypeDemoDTO)",
    "options":"9000,MILLISECONDS,5000,MILLISECONDS,true"
}'

修改结果

changed

标签:Feign,feign,method,project,public,Options,Timeout,archetype,Method
From: https://www.cnblogs.com/ranyabu/p/17378083.html

相关文章

  • MethodHandler 不会产生 boxing
    staticintadd(inta,intb){returna+b;}@TestpublicvoiddirectAdd()throwsThrowable{//编译31msSystem.out.println(System.currentTimeMillis());intr=0;for(inti=0;i<10000;i++)......
  • 从0开始搭建一个微服务项目(使用openfeign)
    本文发布时间:2023-05-05尚在学习当中,如有不足,请指正!!!项目结构本篇文章是之前项目的后续版本,前面的内容可看链接:从0开始搭建一个微服务项(并注册到nacos)_bgbgking的博客-CSDN博客因本篇内容较前篇跨幅较大,有兴趣可查看源码链接:spring-cloud-demo:springcloud基础架构及其......
  • feign调用报错status 404
    feign调用报错status404使用feign,报错:FeignException:status404。如下:Causedby:feign.FeignException:status404reading#; atfeign.FeignException.errorStatus(FeignException.java:62) atfeign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:91) atfe......
  • 2023AAAI_Ultra-High-Definition Low-Light Image Enhancement: A Benchmark and Tran
    一.motivition1.之前的数据集分辨率较低二.contribution1.提出两个超高清数据集UHD-4k和UHD-8k2.网络结构LLFormer(网络结构类似2022CVPR_Restormer:EffificientTransformerforHigh-ResolutionImageRestoration.)三.Network 网络架构类似于:2022CVPR_Restormer:......
  • @enableFeignClients注解的basePackages属性的作用
    basePackages属性是@EnableFeignClients注解的一个可选属性,它用于指定需要扫描的包路径。通过设置该属性,可以告诉Spring在哪些包下查找用@FeignClient注解标记的接口。basePackages中的包可以指定其他模块的包。在多模块的项目中,如果你想要在一个模块中使用另一个模块的......
  • 【HMS Core】获取用户信息接口,返回 session timeout
    【问题描述】集成华为账号服务,获取用户信息,调用相关接口一直返回sessiontimeout,参考链接:https://developer.huawei.com/consumer/cn/doc/development/HMSCore-References/get-user-info-0000001060261938​ 【解决方案】1、如果是走的端测的比如OkHttpClient这种方式的是需......
  • SSH工具远程登录Linux系统错误解决方法,错误提示Disconnected:No supported authentica
    一、使用轻量云控制面板的登录,sudosu获取root账号权限;二、执行passwd命令,输入新密码来修改root密码。三、修改密码登录为yes,步骤如下1、运行命令vi/etc/ssh/sshd_config2、将参数PasswordAuthentication设置为yes,前面不能有#号键3、重启SSH服务使用的系统是centos7.2,Cen......
  • 简单工厂模式(Static Factory Method)
    创建性设计模式——简单工厂模式(StaticFactorymethod)模式动机只需要知道参数的名字则可得到相应的对象软件开发时,有时需要创建一些来自于相同父类的类的实例。可以专门定义一个类(工厂)负责创建这些类的实例。可以通过传入不同的参数从而获得不同的对象。Java中可以将创建其......
  • Feign远程调用会丢失header信息,如果设置远程调用的header信息
    场景:订单模块需要查询在购物车模块的商品信息,但是在购物车模块中存在两种购物车,一个是登录的用户的购物车,一个是没有登录的零时用户的购物车,如果用户已经的登录,我们就将用户的信息放入session中,我们通过创建一个拦截器进行判断用户是否进行登录,如果登陆了,就将用户的信息放入Threa......
  • weblogic中使用commons-lang 出现 NoSuchMethodError错误
    weblogic中使用commons-lang出现NoSuchMethodError错误 项目中使用了commons-lang-2.4.jarweblogic启动时预先加载了一个commons-lang的包(bea11g\modules\com.bea.core.apache.commons.lang_2.1.0.jar)这样jar包版本出现冲突 在plan/WEB-INF下面添加weblogic.xml文件,其中添加以......