文件上传接收处理,以及缩略图生成,辅助类UploadUtil的基本使用
1.需求场景 文件上传是比较常用的功能,一般都是通过表单的file控件,以post方式提交到服务端。在服务端收到数据后,进行存储。
表单需要设置为multipart/form-data属性。如果是通过JS动态创建表单,则需要追加文件对象到表单中。
web前端配置正确后,在服务端如何方便的处理?在此进行一些简单说明。
2.使用说明 框架封装了UploadUtil通用类,专用于处理文件上传相关操作。如果只需要处理文件上传,那么只需要调用saveUploadFile方法即可。同时,UploadUtil类还提供一些额外方法,对文件名和路径进行处理。
2.1.接收文件上传 web客户端通过表单post过来文件数据,servlet收到数据后,调用saveUploadFile方法接收。即可自动完成存储与路径返回。
样例代码如下:saveUploadFile方法接收3个参数:
- request对象。当前servlet的request对象,会从servlet中取文件流数据。
- 允许上传的文件扩展名列表,为空则不限制上传文件类型。样例格式:jpg#jpeg#png#bmp#gif#mp4#avi#mp3
- 允许上传的文件大小(单位字节kb)。
当接收文件成功后,存储在服务端的文件路径,通过noCodeResult.getData()进行获取。返回的文件路径样例为:UploadFiles/Pics/202002/项目线路图(2)_S.png
如果上传失败,则通过 noCodeResult.getInfo()获取错误提示信息。
//上传图片和文件
private void uploadFile(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//处理上传。可上传多个文件,每个文件都会放到相应的文件类型文件夹中。每个文件遇到重名,都会自动更名。
//最终如果是多个文件,则多个文件的路径会组合在一起,存放到data数据字段中,中间用英文逗号隔开。
NoCodeResult noCodeResult = UploadUtil.saveUploadFile(request, "jpg#jpeg#png#bmp#gif#mp4#avi#mp3", 100*1024);
//返回上传结果
if (noCodeResult.isSuccess()) {
ExceptionUtil.printlnSuccess(response, "上传成功", noCodeResult.getData());
} else {
ExceptionUtil.printlnFailure(response, noCodeResult.getInfo());
}
}
框架的文件接收与保存,会默认处理很多工作,说明如下:
- 图片类型文件上传后,会默认生成缩略图。
- 缩略图的文件名与原图文件名一致,会在后面追加“_S”。比如原文件名为:风景图.jpeg,那么缩略图文件名为:风景图_S.jpeg。在列表页和详情页,会展示缩略图。点击缩略图后,会链接到原图。这样在列表页就可以快速加载图片。
- 缩略图的尺寸,为原图等比例缩放。最宽400像素,最高400像素。宽和高哪个最先达到400像素,就以哪个为准。
- 文件上传到服务器后,不会自动更名。慎重考虑,因为很多附件,在上传之前,就是正规的名称。特别是一些文件、资料。变更名字后,都是一些无意义的标识,当下载之后,还需要重新改名,比较麻烦。
- 重名处理。由于上传文件不会自动更名,所以多次上传同一个文件时,就会出现重名问题。为了安全起见,不采用覆盖现有文件的方式,而是对新上传的文件重新命名。重命名的方式,采用Windows系统重名方式,自动在原文件名的后面加上数字后缀。比如文件名为:新建图片.jpg。如果重名,则更名为:新建图片(2).jpg;如果还重名,则更名为:新建图片(3).jpg;直到最后不重名。
- 文件名中会去掉英文逗号。由于多文件上传时,会将多个文件的路径统一保存在一个字段里,中间使用英文逗号隔开。所以虽然文件不会更名,但是如果文件名中有英文逗号,则会被过滤掉。
如果是多个文件上传,同样调用saveUploadFile方法即可完成接收处理。即saveUploadFile方法支持接收单个和多个文件上传。
当多个文件都成功保存后,所有文件的路径会组合为一整个路径字符串,中间用英文逗号隔开。
比如:UploadFiles/Pics/202002/项目线路图(2)_S.png,UploadFiles/Pics/202002/(201565185353)工作流并行会审_S.png,UploadFiles/Pics/202002/IMG_20181019_151336_S.png
如果中途有文件上传失败,则会跳过,继续处理下一个。比如中途有的文件大小超标,有的文件类型不允许。选择了5个文件上传,可能只成功上传4个。如果成功上传4个,则会返回4个文件的路径组合。
3.源码解析 为了方便深入了解功能原理,把源码贴出来方便大家参考和理解。
3.1.通用类UploadUtil源码 3.1.1.依赖引入与变量定义
定义的UploadUtil通用类,引入了Thumbnails进行缩略图处理,以及apache.commons.fileupload进行文件上传处理。
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import tech.qidian.dev.admincommon.entity.NoCodeResult;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class UploadUtil { //定义文件类型
public static final int FILE_TYPE_UNKNOWN = 0;
//未知文件
public static final int FILE_TYPE_FILE = 1;
//普通文件
public static final int FILE_TYPE_IMAGE = 2;
//图片
public static final int FILE_TYPE_VIDEO = 3;
//视频
public static final int FILE_TYPE_AUDIO = 4;
//音频
public static final int FILE_TYPE_EXCEL = 5;
//Excel文件
}
3.1.2.saveUploadFile:保存上传文件
接收servlet数据,从servlet中直接取文件流,进行处理。所以要注意,提交文件上传,最好是一个单独的表单,不要再混合提交其他的表单字段数据进来。因为流只允许接收一次,如果中途拦截取了参数数据,可能导致取不到文件流。
//上传多个文件。每个文件都根据类型存放到相应的文件夹中。最终将所有文件组合为一个路径列表,以英文逗号隔开。
public static NoCodeResult saveUploadFile(HttpServletRequest request, String allowFileTypes, long allowFileSize) {
NoCodeResult noCodeResult = new NoCodeResult();
noCodeResult.setFailureInfo("上传失败,未知原因");
//根目录绝对路径
String rootRealDir = request.getServletContext().getRealPath("/");
//region 初始化存储临时目录
String tempPath = request.getServletContext().getRealPath("/WEB-INF/Temp");
if (!createFileDir(tempPath)) {
noCodeResult.setFailureInfo("临时目录创建失败");
return noCodeResult;
}
//endregionString fileName, filePath, fileThumbPath;
try {
//region 基本设置与对象创建
//1、创建一个DiskFileItemFactory工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
//设置工厂的缓冲区的大小,当上传的文件大小超过缓冲区的大小时,就会生成一个临时文件存放到指定的临时目录当中。
factory.setSizeThreshold(1024 * 100);
//设置缓冲区的大小为100KB,如果不指定,那么缓冲区的大小默认是10KB//设置上传时生成的临时文件的保存目录
factory.setRepository(new File(tempPath));
//2、创建一个文件上传解析器
ServletFileUpload upload = new ServletFileUpload(factory);
//3、判断提交上来的数据是否是上传表单的数据
if (!ServletFileUpload.isMultipartContent(request)) {
noCodeResult.setFailureInfo("提交的非文件数据。请以表单form的方式提交上传。");
return noCodeResult;
}//设置上传单个文件的大小的最大值。根据设定设置,最大4G。
upload.setFileSizeMax(allowFileSize);
//设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,最大为4G
upload.setSizeMax(4L * 1024 * 1024 * 1024);
//4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List集合,每一个FileItem对应一个Form表单的输入项
List list = upload.parseRequest(request);
//endregion//region 循环处理上传文件
//文件数量索引,以及上传成功的数量索引
int index = 0, successIndex = 0;
StringBuilder sbFilesPath = new StringBuilder();
StringBuilder sbInfo = new StringBuilder();
for (FileItem item : list) {
//如果fileitem中封装的是上传文件
if (!item.isFormField()) {
//索引数量加1
index++;
//region 处理文件夹、文件名、文件路径
//得到上传的文件名称
fileName = item.getName();
//注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
//处理获取到的上传文件的文件名的路径部分,只保留文件名部分
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
if (StringUtil.isEmpty(fileName)) {
item.delete();
sbInfo.append("第").append(index).append("个文件名获取失败");
continue;
}//获取文件上传存储文件夹。图片、音频、视频、文件,分别存储到不同的文件夹。
String uploadDirAbsolute = createUploadFileDir(rootRealDir, fileName);
//初始化文件夹
if (StringUtil.isEmpty(uploadDirAbsolute)) {
sbInfo.append("第").append(index).append("个存储文件夹创建失败");
continue;
}//得到上传文件的扩展名
String fileExtName = getFileExtName(fileName);
//如果扩展名不包含在内,则不允许上传。
if (!allowFileType(allowFileTypes, fileExtName)) {
item.delete();
sbInfo.append(fileExtName).append("文件类型不允许上传");
continue;
}//判断文件是否重名。如果重名会更名,返回不重名的新名称。去掉英文逗号。
fileName = getFileNameByCheckExist(uploadDirAbsolute, fileName);
if (fileName == null) {
item.delete();
sbInfo.append("第").append(index).append("个文件名称处理失败");
continue;
}
//endregion//region 处理文件写入服务器上
//获取文件路径:文件夹+文件名
filePath = combineFilePath(uploadDirAbsolute, fileName);
//获取item中的上传文件的输入流
InputStream in = item.getInputStream();
//创建一个文件输出流
FileOutputStream out = new FileOutputStream(filePath);
//创建一个缓冲区
byte[] buffer = new byte[4096];
//判断输入流中的数据是否已经读完的标识
int len;
//循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
while ((len = in.read(buffer)) > 0) {
//使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\\" + filename)当中
out.write(buffer, 0, len);
}
//关闭输入流
in.close();
//关闭输出流
out.close();
//删除处理文件上传时生成的临时文件
item.delete();
//endregion//判断文件类型。如果是图片,则生成缩略图;如果是视频,则提取缩略图
switch (getFileTypeByName(filePath)) {
case UploadUtil.FILE_TYPE_IMAGE://生成缩略图
filePath = createThumbnailPic(filePath);
if (StringUtil.isEmpty(filePath)) {
sbInfo.append("第").append(index).append("个缩略图生成失败");
continue;
}
break;
}//设置上传成功后的文件路径。将绝对路径换成相对路径。
sbFilesPath.append(filePath.substring(rootRealDir.length()));
sbFilesPath.append(",");
//成功上传,数量加1
successIndex++;
}}
//endregion//如果成功上传文件为0个,则返回失败
if (successIndex == 0) {
noCodeResult.setFailureInfo(sbInfo.toString());
} else {
noCodeResult.setSuccessInfo("成功上传" + index + "个文件");
}//将所有文件路径返回。删除最后面的逗号
StringUtil.deleteLastString(sbFilesPath, ",");
noCodeResult.setData(sbFilesPath.toString());
} catch (FileUploadBase.FileSizeLimitExceededException e) {
noCodeResult.setFailureInfo("文件大小超出限制:" + StringUtil.getFileSizeString(allowFileSize));
ExceptionUtil.insertDB(e, noCodeResult.getInfo());
return noCodeResult;
} catch (FileUploadBase.SizeLimitExceededException e) {
noCodeResult.setFailureInfo("文件总大小超出限制:4GB");
ExceptionUtil.insertDB(e, noCodeResult.getInfo());
return noCodeResult;
} catch (Exception e) {
noCodeResult.setFailureInfo("上传解析异常");
ExceptionUtil.insertDB(e, noCodeResult.getInfo());
return noCodeResult;
}return noCodeResult;
}
3.1.3.createThumbnailPic:生成缩略图
目前采用的方法,是调用Thumbnails进行等比例缩略图生成。缩略图的最大高宽都是400像素。
public static String createThumbnailPic(String fileRealPath) throws IOException {
if (StringUtil.isEmpty(fileRealPath)) {
return "";
} String fileThumbnail = getThumbPathFromOriginal(fileRealPath);
Thumbnails.of(fileRealPath).size(400, 400).toFile(fileThumbnail);
return fileThumbnail;
}
3.1.4.createUploadFileDir:创建存储文件夹
文件会按照日期、类型进行分门别类的存储。这些文件夹都会自动创建,避免人为的遗忘创建,导致文件写入失败。
目前已分类的有:图片、视频、音频、Excel、文件、其他。
//生成文件上传夹,返回绝对路径。格式:D:\wwwroot\test\UploadFiles\Pics\202002。中间会根据文件类型,放到各个类型文件夹中
public static String createUploadFileDir(String dirRoot, String fileName) {
String dirUploadFiles = combineFilePath(dirRoot, "UploadFiles");
//“UploadFiles”文件夹
if (!createFileDir(dirUploadFiles)) {
return null;
}//创建图片、文件、Excel文件夹
int uploadType = getFileTypeByName(fileName);
String dirUploadType;
switch (uploadType) {
case FILE_TYPE_FILE:
dirUploadType = "Files";
break;
case FILE_TYPE_IMAGE:
dirUploadType = "Pics";
break;
case FILE_TYPE_VIDEO:
dirUploadType = "Video";
break;
case FILE_TYPE_AUDIO:
dirUploadType = "Audio";
break;
case FILE_TYPE_EXCEL:
dirUploadType = "Excels";
break;
case FILE_TYPE_UNKNOWN:
default:
dirUploadType = "Others";
break;
}dirUploadType = combineFilePath(dirUploadFiles, dirUploadType);
if (!createFileDir(dirUploadType)) {
return null;
}//创建月份文件夹
String dirDate = new SimpleDateFormat("yyyyMM").format(new Date());
dirDate = combineFilePath(dirUploadType, dirDate);
if (!createFileDir(dirDate)) {
return null;
}return dirDate;
} //创建文件夹。如果不存在,就创建;存在,就跳过。
public static boolean createFileDir(String dirPath) {
File fileDir = new File(dirPath);
//如果存在,直接返回
if (fileDir.exists() && fileDir.isDirectory()) {
return true;
}
return fileDir.mkdir();
}
3.1.5.getFileNameByCheckExist:获取可用的文件名
在服务器验证文件是否已存在。如果已存在,则进行重名;不存在,则写入。不会采用覆盖现有文件的方式。最终获取一个可用的文件名。
//生成文件名。如果重名,在文件名后面追加“(1)、(2)...”直到不重名。该功能重点是判断是否重名。
public static String getFileNameByCheckExist(String dirRealPath, String fileName) {
if (StringUtil.isEmpty(dirRealPath) || StringUtil.isEmpty(fileName)) {
return null;
}//最终文件名。替换文件名中的英文逗号,因为在多文件上传中会以英文逗号隔开。
String fileNameNew = fileName.replace(",", "");
//判断是否重名
File file = new File(dirRealPath, fileNameNew);
int index = 2;
while (file.exists()) {
fileNameNew = getFileNameNoExt(fileName) + "(" + index + ")." + getFileExtName(fileName);
file = new File(dirRealPath, fileNameNew);
index++;
}return fileNameNew;
}
3.1.6.combineFilePath:合并路径
如果在一个现有的文件夹路径上,追加一个文件名,变成文件路径,就需要合并路径。如果仅靠手动的去处理路径字符串,既要考虑正斜杠”/“和反效果”\“的问题,还要考虑前后路径的首尾是否有斜杠的问题。处理起来比较麻烦,所以这里借用系统自带的File类来处理。
//合并路径。传入父路径和子路径,合并为一个完整路径
public static String combineFilePath(String parentPath, String childPath) {
File fileDir = new File(parentPath, childPath);
return fileDir.getPath();
}
3.1.7.getThumbPathFromOriginal:根据原图路径获取缩略图路径
缩略图文件名会在原文件名后面追加”_S“,其他部分与原文件名路径保持一致。
//根据文件路径,生成缩略图路径
public static String getThumbPathFromOriginal(String orginalPath) {
if (StringUtil.isEmpty(orginalPath)) {
return "";
}return getFileNameNoExt(orginalPath) + "_S." + getFileExtName(orginalPath);
}
3.1.8.getOriginalPathFromThumbPath:根据缩略图路径获取原图路径
去掉缩略图文件名中的”_S“,返回原文件名称。传入文件路径也行。
//根据缩略图路径,获取原图路径
public static String getOriginalPathFromThumbPath(String thumbPath) {
if (StringUtil.isEmpty(thumbPath)) {
return "";
}//获取扩展名,
String fileNameNoExt = getFileNameNoExt(thumbPath);
//去除"_S"结尾
fileNameNoExt = StringUtil.deleteLastString(fileNameNoExt, "_S");
//小写"_s"结尾
fileNameNoExt = StringUtil.deleteLastString(fileNameNoExt, "_s");
return fileNameNoExt + "." + getFileExtName(thumbPath);
}
3.1.9.getFileNameNoExt:获取无扩展名的文件名或路径
去掉文件的扩展名(包括点后)。
public static String getFileNameNoExt(String fileName) {
if (StringUtil.isEmpty(fileName)) {
return "";
}
return fileName.substring(0, fileName.lastIndexOf("."));
}
3.1.10.getFileExtName:获取文件扩展名
获取文件的扩展名,根据点号分隔获取。
public static String getFileExtName(String fileName) {
if (StringUtil.isEmpty(fileName)) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
3.1.11.allowFileType:判断扩展名是否包含
【文件上传接收处理,以及缩略图生成,辅助类UploadUtil的基本使用】没有直接采用字符串的contains方法,而是要在字符串前后都加上分隔符,否则会存在判断逻辑漏洞。
//判断文件类型是否包含
public static boolean allowFileType(String allowFileTypes, String fileNameExt) {
//不限制,就允许
if (StringUtil.isEmpty(allowFileTypes) || StringUtil.isEmpty(fileNameExt)) {
return true;
}return ("#" + allowFileTypes.toLowerCase() + "#").contains("#" + fileNameExt.toLowerCase() + "#");
}
3.1.12.getFileTypeByName:获取文件类型
根据文件扩展名,进行文件类别归类。
//根据文件名,获取文件类型
public static int getFileTypeByName(String fileName) {
if (StringUtil.isEmpty(fileName)) {
return FILE_TYPE_UNKNOWN;
}
//提取扩展名
int pointIndex = fileName.lastIndexOf(".");
if (pointIndex <= 0) {
return FILE_TYPE_UNKNOWN;
}
String extName = fileName.substring(pointIndex + 1).toLowerCase();
switch (extName) {
case "pdf":
case "doc":
case "docx":
case "zip":
case "rar":
return FILE_TYPE_FILE;
//图片
case "jpg":
case "jpeg":
case "gif":
case "png":
case "bmp":
return FILE_TYPE_IMAGE;
//视频
case "mp4":
case "avi":
return FILE_TYPE_VIDEO;
//音频
case "mp3":
return FILE_TYPE_AUDIO;
case "xls":
case "xlsx":
return FILE_TYPE_EXCEL;
//其他
default:
return FILE_TYPE_UNKNOWN;
} }
3.2.前端样例
推荐阅读
- django-前后端交互
- 如何在Mac中的文件选择框中打开系统隐藏文件夹
- 使用composer自动加载类文件
- ssh生成公钥秘钥
- Android系统启动之init.rc文件解析过程
- 微信小程序基础知识
- 误删/清空.bashrc文件/内容
- iview|iview upload 动态改变上传参数
- JavaScript|vue 基于axios封装request接口请求——request.js文件
- 插件化无法获取或找到.so文件