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.
- 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.
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); } } }
@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())); } }
@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)));
}
@BeforeAllpublic 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"))); }
No hay comentarios:
Publicar un comentario