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

一、核心概念与原理

Spring Boot通过Spring MVC的MultipartFile接口处理文件上传,该接口封装了上传文件的所有信息,包括文件内容、文件名、MIME类型等。文件上传的本质是将HTTP请求中的multipart/form-data格式数据解析为文件流并保存到指定位置,而文件下载则是将文件流写入HTTP响应体并设置相应的响应头信息。

二、环境配置

1. 项目依赖配置

在pom.xml中添加web依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. 文件上传配置

在application.properties中配置上传参数:

# 单个文件最大大小
spring.servlet.multipart.max-file-size=10MB
# 单次请求最大大小
spring.servlet.multipart.max-request-size=50MB
# 启用文件上传
spring.servlet.multipart.enabled=true
# 字符编码
spring.servlet.encoding.charset=UTF-8
spring.servlet.encoding.force=true

或者在application.yml中配置:

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 50MB
      enabled: true
    encoding:
      charset: UTF-8
      force: true

注意事项

  • max-file-size必须小于等于max-request-size
  • 生产环境建议配合前端校验和分片上传
  • 如需GB级上传,建议使用OSS对象存储服务

三、文件上传实现

1. 单文件上传

@RestController
@RequestMapping("/file")
public class FileUploadController {
    
    private static final String UPLOAD_DIR = "uploads/";
    
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        // 校验文件是否为空
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("请选择要上传的文件");
        }
        
        try {
            // 获取原始文件名
            String originalFilename = file.getOriginalFilename();
            // 生成唯一文件名防止覆盖
            String fileName = UUID.randomUUID().toString() + "_" + originalFilename;
            
            // 创建上传目录
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
            
            // 保存文件
            File dest = new File(uploadDir.getAbsolutePath() + File.separator + fileName);
            file.transferTo(dest);
            
            return ResponseEntity.ok("文件上传成功: " + fileName);
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("文件上传失败: " + e.getMessage());
        }
    }
}

2. 多文件上传

@PostMapping("/multi-upload")
public ResponseEntity<List<String>> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
    List<String> results = new ArrayList<>();
    
    for (MultipartFile file : files) {
        if (file.isEmpty()) {
            results.add("文件为空: " + file.getOriginalFilename());
            continue;
        }
        
        try {
            String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
            File dest = new File(UPLOAD_DIR + fileName);
            file.transferTo(dest);
            results.add("上传成功: " + fileName);
        } catch (IOException e) {
            results.add("上传失败: " + file.getOriginalFilename());
        }
    }
    
    return ResponseEntity.ok(results);
}

3. MultipartFile核心方法

方法说明
getOriginalFilename()获取原始文件名(含扩展名)
getName()获取表单中的参数名
getContentType()获取文件MIME类型
isEmpty()判断文件是否为空
getSize()获取文件大小(字节数)
getBytes()获取文件字节数组
getInputStream()获取文件输入流
transferTo(File dest)将文件保存到指定路径

四、文件下载实现

1. 使用HttpServletResponse方式

@GetMapping("/download")
public void downloadFile(@RequestParam String fileName, HttpServletResponse response) {
    try {
        File file = new File(UPLOAD_DIR + fileName);
        
        // 校验文件是否存在
        if (!file.exists()) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            response.getWriter().write("文件不存在");
            return;
        }
        
        // 设置响应头
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
        response.setContentLength((int) file.length());
        
        // 写入响应流
        try (InputStream inputStream = new FileInputStream(file);
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            outputStream.flush();
        }
    } catch (IOException e) {
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}

2. 使用ResponseEntity方式(推荐)

@GetMapping("/download2")
public ResponseEntity<Resource> downloadFile2(@RequestParam String fileName) {
    try {
        File file = new File(UPLOAD_DIR + fileName);
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }
        
        Resource resource = new FileSystemResource(file);
        
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
        
        return ResponseEntity.ok()
                .headers(headers)
                .contentLength(file.length())
                .body(resource);
    } catch (Exception e) {
        return ResponseEntity.internalServerError().build();
    }
}

3. 使用StreamingResponseBody(大文件优化)

@GetMapping("/download3")
public ResponseEntity<StreamingResponseBody> downloadFile3(@RequestParam String fileName) {
    File file = new File(UPLOAD_DIR + fileName);
    if (!file.exists()) {
        return ResponseEntity.notFound().build();
    }
    
    StreamingResponseBody responseBody = outputStream -> {
        try (InputStream inputStream = new FileInputStream(file)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        }
    };
    
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
    
    return ResponseEntity.ok()
            .headers(headers)
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(responseBody);
}

五、安全与校验

1. 文件类型校验

private static final String[] ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"};

private boolean isValidFileType(String fileName) {
    if (fileName == null) {
        return false;
    }
    String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
    return Arrays.asList(ALLOWED_EXTENSIONS).contains(extension);
}

// 在Controller中使用
if (!isValidFileType(file.getOriginalFilename())) {
    return ResponseEntity.badRequest().body("不支持的文件类型");
}

2. 文件大小校验

private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

if (file.getSize() > MAX_FILE_SIZE) {
    return ResponseEntity.badRequest().body("文件大小超过限制");
}

3. 文件名安全处理

// 防止路径遍历攻击
String safeFileName = new File(file.getOriginalFilename()).getName();
// 替换特殊字符
safeFileName = safeFileName.replaceAll("[^a-zA-Z0-9._-]", "_");

六、大文件优化方案

1. 分片上传

@PostMapping("/upload-chunk")
public ResponseEntity<String> uploadChunk(
        @RequestParam("chunk") MultipartFile chunk,
        @RequestParam("fileName") String fileName,
        @RequestParam("chunkNumber") int chunkNumber,
        @RequestParam("totalChunks") int totalChunks) throws IOException {
    
    // 保存分片到临时目录
    File tempFile = new File(TEMP_DIR + fileName + "_" + chunkNumber);
    chunk.transferTo(tempFile);
    
    // 如果所有分片都已上传,合并文件
    if (chunkNumber == totalChunks) {
        mergeFile(fileName, totalChunks);
    }
    
    return ResponseEntity.ok("分片上传成功");
}

private void mergeFile(String fileName, int totalChunks) throws IOException {
    File mergedFile = new File(UPLOAD_DIR + fileName);
    try (RandomAccessFile accessFile = new RandomAccessFile(mergedFile, "rw")) {
        for (int i = 1; i <= totalChunks; i++) {
            File chunkFile = new File(TEMP_DIR + fileName + "_" + i);
            try (RandomAccessFile chunkAccessFile = new RandomAccessFile(chunkFile, "r")) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = chunkAccessFile.read(buffer)) != -1) {
                    accessFile.write(buffer, 0, len);
                }
            }
            chunkFile.delete(); // 删除临时分片
        }
    }
}

2. 断点续传

@PostMapping("/upload-resume")
public ResponseEntity<String> uploadResume(
        @RequestParam("file") MultipartFile file,
        @RequestParam("index") Integer index,
        @RequestParam("md5") String md5,
        HttpServletRequest request) throws Exception {
    
    // 检查分片是否已上传
    if (chunkService.isAlreadyUpload(md5, index)) {
        return ResponseEntity.ok("分片已存在");
    }
    
    // 获取已上传字节数
    long uploadedBytes = getUploadedBytes(md5, index);
    
    // 续传逻辑
    String path = getFilePath(md5, index);
    File dest = new File(path);
    long fileLength = file.getSize();
    
    try (RandomAccessFile accessFile = new RandomAccessFile(dest, "rw")) {
        accessFile.seek(uploadedBytes);
        try (InputStream inputStream = file.getInputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                accessFile.write(buffer, 0, len);
                uploadedBytes += len;
                // 记录上传进度
                chunkService.saveResume(md5, index, uploadedBytes);
            }
        }
    }
    
    return ResponseEntity.ok("续传成功");
}

七、异常处理

1. 全局异常处理器

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<String> handleMaxSizeException(MaxUploadSizeExceededException e) {
        return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                .body("文件大小超过限制");
    }
    
    @ExceptionHandler(MultipartException.class)
    public ResponseEntity<String> handleMultipartException(MultipartException e) {
        return ResponseEntity.badRequest()
                .body("文件上传失败: " + e.getMessage());
    }
    
    @ExceptionHandler(IOException.class)
    public ResponseEntity<String> handleIOException(IOException e) {
        return ResponseEntity.internalServerError()
                .body("文件操作失败: " + e.getMessage());
    }
}

2. Tomcat配置优化

# 设置Tomcat最大吞吐量
server.tomcat.max-swallow-size=100MB
# 或设置为-1不限制
server.tomcat.max-swallow-size=-1

八、性能优化建议

  1. 异步处理:使用@Async注解异步处理文件上传下载任务,避免主线程阻塞
  2. 流式处理:使用StreamUtils.copy()替代手动字节数组操作,减少内存占用
  3. CDN集成:使用云存储服务(如阿里云OSS、AWS S3)存储文件,减轻服务器压力
  4. 分片上传:对于大文件采用分片上传和断点续传机制
  5. 内存优化:避免将大文件全部加载到内存中,使用流式读写
  6. 限流熔断:对上传下载接口进行限流,防止服务器过载
  7. 日志监控:详细记录文件操作的关键步骤,监控磁盘空间和I/O性能

九、前端调用示例

HTML表单

<form method="POST" action="/file/upload" enctype="multipart/form-data">
    <input type="file" name="file" accept=".jpg,.jpeg,.png,.pdf">
    <button type="submit">上传</button>
</form>

JavaScript调用

// 单文件上传
const formData = new FormData();
formData.append('file', fileInput.files[0]);

fetch('/file/upload', {
    method: 'POST',
    body: formData
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('上传失败:', error));

// 文件下载
function downloadFile(fileName) {
    window.open(`/file/download?fileName=${encodeURIComponent(fileName)}`);
}

十、完整项目结构

src/main/java/
├── com.example.demo
│   ├── config
│   │   ├── MultipartConfig.java
│   │   └── WebMvcConfig.java
│   ├── controller
│   │   └── FileController.java
│   ├── exception
│   │   └── GlobalExceptionHandler.java
│   ├── service
│   │   └── FileService.java
│   └── DemoApplication.java
src/main/resources/
├── application.properties
└── uploads/

总结

Spring Boot文件上传下载功能虽然看似简单,但在实际开发中需要考虑文件大小限制、类型校验、安全防护、性能优化等多个方面。通过本文的完整实现方案,可以快速构建稳定、安全、高效的文件处理系统。对于生产环境,建议结合云存储服务、CDN加速、分片上传等高级特性,进一步提升系统的可靠性和用户体验。


作 者:南烛
链 接:https://www.itnotes.top/archives/1228
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇