文件上传的本质是:
客户端(浏览器)把本地文件的二进制数据,通过 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 已经帮我们封装好了文件上传的逻辑,我们只需要用
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";
}
}
multipart/form-data 数据 的工作,我们不用关心底层的二进制流处理。
MultipartFile 封装了文件的所有信息(文件名、大小、类型、二进制数据),提供了简单的方法(
getOriginalFilename()、
transferTo())来操作。
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("上传失败:未选择文件或文件为空!");
}
}
}
@MultipartConfig 注解开启上传。获取文件用
req.getPart("name"),而 Spring MVC 用
MultipartFile 参数。保存文件用
part.write(),而 Spring MVC 用
transferTo()。原生 Servlet 更底层,Spring MVC 是对 Servlet 的封装,更方便。
在 Servlet 3.0 之前,官方没有提供文件上传的 API,所以需要用第三方工具(Apache Commons FileUpload)来解析
multipart/form-data 数据。
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());
}
}
}
getRealPath("/uploads") 获取的是 Web 应用部署目录下的路径,项目重新部署(比如 Tomcat 重启、war 包重新上传)时,这个目录下的文件会丢失。生产环境中,建议把文件存到项目外部的固定路径(比如
D:uploads),或者用云存储(阿里云 OSS、腾讯云 COS)。文件大小限制:必须设置文件大小限制,防止用户上传超大文件导致服务器磁盘占满或内存溢出。文件名乱码:前端和后端都要处理编码(比如
UTF-8),否则中文文件名会变成乱码。安全问题:要校验文件类型(比如只允许上传图片、文档),防止用户上传恶意文件(比如
.exe 病毒)。可以通过文件名后缀或文件的 MIME 类型校验。
IO(Input/Output)即输入与输出,是设备或程序与外部环境的数据交互。比如键盘向程序输入内容是输入,程序向显示器输出图像、向文件写入内容是输出。
Java中为了简化IO操作,将数据传输抽象为流(可以理解为数据流动的管道),所有IO操作都通过操作流对象完成。以游戏程序为例,读取配置文件是输入操作,保存游戏存档是输出操作。
Java中的IO流从两个维度进行划分,覆盖所有场景需求:
按输入输出方向划分 输入流:数据从外部(文件、键盘等)流入程序,用于读取数据。输出流:数据从程序流出到外部,用于写入数据。 按传输数据类型划分 字符流:传输文本数据(如文章、Java代码文件),我们能直接读懂内容,核心操作类以
Reader(输入)和
Writer(输出)结尾。字节流:传输二进制数据(如图片、音频、视频),用文本打开是乱码,核心操作类以
InputStream(输入)和
OutputStream(输出)结尾。
字符流专门处理文本数据,核心抽象类是Reader(输入流)和Writer(输出流),子类需实现其抽象方法完成具体操作。
Closeable(关闭流)、
Flushable(刷新流)、
Appendable(追加内容)接口。子类有
FileWriter、
BufferedWriter等,必须实现
write(char[], int, int)、
flush()、
close()三个方法。Reader:字符输入流抽象类,实现了
Closeable和
Readable(读取数据)接口。子类有
FileReader、
BufferedReader等,用于读取文本文件内容。
字符流操作的核心步骤:创建文件对象→创建流对象→操作数据→关闭流(释放资源,必须在
finally中执行,防止异常导致资源泄漏)
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();
}
}
}
}
}
写入内容并换行
,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(字节流转字符流)作为转换桥梁,还可指定字符编码。
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是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(反序列化)。
Serializable接口(标记接口,无抽象方法)。建议显式声明
serialVersionUID,避免类结构修改后无法反序列化。多个对象序列化时,需存入集合(如
List)后整体序列化。
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();
}
}
}
}
}
计算机存储字符时,会将字符映射为对应数值的二进制形式;读取时,再通过编码表将数值映射回字符。核心编码格式及规则如下:
| 编码格式 | 特点 | 示例(字符“中”) |
|---|---|---|
| ASCII | 仅支持英文,1字节存储 | 无对应值 |
| GBK | 支持中文,1个中文占2字节 | 字节数组:[-42, -48] |
| UTF - 8 | 国际通用,1个中文占3字节 | 字节数组:[-28, -72, -83] |
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));
}
}