Java Web中Servlet文件操作:上传与下载功能详解
在Java Web开发中,基于Servlet实现文件上传与下载功能,相比于使用如Struts2这类更高级的框架,可能需要处理更多底层细节。然而,理解并掌握这些基础操作对于深入Web应用开发至关重要。
文件上传
实现文件上传功能时,通常会借助于Apache Commons FileUpload和Apache Commons IO这两个库。它们极大地简化了Servlet API中处理multipart/form-data类型请求的复杂性。
所需依赖
commons-fileupload-x.x.jar(例如:commons-fileupload-1.4.jar)commons-io-x.x.jar(例如:commons-io-2.11.0.jar)
前端页面示例
上传文件的前端表单需要使用POST方法,并且enctype属性必须设置为multipart/form-data,这是处理文件上传请求的必要条件。
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="fileInput">选择文件:</label>
<input type="file" id="fileInput" name="uploadFile"/><br/>
<input type="submit" value="上传"/>
</form>
Servlet文件上传核心步骤
- 创建磁盘文件项工厂(
DiskFileItemFactory):该工厂用于设置上传文件时的临时存储目录和内存阈值。当文件大小超过内存阈值时,文件会先写入临时目录。 - 创建文件上传处理器(
ServletFileUpload):基于工厂创建文件处理器,负责解析HTTP请求中的文件数据。 - 解析请求获取文件项(
FileItem):使用处理器解析HttpServletRequest对象,获取一个包含所有表单字段(包括普通文本字段和文件字段)的FileItem列表。 - 遍历处理文件项:迭代
FileItem列表。对于文件字段,从中提取文件名、保存文件内容;对于普通表单字段,则获取其对应的文本值。
服务器端Servlet代码示例
为了提高文件管理的效率和安全性,在实际应用中,通常会将上传文件保存到Web应用目录下的/WEB-INF中(该目录下的资源无法通过浏览器直接访问),并采取如"目录打散"、"生成唯一文件名"等策略。
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. 检查请求是否为multipart类型
if (!ServletFileUpload.isMultipartContent(request)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "请求必须是multipart/form-data类型");
return;
}
// 2. 配置工厂和文件上传处理器
// 设置临时文件存储目录(系统默认临时目录)和内存阈值(10MB)
File tempRepository = new File(System.getProperty("java.io.tmpdir"));
DiskFileItemFactory factory = new DiskFileItemFactory(10 * 1024 * 1024, tempRepository);
ServletFileUpload uploadHandler = new ServletFileUpload(factory);
uploadHandler.setHeaderEncoding("UTF-8"); // 解决上传文件名中文乱码问题
try {
// 3. 解析请求,获取所有文件项
List<FileItem> formItems = uploadHandler.parseRequest(request);
if (formItems == null || formItems.isEmpty()) {
response.getWriter().println("没有发现上传文件。");
return;
}
// 获取Web应用的实际保存路径:/WEB-INF/uploads
String uploadBaseDirPath = getServletContext().getRealPath("/WEB-INF/uploads");
File baseDirectory = new File(uploadBaseDirPath);
if (!baseDirectory.exists()) {
baseDirectory.mkdirs(); // 确保上传根目录存在
}
// 4. 遍历处理每个文件项
for (FileItem item : formItems) {
if (!item.isFormField()) { // 如果是文件字段
String originalClientFileName = item.getName(); // 客户端上传的原始文件名
// 兼容不同操作系统和浏览器,处理文件名中可能包含的路径信息
if (originalClientFileName != null) {
int pathSeparatorIndex = Math.max(originalClientFileName.lastIndexOf('/'), originalClientFileName.lastIndexOf('\\'));
if (pathSeparatorIndex != -1) {
originalClientFileName = originalClientFileName.substring(pathSeparatorIndex + 1);
}
}
if (originalClientFileName == null || originalClientFileName.trim().isEmpty()) {
continue; // 空文件名,跳过
}
// 生成唯一文件名,避免文件名冲突,例如:UUID_原始文件名.扩展名
String uniqueStoredFileName = UUID.randomUUID().toString() + "_" + originalClientFileName;
// 使用原始文件名的哈希值进行目录打散,提高查找效率和目录性能
// 例如:/WEB-INF/uploads/h/a/uniqueStoredFileName.ext
int fileNameHash = originalClientFileName.hashCode();
String hashHex = Integer.toHexString(fileNameHash);
File subDirectory = new File(baseDirectory, hashHex.charAt(0) + File.separator + hashHex.charAt(1));
if (!subDirectory.exists()) {
subDirectory.mkdirs(); // 创建打散后的子目录
}
// 5. 将文件写入目标位置
File targetFile = new File(subDirectory, uniqueStoredFileName);
item.write(targetFile);
response.getWriter().println("文件 "" + originalClientFileName + "" 上传成功,保存为: " + targetFile.getName());
} else {
// 处理普通表单字段
String fieldName = item.getFieldName();
String fieldValue = item.getString("UTF-8"); // 获取字段值,注意编码
System.out.println("普通表单字段: " + fieldName + "=" + fieldValue);
}
}
} catch (FileUploadException e) {
throw new ServletException("文件上传处理失败", e);
} catch (Exception e) { // 捕获 item.write() 抛出的其他异常
throw new ServletException("文件保存失败", e);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
}
文件下载
文件下载功能相对简单,主要涉及到设置HTTP响应头和通过输入输出流传输文件内容。
所需依赖
commons-io-x.x.jar(例如:commons-io-2.11.0.jar)
Servlet文件下载核心要素
文件下载通常被称为"一个流,两个头":
- 响应头1:
Content-Type:告知浏览器文件的MIME类型,例如image/jpeg、application/pdf或application/octet-stream(通用二进制文件)。 - 响应头2:
Content-Disposition:指示浏览器如何处理文件。设置为attachment;filename="文件名"表示作为附件下载,并指定下载时显示的文件名。 - 流:文件数据流:通过
FileInputStream读取服务器上的文件,并通过ServletOutputStream将文件内容写入HTTP响应体。
服务器端Servlet代码示例
在处理下载文件名时,需要特别注意不同浏览器对中文或特殊字符的编码处理方式,以避免乱码。
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 从请求参数中获取要下载的唯一文件名(例如:UUID_原始文件名.ext)
String uniqueFileName = request.getParameter("filename");
if (uniqueFileName == null || uniqueFileName.trim().isEmpty()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少文件名参数。");
return;
}
// 从唯一文件名中提取原始文件名,以便计算哈希值和作为下载时的显示名称
String originalFileNameForDisplay = uniqueFileName;
int underscoreIndex = uniqueFileName.indexOf('_');
if (underscoreIndex != -1 && underscoreIndex < uniqueFileName.length() - 1) {
originalFileNameForDisplay = uniqueFileName.substring(underscoreIndex + 1);
}
// 重新构建文件在服务器上的实际存储路径
String uploadBaseDirPath = getServletContext().getRealPath("/WEB-INF/uploads");
File baseDirectory = new File(uploadBaseDirPath);
// 使用原始文件名的哈希值计算打散目录
int fileNameHash = originalFileNameForDisplay.hashCode();
String hashHex = Integer.toHexString(fileNameHash);
File actualFile = null;
if (hashHex.length() >= 2) {
File subDirectory = new File(baseDirectory, hashHex.charAt(0) + File.separator + hashHex.charAt(1));
actualFile = new File(subDirectory, uniqueFileName); // 最终文件路径
} else {
// 极少数情况哈希值字符串长度不足,直接放在根目录或采取其他策略
actualFile = new File(baseDirectory, uniqueFileName);
}
if (!actualFile.exists() || !actualFile.isFile()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到或无法访问。");
return;
}
// 1. 获取文件的MIME类型
String mimeType = getServletContext().getMimeType(actualFile.getName());
if (mimeType == null) {
mimeType = "application/octet-stream"; // 默认MIME类型,表示是二进制流
}
// 2. 设置响应头:Content-Type
response.setContentType(mimeType);
// 3. 设置响应头:Content-Disposition,指示浏览器以附件形式下载
String encodedDisplayFileName = getEncodedFilenameForDownload(originalFileNameForDisplay, request);
String contentDispositionHeader = "attachment; filename=\"" + encodedDisplayFileName + "\"";
response.setHeader("Content-Disposition", contentDispositionHeader);
// 4. 通过流将文件内容写入响应
try (InputStream inputStream = new FileInputStream(actualFile);
OutputStream outputStream = response.getOutputStream()) {
IOUtils.copy(inputStream, outputStream); // 使用Apache Commons IO简化流复制
} catch (IOException e) {
throw new ServletException("文件下载失败。", e);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
/**
* 根据不同的浏览器Agent对文件名进行编码,以解决下载时的中文乱码问题。
* @param filename 要进行编码的原始文件名
* @param request HTTP请求对象
* @return 编码后的文件名字符串,适用于Content-Disposition头
* @throws IOException 编码失败时抛出
*/
private String getEncodedFilenameForDownload(String filename, HttpServletRequest request) throws IOException {
String userAgent = request.getHeader("User-Agent");
String encodedFilename;
if (userAgent.contains("Firefox")) {
// Firefox浏览器使用Base64编码
encodedFilename = "=?UTF-8?B?" + Base64.getEncoder().encodeToString(filename.getBytes(StandardCharsets.UTF_8)) + "?=";
} else if (userAgent.contains("MSIE") || userAgent.contains("Trident")) { // IE及Edge兼容模式
// IE/Edge浏览器使用URL编码,且需要将空格编码为%20
encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()).replace("+", "%20");
} else if (userAgent.contains("Chrome")) {
// Chrome浏览器也使用URL编码,同样需要将空格编码为%20
encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()).replace("+", "%20");
} else {
// 对于其他未知浏览器,默认使用URL编码
encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString());
}
return encodedFilename;
}
}