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同时存在的解决办法的主要内容,如果未能解决你的问题,请参考以下文章
spring cloud实战与思考 微服务之间通过fiegn上传多个文件1
Spring Cloud下使用Feign Form实现微服务之间的文件上传
SpringCloud http客户端Feign -- 自定义Feign的配置(一般情况下需要配置的是日志级别)Feign的配置优化