Spring Boot 文件下载

发布于:2025-06-26 ⋅ 阅读:(15) ⋅ 点赞:(0)

下面我将为你提供一个完整的Spring Boot后端实现,用于支持Vue3前端的文件下载功能。这个实现包括文件下载API、中文文件名支持、安全检查和错误处理。

Spring Boot文件下载实现

java

复制

下载

import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;

@RestController
@RequestMapping("/api/files")
public class FileDownloadController {

    // 文件存储的基础目录(根据实际情况修改)
    private static final String FILE_DIRECTORY = "/path/to/your/files";

    /**
     * 基础文件下载API
     * @param filename 文件名
     * @return 文件资源响应
     */
    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile(@RequestParam String filename) {
        try {
            // 安全校验:防止路径遍历攻击
            if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
                throw new SecurityException("非法文件名");
            }

            Path filePath = Paths.get(FILE_DIRECTORY).resolve(filename).normalize();
            Resource resource = new UrlResource(filePath.toUri());

            if (!resource.exists()) {
                return ResponseEntity.notFound().build();
            }

            // 处理中文文件名
            String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), "ISO-8859-1");

            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "attachment; filename=\"" + encodedFilename + "\"")
                    .body(resource);
            
        } catch (MalformedURLException e) {
            return ResponseEntity.badRequest().body(null);
        } catch (SecurityException e) {
            return ResponseEntity.status(403).body(null);
        }
    }

    /**
     * 从数据库下载文件的API
     * @param fileId 文件ID
     * @return 文件流响应
     */
    @GetMapping("/download-from-db")
    public ResponseEntity<StreamingResponseBody> downloadFromDatabase(@RequestParam Long fileId) {
        // 实际项目中从数据库获取文件信息
        FileInfo fileInfo = fileService.getFileInfo(fileId);
        
        if (fileInfo == null) {
            return ResponseEntity.notFound().build();
        }

        // 处理中文文件名
        String encodedFilename = new String(fileInfo.getFilename().getBytes(StandardCharsets.UTF_8), "ISO-8859-1");

        StreamingResponseBody responseBody = outputStream -> {
            try (InputStream inputStream = fileService.getFileContent(fileId)) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }
        };

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment; filename=\"" + encodedFilename + "\"")
                .contentType(MediaType.parseMediaType(fileInfo.getContentType()))
                .body(responseBody);
    }

    /**
     * 大文件下载(流式传输)
     * @param filename 文件名
     * @return 流式响应体
     */
    @GetMapping("/download-large")
    public ResponseEntity<StreamingResponseBody> downloadLargeFile(@RequestParam String filename) {
        Path filePath = Paths.get(FILE_DIRECTORY).resolve(filename).normalize();
        java.io.File file = filePath.toFile();
        
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }

        // 处理中文文件名
        String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), "ISO-8859-1");

        StreamingResponseBody responseBody = outputStream -> {
            try (InputStream inputStream = new java.io.FileInputStream(file)) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }
        };

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFilename + "\"")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(responseBody);
    }
}

// 文件信息实体类(示例)
@Data
class FileInfo {
    private Long id;
    private String filename;
    private String contentType;
    private long size;
}

// 文件服务接口(示例)
@Service
class FileService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public FileInfo getFileInfo(Long fileId) {
        // 实际数据库查询逻辑
        return jdbcTemplate.queryForObject(
            "SELECT id, filename, content_type, size FROM files WHERE id = ?",
            (rs, rowNum) -> {
                FileInfo info = new FileInfo();
                info.setId(rs.getLong("id"));
                info.setFilename(rs.getString("filename"));
                info.setContentType(rs.getString("content_type"));
                info.setSize(rs.getLong("size"));
                return info;
            },
            fileId
        );
    }
    
    public InputStream getFileContent(Long fileId) {
        // 实际获取文件流的逻辑
        return jdbcTemplate.queryForObject(
            "SELECT content FROM files WHERE id = ?",
            (rs, rowNum) -> rs.getBinaryStream("content"),
            fileId
        );
    }
}

关键配置说明

  1. 文件存储路径

    java

    复制

    下载

    private static final String FILE_DIRECTORY = "/path/to/your/files";

    替换为你的实际文件存储路径

  2. 中文文件名处理

    java

    复制

    下载

    String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), "ISO-8859-1");

    确保浏览器正确显示中文文件名

  3. 安全措施

    java

    复制

    下载

    if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
        throw new SecurityException("非法文件名");
    }

    防止路径遍历攻击

  4. 大文件处理
    使用StreamingResponseBody实现流式传输,避免内存溢出:

    java

    复制

    下载

    StreamingResponseBody responseBody = outputStream -> {
        // 流式读写逻辑
    };

前端对应调整

根据后端API调整前端请求:

typescript

复制

下载

// 下载本地文件
const handleDownload = async () => {
  try {
    const response = await axios.get('/api/files/download', {
      params: { filename: '报告.pdf' },
      responseType: 'blob'
    });
    // ... 处理下载逻辑
  } catch (error) {
    // 错误处理
  }
};

// 下载数据库文件
const downloadFromDB = async (fileId: number) => {
  try {
    const response = await axios.get('/api/files/download-from-db', {
      params: { fileId },
      responseType: 'blob'
    });
    // ... 处理下载逻辑
  } catch (error) {
    // 错误处理
  }
};

错误处理增强

在Spring Boot中添加全局异常处理:

java

复制

下载

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(SecurityException.class)
    public ResponseEntity<String> handleSecurityException(SecurityException ex) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body("文件访问被拒绝: " + ex.getMessage());
    }

    @ExceptionHandler(FileNotFoundException.class)
    public ResponseEntity<String> handleFileNotFound(FileNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body("文件不存在: " + ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("服务器错误: " + ex.getMessage());
    }
}

响应头说明

后端设置的关键响应头:

  1. Content-Typeapplication/octet-stream(通用二进制流)

  2. Content-Dispositionattachment; filename="filename.ext"(触发浏览器下载)

  3. Content-Length:自动设置(大文件下载建议设置)

数据库设计示例

文件存储表结构:

sql

复制

下载

CREATE TABLE files (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    filename VARCHAR(255) NOT NULL,
    content_type VARCHAR(100) NOT NULL,
    size BIGINT NOT NULL,
    content LONGBLOB NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

最佳实践建议

  1. 文件存储策略

    • 小文件(<1MB)可直接存数据库

    • 大文件建议存储在文件系统或对象存储(如S3)

    • 敏感文件需要加密存储

  2. 访问控制

    java

    复制

    下载

    @PreAuthorize("hasPermission(#fileId, 'FILE', 'DOWNLOAD')")
    @GetMapping("/download-secured")
    public ResponseEntity<Resource> downloadSecuredFile(@RequestParam Long fileId) {
        // 需要权限校验的文件下载
    }
  3. 下载限流

    java

    复制

    下载

    @RateLimiter(name = "fileDownload", fallbackMethod = "downloadLimitExceeded")
    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile() {
        // ...
    }
  4. 日志记录

    java

    复制

    下载

    @PostFilter("hasPermission(filterObject, 'READ')")
    public void logDownload(String filename, String user) {
        log.info("用户 {} 下载了文件 {}", user, filename);
    }

这个实现提供了完整的文件下载功能,包括:

  • 本地文件系统下载

  • 数据库文件下载

  • 大文件流式传输

  • 中文文件名支持

  • 安全防护

  • 错误处理

  • 权限控制

前端可以无缝对接这些API实现文件下载功能。


网站公告

今日签到

点亮在社区的每一天
去签到