Implementación de Clientes HTTP con Feign Independiente

Configuración Esencial para Clientes HTTP con Feign sin Spring Cloud

Este artículo detalla la configuración de Feign de forma "nativa" o independiente para realizar invocaciones HTTP, lo cual es útil en proyectos que no utilizan el ecosistema completo de Spring Cloud. A través de una serie de pasos, se establece un cliente HTTP robusto, capaz de manejar serialización/deserialización, reintentos, tiempos de espera y la resolución de variables de entorno en los encabezados de las solicitudes.

1. Adición de Dependencias

Para empezar, se deben incluir las dependencias necesarias en el archivo pom.xml del proyecto Maven. Estas bibliotecas proporcionan la funcionalidad central de Feign, así como soporte para serialización JSON con Jackson y un cliente HTTP basado en OkHttp.

<dependencies>
    <!-- Core de Feign -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-core</artifactId>
        <version>11.10</version> <!-- Usar una versión reciente -->
    </dependency>
    <!-- Cliente HTTP basado en OkHttp -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-okhttp</artifactId>
        <version>11.10</version> <!-- Coincidir con la versión de feign-core -->
    </dependency>
    <!-- Serialización/Deserialización JSON con Jackson -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-jackson</artifactId>
        <version>11.10</version> <!-- Coincidir con la versión de feign-core -->
    </dependency>

    <!-- Lombok para reducir código boilerplate (opcional) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version> <!-- Usar una versión compatible -->
        <scope>provided</scope>
    </dependency>
</dependencies>

2. Cnofiguración del Cliente Feign

Se define una clase de configuración para ensamblar la instancia de Feign. Aquí se especifican el codificador (para serializar objetos a JSON), el decodificador (para deserializar respuestas JSON), las opciones de conexión (tiempos de espera) y la política de reintentos. Es crucial un contrato personalizado para la resolución de variables de entorno en los encabezados.

import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.jackson.JacksonEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configuración principal para la construcción del cliente Feign.
 */
@Configuration
public class ConfiguracionClienteHttpFeign {

    @Autowired
    private ContratoVariablesEntornoFeign contratoEnv; // Nuestro contrato personalizado

    @Bean
    public Feign constructorClienteHttp() {
        return Feign.builder()
                .encoder(new JacksonEncoder()) // Codificador para transformar objetos a JSON
                .decoder(new DecodificadorRespuestaGenerico()) // Decodificador personalizado para manejar la respuesta
                .options(new Request.Options(2000, 5000)) // Timeout de conexión (2s) y lectura (5s)
                .retryer(new Retryer.Default(3000, 3000, 3)) // Reintentos: 3 segundos de intervalo, 3 intentos
                .contract(contratoEnv) // Aplica el contrato para la resolución de variables
                .build();
    }
}

3. Contrato Personalizado para Variables de Entorno

Feign utiliza un mecanismo de "contrato" para procesar anotaciones y metadatos. Se extiende Contract.Default para interceptar y resolver marcadores de posición (placeholders) en los encabezados HTTP, permitiendo inyectar valores desde el entorno de Spring (application.properties, variables de sistema, etc.).

import feign.Contract;
import feign.MethodMetadata;
import feign.codec.EncodeException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Contrato de Feign para resolver variables de entorno en encabezados HTTP.
 * Permite usar ${nombre_propiedad} en las anotaciones @Headers.
 */
@Configuration
public class ContratoVariablesEntornoFeign extends Contract.Default {

    @Autowired
    private Environment entornoAplicacion; // Inyecta el entorno de Spring

    private static final String PREFIJO_MARCADOR = "${";
    private static final String SUFIJO_MARCADOR = "}";

    @Override
    protected void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) {
        super.processAnnotationOnClass(data, targetType);
        resolverPlaceholdersEnEncabezados(data);
    }

    @Override
    protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {
        super.processAnnotationOnMethod(data, annotation, method);
        resolverPlaceholdersEnEncabezados(data);
    }

    // No es necesario modificar processAnnotationsOnParameter para este caso

    private void resolverPlaceholdersEnEncabezados(MethodMetadata metadata) {
        if (metadata == null || CollectionUtils.isEmpty(metadata.template().headers())) {
            return;
        }

        // Crear un nuevo mapa para almacenar los encabezados resueltos
        Map<String, Collection<String>> encabezadosResueltos = metadata.template().headers().entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                entry -> entry.getValue().stream()
                    .map(this::obtenerValorDePropiedad)
                    .collect(Collectors.toList())
            ));

        // Reemplazar los encabezados originales con los resueltos
        metadata.template().headers().clear();
        metadata.template().headers().putAll(encabezadosResueltos);
    }

    private String obtenerValorDePropiedad(String valor) {
        if (valor.startsWith(PREFIJO_MARCADOR) && valor.endsWith(SUFIJO_MARCADOR)) {
            String clavePropiedad = valor.substring(PREFIJO_MARCADOR.length(), valor.length() - SUFIJO_MARCADOR.length());
            String valorResuelto = entornoAplicacion.getProperty(clavePropiedad);
            if (valorResuelto == null) {
                throw new EncodeException(String.format("Error: La propiedad '%s' no se encontró en el entorno para el encabezado.", clavePropiedad));
            }
            return valorResuelto;
        }
        return valor;
    }
}

4. Decodificador de Respuestas Personalizado

Para manejar un formato de respuesta común que envuelve los datos reales (ej. {"mensaje": "OK", "exito": true, "datos": {...}}), se implementa un decodificador personalizado. Este decodificador deserializa la respuesta en un contenedor genérico, verifica el estado de éxito y extrae los datos útiles o lanza una excepción si hay un eror.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JavaType;
import feign.Response;
import feign.codec.Decoder;
import feign.jackson.JacksonDecoder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.lang.reflect.Type;

/**
 * Decodificador personalizado para procesar respuestas envueltas.
 * Espera un formato como ContenedorRespuesta<T> y extrae los datos si es exitoso.
 */
public class DecodificadorRespuestaGenerico implements Decoder {

    private final JacksonDecoder decodificadorBaseJackson = new JacksonDecoder();
    private final ObjectMapper objectMapper = new ObjectMapper(); // Instancia de ObjectMapper

    @Override
    public Object decode(Response response, Type tipoDestino) throws IOException {
        // Construye un tipo Java que representa ContenedorRespuesta<tipoDestino>
        JavaType tipoEnvoltorio = objectMapper.getTypeFactory().constructParametricType(ContenedorRespuesta.class, tipoDestino);

        // Intenta decodificar la respuesta HTTP en nuestro contenedor genérico
        Object objetoDecodificado = decodificadorBaseJackson.decode(response, tipoEnvoltorio);

        if (objetoDecodificado instanceof ContenedorRespuesta) {
            ContenedorRespuesta<?> respuestaContenedor = (ContenedorRespuesta<?>) objetoDecodificado;
            if (!respuestaContenedor.isExito()) {
                throw new RuntimeException("Error del servicio: " + respuestaContenedor.getMensaje());
            }
            return respuestaContenedor.getDatos(); // Retorna los datos internos si la operación fue exitosa
        }
        throw new RuntimeException("El formato de respuesta del servicio no es el esperado.");
    }
}

5. Definición de la Interfaz del Cliente API

Se crea una interfaz Java que define los métodos para interactuar con la API remota. Utilizando anotaciones de Feign como @RequestLine y @Headers, se especifica la URL del endpoint, el método HTTP, los encabezados y los tipos de solicitud/respuesta.

import feign.Headers;
import feign.RequestLine;

import java.util.List;

/**
 * Interfaz para invocar endpoints de la API remota relacionados con métricas de construcción.
 */
public interface InterfazMetricasServicio {

    @Headers({
        "Content-Type: application/json",
        "Accept: application/json",
        "Authorization: Bearer ${credenciales.token-api}" // Uso de la variable de entorno
    })
    @RequestLine("POST /api/proyectos/metricas-compilacion") // Ruta del endpoint
    List<ResultadoCompilacion> obtenerReporteCompilaciones(PeticionMetricasCompilacion solicitud);
}

6. Fábrica de Clientes Feign

Una fábrica permite crear instancias de los clientes de servicio definidos en el paso anterior. Esto centraliza la creación de los clientes Feign, inyectando la URL base del servicio y el constructor Feign configurado previamente.

import feign.Feign;
import feign.Target;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Fábrica para la creación de instancias de clientes de servicio Feign.
 */
@Configuration
public class FactoriaClientesApi {

    @Value("${servicio.base-url}") // Valor inyectado desde application.properties
    private String urlBaseServicioRemoto;

    @Autowired
    private Feign constructorFeignBase;

    @Bean
    public InterfazMetricasServicio crearClienteMetricas() {
        return constructorFeignBase.newInstance(new Target.HardCodedTarget<>(InterfazMetricasServicio.class, urlBaseServicioRemoto));
    }
}

7. Clases de Modelo de Datos

Se definen las clases que representan la estructura de los datos enviados en las solicitudes y reciibdos en las respuestas. Estas clases son POJOs simples, a menudo complementados con anotaciones de Lombok para reducir el código boilerplate.

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Builder;

/**
 * Clase para contener la respuesta genérica de la API.
 * @param <T> Tipo de los datos de la respuesta.
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ContenedorRespuesta<T> {
    private String mensaje;
    private boolean exito; // Indica si la operación fue exitosa
    private T datos; // Los datos reales de la respuesta
}

/**
 * Clase de petición para solicitar métricas de compilación.
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PeticionMetricasCompilacion {
    private String tipoAgrupacion; // Ej. "diario", "semanal"
    private String fechaInicial;
    private String fechaFinal;
}

/**
 * Clase de respuesta que detalla los resultados de una compilación.
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ResultadoCompilacion {
    private int idCompilacion;
    private String nombreProyecto;
    private String estadoFinal; // Ej. "EXITO", "FALLO"
    private int duracionSegundos;
    private String versionSoftware;
}

8. Archivo de Propiedades

Finalmente, se configuran las propiedades necesarias en application.properties o application.yml. Esto incluye la URL base del servicio remoto y cualquier token o credencial que deba ser inyectado en los encabezados.

# URL base del servicio API remoto
servicio.base-url=http://localhost:8080/servicios-api

# Token de autenticación a inyectar en los encabezados
credenciales.token-api=mi_token_secreto_para_la_api_xyz123

Etiquetas: Feign java Spring Framework HTTP Client RESTful

Publicado el 8-10 11:57