|
| 1 | +# async-file |
| 2 | + |
| 3 | +[](https://gitee.com/pinweiwan/async-file/stargazers) |
| 4 | +[](https://gitee.com/pinweiwan/async-file/members) |
| 5 | +[](https://search.maven.org/artifact/io.github.kavahub/kavahub-async-file) |
| 6 | +[](https://github.com/kavahub/async-file/blob/main/LICENSE) |
| 7 | +[](https://github.com/kavahub/async-file/stargazers) |
| 8 | +[](https://github.com/kavahub/async-file/network/members) |
| 9 | +[](https://github.com/kavahub/async-file/watchers) |
| 10 | +[](https://github.com/kavahub/async-file/releases) |
| 11 | + |
| 12 | +[Gitee](https://gitee.com/pinweiwan/async-file) |
| 13 | + |
| 14 | +[GitHub](https://github.com/kavahub/async-file) |
| 15 | + |
| 16 | +#### 介绍 |
| 17 | +async-file工具提供Java异步读写文件的能力,使用Java NIO 库开发。Java应用程序引入框架可以简单的,异步和非阻塞的读写文件。框架包含三个工具类: |
| 18 | + |
| 19 | + |
| 20 | +- [`AIOFileReader`](src/main/java/io/github/kavahub/file/reader/AIOFileReader.java) 异步读取文件,使用Java NIO库 [`AsynchronousFileChannel`](https://docs.oracle.com/javase/10/docs/api/java/nio/channels/AsynchronousFileChannel.html) 和 [`CompletionHandler`](https://docs.oracle.com/javase/10/docs/api/java/nio/channels/CompletionHandler.html) 实现。 |
| 21 | + |
| 22 | +- [`AIOFileWriter`](src/main/java/io/github/kavahub/file/writer/AIOFileWriter.java) 异步写入文件,使用Java NIO库 [`AsynchronousFileChannel`](https://docs.oracle.com/javase/10/docs/api/java/nio/channels/AsynchronousFileChannel.html) 和 [`CompletionHandler`](https://docs.oracle.com/javase/10/docs/api/java/nio/channels/CompletionHandler.html) 实现。 |
| 23 | + |
| 24 | +- [`NIOFileLineReader`](src/main/java/io/github/kavahub/file/reader/NIOFileLineReader.java) 非阻塞读取文件,使用 [`ForkJoinPool`](https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/ForkJoinPool.html) 和 [`BufferedReader`](https://docs.oracle.com/javase/10/docs/api/java/io/BufferedReader.html) 实现 |
| 25 | + |
| 26 | +提示:Java提供的 [`Files`](https://docs.oracle.com/javase/10/docs/api/java/nio/file/Files.html) 文件读取功能是阻塞的。 |
| 27 | + |
| 28 | +#### 安装教程 |
| 29 | + |
| 30 | +首先,如果项目使用Maven工具,在项目的pom.xml文件中添加依赖 |
| 31 | + |
| 32 | +```xml |
| 33 | +<dependency> |
| 34 | + <groupId>io.github.kavahub</groupId> |
| 35 | + <artifactId>kava-async-file</artifactId> |
| 36 | + <version>1.0.0.RELEASE</version> |
| 37 | +</dependency> |
| 38 | +``` |
| 39 | + |
| 40 | +如果是Gradle项目,需要添加依赖: |
| 41 | + |
| 42 | +```groovy |
| 43 | +implementation 'io.github.kavahub:kava-async-file:1.0.0.RELEASE' |
| 44 | +``` |
| 45 | + |
| 46 | +#### 使用说明 |
| 47 | + |
| 48 | +[`AIOFileReader`](src/main/java/io/github/kavahub/file/reader/AIOFileReader.java)方法列表: |
| 49 | + |
| 50 | +- `Query<byte[]> bytes(Path file)` : 读取文件,返回文件数据字节数组,读取的大小有默认缓冲区决定。 |
| 51 | + |
| 52 | +- `Query<byte[]> allBytes(Path file)` : 读取文件,返回文件所有数据字节数组。每次按默认缓冲区读取文件,完成后合并。 |
| 53 | + |
| 54 | +- `Query<String> line(Path file)` : 读取文件,返回文件行字符串。每次按默认缓冲区读取文件数据字节数组,按换行符分割字节数组。 |
| 55 | + |
| 56 | +- `Query<String> allLines(Path file)` : 读取文件,返回文件所有数据字符串。每次按默认缓冲区读取文件数据字节数组,合并后转换成字符串。 |
| 57 | + |
| 58 | +默认缓冲区大小定义: |
| 59 | + |
| 60 | +```java |
| 61 | +public static final int BUFFER_SIZE = 4096 * 4; |
| 62 | +``` |
| 63 | + |
| 64 | +示例: |
| 65 | + |
| 66 | +```java |
| 67 | + // 按行读取文件,并输出到控制台 |
| 68 | + final Path FILE = Paths.get("src", "test", "resources", "fileWithmanyOfLine.txt"); |
| 69 | + AIOFileReader.line(FILE).subscribe((data, err) -> { |
| 70 | + if (err != null) { |
| 71 | + // 处理异常,如记录日志 |
| 72 | + err.printStackTrace(); |
| 73 | + } |
| 74 | + |
| 75 | + if (data != null) { |
| 76 | + // 文件行处理,如输出到控制台 |
| 77 | + System.out.println(data); |
| 78 | + } |
| 79 | + }) |
| 80 | + // 等待所有行处理完成 |
| 81 | + .join(); |
| 82 | +``` |
| 83 | + |
| 84 | +示例: |
| 85 | + |
| 86 | +```java |
| 87 | + // 统计文件中单词个数,并找出次数最多的单词 |
| 88 | + final Path FILE = Paths.get("src", "test", "resources", "fileToCount.txt"); |
| 89 | + |
| 90 | + final int MIN = 5; |
| 91 | + final int MAX = 10; |
| 92 | + |
| 93 | + ConcurrentHashMap<String, Integer> words = new ConcurrentHashMap<>(); |
| 94 | + AIOFileReader.line(FILE) |
| 95 | + // 过滤掉前14行 |
| 96 | + .filter(line -> !line.trim().isEmpty()).skip(14) |
| 97 | + // 使用空格分隔 |
| 98 | + .flatMapMerge(line -> Query.of(line.split(" "))) |
| 99 | + // 过滤单词 |
| 100 | + .filter(word -> word.length() > MIN && word.length() < MAX) |
| 101 | + // 统计单词次数 |
| 102 | + .onNext((w, err) -> words.merge(w, 1, Integer::sum)) |
| 103 | + // 阻塞,直到文件统计完毕 |
| 104 | + .blockingSubscribe(); |
| 105 | + |
| 106 | + Map.Entry<String, ? extends Number> common = Collections.max(words.entrySet(), |
| 107 | + Comparator.comparingInt(e -> e.getValue().intValue())); |
| 108 | + assertEquals("Hokosa", common.getKey()); |
| 109 | + assertEquals(183, common.getValue().intValue()); |
| 110 | +``` |
| 111 | + |
| 112 | +示例: |
| 113 | + |
| 114 | +```java |
| 115 | + // 统计“*** END OF ”行之前所有单词的数量 |
| 116 | + // 当读取到"*** END OF "行时,读线程会取消读操作,避免继续读取不需要处理的数据 |
| 117 | + |
| 118 | + final Path FILE = Paths.get("src", "test", "resources", "fileToCount.txt"); |
| 119 | + |
| 120 | + int[] count = { 0 }; |
| 121 | + AIOFileReader.line(FILE) |
| 122 | + // 过滤空行 |
| 123 | + .filter(line -> !line.trim().isEmpty()) |
| 124 | + // 忽略前14行 |
| 125 | + .skip(14) |
| 126 | + // 忽略掉‘*** END OF ’以后的行 |
| 127 | + .takeWhile(line -> !line.contains("*** END OF ")) |
| 128 | + // 行按空格切割成单词 |
| 129 | + .flatMapMerge(line -> Query.of(line.split("\\W+"))) |
| 130 | + // 去重 |
| 131 | + .distinct() |
| 132 | + // 统计数量 |
| 133 | + .onNext((word, err) -> { |
| 134 | + if (err == null) |
| 135 | + count[0]++; |
| 136 | + }) |
| 137 | + // 显示处理中的异常 |
| 138 | + .onNext((word, err) -> { |
| 139 | + if (err != null) |
| 140 | + err.printStackTrace(); |
| 141 | + }) |
| 142 | + // 阻塞,知道文件读取完成 |
| 143 | + .blockingSubscribe(); |
| 144 | + assertEquals(5206, count[0]); |
| 145 | +``` |
| 146 | + |
| 147 | +示例: |
| 148 | + |
| 149 | +```java |
| 150 | + // 详细演示takeWhile的功能: |
| 151 | + // 1. 控制台输出前部文件内容,框架日志提示[Cancel file reading. [16384 bytes] has been readed],读取操作取消,不在读取文件数据。 |
| 152 | + // 2. [16384 bytes] 信息中,16384是框架默认读取缓冲区大小,由此可以判断:文件只读取了一次 |
| 153 | + final Path FILE = Paths.get("src", "test", "resources", "fileWithmanyOfLine.txt"); |
| 154 | + |
| 155 | + int[] count = { 0 }; |
| 156 | + AIOFileReader.line(FILE) |
| 157 | + // 控制台输出 |
| 158 | + .onNext((data, err) -> { |
| 159 | + if (err != null) { |
| 160 | + err.printStackTrace(); |
| 161 | + } |
| 162 | + |
| 163 | + if (data != null) { |
| 164 | + System.out.println("before:" + data); |
| 165 | + } |
| 166 | + }) |
| 167 | + // 终止文件读操纵。 |
| 168 | + .takeWhile(line -> false) |
| 169 | + .onNext((data, err) -> { |
| 170 | + if (err != null) { |
| 171 | + err.printStackTrace(); |
| 172 | + } |
| 173 | + |
| 174 | + if (data != null) { |
| 175 | + System.out.println("after:" +data); |
| 176 | + } |
| 177 | + }).blockingSubscribe(); |
| 178 | + |
| 179 | + assertEquals(0, count[0]); |
| 180 | +``` |
| 181 | + |
| 182 | +示例: |
| 183 | + |
| 184 | +```java |
| 185 | + // 也可以使用cancel方法中断读文件操作 |
| 186 | + |
| 187 | + final Path FILE = Paths.get("src", "test", "resources", "fileWithmanyOfLine.txt"); |
| 188 | + |
| 189 | + CompletableFuture<Void> future = AIOFileReader.line(FILE).subscribe((data, err) -> { |
| 190 | + if (err != null) { |
| 191 | + System.out.println("error:" + err.getMessage()); |
| 192 | + } |
| 193 | + try { |
| 194 | + TimeUnit.MILLISECONDS.sleep(10); |
| 195 | + } catch (InterruptedException e) { |
| 196 | + e.printStackTrace(); |
| 197 | + } |
| 198 | + System.out.println(Thread.currentThread().getName()); |
| 199 | + }); |
| 200 | + |
| 201 | + TimeUnit.MILLISECONDS.sleep(1000); |
| 202 | + |
| 203 | + future.cancel(false); |
| 204 | +``` |
| 205 | + |
| 206 | +示例: |
| 207 | + |
| 208 | +```java |
| 209 | + // 显示读文件线程的名称 |
| 210 | + final Path FILE = Paths.get("src", "test", "resources", "fileWithmanyOfLine.txt"); |
| 211 | + |
| 212 | + AIOFileReader.bytes(FILE).subscribe((data, err) -> { |
| 213 | + if (err != null) { |
| 214 | + err.printStackTrace(); |
| 215 | + } |
| 216 | + System.out.println(Thread.currentThread().getName()); |
| 217 | + }).join(); |
| 218 | +``` |
| 219 | + |
| 220 | +输出结果如下: |
| 221 | + |
| 222 | +```text |
| 223 | +Thread-8 |
| 224 | +Thread-7 |
| 225 | +Thread-8 |
| 226 | +Thread-7 |
| 227 | +Thread-8 |
| 228 | +Thread-7 |
| 229 | +Thread-8 |
| 230 | +Thread-7 |
| 231 | +Thread-8 |
| 232 | +Thread-7 |
| 233 | +... |
| 234 | +``` |
| 235 | + |
| 236 | +其结果表明:有两个线程读取文件,线程交替读取以保证读取文件数据的顺序,这是 [`AsynchronousFileChannel`](https://docs.oracle.com/javase/10/docs/api/java/nio/channels/AsynchronousFileChannel.html) 实现的 |
| 237 | + |
| 238 | + |
| 239 | +[`AIOFileWriter`](src/main/java/io/github/kavahub/file/writer/AIOFileWriter.java)方法列表: |
| 240 | + |
| 241 | +- `CompletableFuture<Integer> write(Path file, byte[] bytes)` : 字节数组数据写入文件。 |
| 242 | + |
| 243 | +- `CompletableFuture<Integer> write(Path file, String line)` : 字符串数据写入文件。 |
| 244 | + |
| 245 | +- `CompletableFuture<Integer> write(Path file, Query<String> lines)` : 字符串流数据写入文件。 |
| 246 | + |
| 247 | +- `CompletableFuture<Integer> write(Path file, Iterable<String> lines)` : 字符串集合数据写入文件。 |
| 248 | + |
| 249 | +示例: |
| 250 | + |
| 251 | +```java |
| 252 | + // 写入字符串 |
| 253 | + AIOFileWriter.write(Paths.get(FILE_TO_WRITE), "This is file content:你好").join(); |
| 254 | +``` |
| 255 | + |
| 256 | +示例: |
| 257 | + |
| 258 | +```java |
| 259 | + // 分割字符串写入 |
| 260 | + final String content = "This is file content:你好"; |
| 261 | + |
| 262 | + AIOFileWriter.write(Paths.get(FILE_TO_WRITE), String.join(System.lineSeparator(), content.split(" "))).join(); |
| 263 | +``` |
| 264 | + |
| 265 | +示例: |
| 266 | + |
| 267 | +```java |
| 268 | + // 字符流写入 |
| 269 | + Query<String> data = Query.of("This is file content:你好") |
| 270 | + .flatMapMerge(line -> Query.of(line.split(" "))) |
| 271 | + .map((line) -> line + System.lineSeparator()); |
| 272 | + AIOFileWriter.write(Paths.get(FILE_TO_WRITE), data).join(); |
| 273 | +``` |
| 274 | + |
| 275 | +示例: |
| 276 | + |
| 277 | +```java |
| 278 | + // 字符流转换后写入 |
| 279 | + Query<String> data = Query.of("This is file content:你好") |
| 280 | + .flatMapMerge(line -> Query.of(line.split(" "))) |
| 281 | + .map((line) -> line + System.lineSeparator()); |
| 282 | + AIOFileWriter.write(Paths.get(FILE_TO_WRITE), data).join(); |
| 283 | +``` |
| 284 | + |
| 285 | +示例: |
| 286 | + |
| 287 | +```java |
| 288 | + // 边读边写 |
| 289 | + final Path FILE = Paths.get("src", "test", "resources", "fileWithmanyOfLine.txt"); |
| 290 | + |
| 291 | + Query<String> reader = AIOFileReader.line(FILE) |
| 292 | + // 忽略前2行 |
| 293 | + .skip(2) |
| 294 | + // 过滤掉空行 |
| 295 | + .filter(line -> !line.isBlank()) |
| 296 | + // 转换成大写 |
| 297 | + .map(String::toUpperCase) |
| 298 | + // 加入换行符 |
| 299 | + .map((line) -> line + System.lineSeparator()); |
| 300 | + AIOFileWriter.write(Paths.get(FILE_TO_WRITE), reader).join(); |
| 301 | +``` |
| 302 | + |
| 303 | +[`NIOFileLineReader`](src/main/java/io/github/kavahub/file/reader/NIOFileLineReader.java) 方法列表: |
| 304 | + |
| 305 | +`Query<String> read(Path file)` : 读取文件行。 |
| 306 | + |
| 307 | +```java |
| 308 | + // 读取文件行并过滤 |
| 309 | + final Path FILE = Paths.get("src", "test", "resources", "fileWithmanyOfLine.txt"); |
| 310 | + NIOFileLineReader.read(FILE).filter(line -> !line.trim().isEmpty()).onNext((data, err) -> { |
| 311 | + System.out.println(data); |
| 312 | + }).blockingSubscribe(); |
| 313 | +``` |
| 314 | + |
| 315 | + |
| 316 | +关于使用的建议: |
| 317 | + |
| 318 | +- 文件的异步读写,并不是为了提高文件的读取性能,而是提高文件读取的吞吐量(读取更多的文件,并保持性能,使JVM可以稳定运行)。 |
| 319 | +- 在大多数情况下,使用Jdk提供的[`Files`](https://docs.oracle.com/javase/10/docs/api/java/nio/file/Files.html)或许更合适。 |
| 320 | +- 不要为了异步而异步,找到问题所在,也许解决问题的关键不是异步。 |
| 321 | + |
| 322 | +建议使用优先级: `Java NIO Files` > `NIOFileLineReader` > `AIOFileReader` |
| 323 | + |
| 324 | +#### 性能 |
| 325 | + |
| 326 | +性能测试,参考 [`ReadLineBenchmark`](src/test/java/io/github/kavahub/file/performance/ReadLineBenchmark.java) 。 其他开源项目文件读写的性能测试 [`ReadFileBenchmark`](https://gitee.com/yangyunjiao/learn-java/blob/master/core-java/core-java-io/src/main/java/net/learnjava/ReadFileBenchmark.java) |
| 327 | + |
| 328 | + |
| 329 | +#### 构建项目 |
| 330 | + |
| 331 | +克隆代码到本地,然后运行mvn命令, 执行编译,测试,打包项目: |
| 332 | + |
| 333 | +```text |
| 334 | +mvn clean install |
| 335 | +``` |
| 336 | + |
| 337 | +#### 发布项目 |
| 338 | + |
| 339 | +首先,确保项目可以正确构建。然后执行下面的命令(发布的文件要以release结尾,如:kavahub-async-file-1.0.0.RELEASE.jar): |
| 340 | + |
| 341 | +```text |
| 342 | +mvn release:prepare -Prelease |
| 343 | +mvn release:perform -Prelease |
| 344 | +``` |
| 345 | + |
| 346 | +上面操作将包上传到了Staging Repository,需要转入Release Repository,执行命令: |
| 347 | + |
| 348 | +```text |
| 349 | +mvn nexus-staging:release |
| 350 | +``` |
| 351 | + |
| 352 | +以上操作全部成功,发布完成。 |
| 353 | + |
| 354 | +发布SNAPSHOT包到仓库,命令如下: |
| 355 | + |
| 356 | +```text |
| 357 | +mvn clean deploy -Prelease |
| 358 | +``` |
| 359 | + |
| 360 | +取消Staging Repository中的包,命令如下: |
| 361 | + |
| 362 | +```text |
| 363 | +mvn nexus-staging:drop |
| 364 | +``` |
| 365 | + |
| 366 | +#### 其他开源项目 |
| 367 | + |
| 368 | +- [RxIo](https://github.com/javasync/RxIo) : Asynchronous non-blocking File Reader and Writer library for Java |
| 369 | + |
| 370 | +#### 参考文档 |
| 371 | + |
| 372 | +- [vsCode利用git连接github](https://www.jianshu.com/p/f836da434e18) |
| 373 | +- [如何将自己的代码发布到Maven中央仓库](https://www.cnblogs.com/songyz/p/11387978.html) |
| 374 | +- [Deploying to OSSRH with Apache Maven - Introduction](https://central.sonatype.org/publish/publish-maven/) |
| 375 | + |
| 376 | + |
| 377 | + |
0 commit comments