Cómo realizar solicitudes HTTP en Java de manera efectiva

Cómo realizar solicitudes HTTP en Java: Una Guía Completa

1. Overview

En este tutorial rápido, presentamos una manera de realizar solicitudes HTTP en Java — utilizando la clase integrada de Java llamada HttpUrlConnection. Es importante notar que a partir de JDK 11, Java proporciona una nueva API para realizar solicitudes HTTP, diseñada como reemplazo de HttpUrlConnection, conocida como HttpClient API. Explora la nueva API HttpClient en Java.

2. HttpUrlConnection

La clase HttpUrlConnection nos permite realizar solicitudes HTTP básicas sin la necesidad de bibliotecas adicionales. Todas las clases necesarias forman parte del paquete java.net. Sin embargo, uno de los inconvenientes de usar este método es que el código puede ser más engorroso que otras bibliotecas HTTP y no proporciona funcionalidades más avanzadas como métodos dedicados para agregar encabezados o autenticación.

3. Creando una Solicitud

Para crear una instancia de HttpUrlConnection, utilizamos el método openConnection() de la clase URL. Es importante destacar que este método solo crea un objeto de conexión pero no establece la conexión inmediatamente.

La clase HttpUrlConnection se utiliza para todos los tipos de solicitudes, configurando el atributo requestMethod a uno de los siguientes valores: GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE.

Aquí hay un ejemplo de cómo crear una conexión a una URL dada utilizando el método GET:

URL url = new URL("http://example.com");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");

4. Añadiendo Parámetros a la Solicitud

Si queremos agregar parámetros a una solicitud, tenemos que establecer la propiedad doOutput en true, y luego escribir un String en el formato param1=value&param2=value en el OutputStream de la instancia HttpUrlConnection:

Map<String, String> parameters = new HashMap<>();
parameters.put("param1", "val");

con.setDoOutput(true);
DataOutputStream out = new DataOutputStream(con.getOutputStream());
out.writeBytes(ParameterStringBuilder.getParamsString(parameters));
out.flush();
out.close();

Para facilitar la transformación del mapa de parámetros, hemos escrito una clase utilitaria llamada ParameterStringBuilder que contiene un método estático, getParamsString(), que transforma un Map en un String con el formato requerido:

public class ParameterStringBuilder {
    public static String getParamsString(Map<String, String> params) 
      throws UnsupportedEncodingException{
        StringBuilder result = new StringBuilder();

        for (Map.Entry<String, String> entry : params.entrySet()) {
          result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
          result.append("=");
          result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
          result.append("&");
        }

        String resultString = result.toString();
        return resultString.length() > 0
          ? resultString.substring(0, resultString.length() - 1)
          : resultString;
    }
} 

5. Estableciendo Encabezados de Solicitud

Agregar encabezados a una solicitud se logra mediante el método setRequestProperty():

con.setRequestProperty("Content-Type", "application/json");

Para leer el valor de un encabezado de una conexión, podemos utilizar el método getHeaderField():

String contentType = con.getHeaderField("Content-Type");

6. Configurando Tiempos de Espera

La clase HttpUrlConnection permite configurar los tiempos de espera para conectar y leer. Estos valores definen el intervalo de tiempo a esperar para que se establezca la conexión al servidor o para que los datos estén disponibles para lectura.

Para establecer los valores de tiempo de espera, utilizamos los métodos setConnectTimeout() y setReadTimeout():

con.setConnectTimeout(5000);
con.setReadTimeout(5000);

En el ejemplo, ambos valores de tiempo de espera se establecen a cinco segundos.

7. Manejo de Cookies

El paquete java.net contiene clases que facilitan el trabajo con cookies, como CookieManager y HttpCookie.

Para leer las cookies de una respuesta, recuperamos el valor del encabezado Set-Cookie y lo analizamos a una lista de objetos HttpCookie:

String cookiesHeader = con.getHeaderField("Set-Cookie");
List<HttpCookie> cookies = HttpCookie.parse(cookiesHeader);

Luego agregamos las cookies al almacenamiento de cookies:

cookies.forEach(cookie -> cookieManager.getCookieStore().add(null, cookie));

Podemos verificar si hay una cookie llamada username presente, y si no, la agregamos al almacenamiento de cookies con un valor de “john”:

Optional<HttpCookie> usernameCookie = cookies.stream()
  .findAny().filter(cookie -> cookie.getName().equals("username"));
if (usernameCookie.isEmpty()) {
    cookieManager.getCookieStore().add(null, new HttpCookie("username", "john"));
}

Finalmente, para agregar las cookies a la solicitud, necesitamos establecer el encabezado Cookie, después de cerrar y volver a abrir la conexión:

con.disconnect();
con = (HttpURLConnection) url.openConnection();

con.setRequestProperty("Cookie", 
  StringUtils.join(cookieManager.getCookieStore().getCookies(), ";"));

8. Manejo de Redirecciones

Podemos habilitar o deshabilitar el seguimiento automático de redirecciones para una conexión específica utilizando el método setInstanceFollowRedirects() con el parámetro true o false:

con.setInstanceFollowRedirects(false);

También es posible habilitar o deshabilitar las redirecciones automáticas para todas las conexiones:

HttpURLConnection.setFollowRedirects(false);

Por defecto, el comportamiento está habilitado. Cuando una solicitud devuelve un código de estado 301 o 302, que indica una redirección, podemos recuperar el encabezado Location y crear una nueva solicitud a la nueva URL:

if (status == HttpURLConnection.HTTP_MOVED_TEMP
  || status == HttpURLConnection.HTTP_MOVED_PERM) {
    String location = con.getHeaderField("Location");
    URL newUrl = new URL(location);
    con = (HttpURLConnection) newUrl.openConnection();
}

9. Leyendo la Respuesta

Leer la respuesta de la solicitud se puede hacer analizando el InputStream de la instancia HttpUrlConnection.

Para ejecutar la solicitud, podemos utilizar los métodos getResponseCode(), connect(), getInputStream() o getOutputStream():

int status = con.getResponseCode();

Finalmente, leemos la respuesta de la solicitud y la guardamos en una String llamada content:

BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer content = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    content.append(inputLine);
}
in.close();

Para cerrar la conexión, utilizamos el método disconnect():

con.disconnect();

10. Leyendo la Respuesta en Solicitudes Fallidas

Si la solicitud falla, intentar leer el InputStream de la instancia HttpUrlConnection no funcionará. En su lugar, podemos consumir el flujo proporcionado por HttpUrlConnection.getErrorStream().

Podemos decidir qué InputStream usar comparando el código de estado HTTP:

int status = con.getResponseCode();

Reader streamReader = null;

if (status > 299) {
    streamReader = new InputStreamReader(con.getErrorStream());
} else {
    streamReader = new InputStreamReader(con.getInputStream());
}

Y finalmente, podemos leer el streamReader de la misma manera que en la sección anterior.

11. Construyendo la Respuesta Completa

No es posible obtener la representación de respuesta completa utilizando la instancia HttpUrlConnection. Sin embargo, podemos construirla utilizando algunos de los métodos que ofrece la instancia HttpUrlConnection:

public class FullResponseBuilder {
    public static String getFullResponse(HttpURLConnection con) throws IOException {
        StringBuilder fullResponseBuilder = new StringBuilder();

        // leer estado y mensaje

        // leer encabezados

        // leer contenido de la respuesta

        return fullResponseBuilder.toString();
    }
}

Aquí, estamos leyendo las partes de las respuestas, incluyendo el código de estado, el mensaje de estado y los encabezados, y añadiéndolos a una instancia de StringBuilder.

Primero, añadimos la información del estado de la respuesta:

fullResponseBuilder.append(con.getResponseCode())
  .append(" ")
  .append(con.getResponseMessage())
  .append("\n");

A continuación, obtenemos los encabezados usando getHeaderFields() y agregamos cada uno de ellos a nuestro StringBuilder en el formato HeaderName: HeaderValues:

con.getHeaderFields().entrySet().stream()
  .filter(entry -> entry.getKey() != null)
  .forEach(entry -> {
      fullResponseBuilder.append(entry.getKey()).append(": ");
      List headerValues = entry.getValue();
      Iterator it = headerValues.iterator();
      if (it.hasNext()) {
          fullResponseBuilder.append(it.next());
          while (it.hasNext()) {
              fullResponseBuilder.append(", ").append(it.next());
          }
      }
      fullResponseBuilder.append("\n");
});

Finalmente, leeremos el contenido de la respuesta como lo hicimos anteriormente y lo añadiremos.

Es importante notar que el método getFullResponse validará si la solicitud fue exitosa o no para decidir si necesita usar con.getInputStream() o con.getErrorStream() para recuperar el contenido de la solicitud.

12. Conclusión

En este artículo, mostramos cómo podemos realizar solicitudes HTTP utilizando la clase HttpUrlConnection.

Consejos prácticos para programadores de Java:

  • Evalúa el uso de HttpClient introducido en JDK 11 para una funcionalidad más robusta y un código más limpio.
  • Siempre maneja errores y excepciones al realizar solicitudes HTTP para garantizar una experiencia de usuario fluida.
  • Piensa en implementar lógica de reintentos en caso de fallos de red.
  • Asegúrate de cerrar las conexiones HTTP correctamente para liberar recursos.

Al dominar estas técnicas, mejorarás significativamente tus habilidades de programación en Java, especialmente en interacciones de red.

Para obtener más información, revisa los siguientes artículos sobre temas relacionados: