乐趣区

Feign动态设置header和原理

项目中用到了 Feign 做远程调用, 有部分场景需要动态配置 header

开始的做法是通过 @RequestHeader 设置参数来实现动态的 header 配置

例如:

@GetMapping(value = "/test", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE})    
String access(@RequestHeader("Auth") String auth, @RequestBody Expression expression);

这种方式虽然可以达到 header 的动态配置, 但是当参数过多时会降低接口可用性, 所以想通过传递 bean 的方式来设置 header

先说解决办法:

public class HeaderInterceptor implements RequestInterceptor {



    @Override
    public void apply(RequestTemplate requestTemplate) {byte[] bytes = requestTemplate.requestBody().asBytes();
        Identity identity = JSONObject.parseObject(bytes, Identity.class);
        requestTemplate.header("Auth", identity.getSecret());
    }
} 
/**
 * configuration 指定 Interceptor
**/
@FeignClient(name = "test", url = "127.0.0.1:8300", configuration = HeaderInterceptor.class)
public interface GolangTestHandle2 {@GetMapping(value = "/handler", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    String handle(Identity identity);

}

自定义 Interceptor 实现 RequestInterceptor 接口, 回调方法 apply 提供了 RequestTemplate 对象, 对象内部封装了 request 的所有信息, 最后通过 configuration 指定接口, 之后就随便你怎么玩了(例如通过 body 获取接口参数并动态设置 header)

值得注意的一点是 HeaderInterceptor 如果 注入到 Springboot 容器 的话会全局生效, 就是说及时没有指定 configuration 也会对全局 feign 接口生效, 为什么呢? 这里简单说明一下

首先 Feign 为每个 feign class 创建 springcontext 上下文
spring 通过调用 getObject 获取 feign 工厂实例

    @Override
    public Object getObject() throws Exception {return getTarget();
    }
    

内部调用 FeignClientFatoryBean.getTarget()方法

<T> T getTarget() {
        // 获取 feign 上下文
        FeignContext context = this.applicationContext.getBean(FeignContext.class);
        // 构建 feign Builder
        Feign.Builder builder = feign(context);
        ...
    }

根据 feign(FeignContext context)构建 Builder

protected Feign.Builder feign(FeignContext context) {FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
        Logger logger = loggerFactory.create(this.type);

        // @formatter:off
        Feign.Builder builder = get(context, Feign.Builder.class)
                // required values
                .logger(logger)
                // 默认 springEncoder
                .encoder(get(context, Encoder.class))
                // 默认 OptionalDecoder
                .decoder(get(context, Decoder.class))
                // 默认 SpringMvcContrat
                .contract(get(context, Contract.class));
        // @formatter:on
        // 配置该 feign 的 context
        configureFeign(context, builder);

        return builder;
    }
    

在构建过程中通过 FeignClientFactoryBean.configureUsingConfiguration 为 feign class 注册基本的配置项, 其中也包括了 Interceptor 的注册

    protected void configureUsingConfiguration(FeignContext context,
            Feign.Builder builder) {Logger.Level level = getOptional(context, Logger.Level.class);
        if (level != null) {builder.logLevel(level);
        }
        Retryer retryer = getOptional(context, Retryer.class);
        if (retryer != null) {builder.retryer(retryer);
        }
        ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
        if (errorDecoder != null) {builder.errorDecoder(errorDecoder);
        }
        Request.Options options = getOptional(context, Request.Options.class);
        if (options != null) {builder.options(options);
        }
        // 从 feign context 获取 interceptors
        Map<String, RequestInterceptor> requestInterceptors = context
                .getInstances(this.contextId, RequestInterceptor.class);
        if (requestInterceptors != null) {builder.requestInterceptors(requestInterceptors.values());
        }

        if (this.decode404) {builder.decode404();
        }
    }

contextId 为具体的 feign class id, RequestInterceptor 为具体的接口, 即是说通过 context.getInstances 获取所有 RequestInterceptor 实例并注册到 builder 中.


    public <T> Map<String, T> getInstances(String name, Class<T> type) {AnnotationConfigApplicationContext context = getContext(name);
        // 使用 beanNamesForTypeIncludingAncestors
        if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                type).length > 0) {return BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type);
        }
        return null;
    }

获取工厂中的实例使用的是 beanNamesForTypeIncludingAncestors 方法, 该不仅会总 feign 的 factory 中查找, 也会通过父级别 spring 工厂查找相应实例(类似于 springmvc 的工厂)

也是因为该方法, 及时你没有在 FeignClient 中配置 configuration, 但是你的 Interceptor 通过 @Component 等方法注入容器的话也会全局生效的, 所以如果指向让你的 Interceptor 部分生效不让它注入到 Spring 容器就好

退出移动版