Mostrando entradas con la etiqueta web service client. Mostrar todas las entradas
Mostrando entradas con la etiqueta web service client. Mostrar todas las entradas

domingo, 7 de enero de 2024

Wiremock y Quarkus: Como configurarlo

Wiremock es una gran herramienta que puede ayudarte en la realización de pruebas de calidad de tu código. Un paso fundamental a la hora de desarrollar pruebas de aplicaciones o servicios que invocan a otros. Las pruebas son un paso fundamental en el desarrollo de software. Y Quarkus es un framework Java diseñado especialmente para el desarrollo de aplicaciones en contenedores, sin servidor y en la nube, especialmente para Kubernetes. De ambos hemos hablado varias veces ya.

Hoy veremos cómo podemos usarlos juntos, puesto que su uso no es tan sencillo, como pudiera ser con otra aplicación realizada con otro framework Java. Y para lograrlo haremos uso de la anotación @QuarkusTestResource. En una clase de testing debemos añadir dicha anotación asociada a una clase que implemente a su vez la interfaz QuarkusTestResourceLifecycleManager. Esto nos permitirá inicializar un servicio externo antes de la ejecución de las propias pruebas. En nuestro caso, nos permitirá arrancar el servidor Wiremock.

public class WiremockTestResource implements QuarkusTestResourceLifecycleManager {
  public static WireMockServer wireMockServer;
  @Override
  public Map<String, String> start() {
    wireMockServer = new WireMockServer(57001);
    wireMockServer.start();
    return new HashMap<String, String>();
  }
  @Override
  public void stop() {
    if (wireMockServer != null) {
      wireMockServer.stop();
    }
  }
}

Una vez creada la clase que permite arrancar Wiremock, haremos uso de ella a través de la anotación @QuarkusTestResource. De esta forma ya solo nos quedará indicar los stubs asociados a nuestros tests.

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class) //Resource that call external service
@QuarkusTestResource(WiremockTestResource.class)
public class WiremockResourceBasicTest {
  @Test
  public void getById() {
    Log.info("WiremockResourceBasicTest - getById");
    ObjectMapper mapper = new ObjectMapper();
    Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
    Log.info("WM port: " + WiremockTestResource.wireMockServer.getOptions().portNumber());
    WiremockTestResource.wireMockServer.stubFor(
      WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse()
      .withJsonBody(mapper.valueToTree(movie))
      .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
    when().get("/1").andReturn().then().statusCode(200)
      .body("id", is(1)).body("name", is("Dune")).body("director",
      is("Denis Villeneuve"));
  }
}

Como veis es muy sencillo. Ahora veremos una opción un poco más complejas pero que permitirá crear el servidor de Wiremock con más opciones de configuración y modificarlas dinámicamente. Aunque el desarrollo es un poco lioso, debido a que tiene referencias cruzadas. 

Por un lado crearemos una anotación que tenga las propiedades que queramos modificar dinámicamente posteriormente a través de la propia anotación. Pero también le añadiremos la anotación @QuarkusTestResource que hará referencia a la clase que controla el servidor de Wiremock. 

@QuarkusTestResource(value = WiremockResourceConfigurable.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WiremockTestAnnotation {
    String port() default "57005";
}

Ahora mostraremos la clase que configura el servidor de Wiremock. Será similar al ejemplo anterior, pero con tres añadidos. Para empezar, la interfaz que vamos a implementar será QuarkusTestResourceConfigurableLifecycleManager, y está estará asociada a la anotación creada anteriormente. Luego, a través de los atributos de la anotación, en el método init, podremos configurar dinámicamente el servidor. Y por último, con el retorno del método start, también podremos sobre escribir valores del fichero application.properties con los valores dinámicos. En este caso, a través de esta sobre escritura, podremos modificar el puerto del servicio externo pre configurado para el cliente.

public class WiremockResourceConfigurable 
  implements QuarkusTestResourceConfigurableLifecycleManager<WiremockTestAnnotation> {	
    public static WireMockServer server;
    private String port;
    @Override
    public void init(WiremockTestAnnotation params) {
        port = params.port();
    }
    @Override
    public Map<String, String> start() {
	server = new WireMockServer(Integer.valueOf(port));
        server.start();
	return  Map.of(
          "quarkus.rest-client.\"com.home.example.service.ExternalService\".url", 
          "http://localhost:"+port
          );
    }
    //...
}

Ya solo nos quedaría mostrar el test, haciendo uso de la anotación que nos permitiría modificar el puerto del servidor wiremock y del servicio externo dinámicamente. Muy similar al anterior. 

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
@WiremockTestAnnotation(port = "57005")
public class WiremockResourceTest {
    @Inject
    @ConfigProperty(name = "quarkus.rest-client.\"com.home.example.service.ExternalService\".url")
    private String serverUrl;
    @Test
    public void getById() {
        Log.info("serverUrl: " + serverUrl);
        ObjectMapper mapper = new ObjectMapper();
        Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
        WiremockResourceConfigurable.server.stubFor(
            WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse().withJsonBody(mapper.valueToTree(movie))
	    .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));	
        when().get("/1").andReturn().then().statusCode(200)
            .body("id", is(1)).body("name", is("Dune")).body("director",is("Denis Villeneuve"));
    }
}

Con esto hemos visto una particularidad más para mejorar los test en Quarkus. Y aunque Wiremock es una gran herramienta que desde hace tiempo es muy util para el desarrollo de pruebas. No quisiera omitir, que también es posible mockear el servicio externo de una forma sencilla con Quarkus.  Y esto es posible haciendo uso de las anotaciones @InjectMock y @RestClient. En el ejemplo que mostramos a continuación, ExternalService ha sido todo el tiempo el cliente web que hemos mockeado con Wiremock.  

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
public class WiremockResourceInjectTest {
    @InjectMock
    @RestClient
    ExternalService extService;
    @Test
    public void getById() {
        Mockito.when(extService.getMovieById(Mockito.anyLong())).thenReturn(new Movie(1L, "Denis Villeneuve", "Dune"));
        when().get("/1").andReturn().then().statusCode(200).body("id", is(1)).body("name", is("Dune")).body("director",
            is("Denis Villeneuve"));
    }
}

Espero que como siempre haya sido util, y si quieres puedes ver el código aquí. 

miércoles, 18 de enero de 2023

Http Client 5: Manual básico

Ya hemos hecho otros posts sobre esta gran librería, sobre configuración y uso de la interfaz Fluent. Hoy veremos un manual básico sobre la nueva versión, HTTP Client 5 en su versión clásica. Y cuatro sencillos ejemplos para su uso más común. 

Veremos el ejemplo más básico y completo, con una llamada HTTP Post. En 5 sencillos pasos, podemos invocar el método y obtener su respuesta. Incluso en métodos que no necesitan enviar datos, como GET o DELETE se puede hacer incluso en menos pasos. 

public static String post(final String url, final String jsonBody) {
  // 1. Create HTTP Method
  HttpPost httpPost = new HttpPost(url);
  // 2. Set payload and content-type
  httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
  String result = null;
  // 3. Create HTTP client
  try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
    // 4. Execute the method through the HTTP client
    try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
      // 5. Read Response
      result = EntityUtils.toString(response.getEntity());
      log.info("Status Code: " + response.getCode() + " " + response.getReasonPhrase());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

El siguiente punto añadir un CookieStore que nos permita almacenar las cookies que nos envíe el servidor a través de la cabecera 'set-cookie'. Y que nos permita devolver dicha cookie al servidor. Muy útil para los casos en que se necesita una comunicación con el servidor, sobre todo con tareas de login.

private static CookieStore cookieStore = new BasicCookieStore();
private static CloseableHttpClient httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

public static String postWithCookieStore(final String url, final String jsonBody) {
  // 1. Create HTTP Method
  HttpPost httpPost = new HttpPost(url);
  // 2. Set payload and content-type
  httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
  String result = null;
  try {
    // 3. Execute the method through the HTTP client
    try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
      // 4. Read Response
      result = EntityUtils.toString(response.getEntity());
      log.info("Status Code: " + response.getCode() + " " + response.getReasonPhrase());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

Ahora veremos cómo realizar una llamada con autenticación de usuario y contraseña. Y aunque siempre se puede añadir la cabecera a mano en el método HTTP, esta librería tiene sus propias clases que permiten realizar la autenticación de una forma más segura. 

public static String getWithBasicAuth(final String url, final String user, final String pass)
    throws URISyntaxException, IOException, ParseException {
  String result = null;
  URI uri = new URI(url);
  // 1. Create a Basic Credentials provider to authenticate the call
  final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
  AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort());
  credsProvider.setCredentials(authScope, new UsernamePasswordCredentials(user, pass.toCharArray()));
  // 2. Add the credentials to the HHTP Client to use it in the call
  try (final CloseableHttpClient httpclient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build()) {
    final HttpGet httpget = new HttpGet(url);
    try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
      result = EntityUtils.toString(response.getEntity());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
} 

Con BasicCredentialsProvider, el cliente web no adjuntará la cabecera de autenticación a menos que reciba un código de petición 401. Aquí podemos ver una prueba de su funcionamiento con wiremock.

final String book = "{\"name\":\"Dune\",\"author\":\"Frank Herbert\"}";
final String bookUrl = "http://localhost:57001/book";
final String basicAuth = "Basic dXNlcjpwYXNz";

@RegisterExtension
static WireMockExtension wm1 = WireMockExtension.newInstance().options(wireMockConfig().port(57001).notifier(new ConsoleNotifier(true))).build();

@Test
public void getTest(final WireMockRuntimeInfo wmRuntimeInfo) throws URISyntaxException, ParseException, IOException {
  wm1.stubFor(get("/book/1")
      .willReturn(aResponse().withStatus(401).withHeader("Connection", "keep-alive").withHeader("WWW-Authenticate", "Basic realm=\"Fake Realm\"")));
  wm1.stubFor(get("/book/1").withHeader("Authorization", equalToIgnoreCase(basicAuth)).willReturn(ok().withBody(book)));
  String result = HttpClientUtil.getWithBasicAuth(bookUrl + "/1", "user", "pass");
  assertThat(result, equalTo(book));
  wm1.verify(2, getRequestedFor(urlEqualTo("/book/1")));
  wm1.verify(1, getRequestedFor(urlEqualTo("/book/1")).withoutHeader("Authorization"));
  wm1.verify(1, getRequestedFor(urlEqualTo("/book/1")).withHeader("Authorization", equalToIgnoreCase(basicAuth)));
}

El último ejemplo será crear un cliente que invocar a cualquier endpoint con certificado autofirmado y por tanto no de confianza. No hace falta mencionar, que aunque esto es muy util en entornos de desarrollo, no se debe realizar nunca en entornos productivos

public static String trustedAllPost(final String url) {
  String result = null;
  try {
    // 1. Create SSLContextBuilder to trust in every host
    SSLContextBuilder builder = new SSLContextBuilder();
    builder.loadTrustMaterial(null, (chain, authType) -> true);
    // 2. Create a SSLConnectionSocketFactory to not verify any hostname
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(), new NoopHostnameVerifier());
    HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create().setSSLSocketFactory(sslsf).build();
    // 3. Associate both configurations through HttpClientConnectionManager to the HTTP client
    try (CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build()) {
      HttpPost method = new HttpPost(url);
      try (CloseableHttpResponse response = httpclient.execute(method)) {
        result = EntityUtils.toString(response.getEntity());
      }
    }
  } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

domingo, 8 de agosto de 2021

Feign: Como crear clientes web

Ya hemos visto varios posts de como hacer clientes para servicios web. Hoy veremos uno más, Feign. La idea de este nuevo framework es minimizar la cantidad de código que tenemos que desarrollar para crear un cliente.  Esto lo realiza creando una capa por encima de otras librerías para la creación de clientes y a través de anotaciones en una interfaz, que nos permitirán abstraernos de la lógica interna. 

Indicar también que Feign empezó siendo un proyecto desarrollado por Netflix que formó parte de el conjunto de librerías de Spring Cloud, pero que ahora se ha liberado y se puede encontrar en modo standalone aquí. 

Como veremos, el código del cliente será muy básico y solo tendremos que explicar un par de anotaciones. En la configuración y las implementaciones que usemos es donde podremos ver más la potencia de esta librería. A continuación mostramos como puede ser un típico cliente para un servicio REST. 

@Headers("Accept: application/json")
public interface BookClient {

    @RequestLine("POST")
    @Headers("Content-Type: application/json")
    void create(Book book);

    @RequestLine("GET")
    List<Book> findAll();

    @RequestLine("GET /{id}")
    Book findById(@Param("id") Integer id);

    @RequestLine("DELETE /{id}")
    void remove(@Param("id") Integer id);

    @RequestLine("PUT /{id}")
    @Headers("Content-Type: application/json")
    void update(@Param("id") Integer id, Book book);
}

Como podemos ver muy sencillo y con 3 anotaciones. Y aunque es prácticamente auto explicativo indicaremos para que sirven cada una y además indicaremos las otras 3 que no se ven en este ejemplo:

  • @RequestLine: Nos permite indicar el contexto y método HTTP a utilizar. Nos permite con las llaves indicar valores dinámicos.
  • @Headers: Nos permite indicar una cabecera de forma fija o dinámica. Se pueden indicar a nivel global poniéndose en la interface o de forma específica poniéndola en el método que deseemos. 
  • @Param: Nos permite asociar un parámetro dinámico de la URL con un parámetro de entrada al método.
  • @Body: Se indica en el método y nos permite crear una petición con el formato que le indiquemos y con la ayuda de los parámetros. 
  • @QueryMap y @HeaderMap: Nos permiten indicar mapas de parejas clave-valor a nivel de parámetro del método que estén asociadas a parámetros de la petición o cabeceras. 
Una vez tengamos la definición, vamos a generar el cliente. Como hemos indicado anteriormente otro de los puntos fuertes de Feign es que nos abstrae de detalles de la implementación. Pero será durante la creación del cliente donde deberemos decidir con que otra librería queremos llevar a cabo la creación de dicho cliente. Tendremos varios aspectos a definir, los más básicos son:
  • Cliente: Indicar con ayuda de que librería queremos implementar dicho cliente, como OkHttp, Java 11 Http2 o Ribbon.  
  • Codificador y decodificador del cuerpo de la respuesta: Pudiendo elegir entre librerías para JSON como Jackson o GSON o XML como SAX o JAXB. 
  • Logs: Indicar la librería con la que queremos controlar las trazas. Pudiendo elegir entre la de por defecto (que van a deprecar) y SLF4J. 
  • Target: URL a la que vamos a atacar y declaración del cliente que vamos a utilizar. 

@Slf4j
public class FeignTest {
    private static BookClient bookClient;
    private static WireMockServer wireMockServer;
    private final static String FOLDER = "src/test/resources/wiremock";

    @BeforeAll
    public static void setup() {
	bookClient = Feign.builder().client(new OkHttpClient())
		.encoder(new GsonEncoder()).decoder(new GsonDecoder())
		.logger(new Slf4jLogger(BookClient.class)).logLevel(Logger.Level.FULL)
		.target(BookClient.class, "http://localhost:57002/api/books");

	wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig()
		.withRootDirectory(FOLDER).port(57002)
		.notifier(new ConsoleNotifier(true))); 
	wireMockServer.start();
    }

    @Test
    public void findById() throws Exception {
	wireMockServer.stubFor(get(urlPathEqualTo("/api/books/1"))
		.willReturn(aResponse().withBodyFile("book1.json")));

	Book book = bookClient.findById(1);
	assertThat(book.getAuthor(), equalTo("Orson S. Card"));
	wireMockServer.verify(1, getRequestedFor(urlEqualTo("/api/books/1"))
		.withHeader("Accept", WireMock.equalTo("application/json")));
    }
}

También deberemos tener en cuenta que en base a las opciones que escojamos para crear nuestro cliente deberemos indicar una dependencias u otras. Para este caso por ejemplo hemos utilizado:

  • feign-okhttp
  • feign-gson
  • feign-slf4j. Que a su vez necesita slf4j-log4j12. 
Ahora veremos como hacer manejo de errores e indicar la política de reintentos. Por un lado crearemos una clase que nos permita manejar los errores, la cual nos permitirá mandar los mensajes que requiramos o volver a intentar en función de errores concretos. Esto lo podemos hacer creando una clase que implemente ErrorDecoder. En el método que debemos sobrescribir debemos lanzar una excepción. Debemos tener en cuenta que si  no indicamos la excepción en la declaración del Web Services, el cliente lanzará una excepción del tipo UndeclaredThrowableException.

public class CustomErrorDecode implements ErrorDecoder {
	@Override
	public Exception decode(final String methodKey, final Response response) {
		if (response.status() > 399) {
			return new CustomException("Error detected in backend");
		} else {
			return new Default().decode(methodKey, response);
		}
	}
}

Para la política de reintentos podremos realizarlo fácilmente instanciando la clase Retryer e indicando los valores de espera máximo y número de reintentos. Ambas configuraciones las indicaremos en el momento de crear el cliente. 

@BeforeAll
public static void setup() {
    bookClient = Feign.builder().client(new OkHttpClient()).encoder(new GsonEncoder()).decoder(new GsonDecoder())
            .logger(new Slf4jLogger(BookClient.class)).logLevel(Logger.Level.FULL)
            .retryer(new feign.Retryer.Default(100, 1000, 3)).errorDecoder(new CustomErrorDecode())
            .options(new Request.Options(2, TimeUnit.SECONDS, 4, TimeUnit.SECONDS, true))
            .target(BookClient.class, "http://localhost:57005/api/books");

    wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().withRootDirectory(FOLDER).port(57005)
            .notifier(new ConsoleNotifier(true)));
    wireMockServer.start();
}

@Test
public void findById_ko() throws Exception {
    wireMockServer.stubFor(get(urlPathEqualTo("/api/books/1"))
            .willReturn(aResponse().withStatus(500)));

    Throwable exceptionThatWasThrown = Assertions.assertThrows(CustomException.class, () -> {
        bookClient.findById(1);
    });
    assertThat(exceptionThatWasThrown.getMessage(), equalTo("Error detected in backend"));
}

@Test
public void findById_timeout() throws Exception {
    wireMockServer.stubFor(get(urlPathEqualTo("/api/books/2"))
            .willReturn(aResponse().withBodyFile("book1.json")
            .withFixedDelay(5000)));

    Assertions.assertThrows(RetryableException.class, () -> {
        bookClient.findById(2);
    });
    wireMockServer.verify(3, getRequestedFor(urlEqualTo("/api/books/2")));
}

Otra configuración que podemos realizar es la asociación de Interceptors que nos permitan modificar todas las llamadas que vamos ha hacer a un determinado destino. Uno de las más comunes puede ser la que añada autenticación básica y para dicho objetivo ya hay un clase específica, BasicAuthRequestInterceptor. Pero si queremos crear nuestro propio interceptor deberemos empezar por implementar la interfaz RequestInterceptor. 

public class CustomTokenGeneratorInterceptor implements RequestInterceptor {
    private Token token;
    public CustomTokenGeneratorInterceptor(final String clientId, final String clientSecret) {
	token = new Token(clientId, clientSecret);
    }
    @Override
    public void apply(final RequestTemplate template) {
	template.header("X-Auth-Token", Base64.getEncoder().encodeToString(new Gson().toJson(token).getBytes()));
    }
}

Y para utilizarlo solo tendremos que añadirlo a nuestra configuración. 

@BeforeAll
public static void setup() {
    bookClient = Feign.builder().client(new OkHttpClient()).encoder(new GsonEncoder()).decoder(new GsonDecoder())
        .logger(new Slf4jLogger(BookClient.class)).logLevel(Logger.Level.FULL)
        .requestInterceptor(new CustomTokenGeneratorInterceptor("123456", "654321"))
        .target(BookClient.class, "http://localhost:57010/api/books");
    wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().withRootDirectory(FOLDER).port(57010)
        .notifier(new ConsoleNotifier(true)));
    wireMockServer.start();
}

@Test
public void findById() throws Exception {
    wireMockServer.stubFor(get(urlPathEqualTo("/api/books/1")).willReturn(aResponse().withBodyFile("book1.json")));

    Book book = bookClient.findById(1);
    String token = "eyJjbGllbnRJZCI6IjEyMzQ1NiIsImNsaWVudFNlY3JldCI6IjY1NDMyMSJ9";
    assertThat(book.getAuthor(), equalTo("Orson S. Card"));
    wireMockServer.verify(1,
        getRequestedFor(urlEqualTo("/api/books/1"))
            .withHeader("Accept", WireMock.equalTo("application/json"))
            .withHeader("X-Auth-Token", WireMock.equalTo(token)));
}

También podemos hacer uso de Ribbon como nuestra implementación del cliente. Esto nos permitirá invocar un determinado Web Service o microservicio a través de un nombre y no una IP concreta. Como la posibilidad de balancear entre distintos servidores con el servicio desplegado. Para llevarlo a cabo deberemos incluir la dependencia correspondiente y configurar el cliente como en los casos anteriores. Pero además tendremos que incluir en el classpath un fichero denominado config.properties en el cual indicaremos la lista de servidores asociados al nombre del Web Service que queremos invocar. 

@BeforeAll
public static void setup() { // config.properties include: discovery-client.ribbon.listOfServers=localhost:57010,localhost:57011 bookClient = Feign.builder().client(RibbonClient.create()).decoder(new GsonDecoder()) .logger(new Slf4jLogger(BookClient.class)).logLevel(Logger.Level.FULL) .target(BookClient.class, "http://discovery-client/api/books"); wireMockServer1 = new WireMockServer(WireMockConfiguration.wireMockConfig().withRootDirectory(FOLDER) .port(57010).notifier(new ConsoleNotifier(true))); wireMockServer1.start(); wireMockServer1.stubFor(get(urlPathEqualTo("/api/books")).willReturn(aResponse().withBodyFile("books.json"))); wireMockServer2 = new WireMockServer(WireMockConfiguration.wireMockConfig().withRootDirectory(FOLDER) .port(57011).notifier(new ConsoleNotifier(true))); wireMockServer2.start(); wireMockServer2.stubFor(get(urlPathEqualTo("/api/books")).willReturn(aResponse().withBodyFile("books.json"))); } @Test public void findAll() throws Exception { List<Book> books1 = bookClient.findAll(); List<Book> books2 = bookClient.findAll(); assertThat(books1.size(), equalTo(3)); assertThat(books2.size(), equalTo(3)); wireMockServer1.verify(1, getRequestedFor(urlEqualTo("/api/books"))); wireMockServer2.verify(1, getRequestedFor(urlEqualTo("/api/books"))); }

Y con esto hemos visto algunas de las características de Feign. Y otra forma de crear un cliente de un Web Services o Microservicio. Si quieres ver el ejemplo concreto lo puedes ver aquí.