当前位置 : 首页 » 文章分类 :  开发  »  Java-File

Java-File

Java 文件操作相关笔记
Java 中 File 可以代表文件或目录。


文件下载

SpringBoot 文件下载接口示例

@RestController
public class ImageController {
    @RequestMapping("/api/download")
    void export(@RequestParam("id") Long id, HttpServletResponse response) {
        User user = getFromDB(id);
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; fileName=%s", user.getName()));
        try (
                OutputStream outputStream = response.getOutputStream();
        ) {
            IOUtils.write(JsonUtils.toJson(user), outputStream, UTF_8);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

SpringBoot 图片下载接口示例

@RestController
public class ImageController {
    @GetMapping(value = "/api/image")
    public void imageDownload(HttpServletResponse response, @RequestParam("filename") String filename) {
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.IMAGE_JPEG_VALUE);
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;fileName=" + filename);
        try (
                // 从 classpath 下读取图片
                InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(filename);
                OutputStream outputStream = response.getOutputStream();
        ) {
            IOUtils.copy(inputStream, outputStream);
        } catch (IOException e) {
            log.error("图片读取失败");
        }
    }
}

Content-Disposition fileName 中文报错

文件下载接口中设置 Content-Disposition fileName 为中文文件名:

response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; fileName=%s", "用户信息.yml"));

报错:

2023-12-06 16:55:17.161 [http-nio-8125-exec-2] WARN  org.apache.coyote.http11.Http11Processor.log:175 - The HTTP response header [Content-Disposition] with value [attachment; fileName=用户信息.yml] has been removed from the response because it is invalid
java.lang.IllegalArgumentException: The Unicode character [用] at code point [30,693] cannot be encoded as it is outside the permitted range of 0 to 255

原因:
RFC 822 规定 header 只支持 ASCII
RFC 2231 扩展了编码机制,可以使用 url编码中文。

解决:

response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; fileName=%s", URLEncodeUtil.encode("用户信息.yml")));

下载下来的文件名是中文的 用户信息.yml


SpringBoot 下载文件变为 Transfer-Encoding: chunked 响应头

当使用 Spring Boot 中的 HttpServletResponse 对象进行响应时,如果返回内容的长度未知或无法确定,服务器将自动选择使用分块传输编码(Chunked Transfer Encoding)方式进行响应。
分块传输编码是一种在 HTTP 响应中动态生成数据并将其以多个块(chunks)发送给客户端的机制。这种编码方式允许服务器在生成响应时逐步发送数据,而不需要事先确定整个响应的长度。
当你看到 Transfer-Encoding: chunked 的响应头时,说明服务器正在使用分块传输编码方式进行响应。这是一种正常的行为


文件压缩/解压

https://blog.csdn.net/hj7jay/article/details/102798664
https://zhuanlan.zhihu.com/p/364142487

Java 使用 pigz 并行压缩文件/目录

需求:
需要压缩一个目录,目录总大小为30g,里面有四五个文件,其中最大的一个文件大小为29g
使用 Hutool ZipUtil 工具类压缩,内部还是使用 FileInputStream 经过 byte[] buffer 缓冲区拷贝到 ZipOutputStream 来实现压缩,单线程顺序压缩多个文件,很慢。
经测试,压缩需要 25 分钟左右,在性能差的服务器上甚至需要1小时+

压缩优化
根据网上的优化文字,试了使用 BufferedInputStream、NIO FileChannel 都不好用,因为这里 主要时间都耗在单个大文件的 CPU 压缩编码上,而不是多个小文件的文件传输上,单核压缩超大文件注定很慢
commons-compress 中的 ParallelScatterZipCreator 也无法满足需求,ParallelScatterZipCreator 只能实现多个 文件/ZipEntry 并行压缩,而我这里是单个文件太大需要分片并行压缩,无法加速。

Java 并行压缩
找到个 Java 并行压缩工具 https://github.com/shevek/parallelgzip 压缩单个文件确实很快,但不支持设置 ZipEntry,只能压缩一个文件,也无法满足需求。

pigz 命令并行压缩
tar cf - path-to-dir | pigz -k > xx.tar
50核CPU,30G 压缩到 22G 耗时1分钟出头

最终方案
Java 中直接调用 Linux 命令 pigz 实现并行压缩,pigz 会将单个大文件切片并行压缩,也可以和 tar 命令结合来压缩整个目录。
在一台联网的机器上 yum install pigz 安装 pigz,安装后拷贝 /usr/bin/pigz 到 Java 服务容器内的 /usr/bin/,以便 Java 容器内可直接调用 pigz 命令。
使用
RuntimeUtil.execForStr(format(“tar cf - %s | pigz -k > %s”, dir, tarFile));

ProcessBuilder 执行管道命令

从 Java 9 开始,ProcessBuilder 引入了管道概念,可以把一个进程的输出作为另一个进程的输入再次操作。

ProcessBuilder ls = new ProcessBuilder("/bin/bash", "-c", "ls -l");
ProcessBuilder wc = new ProcessBuilder("wc", "-l");
List<Process> processes = ProcessBuilder.startPipeline(Arrays.asList(ls, wc));
Process process = processes.get(processes.size() - 1);
process.waitFor();
IoUtil.read(process.getInputStream(), Charset.defaultCharset());

异常

Zip64RequiredException size exceeds the limit of 4GByte

ZipArchiveOutputStream 压缩 10gb 大文件完成后报错:
org.apache.commons.compress.archivers.zip.Zip64RequiredException: file’s size exceeds the limit of 4GByte.

原因:
尝试创建一个超过4GB的ZIP文件,但没有启用ZIP64扩展

解决:
在 Apache Commons Compress 库中,可以通过调用 ZipArchiveOutputStream 的 setUseZip64 方法来启用 ZIP64 扩展

ZipArchiveOutputStream zaos = new ZipArchiveOutputStream(outputFile);
zaos.setUseZip64(Zip64Mode.AsNeeded);

Zip64Mode.AsNeeded 表示只有当文件大小超过4GB时才使用ZIP64扩展

entry size ‘10737418240’ is too big ( > 8589934591 )

TarArchiveOutputStream 压缩 10gb 大文件报错
java.lang.IllegalArgumentException: entry size ‘10737418240’ is too big ( > 8589934591 ).

原因:
创建超过 8gb 的 tar 文件时需要设置 bigNumberMode

解决:
setBigNumberMode

GzipCompressorOutputStream gcos = new GzipCompressorOutputStream(new FileOutputStream("file"));
TarArchiveOutputStream taos = new TarArchiveOutputStream(gcos);
taos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);

https://issues.apache.org/jira/browse/COMPRESS-194


ZipInputStream 解压报错 malformed

ZipInputStream 解压时 getNextEntry 报错

java.lang.IllegalArgumentException: malformed input off : 5, length : 1
at java.base/java.lang.String.throwMalformed(String.java:1238)
at java.base/java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:124)
Caused by: java.nio.charset.MalformedInputException: Input length = 1

原因:
尝试解压的文件包含了特殊字符,或者文件编码与你的系统编码不一致。

解决:
使用 Apache Commons Compress 库来处理 ZIP 文件,可以解决 zip 包中文件名有中文时跨平台的乱码问题。

File zipFile = new File("example.zip");
File outputDir = new File("output");
try (ZipArchiveInputStream zipInput = new ZipArchiveInputStream(new FileInputStream(zipFile))) {
    ZipArchiveEntry entry;
    while ((entry = zipInput.getNextZipEntry()) != null) {
        String name = entry.getName();
        IOUtils.copy(zipInput, new FileOutputStream(new File(name)));
    }
} catch (IOException e) {
    e.printStackTrace();
}

https://www.cnblogs.com/wang-meng/p/6886455.html


InputStream

FileInputStream 无法重复读

InputStream 是无法重复读取的。

如下单测,两次计算文件输入流的 md5 值不同,因为第一遍读完 FileInputStream 后,就是个空的 InputStream 了。
第一次是计算完整输入流的 md5,第二次是计算一个空的输入流的 md5。把第二次和第三次结果对比,发现第二次的 md5 结果其实就是空字符串的 md5。

@Test
public void testMd5() throws Exception {
    File file = new File("/Users/xx/git/my/hexo/source/_posts/Java/Java-Math.md");
    InputStream inputStream = new FileInputStream(file);
    log.info(DigestUtils.md5Hex(inputStream));
    log.info(DigestUtils.md5Hex(inputStream));
    log.info(DigestUtils.md5Hex(""));
}

结果:
bc71dce15c32ff0dea6636be3aeab5ee
d41d8cd98f00b204e9800998ecf8427e
d41d8cd98f00b204e9800998ecf8427e

曾经犯的一个错就是在按行读取文件流后,再用这个 InputStream 计算 md5 值,结果发现全部文件的 md5 都相同,都是 d41d8cd98f00b204e9800998ecf8427e,排查了一会儿才发现读的都是空的输入流的 md5,也就是空字符串的 md5。


File

java.io.File

public class File
extends Object
implements Serializable, Comparable<File>{}

separator 路径分隔符

File.separator Linux 上是 /,Windows 上是 \\


getName() 获取文件名(无路径有扩展名)

public String getName()
获取不带路径的文件名,包含扩展名。


getPath() 返回路径名

public String getPath()
返回的是定义时的路径,可能是相对路径,也可能是绝对路径,这个取决于定义时用的是相对路径还是绝对路径。
如果定义时用的是绝对路径,那么使用 getPath() 返回的结果跟用 getAbsolutePath() 返回的结果一样。
getPath() 返回的是 File 构造方法里的路径,是什么就是什么,不增不减。

getAbsolutePath() 返回绝对路径(带扩展名)

public String getAbsolutePath()

返回此抽象路径名的绝对路径名字符串。
如果此抽象路径名已经是绝对路径名,则返回该路径名字符串,这与 getPath() 方法一样。
如果此抽象路径名是空抽象路径名,则返回当前用户目录的路径名字符串,该目录由系统属性 user.dir 指定。
否则,使用与系统有关的方式解析此路径名。
在 UNIX 系统上,根据当前用户目录解析相对路径名,可使该路径名成为绝对路径名。
在 Microsoft Windows 系统上,根据路径名指定的当前驱动器目录(如果有)解析相对路径名,可使该路径名成为绝对路径名;否则,可以根据当前用户目录解析它。

getAbsolutePath() 不会处理路径中的 ...

getCanonicalPath() 返回规范化绝对路径(带扩展名)

public String getCanonicalPath() throws IOException
返回的是规范化的绝对路径,等价于先调用 getAbsolutePath() 获取绝对路径再将 ... 解析成对应的正确的路径

获取文件扩展名

File file = new File("/tmp/file/aaa.xlsx");
String extension = file.getAbsolutePath().substring(file.getAbsolutePath().lastIndexOf("."));

结果为 “.xlsx”


exists() 判断文件或目录是否存在

public boolean exists()
检查 File 代表的文件路径是否是已经存在的文件或目录。若已存在,返回 true


listFiles(FileFilter filter) 根据filter筛选文件

返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
除了返回数组中的路径名必须满足过滤器外,此方法的行为与 listFiles() 方法相同。
如果给定 filter 为 null,则接受所有路径名。否则,当且仅当在路径名上调用过滤器的 FileFilter.accept(java.io.File) 方法返回 true 时,该路径名才满足过滤器。
参数:filter - 文件过滤器
返回:抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件和目录。如果目录为空,那么数组也将为空。如果抽象路径名不表示一个目录,或者发生 I/O 错误,则返回 null。
抛出: SecurityException - 如果存在安全管理器,且其 SecurityManager.checkRead(java.lang.String) 方法拒绝对目录进行读访问

public File[] listFiles(FileFilter filter) 接受一个 FileFilter 文件过滤器参数
FileFilter 本身是一个函数式接口,可以直接用一个lambda表达式代替,例如:
筛选目录下的所有 .xlsx 文件
File[] fileArray = dir.listFiles(file -> file.getName().toLowerCase().endsWith(".xlsx"));

或者先定义 FileFilter lambda 表达式,之后多次可用:

public static final FileFilter FILE_FILTER = file -> !file.isHidden() && !file.getName().contains("__MACOSX");
dir.listFiles(FILE_FILTER)

java 8 lambda expression for FilenameFilter
https://stackoverflow.com/questions/29316310/java-8-lambda-expression-for-filenamefilter

java.io.File
https://docs.oracle.com/javase/9/docs/api/java/io/File.html


按文件名过滤目录中的文件列表(lambda表达式)

只列出目录中的扩展名为.xlsx的文件

private void filtFiles() {
    String fileDir = "/tmp/files/";
    File dir = new File(fileDir);
    if (!dir.exists()) {
        logger.info("Dir {} does not exist", fileDir);
        return;
    }

    File[] fileArray = dir.listFiles(file -> file.getName().toLowerCase().endsWith(".xlsx"));
    if (fileArray.length == 0) {
        logger.info("Dir {} doesn't have xlsx files", fileDir);
        return;
    }

    for (File file : fileArray) {
      System.out.println(file.getName());
    }
}

createTempFile() 创建临时文件

File tempFile = File.createTempFile("temp", ".png");
File tempFile = File.createTempFile("temp", ".txt");

FileNotFoundException File name too long

Java File.open 报错
java.io.FileNotFoundException: /xx/xx/xx.txt (File name too long)

Linux 中,英文文件名最长 255 个字符,中文文件名最长 127 个中文字符,都包括路径在内。

解决:
在 Mac 上用一个较短的路径处理


Files

readAllBytes 读文本文件到string

try {
    String json = new String(Files.readAllBytes(Paths.get("/Users/xxx/Downloads/xxx.json")));
} catch (Exception e) {
    log.error("read file error");
}

readAttributes() 读取文件属性(创建时间等)

/**
 * 读取文件属性
 */
@Test
public void readFileAttributes() throws Exception {
    File file = new File("/Users/ddd/arthas-boot.jar");
    BasicFileAttributes attributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
    log.info("创建时间 {} ", attributes.creationTime());
    log.info("修改时间 {} ", attributes.lastModifiedTime());
}

结果:

创建时间 2022-06-21T03:11:02Z 
修改时间 2022-06-21T03:11:02Z 

Java 处理目录中的同名文件:只保留创建日期最新的

找出目录中的同名文件(基础名相同就算同名,不看扩展名),按创建时间升序排序,只保留创建时间最新的,其他删掉

@Test
public void processSameNameFile() {
    String dir = "/Users/xxx/dir";
    Map<String, List<File>> baseNameFilesMap =
            FileUtils.listFiles(new File(dir), new String[]{"doc", "docx", "pdf", "txt"}, false).stream().collect(
                    Collectors.groupingBy(file -> FilenameUtils.getBaseName(file.getName())));
    baseNameFilesMap.forEach((baseName, sameNamefiles) -> {
        if (sameNamefiles.size() > 1) {
            log.info("重名文件 {},个数 {}", baseName, sameNamefiles.size());
            // 按创建时间升序排序
            sameNamefiles.sort(Comparator.comparing(file -> {
                try {
                    return Files.readAttributes(file.toPath(), BasicFileAttributes.class).creationTime();
                } catch (IOException e) {
                    return FileTime.fromMillis(new Date().getTime());
                }
            }));
            sameNamefiles.forEach(file -> {
                try {
                    log.info("文件:{},创建时间:{}", file.getName(), Files.readAttributes(file.toPath(), BasicFileAttributes.class).creationTime());
                } catch (IOException e) {
                    log.error("读取文件属性失败", e);
                }
            });
            for (int i = 0; i < sameNamefiles.size() - 1; i++) {
                FileUtil.del(sameNamefiles.get(i));
                log.info("删除文件 {}", sameNamefiles.get(i).getName());
            }
        }
    });
}

示例

用ClassLoader读取classpath下的文件

类 ClassLoader 有个 getResource 方法,用以classpath下的资源
public URL getResource(String name)
查找具有给定名称的资源。资源是可以通过类代码以与代码基无关的方式访问的一些数据(图像、声音、文本等)。
资源名称是以 ‘/‘ 分隔的标识资源的路径名称。
此方法首先搜索资源的父类加载器;如果父类加载器为 null,则搜索的路径就是虚拟机的内置类加载器的路径。如果搜索失败,则此方法将调用 findResource(String) 来查找资源。
参数: name - 资源名称
返回:读取资源的 URL 对象;如果找不到该资源,或者调用者没有足够的权限获取该资源,则返回 null。

读取jar包中classpath内文件

this.getClass().getClassLoader().getResource(“file.txt”) 读取classpath中文件的方式在 idea 中本地运行是没问题的,但如果打成jar包运行就会读取不到文件,此时必须使用 getResourceAsStream() 方法才行

两种方法的示例如下:

package com.masikkk.blog.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import org.apache.commons.io.IOUtils;
import org.junit.Test;

public class FileTest {
    // 从Jar包中的classpath下读取文件
    @Test
    public void readClasspathFileInJar() throws Exception {
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("file.txt");
        IOUtils.readLines(inputStream).forEach(System.out::println);
    }

    // 从classpath下读取文件,打成Jar包后此方法读不到文件
    @Test
    public void readClasspathFile() throws Exception {
        URL url = this.getClass().getClassLoader().getResource("file.txt");
        System.out.println("绝对路径文件名: " + url.getFile());
        File f = new File(url.getFile());
        InputStream inputStream = new FileInputStream(f);
        IOUtils.readLines(inputStream).forEach(System.out::println);
    }
}

如果想转为 File,可以

InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("xxx.txt");
File tempFile = File.createTempFile("temp", ".txt");
FileUtils.copyInputStreamToFile(inputStream, tempFile);

ClassPathResource 读取 classpath 文件

利用 Spring 的 ClassPathResource 读取 classpath 资源。

import org.springframework.core.io.ClassPathResource;

try (
    InputStream inputStream = new ClassPathResource("ok_data_level3.csv").getInputStream()
) {
    IOUtils.readLines(inputStream, "UTF-8").forEach();
} catch (Exception e) {
    e.printStackTrace();
}

BufferedWriter写文件

File file = new File("/Users/xxxx/xxxx.txt");
if (!file.exists()) {
    try {
      file.createNewFile();
    } catch (IOException e) {
      e.printStackTrace();
    }
    logger.info("Create local file: {}", file.getAbsolutePath());
}
BufferedWriter out = new BufferedWriter(new FileWriter(file));
out.write("写入文件\r\n"); // \r\n即为换行
out.flush(); // 把缓存区内容压入文件
out.close(); // 最后记得关闭文件

FileWriter文件覆盖和追加

在实际写入文件时,有两种写入文件的方式:覆盖和追加。
“覆盖”是指清除原文件的内容,写入新的内容,默认采用该种形式写文件,
“追加”是指在已有文件的末尾写入内容,保留原来的文件内容,例如写日志文件时,一般采用追加。

在实际使用时可以根据需要采用适合的形式,可以使用: public FileOutputStream(String name, boolean append) throws FileNotFoundException 只需要使用该构造方法在构造 FileOutputStream 对象时,将第二个参数 append 的值设置为 true 即可。

覆盖:

try {
   BufferedWriter out = new BufferedWriter(new FileWriter("outfilename"));
   out.write("aString");
   out.close();
} catch (IOException e) {
}

追加:

try {
   BufferedWriter out = new BufferedWriter(new FileWriter("filename", true));
   out.write("aString");
   out.close();
} catch (IOException e) {
}

Java追加内容到文件末尾的方法

方法一、使用FileOutputStream,在构造FileOutputStream时,把第二个参数append设为true

BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true)));
bufferedWriter.write(conent);
bufferedWriter.close();

二、打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件

FileWriter writer = new FileWriter(fileName, true);
writer.write(content);
writer.close();

三、打开一个随机访问文件流,按读写方式

RandomAccessFile randomFile = new RandomAccessFile(fileName, "rw");
// 文件长度,字节数
long fileLength = randomFile.length();
// 将写文件指针移到文件尾。
randomFile.seek(fileLength);
randomFile.writeBytes(content);
randomFile.close();

java 追加内容到文件末尾的几种常用方法
https://blog.csdn.net/jsjwk/article/details/3942167


BufferedWriter 逐行追加写入文件

/**
 * 将行数据写入File
 * @param rows
 * @param append 是否追加
 */
private void writeFile(List<String> rows, boolean append) throws IOException {
    // 创建目录
    File dir = new File(FILE_DIR);
    if (!dir.exists()) {
        dir.mkdirs();
        logger.info("Create local dir: {}", FILE_DIR);
    }

    // 计算文件名
    String fileName = String.format(FILE_NAME, DateTimeUtils.toDate(new Date()));
    File file = new File(FILE_DIR + fileName);

    // 新建文件
    if (!file.exists()) {
        file.createNewFile();
        logger.info("Create local file: {}", file.getAbsolutePath());
    }

    BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, append), "utf-8"));

    // 写入文件
    for (String row : rows) {
        bufferedWriter.write(row);
        bufferedWriter.newLine();
    }
    bufferedWriter.close();
}

Java BufferedReader 读入文件到List

File file = new File("/xx/xx.txt");
if (file != null && file.exists() && file.isFile()) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8"));
    // 第一行是表头
    bufferedReader.readLine();
    String lineStr = bufferedReader.readLine();
    while (StringUtils.isNotEmpty(lineStr)) {
        updatedUuidList.add(Long.valueOf(lineStr));
        lineStr = bufferedReader.readLine();
    }
    bufferedReader.close();
}

WatchService

WatchService 可以实时的监控操作系统中文件的变化,包括创建、更新和删除事件。
WatchService 类似于在观察者模式中的观察者,Watchable 类似域观察者模式中的被观察者。

监听用户家目录的文件变动,打印相应的信息

public class DirectoryWatcherExample {

    public static void main(String[] args) {
        WatchService watchService
          = FileSystems.getDefault().newWatchService();

        Path path = Paths.get(System.getProperty("user.home"));

        path.register(
          watchService,
            StandardWatchEventKinds.ENTRY_CREATE,
              StandardWatchEventKinds.ENTRY_DELETE,
                StandardWatchEventKinds.ENTRY_MODIFY);

        WatchKey key;
        while ((key = watchService.take()) != null) {
            for (WatchEvent<?> event : key.pollEvents()) {
                System.out.println(
                  "Event kind:" + event.kind()
                    + ". File affected: " + event.context() + ".");
            }
            key.reset();
        }
    }
}

上一篇 关于

下一篇 Apache-POI

阅读
评论
4.6k
阅读预计20分钟
创建日期 2019-01-04
修改日期 2024-04-13
类别
标签

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论