Как использовать java.net.URLConnection для запуска и обработки HTTP-запросов
Здесь довольно часто спрашивают об использовании java.net.URLConnection
, и в руководстве по Oracle об этом слишком кратко.
В этом руководстве в основном показано только, как запустить запрос GET и прочитать ответ. Нигде не объясняется, как его использовать, среди прочего, для выполнения запроса POST, установки заголовков запроса, чтения заголовков ответа, обработки файлов cookie, отправки HTML-формы, загрузки файла и т.д.
Итак, как я могу использовать java.net.URLConnection
для запуска и обработки "продвинутых" HTTP-запросов?
Переведено автоматически
Ответ 1
Заранее оговорюсь: все опубликованные фрагменты кода являются базовыми примерами. Вам нужно будет самостоятельно обрабатывать тривиальные IOException
s и RuntimeException
им подобные NullPointerException
, ArrayIndexOutOfBoundsException
и консорты.
Если вы разрабатываете для Android, а не для Java, обратите также внимание, что с момента введения API 28-го уровня HTTP-запросы открытого текста отключены по умолчанию. Рекомендуется использовать HttpsURLConnection
, но если это действительно необходимо, в манифесте приложения можно включить открытый текст.
Подготовка
Сначала нам нужно знать хотя бы URL и кодировку. Параметры необязательны и зависят от функциональных требований.
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
формате и объединяться с помощью &
. Обычно вы также кодируете URL параметры запроса с указанной кодировкой, используя URLEncoder#encode()
.
String#format()
Это просто для удобства. Я предпочитаю, когда мне понадобится оператор конкатенации строк +
более двух раз.
Запуск HTTP GET запроса с (необязательно) параметрами запроса
Это тривиальная задача. Это метод запроса по умолчанию.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Любая строка запроса должна быть объединена с URL с помощью ?
. Accept-Charset
Заголовок может подсказать серверу, в какой кодировке находятся параметры. Если вы не отправляете никакой строки запроса, вы можете оставить Accept-Charset
заголовок. Если вам не нужно устанавливать какие-либо заголовки, вы даже можете использовать URL#openStream()
метод быстрого доступа.
InputStream response = new URL(url).openStream();
// ...
В любом случае, если другой стороной является HttpServlet
, то будет вызван ее doGet()
метод и параметры будут доступны через HttpServletRequest#getParameter()
.
В целях тестирования вы можете распечатать тело ответа в стандартный вывод, как показано ниже:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Запуск HTTP POST запроса с параметрами запроса
Установка URLConnection#setDoOutput()
на true
неявно устанавливает метод запроса на POST . Стандартный HTTP POST, который выполняют веб-формы, имеет тип, application/x-www-form-urlencoded
в котором строка запроса записывается в тело запроса.
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();
// ...
Примечание: всякий раз, когда вы хотите отправить HTML-форму программно, не забудьте включить name=value
пары любых <input type="hidden">
элементов в строку запроса и, конечно, также name=value
пару <input type="submit">
элемента, который вы хотели бы "нажать" программно (потому что это обычно используется на стороне сервера, чтобы различать, была ли нажата кнопка, и если да, то какая).
Вы также можете привести полученный URLConnection
к HttpURLConnection
и использовать его HttpURLConnection#setRequestMethod()
вместо этого. Но если вы пытаетесь использовать соединение для вывода, вам все равно нужно установить URLConnection#setDoOutput()
в true
.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
В любом случае, если другой стороной является HttpServlet
, то будет вызван ее doPost()
метод и параметры будут доступны через HttpServletRequest#getParameter()
.
Фактическое выполнение HTTP-запроса
Вы можете явно запустить HTTP-запрос с помощью URLConnection#connect()
, но запрос будет автоматически запускаться по запросу, когда вы захотите получить какую-либо информацию о HTTP-ответе, такую как тело ответа с помощью URLConnection#getInputStream()
и так далее. Приведенные выше примеры делают именно это, поэтому connect()
вызов фактически излишен.
Сбор информации об ответах HTTP
Вам нужен HttpURLConnection
здесь. При необходимости сначала выполните его.
int status = httpConnection.getResponseCode();
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
System.out.println(header.getKey() + "=" + header.getValue());
}
Если Content-Type
содержит charset
параметр, то тело ответа, скорее всего, текстовое, и тогда мы хотели бы обработать тело ответа с указанной на стороне сервера кодировкой символов.
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 {
// It's likely binary content, use InputStream/OutputStream.
}
Поддержание сеанса
Сеанс на стороне сервера обычно поддерживается файлом cookie. Некоторые веб-формы требуют, чтобы вы вошли в систему и / или отслеживались сеансом. Вы можете использовать CookieHandler
API для сохранения файлов cookie. Перед отправкой всех HTTP-запросов необходимо подготовить CookieManager
с помощью CookiePolicy
of ACCEPT_ALL
.
// 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();
// ...
Обратите внимание, что это, как известно, не всегда работает должным образом при любых обстоятельствах. Если у вас это не получается, то лучше всего вручную собрать и установить заголовки файлов cookie. По сути, вам нужно захватить все Set-Cookie
заголовки из ответа на вход в систему или первый GET
запрос, а затем передать это через последующие запросы.
// 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]
Предназначен для удаления атрибутов cookie, которые не имеют отношения к стороне сервера, таких как expires
, path
и т.д. В качестве альтернативы вы также можете использовать cookie.substring(0, cookie.indexOf(';'))
вместо split()
.
Режим потоковой передачи
HttpURLConnection
По умолчанию будет буферизовано все тело запроса перед его фактической отправкой, независимо от того, установили ли вы фиксированную длину содержимого самостоятельно, используя connection.setRequestProperty("Content-Length", contentLength);
. Это может вызвать OutOfMemoryException
s всякий раз, когда вы одновременно отправляете большие POST-запросы (например, загружаете файлы). Чтобы избежать этого, вы хотели бы установить HttpURLConnection#setFixedLengthStreamingMode()
.
httpConnection.setFixedLengthStreamingMode(contentLength);
Но если длина содержимого действительно заранее неизвестна, то вы можете использовать режим фрагментированной потоковой передачи, установив HttpURLConnection#setChunkedStreamingMode()
соответствующим образом. Это установит для Transfer-Encoding
заголовка HTTP значениеchunked
, которое принудительно отправит тело запроса порциями. В приведенном ниже примере тело будет отправляться порциями по 1 КБ.
httpConnection.setChunkedStreamingMode(1024);
User-Agent
Может случиться так, что запрос возвращает неожиданный ответ, в то время как в реальном веб-браузере он отлично работает. Вероятно, серверная сторона блокирует запросы на основе User-Agent
заголовка запроса. В URLConnection
по умолчанию будет установлено значение Java/1.6.0_19
, где последняя часть, очевидно, является версией JRE. Вы можете переопределить это следующим образом:
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.
Используйте строку User-Agent из последнего браузера.
Обработка ошибок
Если код ответа HTTP является 4nn
(Ошибка клиента) или 5nn
(Ошибка сервера), то вы можете прочитать HttpURLConnection#getErrorStream()
, чтобы узнать, отправил ли сервер какую-либо полезную информацию об ошибке.
InputStream error = ((HttpURLConnection) connection).getErrorStream();
Если код ответа HTTP равен -1, значит, что-то пошло не так с подключением и обработкой ответа. HttpURLConnection
Реализация в старых JRES несколько глючит с поддержанием работоспособности соединений. Возможно, вы захотите отключить это, установив для http.keepAlive
системного свойства значение false
. Вы можете сделать это программно в начале вашего приложения с помощью:
System.setProperty("http.keepAlive", "false");
Загрузка файлов
Обычно вы используете multipart/form-data
кодировку для смешанного содержимого POST (двоичные и символьные данные). Кодировка более подробно описана в 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
, то будет вызван ее doPost()
метод, и части будут доступны по HttpServletRequest#getPart()
(обратите внимание, таким образом, не getParameter()
и так далее!). Однако getPart()
метод является относительно новым, он представлен в Servlet 3.0 (Glassfish 3, Tomcat 7 и т.д.). До выхода Servlet 3.0 лучшим выбором было использование Apache Commons FileUpload для разбора multipart/form-data
запроса. Также смотрите Этот ответ для примеров подходов FileUpload и Servelt 3.0.
Работа с ненадежными или неправильно настроенными сайтами HTTPS
Если вы разрабатываете для Android, а не для Java, будьте осторожны: приведенный ниже обходной путь может спасти ваше положение, если у вас нет правильных сертификатов, развернутых во время разработки. Но вы не должны использовать его для производства. В эти дни (апрель 2021 г.) Google не разрешит распространение вашего приложения в Play Store, если они обнаружат небезопасный верификатор имени хоста, см. https://support.google.com/faqs/answer/7188426.
Иногда вам нужно подключить HTTPS URL, возможно, потому, что вы пишете web scraper. В этом случае вы, вероятно, столкнетесь с javax.net.ssl.SSLException: Not trusted server certificate
на некоторых HTTPS-сайтах, которые не поддерживают свои SSL-сертификаты в актуальном состоянии, или с java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
или javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
на некоторых неправильно настроенных HTTPS-сайтах.
Следующий одноразовый static
инициализатор в вашем классе web scraper должен сделать HttpsURLConnection
более снисходительным отношение к этим HTTPS-сайтам и, таким образом, больше не создавать эти исключения.
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 HttpContents HttpClient намного удобнее во всем этом :)
Синтаксический анализ и извлечение HTML
Если все, что вам нужно, это синтаксический анализ и извлечение данных из HTML, то лучше использовать анализатор HTML, такой как Jsoup.
Ответ 2
При работе с HTTP почти всегда полезнее ссылаться на HttpURLConnection
, а не на базовый класс URLConnection
(поскольку URLConnection
это абстрактный класс, когда вы запрашиваете URLConnection.openConnection()
по HTTP-URL, вы все равно получите ответ).
Тогда вы можете вместо того, чтобы полагаться на URLConnection#setDoOutput(true)
неявно установить для метода запроса значение POST вместо do httpURLConnection.setRequestMethod("POST")
, что некоторым может показаться более естественным (и что также позволяет вам указывать другие методы запроса, такие как PUT, DELETE, ...).
Он также предоставляет полезные HTTP-константы, чтобы вы могли:
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Ответ 3
Вдохновленный этим и другими вопросами о Stack Overflow, я создал минимальный открытый исходный код basic-http-client, который воплощает большинство методов, найденных здесь.
google-http-java-client также является отличным ресурсом с открытым исходным кодом.
Ответ 4
Я предлагаю вам взглянуть на код на kevinsawicki / http-request, это в основном оболочка поверх HttpUrlConnection
он предоставляет гораздо более простой API на случай, если вы просто хотите отправлять запросы прямо сейчас, или вы можете взглянуть на исходники (они не слишком большие), чтобы понять, как обрабатываются соединения.
Пример: Создайте GET
запрос с типом содержимого application/json
и некоторыми параметрами запроса:
// GET http://google.com?q=baseball%20gloves&size=100
String response = HttpRequest.get("http://google.com", true, "q", "baseball gloves", "size", 100)
.accept("application/json")
.body();
System.out.println("Response was: " + response);