书接上文。

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

主要导致的后果是:

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

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

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

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同时存在的解决办法(2)的更多相关文章

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

    最近项目转型使用SpringCloud框架下的微服务架构,各微服务之间使用Feign进行调用.期间,发现若被调用方法涉及到文件上传且仅存在单个文件时,一切正常,代码片段如下: @RequestMapp ...

  2. 不同环境下文件上传Uncaught SyntaxError: Unexpected end of input

    很奇怪的问题,相同的代码和相同的数据,在两台linux服务器上执行文件上传,一台正常上传,一台在ftl页面 报:Uncaught SyntaxError: Unexpected end of inpu ...

  3. ie下文件上传无权访问的问题

    最近项目遇到个问题,ie下文件上传无权访问,在网上找了很久才找到答案,原来是因为ie下不能用js触发input=file的点击事件,必须手动点击才可以.

  4. [原创]Struts2奇葩环境任意文件上传工具(解决菜刀无法传文件或上传乱码等问题)

    上面这问题问得好  1 不知道大家有没碰到有些Strus2站点  上传JSP后访问404 或者503    注意我说的是404或503不是403(要是403换个css/img等目录或许可以)    但 ...

  5. Web下文件上传下载的路径问题

    工程结构

  6. linux 下文件上传的两种工具(XFTP5和Putty之pscp)方式

    一.使用XFTP(,需要先在LINUX上安装启用FTP服务) 然后,在WINDOWS上启动XFPT6客户端,将下载的文件上传至LINUX 指定目录: 二.使用PUTTY软件安装目录下的PSCP命令 1 ...

  7. asp.net mvc下文件上传

    典型的文件上传表单 <form action="/File" enctype="multipart/form-data" method="pos ...

  8. SpringBoot下文件上传与下载的实现

    原文:http://blog.csdn.net/colton_null/article/details/76696674 SpringBoot后台如何实现文件上传下载? 最近做的一个项目涉及到文件上传 ...

  9. IE下文件上传, SCRIPT5: 拒绝访问 问题

    最近遇到一个比较奇葩的问题,某些ie浏览器在页面中上传文件时,无法上传.查看控制台报错: SCRIPT5: 拒绝访问. jquery-3.2.1.min.js, 行4 字符5725 .并且我的最新版I ...

随机推荐

  1. OpenCL科普及在ubuntu 16.04 LTS上的安装

    OpenCL(Open Computing Language,开放计算语言)是一个为异构平台编写程序的框架,此异构平台可由CPU.GPU.DSP.FPGA或其他类型的处理器與硬體加速器所组成.Open ...

  2. 【原创】InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times

    一.背景 基于SpringBoot 构建了一个http文件下载服务,检查tomcat access 发现偶尔出现500 状态码的请求,检查抛出的异常堆栈 2019-03-20 10:03:14,273 ...

  3. [视频]K8飞刀 shellcode loader演示教程

    [视频]K8飞刀 shellcode loader演示教程 https://pan.baidu.com/s/1eQ77lPw

  4. 什么是js的严格模式

    设立严格模式的原因: - 消除Javascript语法的一些不合理.不严谨之处,减少一些怪异行为; - 消除代码运行的一些不安全之处,保证代码运行的安全: - 提高编译器效率,增加运行速度: - 为未 ...

  5. html css 其他常用 onclick跳转

    opacity: 0.5. 0-1 透明度 cursor: pointer;手指 clear:both 清楚浮动 我是医生不是人 文本内容超出框word-wrap:break-word; word-b ...

  6. 如何将打包好的文件做成一个APP

    本文主要是用来简短的对做成一个APP进行说明,内容可能不是多详细,但会给出具体思路.(仅供参考) 因为各种打包和生成APP的方式多样,今天这里仅仅对用Hbuilder打包进行说明. 1.首先当然需要一 ...

  7. redhat 下搭建网站

    1.修改yum源 把iso重新挂载到/media路径下,media是个只读的文件 vi  /etc/yum.repos.d/rhel-source.repo            //编辑yum源文件 ...

  8. VirtualBox centos7扩容

    有时候扩容还真不如重新建立一个大硬盘的系统,但是如果你安装了好多东西的话,那还是来扩容一下吧. 查看磁盘格式           在virtualBox中右键点击虚拟机->设置->存储,如 ...

  9. 深入理解RDD原理

    首先我们来了解一些Spark的优势:1.每一个作业独立调度,可以把所有的作业做一个图进行调度,各个作业之间相互依赖,在调度过程中一起调度,速度快.2.所有过程都基于内存,所以通常也将Spark称作是基 ...

  10. CSS学习笔记11 CSS背景

    background-color:背景色 前面我们经常用background-color这个属性来设置元素的背景色,例如下面这条css可将段落的背景色设置为灰色 p {background-color ...