SpringBoot实现文件上传下载的完整指南

 更新时间:2026年06月15日 08:28:01   作者:爱码少年 00fly.online  
在现代Web应用中,文件上传下载是几乎每个系统都需要的基础功能,无论是用户头像上传、文档管理、还是大数据文件处理,文件操作都扮演着重要角色,本文将全面讲解Spring Boot中文件上传下载的实现方式,需要的朋友可以参考下

1. 引言

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

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

2. 环境准备

2.1 项目创建

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

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

2.2 配置文件

application.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 单文件上传实现

@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 多文件上传

@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 基础文件下载

@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 带进度显示的文件下载

@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 大文件分片上传

@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 文件合并逻辑

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 文件类型验证

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 文件大小限制与病毒扫描

@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 表单

<!-- 基础文件上传表单 -->
<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 上传逻辑

// 使用 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 监控与日志

@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中调整以下配置:

spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=200MB

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

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

String newFileName = UUID.randomUUID() + 
                    "_" + System.currentTimeMillis() + 
                    getFileExtension(originalFileName);

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

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

// 本地存储
String fileUrl = "/api/files/download/" + fileName;

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

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

A: 使用Thumbnailator等库处理:

Thumbnails.of(originalFile)
    .size(200, 200)
    .outputFormat("jpg")
    .toFile(thumbnailFile);

10. 总结

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

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

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

以上就是SpringBoot实现文件上传下载的完整指南的详细内容,更多关于SpringBoot文件上传下载的资料请关注脚本之家其它相关文章!

相关文章

  • hibernate-validator如何使用校验框架

    hibernate-validator如何使用校验框架

    高效、合理的使用hibernate-validator校验框架可以提高程序的可读性,以及减少不必要的代码逻辑,本文主要介绍了hibernate-validator如何使用校验框架,感兴趣的可以了解一下
    2022-04-04
  • jmeter断言的三种实现方式

    jmeter断言的三种实现方式

    在使用Jmeter进行性能测试或者接口自动化测试工作中,经常会用到的一个功能,就是断言,本文主要介绍了jmeter断言的三种实现方式,
    2024-01-01
  • Java8中Map常用的遍历方式

    Java8中Map常用的遍历方式

    这篇文章主要给大家介绍了关于Java8中Map常用的遍历方式,map属于java中的顶级接口之一,区别于list,map是键值对的形式存在,需要的朋友可以参考下
    2023-07-07
  • Spring Boot 3.x GraalVM原生镜像构建内存溢出问题解决方案

    Spring Boot 3.x GraalVM原生镜像构建内存溢出问题解决方案

    文章解析了Spring Boot 3.x与GraalVM Native Image构建过程中出现的内存溢出问题,从问题概述、根本原因、诊断工具、解决方案到高级调优技巧和应急解决方案,全面覆盖了构建优化的各个方面,本文给大家介绍的非常详细,感兴趣的朋友一起学习下吧
    2026-01-01
  • spring aop execution表达式的用法

    spring aop execution表达式的用法

    这篇文章主要介绍了spring aop execution表达式的用法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • SpringCloud集成Sleuth和Zipkin的思路讲解

    SpringCloud集成Sleuth和Zipkin的思路讲解

    Zipkin 是 Twitter 的一个开源项目,它基于 Google Dapper 实现,它致力于收集服务的定时数据,以及解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现,这篇文章主要介绍了SpringCloud集成Sleuth和Zipkin,需要的朋友可以参考下
    2022-11-11
  • SpringMVC一步到位精通拦截器

    SpringMVC一步到位精通拦截器

    拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行。本文将详细讲讲SpringMVC中拦截器的概念及入门案例,感兴趣的可以尝试一下
    2022-12-12
  • 教你怎么用Java数组和链表实现栈

    教你怎么用Java数组和链表实现栈

    本篇文章为大家详细介绍了怎么用Java数组和链表实现栈,文中有非常详细的代码示例及注释,对正在学习java的小伙伴们很有帮助,需要的朋友可以参考下
    2021-05-05
  • Mybatis查询方法如何实现没有返回值

    Mybatis查询方法如何实现没有返回值

    这篇文章主要介绍了Mybatis查询方法如何实现没有返回值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • 总结一下Java回调机制的相关知识

    总结一下Java回调机制的相关知识

    今天给大家带来的是关于Java的相关知识,文章围绕着Java回调机制展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06

最新评论