Spring Boot Excel模板下載異常:Maven資源過濾導致的文件損壞問題排查

問題背景

在Spring Boot項目中,我們經常需要提供文件下載功能,特別是Excel模板下載。最近在開發一個M系統時,遇到了一個奇怪的問題:Excel模板文件可以正常下載,但下載後的文件無法打開,提示文件損壞。 image.png

問題現象

  • 文件下載接口正常返回,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>

問題機制:

  1. 資源過濾的作用:Maven在構建過程中會對資源文件進行變量替換,將${property.name}替換為實際值
  2. Excel文件本質.xlsx文件實際上是ZIP格式的壓縮包,包含XML文件和其他資源
  3. 過濾破壞結構:Maven把Excel文件當作文本文件處理,嘗試替換其中的"佔位符",破壞了ZIP壓縮包的結構
  4. 文件內容變化:過濾過程中可能改變文件編碼,插入或替換內容,導致文件損壞

解決方案

方案一:完全禁用資源過濾(推薦)

<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);
    }
}

經驗總結

  1. 二進制文件不應過濾:Office文檔、圖片、壓縮包等二進制文件不應啓用Maven資源過濾
  2. 明確過濾範圍:如果確實需要資源過濾,應該明確指定需要過濾的文件類型
  3. 構建後驗證:重要的資源文件在構建後應該驗證其完整性和正確性
  4. 測試覆蓋:文件下載功能應該有集成測試,驗證文件的完整性和可讀性

排查工具

在排查過程中,可以使用以下工具方法輔助診斷:

// 文件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資源過濾對二進制文件的影響,為今後類似問題的解決提供了寶貴經驗。