RequestInterceptor介紹
現(xiàn)在很多開發(fā)都是用feign來請求三方接口。feign很方便,通過接口的方式來請求三方,有助于我們以面向接口編程,也簡化了之前手工創(chuàng)建httpclient等麻煩的流程。但是對于三方接口中需要統(tǒng)一添加簽名要怎么辦呢?
莫慌,F(xiàn)eign給我們預(yù)留了一個RequestInterceptor接口,它可以在我們的請求發(fā)送之前對請求內(nèi)容(包裝成一個RequestTemplate)做統(tǒng)一的處理。那我們就可以在這里對請求參數(shù)做一些統(tǒng)一處理了
攔截并修改post json請求體
我們有一個三方的接口是post json的,并且有統(tǒng)一的參數(shù)如下
{
"appId": xxx,
"sign": xxx,
"timestampe": xxx,
"data": {"a": xxx} //真正的數(shù)據(jù)以json格式放在data中
}
那我們聲明的feign接口,使用的時候不可能每次都去構(gòu)造這些通用的參數(shù),應(yīng)該只需要傳變化的東西進(jìn)來就好了。例如上面的{"a": xxx}。那么不變的部分在哪里添加呢?答案就是我們的RequestInterceptor
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 通過template獲取到請求體(已經(jīng)被轉(zhuǎn)成json)
String jsonBody = template.requestBody().asString();
// 構(gòu)造通用的請求體
BaseReq baseReq = translateToBaseReq(jsonBody);
// 替換請求體
String baseReqStr = JSON.toJSONString(baseReq);
template.body(baseReqStr);
}
}
然后在我們需要的Feign接口的注解中配置configuration,標(biāo)明使用這個攔截器配置就可以了
@FeignClient(name = "hello", url = "hello", configuration = FeignInterceptor.class)
public interface HelloFeign {
@PostMapping("test")
void test(@RequestBody ConcreteData data);
}
這樣就ok了,是不是很簡單,然后我們的接口參數(shù)中只需要寫實際要傳的具體數(shù)據(jù)的類就行了。
攔截并修改post form請求體
post json搞定了,但接下來又出現(xiàn)了一個三方。它的接口是post表單形式的。有同學(xué)說,post表單我會。
網(wǎng)上也有很多這方面的教程,例如:2018-06-19 SpringCloud Feign Post表單請求,但是關(guān)鍵是post表單了之后,怎么處理統(tǒng)一的請求體呢?很明顯,像上面直接通過template.body方式替換是不行的,這樣請求體就是json字符串了。而form格式是a=xxx&b=xxx這樣的。那有同學(xué)就說,我自己這樣構(gòu)造不就可以了?可以是可以,但是這就是在重復(fù)造輪子了。feign既然能發(fā)送post form的請求,說明它已經(jīng)實現(xiàn)過了。那我們是不是可以借鑒下呢?
一覽源碼
那我們就順著請求來看看feign是怎么post form的吧。(debug模式中在調(diào)用feign接口的地方step into)
首先來到了ReflectiveFeign類的 public Object invoke(Object proxy, Method method, Object[] args)方法。繼續(xù)往下走在return dispatch.get(method).invoke(args);這里繼續(xù)step into來到了SynchronousMethodHandler類的invoke方法。
public Object invoke(Object[] argv) throws Throwable {
//這里將參數(shù)構(gòu)造成了最終的RequestTemplate,我們從這里進(jìn)去看看
RequestTemplate template = buildTemplateFromArgs.create(argv);
....
}
@Override
public RequestTemplate create(Object[] argv) {
// 通過元數(shù)據(jù)初始化了一個RequestTemplate(不包含請求體)
RequestTemplate mutable = RequestTemplate.from(metadata.template());
......
// 這里才是生成最后的template的地方,繼續(xù)進(jìn)去
RequestTemplate template = resolve(argv, mutable, varBuilder);
......
}
protected RequestTemplate resolve(Object[] argv,
RequestTemplate mutable,
Map<String, Object> variables) {
......
// 在這里對template的body進(jìn)行了組裝
encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
......
}
從這里encode方法就會調(diào)用SpringFormEncoder的encode方法,然后就會到FormEncoder的encode,最后調(diào)用到UrlencodedFormContentProcessor的process方法
@Override
public void process (RequestTemplate template, Charset charset, Map<String, Object> data) throws EncodeException {
val bodyData = new StringBuilder();
// 這里對請求體中的參數(shù)進(jìn)行處理(Map<String,?>)
for (Entry<String, Object> entry : data.entrySet()) {
if (entry == null || entry.getKey() == null) {
continue;
}
// 參數(shù)之間用&連接
if (bodyData.length() > 0) {
bodyData.append(QUERY_DELIMITER);
}
// 參數(shù)key value之間用=號連接
bodyData.append(createKeyValuePair(entry, charset));
}
// 構(gòu)造application/x-www-form-urlencoded的請求頭和charset
val contentTypeValue = new StringBuilder()
.append(getSupportedContentType().getHeader())
.append("; charset=").append(charset.name())
.toString();
val bytes = bodyData.toString().getBytes(charset);
val body = Request.Body.encoded(bytes, charset);
// 清空原來的header,然后設(shè)置新的header以及替換上面的body
template.header(CONTENT_TYPE_HEADER, Collections.<String>emptyList()); // reset header
template.header(CONTENT_TYPE_HEADER, contentTypeValue);
template.body(body);
}
分析改造
從上面的源碼中,我們可以看到其實feign就是通過SpringFormEncoder的encode方法,來將template的body替換成需要的表單數(shù)據(jù)的。那么這么encoder其實也是我們在post form的時候自己配置了@Bean注入的,那么我們同樣也可以拿來用啊。
于是開始改造原來的Interceptor。
public class FeignFormInterceptor implements RequestInterceptor {
@Autowired
SpringFormEncoder encoder;
@Override
public void apply(RequestTemplate template) {
// 通過template獲取到請求體(已經(jīng)被轉(zhuǎn)成json)
String jsonBody = template.requestBody().asString();
// 構(gòu)造通用的請求體
BaseReq baseReq = translateToBaseReq(jsonBody);
// 通過encoder的encode方法,將我們的數(shù)據(jù) 改成表單數(shù)據(jù),并替換掉原來的template中的body
encoder.encode(baseReq, Encoder.MAP_STRING_WILDCARD, template);
}
}
@FeignClient(name = "hello", url = "hello", configuration = FeignFormInterceptor.class)
public interface HelloFeign {
@PostMapping(value = "testForm", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
void testForm(@RequestBody ConcreteData data);
}
看起來似乎ok了?nonono,還是出問題了。因為我們?nèi)〕鰜淼脑瓉淼腷ody中的數(shù)據(jù)(通過template.requestBody().asString())不是json字符串。因為我們的feign接口定義的是post表單的,所以請求參數(shù)就被改造成a=xxx&b=xxx的形式了。所以這樣就導(dǎo)致我們?nèi)〕鰜淼牟皇莏son串,那這樣我們實際發(fā)送的data,也就是baseReq中的data的數(shù)據(jù)就是a=xxx&b=xxx,但實際我們要求的是json形式的。
那這可咋辦?看起來似乎只能夠改造這個數(shù)據(jù)成json格式了。但這樣未免稍嫌麻煩,而且也不知道中間有什么坑沒有。我們不是想獲得json串嗎?那我接口還是定義成post json的不就可以了嗎?機智
@FeignClient(name = "hello", url = "hello", configuration = FeignFormInterceptor.class)
public interface HelloFeign {
@PostMapping(value = "testForm")
void testForm(@RequestBody ConcreteData data);
}
但是這樣的話,請求三方的header就又變成application/json的,并且數(shù)據(jù)也是json格式的。有人會說,不是encode里面會將header改造成application/x-www-form-urlencoded的嗎?但那是在我們設(shè)置了consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE才會進(jìn)入到最后的process方法??聪逻@個FormCoder的encode方法就知道了
@Override
@SuppressWarnings("unchecked")
public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
String contentTypeValue = getContentTypeValue(template.headers());
// 這里獲取了我們設(shè)置的header類型,也就是默認(rèn)的application/json
val contentType = ContentType.of(contentTypeValue);
// 沒有處理這個contentType的processors,就直接返回了。
if (!processors.containsKey(contentType)) {
delegate.encode(object, bodyType, template);
return;
}
......
val charset = getCharset(contentTypeValue);
// 而我們之前設(shè)置consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE的時候就會到這里,然后調(diào)用到UrlencodedFormContentProcessor的process方法。那里才能改造header
processors.get(contentType).process(template, charset, data);
}
知道了原理后,那其實我們只要在進(jìn)入這個encode方法之前,將我們的header改成application/x-www-form-urlencoded不就可以了嗎?于是乎
public class FeignFormInterceptor implements RequestInterceptor {
@Autowired
SpringFormEncoder encoder;
@Override
public void apply(RequestTemplate template) {
// 通過template獲取到請求體(已經(jīng)被轉(zhuǎn)成json)
String jsonBody = template.requestBody().asString();
// 構(gòu)造通用的請求體
BaseReq baseReq = translateToBaseReq(jsonBody);
// 先改造下header成表單頭,magic就出現(xiàn)了哈
template.header(CONTENT_TYPE_HEADER, Collections.<String>emptyList()); // reset header
template.header(CONTENT_TYPE_HEADER, URLENCODED.getHeader());
// 通過encoder的encode方法,將我們的數(shù)據(jù) 改成表單數(shù)據(jù),并替換掉原來的template中的body
encoder.encode(baseReq, Encoder.MAP_STRING_WILDCARD, template);
}
}
到此,重要成功地攔截了feign的post表單請求,并統(tǒng)一加上了公用參數(shù)、簽名等。
總結(jié)
啪啪一通,總結(jié)下最后的解決方案吧。
- 還是按照正常的post json的方式去寫feign接口
- 在Interceptor中
- 獲取到j(luò)son串并改造成最后的請求對象
- 修改header為application/x-www-form-urlencoded
- 通過springEncoder的encode方法構(gòu)造最終的表單請求體,并替換掉template中的(SpringFormEncoder還是要我們自己注入到容器的,在feign的post表單教程中都會提到)
為什么不直接用aop
有的同學(xué)會說,整那么多事,直接搞個aop不就行。無論是post表單還是json,改造下請求參數(shù)就可以了。這里我也想過要試試aop,但是有個需求aop不好滿足,就是我還要根據(jù)feign的url來修改請求體。通過aop的話,可能不是很好獲得這個url。而攔截器通過template可以輕松取到。所以整體來說還是Interceptor功能更強勁些。畢竟是原生的擴(kuò)展??
本文由博客群發(fā)一文多發(fā)等運營工具平臺 OpenWrite 發(fā)布