5 Stimmen

JavaFX WebEngine warten Sie darauf, dass Ajax abgeschlossen ist

Ich entwickle eine Daten-Mining-Anwendung in JavaFX, die auf dem WebView (und somit auch dem WebEngine) basiert. Das Mining erfolgt in 2 Schritten: Zuerst verwendet der Benutzer die Benutzeroberfläche, um auf einer Website im WebView zu navigieren und zu konfigurieren, wo interessante Daten gesucht werden können. Zweitens lädt ein Hintergrundtask, der regelmäßig ausgeführt wird, ein WebEngine das gleiche Dokument und versucht, die Daten aus dem geladenen Dokument zu extrahieren.

Dies funktioniert für die meisten Fälle einwandfrei, aber kürzlich bin ich auf Probleme mit Seiten gestoßen, die AJAX verwenden, um Inhalte zu rendern. Um zu überprüfen, ob der WebEngine das Dokument geladen hat, höre ich auf das loadWorker's stateProperty. Wenn der Zustand erfolgreich wird, weiß ich, dass das Dokument geladen ist (zusammen mit etwaigem Javascript, das bei document.ready() oder Ähnlichem ausgeführt werden könnte). Dies liegt daran, dass Javascript auf dem JavaFX-Thread ausgeführt wird, wenn ich mich nicht irre (Quelle: https://blogs.oracle.com/javafx/entry/communicating_between_javascript_and_javafx). Wenn jedoch ein AJAX-Aufruf gestartet wird, wird die Javascript-Ausführung beendet und der Motor informiert mich, dass das Dokument bereit ist, obwohl es offensichtlich nicht so ist, da sich der Inhalt aufgrund des ausstehenden AJAX-Aufrufs noch ändern könnte.

Gibt es einen Weg, dies zu umgehen, um eine Benachrichtigung zu erhalten, wenn AJAX-Aufrufe beendet sind? Ich habe versucht, einen Standard-Completion-Handler in $.ajaxSetup() zu installieren, aber das ist ziemlich fragwürdig, weil wenn ein AJAX-Aufruf den Completion-Handler überschreibt, der Standard nicht aufgerufen wird. Außerdem kann ich dies nur einfügen, nachdem das Dokument erstmalig geladen wurde (und zu diesem Zeitpunkt könnten bereits einige AJAX-Aufrufe ausgeführt werden). Ich habe diese Injektion mit einem Upcall getestet und sie funktioniert gut für AJAX-Anrufe, die auf Befehl gestartet werden (nach der Injektion des Standard-Handlers), die keinen eigenen Completion-Handler bereitstellen.

Ich suche nach zwei Dingen: Erstens: Eine generische Möglichkeit, sich in den Completion-Handler von AJAX-Aufrufen einzuklinken, und Zweitens: Eine Möglichkeit, auf den WebEngine zu warten, bis alle AJAX-Aufrufe beendet sind und mich anschließend zu benachrichtigen.

5voto

MightyMalcolm Punkte 191

Erklärung

Ich hatte auch dieses Problem und löste es, indem ich meine eigene Implementierung von sun.net.www.protocol.http.HttpURLConnection bereitstellte, die ich zur Verarbeitung von AJAX-Anfragen verwende. Meine Klasse, praktischerweise AjaxHttpURLConnection genannt, klinkt sich in die Funktion getInputStream() ein, gibt jedoch nicht ihren originalen Eingabestrom zurück. Stattdessen gebe ich eine Instanz von PipedInputStream an die WebEngine zurück. Dann lese ich alle Daten aus dem originalen Eingabestrom und leite sie an meinen Piped-Stream weiter. Auf diese Weise erhalte ich 2 Vorteile:

  1. Ich weiß, wann das letzte Byte empfangen wurde und somit die AJAX-Anfrage vollständig verarbeitet wurde.
  2. Ich kann sogar alle eingehenden Daten abfangen und bereits damit arbeiten (wenn ich wollte).

Beispiel

Zunächst müssen Sie Java mitteilen, dass es Ihre URL-Verbindungsimplementierung verwenden soll, anstelle der Standardimplementierung. Hierzu müssen Sie ihm Ihre eigene Version von URLStreamHandlerFactory bereitstellen. Sie können viele Threads hier auf SO (zum Beispiel dieser) oder über Google zu diesem Thema finden. Um Ihre Factory-Instanz festzulegen, platzieren Sie folgendes frühzeitig in Ihrer main-Methode. So sieht meine aus.

import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;

public class MeineAnwendung extends Anwendung {

    // ...

    public static void main(String[] args) {
        URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
            public URLStreamHandler createURLStreamHandler(String protocol) {
                if ("http".equals(protocol)) {
                    return new MyUrlConnectionHandler();    
                }
                return null; // Lassen Sie die Standardhandler damit umgehen, was auch immer hier kommt (z. B. https, jar, ...)
            }
        });
        launch(args);
    }
}

Zweitens müssen wir einen eigenen Handler erstellen, der dem Programm mitteilt, wann welche Art von URLConnection verwendet werden soll.

import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;

import sun.net.www.protocol.http.Handler;
import sun.net.www.protocol.http.HttpURLConnection;

public class MyUrlConnectionHandler extends Handler {

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

        if (url.toString().contains("ajax=1")) {
            return new AjaxHttpURLConnection(url, proxy, this);
        }

        // Gibt eine Standard-HttpURLConnection-Instanz zurück.
        return new HttpURLConnection(url, proxy);
    }
}

Zu guter Letzt, hier kommt die AjaxHttpURLConnection.

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.Proxy;
import java.net.URL;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.io.IOUtils;

import sun.net.www.protocol.http.Handler;
import sun.net.www.protocol.http.HttpURLConnection;

public class AjaxHttpURLConnection extends HttpURLConnection {

    private PipedInputStream pipedIn;
    private ReentrantLock lock;

    protected AjaxHttpURLConnection(URL url, Proxy proxy, Handler handler) {
        super(url, proxy, handler);
        this.pipedIn = null;
        this.lock = new ReentrantLock(true);
    }

    @Override
    public InputStream getInputStream() throws IOException {

        lock.lock();
        try {

            // Müssen wir unseren eigenen Eingabestream einrichten?
            if (pipedIn == null) {

                PipedOutputStream pipedOut = new PipedOutputStream();
                pipedIn = new PipedInputStream(pipedOut);

                InputStream in = super.getInputStream();
                /*
                 * Vorsicht hier! Aus irgendeinem Grund ruft die getInputStream-Methode sich selbst auf (keine Ahnung warum). Daher, wenn wir pipedIn nicht vor dem Aufruf von super.getInputStream() gesetzt haben, geraten wir in eine Schleife oder in EOFExceptions!
                 */

                // TODO: Timeout?
                new Thread(new Runnable() {
                    public void run() {
                        try {

                            // Leiten Sie die originalen Daten an den Browser weiter.
                            byte[] data = IOUtils.toByteArray(in);
                            pipedOut.write(data);
                            pipedOut.flush();
                            pipedOut.close();

                            // Etwas mit den Daten machen? Zum Beispiel, sie entschlüsseln, wenn sie gzipped waren.

                            // Signalisieren, dass der Browser fertig ist.

                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } finally {
            lock.unlock();
        }
        return pipedIn;
    }
}

Weitere Überlegungen

  • Wenn Sie mehrere WebEngine-Objekte verwenden, könnte es schwierig sein zu erkennen, welches tatsächlich die URLConnection geöffnet hat und somit welcher Browser fertig geladen hat.
  • Sie haben vielleicht bemerkt, dass ich mich nur mit http-Verbindungen befasst habe. Ich habe nicht getestet, inwieweit mein Ansatz auf https usw. übertragen werden könnte (bin kein Experte hier :O).
  • Wie Sie gesehen haben, ist mein einziger Anhaltspunkt, wann ich tatsächlich meinen AjaxHttpURLConnection verwenden soll, wenn die entsprechende URL ajax=1 enthält. In meinem Fall war das ausreichend. Da ich jedoch nicht so gut mit html und http umgehen kann, weiß ich nicht, ob die WebEngine AJAX-Anfragen auf eine andere Weise stellen kann (z. B. die Headerfelder?). Wenn Sie unsicher sind, könnten Sie einfach immer eine Instanz unserer modifizierten URL-Verbindung zurückgeben, aber das würde natürlich einige Overhead bedeuten.
  • Wie am Anfang erwähnt, können Sie sofort mit den Daten arbeiten, sobald sie aus dem Eingabestream abgerufen wurden, wenn Sie dies wünschen. Sie können die Anfragedaten, die Ihre WebEngine sendet, auf ähnliche Weise abfangen. Wickeln Sie einfach die Funktion getOutputStream() ein und platzieren Sie einen weiteren Zwischenstrom, um zu erfassen, was gesendet wird, und leiten Sie es dann an den ursprünglichen Ausgabestrom weiter.

0voto

FLUXparticle Punkte 511

Dies ist eine Erweiterung der Antwort von @dadoosh...

Das Durchführen dieses Vorgangs für HTTPS ist ein Albtraum der Delegation, da HttpsURLConnection (Impl) nicht einfach instanziiert werden kann wie HttpURLConnection

import sun.net.www.protocol.https.Handler;

public class MyStreamHandler extends Handler {

    @Override
    protected URLConnection openConnection(URL url) throws IOException {
        URLConnection connection = super.openConnection(url);
        if (url.toString().contains("ajax=1")) {
            return new MyConnection((HttpsURLConnection) connection);
        } else {
            return connection;
        }
    }
}

Also hole ich mir die Verbindung zurück, die zurückgegeben worden wäre, und falls nötig gebe ich sie an MyConnection weiter, damit es alle Aufrufe delegieren und die Methode getInputStream() modifizieren kann.

Übrigens habe ich eine weitere Lösung zum Erkennen des Endes einer Ajax-Anforderung gefunden. Ich warte einfach darauf, dass die Methode close() aufgerufen wird:

@Override
public synchronized InputStream getInputStream() throws IOException {
    if (cachedInputStream != null) {
        return cachedInputStream;
    }

    System.out.println("Öffnen " + getURL());
    InputStream inputStream = delegate.getInputStream();

    cachedInputStream = new FilterInputStream(inputStream) {
        @Override
        public void close() throws IOException {
            super.close();
            // Signal, dass der Browser fertig ist.
        }
    };

    return cachedInputStream;
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X