SpringMVC之文件上传

  • 时间:2025-11-25 23:05 作者: 来源: 阅读:0
  • 扫一扫,手机访问
摘要: 一、文件上传的核心原理 文件上传的本质是: 客户端(浏览器)把本地文件的二进制数据,通过 HTTP 请求 发送给服务器。服务器接收这些二进制数据,再写入到服务器的磁盘中。 关键要点: HTTP 方法必须是 POST GET 方法会把参数拼在 URL 里,有长度限制(比如 2KB),无法传输大文件的二进制数据。POST 方法把数据放在请求体(body)里,没有长度限制。 请求头的 Cont

一、文件上传的核心原理

文件上传的本质是:

客户端(浏览器)把本地文件的二进制数据,通过 HTTP 请求 发送给服务器。服务器接收这些二进制数据,再写入到服务器的磁盘中。

关键要点:

HTTP 方法必须是 POST

GET 方法会把参数拼在 URL 里,有长度限制(比如 2KB),无法传输大文件的二进制数据。POST 方法把数据放在请求体(body)里,没有长度限制。

请求头的 Content-Type 必须是 multipart/form-data

默认表单提交是 application/x-www-form-urlencoded,只能传文本。 multipart/form-data 是专门用来传输二进制文件的编码格式,它会把文件分成一个个“块”(part)来传输。

服务器要解析 multipart/form-data 格式的数据

这种格式的数据不像普通表单那样是 key=value 形式,而是有分隔符、文件信息头(比如文件名、类型)和二进制内容。解析这种数据比较复杂,所以才有了各种工具(比如 Servlet 3.0+ 的 Part、Apache Commons FileUpload、Spring MVC 的 MultipartFile)来帮我们处理。

二、前端表单的写法(必须配合)

不管后端用哪种方式,前端表单都要满足以下条件:


<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="uploadFile"> <!-- name 属性很重要,后端要通过它获取文件 -->
  <button type="submit">上传</button>
</form>
action:后端处理上传的接口地址。 method="post":必须是 POST。 enctype="multipart/form-data":必须指定,否则服务器无法识别文件。 input type="file":用户选择本地文件的控件, name 属性的值(比如 uploadFile)要和后端代码中获取文件的参数名一致。

三、后端实现方式详解(从简单到复杂)

我们先看最容易理解的 Spring MVC 方式,再回头看 Servlet 和 Commons FileUpload。

方式三:Spring MVC(最简单,推荐在 Spring 项目中使用)

核心思想

Spring MVC 已经帮我们封装好了文件上传的逻辑,我们只需要用 MultipartFile 接口接收文件,调用它的方法就能完成保存。

代码逐行解析

@Controller // 标记这是一个 Spring MVC 控制器,能接收请求
public class UploadController {

    // 接收 /fileupload.do 请求,且是 POST 方式(默认支持 GET 和 POST,建议用 @PostMapping 明确)
    @RequestMapping("/fileupload.do")
    public String upload(
            MultipartFile upload, // 核心:接收文件,变量名 "upload" 必须和前端 input 的 name 一致
            HttpServletRequest request // 用来获取服务器路径
    ) throws IOException {

        // 1. 确定文件在服务器上的存储路径
        // getRealPath("/uploads"):获取 Web 应用根目录下的 uploads 文件夹的绝对路径
        // 比如 Tomcat 部署后,路径可能是:D:	omcatwebappsyour_projectuploads
        String realPath = request.getSession().getServletContext().getRealPath("/uploads");

        // 2. 检查并创建目录(如果不存在)
        File fileDir = new File(realPath);
        if (!fileDir.exists()) {
            fileDir.mkdirs(); // mkdirs() 能创建多级目录(比如 uploads 不存在就创建)
        }

        // 3. 获取原始文件名(比如用户上传的文件叫 "头像.jpg")
        String originalFilename = upload.getOriginalFilename();

        // 4. 生成唯一文件名(防止同名文件覆盖)
        // UUID 是全球唯一的字符串,替换掉 "-" 后更简洁
        String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        // 最终文件名:UUID_原始文件名(比如 "550E8400E29B41D4A716446655440000_头像.jpg")
        String targetFilename = uuid + "_" + originalFilename;

        // 5. 保存文件到服务器
        // transferTo():Spring MVC 封装的方法,直接把上传的文件写入到指定路径
        upload.transferTo(new File(fileDir, targetFilename));

        // 6. 跳转成功页面(Spring MVC 会根据视图解析器找到 suc.jsp)
        return "suc";
    }
}
为什么简单?
Spring MVC 已经帮我们完成了 解析 multipart/form-data 数据 的工作,我们不用关心底层的二进制流处理。 MultipartFile 封装了文件的所有信息(文件名、大小、类型、二进制数据),提供了简单的方法( getOriginalFilename() transferTo())来操作。

方式一:原生 Servlet(Servlet 3.0+ 支持,不用额外导包)

核心思想

Servlet 3.0 以后,官方提供了 Part 接口来处理文件上传,本质是 Servlet 容器(比如 Tomcat)帮我们解析了 multipart/form-data 数据,把每个文件封装成一个 Part 对象。

代码逐行解析

// 1. 注册 Servlet 路径,前端 form 的 action 要指向这个路径
@WebServlet("/nativeUpload")

// 2. 开启文件上传支持(必须加这个注解,否则 getPart() 会报错)
@MultipartConfig(
        fileSizeThreshold = 1024 * 1024 * 2, // 2MB 以下存内存,以上存临时文件
        maxFileSize = 1024 * 1024 * 10,      // 单个文件最大 10MB
        maxRequestSize = 1024 * 1024 * 50    // 一次请求最大 50MB(多个文件+其他字段)
)
public class NativeUploadServlet extends HttpServlet {

    // 处理 POST 请求(文件上传必须用 POST)
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 3. 处理乱码(文件名和表单字段的中文不会乱码)
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");

        // 4. 获取文件对象(参数 "upload" 要和前端 input 的 name 一致)
        Part part = req.getPart("upload");

        // 5. 校验文件是否为空(用户没选文件就提交的情况)
        if (part != null && part.getSize() > 0) {

            // 6. 确定存储路径(和 Spring MVC 一样)
            String realPath = getServletContext().getRealPath("/uploads");
            File fileDir = new File(realPath);
            if (!fileDir.exists()) {
                fileDir.mkdirs();
            }

            // 7. 获取原始文件名(Servlet 3.1+ 支持 getSubmittedFileName())
            String originalFilename = part.getSubmittedFileName();

            // 8. 生成唯一文件名(和 Spring MVC 一样)
            String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
            String targetFilename = uuid + "_" + originalFilename;

            // 9. 保存文件(part.write() 是 Servlet 提供的方法)
            part.write(realPath + File.separator + targetFilename);

            // 10. 响应成功信息
            resp.getWriter().print("上传成功!保存路径:" + realPath + File.separator + targetFilename);
        } else {
            resp.getWriter().print("上传失败:未选择文件或文件为空!");
        }
    }
}
和 Spring MVC 的区别?
原生 Servlet 需要手动加 @MultipartConfig 注解开启上传。获取文件用 req.getPart("name"),而 Spring MVC 用 MultipartFile 参数。保存文件用 part.write(),而 Spring MVC 用 transferTo()。原生 Servlet 更底层,Spring MVC 是对 Servlet 的封装,更方便。

方式二:Apache Commons FileUpload(兼容旧版 Servlet,需要导包)

核心思想

在 Servlet 3.0 之前,官方没有提供文件上传的 API,所以需要用第三方工具(Apache Commons FileUpload)来解析 multipart/form-data 数据。

依赖(必须导入这两个 jar 包)
commons-fileupload-1.4.jar(核心解析逻辑) commons-io-2.11.0.jar(文件流辅助工具)
代码逐行解析

@WebServlet("/commonsUpload")
public class CommonsUploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 处理乱码
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");

        // 2. 校验表单是否是 multipart/form-data 类型
        if (!ServletFileUpload.isMultipartContent(req)) {
            resp.getWriter().print("错误:表单必须加 enctype='multipart/form-data'!");
            return;
        }

        // 3. 创建磁盘文件项工厂(配置临时文件存储)
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 可选配置:factory.setSizeThreshold(1024*1024); // 1MB 以下存内存,以上存临时文件

        // 4. 创建文件上传解析器
        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setHeaderEncoding("UTF-8"); // 解决文件名中文乱码

        try {
            // 5. 解析请求,获取所有表单项(文件+普通字段)
            List<FileItem> items = upload.parseRequest(req);

            // 6. 遍历表单项
            for (FileItem item : items) {
                // 7. 区分普通字段和文件字段
                if (item.isFormField()) {
                    // 处理普通字段(比如 input type="text")
                    String fieldName = item.getFieldName(); // 字段名
                    String fieldValue = item.getString("UTF-8"); // 字段值(必须指定编码)
                    System.out.println("普通字段:" + fieldName + " = " + fieldValue);
                } else {
                    // 处理文件字段
                    String originalFilename = item.getName(); // 获取原始文件名

                    // 8. 校验文件是否为空
                    if (originalFilename != null && !originalFilename.trim().equals("")) {
                        // 兼容旧浏览器:有些浏览器会传完整路径(比如 C:UsersDesktopa.jpg),只取文件名
                        String filename = new File(originalFilename).getName();

                        // 9. 确定存储路径(和之前一样)
                        String realPath = getServletContext().getRealPath("/uploads");
                        File dir = new File(realPath);
                        if (!dir.exists()) dir.mkdirs();

                        // 10. 生成唯一文件名(和之前一样)
                        String uuid = UUID.randomUUID().toString().replace("-", "");
                        String targetFilename = uuid + "_" + filename;

                        // 11. 保存文件
                        File saveFile = new File(dir, targetFilename);
                        item.write(saveFile); // FileItem 的 write() 方法

                        // 12. 清理临时文件(可选,防止磁盘占满)
                        item.delete();

                        // 13. 响应成功信息
                        resp.getWriter().print("上传成功!保存文件名:" + targetFilename);
                    }
                }
            }
        } catch (Exception e) {
            // 捕获异常(比如文件过大、磁盘空间不足)
            e.printStackTrace();
            resp.getWriter().print("上传失败:" + e.getMessage());
        }
    }
}
和原生 Servlet 的区别?
Commons FileUpload 是第三方工具,需要导包;原生 Servlet 是官方 API,不用导包。Commons FileUpload 能解析出 普通表单项(比如 text 输入框)和文件项,原生 Servlet 需要额外处理普通字段。Commons FileUpload 兼容旧版 Servlet(比如 Servlet 2.5),而原生 Servlet 3.0+ 才支持。

注意事项

文件存储路径 getRealPath("/uploads") 获取的是 Web 应用部署目录下的路径,项目重新部署(比如 Tomcat 重启、war 包重新上传)时,这个目录下的文件会丢失。生产环境中,建议把文件存到项目外部的固定路径(比如 D:uploads),或者用云存储(阿里云 OSS、腾讯云 COS)。文件大小限制:必须设置文件大小限制,防止用户上传超大文件导致服务器磁盘占满或内存溢出。文件名乱码:前端和后端都要处理编码(比如 UTF-8),否则中文文件名会变成乱码。安全问题:要校验文件类型(比如只允许上传图片、文档),防止用户上传恶意文件(比如 .exe 病毒)。可以通过文件名后缀或文件的 MIME 类型校验。

四、IO流知识回顾

一、IO概述与流的分类

1. IO核心概念

IO(Input/Output)即输入与输出,是设备或程序与外部环境的数据交互。比如键盘向程序输入内容是输入,程序向显示器输出图像、向文件写入内容是输出

Java中为了简化IO操作,将数据传输抽象为(可以理解为数据流动的管道),所有IO操作都通过操作流对象完成。以游戏程序为例,读取配置文件是输入操作,保存游戏存档是输出操作。

2. 流的两大分类

Java中的IO流从两个维度进行划分,覆盖所有场景需求:

按输入输出方向划分 输入流:数据从外部(文件、键盘等)流入程序,用于读取数据。输出流:数据从程序流出到外部,用于写入数据。 按传输数据类型划分 字符流:传输文本数据(如文章、Java代码文件),我们能直接读懂内容,核心操作类以 Reader(输入)和 Writer(输出)结尾。字节流:传输二进制数据(如图片、音频、视频),用文本打开是乱码,核心操作类以 InputStream(输入)和 OutputStream(输出)结尾。

二、字符流详解

字符流专门处理文本数据,核心抽象类是Reader(输入流)和Writer(输出流),子类需实现其抽象方法完成具体操作。

1. 核心父类介绍
Writer:字符输出流抽象类,实现了 Closeable(关闭流)、 Flushable(刷新流)、 Appendable(追加内容)接口。子类有 FileWriter BufferedWriter等,必须实现 write(char[], int, int) flush() close()三个方法。Reader:字符输入流抽象类,实现了 Closeable Readable(读取数据)接口。子类有 FileReader BufferedReader等,用于读取文本文件内容。
2. 字符流实战案例(附详细注释代码)

字符流操作的核心步骤:创建文件对象→创建流对象→操作数据→关闭流(释放资源,必须在 finally中执行,防止异常导致资源泄漏)

基础案例:向文件写入HelloWorld

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class IOTest {
    public static void main(String[] args) {
        // 1. 创建要操作的文件对象,文件名为test.txt
        File file = new File("test.txt");
        // 2. 声明Writer对象,初始化为null(方便finally中关闭)
        Writer writer = null;

        try {
            // 3. 创建FileWriter对象,将流指向test.txt文件
            writer = new FileWriter(file);
            // 4. 向文件写入字符串
            writer.write("HelloWorld");

        } catch (IOException e) {
            // 捕获IO异常(如文件权限不足、磁盘满等)
            e.printStackTrace();
        } finally {
            // 5. 关闭流:先判断writer不为null,避免空指针异常
            if (writer != null) {
                try {
                    writer.close(); // 关闭流会自动刷新缓冲区数据
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
文件内容追加
默认 FileWriter会覆盖文件原有内容,若需追加,在构造方法中添加 true参数:

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class IOTest4 {
    public static void main(String[] args) {
        Writer writer = null;

        try {
            // 构造方法第二个参数true表示追加模式,不覆盖原有内容
            writer = new FileWriter(new File("test1.txt"), true);
            // 向文件追加内容"liangliang"
            writer.write("liangliang");

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭流,释放资源
            if (writer != null) {
                try {
                    writer.close(); // 关闭前会自动执行flush()
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
写入内容并换行
不同操作系统换行符不同,Windows用 ,Linux用 ,Mac用 。可手动指定 适配Windows,同时演示 flush()方法(强制刷新缓冲区,将数据写入文件):

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class IOTest5 {
    public static void main(String[] args) {
        File file = new File("test.txt");
        Writer writer = null;
        try {
            writer = new FileWriter(file);
            // 循环写入100次HelloWorld,每次换行
            for (int i = 0; i < 100; i++) {
                writer.write("HelloWorld
"); // Windows换行符
                // 每写入10次,手动刷新缓冲区
                if (i % 10 == 0) {
                    writer.flush();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close(); // 关闭流时会自动flush
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
Writer的五种写入方法
Writer提供多种写入方式,支持字符数组、单个字符、字符串截取等,以下演示常用方法:

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class IOTest6 {
    public static void main(String[] args) {
        File file = new File("test.txt");
        Writer writer = null;
        try {
            writer = new FileWriter(file);
            // 定义字符数组
            char[] c = {'a', 'b', 'p', 'b', 'p'};

            // 方法1:写入整个字符数组
            // writer.write(c);

            // 方法2:写入字符数组的一部分(从索引2开始,写2个字符:'p','b')
            // writer.write(c, 2, 2);

            // 方法3:写入单个字符(97是'a'的ASCII码)
            // writer.write(97);

            // 方法4:写入字符串
            // writer.write("helloworld");

            // 方法5:写入字符串的一部分(从索引2开始,写2个字符:'l','l')
            writer.write("helloworld", 2, 2);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三、字符流与字节流的转换桥梁

字符流处理文本更方便,但底层文件存储、网络传输本质上都是字节流。Java提供OutputStreamWriter(字符流转字节流)和InputStreamReader(字节流转字符流)作为转换桥梁,还可指定字符编码。

核心案例:字符流转字节流(指定GBK编码)

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class ConverterDemo {
    public static void main(String[] args) {
        // 声明转换流对象
        OutputStreamWriter ow = null;

        try {
            // 1. 创建字节输出流FileOutputStream,指向b.txt
            // 2. 包装成转换流OutputStreamWriter,并指定编码为GBK
            ow = new OutputStreamWriter(new FileOutputStream("b.txt"), "GBK");
            // 写入中文字符(按GBK编码存储)
            ow.write("中");
            // 刷新缓冲区,确保数据写入文件
            ow.flush();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭转换流(会自动关闭底层字节流)
            try {
                if (ow != null) {
                    ow.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

四、Properties类

Properties是Java中专门处理配置文件的类,继承自 Hashtable,线程安全,仅存储字符串类型的键值对,支持从流中加载配置、将配置保存到流中。

核心案例:从配置文件加载属性

假设存在 names.properties配置文件,通过 Properties读取其内容:


import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class PropTest6 {
    public static void main(String[] args) {
        // 创建Properties对象(存储键值对配置)
        Properties prop = new Properties();
        // 声明输入流,用于读取配置文件
        InputStream in = null;

        try {
            // 通过类加载器获取配置文件的字节输入流
            // 配置文件需放在resources目录下
            in = PropTest6.class.getClassLoader().getResourceAsStream("names.properties");
            // 从字节流中加载配置到Properties对象
            prop.load(in);
            // 打印所有配置键值对
            System.out.println(prop);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭输入流
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

五、序列化流

序列化流用于将Java对象转换为二进制数据存储或传输,反之称为反序列化。核心类是 ObjectOutputStream(序列化)和 ObjectInputStream(反序列化)。

1. 核心规则
要序列化的对象必须实现 Serializable接口(标记接口,无抽象方法)。建议显式声明 serialVersionUID,避免类结构修改后无法反序列化。多个对象序列化时,需存入集合(如 List)后整体序列化。
2. 核心案例
创建可序列化的实体类

import java.io.Serializable;
import java.util.Date;

// 实现Serializable接口,标记该类可序列化
public class Person implements Serializable {
    // 显式声明序列化版本号,确保类修改后仍可反序列化
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private Date birthDate;

    // 构造方法
    public Person(String name, int age, Date birthDate) {
        this.name = name;
        this.age = age;
        this.birthDate = birthDate;
    }

    // getter/setter省略
}
对象序列化

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;

public class SerialTest {
    public static void main(String[] args) {
        // 创建要序列化的对象
        Person p = new Person("liangge", 19, new Date());
        // 声明对象输出流
        ObjectOutputStream out = null;

        try {
            // 创建对象输出流,包装字节输出流,指向person-data.txt
            out = new ObjectOutputStream(new FileOutputStream("person-data.txt"));
            // 序列化对象,写入文件
            out.writeObject(p);
            // 刷新缓冲区
            out.flush();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭流
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

六、字符编码

计算机存储字符时,会将字符映射为对应数值的二进制形式;读取时,再通过编码表将数值映射回字符。核心编码格式及规则如下:

1. 常见编码表
编码格式特点示例(字符“中”)
ASCII仅支持英文,1字节存储无对应值
GBK支持中文,1个中文占2字节字节数组:[-42, -48]
UTF - 8国际通用,1个中文占3字节字节数组:[-28, -72, -83]
2. 核心规则
编码与解码必须一致:用UTF - 8编码写入的文件,必须用UTF - 8解码读取,否则会出现乱码。获取字符串字节数组:通过 String getBytes()方法,可指定编码格式获取字节数组,示例如下:

public class EncodingTest {
    public static void main(String[] args) throws Exception {
        String str = "中";
        // 默认编码获取字节数组
        byte[] defaultBytes = str.getBytes();
        // GBK编码获取字节数组
        byte[] gbkBytes = str.getBytes("GBK");
        // UTF - 8编码获取字节数组
        byte[] utf8Bytes = str.getBytes("UTF - 8");

        System.out.println("默认编码字节:" + java.util.Arrays.toString(defaultBytes));
        System.out.println("GBK编码字节:" + java.util.Arrays.toString(gbkBytes));
        System.out.println("UTF - 8编码字节:" + java.util.Arrays.toString(utf8Bytes));
    }
}
  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部