| @@ -0,0 +1,519 @@ | |||||
| # spring-boot-demo-upload | |||||
| > 本 demo 演示了 Spring Boot 如何实现本地文件上传以及如何上传文件至七牛云平台。前端使用 vue 和 iview 实现上传页面。 | |||||
| ## pom.xml | |||||
| ```xml | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||||
| <modelVersion>4.0.0</modelVersion> | |||||
| <artifactId>spring-boot-demo-upload</artifactId> | |||||
| <version>1.0.0-SNAPSHOT</version> | |||||
| <packaging>jar</packaging> | |||||
| <name>spring-boot-demo-upload</name> | |||||
| <description>Demo project for Spring Boot</description> | |||||
| <parent> | |||||
| <groupId>com.xkcoding</groupId> | |||||
| <artifactId>spring-boot-demo</artifactId> | |||||
| <version>1.0.0-SNAPSHOT</version> | |||||
| </parent> | |||||
| <properties> | |||||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||||
| <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | |||||
| <java.version>1.8</java.version> | |||||
| </properties> | |||||
| <dependencies> | |||||
| <dependency> | |||||
| <groupId>org.projectlombok</groupId> | |||||
| <artifactId>lombok</artifactId> | |||||
| <optional>true</optional> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>org.springframework.boot</groupId> | |||||
| <artifactId>spring-boot-starter-web</artifactId> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>org.springframework.boot</groupId> | |||||
| <artifactId>spring-boot-starter-thymeleaf</artifactId> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>org.springframework.boot</groupId> | |||||
| <artifactId>spring-boot-starter-test</artifactId> | |||||
| <scope>test</scope> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>cn.hutool</groupId> | |||||
| <artifactId>hutool-all</artifactId> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>com.qiniu</groupId> | |||||
| <artifactId>qiniu-java-sdk</artifactId> | |||||
| <version>[7.2.0, 7.2.99]</version> | |||||
| </dependency> | |||||
| </dependencies> | |||||
| <build> | |||||
| <finalName>spring-boot-demo-upload</finalName> | |||||
| <plugins> | |||||
| <plugin> | |||||
| <groupId>org.springframework.boot</groupId> | |||||
| <artifactId>spring-boot-maven-plugin</artifactId> | |||||
| </plugin> | |||||
| </plugins> | |||||
| </build> | |||||
| </project> | |||||
| ``` | |||||
| ## UploadConfig.java | |||||
| ```java | |||||
| /** | |||||
| * <p> | |||||
| * 上传配置 | |||||
| * </p> | |||||
| * | |||||
| * @package: com.xkcoding.upload.config | |||||
| * @description: 上传配置 | |||||
| * @author: yangkai.shen | |||||
| * @date: Created in 2018/10/23 14:09 | |||||
| * @copyright: Copyright (c) 2018 | |||||
| * @version: V1.0 | |||||
| * @modified: yangkai.shen | |||||
| */ | |||||
| @Configuration | |||||
| @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class}) | |||||
| @ConditionalOnProperty(prefix = "spring.http.multipart", name = "enabled", matchIfMissing = true) | |||||
| @EnableConfigurationProperties(MultipartProperties.class) | |||||
| public class UploadConfig { | |||||
| @Value("${qiniu.accessKey}") | |||||
| private String accessKey; | |||||
| @Value("${qiniu.secretKey}") | |||||
| private String secretKey; | |||||
| private final MultipartProperties multipartProperties; | |||||
| @Autowired | |||||
| public UploadConfig(MultipartProperties multipartProperties) { | |||||
| this.multipartProperties = multipartProperties; | |||||
| } | |||||
| /** | |||||
| * 上传配置 | |||||
| */ | |||||
| @Bean | |||||
| @ConditionalOnMissingBean | |||||
| public MultipartConfigElement multipartConfigElement() { | |||||
| return this.multipartProperties.createMultipartConfig(); | |||||
| } | |||||
| /** | |||||
| * 注册解析器 | |||||
| */ | |||||
| @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) | |||||
| @ConditionalOnMissingBean(MultipartResolver.class) | |||||
| public StandardServletMultipartResolver multipartResolver() { | |||||
| StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); | |||||
| multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); | |||||
| return multipartResolver; | |||||
| } | |||||
| /** | |||||
| * 华东机房 | |||||
| */ | |||||
| @Bean | |||||
| public com.qiniu.storage.Configuration qiniuConfig() { | |||||
| return new com.qiniu.storage.Configuration(Zone.zone0()); | |||||
| } | |||||
| /** | |||||
| * 构建一个七牛上传工具实例 | |||||
| */ | |||||
| @Bean | |||||
| public UploadManager uploadManager() { | |||||
| return new UploadManager(qiniuConfig()); | |||||
| } | |||||
| /** | |||||
| * 认证信息实例 | |||||
| */ | |||||
| @Bean | |||||
| public Auth auth() { | |||||
| return Auth.create(accessKey, secretKey); | |||||
| } | |||||
| /** | |||||
| * 构建七牛空间管理实例 | |||||
| */ | |||||
| @Bean | |||||
| public BucketManager bucketManager() { | |||||
| return new BucketManager(auth(), qiniuConfig()); | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## UploadController.java | |||||
| ```java | |||||
| /** | |||||
| * <p> | |||||
| * 文件上传 Controller | |||||
| * </p> | |||||
| * | |||||
| * @package: com.xkcoding.upload.controller | |||||
| * @description: 文件上传 Controller | |||||
| * @author: yangkai.shen | |||||
| * @date: Created in 2018/11/6 16:33 | |||||
| * @copyright: Copyright (c) 2018 | |||||
| * @version: V1.0 | |||||
| * @modified: yangkai.shen | |||||
| */ | |||||
| @RestController | |||||
| @Slf4j | |||||
| @RequestMapping("/upload") | |||||
| public class UploadController { | |||||
| @Value("${spring.servlet.multipart.location}") | |||||
| private String fileTempPath; | |||||
| @Value("${qiniu.prefix}") | |||||
| private String prefix; | |||||
| private final IQiNiuService qiNiuService; | |||||
| @Autowired | |||||
| public UploadController(IQiNiuService qiNiuService) { | |||||
| this.qiNiuService = qiNiuService; | |||||
| } | |||||
| @PostMapping(value = "/local", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | |||||
| public Dict local(@RequestParam("file") MultipartFile file) { | |||||
| if (file.isEmpty()) { | |||||
| return Dict.create().set("code", 400).set("message", "文件内容为空"); | |||||
| } | |||||
| String fileName = file.getOriginalFilename(); | |||||
| String rawFileName = StrUtil.subBefore(fileName, ".", true); | |||||
| String fileType = StrUtil.subAfter(fileName, ".", true); | |||||
| String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType; | |||||
| try { | |||||
| file.transferTo(new File(localFilePath)); | |||||
| } catch (IOException e) { | |||||
| log.error("【文件上传至本地】失败,绝对路径:{}", localFilePath); | |||||
| return Dict.create().set("code", 500).set("message", "文件上传失败"); | |||||
| } | |||||
| log.info("【文件上传至本地】绝对路径:{}", localFilePath); | |||||
| return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", localFilePath)); | |||||
| } | |||||
| @PostMapping(value = "/yun", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | |||||
| public Dict yun(@RequestParam("file") MultipartFile file) { | |||||
| if (file.isEmpty()) { | |||||
| return Dict.create().set("code", 400).set("message", "文件内容为空"); | |||||
| } | |||||
| String fileName = file.getOriginalFilename(); | |||||
| String rawFileName = StrUtil.subBefore(fileName, ".", true); | |||||
| String fileType = StrUtil.subAfter(fileName, ".", true); | |||||
| String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType; | |||||
| try { | |||||
| file.transferTo(new File(localFilePath)); | |||||
| Response response = qiNiuService.uploadFile(new File(localFilePath)); | |||||
| if (response.isOK()) { | |||||
| JSONObject jsonObject = JSONUtil.parseObj(response.bodyString()); | |||||
| String yunFileName = jsonObject.getStr("key"); | |||||
| String yunFilePath = StrUtil.appendIfMissing(prefix, "/") + yunFileName; | |||||
| FileUtil.del(new File(localFilePath)); | |||||
| log.info("【文件上传至七牛云】绝对路径:{}", yunFilePath); | |||||
| return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", yunFileName).set("filePath", yunFilePath)); | |||||
| } else { | |||||
| log.error("【文件上传至七牛云】失败,{}", JSONUtil.toJsonStr(response)); | |||||
| FileUtil.del(new File(localFilePath)); | |||||
| return Dict.create().set("code", 500).set("message", "文件上传失败"); | |||||
| } | |||||
| } catch (IOException e) { | |||||
| log.error("【文件上传至七牛云】失败,绝对路径:{}", localFilePath); | |||||
| return Dict.create().set("code", 500).set("message", "文件上传失败"); | |||||
| } | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## QiNiuServiceImpl.java | |||||
| ```java | |||||
| /** | |||||
| * <p> | |||||
| * 七牛云上传Service | |||||
| * </p> | |||||
| * | |||||
| * @package: com.xkcoding.upload.service.impl | |||||
| * @description: 七牛云上传Service | |||||
| * @author: yangkai.shen | |||||
| * @date: Created in 2018/11/6 17:22 | |||||
| * @copyright: Copyright (c) 2018 | |||||
| * @version: V1.0 | |||||
| * @modified: yangkai.shen | |||||
| */ | |||||
| @Service | |||||
| @Slf4j | |||||
| public class QiNiuServiceImpl implements IQiNiuService, InitializingBean { | |||||
| private final UploadManager uploadManager; | |||||
| private final Auth auth; | |||||
| @Value("${qiniu.bucket}") | |||||
| private String bucket; | |||||
| private StringMap putPolicy; | |||||
| @Autowired | |||||
| public QiNiuServiceImpl(UploadManager uploadManager, Auth auth) { | |||||
| this.uploadManager = uploadManager; | |||||
| this.auth = auth; | |||||
| } | |||||
| /** | |||||
| * 七牛云上传文件 | |||||
| * | |||||
| * @param file 文件 | |||||
| * @return 七牛上传Response | |||||
| * @throws QiniuException 七牛异常 | |||||
| */ | |||||
| @Override | |||||
| public Response uploadFile(File file) throws QiniuException { | |||||
| Response response = this.uploadManager.put(file, file.getName(), getUploadToken()); | |||||
| int retry = 0; | |||||
| while (response.needRetry() && retry < 3) { | |||||
| response = this.uploadManager.put(file, file.getName(), getUploadToken()); | |||||
| retry++; | |||||
| } | |||||
| return response; | |||||
| } | |||||
| @Override | |||||
| public void afterPropertiesSet() { | |||||
| this.putPolicy = new StringMap(); | |||||
| putPolicy.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"width\":$(imageInfo.width), \"height\":${imageInfo.height}}"); | |||||
| } | |||||
| /** | |||||
| * 获取上传凭证 | |||||
| * | |||||
| * @return 上传凭证 | |||||
| */ | |||||
| private String getUploadToken() { | |||||
| return this.auth.uploadToken(bucket, null, 3600, putPolicy); | |||||
| } | |||||
| } | |||||
| ``` | |||||
| ## index.html | |||||
| ```html | |||||
| <!doctype html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <meta name="viewport" | |||||
| content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> | |||||
| <meta http-equiv="X-UA-Compatible" content="ie=edge"> | |||||
| <title>spring-boot-demo-upload</title> | |||||
| <!-- import Vue.js --> | |||||
| <script src="https://cdn.bootcss.com/vue/2.5.17/vue.min.js"></script> | |||||
| <!-- import stylesheet --> | |||||
| <link href="https://cdn.bootcss.com/iview/3.1.4/styles/iview.css" rel="stylesheet"> | |||||
| <!-- import iView --> | |||||
| <script src="https://cdn.bootcss.com/iview/3.1.4/iview.min.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="app"> | |||||
| <Row :gutter="16" style="background:#eee;padding:10%"> | |||||
| <i-col span="12"> | |||||
| <Card style="height: 300px"> | |||||
| <p slot="title"> | |||||
| <Icon type="ios-cloud-upload"></Icon> | |||||
| 本地上传 | |||||
| </p> | |||||
| <div style="text-align: center;"> | |||||
| <Upload | |||||
| :before-upload="handleLocalUpload" | |||||
| action="/demo/upload/local" | |||||
| ref="localUploadRef" | |||||
| :on-success="handleLocalSuccess" | |||||
| :on-error="handleLocalError" | |||||
| > | |||||
| <i-button icon="ios-cloud-upload-outline">选择文件</i-button> | |||||
| </Upload> | |||||
| <i-button | |||||
| type="primary" | |||||
| @click="localUpload" | |||||
| :loading="local.loadingStatus" | |||||
| :disabled="!local.file"> | |||||
| {{ local.loadingStatus ? '本地文件上传中' : '本地上传' }} | |||||
| </i-button> | |||||
| </div> | |||||
| <div> | |||||
| <div v-if="local.log.status != 0">状态:{{local.log.message}}</div> | |||||
| <div v-if="local.log.status === 200">文件名:{{local.log.fileName}}</div> | |||||
| <div v-if="local.log.status === 200">文件路径:{{local.log.filePath}}</div> | |||||
| </div> | |||||
| </Card> | |||||
| </i-col> | |||||
| <i-col span="12"> | |||||
| <Card style="height: 300px;"> | |||||
| <p slot="title"> | |||||
| <Icon type="md-cloud-upload"></Icon> | |||||
| 七牛云上传 | |||||
| </p> | |||||
| <div style="text-align: center;"> | |||||
| <Upload | |||||
| :before-upload="handleYunUpload" | |||||
| action="/demo/upload/yun" | |||||
| ref="yunUploadRef" | |||||
| :on-success="handleYunSuccess" | |||||
| :on-error="handleYunError" | |||||
| > | |||||
| <i-button icon="ios-cloud-upload-outline">选择文件</i-button> | |||||
| </Upload> | |||||
| <i-button | |||||
| type="primary" | |||||
| @click="yunUpload" | |||||
| :loading="yun.loadingStatus" | |||||
| :disabled="!yun.file"> | |||||
| {{ yun.loadingStatus ? '七牛云文件上传中' : '七牛云上传' }} | |||||
| </i-button> | |||||
| </div> | |||||
| <div> | |||||
| <div v-if="yun.log.status != 0">状态:{{yun.log.message}}</div> | |||||
| <div v-if="yun.log.status === 200">文件名:{{yun.log.fileName}}</div> | |||||
| <div v-if="yun.log.status === 200">文件路径:{{yun.log.filePath}}</div> | |||||
| </div> | |||||
| </Card> | |||||
| </i-col> | |||||
| </Row> | |||||
| </div> | |||||
| <script> | |||||
| new Vue({ | |||||
| el: '#app', | |||||
| data: { | |||||
| local: { | |||||
| // 选择文件后,将 beforeUpload 返回的 file 保存在这里,后面会用到 | |||||
| file: null, | |||||
| // 标记上传状态 | |||||
| loadingStatus: false, | |||||
| log: { | |||||
| status: 0, | |||||
| message: "", | |||||
| fileName: "", | |||||
| filePath: "" | |||||
| } | |||||
| }, | |||||
| yun: { | |||||
| // 选择文件后,将 beforeUpload 返回的 file 保存在这里,后面会用到 | |||||
| file: null, | |||||
| // 标记上传状态 | |||||
| loadingStatus: false, | |||||
| log: { | |||||
| status: 0, | |||||
| message: "", | |||||
| fileName: "", | |||||
| filePath: "" | |||||
| } | |||||
| } | |||||
| }, | |||||
| methods: { | |||||
| // beforeUpload 在返回 false 或 Promise 时,会停止自动上传,这里我们将选择好的文件 file 保存在 data里,并 return false | |||||
| handleLocalUpload(file) { | |||||
| this.local.file = file; | |||||
| return false; | |||||
| }, | |||||
| // 这里是手动上传,通过 $refs 获取到 Upload 实例,然后调用私有方法 .post(),把保存在 data 里的 file 上传。 | |||||
| // iView 的 Upload 组件在调用 .post() 方法时,就会继续上传了。 | |||||
| localUpload() { | |||||
| this.local.loadingStatus = true; // 标记上传状态 | |||||
| this.$refs.localUploadRef.post(this.local.file); | |||||
| }, | |||||
| // 上传成功后,清空 data 里的 file,并修改上传状态 | |||||
| handleLocalSuccess(response) { | |||||
| this.local.file = null; | |||||
| this.local.loadingStatus = false; | |||||
| if (response.code === 200) { | |||||
| this.$Message.success(response.message); | |||||
| this.local.log.status = response.code; | |||||
| this.local.log.message = response.message; | |||||
| this.local.log.fileName = response.data.fileName; | |||||
| this.local.log.filePath = response.data.filePath; | |||||
| this.$refs.localUploadRef.clearFiles(); | |||||
| } else { | |||||
| this.$Message.error(response.message); | |||||
| this.local.log.status = response.code; | |||||
| this.local.log.message = response.message; | |||||
| } | |||||
| }, | |||||
| // 上传失败后,清空 data 里的 file,并修改上传状态 | |||||
| handleLocalError() { | |||||
| this.local.file = null; | |||||
| this.local.loadingStatus = false; | |||||
| this.$Message.error('上传失败'); | |||||
| }, | |||||
| // beforeUpload 在返回 false 或 Promise 时,会停止自动上传,这里我们将选择好的文件 file 保存在 data里,并 return false | |||||
| handleYunUpload(file) { | |||||
| this.yun.file = file; | |||||
| return false; | |||||
| }, | |||||
| // 这里是手动上传,通过 $refs 获取到 Upload 实例,然后调用私有方法 .post(),把保存在 data 里的 file 上传。 | |||||
| // iView 的 Upload 组件在调用 .post() 方法时,就会继续上传了。 | |||||
| yunUpload() { | |||||
| this.yun.loadingStatus = true; // 标记上传状态 | |||||
| this.$refs.yunUploadRef.post(this.yun.file); | |||||
| }, | |||||
| // 上传成功后,清空 data 里的 file,并修改上传状态 | |||||
| handleYunSuccess(response) { | |||||
| this.yun.file = null; | |||||
| this.yun.loadingStatus = false; | |||||
| if (response.code === 200) { | |||||
| this.$Message.success(response.message); | |||||
| this.yun.log.status = response.code; | |||||
| this.yun.log.message = response.message; | |||||
| this.yun.log.fileName = response.data.fileName; | |||||
| this.yun.log.filePath = response.data.filePath; | |||||
| this.$refs.yunUploadRef.clearFiles(); | |||||
| } else { | |||||
| this.$Message.error(response.message); | |||||
| this.yun.log.status = response.code; | |||||
| this.yun.log.message = response.message; | |||||
| } | |||||
| }, | |||||
| // 上传失败后,清空 data 里的 file,并修改上传状态 | |||||
| handleYunError() { | |||||
| this.yun.file = null; | |||||
| this.yun.loadingStatus = false; | |||||
| this.$Message.error('上传失败'); | |||||
| } | |||||
| } | |||||
| }) | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||
| ``` | |||||
| ## 参考 | |||||
| 1. Spring 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#howto-multipart-file-upload-configuration | |||||
| 2. 七牛云官方文档:https://developer.qiniu.com/kodo/sdk/1239/java#5 | |||||