Cómo utilizar java.net.URLConnection para activar y manejar solicitudes HTTP
java.net.URLConnection
Aquí se pregunta con bastante frecuencia sobre el uso de , y el tutorial de Oracle también lo es. conciso al respecto.
Básicamente, ese tutorial solo muestra cómo activar una solicitud GET y leer la respuesta. No explica por ningún lado cómo usarlo para, entre otros, realizar un POST , configurar encabezados de solicitud, leer encabezados de respuesta, manejar cookies, enviar un formulario HTML, cargar un archivo, etc.
Entonces, ¿cómo puedo utilizarlo java.net.URLConnection
para activar y manejar solicitudes HTTP "avanzadas"?
Primero, un descargo de responsabilidad de antemano: los fragmentos de código publicados son todos ejemplos básicos. Necesitarás manejar IOException
mensajes triviales RuntimeException
como NullPointerException
y ArrayIndexOutOfBoundsException
consortes tú mismo.
En caso de que esté desarrollando para Android en lugar de Java, tenga en cuenta también que desde la introducción del nivel 28 de API, las solicitudes HTTP de texto sin cifrar están deshabilitadas de forma predeterminada . Se le recomienda utilizar HttpsURLConnection
, pero si es realmente necesario, se puede habilitar el texto sin cifrar en el Manifiesto de la aplicación.
Preparando
Primero necesitamos saber al menos la URL y el juego de caracteres. Los parámetros son opcionales y dependen de los requisitos funcionales.
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));
Los parámetros de consulta deben estar en name=value
formato y estar concatenados por &
. Normalmente, también codificaría en URL los parámetros de consulta con el juego de caracteres especificado usando URLEncoder#encode()
.
Es String#format()
sólo por conveniencia. Lo prefiero cuando necesito el operador de concatenación de cadenas +
más de dos veces.
Activar una solicitud HTTP GET con (opcionalmente) parámetros de consulta
Es una tarea trivial. Es el método de solicitud predeterminado.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Cualquier cadena de consulta debe concatenarse con la URL mediante ?
. El Accept-Charset
encabezado puede indicarle al servidor en qué codificación se encuentran los parámetros. Si no envía ninguna cadena de consulta, puede omitir el Accept-Charset
encabezado. Si no necesita configurar ningún encabezado, incluso puede utilizar el URL#openStream()
método de acceso directo.
InputStream response = new URL(url).openStream();
// ...
De cualquier manera, si el otro lado es un HttpServlet
, entonces doGet()
se llamará a su método y los parámetros estarán disponibles para HttpServletRequest#getParameter()
.
Para fines de prueba, puede imprimir el cuerpo de la respuesta en la salida estándar como se muestra a continuación:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Activar una solicitud HTTP POST con parámetros de consulta
Establecer URLConnection#setDoOutput()
implícitamente true
el método de solicitud en POST. El HTTP POST estándar como lo hacen los formularios web es de un tipo application/x-www-form-urlencoded
en el que la cadena de consulta se escribe en el cuerpo de la solicitud.
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();
// ...
Nota: siempre que desee enviar un formulario HTML mediante programación, no olvide incluir los name=value
pares de cualquier <input type="hidden">
elemento en la cadena de consulta y, por supuesto, también el name=value
par del <input type="submit">
elemento que desea "presionar" mediante programación (porque (que generalmente se usa en el lado del servidor para distinguir si se presionó un botón y, de ser así, cuál).
También puedes lanzar lo obtenido URLConnection
y HttpURLConnection
usarlo en su HttpURLConnection#setRequestMethod()
lugar. Pero si estás intentando usar la conexión para la salida, aún debes configurarla URLConnection#setDoOutput()
en true
.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
De cualquier manera, si el otro lado es un HttpServlet
, entonces doPost()
se llamará a su método y los parámetros estarán disponibles para HttpServletRequest#getParameter()
.
Realmente activando la solicitud HTTP
Puede activar la solicitud HTTP explícitamente con URLConnection#connect()
, pero la solicitud se activará automáticamente cuando desee obtener información sobre la respuesta HTTP, como el uso del cuerpo de la respuesta, URLConnection#getInputStream()
etc. Los ejemplos anteriores hacen exactamente eso, por lo que la connect()
llamada es superflua.
Recopilación de información de respuesta HTTP
- Estado de respuesta HTTP :
Necesitas un HttpURLConnection
aquí. Lanzalo primero si es necesario.
int status = httpConnection.getResponseCode();
Encabezados de respuesta HTTP :
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) { System.out.println(header.getKey() + "=" + header.getValue()); }
Codificación de respuesta HTTP :
Cuando Content-Type
contiene un charset
parámetro, entonces el cuerpo de la respuesta probablemente esté basado en texto y entonces nos gustaría procesar el cuerpo de la respuesta con la codificación de caracteres especificada del lado del servidor.
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.
}
manteniendo la sesión
La sesión del lado del servidor suele estar respaldada por una cookie. Algunos formularios web requieren que haya iniciado sesión y/o que se le realice un seguimiento mediante una sesión. Puede utilizar la CookieHandler
API para mantener las cookies. Debe preparar un CookieManager
con un CookiePolicy
de ACCEPT_ALL
antes de enviar todas las solicitudes 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();
// ...
Tenga en cuenta que se sabe que esto no siempre funciona correctamente en todas las circunstancias. Si le falla, lo mejor es recopilar y configurar manualmente los encabezados de las cookies. Básicamente, necesita tomar todos Set-Cookie
los encabezados de la respuesta del inicio de sesión o de la primera GET
solicitud y luego pasarlos a través de las solicitudes posteriores.
// 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]);
}
// ...
Está split(";", 2)[0]
ahí para deshacerse de los atributos de las cookies que son irrelevantes para el lado del servidor como expires
, path
etc. Alternativamente, también puede usarlos cookie.substring(0, cookie.indexOf(';'))
en lugar de split()
.
Modo de transmisión
De HttpURLConnection
forma predeterminada, almacenará en búfer todo el cuerpo de la solicitud antes de enviarla, independientemente de si usted mismo ha establecido una longitud de contenido fija usando connection.setRequestProperty("Content-Length", contentLength);
. Esto puede causar OutOfMemoryException
mensajes de correo electrónico cada vez que envía simultáneamente solicitudes POST grandes (por ejemplo, carga de archivos). Para evitar esto, le gustaría configurar el archivo HttpURLConnection#setFixedLengthStreamingMode()
.
httpConnection.setFixedLengthStreamingMode(contentLength);
Pero si realmente no se conoce la duración del contenido de antemano, entonces puede utilizar el modo de transmisión fragmentada configurando el modo HttpURLConnection#setChunkedStreamingMode()
correspondiente. Esto establecerá el Transfer-Encoding
encabezado HTTP chunked
, lo que obligará a que el cuerpo de la solicitud se envíe en fragmentos. El siguiente ejemplo enviará el cuerpo en fragmentos de 1 KB.
httpConnection.setChunkedStreamingMode(1024);
Agente de usuario
Puede suceder que una solicitud devuelva una respuesta inesperada, mientras funciona bien con un navegador web real . El lado del servidor probablemente esté bloqueando solicitudes según el User-Agent
encabezado de la solicitud. De forma predeterminada, lo URLConnection
configurará Java/1.6.0_19
donde la última parte es obviamente la versión JRE. Puede anular esto de la siguiente manera:
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.
Utilice la cadena User-Agent de un navegador reciente .
Manejo de errores
Si el código de respuesta HTTP es 4nn
(Error del cliente) o 5nn
(Error del servidor), es posible que desee leerlo HttpURLConnection#getErrorStream()
para ver si el servidor ha enviado alguna información de error útil.
InputStream error = ((HttpURLConnection) connection).getErrorStream();
Si el código de respuesta HTTP es -1, entonces algo salió mal con la conexión y el manejo de la respuesta. La HttpURLConnection
implementación en JRE más antiguos tiene algunos errores a la hora de mantener vivas las conexiones. Es posible que desee desactivarlo configurando la http.keepAlive
propiedad del sistema en false
. Puede hacer esto mediante programación al comienzo de su aplicación:
System.setProperty("http.keepAlive", "false");
Subiendo archivos
Normalmente usarías multipart/form-data
codificación para contenido POST mixto (datos binarios y de caracteres). La codificación se describe con más detalle en 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();
}
Si el otro lado es un HttpServlet
, entonces doPost()
se llamará a su método y las partes estarán disponibles HttpServletRequest#getPart()
(nota, por lo tanto no getParameter()
, ¡y así sucesivamente!). Sin embargo, el getPart()
método es relativamente nuevo y se introdujo en Servlet 3.0 (Glassfish 3, Tomcat 7, etc.). Antes de Servlet 3.0, su mejor opción era utilizar Apache Commons FileUpload para analizar una multipart/form-data
solicitud. Consulte también esta respuesta para ver ejemplos de los enfoques FileUpload y Servelt 3.0.
Tratar con sitios HTTPS que no son de confianza o mal configurados
En caso de que esté desarrollando para Android en lugar de Java, tenga cuidado : la siguiente solución puede salvarle el día si no ha implementado los certificados correctos durante el desarrollo. Pero no deberías usarlo para la producción. En estos días (abril de 2021), Google no permitirá que su aplicación se distribuya en Play Store si detecta un verificador de nombre de host inseguro; consulte https://support.google.com/faqs/answer/7188426.
A veces necesitas conectar una URL HTTPS, tal vez porque estás escribiendo un raspador web. En ese caso, es probable que te encuentres con alguien javax.net.ssl.SSLException: Not trusted server certificate
en algunos sitios HTTPS que no mantienen sus certificados SSL actualizados, o con alguien java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
en javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
algunos sitios HTTPS mal configurados.
El siguiente inicializador de ejecución única static
en su clase de raspador web debería ser HttpsURLConnection
más indulgente con esos sitios HTTPS y, por lo tanto, ya no generar esas excepciones.
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);
}
}
Ultimas palabras
Apache HttpComponents HttpClient es mucho más conveniente en todo esto :)
- Tutorial de HttpClient
- Ejemplos de HttpClient
Analizar y extraer HTML
Si todo lo que desea es analizar y extraer datos de HTML, entonces será mejor que utilice un analizador HTML como Jsoup .
- ¿Cuáles son las ventajas y desventajas de los principales analizadores HTML en Java?
- Cómo escanear y extraer una página web en Java
Cuando se trabaja con HTTP, casi siempre es más útil hacer referencia a ella HttpURLConnection
que a la clase base URLConnection
(ya que URLConnection
es una clase abstracta cuando solicitas URLConnection.openConnection()
una URL HTTP, eso es lo que obtendrás de todos modos).
URLConnection#setDoOutput(true)
Luego, en lugar de confiar en establecer implícitamente el método de solicitud en POST , puede hacer httpURLConnection.setRequestMethod("POST")
lo que algunos podrían encontrar más natural (y que también le permite especificar otros métodos de solicitud como PUT , DELETE , ...).
También proporciona constantes HTTP útiles para que puedas hacer:
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Inspirándome en esta y otras preguntas sobre Stack Overflow, he creado un cliente http básico de código abierto mínimo que incorpora la mayoría de las técnicas que se encuentran aquí.
google-http-java-client también es un excelente recurso de código abierto.
Le sugiero que eche un vistazo al código en kevinsawicki/http-request , es básicamente un contenedor HttpUrlConnection
que proporciona una API mucho más simple en caso de que solo desee realizar las solicitudes ahora mismo o puede echar un vistazo a las fuentes ( no es demasiado grande) para ver cómo se manejan las conexiones.
Ejemplo: realizar una GET
solicitud con tipo de contenido application/json
y algunos parámetros de consulta:
// 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);
Actualizar
El nuevo Cliente HTTP se envió con Java 9 pero como parte de un módulo Incubator llamado
jdk.incubator.httpclient
. Los módulos de incubadora son un medio para poner API no finales en manos de los desarrolladores mientras las API avanzan hacia su finalización o eliminación en una versión futura.
En Java 9, puedes enviar una GET
solicitud como:
// GET
HttpResponse response = HttpRequest
.create(new URI("http://www.stackoverflow.com"))
.headers("Foo", "foovalue", "Bar", "barvalue")
.GET()
.response();
Luego puedes examinar lo devuelto HttpResponse
:
int statusCode = response.statusCode();
String responseBody = response.body(HttpResponse.asString());
Dado que este nuevo cliente HTTP está enjava.httpclient
jdk.incubator.httpclient
módulo, debes declarar esta dependencia en tu module-info.java
archivo:
module com.foo.bar {
requires jdk.incubator.httpclient;
}
Hay dos opciones que puede utilizar con accesos a URL HTTP: GET / POST
OBTENER solicitud:
HttpURLConnection.setFollowRedirects(true); // Defaults to true
String url = "https://name_of_the_url";
URL request_url = new URL(url);
HttpURLConnection http_conn = (HttpURLConnection)request_url.openConnection();
http_conn.setConnectTimeout(100000);
http_conn.setReadTimeout(100000);
http_conn.setInstanceFollowRedirects(true);
System.out.println(String.valueOf(http_conn.getResponseCode()));
Solicitud de publicación:
HttpURLConnection.setFollowRedirects(true); // Defaults to true
String url = "https://name_of_the_url"
URL request_url = new URL(url);
HttpURLConnection http_conn = (HttpURLConnection)request_url.openConnection();
http_conn.setConnectTimeout(100000);
http_conn.setReadTimeout(100000);
http_conn.setInstanceFollowRedirects(true);
http_conn.setDoOutput(true);
PrintWriter out = new PrintWriter(http_conn.getOutputStream());
if (urlparameter != null) {
out.println(urlparameter);
}
out.close();
out = null;
System.out.println(String.valueOf(http_conn.getResponseCode()));