Weiche den Fallen aus, die sich in der URLConnection-Klasse verstecken

Eine Falle ist Java-Code, der gut kompiliert werden kann, aber zu fehlerhaften und manchmal katastrophalen Ergebnissen führt. Das Vermeiden von Fallstricken kann Ihnen Stunden der Frustration ersparen. In diesem Artikel werde ich eine Falle vorstellen, die beim Posten auf einer URL auftreten kann, und eine andere, die Java-Anfänger plagt.

Fallstrick 5: Die versteckte Komplexität des Posten auf einer URL

Da das SOAP (Simple Object Access Protocol) und andere XML-Remoteprozeduraufrufe (RPCs) immer beliebter werden, wird das Posten an eine URL häufiger und wichtiger - es ist die Methode zum Senden der SOAP- oder RPC-Anforderung an den jeweiligen Server.

Bei der Implementierung eines eigenständigen SOAP-Servers bin ich auf mehrere Fallstricke beim Posten auf einer URL gestoßen, angefangen beim nicht intuitiven Design der URL-bezogenen Klassen bis hin zu bestimmten Fallstricken bei der Benutzerfreundlichkeit in der URLConnectionKlasse.

Eine einfache HttpClientKlasse wäre der direkteste Weg, um eine HTTP-Post-Operation für eine URL auszuführen. Nach dem Scannen der java.net packagewerden Sie jedoch leer angezeigt. Einige Open Source HTTP-Clients sind verfügbar, aber ich habe sie nicht getestet. (Wenn Sie diese Clients getestet haben, senden Sie mir eine E-Mail bezüglich ihres Nutzens und ihrer Stabilität.) Interessanterweise enthält HttpClientdas sun.net.www.httpPaket eine, die mit dem JDK geliefert wird (und von diesem verwendet wird HttpURLConnection), die jedoch nicht Teil der öffentlichen API ist. Stattdessen wurden die java.netURL-Klassen so konzipiert, dass sie äußerst allgemein gehalten sind und das dynamische Laden von Klassen sowohl von Protokollen als auch von Inhaltshandlern nutzen. Bevor wir uns mit den spezifischen Problemen beim Posten befassen, untersuchen wir die Gesamtstruktur der Klassen, die wir verwenden werden (entweder direkt oder indirekt).

Dieses UML-Diagramm der URL-bezogenen Klassen im java.netPaket veranschaulicht die Wechselbeziehung der Klassen. (Das Diagramm wurde mit erstellt ArgoUML- siehe Ressourcen für einen Link.) Der Kürze halber zeigt das Diagramm nur Schlüsselmethoden und keine Datenelemente.

Fallstrick 5 konzentriert sich auf die Hauptklasse : URLConnection. Sie können diese Klasse jedoch nicht direkt instanziieren - sie ist abstrakt. Stattdessen erhalten Sie URLConnectionüber die URLKlasse einen Verweis auf eine bestimmte Unterklasse von .

Zugegebenermaßen ist die obige Abbildung komplex. Die allgemeine Abfolge der Ereignisse funktioniert folgendermaßen: Eine statische URL gibt normalerweise den Speicherort einiger Inhalte und das Protokoll an, das für den Zugriff darauf erforderlich ist. Bei der ersten Verwendung der URLKlasse wird ein URLStreamHandlerFactorySingleton erstellt. Diese Factory generiert ein URLStreamHandlerProtokoll, das das in der URL angegebene Zugriffsprotokoll versteht. Das URLStreamHandlerinstanziiert die entsprechende URLConnectionKlasse, wodurch eine Verbindung zur URL hergestellt wird, und instanziiert die entsprechende Klasse, ContentHandlerum den Inhalt unter der URL zu verarbeiten.

Also, wo liegt das Problem? Aufgrund des übermäßig generischen Designs der Klassen fehlt ihnen ein klares konzeptionelles Modell. In seinem Buch " Das Design alltäglicher Dinge" (Doubleday, 1990) stellt Donald Norman fest, dass eines der Hauptprinzipien guten Designs ein solides konzeptionelles Modell ist, das es uns ermöglicht, "die Auswirkungen unserer Handlungen vorherzusagen". Einige Probleme mit dem URLkonzeptionellen Modell der Klassen sind:

  • Die URLKlasse ist konzeptionell überlastet. Eine URL ist lediglich eine Abstraktion für eine Adresse oder einen Endpunkt. Tatsächlich würde ein besseres Design URL-Unterklassen enthalten, die statische Ressourcen von dynamischen Diensten unterscheiden. Konzeptionell fehlt eine URLClientKlasse, die die URL als Endpunkt zum Lesen oder Schreiben verwendet.
  • Die URLKlasse ist darauf ausgerichtet, Daten von einer URL abzurufen. Es gibt drei Methoden, um Inhalte von einer URL abzurufen, aber nur eine, die Daten in eine URL schreibt. Diese Ungleichheit würde besser mit einer URL-Unterklasse für statische Ressourcen bedient, die nur eine Leseoperation hat. Die URL-Unterklasse für dynamische Dienste verfügt sowohl über Lese- als auch über Schreibmethoden. Dieses Design würde ein sauberes konzeptionelles Modell für die Verwendung liefern.
  • Das Aufrufen der Protokoll-Handler "Stream" -Handler ist verwirrend, da ihr Hauptzweck darin besteht, eine Verbindung zu generieren (oder aufzubauen). Ein besseres Modell würde die Java-API für die XML-Verarbeitung (JAXP) emulieren, wobei a a DocumentBuilderFactoryerzeugt DocumentBuilder, was a erzeugt Document. Das Anwenden dieses Modells auf die URL-Klassen würde a ergeben URLConnectorFactory, das a URLConnectorerzeugt , das a erzeugt URLConnection.

Jetzt können Sie die URLConnectionKlasse angehen und versuchen, auf einer URL zu posten. Ziel ist es, ein einfaches Java-Programm zu erstellen, das Text an ein CGI-Programm (Common Gateway Interface) sendet. Um die Programme zu testen, habe ich in C ein einfaches CGI-Programm erstellt, das (in einem HTML-Wrapper) alles wiedergibt, was in das Programm übergeht. (Informationen zum Herunterladen des Quellcodes für alle Programme in diesem Artikel, einschließlich des CGI-Programms, finden Sie unter Ressourcen.)

Die URLConnectionKlasse hat getOutputStream()und getInputStream()Methoden, genau wie die SocketKlasse. Aufgrund dieser Ähnlichkeit würden Sie erwarten, dass das Senden von Daten an eine URL so einfach ist wie das Schreiben von Daten an eine Socket. Mit diesen Informationen und einem Verständnis des HTTP-Protokolls schreiben wir das Programm in Listing 5.1 , BadURLPost.java.

Listing 5.1: BadURLPost.java

Paket com.javaworld.jpitfalls.article3; import java.net. *; import java.io. *; öffentliche Klasse BadURLPost {public static void main (String args []) {// eine HTTP-Verbindung zum POST abrufen, wenn (args.length <1) {System.out.println ("USAGE: java GOV.dia.mditds.util .BadURLPost url "); System.exit (1); } try {// URL als String abrufen String surl = args [0]; URL url = neue URL (surl); URLConnection con = url.openConnection (); System.out.println ("Received a:" + con.getClass (). GetName ()); con.setDoInput (true); con.setDoOutput (true); con.setUseCaches (false); String msg = "Hallo HTTP-Server! Nur ein kurzes Hallo!"; con.setRequestProperty ("CONTENT_LENGTH", "5"); // Nicht geprüft con.setRequestProperty ("Stupid", "Nonsense"); System.out.println ("Abrufen eines Eingabestreams ...");InputStream ist = con.getInputStream (); System.out.println ("Ausgabestream abrufen ..."); OutputStream os = con.getOutputStream (); / * con.setRequestProperty ("CONTENT_LENGTH", "" + msg.length ()); Unzulässiger Zugriffsfehler - Methode kann nicht zurückgesetzt werden. * / OutputStreamWriter osw = neuer OutputStreamWriter (os); osw.write (msg); osw.flush (); osw.close (); System.out.println ("Nach dem Leeren des Ausgabestreams."); // Irgendeine Antwort? InputStreamReader isr = neuer InputStreamReader (is); BufferedReader br = neuer BufferedReader (isr); String line = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} catch (Throwable t) {t.printStackTrace (); }}}setRequestProperty ("CONTENT_LENGTH", "" + msg.length ()); Unzulässiger Zugriffsfehler - Methode kann nicht zurückgesetzt werden. * / OutputStreamWriter osw = neuer OutputStreamWriter (os); osw.write (msg); osw.flush (); osw.close (); System.out.println ("Nach dem Leeren des Ausgabestreams."); // Irgendeine Antwort? InputStreamReader isr = neuer InputStreamReader (is); BufferedReader br = neuer BufferedReader (isr); String line = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} catch (Throwable t) {t.printStackTrace (); }}}setRequestProperty ("CONTENT_LENGTH", "" + msg.length ()); Unzulässiger Zugriffsfehler - Methode kann nicht zurückgesetzt werden. * / OutputStreamWriter osw = neuer OutputStreamWriter (os); osw.write (msg); osw.flush (); osw.close (); System.out.println ("Nach dem Leeren des Ausgabestreams."); // Irgendeine Antwort? InputStreamReader isr = neuer InputStreamReader (is); BufferedReader br = neuer BufferedReader (isr); String line = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} catch (Throwable t) {t.printStackTrace (); }}}// Irgendeine Antwort? InputStreamReader isr = neuer InputStreamReader (is); BufferedReader br = neuer BufferedReader (isr); String line = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} catch (Throwable t) {t.printStackTrace (); }}}// Irgendeine Antwort? InputStreamReader isr = neuer InputStreamReader (is); BufferedReader br = neuer BufferedReader (isr); String line = null; while ((line = br.readLine ())! = null) {System.out.println ("line:" + line); }} catch (Throwable t) {t.printStackTrace (); }}}

Ein Durchlauf von Listing 5.1 ergibt:

E: \ classes \ com \ javaworld \ jpitfalls \ article3> java -Djava.compiler = NONE com.javaworld.jpitfalls.article3.BadURLPost //localhost/cgi-bin/echocgi.exe Erhalten a: sun.net.www.protocol .http.HttpURLConnection Abrufen eines Eingabestreams ... Abrufen eines Ausgabestreams ... java.net.ProtocolException: Methode kann nicht zurückgesetzt werden: bereits verbunden bei java.net.HttpURLConnection.setRequestMethod (HttpURLConnection.java:10 2) bei Sonne .net.www.protocol.http.HttpURLConnection.getOutputStream (HttpURLCo nnection.java:349) unter com.javaworld.jpitfalls.article2.BadURLPost.main (BadURLPost.java:38) 

Wenn wir versuchen, den HttpURLConnectionAusgabestream der Klasse abzurufen, teilt uns das Programm mit, dass wir die Methode nicht zurücksetzen können, da wir bereits verbunden sind. Das Javadoc für die HttpURLConnectionKlasse enthält keinen Verweis auf das Festlegen einer Methode. Das Programm bezieht sich auf die HTTP-Methode, die verwendet werden sollte, POSTwenn Daten an die URL gesendet und GETDaten von der URL abgerufen werden.

Die getOutputStream()Methode veranlasst das Programm, eine ProtocolExceptionmit der Fehlermeldung " Die Methode kann nicht zurückgesetzt werden" auszulösen. Der JDK-Quellcode zeigt, dass die Fehlermeldung getInputStream()angezeigt wird, da die Methode den Nebeneffekt hat, dass die Anforderung (deren Standardanforderungsmethode lautet GET) an den Webserver gesendet wird. Dies ähnelt einem Nebeneffekt in ObjectInputStreamund ObjectOutputStreamKonstruktoren, der in meinem Buch Java Pitfalls: Zeitsparende Lösungen und Problemumgehungen zur Verbesserung von Programmen (John Wiley & Sons, 2000) beschrieben wird.

Die Falle ist die Annahme, dass sich die Methoden getInputStream()und getOutputStream()genauso verhalten wie für eine SocketVerbindung. Da der zugrunde liegende Mechanismus für die Kommunikation mit dem Webserver tatsächlich ein ist Socket, ist dies keine unangemessene Annahme. Eine bessere Implementierung von HttpURLConnectionwürde die Nebenwirkungen auf das anfängliche Lesen oder Schreiben in den jeweiligen Eingabe- oder Ausgabestream verschieben. Sie können dies tun, indem Sie ein HttpInputStreamund ein erstellen HttpOutputStream, wodurch das SocketModell intakt bleibt. Sie könnten argumentieren, dass HTTP ein zustandsloses Anforderungs- / Antwortprotokoll ist und das SocketModell nicht passt. Trotzdem sollte die API zum konzeptionellen Modell passen. wenn das aktuelle Modell mit a identisch istSocketVerbindung sollte es sich als solche verhalten. Wenn dies nicht der Fall ist, haben Sie die Grenzen der Abstraktion zu weit ausgedehnt.

Zusätzlich zur Fehlermeldung gibt es zwei Probleme mit dem obigen Code:

  • Die setRequestProperty()Methodenparameter werden nicht überprüft, was wir demonstrieren, indem wir eine Eigenschaft namens dumm mit dem Wert unsinn setzen. Da diese Eigenschaften tatsächlich in die HTTP-Anforderung eingehen und nicht von der Methode validiert werden (wie sie sein sollten), müssen Sie besonders darauf achten, dass die Parameternamen und -werte korrekt sind.
  • Obwohl der Code auskommentiert ist, ist es auch illegal, nach dem Abrufen eines Eingabe- oder Ausgabestreams zu versuchen, eine Anforderungseigenschaft festzulegen. In der Dokumentation für URLConnectionwird die Reihenfolge zum Einrichten einer Verbindung angegeben, obwohl nicht angegeben wird, dass es sich um eine obligatorische Reihenfolge handelt.

Wenn wir nicht den Luxus hätten, den Quellcode zu untersuchen - was definitiv keine Voraussetzung für die Verwendung einer API sein sollte -, würden wir uns auf Versuch und Irrtum beschränken, die absolut schlechteste Art zu programmieren. Weder die Dokumentation noch die API der HttpURLConnectionKlasse geben uns ein Verständnis dafür, wie das Protokoll implementiert ist. Daher versuchen wir schwach, die Reihenfolge der Aufrufe von getInputStream()und umzukehren getOutputStream(). Listing 5.2 BadURLPost1.javaist eine abgekürzte Version dieses Programms.

Listing 5.2: BadURLPost1.java

package com.javaworld.jpitfalls.article3; import java.net.*; import java.io.*; public class BadURLPost1 { public static void main(String args[]) { // ... try { // ... System.out.println("Getting an output stream..."); OutputStream os = con.getOutputStream(); System.out.println("Getting an input stream..."); InputStream is = con.getInputStream(); // ... } catch (Throwable t) { t.printStackTrace(); } } } 

A run of Listing 5.2 produces:

E:\classes\com\javaworld\jpitfalls\article3>java -Djava.compiler=NONE com.javaworld.jpitfalls.article3.BadURLPost1 //localhost/cgi-bin/echocgi.exe Received a : sun.net.www.protocol.http.HttpURLConnection Getting an output stream... Getting an input stream... After flushing output stream. line:  line:  Echo CGI program  line:  line:  line: 

Echo

line: line: No content! ERROR! line: line:

Obwohl das Programm kompiliert und ausgeführt wird, meldet das CGI-Programm, dass keine Daten gesendet wurden! Warum? Die Nebenwirkungen von getInputStream()Bite uns erneut, was dazu führt, dass die POSTAnfrage gesendet wird, bevor etwas in den Ausgabepuffer des Posts gestellt wird, wodurch eine leere POSTAnfrage gesendet wird .

Nach zweimaligem Fehlschlagen getInputStream()ist dies die Schlüsselmethode, mit der die Anforderungen tatsächlich auf den Server geschrieben werden. Daher müssen wir die Operationen seriell ausführen (Ausgabe öffnen, schreiben, Eingabe öffnen, lesen), wie in Listing 5.3 , GoodURLPost.

Listing 5.3: GoodURLPost.java