ps:【对之前的另一篇博客:FastDFS+SpringBoot+Mybatis 整合 实现文件上传下载又实现了改造,单文件上传。】
1、文件上传需要创建临时路径,否者上传可能会报错。(文件上传报错:Could not parse multipart servlet request; nested exception is java.io.IOException)
package com.dhcc.znbt.business.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.MultipartConfigElement;
import java.io.File;
/**
* @Author: Lucifer
* @Date: 2019/1/31 16:08
* @Description: 创建文件临时路径 配置类
*/
@Configuration
public class MultipartConfig {
@Autowired
public OperatingSystemChangeConfiguration configuration;
/**
* 文件上传临时路径
* 注:文件存储临时文件夹必须有,否则上传文件,可能会报错:
* Could not parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location...
*/
@Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//System.getProperty("user.dir") 获取当前工作空间
String path = System.getProperty("user.dir") + "/temp";
//判断是否有文件夹,没有就创建
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
//文件路径,如:d:\temp\
factory.setLocation(path);
return factory.createMultipartConfig();
}
}
2、根据不同操作系统读取不同配置(因为测试是Linux,生产是Windows系统,公司有人又用苹果电脑,为了考虑这点,按操作系统读取不同配置)
package com.dhcc.znbt.business.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
/**
* @Author: Lucifer
* @Date: 2019/1/31 17:43
* @Description: 根据不同操作系统读取不同配置
*/
@Component
@Slf4j
public class OperatingSystemChangeConfiguration {
@Value("${xxxx.upload.filePath.windowsBackPart}")
private String windowsBackPart;
@Value("${xxxx.upload.filePath.linux}")
private String linuxFilePath;
@Value("${xxxx.upload.filePath.mac}")
private String macFilePath;
private static final String windows = "Windows";
private static final String Linux = "Linux";
private static final String Mac = "Mac OS X";
/**
* 选择系统盘符
*
* @return
*/
public String chooseSystemDisk() {
File[] roots = File.listRoots();
String systemDisk;
//生产C、E盘 默认是不选择C盘存储
for (int i = 0; i < roots.length; i++) {
//过滤掉非本地硬盘的所有盘符
FileSystemView fileSystemView = FileSystemView.getFileSystemView();
// 获取磁盘的类型描述信息
String diskType = fileSystemView.getSystemTypeDescription(roots[i]);
//盘符类型包括:本地磁盘、可移动磁盘、CD 驱动器等
if (diskType.equals("本地磁盘")) {
if (!roots[i].toString().contains("C:")) {
systemDisk = roots[i].toString();
log.info("【你的操作系统选择的盘符】:" + systemDisk);
return systemDisk;
}
}
}
return null;
}
/**
* 文件存储地址根据操作系统判断
*
* @return
*/
public String changeConfigurationAccordingToOperatingSystem() {
final String os = System.getProperty("os.name");
if (os != null) {
if (os.startsWith(windows)) {
return chooseSystemDisk() + windowsBackPart;
} else if (os.startsWith(Linux)) {
return linuxFilePath;
} else if (os.startsWith(Mac)) {
return macFilePath;
} else {
try {
throw new IOException("Unknown operating system:" + os);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
(原本的猜想)如果不排除CD驱动器或者U盘,又出现下图这种情况,就会导致配置类选择CD 驱动器,而不是本地硬盘E盘,然后导致文件上传不进去,建议排除非本地磁盘的所有盘。
由于本地是win10,测试是Linux,生产是win2008,本地和测试获取盘符都没有任何问题,但是生产调用chooseSystemDisk()去选择盘符中,需要获取盘符属性,会报错。我猜想是因为windows系统存在了CD 驱动器,然后FileSystemView去调用getSystemTypeDescription报的错。
因此我将方法改变一下:chooseSystemDisk方法如下:(但是有个缺点是,如果电脑后面插入了U盘,而且U盘的盘符假设是D,那么会选择U盘,而这时候把U盘拔了,文件肯定就不在Windows系统中了,目前就这么解决吧。)
/**
* 选择系统盘符
*
* @return
*/
private String chooseSystemDisk() {
File[] roots = File.listRoots();
String systemDisk;
//生产C、E盘 默认是不选择C盘存储
for (File root : roots) {
if (!root.toString().contains("C:")&&root.getFreeSpace()!=0) {
systemDisk = root.toString();
log.info("【你的操作系统选择的盘符】:" + systemDisk);
return systemDisk;
}
/* //过滤掉非本地硬盘的所有盘符
FileSystemView fileSystemView = FileSystemView.getFileSystemView();
// 获取磁盘的类型描述信息
String diskType = fileSystemView.getSystemTypeDescription(root);
//盘符类型包括:本地磁盘、可移动磁盘、CD 驱动器等
if (diskType.equals("本地磁盘")&& StringUtils.isNotBlank(diskType)) {
if (!root.toString().contains("C:")) {
systemDisk = root.toString();
log.info("【你的操作系统选择的盘符】:" + systemDisk);
return systemDisk;
}
}*/
}
return null;
}
3、文件上传的自定义配置类
package com.dhcc.znbt.business.util;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* @Author: Lucifer
* @Date: 2019/1/21 15:04
* @Description: 文件上传的配置类
*/
@Data
@ConfigurationProperties(prefix = "xxxx.upload")
public class UploadProperties {
//允许上传文件的格式
private List allowTypes;
//允许上传文件大小
private long fileSize;
}
4、定时清理无效文件数据,由于表单保存跟文件上传是两个接口,所以文件上传成功,表单这时候点取消
package tk.mybatis.springboot.controller;
import com.dhcc.znbt.business.mybatis.uploadFile.mapper.UploadFileMapper;
import com.dhcc.znbt.business.mybatis.uploadFile.model.UploadFile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* @Author: Lucifer
* @Date: 2019/1/30 14:30
* @Description: 定时清理无效文件和数据库无效文件数据
*/
@Component
@Slf4j
public class CleanUnUseFileTask {
@Autowired
private UploadFileMapper uploadFileMapper;
// @Scheduled(cron = "0 0/5 * * * ?") //每5分钟执行一次,用于测试
//@Scheduled(cron = "0 0 3 1/1 * ?") //每天凌晨3点钟执行
public void cleanUnUseFile() {
log.info("定时任务开始:=====================================》清理无效文件数据时间为:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
/**
* 查询支付报销单、差旅报销单、借款单 均未与upload_file表关联的文件信息
*/
List uploadFileList = uploadFileMapper.selectUnUseFile();
log.info("预计清理个数:==========================================》" + uploadFileList.size());
for (UploadFile uploadFile : uploadFileList) {
log.info("准备清理====================》文件数据:" + uploadFile);
//删除文件
File file = new File(uploadFile.getFilesPath());
if (file.exists()) {
file.delete();
}
//删除数据库
Example example = new Example(UploadFile.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("uid", uploadFile.getUid());
uploadFileMapper.deleteByExample(example);
}
}
}
5、mapper接口
package com.dhcc.znbt.business.mybatis.uploadFile.mapper;
import com.dhcc.znbt.business.mybatis.uploadFile.model.UploadFile;
import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;
import java.util.List;
/**
* @Author: Lucifer
* @Date: 2019/1/21 15:50
* @Description:附件上传mapper接口
*/
public interface UploadFileMapper extends Mapper, MySqlMapper {
List selectUnUseFile();
}
6、mapper.xml
7、文件上传的控制层
package tk.mybatis.springboot.controller.uploadFile;
import com.dhcc.znbt.business.mybatis.cw.base.ResponseBean;
import com.dhcc.znbt.business.mybatis.uploadFile.model.BXAndUploadDTO;
import com.dhcc.znbt.business.mybatis.uploadFile.model.UploadFile;
import com.dhcc.znbt.business.uploadFile.UploadFileService;
import groovy.util.logging.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* @Author: Lucifer
* @Date: 2019/1/21 15:10
* @Description: 文件上传的控制层
*/
@Slf4j
@RestController
@RequestMapping(value = "upload")
public class UploadFileController {
@Autowired
private UploadFileService uploadFileService;
/**
* 上传文件
*
* @param file
* @param dto
* @return
*/
@PostMapping(value = "uploadFile")
public ResponseBean uploadFile(@RequestParam("file") MultipartFile file, BXAndUploadDTO dto) {
Map map;
try {
map = uploadFileService.uploadFile(file, dto);
} catch (Exception e) {
e.printStackTrace();
return ResponseBean.fail(e.getMessage());
}
return ResponseBean.success(map);
}
/**
* 删除文件
*
* @param uploadFile
*/
@PostMapping(value = "delFile")
public ResponseBean delFile(@RequestBody UploadFile uploadFile) {
Map map;
try {
map = uploadFileService.deleteFile(uploadFile);
} catch (Exception e) {
e.printStackTrace();
return ResponseBean.fail(e.getMessage());
}
return ResponseBean.success(map);
}
/**
* 查询文件
*
* @param uploadFile
* @return
*/
@PostMapping(value = "fileFileByCode")
public ResponseBean findFileByCode(@RequestBody UploadFile uploadFile) {
Map map;
try {
map = uploadFileService.findFileByCode(uploadFile);
} catch (Exception e) {
return ResponseBean.fail(e.getMessage());
}
return ResponseBean.suc(map);
}
}
8、文件上传service接口
package com.dhcc.znbt.business.uploadFile;
import com.dhcc.znbt.business.exception.ZnbtException;
import com.dhcc.znbt.business.mybatis.uploadFile.model.BXAndUploadDTO;
import com.dhcc.znbt.business.mybatis.uploadFile.model.UploadFile;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* @Author: Lucifer
* @Date: 2019/1/21 15:41
* @Description:附件上传接口
*/
public interface UploadFileService {
/**
* 上传文件
*
* @param bxAndUploadDTO
* @param file
* @throws Exception
*/
Map uploadFile(MultipartFile file, BXAndUploadDTO bxAndUploadDTO) throws Exception;
/**
* 删除附件
*
* @param uploadFile
* @throws Exception
*/
Map deleteFile(UploadFile uploadFile) throws Exception;
/**
* 查询文件
* @param uploadFile
* @return
*/
Map findFileByCode(UploadFile uploadFile);
}
9、文件上传service实现类(注:由于预览中是ip+虚拟地址+文件完整名称,所以查看中地址返回的不包含盘符,预览地址是拼接的。详情查看13)
package com.dhcc.znbt.business.uploadFile.impl;
import com.dhcc.znbt.business.mybatis.uploadFile.mapper.UploadFileMapper;
import com.dhcc.znbt.business.mybatis.uploadFile.model.BXAndUploadDTO;
import com.dhcc.znbt.business.mybatis.uploadFile.model.UploadFile;
import com.dhcc.znbt.business.uploadFile.UploadFileService;
import com.dhcc.znbt.business.util.FileUtil;
import com.dhcc.znbt.business.util.OperatingSystemChangeConfiguration;
import com.dhcc.znbt.business.util.UploadProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import tk.mybatis.mapper.entity.Example;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @Author: Lucifer
* @Date: 2019/1/21 15:42
* @Description: 附件上传实现类
*/
@Slf4j
@Service
@EnableConfigurationProperties(UploadProperties.class)
public class UploadFileServiceImpl implements UploadFileService {
@Autowired
private UploadProperties uploadProperties;
@Autowired
private UploadFileMapper uploadFileMapper;
@Autowired
private OperatingSystemChangeConfiguration configuration;
@Override
@Transactional(rollbackFor = Exception.class)
public Map uploadFile(MultipartFile aFile, BXAndUploadDTO dto) throws Exception {
Map map = new HashMap<>();
String path = null;
String originalFilename;
try {
//检查文件类型,大小
checkFileType(aFile);
//带后缀名,文件名
originalFilename = aFile.getOriginalFilename();
//不带后缀名,文件名
String fileName = FileUtil.getFileName(aFile);
//后缀名
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//时间戳
Long time = System.currentTimeMillis();
path = configuration.changeConfigurationAccordingToOperatingSystem() + time + suffix;
File filePath = new File(path);
if (!filePath.getParentFile().exists()) {
filePath.getParentFile().mkdirs();
}
if (StringUtils.isBlank(dto.getBorrowCode())) {
throw new RuntimeException("报销单号不允许为空");
}
if (StringUtils.isBlank(dto.getUserName())) {
throw new RuntimeException("附件上传人不能为空");
}
Example example = new Example(UploadFile.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("borrowCode", dto.getBorrowCode());
if (uploadFileMapper.selectByExample(example).size() > 0) {
throw new RuntimeException("报销单号:" + dto.getBorrowCode() + ",数据库中已存在");
}
UploadFile uploadFile = new UploadFile();
String fileId = UUID.randomUUID().toString().replace("-", "");
uploadFile.setUid(fileId);
uploadFile.setName(fileName);
uploadFile.setFilesType(suffix);
uploadFile.setFilesPath(path);
uploadFile.setUploadTime(time);
uploadFile.setSize(aFile.getSize());
map.put("filePath", path);
map.put("fileId", fileId);
uploadFile.setBorrowCode(dto.getBorrowCode());
uploadFile.setCreateUser(dto.getUserName());
int insert = uploadFileMapper.insert(uploadFile);
if (insert > 0) {
//将文件写入硬盘
aFile.transferTo(filePath);
}
} catch (IOException e) {
log.error("文件写入失败,路径:" + path);
throw new IOException("文件写入失败");
} catch (Exception e) {
//判断文件是否存在
if (StringUtils.isNotBlank(path) && new File(path).exists()) {
new File(path).delete();
}
log.error("文件上传失败,正在清理文件,{}", e.getMessage());
e.printStackTrace();
throw new Exception(e.getMessage());
}
return map;
}
/**
* 根据文件名删除本地文件,根据id删除数据库数据
*
* @param uploadFile
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map deleteFile(UploadFile uploadFile) throws Exception {
boolean b;
Map map = new HashMap<>();
try {
if (StringUtils.isBlank(uploadFile.getFilesPath())) {
throw new RuntimeException("文件路径不能为空!");
}
if (StringUtils.isBlank(uploadFile.getUid())) {
throw new RuntimeException("文件id不能为空!");
}
//若filePath 不包含盘符的完整路径,则重写文件路径
String filePath = uploadFile.getFilesPath();
if (!filePath.contains(configuration.changeConfigurationAccordingToOperatingSystem())) {
filePath = configuration.changeConfigurationAccordingToOperatingSystem() + filePath;
}
String fileId = uploadFile.getUid();
File file = new File(filePath);
log.info("正在删除文件,文件路径为:" + filePath);
Example example = new Example(UploadFile.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("uid", fileId);
if (uploadFileMapper.selectByExample(example) == null) {
log.error("文件id:" + fileId + "数据库中不存在!");
throw new RuntimeException("文件id在数据库中不存在!");
}
//判断文件是否存在
if (file.exists()) {
b = file.delete();
} else {
log.info("文件不存在,路径:" + uploadFile.getFilesPath());
throw new RuntimeException("文件不存在");
}
if (b) {
uploadFileMapper.deleteByExample(example);
log.info("文件路径:" + uploadFile.getFilesPath() + ",删除文件成功");
} else {
log.info("文件路径:" + uploadFile.getFilesPath() + ",删除文件失败");
throw new RuntimeException("文件删除失败");
}
} catch (Exception e) {
e.printStackTrace();
throw new Exception(e.getMessage());
}
return map;
}
@Override
public Map findFileByCode(UploadFile uploadFile) throws Exception {
String s ;
Map map;
try {
if (StringUtils.isBlank(uploadFile.getBorrowCode())) {
throw new RuntimeException("报销单号不能为空");
}
UploadFile up = new UploadFile();
up.setBorrowCode(uploadFile.getBorrowCode());
UploadFile one = uploadFileMapper.selectOne(up);
if (one != null && StringUtils.isNotBlank(one.getFilesPath())) {
s = one.getFilesPath().replace(configuration.changeConfigurationAccordingToOperatingSystem(), "").trim();
File file = new File(one.getFilesPath());
//数据库、硬盘 查询
if (StringUtils.isBlank(s) || !file.exists()) {
throw new RuntimeException("文件不存在");
}
one.setFilesPath(s);
}
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
throw new Exception(e.getMessage());
}
map = new HashMap<>();
map.put("data", uploadFile);
return map;
}
/**
* 检查文件是否是允许的类型
*
* @param uploadFile
*/
private void checkFileType(MultipartFile uploadFile) {
String type = uploadFile.getContentType();
//校验文件类型是否被允许可以上传
if (!uploadProperties.getAllowTypes().contains(type)) {
log.info("该文件类型不允许上传,{}", type);
throw new RuntimeException("该文件类型不允许上传,目前只允许的上传类型:" + uploadProperties.getAllowTypes());
}
if (uploadFile.getSize() > uploadProperties.getFileSize()) {
log.info("本次上传附件大小:{},最大允许上传附件大小为:{}", convertFileSize(uploadFile.getSize()), convertFileSize(uploadProperties.getFileSize()));
throw new RuntimeException("允许上传的附件最大为:" + convertFileSize(uploadProperties.getFileSize()));
}
}
/**
* 将字节大小转换为KB/MB/GB/B
*
* @param size
* @return
*/
public static String convertFileSize(long size) {
long kb = 1024;
long mb = kb * 1024;
long gb = mb * 1024;
if (size >= gb) {
return String.format("%.1f GB", (float) size / gb);
} else if (size >= mb) {
float f = (float) size / mb;
return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
} else if (size >= kb) {
float f = (float) size / kb;
return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
} else {
return String.format("%d B", size);
}
}
}
10.返回状态码
package com.dhcc.znbt.business.mybatis.cw.base;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*/
public class ResponseBean extends HashMap {
private static final long serialVersionUID = 1L;
public ResponseBean() {
}
public ResponseBean put(String key, Object value) {
super.put(key, value);
return this;
}
public static ResponseBean success(Object map) {
ResponseBean r = new ResponseBean();
r.put("statusCode", 200);
r.put("massage", "文件操作成功!");
r.put("data", map);
return r;
}
public static ResponseBean suc(Map oMap) {
ResponseBean r = new ResponseBean();
Map map = new HashMap<>();
map.put("statusCode", 200);
map.put("massage", "查询成功!");
map.put("data",oMap.get("data"));
r.put("data", map);
return r;
}
public static ResponseBean fail(String message) {
ResponseBean r = new ResponseBean();
Map map = new HashMap<>();
map.put("statusCode", 500);
map.put("massage", message);
r.put("data", map);
return r;
}
}
11、文件上传实体类
package com.dhcc.znbt.business.mybatis.uploadFile.model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Table;
/**
* @Author: Lucifer
* @Date: 2019/1/21 15:38
* @Description: 文件上传实体类
*/
@Data
@Table(name = "upload_file")
public class UploadFile {
@ApiModelProperty(value = "ID")
@Column(name = "id")
private String uid;
@ApiModelProperty(value = "上传时间")
@Column(name = "uploadTime")
private Long uploadTime;
@ApiModelProperty(value = "文件名")
@Column(name = "filesName")
private String name;
@ApiModelProperty(value = "文件地址")
@Column(name = "filesPath")
private String filesPath;
@ApiModelProperty(value = "文件类型")
@Column(name = "filesType")
private String filesType;
@ApiModelProperty(value = "文件大小")
@Column(name = "fileSize")
private Long size;
@ApiModelProperty(value = "报销单号")
@Column(name = "borrowCode")
private String borrowCode;
@ApiModelProperty(value = "上传人")
@Column(name = "createUser")
private String createUser;
}
12、springboot配置文件
xxxx:
upload:
filePath: #上传存储本地文件的位置
windowsBackPart: uploadFile\ #本地或者生产 完整路径:如:D:\uploadFile\,此处不需要加盘符 盘符动态获取,如:D:\
linux: /home/xxxx/uploadFile/ #测试
mac: /usr/uploadFile/
allowTypes: #允许上传的文件类型
- application/pdf
fileSize: 10485760 #字节,允许上传文件大小10M
13、如果想实现文件预览。可以利用Tomcat。本地的话,谷歌浏览器为了考虑安全性,是不允许js去打开本地文件,比如:D:\A.pdf,当然直接复制文件路径在浏览器是可以打开的,不过对于需要前端打开本地文件,这个好像并没有什么作用,因此,需要使用虚拟路径。
参考另一篇博客:通过tomcat 用ip访问Linux路径下的文件