上游服務(wù)提供者
- 使用spring接收文件可以使用
MultipartFile
對(duì)象,并同時(shí)使用RequestPart
注解標(biāo)識(shí)這個(gè)一個(gè)多媒體參數(shù)。 - 也就是request的
content-Type=multipart/form-data
- 文件上傳provider代碼:
@PostMapping("/form")
public String formData(@RequestPart("file") MultipartFile multipartFile) {
multipartFile.getBytes();
}
- 同時(shí)你可以可以還接收其他參數(shù),例如:
- 或者這種形式
測(cè)試服務(wù)提供者
- 首先要保證服務(wù)提供者能夠正常接收多媒體
MultipartFile
文件參數(shù)以及其他的參數(shù)。我這里使用postman測(cè)試,直接看圖: - 使用
multipart/form-data
的content-Type,并且傳遞一個(gè)文件參數(shù),一個(gè)字符串參數(shù)。 - 可以看到文件和字符串參數(shù)都是有值了,也就是服務(wù)提供者是可以調(diào)通的。
下游消費(fèi)者
-
消費(fèi)者直接通過Feign去調(diào)用文件上傳接口。
-
在另外一個(gè)項(xiàng)目中定義了一個(gè)FeignClient,并在consumer中直接通過Feign去調(diào)用
-
結(jié)果就是拋出異常
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.LinkedHashMap["file"]->org.springframework.mock.web.MockMultipartFile["inputStream"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1276) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:808) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:764) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:720) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:35) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:400) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1510) ~[jackson-databind-2.12.5.jar:2.12.5]
at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1006) ~[jackson-databind-2.12.5.jar:2.12.5]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:454) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:104) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.cloud.openfeign.support.SpringEncoder.checkAndWrite(SpringEncoder.java:195) ~[spring-cloud-openfeign-core-3.0.4.jar:3.0.4]
at org.springframework.cloud.openfeign.support.SpringEncoder.encodeWithMessageConverter(SpringEncoder.java:124) ~[spring-cloud-openfeign-core-3.0.4.jar:3.0.4]
at org.springframework.cloud.openfeign.support.SpringEncoder.encode(SpringEncoder.java:114) ~[spring-cloud-openfeign-core-3.0.4.jar:3.0.4]
at feign.ReflectiveFeign$BuildFormEncodedTemplateFromArgs.resolve(ReflectiveFeign.java:358) ~[feign-core-10.12.jar:na]
at feign.ReflectiveFeign$BuildTemplateByResolvingArgs.create(ReflectiveFeign.java:232) ~[feign-core-10.12.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:84) ~[feign-core-10.12.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-10.12.jar:na]
at com.sun.proxy.$Proxy85.formData(Unknown Source) ~[na:na]
at com.maple.cloud10feignconsumer.controller.ConsumerController.form(ConsumerController.java:59) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.12.jar:5.3.12]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.12.jar:5.3.12]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.54.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.12.jar:5.3.12]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.54.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.54.jar:9.0.54]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.12.jar:5.3.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.12.jar:5.3.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.12.jar:5.3.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.12.jar:5.3.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.StandardContextValve.__invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:41002) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1722) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-embed-core-9.0.54.jar:9.0.54]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.54.jar:9.0.54]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
- 看這個(gè)異常信息可能看不出什么,其實(shí)就是因?yàn)镕eign在構(gòu)建請(qǐng)求報(bào)文的時(shí)候,
content-type
不對(duì)導(dǎo)致的。圖片說的很清楚了,建議認(rèn)真看。
異常原因
- 也就是是說,F(xiàn)eign在調(diào)用時(shí),沒有給content-type導(dǎo)致報(bào)錯(cuò),而且這個(gè)報(bào)錯(cuò)是在下游服務(wù)構(gòu)建報(bào)文時(shí),是沒有請(qǐng)求到上游服務(wù)的。
- 那么解決方案也就是往
RequestTemplate
的header
中設(shè)置content-type=multipart/form-data
就好了。
錯(cuò)誤解決方案
- header中既然沒有content-type,那就設(shè)置一個(gè)進(jìn)去不就好了,但是很遺憾,RequestTemplate是一個(gè)具體的類,而不是一個(gè)接口,沒有提供很好的擴(kuò)展進(jìn)制。
- 你可能想到了
RequestInterceptor
,請(qǐng)求攔截器,feign在調(diào)用其他服務(wù)之前會(huì)先走攔截器。在攔截器中添加content-type不就好了。 - 但是很遺憾,RequestTemplate的body報(bào)文組裝編碼是在攔截器之前,話不說說,看代碼。
通過Feign調(diào)用接口,來到j(luò)dk動(dòng)態(tài)代理的invoke方法,拿到分發(fā)器,執(zhí)行invoke邏輯。
文章來源:http://www.zghlxwxcb.cn/news/detail-441694.html
invoke方法:構(gòu)建ReuqestTemplate以及請(qǐng)求報(bào)文,執(zhí)行并解密,執(zhí)行請(qǐng)求攔截器。
文章來源地址http://www.zghlxwxcb.cn/news/detail-441694.html
- 可以很清晰的看到,是先構(gòu)建了ReuqestTemplate的請(qǐng)求報(bào)文,然后在執(zhí)行的攔截器;正常流程在構(gòu)建請(qǐng)求報(bào)文編碼就報(bào)錯(cuò)了。
可行的解決方案
- 需要重新思考一個(gè)添加content-type的可行時(shí)機(jī)。
- 可以看到,F(xiàn)eign在執(zhí)行解析編碼的時(shí)候,是通過SpringEncoder去編碼,獲取content-type的。那我們就可以自己去創(chuàng)建一個(gè)
Encoder
去替換容器中的SpringEncoder
,然后在編碼之前,往RequestTemplate的header中添加content-type,或者重寫encode的邏輯。 - 接下來就是要看這個(gè)
SpringEncoder
是從哪里來的。
尋找SpringEncoder來源
- SpringEncoder的獲取是在容器啟動(dòng)時(shí)創(chuàng)建的,核心代碼如下
- 也就是在Ioc容器中獲取,那么現(xiàn)在要做的,就是找到他是何時(shí)加入到容器的。
- 通過調(diào)用棧可以發(fā)現(xiàn)
SpringEncoder
是通過配置類的@Bean,調(diào)用其他方法創(chuàng)建的。那我們就可以覆蓋這個(gè)SpringEncoder,注冊(cè)自己的Encoder對(duì)象。
注冊(cè)自定義Encoder
- 直接模擬spring的創(chuàng)建方式即可。
package com.maple.cloud10feignconsumer.config;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author maple
* @date 2022/12/29 16:08
* desc:
*/
@Configuration
public class EncoderConfiguration {
@Autowired(required = false)
private FeignEncoderProperties encoderProperties;
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
return mapleEncoder(formWriterProvider, encoderProperties);
}
private Encoder mapleEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
FeignEncoderProperties encoderProperties) {
return new MapleEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
}
}
編寫自定義Encoder
- encode的邏輯也就是在調(diào)用SpringEncoder之前,往RequestTemplate中添加
content-type
。那么什么時(shí)候能夠添加content-type,就是我們需要思考的問題,因?yàn)槲覀儾荒苡绊懫渌腇eignClient使用。 - 我的方案是使用自定義注解,標(biāo)識(shí)這是一個(gè)多媒體文件上傳接口,需要添加content-type,然后在下游FeignClient中標(biāo)記,并在自定義的Encoder中掃描。
自定義文件上傳接口標(biāo)識(shí)注解
package com.maple.cloud10feignconsumer.config;
import java.lang.annotation.*;
/**
* @author maple
* @date 2022/12/29 16:37
* desc:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FormData {
}
- 并在FeignClient中標(biāo)記
編寫encode邏輯
- 并在encode中掃描
package com.maple.cloud10feignconsumer.config;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import java.lang.reflect.Type;
/**
* @author maple
* @date 2022/12/29 15:56
* desc:
*/
public class MapleEncoder extends SpringEncoder {
public MapleEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
super(messageConverters);
}
public MapleEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters) {
super(springFormEncoder, messageConverters);
}
public MapleEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters, FeignEncoderProperties encoderProperties) {
super(springFormEncoder, messageConverters, encoderProperties);
}
@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
// 是表單請(qǐng)求,則添加header【content-type=multipart/form-data】
if (request.methodMetadata().method().isAnnotationPresent(FormData.class)) {
request.header("content-type", "multipart/form-data");
}
// 執(zhí)行SpringEncoder的邏輯
super.encode(requestBody, bodyType, request);
}
}
測(cè)試
- 可以看到,現(xiàn)在解析編碼就是我自定義的Encoder。
- 然后如果是文件上傳請(qǐng)求,就會(huì)在header中添加content-type。
- 然后也就能夠成功的調(diào)用到上游服務(wù)了,并且獲取數(shù)據(jù)。
總結(jié)
- 通過Feign直接去調(diào)用上游的文件上傳服務(wù),會(huì)報(bào)錯(cuò),原因是因?yàn)?code>RequestTemplate的header中沒有
content-type
,會(huì)導(dǎo)致錯(cuò)誤的編碼請(qǐng)求報(bào)文。 - 通過自定義
Encoder
繼承SpringEncoder
并重寫encode
方法,在編碼之前添加content-type。 - 并不是所有的Feign調(diào)用都需要自己添加conent-type,所有需要標(biāo)記一下接口,例如自定義注解。
到了這里,關(guān)于Feign傳遞文件以及MultipartFile對(duì)象的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!