简要地记录下 SpringBoot 与 Vue 实现文件的上传与下载
前台使用 ElementUI 的 Upload 组件或者是 Axios,后台使用 SpringBoot 来实现文件的上传与下载
使用 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>
前台页面:
上传:可以上传多个文件,最多5个,但每次只能上传一个文件,需要上传多次。然后,文件大小不能超过 10 Kb(handleBeforeUpload()方法中有校验)。否则,上传失败。
下载:前端的下载就是通过链接访问后台地址即可。这里的 fileName 是作为一个测试的下载文件,你可以换成其他的文件,只要本地磁盘存在这个文件就行(重点看后台代码逻辑,在下面呢)。
后台是一个父子关系的多模块项目。不太熟悉的话,可以参考此博文:
Maven 多模块项目的创建与配置
父 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 上面。但又不想残忍地只贴出部分代码(以免部分读者很迷惑),所以,这里就全贴出来了哈。
看看上面的前端上传代码,使用 Upload 组件自动地上传到后台,无法接收后台接口传过来的值。这就会导致一个问题:如果上传过程中,遇见什么错误,导致上传失败,那么就需要提示给用户了。但这种做法是无法实现的。看了多篇博客,大多是使用了 http-request() 方法。在这个方法里,通过 axios 请求后台,并能获取后台的返回值。
前台代码
<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() 方法中,先对上传的文件进行校验,只有校验通过的文件才能去请求后台。
后台编码不变
前后端项目启动,发现依旧能交互哈。
使用 http-request() 方法时有个问题:当我们上传文件时,前端校验失败,也会显示上传的文件列表(show-file-list),这样会造成用户体验感差。
所以,我们要对它进行限制:前端校验失败时,就不要把此文件上传到文件列表中去,这样就不会显示了。前端代码修改为:
<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>