Użycie java.net.URLConnection
jest pytane tutaj dość często, a Oracle tutorial jest zbyt zwięzły w tej kwestii.
Ten samouczek w zasadzie pokazuje tylko, jak wystrzelić żądanie GET i odczytać odpowiedź. Nie wyjaśnia on nigdzie jak używać go między innymi do wykonywania żądania POST, ustawiania nagłówków żądania, odczytywania nagłówków odpowiedzi, radzenia sobie z ciasteczkami, wysyłania formularza HTML, wysyłania pliku, itd.
Jak więc mogę użyć java.net.URLConnection
do odpalenia i obsługi "zaawansowanych" żądań HTTP?
IOException
i RuntimeException
, takimi jak NullPointerException
, ArrayIndexOutOfBoundsException
i innymi.Najpierw musimy znać przynajmniej adres URL i zestaw znaków. Parametry są opcjonalne i zależą od wymagań funkcjonalnych.
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
i być połączone przez &
. Zazwyczaj należy również URL-encode parametrów zapytania z określonym charsetem używając URLEncoder#encode()
.
The String#format()
jest tylko dla wygody. Wolę to, gdy potrzebuję operatora konkatenacji +
więcej niż dwa razy.To banalne zadanie. Jest to domyślna metoda żądania.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Każdy ciąg zapytania powinien być konkatenowany do adresu URL za pomocą ?
. Nagłówek Accept-Charset
może podpowiedzieć serwerowi w jakim kodowaniu są parametry. Jeśli nie wysyłasz żadnego łańcucha zapytania, wtedy możesz pominąć nagłówek Accept-Charset
. Jeśli nie potrzebujesz ustawiać żadnych nagłówków, możesz nawet użyć metody skrótu URL#openStream()
.
InputStream response = new URL(url).openStream();
// ...
Tak czy inaczej, jeśli druga strona jest HttpServlet
, to jej metoda doGet()
zostanie wywołana, a parametry będą dostępne przez HttpServletRequest#getParameter()
.
Dla celów testowych, możesz wydrukować treść odpowiedzi na stdout jak poniżej:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Ustawienie URLConnection#setDoOutput()
na true
domyślnie ustawia metodę żądania na POST. Standardowy HTTP POST, jak to robią formularze internetowe, jest typu application/x-www-form-urlencoded
, gdzie łańcuch zapytania jest zapisany w treści żądania.
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();
// ...
Uwaga: ilekroć'chciałbyś przesłać formularz HTML programowo, nie zapomnij wziąć par name=value
z dowolnych elementów <input type="hidden">
do łańcucha zapytania i oczywiście także pary name=value
z elementów <input type="submit">
elementu, który chciałbyś "wcisnąć" programowo (ponieważ jest on zazwyczaj używany po stronie serwera do rozróżnienia, czy przycisk został wciśnięty, a jeśli tak, to który).
Możesz również rzutować uzyskane URLConnection
na HttpURLConnection
i użyć jego HttpURLConnection#setRequestMethod()
zamiast tego. Ale jeśli'próbujesz użyć tego połączenia do wyjścia, nadal musisz ustawić URLConnection#setDoOutput()
na true
.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
HttpServlet
, to jej metoda doPost()
zostanie wywołana, a parametry będą dostępne przez HttpServletRequest#getParameter()
.URLConnection#connect()
, ale żądanie zostanie automatycznie odpalone na żądanie, gdy będziesz chciał uzyskać jakiekolwiek informacje o odpowiedzi HTTP, takie jak ciało odpowiedzi za pomocą URLConnection#getInputStream()
i tak dalej. Powyższe przykłady robią dokładnie to, więc wywołanie connect()
jest w rzeczywistości zbędne.HttpURLConnection
. Rzuć go najpierw, jeśli to konieczne.
int status = httpConnection.getResponseCode();Content-Type
zawiera parametr charset
, wtedy ciało odpowiedzi jest prawdopodobnie tekstowe i my'chcielibyśmy przetworzyć ciało odpowiedzi z określonym po stronie serwera kodowaniem znaków wtedy.
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) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) {
for (String line; (line = reader.readLine()) != null;) {
// ... System.out.println(line) ?
}
}
} else {
// Jest to prawdopodobnie zawartość binarna, użyj InputStream/OutputStream.
}Sesja po stronie serwera jest zwykle wspierana przez plik cookie. Niektóre formularze internetowe wymagają, aby użytkownik'był zalogowany i/lub są śledzone przez sesję. Możesz użyć API CookieHandler
do utrzymania ciasteczek. Musisz przygotować CookieManager
z CookiePolicy
o wartości ACCEPT_ALL
przed wysłaniem wszystkich żądań HTTP.
// 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();
// ...
Zauważ, że to nie zawsze działa poprawnie w każdych okolicznościach. Jeśli to się nie powiedzie, wtedy najlepiej jest ręcznie zebrać i ustawić nagłówki ciasteczek. Zasadniczo musisz złapać wszystkie nagłówki Set-Cookie
z odpowiedzi logowania lub pierwszego żądania GET
i następnie przekazać to przez kolejne żądania.
// 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]
jest tam, aby pozbyć się atrybutów ciasteczek, które są nieistotne dla strony serwera jak expires
, path
, itp. Alternatywnie, można również użyć cookie.substring(0, cookie.indexOf(';'))
zamiast split()
.Połączenie HttpURLConnection
będzie domyślnie buforować całą treść żądania przed wysłaniem, niezależnie od tego, czy ustawiłeś stałą długość zawartości używając connection.setRequestProperty("Content-Length", contentLength);
. Może to powodować wyjątek OutOfMemoryException
za każdym razem, gdy jednocześnie wysyłasz duże żądania POST (np. wgrywając pliki). Aby tego uniknąć, powinieneś ustawić HttpURLConnection#setFixedLengthStreamingMode()
.
httpConnection.setFixedLengthStreamingMode(contentLength);
Ale jeśli długość treści naprawdę nie jest znana wcześniej, to można skorzystać z trybu strumieniowego chunked, ustawiając odpowiednio HttpURLConnection#setChunkedStreamingMode()
. Spowoduje to ustawienie nagłówka HTTP Transfer-Encoding
na chunked
, co wymusi wysyłanie treści żądania w kawałkach. Poniższy przykład wyśle ciało w kawałkach po 1KB.
httpConnection.setChunkedStreamingMode(1024);
Może się zdarzyć, że żądanie zwraca nieoczekiwaną odpowiedź, podczas gdy działa dobrze w prawdziwej przeglądarce internetowej. Strona serwera prawdopodobnie blokuje żądania w oparciu o nagłówek żądania User-Agent
. Połączenie URLConnection
domyślnie ustawia go na Java/1.6.0_19
, gdzie ostatnia część jest oczywiście wersją JRE. Możesz to nadpisać w następujący sposób:
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.
Jeśli kod odpowiedzi HTTP to 4nn
(Błąd klienta) lub 5nn
(Błąd serwera), to możesz chcieć przeczytać HttpURLConnection#getErrorStream()
, aby sprawdzić, czy serwer wysłał jakieś użyteczne informacje o błędzie.
InputStream error = ((HttpURLConnection) connection).getErrorStream();
Jeśli kod odpowiedzi HTTP wynosi -1, to znaczy, że coś poszło nie tak z obsługą połączenia i odpowiedzi. Implementacja HttpURLConnection
jest w starszych JRE nieco zabugowana z utrzymywaniem połączeń przy życiu. Możesz chcieć to wyłączyć poprzez ustawienie właściwości systemowej http.keepAlive
na false
. Możesz to zrobić programowo na początku swojej aplikacji przez:
System.setProperty("http.keepAlive", "false");
Normalnie używałbyś multipart/form-data
kodowania dla mieszanej zawartości POST (dane binarne i znakowe). Kodowanie to jest bardziej szczegółowo opisane w 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
, to jego metoda doPost()
zostanie wywołana, a części będą dostępne przez HttpServletRequest#getPart()
(uwaga, w ten sposób nie getParameter()
i tak dalej!). Metoda getPart()
jest jednak stosunkowo nowa, została wprowadzona w Servlet 3.0 (Glassfish 3, Tomcat 7, etc). Przed Servlet 3.0, twoim najlepszym wyborem jest użycie Apache Commons FileUpload do parsowania żądania multipart/form-data
. Zobacz także ta odpowiedź dla przykładów obu podejść FileUpload i Servelt 3.0.Czasami musisz połączyć się z adresem URL HTTPS, być może dlatego, że piszesz web scrapera. W takim przypadku, możesz prawdopodobnie napotkać wyjątek javax.net.ssl.SSLException: Not trusted server certificate
na niektórych stronach HTTPS, które nie'trzymają swoich certyfikatów SSL na bieżąco, lub wyjątek java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
lub javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
na niektórych źle skonfigurowanych stronach HTTPS.
Następujący jednorazowy inicjalizator static
w twojej klasie web scrapera powinien sprawić, że HttpsURLConnection
będzie bardziej pobłażliwy dla tych stron HTTPS i tym samym nie będzie już rzucał tych wyjątków.
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]41 jest dużo wygodniejszy w tym wszystkim :)
Jeśli wszystko czego chcesz to parsowanie i wyodrębnianie danych z HTML, to lepiej użyć parsera HTML jak Jsoup
Podczas pracy z HTTP prawie zawsze bardziej użyteczne jest odwołanie się do HttpURLConnection
niż do klasy bazowej URLConnection
(ponieważ URLConnection
jest klasą abstrakcyjną, gdy zapytasz o URLConnection.openConnection()
na URL HTTP, to właśnie to otrzymasz z powrotem).
Wtedy możesz zamiast polegać na URLConnection#setDoOutput(true)
aby niejawnie ustawić metodę żądania na POST zamiast tego zrobić httpURLConnection.setRequestMethod("POST")
co niektórzy mogą uznać za bardziej naturalne (i co pozwala również na określenie innych metod żądania takich jak PUT, DELETE, ...).
Dostarcza również użytecznych stałych HTTP, dzięki czemu można zrobić:
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Zainspirowany tym i innymi pytaniami na SO, I've stworzył minimalny open source basic-http-client, który ucieleśnia większość technik znalezionych tutaj.
google-http-java-client jest również świetnym źródłem open source.