Schlagwort-Archive: Java

Mehrere Rückgabewerte eines HTTP-Requests

Manchmal ist es notwendig, dass ein Webservice mehrere Ergebnisse nacheinander zurückliefert. Zum Beispiel, wenn ein Request abgesetzt wird, der zunächst validiert werden muss, bevor tatsächlich eine Prozessierung beginnt und die Validierung selbst eine längere Zeit dauert. Dann soll der Server nach Möglichkeit den Request direkt beenden und die Validierung im Hintergrund ablaufen. Andernfalls ist die Webseite nicht responsiv, was den Nutzer verunsichern könnte und keine gute User Experience ist. In einem Spring Boot basierten Webservice kann das zum Beispiel mit asynchroner Ausführung und long polling zusammen mit einem Deferred Result erreicht werden, wie im folgenden beschrieben.

Das Szenario

Im folgenden soll ein kleiner Webservice in Spring Boot implementiert werden, der eine Anfrage auslöst, die zwei Stadien durchläuft. Direkt bei der Anfrage wird in einen Zustand INITIALIZING gewechselt, bis nach einigen Sekunden der Zustand PHASE1 erreicht wird. Den Abschluss markiert der Zustand DONE. Die zugehörige Webseite soll jeweils den aktuellen Status anzeigen und bei einem Statuswechsel automatisch aktualisiert werden.

Eine Beispielimplementation des Webservers habe ich auf GitHub hochgeladen. Es kann nach dem Auschecken mit mvn spring-boot:run gestartet werden, der Webservice ist unter http://localhost:8080 erreichbar.

Beispiel-Service für asynchrone Ausführung und Long-Polling

Beispiel-Service für asynchrone Ausführung und Long-Polling

Bei einem Klick auf die Schaltfläche geht es los.

Die serverseitige Implementierung

Zu Beginn wird ein POST-Request über an /request geschickt. Die Methode soll sofort ein Ergebnis zurückliefern, auch wenn die auszuführende Aktion länger dauert. Dazu wird nicht der tatsächliche Ergebnistyp zurückgeliefert sondern ein DeferredResult. Die Webseite kann daraufhin direkt aktualisiert werden, da die Anfrage schnell bearbeitet wird, das tatsächliche Ergebnis ist im Ergebnis erst vorhanden, sobald setResult aufgerufen wird.

@RequestMapping(name = "/request", method = RequestMethod.POST)
@ResponseBody
public DeferredResult request() {
    // Prepare already for the first state change
    DeferredResult result = service.getStatus();
 
    // Actually let the asynchronous service do something
    service.doSomething();
 
    // Return the deferred result that will be set in the above asynchronous call
    return result;
}
Die Anfrage wurde abgesetzt.

Die Anfrage wurde abgesetzt.

Beim Long Polling kann der Webserver verspätet antworten. Typischerweise wird ein Timeout gesetzt, so dass bei einem Verbindungsabbruch eine neue Anfrage gestartet werden kann. Da unklar ist, wie lange die Antwort tatsächlich dauert, sollte mit dem Verbindungsabbruch gerechnet werden. Die Statusabfrage für das Long Polling wird über den Endpunkt /poll durchgeführt. Das Ergebnis wird vom ausführenden Service geholt und zurückgeliefert. Das zurückgelieferte Object vom Typ DeferredResult wird automatisch so lange zurückgehalten, bis der Service über einen Aufruf von setStatus() ein Ergebnis bereitstellt.

@RequestMapping(name = "/poll", method = RequestMethod.GET)
@ResponseBody
public DeferredResult requestStatus() {
    // Simply forward the request to the service
    return service.getStatus();
}

Asynchrone Ausführung der Anfrage

Damit der ganze Prozess funktioniert, muss die Arbeit des Services (service.doSomething() im Beispiel oben) asynchron ablaufen. Das heißt, nach dem Aufruf von doSomething kann direkt etwas zurückgeliefert werden. Spring Boot ermöglicht durch die Annotation @Async, dass eine Methode eines Services automatisch in einem anderen Thread asynchron ausgeführt wird. Die folgende Methode simuliert, dass der Status zunächst auf INITIALIZING gesetzt wird, anschließend eine längere Zeit gearbeitet wird und zuletzt beendet wird sobald der Status auf DONE steht.

@Async
void doSomething() {
    // Initialize the current status and notify any listener that is already present
    currentStatus = LongRequestStatus.INITIALIZING;
    publishStatus(currentStatus);
 
    // Perform some work until the final state is reached.
    do {
        currentStatus = doNecessaryWork(currentStatus);
        publishStatus(currentStatus);
    } while (currentStatus != LongRequestStatus.DONE);
}

Die Methode wird bei einem POST-Request an /request aufgerufen. Den Status der parallel ausgeführten Methode wird mit publishStatus gesetzt. Falls es in der Zwischenzeit eine eventuell vorhandene Long Polling-Anfrage (mit einem Status als DeferredResult) gab, wird der Status gesetzt.

private synchronized void publishStatus(LongRequestStatus nextToReturn) {
    if (waitingResponse != null) {
        waitingResponse.setResult(nextToReturn.toString());
    }
    waitingResponse = null;
}
Die Anfrage wird gerade Bearbeitet.

Die Anfrage wird gerade Bearbeitet.

Eine Polling-Anfrage ruft ausgehend vom Controller die Methode getStatus auf. Diese erzeugt das DeferredResult waitingResponse, das direkt beantwortet wird, wenn bereits Informationen da sind. Andernfalls wird die Anfrage gespeichert und das Ergebnis gegebenenfalls in obiger Methode publishStatus gesetzt.

public synchronized DeferredResult getStatus() {
    waitingResponse = new DeferredResult<>();
 
    // If the request completed, immediately return the status because it will never change
    if (currentStatus == LongRequestStatus.DONE) {
        waitingResponse.setResult(currentStatus.toString());
    }
 
return waitingResponse;

Die Webseite

Auf der Clientseite wird das Statusupdate mit JavaScript durchgeführt. Bei einem Click auf die Schaltfläche wird zunächst wird die Anfrage ausgeführt und bei Erfolg die Long Polling-Abfrage gestartet.

function request() {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/request", true);
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4 && xhr.status === 200) {
            // Do something with the response
            console.log(xhr.responseText);
 
            // Start long polling and get the next status
            poll();
        }
    }
    xhr.send();
}

Die Methode poll sendet die Anfrage an den Server um den aktuellen Status abzuholen. Die Anfrage wird solange erneut ausgeführt, bis das Endergebnis DONE zurückgeliefert wird. Als Timeout ist eine Sekunde gesetzt, was relativ kurz ist und dafür sorgt, dass jede Sekunde erneut beim Server nachgefragt wird.

function poll() {
    var xhr = new XMLHttpRequest();
 
    xhr.open("GET", "/poll", true);
 
    xhr.onreadystatechange = function() {
        if(xhr.readyState == 4 && xhr.status == 200) {
            if (xhr.responseText !== "DONE") {
                // Start the next polling request
                setTimeout(poll, 1000);
            } else {
                // Stop polling
            }
        }
    }
    xhr.send();
}
Die Anfrage wurde komplett bearbeitet.

Die Anfrage wurde komplett bearbeitet.

Was fehlt?

Das Grundgerüst für mehrere konsekutive Ergebnisse, die auf eine Anfrage zurückgegeben werden sollen steht damit. Es schließen sich nun einige Möglichkeiten der Erweiterung an.

Da Server normalerweise mehrere Nutzer haben, sollte es natürlich möglich sein, dass mehrere Anfragen parallel ablaufen. Dazu müssen für alle diese Anfragen die Stati gespeichert werden. Der erste Request kann dann ein Token (zum Beispiel eine UUID) zurückgeben, mit der anschließend beim Polling der Status der passenden Anfrage abgefragt werden kann. Gegebenenfalls muss hier natürlich noch mit Authentifizierung gearbeitet werden, so dass jeder Nutzer nur seine eigenen Requests anfragen kann.

Geschrieben von Kap. Zuletzt geändert am 16. Februar 2019.

Der Java user.home-Bug

Ich habe kürzlich mein System neu aufgesetzt und bin dabei auf ein Problem gestoßen, das die Java-Entwickler seit Jahren nicht lösen.

Ich unterteile (unter Windows) mein System in zwei Teile,C: und D:. Auf ersterem Laufwerk installiere ich die Programme und Windows, auf letzterem lagere ich meine Daten. Naheliegend, weil D nach C kommt aber auch für Daten ;) . So kann ich das System neu aufsetzen und die Daten einfach mitnehmen. Nach der Neuinstallation muss ich nur die Ordner von zum Beispiel C:\Users\Benutzername\Desktop wieder auf D:\Desktop umbiegen und alles ist wie gehabt.

Nun wollte ich NetBeans installieren, das schlug aber fehl mit dem Hinweis, dass im Verzeichnis D:\ nicht mehr genug Platz vorhanden sei (etwa 600 MiB waren nötig, meine alte Platte war aber voll) um einen Ordner .nbi anzulegen oO . Nun erinnerte ich mich, dass ich schon immer den Ordner D:\.nbi hatte und den auch durch Löschen nicht dauerhaft entfernen konnte. Eine schnelle Suche ergab folgendes:

Der Ordner .nbi wird von der Installationsroutine angelegt und sollte eigentlich in %USERPROFILE%, also in der Windows-Variante des Home-Verzeichnisses liegen. Im Blog von Tim Ehat fand ich die Lösung: Das Home-Verzeichnis wird von der JVM über eine Systemeigenschaft „user.home“ abgefragt, zum Beispiel so:

public class PropertyTest {
    public static void main(String[] args)
        throws Exception {
			System.out.println( System.getProperty("user.name") );
			System.out.println( System.getProperty("user.home") );
			System.out.println( System.getProperty("user.dir") );
		}
}

In Windows wird jedoch das Elterverzeichnis des Desktops genommen (der bei mir nunmal unter D:\Desktop liegt). Das ist nun auch die Erklärung, warum ich diese Verzeichnisse auf Laufwerk D bekomme.

Man kann das nun ganz einfach Umbiegen: Die JVM fragt eine Umgebungsvariable _JAVA_OPTIONS ab, in der man den Java-Parameter -Duser.home=%USERPROFILE%\AppData\Roaming eingeben kann, oder einen beliebigen anderen Pfad.

Fazit: endlich ist mein System sauber und die Datenplatte von unnötigem Müll befreit. Was die Entwickler dazu getrieben hat, einen selten dämlichen Algorithmus zur Bestimmung des Home-Verzeichnisses zu wählen und das nicht zu fixen, weiß ich leider auch nicht. Im Kommentarbereich von Tims Blog wird angedeutet, dass es an Abwärtskompatibilität liegt. Aber mal ehrlich: Wenn man den Pfad zumindest auf %USERPROFILE% setzt bekommen das normale Anwender nicht mit. Und Nutzer, die ihren Desktop umlegen sollten in der Lage sein, eventuell auftretende Probleme zu Fixen.

Geschrieben von Kap. Zuletzt geändert am 4. Januar 2014.

Zufällige Matrizen generieren

Zum Testen einer Methode zur Matrix-Multiplikation wollte ich gern Matrizen haben, dass das Ergebnis einer Matrixmultiplikation eine ganzzahlige Matrix mit „schönem“ Aussehen ist, so dass die Korrektheit der Implementation leicht überprüft werden kann. Sämtliche Matrizen sollten nach möglichkeit ganzzahlig sein. Um das Ziel zu erreichen habe ich einen Matrix-Generator geschrieben, mit dem ich das vorgestellte Verfahren erleutere. Der Generator benutzt meine simple Implementation einer Matrix-Klasse, die die wichtigsten Methoden bereitstellt.

Zuerst wollte ich eine Einheitsmatrix erzeugen, ich suchte also zwei Matrizen A und B mit A×B=E. Dies würde jede invertierbare Matrix erfüllen. Zusätzlich sollten nun aber A und B noch ganzzahlig sein. Allgemein ist für eine ganzzahlige Matrix A die Inverse A1 zwar rational, aber nicht ganzzahlig.

Meine Kenntnisse der linearen Algebra waren etwas eingerostet, aber nach kurzer Suche fand ich einen Artikel von Nathan Brixius, der ebenfalls über die Erzeugung spezieller Matrizen schrieb und mir bei der Erinnerung half. Für die Determinanten von Matrizen gilt det(A) · det(B)=det(AB) und die Determinante der Einheitsmatrix ist natürlich 1. Außerdem kann man die Determinante von Matrizen in oberer oder unterer Dreiecksform als Produkt der Diagonalen leicht bestimmen.

Wir erzeugen zunächst eine Matrix in unterer Dreiecksform:

double[][] values = new double[dimension][dimension];
for( int i = 0; i < dimension; ++i )
	values[i][i] = 1;
for( int i = 1; i < dimension; ++i )
	for( int j = 0; j < i; ++j )
		values[i][j] = getValue(i, j);
Matrix lowerTriangular = new Matrix( values );

Es wird ein 2-dimensionales Array erzeugt, das die Werte enthält. Die Diagonale wird mit 1 gefüllt und die untere Hälfte mit Werten, die in der Methode getValue erzeugt werden (z.B. zufällig).

Genauso wird eine Matrix in oberer Dreiecksform benötigt:

values = new double[dimension][dimension];
for( int i = 0; i < dimension; ++i )
	values[i][i] = 1;
for( int i = 0; i < dimension-1; ++i )
	for( int j = i+1; j < dimension; ++j )
		values[i][j] = getValue(i, j);
Matrix upperTriangular = new Matrix( values );

Das Prinzip entspricht dem der LU-Zerlegung einer Matrix: Es werden zwei Matrizen L und U in unterer beziehungsweise oberer Dreiecksform erzeugt, so dass die Determinante 1 ist (d.h. die Diagonale ist 1). Die Matrix M:=L×U ist ganzzahlig und invertierbar und nach der Bestimmung der Inversen über die Determinante ist die inverse Matrix A1 ebenfalls ganzzahlig.

Matrix matrix = lowerTriangular.mult( upperTriangular );
Matrix inverse = matrix.invert();

Mögliche Matrizen als Testinstanzen sind damit also M und A1. Um Abwechslung zu erzeugen kann man nun beliebige Matrizen F an M von links anmultiplizieren (oder an A1 von rechts) und erhält als Ergebnis nicht mehr die Einheitsmatrix, sondern F: F×M×M1=F×E=F.

Als Beispiel erzeugen wir eine Matrix, die natürliche Zahlen von 1 aufsteigend enthält:

values = new double[dimension][dimension];
int c = 1;
for( int i = 0; i < dimension; ++i )
	for( int j = 0; j < dimension; ++j )
		values[i][j] = c++;
Matrix F = new Matrix( values );

Damit kann die Ausgabematrix erstellt werden:

Matrix result = matrix.mult( inverse );

Die beiden Testinstanzen sind dann result und inverse, das Ergebnis der Multiplikation ist F. Natürlich kann F auch rationale Zahlen enthalten, dann ist natürlich die Eingabematrix auch nicht mehr ganzzahlig.

Geschrieben von Kap. Zuletzt geändert am 11. Januar 2014.

SVN-Revisionen mit NetBeans automatisch einfügen

Insbesondere in OpenSource-Projekten, bei denen Benutzer verschiedene Versionen der Software haben können, kann es zu Debugging-Zwecken sehr interessant sein, die SVN-Version einfach lesbasr zu machen.

Ich beschreibe hier eine Variante unter Windows mit TortoiseSVN und NetBeans.

SVN bietet die Möglichkeit, bestimmte Strings durch die Versionsnummer einer einzelnen Datei ersetzen zu lassen. Schreibt man in eine Datei den String $Rev:$, wird von SVN der Text automatisch durch die aktuelle Revision der Datei ersetzt, also zum Beispiel zu $Rev: 2658 $. Man muss dazu für die ausgewählte Datei ein Schlüsselwort aktualisieren. Bei Tortoise muss im Kontextmenü der Datei der Menüeintrag „TortoiseSVN | Properties“ ausgewählt werden und im sich öffnenden Fenster „New | Keywords“. Von den möglichen Keywords muss „Revision“ ausgewählt werden.

Durch diese Einstellung kann für jede Datei die Version der Datei gespeichert werden. Nun mag es aber auch interessant sein, die aktuellste Version zu haben. Tortoise bietet dazu das Tool subwcrev an. Es kann eine Template-Datei lesen und in dieser Datei sämtliche Vorkommen von $WCREV$ durch die maximale SVN-Revision aller Dateien im Repository zu ersetzen. Bei einer standardmäßigen Installation ist das Tool im Windows-Pfad enthalten und kann direkt aufgerufen werden. Ansonsten ist es unter /bin im Tortoise-Verzeichnis zu finden.

Wir gehen davon aus, dass wir eine Datei version.tmpl haben, in der nur $WCREV$ steht. Wir wollen eine Datei version.txt erstellen, in der die Versionsnummer steht. Der der Aufruf auf Befehlszeile lautet subwcrev . version.tmpl version.txt. Die Ausgabe enthält noch weitere Informationen und sieht zum Beispiel so aus
SubWCRev: 'D:DokumenteProgrammeTool'
Last committed at revision 2657
Mixed revision range 2655:2657
Local modifications found

Damit dieser Befehl nicht jedes mal manuell aufgerufen werden muss, kann man ihn in das NetBeans-Build-Script eingefügt werden. Dazu wird die Datei build.xml bearbeitet. Direkt nach <import file="nbproject/build-impl.xml"/>

fügen wir das Folgende hinzu:

<target name="-pre-compile">
<exec executable="subwcrev" output="svn-revision.log">
<arg line=" . version.tmpl version.txt">
<exec>
<target>

Damit teilen wir der Entwicklungsumgebung mit, dass vor dem Kompilieren das subwcrev-Tool ausgeführt werden soll, welche Parameter übergeben werden wollen und dass die Ausgabe in der datei svn-revision.log gespeichert werden soll. Falls etwas falsch angegeben worden ist, werden die Fehlermeldungen ebenfalls in diese Datei geschrieben.

Nun noch ein Beispiel, wie im Programm auf die Versionsinfos zugegriffen werden kann. Ich habe einen doppelten Ansatz gewählt: es wird versucht, den Text der Datei version.txt einzulesen. Falls dabei etwas schiefgeht, wird die lokale SVN-Revsionsnummer genommen.

/** SVN version */
public final static String revision = getVersion();
 
/**
* Reads the svn revision from a file with name 'version.txt'. If an exception
* occurs, the revision of this source code file is returned. WARNING: no error
* checks are performed!
*/
private static String getVersion() {
  try {
    return new String( Files.readAllBytes( Paths.get( "./version.txt" ) ) );
  } catch( IOException ex ) {
    return "> $Rev: 2658 $"; // return emergency value
  }
}

Das war’s auch schon. Nun kann man im Programm auf die aktuelle Revisionsnummer zugreifen. Die Textdatei wird bei jeder Ausführung von Clean & Build ausgefüht.

Alternativ kann man natürlich auch eine Java-Klasse als Template erzeugen und somit auf das Einlesen verzichten. In dem Fall bietet es sich an, eine eigene Klasse nur für den Versionstext zu entwickeln, da sonst immer das Template verändert werden muss, nicht die „richtige“ Quelldatei.

Geschrieben von Kap. Zuletzt geändert am 18. März 2013.