一、项目背景详细介绍
1.1 文件分割的概念与应用场景
随着大数据、云存储、网络传输等技术的飞速发展,文件体积越来越大。例如,高清视频文件往往超过数十GB,日志文件在大规模系统中每天也会产生数百GB甚至TB的数据,而在一些科研场景中,如基因测序数据、卫星遥感影像数据,更是达到PB级别。这些巨量文件在以下场景中需要被分割(Split)或分片(Chunking),以便更高效地存储、传输和处理:
-
网络传输与断点续传
-
当文件体积过大时,一次性传输往往耗时长、易因网络抖动或断连导致失败。将文件切分成若干较小的块后,可以对每个块独立传输,一旦某个块传输失败,只需重传该块,而不必重传整个文件。这对于HTTP/FTP下载、断点续传技术尤为重要。
-
在点对点(P2P)网络或分布式文件存储系统(如HDFS、FastDFS)中,将大文件切成均等的小块,可以并行传输与存储,极大提高吞吐量与可靠性。
-
-
存储与备份
-
在刻录光盘(如DVD、CD)时,单盘可存储空间有限。如果想备份大文件或大文件夹,需要先切分为与光盘容量相符的小块,再逐个刻录。
-
在移动存储介质(如U盘、移动硬盘)上进行数据备份时,有时单个文件过大,移动设备或文件系统对单个文件大小有限制(如FAT32文件系统单文件不能超过4GB)。此时需要先将大文件切分,再拷贝。
-
-
并行处理与分布式计算
-
在大数据计算框架(如Hadoop MapReduce、Spark)中,输入数据通常分布式存储为多个块(Split),由不同节点并行计算。将大文件切分成块后,不同计算节点可以同时对不同块进行处理,实现高效并行计算。
-
在图像渲染、视频转码等多媒体处理场景,也会将大文件切分成时间片或帧组,分发到多个线程/节点并行计算,加速处理过程。
-
-
日志分析与流处理
-
当海量日志文件积累时,为了方便按时间段或事件类型进行分析,常会将日志文件按日期、大小或行数分割。例如,每天生成一个日志文件,每个文件大小不超过100MB,之后再按业务需求进行归档或分析。
-
在流式处理场景,如Kafka消费到的日志或消息文件,也可以先切分成较小的块作为离线处理单元。
-
-
资源受限环境(嵌入式、移动设备)
-
在内存与存储资源受限的嵌入式设备或移动端,往往无法一次性加载大文件,此时需要通过分块方式逐块处理,避免一次性占用大量内存,从而降低 OOM 风险,提升应用稳定性。
-
-
数据安全与加密
-
在安全传输场景中,为了增强大文件的完整性校验与防篡改,可以将文件切块后对每块单独进行哈希校验,或者对每块单独加密,再进行合并或传输。若某块数据被篡改,只需重传该块即可,校验更灵活。
-
综上所述,无论是网络传输、存储备份、并行计算,还是日志分析、资源受限环境,都对文件分割提出了强烈需求。虽然在 Linux/Unix 下有 split
工具、在 Windows 下有第三方软件可以完成文件分割任务,但在某些定制化系统或跨平台需求下,需要开发者自己实现一套灵活、高效、可拓展的文件分割工具,特别是基于 Java 语言的场景,可以方便地集成到 Java 应用中或作为独立工具执行。
1.2 项目目标与需求分析
本项目旨在编写一个通用的 Java 文件分割(File Splitter)与合并(File Merger)工具,满足以下核心需求:
-
支持多种分割策略
-
按字节大小分割:按用户输入的块大小(例如 10MB、100MB)将原文件分割成多个等大小(最后一块可小于设定大小)的文件块。
-
按行数分割(针对文本文件):按用户指定的行数(例如每 1000 行分割成一个块)进行切分。适用于日志文件或 CSV 数据文件。
-
按块数分割:将文件均等分成指定数量的块,每块大小自动计算(余数放最后一块)。
-
按自定义字节位置分割:用户可以提供一系列偏移量(offset),在这些偏移位置切分。适用于更复杂的分割场景。
-
-
文件合并功能
-
支持将分割后的多个块按照文件名中的序号或用户指定顺序,依次读出并合并为原始大文件。
-
合并过程中要保持字节顺序一致,支持大文件合并,可指定输出路径和文件名。
-
-
高效读取与写入
-
采用缓存缓冲区(Buffered I/O)和随机访问(RandomAccessFile)机制,避免一次性加载过大文件到内存。
-
在分割和合并时,采用合理大小的字节缓冲区(如 4KB、8KB、64KB)逐块读取和写入,提高磁盘 I/O 性能。
-
多线程扩展:在分割时,可以将分割任务分为多个线程并行执行(例如按块区间并行读取写入),提高速度;在合并时,也可并行读取多个文件块到内存缓冲区,再单线程按顺序写入到目标文件,从而提高整体性能。作为可选扩展,此版本初期以单线程实现为主,保证逻辑简单易懂。
-
-
命令行界面或 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 界面,提供按钮、文本框让用户可视化地选择文件并设置参数,点击后执行分割或合并。
-
-
异常处理与健壮性
-
针对文件读写、参数不合法、存储空间不足等常见问题,设计严谨的异常捕获与友好提示,避免程序崩溃。
-
对分割前的输入文件进行校验,如文件是否存在、是否可读;对输出目录进行校验,如是否存在或无法创建;可选对已存在的分割文件块进行覆盖或提示。
-
对合并操作进行校验,如检查所有要合并的块是否都存在、是否按序排列、文件块大小是否一致(除最后一块)。
-
-
扩展性与可维护性
-
采用面向对象设计,将分割逻辑与合并逻辑拆分为独立模块,如
Splitter
、Merger
类,各自提供接口方法,方便后续维护与功能扩展。 -
对不同分割策略实现不同的策略类(如
SizeBasedSplitter
、LineBasedSplitter
、BlockCountSplitter
),并通过工厂模式动态创建对应策略,对外统一接口,满足“开闭原则”。 -
将 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 中,对大文件进行分割或合并时,需要注意以下几点,以保证性能与稳定性:
-
避免一次性将大文件读入内存
-
对于几百 MB、几 GB 甚至更大的文件,直接使用
byte[] buffer = new byte[(int)file.length()]
进行读取会导致内存耗尽(OutOfMemoryError
)。 -
正确做法是采用分块读取方式,如使用
BufferedInputStream
、BufferedOutputStream
、RandomAccessFile
等,配合一个固定大小的 byte 数组(例如 4KB、8KB、16KB),循环读取文件流,再将数据写入目标流。这样整个文件在磁盘 I/O 过程中占用的内存仅是缓冲区大小。
-
-
RandomAccessFile 的应用
-
当按字节位置分割或合并需要对文件进行随机访问时,可以使用
RandomAccessFile
,它提供了读写指针seek(long pos)
方法,可定位到文件指定偏移位置读取或写入。 -
在按字节大小切分时,可以先通过
RandomAccessFile.length()
获取文件总大小,再按分割点startPos = chunkIndex * chunkSize
、endPos = min(startPos + chunkSize, totalSize)
定位到分割起始位置,读取chunkSize
大小的数据写入到分割文件中。
-
-
文本文件按行分割
-
当按行数进行分割时,需要按文本行边界来切分,即不能在半行处切断。可使用
BufferedReader.readLine()
逐行读取,计数当达到指定行数时,将当前缓冲区中的内容写入一个新文件,重置行计数,继续读取下一块。 -
读取时使用
BufferedReader
比直接使用字符流更高效,并避免因为编码问题导致的读取不完整。写入时可使用BufferedWriter
,在写入时保持统一的行结束符(\n
或\r\n
),注意不同系统换行符差异。
-
-
UTF-8/字符编码注意事项
-
当按字节大小分割包含多字节编码(如 UTF-8)的文本文件时,若分割点落在字符中间,可能导致最后一个字符出现半截情况,写入分割文件时可能出现乱码或无法解析的字符。
-
解决办法一:只在字符边界处分割,需先找到分块末尾最近的行边界或字符边界(低效)。
-
解决办法二:针对文本文件推荐使用按行分割,而非按字节切分,以保证字符完整性。
-
如果业务场景对文本的字符完整性要求不高,可直接按字节分割,然后在合并时完整恢复原文件,整体转换与解析按整个文件读取即可。
-
-
合并多个文件块
-
将多个文件块按顺序依次读取并写入到目标文件即可。合并时需保证写入顺序与原分割时的顺序一致,可通过文件名中的序号或特定命名规则(如
filename.part1
、filename.part2
)来保证正确顺序。 -
合并时依旧采用
BufferedInputStream
+BufferedOutputStream
分块读写,避免一次性将所有块加载到内存。 -
合并过程中可以检查每个块的字节数是否与预期一致,最后合并的总字节数是否与原始文件大小相同,用于校验合并正确性。
-
-
异常处理与资源释放
-
在进行文件 I/O 时,要注意使用
try-with-resources
或在finally
块中关闭流,避免出现资源泄露导致文件句柄耗尽。 -
针对常见异常如
FileNotFoundException
、IOException
进行捕获,并给出友好提示,如“分割失败:找不到输入文件”、“合并失败:磁盘空间不足”等。 -
当分割过程中某个块写入失败,应及时关闭已打开的流,并可选删除已生成的部分块文件,以保证磁盘不被无效文件占满。
-
-
性能优化与多线程扩展
-
默认采用单线程顺序处理方式,逻辑简单易懂;如果要进一步提升处理速度,可通过多线程并行分割或并行合并来提高磁盘 I/O 并行度。例如:
-
将大文件按块区间划分给多个线程,各自使用
RandomAccessFile
定位到不同起始偏移并读取对应数据写入独立分块文件。 -
合并时,可先将所有块文件并行读取到内存缓冲,再按顺序写入目标文件(需注意内存占用)。
-
-
多线程版本需要在写入时避免竞争写同一个输出文件的位置,可通过锁或同步块控制写入顺序。
-
-
命令行参数解析
-
可使用 Apache Commons CLI(
commons-cli
)库来解析命令行参数,定义选项(Options)及其短参数、长参数、是否必选、是否带值等属性。例如定义-mode
、-input
、-output
、-size
、-lines
、-chunks
、-offsets
等选项。 -
或者手动解析
String[] args
,通过循环检查数组中匹配的标志位,并读取下一个元素作为参数值;对于缺少必选参数或参数格式不正确的情况,打印帮助信息并退出。
-
-
项目扩展性设计
-
策略模式(Strategy Pattern):定义
Splitter
接口,将不同分割策略(按大小、按行、按块数、按偏移)实现为不同类,实现接口的统一方法split(File inputFile, File outputDir, Map<String, Object> params)
;在运行时根据命令行参数决定使用哪个具体实现。这样后续若需增加新的分割策略,只需新增实现类,修改工厂方法即可,不影响已有代码。 -
工厂模式(Factory Pattern):设计一个
SplitterFactory
,给定mode
(如size
、lines
、chunks
、offsets
),返回相应的Splitter
实例。 -
统一返回结果与日志:所有
Splitter
、Merger
方法在执行后应返回统一的结果对象(如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>