深入分析JavaWeb Item40 -- 文件上传和下载

时间:2022-02-15 13:11:06

在Web应用系统开发中,文件上传和下载功能是很经常使用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现。

  对于文件上传。浏览器在上传的过程中是将文件以流的形式提交到server端的。假设直接使用Servlet获取上传文件的输入流然后再解析里面的请求參数是比較麻烦,所以一般选择採用apache的开源工具common-fileupload这个文件上传组件。

这个common-fileupload上传组件的jar包能够去apache官网上面下载。也能够在struts的lib文件夹以下找到。struts上传的功能就是基于这个实现的。common-fileupload是依赖于common-io这个包的,所以还须要下载这个包。

一、开发环境搭建

  创建一个FileUploadAndDownLoad项目。增加Apache的commons-fileupload文件上传组件的相关Jar包,例如以下图所看到的:

  深入分析JavaWeb Item40 -- 文件上传和下载

二、实现文件上传

2.1、文件上传页面和消息提示页面

  upload.jsp页面的代码例如以下:

<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
  <head>
    <title>文件上传</title>
  </head>

  <body>
    <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>
  </body>
</html>

  message.jsp的代码例如以下:

<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
  <head>
    <title>消息提示</title>
  </head>

  <body>
        ${message}
  </body>
</html>

2.2、处理文件上传的Servlet

  UploadHandleServlet的代码例如以下:

package me.gacl.web.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

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");
                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;
                            }
                            //注意:不同的浏览器提交的文件名称是不一样的,有些浏览器提交上来的文件名称是带有路径的,如: c:\a\b\1.txt,而有些仅仅是单纯的文件名称。如:1.txt
                            //处理获取到的上传文件的文件名称的路径部分,仅仅保留文件名称部分
                            filename = filename.substring(filename.lastIndexOf("\\")+1);
                            //获取item中的上传文件的输入流
                            InputStream in = item.getInputStream();
                            //创建一个文件输出流
                            FileOutputStream out = new FileOutputStream(savePath + "\\" + filename);
                            //创建一个缓冲区
                            byte buffer[] = new byte[1024];
                            //推断输入流中的数据是否已经读完的标识
                            int len = 0;
                            //循环将输入流读入到缓冲区其中。(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();
                            message = "文件上传成功!";
                        }
                    }
                }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); } }

  在Web.xml文件里注冊UploadHandleServlet

<servlet>
    <servlet-name>UploadHandleServlet</servlet-name>
    <servlet-class>me.gacl.web.controller.UploadHandleServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>UploadHandleServlet</servlet-name>
    <url-pattern>/servlet/UploadHandleServlet</url-pattern>
</servlet-mapping>

  执行效果例如以下:

  深入分析JavaWeb Item40 -- 文件上传和下载

  文件上传成功之后,上传的文件保存在了WEB-INF文件夹下的upload文件夹,例如以下图所看到的:

  深入分析JavaWeb Item40 -- 文件上传和下载

2.3、文件上传的细节

  上述的代码尽管能够成功将文件上传到server上面的指定文件夹其中,可是文件上传功能有很多须要注意的小细节问题,以下列出的几点须要特别注意的

  **1、为保证server安全,上传文件应该放在外界无法直接訪问的文件夹下。比方放于WEB-INF文件夹下。
  2、为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名称。
  3、为防止一个文件夹以下出现太多文件。要使用hash算法打散存储。


  4、要限制上传文件的最大值。
  5、要限制上传文件的类型,在收到上传文件名称时。推断后缀名是否合法。**

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

package me.gacl.web.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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;

/** * @ClassName: UploadHandleServlet * @Description: TODO(这里用一句话描写叙述这个类的作用) * @author: 孤傲苍狼 * @date: 2015-1-3 下午11:35:50 * */ 
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);
                            /** * 文件大小为:14608,当前已处理:4096 文件大小为:14608,当前已处理:7367 文件大小为:14608,当前已处理:11419 文件大小为:14608,当前已处理:14608 */
                        }
                    });
                     //解决上传文件名称的中文乱码
                    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;
                            }
                            //注意:不同的浏览器提交的文件名称是不一样的,有些浏览器提交上来的文件名称是带有路径的,如: c:\a\b\1.txt。而有些仅仅是单纯的文件名称,如:1.txt
                            //处理获取到的上传文件的文件名称的路径部分,仅仅保留文件名称部分
                            filename = filename.substring(filename.lastIndexOf("\\")+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 + "\\" + saveFilename);
                            //创建一个缓冲区
                            byte buffer[] = new byte[1024];
                            //推断输入流中的数据是否已经读完的标识
                            int len = 0;
                            //循环将输入流读入到缓冲区其中,(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();
                            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); } /** * @Method: makeFileName * @Description: 生成上传文件的文件名称。文件名称以:uuid+"_"+文件的原始名称 * @Anthor:孤傲苍狼 * @param filename 文件的原始名称 * @return uuid+"_"+文件的原始名称 */ private String makeFileName(String filename){ //2.jpg //为防止文件覆盖的现象发生。要为上传文件产生一个唯一的文件名称 return UUID.randomUUID().toString() + "_" + filename; } /** * 为防止一个文件夹以下出现太多文件。要使用hash算法打散存储 * @Method: makePath * @Description: * @Anthor:孤傲苍狼 * * @param filename 文件名称,要依据文件名称生成存储文件夹 * @param savePath 文件存储路径 * @return 新的存储文件夹 */ 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 + "\\" + dir1 + "\\" + 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); } }

  针对上述提出的5点小细节问题进行改进之后,我们的文件上传功能就算是做得比較完好了。

三、文件下载

3.1、列出提供下载的文件资源

  我们要将Web应用系统中的文件资源提供给用户进行下载,首先我们要有一个页面列出上传文件文件夹下的全部文件。当用户点击文件下载超链接时就进行下载操作,编写一个ListFileServlet,用于列出Web应用系统中全部下载文件。

  ListFileServlet的代码例如以下:

package me.gacl.web.controller;

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;

/** * @ClassName: ListFileServlet * @Description: 列出Web系统中全部下载文件 * @author: 孤傲苍狼 * @date: 2015-1-4 下午9:54:40 * */ 
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("/listfile.jsp").forward(request, response);
    }

    /** * @Method: listfile * @Description: 递归遍历指定文件夹下的全部文件 * @Anthor:孤傲苍狼 * @param file 即代表一个文件,也代表一个文件文件夹 * @param map 存储文件名称的Map集合 */ 
    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_阿_凡_达.avi 那么file.getName().substring(file.getName().indexOf("_")+1)处理之后就能够得到阿_凡_达.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方法,listfile方法是用来列出文件夹下的全部文件的。listfile方法内部用到了递归。在实际开发其中。我们肯定会在数据库创建一张表。里面会存储上传的文件名称以及文件的详细存放文件夹,我们通过查询表就能够知道文件的详细存放文件夹,是不须要用到递归操作的。这个样例是由于没有使用数据库存储上传的文件名称和文件的详细存放位置,而上传文件的存放位置又使用了散列算法打散存放,所以须要用到递归,在递归时,将获取到的文件名称存放到从外面传递到listfile方法里面的Map集合其中。这样就能够保证全部的文件都存放在同一个Map集合其中。

  在Web.xml文件里配置ListFileServlet

<servlet>
     <servlet-name>ListFileServlet</servlet-name>
     <servlet-class>me.gacl.web.controller.ListFileServlet</servlet-class>
</servlet>

<servlet-mapping>
     <servlet-name>ListFileServlet</servlet-name>
    <url-pattern>/servlet/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="/servlet/DownLoadServlet" 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页面中显示提供给用户下载的文件资源,例如以下图所看到的:

  深入分析JavaWeb Item40 -- 文件上传和下载

3.2、实现文件下载

  编写一个用于处理文件下载的Servlet,DownLoadServlet的代码例如以下:

package me.gacl.web.controller;

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-阿凡达.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 + "\\" + 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 + "\\" + 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(); } /** * @Method: findFileSavePathByFileName * @Description: 通过文件名称和存储上传文件根文件夹找出要下载的文件的所在路径 * @Anthor:孤傲苍狼 * @param filename 要下载的文件名称 * @param saveRootPath 上传文件保存的根文件夹,也就是/WEB-INF/upload文件夹 * @return 要下载的文件的存储文件夹 */ 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 + "\\" + dir1 + "\\" + 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>me.gacl.web.controller.DownLoadServlet</servlet-class>
</servlet>

<servlet-mapping>
      <servlet-name>DownLoadServlet</servlet-name>
      <url-pattern>/servlet/DownLoadServlet</url-pattern>
</servlet-mapping>

  点击【下载】超链接,将请求提交到DownLoadServlet即可处理就能够实现文件下载了,执行效果例如以下图所看到的:

  深入分析JavaWeb Item40 -- 文件上传和下载

  从执行结果能够看到,我们的文件下载功能已经能够正常下载文件了。

  关于JavaWeb中的文件上传和下载功能的内容就这么多。