feign RequestInterceptor 攔截器統(tǒng)一修改post表單請求體

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é)下最后的解決方案吧。

  1. 還是按照正常的post json的方式去寫feign接口
  2. 在Interceptor中
    1. 獲取到j(luò)son串并改造成最后的請求對象
    2. 修改header為application/x-www-form-urlencoded
    3. 通過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ā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容