核心概念
- 后端: 运行在 Linux 服务器上的 Java 应用(使用 Spring Boot, Servlet),它负责接收 HTTP 请求中的文件数据,并将其保存到服务器的磁盘上。
- 前端/客户端: 可以是网页、移动 App 或其他服务,它负责将本地文件打包成一个 HTTP 请求,并发送给后端。
- HTTP 请求: 文件上传通常使用
POST方法,并设置Content-Type为multipart/form-data,这是一种特殊的编码格式,允许在一个请求中同时发送文本数据和文件数据。
使用 Spring Boot (推荐,现代且高效)
Spring Boot 是目前 Java 后端开发的主流框架,它极大地简化了文件上传的实现。
项目准备
创建一个 Spring Boot 项目,你可以使用 Spring Initializr 来快速生成项目。
- Project: Maven Project
- Language: Java
- Spring Boot: 选择一个稳定版本 (如 3.x.x)
- Project Metadata: 填写你的 Group, Artifact, Name 等。
- Dependencies: 添加
Spring Web。
点击 "Generate" 下载项目,并用你喜欢的 IDE (如 IntelliJ IDEA 或 VS Code) 打开。
编写后端代码
在 src/main/java/your/package/controller 目录下,创建一个控制器类 FileUploadController.java。
package com.example.demo.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
// 定义一个固定的上传目录,为了方便,我们放在项目根目录下的 "uploads" 文件夹
// 在 Linux 系统中,确保这个目录有写入权限
private final String UPLOAD_DIR = "/path/to/your/project/uploads/"; // !!! 重要:请修改为你的实际路径
// 确保上传目录存在
public FileUploadController() {
try {
Files.createDirectories(Paths.get(UPLOAD_DIR));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理单个文件上传
* @param file 前端传来的文件,由 MultipartFile 自动封装
* @return 返回上传结果信息
*/
@PostMapping("/single")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择一个文件上传");
}
try {
// 为了防止文件名冲突,使用 UUID 生成新的文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = UUID.randomUUID().toString() + fileExtension;
// 构建文件在服务器上的完整保存路径
Path destinationPath = Paths.get(UPLOAD_DIR + newFilename);
// 将文件内容写入到指定路径
Files.copy(file.getInputStream(), destinationPath);
System.out.println("文件上传成功: " + destinationPath.toString());
return ResponseEntity.ok("文件上传成功: " + newFilename);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.internalServerError().body("文件上传失败: " + e.getMessage());
}
}
/**
* 处理多个文件上传
* @param files 前端传来的文件数组
* @return 返回上传结果信息
*/
@PostMapping("/multiple")
public ResponseEntity<String> uploadFiles(@RequestParam("files") MultipartFile[] files) {
if (files == null || files.length == 0) {
return ResponseEntity.badRequest().body("请选择至少一个文件上传");
}
int successCount = 0;
int failCount = 0;
for (MultipartFile file : files) {
if (file.isEmpty()) {
failCount++;
continue;
}
try {
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = UUID.randomUUID().toString() + fileExtension;
Path destinationPath = Paths.get(UPLOAD_DIR + newFilename);
Files.copy(file.getInputStream(), destinationPath);
successCount++;
System.out.println("文件上传成功: " + destinationPath.toString());
} catch (IOException e) {
failCount++;
System.err.println("文件上传失败: " + file.getOriginalFilename() + " - " + e.getMessage());
}
}
return ResponseEntity.ok("上传完成,成功: " + successCount + ", 失败: " + failCount);
}
}
配置 (重要)
a) 设置上传文件大小限制
默认情况下,Spring Boot 对上传文件的大小有限制(通常是 1MB),你需要在 application.properties 或 application.yml 文件中进行配置。
src/main/resources/application.properties
# 单个文件最大大小 (10MB) spring.servlet.multipart.max-file-size=10MB # 总请求最大大小 (包含多个文件和表单数据) (11MB) spring.servlet.multipart.max-request-size=11MB
b) 确保上传目录权限
这是在 Linux 环境下最关键的一步!你的 Java 应用运行在某个用户下(tomcat, root, 或普通用户),该用户必须对上传目录有写权限。
假设你的上传目录是 /var/www/uploads,并且你的 Java 应用由 tomcat 用户运行。
在 Linux 终端中执行:
# 1. 创建目录 (如果不存在) sudo mkdir -p /var/www/uploads # 2. 将目录的所有权赋予运行 Java 应用的用户 (tomcat) sudo chown -R tomcat:tomcat /var/www/uploads # 3. 设置适当的权限 (755 表示所有者有读写执行权限,组和其他用户有读和执行权限) sudo chmod -R 755 /var/www/uploads
注意: 如果你使用的是 Spring Boot 内嵌的 Tomcat 服务器,它通常以当前启动用户(可能是你的登录用户)的身份运行,你需要根据实际情况调整 chown 命令中的用户。
运行和测试
- 运行你的 Spring Boot 应用。
- 你可以使用
curl命令来测试上传功能。
测试单个文件上传:
# 假设你的应用运行在 8080 端口 curl -X POST -F "file=@/path/to/your/local/test.txt" http://localhost:8080/api/upload/single
测试多个文件上传:
curl -X POST -F "files=@/path/to/your/local/file1.txt" -F "files=@/path/to/your/local/file2.jpg" http://localhost:8080/api/upload/multiple
使用传统 Servlet API
如果你不使用 Spring Boot,或者在一个传统的 Java Web 项目(如部署在 Tomcat, Jetty 中)中,可以使用 Servlet 3.0+ 原生的 API。
后端代码 (UploadServlet.java)
package com.example.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@WebServlet("/api/upload/servlet")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1 MB
maxFileSize = 10 * 1024 * 1024, // 10 MB
maxRequestSize = 11 * 1024 * 1024 // 11 MB
)
public class UploadServlet extends HttpServlet {
private final String UPLOAD_DIR = "/path/to/your/webapp/uploads/"; // !!! 重要:请修改为你的实际路径
@Override
public void init() throws ServletException {
super.init();
try {
Files.createDirectories(Paths.get(UPLOAD_DIR));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取 Part 对象,对应于前端表单中的 <input type="file" name="file">
Part filePart = req.getPart("file");
if (filePart == null || filePart.getSize() == 0) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "No file uploaded");
return;
}
String originalFilename = filePart.getSubmittedFileName();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = UUID.randomUUID().toString() + fileExtension;
Path destinationPath = Paths.get(UPLOAD_DIR + newFilename);
// 使用 Part 的 write 方法直接写入文件
// 注意:write 方法的路径是相对于服务器根目录的,或者使用绝对路径
// 为了清晰,我们使用绝对路径
filePart.write(destinationPath.toString());
System.out.println("文件上传成功: " + destinationPath.toString());
resp.getWriter().write("File uploaded successfully: " + newFilename);
}
}
配置
a) web.xml 配置 (可选,如果使用注解则不需要)
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>com.example.servlet.UploadServlet</servlet-class>
<multipart-config>
<file-size-threshold>1048576</file-size-threshold> <!-- 1MB -->
<max-file-size>10485760</max-file-size> <!-- 10MB -->
<max-request-size>11534336</max-request-size> <!-- 11MB -->
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/api/upload/servlet</url-pattern>
</servlet-mapping>
b) 文件大小限制
- 在 Servlet 3.0 中,可以通过
@MultipartConfig注解或web.xml中的<multipart-config>来设置。 - 如果使用 Tomcat,还可以在
conf/context.xml或conf/server.xml中为整个主机或上下文设置:<Context> <Manager pathname="" /> <Resources allowLinking="true" /> <!-- 设置上传限制 --> <multipart-config> <max-file-size>10485760</max-file-size> <max-request-size>11534336</max-request-size> <file-size-threshold>1048576</file-size-threshold> </multipart-config> </Context>
c) 目录权限 与 Spring Boot 方案相同,确保运行 Tomcat 的用户对上传目录有写权限。
前端调用示例 (HTML + JavaScript)
这是一个简单的 HTML 页面,用于选择文件并发送到后端。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">文件上传测试</title>
</head>
<body>
<h1>单个文件上传</h1>
<form action="http://localhost:8080/api/upload/single" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">上传</button>
</form>
<hr>
<h1>多个文件上传</h1>
<form action="http://localhost:8080/api/upload/multiple" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple>
<button type="submit">上传</button>
</form>
<hr>
<h1>使用 JavaScript (AJAX) 上传</h1>
<input type="file" id="jsFileInput">
<button id="jsUploadBtn">上传</button>
<p id="jsStatus"></p>
<script>
document.getElementById('jsUploadBtn').addEventListener('click', function() {
const fileInput = document.getElementById('jsFileInput');
const status = document.getElementById('jsStatus');
const file = fileInput.files[0];
if (!file) {
status.textContent = '请先选择一个文件';
return;
}
const formData = new FormData();
formData.append('file', file); // 'file' 必须与后端 @RequestParam("file") 的名字一致
status.textContent = '上传中...';
fetch('http://localhost:8080/api/upload/single', {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(data => {
status.textContent = data;
})
.catch(error => {
status.textContent = '上传失败: ' + error;
console.error('Error:', error);
});
});
</script>
</body>
</html>
Linux 环境下的重要注意事项
- 磁盘空间: 确保 Linux 服务器的
/var或你指定的上传分区有足够的磁盘空间。 - 文件名和路径安全:
- 永远不要信任客户端传来的文件名,恶意用户可能提交
../../../etc/passwd这样的路径,尝试覆盖系统文件。 - 始终使用安全的文件名,如上面代码中使用的
UUID,这样可以防止文件名冲突和恶意路径注入。 - 不要将文件保存在 Web 根目录下,否则文件可能被直接通过 URL 访问,最好保存在 Web 根目录之外的、非公开的目录中。
- 永远不要信任客户端传来的文件名,恶意用户可能提交
- 权限管理: 如前所述,仔细设置文件和目录的权限 (
chmod,chown),遵循最小权限原则,只给运行应用的用户必要的写入权限。 - 病毒扫描: 对于生产环境,上传的文件应经过病毒扫描,特别是当这些文件会被其他用户下载时。
- 日志记录: 详细记录文件上传的日志,包括上传时间、用户、文件名、文件大小、IP地址等,便于审计和排查问题。
- 性能考虑: 上传大文件会消耗大量内存和 I/O,对于非常大的文件,考虑实现分块上传和断点续传功能,这通常需要前端和后端协同配合。
