Web笔记-上传下载

文件的上传
  1. 文件上传的条件和原理

文件上传页面书写必须符合以下条件:

  • Form表单的请求方式必须为post
  • Form表单中提供type="file"类型的上传输入框
  • Form表单enctype属性必须是multipart/form-data,enctype默认值为application/x-www-urlencoded

文件上传原理:
使用request.getInputStream();通过流来取得用户上传的数据,关键问题是流的解析


图1
图2

2、借助三方组件实现文件上传
使用Apache提供的commons-fileupload组件进行上传功能开发(内部核心封装的为文件流解析方法)
commons-io为fileupload组件在1.1开始就依赖的jar包,用于辅助文件的io操作

示例代码:

package com.gaojinze.web.upload.servlet;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
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.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;
/**
 * 使用第三方组件进行上传
 * apache的
 * commons-fileupload.jar:核心内容解析请求正文实现上传
 * commons-io.jar:是apache对jdk对java.io.*对扩展和增强,fileupload从1.1就依赖该包
 * @author gaopengfei
 *
 */
public class UploadServlet3 extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    public UploadServlet3() {
        super();
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        //检验表单enctype属性
        boolean isMultipartContent = ServletFileUpload.isMultipartContent(request);
        if (!isMultipartContent) {
            throw new RuntimeException("Please check your form enctype attribute,the value msut be multipart/form-data");
        }
        
        //创建核心解析器实例
        DiskFileItemFactory factory = new DiskFileItemFactory();//创建工厂实例,该工厂可以创建FileItem对象
        ServletFileUpload upload = new ServletFileUpload(factory);//利用工厂创建解析器实例
        upload.setFileSizeMax(4 * 1024 * 1024);//设置单个文件的上传大小为4M
        upload.setSizeMax(8 * 1024 * 1024);//设置总上传文件的大小为8M
        
        List<FileItem> fileItems = new ArrayList<>();
        try {
            fileItems = upload.parseRequest(request);
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            response.getWriter().write("单个文件上传大小不得超过4M");
        }catch (FileUploadBase.SizeLimitExceededException e) {
            
            response.getWriter().write("多文件上传总大小不得超过8M");
        }catch (FileUploadException e) {
            e.printStackTrace();
        }
        for (FileItem fileItem : fileItems) {
            
            if (fileItem.isFormField()) {
                //非上传字段
                processFormField(fileItem);
            }else {
                //上传字段
                processUploadField(fileItem);
            }
        }
        response.getWriter().write("上传成功!");
        response.setHeader("refresh", "1;url=" + request.getContextPath() + "/upload.jsp");
//      response.sendRedirect(request.getContextPath() + "/upload.jsp");
    }
    /**
     * 处理非上传字段
     * @param fileItem
     */
    private void processFormField(FileItem fileItem) {
    
        String fieldName = fileItem.getFieldName();
        String fieldValue = null;
        try {
            fieldValue = fileItem.getString("UTF-8");
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(fieldName + ":" + fieldValue);
    }
    /**
     * 处理上传文件
     * @param fileItem
     */
    private void processUploadField(FileItem fileItem) {
        
        try {
            
            //获取文件名(因为浏览器不同导致在选择完文件后的name也不同,如IE选择后,文件名为:D:\XXX\a.txt)
            String fileName = fileItem.getName();
            //判断是否有未选择的空项
            if ("".equals(fileName) || null == fileName) {
                return;
            }
            //通过扩展名来限制上传文件类型
            String extension = FilenameUtils.getExtension(fileName);
            //获取上传文件的MIME类型
            String contentType = fileItem.getContentType();
            if (!extension.equalsIgnoreCase("jpg"))
                return;
            if (!contentType.startsWith("image")) {
                return;
            }
            fileName = FilenameUtils.getName(fileName);
            String uuidFileName = UUID.randomUUID().toString() + "_" + fileName;
            //得到存放文件的真实路径
            String uploadBasePath = getServletContext().getRealPath("/WEB-INF/files");
//          String childUploadDir = getChildUploadDir(uploadBasePath);
            String childUploadDir = getChildUploadDir2(uploadBasePath, uuidFileName);
            //保存文件,并清理缓存
            fileItem.write(new File(uploadBasePath + File.separator +childUploadDir, uuidFileName));
        } catch (Exception e) {
            throw new RuntimeException("上传失败!");
        }
    }
    /**
     * 按照日期进行子目录创建
     * @param uploadPath
     * @return
     */
    private String getChildUploadDir(String uploadBasePath) {
        
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String childUploadDir = dateFormat.format(date);
        File childDir = new File(uploadBasePath, childUploadDir);
        if (!childDir.exists()) {
            //若目录不存在则创建
            childDir.mkdirs();
        }
        return childUploadDir;
    }
    
    /**
     * 通过uuid的hascode分散目录
     * @param uploadBasePath
     * @param uuidFileName
     * @return
     */
    private String getChildUploadDir2(String uploadBasePath, String uuidFileName){
        
        int hashCode = uuidFileName.hashCode();
        //作为一级目录名
        int dir1 = hashCode&0xf;
        //作为二级目录名
        int dir2 = (hashCode&0xf0)>>4;
        String childUploadDir = dir1 + File.separator + dir2;
        File childDir = new File(uploadBasePath, childUploadDir);
        if (!childDir.exists()) {
            //若目录不存在则创建
            childDir.mkdirs();
        }
        return childUploadDir;
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

3、三方组件核心类介绍以及实际开发中需要注意的细节

  • 如何保证服务器安全?
    将文件上传目录放置在WEB-INF目录下即可
  • 如何避免文件重名而导致上传后文件覆盖?
    创建唯一文件名,使用UUID.randomUUID().toString() + "_" + fileName;即可
  • 如何避免同一个目录文件过多而查找难?
    解决方法:
    a)以日期分类目录
/**
     * 按照日期进行子目录创建
     * @param uploadPath
     * @return
     */
    private String getChildUploadDir(String uploadBasePath) {
        
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String childUploadDir = dateFormat.format(date);
        File childDir = new File(uploadBasePath, childUploadDir);
        if (!childDir.exists()) {
            //若目录不存在则创建
            childDir.mkdirs();
        }
        return childUploadDir;
    }

b)利用UUID文件名的hash码进行分散存储

/**
 * 通过uuid的hascode分散目录
 * @param uploadBasePath
 * @param uuidFileName
 * @return
 */
private String getChildUploadDir2(String uploadBasePath, String uuidFileName){
        
    int hashCode = uuidFileName.hashCode();
    //作为一级目录名
    int dir1 = hashCode&0xf;
    //作为二级目录名
    int dir2 = (hashCode&0xf0)>>4;
    String childUploadDir = dir1 + File.separator + dir2;
    File childDir = new File(uploadBasePath, childUploadDir);
    if (!childDir.exists()) {
        //若目录不存在则创建
        childDir.mkdirs();
    }
    return childUploadDir;
}
hashcode
  1. 文件的限制
  • 限制文件上传大小
    Web上传不适合上传太大的文件,一般为2M
/创建核心解析器实例
DiskFileItemFactory factory = new DiskFileItemFactory();//创建工厂实例,该工厂可以创建FileItem对象
ServletFileUpload upload = new ServletFileUpload(factory);//利用工厂创建解析器实例
upload.setFileSizeMax(4 * 1024 * 1024);//设置单个文件的上传大小为4M
upload.setSizeMax(8 * 1024 * 1024);//设置总上传文件的大小为8M
  • 限制上传文件的类型
    通过MIME类型
    操作系统是根据扩展名来区分文件类型的
    信息传输的数据通过MIME来区分类型,详细的对应关系,可以查看tomcat目录下的web.xml有记录
//通过扩展名来限制上传文件类型,该方式不靠谱
String extension = FilenameUtils.getExtension(fileName);
if (!extension.equalsIgnoreCase("jpg")) {
    return;
}
------------------------------------------------------------
//获取上传文件的MIME类型
String contentType = fileItem.getContentType();
if (!extension.equalsIgnoreCase("jpg"))
    return;
if (!contentType.startsWith("image")) {
    return;
}
------------------------------------------------------------
//判断是否有未选择的空项
if ("".equals(fileName) || null == fileName) {
    return;
}
  1. 上传时的临时文件处理
    DiskFileItemFactory创建FileItem对象时会使用缓存,默认10kb,若上传文件大小超过10kb,则会采用磁盘进行缓存(磁盘缓存即为垃圾文件
//创建核心解析器实例
DiskFileItemFactory factory = new DiskFileItemFactory();//创建工厂实例,该工厂可以创建FileItem对象
//设置缓存文件大小
factory.setSizeThreshold(100 * 1024);
//设置缓存文件的存放目录,默认是系统的临时文件目录
factory.setRepository(new File("f:/"));

若使用原生io流进行文件读写存储,则在关闭流后使用fileItem.delete();方法进行缓存清理
若使用fileItem.write();进行读写存储,则无需处理,其内部已经进行流缓存清理

  1. 中文乱码问题
  • 普通字段中文乱码解决
    String fieldValue = fileItem.getString("UTF-8");
  • 中文文件名乱码解决
    request.setCharacterEncoding("UTF-8");

文件的下载
  • FilelistServlet(文件列表获取)
package com.gaojinze.web.upload.servlet.download;
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;
/**
 * 文件列表Servlet
 * @author gaopengfei
 */
public class FilelistServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    public FilelistServlet() {
        super();
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        //创建存储key为文件在服务器中的名称,value为用户上传文件名
        Map<String, String> fileMap = new HashMap<>();
        
        String fileBasePath = getServletContext().getRealPath("/WEB-INF/files");
        File uploadRootDir = new File(fileBasePath);
        findAllFiles(uploadRootDir, fileMap);
        request.setAttribute("fileMap", fileMap);
        request.getRequestDispatcher("/filelist.jsp").forward(request, response);
    }
    /**
     * 遍历服务器所有文件并将文件信息存储到map中
     * @param file
     * @param fileMap
     */
    private void findAllFiles(File file, Map<String, String> fileMap) {
        
        if (file.isFile()) {
            
            String fileUUIDName = file.getName();
            String fileRealName = fileUUIDName.substring(fileUUIDName.indexOf("_") + 1);
            fileMap.put(fileUUIDName, fileRealName);
        }else {
            
            //如果是目录则递归进行遍历
            File[] listFiles = file.listFiles();
            if (listFiles == null || listFiles.length == 0) {
                return;
            }
            for (File f : listFiles) {
                findAllFiles(f, fileMap);
            }
        }
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}
  • filelist.jsp文件列表展示页
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件列表</title>
</head>
<body>
    <h3 align="center">文件列表</h3>
    <table align="center" border="1">
        <tr>
            <th>文件名</th>
            <th>操作</th>
        </tr>
        <c:forEach items="${ fileMap }" var="mk">
            <!-- 以下标签用于构建一个URL /Web-Upload/servlet/Download?filename=经过url编码的中文 -->
            <c:url var="url" value="/servlet/DownloadServlet">
                <c:param name="filename" value="${ mk.key }"></c:param>
            </c:url>
            <tr>
                <td>${ mk.value }</td>
                <!-- 中文名跟在URL后需要进行URL编码 -->
                <!-- ${pageContext.request.contextPath }"/servlet/DownloadServlet?filename=${ mk.key } -->
                <td><a href="${ url }">下载</a></td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>
  • DownloadServlet(文件下载)
package com.gaoshiyi.web.upload.servlet.download;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
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;
/**
 * 文件下载
 * @author gaopengfei
 *
 */
public class DownloadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    public DownloadServlet() {
        super();
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String uuidName = request.getParameter("filename");
        String realName = uuidName.substring(uuidName.indexOf("_") + 1);
        // 上传文件跟目录
        String uploadRootPath = getServletContext().getRealPath("/WEB-INF/files");
        // 获取对应uuidname文件的子目录
        String childDirPath = getChildDirPathByUUIDName(uploadRootPath, uuidName);
        // 构建文件读取流
        InputStream in = new FileInputStream(new File(uploadRootPath + File.separator + childDirPath, uuidName));
        //处理下载时的文件名称中文问题
        String userAgent = request.getHeader("User-Agent");
        if (userAgent.contains("Firefox")) {
            //单独处理火狐浏览器
            realName = new String(realName.getBytes("UTF-8"), "ISO-8859-1");
        }else {
            //其他浏览器
            realName = URLEncoder.encode(realName, "UTF-8");
        }
        // 告知浏览器以下载方式下载
        response.setHeader("Content-Disposition", "attachment;filename=" + realName);
        // 设置头信息告知客户端文件大小 
        response.setContentLength(in.available());
        // 设置头信息告知客户端文件MIME类型 
        response.setHeader("Content-Type", "application/octet-stream");
        // 输出
        OutputStream out = response.getOutputStream();
        int len = 0;
        byte[] buff = new byte[1024];
        while ((len = in.read(buff)) != 1) {
            out.write(buff, 0, len);
        }
        in.close();
        out.close();
    }
    /**
     * 获取子目录
     * 
     * @param uploadRootPath
     * @param uuidName
     * @return
     */
    private String getChildDirPathByUUIDName(String uploadRootPath, String uuidName) {
        int hashCode = uuidName.hashCode();
        int dir1 = hashCode&0xf;
        int dir2 = (hashCode&0xf0) >> 4;
        String cDir = dir1 + File.separator + dir2;
        File childDir = new File(uploadRootPath, cDir);
        if (!childDir.exists()) {
            childDir.mkdirs();
        }
        return cDir;
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容