¡Compártelo!
Compartir en facebook
Compartir en twitter
Compartir en linkedin

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

Oh My ZSH

Vitamina tu terminal con Oh My ZSH

Para todos los que nos dedicamos al mundo del desarrollo software, una gran parte del tiempo de nuestro trabajo pasa al lado de nuestra querida terminal. En este post hablaremos de Oh My ZSH, un paquete de utilidades, plugins y plantillas para el shell ZSH que

sdkman

SDKMAN!: ¿qué es, cómo instalarlo y utilizarlo?

En este artículo vamos a hablar un poco sobre qué es SDKMAN!, cómo se instala y qué nos ofrece. Veremos también un ejemplo real de cómo instalar varias versiones de Java utilizando esta herramienta. ¡Sigue leyendo! ¿Qué es SDKMAN!? SDKMAN! es una herramienta que nos

DevToys

DevToys, o cómo facilitar el día a día de un desarrollador software

Microsoft nos presenta, de la mano de Etienne Baudoux, DevToys, una pequeña suite de utilidades de código abierto enfocada a facilitar el día a día de los desarrolladores de software. Su creador la define como una “navaja suiza para desarrolladores” y, desde hace pocos meses,