java实现文件分割(附带源码)

一、项目背景详细介绍

1.1 文件分割的概念与应用场景

随着大数据、云存储、网络传输等技术的飞速发展,文件体积越来越大。例如,高清视频文件往往超过数十GB,日志文件在大规模系统中每天也会产生数百GB甚至TB的数据,而在一些科研场景中,如基因测序数据、卫星遥感影像数据,更是达到PB级别。这些巨量文件在以下场景中需要被分割(Split)或分片(Chunking),以便更高效地存储、传输和处理:

  1. 网络传输与断点续传

    • 当文件体积过大时,一次性传输往往耗时长、易因网络抖动或断连导致失败。将文件切分成若干较小的块后,可以对每个块独立传输,一旦某个块传输失败,只需重传该块,而不必重传整个文件。这对于HTTP/FTP下载、断点续传技术尤为重要。

    • 在点对点(P2P)网络或分布式文件存储系统(如HDFS、FastDFS)中,将大文件切成均等的小块,可以并行传输与存储,极大提高吞吐量与可靠性。

  2. 存储与备份

    • 在刻录光盘(如DVD、CD)时,单盘可存储空间有限。如果想备份大文件或大文件夹,需要先切分为与光盘容量相符的小块,再逐个刻录。

    • 在移动存储介质(如U盘、移动硬盘)上进行数据备份时,有时单个文件过大,移动设备或文件系统对单个文件大小有限制(如FAT32文件系统单文件不能超过4GB)。此时需要先将大文件切分,再拷贝。

  3. 并行处理与分布式计算

    • 在大数据计算框架(如Hadoop MapReduce、Spark)中,输入数据通常分布式存储为多个块(Split),由不同节点并行计算。将大文件切分成块后,不同计算节点可以同时对不同块进行处理,实现高效并行计算。

    • 在图像渲染、视频转码等多媒体处理场景,也会将大文件切分成时间片或帧组,分发到多个线程/节点并行计算,加速处理过程。

  4. 日志分析与流处理

    • 当海量日志文件积累时,为了方便按时间段或事件类型进行分析,常会将日志文件按日期、大小或行数分割。例如,每天生成一个日志文件,每个文件大小不超过100MB,之后再按业务需求进行归档或分析。

    • 在流式处理场景,如Kafka消费到的日志或消息文件,也可以先切分成较小的块作为离线处理单元。

  5. 资源受限环境(嵌入式、移动设备)

    • 在内存与存储资源受限的嵌入式设备或移动端,往往无法一次性加载大文件,此时需要通过分块方式逐块处理,避免一次性占用大量内存,从而降低 OOM 风险,提升应用稳定性。

  6. 数据安全与加密

    • 在安全传输场景中,为了增强大文件的完整性校验与防篡改,可以将文件切块后对每块单独进行哈希校验,或者对每块单独加密,再进行合并或传输。若某块数据被篡改,只需重传该块即可,校验更灵活。

综上所述,无论是网络传输、存储备份、并行计算,还是日志分析、资源受限环境,都对文件分割提出了强烈需求。虽然在 Linux/Unix 下有 split 工具、在 Windows 下有第三方软件可以完成文件分割任务,但在某些定制化系统或跨平台需求下,需要开发者自己实现一套灵活、高效、可拓展的文件分割工具,特别是基于 Java 语言的场景,可以方便地集成到 Java 应用中或作为独立工具执行。

1.2 项目目标与需求分析

本项目旨在编写一个通用的 Java 文件分割(File Splitter)与合并(File Merger)工具,满足以下核心需求:

  1. 支持多种分割策略

    • 按字节大小分割:按用户输入的块大小(例如 10MB、100MB)将原文件分割成多个等大小(最后一块可小于设定大小)的文件块。

    • 按行数分割(针对文本文件):按用户指定的行数(例如每 1000 行分割成一个块)进行切分。适用于日志文件或 CSV 数据文件。

    • 按块数分割:将文件均等分成指定数量的块,每块大小自动计算(余数放最后一块)。

    • 按自定义字节位置分割:用户可以提供一系列偏移量(offset),在这些偏移位置切分。适用于更复杂的分割场景。

  2. 文件合并功能

    • 支持将分割后的多个块按照文件名中的序号或用户指定顺序,依次读出并合并为原始大文件。

    • 合并过程中要保持字节顺序一致,支持大文件合并,可指定输出路径和文件名。

  3. 高效读取与写入

    • 采用缓存缓冲区(Buffered I/O)和随机访问(RandomAccessFile)机制,避免一次性加载过大文件到内存。

    • 在分割和合并时,采用合理大小的字节缓冲区(如 4KB、8KB、64KB)逐块读取和写入,提高磁盘 I/O 性能。

    • 多线程扩展:在分割时,可以将分割任务分为多个线程并行执行(例如按块区间并行读取写入),提高速度;在合并时,也可并行读取多个文件块到内存缓冲区,再单线程按顺序写入到目标文件,从而提高整体性能。作为可选扩展,此版本初期以单线程实现为主,保证逻辑简单易懂。

  4. 命令行界面或 GUI

    • 提供一个命令行界面(CLI),用户可以通过命令行参数指定分割或合并操作。

    • 命令行模式参数示例:

java -jar FileSplitter.jar split -input /path/to/largefile.zip -output /path/to/chunks/ -mode size -size 100MB
java -jar FileSplitter.jar split -input /path/to/log.txt -output /path/to/chunks/ -mode lines -lines 1000
java -jar FileSplitter.jar merge -input /path/to/chunks/ -output /path/to/reconstructed.zip
    • (可选)后期可使用 Swing 或 JavaFX 开发简单 GUI 界面,提供按钮、文本框让用户可视化地选择文件并设置参数,点击后执行分割或合并。

  • 异常处理与健壮性

    • 针对文件读写、参数不合法、存储空间不足等常见问题,设计严谨的异常捕获与友好提示,避免程序崩溃。

    • 对分割前的输入文件进行校验,如文件是否存在、是否可读;对输出目录进行校验,如是否存在或无法创建;可选对已存在的分割文件块进行覆盖或提示。

    • 对合并操作进行校验,如检查所有要合并的块是否都存在、是否按序排列、文件块大小是否一致(除最后一块)。

  • 扩展性与可维护性

    • 采用面向对象设计,将分割逻辑与合并逻辑拆分为独立模块,如 SplitterMerger 类,各自提供接口方法,方便后续维护与功能扩展。

    • 对不同分割策略实现不同的策略类(如 SizeBasedSplitterLineBasedSplitterBlockCountSplitter),并通过工厂模式动态创建对应策略,对外统一接口,满足“开闭原则”。

    • 将 I/O 操作封装到工具类(如 FileUtil),提供通用的文件读写辅助方法,减少重复代码。

    • 对命令行参数解析使用第三方库(如 Apache Commons CLI)或自行实现简易解析器,便于后期添加更多参数选项。

    • 代码注释详细,按照包结构划分合理,方便读者学习与团队协作。

  • 使用示例与文档

    • 提供若干示例,如将一个 500MB 的视频文件按照 100MB 分割;将一个 10GB 的日志文件按照每 1GB 分割;将若干块合并回原始文件;将一个包含 5000 行的 CSV 文件按照每 1000 行分割;将一个文件分割为 4 块。

    • 为每个示例给出命令行示例及执行结果截图或日志输出片段,帮助用户直观理解工具使用。

    • 编写详细的项目说明文档(README),包括功能介绍、使用方法、参数说明、注意事项以及常见问题解答,让用户快速上手。

  • 项目目录与包结构
    为了让代码具有良好的可维护性与可扩展性,按层次化原则将项目划分为以下主要包或文件夹:

src/
 ├── core/               # 核心逻辑包
 │     ├── splitter/     # 分割策略相关类
 │     │     ├── Splitter.java           # 分割接口
 │     │     ├── SizeBasedSplitter.java  # 按字节大小分割
 │     │     ├── LineBasedSplitter.java  # 按行数分割
 │     │     ├── BlockCountSplitter.java # 按块数分割
 │     │     └── OffsetBasedSplitter.java# 按自定义偏移分割
 │     ├── merger/       # 合并相关类
 │     │     ├── Merger.java              # 合并接口
 │     │     └── SimpleMerger.java        # 依赖文件名顺序合并
 │     ├── util/         # 工具类
 │     │     ├── FileUtil.java            # 文件读写辅助
 │     │     └── PathUtil.java            # 路径与文件名解析辅助
 │     └── app/          # 应用入口及参数解析
 │           ├── CommandLineParser.java   # 命令行参数解析
 │           └── FileSplitterApp.java     # 应用主类,执行分割或合并
 └── resources/          # 资源目录(如日志配置、示例文件等)
    • core.splitter 包负责定义分割接口与各种分割策略的实现。后续可根据需求添加更多策略(如按模式分割、按哈希值分割等)。

    • core.merger 包负责定义合并接口与简单合并策略。后续可添加校验合并、验签合并等功能。

    • core.util 包存放文件读写、路径解析等通用工具类,供分割和合并模块使用。

    • core.app 包负责解析用户通过命令行传入的参数,判断执行分割还是合并操作,动态调度对应的分割或合并类进行执行。

1.3 核心技术点与实现思路

1.3.1 读取与写入大文件的基本原理

在 Java 中,对大文件进行分割或合并时,需要注意以下几点,以保证性能与稳定性:

  1. 避免一次性将大文件读入内存

    • 对于几百 MB、几 GB 甚至更大的文件,直接使用 byte[] buffer = new byte[(int)file.length()] 进行读取会导致内存耗尽(OutOfMemoryError)。

    • 正确做法是采用分块读取方式,如使用 BufferedInputStreamBufferedOutputStreamRandomAccessFile 等,配合一个固定大小的 byte 数组(例如 4KB、8KB、16KB),循环读取文件流,再将数据写入目标流。这样整个文件在磁盘 I/O 过程中占用的内存仅是缓冲区大小。

  2. RandomAccessFile 的应用

    • 当按字节位置分割或合并需要对文件进行随机访问时,可以使用 RandomAccessFile,它提供了读写指针 seek(long pos) 方法,可定位到文件指定偏移位置读取或写入。

    • 在按字节大小切分时,可以先通过 RandomAccessFile.length() 获取文件总大小,再按分割点 startPos = chunkIndex * chunkSizeendPos = min(startPos + chunkSize, totalSize) 定位到分割起始位置,读取 chunkSize 大小的数据写入到分割文件中。

  3. 文本文件按行分割

    • 当按行数进行分割时,需要按文本行边界来切分,即不能在半行处切断。可使用 BufferedReader.readLine() 逐行读取,计数当达到指定行数时,将当前缓冲区中的内容写入一个新文件,重置行计数,继续读取下一块。

    • 读取时使用 BufferedReader 比直接使用字符流更高效,并避免因为编码问题导致的读取不完整。写入时可使用 BufferedWriter,在写入时保持统一的行结束符(\n\r\n),注意不同系统换行符差异。

  4. UTF-8/字符编码注意事项

    • 当按字节大小分割包含多字节编码(如 UTF-8)的文本文件时,若分割点落在字符中间,可能导致最后一个字符出现半截情况,写入分割文件时可能出现乱码或无法解析的字符。

    • 解决办法一:只在字符边界处分割,需先找到分块末尾最近的行边界或字符边界(低效)。

    • 解决办法二:针对文本文件推荐使用按行分割,而非按字节切分,以保证字符完整性。

    • 如果业务场景对文本的字符完整性要求不高,可直接按字节分割,然后在合并时完整恢复原文件,整体转换与解析按整个文件读取即可。

  5. 合并多个文件块

    • 将多个文件块按顺序依次读取并写入到目标文件即可。合并时需保证写入顺序与原分割时的顺序一致,可通过文件名中的序号或特定命名规则(如 filename.part1filename.part2)来保证正确顺序。

    • 合并时依旧采用 BufferedInputStream + BufferedOutputStream 分块读写,避免一次性将所有块加载到内存。

    • 合并过程中可以检查每个块的字节数是否与预期一致,最后合并的总字节数是否与原始文件大小相同,用于校验合并正确性。

  6. 异常处理与资源释放

    • 在进行文件 I/O 时,要注意使用 try-with-resources 或在 finally 块中关闭流,避免出现资源泄露导致文件句柄耗尽。

    • 针对常见异常如 FileNotFoundExceptionIOException 进行捕获,并给出友好提示,如“分割失败:找不到输入文件”、“合并失败:磁盘空间不足”等。

    • 当分割过程中某个块写入失败,应及时关闭已打开的流,并可选删除已生成的部分块文件,以保证磁盘不被无效文件占满。

  7. 性能优化与多线程扩展

    • 默认采用单线程顺序处理方式,逻辑简单易懂;如果要进一步提升处理速度,可通过多线程并行分割或并行合并来提高磁盘 I/O 并行度。例如:

      • 将大文件按块区间划分给多个线程,各自使用 RandomAccessFile 定位到不同起始偏移并读取对应数据写入独立分块文件。

      • 合并时,可先将所有块文件并行读取到内存缓冲,再按顺序写入目标文件(需注意内存占用)。

    • 多线程版本需要在写入时避免竞争写同一个输出文件的位置,可通过锁或同步块控制写入顺序。

  8. 命令行参数解析

    • 可使用 Apache Commons CLI(commons-cli)库来解析命令行参数,定义选项(Options)及其短参数、长参数、是否必选、是否带值等属性。例如定义 -mode-input-output-size-lines-chunks-offsets 等选项。

    • 或者手动解析 String[] args,通过循环检查数组中匹配的标志位,并读取下一个元素作为参数值;对于缺少必选参数或参数格式不正确的情况,打印帮助信息并退出。

  9. 项目扩展性设计

    • 策略模式(Strategy Pattern):定义 Splitter 接口,将不同分割策略(按大小、按行、按块数、按偏移)实现为不同类,实现接口的统一方法 split(File inputFile, File outputDir, Map<String, Object> params);在运行时根据命令行参数决定使用哪个具体实现。这样后续若需增加新的分割策略,只需新增实现类,修改工厂方法即可,不影响已有代码。

    • 工厂模式(Factory Pattern):设计一个 SplitterFactory,给定 mode(如 sizelineschunksoffsets),返回相应的 Splitter 实例。

    • 统一返回结果与日志:所有 SplitterMerger 方法在执行后应返回统一的结果对象(如 ResultInfo)或抛出异常,由上层捕获后打印日志或进度信息。

    • 配置与常量管理:将默认缓冲区大小、日志格式、扩展名后缀(如 .part.chunk)等放入常量类 Constants 或配置文件(如 config.properties),方便后期调整。

1.4 伪代码与模块设计

在正式进入代码之前,下面给出项目的伪代码流程和模块划分,帮助理清各个组件的职责与调用关系。

1.4.1 命令行入口(FileSplitterApp)
主函数(args):
    // 解析命令行参数
    options = CommandLineParser.parse(args)
    if options.mode == "split":
        // 分割操作
        inputFilePath = options.get("input")
        outputDirPath = options.get("output")
        splitMode = options.get("splitMode")  // "size" / "lines" / "chunks" / "offsets"
        params = 收集与 splitMode 对应的参数,如 sizeValue / linesValue / chunksCount / offsetsList
        splitter = SplitterFactory.getSplitter(splitMode)
        try:
            splitter.split(inputFile, outputDir, params)
            print("分割成功")
        catch 异常 e:
            print("分割失败:" + e.getMessage())
    else if options.mode == "merge":
        // 合并操作
        inputDirPath = options.get("input")
        outputFilePath = options.get("output")
        merger = new SimpleMerger()  // 默认按文件名顺序合并
        try:
            merger.merge(inputDir, outputFile)
            print("合并成功")
        catch 异常 e:
            print("合并失败:" + e.getMessage())
    else:
        print(Help 信息)

1.5 项目运行环境与技术栈

  • 开发语言:Java 8 及以上。

  • 构建工具:推荐使用 Maven 进行依赖管理与打包。示例 pom.xml 可引入 commons-cli 依赖,用于命令行解析。

  • 打包方式:生成可执行的 fat JAR(包含第三方依赖),使用 mvn package 后得到 FileSplitter.jar

  • 依赖库

    • Apache Commons CLI(commons-cli:commons-cli:1.4)用于命令行参数解析。

    • (可选)SLF4J + Logback 用于日志输出。如果仅演示可直接使用 System.out.println

  • IDE 推荐:IntelliJ IDEA、Eclipse、VS Code 等;在 IDE 中可直接运行 FileSplitterApp 类进行调试或命令行传参执行。

  • 操作系统:跨平台,支持 Windows、Linux、macOS,只要安装了 Java 运行时(JRE/JDK)即可运行。

二、完整实现代码

// ============================================================
// File: core/util/FileUtil.java
// 工具类:提供文件切块读取/写入、路径与文件名解析等通用方法
// ============================================================
package core.util;

import java.io.*;

/**
 * FileUtil 工具类
 * 提供用于文件分割与合并的常用静态方法
 */
public class FileUtil {

    // 默认缓冲区大小(8KB)
    public static final int DEFAULT_BUFFER_SIZE = 8192;

    /**
     * 读取 inputFile 的一个字节区间 [startPos, startPos + length) 并写入到 outputFile
     *
     * @param inputFile   源文件
     * @param startPos    起始偏移位置(字节)
     * @param length      要读取的字节长度
     * @param outputFile  输出文件
     * @throws IOException 如果读写异常
     */
    public static void readChunkAndWrite(File inputFile, long startPos, long length, File outputFile) throws IOException {
        // 确保输出目录存在
        File parentDir = outputFile.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            if (!parentDir.mkdirs()) {
                throw new IOException("无法创建输出目录: " + parentDir.getAbsolutePath());
            }
        }

        // 使用 RandomAccessFile 定位到指定偏移量
        try (RandomAccessFile raf = new RandomAccessFile(inputFile, "r");
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outputFile))) {

            raf.seek(startPos);
            long bytesRemaining = length;
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            while (bytesRemaining > 0) {
                int bytesToRead = (int) Math.min(buffer.length, bytesRemaining);
                int bytesRead = raf.read(buffer, 0, bytesToRead);
                if (bytesRead == -1) {
                    break; // 提前到达文件末尾
                }
                bos.write(buffer, 0, bytesRead);
                bytesRemaining -= bytesRead;
            }
            bos.flush();
        }
    }

    /**
     * 根据文件名获取文件扩展名(不包含点),如果没有扩展名则返回空字符串
     *
     * @param fileName 文件名
     * @return 扩展名,不包含点,如 "txt";如果没有扩展名返回 ""
     */
    public static String getFileExtension(String fileName) {
        if (fileName == null) {
            return "";
        }
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1) {
            return "";
        }
        return fileName.substring(lastDotIndex + 1);
    }

    /**
     * 根据文件名获取不带扩展名的文件基名
     *
     * @param fileName 文件名
     * @return 文件基名,如 "example.txt" 返回 "example"
     */
    public static String getFileBaseName(String fileName) {
        if (fileName == null) {
            return "";
        }
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex == -1) {
            return fileName;
        }
        return fileName.substring(0, lastDotIndex);
    }

    /**
     * 将用户输入的大小字符串(如 "100MB", "1GB", "500KB", "1024")解析为字节值(long)。
     * 支持后缀:B(字节)、KB、MB、GB、TB,不区分大小写。
     * 如果无后缀则默认为字节(B)。
     *
     * @param sizeStr 用户输入的大小字符串
     * @return 对应的字节数
     * @throws IllegalArgumentException 当格式不合法或数值溢出时抛出
     */
    public static long parseSizeStringToBytes(String sizeStr) {
        if (sizeStr == null || sizeStr.trim().isEmpty()) {
            throw new IllegalArgumentException("大小字符串不能为空");
        }
        String s = sizeStr.trim().toUpperCase();
        long multiplier = 1L;
        if (s.endsWith("TB")) {
            multiplier = 1024L * 1024L * 1024L * 1024L;
            s = s.substring(0, s.length() - 2).trim();
        } else if (s.endsWith("GB")) {
            multiplier = 1024L * 1024L * 1024L;
            s = s.substring(0, s.length() - 2).trim();
        } else if (s.endsWith("MB")) {
            multiplier = 1024L * 1024L;
            s = s.substring(0, s.length() - 2).trim();
        } else if (s.endsWith("KB")) {
            multiplier = 1024L;
            s = s.substring(0, s.length() - 2).trim();
        } else if (s.endsWith("B")) {
            multiplier = 1L;
            s = s.substring(0, s.length() - 1).trim();
        }
        try {
            double value = Double.parseDouble(s);
            double bytesDouble = value * multiplier;
            if (bytesDouble < 0 || bytesDouble > Long.MAX_VALUE) {
                throw new IllegalArgumentException("指定大小超出可表示范围");
            }
            return (long) bytesDouble;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("无法解析大小字符串: " + sizeStr);
        }
    }

    /**
     * 从目录 inputDir 中获取所有分割块文件,并按文件名中附加的序号排序后返回 File 数组。
     * 要求文件名符合 pattern: baseName.partN(例如 "video.mp4.part0", "video.mp4.part1")。
     * 如果不是这种后缀模式,则尝试按文件名字母序排序返回所有文件。
     *
     * @param inputDir   存放分割文件块的目录
     * @param baseName   原始文件基名(不带扩展名),用于匹配
     * @param extension  原始文件扩展名,用于匹配
     * @return 排序后的分割块文件数组
     */
    public static File[] listAndSortChunkFiles(File inputDir, String baseName, String extension) {
        if (!inputDir.exists() || !inputDir.isDirectory()) {
            return new File[0];
        }
        // 定义文件过滤器:匹配 baseName + "." + extension + ".part" + number
        final String prefix = baseName + (extension.isEmpty() ? "" : ("." + extension)) + ".part";
        File[] allFiles = inputDir.listFiles((dir, name) -> name.startsWith(prefix));
        if (allFiles == null) {
            return new File[0];
        }
        // 按文件名按数字后缀排序
        java.util.Arrays.sort(allFiles, (f1, f2) -> {
            String n1 = f1.getName().substring(prefix.length());
            String n2 = f2.getName().substring(prefix.length());
            try {
                int idx1 = Integer.parseInt(n1);
                int idx2 = Integer.parseInt(n2);
                return Integer.compare(idx1, idx2);
            } catch (NumberFormatException e) {
                return f1.getName().compareTo(f2.getName());
            }
        });
        return allFiles;
    }

    /**
     * 将一个目录下的文件按名称(字典序)合并到 outputFile 中。例如将分块文件合并成一个大文件。
     *
     * @param inputDir   存放要合并的文件块的目录
     * @param outputFile 合并后的目标文件
     * @throws IOException 如果读写异常
     */
    public static void mergeFilesInDirectory(File inputDir, File outputFile) throws IOException {
        if (!inputDir.exists() || !inputDir.isDirectory()) {
            throw new FileNotFoundException("输入目录不存在或不是目录: " + inputDir.getAbsolutePath());
        }

        // 获取目录下所有文件并按字典序排序
        File[] files = inputDir.listFiles(File::isFile);
        if (files == null || files.length == 0) {
            throw new FileNotFoundException("输入目录中没有可合并的文件: " + inputDir.getAbsolutePath());
        }
        java.util.Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

        // 确保输出目录存在
        File parentDir = outputFile.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            if (!parentDir.mkdirs()) {
                throw new IOException("无法创建合并输出目录: " + parentDir.getAbsolutePath());
            }
        }

        // 逐个读取并写入
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outputFile))) {
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            for (File f : files) {
                try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f))) {
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        bos.write(buffer, 0, bytesRead);
                    }
                }
            }
            bos.flush();
        }
    }
}


// ============================================================
// File: core/splitter/Splitter.java
// 分割策略接口:定义统一的 split 方法
// ============================================================
package core.splitter;

import java.io.File;
import java.util.Map;

/**
 * Splitter 接口定义
 * 提供统一的 split 方法,将输入文件按照指定参数切分到输出目录
 */
public interface Splitter {

    /**
     * 将 inputFile 按照 params 中的参数,切分到 outputDir 中
     *
     * @param inputFile  待分割的源文件
     * @param outputDir  分割后的文件块存放目录
     * @param params     Map 类型,包含具体分割模式所需参数:
     *                   对于 SizeBasedSplitter,需要 "chunkSize" (Long);
     *                   对于 LineBasedSplitter,需要 "linesPerChunk" (Integer);
     *                   对于 BlockCountSplitter,需要 "chunksCount" (Integer);
     *                   对于 OffsetBasedSplitter,需要 "offsets" (List<Long>);
     * @throws Exception 可能抛出 IOException 或自定义 SplitException
     */
    void split(File inputFile, File outputDir, Map<String, Object> params) throws Exception;
}


// ============================================================
// File: core/splitter/SizeBasedSplitter.java
// 按字节大小分割实现,基于 Splitter 接口
// ============================================================
package core.splitter;

import core.util.FileUtil;

import java.io.File;
import java.util.Map;

/**
 * 按字节大小进行文件分割
 * 以字节为单位将大文件切分为多个块(最后一个块可小于 chunkSize)
 */
public class SizeBasedSplitter implements Splitter {

    @Override
    public void split(File inputFile, File outputDir, Map<String, Object> params) throws Exception {
        if (inputFile == null || !inputFile.exists() || !inputFile.isFile()) {
            throw new IllegalArgumentException("输入文件不存在或不是文件: " + inputFile);
        }
        if (outputDir == null) {
            throw new IllegalArgumentException("输出目录不能为空");
        }
        // 若输出目录不存在,则创建
        if (!outputDir.exists()) {
            if (!outputDir.mkdirs()) {
                throw new IllegalArgumentException("无法创建输出目录: " + outputDir.getAbsolutePath());
            }
        }

        // 获取 chunkSize 参数
        Object obj = params.get("chunkSize");
        if (!(obj instanceof Long)) {
            throw new IllegalArgumentException("SizeBasedSplitter 需要参数 \"chunkSize\" 类型为 Long");
        }
        long chunkSize = (Long) obj;
        if (chunkSize <= 0) {
            throw new IllegalArgumentException("每块大小必须大于 0");
        }

        long totalSize = inputFile.length();
        if (chunkSize > totalSize) {
            // 如果指定每块大小大于文件总大小,则直接将整个文件复制到输出目录
            File outputFile = new File(outputDir, inputFile.getName() + ".part0");
            FileUtil.readChunkAndWrite(inputFile, 0, totalSize, outputFile);
            return;
        }

        // 计算需要的块数
        long numChunks = totalSize / chunkSize;
        if (totalSize % chunkSize != 0) {
            numChunks++;
        }

        String fileName = FileUtil.getFileBaseName(inputFile.getName());
        String extension = FileUtil.getFileExtension(inputFile.getName());

        for (int i = 0; i < numChunks; i++) {
            long startPos = i * chunkSize;
            long currentChunkSize = (i == numChunks - 1) ? (totalSize - startPos) : chunkSize;

            // 生成输出文件名: baseName.extension.part{i}
            String partFileName;
            if (extension.isEmpty()) {
                partFileName = String.format("%s.part%d", fileName, i);
            } else {
                partFileName = String.format("%s.%s.part%d", fileName, extension, i);
            }
            File partFile = new File(outputDir, partFileName);

            // 读取并写入一个块
            FileUtil.readChunkAndWrite(inputFile, startPos, currentChunkSize, partFile);
            System.out.printf("已生成分块: %s, 大小: %d 字节%n", partFile.getAbsolutePath(), currentChunkSize);
        }
    }
}


// ============================================================
// File: core/splitter/LineBasedSplitter.java
// 按行数分割实现,基于 Splitter 接口
// ============================================================
package core.splitter;

import java.io.*;
import java.util.Map;

/**
 * 按行数进行文件分割
 * 读取文本文件并按指定行数切分为多个块(保持编码和换行符一致)
 */
public class LineBasedSplitter implements Splitter {

    @Override
    public void split(File inputFile, File outputDir, Map<String, Object> params) throws Exception {
        if (inputFile == null || !inputFile.exists() || !inputFile.isFile()) {
            throw new IllegalArgumentException("输入文件不存在或不是文件: " + inputFile);
        }
        if (outputDir == null) {
            throw new IllegalArgumentException("输出目录不能为空");
        }
        // 若输出目录不存在,则创建
        if (!outputDir.exists()) {
            if (!outputDir.mkdirs()) {
                throw new IllegalArgumentException("无法创建输出目录: " + outputDir.getAbsolutePath());
            }
        }

        // 获取 linesPerChunk 参数
        Object obj = params.get("linesPerChunk");
        if (!(obj instanceof Integer)) {
            throw new IllegalArgumentException("LineBasedSplitter 需要参数 \"linesPerChunk\" 类型为 Integer");
        }
        int linesPerChunk = (Integer) obj;
        if (linesPerChunk <= 0) {
            throw new IllegalArgumentException("每块行数必须大于 0");
        }

        String fileName = inputFile.getName();
        // 获取不带扩展名的基名
        String baseName = fileName;
        String extension = "";
        int lastDot = fileName.lastIndexOf('.');
        if (lastDot != -1) {
            baseName = fileName.substring(0, lastDot);
            extension = fileName.substring(lastDot + 1);
        }

        // 读取文本并按行数切分
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile)))) {
            int chunkIndex = 0;
            int currentLineCount = 0;
            String partFileName = extension.isEmpty()
                    ? String.format("%s.part%d", baseName, chunkIndex)
                    : String.format("%s.%s.part%d", baseName, extension, chunkIndex);
            File partFile = new File(outputDir, partFileName);
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(partFile)));

            String line;
            while ((line = reader.readLine()) != null) {
                if (currentLineCount >= linesPerChunk) {
                    // 关闭当前 writer 并创建下一个块文件
                    writer.close();
                    chunkIndex++;
                    currentLineCount = 0;
                    partFileName = extension.isEmpty()
                            ? String.format("%s.part%d", baseName, chunkIndex)
                            : String.format("%s.%s.part%d", baseName, extension, chunkIndex);
                    partFile = new File(outputDir, partFileName);
                    writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(partFile)));
                }
                writer.write(line);
                writer.newLine();
                currentLineCount++;
            }
            // 关闭最后一个 writer
            writer.close();
            System.out.printf("按行数分割完成,生成 %d 个文件块,每块最多 %d 行%n", chunkIndex + 1, linesPerChunk);
        }
    }
}


// ============================================================
// File: core/splitter/BlockCountSplitter.java
// 按要分割的块数进行分割实现,基于 Splitter 接口
// ============================================================
package core.splitter;

import core.util.FileUtil;

import java.io.File;
import java.util.Map;

/**
 * 按块数进行文件分割
 * 将文件划分为指定数量的块,每块大小相同(最后一个块大小可能略大)
 */
public class BlockCountSplitter implements Splitter {

    @Override
    public void split(File inputFile, File outputDir, Map<String, Object> params) throws Exception {
        if (inputFile == null || !inputFile.exists() || !inputFile.isFile()) {
            throw new IllegalArgumentException("输入文件不存在或不是文件: " + inputFile);
        }
        if (outputDir == null) {
            throw new IllegalArgumentException("输出目录不能为空");
        }
        // 若输出目录不存在,则创建
        if (!outputDir.exists()) {
            if (!outputDir.mkdirs()) {
                throw new IllegalArgumentException("无法创建输出目录: " + outputDir.getAbsolutePath());
            }
        }

        // 获取 chunksCount 参数
        Object obj = params.get("chunksCount");
        if (!(obj instanceof Integer)) {
            throw new IllegalArgumentException("BlockCountSplitter 需要参数 \"chunksCount\" 类型为 Integer");
        }
        int chunksCount = (Integer) obj;
        if (chunksCount <= 0) {
            throw new IllegalArgumentException("分割块数必须大于 0");
        }

        long totalSize = inputFile.length();
        if (chunksCount == 1) {
            // 直接复制成一个块
            File outputFile = new File(outputDir, inputFile.getName() + ".part0");
            FileUtil.readChunkAndWrite(inputFile, 0, totalSize, outputFile);
            return;
        }

        long chunkSize = totalSize / chunksCount;
        if (chunkSize <= 0) {
            throw new IllegalArgumentException("块数过大,导致每块大小为 0");
        }

        String fileName = FileUtil.getFileBaseName(inputFile.getName());
        String extension = FileUtil.getFileExtension(inputFile.getName());

        for (int i = 0; i < chunksCount; i++) {
            long startPos = i * chunkSize;
            long currentChunkSize;
            if (i == chunksCount - 1) {
                // 最后一个块包含余数
                currentChunkSize = totalSize - startPos;
            } else {
                currentChunkSize = chunkSize;
            }

            String partFileName = extension.isEmpty()
                    ? String.format("%s.part%d", fileName, i)
                    : String.format("%s.%s.part%d", fileName, extension, i);
            File partFile = new File(outputDir, partFileName);

            FileUtil.readChunkAndWrite(inputFile, startPos, currentChunkSize, partFile);
            System.out.printf("已生成分块: %s, 大小: %d 字节%n", partFile.getAbsolutePath(), currentChunkSize);
        }
    }
}


// ============================================================
// File: core/splitter/OffsetBasedSplitter.java
// 按自定义偏移分割实现,基于 Splitter 接口
// ============================================================
package core.splitter;

import core.util.FileUtil;

import java.io.File;
import java.util.List;
import java.util.Map;

/**
 * 按自定义偏移位置进行文件分割
 * 用户提供一个有序的偏移量列表,在这些偏移位置切分文件
 */
public class OffsetBasedSplitter implements Splitter {

    @Override
    @SuppressWarnings("unchecked")
    public void split(File inputFile, File outputDir, Map<String, Object> params) throws Exception {
        if (inputFile == null || !inputFile.exists() || !inputFile.isFile()) {
            throw new IllegalArgumentException("输入文件不存在或不是文件: " + inputFile);
        }
        if (outputDir == null) {
            throw new IllegalArgumentException("输出目录不能为空");
        }
        // 若输出目录不存在,则创建
        if (!outputDir.exists()) {
            if (!outputDir.mkdirs()) {
                throw new IllegalArgumentException("无法创建输出目录: " + outputDir.getAbsolutePath());
            }
        }

        // 获取 offsets 列表
        Object obj = params.get("offsets");
        if (!(obj instanceof List)) {
            throw new IllegalArgumentException("OffsetBasedSplitter 需要参数 \"offsets\" 类型为 List<Long>");
        }
        List<Long> offsets = (List<Long>) obj;
        if (offsets.isEmpty()) {
            throw new IllegalArgumentException("偏移量列表不能为空");
        }

        // 对偏移列表排序并验证
        java.util.Collections.sort(offsets);
        long fileSize = inputFile.length();
        for (Long off : offsets) {
            if (off < 0 || off > fileSize) {
                throw new IllegalArgumentException("偏移值超出文件范围: " + off);
            }
        }
        // 在末尾添加文件总大小,以方便最后一个块切分
        if (offsets.get(offsets.size() - 1) != fileSize) {
            offsets.add(fileSize);
        }

        String fileName = FileUtil.getFileBaseName(inputFile.getName());
        String extension = FileUtil.getFileExtension(inputFile.getName());

        long prevOffset = 0;
        int chunkIndex = 0;
        for (Long nextOffset : offsets) {
            long currentChunkSize = nextOffset - prevOffset;
            if (currentChunkSize <= 0) {
                prevOffset = nextOffset;
                chunkIndex++;
                continue; // 跳过无效或重复偏移
            }

            String partFileName = extension.isEmpty()
                    ? String.format("%s.part%d", fileName, chunkIndex)
                    : String.format("%s.%s.part%d", fileName, extension, chunkIndex);
            File partFile = new File(outputDir, partFileName);

            FileUtil.readChunkAndWrite(inputFile, prevOffset, currentChunkSize, partFile);
            System.out.printf("已生成分块: %s, 起始: %d, 大小: %d 字节%n", partFile.getAbsolutePath(), prevOffset, currentChunkSize);

            prevOffset = nextOffset;
            chunkIndex++;
        }
    }
}


// ============================================================
// File: core/merger/Merger.java
// 合并接口:定义统一的 merge 方法
// ============================================================
package core.merger;

import java.io.File;

/**
 * Merger 接口定义
 * 提供统一的 merge 方法,将输入目录下的分块文件合并为一个大文件
 */
public interface Merger {

    /**
     * 将 inputDir 下的分割块文件按照一定顺序合并到 outputFile
     *
     * @param inputDir  存放分割块文件的目录
     * @param outputFile 合并后的目标文件
     * @throws Exception 如果读写异常或合并异常
     */
    void merge(File inputDir, File outputFile) throws Exception;
}


// ============================================================
// File: core/merger/SimpleMerger.java
// 简单按文件名顺序合并实现,基于 Merger 接口
// ============================================================
package core.merger;

import core.util.FileUtil;

import java.io.File;

/**
 * SimpleMerger 实现:按文件名字典序读取 inputDir 中的文件,并合并到 outputFile
 */
public class SimpleMerger implements Merger {

    @Override
    public void merge(File inputDir, File outputFile) throws Exception {
        if (inputDir == null || !inputDir.exists() || !inputDir.isDirectory()) {
            throw new IllegalArgumentException("输入目录不存在或不是目录: " + inputDir);
        }
        if (outputFile == null) {
            throw new IllegalArgumentException("输出文件不能为空");
        }
        // 获取目录下所有文件并按名称排序
        File[] files = inputDir.listFiles(File::isFile);
        if (files == null || files.length == 0) {
            throw new IllegalArgumentException("输入目录中没有要合并的文件: " + inputDir.getAbsolutePath());
        }
        java.util.Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

        // 合并所有块到输出文件
        FileUtil.mergeFilesInDirectory(inputDir, outputFile);
        System.out.printf("已将 %d 个文件合并为 %s%n", files.length, outputFile.getAbsolutePath());
    }
}


// ============================================================
// File: core/app/CommandLineParser.java
// 命令行参数解析:使用 Apache Commons CLI (commons-cli-1.4.jar) 库
// ============================================================
package core.app;

import org.apache.commons.cli.*;

import java.util.*;

/**
 * 命令行参数解析类
 * 使用 Apache Commons CLI 解析用户传入的参数,并封装在 ParsedOptions 对象中
 */
public class CommandLineParser {

    /**
     * 定义可选参数及其描述,并解析 args
     *
     * @param args 用户输入的命令行参数数组
     * @return ParsedOptions 封装了解析后的参数
     * @throws Exception 如果缺少必需参数或格式不合法
     */
    public static ParsedOptions parse(String[] args) throws Exception {
        // 创建 Options 对象
        Options options = new Options();

        // 必选参数 -mode (split|merge)
        Option modeOpt = Option.builder("mode")
                .hasArg()
                .argName("split|merge")
                .desc("操作模式:split(分割) 或 merge(合并)")
                .required()
                .build();
        options.addOption(modeOpt);

        // 必选参数 -input <path>
        Option inputOpt = Option.builder("input")
                .hasArg()
                .argName("path")
                .desc("输入文件或目录路径,split 模式下为源文件路径,merge 模式下为分块目录路径")
                .required()
                .build();
        options.addOption(inputOpt);

        // 必选参数 -output <path>
        Option outputOpt = Option.builder("output")
                .hasArg()
                .argName("path")
                .desc("输出目录或文件路径,split 模式下为分块输出目录,merge 模式下为合并后文件路径")
                .required()
                .build();
        options.addOption(outputOpt);

        // 可选参数 -splitMode <size|lines|chunks|offsets>,split 模式下必选
        Option splitModeOpt = Option.builder("splitMode")
                .hasArg()
                .argName("size|lines|chunks|offsets")
                .desc("分割模式,仅在 split 模式下有效:size(按大小) lines(按行) chunks(按块数) offsets(按偏移)")
                .build();
        options.addOption(splitModeOpt);

        // -size <value>(如 10MB, 1024KB, 1GB)
        Option sizeOpt = Option.builder("size")
                .hasArg()
                .argName("size")
                .desc("分块大小,例如 10MB、1024KB、1GB,必须与 splitMode=size 配合使用")
                .build();
        options.addOption(sizeOpt);

        // -lines <number>
        Option linesOpt = Option.builder("lines")
                .hasArg()
                .argName("number")
                .desc("每块行数,例如 1000,必须与 splitMode=lines 配合使用")
                .build();
        options.addOption(linesOpt);

        // -chunks <number>
        Option chunksOpt = Option.builder("chunks")
                .hasArg()
                .argName("number")
                .desc("切分成块数,例如 5,必须与 splitMode=chunks 配合使用")
                .build();
        options.addOption(chunksOpt);

        // -offsets <off1,off2,...>
        Option offsetsOpt = Option.builder("offsets")
                .hasArg()
                .argName("off1,off2,...")
                .desc("切分偏移量列表,以逗号分隔(字节),例如 1048576,2097152,必须与 splitMode=offsets 配合使用")
                .build();
        options.addOption(offsetsOpt);

        // 创建解析器
        CommandLineParser parser = new CommandLineParser();
        CommandLineParser apacheParser = new DefaultParser().getClass().newInstance();

        // 解析
        CommandLine cmd;
        try {
            cmd = ((DefaultParser) apacheParser).parse(options, args);
        } catch (ParseException e) {
            throw new IllegalArgumentException("参数解析失败: " + e.getMessage(), e);
        }

        ParsedOptions parsedOptions = new ParsedOptions();
        String mode = cmd.getOptionValue("mode").trim().toLowerCase();
        if (!mode.equals("split") && !mode.equals("merge")) {
            throw new IllegalArgumentException("mode 参数值必须为 split 或 merge");
        }
        parsedOptions.setMode(mode);

        String inputPath = cmd.getOptionValue("input").trim();
        parsedOptions.setInputPath(inputPath);

        String outputPath = cmd.getOptionValue("output").trim();
        parsedOptions.setOutputPath(outputPath);

        if ("split".equals(mode)) {
            if (!cmd.hasOption("splitMode")) {
                throw new IllegalArgumentException("splitMode 为 split 模式下必选参数");
            }
            String splitMode = cmd.getOptionValue("splitMode").trim().toLowerCase();
            if (!Arrays.asList("size", "lines", "chunks", "offsets").contains(splitMode)) {
                throw new IllegalArgumentException("splitMode 参数值必须为 size, lines, chunks 或 offsets");
            }
            parsedOptions.setSplitMode(splitMode);

            // 根据 splitMode 校验相应参数
            switch (splitMode) {
                case "size":
                    if (!cmd.hasOption("size")) {
                        throw new IllegalArgumentException("splitMode=size 时必须提供 size 参数");
                    }
                    String sizeStr = cmd.getOptionValue("size").trim();
                    parsedOptions.setSizeStr(sizeStr);
                    break;
                case "lines":
                    if (!cmd.hasOption("lines")) {
                        throw new IllegalArgumentException("splitMode=lines 时必须提供 lines 参数");
                    }
                    String linesStr = cmd.getOptionValue("lines").trim();
                    try {
                        int linesPerChunk = Integer.parseInt(linesStr);
                        if (linesPerChunk <= 0) {
                            throw new NumberFormatException();
                        }
                        parsedOptions.setLinesPerChunk(linesPerChunk);
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("lines 参数必须为大于 0 的整数");
                    }
                    break;
                case "chunks":
                    if (!cmd.hasOption("chunks")) {
                        throw new IllegalArgumentException("splitMode=chunks 时必须提供 chunks 参数");
                    }
                    String chunksStr = cmd.getOptionValue("chunks").trim();
                    try {
                        int chunksCount = Integer.parseInt(chunksStr);
                        if (chunksCount <= 0) {
                            throw new NumberFormatException();
                        }
                        parsedOptions.setChunksCount(chunksCount);
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("chunks 参数必须为大于 0 的整数");
                    }
                    break;
                case "offsets":
                    if (!cmd.hasOption("offsets")) {
                        throw new IllegalArgumentException("splitMode=offsets 时必须提供 offsets 参数");
                    }
                    String offsetsStr = cmd.getOptionValue("offsets").trim();
                    // 解析逗号分隔的 long 列表
                    String[] offsetArr = offsetsStr.split(",");
                    List<Long> offsetList = new ArrayList<>();
                    try {
                        for (String offStr : offsetArr) {
                            long off = Long.parseLong(offStr.trim());
                            if (off < 0) {
                                throw new NumberFormatException();
                            }
                            offsetList.add(off);
                        }
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("offsets 参数必须为逗号分隔的正整数列表");
                    }
                    parsedOptions.setOffsets(offsetList);
                    break;
            }
        }
        return parsedOptions;
    }
}


// ============================================================
// File: core/app/ParsedOptions.java
// 参数解析结果封装类,保存解析后各参数值
// ============================================================
package core.app;

import java.util.List;

/**
 * ParsedOptions 类
 * 用于封装命令行解析后的各参数值,供 FileSplitterApp 使用
 */
public class ParsedOptions {
    // 模式: "split" 或 "merge"
    private String mode;
    // 输入路径: 如果模式为 split,则为文件路径;如果模式为 merge,则为目录路径
    private String inputPath;
    // 输出路径: 如果模式为 split,则为目录;如果模式为 merge,则为文件路径
    private String outputPath;

    // 以下仅在 split 模式下有效
    private String splitMode;         // "size" / "lines" / "chunks" / "offsets"
    private String sizeStr;           // 当 splitMode=size 时有效,如 "100MB"
    private Integer linesPerChunk;    // 当 splitMode=lines 时有效
    private Integer chunksCount;      // 当 splitMode=chunks 时有效
    private List<Long> offsets;       // 当 splitMode=offsets 时有效

    // Getters 和 Setters
    public String getMode() {
        return mode;
    }
    public void setMode(String mode) {
        this.mode = mode;
    }
    public String getInputPath() {
        return inputPath;
    }
    public void setInputPath(String inputPath) {
        this.inputPath = inputPath;
    }
    public String getOutputPath() {
        return outputPath;
    }
    public void setOutputPath(String outputPath) {
        this.outputPath = outputPath;
    }
    public String getSplitMode() {
        return splitMode;
    }
    public void setSplitMode(String splitMode) {
        this.splitMode = splitMode;
    }
    public String getSizeStr() {
        return sizeStr;
    }
    public void setSizeStr(String sizeStr) {
        this.sizeStr = sizeStr;
    }
    public Integer getLinesPerChunk() {
        return linesPerChunk;
    }
    public void setLinesPerChunk(Integer linesPerChunk) {
        this.linesPerChunk = linesPerChunk;
    }
    public Integer getChunksCount() {
        return chunksCount;
    }
    public void setChunksCount(Integer chunksCount) {
        this.chunksCount = chunksCount;
    }
    public List<Long> getOffsets() {
        return offsets;
    }
    public void setOffsets(List<Long> offsets) {
        this.offsets = offsets;
    }
}


// ============================================================
// File: core/app/SplitterFactory.java
// 工厂类:根据 splitMode 创建对应的 Splitter 实例
// ============================================================
package core.app;

import core.splitter.*;

import java.util.HashMap;
import java.util.Map;

/**
 * SplitterFactory 类
 * 根据 splitMode 动态返回相应的 Splitter 实现
 */
public class SplitterFactory {

    /**
     * 获取指定分割模式的 Splitter 实例
     *
     * @param splitMode 分割模式字符串 ("size", "lines", "chunks", "offsets")
     * @return 对应的 Splitter 实例
     * @throws IllegalArgumentException 如果 splitMode 不支持
     */
    public static Splitter getSplitter(String splitMode) {
        if (splitMode == null) {
            throw new IllegalArgumentException("splitMode 不能为空");
        }
        switch (splitMode.toLowerCase()) {
            case "size":
                return new SizeBasedSplitter();
            case "lines":
                return new LineBasedSplitter();
            case "chunks":
                return new BlockCountSplitter();
            case "offsets":
                return new OffsetBasedSplitter();
            default:
                throw new IllegalArgumentException("不支持的 splitMode: " + splitMode);
        }
    }

    /**
     * 将 ParsedOptions 中的参数转为 Map<String, Object> 形式,便于传递给 Splitter
     *
     * @param options ParsedOptions 对象
     * @return 参数 Map
     */
    public static Map<String, Object> buildParamsMap(ParsedOptions options) {
        Map<String, Object> params = new HashMap<>();
        String splitMode = options.getSplitMode();
        switch (splitMode) {
            case "size": {
                // 将 sizeStr 转为字节数
                long chunkSize = core.util.FileUtil.parseSizeStringToBytes(options.getSizeStr());
                params.put("chunkSize", chunkSize);
                break;
            }
            case "lines": {
                params.put("linesPerChunk", options.getLinesPerChunk());
                break;
            }
            case "chunks": {
                params.put("chunksCount", options.getChunksCount());
                break;
            }
            case "offsets": {
                params.put("offsets", options.getOffsets());
                break;
            }
            default:
                break;
        }
        return params;
    }
}


// ============================================================
// File: core/app/FileSplitterApp.java
// 应用主类:根据用户输入执行分割或合并操作
// ============================================================
package core.app;

import core.merger.Merger;
import core.merger.SimpleMerger;
import core.splitter.Splitter;

import java.io.File;
import java.util.Map;

/**
 * FileSplitterApp 类
 * 应用程序入口,根据命令行参数执行分割或合并功能
 */
public class FileSplitterApp {

    /**
     * 主函数
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        try {
            // 解析命令行参数
            ParsedOptions options = CommandLineParser.parse(args);
            String mode = options.getMode();

            if ("split".equals(mode)) {
                // 分割模式
                String inputPath = options.getInputPath();
                String outputPath = options.getOutputPath();
                String splitMode = options.getSplitMode();

                File inputFile = new File(inputPath);
                if (!inputFile.exists() || !inputFile.isFile()) {
                    System.err.println("输入文件不存在或不是文件: " + inputPath);
                    System.exit(1);
                }

                File outputDir = new File(outputPath);
                // outputDir 可以不存在,后续会创建

                // 构建参数 Map
                Map<String, Object> paramsMap = SplitterFactory.buildParamsMap(options);
                // 获取对应的 Splitter 实例
                Splitter splitter = SplitterFactory.getSplitter(splitMode);

                System.out.println("开始分割,模式: " + splitMode + ",文件: " + inputFile.getAbsolutePath());
                splitter.split(inputFile, outputDir, paramsMap);
                System.out.println("分割完成,输出目录: " + outputDir.getAbsolutePath());

            } else if ("merge".equals(mode)) {
                // 合并模式
                String inputPath = options.getInputPath();
                String outputPath = options.getOutputPath();

                File inputDir = new File(inputPath);
                if (!inputDir.exists() || !inputDir.isDirectory()) {
                    System.err.println("输入目录不存在或不是目录: " + inputPath);
                    System.exit(1);
                }

                File outputFile = new File(outputPath);
                // 如果输出文件所在目录不存在,则创建
                File parentDir = outputFile.getParentFile();
                if (parentDir != null && !parentDir.exists()) {
                    if (!parentDir.mkdirs()) {
                        System.err.println("无法创建输出目录: " + parentDir.getAbsolutePath());
                        System.exit(1);
                    }
                }

                // 使用简单合并实现
                Merger merger = new SimpleMerger();
                System.out.println("开始合并,目录: " + inputDir.getAbsolutePath());
                merger.merge(inputDir, outputFile);
                System.out.println("合并完成,输出文件: " + outputFile.getAbsolutePath());

            } else {
                // 不支持的模式
                System.err.println("不支持的模式: " + mode);
                System.exit(1);
            }
        } catch (IllegalArgumentException e) {
            System.err.println("参数错误: " + e.getMessage());
            System.err.println("请查看帮助信息,示例: java -jar FileSplitter.jar -mode split -input <file> -output <dir> -splitMode size -size 100MB");
            System.exit(1);
        } catch (Exception e) {
            System.err.println("执行过程中发生异常: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }
}


// ============================================================
// File: pom.xml
// Maven 构建配置,包含 commons-cli 依赖和打包插件配置
// ============================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 项目坐标 -->
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>FileSplitter</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>FileSplitter</name>
    <description>Java 实现文件分割与合并工具</description>

    <!-- 依赖管理 -->
    <dependencies>
        <!-- Apache Commons CLI: 用于命令行参数解析 -->
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.4</version>
        </dependency>
        <!-- (可选) 日志框架 SLF4J / Logback -->
        <!--
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        -->
    </dependencies>

    <!-- 构建配置 -->
    <build>
        <plugins>
            <!-- maven-compiler-plugin: 指定 Java 版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <!-- maven-assembly-plugin: 用于打包可执行 JAR,包括依赖 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <!-- 指定主类为应用主入口 -->
                            <mainClass>core.app.FileSplitterApp</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- 运行时 javafx:run 阶段生效 -->
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值