¡Compártelo!

Spring Boot: cómo crear un WebClient a través de un starter paso a paso

Una de las bondades que nos ofrece Spring Boot y que más me gustan son sus starters. Un starter es una librería que podrá crear beans en el contexto de Spring para poder usarlos posteriormente. Pero, la verdadera ventaja de esto es que estos beans los podremos customizar mediante la información que proporcionamos en el archivo de configuración de nuestro proyecto. Por ejemplo, podremos crear WebClient con conexiones seguras mediante SSL predeterminadas, realizar algún tipo de autenticación antes de llamar a nuestros APIs, etc.

Para este caso, crearemos un WebClient mediante un starter y éste estará configurado para loguear todas las llamadas y respuestas. Adicionalmente a esto, crearemos una configuración para que la llamada la realice a través de un proxy, en el caso de que estemos conectados a una VPN corporativa. Por último, implementaremos nuestro propio encoder y decoder para el manejo de fechas. WebClient permite hacer muchas más configuraciones, pero para no hacer este post tan extenso usaremos únicamente las configuraciones anteriores.

Prerrequisitos:

Cómo crear un WebClient en Spring Boot paso a paso

Spring factories

Para comenzar, hablaremos del archivo spring.factories, el cual es un fichero que se encarga de indicarle a Spring los beans externos que deberá tener en cuenta para que registre en su contexto. 

En este fichero se encontrarán todas las llamadas a las clases de autoconfiguración y, cuándo Spring Boot se esté iniciando, buscará este archivo en la ruta de clases para saber los beans que deberá crear. Este fichero se debe crear dentro del directorio src/resources/META-INF. Un ejemplo de cómo Spring utiliza este tipo de configuración lo podemos ver en spring-boot-autoconfigure.

Spring Boot Starter WebClient Service

Para crear nuestro propio proyecto Spring Boot, vamos a https://start.spring.io/ y seleccionamos la siguiente configuración:

Crear un proyecto de Spring Boot
Configuración de nuestro proyecto de Spring Boot

Definición del WebClient

Cómo comentamos anteriormente, cargaremos toda la configuración que tendrá nuestro WebClient desde el fichero de configuración application.yml, del proyecto que importe la dependencia del starter

Para eso crearemos una clase, la cual tendrá definidas todas las propiedades que serán necesarias para configurar el WebClient de la manera que queremos. La clase que implementaremos en este ejemplo será WebClientDefinition.

@Data
@ConfigurationProperties(prefix = "webclient")
public class WebClientDefinition {

   private String baseUrl;
   private String accept = "application/json";
   /* Connection timeout in millis */
   private int connectTimeOut = 30000;
   /* Connection timeout in seconds */
   private int responseTimeOut = 30;
   /* Connection timeout in seconds */
   private int readTimeOut = 30;
   /* Connection timeout in seconds */
   private int writeTimeOut = 30;
   /* Enabled proxy authentication */
   private boolean proxyEnabled = false;
   /* Custom proxy definition */
   @NestedConfigurationProperty
   private ProxyDefinition proxy;

   @Data
   static class ProxyDefinition {
       private String host;
       private int port;
       private String username;
       private String password;
       public String nonProxyHosts;
   }
}

Como podemos ver, en esta clase definiremos propiedades generales como son: baseUrl, timeOut y la configuración de nuestro proxy. Todas estas propiedades se leerán desde la propiedad raíz que llamaremos WebClient, como lo indicamos en la anotación @ConfigurationProperties.

Tip: Usaremos la dependencia spring-boot-configuration-processor para agregar metadata a nuestro starter y que al momento de configurarlo el editor sea capaz de autocompletar las propiedades disponibles. Para más información puedes dar un vistazo a este articulo.

Una vez tenemos nuestra definición establecida, podremos crear nuestro WebClient indicando la forma en que se deberá crear por defecto. Para esto crearemos esta definición en la clase WebClientConfiguration.

@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(WebClientDefinition.class)
public class WebClientConfiguration {

   private static final String ISO_LOCAL_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
   private static final String LOCAL_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
   private static final String LOCAL_DATE_FORMAT = "yyyy-MM-dd";

   private final WebClientDefinition clientDefinition;

   @Bean
   public WebClient configureWebClient() {
       return WebClient.builder()
               .baseUrl(clientDefinition.getBaseUrl())
               .defaultHeader(HttpHeaders.ACCEPT, clientDefinition.getAccept())
               .exchangeStrategies(this.buildExchangeStrategies())
               .clientConnector(this.buildClientHttpConnector())
               .filters(this::buildFilters)
               .build();
   }
...
}

Inicialmente crearemos nuestro WebClient, el cual tendrá configurados los atributos que hemos cargado anteriormente de nuestra clase WebClientDefinition. Las propiedades que configuraremos son exchangeStrategies, filters y clientConnector, las cuales iremos describiendo una a una.

ExchangeStrategies

Con esto podremos sobrescribir el objeto ObjectMapper por uno propio con nuestra propia definición. En este caso, sobrescribiremos el encoder y decoder de nuestro WebClient, para que nuestras peticiones y respuestas manejen un formato de fechas específico.

private ExchangeStrategies buildExchangeStrategies() {
 ObjectMapper objectMapper = this.buildCustomWebClientObjectMapper();
 return ExchangeStrategies.builder()
         .codecs(configurer -> {
              configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, APPLICATION_JSON));
              configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, APPLICATION_JSON));
         }).build();
}

private ObjectMapper buildCustomWebClientObjectMapper() {
 return new Jackson2ObjectMapperBuilder()
          .indentOutput(true)
          .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
          .simpleDateFormat(ISO_LOCAL_DATE_TIME_FORMAT)
          .serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(LOCAL_DATE_FORMAT)))
          .serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(LOCAL_DATE_TIME_FORMAT)))
          .build();
}

Filters

Para loguear las peticiones y las respuestas nos apoyaremos de la clase ExchangeFilterFunction, la cual nos proporciona los métodos ofRequestProcessor y ofResponseProcessor, que serán los encargados de trazar nuestras peticiones y respuestas. 

private void buildFilters(List<ExchangeFilterFunction> filterFunctions) {
   filterFunctions.add(logFilterRequest());
   filterFunctions.add(logFilterResponse());
}

private static ExchangeFilterFunction logFilterRequest() {
   return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
       StringBuilder builder = new StringBuilder("Request: ");
       builder.append(System.lineSeparator())
               .append(clientRequest.method()).append(" ").append(clientRequest.url())
               .append(System.lineSeparator())
               .append("Headers :");

       clientRequest.headers().forEach((name, values) -> {
           values.forEach(value -> builder.append(name).append(": ").append(value)
                   .append(System.lineSeparator()));
       });
       log.info(builder.toString());
       return Mono.just(clientRequest);
   });
}

ClientConnector

Con esta configuración podremos definir si nuestro WebClient tendrá que realizar la llamada mediante un proxy o no. Para indicarle a nuestro método si deberá crear un proxy para la llamada, lo haremos con el atributo isProxyEnabled, el cual definimos en la clase WebClientDefinition.

private ReactorClientHttpConnector buildClientHttpConnector() {
    HttpClient httpClient = this.createHttpClient();
    return new ReactorClientHttpConnector(httpClient);
}

private HttpClient createHttpClient() {
    if (clientDefinition.isProxyEnabled()) {
        Objects.requireNonNull(clientDefinition.getProxy(), "If proxy is enabled clientDefinition cannot be null.");
        return HttpClient.create()
                .responseTimeout(Duration.ofSeconds(clientDefinition.getResponseTimeOut()))
                .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP)
                        .host(clientDefinition.getProxy().getHost())
                        .port(clientDefinition.getProxy().getPort())
                        .username(clientDefinition.getProxy().getUsername())
                        .password(username -> clientDefinition.getProxy().getPassword())
.nonProxyHosts(clientDefinition.getProxy().getNonProxyHosts())
                )
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientDefinition.getConnectTimeOut())
                .doOnConnected(this::connectionTimeOut);
    } else {
        return HttpClient.create()
                .responseTimeout(Duration.ofSeconds(clientDefinition.getResponseTimeOut()))
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientDefinition.getConnectTimeOut())
                .doOnConnected(this::connectionTimeOut);
    }
}

private void connectionTimeOut(Connection connection) {
  connection.addHandlerLast(new ReadTimeoutHandler(clientDefinition.getReadTimeOut(), TimeUnit.SECONDS));
    connection.addHandlerLast(new WriteTimeoutHandler(clientDefinition.getWriteTimeOut(), TimeUnit.SECONDS));
}

Servicio Star Wars

De la misma manera que creamos el proyecto anterior, crearemos el servicio que hará uso de nuestro starter con la siguiente configuración.

Configuración del servicio
Configuración del servicio.

Descripción

Para facilitar el desarrollo de nuestro servicio, consumiremos el API de Star Wars, la cual nos proporcionará los endpoint necesarios para poder realizar nuestra prueba.

Agregar la dependencia del starter

Una vez creado nuestro proyecto, agregaremos la dependencia de nuestro starter, el cual nos ayudará a definir nuestro propio WebClient mediante la configuración proporcionada en nuestro fichero application.yml.

<dependency>
   <groupId>com.dberna2.webclient</groupId>
   <artifactId>spring-boot-starter-webclient</artifactId>
   <version>1.0.0-SNAPSHOT</version>
</dependency>

Configuración de nuestro WebClient

Configuramos nuestro WebClient como se ve en la siguiente imagen:

Configuración del WebClient
Configuración de nuestro WebClient.

Nota: Como mencionamos anteriormente, la dependencia spring-boot-configuration-processor nos permitirá autocompletar las propiedades disponibles definidas en nuestro starter. 

Crear un controlador API

@RequestMapping("/sw/api")
public interface StarWarsController {
    @GetMapping("/planets/{id}")
    Mono<PlanetDto> getPlanetById(@PathVariable String id);
}

Como podemos ver, nuestro controlador solo cuenta con un método, el cual consultara un planeta por identificador mediante la url http://localhost:8080/sw/api/planets/1

Usar el WebClient configurado

@Service
@RequiredArgsConstructor
public class StarWarsServiceImpl implements StarWarsService {

    private final WebClient swClient;

    @Override
    public Mono<PlanetDto> getPlanetById(String id) {
        return swClient.get()
                .uri("/planets/{id}", id)
                .retrieve()
                .bodyToMono(PlanetDto.class);
    }
}

Como se ve, hemos dejado toda la responsabilidad de configurar el WebClient a nuestro starter y ahora nuestro servicio solo tendrá lo realmente necesario para poder funcionar. Con esta llamada se ejecutara una llamada GET a la url https://swapi.dev/api/planets/{id} teniendo como respuesta:

http://localhost:8080/sw/api/planets/1
{
  "name": "Tatooine",
  "gravity": "1 standard",
  "climate": "arid",
  "terrain": "desert",
  "created": "2014-12-09T13:50:49.641Z",
  "edited": "2014-12-20T20:58:18.411Z",
  "diameter": 10465,
  "population": 200000
}

Verificando la configuración de los filtros

Después de ejecutar nuestra aplicación, podemos ver los registros antes y después de la solicitud.

Request:

2021-12-06 12:59:23.256 INFO 58484 --- [main] c.d.w.s.config.WebClientConfiguration    : Request:
GET https://swapi.dev/api/planets/1
Headers :Accept: application/json

Response:

2021-12-06 12:59:25.001  INFO 58484 --- [ctor-http-nio-2] c.d.w.s.config.WebClientConfiguration    : Returned status code (200 OK)

Conclusión

En este artículo, hemos visto cómo crear un WebClient en Spring Boot a través de un starter paso a paso .Puedes ver el ejemplo completo en https://github.com/dberna2/custom-spring-boot-starter-webclient.git

Aprende más sobre Spring Boot en nuestro canal de YouTube. ¡Suscríbete!

Artículos relacionados

Apache Kafka

Qué es Apache Kafka y cómo dar los primeros pasos

En este artículo vamos a ver qué es Apache Kafka y cuáles son los primeros pasos para empezar a utilizar esta tecnología clave para realizar procesamiento de datos en tiempo real. ¿Qué es Apache Kafka? Apache Kafka es un sistema de mensajería distribuido y escalable

microfrontends

Microfrontends: qué son, ventajas y cuándo utilizarlos

En este post analizamos qué son los microfrontends, por qué y cuándo utilizarlos y cuáles son sus principales ventajas e inconvenientes. ¡Vamos allá! ¿Qué son los microfrontends? Los microfrontends extienden los conceptos de microservicios al ecosistema frontend. Esto surge ante la necesidad de dividir en

Desarrollador Full Stack

Desarrollador Full Stack: qué es, qué hace y qué necesita saber

Seguro que en los últimos tiempos has oído hablar de la figura del desarrollador Full Stack. Y no es extraño, ya que se ha convertido en uno de los profesionales más solicitados por las empresas en la actualidad. Si no tienes muy claro qué hace