杰瑞科技汇

Struts文件下载如何实现?

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

Struts文件下载如何实现?-图1
(图片来源网络,侵删)

核心原理

在 Web 应用中实现文件下载,本质上就是告诉浏览器(客户端):

  1. 不要直接解析我返回的内容(比如不要把 PDF 文件当作 HTML 显示)。
  2. 我返回的是一个文件,请提示用户保存
  3. 这个文件的原始文件名是什么

这主要通过 HTTP 响应头(Response Headers)来实现,特别是以下几个头信息:

  • Content-Type: 指定文件的 MIME 类型(application/pdf, image/jpeg),浏览器根据这个类型决定如何处理。
  • Content-Disposition: 这是关键,它告诉浏览器如何处理响应体,当其值为 attachment 时,表示浏览器应该弹出“另存为”对话框,我们还可以通过 filename 参数指定默认的文件名。
  • Content-Length: 指定文件的大小,有助于浏览器显示下载进度条。

Struts 2 通过 InputStream 类型作为 Action 的返回值,可以非常方便地将文件流返回给客户端,并自动处理上述的响应头设置。


详细步骤 (Struts 2)

步骤 1:准备工作

  1. 项目环境: 确保你已经有一个 Struts 2 项目,如果你使用 Maven,确保 pom.xml 中包含了 Struts 2 的核心依赖。
    <dependency>
        <groupId>org.apache.struts</groupId>
        <artifactId>struts2-core</artifactId>
        <version>2.5.33</version> <!-- 使用较新稳定的版本 -->
    </dependency>
  2. 准备文件: 在你的 Web 项目中,放置一个用于下载的测试文件,为了方便管理,通常放在 WebContent/src/main/webapp/ 目录下的某个文件夹中,WebContent/downloads/myfile.pdf

步骤 2:创建 Action (业务逻辑层)

Action 是处理请求和响应的核心,我们需要一个 Action 来:

Struts文件下载如何实现?-图2
(图片来源网络,侵删)
  • 指定要下载的文件路径。
  • 提供一个 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"> 参数详解:

Struts文件下载如何实现?-图3
(图片来源网络,侵删)
  • 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.javagetDownloadFileName() 方法中,对文件名进行双重编码:

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
    // ...
}

正确的做法:

  1. 白名单验证: 如果文件名是动态的,只允许特定的、安全的字符和扩展名。
  2. 映射到安全目录: 将用户请求的文件名映射到一个预先定义好的、安全的目录中,而不是直接拼接路径。
  3. 使用数据库: 最佳实践是文件名和服务器上的存储路径分开,在数据库中存储文件信息(如 file_id, original_name, stored_path),Action 接收 file_id,然后从数据库中查找 stored_path,完全避免用户输入影响文件路径。

大文件下载和性能

  • 内存消耗: 使用 InputStream 是流式处理,文件内容不会一次性全部加载到内存中,这对于大文件非常友好。
  • 缓冲区: 可以调整 struts.xml 中的 bufferSize 参数来优化性能,较大的缓冲区可以提高 I/O 效率,但会占用更多内存。40968192 是常见的值。
  • 连接超时: 对于非常大的文件或网络较慢的情况,需要注意 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 文件下载的要点如下:

  1. Action: 返回类型为 InputStream,并提供 getInputStream() 方法,提供一个 getDownloadFileName() 方法来处理文件名。
  2. struts.xml: Action 的 resulttype 必须是 "stream",并配置 inputName, contentType, contentDisposition 等参数。
  3. 安全: 警惕文件路径安全问题,不要信任用户输入的文件名。
  4. 编码: 妥善处理中文文件名,确保在不同浏览器上都能正确显示。

遵循以上步骤和注意事项,你就可以在 Struts 2 应用中稳定、安全地实现文件下载功能。

分享:
扫描分享到社交APP
上一篇
下一篇