SpringBoot + Vue 实现文件的上传与下载

文章目录

  • 1. 前言
  • 2. 简单案例
    • 2.1 功能需求
    • 2.2 开发环境
    • 2.3 编写代码
      • 2.3.1 上传、下载
        • 2.3.1.1 前端
        • 2.3.1.2 后端
  • 3. 使用 Axios
    • 3.1 前台使用 http-request() 方法
    • 3.2 后台代码
    • 3.3 前台使用 before-upload() 方法
  • 总结


1. 前言

简要地记录下 SpringBoot 与 Vue 实现文件的上传与下载


2. 简单案例

2.1 功能需求

前台使用 ElementUI 的 Upload 组件或者是 Axios,后台使用 SpringBoot 来实现文件的上传与下载

2.2 开发环境

  • IDEA-2019.1
  • SpringBoot-2.0.2.RELEASE
  • Maven-3.5.3
  • HBuilderX

2.3 编写代码

2.3.1 上传、下载

2.3.1.1 前端

使用 ElementUI 的 Upload 组件
我这里在 html 页面引入:


<html>
	<head>
		<meta charset="utf-8" />
		<title>title>
		<script src="./js/vue.min.js">script>
		<link rel="stylesheet" href="./css/index.css">
		<script src="./js/index.js">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="form.fileName" 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">不超过10Kbdiv>
						el-upload>
					el-form-item>

				el-form>
			div>
		div>
	body>

	<script type="application/javascript">
		var app = new Vue({
			el: '#app',
			data: {
				form: {
					fileName: 'test.txt'
				},
				// 后台请求url
				uploadUrl: 'http://localhost:8080/file/upload',
				fileList: [],
				isUpload: false
			},
			methods: {
				handleExceed(files, fileList) {
					this.$message.warning(`当前限制选择 5 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
				},
				handleUploadError(error, file) {
					this.$notify.error({
						title: 'error',
						message: '上传出错:' + error,
						type: 'error',
						position: 'bottom-right'
					})
				},
				handleBeforeUpload(file) {
					this.isUpload = file.size / (1024 * 10) <= 1 ? '1' : '0'
					if (this.isUpload === '0') {
						this.$message({
							message: '上传文件大小不能超过10k',
							type: 'error'
						})
					}
					return this.isUpload === '1' ? true : false
				},
				onSuccess() {
					this.$message.success(`上传成功!`)
				},
				handleDownLoad() {
					window.location.href = `http://localhost:8080/file/download?fileName=` + this.form.fileName
				}
			}
		})
	script>
html>
  • uploadUrl:data 中的属性。指的是上传到后台的地址
  • 主要是 el-upload 标签,当点击“点击上传”按钮时,选择文件后,会自动提交(auto-upload)到后台,auto-upload 属性默认为 true,可修改。

前台页面:

SpringBoot + Vue 实现文件的上传与下载_第1张图片
上传:可以上传多个文件,最多5个,但每次只能上传一个文件,需要上传多次。然后,文件大小不能超过 10 Kb(handleBeforeUpload()方法中有校验)。否则,上传失败。

下载:前端的下载就是通过链接访问后台地址即可。这里的 fileName 是作为一个测试的下载文件,你可以换成其他的文件,只要本地磁盘存在这个文件就行(重点看后台代码逻辑,在下面呢)。

2.3.1.2 后端

后台是一个父子关系的多模块项目。不太熟悉的话,可以参考此博文:
Maven 多模块项目的创建与配置

项目结构图
SpringBoot + Vue 实现文件的上传与下载_第2张图片

父 POM 文件


...
  <parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.0.2.RELEASEversion>
  parent>

  <properties>
    
    <lombok.version>1.18.8lombok.version>
    <junit.test.version>4.11junit.test.version>
  properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>${lombok.version}version>
      dependency>
      <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <version>${junit.test.version}version>
        <scope>testscope>
      dependency>
    dependencies>
  dependencyManagement>

project>

sb_vue(此项目) POM 文件

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>
    
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <scope>testscope>
    dependency>
dependencies>

application.yml

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 fileService.uploadFile(file);
    }

    // 文件下载
    @RequestMapping("/download")
    public ResultVo<String> downloadFile(@RequestParam("fileName") String fileName, final HttpServletResponse response) {
        return fileService.downloadFile(fileName, response);
    }
}

Service 层
这里只贴出实现类的代码了

@Slf4j    // 日志注解
@Service
public class FileServiceImpl implements FileService {

    @Override
    public ResultVo<String> uploadFile(MultipartFile file) {
        if (file.isEmpty()) {
            log.error("上传的文件为空");
            throw new ParameterValidateExcepiton(ResultCodeEnum.PARAMETER_ERROR.getCode(), "上传的文件为空");
        }

        try {
            FileUtil.uploadFile(file);
        } catch (IOException e) {
            log.error("文件{}上传失败", file);
            return ResultUtil.error("上传失败");
        }

        log.info("文件上传成功");
        return ResultUtil.success("上传成功!");
    }

    @Override
    public ResultVo<String> downloadFile(String fileName, HttpServletResponse response) {
        if (fileName.isEmpty()) {
            log.error("文件名为空");
            throw new ParameterValidateExcepiton(ResultCodeEnum.PARAMETER_ERROR.getCode(), "文件名为空");
        }

        return FileUtil.downloadFile(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 = DateUtil.getNowStr() + 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 = file.getOriginalFilename();
        // 获取文件后缀名,将小数点“.” 进行转译
        String[] split = oldFileName.split("\\.");

        // 文件名
        String fileName = null;
        StringBuilder builder = new StringBuilder();
        if (split.length > 0) {
            String suffix = split[split.length - 1];
            for (int i = 0; i < split.length -1; i++) {
                builder.append(split[i]).append(UNDER_LINE);
            }
            // 防止文件名重复
            fileName = builder.append(System.nanoTime()).append(".").append(suffix).toString();
        } else {
            fileName = builder.append(oldFileName).append(UNDER_LINE).append(System.nanoTime()).toString();
        }

        // 上传文件的存储路径
        String filePath = ROOT_PATH + FILE_UPLOAD_PATH + DATE_PATH;
        // 生成文件夹
        mkdirs(filePath);
        // 文件全路径
        String fileFullPath = filePath + fileName;
        log.info("上传的文件:" + file.getName() + "," + file.getContentType() + ",保存的路径为:" + fileFullPath);

        // 转存文件
        Streams.copy(file.getInputStream(), new FileOutputStream(fileFullPath), true);

        //file.transferTo(new File(fileFullPath));

        //Path path = Paths.get(fileFullPath);
        //Files.write(path,file.getBytes());
        return fileFullPath;
    }

    // 根据文件名下载文件
    public static ResultVo<String> downloadFile(String fileName, HttpServletResponse response) {
        InputStream in = null;
        OutputStream out = null;
        try {
            // 获取输出流
            out = response.getOutputStream();
            setResponse(fileName, response);

            String downloadPath = new StringBuilder().append(ROOT_PATH).append(FILE_DOWNLOAD_PATH).append(fileName).toString();
            File file = new File(downloadPath);
            if (!file.exists()) {
                log.error("下载附件失败,请检查文件" + downloadPath + "是否存在");
                return ResultUtil.error("下载附件失败,请检查文件" + downloadPath + "是否存在");
            }

            // 获取输入流
            in = new FileInputStream(file);
            if (null == in) {
                log.error("下载附件失败,请检查文件" + fileName + "是否存在");
                throw new FileNotFoundException("下载附件失败,请检查文件" + fileName + "是否存在");
            }
            // 复制
            IOUtils.copy(in, response.getOutputStream());
            response.getOutputStream().flush();

            try {
                close(in, out);
            } catch (IOException e) {
                log.error("关闭流失败");
                return ResultUtil.error(ResultCodeEnum.CLOSE_FAILD.getCode(), "关闭流失败");
            }
        } catch (IOException e) {
            log.error("响应对象response获取输出流错误");
            return ResultUtil.error("响应对象response获取输出流错误");
        }
        return ResultUtil.success("文件下载成功");
    }

    // 设置响应头
    public static void setResponse(String fileName, HttpServletResponse response) {
        // 清空输出流
        response.reset();
        response.setContentType("application/x-download;charset=GBK");
        try {
            response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes(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(!file.exists() || !file.isDirectory()) {
            file.mkdirs();
        }
    }
}

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 = formatMap.get(pattern);
        if (format == null) {
            format = new SimpleDateFormat(pattern);
            formatMap.put(pattern, format);
        }
        return format;
    }
    
    // 将当前时间转换为字符串
    public static String getNowStr() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern(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();
        result.setCode(ResultCodeEnum.SUCCESS.getCode());
        result.setMsg("成功");
        result.setData(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();

        result.setCode(code);
        result.setMsg(msg);
        result.setData(object);
        return result;
    }

    public static ResultVo error( String msg) {
        ResultVo result = new ResultVo();
        result.setCode(ResultCodeEnum.ERROR.getCode());
        result.setMsg(msg);
        return result;
    }
    public static ResultVo error(Integer code, String msg) {
        ResultVo result = new ResultVo();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
}

ParameterValidateExcepiton
自定义异常

@Getter
public class ParameterValidateExcepiton extends RuntimeException {

    // 错误码
    private Integer code;
    // 错误消息
    private String msg;

    public ParameterValidateExcepiton() {
        this(ResultCodeEnum.PARAMETER_ERROR.getCode(), ResultCodeEnum.PARAMETER_ERROR.getMessage());
    }
    public ParameterValidateExcepiton(String msg) {
        this(ResultCodeEnum.PARAMETER_ERROR.getCode(), msg);
    }
    public ParameterValidateExcepiton(Integer code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

ExceptionControllerAdvice
统一异常处理类

@RestControllerAdvice
public class ExceptionControllerAdvice {

    // 处理文件为空的异常
    @ExceptionHandler(ParameterValidateExcepiton.class)
    public ResultVo<String> fileExceptionHandler(ParameterValidateExcepiton excepiton) {
        return ResultUtil.error(ResultCodeEnum.PARAMETER_ERROR.getCode(), excepiton.getMsg());
    }

    // 文件不存在异常
    @ExceptionHandler(FileNotFoundException.class)
    public ResultVo<String> fileNotFoundExceptionHandler(FileNotFoundException exception) {
        return ResultUtil.error(ResultCodeEnum.FILE_NOT_EXIST.getCode(), exception.getMessage());
    }
}

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) {
        this.code = code;
        this.message = message;
    }
}

        总体上看,代码量有点大哈(主要是代码写得比较优雅),各位就将就点吧。本来是想着上传到 github 上面,但公司电脑用的是内网,无法访问到外网。即使配置了代理,但 IDEA 也无法连接到 github 上面。但又不想残忍地只贴出部分代码(以免部分读者很迷惑),所以,这里就全贴出来了哈。

3. 使用 Axios

        看看上面的前端上传代码,使用 Upload 组件自动地上传到后台,无法接收后台接口传过来的值。这就会导致一个问题:如果上传过程中,遇见什么错误,导致上传失败,那么就需要提示给用户了。但这种做法是无法实现的。看了多篇博客,大多是使用了 http-request() 方法。在这个方法里,通过 axios 请求后台,并能获取后台的返回值。
在这里插入图片描述

3.1 前台使用 http-request() 方法

前台代码


<html>
	<head>
		<meta charset="utf-8" />
		<title>title>
		<script src="./js/vue.min.js">script>
		
		<link rel="stylesheet" href="./css/index.css">
		
		<script src="./js/index.js">script>
		
		<script src="js/axios.min.js">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文件,且不超过500kbdiv>
			el-upload>
		div> 
	body>

	<script type="application/javascript">
		var app = new Vue({
			el: '#app',
			data: {
				
			},
			methods: {
				onExceed(files, fileList) {
					this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
				},
				
				// 上传文件
				submitUpload(content) {
					console.log(content)
					let file = content.file;
					if (file != null && file != '') {
						let isJPG = file.type === 'image/jpeg'
						let isPNG = file.type === 'image/png'
						let isLt2M = file.size / 1024 / 1024 < 0.5
						if (!isPNG && !isJPG) {
							this.$message.error('上传图片只能是 JPG/PNG 格式!')
							return false
						} else if (!isLt2M) {
							this.$message.error('上传图片大小不能超过 200kb!')
							return false
						} else if (isLt2M && (isPNG || isJPG)) {
							let data = new FormData();
							data.append("file", file)
							//console.log(data.get('file'))
							let url = 'http://localhost:8080/file/upload'
							let headers = {
									'Content-Type': 'multipart/form-data'
							}
							axios.post(url, data, headers)
							.then(res => {
								if (res.data.code === 200) {
									this.$message({
										type: 'success',
										message: res.data.msg
									})
								} else {
									this.$message({
										type: 'warning',
										message: res.data.msg
									})
								}
							}).catch(error => {
								this.$message({
									type: 'error',
									message: error
								})
							})
						}
					}
				}
			}

		})
	script>
html>

在 submitUpload() 方法中,先对上传的文件进行校验,只有校验通过的文件才能去请求后台。

3.2 后台代码

后台编码不变

前后端项目启动,发现依旧能交互哈。

3.3 前台使用 before-upload() 方法

使用 http-request() 方法时有个问题:当我们上传文件时,前端校验失败,也会显示上传的文件列表(show-file-list),这样会造成用户体验感差。
SpringBoot + Vue 实现文件的上传与下载_第3张图片

所以,我们要对它进行限制:前端校验失败时,就不要把此文件上传到文件列表中去,这样就不会显示了。前端代码修改为:


<html>
	<head>
		<meta charset="utf-8" />
		<title>title>
		<script src="./js/vue.min.js">script>
		
		<link rel="stylesheet" href="./css/index.css">
		
		<script src="./js/index.js">script>
		
		<script src="js/axios.min.js">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="url" 
				:limit="1" 
				:on-exceed="onExceed" 
				:before-upload="beaforeUpload"
				:auto-upload="true">
				<el-button slot="trigger" size="small" type="primary">选取文件el-button>
				<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kbdiv>
			el-upload>
		div>
	body>

	<script type="application/javascript">
		var app = new Vue({
			el: '#app',
			data: {
				url: 'http://localhost:8080/file/upload'
			},
			methods: {
				onExceed(files, fileList) {
					this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
				},
				beaforeUpload(file) {
					console.log(file)
					if (file != null && file != '') {
						let isJPG = file.type === 'image/jpeg'
						let isPNG = file.type === 'image/png'
						if (!isPNG && !isJPG) {
							this.$message.error('上传图片只能是 JPG/PNG 格式!')
							return false
						} else {
							let url = this.url
							let data = new FormData()
							data.append("file", file)
							let headers = {
								'Content-Type': 'multipart/form-data'
							}

							axios({
									method: 'post',
									url: url,
									data: data,
									headers: headers
								})
								.then(res => {
									if (res.data.code === 200) {
										this.$message({
											type: 'success',
											message: res.data.msg
										})
									} else {
										this.$message({
											type: 'warning',
											message: res.data.msg
										})
									}
								})
								.catch(error => {
									this.$message({
										type: 'error',
										message: error
									})
								})
						}
					}
				}
			}
		})
	script>
html>

总结

        以上就是今天要讲的内容,本文仅仅简单地介绍了使用 SpringBoot 和 Vue 实现文件的上传与下载。主要考虑到公司中有使用到 Vue 中的 Upload 组件上传文件,所以,自己也就接触了下 它。奈何自己对 Vue 的造诣不深,使用 axios 进行文件上传的方法也就找到了那一个,但我总感觉不是很理想,如果,有读者有更好的想法可以分享一下。

你可能感兴趣的:(springboot,Vue)