All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.aoju.bus.http.cache.Cache Maven / Gradle / Ivy

/*********************************************************************************
 *                                                                               *
 * 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.ByteString;
import org.aoju.bus.core.io.FileSystem;
import org.aoju.bus.core.io.buffer.Buffer;
import org.aoju.bus.core.io.sink.AssignSink;
import org.aoju.bus.core.io.sink.BufferSink;
import org.aoju.bus.core.io.sink.Sink;
import org.aoju.bus.core.io.source.AssignSource;
import org.aoju.bus.core.io.source.BufferSource;
import org.aoju.bus.core.io.source.Source;
import org.aoju.bus.core.lang.Header;
import org.aoju.bus.core.lang.Http;
import org.aoju.bus.core.lang.MediaType;
import org.aoju.bus.core.lang.Symbol;
import org.aoju.bus.core.net.tls.TlsVersion;
import org.aoju.bus.core.toolkit.IoKit;
import org.aoju.bus.http.*;
import org.aoju.bus.http.accord.platform.Platform;
import org.aoju.bus.http.bodys.ResponseBody;
import org.aoju.bus.http.metric.http.StatusLine;
import org.aoju.bus.http.secure.CipherSuite;
import org.aoju.bus.http.socket.Handshake;

import java.io.Closeable;
import java.io.File;
import java.io.Flushable;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.*;

/**
 * 缓存HTTP和HTTPS对文件系统的响应,以便可以重用它们,从而节省时间和带宽.
 *
 * @author Kimi Liu
 * @since Java 17+
 */
public class Cache implements Closeable, Flushable {

    private static final int VERSION = 201105;
    private static final int ENTRY_METADATA = 0;
    private static final int ENTRY_BODY = 1;
    private static final int ENTRY_COUNT = 2;
    final DiskLruCache cache;
    int writeSuccessCount;
    int writeAbortCount;
    private int networkCount;
    private int hitCount;
    private int requestCount;

    public final InternalCache internalCache = new InternalCache() {

        @Override
        public Response get(Request request) {
            return Cache.this.get(request);
        }

        @Override
        public CacheRequest put(Response response) {
            return Cache.this.put(response);
        }

        @Override
        public void remove(Request request) throws IOException {
            Cache.this.remove(request);
        }

        @Override
        public void update(Response cached, Response network) {
            Cache.this.update(cached, network);
        }

        @Override
        public void trackConditionalCacheHit() {
            Cache.this.trackConditionalCacheHit();
        }

        @Override
        public void trackResponse(CacheStrategy cacheStrategy) {
            Cache.this.trackResponse(cacheStrategy);
        }
    };

    /**
     * 在{@code directory}中创建最多{@code maxSize}字节的缓存
     *
     * @param directory 目录
     * @param maxSize   缓存的最大大小(以字节为单位)
     */
    public Cache(File directory, long maxSize) {
        this(directory, maxSize, FileSystem.SYSTEM);
    }

    public Cache(File directory, long maxSize, FileSystem fileSystem) {
        this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
    }

    public static String key(UnoUrl url) {
        return ByteString.encodeUtf8(url.toString()).md5().hex();
    }

    static int readInt(BufferSource source) throws IOException {
        try {
            long result = source.readDecimalLong();
            String line = source.readUtf8LineStrict();
            if (result < 0 || result > Integer.MAX_VALUE || !line.isEmpty()) {
                throw new IOException("expected an int but was \"" + result + line + "\"");
            }
            return (int) result;
        } catch (NumberFormatException e) {
            throw new IOException(e.getMessage());
        }
    }

    Response get(Request request) {
        String key = key(request.url());
        DiskLruCache.Snapshot snapshot;
        Entry entry;
        try {
            snapshot = cache.get(key);
            if (null == snapshot) {
                return null;
            }
        } catch (IOException e) {
            // 放弃,因为缓存无法读取
            return null;
        }

        try {
            entry = new Entry(snapshot.getSource(ENTRY_METADATA));
        } catch (IOException e) {
            IoKit.close(snapshot);
            return null;
        }

        Response response = entry.response(snapshot);

        if (!entry.matches(request, response)) {
            IoKit.close(response.body());
            return null;
        }

        return response;
    }

    CacheRequest put(Response response) {
        String requestMethod = response.request().method();

        if (Http.invalidatesCache(response.request().method())) {
            try {
                remove(response.request());
            } catch (IOException ignored) {
                // 无法写入缓存
            }
            return null;
        }
        if (!Http.GET.equals(requestMethod)) {
            // 不要缓存非get响应。从技术上讲,我们可以缓存HEAD请求和POST请求,但是这样做的复杂性很高,好处很少
            return null;
        }

        if (Headers.hasVaryAll(response)) {
            return null;
        }

        Entry entry = new Entry(response);
        DiskLruCache.Editor editor = null;
        try {
            editor = cache.edit(key(response.request().url()));
            if (null == editor) {
                return null;
            }
            entry.writeTo(editor);
            return new CacheRequestImpl(editor);
        } catch (IOException e) {
            abortQuietly(editor);
            return null;
        }
    }

    void remove(Request request) throws IOException {
        cache.remove(key(request.url()));
    }

    void update(Response cached, Response network) {
        Entry entry = new Entry(network);
        DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
        DiskLruCache.Editor editor = null;
        try {
            // 如果快照不是当前的,则返回null
            editor = snapshot.edit(); // Returns null if snapshot is not current.
            if (editor != null) {
                entry.writeTo(editor);
                editor.commit();
            }
        } catch (IOException e) {
            abortQuietly(editor);
        }
    }

    private void abortQuietly(DiskLruCache.Editor editor) {
        // 放弃,因为缓存无法写入
        try {
            if (null != editor) {
                editor.abort();
            }
        } catch (IOException ignored) {
        }
    }

    /**
     * 始化缓存。这将包括从存储器中读取日志文件并构建必要的内存缓存信息
     * 注意,如果应用程序选择不调用此方法来初始化缓存。默认情况下,将在第一次使用缓存时执行延迟初始化
     *
     * @throws IOException 初始化异常
     */
    public void initialize() throws IOException {
        cache.initialize();
    }

    /**
     * 关闭缓存并删除其所有存储值。这将删除缓存目录中的所有文件,包括没有由缓存创建的文件
     *
     * @throws IOException 删除异常
     */
    public void delete() throws IOException {
        cache.delete();
    }

    /**
     * 删除缓存中存储的所有值。缓存中的写操作将正常完成,但不会存储相应的响应
     *
     * @throws IOException 清除异常
     */
    public void evictAll() throws IOException {
        cache.evictAll();
    }


    /**
     * 在此缓存中的url上返回一个迭代器,该迭代器支持{@linkplain Iterator#remove}。
     * 从迭代器中删除URL将从缓存中删除相应的响应。使用此来清除选定的响应
     *
     * @return 迭代器
     * @throws IOException 异常
     */
    public Iterator urls() throws IOException {
        return new Iterator<>() {
            final Iterator delegate = cache.snapshots();

            String nextUrl;
            boolean canRemove;

            @Override
            public boolean hasNext() {
                if (null != nextUrl) {
                    return true;
                }

                canRemove = false;
                // 删除()在错误的内容
                while (delegate.hasNext()) {
                    try (DiskLruCache.Snapshot snapshot = delegate.next()) {
                        BufferSource metadata = IoKit.buffer(snapshot.getSource(ENTRY_METADATA));
                        nextUrl = metadata.readUtf8LineStrict();
                        return true;
                    } catch (IOException ignored) {
                        // 无法读取此快照的元数据;可能是因为主机文件系统已经消失了!跳过它
                    }
                }

                return false;
            }

            @Override
            public String next() {
                if (!hasNext()) throw new NoSuchElementException();
                String result = nextUrl;
                nextUrl = null;
                canRemove = true;
                return result;
            }

            @Override
            public void remove() {
                if (!canRemove) throw new IllegalStateException("remove() before next()");
                delegate.remove();
            }
        };
    }

    public synchronized int writeAbortCount() {
        return writeAbortCount;
    }

    public synchronized int writeSuccessCount() {
        return writeSuccessCount;
    }

    public long size() throws IOException {
        return cache.size();
    }

    public long maxSize() {
        return cache.getMaxSize();
    }

    @Override
    public void flush() throws IOException {
        cache.flush();
    }

    @Override
    public void close() throws IOException {
        cache.close();
    }

    public File directory() {
        return cache.getDirectory();
    }

    public boolean isClosed() {
        return cache.isClosed();
    }

    synchronized void trackResponse(CacheStrategy cacheStrategy) {
        requestCount++;

        if (null != cacheStrategy.networkRequest) {
            // 如果这是一个条件请求,我们将增加hitCount如果/当它命中。
            networkCount++;
        } else if (null != cacheStrategy.cacheResponse) {
            // 此响应使用缓存而不是网络。这就是缓存命中
            hitCount++;
        }
    }

    synchronized void trackConditionalCacheHit() {
        hitCount++;
    }

    public synchronized int networkCount() {
        return networkCount;
    }

    public synchronized int hitCount() {
        return hitCount;
    }

    public synchronized int requestCount() {
        return requestCount;
    }

    private static class Entry {
        /**
         * 合成响应标头:请求发送时的本地时间
         */
        private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";

        /**
         * 合成响应标头:接收到响应的本地时间
         */
        private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";

        private final String url;
        private final Headers varyHeaders;
        private final String requestMethod;
        private final Protocol protocol;
        private final int code;
        private final String message;
        private final Headers responseHeaders;
        private final Handshake handshake;
        private final long sentRequestMillis;
        private final long receivedResponseMillis;

        /**
         * 从输入流中读取项。一个典型的案例:
         * 
{@code
         *   http://google.com/foo
         *   GET
         *   2
         *   Accept-Language: fr-CA
         *   Accept-Charset: UTF-8
         *   HTTP/1.1 200 OK
         *   3
         *   Content-Type: image/png
         *   Content-Length: 100
         *   Cache-Control: max-age=600
         * }
* *

HTTPS文件是这样的: *

{@code
         *   https://google.com/foo
         *   GET
         *   2
         *   Accept-Language: fr-CA
         *   Accept-Charset: UTF-8
         *   HTTP/1.1 200 OK
         *   3
         *   Content-Type: image/png
         *   Content-Length: 100
         *   Cache-Control: max-age=600
         *
         *   AES_256_WITH_MD5
         *   2
         *   base64-encoded peerCertificate[0]
         *   base64-encoded peerCertificate[1]
         *   -1
         *   TLSv1.2
         * }
* * @param in 输入流 * @throws IOException 异常 */ Entry(Source in) throws IOException { try { BufferSource source = IoKit.buffer(in); url = source.readUtf8LineStrict(); requestMethod = source.readUtf8LineStrict(); Headers.Builder varyHeadersBuilder = new Headers.Builder(); int varyRequestHeaderLineCount = readInt(source); for (int i = 0; i < varyRequestHeaderLineCount; i++) { varyHeadersBuilder.addLenient(source.readUtf8LineStrict()); } varyHeaders = varyHeadersBuilder.build(); StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); protocol = statusLine.protocol; code = statusLine.code; message = statusLine.message; Headers.Builder responseHeadersBuilder = new Headers.Builder(); int responseHeaderLineCount = readInt(source); for (int i = 0; i < responseHeaderLineCount; i++) { responseHeadersBuilder.addLenient(source.readUtf8LineStrict()); } String sendRequestMillisString = responseHeadersBuilder.get(SENT_MILLIS); String receivedResponseMillisString = responseHeadersBuilder.get(RECEIVED_MILLIS); responseHeadersBuilder.removeAll(SENT_MILLIS); responseHeadersBuilder.removeAll(RECEIVED_MILLIS); sentRequestMillis = null != sendRequestMillisString ? Long.parseLong(sendRequestMillisString) : 0L; receivedResponseMillis = null != receivedResponseMillisString ? Long.parseLong(receivedResponseMillisString) : 0L; responseHeaders = responseHeadersBuilder.build(); if (isHttps()) { String blank = source.readUtf8LineStrict(); if (blank.length() > 0) { throw new IOException("expected \"\" but was \"" + blank + "\""); } String cipherSuiteString = source.readUtf8LineStrict(); CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString); List peerCertificates = readCertificateList(source); List localCertificates = readCertificateList(source); TlsVersion tlsVersion = !source.exhausted() ? TlsVersion.forJavaName(source.readUtf8LineStrict()) : TlsVersion.SSL_3_0; handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates); } else { handshake = null; } } finally { in.close(); } } Entry(Response response) { this.url = response.request().url().toString(); this.varyHeaders = Headers.varyHeaders(response); this.requestMethod = response.request().method(); this.protocol = response.protocol(); this.code = response.code(); this.message = response.message(); this.responseHeaders = response.headers(); this.handshake = response.handshake(); this.sentRequestMillis = response.sentRequestAtMillis(); this.receivedResponseMillis = response.receivedResponseAtMillis(); } public void writeTo(DiskLruCache.Editor editor) throws IOException { BufferSink sink = IoKit.buffer(editor.newSink(ENTRY_METADATA)); sink.writeUtf8(url) .writeByte(Symbol.C_LF); sink.writeUtf8(requestMethod) .writeByte(Symbol.C_LF); sink.writeDecimalLong(varyHeaders.size()) .writeByte(Symbol.C_LF); for (int i = 0, size = varyHeaders.size(); i < size; i++) { sink.writeUtf8(varyHeaders.name(i)) .writeUtf8(": ") .writeUtf8(varyHeaders.value(i)) .writeByte(Symbol.C_LF); } sink.writeUtf8(new StatusLine(protocol, code, message).toString()) .writeByte(Symbol.C_LF); sink.writeDecimalLong(responseHeaders.size() + 2) .writeByte(Symbol.C_LF); for (int i = 0, size = responseHeaders.size(); i < size; i++) { sink.writeUtf8(responseHeaders.name(i)) .writeUtf8(": ") .writeUtf8(responseHeaders.value(i)) .writeByte(Symbol.C_LF); } sink.writeUtf8(SENT_MILLIS) .writeUtf8(": ") .writeDecimalLong(sentRequestMillis) .writeByte(Symbol.C_LF); sink.writeUtf8(RECEIVED_MILLIS) .writeUtf8(": ") .writeDecimalLong(receivedResponseMillis) .writeByte(Symbol.C_LF); if (isHttps()) { sink.writeByte(Symbol.C_LF); sink.writeUtf8(handshake.cipherSuite().javaName()) .writeByte(Symbol.C_LF); writeCertList(sink, handshake.peerCertificates()); writeCertList(sink, handshake.localCertificates()); sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte(Symbol.C_LF); } sink.close(); } private boolean isHttps() { return url.startsWith(Http.HTTPS_PREFIX); } private List readCertificateList(BufferSource source) throws IOException { int length = readInt(source); if (length == -1) return Collections.emptyList(); try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); List result = new ArrayList<>(length); for (int i = 0; i < length; i++) { String line = source.readUtf8LineStrict(); Buffer bytes = new Buffer(); bytes.write(ByteString.decodeBase64(line)); result.add(certificateFactory.generateCertificate(bytes.inputStream())); } return result; } catch (CertificateException e) { throw new IOException(e.getMessage()); } } private void writeCertList(BufferSink sink, List certificates) throws IOException { try { sink.writeDecimalLong(certificates.size()) .writeByte(Symbol.C_LF); for (int i = 0, size = certificates.size(); i < size; i++) { byte[] bytes = certificates.get(i).getEncoded(); String line = ByteString.of(bytes).base64(); sink.writeUtf8(line) .writeByte(Symbol.C_LF); } } catch (CertificateEncodingException e) { throw new IOException(e.getMessage()); } } public boolean matches(Request request, Response response) { return url.equals(request.url().toString()) && requestMethod.equals(request.method()) && Headers.varyMatches(response, varyHeaders, request); } public Response response(DiskLruCache.Snapshot snapshot) { String mediaType = responseHeaders.get(Header.CONTENT_TYPE); String length = responseHeaders.get(Header.CONTENT_LENGTH); Request request = new Request.Builder() .url(url) .method(requestMethod, null) .headers(varyHeaders) .build(); return new Response.Builder() .request(request) .protocol(protocol) .code(code) .message(message) .headers(responseHeaders) .body(new CacheResponseBody(snapshot, mediaType, length)) .handshake(handshake) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(receivedResponseMillis) .build(); } } private static class CacheResponseBody extends ResponseBody { final DiskLruCache.Snapshot snapshot; private final BufferSource bodySource; private final String mediaType; private final String length; CacheResponseBody(final DiskLruCache.Snapshot snapshot, String mediaType, String length) { this.snapshot = snapshot; this.mediaType = mediaType; this.length = length; Source source = snapshot.getSource(ENTRY_BODY); bodySource = IoKit.buffer(new AssignSource(source) { @Override public void close() throws IOException { snapshot.close(); super.close(); } }); } @Override public MediaType mediaType() { return null != mediaType ? MediaType.valueOf(mediaType) : null; } @Override public long length() { try { return null != length ? Long.parseLong(length) : -1; } catch (NumberFormatException e) { return -1; } } @Override public BufferSource source() { return bodySource; } } private class CacheRequestImpl implements CacheRequest { private final DiskLruCache.Editor editor; boolean done; private Sink cacheOut; private Sink body; CacheRequestImpl(final DiskLruCache.Editor editor) { this.editor = editor; this.cacheOut = editor.newSink(ENTRY_BODY); this.body = new AssignSink(cacheOut) { @Override public void close() throws IOException { synchronized (Cache.this) { if (done) { return; } done = true; writeSuccessCount++; } super.close(); editor.commit(); } }; } @Override public void abort() { synchronized (Cache.this) { if (done) { return; } done = true; writeAbortCount++; } IoKit.close(cacheOut); try { editor.abort(); } catch (IOException ignored) { } } @Override public Sink body() { return body; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy