Spring Boot 文件上传下载完整指南:从基础到高级实践

文章目录

    • [1. 引言](#1. 引言)
    • [2. 环境准备](#2. 环境准备)
      • [2.1 项目创建](#2.1 项目创建)
      • [2.2 配置文件](#2.2 配置文件)
    • [3. 基础文件上传](#3. 基础文件上传)
      • [3.1 单文件上传实现](#3.1 单文件上传实现)
      • [3.2 多文件上传](#3.2 多文件上传)
    • [4. 文件下载实现](#4. 文件下载实现)
      • [4.1 基础文件下载](#4.1 基础文件下载)
      • [4.2 带进度显示的文件下载](#4.2 带进度显示的文件下载)
    • [5. 高级功能实现](#5. 高级功能实现)
      • [5.1 大文件分片上传](#5.1 大文件分片上传)
      • [5.2 文件合并逻辑](#5.2 文件合并逻辑)
    • [6. 安全性考虑](#6. 安全性考虑)
      • [6.1 文件类型验证](#6.1 文件类型验证)
      • [6.2 文件大小限制与病毒扫描](#6.2 文件大小限制与病毒扫描)
    • [7. 前端集成示例](#7. 前端集成示例)
      • [7.1 HTML 表单](#7.1 HTML 表单)
      • [7.2 JavaScript 上传逻辑](#7.2 JavaScript 上传逻辑)
    • [8. 最佳实践与性能优化](#8. 最佳实践与性能优化)
      • [8.1 存储策略选择](#8.1 存储策略选择)
      • [8.2 性能优化建议](#8.2 性能优化建议)
      • [8.3 监控与日志](#8.3 监控与日志)
    • [9. 常见问题与解决方案](#9. 常见问题与解决方案)
      • [Q1: 文件上传大小限制如何调整?](#Q1: 文件上传大小限制如何调整?)
      • [Q2: 如何防止文件名冲突?](#Q2: 如何防止文件名冲突?)
      • [Q3: 上传文件后如何提供访问链接?](#Q3: 上传文件后如何提供访问链接?)
      • [Q4: 如何实现图片缩略图?](#Q4: 如何实现图片缩略图?)
    • [10. 总结](#10. 总结)
    • 附录:完整项目结构

1. 引言

在现代Web应用中,文件上传下载是几乎每个系统都需要的基础功能。无论是用户头像上传、文档管理、还是大数据文件处理,文件操作都扮演着重要角色。Spring Boot作为Java领域最流行的微服务框架,提供了强大而灵活的文件处理能力。

本文将全面讲解Spring Boot中文件上传下载的实现方式,涵盖从基础的单文件上传到高级的分片上传、断点续传等场景,并提供完整的代码示例和最佳实践建议。

2. 环境准备

2.1 项目创建

使用Spring Initializr创建项目,选择以下依赖:

  • Spring Web
  • Spring Boot DevTools
  • Lombok(可选,简化代码)

2.2 配置文件

application.properties中添加文件上传配置:

properties 复制代码
# 单个文件最大大小
spring.servlet.multipart.max-file-size=10MB
# 单次请求最大大小
spring.servlet.multipart.max-request-size=100MB
# 文件存储路径
file.upload-dir=./uploads
# 启用文件上传
spring.servlet.multipart.enabled=true

3. 基础文件上传

3.1 单文件上传实现

java 复制代码
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
    
    @Value("${file.upload-dir}")
    private String uploadDir;
    
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("请选择要上传的文件");
            }
            
            // 生成唯一文件名
            String fileName = UUID.randomUUID().toString() + 
                            "_" + file.getOriginalFilename();
            
            // 创建存储目录
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }
            
            // 保存文件
            Path filePath = uploadPath.resolve(fileName);
            Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
            
            return ResponseEntity.ok("文件上传成功: " + fileName);
            
        } catch (IOException e) {
            return ResponseEntity.status(500).body("文件上传失败: " + e.getMessage());
        }
    }
}

3.2 多文件上传

java 复制代码
@PostMapping("/upload-multiple")
public ResponseEntity<List<String>> uploadMultipleFiles(
        @RequestParam("files") MultipartFile[] files) {
    
    List<String> fileNames = new ArrayList<>();
    
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            try {
                String fileName = saveFile(file);
                fileNames.add(fileName);
            } catch (IOException e) {
                return ResponseEntity.status(500)
                    .body(Collections.singletonList("部分文件上传失败"));
            }
        }
    }
    
    return ResponseEntity.ok(fileNames);
}

private String saveFile(MultipartFile file) throws IOException {
    String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
    Path filePath = Paths.get(uploadDir).resolve(fileName);
    Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
    return fileName;
}

4. 文件下载实现

4.1 基础文件下载

java 复制代码
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
    try {
        Path filePath = Paths.get(uploadDir).resolve(fileName).normalize();
        Resource resource = new UrlResource(filePath.toUri());
        
        if (resource.exists() && resource.isReadable()) {
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                       "attachment; filename=\"" + resource.getFilename() + "\"")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(resource);
        } else {
            return ResponseEntity.notFound().build();
        }
        
    } catch (MalformedURLException e) {
        return ResponseEntity.badRequest().build();
    }
}

4.2 带进度显示的文件下载

java 复制代码
@GetMapping("/download-with-progress/{fileName}")
public StreamingResponseBody downloadWithProgress(
        @PathVariable String fileName, 
        HttpServletResponse response) {
    
    Path filePath = Paths.get(uploadDir).resolve(fileName);
    
    return outputStream -> {
        try (InputStream inputStream = Files.newInputStream(filePath)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            long totalBytes = Files.size(filePath);
            long bytesCopied = 0;
            
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                bytesCopied += bytesRead;
                
                // 计算并记录进度(实际项目中可推送到前端)
                int progress = (int) ((bytesCopied * 100) / totalBytes);
                System.out.println("下载进度: " + progress + "%");
            }
        }
    };
}

5. 高级功能实现

5.1 大文件分片上传

java 复制代码
@PostMapping("/chunk-upload")
public ResponseEntity<Map<String, Object>> chunkUpload(
        @RequestParam("file") MultipartFile file,
        @RequestParam("chunkNumber") int chunkNumber,
        @RequestParam("totalChunks") int totalChunks,
        @RequestParam("identifier") String identifier) {
    
    try {
        // 创建临时目录存储分片
        Path tempDir = Paths.get(uploadDir, "temp", identifier);
        if (!Files.exists(tempDir)) {
            Files.createDirectories(tempDir);
        }
        
        // 保存分片文件
        Path chunkPath = tempDir.resolve(chunkNumber + ".part");
        Files.copy(file.getInputStream(), chunkPath, StandardCopyOption.REPLACE_EXISTING);
        
        Map<String, Object> response = new HashMap<>();
        response.put("chunkNumber", chunkNumber);
        response.put("totalChunks", totalChunks);
        
        // 检查是否所有分片都已上传
        if (chunkNumber == totalChunks) {
            response.put("mergeRequired", true);
        }
        
        return ResponseEntity.ok(response);
        
    } catch (IOException e) {
        return ResponseEntity.status(500).body(
            Collections.singletonMap("error", "分片上传失败"));
    }
}

5.2 文件合并逻辑

java 复制代码
private void mergeChunks(String identifier, String originalFileName) throws IOException {
    Path tempDir = Paths.get(uploadDir, "temp", identifier);
    Path outputFile = Paths.get(uploadDir).resolve(originalFileName);
    
    try (OutputStream outputStream = Files.newOutputStream(outputFile, 
            StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
        
        // 按顺序合并所有分片
        Files.list(tempDir)
            .sorted((p1, p2) -> {
                int n1 = Integer.parseInt(p1.getFileName().toString().replace(".part", ""));
                int n2 = Integer.parseInt(p2.getFileName().toString().replace(".part", ""));
                return Integer.compare(n1, n2);
            })
            .forEach(chunkPath -> {
                try {
                    Files.copy(chunkPath, outputStream);
                } catch (IOException e) {
                    throw new RuntimeException("合并分片失败", e);
                }
            });
        
        // 清理临时文件
        Files.walk(tempDir)
            .sorted(Comparator.reverseOrder())
            .map(Path::toFile)
            .forEach(File::delete);
    }
}

6. 安全性考虑

6.1 文件类型验证

java 复制代码
private boolean isValidFileType(MultipartFile file) {
    String fileName = file.getOriginalFilename();
    if (fileName == null) return false;
    
    String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
    Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "pdf", "doc", "docx");
    
    return allowedExtensions.contains(extension);
}

private boolean isValidContentType(MultipartFile file) {
    String contentType = file.getContentType();
    return contentType != null && 
           (contentType.startsWith("image/") || 
            contentType.equals("application/pdf") ||
            contentType.equals("application/msword"));
}

6.2 文件大小限制与病毒扫描

java 复制代码
@Component
public class FileSecurityService {
    
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    
    public void validateFile(MultipartFile file) {
        // 大小检查
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new FileSizeExceededException("文件大小超过限制");
        }
        
        // 文件名安全检查
        String fileName = file.getOriginalFilename();
        if (fileName != null && fileName.contains("..")) {
            throw new SecurityException("文件名包含非法字符");
        }
        
        // 实际项目中可集成病毒扫描服务
        // scanForViruses(file);
    }
}

7. 前端集成示例

7.1 HTML 表单

html 复制代码
<!-- 基础文件上传表单 -->
<form id="uploadForm" enctype="multipart/form-data">
    <input type="file" name="file" id="fileInput" multiple>
    <button type="submit">上传文件</button>
</form>

<!-- 进度显示 -->
<div id="progressContainer" style="display: none;">
    <progress id="uploadProgress" value="0" max="100"></progress>
    <span id="progressText">0%</span>
</div>

7.2 JavaScript 上传逻辑

javascript 复制代码
// 使用 Fetch API 上传文件
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    
    try {
        const response = await fetch('/api/files/upload', {
            method: 'POST',
            body: formData
        });
        
        if (response.ok) {
            const result = await response.text();
            console.log('上传成功:', result);
            return result;
        } else {
            throw new Error('上传失败');
        }
    } catch (error) {
        console.error('上传错误:', error);
        throw error;
    }
}

// 带进度显示的上传
function uploadWithProgress(file) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', (event) => {
            if (event.lengthComputable) {
                const percentComplete = (event.loaded / event.total) * 100;
                updateProgress(percentComplete);
            }
        });
        
        xhr.addEventListener('load', () => {
            if (xhr.status === 200) {
                resolve(xhr.responseText);
            } else {
                reject(new Error('上传失败'));
            }
        });
        
        xhr.addEventListener('error', () => reject(new Error('网络错误')));
        
        const formData = new FormData();
        formData.append('file', file);
        
        xhr.open('POST', '/api/files/upload');
        xhr.send(formData);
    });
}

8. 最佳实践与性能优化

8.1 存储策略选择

  • 本地存储:适合小型应用,部署简单
  • 对象存储(OSS):推荐生产环境使用(如阿里云OSS、AWS S3)
  • 分布式文件系统:适合大规模文件存储

8.2 性能优化建议

  1. 启用GZIP压缩:减少传输数据量
  2. 使用CDN加速:静态文件通过CDN分发
  3. 实现断点续传:大文件上传更可靠
  4. 异步处理:耗时操作放入消息队列
  5. 缓存策略:频繁访问的文件添加缓存

8.3 监控与日志

java 复制代码
@Slf4j
@RestControllerAdvice
public class FileUploadExceptionHandler {
    
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<String> handleSizeExceeded(MaxUploadSizeExceededException e) {
        log.warn("文件大小超过限制", e);
        return ResponseEntity.status(413).body("文件大小超过限制");
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception e) {
        log.error("文件上传处理异常", e);
        return ResponseEntity.status(500).body("服务器内部错误");
    }
}

9. 常见问题与解决方案

Q1: 文件上传大小限制如何调整?

A : 在application.properties中调整以下配置:

properties 复制代码
spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=200MB

Q2: 如何防止文件名冲突?

A: 使用UUID或时间戳重命名文件:

java 复制代码
String newFileName = UUID.randomUUID() + 
                    "_" + System.currentTimeMillis() + 
                    getFileExtension(originalFileName);

Q3: 上传文件后如何提供访问链接?

A: 根据存储方式生成访问URL:

java 复制代码
// 本地存储
String fileUrl = "/api/files/download/" + fileName;

// 对象存储
String fileUrl = "https://bucket.region.aliyuncs.com/" + fileName;

Q4: 如何实现图片缩略图?

A: 使用Thumbnailator等库处理:

java 复制代码
Thumbnails.of(originalFile)
    .size(200, 200)
    .outputFormat("jpg")
    .toFile(thumbnailFile);

10. 总结

本文详细介绍了Spring Boot中文件上传下载的完整实现方案,从基础的单文件操作到高级的分片上传、安全性考虑和性能优化。关键要点包括:

  1. 基础实现 :掌握MultipartFile的基本用法
  2. 高级功能:实现大文件分片上传和断点续传
  3. 安全性:严格验证文件类型和内容
  4. 性能优化:合理配置和存储策略选择
  5. 错误处理:完善的异常处理和用户反馈

在实际项目中,建议根据具体需求选择合适的存储方案,并充分考虑安全性和性能因素。随着业务发展,可以考虑迁移到专业的对象存储服务,以获得更好的可扩展性和可靠性。

附录:完整项目结构

源码下载 springboot-file

下一步建议:在实际项目中,可以考虑添加文件预览、在线编辑、版本管理等功能,打造更完善的文件管理系统。

相关推荐
Flittly1 小时前
【AgentScope Java新手村系列】(7)子Agent编排
java·spring boot·笔记·spring·ai
一个做软件开发的牛马2 小时前
Spring Boot Web 开发实战:RESTful API 设计、统一异常处理、参数校验与拦截器
java·后端
yurenpai(27届找实习中)2 小时前
Feed 流推送与附近商户:从推模式到 GeoHash,一条 Timeline 的完整旅程
java·数据库·oracle·feed
小bo波2 小时前
Java反射机制——运行时"透视"类的秘密
java·jvm·反射·源码分析·动态代理·进阶·spring底层·框架原理
IT 行者2 小时前
GitHub Spec Kit 实战(三):写一份能管住所有 spec 的 /speckit.constitution
java·github·ai编程·claude
java1234_小锋2 小时前
Spring Boot 的核心注解 @SpringBootApplication 由哪三个注解组成?
java·spring boot·后端
::呵呵哒::2 小时前
在macOS/Linux上优雅管理多个JDK版本:环境变量与别名配置指南
java·linux·macos
Master_Azur2 小时前
Web后端基础-Spring分层解耦
spring boot·后端·spring
IT 行者2 小时前
GitHub Spec Kit 实战(二):写一份不偏的 /speckit.specify
java·github·ai编程·claude