Spring Cloud Feign 添加自定義 Header

背景

最近在調(diào)用一個接口甥厦,接口要求將 token 放在 header 中傳遞妨猩。由于我的項目使用了 feign, 那么給請求中添加 header 就必須要去 feign 中找方法了竖幔。

方案一:自定義 RequestInterceptor

在給 @FeignClient 注解的接口生成代理對象的時候沃暗,有這么一段:

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    @Override
    public Object getObject() throws Exception {
        return getTarget();
    }
    // getTarget() 最終會調(diào)用到 configureUsingConfiguration()
    protected void configureUsingConfiguration(FeignContext context,Feign.Builder builder) {
        Map<String, RequestInterceptor> requestInterceptors = context.getInstances(this.contextId, RequestInterceptor.class);
        if (requestInterceptors != null) {
            builder.requestInterceptors(requestInterceptors.values());
        }
        ...
    }
}

生成代理類時绽诚,會使用到 spring 上下文的 RequestInterceptor日麸, 而 @FeignClient 的代理類在執(zhí)行的時候,會去使用該攔截器:

final class SynchronousMethodHandler implements MethodHandler {
    Request targetRequest(RequestTemplate template) {
      for (RequestInterceptor interceptor : requestInterceptors) {
        interceptor.apply(template);
      }
      return target.apply(template);
  }
}

所以自定義自己的攔截器纽哥,然后注入到 spring 上下文中钠乏,這樣就可以在請求的上下文中添加自定義的請求頭:

@Service
public class MyRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("my-header","header");
    }
}

優(yōu)點

實現(xiàn)簡單,使用現(xiàn)有接口注入即可

缺點

操作的是全局的 RequestTemplate春塌,比較難以根據(jù)不同的服務(wù)方提供不同的 header晓避。 雖然可以在 template 中根據(jù) uri 來判斷不同的服務(wù)提供方,然后添加對應(yīng)的 header只壳,但是憑空多了很多配置信息俏拱,維護也比較困難。

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

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

// openfeign 官方文檔
public interface ContentService {
  @RequestLine("GET /api/documents/{contentType}")
  @Headers("Accept: {contentType}")
  String getDocumentByType(@Param("contentType") String type);
}

通過上述官方代碼示例锅必,我們可以發(fā)現(xiàn),其實使用原生的 API 就可以滿足我們的需求:

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

然而比較遺憾的是惕艳,@Headers 并沒有生效况毅,生成的 RequestTemplate 中,沒有上述兩個 Header 信息尔艇。 跟蹤代碼尔许,我們發(fā)現(xiàn),ReflectFeign 在生成遠程服務(wù)的代理類的時候终娃,會通過 Contract 接口準備數(shù)據(jù)味廊。 而 @Headers 注解沒有生效的原因是:官方的 Contract 沒有生效:

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    protected Feign.Builder feign(FeignContext context) {
        Feign.Builder builder = get(context, Feign.Builder.class)
                // required values
                .logger(logger)
                .encoder(get(context, Encoder.class))
                .decoder(get(context, Decoder.class))
                .contract(get(context, Contract.class));
                ...
    }

}

對于 springcloud-openfeign 來說,在創(chuàng)建 Feign 相關(guān)類的時候,使用的是容器中注入的 Contract:

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
    return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}

public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
    @Override
    public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
        ....
        // 注意這里余佛,它只取了 RequestMapping 注解
        RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class);
        ....
        parseHeaders(md, method, classAnnotation);
        }
        return md;
    }
}

到這里我們就梳理出來整個事情的來龍去脈了:

  1. openfeign 是支持給方法加上自定義 header 的柠新,它用的是自己的注解 @Headers
  2. springcloud-openfeign 使用了 openfeign 的核心功能,但是關(guān)于 @Headers 的注解沒有使用
  3. springcloud 使用了自己的 SpringMvcContract 來處理請求的相關(guān)資源信息辉巡,里面只使用 @RequestMapping 注解

我們比較容易想到的是恨憎,既然 @RequestMapping 注解中有 headers 的屬性,我們可以試一下

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

親測可用郊楣,這樣我們就可以給特定的服務(wù)單獨定制頭信息啦憔恳。

優(yōu)點

實現(xiàn)更加簡單了,甚至都不用自己實現(xiàn)接口净蚤,只需要自己在相關(guān)注解中增加對應(yīng)屬性配置即可

缺點

雖然不用給全局的請求增加 header钥组,但是對于相同的服務(wù)方,卻要在每個 @RequestMapping 注解中添加相同的 header 配置今瀑,會比較麻煩程梦,能否添加全局的呢?

方案三:自定義 Contract

通過 SpringMvcContract 代碼我們也很容易發(fā)現(xiàn),對于類的注解橘荠,它只會處理 RequestMapping屿附,其它也都忽略了。 那么如果我們重新定義自己的 Contract哥童,就可以隨心所欲實現(xiàn)自己的想要的功能啦挺份。

  1. 方便起見,我們直接復(fù)用 openfeign 的 @Header
  2. 簡單起見如蚜,我們直接繼承 SpringMvcContract
  3. 自定義自己的 Contract压恒,然后注入到 spring 上下文中
/**
  * 為了處理簡單影暴,我們直接繼承 SpringMvcContract
  */
@Service
public class MyContract extends SpringMvcContract {
    /**
         * 該屬性是為了使用 springcloud config
         */
    private ResourceLoader resourceLoader;

    @Override
    protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
        //這里復(fù)用原有 SpringMvcContract 邏輯
        super.processAnnotationOnClass(data, clz);

        // 以下是新加的邏輯(其實是使用的 openfeign 自帶的 Contract.Default的邏輯)
        if (clz.isAnnotationPresent(Headers.class)) {
            String[] headersOnType = clz.getAnnotation(Headers.class).value();
            Map<String, Collection<String>> headers = toMap(headersOnType);
            headers.putAll(data.template().headers());
            data.template().headers(null); // to clear
            data.template().headers(headers);
        }
    }

    private Map<String, Collection<String>> toMap(String[] input) {
        Map<String, Collection<String>> result = new LinkedHashMap<>(input.length);
        for (String header : input) {
            int colon = header.indexOf(':');
            String name = header.substring(0, colon);
            if (!result.containsKey(name)) {
                result.put(name, new ArrayList<>(1));
            }
            result.get(name).add(resolve(header.substring(colon + 1).trim()));
        }
        return result;
    }

    private String resolve(String value) {
        if (StringUtils.hasText(value)
            && resourceLoader instanceof ConfigurableApplicationContext) {
            return ((ConfigurableApplicationContext) this.resourceLoader).getEnvironment()
                .resolvePlaceholders(value);
        }
        return value;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
        // 注意错邦,因為SpringMvcContract 也使用了 resourceLoader,所以必須給它指定解析器型宙,否則不會解析占位符
        super.setResourceLoader(resourceLoader);
    }
}

使用的時候直接對 接口做 header 的配置即可:

@Headers({"app: test-app","token: ${test-app.token}"})
public interface FeignTest {
    @RequestMapping(value = "/test")
    String test();
}

優(yōu)點

可以根據(jù)自己的需要自由定義

缺點

自定義帶來一定的學(xué)習(xí)成本撬呢,而且因為是直接繼承 spring 的實現(xiàn),為以后升級留下隱患

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

聰明的讀者也許在方案二的結(jié)尾就能反應(yīng)過來:springcloud 支持 *@RequestMapping * 注解的 header魂拦,而該注解完全可以用在類上面!

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

優(yōu)點

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

缺點

基本沒有搁嗓。 可能對于有些不習(xí)慣在類上使用 @RequestMapping 注解的同學(xué)來說芯勘,有點強迫癥,不過基本可以忽略

思考:為什么沒有一開始想到將注解放到接口定義那里

  1. 思維定勢腺逛,工作內(nèi)容問題荷愕,很少會在 feign 接口上使用 @RequestMapping
  2. SpringMvcContract 中的 processAnnotationOnClass 方法中沒有關(guān)于對 header 的處理,導(dǎo)致一開始忽略這個
  3. SpringMvcContract 是在 parseAndValidatateMetadata 中解決類上面的 header 的問題

總結(jié)

  1. 本文主要是探討了 Contract 的一些功能,以及 springcloud 對它的處理
  2. 網(wǎng)上很多在說 @Headers 無效安疗,但是基本上都沒說原因抛杨,這里對它做一個解釋
  3. 繞了一圈,還是回歸到最簡單的辦法荐类,使用 @RequestMapping
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怖现,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子玉罐,更是在濱河造成了極大的恐慌屈嗤,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厌小,死亡現(xiàn)場離奇詭異恢共,居然都是意外死亡,警方通過查閱死者的電腦和手機璧亚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門讨韭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人癣蟋,你說我怎么就攤上這事透硝。” “怎么了疯搅?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵濒生,是天一觀的道長。 經(jīng)常有香客問我幔欧,道長罪治,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任礁蔗,我火速辦了婚禮觉义,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘浴井。我一直安慰自己晒骇,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布磺浙。 她就那樣靜靜地躺著洪囤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撕氧。 梳的紋絲不亂的頭發(fā)上瘤缩,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機與錄音伦泥,去河邊找鬼剥啤。 笑死何暮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铐殃。 我是一名探鬼主播海洼,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼富腊!你這毒婦竟也來了坏逢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤赘被,失蹤者是張志新(化名)和其女友劉穎是整,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體民假,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡浮入,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了羊异。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片事秀。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖野舶,靈堂內(nèi)的尸體忽然破棺而出易迹,到底是詐尸還是另有隱情,我是刑警寧澤平道,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布睹欲,位于F島的核電站,受9級特大地震影響一屋,放射性物質(zhì)發(fā)生泄漏窘疮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一冀墨、第九天 我趴在偏房一處隱蔽的房頂上張望闸衫。 院中可真熱鬧,春花似錦轧苫、人聲如沸楚堤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衅胀,卻和暖如春岔乔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滚躯。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工雏门, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嘿歌,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓茁影,卻偏偏與公主長得像宙帝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子募闲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容