RequestInterceptor介绍
现在很多开发都是用feign来请求三方接口。feign很方便,通过接口的方式来请求三方,有助于我们以面向接口编程,也简化了之前手工创建httpclient等麻烦的流程。但是对于三方接口中需要统一添加签名要怎么办呢?
莫慌,Feign给我们预留了一个RequestInterceptor接口,它可以在我们的请求发送之前对请求内容(包装成一个RequestTemplate)做统一的处理。那我们就可以在这里对请求参数做一些统一处理了
拦截并修改post json请求体
我们有一个三方的接口是post json的,并且有统一的参数如下
1 2 3 4 5 6
| { "appId": xxx, "sign": xxx, "timestampe": xxx, "data": {"a": xxx} }
|
那我们声明的feign接口,使用的时候不可能每次都去构造这些通用的参数,应该只需要传变化的东西进来就好了。例如上面的{“a”: xxx}。那么不变的部分在哪里添加呢?答案就是我们的RequestInterceptor
1 2 3 4 5 6 7 8 9 10 11 12
| public class FeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String jsonBody = template.requestBody().asString(); BaseReq baseReq = translateToBaseReq(jsonBody); String baseReqStr = JSON.toJSONString(baseReq); template.body(baseReqStr); } }
|
然后在我们需要的Feign接口的注解中配置configuration,标明使用这个拦截器配置就可以了
1 2 3 4 5
| @FeignClient(name = "hello", url = "hello", configuration = FeignInterceptor.class) public interface HelloFeign { @PostMapping("test") void test(@RequestBody ConcreteData data); }
|
这样就ok了,是不是很简单,然后我们的接口参数中只需要写实际要传的具体数据的类就行了。
拦截并修改post form请求体
post json搞定了,但接下来又出现了一个三方。它的接口是post表单形式的。有同学说,post表单我会。
网上也有很多这方面的教程,例如:2018-06-19 SpringCloud Feign Post表单请求,但是关键是post表单了之后,怎么处理统一的请求体呢?很明显,像上面直接通过template.body方式替换是不行的,这样请求体就是json字符串了。而form格式是a=xxx&b=xxx这样的。那有同学就说,我自己这样构造不就可以了?可以是可以,但是这就是在重复造轮子了。feign既然能发送post form的请求,说明它已经实现过了。那我们是不是可以借鉴下呢?
一览源码
那我们就顺着请求来看看feign是怎么post form的吧。(debug模式中在调用feign接口的地方step into)
首先来到了ReflectiveFeign类的 public Object invoke(Object proxy, Method method, Object[] args)方法。继续往下走在return dispatch.get(method).invoke(args);这里继续step into来到了SynchronousMethodHandler类的invoke方法。
1 2 3 4 5
| public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); .... }
|
1 2 3 4 5 6 7 8 9
| @Override public RequestTemplate create(Object[] argv) { RequestTemplate mutable = RequestTemplate.from(metadata.template()); ...... RequestTemplate template = resolve(argv, mutable, varBuilder); ...... }
|
1 2 3 4 5 6 7 8
| protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) { ...... encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable); ...... }
|
从这里encode方法就会调用SpringFormEncoder的encode方法,然后就会到FormEncoder的encode,最后调用到UrlencodedFormContentProcessor的process方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Override public void process (RequestTemplate template, Charset charset, Map<String, Object> data) throws EncodeException { val bodyData = new StringBuilder(); for (Entry<String, Object> entry : data.entrySet()) { if (entry == null || entry.getKey() == null) { continue; } if (bodyData.length() > 0) { bodyData.append(QUERY_DELIMITER); } bodyData.append(createKeyValuePair(entry, 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); template.header(CONTENT_TYPE_HEADER, Collections.<String>emptyList()); template.header(CONTENT_TYPE_HEADER, contentTypeValue); template.body(body); }
|
分析改造
从上面的源码中,我们可以看到其实feign就是通过SpringFormEncoder的encode方法,来将template的body替换成需要的表单数据的。那么这么encoder其实也是我们在post form的时候自己配置了@Bean注入的,那么我们同样也可以拿来用啊。
于是开始改造原来的Interceptor。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class FeignFormInterceptor implements RequestInterceptor { @Autowired SpringFormEncoder encoder;
@Override public void apply(RequestTemplate template) { String jsonBody = template.requestBody().asString(); BaseReq baseReq = translateToBaseReq(jsonBody); encoder.encode(baseReq, Encoder.MAP_STRING_WILDCARD, template); } }
|
1 2 3 4 5
| @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,还是出问题了。因为我们取出来的原来的body中的数据(通过template.requestBody().asString())不是json字符串。因为我们的feign接口定义的是post表单的,所以请求参数就被改造成a=xxx&b=xxx的形式了。所以这样就导致我们取出来的不是json串,那这样我们实际发送的data,也就是baseReq中的data的数据就是a=xxx&b=xxx,但实际我们要求的是json形式的。
那这可咋办?看起来似乎只能够改造这个数据成json格式了。但这样未免稍嫌麻烦,而且也不知道中间有什么坑没有。我们不是想获得json串吗?那我接口还是定义成post json的不就可以了吗?机智
1 2 3 4 5
| @FeignClient(name = "hello", url = "hello", configuration = FeignFormInterceptor.class) public interface HelloFeign { @PostMapping(value = "testForm") void testForm(@RequestBody ConcreteData data); }
|
但是这样的话,请求三方的header就又变成application/json的,并且数据也是json格式的。有人会说,不是encode里面会将header改造成application/x-www-form-urlencoded的吗?但那是在我们设置了consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE才会进入到最后的process方法。看下这个FormCoder的encode方法就知道了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Override @SuppressWarnings("unchecked") public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException { String contentTypeValue = getContentTypeValue(template.headers()); val contentType = ContentType.of(contentTypeValue); if (!processors.containsKey(contentType)) { delegate.encode(object, bodyType, template); return; } ...... val charset = getCharset(contentTypeValue); processors.get(contentType).process(template, charset, data); }
|
知道了原理后,那其实我们只要在进入这个encode方法之前,将我们的header改成application/x-www-form-urlencoded不就可以了吗?于是乎
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class FeignFormInterceptor implements RequestInterceptor { @Autowired SpringFormEncoder encoder;
@Override public void apply(RequestTemplate template) { String jsonBody = template.requestBody().asString(); BaseReq baseReq = translateToBaseReq(jsonBody); template.header(CONTENT_TYPE_HEADER, Collections.<String>emptyList()); template.header(CONTENT_TYPE_HEADER, URLENCODED.getHeader()); encoder.encode(baseReq, Encoder.MAP_STRING_WILDCARD, template); } }
|
到此,重要成功地拦截了feign的post表单请求,并统一加上了公用参数、签名等。
总结
啪啪一通,总结下最后的解决方案吧。
- 还是按照正常的post json的方式去写feign接口
- 在Interceptor中
- 获取到json串并改造成最后的请求对象
- 修改header为application/x-www-form-urlencoded
- 通过springEncoder的encode方法构造最终的表单请求体,并替换掉template中的(SpringFormEncoder还是要我们自己注入到容器的,在feign的post表单教程中都会提到)
扩展
直接用aop?
有的同学会说,整那么多事,直接搞个aop不就行。无论是post表单还是json,改造下请求参数就可以了。
确实这里直接用spring aop应该也能实现。有兴趣的同学可以试试。但是还是相对来说没那么直观,毕竟Interceptor是框架原生扩展,直接把参数都封装成RestTemplate给你了。如果用aop,可能要得去获取签名,签名做一些判断等
感谢以下的博主分享:
Feign RequestInterceptor in Spring Boot