resttemplate-bytearray-upload.html 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta name="generator" content="Asciidoctor 2.0.15">
  8. <meta name="author" content="pxzxj, pudge.zxj@gmail.com, 2024/02/20">
  9. <title>RestTemplate使用字节数组实现文件上传</title>
  10. <link rel="stylesheet" href="css/site.css">
  11. <link href="css/custom.css" rel="stylesheet">
  12. <script src="js/setup.js"></script><script defer src="js/site.js"></script>
  13. </head>
  14. <body class="article toc2 toc-left"><div id="banner-container" class="container" role="banner">
  15. <div id="banner" class="contained" role="banner">
  16. <div id="switch-theme">
  17. <input type="checkbox" id="switch-theme-checkbox" />
  18. <label for="switch-theme-checkbox">Dark Theme</label>
  19. </div>
  20. </div>
  21. </div>
  22. <div id="tocbar-container" class="container" role="navigation">
  23. <div id="tocbar" class="contained" role="navigation">
  24. <button id="toggle-toc"></button>
  25. </div>
  26. </div>
  27. <div id="main-container" class="container">
  28. <div id="main" class="contained">
  29. <div id="doc" class="doc">
  30. <div id="header">
  31. <h1>RestTemplate使用字节数组实现文件上传</h1>
  32. <div class="details">
  33. <span id="author" class="author">pxzxj</span><br>
  34. <span id="author2" class="author">pudge.zxj@gmail.com</span><br>
  35. <span id="author3" class="author">2024/02/20</span><br>
  36. </div>
  37. <div id="toc" class="toc2">
  38. <div id="toctitle">Table of Contents</div>
  39. <span id="back-to-index"><a href="index.html">Back to index</a></span><ul class="sectlevel1">
  40. <li><a href="#_工程准备">1. 工程准备</a></li>
  41. <li><a href="#file-upload">2. 上传本地文件</a></li>
  42. <li><a href="#_上传字节数组">3. 上传字节数组</a></li>
  43. </ul>
  44. </div>
  45. </div>
  46. <div id="content">
  47. <div id="preamble">
  48. <div class="sectionbody">
  49. <div class="paragraph">
  50. <p><code>RestTemplate</code> 是Spring提供的一个执行Http请求的工具类,使用它可以发送文件上传请求但必须依赖本地文件,本文介绍一种直接使用字节数组创建文件上传请求的方法</p>
  51. </div>
  52. </div>
  53. </div>
  54. <div class="sect1">
  55. <h2 id="_工程准备"><a class="anchor" href="#_工程准备"></a>1. 工程准备</h2>
  56. <div class="sectionbody">
  57. <div class="paragraph">
  58. <p>假设存在如下服务端代码用于接收上传的文件</p>
  59. </div>
  60. <div class="listingblock">
  61. <div class="content">
  62. <pre class="highlight"><code class="language-java" data-lang="java"> @PostMapping("/create")
  63. public void create(String pathname, @RequestParam("file") MultipartFile multipartFile) {
  64. //...
  65. }
  66. </code></pre>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. <div class="sect1">
  72. <h2 id="file-upload"><a class="anchor" href="#file-upload"></a>2. 上传本地文件</h2>
  73. <div class="sectionbody">
  74. <div class="paragraph">
  75. <p>在网络上搜索使用RestTemplate上传文件都会得到类似如下代码的答案,其中的关键点一是使用 <code>MultiValueMap</code>,二是使用 <code>FileSystemResource</code></p>
  76. </div>
  77. <div class="listingblock">
  78. <div class="content">
  79. <pre class="highlight"><code class="language-java" data-lang="java"> MultiValueMap&lt;String, Object&gt; multiValueMap = new inkedMultiValueMap&lt;&gt;();
  80. File file = new File(filepath);
  81. multiValueMap.add("file", new FileSystemResource(file));
  82. multiValueMap.add("pathname", pathname);
  83. restTemplate.postForEntity(url, new HttpEntity&lt;&gt;(multiValueMap), String.class);
  84. </code></pre>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. <div class="sect1">
  90. <h2 id="_上传字节数组"><a class="anchor" href="#_上传字节数组"></a>3. 上传字节数组</h2>
  91. <div class="sectionbody">
  92. <div class="paragraph">
  93. <p>在很多场景下上传的内容不是来自于本地文件,可能是代码中生成或者从其它服务读取到,不论来源如何都可以转换为字节数组,如果按照 <a href="#file-upload">上面</a>的方案就需要先将字节数组存储为本地文件再次读取为 <code>FileSystemResource</code> 对象,在已经拥有文件内容数组的情况下再把它保存为文件再读取出来显然是多此一举,那么能不能直接使用已有的字节数组来创建文件上传请求呢?刚好存在一个类似的 <code>ByteArrayResource</code>,是不是只要把 <code>new FileSystemResource(file)</code> 替换为 <code>new ByteArrayResource(bytes)</code> 就可以了呢?</p>
  94. </div>
  95. <div class="paragraph">
  96. <p>此处省去调试的过程直接介绍最终的结论,直接替换是不行的,<code>FileSystemResource</code> 和 <code>ByteArrayResource</code> 都实现了 <code>org.springframework.core.io.Resource</code> 接口,而 <code>Resource</code> 接口有一个方法 <code>@Nullable String getFilename();</code>,<code>FileSystemResource</code> 接口的次方法会返回底层的文件名,而 <code>ByteArrayResource</code> 仅返回null</p>
  97. </div>
  98. <div class="paragraph">
  99. <p><code>RestTemplate</code> 执行文件上传请求时会调用 <code>FormHttpMessageConverter.writePart</code> 方法来构建请求信息,方法内部会调用 <code>Resource.getFilename()</code> 来生成 <code>Content-Disposition</code> 请求头,<code>getFilename()</code> 方法返回null时 <code>Content-Disposition</code> 请求头就不包含 <code>filename</code> 信息,进一步服务端不会将其识别为文件</p>
  100. </div>
  101. <div class="imageblock">
  102. <div class="content">
  103. <img src="images/form-converter.png" alt="form converter">
  104. </div>
  105. </div>
  106. <div class="paragraph">
  107. <p>明确了不能直接替换的原因后解决方案也很简单,只需要让 <code>ByteArrayResource</code> 的 <code>getFilename()</code> 方法返回一个非null的值即可</p>
  108. </div>
  109. <div class="listingblock">
  110. <div class="title">ByteArrayWithNameResource.java</div>
  111. <div class="content">
  112. <pre class="highlight"><code class="language-java" data-lang="java">class ByteArrayWithNameResource extends ByteArrayResource {
  113. private String name = "useless";
  114. ByteArrayWithNameResource(byte[] byteArray) {
  115. super(byteArray);
  116. }
  117. ByteArrayWithNameResource(byte[] byteArray, String description) {
  118. super(byteArray, description);
  119. }
  120. ByteArrayWithNameResource(String name, byte[] byteArray) {
  121. super(byteArray);
  122. this.name = name;
  123. }
  124. ByteArrayWithNameResource(String name, byte[] byteArray, String description) {
  125. super(byteArray, description);
  126. this.name = name;
  127. }
  128. @Override
  129. public String getFilename() {
  130. return name;
  131. }
  132. }
  133. </code></pre>
  134. </div>
  135. </div>
  136. <div class="paragraph">
  137. <p><code>MultiValueMap</code> 中添加 <code>ByteArrayWithNameResource</code> 对象即可</p>
  138. </div>
  139. <div class="listingblock">
  140. <div class="content">
  141. <pre class="highlight"><code class="language-java" data-lang="java">multiValueMap.add("file", new ByteArrayWithNameResource("my.txt", bytes));
  142. </code></pre>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. <div id="footer">
  149. <div id="footer-text">
  150. Last updated 2024-03-18 05:44:42 UTC
  151. </div>
  152. </div>
  153. <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"></script>
  154. <script>
  155. if (!hljs.initHighlighting.called) {
  156. hljs.initHighlighting.called = true
  157. ;[].slice.call(document.querySelectorAll('pre.highlight > code')).forEach(function (el) { hljs.highlightBlock(el) })
  158. }
  159. </script>
  160. <script src="https://utteranc.es/client.js"
  161. repo="pxzxj/articles"
  162. issue-term="title"
  163. label="utteranc"
  164. theme="github-light"
  165. crossorigin="anonymous"
  166. async>
  167. </script>
  168. </div>
  169. </div>
  170. </div>
  171. </body>
  172. </html>