一、文件上传简介
1、文件上传的步骤
(1)要有一个 form 表单,请求方式为 post 请求(因为上传的文件一般都超出长度限制)。

(2)form 标签的 encType 属性值必须为 multipart/form-data。

表示提交的数据,以多段的形式进行拼接,然后以二进制流的形式发送给服务器。

多段:一个表单项代表一个数据段

(3)在 form 标签中使用 input-type = file 添加上传的文件。

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

2、一个简单的上传 Demo
(1)uploadFile.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <base href = "http://localhost:8080/file_war_exploded/"/>
</head>
<body>
    <form action = "./start_uploadFileServlet" method = "POST" enctype = "multipart/form-data">
        用户名:<input type = "text" name = "username"/> <br/>
        头像:<input type = "file" name = "photo"/> <br/>
        <input type = "submit" value = "上传"/>
    </form>
</body>
</html>


(2)uploadFileServlet 类:

package com.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;

public class uploadFileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("文件上传成功");
    }
}


3、上传的 http 协议内容
(1)请求体【负载】-【查看源代码】

 

JavaWeb——文件上传与下载_java

 

(2)分析协议内容

 

JavaWeb——文件上传与下载_java_02

 

(3)服务器如何接收

因为客户端是以流的形式发送的数据,所以服务器也以流的形式接收

package com.servlet;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;

public class uploadFileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("文件上传成功");
        // 获取流
        ServletInputStream servletIS = req.getInputStream();
        // 缓冲区
        byte[] buffer = new byte[1024000];
        int read = servletIS.read(buffer);
        // 以字符串形式输出
        System.out.println(new String(buffer, 0, read));
    }
}

4、commons-fileupload.jar 常用 API 介绍
在使用之前需要导入两个 jar 包(版本自定):

commons-fileupload-1.2.1.jar
commons-io-1.4.jar
(1)常用的类和方法:

ServletFileUpload 类:用于解析上传的数据
FileItem 类:表示每一个表单项
Boolean isMultipartContent( request ) 方法:判断当前上传的数据格式是否为多段的格式(一般文件的格式才是多段的)
List<FileItem> parseRequest( request ) 方法:解析上传的数据
Boolean isFormField() 方法:判断当前这个表单项,是普通表单项(true),还是上传文件的表单项(false)
String getFieldName() 方法:获取表单项的 name 属性值
String getString() 方法:获取当前表单项的值(对普通表单项而言就是 value 属性值;对文件而言就是文件)
String getName() 方法:获取上传的文件名
void write( file ) 方法:将上传的文件写到 file 对象指向的磁盘路径
(2)示例:

package com.servlet;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.File;
import java.io.IOException;
import java.util.*;

public class uploadFileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("文件上传成功");
        // 先判断上传的数据是否为多段,只有多段的数据才是文件上传的
        if (ServletFileUpload.isMultipartContent(req)) {
            // 创建 FileItemFactory 实现类
            FileItemFactory fileItemFactory = new DiskFileItemFactory();
            // 创建用于解析上传数据的工具类 ServletFileUpload 类
            ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
            try {
                // 解析上传的数据,获得表单项列表 List<FileItem>
                List<FileItem> fileItemList = servletFileUpload.parseRequest(req);
                // 遍历表单项,判断是否为文件
                for (FileItem fileItem : fileItemList) {
                    if (fileItem.isFormField()) { // 普通
                        System.out.println("表单项的name属性值:" + fileItem.getFieldName());
                        System.out.println("表单项的value属性值:" + fileItem.getString("utf-8"));
                    } else { // 文件
                        System.out.println("表单项的name属性值:" + fileItem.getFieldName());
                        System.out.println("上传的文件名:" + fileItem.getName());
                        // 绝对路径保存到本地(服务器)
                        fileItem.write(new File("D:/Programming Project/JavaProject/WebDemoProject/Demo/file/web/src/upload/" + fileItem.getName()));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

二、文件下载简介
1、文件下载的步骤
(1)客户端请求下载

(2)服务器获取要下载的文件名,在服务器硬盘读取要下载的文件内容

(3)获取文件数据类型,通过响应头告诉客户端,服务器即将返回此数据类型

(4)通过响应头告诉客户端,返回的文件用于下载,而非显示到浏览器页面上

(5)将资源作为输入流,返回给(输出到)客户端

2、一个简单的下载 Demo
注意:相对路径不要写错了,在浏览器上是不会有报错的。

package com.servlet;

import org.apache.commons.io.IOUtils;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class downloadFileServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1.获取要下载的文件名
        String downloadFileName = "R-C.jpg";
        // 2.读取要下载的文件内容(使用ServletContext对象可以读取)
        ServletContext servletContext = getServletContext();
        // 3.获取文件数据类型,并通过响应头告诉客户端,服务器即将返回此数据类型
        String mime = servletContext.getMimeType("./src/upload/" + downloadFileName); // 暂时将 upload 的文件作为可下载的文件
        resp.setContentType(mime);
        // 4.通过响应头告诉客户端,返回的文件用于下载,而非显示到浏览器页面上
        /*
            content-Disposition:表示如何处理数据
            attachment:表示附件
            filename:表示下载的文件名
         */
        resp.setHeader("content-Disposition", "attachment;filename=" + downloadFileName);
        // 5.将资源作为输入流,返回给(输出到)客户端
        InputStream inputStream = servletContext.getResourceAsStream("./src/upload/" + downloadFileName);
        OutputStream outputStream = resp.getOutputStream();
        IOUtils.copy(inputStream, outputStream);
    }
}

3、文件名包含中文的解决方法
这是个非常常见的问题,可以通过 URL 编码以及 Base64 编码这些操作解决。

这两种 encode 都没有进行 decode,解码工作交给了浏览器完成(可以发现下面的示例中,都没有 decode 相关的操作)。

(1)URL 编码

(1-1)使用 URLEncoder 类的 encode 函数,传入文件名和编码格式即可:

String filename = URLEncoder.encode(downloadFileName, "UTF-8");
resp.setHeader("content-Disposition", "attachment;filename=" + filename);
URL 编码会将文件名转化为 %xx%xx 的格式,其中 xx 是十六进制。

(2)Base64 编码

(注意 Base64 有一个老版本是 Base64Encoder,jdk1.9 之后就不再使用了,需要将其更换为 Base64)

(2-1)另外需要注意的是,使用 Base64 编码,对请求头也有一定的编码格式要求:

content-Disposition: attachment; filename == ?charset?B?fileName?=
=? 表示编码内容的开始
charset 表示字符集
B 表示 Base64 编码
fileName 表示文件名编码后的内容
?= 表示编码内容的结束
(2-2)示例:

Base64.Encoder base64Encoder = Base64.getEncoder();
String encode = base64Encoder.encodeToString(downloadFileName.getBytes("UTF-8"));
// 注意 filename==?,== 两端不能有空格
resp.setHeader("content-Disposition", "attachment; filename==?UTF-8?B?" + encode + "?=");
4、不同浏览器的编码方式
上面介绍了两种解决中文编码的方法,但并不是所有浏览器都能兼容某一种方法。因此需要额外判断浏览器是哪一种浏览器。

具体方法就是:用域对象获取请求头中的 User-Agent 的值,然后查看是否包含某种浏览器的关键字。

if (req.getHeader("User-Agent").contains("Chrome")) {
String filename = URLEncoder.encode(downloadFileName, "UTF-8");
resp.setHeader("content-Disposition", "attachment; filename=" + filename);
} else {
// 其他编码方式
}
三、示例
编写简单的上传和下载页面,并作相应操作。

1、uploadFile.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <base href = "http://localhost:8080/file_war_exploded/"/>
</head>
<body>
    <form action = "./start_uploadFileServlet" method = "POST" enctype = "multipart/form-data">
        用户名:<input type = "text" name = "username"/> <br/>
        头像:<input type = "file" name = "uploadPhoto"/> <br/>
        <input type = "submit" value = "上传"/>
    </form>
</body>
</html>


2、downloadFile.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <base href = "http://localhost:8080/file_war_exploded/"/>
</head>
<body>
    <form action = "./start_downloadFileServlet" method = "POST" enctype = "application/x-www-form-urlencoded">
        文件名:<input type = "text" name = "filename"/>
        <input type = "submit" value = "下载"/>
    </form>
</body>
</html>


3、uploadFileServlet 类:

package com.servlet;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.File;
import java.io.IOException;
import java.util.*;

public class uploadFileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("文件上传成功");
        // 先判断上传的数据是否为多段,只有多段的数据才是文件上传的
        if (ServletFileUpload.isMultipartContent(req)) {
            // 创建 FileItemFactory 实现类
            FileItemFactory fileItemFactory = new DiskFileItemFactory();
            // 创建用于解析上传数据的工具类 ServletFileUpload 类
            ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
            try {
                // 解析上传的数据,获得表单项列表 List<FileItem>
                List<FileItem> fileItemList = servletFileUpload.parseRequest(req);
                // 遍历表单项,判断是否为文件
                for (FileItem fileItem : fileItemList) {
                    if (fileItem.isFormField()) { // 普通
                        System.out.println("表单项的name属性值:" + fileItem.getFieldName());
                        System.out.println("表单项的value属性值:" + fileItem.getString("utf-8"));
                    } else { // 文件
                        System.out.println("表单项的name属性值:" + fileItem.getFieldName());
                        System.out.println("上传的文件名:" + fileItem.getName());
                        // 绝对路径保存到本地(服务器),不是服务器去解析地址,因此不能用 / 映射至工程
                        fileItem.write(new File("D:/Programming Project/JavaProject/WebDemoProject/Demo/file/web/src/upload/" + fileItem.getName()));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

4、downloadFileServlet 类:

package com.servlet;

import org.apache.commons.io.IOUtils;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.URLEncoder;

public class downloadFileServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1.获取要下载的文件名
        String downloadFileName = req.getParameter("filename");
        // 2.读取要下载的文件内容(使用ServletContext对象可以读取)
        ServletContext servletContext = getServletContext();
        // 3.获取文件数据类型,并通过响应头告诉客户端,服务器即将返回此数据类型
        String mime = servletContext.getMimeType("./src/upload/" + downloadFileName); // 暂时将 upload 的文件作为可下载的文件
        resp.setContentType(mime);
        // 4.通过响应头告诉客户端,返回的文件用于下载,而非显示到浏览器页面上
        /*
            content-Disposition:表示如何处理数据
            attachment:表示附件
            filename:表示下载的文件名
         */
        if (req.getHeader("User-Agent").contains("Chrome")) {
            String filename = URLEncoder.encode(downloadFileName, "UTF-8");
            resp.setHeader("content-Disposition", "attachment; filename=" + filename);
        } else {
            // 其他编码方式
        }
        // 5.将资源作为输入流,返回给(输出到)客户端
        InputStream inputStream = servletContext.getResourceAsStream("./src/upload/" + downloadFileName);
        OutputStream outputStream = resp.getOutputStream();
        IOUtils.copy(inputStream, outputStream);
    }
}

5、index.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>$Title$</title>
    <base href = "http://localhost:8080/file_war_exploded/"/>
</head>
<body>
    <a href = "./jsp/uploadFile.jsp">上传</a> <br/>
    <a href = "./jsp/downloadFile.jsp">下载</a> <br/>
</body>
</html>


6、说明
(1)上传/下载容量较小的文件,使用 GET/POST 都可以(POST 请求更安全)

multipart/form-data 是最常用的表单上传属性值
(2)下载时 GET/POST 的区别

使用 GET 请求,那么 form 表单的 enctype 属性值是什么都可以
使用 POST 请求,那么 form 表单的 enctype 属性值必须是 application/x-www-form-urlencoded(默认值)
(3)第(2)点很重要

(4)第(3)点很重要

 

参考文章:http://blog.ncmem.com/wordpress/2023/12/12/javaweb-文件上传与下载/