JavaWeb之文件上传、下载

 

时间:2016-12-17 18:07

 

——文件上传概述

上传不能使用BaseServlet


1、文件上传的作用
    例如网络硬盘,就是用来上传和下载文件的。

2、文件上传对表单的限制
    1)必须使用表单,而不能是超链接
    2)表单的method必须是POST,而不能是GET
    3)表单的enctype必须是multipart/form-data
    4)在表单中添加type="file",即<input type="file" name="" />

    <form action="xxx" method="post" enctype="multipart/form-data">
        用户名:<input type="text" name="username" /><br/>
        照    片:<input type="file" name="photo" /><br/>
        <input type="submit" value="上传" />
    </form>

3、文件上传对Servlet的限制
    1)当表单为enctype="multipart/form-data"时,不能使用request.getParameter()方法,该方法永远返回null。
    2)使用ServletInputStream  request.getInputStream()来获取完整的请求体。

4、多部件表单结构
    1)分割出多个部件,即一个表单项生成一个部件。
    2)一个部件中会自己包含请求头和空行以及请求体。
    3)普通表单项:
        >   一个头:
            *   Content-Disposition,包含name="xxx",即表单项名称。
        >   体就是表单项的值。
    4)文件表单项:
        >   两个头:
            *   Content-Disposition,包含name="xxx",即表单项名称,还有一个filename="xxx",表示上传文件的名称。
            *   Content-Type:它是上传文件的MIME类型,例如:image/jpeg,表示上传的是图片。
        >   体就是上传文件的内容。


图片




——commons-fileupload

    *   commins-fileupload.jar
    *   依赖包:commons-io.jar

这个组件会解析request中的上传数据,解析后的结果是一个表单项数据封装到一个FileItem对象中,只需要调用FIleItem对象的方法即可。(多个表单项会封装多个FileItem,即List<FileItem>)

1、上传三个步骤

相关类:
    *   工厂类:DiskFileItemFactory
    *   解析器:ServletFileUpload
    *   表单项:FileItem(该类对应一个表单项)

    1)创建工厂:
        DiskFileItemFactory factory = new DiskFileItemFactory();

    2)创建解析器:
        ServletFileUpload sfu = new ServletFileUpload(factory);

    3)使用解析器来解析request,得到FileItem集合:
        List<FileItem> fileItemList = sfu.parseRequest(request);

2、FileItem类

普通表单项相关方法:

    *   boolean isFormField()
        是否为普通表单项。
        true为普通表单项。
        false为文件表单项。

    *   String getFieldName()
        返回当前表单项的名称。

    *   String getString(String charset)
        通过指定编码获取表单项的值。
        该方法不适用于文件表单项。
        必须指定编码。 

文件表单项相关方法:

    *   String getName()
        返回上传的文件名称。

    *   long getSize()
        返回上传文件的字节数。

     *   String getContentType()
        获取MIME类型。

    *   InputStream getInputStream()
        返回上传文件对应的输入流。

    *   void write(File file)
        将上传的文件写入指定文件中。
        如果file已存在,则会替换目标文件,如果file不存在,则会创建该文件。


——文件上传简单示例

===============================================================================

com.wyc.servlet.UploadServlet

import java.io.File;
import java.io.IOException;
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 Upload2Servlet extends HttpServlet {
 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
 
        /*
         * 上传三步
         * 1、得到工厂
         * 2、通过工厂创建解析器
         * 3、解析request,得到FileItem集合
         * 4、遍历FileItem集合,遍历其API完成文件的保存
         */
        DiskFileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload sfu = new ServletFileUpload(factory);
        try {
            List<FileItem> fileItemList = sfu.parseRequest(request);
            for(FileItem f : fileItemList){
                if(f.isFormField()){
                    System.out.println("普通表单项:" + f.getFieldName() + "=" + f.getString("utf-8"));
                } else {
                    System.out.println("文件表单项:");

                    // 获取文件MIME类型
                    String mime = f.getContentType();
                    System.out.println("Content-Type:" + mime);

                    // 获取文件大小(字节数)
                    long size = f.getSize();

                    System.out.println("文件大小:" + size);

                    // 获取文件名
                    String fileName = f.getName();
                    System.out.println("文件名称:" + fileName);
 
                    // 保存文件
                    String filePath = request.getServletContext().getRealPath("") + "/file/" + fileName;
                    File destFile = new File(filePath);
                    f.write(destFile);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}


------------------------------------------------------------------------------------------------------------------------------

form.jsp

<body>
    <h1>上传</h1>
    <form action="<c:url value='/Upload2Servlet'/>" method="post" enctype="multipart/form-data">
        用户名:<input type="text" name="username" value="张三" /><br/>
        照    片:<input type="file" name="photo" /><br/>
        <input type="submit" value="上传" />
    </form>
</body>

===============================================================================

——文件上传细节

1、把上传的文件放到WEB-INF目录下
    如果没有把用户上传的文件放到WEB-INF目录下,那么用户就可以通过浏览器直接访问上传的文件,这是非常危险的。

    假如说用户上传了一个JSP文件,然后用户再通过浏览器去访问这个JSP文件,那么服务器就会执行JSP文件中的内容,如果在JSP文件中有如下语句:Runtime.getRuntime().exec("shutdown -s -t 1");.........

    通常我们会在WEB-INF目录下创建一个upload目录来存放上传的文件,而在Servlet中找到这个目录需要使用ServletContext()的getRealPath(String)方法,例如在项目中有如下语句:
        ServletContext servletContext = this.getServletContext();
        String uploadPath = servletContext.getRealPath("/WEB-INF/uploads");
    其中uploadPath为:G:\tomcat\webapps\FileDemo\WEB-INF\uploads

2、文件名称(完整路径)
    上传的文件名称可能是完整路径。
    IE6获取的上传文件名称是完整路径,而在其他浏览器获取的上传文件名称只是文件名称而已,浏览器差异的问题还需要处理一下。
    例如:C:\file\哈哈.jpg
    可以通过分割字符串来获取文件名。
    int index = fileName.lastIndexOf("\\");
    if(index != -1)
        fileName = fileName.substring(index+1);

3、处理文件名称乱码和普通表单项乱码
    当上传的文件名称中包含中文时,需要设置编码,commons-fileupload组件为我们提供了两种设置编码的方式:
        request.setCharacterEncoding("utf-8");
        fileUpload.setHeaderEncoding(String charset);    // 这种方式的优先级高于前一种。
    commons-fileupload内部会调用request.getCharacterEncoding();来指定编码。

    上传文件的文件内容中包含中文:
        通常我们不需要关心上传文件的内容,因为我们会把上传文件保存到硬盘上,也就是说,原文件是什么样子,保存到服务器之后还是什么样子。
        如果想要在控制台显示上传文件的内容,那么可以使用fileItem.getString(String encoding)来使用指定编码处理字符串。

        文本内容和普通表单项内容都是用FileItem类的getString(String encoding)来处理编码。

4、文件重名问题
        应该为每个文件名添加前缀名称,这个前缀要保证不能重复,可以使用UUID。
            fileName = CommonUtils.getUuid() + "_" + fileName;

===============================================================================

5、一个目录不能存放过多文件(存放目录打散)
    一个目录下不应该存放过多的文件,一般一个目录存放1000个文件就是上限了,如果再多,那么打开目录时就会很“卡”。
    也就是说,我们需要把上传的文件存放到不同的目录中,但是也不能为每个上传的文件都新建一个目录,这样会导致目录过多,所以我们应该采用某种算法来“打散”存放文件的目录。
    方法有很多,例如使用日期来打散,每天生成一个目录,也可以使用文件名的首字母来生成目录,相同首字母的文件放到同一目录下。

    日期打散算法:如果某一天上传的文件过多,那么也会出现一个目录文件过多的情况。
    首字母打散算法:
        例如abc.txt,那么可以把文件保存到a目录下,如果a目录不存在,则创建a目录。
        如果文件名是中文的,因为中文过多,所以会导致目录过多的情况。

    哈希打散算法:
        *   通过文件名称得到一个int值,即调用hashCode()获取哈希值。
        *   将int值转换成16进制。
        *   获取16进制的前两位用来生成目录,目录为两层:例如1B2C3D4E5F,使用/1/B/ 来保存文件。
        该存储方式非常不直观,一般会将文件存放的目录保存起来,以后访问下载时可以直接获取。

import java.io.File;
import java.io.IOException;
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;
 
import com.wyc.bean.CommonUtils;
 
public class Upload3Servlet extends HttpServlet {
 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        /*
         * 上传三步
         */
        // 得到工厂
        DiskFileItemFactory factory = new DiskFileItemFactory();
 
        // 得到解析器
        ServletFileUpload fileUpload = new ServletFileUpload(factory);
 
        // 解析request
        try {
            List<FileItem> fileItemList = fileUpload.parseRequest(request);
 
            // 判断表单项
            for(FileItem f : fileItemList){
                if(f.isFormField()){
                    System.out.println(f.getFieldName() + "=" + f.getSize());
                } else {
                    /*
                     * 1、得到文件保存的根路径,在此路径下创建多级目录
                     */
                    String realPath = this.getServletContext().getRealPath("/WEB-INF/files");
                    /*
                     * 2、生成二级目录
                     *   1)得到文件名称
                     *   2)得到hashCode
                     *   3)转成16进制
                     *   4)获取前两个字符用来生成目录
                     */
 
                    /*
                     * 获取文件名称,并处理文件名的绝对路径问题
                     */
                    String fileName = f.getName();
                    int index = fileName.lastIndexOf("\\");
                    if(index != -1){
                        fileName = fileName.substring(index + 1);
                    }
 
                    /*
                     * 处理文件重名问题,给文件名称添加UUID前缀
                     */
                    String saveName = CommonUtils.getUuid() + "_" + fileName;
 
                    /*
                     * 得到hashCode,并转换成16进制
                     */
                    int hashCode = fileName.hashCode();
                    String hexCode = Integer.toHexString(hashCode);
 
                    /*
                     * 获取hexCode的前两个字符,与根路径连接在一起,生成一个完整的路径
                     * 将此抽象路径传递给File类的构造方法
                     */
                    File dirFile = new File(realPath, hexCode.charAt(0) + "/" + hexCode.charAt(1));
 
                    /*
                     * 创建目录
                     * mkdirs():创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。
                     */
                     dirFile.mkdirs();
 
                    /*
                     * 创建目标文件
                     * 根据parent抽象路径名和child路径名字符串创建一个新 File 实例。
                     */
                    File destFile = new File(dirFile, saveName);
 
                    /*
                     * 写入硬盘(保存)
                     */
                    f.write(destFile);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}


===============================================================================

6、上传的单个文件、整个表单的大小限制
    限制上传文件的大小很简单,ServletFileUpload类的setFileSizeMax(long)就可以实现了,参数就是上传文件的上限字节数,例如:servletFileUpload.setFileSizeMax(1024 * 10),表示上限为10kb。
    该方法一定要在parseRequest()方法之前调用。
    一旦上传的文件大小超出了上限,那么就会在执行parseRequest()方法时抛出FileUploadBase.FileSizeLimitExceededException异常。
    我们可以在Servlet中获取这个异常,然后想页面输出“上传的文件大小超出限制”。

    限制整个表单的大小:
        fileUpload.setSizeMax(long size);
    该方法必须在parseRequest()方法之前调用。
    如果大小超出限制,会抛出FileUploadBase.SizeLimitExceededException异常。

public class Upload4Servlet extends HttpServlet {
 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
 
        DiskFileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload fileUpload = new ServletFileUpload(factory);
 
        // 限制单个文件大小为10M
        fileUpload.setFileSizeMax(1024 * 10000);
 
        // 限制整个表单大小不能超过100M
        fileUpload.setSizeMax(1024 * 100000);
 
        List<FileItem> fileItemList;
        try {
            fileItemList = fileUpload.parseRequest(request);
            FileItem f = fileItemList.get(1);
        } catch (FileUploadException e) {
            if (e instanceof FileUploadBase.FileSizeLimitExceededException)
                throw new RuntimeException("上传单个文件大小超出限制");
            else if (e instanceof FileUploadBase.SizeLimitExceededException)
                throw new RuntimeException("表单大小超出限制");
        }
    }
}


===============================================================================

7、设置缓存大小与临时目录
    *   缓存大小:
            默认为10kb,当上传文件超出缓存大小时,先将文件保存到硬盘,当文件上传完毕后,再将文件保存。
    *   临时目录:
            缓存的保存目录。

    *   可以通过DiskFileItemFactory的构造方法来指定缓存大小和临时目录
            DiskFileItemFactory(int sizeThreshold, File repository)
                >   默认的临时目录为:System.getProperty("java.io.tmpdir");,打印后:G:\apache-tomcat-7.0.72\temp
                >   例如:new DiskFileItemFactory(1024 * 10, new File("F:/temp"));

public class Upload5Servlet extends HttpServlet {
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");

        // 打印默认临时目录 
        System.out.println(System.getProperty("java.io.tmpdir"));
 
        // 设置缓存大小与临时目录
        DiskFileItemFactory factory = new DiskFileItemFactory(1024 * 10, new File("F:/temp"));
 
        ServletFileUpload fileUpload = new ServletFileUpload(factory);
        try {
            List<FileItem> fileItemList = fileUpload.parseRequest(request);
            FileItem f = fileItemList.get(1);
            File file = new File("F:/temp/" + f.getName());
            f.write(file);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
 

===============================================================================


——文件下载

被下载的资源必须放到WEB-INF目录下,然后通过Servlet完成下载。
在JSP页面中给出超链接,链接到DownloadServlet,并提供要下载的文件名称,然后在DownloadServlet获取文件的真实路径,然后把文件写入到response.getOutputStream()流中。


1、概述
    下载就是向客户端响应字节数据。
    之前响应的都是HTML的字符数据。
    其实就是把一个文件编程字节数组,使用response.getOutputStream()来将文件响应给浏览器。

2、下载的要求
    两个头、一个流
        *   Content-Type:表示传递给客户端文件的MIME类型。
            >   通过文件名称调用ServletContext的getMimeType()方法得到MIME类型。
        *   Content-Disposition:
            >   它的默认值为inline,表示在浏览器窗口中打开(当打不开时会弹框)。
            >   attachment;filename=xxx:表示类型为附件,并在弹框的界面中显示下载的文件名。
        *   流:要下载的文件数据。
            >   自己new 一个输入流即可,然后通过IOUtils完成流的数据写入。

——下载示例

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.apache.commons.io.IOUtils;
 
public class DownloadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
 
        /*
         * 准备两个头,一个流
         * 1、Content-Type
         * 2、Content-Disposition
         * 3、流:下载文件的数据
         */
 
        /*
         * 如何获取文件的MIME类型
         */
        String fileName = "F:/Acacia - Valder Fields.mp3";

        // 通过文件名获取文件MIME类型
        String contentType = this.getServletContext().getMimeType(fileName);
 
        String contentDisposition = "attachment;filename=Acacia - Valder Fields.mp3";
 
        // 设置响应头
        response.setHeader("Content-Type", contentType);
        response.setHeader("Content-Disposition", contentDisposition);
 
        // 获取绑定了客户端的响应流
        ServletOutputStream output = response.getOutputStream();
 
        // 获取要下载的文件
        FileInputStream input = new FileInputStream(new File("F:/Acacia - Valder Fields.mp3"));
 
        // 将输入流中的数据复制到输出流中
        IOUtils.copy(input, output);
 
        input.close();
    }
}



——DownloadUtils(下载文件名乱码问题)

显示在下载框中包含中文名称时,会出现乱码。
    *   FireFox,使用Base64编码。
    *   其他大部分浏览器,使用URL编码。

    通用方法:filename = new String(filename.getBytes("gbk"), "iso-8859-1");
    浏览器能读懂ISO-8859-1编码 

public class DownloadServlet extends HttpServlet {
 
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        /*
          * 准备两个头,一个流
          * 1、Content-Type
          * 2、Content-Disposition
          * 3、流:下载文件的数据
          */
 
        /*
          * 如何获取文件的MIME类型
          */
        String fileName = "F:/哈哈.mp3";
 
        // 解决乱码问题
        // String frameName = new String(fileName.substring(fileName.lastIndexOf("/") + 1).getBytes("GBK"), "ISO-8859-1");
 
        // 使用DownloadUtils解决乱码问题
        String frameName;
        try {
            frameName = fileNameEncoding("哈哈.mp3", request);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
 
        // 通过文件名获取文件MIME类型
        String contentType = this.getServletContext().getMimeType(fileName);
 
        String contentDisposition = "attachment;filename=" + frameName;
 
        // 设置响应头
        response.setHeader("Content-Type", contentType);
        response.setHeader("Content-Disposition", contentDisposition);
 
        // 获取绑定了客户端的响应流
        ServletOutputStream output = response.getOutputStream();
 
        // 获取要下载的文件
        FileInputStream input = new FileInputStream(new File("F:/哈哈.mp3"));
 
        // 将输入流中的数据复制到输出流中
        IOUtils.copy(input, output);
 
        input.close();
    }

    public static String fileNameEncoding(String fileName, HttpServletRequest request) throws Exception{
        String agent = request.getHeader("User-Agent");
        if(agent.contains("Firefox")){
            BASE64Encoder base64Encoder = new BASE64Encoder();
            fileName = "=?utf-8?B?" + base64Encoder.encode(fileName.getBytes("utf-8")) + "?=";
        } else if (agent.contains("<SIE")) {
            fileName = URLEncoder.encode(fileName, "utf-8");
        } else {
            fileName = URLEncoder.encode(fileName, "utf-8");
        }
        return fileName;
    }
}


注意:
    Base64Encoder并不属于JDK标准库范畴,但是又包含在了JDK中,解决方法:
        1、按照如下方法设置Eclipse导入%JAVA_HOME%\jre\lib目录下的rt.jar包即可,Project->Properties,选择Java Build Path设置项,再选择Libraries标签,Add External Jars添加%JAVA_HOME%\jre\lib\rt.jar就可以使用了。
 
        2、MyEclipse —— Preferences —— Java —— Compiler —— Errors/Warnings —— Deprecated and restricted API —— Forbidden reference(access rules): Ignore

——BASE64Encoder、BASE64Decoder

public void fun() throws Exception {
    // BASE64编码
    String s = "haha";
    BASE64Encoder encoder = new BASE64Encoder();
    s = encoder.encode(s.getBytes("utf-8"));
    System.out.println(s);
 
    // BASE64解码
    BASE64Decoder decoder = new BASE64Decoder();
    byte[] b = decoder.decodeBuffer(s);
    System.out.println(new String(b, "utf-8"));
}

 

posted @ 2017-02-07 18:01  WWWYC  阅读(240)  评论(0编辑  收藏  举报