Java
Java Web基础

Java Web文件上传和下载

简介:浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦的,所以一般选择采用Apache的开源工具common-fileupload这个文件上传组件。

1. 文件上传

浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦的,所以一般选择采用Apache的开源工具common-fileupload这个文件上传组件。这里通过Maven来管理该Jar包的依赖:

  • <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
  • <dependency>
  • <groupId>commons-fileupload</groupId>
  • <artifactId>commons-fileupload</artifactId>
  • <version>1.3.1</version>
  • </dependency>

该包还依赖commons-io:commons-io.jar这个包。

1.1. 简单上传功能

在该文章中使用表单的形式来演示上传文件的案例,首先我们需要一个上传的表单,代码如下:

  • <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  • <html>
  • <head>
  • <title>Index Page</title>
  • </head>
  • <body>
  • <form action="${pageContext.request.contextPath}/doUpload" enctype="multipart/form-data" method="post">
  • 用户名:<input type="text" name="username" /><br />
  • 文件一:<input type="file" name="file1" /><br />
  • 文件二:<input type="file" name="file2" /><br />
  • <button type="submit">提交</button>
  • </form>
  • </body>
  • </html>

在该表单中,存在一个普通文本输入框和两个文件输入框。需要注意的是,如果想要上传文件,form表单的enctype类型必须是multipart/form-data,method必须是post。这个表单在点击提交后会将数据提交到${pageContext.request.contextPath}/doUpload路径,因此我们还需要一个Servlet处理提交的数据,完整代码如下:

  • package com.coderap.fileupload;
  • import org.apache.commons.fileupload.FileItem;
  • 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;
  • public class UploadHandleServlet extends HttpServlet {
  • @Override
  • protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  • // 得到上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
  • String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
  • File file = new File(savePath);
  • // 判断上传文件的保存目录是否存在
  • if (!file.exists() && !file.isDirectory()) {
  • System.out.println(savePath + "目录不存在,需要创建");
  • // 创建目录
  • file.mkdir();
  • }
  • // 消息提示
  • String message = "";
  • try {
  • // 使用Apache文件上传组件处理文件上传步骤:
  • // 1、创建一个DiskFileItemFactory工厂
  • DiskFileItemFactory factory = new DiskFileItemFactory();
  • // 2、创建一个文件上传解析器
  • ServletFileUpload upload = new ServletFileUpload(factory);
  • // 解决上传文件名的中文乱码
  • upload.setHeaderEncoding("UTF-8");
  • // 3、判断提交上来的数据是否是上传表单的数据
  • if (!ServletFileUpload.isMultipartContent(request)) {
  • // 按照传统方式获取数据
  • return;
  • }
  • // 4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
  • List<FileItem> list = upload.parseRequest(request);
  • for (FileItem item : list) {
  • // 如果fileitem中封装的是普通输入项的数据
  • if (item.isFormField()) {
  • String name = item.getFieldName();
  • // 解决普通输入项的数据的中文乱码问题
  • String value = item.getString("UTF-8");
  • // value = new String(value.getBytes("iso8859-1"),"UTF-8");
  • System.out.println(name + "=" + value);
  • } else {
  • // 如果fileitem中封装的是上传文件
  • // 得到上传的文件名称,
  • String filename = item.getName();
  • System.out.println(filename);
  • if (filename == null || filename.trim().equals("")) {
  • continue;
  • }
  • // 注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:/a/b/1.txt,
  • // 而有些只是单纯的文件名,如:1.txt
  • // 处理获取到的上传文件的文件名的路径部分,只保留文件名部分
  • // 处理路径分隔符统一使用File.separator以消除平台差异
  • filename = filename.substring(filename.lastIndexOf(File.separator) + 1);
  • // 获取item中的上传文件的输入流
  • InputStream in = item.getInputStream();
  • // 创建一个文件输出流
  • FileOutputStream out = new FileOutputStream(savePath + File.separator + filename);
  • // 创建一个缓冲区
  • byte buffer[] = new byte[1024];
  • // 判断输入流中的数据是否已经读完的标识
  • int len = 0;
  • // 循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
  • while ((len = in.read(buffer)) > 0) {
  • // 使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + File.separator + filename)当中
  • out.write(buffer, 0, len);
  • }
  • // 关闭输入流
  • in.close();
  • // 关闭输出流
  • out.close();
  • // 删除处理文件上传时生成的临时文件
  • item.delete();
  • message = "文件上传成功!";
  • }
  • }
  • } catch (Exception e) {
  • message = "文件上传失败!";
  • e.printStackTrace();
  • }
  • request.setAttribute("message", message);
  • request.getRequestDispatcher("/message.jsp").forward(request, response);
  • }
  • @Override
  • protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  • this.doGet(req, resp);
  • }
  • }

有了上面的UploadHandleServlet,还需要将其配置到web.xml文件中:

  • <servlet>
  • <servlet-name>UploadHandleServlet</servlet-name>
  • <servlet-class>com.coderap.fileupload.UploadHandleServlet</servlet-class>
  • </servlet>
  • <servlet-mapping>
  • <servlet-name>UploadHandleServlet</servlet-name>
  • <url-pattern>/doUpload</url-pattern>
  • </servlet-mapping>

这里的url-pattern是与表单中提交的地址对应的。

另外,在这个UploadHandleServlet中还用到了message.jsp页面用做消息提示,代码如下:

  • <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  • <html>
  • <head>
  • <title>Message</title>
  • </head>
  • <body>
  • ${message}
  • </body>
  • </html>

1.2. 改进后的上传功能

上述的代码虽然可以成功将文件上传到服务器上面的指定目录当中,但是文件上传功能有许多需要注意的小细节问题,以下列出的几点需要特别注意的:

  1. 为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如放于WEB-INF目录下。
  2. 使用File.separator作为路径分隔符可以避免平台差异导致的问题。
  3. 为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名。
  4. 为防止一个目录下面出现太多文件,要使用hash算法打散存储。
  5. 要限制上传文件的最大值。
  6. 要限制上传文件的类型,在收到上传文件名时,判断后缀名是否合法。

针对上述提出的5点细节问题,我们来改进一下UploadHandleServlet,改进后的代码如下:

  • package com.coderap.fileupload;
  • 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;
  • 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 tmpFile = new File(tempPath);
  • if (!tmpFile.exists()) {
  • // 创建临时目录
  • tmpFile.mkdir();
  • }
  • // 消息提示
  • String message = "";
  • try {
  • // 使用Apache文件上传组件处理文件上传步骤:
  • // 1、创建一个DiskFileItemFactory工厂
  • DiskFileItemFactory factory = new DiskFileItemFactory();
  • // 设置工厂的缓冲区的大小,当上传的文件大小超过缓冲区的大小时,就会生成一个临时文件存放到指定的临时目录当中。
  • factory.setSizeThreshold(1024 * 100);// 设置缓冲区的大小为100KB,如果不指定,那么缓冲区的大小默认是10KB
  • // 设置上传时生成的临时文件的保存目录
  • factory.setRepository(tmpFile);
  • // 2、创建一个文件上传解析器
  • ServletFileUpload upload = new ServletFileUpload(factory);
  • // 监听文件上传进度
  • upload.setProgressListener(new ProgressListener() {
  • public void update(long pBytesRead, long pContentLength, int arg2) {
  • System.out.println("文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead);
  • }
  • });
  • // 解决上传文件名的中文乱码
  • upload.setHeaderEncoding("UTF-8");
  • // 3、判断提交上来的数据是否是上传表单的数据
  • if (!ServletFileUpload.isMultipartContent(request)) {
  • // 按照传统方式获取数据
  • return;
  • }
  • // 设置上传单个文件的大小的最大值,目前是设置为1024*1024字节,也就是1MB
  • upload.setFileSizeMax(1024 * 1024);
  • // 设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为10MB
  • upload.setSizeMax(1024 * 1024 * 10);
  • // 4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
  • List<FileItem> list = upload.parseRequest(request);
  • for (FileItem item : list) {
  • // 如果fileitem中封装的是普通输入项的数据
  • if (item.isFormField()) {
  • String name = item.getFieldName();
  • // 解决普通输入项的数据的中文乱码问题
  • String value = item.getString("UTF-8");
  • // value = new String(value.getBytes("iso8859-1"),"UTF-8");
  • System.out.println(name + "=" + value);
  • } else {// 如果fileitem中封装的是上传文件
  • // 得到上传的文件名称,
  • String filename = item.getName();
  • System.out.println(filename);
  • if (filename == null || filename.trim().equals("")) {
  • continue;
  • }
  • // 注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:/a/b/1.txt,
  • // 而有些只是单纯的文件名,如:1.txt
  • // 处理获取到的上传文件的文件名的路径部分,只保留文件名部分
  • filename = filename.substring(filename.lastIndexOf(File.separator) + 1);
  • // 得到上传文件的扩展名
  • String fileExtName = filename.substring(filename.lastIndexOf(".") + 1);
  • // 如果需要限制上传的文件类型,那么可以通过文件的扩展名来判断上传的文件类型是否合法
  • System.out.println("上传的文件的扩展名是:" + fileExtName);
  • // 获取item中的上传文件的输入流
  • InputStream in = item.getInputStream();
  • // 得到文件保存的名称
  • String saveFilename = makeFileName(filename);
  • // 得到文件的保存目录
  • String realSavePath = makePath(saveFilename, savePath);
  • // 创建一个文件输出流
  • FileOutputStream out = new FileOutputStream(realSavePath + File.separator + saveFilename);
  • // 创建一个缓冲区
  • byte buffer[] = new byte[1024];
  • // 判断输入流中的数据是否已经读完的标识
  • int len = 0;
  • // 循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
  • while ((len = in.read(buffer)) > 0) {
  • // 使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + File.separator + filename)当中
  • out.write(buffer, 0, len);
  • }
  • // 关闭输入流
  • 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);
  • }
  • /**
  • * @param filename 文件的原始名称
  • * @return uuid+"_"+文件的原始名称
  • * @Method: makeFileName
  • * @Description: 生成上传文件的文件名,文件名以:uuid+"_"+文件的原始名称
  • */
  • private String makeFileName(String filename) { // 2.jpg
  • // 为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名
  • return UUID.randomUUID().toString() + "_" + filename;
  • }
  • /**
  • * 为防止一个目录下面出现太多文件,要使用hash算法打散存储
  • *
  • * @param filename 文件名,要根据文件名生成存储目录
  • * @param savePath 文件存储路径
  • * @return 新的存储目录
  • * @Method: makePath
  • */
  • private String makePath(String filename, String savePath) {
  • // 得到文件名的hashCode的值,得到的就是filename这个字符串对象在内存中的地址
  • int hashcode = filename.hashCode();
  • int dir1 = hashcode & 0xf; // 0--15
  • int dir2 = (hashcode & 0xf0) >> 4; // 0-15
  • // 构造新的保存目录
  • String dir = savePath + File.separator + dir1 + File.separator + dir2; // upload/2/3 upload/3/5
  • // File既可以代表文件也可以代表目录
  • File file = new File(dir);
  • // 如果目录不存在
  • if (!file.exists()) {
  • // 创建目录
  • file.mkdirs();
  • }
  • return dir;
  • }
  • public void doPost(HttpServletRequest request, HttpServletResponse response)
  • throws ServletException, IOException {
  • doGet(request, response);
  • }
  • }

测试上传载功能,可以得到上传后的文件如下:

1.上传后后端处理完成的文件.png

2. 文件下载

文件下载的功能与上传类似,都是通过流将服务端相应的文件传输到浏览器。在本案例中,提供一个文件列表页面用于浏览所有的文件,并且在该页面提供下载按钮用于下载,点击下载按钮后会通过流处理将服务器上的文件传输给浏览器。

2.1. 文件列表页面

要将Web应用系统中的文件资源提供给用户进行下载,首先我们要有一个页面列出上传文件目录下的所有文件,当用户点击文件下载超链接时就进行下载操作,编写一个ListFileServlet,用于列出Web应用系统中所有提供下载的文件。这里使用ListFileServlet,代码如下:

  • package com.coderap.fileupload;
  • import java.io.File;
  • import java.io.IOException;
  • import java.util.HashMap;
  • import java.util.Map;
  • import javax.servlet.ServletException;
  • import javax.servlet.http.HttpServlet;
  • import javax.servlet.http.HttpServletRequest;
  • import javax.servlet.http.HttpServletResponse;
  • public class ListFileServlet extends HttpServlet {
  • public void doGet(HttpServletRequest request, HttpServletResponse response)
  • throws ServletException, IOException {
  • // 获取上传文件的目录
  • String uploadFilePath = this.getServletContext().getRealPath("/WEB-INF/upload");
  • // 存储要下载的文件名
  • Map<String, String> fileNameMap = new HashMap<String, String>();
  • // 递归遍历filepath目录下的所有文件和目录,将文件的文件名存储到map集合中
  • listfile(new File(uploadFilePath), fileNameMap);// File既可以代表一个文件也可以代表一个目录
  • // 将Map集合发送到listfile.jsp页面进行显示
  • request.setAttribute("fileNameMap", fileNameMap);
  • request.getRequestDispatcher("/file/listfile.jsp").forward(request, response);
  • }
  • /**
  • * @param file 即代表一个文件,也代表一个文件目录
  • * @param map 存储文件名的Map集合
  • * @Method: listfile
  • * @Description: 递归遍历指定目录下的所有文件
  • */
  • public void listfile(File file, Map<String, String> map) {
  • // 如果file代表的不是一个文件,而是一个目录
  • if (!file.isFile()) {
  • // 列出该目录下的所有文件和目录
  • File files[] = file.listFiles();
  • // 遍历files[]数组
  • for (File f : files) {
  • // 递归
  • listfile(f, map);
  • }
  • } else {
  • /**
  • * 处理文件名,上传后的文件是以uuid_文件名的形式去重新命名的,去除文件名的uuid_部分
  • file.getName().indexOf("_")检索字符串中第一次出现"_"字符的位置,如果文件名类似于:9349249849-88343-8344_1_copy.avi
  • 那么file.getName().substring(file.getName().indexOf("_")+1)处理之后就可以得到1_copy.avi部分
  • */
  • String realName = file.getName().substring(file.getName().indexOf("_") + 1);
  • // file.getName()得到的是文件的原始名称,这个名称是唯一的,因此可以作为key,realName是处理过后的名称,有可能会重复
  • map.put(file.getName(), realName);
  • }
  • }
  • public void doPost(HttpServletRequest request, HttpServletResponse response)
  • throws ServletException, IOException {
  • doGet(request, response);
  • }
  • }

这里简单说一下ListFileServlet中listfile(File, Map<String, String>)方法,该方法是用来列出目录下的所有文件的,该方法内部用到了递归,在实际开发当中,我们肯定会在数据库创建一张表,里面会存储上传的文件名以及文件的具体存放目录,我们通过查询表就可以知道文件的具体存放目录,是不需要用到递归操作的,这个例子是因为没有使用数据库存储上传的文件名和文件的具体存放位置,而上传文件的存放位置又使用了散列算法打散存放,所以需要用到递归,在递归时,将获取到的文件名存放到从外面传递到该方法里面的Map集合当中,这样就可以保证所有的文件都存放在同一个Map集合当中。

在Web.xml文件中配置ListFileServlet:

  • <servlet>
  • <servlet-name>ListFileServlet</servlet-name>
  • <servlet-class>com.coderap.fileupload.ListFileServlet</servlet-class>
  • </servlet>
  • <servlet-mapping>
  • <servlet-name>ListFileServlet</servlet-name>
  • <url-pattern>/listFileServlet</url-pattern>
  • </servlet-mapping>

展示下载文件的listfile.jsp页面如下:

  • <%@ page language="java" import="java.util.*" pageEncoding="UTF-8" %>
  • <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>下载文件显示页面</title>
  • </head>
  • <body>
  • <!-- 遍历Map集合 -->
  • <c:forEach var="me" items="${fileNameMap}">
  • <c:url value="${pageContext.request.contextPath}/doDownload" var="downurl">
  • <c:param name="filename" value="${me.key}"></c:param>
  • </c:url>
  • ${me.value}<a href="${downurl}">下载</a>
  • <br/>
  • </c:forEach>
  • </body>
  • </html>

访问ListFileServlet,就可以在listfile.jsp页面中显示提供给用户下载的文件资源,显示效果如下:

2.下载文件显示页面.png

2.2. 文件下载功能

接下来实现文件下载,编写一个用于处理文件下载的Servlet,DownloadServlet的代码如下:

  • package com.coderap.fileupload;
  • import java.io.File;
  • import java.io.FileInputStream;
  • import java.io.IOException;
  • import java.io.OutputStream;
  • import java.net.URLEncoder;
  • import javax.servlet.ServletException;
  • import javax.servlet.http.HttpServlet;
  • import javax.servlet.http.HttpServletRequest;
  • import javax.servlet.http.HttpServletResponse;
  • public class DownloadServlet extends HttpServlet {
  • public void doGet(HttpServletRequest request, HttpServletResponse response)
  • throws ServletException, IOException {
  • // 得到要下载的文件名
  • String fileName = request.getParameter("filename"); // 23239283-92489-1_copy.avi
  • fileName = new String(fileName.getBytes("iso8859-1"), "UTF-8");
  • // 上传的文件都是保存在/WEB-INF/upload目录下的子目录当中
  • String fileSaveRootPath = this.getServletContext().getRealPath("/WEB-INF/upload");
  • // 通过文件名找出文件的所在目录
  • String path = findFileSavePathByFileName(fileName, fileSaveRootPath);
  • // 得到要下载的文件
  • File file = new File(path + File.separator + fileName);
  • // 如果文件不存在
  • if (!file.exists()) {
  • request.setAttribute("message", "您要下载的资源已被删除!!");
  • request.getRequestDispatcher("/message.jsp").forward(request, response);
  • return;
  • }
  • // 处理文件名
  • String realname = fileName.substring(fileName.indexOf("_") + 1);
  • // 设置响应头,控制浏览器下载该文件
  • response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(realname, "UTF-8"));
  • // 读取要下载的文件,保存到文件输入流
  • FileInputStream in = new FileInputStream(path + File.separator + 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();
  • }
  • /**
  • * @param filename 要下载的文件名
  • * @param saveRootPath 上传文件保存的根目录,也就是/WEB-INF/upload目录
  • * @return 要下载的文件的存储目录
  • * @Method: findFileSavePathByFileName
  • * @Description: 通过文件名和存储上传文件根目录找出要下载的文件的所在路径
  • */
  • public String findFileSavePathByFileName(String filename, String saveRootPath) {
  • int hashcode = filename.hashCode();
  • int dir1 = hashcode & 0xf; // 0--15
  • int dir2 = (hashcode & 0xf0) >> 4; // 0-15
  • String dir = saveRootPath + File.separator + dir1 + File.separator + dir2; // upload/2/3 upload/3/5
  • File file = new File(dir);
  • if (!file.exists()) {
  • // 创建目录
  • file.mkdirs();
  • }
  • return dir;
  • }
  • public void doPost(HttpServletRequest request, HttpServletResponse response)
  • throws ServletException, IOException {
  • doGet(request, response);
  • }
  • }

在Web.xml文件中配置DownLoadServlet:

  • <servlet>
  • <servlet-name>DownloadServlet</servlet-name>
  • <servlet-class>com.coderap.fileupload.DownloadServlet</servlet-class>
  • </servlet>
  • <servlet-mapping>
  • <servlet-name>DownloadServlet</servlet-name>
  • <url-pattern>/doDownload</url-pattern>
  • </servlet-mapping>

在下载页面点击下载超链接,将请求提交到DownloadServlet就行处理就可以实现文件下载了。