El uso de java.net.URLConnection
se pregunta con bastante frecuencia aquí, y el tutorial de Oracle es demasiado conciso al respecto.
Ese tutorial básicamente sólo muestra cómo lanzar una petición GET y leer la respuesta. No explica en ninguna parte cómo usarlo para, entre otras cosas, realizar una petición POST, establecer las cabeceras de la petición, leer las cabeceras de la respuesta, tratar con las cookies, enviar un formulario HTML, subir un archivo, etc.
Entonces, ¿cómo puedo utilizar java.net.URLConnection
para disparar y manejar "avanzadas" peticiones HTTP?
IOException
s y RuntimeException
s triviales como NullPointerException
, ArrayIndexOutOfBoundsException
y otras similares.Primero necesitamos saber al menos la URL y el charset. 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));
name=value
y estar concatenados por &
. Normalmente también URL-encode los parámetros de consulta con el conjunto de caracteres especificado utilizando URLEncoder#encode()
.
El String#format()
es sólo por conveniencia. Lo prefiero cuando necesito el operador de concatenación de cadenas +
más de dos veces.Es una tarea trivial. Es el método de petición por defecto.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Cualquier cadena de consulta debe ser concatenada a la URL usando ?
. La cabecera Accept-Charset
puede indicar al servidor en qué codificación están los parámetros. Si no envías ninguna cadena de consulta, puedes dejar de lado la cabecera Accept-Charset
. Si no necesita establecer ninguna cabecera, puede incluso utilizar el método abreviado URL#openStream()
.
InputStream response = new URL(url).openStream();
// ...
En cualquier caso, si la otra parte es un HttpServlet
, entonces se llamará a su método doGet()
y los parámetros estarán disponibles mediante HttpServletRequest#getParameter()
.
Para probarlo, puede imprimir el cuerpo de la respuesta en stdout como se indica a continuación:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Al establecer el valor de URLConnection#setDoOutput()
a true
se establece implícitamente el método de solicitud a POST. El HTTP POST estándar, tal y como lo hacen los formularios web, es del tipo application/x-www-form-urlencoded
en el que la cadena de consulta se escribe en el cuerpo de la petición.
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 quieras enviar un formulario HTML mediante programación, no olvides tomar los pares name=value
de cualquier elemento <input type="hidden">
en la cadena de consulta y, por supuesto, también el par name=value
del elemento <input type="submit">
que quieras "pulsar" mediante programación (porque normalmente se utiliza en el lado del servidor para distinguir si se ha pulsado un botón y, si es así, cuál).
También puedes convertir la URLConnection
obtenida en HttpURLConnection
y utilizar su HttpURLConnection#setRequestMethod()
. Pero si estás tratando de usar la conexión para la salida todavía tienes que establecer URLConnection#setDoOutput()
a true
.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
HttpServlet
, entonces su método doPost()
será llamado y los parámetros estarán disponibles por HttpServletRequest#getParameter()
.URLConnection#connect()
, pero la petición se lanzará automáticamente bajo demanda cuando quieras obtener cualquier información sobre la respuesta HTTP, como el cuerpo de la respuesta usando URLConnection#getInputStream()
y demás. Los ejemplos anteriores hacen exactamente eso, así que la llamada a connect()
es de hecho superflua.HttpURLConnection
. Castéalo primero si es necesario.
int status = httpConnection.getResponseCode();Content-Type
contiene un parámetro charset
, es probable que el cuerpo de la respuesta esté basado en texto y nos gustaría procesar el cuerpo de la respuesta con la codificación de caracteres especificada por el 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;
romper;
}
}
if (charset != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset)) {
for (String line; (line = reader.readLine()) != null;) {
// ... System.out.println(line) ?
}
}
} else {
// Es probable que sea contenido binario, usa InputStream/OutputStream.
}La sesión del lado del servidor suele estar respaldada por una cookie. Algunos formularios web requieren que se inicie una sesión y/o que se siga una sesión. Puedes utilizar la API CookieHandler
para mantener las cookies. Necesitas preparar un CookieManager
con una CookiePolicy
de ACCEPT_ALL
antes de enviar todas las peticiones 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 recoger y establecer manualmente las cabeceras de las cookies. Básicamente, necesitas coger todas las cabeceras Set-Cookie
de la respuesta del inicio de sesión o de la primera petición GET
y luego pasar esto a través de las peticiones 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]);
}
// ...
split(";", 2)[0]
está ahí para deshacerse de los atributos de las cookies que son irrelevantes para el lado del servidor como expires
, path
, etc. Como alternativa, también puede utilizar cookie.substring(0, cookie.indexOf(';'))
en lugar de split()
.La HttpURLConnection
almacenará por defecto todo el cuerpo de la petición antes de enviarlo, independientemente de que se haya establecido una longitud de contenido fija mediante connection.setRequestProperty("Content-Length", contentLength);
. Esto puede provocar una OutOfMemoryException
cuando se envían simultáneamente grandes peticiones POST (por ejemplo, al subir archivos). Para evitarlo, es conveniente establecer la función HttpURLConnection#setFixedLengthStreamingMode()
.
httpConnection.setFixedLengthStreamingMode(contentLength);
Pero si la longitud del contenido no se conoce de antemano, entonces puede hacer uso del modo de transmisión en trozos estableciendo el HttpURLConnection#setChunkedStreamingMode()
en consecuencia. Esto establecerá la cabecera HTTP Transfer-Encoding
como chunked
lo que forzará el envío del cuerpo de la petición en trozos. El siguiente ejemplo enviará el cuerpo en trozos de 1KB.
httpConnection.setChunkedStreamingMode(1024);
Puede ocurrir que una petición devuelva una respuesta inesperada, mientras que funciona bien con un navegador real. El lado del servidor probablemente está bloqueando las peticiones basándose en la cabecera de petición User-Agent
. La cabecera URLConnection
por defecto se establece en Java/1.6.0_19
donde la última parte es obviamente la versión de 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.
Si el código de respuesta HTTP es 4nn
(Error del Cliente) o 5nn
(Error del Servidor), entonces puede querer leer el 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 ha ido mal en el manejo de la conexión y la respuesta. La implementación de HttpURLConnection
es, en las JREs más antiguas, un poco problemática a la hora de mantener las conexiones vivas. Es posible que quiera desactivarla estableciendo la propiedad del sistema http.keepAlive
a false
. Puedes hacer esto de forma programada al principio de tu aplicación mediante:
System.setProperty("http.keepAlive", "false");
Normalmente se utiliza la codificación multipart/form-data
para contenidos POST mixtos (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();
}
HttpServlet
, entonces se llamará a su método doPost()
y las partes estarán disponibles por medio de HttpServletRequest#getPart()
(¡nótese, por tanto no getParameter()
y demás!). El método getPart()
es sin embargo relativamente nuevo, se introdujo en Servlet 3.0 (Glassfish 3, Tomcat 7, etc). Antes de Servlet 3.0, su mejor opción es usar Apache Commons FileUpload para analizar una petición multipart/form-data
. Vea también esta respuesta para ejemplos de ambos enfoques, el de FileUpload y el de Servelt 3.0.A veces necesitas conectar una URL HTTPS, quizás porque estás escribiendo un raspador web. En ese caso, es probable que te encuentres con una javax.net.ssl.SSLException: Not trusted server certificate
en algunos sitios HTTPS que no mantienen sus certificados SSL actualizados, o una java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
o javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
en algunos sitios HTTPS mal configurados.
El siguiente inicializador static
que se ejecuta una sola vez en su clase de raspador web debería hacer que HttpsURLConnection
sea más indulgente en cuanto a esos sitios HTTPS y, por lo tanto, ya no lance 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);
}
}
El Apache HttpComponents HttpClient es mucho más conveniente en todo esto :)
Si todo lo que quieres es parsear y extraer datos de HTML, entonces mejor usa un parser de HTML como Jsoup
Cuando se trabaja con HTTP es casi siempre más útil referirse a HttpURLConnection
que a la clase base URLConnection
(ya que URLConnection
es una clase abstracta cuando se pide URLConnection.openConnection()
en una URL HTTP que es lo que se obtiene de todos modos).
Entonces puedes, en lugar de confiar en URLConnection#setDoOutput(true)
para establecer implícitamente el método de petición a POST, hacer httpURLConnection.setRequestMethod("POST")
que algunos podrían encontrar más natural (y que también te permite especificar otros métodos de petición como PUT, DELETE, ...).
También proporciona útiles constantes HTTP para que pueda hacer:
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Inspirado por esta y otras preguntas en SO, he creado un código abierto mínimo basic-http-client que incorpora la mayoría de las técnicas encontradas aquí.
google-http-java-client es también un gran recurso de código abierto.