SpringCloud+Feign环境下文件上传与form-data同时存在的解决办法

Posted oilamp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringCloud+Feign环境下文件上传与form-data同时存在的解决办法相关的知识,希望对你有一定的参考价值。

书接上文。

上文中描述了如何在 SpringCloud+Feign环境下上传文件与form-data同时存在的解决办法,实践证明基本可行,但却会引入其他问题。

主要导致的后果是:

1. 无法与普通Feign方法并存

2. 几率性(不确定条件下)导致其他form-data类型参数无法识别,无法正常工作,错误信息大致如下:

org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part ‘file‘ is not present

分析原因发现是Feign的Encoder体系中缺乏对应的配置从而无法工作;但将这些Encoder一一补上却过于困难,因此,决定换一个思路,使用Spring技术解决该问题。

该问题的本质是controller层传参时参数的编解码问题,因此,应该属于HttpMessageConverter范畴的事情,这参考SpringEncoder的代码即可得知。

最终,解决方案如下:

1. 去掉依赖,因为不再需要

 1             <dependency>
 2                 <groupId>io.github.openfeign.form</groupId>
 3                 <artifactId>feign-form-spring</artifactId>
 4                 <version>3.2.2</version>
 5             </dependency>
 6 
 7             <dependency>
 8                 <groupId>io.github.openfeign.form</groupId>
 9                 <artifactId>feign-form</artifactId>
10                 <version>3.2.2</version>
11             </dependency>

2. 去掉类  FeignSpringFormEncoder ,不再需要

3. 注解  RequestPartParam 、类  RequestPartParamParameterProcessor 以及 bean 的定义均与上文保持一致

4. 参考类  FormHttpMessageConverter  添加类  LinkedHashMapFormHttpMessageConverter  ,并注意使用 @Conponent注解进行Spring托管。代码如下:

  1 import org.springframework.core.io.Resource;
  2 import org.springframework.http.HttpEntity;
  3 import org.springframework.http.HttpHeaders;
  4 import org.springframework.http.HttpInputMessage;
  5 import org.springframework.http.HttpOutputMessage;
  6 import org.springframework.http.MediaType;
  7 import org.springframework.http.StreamingHttpOutputMessage;
  8 import org.springframework.http.converter.AbstractHttpMessageConverter;
  9 import org.springframework.http.converter.ByteArrayHttpMessageConverter;
 10 import org.springframework.http.converter.HttpMessageConverter;
 11 import org.springframework.http.converter.HttpMessageNotReadableException;
 12 import org.springframework.http.converter.HttpMessageNotWritableException;
 13 import org.springframework.http.converter.ResourceHttpMessageConverter;
 14 import org.springframework.http.converter.StringHttpMessageConverter;
 15 import org.springframework.stereotype.Component;
 16 import org.springframework.util.Assert;
 17 import org.springframework.util.MimeTypeUtils;
 18 import org.springframework.web.multipart.MultipartFile;
 19 
 20 import javax.mail.internet.MimeUtility;
 21 
 22 import java.io.IOException;
 23 import java.io.OutputStream;
 24 import java.io.UnsupportedEncodingException;
 25 import java.nio.charset.Charset;
 26 import java.util.ArrayList;
 27 import java.util.Collections;
 28 import java.util.LinkedHashMap;
 29 import java.util.List;
 30 import java.util.Map;
 31 
 32 /**
 33  * 参考 FormHttpMessageConverter
 34  */
 35 @Component
 36 public class LinkedHashMapFormHttpMessageConverter implements HttpMessageConverter<LinkedHashMap<String, ?>> {
 37 
 38 
 39     public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
 40 
 41 
 42     private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
 43 
 44     private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
 45 
 46     private Charset charset = DEFAULT_CHARSET;
 47 
 48     private Charset multipartCharset;
 49 
 50 
 51     public LinkedHashMapFormHttpMessageConverter() {
 52         this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
 53 
 54         StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
 55         stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316
 56 
 57         this.partConverters.add(new ByteArrayHttpMessageConverter());
 58         this.partConverters.add(stringHttpMessageConverter);
 59         this.partConverters.add(new ResourceHttpMessageConverter());
 60 
 61         MultipartFileHttpMessageConverter multipartFileHttpMessageConverter = new MultipartFileHttpMessageConverter();
 62         this.partConverters.add(multipartFileHttpMessageConverter);
 63 
 64         applyDefaultCharset();
 65     }
 66 
 67     @Override
 68     public List<MediaType> getSupportedMediaTypes() {
 69         return Collections.unmodifiableList(this.supportedMediaTypes);
 70     }
 71 
 72     public void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
 73         Assert.notEmpty(partConverters, "‘partConverters‘ must not be empty");
 74         this.partConverters = partConverters;
 75     }
 76 
 77     public void addPartConverter(HttpMessageConverter<?> partConverter) {
 78         Assert.notNull(partConverter, "‘partConverter‘ must not be null");
 79         this.partConverters.add(partConverter);
 80     }
 81 
 82     public void setCharset(Charset charset) {
 83         if (charset != this.charset) {
 84             this.charset = (charset != null ? charset : DEFAULT_CHARSET);
 85             applyDefaultCharset();
 86         }
 87     }
 88 
 89     private void applyDefaultCharset() {
 90         for (HttpMessageConverter<?> candidate : this.partConverters) {
 91             if (candidate instanceof AbstractHttpMessageConverter) {
 92                 AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
 93                 // Only override default charset if the converter operates with a charset to begin with...
 94                 if (converter.getDefaultCharset() != null) {
 95                     converter.setDefaultCharset(this.charset);
 96                 }
 97             }
 98         }
 99     }
100 
101     public void setMultipartCharset(Charset charset) {
102         this.multipartCharset = charset;
103     }
104 
105 
106     @Override
107     public boolean canRead(Class<?> clazz, MediaType mediaType) {
108         return false;
109     }
110 
111     @Override
112     public boolean canWrite(Class<?> clazz, MediaType mediaType) {
113         if (!LinkedHashMap.class.isAssignableFrom(clazz)) {
114             return false;
115         }
116         if (mediaType == null || MediaType.ALL.equals(mediaType)) {
117             return false;
118         }
119         for (MediaType supportedMediaType : getSupportedMediaTypes()) {
120             if (supportedMediaType.isCompatibleWith(mediaType)) {
121                 return true;
122             }
123         }
124         return false;
125     }
126 
127     @Override
128     public LinkedHashMap<String, String> read(Class<? extends LinkedHashMap<String, ?>> clazz,
129                                               HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
130         throw new HttpMessageNotReadableException("Not supportted for read.");
131     }
132 
133     @Override
134     @SuppressWarnings("unchecked")
135     public void write(LinkedHashMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
136             throws IOException, HttpMessageNotWritableException {
137 
138         writeMultipart((LinkedHashMap<String, Object>) map, outputMessage);
139     }
140 
141     private void writeMultipart(final LinkedHashMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
142         final byte[] boundary = generateMultipartBoundary();
143         Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
144 
145         MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
146         HttpHeaders headers = outputMessage.getHeaders();
147         headers.setContentType(contentType);
148 
149         if (outputMessage instanceof StreamingHttpOutputMessage) {
150             StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
151             streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
152                 @Override
153                 public void writeTo(OutputStream outputStream) throws IOException {
154                     writeParts(outputStream, parts, boundary);
155                     writeEnd(outputStream, boundary);
156                 }
157             });
158         }
159         else {
160             writeParts(outputMessage.getBody(), parts, boundary);
161             writeEnd(outputMessage.getBody(), boundary);
162         }
163     }
164 
165     private void writeParts(OutputStream os, LinkedHashMap<String, Object> parts, byte[] boundary) throws IOException {
166         for (Map.Entry<String, Object> entry : parts.entrySet()) {
167             String name = entry.getKey();
168             Object part = entry.getValue();
169             if (part != null) {
170                 writeBoundary(os, boundary);
171                 writePart(name, getHttpEntity(part), os);
172                 writeNewLine(os);
173             }
174         }
175     }
176 
177     @SuppressWarnings("unchecked")
178     private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
179         Object partBody = partEntity.getBody();
180         Class<?> partType = partBody.getClass();
181         HttpHeaders partHeaders = partEntity.getHeaders();
182         MediaType partContentType = partHeaders.getContentType();
183         for (HttpMessageConverter<?> messageConverter : this.partConverters) {
184             if (messageConverter.canWrite(partType, partContentType)) {
185                 HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
186                 multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
187                 if (!partHeaders.isEmpty()) {
188                     multipartMessage.getHeaders().putAll(partHeaders);
189                 }
190                 ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
191                 return;
192             }
193         }
194         throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
195                 "found for request type [" + partType.getName() + "]");
196     }
197 
198     protected byte[] generateMultipartBoundary() {
199         return MimeTypeUtils.generateMultipartBoundary();
200     }
201 
202     protected HttpEntity<?> getHttpEntity(Object part) {
203         return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
204     }
205 
206     protected String getFilename(Object part) {
207         if (part instanceof Resource) {
208             Resource resource = (Resource) part;
209             String filename = resource.getFilename();
210             if (filename != null && this.multipartCharset != null) {
211                 filename = MimeDelegate.encode(filename, this.multipartCharset.name());
212             }
213             return filename;
214         } else if (part instanceof MultipartFile) {
215             MultipartFile multipartFile = (MultipartFile) part;
216             String filename = multipartFile.getName();
217             if (filename == null || filename.isEmpty()) {
218                 filename = multipartFile.getOriginalFilename();
219             }
220             return filename;
221         }
222         else {
223             return null;
224         }
225     }
226 
227 
228     private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
229         os.write(‘-‘);
230         os.write(‘-‘);
231         os.write(boundary);
232         writeNewLine(os);
233     }
234 
235     private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
236         os.write(‘-‘);
237         os.write(‘-‘);
238         os.write(boundary);
239         os.write(‘-‘);
240         os.write(‘-‘);
241         writeNewLine(os);
242     }
243 
244     private static void writeNewLine(OutputStream os) throws IOException {
245         os.write(‘
‘);
246         os.write(‘
‘);
247     }
248 
249     private static class MultipartHttpOutputMessage implements HttpOutputMessage {
250 
251         private final OutputStream outputStream;
252 
253         private final HttpHeaders headers = new HttpHeaders();
254 
255         private boolean headersWritten = false;
256 
257         public MultipartHttpOutputMessage(OutputStream outputStream) {
258             this.outputStream = outputStream;
259         }
260 
261         @Override
262         public HttpHeaders getHeaders() {
263             return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
264         }
265 
266         @Override
267         public OutputStream getBody() throws IOException {
268             writeHeaders();
269             return this.outputStream;
270         }
271 
272         private void writeHeaders() throws IOException {
273             if (!this.headersWritten) {
274                 for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
275                     byte[] headerName = getAsciiBytes(entry.getKey());
276                     for (String headerValueString : entry.getValue()) {
277                         byte[] headerValue = getAsciiBytes(headerValueString);
278                         this.outputStream.write(headerName);
279                         this.outputStream.write(‘:‘);
280                         this.outputStream.write(‘ ‘);
281                         this.outputStream.write(headerValue);
282                         writeNewLine(this.outputStream);
283                     }
284                 }
285                 writeNewLine(this.outputStream);
286                 this.headersWritten = true;
287             }
288         }
289 
290         private byte[] getAsciiBytes(String name) {
291             try {
292                 return name.getBytes("US-ASCII");
293             }
294             catch (UnsupportedEncodingException ex) {
295                 // Should not happen - US-ASCII is always supported.
296                 throw new IllegalStateException(ex);
297             }
298         }
299     }
300 
301     private static class MimeDelegate {
302 
303         public static String encode(String value, String charset) {
304             try {
305                 return MimeUtility.encodeText(value, charset, null);
306             }
307             catch (UnsupportedEncodingException ex) {
308                 throw new IllegalStateException(ex);
309             }
310         }
311     }
312 }

5. 参考类  ResourceHttpMessageConverter  添加类  MultipartFileHttpMessageConverter ,代码如下:

 1 import org.springframework.http.HttpInputMessage;
 2 import org.springframework.http.HttpOutputMessage;
 3 import org.springframework.http.MediaType;
 4 import org.springframework.http.converter.AbstractHttpMessageConverter;
 5 import org.springframework.http.converter.HttpMessageNotReadableException;
 6 import org.springframework.http.converter.HttpMessageNotWritableException;
 7 import org.springframework.util.StreamUtils;
 8 import org.springframework.web.multipart.MultipartFile;
 9 
10 import java.io.FileNotFoundException;
11 import java.io.IOException;
12 import java.io.InputStream;
13 
14 public class MultipartFileHttpMessageConverter extends AbstractHttpMessageConverter<MultipartFile> {
15 
16     public MultipartFileHttpMessageConverter() {
17         super(MediaType.APPLICATION_OCTET_STREAM);
18     }
19 
20     @Override
21     protected boolean supports(Class<?> clazz) {
22         return MultipartFile.class.isAssignableFrom(clazz);
23     }
24 
25     @Override
26     protected MultipartFile readInternal(Class<? extends MultipartFile> clazz, HttpInputMessage inputMessage)
27             throws IOException, HttpMessageNotReadableException {
28         throw new HttpMessageNotReadableException("Not supportted for read.");
29     }
30 
31     @Override
32     protected MediaType getDefaultContentType(MultipartFile multipartFile) {
33         try {
34             String contentType = multipartFile.getContentType();
35             MediaType mediaType = MediaType.valueOf(contentType);
36             if (mediaType != null) {
37                 return mediaType;
38             }
39         } catch (Exception ex) {
40         }
41         return MediaType.APPLICATION_OCTET_STREAM;
42     }
43 
44     @Override
45     protected Long getContentLength(MultipartFile multipartFile, MediaType contentType) throws IOException {
46         long contentLength = multipartFile.getSize();
47         return (contentLength < 0 ? null : contentLength);
48     }
49 
50     @Override
51     protected void writeInternal(MultipartFile multipartFile, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
52         writeContent(multipartFile, outputMessage);
53     }
54 
55     protected void writeContent(MultipartFile multipartFile, HttpOutputMessage outputMessage)
56             throws IOException, HttpMessageNotWritableException {
57         try {
58             InputStream in = multipartFile.getInputStream();
59             try {
60                 StreamUtils.copy(in, outputMessage.getBody());
61             }
62             catch (NullPointerException ex) {
63                 // ignore, see SPR-13620
64             }
65             finally {
66                 try {
67                     in.close();
68                 }
69                 catch (Throwable ex) {
70                     // ignore, see SPR-12999
71                 }
72             }
73         }
74         catch (FileNotFoundException ex) {
75             // ignore, see SPR-12999
76         }
77     }
78 
79 }

按照上述配置即可工作,但需要严格注意:

由于使用 LinkedHashMapFormHttpMessageConverter 的原因,会导致:

当某次http请求的参数或者返回值中包含 LinkedHashMap 且其请求的MediaType兼容 MULTIPART_FORM_DATA 时,该次参数或者返回值会错误的被 LinkedHashMapFormHttpMessageConverter 处理,但此时很显然是不正确的处理方式。

对应的办法:避开该情况,或者有更好的办法请务必分享给我,不胜感激。

 

以上是关于SpringCloud+Feign环境下文件上传与form-data同时存在的解决办法的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud---Feign上传下载详解

spring cloud实战与思考 微服务之间通过fiegn上传多个文件1

springcloud 使用feign 上传图片

Spring Cloud下使用Feign Form实现微服务之间的文件上传

Spring Cloud Feign的文件上传实现

SpringCloud http客户端Feign -- 自定义Feign的配置(一般情况下需要配置的是日志级别)Feign的配置优化