JavaFx 8 WebEngine - Кеширане на изображения чрез директория с потребителски данни?

Аз съм нов в JavaFx и се опитах да внедря неговия браузър в моето приложение. Тъй като презареждането на всички изображения при всяко ново стартиране отнема доста време, бих искал да ги съхраня в някаква кеш директория, но не успявам да го направя. Опитах да използвам setUserDataDirectory(...), но това води само до създаване на празна папка, наречена localstorage, и празен .lock файл.

След това намерих тази SO тема, но първо, все още не ми е разрешено да коментирам там и второ , изглежда, че адресира само JavaFx 2.2. Публикуваното там все още ли е вярно за JavaFx 8? Ако е така: има ли прост начин за внедряване на такъв URLConnection кеш?

Много благодаря за всяка помощ :)


person MightyMalcolm    schedule 21.09.2015    source източник


Отговори (2)


И така, след 2 дни ровене в проблема и научаване за класа URLConnection, най-накрая успях да измисля моя собствена (вероятно груба) реализация, която бих искал да споделя тук, в случай че някой с толкова малко познания като мен се случи да се спъвам в тази тема. Отново основната идея беше да се поставят само определени типове файлове в кеша, а не всичко. Избрах да съхранявам jpg, png, gif и js файлове, тъй като ги смятах за най-натоварващи, въпреки че всеки друг файлов формат също трябва да е възможен. На първо място: browser.getEngine().setUserDataDirectory(...) определено НЕ върши работата. Все още нямам представа за какво е добър, но със сигурност не съхранява файлове с изображения %)

Вместо това, това, което направих, беше основно създаване на 5 класа:

  • CachedResource: Състои се от byte[] масив, съдържащ необработените данни на ресурса и някаква мета информация (заглавни полета, lastModified)
  • ResourceCache: Съдържа всички текущо кеширани ресурсни обекти.
  • MyHttpUrlConnection (разширява sun.net.www.protocol.http.HttpURLConnection): Клас обвивка, който отговаря за извличането на файла, към който сочи дадения му URL адрес. Той прави цялата мрежова магия.
  • CachedUrlConnection (разширява java.net.URLConnection): (почти) празна реализация, която вече има всички необходими данни и само чака системата да я извика.
  • MyUrlConnectionHandler (разширява sun.net.www.protocol.http.Handler): Този клас се регистрира при стартиране на приложението и решава кога да използва кой URLConnection (вижте по-долу).

Класовете ResourceCache, CachedResource и CachedUrlConnection са доста малки и лесни за писане. Проектирах кеша на ресурсите, за да съпоставя ресурса на url със съответния му CachedResource обект, по този начин: ConcurrentHashMap<URL, CachedResource> плюс функция за получаване и addResource(...). Добавих някои други неща към него като съхраняване на файлове локално, но това води извън темата.

След това внедрих класа CachedUrlConnection както следва:

public class CachedUrlConnection extends URLConnection {

    private CachedResource resource;
    private ByteArrayInputStream inputStream;

    /* Constructors */
    public CachedUrlConnection(URL url, CachedResource resource) throws IOException {
        super(url);
        this.resource = resource;
        this.inputStream = new ByteArrayInputStream(resource.getByteData());
    }

    @Override
    public void connect() throws IOException {
        // No need to do anything.
    }

    /* Object Methods */

    /* Getters and Setters */
    @Override
    public String getHeaderField(int index) { ... }

    @Override
    public String getHeaderField(String key) { ... }

    @Override
    public Map<String, List<String>> getHeaderFields() { ... }

    @Override
    public InputStream getInputStream() throws IOException {
        return inputStream; // <---- Here, the system can grab the data.
    }
}

Когато разглеждате изходния код на URLConnection (например тук), бързо ще забележите, че повечето имплементации на метода са фиктивни, които или връщат null, или хвърлят UnknownServiceException.

Това е важно: не знам точно кое от тях трябва да внедрите!

За да разбера, използвах класа MyHttpUrlConnection и добавих към почти всяка функция

System.out.println("function xyz called!");
super.xyz();

но ме мързеше и не ги проверих всичките. Засега изглежда всичко работи добре %)

Следващият клас беше MyHttpUrlConnection. Не съм 100% сигурен дали наистина трябваше да презапиша класа HttpURLConnection, но все пак го направих, защото той има конструктор protected, който имплицитно ще бъде извикан с нов sun.net.www.protocol.http.Handler. Този манипулатор очевидно няма да следва нашата http политика, така че просто исках да съм сигурен (вж. изходен код ред 801). Така класът изглежда доста празен:

public class MyHttpUrlConnection extends HttpURLConnection {

    protected MyHttpUrlConnection(URL url, Handler handler) {
        this(url, null, handler);
    }

    public MyHttpUrlConnection(URL url, Proxy proxy) {
        this(url, proxy, new MyUrlConnectionHandler()); // <--- No way sneaking around^^
    }

    protected MyHttpUrlConnection(URL url, Proxy proxy, Handler handler) {
        super(url, proxy, handler);
    }

    public MyHttpUrlConnection(URL url, String host, int port) {
        this(url, new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(host, port))); // Taken over from the HttpURLConnection sourcecode.
    }
}

Сега идва най-важната част: MyUrlConnectionHandler. Отново проверете тази нишка за това къде да го поставите. Всичко, от което се нуждае самият клас, е да презапише функцията openConnection(URL, Proxy). Преди да публикувам моя код, ще ви дам преглед на това, което прави.

  1. Ако даденият URL адрес е jpg, png, ... файл:
  2. Използвайте обект MyHttpUrlConnection, за да получите последната дата на промяна на ресурса на сървъра. това трябва да извиква само заглавката, а НЕ целия ресурс. Иначе нямаше да спечелим нищо. Кредитите отиват в тази тема. Не съм напълно сигурен обаче дали затварям URLConnection правилно тук. По-добре проверете отново, ако се съмнявате;)
  3. Ако няма ресурс в кеша ИЛИ ресурсът в кеша е остарял:
  4. Затворете мини връзката и отворете „правилна“, за да изтеглите всичко.
  5. Създайте нов CachedResource обект и го добавете към кеша.
  6. Затворете и тази нова връзка.
  7. Върнете нов CachedUrlConnection обект, който съдържа данните. Това може да изглежда малко глупаво, тъй като вече имаме всичко, но функцията трябва да върне URLConnection.
  8. Ако има някакво изключение или просто не сме работили с jpg, png, ... файл, върнете "по подразбиране" MyHttpUrlConnection обект, за да обработите URL нормално.

И съответният код изглежда по следния начин. Имайте предвид, че използвах Apache org.apache.commons.io.IOUtils:

@Override
protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {

    try {

        // Is this some resource that we'd like to cache?
        if (ResourceCache.isCachableURL(url)) {

            // Retrieve whatever is in the cache first.
            ResourceCache cache = ResourceCache.getInstance();
            CachedResource resource = cache.getCachedResource(url);

            // Open a connection to the server to at least check for the last-modified field.
            MyHttpUrlConnection conn = new MyHttpUrlConnection(url, this); // Don't use URL#openConnection to avoid looping!
            conn.setRequestMethod("HEAD");
            conn.connect();
            long lastModified = conn.getLastModified();

            // Did we get the last-modified value at all?
            if (lastModified == 0) {
                throw new Exception("No last-modified value could be read! \n\t" + url);
            }

            // Resource not cached or out of date?
            if (resource == null || resource.getLastModified() < lastModified) {

                conn = new MyHttpUrlConnection(url, this);
                conn.connect();
                InputStream input = conn.getInputStream();
                byte[] data = IOUtils.toByteArray(input);
                Map<String, List<String>> headerFields = conn.getHeaderFields();
                IOUtils.closeQuietly(input); 

                resource = new CachedResource(url.getFile(), data, headerFields, lastModified); // I use url.getFile() to store the file on my hard drive.
                cache.addCachedResource(url, resource);
            }

            return new CachedUrlConnection(url, resource);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    // Return the default HttpURLConnection in our wrapper class.
    return new MyHttpUrlConnection(url, proxy, this);
}

Едно последно нещо: За по-сигурно, НИКОГА не използвайте метода URL#openConnection във функцията MyUrlConnectionHandler#openConnection. Записвайки го по този начин, става доста очевидно защо, но днес ми отне доста време, за да разбера откъде идва безкрайният цикъл %) Използвайте конструктора и извикайте connect() вместо това.

Надявам се, че това някога ще помогне на някого, иначе беше добро упражнение за мен ^^

person MightyMalcolm    schedule 23.09.2015

Доколкото знам това все още е валидно. Ако искате да приложите това само за конкретна цел, не е толкова трудно, но решение с общо предназначение би било по-предизвикателно.

person mipa    schedule 21.09.2015
comment
Е, засега ще се радвам, ако мога поне да съхранявам тежките файлове с изображения на моя твърд диск и след това да ги зареждам от там. Нямам представа обаче как да го направя. Би ли било възможно да разширя net.www.protocol.http.Handler или HttpURLConnection и просто да променя малко от тяхната функционалност, за да отговарят на моите нужди? (Трябва да поддържам само http в момента.) - person MightyMalcolm; 21.09.2015