背景:使用feign传文件给下游服务。
问题1
Current request is not a multipart request
这个异常分析一下,是下游service的spring MVC抛出的。所以理解应该是,feign在调用下游服务时候,没有正确的设置Request headers。
因此需要我们了解feign是怎么调用下游service 以及Request headers怎么设置的。
1、通过IDE debug call stack找到feign顶层调用
spring-cloud-openfeign-core-2.0.0.RELEASE-sources.jar!/org/springframework/cloud/openfeign/FeignClientFactoryBean.java
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
30
31
32
33
34
| @Override
public Object getObject() throws Exception {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
url += cleanPath();
return loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not lod balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}
|
进而推算出feign的初始化:
@EnableFeignClients里@Import(FeignClientsRegistrar.class) ->
org/springframework/cloud/openfeign/FeignClientsRegistrar.java#registerBeanDefinitions#registerFeignClients ->
org/springframework/cloud/openfeign/FeignClientFactoryBean.java#getObject ->
feign-core-9.5.1-sources.jar!/feign/Feign.java#target ->
feign-core-9.5.1-sources.jar!/feign/ReflectiveFeign.java#newInstance
其中FeignClientFactoryBean.java#getObject会调用FeignClientsConfiguration.java注入的默认配置如builder/encoder/decoder
org/springframework/cloud/openfeign/FeignClientsConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
| @Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
@Bean
@ConditionalOnMissingBean
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}
|
2、初始化feign client
feign-core-9.5.1-sources.jar!/feign/ReflectiveFeign.java#newInstance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if(Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
|
这段代码 就是通过reflection获取@FeignClient修饰方法的注解,进而初始化client。所以,我们刚刚遇到的Current request is not a multipart request
问题,在这里通过debug发现
生成的nameToHandler里,metadata里 template是没有header配置的,所以在下游调用的时候,并不会加上multipart/form-data这个header。
所以我们怎么通过配置来告诉feign加上这个header呢。继续看源码:
feign-core-9.5.1-sources.jar!/feign/ReflectiveFeign.java#ParseHandlersByName
里会调用contract.parseAndValidatateMetadata(key.type())获取方法的metadata。
contract调用链是: feign/Contract.java
-> org/springframework/cloud/openfeign/support/SpringMvcContract.java
进一步发现org/springframework/cloud/openfeign/support/SpringMvcContract.java#processAnnotationOnMethod
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
30
31
32
33
34
35
36
37
38
39
40
41
42
| protected void processAnnotationOnMethod(MethodMetadata data,
Annotation methodAnnotation, Method method) {
if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
.annotationType().isAnnotationPresent(RequestMapping.class)) {
return;
}
RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
// HTTP Method
RequestMethod[] methods = methodMapping.method();
if (methods.length == 0) {
methods = new RequestMethod[] { RequestMethod.GET };
}
checkOne(method, methods, "method");
data.template().method(methods[0].name());
// path
checkAtMostOne(method, methodMapping.value(), "value");
if (methodMapping.value().length > 0) {
String pathValue = emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// Append path from @RequestMapping if value is present on method
if (!pathValue.startsWith("/")
&& !data.template().toString().endsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().append(pathValue);
}
}
// produces
parseProduces(data, method, methodMapping);
// consumes
parseConsumes(data, method, methodMapping);
// headers
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
}
|
它会反射解析method,进行配置。parseConsumes就是根据requestMapping的consumer来进行restTempalte request headers配置。
3、好了到现在知道怎么配置的了,那我们在确认下,feign在call时候 是否用到了这些配置呢。
通过debug进入,feign/ReflectiveFeign.java#FeignInvocationHandler#invoke
-> feign/SynchronousMethodHandler.java#invoke
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
|
而这时的SynchronousMethodHandler.java已经buildTemplateFromArgs已经在上面的ReflectiveFeign.java#newInstance创建好了
InvocationHandler handler = factory.create(target, methodToHandler);
所以client发出的request一定是 我们刚刚初始化配置的那个handler里对应的request。
因此,feign在初始化的时候,会把client的相关配置都配置好并缓存起来。如encoder和请求时的handler。
所以,要解决Current request is not a multipart request
,需要在@FeignClient声明的方法上@RequestMapping里加入consumer=MediaType.MULTIPART_FORM_DATA_VALUE
,显式地告诉feign给Request headers加上multipart/form-data。
加上后,接着会出现第二个问题。
问题2
the request was rejected because no multipart boundary was found
这个问题是feign在掉下游服务时候,request header Content-Type里没有加boundary。关于boundary,它主要起到分隔request body。
所以初步断定是,feign methodHandler 没有正确的处理。继续看代码。
feign/ReflectiveFeign.java#newInstance 如下
1
| Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
|
进而调用的是 feign/ReflectiveFeign.java#ParseHandlersByName#apply 方法 如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public Map<String, MethodHandler> apply(Target key) {
List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
} else if (md.bodyIndex() != null) {
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
} else {
buildTemplate = new BuildTemplateByResolvingArgs(md);
}
result.put(md.configKey(),
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
}
return result;
}
|
可以看出通过contract获取metadata,进而构造methodHandler。
继续debug到 feign/Contract.java#BaseContract#parseAndValidatateMetadata#parseAndValidateMetadata
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
....
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation) {
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
...
}
|
看出feign是通过反射获取methodParam的注解,通过!isHttpAnnotation
来设置metadata。而feign判断isHttpAnnotation的依据是在 SpringMvcContract.java#processAnnotationsOnParameter
AnnotatedParameterProcessor processor = this.annotatedArgumentProcessors.get(parameterAnnotation.annotationType())
annotatedArgumentProcessors 只有三种requestParam requestHeader pathVariable。所以,我们使用requestParam告诉了feign这是一个httpAnnotation,进而不会设置metadata的body与formparam。所以构造的是没有encoder的template。进而在不会创建boundary。
所以,修改@RequestParam 为 @RequestPart。这样在构造metadata时准确的获取到了bodyType。进而注入methodHandler的encoder是org/springframework/cloud/openfeign/support/SpringEncoder.java。
问题3
上面@RequestPart解决了给methodHandler加入了encoder,但是由于SpringEncoder无法正确转换Request Body。所以抛出了异常
feign.codec.EncodeException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile] and content type [multipart/form-data]
这时候,才要用到OpenFeign官方form支持。加入
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Configuration
public class FeignClientConfiguration {
private final ObjectFactory<HttpMessageConverters> messageConverters;
public FeignClientConfiguration(ObjectFactory<HttpMessageConverters> messageConverters) {
this.messageConverters = messageConverters;
}
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
|
增加form encoder,才能最终完全解决这个问题。