Die Verwendung von java.net.URLConnection
wird hier ziemlich oft nachgefragt, und das Oracle-Tutorial ist zu prägnant dazu.
Dieses Tutorial zeigt im Grunde nur, wie man eine GET-Anfrage stellt und die Antwort liest. Es wird nirgends erklärt, wie man damit u.a. eine POST-Anfrage stellt, Anfrage-Header setzt, Antwort-Header liest, mit Cookies umgeht, ein HTML-Formular abschickt, eine Datei hochlädt, etc.
Wie kann ich also java.net.URLConnection
verwenden, um "erweiterte" HTTP-Anfragen auszulösen und zu bearbeiten?
IOException
und RuntimeException
wie NullPointerException
, ArrayIndexOutOfBoundsException
und Konsorten selbst behandeln.*Zunächst müssen wir zumindest die URL und den Zeichensatz kennen. Die Parameter sind optional und hängen von den funktionalen Anforderungen ab.
String url = "http://example.com";
String charset = "UTF-8"; // Or in Java 7 and later, use the constant: java.nio.charset.StandardCharsets.UTF_8.name()
String param1 = "value1";
String param2 = "value2";
// ...
String query = String.format("param1=%s¶m2=%s",
URLEncoder.encode(param1, charset),
URLEncoder.encode(param2, charset));
Name=Wert
vorliegen und mit &
verkettet werden. Normalerweise würden Sie auch URL-encode die Abfrageparameter mit dem angegebenen Zeichensatz mit URLEncoder#encode()
.
Das String#format()
dient nur der Bequemlichkeit. Ich bevorzuge es, wenn ich den String-Verknüpfungsoperator +
mehr als zweimal benötige.Es ist eine triviale Aufgabe. Es'ist die Standard-Anforderungsmethode.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Jeder Abfrage-String sollte mit ?
an die URL angehängt werden. Der Accept-Charset
Header kann dem Server mitteilen, in welcher Kodierung die Parameter vorliegen. Wenn Sie keine Abfragezeichenfolge senden, können Sie den Accept-Charset
-Header weglassen. Wenn Sie keine Header setzen müssen, können Sie sogar die URL#openStream()
Abkürzungsmethode verwenden.
InputStream response = new URL(url).openStream();
// ...
So oder so, wenn die andere Seite ein HttpServlet
ist, wird dessen doGet()
Methode aufgerufen und die Parameter sind über HttpServletRequest#getParameter()
verfügbar.
Zu Testzwecken können Sie den Antwortkörper wie folgt auf stdout ausgeben:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Das Setzen von URLConnection#setDoOutput()
auf true
setzt die Anfragemethode implizit auf POST. Das Standard-HTTP-POST, wie es Webformulare tun, ist vom Typ application/x-www-form-urlencoded
, wobei der Abfrage-String in den Anfragekörper geschrieben wird.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true); // Triggers POST.
connection.setRequestProperty("Accept-Charset", charset);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + charset);
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes(charset));
}
InputStream response = connection.getInputStream();
// ...
Anmerkung: Wann immer Sie ein HTML-Formular programmatisch absenden möchten, vergessen Sie nicht, die name=value
-Paare aller <input type="hidden">
-Elemente in den Query-String zu übernehmen und natürlich auch das name=value
-Paar des <input type="submit">
Elements, das Sie programmatisch "drücken" möchten (weil das'normalerweise auf der Serverseite verwendet wird, um zu unterscheiden, ob eine Schaltfläche gedrückt wurde und wenn ja, welche).
Sie können auch die erhaltene URLConnection
in HttpURLConnection
umwandeln und stattdessen deren HttpURLConnection#setRequestMethod()
verwenden. Aber wenn Sie versuchen, die Verbindung für die Ausgabe zu verwenden, müssen Sie immer noch URLConnection#setDoOutput()
auf true
setzen.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
HttpServlet
ist, dann wird seine doPost()
Methode aufgerufen und die Parameter werden durch HttpServletRequest#getParameter()
verfügbar.URLConnection#connect()
auslösen, aber die Anfrage wird automatisch bei Bedarf ausgelöst, wenn Sie Informationen über die HTTP-Antwort erhalten möchten, wie z.B. den Antwortkörper mit URLConnection#getInputStream()
und so weiter. Die obigen Beispiele tun genau das, so dass der connect()
Aufruf eigentlich überflüssig ist.HttpURLConnection
. Casten Sie sie zuerst, falls nötig.
int status = httpConnection.getResponseCode();Content-Type
einen charset
Parameter enthält, dann ist der Antwortkörper wahrscheinlich textbasiert und wir möchten den Antwortkörper mit der serverseitig spezifizierten Zeichenkodierung verarbeiten.
String contentType = connection.getHeaderField("Content-Type");
String charset = null;
for (String param : contentType.replace(" ", "").split(";")) {
if (param.startsWith("charset=")) {
charset = param.split("=", 2)1;
break;
}
}
if (Zeichensatz != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) {
for (String line; (line = reader.readLine()) != null;) {
// ... System.out.println(line) ?
}
}
} else {
// Es handelt sich wahrscheinlich um binären Inhalt, verwenden Sie InputStream/OutputStream.
}Die serverseitige Sitzung wird normalerweise durch einen Cookie gesichert. Einige Webformulare erfordern, dass Sie eingeloggt sind und/oder durch eine Sitzung verfolgt werden. Sie können die CookieHandler
API verwenden, um Cookies zu verwalten. Sie müssen einen CookieManager
mit einer CookiePolicy
von ACCEPT_ALL
vorbereiten, bevor Sie alle HTTP-Anfragen senden.
// First set the default cookie manager.
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
// All the following subsequent URLConnections will use the same cookie manager.
URLConnection connection = new URL(url).openConnection();
// ...
connection = new URL(url).openConnection();
// ...
connection = new URL(url).openConnection();
// ...
Beachten Sie, dass dies bekanntermaßen nicht immer und unter allen Umständen funktioniert. Wenn es bei Ihnen nicht funktioniert, ist es am besten, die Cookie-Header manuell zu sammeln und zu setzen. Im Grunde müssen Sie alle "Set-Cookie"-Kopfzeilen aus der Antwort des Logins oder der ersten "GET"-Anfrage abrufen und diese dann an die nachfolgenden Anfragen weitergeben.
// Gather all cookies on the first request.
URLConnection connection = new URL(url).openConnection();
List<String> cookies = connection.getHeaderFields().get("Set-Cookie");
// ...
// Then use the same cookies on all subsequent requests.
connection = new URL(url).openConnection();
for (String cookie : cookies) {
connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
}
// ...
split(";", 2)[0]
ist dazu da, um Cookie-Attribute loszuwerden, die für die Serverseite irrelevant sind, wie expires
, path
, etc. Alternativ könnten Sie auch cookie.substring(0, cookie.indexOf(';'))
anstelle von split()
verwenden.Die HttpURLConnection
puffert standardmäßig den gesamten Request-Body, bevor er tatsächlich gesendet wird, unabhängig davon, ob Sie selbst eine feste Inhaltslänge mit connection.setRequestProperty("Content-Length", contentLength);
eingestellt haben. Dies kann zu OutOfMemoryException
führen, wenn Sie gleichzeitig große POST-Anfragen senden (z.B. beim Hochladen von Dateien). Um dies zu vermeiden, sollten Sie den HttpURLConnection#setFixedLengthStreamingMode()
einstellen.
httpConnection.setFixedLengthStreamingMode(contentLength);
Aber wenn die Länge des Inhalts wirklich nicht vorher bekannt ist, dann können Sie den Chunked-Streaming-Modus verwenden, indem Sie den HttpURLConnection#setChunkedStreamingMode()
entsprechend einstellen. Dadurch wird der HTTP-Transfer-Encoding
Header auf chunked
gesetzt, was dazu führt, dass der Request Body in Chunks gesendet wird. Das folgende Beispiel sendet den Body in 1KB großen Chunks.
httpConnection.setChunkedStreamingMode(1024);
Es kann vorkommen, dass [eine Anfrage eine unerwartete Antwort zurückgibt, während sie mit einem echten Webbrowser problemlos funktioniert] (https://stackoverflow.com/questions/13670692/403-forbidden-with-java-but-not-web-browser). Die Serverseite blockiert wahrscheinlich Anfragen aufgrund des User-Agent
Anfrage-Headers. Die URLConnection
setzt ihn standardmäßig auf Java/1.6.0_19
, wobei der letzte Teil offensichtlich die JRE-Version ist. Sie können dies wie folgt überschreiben:
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); // Do as if you're using Chrome 41 on Windows 7.
Wenn der HTTP-Antwortcode 4nn
(Client-Fehler) oder 5nn
(Server-Fehler) ist, dann sollten Sie den HttpURLConnection#getErrorStream()
lesen, um zu sehen, ob der Server irgendwelche nützlichen Fehlerinformationen gesendet hat.
InputStream error = ((HttpURLConnection) connection).getErrorStream();
Wenn der HTTP-Antwortcode -1 ist, ist bei der Verbindungs- und Antwortbehandlung etwas schief gelaufen. Die HttpURLConnection
-Implementierung ist in älteren JREs etwas fehlerhaft mit dem Aufrechterhalten von Verbindungen. Sie können dies abschalten, indem Sie die Systemeigenschaft http.keepAlive
auf false
setzen. Sie koennen dies programmatisch am Anfang Ihrer Anwendung tun, indem Sie:
System.setProperty("http.keepAlive", "false");
Normalerweise verwenden Sie die Kodierung multipart/form-data
für gemischte POST-Inhalte (Binär- und Zeichendaten). Die Kodierung ist in RFC2388 genauer beschrieben.
String param = "value";
File textFile = new File("/path/to/file.txt");
File binaryFile = new File("/path/to/file.bin");
String boundary = Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
String CRLF = "\r\n"; // Line separator required by multipart/form-data.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (
OutputStream output = connection.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);
) {
// Send normal param.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
writer.append(CRLF).append(param).append(CRLF).flush();
// Send text file.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"textFile\"; filename=\"" + textFile.getName() + "\"").append(CRLF);
writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF); // Text file itself must be saved in this charset!
writer.append(CRLF).flush();
Files.copy(textFile.toPath(), output);
output.flush(); // Important before continuing with writer!
writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.
// Send binary file.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + binaryFile.getName() + "\"").append(CRLF);
writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(binaryFile.getName())).append(CRLF);
writer.append("Content-Transfer-Encoding: binary").append(CRLF);
writer.append(CRLF).flush();
Files.copy(binaryFile.toPath(), output);
output.flush(); // Important before continuing with writer!
writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.
// End of multipart/form-data.
writer.append("--" + boundary + "--").append(CRLF).flush();
}
HttpServlet
ist, dann wird dessen doPost()
Methode aufgerufen und die Teile werden durch HttpServletRequest#getPart()
verfügbar (Achtung, also nicht getParameter()
und so weiter!). Die Methode getPart()
ist jedoch relativ neu, sie wurde in Servlet 3.0 (Glassfish 3, Tomcat 7, etc) eingeführt. Vor Servlet 3.0 verwenden Sie am besten Apache Commons FileUpload, um eine multipart/form-data
-Anfrage zu parsen. Siehe auch diese Antwort für Beispiele sowohl für den FileUpload- als auch für den Servelt 3.0-Ansatz.Manchmal müssen Sie eine HTTPS-URL verbinden, vielleicht weil Sie einen Web Scraper schreiben. In diesem Fall werden Sie wahrscheinlich mit einer javax.net.ssl.SSLException: Not trusted server certificate
auf einigen HTTPS-Sites, die ihre SSL-Zertifikate nicht auf dem neuesten Stand halten, oder eine java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
oder javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
auf einigen falsch konfigurierten HTTPS-Sites.
Der folgende, einmalig ausgeführte static
-Initialisierer in Ihrer Web Scraper-Klasse sollte HttpsURLConnection
gegenüber diesen HTTPS-Sites nachsichtiger machen und somit diese Ausnahmen nicht mehr auslösen.
static {
TrustManager[] trustAllCertificates = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null; // Not relevant.
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
// Do nothing. Just allow them all.
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// Do nothing. Just allow them all.
}
}
};
HostnameVerifier trustAllHostnames = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; // Just allow them all.
}
};
try {
System.setProperty("jsse.enableSNIExtension", "false");
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCertificates, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(trustAllHostnames);
}
catch (GeneralSecurityException e) {
throw new ExceptionInInitializerError(e);
}
}
Der Apache HttpComponents HttpClient ist in dieser Hinsicht viel praktischer :)
Wenn Sie nur Daten aus HTML parsen und extrahieren wollen, dann verwenden Sie besser einen HTML-Parser wie Jsoup
Bei der Arbeit mit HTTP ist es fast immer sinnvoller, sich auf HttpURLConnection
zu beziehen, als auf die Basisklasse URLConnection
(da URLConnection
eine abstrakte Klasse ist, wenn Sie nach URLConnection.openConnection()
auf einer HTTP-URL fragen, ist es das, was Sie sowieso zurückbekommen werden).
Dann können Sie sich statt auf URLConnection#setDoOutput(true)
zu verlassen, um die Anfragemethode implizit auf POST zu setzen, stattdessen httpURLConnection.setRequestMethod("POST")
verwenden, was einige vielleicht natürlicher finden (und was Ihnen auch erlaubt, andere Anfragemethoden wie PUT, DELETE, ... anzugeben).
Es bietet auch nützliche HTTP-Konstanten, so dass Sie tun können:
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Inspiriert durch diese und andere Fragen zu SO, habe ich einen minimalen Open-Source-basic-http-client erstellt, der die meisten der hier vorgestellten Techniken enthält.
google-http-java-client ist ebenfalls eine großartige Open-Source-Ressource.