Hur man använder java.net.URLConnection för att starta och hantera HTTP-begäranden

Användning av java.net.URLConnection frågas ganska ofta här, och Oracle tutorial är för kortfattad om det.

Den handledningen visar i princip bara hur man skickar en GET-förfrågan och läser svaret. Den förklarar inte någonstans hur man använder den för att bland annat utföra en POST-förfrågan, ställa in förfrågningshuvuden, läsa svarshuvuden, hantera cookies, skicka ett HTML-formulär, ladda upp en fil osv.

Så hur kan jag använda java.net.URLConnection för att avfyra och hantera "avancerade" HTTP-förfrågningar?

Lösning

Först en ansvarsfriskrivning i förväg: de kodutdrag som publiceras är alla grundläggande exempel. Du måste själv hantera triviala IOExceptions och RuntimeExceptions som NullPointerException, ArrayIndexOutOfBoundsException och liknande.

Förberedelser

Vi måste först känna till åtminstone URL och charset. Parametrarna är valfria och beror på funktionskraven.

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&param2=%s", 
     URLEncoder.encode(param1, charset), 
     URLEncoder.encode(param2, charset));

Förfrågningsparametrarna måste vara i formatet name=value och sammanfogas med &. Du skulle normalt också URL-koda frågeparametrarna med det angivna charsetet med hjälp av URLEncoder#encode(). String#format() är bara för bekvämlighetens skull. Jag föredrar den när jag behöver String concatenation operatorn + mer än två gånger.

Förfrågan HTTP GET med (valfritt) frågeparametrar

Det är en trivial uppgift. Det är standardmetoden för begäran.

URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...

Varje frågeserie ska konkateneras till URL:en med hjälp av ?. Huvudet Accept-Charset kan ge servern en antydan om vilken kodning parametrarna har. Om du inte skickar någon frågeteckensträng kan du låta bli att ange Accept-Charset-huvudet. Om du inte behöver ställa in några headers kan du till och med använda genvägsmetoden URL#openStream().

InputStream response = new URL(url).openStream();
// ...

Hur som helst, om den andra sidan är en HttpServlet, så kommer dess doGet() metod att anropas och parametrarna kommer att vara tillgängliga genom HttpServletRequest#getParameter(). För teständamål kan du skriva ut svarskroppen till stdout enligt nedan:

try (Scanner scanner = new Scanner(response)) {
    String responseBody = scanner.useDelimiter("\\A").next();
    System.out.println(responseBody);
}

Förfrågan HTTP POST med frågeparametrar

Genom att ställa in URLConnection#setDoOutput() till true ställs förfrågningsmetoden implicit in på POST. Standard-HTTP POST som webbformulär är av typen application/x-www-form-urlencoded, där frågeserien skrivs in i förfrågningskroppen.

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();
// ...

Notera: När du vill skicka ett HTML-formulär programmatiskt, glöm inte att ta med paren name=value för alla <input type="hidden">-element i frågeserien och naturligtvis även paren name=value för <input type="> i frågeserien och naturligtvis även paren name=value för <input type="submit">-elementet som du vill "trycka" programmatiskt (eftersom detta vanligtvis används på serversidan för att skilja ut om en knapp har tryckts och i så fall vilken). Du kan också kasta den erhållna URLConnection till HttpURLConnection och använda dess HttpURLConnection#setRequestMethod() istället. Men om du försöker använda anslutningen för utdata måste du fortfarande ställa in URLConnection#setDoOutput() till true.

HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...

Hur som helst, om den andra sidan är en HttpServlet, så kommer dess doPost() metod att anropas och parametrarna kommer att vara tillgängliga genom HttpServletRequest#getParameter().

För att faktiskt avfyra HTTP-förfrågan

Du kan starta HTTP-förfrågan explicit med URLConnection#connect(), men förfrågan startas automatiskt på begäran när du vill få information om HTTP-svaret, t.ex. svarskroppen med hjälp av URLConnection#getInputStream() och så vidare. Exemplen ovan gör exakt detta, så anropet connect() är faktiskt överflödigt.

Samling av information om HTTP-svar

  1. HTTP-svarsstatus: Du behöver en HttpURLConnection här. Skapa den först om det behövs. int status = httpConnection.getResponseCode();
  2. HTTP response headers: (Entry header : connection.getHeaderFields().entrySet()) { System.out.println(header.getKey() + "=" + header.getValue()); }
  3. HTTP-svarskodning: När Content-Type innehåller en charset-parameter är svarskroppen troligen textbaserad och vi vill behandla svarskroppen med den teckenkodning som serversidan har angett. 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 (charset != null) { försök (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) { for (String line; (line = reader.readLine()) != null;) { // ... System.out.println(line) ? } } } else { // Det är sannolikt binärt innehåll, använd InputStream/OutputStream. }

    Håller sessionen

    Sessionen på serversidan stöds vanligtvis av en cookie. Vissa webbformulär kräver att du är inloggad och/eller spåras av en session. Du kan använda API:et CookieHandler för att hantera cookies. Du måste förbereda en CookieManager med en CookiePolicyACCEPT_ALL innan du skickar alla HTTP-förfrågningar.

// 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();
// ...

Observera att det är känt att detta inte alltid fungerar korrekt under alla omständigheter. Om det misslyckas för dig är det bäst att manuellt samla in och ställa in cookie-huvudena. Du måste i princip samla in alla Set-Cookie-huvuden från svaret på inloggningen eller den första GET-förfrågan och sedan skicka detta genom de efterföljande förfrågningarna.

// Gather all cookies on the first request.
URLConnection connection = new URL(url).openConnection();
List 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] är till för att göra sig av med cookie-attribut som är irrelevanta för serversidan som expires, path, etc. Alternativt kan du också använda cookie.substring(0, cookie.indexOf(';')) istället för split().

Streaming-läge

HttpURLConnection kommer som standard att buffra den totala förfrågningskroppen innan den skickas, oavsett om du själv har satt en fast innehållslängd med hjälp av connection.setRequestProperty("Content-Length", contentLength);. Detta kan orsaka OutOfMemoryExceptions när du samtidigt skickar stora POST-förfrågningar (t.ex. uppladdning av filer). För att undvika detta bör du ställa in HttpURLConnection#setFixedLengthStreamingMode().

httpConnection.setFixedLengthStreamingMode(contentLength);

Men om innehållslängden verkligen inte är känd i förväg kan du använda dig av chunked streaming mode genom att ställa in HttpURLConnection#setChunkedStreamingMode() i enlighet med detta. Detta kommer att ställa in HTTP-huvudet Transfer-Encoding till chunked, vilket gör att förfrågningskroppen skickas i bitar. Nedanstående exempel skickar kroppen i bitar på 1KB.

httpConnection.setChunkedStreamingMode(1024);

User-Agent

Det kan hända att [en begäran returnerar ett oväntat svar, medan det fungerar bra med en riktig webbläsare] (https://stackoverflow.com/questions/13670692/403-forbidden-with-java-but-not-web-browser). Servern blockerar förmodligen förfrågningar baserat på User-Agent förfrågningshuvudet. URLConnection kommer som standard att ställa in den på Java/1.6.0_19 där den sista delen uppenbarligen är JRE-versionen. Du kan åsidosätta detta på följande sätt:

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.

Använd User-Agent-strängen från en nyare webbläsare.

Felhantering

Om HTTP-svarskoden är 4nn (Client Error) eller 5nn (Server Error) kan du läsa HttpURLConnection#getErrorStream() för att se om servern har skickat någon användbar felinformation.

InputStream error = ((HttpURLConnection) connection).getErrorStream();

Om HTTP-svarskoden är -1 är det något som gick fel med anslutningen och svarshanteringen. Implementationen av HttpURLConnection är i äldre JREs något buggig när det gäller att hålla anslutningar vid liv. Du kanske vill stänga av det genom att ställa in systemegenskapen http.keepAlive till false. Du kan göra detta programmatiskt i början av din applikation genom att:

System.setProperty("http.keepAlive", "false");

Uppladdning av filer

Normalt använder du multipart/form-data kodning för blandat POST-innehåll (binära data och teckendata). Kodningen beskrivs närmare i RFC2388.

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();
}

Om den andra sidan är en HttpServlet, kommer dess doPost() metod att anropas och delarna kommer att vara tillgängliga genom HttpServletRequest#getPart() (observera, alltså inte getParameter() och så vidare!). Metoden getPart() är dock relativt ny, den introducerades i Servlet 3.0 (Glassfish 3, Tomcat 7, etc). Före Servlet 3.0 är det bästa valet att använda Apache Commons FileUpload för att analysera en multipart/form-data-förfrågan. Se även det här svaret för exempel på både FileUpload och Servelt 3.0.

Hantering av opålitliga eller felkonfigurerade HTTPS-webbplatser

Ibland behöver du ansluta en HTTPS-URL, kanske för att du skriver en webscraper. I det fallet kan du sannolikt få ett javax.net.ssl.SSLException: Not trusted server certificate på vissa HTTPS-webbplatser som inte håller sina SSL-certifikat uppdaterade, eller ett java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found eller javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name på vissa felkonfigurerade HTTPS-webbplatser. Följande static-initialiserare som körs en gång i din web scraper-klass bör göra HttpsURLConnection mer eftergiven när det gäller dessa HTTPS-webbplatser och därmed inte längre kasta dessa undantag.

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);
    }
}

Sista ord

Apache HttpComponents HttpClient är mycket bekvämare i detta sammanhang :)

Kommentarer (31)

När du arbetar med HTTP är det nästan alltid mer användbart att hänvisa till HttpURLConnection snarare än basklassen URLConnection (eftersom URLConnection är en abstrakt klass när du frågar efter URLConnection.openConnection() på en HTTP-URL är det vad du får tillbaka ändå).

Då kan du istället för att förlita dig på URLConnection#setDoOutput(true) för att implicit ställa in förfrågningsmetoden till POST istället göra httpURLConnection.setRequestMethod("POST") vilket vissa kanske tycker är mer naturligt (och som också gör det möjligt för dig att specificera andra förfrågningsmetoder som PUT, DELETE, ...).

Den tillhandahåller också användbara HTTP-konstanter så att du kan göra:

int responseCode = httpURLConnection.getResponseCode();

if (responseCode == HttpURLConnection.HTTP_OK) {
Kommentarer (2)

Inspirerad av denna och andra frågor på SO har jag skapat en minimal öppen källkod basic-http-client som innehåller de flesta av de tekniker som finns här.

google-http-java-client är också en bra öppen källkodresurs.

Kommentarer (2)