文件上传接收处理,以及缩略图生成,辅助类UploadUtil的基本使用

1.需求场景 文件上传是比较常用的功能,一般都是通过表单的file控件,以post方式提交到服务端。在服务端收到数据后,进行存储。
表单需要设置为multipart/form-data属性。如果是通过JS动态创建表单,则需要追加文件对象到表单中。
web前端配置正确后,在服务端如何方便的处理?在此进行一些简单说明。
2.使用说明 框架封装了UploadUtil通用类,专用于处理文件上传相关操作。如果只需要处理文件上传,那么只需要调用saveUploadFile方法即可。同时,UploadUtil类还提供一些额外方法,对文件名和路径进行处理。
2.1.接收文件上传 web客户端通过表单post过来文件数据,servlet收到数据后,调用saveUploadFile方法接收。即可自动完成存储与路径返回。
样例代码如下:saveUploadFile方法接收3个参数:

  1. request对象。当前servlet的request对象,会从servlet中取文件流数据。
  2. 允许上传的文件扩展名列表,为空则不限制上传文件类型。样例格式:jpg#jpeg#png#bmp#gif#mp4#avi#mp3
  3. 允许上传的文件大小(单位字节kb)。
下图的样例,是上传图片、视频、音频文件,最大允许上传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()); } }

框架的文件接收与保存,会默认处理很多工作,说明如下:
  • 图片类型文件上传后,会默认生成缩略图。
  • 缩略图的文件名与原图文件名一致,会在后面追加“_S”。比如原文件名为:风景图.jpeg,那么缩略图文件名为:风景图_S.jpeg。在列表页和详情页,会展示缩略图。点击缩略图后,会链接到原图。这样在列表页就可以快速加载图片。
  • 缩略图的尺寸,为原图等比例缩放。最宽400像素,最高400像素。宽和高哪个最先达到400像素,就以哪个为准。
  • 文件上传到服务器后,不会自动更名。慎重考虑,因为很多附件,在上传之前,就是正规的名称。特别是一些文件、资料。变更名字后,都是一些无意义的标识,当下载之后,还需要重新改名,比较麻烦。
  • 重名处理。由于上传文件不会自动更名,所以多次上传同一个文件时,就会出现重名问题。为了安全起见,不采用覆盖现有文件的方式,而是对新上传的文件重新命名。重命名的方式,采用Windows系统重名方式,自动在原文件名的后面加上数字后缀。比如文件名为:新建图片.jpg。如果重名,则更名为:新建图片(2).jpg;如果还重名,则更名为:新建图片(3).jpg;直到最后不重名。
  • 文件名中会去掉英文逗号。由于多文件上传时,会将多个文件的路径统一保存在一个字段里,中间使用英文逗号隔开。所以虽然文件不会更名,但是如果文件名中有英文逗号,则会被过滤掉。
2.2.多文件上传 如果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个文件的路径组合。
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.前端样例

    推荐阅读