教程 · 2024年8月11日 0

利用虚引用实现自动清理临时文件

内容目录

学过 Java 的都知道 Java 有强软弱虚引用,本文是利用虚引用特性的一个实践,即利用引用回收时,通过引用队列得到的通知来实现自动清理临时文件的功能。

弱引用和虚引用

虚引用其实和弱引用非常类似,弱引用经常用于存储关系映射,当关系不再使用的时候自动清理掉关系。
而这两者的主要区别就是弱引用能够在引用的对象即将被清理时将其恢复成强引用,虚引用则不行。那么在用于存储关系映射时就非常合适,因为可以在关系失效之前做一些检查来判断是否要继续维持这个关系,Guava 的缓存框架就有这种功能。

我们要实现的临时文件自动清理功能,就适合用虚引用,因为我们只需要获得一下引用的对象被垃圾回收掉的通知就可以了。

引用队列

在对象被回收的时候获取一个通知,需要在构造引用的时候传入一个引用队列,它也是同步队列的一种,当引用对象中的值被回收后,会将该引用添加到引用队列中。

关键思路

别忘了关键的一点,虚引用在调用其 get 方法时始终返回 null,这是因为虚引用在对象被回收后才会触发引用队列的通知,回收后是无法获取实际值的。因此我们需要定义一个自定义的引用来继承虚引用,在其中存储清理工作时需要用到的信息。

示例如下:

// 本功能不需要外部代码访问这个引用,因此修饰符都是 default
class TempFileRef extends PhantomReference<File> {
    private final Path path;

    TempFileRef(File file) {
        super(file);
        this.path = file.toPath();
    }

    void cleanup() {
        try {
            Files.deleteIfExists(path);
        } catch (IOException e) {
            // 处理异常
        }
    }
}

为了保持代码的封装性,我们不想让使用者自己创建引用,因此我们可以自己实现一个文件类继承 File 对象,并提供一个构造工厂方法。

示例如下:

public class TempFile extends File {
    private final String name;

    TempFile(String path, String name) {
        super(path);
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    public synchronized static TempFile createTempFile(String name) {
        try {
            Path file = Files.createTempFile("tempFile-", "");
            TempFile tempFile = new TempFile(file.toAbsolutePath().toString(), name);
            new TempFileRef(tempFile); // 注意它们需要在同一个包下
            return tempFile;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

最后为了实现自动回收功能,需要在引用创建时传入引用队列,并在程序运行时启动一个守护线程监听这个队列。
由于我们的引用对象不会返回给调用方存储,需要一个全局的集合来临时存储避免引用对象自身被回收而失去功能。

完整代码

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;

/**
 * 临时文件
 *
 * @author Gardel
 * @since 2023-10-27 15:37
 */
public class TempFile extends File {
    private final String name;

    TempFile(String path, String name) {
        super(path);
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        if (!super.equals(o)) {
            return false;
        }
        TempFile tempFile = (TempFile) o;
        return Objects.equals(name, tempFile.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), name);
    }

    /**
     * 创建临时文件,垃圾回收时自动删除
     *
     * @param name 文件名
     * @return 临时文件
     */
    public synchronized static TempFile createTempFile(String name) {
        try {
            Path file = Files.createTempFile("tempFile-", "");
            TempFile tempFile = new TempFile(file.toAbsolutePath().toString(), name);
            new TempFileRef(tempFile);
            return tempFile;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * 将现有文件当作临时文件,不会自动删除
     *
     * @param path 文件路径
     * @return 临时文件
     */
    public static TempFile fromExistFile(Path path) {
        return new TempFile(path.toAbsolutePath().toString(), path.getFileName().toString());
    }
}
import java.io.IOException;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;

import lombok.extern.slf4j.Slf4j;

/**
 * 临时文件引用
 *
 * @author Gardel
 * @since 2023-10-27 10:51
 */
@Slf4j
class TempFileRef extends PhantomReference<TempFile> {
    // 全局的引用集合,用于防止引用对象本身被回收而失去作用
    private static final Set<TempFileRef> references;
    // 引用队列
    private static final ReferenceQueue<TempFile> queue;
    // 自动清理线程
    private static final Thread cleanUpTask;

    static {
        references = new HashSet<>();
        queue = new ReferenceQueue<>();
        cleanUpTask = new Thread(TempFileRef::doCleanUp, "TempFileCleanUpTask");
        cleanUpTask.setDaemon(true);
        cleanUpTask.start();
    }

    private final String name;
    private final Path path;

    TempFileRef(TempFile file) {
        super(file, queue);
        this.name = file.getName();
        this.path = file.toPath();
        if (log.isDebugEnabled()) {
            log.debug("创建临时文件,文件路径: {},文件名: {}", path.toAbsolutePath(), name);
        }
        references.add(this);
    }

    void cleanup() {
        try {
            boolean deleted = Files.deleteIfExists(path);
            if (log.isDebugEnabled()) {
                log.debug("删除临时文件,文件路径: {},文件名: {},结果: {}", path.toAbsolutePath(), name, deleted);
            }
            references.remove(this);
        } catch (IOException e) {
            log.error("删除临时文件失败,文件路径: {},文件名: {}", path.toAbsolutePath(), name, e);
        }
    }

    private static void doCleanUp() {
        try {
            TempFileRef tempFile;
            while ((tempFile = (TempFileRef) queue.remove()) != null) {
                tempFile.cleanup();
            }
        } catch (InterruptedException ignored) {
            // 程序退出
        }
    }
}