Spring Boot Excel模板下載異常:Maven資源過濾導致的文件損壞問題排查
問題背景
在Spring Boot項目中,我們經常需要提供文件下載功能,特別是Excel模板下載。最近在開發一個M系統時,遇到了一個奇怪的問題:Excel模板文件可以正常下載,但下載後的文件無法打開,提示文件損壞。
問題現象
- 文件下載接口正常返回,HTTP狀態碼200
- 下載的文件大小比源文件大(從10170字節變為17077字節)
- 下載的文件無法用Excel打開,提示文件損壞
- 直接訪問源文件可以正常打開
初始排查
1. 檢查文件下載接口
最初的下載接口代碼如下:
@GetMapping("/downloadExcel2")
public ResponseEntity<Resource> downloadTemplate2() {
try {
Resource resource = new ClassPathResource("static/order_template.xlsx");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
headers.add("Content-Disposition", "attachment; filename=order_template.xlsx");
return ResponseEntity.ok()
.headers(headers)
.contentLength(resource.contentLength())
.body(resource);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
2. 懷疑消息轉換器問題
最初懷疑是Spring的消息轉換器將二進制數據轉換為了JSON,嘗試了多種方案:
- 使用
ResponseEntity<byte[]>替代ResponseEntity<Resource> - 使用
HttpServletResponse直接輸出流 - 禁用全局消息轉換器配置
但這些方案都沒有解決問題。
3. 文件完整性檢查
通過MD5校驗發現,類路徑下的文件與源文件內容不一致:
@GetMapping("/compareFiles")
public ResponseEntity<String> compareFiles() {
// 類路徑文件MD5: 33bd1c01f4866d18333325304883218e
// 源文件MD5: e18e5185f20519717d966e6e543a9353
// 文件大小: 17077字節 vs 10170字節
}
根本原因分析
問題的根源在於Maven的資源過濾配置。在pom.xml中配置了:
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<excludes>
<exclude>application-dev.yml</exclude>
<exclude>application-prod.yml</exclude>
</excludes>
</resource>
問題機制:
- 資源過濾的作用:Maven在構建過程中會對資源文件進行變量替換,將
${property.name}替換為實際值 - Excel文件本質:
.xlsx文件實際上是ZIP格式的壓縮包,包含XML文件和其他資源 - 過濾破壞結構:Maven把Excel文件當作文本文件處理,嘗試替換其中的"佔位符",破壞了ZIP壓縮包的結構
- 文件內容變化:過濾過程中可能改變文件編碼,插入或替換內容,導致文件損壞
解決方案
方案一:完全禁用資源過濾(推薦)
<build>
<resources>
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
方案二:精確控制過濾範圍
<build>
<resources>
<!-- 只對文本配置文件進行過濾 -->
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.yml</include>
<include>**/*.yaml</include>
<include>**/*.xml</include>
</includes>
</resource>
<!-- 所有其他文件不進行過濾 -->
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
方案三:排除二進制文件類型
<build>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<excludes>
<!-- 排除所有二進制文件 -->
<exclude>**/*.xlsx</exclude>
<exclude>**/*.xls</exclude>
<exclude>**/*.doc</exclude>
<exclude>**/*.docx</exclude>
<exclude>**/*.pdf</exclude>
<exclude>**/*.jpg</exclude>
<exclude>**/*.png</exclude>
<exclude>**/*.gif</exclude>
<exclude>**/*.zip</exclude>
</excludes>
</resource>
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
修復後的下載接口
修復Maven配置後,可以使用簡潔的下載代碼:
@GetMapping("/downloadExcel2")
public void downloadTemplate2(HttpServletResponse response) {
try {
InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("static/order_template.xlsx");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=order_template.xlsx");
OutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
response.setStatus(500);
}
}
經驗總結
- 二進制文件不應過濾:Office文檔、圖片、壓縮包等二進制文件不應啓用Maven資源過濾
- 明確過濾範圍:如果確實需要資源過濾,應該明確指定需要過濾的文件類型
- 構建後驗證:重要的資源文件在構建後應該驗證其完整性和正確性
- 測試覆蓋:文件下載功能應該有集成測試,驗證文件的完整性和可讀性
排查工具
在排查過程中,可以使用以下工具方法輔助診斷:
// 文件MD5校驗
private String calculateMD5(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(data);
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
return "error";
}
}
// 文件頭檢查
private String bytesToHex(byte[] bytes, int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(String.format("%02X ", bytes[i]));
}
return sb.toString();
}
通過這次排查,認識到Maven資源過濾對二進制文件的影響,為今後類似問題的解決提供了寶貴經驗。