我们将以 Struts 2 为例进行讲解,因为 Struts 1 已经非常古老,不再推荐用于新项目,Struts 2 的实现方式更为现代和简洁。

核心原理
在 Web 应用中实现文件下载,本质上就是告诉浏览器(客户端):
- 不要直接解析我返回的内容(比如不要把
PDF文件当作 HTML 显示)。 - 我返回的是一个文件,请提示用户保存。
- 这个文件的原始文件名是什么。
这主要通过 HTTP 响应头(Response Headers)来实现,特别是以下几个头信息:
Content-Type: 指定文件的 MIME 类型(application/pdf,image/jpeg),浏览器根据这个类型决定如何处理。Content-Disposition: 这是关键,它告诉浏览器如何处理响应体,当其值为attachment时,表示浏览器应该弹出“另存为”对话框,我们还可以通过filename参数指定默认的文件名。Content-Length: 指定文件的大小,有助于浏览器显示下载进度条。
Struts 2 通过 InputStream 类型作为 Action 的返回值,可以非常方便地将文件流返回给客户端,并自动处理上述的响应头设置。
详细步骤 (Struts 2)
步骤 1:准备工作
- 项目环境: 确保你已经有一个 Struts 2 项目,如果你使用 Maven,确保
pom.xml中包含了 Struts 2 的核心依赖。<dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.5.33</version> <!-- 使用较新稳定的版本 --> </dependency> - 准备文件: 在你的 Web 项目中,放置一个用于下载的测试文件,为了方便管理,通常放在
WebContent/或src/main/webapp/目录下的某个文件夹中,WebContent/downloads/myfile.pdf。
步骤 2:创建 Action (业务逻辑层)
Action 是处理请求和响应的核心,我们需要一个 Action 来:

- 指定要下载的文件路径。
- 提供一个
InputStream方法,让 Struts 2 可以读取文件内容。
创建一个名为 FileDownloadAction.java 的类:
package com.example.action;
import org.apache.struts2.ServletActionContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class FileDownloadAction {
// 1. 用于接收前端传来的文件名(可选,如果文件名是动态的)
private String fileName;
// 2. Struts 2 会自动调用这个 getter 方法来获取文件输入流
public InputStream getInputStream() throws Exception {
// 如果文件名是动态的,可以在这里拼接路径
// String filePath = "/downloads/" + this.fileName;
// 为了演示,我们直接指定一个文件路径
// 注意:路径是相对于 Web 应用根目录的
String filePath = "/downloads/myfile.pdf";
// 使用 ServletActionContext.getServletContext() 获取 Web 上下文
// 然后获取服务器上的绝对路径
String realPath = ServletActionContext.getServletContext().getRealPath(filePath);
// 创建一个 File 对象
File file = new File(realPath);
// 返回文件输入流
return new FileInputStream(file);
}
// 3. (可选但推荐) 指定下载的文件名
// Struts 2 会自动调用这个方法来设置 Content-Disposition 头中的 filename
public String getDownloadFileName() {
// Action 中有接收文件名,可以直接使用
// if (fileName != null) {
// return fileName;
// }
// 否则,返回一个固定的文件名
// 这里可以处理中文文件名编码问题,防止乱码
return "新文件名.pdf";
}
// 4. (可选) 指定文件的 Content-Type
// Struts 2 会自动根据文件扩展名设置,但也可以手动覆盖
public String getContentType() {
// return "application/pdf"; // 对于 PDF 文件
// return "application/octet-stream"; // 未知类型或二进制文件,强制下载
return null; // 让 Struts 2 自动判断
}
// 5. 提供 fileName 的 setter 方法,用于接收请求参数
public void setFileName(String fileName) {
this.fileName = fileName;
}
// Action 的 execute 方法可以省略,因为 Struts 2 会根据返回的流类型处理
// 但如果你有其他逻辑,可以在这里实现
public String execute() throws Exception {
// 可以在这里添加一些业务逻辑,比如权限检查等
return "success"; // 这个返回值在 struts.xml 中配置
}
}
代码解释:
getInputStream(): 这是核心,Struts 2 在处理 Action 返回类型为InputStream时,会自动调用此方法获取输入流,并将其写入到 HTTP 响应体中。getDownloadFileName(): Struts 2 会调用这个方法,并将其返回值设置到Content-Disposition头的filename部分。强烈建议处理中文文件名,以防止在不同浏览器中出现乱码。getContentType(): 用于设置Content-Type头,如果返回null,Struts 2 会根据文件扩展名(如.pdf,.jpg)自动推断一个合适的 MIME 类型,返回application/octet-stream会强制浏览器下载,而不是尝试打开文件。fileName和它的setter: 如果你想通过 URL 参数动态指定下载的文件名(downloadFile.action?fileName=report.docx),就需要这个属性。
步骤 3:配置 struts.xml
在 src/main/resources/struts.xml 文件中,配置 Action 的映射。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<!-- 配置文件下载 Action -->
<action name="downloadFile" class="com.example.action.FileDownloadAction" method="execute">
<!--
result name="success" 的 type 必须是 "stream"。
这告诉 Struts 2 这是一个文件流下载请求。
-->
<result name="success" type="stream">
<!-- 指定要获取的 InputStream 的方法名 -->
<param name="inputName">inputStream</param>
<!--
指定 Content-Type。
可以直接写死,也可以引用 Action 中的方法(推荐)
-->
<param name="contentType">${contentType}</param>
<!--
指定下载的文件名。
引用 Action 中的 getDownloadFileName() 方法。
这对于处理中文文件名非常关键!
-->
<param name="contentDisposition">attachment;filename="${downloadFileName}"</param>
<!--
(可选) 指定缓冲区大小,单位为字节。
默认是 1024,可以根据需要调整。
-->
<param name="bufferSize">4096</param>
</result>
</action>
</package>
</struts>
<result type="stream"> 参数详解:

inputName: 指定 Action 中哪个get*()方法提供输入流,默认是inputStream,所以我们 Action 中方法名必须是getInputStream()。contentType: 设置响应的Content-Type。${contentType}会调用 Action 的getContentType()方法。contentDisposition: 设置Content-Disposition头。attachment表示下载。${downloadFileName}会调用 Action 的getDownloadFileName()方法。这里是对中文文件名处理最关键的地方!bufferSize: 设置写入响应流时的缓冲区大小,影响性能。
步骤 4:创建 JSP 页面 (视图层)
创建一个简单的 JSP 页面来触发下载。
在 WebContent/ 目录下创建 download.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">Struts 2 文件下载示例</title>
</head>
<body>
<h1>Struts 2 文件下载</h1>
<p>
<a href="<s:url action='downloadFile' />">下载固定文件 (myfile.pdf)</a>
</p>
<hr>
<p>
<s:form action="downloadFile">
<s:textfield name="fileName" label="请输入要下载的文件名 (e.g., image.jpg)" />
<s:submit value="下载指定文件" />
</s:form>
</p>
</body>
</html>
访问 http://localhost:8080/your-project-name/download.jsp,你就可以看到下载链接了。
重要注意事项和最佳实践
中文文件名乱码问题
这是文件下载中最常见的问题,不同的浏览器对文件名的编码处理方式不同。
- Firefox: 支持
UTF-8编码。 - Chrome/IE/Edge: 支持
ISO-8859-1编码。
为了兼容所有浏览器,可以采用以下策略:
在 FileDownloadAction.java 的 getDownloadFileName() 方法中,对文件名进行双重编码:
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public String getDownloadFileName() {
String originalFileName = "测试文件.pdf";
try {
// 先用 UTF-8 编码一次
String encodedFileName = URLEncoder.encode(originalFileName, "UTF-8");
// 将编码后的 '+' 替换为 ' ',因为某些浏览器(如旧版IE)不支持 '+' 作为空格
// 将 '%' 替换为 '%25',防止被二次解析
// 将 ' ' 替换为 '%20'
// 这是一个比较通用的处理方式
return encodedFileName.replaceAll("\\+", "%20");
// 或者,针对不同浏览器返回不同编码(更复杂但更精确)
// String userAgent = ServletActionContext.getRequest().getHeader("User-Agent");
// if (userAgent.contains("MSIE") || userAgent.contains("Trident") || userAgent.contains("Edge")) {
// // IE/Edge 使用 ISO-8859-1
// return new String(originalFileName.getBytes("UTF-8"), "ISO-8859-1");
// } else {
// // Firefox, Chrome 等使用 URL 编码的 UTF-8
// return URLEncoder.encode(originalFileName, "UTF-8");
// }
} catch (UnsupportedEncodingException e) {
// 如果出错,返回一个默认的英文名
return "default.pdf";
}
}
然后在 struts.xml 中配置:
<param name="contentDisposition">attachment;filename="${downloadFileName}"</param>
文件路径安全
绝对不要直接使用用户提供的文件名来拼接路径,这会带来严重的安全漏洞(如路径遍历攻击)。
// 错误的做法!非常危险!
public InputStream getInputStream() throws Exception {
String userProvidedName = this.fileName; // 来自 HTTP 请求
String filePath = "/uploads/" + userProvidedName; // 恶意用户可能传入 ../../etc/passwd
// ...
}
正确的做法:
- 白名单验证: 如果文件名是动态的,只允许特定的、安全的字符和扩展名。
- 映射到安全目录: 将用户请求的文件名映射到一个预先定义好的、安全的目录中,而不是直接拼接路径。
- 使用数据库: 最佳实践是文件名和服务器上的存储路径分开,在数据库中存储文件信息(如
file_id,original_name,stored_path),Action 接收file_id,然后从数据库中查找stored_path,完全避免用户输入影响文件路径。
大文件下载和性能
- 内存消耗: 使用
InputStream是流式处理,文件内容不会一次性全部加载到内存中,这对于大文件非常友好。 - 缓冲区: 可以调整
struts.xml中的bufferSize参数来优化性能,较大的缓冲区可以提高 I/O 效率,但会占用更多内存。4096或8192是常见的值。 - 连接超时: 对于非常大的文件或网络较慢的情况,需要注意 Web 容器(如 Tomcat)的连接超时设置。
Struts 2 的 Content-Type 自动检测
Struts 2 内置了一个 org.apache.struts2.dispatcher.multipart.MultiPartRequest 的实现,它会根据文件的扩展名自动设置 Content-Type。
.pdf->application/pdf.jpg->image/jpeg.txt->text/plain
如果你的文件类型比较特殊,或者你想强制下载(而不是在浏览器中打开),可以在 Action 的 getContentType() 方法中返回 "application/octet-stream"。
实现 Struts 2 文件下载的要点如下:
- Action: 返回类型为
InputStream,并提供getInputStream()方法,提供一个getDownloadFileName()方法来处理文件名。 struts.xml: Action 的result的type必须是"stream",并配置inputName,contentType,contentDisposition等参数。- 安全: 警惕文件路径安全问题,不要信任用户输入的文件名。
- 编码: 妥善处理中文文件名,确保在不同浏览器上都能正确显示。
遵循以上步骤和注意事项,你就可以在 Struts 2 应用中稳定、安全地实现文件下载功能。
