文件上传和下载


文件的上传和下载,是非常常见的功能。很多的系统中,或者软件中都经常使用文件的上传和下载。比如:QQ 头像,就使用了上传。邮箱中也有附件的上传和下载功能。OA 系统中审批有附件材料的上传

一、上传原理和代码分析。

      上传:我们把需要上传的资源,发送给服务器,在服务器上保存下来。

      下载:下载某一个资源时,将服务器上的该资源发送给浏览器。

      难点:服务器端获取资源时比较麻烦,

浏览器端

1、要有一个 form 标签,method=post 请求
2、form 标签的 encType 属性值必须为 multipart/form-data 值
3、在 form 标签中使用 input type=file 添加上传的文件

注意:enctype=multipart/form-data:该属性表明发送的请求体的内容是多表单元素的,通俗点讲,就是有各种各样的数据,可能有二进制数据,也可能有表单数据,等等,所以使用该属性也进行其区分,发送的格式如下(使用火狐中的Firebug插件进行捕捉的信息。)

文件上传,HTTP 协议的说明

服务器端

编写服务器代码(Servlet 程序)接收,处理上传的数据。

如果不使用commons-fileupload插件来帮我们处理上传后的数据而让我们自己手动处理的话,也是可以的,但是十分麻烦,因为我们需要将所有的请求体获取到,然后通过字符串的分割,通过boundary这个属性进行分割,然后一步步获取到我们想要的数据。

commons-fileupload.jar 常用 API 介绍说明

commons-fileupload.jar 需要依赖 commons-io.jar 这个包,所以两个包我们都要引入。
第一步,就是需要导入两个 jar 包:
commons-fileupload-1.2.1.jar
commons-io-1.4.jar
commons-fileupload.jar 和 commons-io.jar 包中,我们常用的类有哪些?
ServletFileUpload 类,用于解析上传的数据。
FileItem 类,表示每一个表单项。
boolean ServletFileUpload.isMultipartContent(HttpServletRequest request);
判断当前上传的数据格式是否是多段的格式。
public List parseRequest(HttpServletRequest request)
解析上传的数据
boolean FileItem.isFormField()
判断当前这个表单项,是否是普通的表单项。还是上传的文件类型。
true 表示普通类型的表单项
false 表示上传的文件类型
String FileItem.getFieldName()
获取表单项的 name 属性值
String FileItem.getString()
获取当前表单项的值。
String FileItem.getName();
获取上传的文件名
void FileItem.write( file );
将上传的文件写到 参数 file 所指向抽硬盘位置 。

               // 工厂
 4             FileItemFactory fileItemFactory = new DiskFileItemFactory();
 5             
 6             //2 核心类
 7             ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
 8             
 9             //3 解析request  ,List存放 FileItem (表单元素的封装对象,一个<input>对应一个对象)
10             List<FileItem> list = servletFileUpload.parseRequest(request);
11             
12             //4 遍历集合获得数据
13             for (FileItem fileItem : list) {
14                 if(fileItem.isFormField()){
15                     // 5 是否为表单字段(普通表单元素)
16                     //5.1.表单字段名称
17                     String fieldName = fileItem.getFieldName();
18                     System.out.println(fieldName);
19                     //5.2.表单字段值
20                     String fieldValue = fileItem.getString();    //中文会出现乱码
21                     System.out.println(fieldValue);
22                 } else {
23                     //6 上传字段(上传表单元素)
24                     //6.1.表单字段名称  fileItem.getFieldName();
25                     //6.2.上传文件名
26                     String fileName = fileItem.getName();
27                     // * 兼容浏览器, IE : C:\Users\xxx\Desktop\abc.txt  ; 其他浏览器 : abc.txt
28                     fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
29                     System.out.println(fileName);    //上传的文件名会中文乱码,
30                     //6.3.上传内容
31                     InputStream is = fileItem.getInputStream();    //获得输入流,
32                     String parentDir = this.getServletContext().getRealPath("/WEB-INF/upload");
33                     File file = new File(parentDir,fileName);
34                     if(! file.getParentFile().exists()){  //父目录不存在
35                         file.getParentFile().mkdirs();        //mkdirs():创建文件夹,如果上级目录没有的话,也一并创建出来。
36                     }
37                     FileOutputStream out = new FileOutputStream(file);    
38                     byte[] buf = new byte[1024];
39                     int len = -1;
40                     while( (len = is.read(buf)) != -1){
41                         out.write(buf, 0, len);
42                     }
43                     
44                     //关闭
45                     out.close();
46                     is.close();
47                 }
48             }
49             
50         } catch (Exception e) {
51             e.printStackTrace();
52             
53             throw new RuntimeException(e);
54             
55         }

其中需要注意的是流的操作,和File类的操作,mkdirs()和mkdir()的区别是什么?它们都是用来创建文件夹的,区别在mkdirs能创建多级目录,而mkdir()只能创建当前的文件夹,如果发现上一层目录并还没有创建时,它也会无动于衷,什么也不干,也就不能创建当前文件夹,必须先要创建了上一级文件夹才可以。

        上面还有很多的问题需要解决,比如上传文件名的乱码问题,比如单表内容的乱码问题,比如上传文件同名问题,如果上传的文件很大,该如何进行处理,比如,放在同一级目录下的文件过多如何处理。每次都自己手动写输出流,将内容存到指定位置,太过麻烦,等等问题,现在写一个加强版,在解决上面所有的问题。

上传文件名乱码问题:使用servletFileUpload.setHeaderEncoding("UTF-8");或者request.setCharacterEncoding("UTF-8")都可以

表单内容乱码问题:使用getString("utf-8")即可,也就是在获取内容时,就可以设置码表。

上传文件同名问题:使用UUID.randomUUID().toString().replace("-", "").获得一个独一无二的32位数字

使用FileUtils.copyInputStreamToFile(is, file);来将内容输出到指定路径文件中去,mkdirs() 自动创建目录

同一级目录下的文件过多问题:创建多级目录,通过文件名的hashcode的值,hashCode为int型,占4个字节,一个字节占8位,也就是占32位,将其分组,4位4位一组,就能分8组,每一次代表一层目录,也就能够分8层目录,每一层中,又可以创建16种不同的文件夹。这样算下来,就能有很多很多个文件夹了,每个文件夹下面都可以存很多文件,看我们的业务需求,来决定要创建几层目录,就从hashcode中拿几组数据出来。原理图如下

1 public class StringUtils {
2 
3     /**
4      * 生成二级目录
5      * @param fileName  abc.txt
6      * @return  /4/5
7      */
8     public static String getDir(String fileName) {
9         //1 hashCode值
10         int hashCode = fileName.hashCode();
11         System.out.println(hashCode);
12         //2 第一层 0xf表示15的16进制数。
13         int dir1 = hashCode & 0xf;
14         //3 第二层目录
15         int dir2 = hashCode >>> 4 & 0xf;
16         
17         //4 拼写
18         return "/" + dir1 + "/" + dir2;
19     }
20     
21     public static void main(String[] args) {
22         System.out.println(getDir("abc.txt"));
23     }
24 
25 }

上传代码

public void doGet(HttpServletRequest request, HttpServletResponse response)
2             throws ServletException, IOException {
3         try {
4             
5             //0.5 检查是否支持文件上传  ,检查请求头Content-Type : multipart/form-data 
6             if(!ServletFileUpload.isMultipartContent(request)){
7                 throw new RuntimeException("不要得瑟,没用");
8             }
9             
10             //1 工厂
11             DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
12             // 1.1 设置是否生产临时文件临界值。大于2M生产临时文件。保证:上传数据完整性。
13             fileItemFactory.setSizeThreshold(1024 * 1024 * 2);  //2MB
14             // 1.2 设置临时文件存放位置
15             // * 临时文件扩展名  *.tmp  ,临时文件可以任意删除。
16             String tempDir = this.getServletContext().getRealPath("/temp");
17             fileItemFactory.setRepository(new File(tempDir));
18             
19             //2 核心类
20             ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
21             // 2.1 如果使用无参构造  ServletFileUpload() ,手动设置工厂
22             //servletFileUpload.setFileItemFactory(fileItemFactory);
23             // 2.2 单个上传文件大小
24             //servletFileUpload.setFileSizeMax(1024*1024 * 2);  //2M
25             // 2.3 整个上传文件总大小
26             //servletFileUpload.setSizeMax(1024*1024*10);        //10M
27             // 2.4 设置上传文件名的乱码
28             // * 首先使用  setHeaderEncoding 设置编码
29             // * 如果没有设置将使用请求编码 request.setCharacterEncoding("UTF-8")
30             // * 以上都没有设置,将使用平台默认编码
31             servletFileUpload.setHeaderEncoding("UTF-8");
32             // 2.5 上传文件进度,提供监听器进行监听。
33             servletFileUpload.setProgressListener(new MyProgressListener());
34             
35             
36             //3 解析request  ,List存放 FileItem (表单元素的封装对象,一个<input>对应一个对象)
37             List<FileItem> list = servletFileUpload.parseRequest(request);
38             
39             //4 遍历集合获得数据
40             for (FileItem fileItem : list) {
41                 // 判断
42                 if(fileItem.isFormField()){
43                     // 5 是否为表单字段(普通表单元素)
44                     //5.1.表单字段名称
45                     String fieldName = fileItem.getFieldName();
46                     System.out.println(fieldName);
47                     //5.2.表单字段值 , 解决普通表单内容的乱码
48                     String fieldValue = fileItem.getString("UTF-8");
49                     System.out.println(fieldValue);
50                 } else {
51                     //6 上传字段(上传表单元素)
52                     //6.1.表单字段名称  fileItem.getFieldName();
53                     //6.2.上传文件名
54                     String fileName = fileItem.getName();
55                     // * 兼容浏览器, IE : C:\Users\liangtong\Desktop\abc.txt  ; 其他浏览器 : abc.txt
56                     fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
57                     // * 文件重名
58                     fileName = UUID.randomUUID().toString().replace("-", "") + fileName;
59                     // * 单个文件夹文件个数过多?
60                     String subDir = StringUtils.getDir(fileName);
61                     
62                     System.out.println(fileName);
63                     //6.3.上传内容
64                     InputStream is = fileItem.getInputStream();
65                     String parentDir = this.getServletContext().getRealPath("/WEB-INF/upload");
66                     File file = new File(parentDir + subDir,fileName);
67 
68                     // 将指定流 写入 到 指定文件中  -- mkdirs() 自动创建目录
69                     FileUtils.copyInputStreamToFile(is, file);
70                     
71                     //7删除临时文件
72                     fileItem.delete();
73                 }
74             }
75             
76         } catch (Exception e) {
77             e.printStackTrace();
78             
79             throw new RuntimeException(e);
80             
81         }
82     }

其中如果需要做上传的进度条时,就可以使用监听器来进行监听上传数据的进度,实现ProgressListener即可。

总结上传:

        其实理解了也不是很难,就是上传文件后的处理比较麻烦,各种小问题,存储过程最为麻烦。

        1、创建工厂类

        2、使用核心类,

        3、解析request请求,

        4、遍历请求体的内容,将上传内容和普通表单内容都获取出来

        5、获取到上传内容时,对其存储位置进行设置

        其中很多细节问题都在上面已经说清楚了,代码中也有很多注释。

二、下载原理和代码实现

下载的常用 API 说明:
response.getOutputStream();
servletContext.getResourceAsStream();
servletContext.getMimeType();
response.setContentType();
response.setHeader("Content-Disposition", "attachment; fileName=1.jpg");
这个响应头告诉浏览器。这是需要下载的。而 attachment 表示附件,也就是下载的一个文件。fileName=后面,
表示下载的文件名。
完成上面的两个步骤,下载文件是没问题了。但是如果我们要下载的文件是中文名的话。你会发现,下载无法正确
显示出正确的中文名。
原因是在响应头中,不能包含有中文字符,只能包含 ASCII 码。

            1、设置响应头,让浏览器知道应该下载,而不是解析

            response.setHeader("content-disposition", "attachment;filename=" +fileName);  //设置content-disposition响应头

            2、获取输入流,指向需要下载的文件

            InputStream is = this.getServletContext().getResourceAsStream("/download/1.jpg");

            3、获取输出流,将其文件传到浏览器端

            ServletOutputStream out = response.getOutputStream();

            4、使用IOUtils.copy(is,out);直接将输入流和输出流传进去,就会帮我们把输出流读到的内容通过输出流输出到浏览器。内部实现原理应该就如下所示

                int b = -1;

                byte[] bf = new byte[1024];

                while((b=is.read(bf)) != -1){

                  out.write(bf,0,b);

                }

文件下载示例:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
IOException {
// 1、获取要下载的文件名
String downloadFileName = "2.jpg";
// 2、读取要下载的文件内容 (通过 ServletContext 对象可以读取)
ServletContext servletContext = getServletContext();
// 获取要下载的文件类型
String mimeType = servletContext.getMimeType("/file/" + downloadFileName);
System.out.println("下载的文件类型:" + mimeType);
// 4、在回传前,通过响应头告诉客户端返回的数据类型
resp.setContentType(mimeType);
// 5、还要告诉客户端收到的数据是用于下载使用(还是使用响应头)
// Content-Disposition 响应头,表示收到的数据怎么处理
// attachment 表示附件,表示下载使用
// filename= 表示指定下载的文件名
resp.setHeader("Content-Disposition", "attachment; filename=" + downloadFileName);
/**
* /斜杠被服务器解析表示地址为 http://ip:prot/工程名/ 映射 到代码的 Web 目录
*/
InputStream resourceAsStream = servletContext.getResourceAsStream("/file/" +
downloadFileName);
// 获取响应的输出流
OutputStream outputStream = resp.getOutputStream();
// 3、把下载的文件内容回传给客户端
// 读取输入流中全部的数据,复制给输出流,输出给客户端
IOUtils.copy(resourceAsStream,outputStream);
}

附件中文名乱码问题解决方案:

先说解决办法:

URLEncoder解决所有浏览器附件中文件名问题。

@Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     //String filename=request.getParameter("filename");
        String userAgent = request.getHeader("User-Agent");
        String fileName="2922Ö中文";
        String encodedFileName=fileName;
        //String encodedFileName=URLEncoder.encode(fileName, "UTF-8");
//     if (userAgent.contains("MSIE")||userAgent.contains("Trident") || userAgent.contains("Chrome") || userAgent.contains("Firefox")) {
//         encodedFileName = URLEncoder.encode(fileName, "UTF-8");
//      }

        encodedFileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");

       //encodedFileName = new String(fileName.getBytes("gbk"),"iso-8859-1");
        System.out.println(encodedFileName);
     //String folder="/download/";
        System.out.println("hello");
        String ua = request.getHeader("User-Agent");
// 判断是否是火狐浏览器
        if (userAgent.contains("Edg") || userAgent.contains("MSIE")||userAgent.contains("Trident") || userAgent.contains("Chrome") || userAgent.contains("Firefox")) {
// 使用下面的格式进行 BASE64 编码后
            String str = "attachment; fileName=" + "=?utf-8?B?"
                    + new BASE64Encoder().encode("中文14.jpg".getBytes("utf-8")) + "?=";
// 设置到响应头中
          //response.setHeader("Content-Disposition", str);
        } else {
// 把中文名进行 UTF-8 编码操作。
            String str = "attachment; fileName=" + URLEncoder.encode("中文14.jpg", "UTF-8");
// 然后把编码后的字符串设置到响应头中

            //response.setHeader("Content-Disposition", str);
        }
        //response.addHeader("Content-Type", "application/octet-stream");
        response.addHeader("Content-Disposition", "attachment;filename=" +encodedFileName);
       // response.setHeader("Content-disposition", String.format("attachment; filename=\"%s.png\"; filename*=utf-8''%s.png", encodedFileName, encodedFileName));
        InputStream is = getServletContext().getResourceAsStream("/download/123.png");
        System.out.println(is);
        ServletOutputStream out = response.getOutputStream();
        System.out.println(out);
        int b ;
        byte[] bf = new byte[1024];

        while((b=is.read(bf)) != -1){
           out.write(bf,0,b);

}

    }
}

下面是一种测试,也有中规范里面的解决办法

response.setHeader("Content-disposition", String.format("attachment; filename=\"%s.txt\"; filename*=utf-8''%s.txt", encodedFileName, encodedFileName));//为了兼容 IE6,原始文件名必须包含英文扩展名!

Content-Disposition: attachment;
                     filename="$encoded_fname";
                     filename*=utf-8''$encoded_fname

其中, $encoded_fname 指的是将 UTF-8 编码的原始文件名按照 RFC 3986 进行百分号编码(percent encoding)后得到的( PHP 中使用 rawurlencode() 函数)。这几行也可以合并为一行(推荐使用一个空格隔开)。

另外,为了兼容 IE6 ,请保证原始文件名必须包含英文扩展名!

Historically, HTTP has allowed field content with text in the ISO-8859-1 charset [ISO-8859-1], supporting other charsets only through use of [RFC2047] encoding. In practice, most HTTP header field values use only a subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD limit their field values to US-ASCII octets. A recipient SHOULD treat other octets in field content (obs-text) as opaque data.

‎从历史上看,HTTP 允许包含 ISO-8859-1 字符集 [ISO-8859-1] 中的文本的字段内容,仅通过 [RFC2047] 编码支持其他字符集。实际上,大多数 HTTP 标头字段值仅使用 US-ASCII 字符集 [USASCII] 的子集。新定义的标头字段应限制其字段值为 US-ASCII 八位字节。收件人应将字段内容(obs-text)中的其他八位字节视为不透明数据。‎(纯机器翻译)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM