org.aoju.bus.http.cache.DiskLruCache Maven / Gradle / Ivy
The newest version!
/*********************************************************************************
* *
* The MIT License (MIT) *
* *
* Copyright (c) 2015-2022 aoju.org and other contributors. *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy *
* of this software and associated documentation files (the "Software"), to deal *
* in the Software without restriction, including without limitation the rights *
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
* copies of the Software, and to permit persons to whom the Software is *
* furnished to do so, subject to the following conditions: *
* *
* The above copyright notice and this permission notice shall be included in *
* all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, *
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN *
* THE SOFTWARE. *
* *
********************************************************************************/
package org.aoju.bus.http.cache;
import org.aoju.bus.core.io.FileSystem;
import org.aoju.bus.core.io.sink.BufferSink;
import org.aoju.bus.core.io.sink.FaultHideSink;
import org.aoju.bus.core.io.sink.Sink;
import org.aoju.bus.core.io.source.BufferSource;
import org.aoju.bus.core.io.source.Source;
import org.aoju.bus.core.lang.Normal;
import org.aoju.bus.core.lang.Symbol;
import org.aoju.bus.core.toolkit.IoKit;
import org.aoju.bus.http.Builder;
import org.aoju.bus.logger.Logger;
import java.io.*;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 使用文件系统上有限空间的缓存。每个缓存条目都有一个字符串键和固定数量的值
* 每个键必须匹配regex [a-z0-9_-]{1,64}。值是字节序列,可以作为流或文件访问
* 每个值必须在{@code 0}和{@code Integer之间。MAX_VALUE}字节的长度
*
* @author Kimi Liu
* @since Java 17+
*/
public class DiskLruCache implements Closeable, Flushable {
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = Symbol.ONE;
static final long ANY_SEQUENCE_NUMBER = -1;
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,120}");
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
final FileSystem fileSystem;
/**
* 缓存存储其数据的目录
*/
final File directory;
final int valueCount;
final LinkedHashMap lruEntries = new LinkedHashMap<>(0, 0.75f, true);
private final File journalFile;
private final File journalFileTmp;
private final File journalFileBackup;
private final int appVersion;
private final Executor executor;
BufferSink journalWriter;
int redundantOpCount;
boolean hasJournalErrors;
boolean initialized;
/**
* 如果缓存已关闭,则为true
*/
boolean closed;
boolean mostRecentTrimFailed;
boolean mostRecentRebuildFailed;
/**
* 存用于存储其数据的最大字节数
*/
private long maxSize;
/**
* 当前用于在此缓存中存储值的字节数
*/
private long size = 0;
/**
* 为了区分旧快照和当前快照,每次提交编辑时都会给每个条目一个序列号。
* 如果快照的序列号不等于其条目的序列号,则该快照将失效
*/
private long nextSequenceNumber = 0;
DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
Executor executor) {
this.fileSystem = fileSystem;
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
this.executor = executor;
}
/**
* 创建一个驻留在{@code directory}中的缓存。此缓存在第一次访问时惰性初始化,如果它不存在,将创建它.
*
* @param fileSystem 文件系统
* @param directory 一个可写目录
* @param appVersion 版本信息
* @param valueCount 每个缓存条目的值数目.
* @param maxSize 此缓存应用于存储的最大字节数
* @return the disk cache
*/
public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
int valueCount, long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), Builder.threadFactory("Httpd DiskLruCache", true));
return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
}
public synchronized void initialize() throws IOException {
assert Thread.holdsLock(this);
if (initialized) {
return;
}
// 如果存在bkp文件,就使用它
if (fileSystem.exists(journalFileBackup)) {
// 如果日志文件也存在,删除备份文件
if (fileSystem.exists(journalFile)) {
fileSystem.delete(journalFileBackup);
} else {
fileSystem.rename(journalFileBackup, journalFile);
}
}
if (fileSystem.exists(journalFile)) {
try {
readJournal();
processJournal();
initialized = true;
return;
} catch (IOException journalIsCorrupt) {
Logger.warn("DiskLruCache " + directory + " is corrupt: "
+ journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
}
// The cache is corrupted, attempt to delete the contents of the directory. This can throw and
// we'll let that propagate out as it likely means there is a severe filesystem problem.
try {
delete();
} finally {
closed = false;
}
}
rebuildJournal();
initialized = true;
}
private void readJournal() throws IOException {
try (BufferSource source = IoKit.buffer(fileSystem.source(journalFile))) {
String magic = source.readUtf8LineStrict();
String version = source.readUtf8LineStrict();
String appVersionString = source.readUtf8LineStrict();
String valueCountString = source.readUtf8LineStrict();
String blank = source.readUtf8LineStrict();
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !Normal.EMPTY.equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
while (true) {
try {
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// 如果我们以截断的行结束,则在添加日志之前重新生成它
if (!source.exhausted()) {
rebuildJournal();
} else {
journalWriter = newJournalWriter();
}
}
}
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(Symbol.C_SPACE);
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(Symbol.C_SPACE, keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (null == entry) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(Symbol.SPACE);
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
private BufferSink newJournalWriter() throws FileNotFoundException {
Sink fileSink = fileSystem.appendingSink(journalFile);
Sink faultHidingSink = new FaultHideSink(fileSink) {
@Override
protected void onException(IOException e) {
assert (Thread.holdsLock(DiskLruCache.this));
hasJournalErrors = true;
}
};
return IoKit.buffer(faultHidingSink);
}
/**
* 计算初始大小并收集垃圾作为打开缓存的一部分。脏条目被认为是不一致的,将被删除
*
* @throws IOException 异常
*/
private void processJournal() throws IOException {
fileSystem.delete(journalFileTmp);
for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
fileSystem.delete(entry.cleanFiles[t]);
fileSystem.delete(entry.dirtyFiles[t]);
}
i.remove();
}
}
}
/**
* 创建一个删除冗余信息的新日志。如果存在当前日志,它将替换它
*
* @throws IOException 异常
*/
synchronized void rebuildJournal() throws IOException {
if (null != journalWriter) {
journalWriter.close();
}
try (BufferSink writer = IoKit.buffer(IoKit.sink(journalFileTmp))) {
writer.writeUtf8(MAGIC).writeByte(Symbol.C_LF);
writer.writeUtf8(VERSION_1).writeByte(Symbol.C_LF);
writer.writeDecimalLong(appVersion).writeByte(Symbol.C_LF);
writer.writeDecimalLong(valueCount).writeByte(Symbol.C_LF);
writer.writeByte(Symbol.C_LF);
for (Entry entry : lruEntries.values()) {
if (null != entry.currentEditor) {
writer.writeUtf8(DIRTY).writeByte(Symbol.C_SPACE);
writer.writeUtf8(entry.key);
writer.writeByte(Symbol.C_LF);
} else {
writer.writeUtf8(CLEAN).writeByte(Symbol.C_SPACE);
writer.writeUtf8(entry.key);
entry.writeLengths(writer);
writer.writeByte(Symbol.C_LF);
}
}
}
if (fileSystem.exists(journalFile)) {
fileSystem.rename(journalFile, journalFileBackup);
}
fileSystem.rename(journalFileTmp, journalFile);
fileSystem.delete(journalFileBackup);
journalWriter = newJournalWriter();
hasJournalErrors = false;
mostRecentRebuildFailed = false;
}
/**
* 返回名为{@code key}的条目的快照,如果条目不存在,则返回null,
* 否则当前无法读取。如果返回一个值,它将被移动到LRU队列的头部
*
* @param key 缓存key
* @return the 快照信息
* @throws IOException 异常
*/
public synchronized Snapshot get(String key) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (null == entry || !entry.readable) {
return null;
}
Snapshot snapshot = entry.snapshot();
if (null == snapshot) {
return null;
}
redundantOpCount++;
journalWriter.writeUtf8(READ).writeByte(Symbol.C_SPACE).writeUtf8(key).writeByte(Symbol.C_LF);
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
return snapshot;
}
/**
* 返回名为{@code key}的条目的编辑器,如果另一个编辑正在进行,则返回null
*
* @param key 文件key
* @return 编辑器
* @throws IOException 异常
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
&& (null == entry || entry.sequenceNumber != expectedSequenceNumber)) {
return null;
}
if (null != entry && null != entry.currentEditor) {
return null;
}
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
executor.execute(cleanupRunnable);
return null;
}
// 在创建文件之前刷新日志,以防止文件泄漏
journalWriter.writeUtf8(DIRTY).writeByte(Symbol.C_SPACE).writeUtf8(key).writeByte(Symbol.C_LF);
journalWriter.flush();
if (hasJournalErrors) {
return null; // Don't edit; the journal can't be written.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
/**
* Returns the directory where this cache stores its data.
*/
public File getDirectory() {
return directory;
}
/**
* Returns the maximum number of bytes that this cache should use to store its data.
*/
public synchronized long getMaxSize() {
return maxSize;
}
/**
* Changes the maximum number of bytes the cache can store and queues a job to trim the existing
* store, if necessary.
*/
public synchronized void setMaxSize(long maxSize) {
this.maxSize = maxSize;
if (initialized) {
executor.execute(cleanupRunnable);
}
}
/**
* Returns the number of bytes currently being used to store the values in this cache. This may be
* greater than the max size if a background deletion is pending.
*/
public synchronized long size() throws IOException {
initialize();
return size;
}
synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!fileSystem.exists(entry.dirtyFiles[i])) {
editor.abort();
return;
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.dirtyFiles[i];
if (success) {
if (fileSystem.exists(dirty)) {
File clean = entry.cleanFiles[i];
fileSystem.rename(dirty, clean);
long oldLength = entry.lengths[i];
long newLength = fileSystem.size(clean);
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
fileSystem.delete(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.writeUtf8(CLEAN).writeByte(' ');
journalWriter.writeUtf8(entry.key);
entry.writeLengths(journalWriter);
journalWriter.writeByte('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.writeUtf8(REMOVE).writeByte(' ');
journalWriter.writeUtf8(entry.key);
journalWriter.writeByte('\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
}
/**
* We only rebuild the journal when it will halve the size of the journal and eliminate at least
* 2000 ops.
*/
boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold
&& redundantOpCount >= lruEntries.size();
}
/**
* Drops the entry for {@code key} if it exists and can be removed. If the entry for {@code key}
* is currently being edited, that edit will complete normally but its value will not be stored.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) return false;
boolean removed = removeEntry(entry);
if (removed && size <= maxSize) mostRecentTrimFailed = false;
return removed;
}
boolean removeEntry(Entry entry) throws IOException {
if (entry.currentEditor != null) {
entry.currentEditor.detach(); // Prevent the edit from completing normally.
}
for (int i = 0; i < valueCount; i++) {
fileSystem.delete(entry.cleanFiles[i]);
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
lruEntries.remove(entry.key);
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
return true;
}
/**
* Returns true if this cache has been closed.
*/
public synchronized boolean isClosed() {
return closed;
}
private synchronized void checkNotClosed() {
if (isClosed()) {
throw new IllegalStateException("cache is closed");
}
}
/**
* Force buffered operations to the filesystem.
*/
@Override
public synchronized void flush() throws IOException {
if (!initialized) return;
checkNotClosed();
trimToSize();
journalWriter.flush();
}
/**
* Closes this cache. Stored values will remain on the filesystem.
*/
@Override
public synchronized void close() throws IOException {
if (!initialized || closed) {
closed = true;
return;
}
// Copying for safe iteration.
for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
closed = true;
}
void trimToSize() throws IOException {
while (size > maxSize) {
Entry toEvict = lruEntries.values().iterator().next();
removeEntry(toEvict);
}
mostRecentTrimFailed = false;
}
/**
* Closes the cache and deletes all of its stored values. This will delete all files in the cache
* directory including files that weren't created by the cache.
*/
public void delete() throws IOException {
close();
fileSystem.deleteContents(directory);
}
/**
* Deletes all stored values from the cache. In-flight edits will complete normally but their
* values will not be stored.
*/
public synchronized void evictAll() throws IOException {
initialize();
// Copying for safe iteration.
for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) {
removeEntry(entry);
}
mostRecentTrimFailed = false;
}
private void validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
throw new IllegalArgumentException(
"keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"");
}
}
/**
* 返回缓存当前项的迭代器。这个迭代器不会抛出{@code ConcurrentModificationException},
* 调用者必须{@link Snapshot#close}每个由{@link Iterator#next}返回的快照
* 如果做不到这一点,就会泄漏打开的文件,返回的迭代器支持 {@link Iterator#remove}.
*
* @return 返回迭代器
* @throws IOException 异常
*/
public synchronized Iterator snapshots() throws IOException {
initialize();
return new Iterator<>() {
/**
* 迭代条目的副本以防止并发修改错误
*/
final Iterator delegate = new ArrayList<>(lruEntries.values()).iterator();
/**
* 要从{@link #next}返回的快照。如果还没有计算出来,就是Null
*/
Snapshot nextSnapshot;
/**
* 要使用{@link #remove}删除的快照。如果删除是非法的,则为Null
*/
Snapshot removeSnapshot;
@Override
public boolean hasNext() {
if (nextSnapshot != null) return true;
synchronized (DiskLruCache.this) {
// 如果缓存关闭,则截断迭代器
if (closed) return false;
while (delegate.hasNext()) {
Entry entry = delegate.next();
if (!entry.readable) continue; // Entry during edit.
Snapshot snapshot = entry.snapshot();
if (snapshot == null) continue; // Evicted since we copied the entries.
nextSnapshot = snapshot;
return true;
}
}
return false;
}
@Override
public Snapshot next() {
if (!hasNext()) throw new NoSuchElementException();
removeSnapshot = nextSnapshot;
nextSnapshot = null;
return removeSnapshot;
}
@Override
public void remove() {
if (removeSnapshot == null) throw new IllegalStateException("remove() before next()");
try {
DiskLruCache.this.remove(removeSnapshot.key);
} catch (IOException ignored) {
// 这里没什么用。未能从缓存中删除。这很可能是因为无法更新日志,但是缓存的条目仍然没有了
} finally {
removeSnapshot = null;
}
}
};
}
/**
* 快照信息
*/
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final Source[] sources;
private final long[] lengths;
Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.sources = sources;
this.lengths = lengths;
}
public String key() {
return key;
}
/**
* Returns an editor for this snapshot's entry, or null if either the entry has changed since
* this snapshot was created or if another edit is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/**
* Returns the unbuffered stream with the value for {@code index}.
*/
public Source getSource(int index) {
return sources[index];
}
/**
* Returns the byte length of the value for {@code index}.
*/
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (Source in : sources) {
IoKit.close(in);
}
}
}
/**
* Edits the values for an entry.
*/
public final class Editor {
final Entry entry;
final boolean[] written;
private boolean done;
Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
/**
* Prevents this editor from completing normally. This is necessary either when the edit causes
* an I/O error, or if the target entry is evicted while this editor is active. In either case
* we delete the editor's created files and prevent new files from being created. Note that once
* an editor has been detached it is possible for another editor to edit the entry.
*/
void detach() {
if (entry.currentEditor == this) {
for (int i = 0; i < valueCount; i++) {
try {
fileSystem.delete(entry.dirtyFiles[i]);
} catch (IOException e) {
// This file is potentially leaked. Not much we can do about that.
}
}
entry.currentEditor = null;
}
}
/**
* Returns an unbuffered input stream to read the last committed value, or null if no value has
* been committed.
*/
public Source newSource(int index) {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (!entry.readable || entry.currentEditor != this) {
return null;
}
try {
return fileSystem.source(entry.cleanFiles[index]);
} catch (FileNotFoundException e) {
return null;
}
}
}
/**
* Returns a new unbuffered output stream to write the value at {@code index}. If the underlying
* output stream encounters errors when writing to the filesystem, this edit will be aborted
* when {@link #commit} is called. The returned output stream does not throw IOExceptions.
*/
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor != this) {
return IoKit.blackhole();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.dirtyFiles[index];
Sink sink;
try {
sink = fileSystem.sink(dirtyFile);
} catch (FileNotFoundException e) {
return IoKit.blackhole();
}
return new FaultHideSink(sink) {
@Override
protected void onException(IOException e) {
synchronized (DiskLruCache.this) {
detach();
}
}
};
}
}
/**
* Commits this edit so it is visible to readers. This releases the edit lock so another edit
* may be started on the same key.
*/
public void commit() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
completeEdit(this, true);
}
done = true;
}
}
/**
* 中止这个编辑。这释放了编辑锁,因此可以在同一个键上启动另一个编辑
*
* @throws IOException 异常
*/
public void abort() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
completeEdit(this, false);
}
done = true;
}
}
public void abortUnlessCommitted() {
synchronized (DiskLruCache.this) {
if (!done && entry.currentEditor == this) {
try {
completeEdit(this, false);
} catch (IOException ignored) {
}
}
}
}
}
private class Entry {
final String key;
/**
* Lengths of this entry's files.
*/
final long[] lengths;
final File[] cleanFiles;
final File[] dirtyFiles;
/**
* True if this entry has ever been published.
*/
boolean readable;
/**
* The ongoing edit or null if this entry is not being edited.
*/
Editor currentEditor;
/**
* The sequence number of the most recently committed edit to this entry.
*/
long sequenceNumber;
Entry(String key) {
this.key = key;
lengths = new long[valueCount];
cleanFiles = new File[valueCount];
dirtyFiles = new File[valueCount];
StringBuilder fileBuilder = new StringBuilder(key).append(Symbol.C_DOT);
int truncateTo = fileBuilder.length();
for (int i = 0; i < valueCount; i++) {
fileBuilder.append(i);
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}
/**
* Set lengths using decimal numbers like "10123".
*/
void setLengths(String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
try {
for (int i = 0; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
/**
* Append space-prefixed lengths to {@code writer}.
*/
void writeLengths(BufferSink writer) throws IOException {
for (long length : lengths) {
writer.writeByte(Symbol.C_SPACE).writeDecimalLong(length);
}
}
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
}
/**
* Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
* single published snapshot. If we opened streams lazily then the streams could come from
* different edits.
*/
Snapshot snapshot() {
if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
Source[] sources = new Source[valueCount];
long[] lengths = this.lengths.clone();
try {
for (int i = 0; i < valueCount; i++) {
sources[i] = fileSystem.source(cleanFiles[i]);
}
return new Snapshot(key, sequenceNumber, sources, lengths);
} catch (FileNotFoundException e) {
for (int i = 0; i < valueCount; i++) {
if (null != sources[i]) {
IoKit.close(sources[i]);
} else {
break;
}
}
try {
removeEntry(this);
} catch (IOException ignored) {
}
return null;
}
}
}
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
if (!initialized | closed) {
return;
}
try {
trimToSize();
} catch (IOException ignored) {
mostRecentTrimFailed = true;
}
try {
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
} catch (IOException e) {
mostRecentRebuildFailed = true;
journalWriter = IoKit.buffer(IoKit.blackhole());
}
}
}
};
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy