You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 17 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. # spring-boot-demo-upload
  2. > 本 demo 演示了 Spring Boot 如何实现本地文件上传以及如何上传文件至七牛云平台。前端使用 vue 和 iview 实现上传页面。
  3. ## pom.xml
  4. ```xml
  5. <?xml version="1.0" encoding="UTF-8"?>
  6. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  7. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  8. <modelVersion>4.0.0</modelVersion>
  9. <artifactId>spring-boot-demo-upload</artifactId>
  10. <version>1.0.0-SNAPSHOT</version>
  11. <packaging>jar</packaging>
  12. <name>spring-boot-demo-upload</name>
  13. <description>Demo project for Spring Boot</description>
  14. <parent>
  15. <groupId>com.xkcoding</groupId>
  16. <artifactId>spring-boot-demo</artifactId>
  17. <version>1.0.0-SNAPSHOT</version>
  18. </parent>
  19. <properties>
  20. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  21. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  22. <java.version>1.8</java.version>
  23. </properties>
  24. <dependencies>
  25. <dependency>
  26. <groupId>org.projectlombok</groupId>
  27. <artifactId>lombok</artifactId>
  28. <optional>true</optional>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-web</artifactId>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  37. </dependency>
  38. <dependency>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-starter-test</artifactId>
  41. <scope>test</scope>
  42. </dependency>
  43. <dependency>
  44. <groupId>cn.hutool</groupId>
  45. <artifactId>hutool-all</artifactId>
  46. </dependency>
  47. <dependency>
  48. <groupId>com.qiniu</groupId>
  49. <artifactId>qiniu-java-sdk</artifactId>
  50. <version>[7.2.0, 7.2.99]</version>
  51. </dependency>
  52. </dependencies>
  53. <build>
  54. <finalName>spring-boot-demo-upload</finalName>
  55. <plugins>
  56. <plugin>
  57. <groupId>org.springframework.boot</groupId>
  58. <artifactId>spring-boot-maven-plugin</artifactId>
  59. </plugin>
  60. </plugins>
  61. </build>
  62. </project>
  63. ```
  64. ## UploadConfig.java
  65. ```java
  66. /**
  67. * <p>
  68. * 上传配置
  69. * </p>
  70. *
  71. * @author yangkai.shen
  72. * @date Created in 2018-10-23 14:09
  73. */
  74. @Configuration
  75. @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
  76. @ConditionalOnProperty(prefix = "spring.http.multipart", name = "enabled", matchIfMissing = true)
  77. @EnableConfigurationProperties(MultipartProperties.class)
  78. public class UploadConfig {
  79. @Value("${qiniu.accessKey}")
  80. private String accessKey;
  81. @Value("${qiniu.secretKey}")
  82. private String secretKey;
  83. private final MultipartProperties multipartProperties;
  84. @Autowired
  85. public UploadConfig(MultipartProperties multipartProperties) {
  86. this.multipartProperties = multipartProperties;
  87. }
  88. /**
  89. * 上传配置
  90. */
  91. @Bean
  92. @ConditionalOnMissingBean
  93. public MultipartConfigElement multipartConfigElement() {
  94. return this.multipartProperties.createMultipartConfig();
  95. }
  96. /**
  97. * 注册解析器
  98. */
  99. @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
  100. @ConditionalOnMissingBean(MultipartResolver.class)
  101. public StandardServletMultipartResolver multipartResolver() {
  102. StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
  103. multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
  104. return multipartResolver;
  105. }
  106. /**
  107. * 华东机房
  108. */
  109. @Bean
  110. public com.qiniu.storage.Configuration qiniuConfig() {
  111. return new com.qiniu.storage.Configuration(Zone.zone0());
  112. }
  113. /**
  114. * 构建一个七牛上传工具实例
  115. */
  116. @Bean
  117. public UploadManager uploadManager() {
  118. return new UploadManager(qiniuConfig());
  119. }
  120. /**
  121. * 认证信息实例
  122. */
  123. @Bean
  124. public Auth auth() {
  125. return Auth.create(accessKey, secretKey);
  126. }
  127. /**
  128. * 构建七牛空间管理实例
  129. */
  130. @Bean
  131. public BucketManager bucketManager() {
  132. return new BucketManager(auth(), qiniuConfig());
  133. }
  134. }
  135. ```
  136. ## UploadController.java
  137. ```java
  138. /**
  139. * <p>
  140. * 文件上传 Controller
  141. * </p>
  142. *
  143. * @author yangkai.shen
  144. * @date Created in 2018-11-06 16:33
  145. */
  146. @RestController
  147. @Slf4j
  148. @RequestMapping("/upload")
  149. public class UploadController {
  150. @Value("${spring.servlet.multipart.location}")
  151. private String fileTempPath;
  152. @Value("${qiniu.prefix}")
  153. private String prefix;
  154. private final IQiNiuService qiNiuService;
  155. @Autowired
  156. public UploadController(IQiNiuService qiNiuService) {
  157. this.qiNiuService = qiNiuService;
  158. }
  159. @PostMapping(value = "/local", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  160. public Dict local(@RequestParam("file") MultipartFile file) {
  161. if (file.isEmpty()) {
  162. return Dict.create().set("code", 400).set("message", "文件内容为空");
  163. }
  164. String fileName = file.getOriginalFilename();
  165. String rawFileName = StrUtil.subBefore(fileName, ".", true);
  166. String fileType = StrUtil.subAfter(fileName, ".", true);
  167. String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType;
  168. try {
  169. file.transferTo(new File(localFilePath));
  170. } catch (IOException e) {
  171. log.error("【文件上传至本地】失败,绝对路径:{}", localFilePath);
  172. return Dict.create().set("code", 500).set("message", "文件上传失败");
  173. }
  174. log.info("【文件上传至本地】绝对路径:{}", localFilePath);
  175. return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", localFilePath));
  176. }
  177. @PostMapping(value = "/yun", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  178. public Dict yun(@RequestParam("file") MultipartFile file) {
  179. if (file.isEmpty()) {
  180. return Dict.create().set("code", 400).set("message", "文件内容为空");
  181. }
  182. String fileName = file.getOriginalFilename();
  183. String rawFileName = StrUtil.subBefore(fileName, ".", true);
  184. String fileType = StrUtil.subAfter(fileName, ".", true);
  185. String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType;
  186. try {
  187. file.transferTo(new File(localFilePath));
  188. Response response = qiNiuService.uploadFile(new File(localFilePath));
  189. if (response.isOK()) {
  190. JSONObject jsonObject = JSONUtil.parseObj(response.bodyString());
  191. String yunFileName = jsonObject.getStr("key");
  192. String yunFilePath = StrUtil.appendIfMissing(prefix, "/") + yunFileName;
  193. FileUtil.del(new File(localFilePath));
  194. log.info("【文件上传至七牛云】绝对路径:{}", yunFilePath);
  195. return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", yunFileName).set("filePath", yunFilePath));
  196. } else {
  197. log.error("【文件上传至七牛云】失败,{}", JSONUtil.toJsonStr(response));
  198. FileUtil.del(new File(localFilePath));
  199. return Dict.create().set("code", 500).set("message", "文件上传失败");
  200. }
  201. } catch (IOException e) {
  202. log.error("【文件上传至七牛云】失败,绝对路径:{}", localFilePath);
  203. return Dict.create().set("code", 500).set("message", "文件上传失败");
  204. }
  205. }
  206. }
  207. ```
  208. ## QiNiuServiceImpl.java
  209. ```java
  210. /**
  211. * <p>
  212. * 七牛云上传Service
  213. * </p>
  214. *
  215. * @author yangkai.shen
  216. * @date Created in 2018-11-06 17:22
  217. */
  218. @Service
  219. @Slf4j
  220. public class QiNiuServiceImpl implements IQiNiuService, InitializingBean {
  221. private final UploadManager uploadManager;
  222. private final Auth auth;
  223. @Value("${qiniu.bucket}")
  224. private String bucket;
  225. private StringMap putPolicy;
  226. @Autowired
  227. public QiNiuServiceImpl(UploadManager uploadManager, Auth auth) {
  228. this.uploadManager = uploadManager;
  229. this.auth = auth;
  230. }
  231. /**
  232. * 七牛云上传文件
  233. *
  234. * @param file 文件
  235. * @return 七牛上传Response
  236. * @throws QiniuException 七牛异常
  237. */
  238. @Override
  239. public Response uploadFile(File file) throws QiniuException {
  240. Response response = this.uploadManager.put(file, file.getName(), getUploadToken());
  241. int retry = 0;
  242. while (response.needRetry() && retry < 3) {
  243. response = this.uploadManager.put(file, file.getName(), getUploadToken());
  244. retry++;
  245. }
  246. return response;
  247. }
  248. @Override
  249. public void afterPropertiesSet() {
  250. this.putPolicy = new StringMap();
  251. putPolicy.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"width\":$(imageInfo.width), \"height\":${imageInfo.height}}");
  252. }
  253. /**
  254. * 获取上传凭证
  255. *
  256. * @return 上传凭证
  257. */
  258. private String getUploadToken() {
  259. return this.auth.uploadToken(bucket, null, 3600, putPolicy);
  260. }
  261. }
  262. ```
  263. ## index.html
  264. ```html
  265. <!doctype html>
  266. <html lang="en">
  267. <head>
  268. <meta charset="UTF-8">
  269. <meta name="viewport"
  270. content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  271. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  272. <title>spring-boot-demo-upload</title>
  273. <!-- import Vue.js -->
  274. <script src="https://cdn.bootcss.com/vue/2.5.17/vue.min.js"></script>
  275. <!-- import stylesheet -->
  276. <link href="https://cdn.bootcss.com/iview/3.1.4/styles/iview.css" rel="stylesheet">
  277. <!-- import iView -->
  278. <script src="https://cdn.bootcss.com/iview/3.1.4/iview.min.js"></script>
  279. </head>
  280. <body>
  281. <div id="app">
  282. <Row :gutter="16" style="background:#eee;padding:10%">
  283. <i-col span="12">
  284. <Card style="height: 300px">
  285. <p slot="title">
  286. <Icon type="ios-cloud-upload"></Icon>
  287. 本地上传
  288. </p>
  289. <div style="text-align: center;">
  290. <Upload
  291. :before-upload="handleLocalUpload"
  292. action="/demo/upload/local"
  293. ref="localUploadRef"
  294. :on-success="handleLocalSuccess"
  295. :on-error="handleLocalError"
  296. >
  297. <i-button icon="ios-cloud-upload-outline">选择文件</i-button>
  298. </Upload>
  299. <i-button
  300. type="primary"
  301. @click="localUpload"
  302. :loading="local.loadingStatus"
  303. :disabled="!local.file">
  304. {{ local.loadingStatus ? '本地文件上传中' : '本地上传' }}
  305. </i-button>
  306. </div>
  307. <div>
  308. <div v-if="local.log.status != 0">状态:{{local.log.message}}</div>
  309. <div v-if="local.log.status === 200">文件名:{{local.log.fileName}}</div>
  310. <div v-if="local.log.status === 200">文件路径:{{local.log.filePath}}</div>
  311. </div>
  312. </Card>
  313. </i-col>
  314. <i-col span="12">
  315. <Card style="height: 300px;">
  316. <p slot="title">
  317. <Icon type="md-cloud-upload"></Icon>
  318. 七牛云上传
  319. </p>
  320. <div style="text-align: center;">
  321. <Upload
  322. :before-upload="handleYunUpload"
  323. action="/demo/upload/yun"
  324. ref="yunUploadRef"
  325. :on-success="handleYunSuccess"
  326. :on-error="handleYunError"
  327. >
  328. <i-button icon="ios-cloud-upload-outline">选择文件</i-button>
  329. </Upload>
  330. <i-button
  331. type="primary"
  332. @click="yunUpload"
  333. :loading="yun.loadingStatus"
  334. :disabled="!yun.file">
  335. {{ yun.loadingStatus ? '七牛云文件上传中' : '七牛云上传' }}
  336. </i-button>
  337. </div>
  338. <div>
  339. <div v-if="yun.log.status != 0">状态:{{yun.log.message}}</div>
  340. <div v-if="yun.log.status === 200">文件名:{{yun.log.fileName}}</div>
  341. <div v-if="yun.log.status === 200">文件路径:{{yun.log.filePath}}</div>
  342. </div>
  343. </Card>
  344. </i-col>
  345. </Row>
  346. </div>
  347. <script>
  348. new Vue({
  349. el: '#app',
  350. data: {
  351. local: {
  352. // 选择文件后,将 beforeUpload 返回的 file 保存在这里,后面会用到
  353. file: null,
  354. // 标记上传状态
  355. loadingStatus: false,
  356. log: {
  357. status: 0,
  358. message: "",
  359. fileName: "",
  360. filePath: ""
  361. }
  362. },
  363. yun: {
  364. // 选择文件后,将 beforeUpload 返回的 file 保存在这里,后面会用到
  365. file: null,
  366. // 标记上传状态
  367. loadingStatus: false,
  368. log: {
  369. status: 0,
  370. message: "",
  371. fileName: "",
  372. filePath: ""
  373. }
  374. }
  375. },
  376. methods: {
  377. // beforeUpload 在返回 false 或 Promise 时,会停止自动上传,这里我们将选择好的文件 file 保存在 data里,并 return false
  378. handleLocalUpload(file) {
  379. this.local.file = file;
  380. return false;
  381. },
  382. // 这里是手动上传,通过 $refs 获取到 Upload 实例,然后调用私有方法 .post(),把保存在 data 里的 file 上传。
  383. // iView 的 Upload 组件在调用 .post() 方法时,就会继续上传了。
  384. localUpload() {
  385. this.local.loadingStatus = true; // 标记上传状态
  386. this.$refs.localUploadRef.post(this.local.file);
  387. },
  388. // 上传成功后,清空 data 里的 file,并修改上传状态
  389. handleLocalSuccess(response) {
  390. this.local.file = null;
  391. this.local.loadingStatus = false;
  392. if (response.code === 200) {
  393. this.$Message.success(response.message);
  394. this.local.log.status = response.code;
  395. this.local.log.message = response.message;
  396. this.local.log.fileName = response.data.fileName;
  397. this.local.log.filePath = response.data.filePath;
  398. this.$refs.localUploadRef.clearFiles();
  399. } else {
  400. this.$Message.error(response.message);
  401. this.local.log.status = response.code;
  402. this.local.log.message = response.message;
  403. }
  404. },
  405. // 上传失败后,清空 data 里的 file,并修改上传状态
  406. handleLocalError() {
  407. this.local.file = null;
  408. this.local.loadingStatus = false;
  409. this.$Message.error('上传失败');
  410. },
  411. // beforeUpload 在返回 false 或 Promise 时,会停止自动上传,这里我们将选择好的文件 file 保存在 data里,并 return false
  412. handleYunUpload(file) {
  413. this.yun.file = file;
  414. return false;
  415. },
  416. // 这里是手动上传,通过 $refs 获取到 Upload 实例,然后调用私有方法 .post(),把保存在 data 里的 file 上传。
  417. // iView 的 Upload 组件在调用 .post() 方法时,就会继续上传了。
  418. yunUpload() {
  419. this.yun.loadingStatus = true; // 标记上传状态
  420. this.$refs.yunUploadRef.post(this.yun.file);
  421. },
  422. // 上传成功后,清空 data 里的 file,并修改上传状态
  423. handleYunSuccess(response) {
  424. this.yun.file = null;
  425. this.yun.loadingStatus = false;
  426. if (response.code === 200) {
  427. this.$Message.success(response.message);
  428. this.yun.log.status = response.code;
  429. this.yun.log.message = response.message;
  430. this.yun.log.fileName = response.data.fileName;
  431. this.yun.log.filePath = response.data.filePath;
  432. this.$refs.yunUploadRef.clearFiles();
  433. } else {
  434. this.$Message.error(response.message);
  435. this.yun.log.status = response.code;
  436. this.yun.log.message = response.message;
  437. }
  438. },
  439. // 上传失败后,清空 data 里的 file,并修改上传状态
  440. handleYunError() {
  441. this.yun.file = null;
  442. this.yun.loadingStatus = false;
  443. this.$Message.error('上传失败');
  444. }
  445. }
  446. })
  447. </script>
  448. </body>
  449. </html>
  450. ```
  451. ## 参考
  452. 1. Spring 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#howto-multipart-file-upload-configuration
  453. 2. 七牛云官方文档:https://developer.qiniu.com/kodo/sdk/1239/java#5