1. 前言
简要地记录下 SpringBoot 与 Vue 实现文件的上传与下载
2. 简单案例
2.1 功能需求
前台使用 ElementUI 的 Upload 组件或者是 Axios,后台使用 SpringBoot 来实现文件的上传与下载
2.2 开发环境
- IDEA-2019.1
- S
- Maven-3.5.3
- HBuilderX
2.3 编写代码
2.3.1 上传、下载
2.3.1.1 前端
使用 ElementUI 的 Upload 组件
我这里在 html 页面引入:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> <script src="./j;></script> <link rel="stylesheet" href="./c;> <script src="./j;></script> </head> <body> <div id="app"> <div style="top:100px;width:300px"> <el-form ref="upload" :model="form" label-width="120px"> <el-form-item label="请输入文件名" required> <el-input v-model="; auto-complete="off" class="el-col-width"></el-input> </el-form-item> <el-form-item> <el-button size="small" type="primary" @click="handleDownLoad">下载</el-button> </el-form-item> <el-form-item> <el-upload class="upload-demo" :action="uploadUrl" :before-upload="handleBeforeUpload" :on-error="handleUploadError" multiple :limit="5" :on-exceed="handleExceed" :file-list="fileList" :on-success="onSuccess"> <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">不超过10Kb</div> </el-upload> </el-form-item> </el-form> </div> </div> </body> <script type="application/javascript"> var app = new Vue({ el: '#app', data: { form: { fileName: '; }, // 后台请求url uploadUrl: 'http://localhost:8080/file/upload', fileList: [], isUpload: false }, methods: { handleExceed(files, fileList) { this.$me(`当前限制选择 5 个文件,本次选择了 ${} 个文件,共选择了 ${ + } 个文件`); }, handleUploadError(error, file) { this.$no({ title: 'error', message: '上传出错:' + error, type: 'error', position: 'bottom-right' }) }, handleBeforeUpload(file) { = / (1024 * 10) <= 1 ? '1' : '0' if ( === '0') { this.$message({ message: '上传文件大小不能超过10k', type: 'error' }) } return === '1' ? true : false }, onSuccess() { this.$me(`上传成功!`) }, handleDownLoad() { window.loca = `http://localhost:8080/file/download?fileName=` + } } }) </script> </html>
- uploadUrl:data 中的属性。指的是上传到后台的地址
- 主要是 el-upload 标签,当点击“点击上传”按钮时,选择文件后,会自动提交(auto-upload)到后台,auto-upload 属性默认为 true,可修改。
前台页面:
上传:可以上传多个文件,最多5个,但每次只能上传一个文件,需要上传多次。然后,文件大小不能超过 10 Kb(handleBeforeUpload()方法中有校验)。否则,上传失败。
下载:前端的下载就是通过链接访问后台地址即可。这里的 fileName 是作为一个测试的下载文件,你可以换成其他的文件,只要本地磁盘存在这个文件就行(重点看后台代码逻辑,在下面呢)。
2.3.1.2 后端
后台是一个父子关系的多模块项目。不太熟悉的话,可以参考此博文:
Maven 多模块项目的创建与配置
项目结构图
父 POM 文件
<?xml version="1.0" encoding="UTF-8"?> ... <parent> <groupId>org.;/groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> </parent> <properties> <!-- 在properties中统一控制依赖包的版本,更清晰--> <lombok.version>1.18.8<; <junit.;4.11</junit.; </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.}</version> <scope>test</scope> </dependency> </dependencies> </dependencyManagement> </project>
sb_vue(此项目) POM 文件
<dependencies> <dependency> <groupId>org.;/groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--父模块pom中使用dependencyManagement来管理依赖版本号,子模块pom中不需要再写版本号--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies>
a
spring: servlet: multipart: enabled: true max-file-size: 10MB # 单个文件上传的最大上限 max-request-size: 10MB # 一次请求总大小上限
Controller 层
@RestController @RequestMapping("/file") @CrossOrigin // 跨域 public class FileController { @Autowired private FileService fileService; // 文件上传 @RequestMapping("/upload") public ResultVo<String> uploadFile(@RequestParam("file") MultipartFile file) { return (file); } // 文件下载 @RequestMapping("/download") public ResultVo<String> downloadFile(@RequestParam("fileName") String fileName, final HttpServletResponse response) { return (fileName, response); } }
Service 层
这里只贴出实现类的代码了
@Slf4j // 日志注解 @Service public class FileServiceImpl implements FileService { @Override public ResultVo<String> uploadFile(MultipartFile file) { if ()) { log.error("上传的文件为空"); throw new ParameterValidateExcepiton(), "上传的文件为空"); } try { FileU(file); } catch (IOException e) { log.error("文件{}上传失败", file); return Re("上传失败"); } log.info("文件上传成功"); return Re("上传成功!"); } @Override public ResultVo<String> downloadFile(String fileName, HttpServletResponse response) { if ()) { log.error("文件名为空"); throw new ParameterValidateExcepiton(), "文件名为空"); } return FileU(fileName, response); } }
FileUtil
文件工具类
@Slf4j public class FileUtil { // 文件上传路径 private static final String FILE_UPLOAD_PATH = "upload" + File.separator; // 文件下载路径 private static final String FILE_DOWNLOAD_PATH = "download" + File.separator; // 日期路径 private static final String DATE_PATH = Da() + File.separator; // 根路径 private static final String ROOT_PATH = "E:" + File.separator; // 下划线 private static final String UNDER_LINE = "_"; // 默认字符集 private static final String DEFAULT_CHARSET = "utf-8"; // 上传文件 public static String uploadFile(MultipartFile file) throws IOException{ // 获取上传的文件名称(包含后缀名) String oldFileName = (); // 获取文件后缀名,将小数点“.” 进行转译 String[] split = oldFileName.split("\\."); // 文件名 String fileName = null; StringBuilder builder = new StringBuilder(); if > 0) { String suffix = split[ - 1]; for (int i = 0; i < -1; i++) { builder.append(split[i]).append(UNDER_LINE); } // 防止文件名重复 fileName = builder.append()).append(".").append(suffix).toString(); } else { fileName = builder.append(oldFileName).append(UNDER_LINE).append()).toString(); } // 上传文件的存储路径 String filePath = ROOT_PATH + FILE_UPLOAD_PATH + DATE_PATH; // 生成文件夹 mkdirs(filePath); // 文件全路径 String fileFullPath = filePath + fileName; log.info("上传的文件:" + () + "," + () + ",保存的路径为:" + fileFullPath); // 转存文件 S(), new FileOutputStream(fileFullPath), true); (new File(fileFullPath)); //Path path = Pa(fileFullPath); (path,()); return fileFullPath; } // 根据文件名下载文件 public static ResultVo<String> downloadFile(String fileName, HttpServletResponse response) { InputStream in = null; OutputStream out = null; try { // 获取输出流 out = re(); setResponse(fileName, response); String downloadPath = new StringBuilder().append(ROOT_PATH).append(FILE_DOWNLOAD_PATH).append(fileName).toString(); File file = new File(downloadPath); if (!()) { log.error("下载附件失败,请检查文件" + downloadPath + "是否存在"); return Re("下载附件失败,请检查文件" + downloadPath + "是否存在"); } // 获取输入流 in = new FileInputStream(file); if (null == in) { log.error("下载附件失败,请检查文件" + fileName + "是否存在"); throw new FileNotFoundException("下载附件失败,请检查文件" + fileName + "是否存在"); } // 复制 IOU(in, re()); re().flush(); try { close(in, out); } catch (IOException e) { log.error("关闭流失败"); return Re(), "关闭流失败"); } } catch (IOException e) { log.error("响应对象response获取输出流错误"); return Re("响应对象response获取输出流错误"); } return Re("文件下载成功"); } // 设置响应头 public static void setResponse(String fileName, HttpServletResponse response) { // 清空输出流 re(); re("application/x-download;charset=GBK"); try { re("Content-Disposition", "attachment;filename=" + new String(DEFAULT_CHARSET), "iso-8859-1")); } catch (UnsupportedEncodingException e) { log.error("文件名{}不支持转换为字符集{}", fileName, DEFAULT_CHARSET); } } // 关闭流 public static void close(InputStream in, OutputStream out) throws IOException{ if (null != in) { in.close(); } if (null != out) { out.close(); } } // 根据目录路径生成文件夹 public static void mkdirs(String path) { File file = new File(path); if(!() || !()) { (); } } }
DateUtil
日期工具类
public class DateUtil { // 默认日期字符串格式 "yyyy-MM-dd" public final static String DATE_DEFAULT = "yyyy-MM-dd"; // 日期字符串格式 "yyyyMMdd" public final static String DATE_YYYYMMDD = "yyyyMMdd"; // 格式 map private static Map<String, SimpleDateFormat> formatMap; // 通过格式获取 SimpleDateFormat 对象 private static SimpleDateFormat getFormat(String pattern) { if (formatMap == null) { formatMap = new HashMap<>(); } SimpleDateFormat format = (pattern); if (format == null) { format = new SimpleDateFormat(pattern); (pattern, format); } return format; } // 将当前时间转换为字符串 public static String getNowStr() { return LocalDa().forma(DATE_YYYYMMDD)); } }
ResultVo
统一接口的返回值
@Data public class ResultVo<T> { // 错误码. private Integer code; // 提示信息. private String msg; // 具体的内容. private T data; }
ResultUtil
返回界面的工具类
public class ResultUtil { public static ResultVo success() { return success(null); } public static ResultVo success(Object object) { ResultVo result = new ResultVo(); re()); re("成功"); re(object); return result; } public static ResultVo success(Integer code, Object object) { return success(code, null, object); } public static ResultVo success(Integer code, String msg, Object object) { ResultVo result = new ResultVo(); re(code); re(msg); re(object); return result; } public static ResultVo error( String msg) { ResultVo result = new ResultVo(); re()); re(msg); return result; } public static ResultVo error(Integer code, String msg) { ResultVo result = new ResultVo(); re(code); re(msg); return result; } }
ParameterValidateExcepiton
自定义异常
@Getter public class ParameterValidateExcepiton extends RuntimeException { // 错误码 private Integer code; // 错误消息 private String msg; public ParameterValidateExcepiton() { this(), Re()); } public ParameterValidateExcepiton(String msg) { this(), msg); } public ParameterValidateExcepiton(Integer code, String msg) { super(msg); = code; = msg; } }
ExceptionControllerAdvice
统一异常处理类
@RestControllerAdvice public class ExceptionControllerAdvice { // 处理文件为空的异常 @ExceptionHandler) public ResultVo<String> fileExceptionHandler(ParameterValidateExcepiton excepiton) { return Re(), exce()); } // 文件不存在异常 @ExceptionHandler) public ResultVo<String> fileNotFoundExceptionHandler(FileNotFoundException exception) { return Re(), exce()); } }
ResultCodeEnum
统一响应码
@Getter public enum ResultCodeEnum { SUCCESS(200, "成功") , ERROR(301, "错误") , UNKNOWERROR(302, "未知错误") , PARAMETER_ERROR(303, "参数错误") , FILE_NOT_EXIST(304, "文件不存在") , CLOSE_FAILD(305, "关闭流失败") ; private Integer code; private String message; ResultCodeEnum(Integer code, String message) { = code; = message; } }
总体上看,代码量有点大哈(主要是代码写得比较优雅),各位就将就点吧。本来是想着上传到 github 上面,但公司电脑用的是内网,无法访问到外网。即使配置了代理,但 IDEA 也无法连接到 github 上面。但又不想残忍地只贴出部分代码(以免部分读者很迷惑),所以,这里就全贴出来了哈。
3. 使用 Axios
看看上面的前端上传代码,使用 Upload 组件自动地上传到后台,无法接收后台接口传过来的值。这就会导致一个问题:如果上传过程中,遇见什么错误,导致上传失败,那么就需要提示给用户了。但这种做法是无法实现的。看了多篇博客,大多是使用了 http-request() 方法。在这个方法里,通过 axios 请求后台,并能获取后台的返回值。
3.1 前台
前台代码
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> <script src="./j;></script> <!-- 引入样式 --> <link rel="stylesheet" href="./c;> <!-- 引入组件库 --> <script src="./j;></script> <!-- 引入axios --> <script src="j;></script> <style> .app { margin-left: 200px; margin-top: 200px; } </style> </head> <body> <div id="app" class="app"> <el-upload class="upload-demo" ref="upload" action="" :http-request="submitUpload" :before-upload="beaforeUpload" :limit="1" :on-exceed="onExceed" :auto-upload="true"> <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </div> </body> <script type="application/javascript"> var app = new Vue({ el: '#app', data: { }, methods: { onExceed(files, fileList) { this.$me(`当前限制选择 1 个文件,本次选择了 ${} 个文件,共选择了 ${ + } 个文件`); }, // 上传文件 submitUpload(content) { con(content) let file = con; if (file != null && file != '') { let isJPG = === 'image/jpeg' let isPNG = === 'image/png' let isLt2M = / 1024 / 1024 < 0.5 if (!isPNG && !isJPG) { this.$me('上传图片只能是 JPG/PNG 格式!') return false } else if (!isLt2M) { this.$me('上传图片大小不能超过 200kb!') return false } else if (isLt2M && (isPNG || isJPG)) { let data = new FormData(); da("file", file) //con('file')) let url = 'http://localhost:8080/file/upload' let headers = { 'Content-Type': 'multipart/form-data' } /* axios({ method: 'post', url: 'http://localhost:8080/file/upload', data: data, headers: { 'Content-Type': 'multipart/form-data' } */ axios.post(url, data, headers) .then(res => { if === 200) { this.$message({ type: 'success', message: res.da }) } else { this.$message({ type: 'warning', message: res.da }) } }).catch(error => { this.$message({ type: 'error', message: error }) }) } } } } }) </script> </html>
在 submitUpload() 方法中,先对上传的文件进行校验,只有校验通过的文件才能去请求后台。
3.2 后台代码
后台编码不变
前后端项目启动,发现依旧能交互哈。
总结
以上就是今天要讲的内容,本文仅仅简单地介绍了使用 SpringBoot 和 Vue 实现文件的上传与下载。主要考虑到公司中有使用到 Vue 中的 Upload 组件上传文件,所以,自己也就接触了下 它。奈何自己对 Vue 的造诣不深,使用 axios 进行文件上传的方法也就找到了那一个,但我总感觉不是很理想,如果,有读者有更好的想法可以分享一下。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: