Java Apache Commons IO 框架

88 阅读7分钟

一、一句话总结

Commons IO = Java IO操作的瑞士军刀,提供工具类简化文件操作,避免手写样板代码

二、核心模块

1. 主要工具类

commons-io
├── IOUtils          - 流操作工具(核心)
├── FileUtils        - 文件操作工具(最常用)
├── FilenameUtils    - 文件名工具
├── FileSystemUtils  - 文件系统工具
└── 其他工具类

三、IOUtils(流操作)

1. 复制流(最常用)

// ✅ 一行代码复制流
try (InputStream input = new FileInputStream("source.txt");
     OutputStream output = new FileOutputStream("target.txt")) {
    
    // 传统方式需要循环读取写入
    // Commons IO一行搞定
    IOUtils.copy(input, output);
    
    // 指定缓冲区大小
    IOUtils.copy(input, output, 8192);  // 8KB缓冲区
    
    // 复制到字符串
    String content = IOUtils.toString(input, StandardCharsets.UTF_8);
    
    // 从字符串复制
    IOUtils.write("Hello World", output, StandardCharsets.UTF_8);
}

2. 读写全部内容

// 读取所有字节
byte[] bytes = IOUtils.toByteArray(inputStream);

// 读取所有行
List<String> lines = IOUtils.readLines(inputStream, StandardCharsets.UTF_8);

// 读取到字符串
String content = IOUtils.toString(inputStream, "UTF-8");

// 写入全部
IOUtils.write("content", outputStream, "UTF-8");
IOUtils.writeLines(lines, System.lineSeparator(), outputStream, "UTF-8");

3. 关闭流(更安全)

// 静默关闭(不抛异常)
IOUtils.closeQuietly(inputStream);        // 已过时,用try-with-resources
IOUtils.closeQuietly(outputStream);

// 批量关闭
IOUtils.closeQuietly(input, output, reader, writer);

// 新方式:closeQuietly已被标记为过时
// 应该使用try-with-resources

四、FileUtils(文件操作)

1. 文件复制

// 复制文件
File srcFile = new File("source.txt");
File destFile = new File("target.txt");

FileUtils.copyFile(srcFile, destFile);

// 复制目录
File srcDir = new File("/source");
File destDir = new File("/backup");
FileUtils.copyDirectory(srcDir, destDir);

// 带过滤器复制
FileFilter filter = file -> file.getName().endsWith(".java");
FileUtils.copyDirectory(srcDir, destDir, filter);

// 复制URL到文件
URL url = new URL("http://example.com/file.txt");
FileUtils.copyURLToFile(url, destFile);

2. 文件读写

// 读取文件内容
String content = FileUtils.readFileToString(file, "UTF-8");
List<String> lines = FileUtils.readLines(file, "UTF-8");
byte[] bytes = FileUtils.readFileToByteArray(file);

// 写入文件
FileUtils.writeStringToFile(file, "content", "UTF-8");
FileUtils.writeLines(file, lines);  // 写入集合
FileUtils.writeByteArrayToFile(file, bytes);

// 追加内容
FileUtils.writeStringToFile(file, "追加内容", "UTF-8", true);

3. 文件和目录操作

// 创建目录(包括父目录)
FileUtils.forceMkdir(dir);

// 强制删除(包括只读文件)
FileUtils.forceDelete(file);
FileUtils.deleteDirectory(dir);  // 删除目录及内容

// 清理目录(删除内容但不删除目录)
FileUtils.cleanDirectory(dir);

// 移动/重命名
FileUtils.moveFile(srcFile, destFile);
FileUtils.moveDirectory(srcDir, destDir);
FileUtils.moveFileToDirectory(srcFile, destDir, true);

// 获取目录大小
long size = FileUtils.sizeOfDirectory(dir);  // 字节数
String sizeDesc = FileUtils.byteCountToDisplaySize(size);  // "1.5 MB"

五、FilenameUtils(文件名处理)

1. 路径操作

// 规范化路径
String normalized = FilenameUtils.normalize("/a/b/../c/./d.txt");
// 结果: "/a/c/d.txt"

// 获取各部分
String baseName = FilenameUtils.getBaseName("/path/to/file.txt");    // "file"
String extension = FilenameUtils.getExtension("/path/to/file.txt");  // "txt"
String name = FilenameUtils.getName("/path/to/file.txt");           // "file.txt"
String path = FilenameUtils.getPath("/path/to/file.txt");           // "/path/to/"

// 连接路径
String fullPath = FilenameUtils.concat("/home/user", "docs/file.txt");
// 结果: "/home/user/docs/file.txt"(自动处理分隔符)

2. 文件名检查

// 扩展名检查
boolean isTxt = FilenameUtils.isExtension("file.txt", "txt");
boolean isImage = FilenameUtils.isExtension("photo.jpg", 
    new String[]{"jpg", "png", "gif"});

// 系统适配
String separatorsToUnix = FilenameUtils.separatorsToUnix("C:\\Windows\\file.txt");
// 结果: "C:/Windows/file.txt"

String separatorsToWindows = FilenameUtils.separatorsToWindows("/usr/local/file");
// 结果: "\\usr\\local\\file"

六、实际应用示例

1. 文件上传处理

public class FileUploadService {
    
    public void saveUploadedFile(InputStream uploadedStream, 
                                 String filename, 
                                 String uploadDir) throws IOException {
        
        File uploadDirectory = new File(uploadDir);
        
        // 确保目录存在
        FileUtils.forceMkdir(uploadDirectory);
        
        // 安全文件名处理
        String safeFilename = FilenameUtils.getName(filename);  // 防止路径遍历攻击
        File targetFile = new File(uploadDirectory, safeFilename);
        
        // 保存文件
        try (OutputStream out = new FileOutputStream(targetFile)) {
            IOUtils.copy(uploadedStream, out);
        }
        
        // 日志记录
        long fileSize = FileUtils.sizeOf(targetFile);
        log.info("文件已保存: {}, 大小: {}", targetFile.getAbsolutePath(),
                FileUtils.byteCountToDisplaySize(fileSize));
    }
}

2. 配置文件管理

public class ConfigManager {
    
    public Properties loadConfig(String configPath) throws IOException {
        File configFile = new File(configPath);
        
        if (!configFile.exists()) {
            // 创建默认配置
            Properties defaults = createDefaultConfig();
            saveConfig(configFile, defaults);
            return defaults;
        }
        
        // 读取配置文件
        String content = FileUtils.readFileToString(configFile, "UTF-8");
        
        Properties props = new Properties();
        try (StringReader reader = new StringReader(content)) {
            props.load(reader);
        }
        
        return props;
    }
    
    public void saveConfig(File configFile, Properties props) throws IOException {
        StringWriter writer = new StringWriter();
        props.store(writer, "Application Configuration");
        
        FileUtils.writeStringToFile(configFile, writer.toString(), "UTF-8");
    }
}

3. 日志文件轮转

public class LogRotator {
    
    public void rotateLogs(File logDir, int maxBackups) throws IOException {
        // 获取所有日志文件
        File[] logFiles = logDir.listFiles((dir, name) -> 
            name.startsWith("app.log"));
        
        if (logFiles == null) return;
        
        // 按修改时间排序
        Arrays.sort(logFiles, Comparator.comparingLong(File::lastModified).reversed());
        
        // 保留最新maxBackups个文件
        for (int i = maxBackups; i < logFiles.length; i++) {
            FileUtils.forceDelete(logFiles[i]);
        }
        
        // 重命名现有文件
        for (int i = Math.min(maxBackups - 1, logFiles.length - 1); i > 0; i--) {
            File oldFile = logFiles[i];
            File newFile = new File(logDir, 
                FilenameUtils.getBaseName(oldFile.getName()) + "." + (i + 1) + 
                "." + FilenameUtils.getExtension(oldFile.getName()));
            
            FileUtils.moveFile(oldFile, newFile);
        }
        
        // 清空当前日志
        File currentLog = new File(logDir, "app.log");
        if (currentLog.exists()) {
            FileUtils.write(currentLog, "", "UTF-8");
        }
    }
}

七、与其他框架集成

1. Spring Boot中使用

@Service
public class DocumentService {
    
    @Value("${app.documents.directory}")
    private String docsDir;
    
    public void processDocuments() throws IOException {
        File directory = new File(docsDir);
        
        // 确保目录存在
        FileUtils.forceMkdir(directory);
        
        // 查找所有PDF文件
        File[] pdfFiles = directory.listFiles((dir, name) -> 
            FilenameUtils.isExtension(name, "pdf"));
        
        if (pdfFiles != null) {
            for (File pdfFile : pdfFiles) {
                processPdfFile(pdfFile);
            }
        }
    }
    
    private void processPdfFile(File pdfFile) throws IOException {
        // 备份原文件
        File backupDir = new File(docsDir, "backup");
        FileUtils.forceMkdir(backupDir);
        
        String timestamp = LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
        String backupName = FilenameUtils.getBaseName(pdfFile.getName()) + 
                           "_" + timestamp + ".pdf";
        
        File backupFile = new File(backupDir, backupName);
        FileUtils.copyFile(pdfFile, backupFile);
        
        // 处理文件...
    }
}

2. 与Apache Commons Compress结合

public class ArchiveService {
    
    public void extractZip(File zipFile, File destDir) throws IOException {
        try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
                new BufferedInputStream(new FileInputStream(zipFile)))) {
            
            ArchiveEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                File entryFile = new File(destDir, entry.getName());
                
                if (entry.isDirectory()) {
                    FileUtils.forceMkdir(entryFile);
                } else {
                    FileUtils.forceMkdir(entryFile.getParentFile());
                    
                    try (OutputStream os = new BufferedOutputStream(
                            new FileOutputStream(entryFile))) {
                        IOUtils.copy(zis, os);
                    }
                }
            }
        }
    }
}

八、性能优化技巧

1. 使用合适的缓冲区大小

public class OptimizedFileProcessor {
    
    // 默认缓冲区大小(可配置)
    private static final int BUFFER_SIZE = 8192;  // 8KB
    
    public void processLargeFile(File input, File output) throws IOException {
        try (InputStream in = new BufferedInputStream(
                new FileInputStream(input), BUFFER_SIZE);
             OutputStream out = new BufferedOutputStream(
                new FileOutputStream(output), BUFFER_SIZE)) {
            
            // 使用自定义缓冲区大小
            IOUtils.copy(in, out, BUFFER_SIZE);
        }
    }
    
    public String readLargeFile(File file) throws IOException {
        // 对于大文件,使用StringBuilder避免多次扩容
        try (InputStream in = new FileInputStream(file);
             Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
            
            StringBuilder sb = new StringBuilder((int) file.length());
            char[] buffer = new char[BUFFER_SIZE];
            int charsRead;
            
            while ((charsRead = reader.read(buffer)) != -1) {
                sb.append(buffer, 0, charsRead);
            }
            
            return sb.toString();
        }
    }
}

2. 并行处理多个文件

public class ParallelFileProcessor {
    
    public void processFilesInParallel(List<File> files, File outputDir) {
        files.parallelStream().forEach(file -> {
            try {
                processSingleFile(file, outputDir);
            } catch (IOException e) {
                log.error("处理文件失败: {}", file.getName(), e);
            }
        });
    }
    
    private void processSingleFile(File inputFile, File outputDir) throws IOException {
        String outputFilename = FilenameUtils.getBaseName(inputFile.getName()) + 
                               "_processed.txt";
        File outputFile = new File(outputDir, outputFilename);
        
        // 读取内容
        String content = FileUtils.readFileToString(inputFile, "UTF-8");
        
        // 处理内容
        String processed = content.toUpperCase();
        
        // 写入文件
        FileUtils.writeStringToFile(outputFile, processed, "UTF-8");
        
        log.debug("已处理: {}", inputFile.getName());
    }
}

九、常见陷阱和解决方案

1. 内存溢出问题

// ❌ 错误:大文件一次性读入内存
byte[] hugeFile = IOUtils.toByteArray(new FileInputStream("10GB文件.bin"));

// ✅ 正确:流式处理大文件
try (InputStream in = new FileInputStream("10GB文件.bin");
     OutputStream out = new FileOutputStream("output.bin")) {
    
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        // 处理每个块
        processChunk(buffer, bytesRead);
        out.write(buffer, 0, bytesRead);
    }
}

// ✅ 正确:使用Apache Commons IO的copyLarge处理大文件
try (InputStream in = new FileInputStream("largefile.bin");
     OutputStream out = new FileOutputStream("output.bin")) {
    
    IOUtils.copyLarge(in, out);  // 内部使用更大的缓冲区
}

2. 路径遍历安全

// ❌ 不安全:直接使用用户输入的文件名
String userInput = "../../etc/passwd";  // 恶意输入
File file = new File(baseDir, userInput);  // 可能访问系统文件

// ✅ 安全:使用FilenameUtils过滤
String safeName = FilenameUtils.getName(userInput);  // 只取文件名部分
File safeFile = new File(baseDir, safeName);  // 安全

// ✅ 额外检查
if (!safeFile.getCanonicalPath().startsWith(baseDir.getCanonicalPath())) {
    throw new SecurityException("路径遍历攻击检测");
}

3. 文件锁和并发访问

public class ConcurrentFileAccess {
    
    private final Lock fileLock = new ReentrantLock();
    
    public void appendToLogFile(String message) throws IOException {
        fileLock.lock();
        try {
            File logFile = new File("app.log");
            
            // 使用FileUtils的追加模式
            FileUtils.writeStringToFile(logFile, 
                LocalDateTime.now() + ": " + message + "\n", 
                "UTF-8", true);  // true表示追加
        } finally {
            fileLock.unlock();
        }
    }
    
    public synchronized void safeFileOperation(File file) throws IOException {
        // 使用synchronized确保同一时间只有一个线程操作文件
        String content = FileUtils.readFileToString(file, "UTF-8");
        // 处理内容
        FileUtils.writeStringToFile(file, content, "UTF-8");
    }
}

十、最佳实践总结

1. 导入和依赖

<!-- Maven依赖 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>  <!-- 使用最新版本 -->
</dependency>

2. 编码规范

// 1. 总是指定字符编码
String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);

// 2. 使用try-with-resources管理流
try (InputStream in = new FileInputStream(file)) {
    byte[] data = IOUtils.toByteArray(in);
}

// 3. 处理可能为null的情况
FileUtils.deleteQuietly(file);  // 如果文件不存在也不抛异常

// 4. 使用常量定义缓冲区大小
private static final int BUFFER_SIZE = 8192;
IOUtils.copy(input, output, BUFFER_SIZE);

3. 错误处理

public void safeFileOperation(File file) {
    try {
        // 使用Commons IO的方法
        if (FileUtils.directoryContains(parentDir, file)) {
            String content = FileUtils.readFileToString(file, "UTF-8");
            // 处理内容
        }
    } catch (IOException e) {
        // 转换为业务异常
        throw new BusinessException("文件操作失败: " + file.getName(), e);
    } catch (IllegalArgumentException e) {
        // 处理无效参数
        log.warn("无效文件参数: {}", file.getPath());
        throw new ValidationException("文件路径无效", e);
    }
}

十一、替代方案对比

场景Commons IOJava NIO.2传统Java IO
文件复制FileUtils.copyFile()Files.copy()需要手写循环
读取所有行FileUtils.readLines()Files.readAllLines()需要BufferReader
目录操作FileUtils.cleanDirectory()需要手写递归复杂
文件名处理FilenameUtils.normalize()Path.normalize()手动处理
流操作IOUtils.copy()Files.copy()手写缓冲区循环

推荐使用:

  • 简单操作:直接用 Files 类(Java 7+)
  • 复杂场景:使用 Commons IO 的工具方法
  • 极高性能:使用 NIO 的 FileChannel.transferTo()

总结要点

核心价值:

  1. 减少样板代码:一行代码完成常见IO操作
  2. 提高安全性:内置路径遍历防护
  3. 统一错误处理:一致的异常处理方式
  4. 跨平台兼容:自动处理路径分隔符

必学方法:

// 1. 文件读写
FileUtils.readFileToString()
FileUtils.writeStringToFile()

// 2. 流操作
IOUtils.copy()
IOUtils.toByteArray()
IOUtils.toString()

// 3. 文件操作
FileUtils.copyFile()
FileUtils.deleteDirectory()
FileUtils.forceMkdir()

// 4. 文件名处理
FilenameUtils.normalize()
FilenameUtils.getExtension()
FilenameUtils.concat()

记住:Commons IO 不是要替代 Java IO,而是要让它更好用!