1. 文件上传

在web开发中文件上传是一个十分重要的操作,通过网页上传文件只能通过表单的 file 表单输入域进行。但是一般情况下通过表单上传的数据都是文本数据,通过键值对传送。就像不可能使用字符流成功拷贝图片一样,显然不能将二进制的文件数据转化为字符串的value进行传送。所以,要传送二进制数据,我们就需要一种新的编码方式 multipart/form-data,通过form表单的enctype属性进行设置 enctype='multipart/form-data'

故而前端页面如下:

<form action="响应路径..." method="post" enctype="multipart/form-data">
    <input type="file" name="upfile" /><br/>
    <input type="text" name="text" value="其他数据" /><br/>
    <input type="radio" name="sex" value="男"/>男<br/>
    <input type="radio" name="sex" value="女"/>女<br/>
    <button type="submit">提交</button><br/>
</form>

multipart/form-data 不是以key-value形式编码,那它是如何编码的呢?

容易发现 servlet 中无法通过getParameter获取参数了,那我们只能通过request.getInputStream() 获取请求数据的输入流,打印一下看看这 multipart/form-data 格式的数据到底长啥样?

servlet如下:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	// 获取输入流并转化为字符流
    ServletInputStream inputStream = request.getInputStream();
    InputStreamReader isr = new InputStreamReader(inputStream,"utf-8"); 
    BufferedReader br = new BufferedReader(isr);
    // 打印请求体的数据
    String buff;
    while((buff=br.readLine())!=null){
        System.out.println(buff);
    }
    br.close();
}

输出结果如下:

可以发现数据被 ------WebKitFormBoundaryhl5cYVh7Z4LDjf3x 分成了几部分。

分析可知每一部分都对应这表单中的一个输入域,也就相当于原来的一组 key-value。

分割行的下一行 Content-Disposition: form-data; name="text" 中name保存了表单输入域的name值。

如果是文件还会多一个filename 项,也就是上传文件的文件名。同时下一行指明文件的类型 Content-Type: image/png,若是普通的表单输入域则不会有这一行。

再往下都是一个空行分割,然后就是表单域对应的值。如上例中的 5-14 行就是上传的文件的数据了,只不过这里被我强转为了字符串输出,出现乱码。

解析数据,完成上传。

了解了请求数据的结构也就好办了,只要我们将其中的二进制截出来放到文件里就行了。

但是还有一个问题:每次请求的分隔符 ------WebKitFormBoundaryhl5cYVh7Z4LDjf3x 都是不一样的,那么如何获得这个分隔符的?

原来这东西已经包含在请求头中一起被提交了,我们只需要从请求头中拿出来就好

要注意:这里少两个 -- ,而上边 的请求体结束时最后一行最后也是 --。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 获取分隔符
    String header = request.getHeader("Content-Type");
    String sp = header.split("boundary=")[1];
    // 存储路径
    String dir = "C:\\Users\\默尘\\Desktop\\新建文件夹\\";
    String fileName=null;
    BufferedOutputStream bos = null;
    // 从输入流中截取文件数据
    ServletInputStream inputStream = request.getInputStream();
    int readFlag = 3;
    for(boolean flag=true;flag;){
        // 读一行
        int c;
        List<Integer> line = new ArrayList<>();
        while((c=inputStream.read())!='\n'){
            if(c==-1){
                flag=false;
                break;
            }
            line.add(c);
        }
        // data中保存二进制数据
        byte[] data = tobyteArray(line);
        // lineStr中保存转码后的字符串
        String lineStr = new String(data,"utf-8");
        // 根据字符串进行判断
        if(lineStr.contains("filename")){
            readFlag=2; // 表示现在进入文件域了,准备读...
            fileName = lineStr.substring(lineStr.indexOf("filename=\"")+10,lineStr.length()-2);
            fileName = URLDecoder.decode(fileName,"utf-8");
            if(fileName.contains("''")) {
                fileName = fileName.split("''")[1];
            }
            bos = new BufferedOutputStream(new FileOutputStream(dir+fileName));
            System.out.println("文件名:" + fileName);
        }else if(readFlag==2 && "\r".equals(lineStr)){
            readFlag=1; // 表示下一行就是文件内容,可以读了。
        }else if(lineStr.contains(sp) && readFlag==1){// 分割行,退出读状态
            bos.close();
            readFlag = 3;
        }else if(readFlag==1){ // 如果处于读状态 就拷贝数据
            bos.write(data);
            bos.write(10); // 把 \n 补上
        }
    }
}
private byte[] tobyteArray(List<Integer> line){
    byte[] tmp = new byte[line.size()];
    for(int i=0;i<tmp.length;i++){
        tmp[i] = line.get(i).byteValue();
    }
    return tmp;
}

使用 FileUpload 工具类完成文件上传

可以发现,从文本与二进制夹杂的输入流中截取数据是非常麻烦的,而且这还只是提取文件而已,普通文本域的数据并未提取。

所以,为了便于文件上传,可以借助一些工具类。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 存储路径
    String dir = "C:\\Users\\默尘\\Desktop\\新建文件夹\\";
    try {
        //1.创建磁盘文件项工厂
        DiskFileItemFactory factory = new DiskFileItemFactory();
        //2.创建文件上传核心类
        ServletFileUpload upload = new ServletFileUpload(factory);
        //2.1 设置上传文件名的编码
        upload.setHeaderEncoding("utf-8");
        //2.2 判断表单是否是文件上传表单
        boolean multipartContent = upload.isMultipartContent(req);
        //2.3 是文件上传表单
        if (multipartContent) {
            //3. 解析request ,获取文件项集合
            List<FileItem> list = upload.parseRequest(req);
            if (list != null) {
                //4.遍历获取表单项
                for (FileItem item : list) {
                    //5. 判断是不是一个普通表单项
                    boolean formField = item.isFormField();
                    if (formField) {
                        //普通表单项
                        String fieldName = item.getFieldName();
                        String value = item.getString("utf-8");//设置编码
                        System.out.println(fieldName + "=" + value);
                    } else {
                        //文件上传项
                        //文件名
                        String fileName = item.getName();
                        //避免图片名重复
                        String newFileName = new Random().nextInt() + "_" + fileName;
                        //获取输入流
                        InputStream in = item.getInputStream();
                        //创建输出流 输出到H盘
                        FileOutputStream fos = new FileOutputStream(dir+newFileName);
                        //使用Java9新特性,copy文件
                        in.transferTo(fos);
                        //关闭流
                        fos.close();
                        in.close();
                    }
                }
            }
        }
    } catch (FileUploadException e) {
        e.printStackTrace();
    }
}

相关类说明:

  1. 导入依赖:
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.2.1</version>
</dependency>
  1. DiskFileItemFactory类
    磁盘文件项工厂, 读取文件时相关的配置,比如: 缓存的大小 , 临时目录的位置
  2. ServletFileUplaod类
    用于解析请求对象。是文件上传的核心类
    isMultipartContent(request); 判断请求是不是 multipart/form-data 格式的。
    parseRequest(request); 解析请求,返回 FileItem 的集合。
    setHeaderEncoding("UTF-8"); 设置每个域中信息头的编码;若不设置,文件名有中文时,可能乱码。
  3. FileItem类
    由ServletFileFactory解析request得到,每个FileItem对象都代表表单中的一个输入域。
    isFormField() 判断输入域是否为 普通文本域
    getFieldName() 获取 \<input /\> 中的name值
    getString("utf-8") 获取普通文本域的 value,指定其编码以防止出现乱码。
    getName() 获取上传的文件名
    getInputStream() 获得上传文件的输入流
    delete() 删除零时文件

使用@MultipartConfig注解完成文件上传

使用工具类虽然简化了代码且功能强大,但还是要导包,也挺麻烦的,servlet3.0之后,servlet就支持了文件上传功能。

首先,要在Servlet上添加注解:@MultipartConfig

然后就可以通过 getParameter( ) 方法获取一般的表单域的数据。通过 Part对象 就可以获取文件数据

通过request的以下方法就可以获取Part对象

request.getPart(String name); 	// 通过表单域的name获取每个文件对应的Part对象
request.getParts(); 			// 获取所有的 Part对象

每个Part对象代表一个表单输入域。

注意:

  • 当多文件上传时一个part只能代表一个文件,这时候就只能使用 getParts() 获取所有的Part再处理,但是普通文本域也会被一起获取到,处理时要注意区分。

Part对象的使用

// 获取文件信息
public long getSize();   // 获取文件的大小
public String getContentType();  // 获取文件的的类型
// 获取Part头部信息
public String getName(); // 获取表单域的name属性
public Colloction<String> getHearderNames(); //获取所有 Part头 的name
public String getHeader(String name);  // 根据 Part头的名字 获取对应的值
public Colloction<String> getHeaders(String name); // 作用同上,用于值有多个的情况
// 文件操作
public void write(String filePath); // 将上传的文件写入到指定的文件中。
public InputStream getInputStream(); // 获取上传文件的输入流
public void delete(); // 删除临时文件

注意:

  • Part头是指每一部分数据之前的内容,一行为一个Part头,如:Content-Disposition
  • Part对象并没有获取文件名的函数,只能自己从Part头信息中截取。
private String getFileName(Part part){
    String header = part.getHeader("Content-Disposition");
    return header.substring(header.indexOf("filename=\"")+10,header.lastIndexOf("\""));
}

举例

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setCharacterEncoding("utf-8");
    // 存储路径
    String dir = "C:\\Users\\默尘\\Desktop\\新建文件夹\\";
    // 根据name获取 Part
    Part upfile = request.getPart("upfile");
    // 上传文件 的 信息
    String fileName = getFileName(upfile);
    System.out.println("文件名称:\t"+fileName);
    System.out.println("文件大小:\t"+upfile.getSize()+" B");
    System.out.println("文件类型:\t"+upfile.getContentType());
    // part 头部信息
    System.out.println("part的name\t"+upfile.getName());
    Collection<String> headerNames = upfile.getHeaderNames();
    System.out.println("headers:");
    for (String headerName : headerNames) {
        String value = upfile.getHeader(headerName);
        System.out.println("\t"+headerName+" : "+value);
    }
    // 存储文件
    upfile.write(dir+fileName);
    System.out.println("over");
}

private String getFileName(Part part){
    String header = part.getHeader("Content-Disposition");
    return header.substring(header.indexOf("filename=\"")+10,header.lastIndexOf("\""));
}

关于@MultipartConfig注解:

常用属性如下:

属性名

类型

说明

location

String

指定容器临时存放文件 的目录

maxFileSize

long

允许上传的文件的最大字节数

fileSizeShreshold

int

但上传的数据大于该值时,就会将数据写到磁盘

也可以通过web.xml配置

<servlet>
    <servlet-name>sss</servlet-name>
    <servlet-class>xxxxx</servlet-class>
    <multipart-config>
        <location></location>
        <file-size-threshold></file-size-threshold>
        <max-file-size></max-file-size>
    </multipart-config>
</servlet>