背景
最近在調(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;
}
}
到這里我們就梳理出來整個事情的來龍去脈了:
- openfeign 是支持給方法加上自定義 header 的柠新,它用的是自己的注解 @Headers
- springcloud-openfeign 使用了 openfeign 的核心功能,但是關(guān)于 @Headers 的注解沒有使用
- 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)自己的想要的功能啦挺份。
- 方便起見,我們直接復(fù)用 openfeign 的 @Header
- 簡單起見如蚜,我們直接繼承 SpringMvcContract
- 自定義自己的 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é)來說芯勘,有點強迫癥,不過基本可以忽略
思考:為什么沒有一開始想到將注解放到接口定義那里
- 思維定勢腺逛,工作內(nèi)容問題荷愕,很少會在 feign 接口上使用 @RequestMapping
- SpringMvcContract 中的 processAnnotationOnClass 方法中沒有關(guān)于對 header 的處理,導(dǎo)致一開始忽略這個
- SpringMvcContract 是在 parseAndValidatateMetadata 中解決類上面的 header 的問題
總結(jié)
- 本文主要是探討了 Contract 的一些功能,以及 springcloud 對它的處理
- 網(wǎng)上很多在說 @Headers 無效安疗,但是基本上都沒說原因抛杨,這里對它做一個解釋
- 繞了一圈,還是回歸到最簡單的辦法荐类,使用 @RequestMapping