Spring Cloud Feign添加自定义Header

冷不防 2022-10-29 01:52 416阅读 0赞

背景

最近在调用一个接口,接口要求将token放在header中传递。由于我的项目使用了feign, 那么给请求中添加 header 就必须要去feign中找方法了。

方案一:自定义 RequestInterceptor

在给 @FeignClient 注解的接口生成代理对象的时候,有这么一段:

  1. class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
  2. @Override
  3. public Object getObject() throws Exception {
  4. return getTarget();
  5. }
  6. // getTarget() 最终会调用到 configureUsingConfiguration()
  7. protected void configureUsingConfiguration(FeignContext context,Feign.Builder builder) {
  8. Map<String, RequestInterceptor> requestInterceptors = context.getInstances(this.contextId, RequestInterceptor.class);
  9. if (requestInterceptors != null) {
  10. builder.requestInterceptors(requestInterceptors.values());
  11. }
  12. ...
  13. }
  14. }
  15. 生成代理类时,会使用到 spring 上下文的 RequestInterceptor @FeignClient 的代理类在执行的时候,会去使用该拦截器:
  16. final class SynchronousMethodHandler implements MethodHandler {
  17. Request targetRequest(RequestTemplate template) {
  18. for (RequestInterceptor interceptor : requestInterceptors) {
  19. interceptor.apply(template);
  20. }
  21. return target.apply(template);
  22. }
  23. }
  24. 所以自定义自己的拦截器,然后注入到 spring 上下文中,这样就可以在请求的上下文中添加自定义的请求头:
  25. @Service
  26. public class MyRequestInterceptor implements RequestInterceptor {
  27. @Override
  28. public void apply(RequestTemplate template) {
  29. template.header("my-header","header");
  30. }
  31. }

优点

实现简单,使用现有接口注入即可

缺点

操作的是全局的 RequestTemplate,比较难以根据不同的服务方提供不同的 header。 虽然可以在 template 中根据 uri 来判断不同的服务提供方,然后添加对应的header,但是凭空多了很多配置信息,维护也比较困难。

方案二:在 @RequestMapping 注解中增加 header 信息

既然我们用到了 openfeign 框架,那我们找找 openfeign 官方是怎么解决的(https://github.com/OpenFeign/feign):

  1. // openfeign 官方文档
  2. public interface ContentService {
  3. @RequestLine("GET /api/documents/{contentType}")
  4. @Headers("Accept: {contentType}")
  5. String getDocumentByType(@Param("contentType") String type);
  6. }

通过上述官方代码示例,我们可以发现,其实使用原生的 API 就可以满足我们的需求:

  1. @FeignClient(name = "feign",url = "127.0.0.1:8080")
  2. public interface FeignTest {
  3. @RequestMapping(value = "/test")
  4. @Headers({"app: test-app","token: ${test-app.token}"})
  5. String test();
  6. }

然而比较遗憾的是,@Headers 并没有生效,生成的RequestTemplate中,没有上述两个 Header 信息。 跟踪代码,我们发现,ReflectFeign在生成远程服务的代理类的时候,会通过 Contract 接口准备数据。 而*@Headers* 注解没有生效的原因是:官方的 Contract 没有生效:

  1. class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
  2. protected Feign.Builder feign(FeignContext context) {
  3. Feign.Builder builder = get(context, Feign.Builder.class)
  4. // required values
  5. .logger(logger)
  6. .encoder(get(context, Encoder.class))
  7. .decoder(get(context, Decoder.class))
  8. .contract(get(context, Contract.class));
  9. ...
  10. }
  11. }
  12. 对于 springcloud-openfeign 来说,在创建 Feign 相关类的时候,使用的是容器中注入的 Contract
  13. @Bean
  14. @ConditionalOnMissingBean
  15. public Contract feignContract(ConversionService feignConversionService) {
  16. return new SpringMvcContract(this.parameterProcessors, feignConversionService);
  17. }
  18. public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
  19. @Override
  20. public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
  21. ....
  22. // 注意这里,它只取了 RequestMapping 注解
  23. RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class);
  24. ....
  25. parseHeaders(md, method, classAnnotation);
  26. }
  27. return md;
  28. }
  29. }
  30. 到这里我们就梳理出来整个事情的来龙去脉了:
  1. openfeign 是支持给方法加上自定义 header 的,它用的是自己的注解 @Headers
  2. springcloud-openfeign 使用了 openfeign的核心功能,但是关于 @Headers 的注解没有使用
  3. springcloud 使用了自己的 SpringMvcContract 来处理请求的相关资源信息,里面只使用 @RequestMapping 注解

我们比较容易想到的是,既然 @RequestMapping 注解中有 headers 的属性,我们可以试一下

  1. @FeignClient(name = "server",url = "127.0.0.1:8080")
  2. public interface FeignTest {
  3. @RequestMapping(value = "/test",headers = {"app=test-app","token=${test-app.token}"})
  4. String test();
  5. }

亲测可用,这样我们就可以给特定的服务单独定制头信息啦。

优点

实现更加简单了,甚至都不用自己实现接口,只需要自己在相关注解中增加对应属性配置即可

缺点

虽然不用给全局的请求增加header,但是对于相同的服务方,却要在每个@RequestMapping注解中添加相同的header配置,会比较麻烦,能否添加全局的呢?

方案三:自定义 Contract

通过 SpringMvcContract 代码我们也很容易发现,对于类的注解,它只会处理 RequestMapping,其它也都忽略了。 那么如果我们重新定义自己的 Contract,就可以随心所欲实现自己的想要的功能啦。

  1. 方便起见,我们直接复用 openfeign 的 @Header
  2. 简单起见,我们直接继承 SpringMvcContract
  3. 自定义自己的 Contract,然后注入到 spring 上下文中

    /**

    • 为了处理简单,我们直接继承 SpringMvcContract
      /
      @Service
      public class MyContract extends SpringMvcContract {
      /*

      1. * 该属性是为了使用 springcloud config
      2. */

      private ResourceLoader resourceLoader;

      @Override
      protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {

      1. //这里复用原有 SpringMvcContract 逻辑
      2. super.processAnnotationOnClass(data, clz);
      3. // 以下是新加的逻辑(其实是使用的 openfeign 自带的 Contract.Default的逻辑)
      4. if (clz.isAnnotationPresent(Headers.class)) {
      5. String[] headersOnType = clz.getAnnotation(Headers.class).value();
      6. Map<String, Collection<String>> headers = toMap(headersOnType);
      7. headers.putAll(data.template().headers());
      8. data.template().headers(null); // to clear
      9. data.template().headers(headers);
      10. }

      }

      private Map> toMap(String[] input) {

      1. Map<String, Collection<String>> result = new LinkedHashMap<>(input.length);
      2. for (String header : input) {
      3. int colon = header.indexOf(':');
      4. String name = header.substring(0, colon);
      5. if (!result.containsKey(name)) {
      6. result.put(name, new ArrayList<>(1));
      7. }
      8. result.get(name).add(resolve(header.substring(colon + 1).trim()));
      9. }
      10. return result;

      }

      private String resolve(String value) {

      1. if (StringUtils.hasText(value)
      2. && resourceLoader instanceof ConfigurableApplicationContext) {
      3. return ((ConfigurableApplicationContext) this.resourceLoader).getEnvironment()
      4. .resolvePlaceholders(value);
      5. }
      6. return value;

      }

      @Override
      public void setResourceLoader(ResourceLoader resourceLoader) {

      1. this.resourceLoader = resourceLoader;
      2. // 注意,因为SpringMvcContract 也使用了 resourceLoader,所以必须给它指定解析器,否则不会解析占位符
      3. super.setResourceLoader(resourceLoader);

      }
      }

使用的时候直接对 接口做 header的配置即可:

  1. @Headers({"app: test-app","token: ${test-app.token}"})
  2. public interface FeignTest {
  3. @RequestMapping(value = "/test")
  4. String test();
  5. }
  6. 优点

可以根据自己的需要自由定义

缺点

自定义带来一定的学习成本,而且因为是直接继承 spring 的实现,为以后升级留下隐患

方案四:在接口上使用 @RequestMapping,并加上 headers 属性

聪明的读者也许在方案二的结尾就能反应过来:springcloud 支持*@RequestMapping*注解的 header,而该注解完全可以用在类上面!

  1. @FeignClient(name = "feign",url = "127.0.0.1:8080")
  2. @RequestMapping(value = "/",headers = {"app=test-app","token=${test-app.token}"})
  3. public interface FeignTest {
  4. @RequestMapping(value = "/test")
  5. String test();
  6. }
  7. 优点

完全不用自定义,原生支持

缺点

基本没有。 可能对于有些不习惯在类上使用 @RequestMapping 注解的同学来说,有点强迫症,不过基本可以忽略

思考:为什么没有一开始想到将注解放到接口定义那里

  1. 思维定势,工作内容问题,很少会在feign接口上使用 @RequestMapping
  2. SpringMvcContract 中的 processAnnotationOnClass 方法中没有关于对 header的处理,导致一开始忽略这个
  3. SpringMvcContract 是在 parseAndValidatateMetadata 中解决类上面的 header 的问题

总结

  1. 本文主要是探讨了 Contract 的一些功能,以及 springcloud 对它的处理
  2. 网上很多在说 @Headers 无效,但是基本上都没说原因,这里对它做一个解释
  3. 绕了一圈,还是回归到最简单的办法,使用 @RequestMapping

参考

https://blog.csdn.net/hxnlyw/article/details/98594567

发表评论

表情:
评论列表 (有 0 条评论,416人围观)

还没有评论,来说两句吧...

相关阅读