18年初跳槽来到新公司,在开发中惊奇的发现公司项目中的文件上传功能竟然存在多个接口,他们为每个需要文件上传的业务实现了接口,仔细看了下这些上传文件代码,他们主要区别在于每个业务存储的文件类型、文件大小、是否要压缩需要限制或者是文件路径需要指定不同。这几个特性完全可以配置化定义规则(上家公司就是这么做的,配置化实现公用文件上传接口是一个正确的选择)。究其原因可能大家害怕更改之前的接口会照成bug,索性自己重写一个上传接口(反正是复制粘贴过来改下内容吧)。
下面我们进入主题,如何实现一个根据配置化定义规则的文件上传设计
后端服务我们直接使用springboot2.X框架,文件上传相关配置信息将入数据库,这样可支持动态修改。测试用例我提供一个html页面,或者你可以直接使用postman。更详细的内容处理请看代码(实现很简单的)
API信息:
接口地址:http://127.0.0.1:8080/uploadFile
请求格式:post | multipart/form-data;
参数名 | 必填/类型 | 说明 |
---|---|---|
bizType | 是/String | 不同业务对应不同的类型,根据此类型查找文件上传的配置 |
bizId | 否/Long | 业务id,可以通过该id找到对于的业务信息 |
isShrink | 否(默认false)/boolean | 是否需要压缩代码为了简介,暂未实现支持是否压缩 |
file | 是 | 文件信息,支持多文件上传 |
返回列表 JSON: [FileInfo]
文件信息配置表 file_conf , 不同业务对应不同的配置,这里主要考虑了文件类型、文件大小、文件存储路径等。(甚至可以扩展出存储目标服务器、文件有效期等规则)
字段 | 类型 | 约束 | 说明 |
---|---|---|---|
id | int(11) | 必填 | 自增主键id |
bize_type | varchar(20) | 必填 | 业务类型,不同业务不同的类型 |
file_type_limit | varchar(200) | 非必填 | 允许上传的文件类型(mine-type标准),为空时不限制类型 |
file_size_limit | varchar(20) | 非必填 | 允许上传的文件大小(kb),为空时不限制大小 |
path | varchar(50) | 必填 | 服务器存储文件的路径 |
description | varchar(100) | 非必填 | 描述,如描述该业务类型对应的文件上传业务功能的业务表 |
resource_realm | varchar (100) | 必填 | 外部访问文件资源相对根路径 |
enabled | tinyint(4) | 必填 | 是否可用(默认1可用,0禁用),用于禁止某个业务上传文件的功能 |
creat_time | datetime | 必填 | 创建时间 |
last_update_time | datetime | 非必填 | 最近修改时间 |
文件信表 file_info ,主要作用保存文件存储的相关信息,方便某些业务需要统计上传文件或删除文件时需要
字段 | 类型 | 约束 | 说明 |
---|---|---|---|
id | int(11) | 必填 | 自增主键id |
bize_type | varchar(20) | 必填 | 业务类型 |
bize_id | int(11) | 非必填 | 业务id |
original_name | varchar (255) | 必填 | 文件原名称 |
new_name | varchar(50) | 必填(唯一) | 文件新名称(随机码 |
file_type | varchar(20) | 必填 | 文件类型 |
file_size | varchar(20) | 必填 | 文件大小(kb) |
file_path | varchar(200) | 必填 | 文件服务器存储绝对路径 |
relative_path | varchar(200) | 必填 | 文件相对路径,域名+此字段为该资源的请求地址 |
creat_time | datetime | 必填 | 创建时间 |
last_update_time | datetime | 非必填 | 最近修改时间 |
del_flag | tinyint(1) | 必填 | 逻辑删除(默认0正常,1文件已被物理删除) |
对应数据库表结构建立对应实体类`
/**
* 文件配置信息
* table: file_conf
* @author lilee
* @version 1.0.0
* @date 2018/12/20 14:55
*/
public class FileConf {
private Long id; // 主键ID
private String bizType; // 上传服务类型
private String fileTypeLimit; // 文件类型(mine-type标准),为空不限制上传类型
private String fileSizeLimit; //(kb)文件限制大小,为空不限制上传大小(但要满足框架支持的上传文件大小)
private String path; // 服务器文件夹路径
private String description; // 描述
private String resourceRealm; // 访问资源路径
private Boolean enabled; // 是否可用(默认1可用,0禁用)
private Date createTime; // 创建时间
private Date lastUpdateTime; // 最后修改时间
// setter & getter
}
/**
* 文件信息
* table: file_info
* @author lilee
* @version 1.0.0
* @date 2018/12/20 14:55
*/
public class FileInfo {
private Long id; // 主键ID
private String originalName; // 文件原名称
private String newName; // 文件新名称
private String fileType; // 文件类型(image/jpg, image/png, video/mp4, xsl,doc等)
private String fileSize; // 文件大小(kb)
private String filePath; // 文件服务器存储路径
private String relativePath; // 文件相对路径
private Long bizId; // 业务ID
private String bizType; // 上传服务类型(业务类型)
private Date createTime; // 创建时间
private Date lastUpdateTime; // 最后修改时间
private Boolean delFlag; // 数据删除标记0=正常,1=文件已物理删除
// setter & getter
dao层,本实例为了简单,并未提供数据库操作相关代码,该模块需要用户根据自己项目架构自己实现。
当前文件存储到/home/data下
@Repository
public class FileConfDao {
// 根据业务类型bizType获取该业务的配置信息
public FileConf selectByBizType(String bizType) {
// todo 为了简单,未正真的对数据库操作
// FileConf fileConf = dbdao.findByBizType(bizType);
FileConf fileConf = new FileConf();
fileConf.setBizType(bizType);
fileConf.setPath("/home/data");
fileConf.setResourceRealm("/res");
fileConf.setEnabled(true);
return fileConf;
}
// 存储文件的信息
public FileInfo insert(FileInfo fileInfo) {
// dbdao.insert(fileInfo)
return fileInfo;
}
}
该模块实现文件上传的核心,主要包括对文件参数信息验证,对文件上传是否符合业务的配置验证、对配置规则的验证
/**
* TODO 文件上传service
*
* @author lilee
* @version 1.0.0
* @date 2018/12/20 14:55
*/
@Service
public class FileUploadService {
@Resource
private FileConfDao fileConfDao;
// @Resource
// private FileInfoDao fileInfoDao;
protected static Logger log = LoggerFactory.getLogger(FileUploadService.class);
/**
* 文件上传
* @param mpfList 文件信息集
* @param bizType 业务类型(必传)
* @param bizId 业务id
* @param extraPath 额外的路径,首部和结尾不能带斜杠'/'
* @return
*/
public List<FileInfo> uploadFile(List<MultipartFile> mpfList, String bizType, Long bizId, String extraPath) {
// 验证数据begin
// 获取对应业务文件配置信息
FileConf fileConf = this.fileConfDao.selectByBizType(bizType);
if(fileConf == null){
log.info("file conf is null"); // 打印文件配置信息
return null;
}
// 验证文件信息是否符合配置信息
if (!validateFileInfo(mpfList, fileConf)) {
// 验证失败
log.info("fileInfo is error"); // 打印文件配置信息
return null;
}
// 信息验证end
List<FileInfo> files = new ArrayList<>();
FileInfo fileInfo = null;
String path = fileConf.getPath(); // 文件存储的目录
// 获取相对路径,由file_conf、额外路径
String relativePath = fileConf.getResourceRealm() + "/"
+ (StringUtils.isEmpty(extraPath) ? "" : extraPath + "/");
// 验证服务器存储路径是否存在,若不存在,则新建文件夹
File serFile = new File(path + relativePath);
if (!serFile.exists()) {
serFile.mkdirs();
}
// 循环上传文件
for (MultipartFile mpf : mpfList) {
String originalFileName = mpf.getOriginalFilename(); // 获取源文件名
// 生成新文件名
String newFileName = "F" + UUID.randomUUID().toString().replace("-", "").toUpperCase()
+ originalFileName.substring(originalFileName.lastIndexOf("."));
// 组装数据
fileInfo = new FileInfo();
fileInfo.setOriginalName(originalFileName);
fileInfo.setFileSize(String.valueOf(mpf.getSize() / 1024)); // 单位(kb)
fileInfo.setFileType(mpf.getContentType()); // 文件类型
fileInfo.setNewName(newFileName); // 文件新名字
fileInfo.setRelativePath(relativePath + newFileName); // 文件相对路径
fileInfo.setFilePath(path + relativePath + newFileName); // 文件物理路径
fileInfo.setBizType(bizType);
fileInfo.setBizId(bizId);
fileInfo.setDelFlag(false);
// 存储文件并记录到数据库
try {
FileCopyUtils.copy(mpf.getBytes(), new FileOutputStream(fileInfo.getFilePath()));
fileConfDao.insert(fileInfo);
} catch (IOException e) {
log.error("upload file error!", e);
return null;
}
files.add(fileInfo);
}
return files;
}
private boolean validateFileInfo(List<MultipartFile> mpfList, FileConf fileConf) {
if (mpfList == null || fileConf == null) { return false; }
for (MultipartFile mpf : mpfList) {
// 验证文件大小是否超出配置大小
if (!StringUtils.isEmpty(fileConf.getFileSizeLimit()) && mpf.getSize() / 1024 > Integer.parseInt(fileConf.getFileSizeLimit())) {
return false;
}
// 验证文件类型是否符合文件配置的要求
if (!StringUtils.isEmpty(fileConf.getFileTypeLimit()) && fileConf.getFileTypeLimit().indexOf(mpf.getContentType()) < 0) {
return false;
}
}
return true;
}
}
****简单controller****
@RestController
public class FileUploadController {
@Resource
private FileUploadService fileUploadService;
/**
* 文件上传接口
* @param request
* @param bizType 业务类型(必传)
* @param bizId 业务id
* @param extraPath 额外的路径,首部和结尾不能带斜杠'/'
* @return
*/
@RequestMapping(value ="/uploadFile", method = RequestMethod.POST)
public List<FileInfo> uploadFile(MultipartHttpServletRequest request, String bizType, Long bizId, String extraPath) {
int count = 0;
List<FileInfo> result = this.fileUploadService.uploadFile(request.getMultiFileMap().get("fileData"), bizType, bizId, extraPath);
return result;
}
}
使用postman测试更简单,记得考虑跨越问题
引用博客
https://www.cnblogs.com/tianyuchen/p/5594641.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>fileUpload Test</title>
<script type="text/javascript">
var xhr;
var ot;//
var oloaded;
//上传文件方法
function upladFile() {
var url = "http://127.0.0.1:8080/uploadFile"; // 接收上传文件的后台地址
var form = new FormData(); // FormData 对象
var fileObj = document.getElementById("file").files; // js 获取文件对象
if (fileObj != null) {
for (var i=0; i< fileObj.length; i++) {
form.append('fileData', fileObj[i], fileObj[i].name);
}
}
xhr = new XMLHttpRequest(); // XMLHttpRequest 对象
xhr.open("post", url, true); //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
xhr.onload = uploadComplete; //请求完成(成功)
xhr.onerror = uploadFailed; //请求失败
xhr.upload.onprogress = progressFunction; //【上传进度调用方法实现】
xhr.upload.onloadstart = function(){ //上传开始执行方法(初始)
ot = new Date().getTime(); //设置上传开始时间
oloaded = 0; //设置上传开始时,以上传的文件大小为0
};
xhr.send(form);
}
//上传进度实现方法,上传过程中会频繁调用该方法
function progressFunction(evt) {
var progressBar = document.getElementById("progressBar");
var percentageDiv = document.getElementById("percentage");
// event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
if (evt.lengthComputable) {
progressBar.max = evt.total;
progressBar.value = evt.loaded;
percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
}
var time = document.getElementById("time");
var nt = new Date().getTime();//获取当前时间
var pertime = (nt-ot) / 1000; //计算出上次调用该方法时到现在的时间差,单位为s
ot = new Date().getTime(); //重新赋值时间,用于下次计算
var perload = evt.loaded - oloaded; //计算该分段上传的文件大小,单位b
oloaded = evt.loaded;//重新赋值已上传文件大小,用以下次计算
//上传速度计算
var speed = perload / pertime; //单位b/s
var bspeed = speed;
var units = 'b/s';//单位名称
if(speed/1024 > 1){
speed /= 1024;
units = 'k/s';
}
if(speed/1024 > 1){
speed /= 1024;
units = 'M/s';
}
speed = speed.toFixed(1);
//剩余时间
var resttime = ((evt.total - evt.loaded) / bspeed).toFixed(1);
time.innerHTML = ',速度:' + speed + units + ',剩余时间:' + resttime + 's';
if(bspeed == 0)
time.innerHTML = '上传已取消';
}
//上传成功响应
function uploadComplete(evt) {
alert("上传成功!");
}
//上传失败
function uploadFailed(evt) {
alert("上传失败!");
}
//取消上传
function cancleUploadFile(){
xhr.abort();
}
</script>
</head>
<body>
<progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
<span id="percentage"></span><span id="time"></span>
<br /><br />
<input type="file" id="file" name="myfile" multiple="multiple"/>
<input type="button" onclick="upladFile()" value="上传(多选)" />
<input type="button" onclick="cancleUploadFile()" value="取消" />
</body>
</html>
跨域问题解决
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("PUT", "DELETE","GET","POST")
.allowedHeaders("*")
.exposedHeaders("access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin",
"access-control-max-age",
"X-Frame-Options")
.allowCredentials(false).maxAge(3600);
}
};
}
综上一个根据配置规则定义的公用上传文件设计便完成了,这样我们项目所有和上传文件相关的业务都可以使用该接口,只需要简单的在数据库中配置下和业务相关的信息便可。当然可能有些业务还需要更细的定制,如要指定存储服务器,是否需要压缩,一定时间内需要删除文件等等,那你只需在该设计上添加新的配置字段,然后实现对该配置字段的支持就行了。