Mostrando entradas con la etiqueta web services. Mostrar todas las entradas
Mostrando entradas con la etiqueta web services. Mostrar todas las entradas

miércoles, 11 de agosto de 2021

Apache Camel: Service Discovery con Consul

Hoy vamos a ver como utilizar la funcionalidad de Service Discovery en un proyecto de Apache Camel, pero esta vez utilizando Consul de HashiCorp. Ya hemos visto esta misma funcionalidad pero con Eureka Netflix, aqui,. Por lo que este nuevo ejemplo no se diferenciará mucho.

Pero antes, averiguemos un poco más de Consul. A igual que Eureka es una herramienta que nos permite el registro de servicios de forma centralizada, permitiendo el descubrimiento de los mismos a otros clientes sin necesidad de conocer su dirección real, solo con un identificador. Pero además puede servirnos para almacenamiento de información o de la configuración a utilizar por los distintos clientes. Pero sobre este último apartado no veremos nada concreto en este ejemplo. 

Lo primero que vamos a hacer es levantar Consul. Para el caso de las pruebas podemos levantar una única instancia de Consul a través de un simple comando de docker. Pero nosotros lo haremos un poco más complejo, ya que Consul también puede ser levantado como un cluster de servidores, donde uno de ellos es el principal. Para llevar a cabo esta aproximación, crearemos un docker compose que nos permita levantar varias instancias de Consul y dejarlas activas para el ejemplo. Aquí abajo tenemos el docker compose y si accedemos a la URL http://localhost:8500 podremos acceder a su interfaz gráfica. 

#https://github.com/consul/consul/blob/master/docker-compose.yml
version: '3'

x-consul-container: &consul-container
  image: consul:latest
  networks:
    - consul-demo

x-consul-server: &consul-server
  <<: *consul-container
  command: "agent -server -retry-join consul-server-bootstrap -client 0.0.0.0"

services:
  consul-server-1:
    hostname: consult-server-1
    container_name: consult-server-1
    <<: *consul-server

  consul-server-bootstrap:
    hostname: consul-server-bootstrap
    container_name:   consul-server-bootstrap
    <<: *consul-agent
    ports:
      - "8400:8400"
      - "8500:8500"
      - "8600:8600"
      - "8600:8600/udp"
    command: "agent -server -bootstrap-expect 2 -ui -client 0.0.0.0"

networks:
  consul-demo:

El siguiente paso será crear un micro servicio con Apache Camel. Este micro servicio será muy básico y se conectará como cliente a Consul y registrará las distintas rutas en el. De esta forma y a través de un identificador, será consumibles por otros clientes del mismo servidor.  

Para ello, por un lado añadimos las siguientes dependencias, además de las normales para un micro servicio:

  • spring-boot-starter-actuator: Nos permite exponer rutas para la verificación del estado y los datos de la aplicación. 
  • camel-consul-starter: Nos permite preconfigurar Consul en nuestra aplicación.
Aparte indicaremos las siguientes propiedades en el fichero de configuración:
  • management.endpoints.web.exposure.include: Nos permite indicar rutas de monitorización a gestionar por Spring Actuator.
  • camel.cloud.consul.enabled: Habilitar el uso de Consul.
  • camel.cloud.consul.service-host: Indicar el servidor donde se encuentra ubicado Consul. Esta propiedad será la encargada de setear el tag service address al servicio en Consul. 
  • camel.cloud.consul.url: Para indicar la ruta completa donde se encuentra desplegado Consul.
Y ahora deberemos crear las rutas a registrar en el registro de Consul. Por cada una de las rutas, indicaremos cual será el identificador y otros meta datos asociados al servicio. Esto lo podremos hacer a través de la clase ServiceRegistrationRoutePolicy y el método routeProperty que nos permitirá indicar dichos datos. 

@Component
public class BookMockRouter extends RouteBuilder {
    private static Map<Integer, Book> books = new HashMap<>();
    static {
	books.put(1, new Book(1, "Dune", "Frank Herbert"));
	books.put(2, new Book(2, "The stars my destination", "Alfred Bester"));
	books.put(3, new Book(3, "Ender's game", "Orson S. Card"));
    }

    @Override
    public void configure() throws Exception {
	ServiceRegistrationRoutePolicy policy = new ServiceRegistrationRoutePolicy();

	rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).route()
		.routeId("mockClientGetAll")
		.routeProperty(ServiceDefinition.SERVICE_META_NAME, "bookGetAll")
		.routeProperty(ServiceDefinition.SERVICE_META_PORT, "9092")
		.routeProperty(ServiceDefinition.SERVICE_META_PATH, "api/book")
		.routePolicy(policy)
		.bean(BookMockRouter.class, "getAll(})").marshal().json();

	rest().get("book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
		.route().routeId("mockGetById")
		.routeProperty(ServiceDefinition.SERVICE_META_NAME, "bookGetById")
		.routeProperty(ServiceDefinition.SERVICE_META_PORT, "9092")
		.routeProperty(ServiceDefinition.SERVICE_META_PATH, "api/book/")
		.routePolicy(policy)
		.bean(BookMockRouter.class, "getById(${header.id})").marshal().json();
    }

    public Collection<Book> getAll() {
	return books.values();
    }
    public Book getById(final Integer id) {
	return books.get(id);
    }
}

Con este simple servicio podremos comprobar dos cosas. Primero que si invocamos la ruta http://localhost:9092/api/book, veremos el listado de libros asociados a la primera de las rutas. Y segundo, que si accedemos de nuevo a Consul, podremos ver como tenemos dos servicios más registrados. Cliqueando sobre los mismos podremos ver los detalles que hemos indicado en el código fuente. 

Para terminar el ejemplo y ver que funciona no solo la parte del registro de aplicaciones de forma dinámica en Consul. Sino que también funciona el descubrimiento de estos servicios, crearemos a continuación otro micro servicio con Apache Camel que hará uso del método serviceCall para invocar a Consul. 

Este nuevo servicio será incluso más sencillo, puesto que solo necesitará las dependencias para usar Consul en una aplicación de Apache Camel. Y en cuanto a propiedades, solo necesitará dos específicas:

  • camel.cloud.consul.enabled: Habilitar el uso de Consul.
  • camel.cloud.consul.service-discovery.url: Indicar la ruta donde se encuentra ubicado Consul.
Y ahora a crear la ruta para descubrir e invocar a nuestro servicio anterior:

@Component
public class BookServiceRouter extends RouteBuilder {
    @Override
    public void configure() throws Exception {
	rest("/serviceCall").get().produces(MediaType.APPLICATION_JSON_VALUE)
		.route().removeHeader(Exchange.HTTP_URI).serviceCall("bookGetAll")
		.convertBodyTo(String.class).log("Body: ${body}").end();

	rest("/serviceCall/{id}").get().produces(MediaType.APPLICATION_JSON_VALUE)
		.route().removeHeader(Exchange.HTTP_PATH)
		.setHeader(Exchange.HTTP_PATH, simple("${header.id}"))
		.removeHeader(Exchange.HTTP_URI).serviceCall("bookGetById")
		.convertBodyTo(String.class).log("Body: ${body}").end();
    }
}

Cada uno de los nuevos servicios, invoca a un servicio diferente a través del método serviceCall. Indicando el identificador del servicio a consultar, Consul nos generará la ruta asociada a dicho servicio y podremos invocarlo. Esa ruta se compone en base a los meta datos indicados en el otro micro servicio.  

Y con esto, hemos aprendido un poco más del funcionamiento de Apache Camel y otro magnifico Service Discovery. Como siempre, todo el código lo podéis ver aquí. 

viernes, 16 de julio de 2021

Apache Camel: Como hacer balanceo de carga con Ribbon

Ya hemos visto otros post sobre como aumentar la resiliencia de los servicios web a través de Hystrix. Y hoy vamos a profundizar un poco más en ello con ayuda de un balanceador de carga como es Netflix Ribbon. El cual nos permitirá enviar las llamadas a uno u otro servicio web y proveerá de mecanismos en caso de error. 

Tradicionalmente en una estructura monolítica se ha tenido siempre un balanceador de carga con IP/DNS conocido que a su vez reparte las invocaciones que se le realizan entre los distintos nodos que tiene configurado. El cual por motivos de resiliencia y consumo se suele mantener en un servidor aparte.

Esta solución nos llevan a diversos problemas, sobre todo si lo orientamos a microservicios desplegados en la nube:

  • Es más difícil gestionarlo si los nodos destinos aumentan o disminuyen dinámicamente. 
  • El mantenerlos en un servidor aparte aumenta el coste de la solución. 
  • Unifica el punto de entrada en una sola entidad de nuestra arquitectura, pudiendo perjudicar a nuestro sistema si esta sufre problemas de conexión o rendimiento. 
  • Aumento del tráfico de red hacia un único punto, generando un cuello de botella. 
Y para solucionar estos problemas se crearon los balanceadores de carga del lado del cliente, como es Netflix Ribbon. Permitiéndonos:
  • Tener más de un punto de entrada. 
  • Asociar nodos al balanceador de forma dinámica. 
  • Reducir el coste en infraestructura.
  • Reducir el coste de trafico de red tanto a la entrada, evitando el cuello de botella. Como a la salida, al realizar llamadas entre iguales y reduciendo la latencia entre el balanceador y el cliente. 

Por tanto, aunque a través de la configuración de Spring podremos configurar cuales son los nodos a los cuales vamos a atacar. Debido a las mejoras que ofrece un balanceador de carga de cliente y dado que Ribbon también pertenece al framework de Spring Cloud Netflix. Vamos a utilizar el patrón Service Discovery, con lo cual, llamaremos a los servicios en base a su nombre y con ayuda del servidor Netflix Eureka. También vimos ya algo del tema aquí. 

Ahora veremos brevemente, como montar el servidor y los clientes. Pero si quieres más detalle puedes mirar el código aquí. El servidor Eureka lo montaremos con una aplicación puramente Spring, con la dependencia spring-cloud-starter-netflix-eureka-server y la anotación @EnableEurekaServer.

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(final String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

Eso sí, al ser una prueba de concepto en local y con un único Service Registry, tendremos que modificar el fichero de configuración de la aplicación para evitar errores de esta configuración y trazas excesivas. Y es debido a que el Service Registry está preparado para que funcione en un cluster y por tanto al iniciarse intenta buscar otras instancias y registrarse en las mismas. Por ello el fichero de configuración quedará de la siguiente forma:

spring.application.name= ${springboot.app.name:eureka-serviceregistry}
server.port = ${server-port:8761}
eureka.instance.hostname= ${springboot.app.name:eureka-serviceregistry}
eureka.client.registerWithEureka= false
eureka.client.fetchRegistry= false
eureka.client.serviceUrl.defaultZone: http://${registry.host:localhost}:${server.port}/eureka/

logging.level.com.netflix.eureka=OFF
logging.level.com.netflix.discovery=OFF

Ahora crearemos los clientes. Van a ser dos mocks, lo único que cambiará será un pequeño valor en la respuesta (para que podamos identificar claramente a quien estamos llamando) y el puerto de arranque. Estos clientes si los realizaremos con Apache Camel:

@Component
public class BookMockRouter extends RouteBuilder {
    private static Map<Integer, Book> books = new HashMap<>();
    static {
	books.put(1, new Book(1, "Dune", "Frank Herbert"));
	books.put(2, new Book(2, "The stars my destination", "Alfred Bester"));
	books.put(3, new Book(3, "Ender's game", "Orson S. Card"));
	books.put(4, new Book(4, "Configure Ribbon", "Server 1"));
    }
    @Override
    public void configure() throws Exception {
	restConfiguration().component("servlet").bindingMode(RestBindingMode.json_xml);

	rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).route().bean(BookMockRouter.class, "getAll(})")
		.marshal().json();
    }
    public Collection<Book> getAll() {
	return books.values();
    }
}

En el fichero de propiedades configuraremos el servidor de Eureka (eureka.client.serviceUrl.defaultZone), con que nombre daremos a conocer al cliente (spring.application.nam) y el puerto donde vamos a desplegar el servicio (server.port). 

#tomcat port
server.port=9090

# the name of Camel
camel.springboot.name=Service1

# to reconfigure the camel servlet context-path mapping to use /api/* instead of /camel/*
camel.component.servlet.mapping.context-path=/api/*

# expose actuator endpoint via HTTP
management.endpoints.web.exposure.include=info,health

# turn on actuator health check
management.endpoint.health.enabled = true

#Eureka configuration
spring.application.name=app-camel-mock-client
eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/
eureka.client.healthcheck.enabled= true
eureka.instance.leaseRenewalIntervalInSeconds= 1
eureka.instance.leaseExpirationDurationInSeconds= 2

Si pensamos en un entorno real y que fueran desplegados con un docker-compose por ejemplo. Lo único que diferenciaría estas instancias sería el puerto de despliegue. Y si el despliegue lo realizásemos en distintas máquinas, la configuración de cada microservicio podría ser exactamente igual. De esta forma, y con la ayuda de Netflix Eureka y Netflix Ribbon, podemos crear un cluster de microservicios. 

Por último nos queda la creación del balanceador de cliente. Aquí lo mismo, que hemos dicho con los microservicios. Como veremos ahora será muy sencillo. Tenemos que indicar al endpoint que vamos a llamar, que al utilizar Netflix Eureka será el nombre del cliente que hemos configurado. Y que deseamos utilizar el balanceador Netflix Ribbon. 

// .removeHeader(Exchange.HTTP_URI) -> avoid to include bridgeEndpoint
rest().get("book").route().removeHeader(Exchange.HTTP_URI).serviceCall()
	.name("app-camel-mock-client/api/book").ribbonLoadBalancer().end();

Si queremos una configuración básica, el fichero de propiedades no se diferenciará mucho de que hemos usado en los clientes. Simplemente añadiremos la propiedad server.ribbon.eureka.enabled=true.

Con esto y una vez que tengamos todos los servidores arrancados podemos probar como llamando a http://localhost:9092/book vamos recibiendo alternativamente la respuesta de un servidor u otro. 

Pero que pasa si se cae uno de los clientes. Hemos hablado que Ribbon proveerá mecanismos para evitar fallos y aumentar la resiliencia. Pero si tiramos uno de los dos clientes, vemos como falla 1 de cada 2 llamadas. Y esto es normal puesto que hace planificación por round-robin, por lo que una vez intentará llamar a un servidor y luego al otro. 

¿Pero porque sucede esto? Básicamente es porque el cliente se ha caído y el Service Registry aún no se ha enterado.  Por lo que Ribbon intentará seguir llamando alternativamente a ambos clientes. ¿Pero Ribbon no tiene algún mecanismo para detectar que no se encuentra activo el nodo? Si, lo tiene pero por defecto utiliza un mecanismo Dummy que dará por bueno siempre al servidor. 

La solución a este problema es implementar una clase que herede de la interfaz IPing. Y que en la construcción del balanceador se la pasemos. De esta forma Ribbon podrá verificar por si mismo si el servidor al que va a llamar se encuentra habilitado o no y evitar llamarlo en caso negativo. 

Para el ejemplo vamos a crear una implementación muy rústica que se basará en si el endpoint actuator/health de los clientes se encuentra activo y devolviendo UP. 

@Log4j2
public class ActuatorHealthPing implements IPing {
    @Override
    public boolean isAlive(final Server server) {
	server.setAlive(false);
	String urlStr = String.format("http://%s/actuator/health", server.getId());
	boolean isAlive = false;
	try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
	    HttpResponse response = httpClient.execute(new HttpGet(urlStr));
	    String content = EntityUtils.toString(response.getEntity());
	    if (content != null && "UP".equals(new ObjectMapper().readTree(content).path("status").asText())) {
		isAlive = true;
	    }
	} catch (IOException e) {
	    log.warn("Error while get server status: " + e.getMessage());
	}
	server.setAlive(isAlive);
	return isAlive;
    }
}

Por otro lado, programáticamente crearemos la configuración de Ribbon. A la cual le pasaremos una instancia de la clase que hemos creado en el paso anterior. Y se la pasaremos a nuestro balanceador de la siguiente forma.

RibbonConfiguration configuration = new RibbonConfiguration();
configuration.setPing(new ActuatorHealthPing());
RibbonServiceLoadBalancer loadBalancer = new RibbonServiceLoadBalancer(configuration);

rest().get("book").route().removeHeader(Exchange.HTTP_URI).serviceCall()
    .name("app-camel-mock-client/api/book").loadBalancer(loadBalancer).end();

Ya solo nos quedaría arrancarlo con la nueva configuración. Y comprobar como Ribbon nos sigue devolviendo una respuesta correcta a pesar de que el servidor se haya caido justo momentos antes.

Como siempre todo el código puedes verlo aquí. 

Por último. Aunque hemos hablado de las bondades de usar un Service Registry como Eureka o un balanceador como Ribbon. Hay que saber también que Apache Camel cuenta con su propia implementación de un Service Discovery, aunque en este caso tendremos que indicar manualmente las IPs de los servicios. Y también con su propia implementación de un balanceadores de carga del lado del cliente. 

sábado, 10 de julio de 2021

RESTEasy: Como crear un Web Service y su cliente

Ya hemos visto varios frameworks que nos pueden ayudar a crear un servicio web y su cliente. Hoy veremos RESTEasy que es un framework de JBoss, el cual ofrece una implementación para las especificaciones JAX-RS 2.0 y 2.1. Y como es normal, aunque esta pensado para una integración perfecta con WildFly, también nos ofrecerá la posibilidad de desplegarlo en cualquier otro contenedor de servlets como Tomcat o Jetty. 

Empecemos con el Web Service, el cual será muy básico, lo necesario como para mostrar como poder montar uno más complejo. Lo primero será indicar cuales son las dependencias mínimas necesarias:

  • resteasy-servlet-initializer
Nos permite generar el Web Service sin necesidad de configuración manual por nuestra parte. Es decir, con las anotaciones que veamos a continuación será suficiente para configurar nuestro servicio. No necesitaremos de configurar nada en el fichero web.xml tal y como hicimos en este otro post. Incluso podriamos no crear un fichero web.xml, pero esto no lo haremos para evitar problemas con Maven. 

Esta a su vez tiene como dependencia a las librerías principales del framework: resteasy-core y las librerías con las implementaciones de la especificaciones.  

  • resteasy-jackson2-provider
Es la librería que nos permite la transformación de la respuesta en formato de objeto Java a JSON. En el caso de que quisiera responder XML debería incluir la librería resteasy-jaxb-provider. Si no incluimos la librería podemos tener un error del tipo: Could not find MessageBodyWriter for response object of type: com.home.example.resteasy.bean.Book of media type: application/json.

Una vez que hemos indicado las dependencias en el fichero pom.xml y que sabemos que nos vale con un fichero web.xml bien foramdo pero sin contenido. Vamos a configurar el servicio en dos sencillos pasos. 

Debemos crear una clase que extienda de javax.ws.rs.core.Application. Y que además tenga la anotación javax.ws.rs.ApplicationPath con la cual podremos indicar el path principal de nuestro Web Service. Por tanto, para poder acceder a los métodos REST deberemos por un lado indicar el nombre del servicio y por otro el application path indicado en la anotación. Ejemplo:

@ApplicationPath("/library")
public class LibraryApplication extends Application {
}
 
El siguiente paso será crear los métodos REST. Lo podremos hacer a través de distintas anotaciones:
  • @Path: Nos permite indicar un path común para todos los métodos. 
  • @POST, @GET, @DELETE o @PUT: Indica a que método HTTP esta asociado el método. 
  • @Produces, @Consume: Permite indicar que formato tendrá la entrada o salida. JSON o XML. 
  • @PathParam, @QueryParam: Permite indicar como recibiremos parámetros a través de la URL de invocación. 
Además de esta hay otras anotaciones que nos permitirán una mayor configuración de los métodos, indicar las cookies, cabeceras, valores por defecto o incluso si los parámetros vienen codificados. 

Si queremos devolver un objeto en un formato concreto no necesitaremos nada más que devolver dicho objeto. Pero si queremos devolver una respuesta más personalizada, donde indiquemos cookies, código de respuesta, etc, lo haremos a través del objeto javax.ws.rs.core.Response. Ejemplo:

@Path("/book")
public class BookServiceImple {
    private static Map<Integer, Book> library = new HashMap<Integer, Book>();
    static {
	library.put(1, new Book(1, "Ender's Game", "Orson S. Card"));
	library.put(2, new Book(2, "The stars my destination", "Alfred Bester"));
    }
    @POST
    @Path("/")
    @Produces({ MediaType.APPLICATION_JSON })
    @Consumes({ MediaType.APPLICATION_JSON })
    public Response create(final Book book) {
	if (null != library.get(book.getId())) {
	    return Response.status(Response.Status.NOT_MODIFIED).entity("Book is already in the library.").build();
	}
	library.put(book.getId(), book);
	return Response.status(Response.Status.CREATED).build();
    }
    @GET
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON })
    public Book read(@PathParam("id") final Integer id) {
	if (library.containsKey(id)) {
	    return library.get(id);
	} else {
	    return null;
	}
    }
}

Si queremos utilizarlo, deberemos invocar el comando Maven: clean package y desplegar el fichero .war generado en nuestro servidor de contenedores o aplicaciones favorito. Y ya podremos utilizarlo invocando el siguiente comando:

curl --location --request GET 'http://localhost:8080/RestEasyService/library/book/1'

Nuestro siguiente paso será crear el cliente. El cual podremos crearlo de dos formas diferentes, pero siempre haciendo uso de la clase de utilidad ResteasyClientBuilder. La cual cuenta con un método básico de creación del cliente pero que también aporta métodos para la configuración de dicho cliente, además de permitirnos crear un objeto de tipo WebTarget. 

Una vez en este punto, podemos realizar la invocación del Web Service de dos formas diferentes. La primera de ellas, será algo más rudimentaria y necesitará de una mayor configuración para cada una de las llamadas, así como indicar la URL exacta a la hora de crear el objeto WebTarget. Este objeto nos permitirá hacer las llamadas REST correspondiente a través de sus métodos get() o post() e indicando que tipo de objeto vamos a recibir. 

public class BookClient {
    final static String path = "http://localhost:8080/RestEasyService/library/";
    public Book read_target(final Integer id) {
	Book retorno = null;
	WebTarget target = ClientBuilder.newClient().target(path + "book/" + id);
	try (Response response = target.request().get()) {
	    retorno = response.readEntity(Book.class);
	}
	return retorno;
    }
}

Para la segunda manera de crear el cliente utilizaremos el framework de RESTEasy Proxy. El cual nos permitirá realizar invocaciones más sencillas pero como punto negativo tendremos que crear una Interface que cumpla con el contrato del Web Service. El mayor inconveniente de esta interface es que tendremos que crearla manualmente a través de anotaciones JAX-RS, pero lo más positivo es podremos crear clientes de Web Services que no tienen porque haber sido generados a su vez por JAX-RS. Un ejemplo:

@Path("/book")
public interface BookService {
    @POST
    @Path("/")
    @Produces({ MediaType.APPLICATION_JSON })
    @Consumes({ MediaType.APPLICATION_JSON })
    Response create(final Book book);

    @GET
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON })
    Book read(@PathParam("id") final Integer id);
}

Para crear un objeto de tipo proxy, lo podremos obtener a través del objeto WebTarget, indicando la interfaz que tiene el contrato del Web Service. Para este caso no necesitaremos indicar la URL exacta de cada uno de los métodos y las invocaciones y tratamientos de la respuesta, la realizaremos de una forma más natural y comprensible. 

public class BookClient {
    final static String path = "http://localhost:8080/RestEasyService/library/";
    private static BookService proxy;
    static {
	WebTarget target = ClientBuilder.newClient().target(path);
	proxy = ((ResteasyWebTarget) target).proxy(BookService.class);
    }
    public Book read_proxy(final Integer id) {
	return proxy.read(id);
    }
}

Con esto ya sabemos crear un Web Service y su cliente pero vamos a ver un último detalle importante como es la configuración a través de la clase MicroProfile Config.

Esta clase nos permitirá cumplir con la metodología de 12factor, permitiéndonos configurar el cliente o servicio a través de variables de entorno, propiedades del sistema o un fichero de configuración específico. A continuación veremos como podemos modificar la creación del cliente anterior para que obtenga la dirección del servicio a través de una variable de entorno. 

static {
    Config config = ConfigProvider.getConfig();
    String path = config.getValue("BOOKWS_URL", String.class);
    WebTarget target = ClientBuilder.newClient().target(path);
    proxy = ((ResteasyWebTarget) target).proxy(BookService.class);
}

Por prioridad primero intentará obtener el valor de una propiedad del sistema, después de una variable del entorno y por último lo buscará en el fichero del classpath META-INF/microprofile-config.properties. Aunque estas valores de prioridad y orígenes de información también son configurables. Esto nos permitirá mantener el código de la aplicación inmutable en cada entorno en el cual despleguemos nuestra aplicación.

Y hasta aquí las nociones básicas para crear Web Services y clientes con el framework de JBoss. Si quieres puedes ver el código fuente aquí. 

domingo, 2 de mayo de 2021

Como emular un servicio SOAP con Wiremock

Ya hemos visto varios ejemplos de como usar wiremock. Y siempre lo hemos usado para simular llamadas REST, hoy veremos como también podemos simular llamadas SOAP. 

Como vamos a ver, si tenemos un poco de conocimiento sobre Wiremock, será una tarea sencilla. Por lo que veremos nos podremos dar más detalles, empezando por la creación del WebService, su cliente y despues el test JUnit en el que usaremos Wiremock.

Para empezar si queremos hacer un Servicio Web podemos utilizar la librería de JAX-WS, en cuatro sencillos pasos:
  • Añadir la anotación @WebService a la clase que contenga la lógica.
  • Añadir la anotación @WebMethod a cada uno de los métodos que queramos exponer. 
  • Crear un fichero sun-jaxws.xml en la carpeta WEB-INF donde se indique la clase a exponer.
  • Modificar el fichero web.xml para añadir el servlet de WSServlet y el listener WSServletContextListener.
Si quieres más información puedes ver un post sobre el tema aquí. 

El siguiente paso será crear un cliente. Esto lo podemos hacer más facilmente todavia, a través de la herramienta wsimport y teniendo acceso al fichero WSDL que tendremos obtendremos al desplegar el WebService. El código del cliente lo podemos obtener con el siguiente comando:

wsimport -keep .\BookServiceImplService.wsdl

Si quieres más información puedes ver un post sobre el tema aquí. 

Una vez que tenemos nuestro cliente, es el momento de ponernos manos a la obra y crear nuestro test JUnit con Wiremock. El primer paso incluir la dependencia:

<dependency>
	<groupId>com.github.tomakehurst</groupId>
	<artifactId>wiremock</artifactId>
	<version>2.25.1</version>
	<scope>test</scope>
</dependency>

Luego configuramos el WiremockServer en la clase de pruebas. A través de este WiremockServer, podremos configurar en que puerto estará escuchando el Wiremock, cual será la carpeta base a usar o mostrar información a través de la consola de salida, aparte de muchas más cosas. Ejemplo:

private final static String FOLDER = "src/test/resources/wiremock";
private static WireMockServer wireMockServer;

@BeforeAll
public static void setup() {
	wireMockServer = new WireMockServer(WireMockConfiguration
		.wireMockConfig().withRootDirectory(FOLDER).port(8080)
		.notifier(new ConsoleNotifier(true)));
	wireMockServer.start();
}

A lo mejor la parte más dificil a la hora de crear el test, es identificar cual debe ser la respuesta a obtener por el Wiremock. Para ello igual podemos ayudarnos de la herramienta SOAPUI, la cual en base a un WSDL nos permite obtener request y response de ejemplo. 

Ahora toca hacer el test. Resumiendo mucho, la diferencia principal entre un servicio REST y otro SOAP, es que el primero se envía y recibe JSON y en el segundo XML. Por tanto para nuestras pruebas deberemos indicar la URL como en un servicio REST, pero indicando que va a hacer una petición y recibir XML. Esto lo podemos hacer a través de los siguientes pasos:
  • Recibir una llamada de tipo POST
  • Indicar que en la cabecera Content-Type vamos a recibir 'text/xml'.
  • Indicar que en la cabecera SOAPAction estará incluido el método a llamar. 
  • La respuesta será un fichero XML con el formato correcto. Que hemos podido obtener de SOAPUI. 
@Test
public void getAll() {
    wireMockServer.stubFor(post(urlPathEqualTo("/soapWSExample/bookService"))
	.withHeader("Content-Type", WireMock.containing("text/xml"))
	.withHeader("SoapAction", WireMock.containing("getAllRequest"))
	.willReturn(aResponse().withBodyFile("getAll.xml")));

    BookServiceImplService service = new BookServiceImplService();
    List<Book> list = service.getBookServiceImplPort().getAll();
    assertThat(list.size(), equalTo(3));
}

Como veis, un post corto y sencillo. Sobre todo si tenéis conocimientos previos de Wiremock. 

viernes, 20 de noviembre de 2020

Apache Camel: Uso de Cache con Caffeine

Hoy vamos a ver como mejorar el rendimiento de nuestros web services en Apache Camel. Para ello usaremos Caffeine Cache, una sencilla pero potente cache en memoria realizada en Java y basada en la cache de Guava.

La idea detras del uso del cache, será poder almacenar el resultado de operaciones costosas en memoría. De esta forma, en sucesivas llamadas podremos acceder a dicho resultado de forma rápida y agil evitando la operación más costosa. 

Empecemos como siempre indicando cuales son las librerías a incluir. Por un lado la propia dependencía para usar Caffeine y por otro el starter de Spring que nos provee de una configuración por defecto. 

<dependency>
  <groupId>org.apache.camel.springboot</groupId>
  <artifactId>camel-caffeine-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-caffeine</artifactId>
</dependency>

¿Como funciona? Debemos pensar en la cache como su fuera un ConcurrentMap. Es decir, iremos colocando objetos asociados a una clave determinada y a través de esa clave podremos recuperarlos. Por tanto para poder usarlo, usaremos acciones del tipo GET o PUT asociados a una clave. El cuerpo del mensaje será el objeto almacenado en la cache. 

Cuando realicemos alguna operación Caffeine nos indicará si dicha operación ha resultado exitosa y si hay algún resultado asociado a la misma. Esta información nos la dará a través de valores almacenados en la cabecera: CamelCaffeineActionSucceeded y CamelCaffeineActionHasResult. Caffeine también tiene una interface, denominada CaffeineConstants, con todas las constantes asociadas a sus operaciones o resultados que nos puede ayudar en el desarrollo. 

A continuación veremos el ejemplo para obtener un libro en base a su identificador. El identificador nos servirá como clave para almacenar el libro asociado en la cache. Primero comprobaremos si se encuentra en la cache (acción GET), en dicho caso devolveremos el valor asociado. En caso contrario realizaremos la operación de consulta a base de datos y almacenaremos el resultado en la cache (acción PUT) para futuras consultas. 

rest().get("cache/book/{id}")
.description("Details of an book by id").outType(Book.class)
.produces(MediaType.APPLICATION_JSON_VALUE).route()
.log("Cache : Select Book By Id: ${header.id}")
.setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_GET))
.setHeader(CaffeineConstants.KEY, header("id"))
.toF("caffeine-cache://%s", "BookCache")
.log("Has Result ${header.CamelCaffeineActionHasResult} ActionSucceeded ${header.CamelCaffeineActionSucceeded}")
.choice().when(header(CaffeineConstants.ACTION_HAS_RESULT).isEqualTo(Boolean.FALSE))
   .to("sql:{{sql.selectById}}")
   .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT))
   .setHeader(CaffeineConstants.KEY, header("id"))
   .toF("caffeine-cache://%s", "BookCache")
.otherwise()
   .log("Cache is working");

Si hacemos una prueba, comprobaremos que los tiempos y el rendimiento con Caffeine son mejores. Si lo comparamos usando hey, aún siendo una operación muy básica vemos como los tiempos se reducen mucho:


Pero tenemos que tener en cuenta que pasaría si el resultado de esa operación costosa cambiase. En dicho caso la cache seguría funcionando y devolviendo un valor erróneo. Para evitar eso tenemos, tenemos dos opciones. Primera opción a través de la configuración, con la cual podremos modificar cuántos resultados y cuanto tiempo se mantendrán en memoría. 

O una segunda opción, que será directamente invalidar manualmente la cache de un determinado objeto. Por ejemplo, en el caso de que borremos un objeto, no queremos que esa cache siga existiendo y devuelva resultados erróneos. Esto podremos hacerlo a través de la acción INVALIDATE. 

rest().delete("cache/book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
.route().to("sql:{{sql.delete}}")
.setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_INVALIDATE))
.setHeader(CaffeineConstants.KEY, header("id"))
.toF("caffeine-cache://%s", "BookCache")
.setHeader(Exchange.CONTENT_TYPE, constant(MediaType.APPLICATION_JSON_VALUE))
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200)).setBody(constant(null));

Por último vamos a ver la opción de como debería comportarse una aplicación cuando se actualiza el objeto cacheado. La solución fácil sería invalidar la cache asociada a la clave y volver a cachearlo cuando sea invocado de nuevo. Pero esta opción nos hará realizar una operación costosa más, de las necesarias. Si ya tenemos el objeto podríamos simplemente almacenarlo de nuevo en la cache y evitar esa futura operación costosa. Ejemplo con el método PUT. 

rest().put("cache/book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
.type(Book.class).route()
.choice().when().simple("${header.id} < 1")
   .bean(BookSQLRouter.class, "negativeId")
.otherwise()
   .log("PUT : Body: ${body}")//${body[id]
   .to("sql:UPDATE BOOK SET NAME = :#${body.name}, AUTHOR = :#${body.author} WHERE ID = :#id")
   .process(exchange -> {
       final Book book = exchange.getIn().getBody(Book.class);
       book.setId(Integer.valueOf(exchange.getIn().getHeader("id").toString()));
    })
   .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT))
   .setHeader(CaffeineConstants.KEY, header("id"))
   .toF("caffeine-cache://%s", "BookCache")
   .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
   .setBody(constant(null));

Como siempre podeis ver el código completo aquí.