前言
项目要求:从前端把用户的图片/文件上传到后端存到服务器中,及从服务器中下载相应的图片/文件。
由于springboot的Tomcat是内置的,不容易配置,且若采用后端提供url来下载文件,很可能要面对不好解决的跨域问题。
故本文章采用的把文件流放在http请求的body
中的方式,使得文件能够在vue前端和spring后端间传输。
并通过简单的方式解决了 下载后的文件乱码/损坏 问题。
后端Springboot
ImageController
@RestController
@RequestMapping("api")
public class ImageController {
@Autowired
private ImageService imageService;
private String uuidName;
@PostMapping(value = "/image/upload")
public ResponseEntity upload(@RequestParam("file") MultipartFile uploadFile){
uuidName = UUID.randomUUID().toString();
String fileName = uploadFile.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf("."));
uuidName += suffix;
imageService.saveImage(uuidName, uploadFile);
return new ResponseEntity(HttpStatus.CREATED);
}
@PostMapping(value = "/image/export")
public ResponseEntity export(@Validated @RequestBody String fileName){
return imageService.exportImage(fileName);
}
@DeleteMapping(value = "/image/{id}")
public ResponseEntity delete(@PathVariable Integer id, @Validated @RequestBody String fileName){
imageService.delete(id, fileName);
return new ResponseEntity(HttpStatus.OK);
}
}
ImageService
public interface ImageService {
/**
* delete
* @param id
* @param fileName
*/
//@CacheEvict(allEntries = true)
void delete(Integer id, String fileName);
/**
* saveImage
* @param uuidName 通过UUID重新命名
* @param uploadFile 图片文件
*/
void saveImage(String uuidName, MultipartFile uploadFile);
/**
* exportImage
* @param fileName
* @return
*/
ResponseEntity exportImage(String fileName);
}
ImageServiceImpl
public class ImageServiceImpl implements ImageService {
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id, String fileName) {
String deleteFilePath = ImageConstants.AGREEMENT_FOLDER + fileName;
try {
FileUtils.deleteFile(new File(deleteFilePath));
} catch (Exception e) {
System.out.println(e);
}
}
@Override
public void saveImage(String uuidName, MultipartFile uploadFile){
String uploadPath = ImageConstants.AGREEMENT_FOLDER;
// 如果目录不存在则创建
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
/*
// 获取源文件名
String fileName = uploadFile.getOriginalFilename();
// //获取源文件名
// String originalFileName = fileName.substring(0, fileName.lastIndexOf("."));
// 获取源文件名后缀
String suffix = fileName.substring(fileName.lastIndexOf("."));
*/
File file = new File(uploadPath, uuidName);
try {
uploadFile.transferTo(file);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public ResponseEntity exportImage(String fileName){
String exportPath = ImageConstants.AGREEMENT_FOLDER + fileName;
try {
File file = new File(exportPath);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=" + fileName);
headers.add("Content-Length", String.valueOf(file.length()));
return ResponseEntity
.ok()
.headers(headers)
.contentLength(file.length())
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(new FileSystemResource(file));
} catch (Exception e) {
e.printStackTrace();
}finally {
}
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
}
ImageConstants
public interface ImageConstants {
/** 存储目录名称 */
public static final String AGREEMENT_FOLDER_NAME = "_image";
/** 存储目录 */
public static final String AGREEMENT_FOLDER = Constants.ROOT_FOLDER + AGREEMENT_FOLDER_NAME + File.separator;
}
FileUtils
public class FileUtils {
/**
* 递归删除文件
* @param file
*/
public static void deleteFile(File file) {
// 判断是否是一个目录, 不是的话跳过, 直接删除; 如果是一个目录, 先将其内容清空.
if(file.isDirectory()) {
// 获取子文件/目录
File[] subFiles = file.listFiles();
// 遍历该目录
for (File subFile : subFiles) {
// 递归调用删除该文件: 如果这是一个空目录或文件, 一次递归就可删除.
// 如果这是一个非空目录, 多次递归清空其内容后再删除
deleteFile(subFile);
}
}
// 删除空目录或文件
file.delete();
}
}
前端Vue
image.vue
<!-- 上传图片 -->
<el-upload
ref="upload"
:multiple="false"
:file-list="fileList"
:auto-upload="false"
:limit="1"
:http-request="importConfirm"
:before-upload="beforeUpload"
action
accept=".jpeg, .jpg, .png"
>
<el-button slot="trigger" size="small" type="primary" plain>选取文件</el-button>
<el-button
style="margin-left: 10px;"
size="small"
type="success"
plain
@click="submitUpload"
>上传到服务器</el-button>-->
<div
slot="tip"
class="el-upload__tip"
>注意:只能上传jpeg/png文件,且文件大小不能超过{{ fileSizeLimit }}MB
</div>
</el-upload>
<!-- 显示图片 -->
<div v-show="isShow" style="margin: 20px auto">
<p>{{ imageName }}</p>
<el-image :src="imageLink" />
</div>
<script>
import { del, upload, download } from '@/api/image'
export default {
data() {
return {
delLoading: false, isShow: false,
imageLink: '', imageName: '',
}
},
created() {
// 略
},
methods: {
// 上传的文件格式、大小限制
beforeUpload(file) {
var testmsg = file.name.substring(file.name.lastIndexOf('.') + 1)
const extension0 = testmsg === 'jpeg'
const extension1 = testmsg === 'jpg'
const extension2 = testmsg === 'png'
const isLt2M = file.size / 1024 / 1024 < this.fileSizeLimit
if (!extension0 && !extension1 && !extension2) {
this.$notify({
message: '上传的文件只能是jpeg、jpg、png格式!',
type: 'warning',
duration: 2500
})
this.loading = false
return
}
if (!isLt2M) {
this.$message({
message: '上传文件大小不能超过 ' + this.fileSizeLimit + 'MB!',
type: 'warning'
});
}
return (extension0 || extension1 || extension2) && isLt2M
},
// 文件上传
importConfirm(item) {
const fileObj = item.file
const formData = new FormData()
formData.append('file', fileObj)
upload(formData).then(res => {
this.$notify({
title: '文件上传成功',
type: 'success',
duration: 2500
})
}).catch(err => {
this.loading = false
console.log(err.response.data.message)
})
},
submitUpload(){
this.$refs.upload.submit() // 会去执行 importConfirm
},
// 图片文件下载
download(filePath) {
download(filePath).then(response => {
const data = response
if (!data) {
return
}
const blob = new Blob([data])
if (window.navigator.msSaveOrOpenBlob) { // 兼容IE10
navigator.msSaveBlob(blob, filePath)
} else { // 其他非IE内核支持H5的浏览器
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', filePath)
document.body.appendChild(link)
link.click()
}
}).catch(function (error) {
console.log(error)
})
},
// 图片文件显示
show(fileName, filePath) {
download(filePath).then(response => {
const data = response
if (!data) {
return
}
const blob = new Blob([data])
this.imageName = fileName
this.imageLink = window.URL.createObjectURL(blob)
this.isShow = true
}).catch(function (error) {
console.log(error)
})
}
}
}
</script>
image.js
// 封装好的axios请求
import request from '@/utils/request'
export function del(id, data) {
return request({
url: 'api/image/' + id,
method: 'delete',
data
})
}
export function upload(data) {
return request({
url: 'api/image/upload',
method: 'post',
data
})
}
export function download(data) {
return request({
url: 'api/image/export',
method: 'post',
responseType: 'arraybuffer', // 解决下载后的文件乱码/损坏问题
data
})
}