核心依賴
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.5.30</version>
</dependency>
<!-- 文件上傳必需 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
配置常量 (struts.xml / struts.properties)
<constant name="struts.multipart.maxSize" value="104857600" /> <!-- 100MB -->
<constant name="struts.multipart.saveDir" value="/tmp/struts-upload" /> <!-- 臨時目錄 -->
<constant name="struts.multipart.parser" value="jakarta" /> <!-- 解析器 -->
Action 實現:文件上傳
@Namespace("/file")
@Action(value = "upload", results = {
@Result(name = "success", type = "json"),
@Result(name = "input", location = "/upload.jsp")
})
public class FileUploadAction extends ActionSupport {
private File upload; // 上傳文件
private String uploadFileName; // 原始文件名
private String uploadContentType; // MIME 類型
private String savePath = "/var/www/uploads"; // 保存目錄
// Struts2 自動注入:屬性名 = 表單 file 域名 + FileName/ContentType
public void setUpload(File upload) { this.upload = upload; }
public void setUploadFileName(String uploadFileName) { this.uploadFileName = uploadFileName; }
public void setUploadContentType(String uploadContentType) { this.uploadContentType = uploadContentType; }
@Override
public String execute() {
if (upload == null) {
addFieldError("upload", "請選擇文件");
return INPUT;
}
// 1. 校驗文件類型/大小
if (!isAllowedType(uploadContentType)) {
addFieldError("upload", "不支持的文件類型");
return INPUT;
}
// 2. 生成唯一文件名防覆蓋
String ext = FilenameUtils.getExtension(uploadFileName);
String newName = UUID.randomUUID() + "." + ext;
File dest = new File(savePath, newName);
dest.getParentFile().mkdirs();
// 3. 移動文件(Struts2 上傳文件在臨時目錄,需手動移動/複製)
try {
FileUtils.copyFile(upload, dest);
// 或 Files.move(upload.toPath(), dest.toPath(), REPLACE_EXISTING);
} catch (IOException e) {
addActionError("保存失敗: " + e.getMessage());
return ERROR;
}
// 4. 返回結果(JSON)
Map result = Map.of(
"name", newName,
"originalName", uploadFileName,
"size", upload.length(),
"url", "/files/" + newName
);
ServletActionContext.getResponse().setContentType("application/json");
try { ServletActionContext.getResponse().getWriter().write(new ObjectMapper().writeValueAsString(result)); }
catch (IOException ignored) {}
return NONE; // 已手動輸出
}
private boolean isAllowedType(String contentType) {
return contentType != null &&
(contentType.startsWith("image/") ||
contentType.equals("application/pdf") ||
contentType.equals("application/msword"));
}
}
JSP 上傳表單
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:form action="file/upload" method="post" enctype="multipart/form-data">
<s:file name="upload" label="選擇文件" accept="image/*,.pdf,.doc"/>
<s:submit value="上傳" />
<s:fielderror />
</s:form>
Action 實現:文件下載
@Action(value = "download", results = {
@Result(name = "success", type = "stream", params = {
"contentType", "application/octet-stream",
"inputName", "inputStream",
"contentDisposition", "attachment;filename=\"${downloadFileName}\"",
"bufferSize", "4096"
})
})
public class FileDownloadAction extends ActionSupport {
private String fileName; // 請求參數
private InputStream inputStream;
private String downloadFileName; // 瀏覽器顯示的文件名
public void setFileName(String fileName) { this.fileName = fileName; }
public InputStream getInputStream() { return inputStream; }
public String getDownloadFileName() { return downloadFileName; }
@Override
public String execute() {
File file = new File("/var/www/uploads", fileName);
if (!file.exists() || !file.isFile()) {
addActionError("文件不存在");
return ERROR;
}
try {
inputStream = new FileInputStream(file);
// 解決中文文件名亂碼(RFC 5987 編碼)
downloadFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
} catch (IOException e) {
return ERROR;
}
return SUCCESS;
}
}
struts.xml 結果類型配置對比
| 類型 | 用途 | 關鍵參數 |
| dispatcher | 默認,轉發到 JSP | location |
| redirect | 重定向 | location, parse=true |
| stream | 文件下載/二進制輸出 | inputName, contentType, contentDisposition, bufferSize |
| json | JSON 響應 | root, contentType |
| freemarker/velocity | 模板引擎 | location |
進階最佳實踐
- 多文件上傳:Action 定義
List uploads、List uploadFileNames
- 大文件斷點續傳:前端分片(WebUploader/Plupload),後端合併臨時分片文件
- 安全加固:
- 文件類型雙重校驗(Content-Type + 魔數檢測)
- 存儲目錄與 Web 根目錄隔離,通過 Stream 結果輸出
- 文件名隨機化,防止路徑遍歷攻擊
- 雲存儲對接:上傳後直接流式傳輸到 OSS/S3,不落地本地磁盤
- 國際化文件名:下載時使用
Content-Disposition: attachment; filename*=UTF-8''encoded_name
常見問題排查
| 現象 | 原因 | 解決 |
| upload 為 null | form 缺少 enctype="multipart/form-data" | 檢查 <s:form enctype="multipart/form-data"> |
| 上傳大文件失敗 | 超過 struts.multipart.maxSize | 調大常量,同時檢查 Tomcat maxPostSize |
| 中文文件名下載亂碼 | Content-Disposition 編碼錯誤 | 使用 RFC 5987 格式或 URLEncoder.encode |
| 臨時文件未清理 | Struts2 不自動刪除臨時文件 | 定時任務清理 struts.multipart.saveDir |
總結
Struts2 文件上傳下載核心在於:Action 屬性命名約定匹配表單域、Stream 結果類型實現下載、安全校驗不可省略。現代項目建議優先考慮 Spring MVC / Spring Boot + MultipartFile,配置更簡潔,功能更強大。