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?
IOException
s och RuntimeException
s som NullPointerException
, ArrayIndexOutOfBoundsException
och liknande.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¶m2=%s",
URLEncoder.encode(param1, charset),
URLEncoder.encode(param2, charset));
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.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);
}
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");
// ...
HttpServlet
, så kommer dess doPost()
metod att anropas och parametrarna kommer att vara tillgängliga genom HttpServletRequest#getParameter()
.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.HttpURLConnection
här. Skapa den först om det behövs.
int status = httpConnection.getResponseCode();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.
}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 CookiePolicy
på ACCEPT_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<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]
ä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()
.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 OutOfMemoryException
s 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);
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.
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");
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();
}
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.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);
}
}
Apache HttpComponents HttpClient är mycket bekvämare i detta sammanhang :)
Om allt du vill göra är att analysera och extrahera data från HTML, är det bättre att använda en HTML-analysator som Jsoup.
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) {
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.