【SpringBoot 3.x 第10节】 文件上传下载还在让你头疼?老司机带你轻松搞定这些“磨人的小妖精“!

🏆本文收录于《滚雪球学Spring Boot》,专门攻坚指数提升,2025 年国内最系统+最强(更新中)。
  
本专栏致力打造最硬核 Spring Boot 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot教程导航帖】,你想学习的都被收集在内,快速投入学习!!两不误。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

🌟 前言:那些年我们一起踩过的文件上传"坑"

  哎呀,说起文件上传下载,我这个码了十几年代码的老家伙真是有一肚子话要说!😅 还记得刚入行那会儿,被一个简单的文件上传功能折磨得死去活来,那时候还是SSH框架的天下,配置文件写得我眼花缭乱,结果上传个图片比登天还难。现在好了,SpringBoot这个"救世主"出现了,让文件操作变得so easy!

  但是呢,别以为SpringBoot把一切都简化了就可以随便写代码了。我见过太多小伙伴因为文件上传的安全问题被老板骂得狗血淋头,也见过因为文件下载的性能问题让服务器直接"罢工"的惨案。今天咱们就来好好聊聊这个看似简单实则"暗藏杀机"的文件上传下载!🎯

📁 文件上传:从"小白"到"老司机"的必经之路

🎪 基础配置:让SpringBoot认识你的"文件朋友们"

  首先,咱们得在application.yml里给SpringBoot打个招呼,告诉它我们要玩文件上传这个游戏:

spring:
  servlet:
    multipart:
      enabled: true  # 启用文件上传支持,这个必须开!
      max-file-size: 10MB  # 单个文件最大10MB,别太贪心哦
      max-request-size: 50MB  # 总请求大小最大50MB
      file-size-threshold: 1KB  # 超过1KB就写入临时文件
      location: /tmp  # 临时文件存储位置

  这配置看起来简单,但每一行都有讲究!我曾经见过一个哥们儿把max-file-size设成了1GB,结果用户上传大文件直接把服务器内存撑爆了,那场面,啧啧啧…🤦‍♂️

🎨 控制器编写:让文件"乖乖听话"

@RestController
@RequestMapping("/api/file")
@Slf4j
public class FileController {
    
    // 文件存储根路径,生产环境记得配置到外部!
    private static final String UPLOAD_PATH = "/app/uploads/";
    
    /**
     * 单文件上传 - 最基础的操作
     * 别小看这个简单的功能,里面的门道可多着呢!
     */
    @PostMapping("/upload")
    public ResponseEntity<Map<String, Object>> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "category", defaultValue = "general") String category) {
        
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 第一步:校验文件是否为空,这是最基本的礼貌!
            if (file.isEmpty()) {
                log.warn("用户尝试上传空文件,这是什么操作?🤷‍♂️");
                result.put("success", false);
                result.put("message", "老铁,你上传个空文件是要闹哪样?");
                return ResponseEntity.badRequest().body(result);
            }
            
            // 第二步:文件类型验证,安全第一!
            if (!isValidFileType(file)) {
                log.warn("检测到非法文件类型上传尝试: {}", file.getContentType());
                result.put("success", false);
                result.put("message", "文件类型不合法,别想搞事情!");
                return ResponseEntity.badRequest().body(result);
            }
            
            // 第三步:生成唯一文件名,避免重名覆盖的悲剧
            String originalFilename = file.getOriginalFilename();
            String fileExtension = getFileExtension(originalFilename);
            String uniqueFilename = System.currentTimeMillis() + "_" + 
                                  UUID.randomUUID().toString().substring(0, 8) + fileExtension;
            
            // 第四步:创建目录结构
            String categoryPath = UPLOAD_PATH + category + "/";
            File categoryDir = new File(categoryPath);
            if (!categoryDir.exists()) {
                boolean created = categoryDir.mkdirs();
                if (!created) {
                    log.error("创建目录失败: {}", categoryPath);
                    throw new RuntimeException("目录创建失败,服务器闹脾气了!");
                }
            }
            
            // 第五步:保存文件
            String filePath = categoryPath + uniqueFilename;
            file.transferTo(new File(filePath));
            
            log.info("文件上传成功: {} -> {}", originalFilename, filePath);
            
            result.put("success", true);
            result.put("message", "上传成功!你的文件已经安全到达目的地 🎉");
            result.put("filename", uniqueFilename);
            result.put("originalName", originalFilename);
            result.put("size", file.getSize());
            result.put("url", "/api/file/download/" + category + "/" + uniqueFilename);
            
            return ResponseEntity.ok(result);
            
        } catch (Exception e) {
            log.error("文件上传失败", e);
            result.put("success", false);
            result.put("message", "上传失败:" + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
        }
    }
    
    /**
     * 多文件上传 - 批量处理的艺术
     * 这个功能在实际项目中超级常用!
     */
    @PostMapping("/upload/batch")
    public ResponseEntity<Map<String, Object>> uploadFiles(
            @RequestParam("files") MultipartFile[] files,
            @RequestParam(value = "category", defaultValue = "batch") String category) {
        
        Map<String, Object> result = new HashMap<>();
        List<Map<String, Object>> successFiles = new ArrayList<>();
        List<Map<String, Object>> failedFiles = new ArrayList<>();
        
        log.info("开始批量上传,文件数量: {}", files.length);
        
        for (MultipartFile file : files) {
            Map<String, Object> fileResult = new HashMap<>();
            fileResult.put("originalName", file.getOriginalFilename());
            
            try {
                if (file.isEmpty()) {
                    fileResult.put("success", false);
                    fileResult.put("message", "空文件跳过");
                    failedFiles.add(fileResult);
                    continue;
                }
                
                if (!isValidFileType(file)) {
                    fileResult.put("success", false);
                    fileResult.put("message", "文件类型不合法");
                    failedFiles.add(fileResult);
                    continue;
                }
                
                // 这里可以复用单文件上传的逻辑
                String uniqueFilename = saveFile(file, category);
                
                fileResult.put("success", true);
                fileResult.put("filename", uniqueFilename);
                fileResult.put("size", file.getSize());
                fileResult.put("url", "/api/file/download/" + category + "/" + uniqueFilename);
                successFiles.add(fileResult);
                
            } catch (Exception e) {
                log.error("批量上传中单个文件失败: {}", file.getOriginalFilename(), e);
                fileResult.put("success", false);
                fileResult.put("message", e.getMessage());
                failedFiles.add(fileResult);
            }
        }
        
        result.put("totalCount", files.length);
        result.put("successCount", successFiles.size());
        result.put("failedCount", failedFiles.size());
        result.put("successFiles", successFiles);
        result.put("failedFiles", failedFiles);
        result.put("message", String.format("批量上传完成!成功%d个,失败%d个", 
                                           successFiles.size(), failedFiles.size()));
        
        return ResponseEntity.ok(result);
    }
    
    /**
     * 文件类型验证 - 安全的守护神
     * 这个方法救过我无数次!
     */
    private boolean isValidFileType(MultipartFile file) {
        String contentType = file.getContentType();
        if (contentType == null) {
            return false;
        }
        
        // 定义允许的文件类型,根据业务需求调整
        String[] allowedTypes = {
            "image/jpeg", "image/png", "image/gif", "image/webp",
            "application/pdf",
            "application/msword", 
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            "text/plain"
        };
        
        return Arrays.asList(allowedTypes).contains(contentType);
    }
    
    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String filename) {
        if (filename == null || !filename.contains(".")) {
            return "";
        }
        return filename.substring(filename.lastIndexOf("."));
    }
    
    /**
     * 保存文件的通用方法
     */
    private String saveFile(MultipartFile file, String category) throws IOException {
        String originalFilename = file.getOriginalFilename();
        String fileExtension = getFileExtension(originalFilename);
        String uniqueFilename = System.currentTimeMillis() + "_" + 
                              UUID.randomUUID().toString().substring(0, 8) + fileExtension;
        
        String categoryPath = UPLOAD_PATH + category + "/";
        File categoryDir = new File(categoryPath);
        if (!categoryDir.exists()) {
            categoryDir.mkdirs();
        }
        
        String filePath = categoryPath + uniqueFilename;
        file.transferTo(new File(filePath));
        
        return uniqueFilename;
    }
}

📥 文件下载:让数据"飞"向用户

  说完上传,咱们再来聊聊下载。别以为下载比上传简单,这里面的坑一点都不少!我记得有一次,一个项目的文件下载功能在并发量大的时候直接把服务器搞崩了,原因就是没有考虑到大文件的流式传输。😱

/**
 * 文件下载 - 让数据优雅地"飞"向用户
 */
@GetMapping("/download/{category}/{filename}")
public ResponseEntity<Resource> downloadFile(
        @PathVariable String category,
        @PathVariable String filename,
        HttpServletRequest request) {
    
    try {
        // 构建文件路径
        String filePath = UPLOAD_PATH + category + "/" + filename;
        Path path = Paths.get(filePath);
        
        // 检查文件是否存在
        if (!Files.exists(path) || !Files.isReadable(path)) {
            log.warn("用户尝试下载不存在的文件: {}", filePath);
            return ResponseEntity.notFound().build();
        }
        
        // 安全检查:防止路径遍历攻击
        if (!isSecurePath(filePath)) {
            log.warn("检测到路径遍历攻击尝试: {}", filePath);
            return ResponseEntity.badRequest().build();
        }
        
        // 创建资源对象
        Resource resource = new UrlResource(path.toUri());
        
        // 确定文件的MIME类型
        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            log.warn("无法确定文件类型: {}", filename);
        }
        
        // 如果无法确定类型,默认为二进制流
        if (contentType == null) {
            contentType = "application/octet-stream";
        }
        
        // 设置文件名,支持中文
        String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString());
        
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                       "attachment; filename=\"" + encodedFilename + "\"")
                .body(resource);
                
    } catch (Exception e) {
        log.error("文件下载失败: {}", filename, e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

/**
 * 在线预览文件 - 让用户"一眼看穿"文件内容
 */
@GetMapping("/preview/{category}/{filename}")
public ResponseEntity<Resource> previewFile(
        @PathVariable String category,
        @PathVariable String filename,
        HttpServletRequest request) {
    
    try {
        String filePath = UPLOAD_PATH + category + "/" + filename;
        Path path = Paths.get(filePath);
        
        if (!Files.exists(path) || !Files.isReadable(path)) {
            return ResponseEntity.notFound().build();
        }
        
        if (!isSecurePath(filePath)) {
            return ResponseEntity.badRequest().build();
        }
        
        Resource resource = new UrlResource(path.toUri());
        String contentType = getContentType(request, resource);
        
        // 对于图片和PDF,支持在线预览
        if (contentType.startsWith("image/") || contentType.equals("application/pdf")) {
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"")
                    .body(resource);
        } else {
            // 其他类型强制下载
            return downloadFile(category, filename, request);
        }
        
    } catch (Exception e) {
        log.error("文件预览失败: {}", filename, e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

/**
 * 大文件流式下载 - 内存友好的下载方式
 * 这个功能在处理视频、大图片时特别有用!
 */
@GetMapping("/stream/{category}/{filename}")
public ResponseEntity<StreamingResponseBody> streamDownload(
        @PathVariable String category,
        @PathVariable String filename) {
    
    try {
        String filePath = UPLOAD_PATH + category + "/" + filename;
        Path path = Paths.get(filePath);
        
        if (!Files.exists(path) || !Files.isReadable(path)) {
            return ResponseEntity.notFound().build();
        }
        
        if (!isSecurePath(filePath)) {
            return ResponseEntity.badRequest().build();
        }
        
        File file = path.toFile();
        long fileSize = file.length();
        
        StreamingResponseBody responseBody = outputStream -> {
            try (FileInputStream inputStream = new FileInputStream(file);
                 BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
                
                byte[] buffer = new byte[8192]; // 8KB 缓冲区
                int bytesRead;
                
                while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                    outputStream.flush();
                }
                
                log.info("流式下载完成: {}, 大小: {} bytes", filename, fileSize);
                
            } catch (IOException e) {
                log.error("流式下载过程中发生错误: {}", filename, e);
                throw e;
            }
        };
        
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                       "attachment; filename=\"" + filename + "\"")
                .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileSize))
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(responseBody);
                
    } catch (Exception e) {
        log.error("流式下载失败: {}", filename, e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

/**
 * 安全路径检查 - 防止路径遍历攻击
 */
private boolean isSecurePath(String filePath) {
    try {
        String canonicalPath = new File(filePath).getCanonicalPath();
        String basePath = new File(UPLOAD_PATH).getCanonicalPath();
        return canonicalPath.startsWith(basePath);
    } catch (IOException e) {
        log.error("路径安全检查失败", e);
        return false;
    }
}

/**
 * 获取内容类型
 */
private String getContentType(HttpServletRequest request, Resource resource) {
    String contentType = null;
    try {
        contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
    } catch (IOException ex) {
        log.debug("无法确定文件类型");
    }
    return contentType == null ? "application/octet-stream" : contentType;
}

🛡️ 高级特性:让你的文件处理"更上一层楼"

🔐 文件安全处理

  安全问题可不能马虎!我见过太多因为文件上传漏洞被黑客攻击的案例了。来看看怎么做安全防护:

@Component
@Slf4j
public class FileSecurityService {
    
    // 恶意文件签名检测
    private static final Map<String, String> FILE_SIGNATURES = new HashMap<>();
    
    static {
        FILE_SIGNATURES.put("jpg", "FFD8FF");
        FILE_SIGNATURES.put("png", "89504E47");
        FILE_SIGNATURES.put("gif", "47494638");
        FILE_SIGNATURES.put("pdf", "255044462D");
        // 可以添加更多文件类型的签名
    }
    
    /**
     * 文件签名验证 - 防止伪造文件扩展名
     */
    public boolean validateFileSignature(MultipartFile file) {
        try {
            String originalFilename = file.getOriginalFilename();
            if (originalFilename == null) {
                return false;
            }
            
            String extension = getFileExtension(originalFilename).toLowerCase().replace(".", "");
            String expectedSignature = FILE_SIGNATURES.get(extension);
            
            if (expectedSignature == null) {
                // 对于不在检测列表中的文件类型,允许通过
                return true;
            }
            
            byte[] fileBytes = file.getBytes();
            if (fileBytes.length < expectedSignature.length() / 2) {
                return false;
            }
            
            StringBuilder actualSignature = new StringBuilder();
            for (int i = 0; i < expectedSignature.length() / 2; i++) {
                actualSignature.append(String.format("%02X", fileBytes[i]));
            }
            
            boolean isValid = actualSignature.toString().startsWith(expectedSignature);
            if (!isValid) {
                log.warn("文件签名验证失败: {} 期望:{} 实际:{}", 
                        originalFilename, expectedSignature, actualSignature.toString());
            }
            
            return isValid;
            
        } catch (Exception e) {
            log.error("文件签名验证过程中出错", e);
            return false;
        }
    }
    
    /**
     * 病毒扫描接口 - 可以集成第三方杀毒引擎
     */
    public boolean scanForVirus(MultipartFile file) {
        // 这里可以集成ClamAV或其他杀毒软件的API
        // 简单示例:检查文件名中是否包含可疑关键词
        String filename = file.getOriginalFilename();
        if (filename != null) {
            String[] suspiciousKeywords = {".exe", ".bat", ".cmd", ".scr", ".vbs"};
            for (String keyword : suspiciousKeywords) {
                if (filename.toLowerCase().contains(keyword)) {
                    log.warn("检测到可疑文件: {}", filename);
                    return false;
                }
            }
        }
        return true;
    }
    
    private String getFileExtension(String filename) {
        if (filename == null || !filename.contains(".")) {
            return "";
        }
        return filename.substring(filename.lastIndexOf("."));
    }
}

📊 文件处理进度追踪

  对于大文件上传,用户体验很重要!没有进度条的上传就像盲人走路,用户会急死的:

@RestController
@RequestMapping("/api/file")
@Slf4j
public class FileProgressController {
    
    private final Map<String, UploadProgress> uploadProgressMap = new ConcurrentHashMap<>();
    
    /**
     * 大文件分片上传 - 让大文件上传变得"丝滑"
     */
    @PostMapping("/upload/chunk")
    public ResponseEntity<Map<String, Object>> uploadChunk(
            @RequestParam("file") MultipartFile chunk,
            @RequestParam("chunkIndex") int chunkIndex,
            @RequestParam("totalChunks") int totalChunks,
            @RequestParam("fileId") String fileId,
            @RequestParam("filename") String filename) {
        
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 创建临时目录存储分片
            String tempDir = UPLOAD_PATH + "temp/" + fileId + "/";
            File tempDirFile = new File(tempDir);
            if (!tempDirFile.exists()) {
                tempDirFile.mkdirs();
            }
            
            // 保存当前分片
            String chunkPath = tempDir + "chunk_" + chunkIndex;
            chunk.transferTo(new File(chunkPath));
            
            // 更新上传进度
            UploadProgress progress = uploadProgressMap.computeIfAbsent(fileId, 
                k -> new UploadProgress(totalChunks, filename));
            progress.addCompletedChunk(chunkIndex);
            
            log.info("分片上传进度: {}/{} - {}", 
                    progress.getCompletedChunks(), totalChunks, filename);
            
            result.put("success", true);
            result.put("chunkIndex", chunkIndex);
            result.put("progress", progress.getProgressPercentage());
            result.put("message", String.format("分片 %d/%d 上传完成", chunkIndex + 1, totalChunks));
            
            // 检查是否所有分片都已上传完成
            if (progress.isComplete()) {
                // 合并分片
                String finalFilePath = mergeChunks(fileId, filename, totalChunks);
                result.put("finalPath", finalFilePath);
                result.put("message", "文件上传完成!所有分片已成功合并 🎉");
                
                // 清理临时文件和进度记录
                cleanupTempFiles(fileId);
                uploadProgressMap.remove(fileId);
            }
            
            return ResponseEntity.ok(result);
            
        } catch (Exception e) {
            log.error("分片上传失败: chunk={}, fileId={}", chunkIndex, fileId, e);
            result.put("success", false);
            result.put("message", "分片上传失败: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
        }
    }
    
    /**
     * 获取上传进度
     */
    @GetMapping("/upload/progress/{fileId}")
    public ResponseEntity<UploadProgress> getUploadProgress(@PathVariable String fileId) {
        UploadProgress progress = uploadProgressMap.get(fileId);
        if (progress == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(progress);
    }
    
    /**
     * 合并分片文件
     */
    private String mergeChunks(String fileId, String filename, int totalChunks) throws IOException {
        String tempDir = UPLOAD_PATH + "temp/" + fileId + "/";
        String finalPath = UPLOAD_PATH + "merged/" + filename;
        
        File finalDir = new File(UPLOAD_PATH + "merged/");
        if (!finalDir.exists()) {
            finalDir.mkdirs();
        }
        
        try (FileOutputStream fos = new FileOutputStream(finalPath);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            
            for (int i = 0; i < totalChunks; i++) {
                String chunkPath = tempDir + "chunk_" + i;
                File chunkFile = new File(chunkPath);
                
                try (FileInputStream fis = new FileInputStream(chunkFile);
                     BufferedInputStream bis = new BufferedInputStream(fis)) {
                    
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        bos.write(buffer, 0, bytesRead);
                    }
                }
            }
        }
        
        log.info("文件合并完成: {} -> {}", fileId, finalPath);
        return finalPath;
    }
    
    /**
     * 清理临时文件
     */
    private void cleanupTempFiles(String fileId) {
        try {
            String tempDir = UPLOAD_PATH + "temp/" + fileId + "/";
            File tempDirFile = new File(tempDir);
            if (tempDirFile.exists()) {
                File[] files = tempDirFile.listFiles();
                if (files != null) {
                    for (File file : files) {
                        file.delete();
                    }
                }
                tempDirFile.delete();
            }
        } catch (Exception e) {
            log.error("清理临时文件失败: {}", fileId, e);
        }
    }
    
    /**
     * 上传进度实体类
     */
    @Data
    public static class UploadProgress {
        private final int totalChunks;
        private final String filename;
        private final Set<Integer> completedChunks = new HashSet<>();
        private final long startTime = System.currentTimeMillis();
        
        public UploadProgress(int totalChunks, String filename) {
            this.totalChunks = totalChunks;
            this.filename = filename;
        }
        
        public void addCompletedChunk(int chunkIndex) {
            completedChunks.add(chunkIndex);
        }
        
        public double getProgressPercentage() {
            return (double) completedChunks.size() / totalChunks * 100;
        }
        
        public boolean isComplete() {
            return completedChunks.size() == totalChunks;
        }
        
        public long getElapsedTime() {
            return System.currentTimeMillis() - startTime;
        }
    }
}

💡 实战经验分享:那些年踩过的"坑"

🕳️ 坑位一:文件名乱码问题

这个问题我遇到过无数次!特别是中文文件名,在不同浏览器下表现还不一样,简直是噩梦:

/**
 * 解决文件名乱码的终极方案
 */
public String getDownloadFilename(String filename, String userAgent) {
    try {
        if (userAgent.contains("MSIE") || userAgent.contains("Edge")) {
            // IE和Edge浏览器
            return URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
        } else if (userAgent.contains("Firefox")) {
            // Firefox浏览器
            return "=?UTF-8?B?" + Base64.getEncoder().encodeToString(filename.getBytes("UTF-8")) + "?=";
        } else {
            // Chrome、Safari等其他浏览器
            return URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
        }
    } catch (UnsupportedEncodingException e) {
        log.error("文件名编码失败", e);
        return filename;
    }
}

🕳️ 坑位二:临时文件清理不及时

有一次我们的服务器磁盘空间突然爆满,排查后发现是临时文件没有及时清理。从那以后我就特别注意这个问题:

@Component
@Slf4j
public class TempFileCleanupScheduler {
    
    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    public void cleanupTempFiles() {
        String tempPath = UPLOAD_PATH + "temp/";
        File tempDir = new File(tempPath);
        
        if (!tempDir.exists()) {
            return;
        }
        
        long cutoffTime = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2); // 2小时前
        int deletedCount = 0;
        
        File[] tempFiles = tempDir.listFiles();
        if (tempFiles != null) {
            for (File file : tempFiles) {
                if (file.lastModified() < cutoffTime) {
                    if (deleteRecursively(file)) {
                        deletedCount++;
                    }
                }
            }
        }
        
        if (deletedCount > 0) {
            log.info("清理过期临时文件: {} 个", deletedCount);
        }
    }
    
    private boolean deleteRecursively(File file) {
        if (file.isDirectory()) {
            File[] children = file.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteRecursively(child);
                }
            }
        }
        return file.delete();
    }
}

🕳️ 坑位三:并发上传导致的文件损坏

在高并发场景下,如果不做好同步处理,很容易出现文件损坏的问题:

@Service
@Slf4j
public class ConcurrentFileService {
    
    private final ReentrantLock uploadLock = new ReentrantLock();
    private final Map<String, ReentrantLock> fileLocks = new ConcurrentHashMap<>();
    
    /**
     * 线程安全的文件操作
     */
    public void safeFileOperation(String fileId, Runnable operation) {
        ReentrantLock lock = fileLocks.computeIfAbsent(fileId, k -> new ReentrantLock());
        
        try {
            lock.lock();
            operation.run();
        } finally {
            lock.unlock();
            // 操作完成后清理锁,避免内存泄露
            if (!lock.hasQueuedThreads()) {
                fileLocks.remove(fileId);
            }
        }
    }
}

🎯 性能优化:让文件操作"飞"起来

⚡ 异步处理

对于大文件处理,同步操作会阻塞用户请求,体验很糟糕。异步处理是王道:

@Service
@Slf4j
public class AsyncFileService {
    
    @Async("fileTaskExecutor")
    public CompletableFuture<String> processFileAsync(MultipartFile file) {
        try {
            // 模拟耗时的文件处理操作
            Thread.sleep(5000);
            
            String processedFilePath = processFile(file);
            log.info("异步文件处理完成: {}", processedFilePath);
            return CompletableFuture.completedFuture(processedFilePath);
            
        } catch (Exception e) {
            log.error("异步文件处理失败", e);
            CompletableFuture<String> future = new CompletableFuture<>();
            future.completeExceptionally(e);
            return future;
        }
    }
    
    private String processFile(MultipartFile file) {
        // 这里可以进行图片压缩、格式转换等操作
        return "processed_" + file.getOriginalFilename();
    }
    
    @Bean("fileTaskExecutor")
    public Executor fileTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("FileAsync-");
        executor.initialize();
        return executor;
    }
}

🎉 总结:文件上传下载的"江湖秘籍"

  哎呀,不知不觉又写了这么多!😅 回顾一下我们今天聊的这些内容,从基础的文件上传下载,到安全防护,再到性能优化,每一个环节都很重要。

  作为一个在代码世界里摸爬滚打了这么多年的老兵,我想说的是:技术永远在进步,但是踏实的基础和细致的思考永远不会过时。SpringBoot确实让文件操作变得简单了很多,但是我们不能因此就掉以轻心。

  安全问题要重视,性能优化要考虑,用户体验要关注。每一行代码都可能影响到用户的使用感受,每一个细节都可能成为系统的瓶颈。

  希望我的这些经验分享能帮到正在路上的你们。记住,代码如人生,细节决定成败!加油,各位代码侠们!🚀

  最后再啰嗦一句:多写代码,多踩坑,多总结。只有经历过的坑,才能真正成为你的经验财富。愿你在文件上传下载的路上,少踩坑,多成长!✨


PS: 如果你在实际开发中遇到了什么奇葩问题,欢迎留言交流。我们一起在代码的海洋里乘风破浪! 🌊

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们一对一参考 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

✨️ Who am I?

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主及影响力最佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bug菌¹

你的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值