文件上传是比较常用的功能,一般都是通过表单的file控件,以post方式提交到服务端。在服务端收到数据后,进行存储。
表单需要设置为multipart/form-data属性。如果是通过JS动态创建表单,则需要追加文件对象到表单中。
web前端配置正确后,在服务端如何方便的处理?在此进行一些简单说明。
框架封装了UploadUtil通用类,专用于处理文件上传相关操作。如果只需要处理文件上传,那么只需要调用saveUploadFile方法即可。同时,UploadUtil类还提供一些额外方法,对文件名和路径进行处理。
web客户端通过表单post过来文件数据,servlet收到数据后,调用saveUploadFile方法接收。即可自动完成存储与路径返回。
样例代码如下:saveUploadFile方法接收3个参数:
下图的样例,是上传图片、视频、音频文件,最大允许上传100KB。
当接收文件成功后,存储在服务端的文件路径,通过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());
}
}
框架的文件接收与保存,会默认处理很多工作,说明如下:
如果web前端是允许上传多个文件,如下代码所示,设置file控件有multiple属性。当选择多个文件上传时,服务端会收到多个文件。
如果是多个文件上传,同样调用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个文件的路径组合。
为了方便深入了解功能原理,把源码贴出来方便大家参考和理解。
定义的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文件
}
接收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;
}
//endregion
String 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;
}
目前采用的方法,是调用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;
}
文件会按照日期、类型进行分门别类的存储。这些文件夹都会自动创建,避免人为的遗忘创建,导致文件写入失败。
目前已分类的有:图片、视频、音频、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();
}
在服务器验证文件是否已存在。如果已存在,则进行重名;不存在,则写入。不会采用覆盖现有文件的方式。最终获取一个可用的文件名。
//生成文件名。如果重名,在文件名后面追加“(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;
}
如果在一个现有的文件夹路径上,追加一个文件名,变成文件路径,就需要合并路径。如果仅靠手动的去处理路径字符串,既要考虑正斜杠”/“和反效果”\“的问题,还要考虑前后路径的首尾是否有斜杠的问题。处理起来比较麻烦,所以这里借用系统自带的File类来处理。
//合并路径。传入父路径和子路径,合并为一个完整路径
public static String combineFilePath(String parentPath, String childPath) {
File fileDir = new File(parentPath, childPath);
return fileDir.getPath();
}
缩略图文件名会在原文件名后面追加”_S“,其他部分与原文件名路径保持一致。
//根据文件路径,生成缩略图路径
public static String getThumbPathFromOriginal(String orginalPath) {
if (StringUtil.isEmpty(orginalPath)) {
return "";
}
return getFileNameNoExt(orginalPath) + "_S." + getFileExtName(orginalPath);
}
去掉缩略图文件名中的”_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);
}
去掉文件的扩展名(包括点后)。
public static String getFileNameNoExt(String fileName) {
if (StringUtil.isEmpty(fileName)) {
return "";
}
return fileName.substring(0, fileName.lastIndexOf("."));
}
获取文件的扩展名,根据点号分隔获取。
public static String getFileExtName(String fileName) {
if (StringUtil.isEmpty(fileName)) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
没有直接采用字符串的contains方法,而是要在字符串前后都加上分隔符,否则会存在判断逻辑漏洞。
//判断文件类型是否包含
public static boolean allowFileType(String allowFileTypes, String fileNameExt) {
//不限制,就允许
if (StringUtil.isEmpty(allowFileTypes) || StringUtil.isEmpty(fileNameExt)) {
return true;
}
return ("#" + allowFileTypes.toLowerCase() + "#").contains("#" + fileNameExt.toLowerCase() + "#");
}
根据文件扩展名,进行文件类别归类。
//根据文件名,获取文件类型
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;
}
}