文件上传下载
文件上传下载功能几乎遍布于所有的软件系统中,其核心的原理机制主要是对文件流的读写处理,开发者既可以手动编写处理文件流底层的程序,也可以调用第三方开源组件提高开发效率。开发过程中常引用Apache下的commons-fileupload和commons-io组件来处理文件上传下载,组件封装了底层技术细节,使文件的上传下载功能实现变得更加简单。
由于B/S架构系统的流行趋势,开发基于Web的应用逐渐成为了开发者的首选,因此本文主要描述Java-Web文件上传下载的核心原理。
一 文件上传
文件上传通常需要一个提交表单,该表单作为用户选择文件并上传的入口,后台服务器端会有处理文件上传的action,如下的表单信息表明:当用户选择了文件并点击提交按钮时,将以POST请求的方式提交给UploadHandleServlet处理,服务器端的UploadHandleServlet读取上传文件的信息并写入指定的存储路径。
<form action="${pageContext.request.contextPath}/servlet/UploadHandleServlet" enctype="multipart/form-data" method="post"> 上传用户:<input type="text" name="username"><br/> 上传文件1:<input type="file" name="file1"><br/> 上传文件2:<input type="file" name="file2"><br/> <input type="submit" value="提交"> </form>
处理文件上传的POST请求时,创建commons-fileupload组件中的DiskFileItemFactory、ServletFileUpload类,其中DiskFileItemFactory为处理文件的工厂类,可设置工厂的缓冲区大小及文件上传时生成的临时文件保存目录;ServletFileUpload是文件处理的核心类,可以解析上传文件的信息、限制上传文件的大小和文件上传容量上限并监听文件上传的进度,核心代码如下:
package com.learn.FileUploadDownload.Controller; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadBase; import org.apache.commons.fileupload.ProgressListener; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.UUID; /** * 文件上传处理Servlet * Created by lfq on 2017/7/7. */ public class UploadHandleServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取上传文件的保存路径,为保证数据安全性,应将文件存储于外界无法直接访问的WEB-INF目录下 String savePath = this.getServletContext().getRealPath("/WEB-INF/upload"); //上传时生成的临时文件保存目录 String tempPath = this.getServletContext().getRealPath("/WEB-INF/temp"); File tempfile = new File(tempPath); if (!tempfile.isDirectory() && !tempfile.exists()) { System.out.println(savePath + "目录不存在,需要创建"); tempfile.mkdir(); } String message = ""; //处理文件的工厂 DiskFileItemFactory factory = new DiskFileItemFactory(); //设置工厂的缓冲区大小,当上传的文件大小超过缓冲区时,就会生成一个临时文件存放至临时目录当中 factory.setSizeThreshold(1024 * 100); //设置上传时生成的临时文件的保存目录 factory.setRepository(tempfile); //Apache文件上传组件的文件上传解析器 ServletFileUpload fileUpload = new ServletFileUpload(factory); //监听文件上传的进度 fileUpload.setProgressListener(new ProgressListener() { @Override public void update(long pByteRead, long pContextLength, int args2) { System.out.println("文件大小为:" + pContextLength + ",当前处理:" + pByteRead); } }); //设置上传单个文件的大小的最大值,目前是设置为1024*1024字节,也就是1MB fileUpload.setFileSizeMax(1024 * 10124); //设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为10MB fileUpload.setSizeMax(1024 * 1024 * 10); //设置上传编码格式为UTF-8防止中文乱码 fileUpload.setHeaderEncoding("UTF-8"); //判断提交的数据是否为表单的数据 if (!ServletFileUpload.isMultipartContent(request)) { return; } try { //解析请求中上传文件的信息并返回List<FileItem> List<FileItem> fileItemList = fileUpload.parseRequest(request); //遍历文件信息 for (FileItem item : fileItemList) { //若为普通的表单域,则打印域名和值 if (item.isFormField()) { String name = item.getFieldName(); String value = item.getString("UTF-8"); System.out.println(name + "=" + value); } else { String fileName = item.getName(); System.out.println(fileName); if (fileName == null || fileName.trim().equals("")) { continue; } fileName = fileName.substring(fileName.lastIndexOf("\\") + 1); String saveFileName = makeFileName(fileName); String saveFilePath = makeFileSavePath(fileName, savePath); //读入文件流 InputStream in = item.getInputStream(); //写文件至目标路径 FileOutputStream out = new FileOutputStream(saveFilePath + "\\" + saveFileName); //设置缓冲区 byte buffer[] = new byte[1024]; int length = 0; while ((length = in.read(buffer)) > 0) { out.write(buffer, 0, length); } //关闭输入流 in.close(); //关闭输出流 out.close(); //删除临时文件 item.delete(); message = "文件上传成功!"; } } }catch (FileUploadBase.FileSizeLimitExceededException e){ e.printStackTrace(); request.setAttribute("message", "单个文件超过最大值"); request.getRequestDispatcher("/message.jsp").forward(request, response); return; }catch (FileUploadBase.SizeLimitExceededException e){ e.printStackTrace(); request.setAttribute("message", "上传文件总量超过最大值"); request.getRequestDispatcher("/message.jsp").forward(request, response); return; } catch (Exception e) { message="文件上传失败"; e.printStackTrace(); } request.setAttribute("message", message); request.getRequestDispatcher("/message.jsp").forward(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } //获取文件保存的名称 private String makeFileName(String originalFile) { //为防止文件覆盖,产生唯一的文件名 return UUID.randomUUID().toString() + "_" + originalFile; } //获取文件保存的路径 private String makeFileSavePath(String saveFileName, String saveFilePath) { int hashCode = saveFileName.hashCode(); int dir1 = hashCode & 0xf; int dir2 = (hashCode & 0xf0) >> 4; //构造新目录 String dir = saveFilePath + "\\" + dir1 + "\\" + dir2; File file = new File(dir); if (!file.exists()) { file.mkdirs(); } return dir; } }
需要注意的是,为防止文件的覆盖,常需要产生唯一的文件名并将同一个目录下的文件打散,通过以上的文件流读写操作便可以将上传的文件存储于目标路径中,以上的注释解释了大部分技术细节,再此不再赘述。
二 文件下载
假设以上的操作未出现异常,则文件被成功的上传至/WEB-INF/upload路径下,现在需要提供文件下载链接列表,完成文件的下载,该操作需要首先遍历服务器存储上传文件路径下的所有文件,然后渲染所有文件列表信息至页面供用户下载,所以服务器端需要读取存储文件列表的Servlet,核心代码如下:
package com.learn.FileUploadDownload.Controller; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 列出下载目录下的所有文件 * Created by lfq on 2017/7/8. */ public class ListFileServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取上传文件保存的目录 String fileSavePath = this.getServletContext().getRealPath("/WEB-INF/upload"); Map<String, String> fileNameMap = new HashMap<>(); listFile(new File(fileSavePath), fileNameMap); request.setAttribute("fileNameMap", fileNameMap); request.getRequestDispatcher("/listFile.jsp").forward(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } //递归遍历指定目录下的文件 public void listFile(File file, Map<String, String> map) { if (!file.isFile()) { File files[] = file.listFiles(); for (File f : files) { listFile(f, map); } } else { String realName = file.getName().substring(file.getName().indexOf("_") + 1); map.put(file.getName(), realName); } } }
文件下载列表页面核心代码如下:
<c:forEach var="map" items="${fileNameMap}"> <li> <c:url value="/servlet/DownLoadServlet" var="downUrl"> <c:param name="fileName" value="${map.key}"> </c:param> </c:url> </li> ${map.value} <a href="${downUrl}">下载</a> </c:forEach>
以上ListFileServlet中会递归读取服务器端存储路径下的文件信息,并以Key(fileName),Value(realName)键值对的形式存于Map,真实开发并不会这么处理,常规会将文件的路径存储于数据库中,下载时仅需从数据库中读取对应的文件存储路径即可。
提供了文件下载链接后,则必须有处理下载请求的Servlet,本案例中DownloadSevlet用于处理文件下载请求,核心代码如下:
package com.learn.FileUploadDownload.Controller; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URLEncoder; /** * 文件下载处理Servlet * Created by lfq on 2017/7/8. */ public class DownLoadServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取下载文件名并设置编码格式 String fileName = new String(request.getParameter("fileName").getBytes("iso8859-1"), "UTF-8"); //获取文件存储路径 String fileSavePath = this.getServletContext().getRealPath("/WEB-INF/upload"); //根据文件名搜索文件真实存储路径 String path = findFileSavePathByFileName(fileName, fileSavePath); File file = new File(path + "\\" + fileName); //如果文件不存在 if (!file.exists()) { request.setAttribute("message", "您要下载的资源已被删除!!"); request.getRequestDispatcher("/message.jsp").forward(request, response); return; } //获取真实文件名 String realFileName = fileName.substring(fileName.indexOf("_") + 1); //设置响应头及文件编码格式 response.setHeader("contend-diposition", "attachment;filename=" + URLEncoder.encode(realFileName, "UTF-8")); FileInputStream in = new FileInputStream(path + "\\" + fileName); OutputStream out = response.getOutputStream(); byte buffer[] = new byte[1024]; int len = 0; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } //关闭输入流 in.close(); //关闭输出流 out.close(); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } public String findFileSavePathByFileName(String fileName, String saveRootPath) { int hashCode = fileName.hashCode(); int dir1 = hashCode & 0xf; int dir2 = (hashCode & 0xf0) >> 4; String fileDir = saveRootPath + "\\" + dir1 + "\\" + dir2; File file = new File(fileDir); if (!file.exists()) { file.mkdirs(); } return fileDir; } }
以上即为文件下载的后台处理过程,需要注意的技术细节便是需要正确设置文件编码格式,并根据上传文件名获取真实的存储路径,底层通过读写文件流完成下载过程。
三 总结
文件上传下载属于比较基础却很常用的技术,其核心就是文件的读写,但实现过程中需要注意维护文件命名的唯一性、文件的安全性(一般存储于外界无法直接访问的/WEB-INF目录下),正确设置文件格式防止中文乱码(一般统一设置编码格式为UTF-8)。实际项目中文件路径一般存储于数据库中,但为了数据安全,有时候重要文件路径需要进行加密处理,而存储的路径可以直接从数据库中读取。