Mostrando entradas con la etiqueta API. Mostrar todas las entradas
Mostrando entradas con la etiqueta API. Mostrar todas las entradas

sábado, 22 de octubre de 2022

Apache Camel: ActiveMQ

Hace poco vimos como hacer una integración con WSO2 y ActiveMQ, que puedes ver aquí. Esta vez. veremos el mismo ejemplo pero con Apache Camel y Spring Boot. Donde además añadiremos la complejidad de utilizar colas que requieren autenticación. Para el ejemplo utilizaremos las siguientes versiones:

  • Apache Camel 3.11.0
  • Spring Boot 2.5.1
  • Active MQ 5.16.2 

1. Docker Compose y configuración

activemq:
  image: rmohr/activemq
  mem_limit: 1G
  hostname: sandbox-activemq
  container_name: sandbox-activemq
  ports:
    - 8161:8161
    - 61616:61616
  volumes:
    - ./activemq.xml:/opt/apache-activemq-5.15.6/conf/activemq.xml

Sobre esta configuración podemos indicar lo siguiente:

  • El puerto 8161 es el de administración y el que nos permitirá acceder a la consola de gestión a través de la URL http://localhost:8161/admin/index.jsp. 
  • El puerto 61616 es el puerto TCP utilizado para la comunicación con ActiveMQ
  • El fichero activemq.xml, el cual se puede encontrar dentro de la misma imagen, es el fichero que nos permitirá configurar el comportamiento de ActiveMQ. Entre otras cosas, la gestión de los usuarios que tengan acceso a las colas o topics. 
Si queremos autenticación para hacer uso de las colas deberemos incluir el siguiente trozo de código en el fichero activemq.xml:

<plugins>
    <simpleAuthenticationPlugin anonymousAccessAllowed="true">
        <users>
            <authenticationUser username="privateUser" password="P#s5W0rd" groups="admins" />
            <authenticationUser username="system" password="manager" groups="admins,publishers,consumers"/>
        </users>
    </simpleAuthenticationPlugin>
    <authorizationPlugin>
        <map>
            <authorizationMap>
                <authorizationEntries>
                    <authorizationEntry queue="privateQueue" read="admins" write="admins" admin="admins" /> 
                    <authorizationEntry queue=">" write="anonymous,admins" read="anonymous,admins" admin="anonymous,admins" />
                    <authorizationEntry topic="ActiveMQ.Advisory.>" write="*" read="*" admin="*" />
                </authorizationEntries>
            </authorizationMap>
        </map>
     </authorizationPlugin>
</plugins>

Con este código estamos configurando lo siguiente:

  • simpleAuthenticationPlugin: Nos permite configurar manualmente usuarios, contraseñas y los grupos a los que están asociados. La otra opción para securizar el acceso, más profesional,  sería a través del JAAS plugin. 
  • anonymousAccessAllowed: Nos permite indicar que también permitiremos el acceso anónimo. De esta forma podemos tener algunas colas securizadas y otras no. El usuario y grupo asociado por defecto es anonymous. 
  • Hay que añadir al usuario system para evitar errores al realizar operaciones desde la consola. 
  • authorizationEntries: Es la configuración que queremos aplicar a las colas de forma genérica o específica. En esta configuración permitimos:
    • Crear, escribir o leer en la cola 'privateQueue' solo a los usuarios administradores. 
    • Crear, escribir o leer de cualquier cola. El símbolo '>' indica 'todos'. Esta configuración esta asociada a la inferior, que permite la creación de los canales de asesoramiento a cualquier usuario, 'ActiveMQ.Advisory.>'. 
Para temrinar la configuración, respecto a Apache Camel/Spring Boot, como usamos la dependencia camel-activemq-starter, no necesitaremos configurar nada especial. Simplemente cual es la ruta de comunicación del broker:
camel.component.activemq.brokerUrl=tcp://localhost:61616

2. Envío de mensajes

Ahora que lo tenemos correctamente configurado podemos proceder a crear el recurso que nos permita realizar el envío de mensajes. La idea es crear un servicio REST que almacene la información que recibe en la cola de ActiveMQ. 

rest().post("activemq/public").produces(MediaType.APPLICATION_JSON_VALUE)
  .consumes(MediaType.APPLICATION_JSON_VALUE).route()
  .routeId("postPublicQueue").log("Message incoming - ${body}")
  .to("activemq:queue:publicQueue?exchangePattern=InOnly")
  .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(202));

Como vemos en la configuración, necesitamos indicar al Apache que no se quede esperando por respuesta, ya que no recibirá ninguna del ActiveMQ. Esto lo podemos hacer a través de la configuración 'exchangePattern=InOnly'. De otra forma, en cada llamada, recibiríamos un mensaje de error similar al siguiente:

org.apache.camel.ExchangeTimedOutException: The OUT message was not received
within: 20000 millis due reply message with correlationID

Ahora sí hiciéramos la siguiente invocación podríamos mandar un mensaje a la cola pública. 

curl --location --request POST 'http://localhost:8080/camel/activemq/public' \
--header 'Content-Type: application/json' \
--data-raw '{"prop":"value"}'

Y para la cola privada será un poco más complicado. Primero creamos una conexión de tipo ActiveMQConnectionFactory que permita configurar un usuario y contraseña. Por defecto usa SingleConnectionFactory y este no admite la autenticación. 

@Bean
private ConnectionFactory activeMQConnectionFactory() {
  return new ActiveMQConnectionFactory();
}

Y segundo, será indicar en la cadena de conexión el usuario y contraseña. Si lo quisiésemos de forma generalizada podríamos configurarlo a nivel del fichero application.properties. Pero en este caso solo se lo queremos aplicar a un route. 

rest().post("activemq/private").produces(MediaType.APPLICATION_JSON_VALUE)
    .consumes(MediaType.APPLICATION_JSON_VALUE).route().routeId("postPrivateQueue")
    .log("Private message incoming - ${body}")
    .to("activemq:queue:privateQueue?connectionFactory=activeMQConnectionFactory&exchangePattern=InOnly&username=privateUser&password={{activemq.privateQueue.password}}")
    .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(202));

3. Lectura de mensajes en la cola

Ahora veremos como hacer un recurso de la API que nos permita leer de la cola. Esta es la parte más sencilla, pues simplemente será cambiar el método to que nos permite producir, por el método from que nos permite consumir. La configuración será la misma. 

// Consumers
from("activemq:queue:publicQueue").log("Reading public message incoming - ${body}").end();

from(
    "activemq:queue:privateQueue?connectionFactory=activeMQConnectionFactory&exchangePattern=InOnly&username=privateUser&password={{activemq.privateQueue.password}}")
        .log("Reading private message incoming - ${body}").end();

Y esto ha sido todo, espero que haya sido util. Yy como siempre puedes ver todo el código aquí. 

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í. 

sábado, 10 de abril de 2021

Java Agents e Instrumentación: Tutorial

Últimamente hemos estado viendo en los posts como realizar tareas de monitorización. Hoy veremos los Java Agents, que son y como funcionan. Los cuales también pueden ayudarnos a realizar tareas de monitorización, entre otras cosas. Algo que intentaremos explicar en este post, primero con un poco de teoría y luego con la ayuda de ejemplos. 

¿Que son los Java Agents?, son básicamente librerías Java en las cuales se incluyen clases que implementan la API de Instrumentación de Java. La cual esta disponible desde la JDK 1.5. Esta es una API muy sencilla pero a la vez muy potente. Y establece las bases de la instrumentación de aplicaciones. 

La funcionalidad principal de esta instrumentación, es que se nos permitirá la adición de código a determinados métodos. Y con esta adición, podremos recompilar datos para ser utilizados con fines principalmente de monitorización y análisis o registro de eventos. Permitiéndonos obtener información vital sobre la aplicación que nos puede ayudar a resolver problemas u obtener datos que no serían posibles de otra forma. 

La principal cualidad de esta API de instrumentación es la alteración del byte-code de las clases que están siendo ejecutadas por la maquina virtual. A través de esta alteración podremos realizar las acciones anteriormente indicadas. 

Pero centrémonos, ¿Que necesitamos para crear un Java Agent?, seguir estos tres pasos:

  1. Desarrollar un método concreto.
  2. Crear y configurar correctamente el fichero MANIFEST.MF
  3. Indicar a la máquina virtual que tenga en cuenta a nuestra librería con el Java Agent. 

El método a desarrollar depende de cuando queramos que sea asociado el agente a la ejecución de aplicaciones en la JVM. La API de instrumentación pondrá a disposición de nosotros dos métodos diferentes:

  • premain: Permitirá la ejecución del Java Agent y su método asociado antes de la ejecución de cualquier otra aplicación en la JVM. Para lo cual el Java Agent debe indicarse en el arranque de la JVM. 
  • agentmain: Permitirá la ejecución del Java Agent una vez que la JVM ya se encuentre ejecutando una aplicación. Esta asociación debe realizarse de forma programática. 
La creación del fichero MANIFEST.MF debe realizarse manualmente, aunque se puede incluir en el jar asociado a través de un plugin de maven. En este fichero deberemos configurar distintas propiedades. Por un lado cual es la clase que contiene al método del Java Agent y propiedades acerca del comportamiento del mismo. Las principales son:
  • Premain-Class: Para indicar la clase que implemente el método premain.
  • Agent-Class: Para indicar la clase que implemente el método agentmain.
  • Can-Redefine-Classes: Permite indicar si el Java Agent puede o no redefinir las clases que interfiera.
  • Can-Retransform-Classes: Permite indicar si el Java Agent puede o no transformar la clase que interfiera. 
Una vez que tenemos desarrollado el método que realice la lógica y hayamos configurado el fichero MANIFEST correctamente, debemos generar la librería. Para ello podemos hacer uso de un par de plugins, los cuales nos facilitarán el trabajo:
  • maven-shade-plugin: nos permite crear la librería incluyendo las dependencias en el caso de que las tenga. 
  • maven-jar-plugin: nos permite asociar el fichero MANIFEST a la librería a crear. 
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
            <archive>
                <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
            </archive>
        </configuration>
    </plugin>
</plugins>

El tercer paso, una vez tengamos la librería, será asociarla a la JVM. Tal y como hemos indicado anteriormente, se puede realizar de dos formas:

Si hemos implementado el método premain y configurado la propiedad Premain-Class, deberemos indicar la librería en el momento de arrancar la aplicación que queramos interferir. Ejemplo:

java -javaagent:/full/path/to/javaAgent.jar -jar app.jar

En el caso de que implementemos el método agentmain y configurado la propiedad Agent-Class, deberemos asociar el Java Agent de forma programática a través de la Java Attach API.

File agentFile = Paths.get("agent.jar").toFile();
VirtualMachine jvm = VirtualMachine.attach(VirtualMachine.list().get(0).id());
jvm.loadAgent(agentFile.getAbsolutePath());
// jvm.detach(); //to finish association

Ahora ya sabemos la teoría por lo que podemos ir a la practica. Vamos a ver un par de ejemplos. Uno sencillo y básico y otro algo más complejo. 

En el ejemplo más sencillo simplemente implementaremos el método premain, y veremos un par de logs asociados a su ejecución. 

@Log4j2
public class BasicAgentExample {
	public static void premain(final String agentArgs, final Instrumentation inst) {
		log.info("Start from premain. agentArgs: " + agentArgs);
		inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
			log.info(String.format("Classname: %s", className));
			return null;
		});
	}
}

Si ejecutamos un método static void main, por ejemplo, y añadimos como argumento el java agent, podremos ver una salida similar a esta:

[INFO ] 2021-03-04 [main] BasicAgentExample - Start from premain. agentArgs: null
[INFO ] 2021-03-04 [main] BasicAgentExample - Classname: sun/misc/PostVMInitHook
....

Como podemos ver, si miramos un poco los métodos disponibles en el objeto Instrumentation, tendremos la posibilidad de transformar o redefinir el funcionamiento de una clase. Pero este debe realizarse con operaciones complejas de modificación de los bytes de las clases. 

En el siguiente ejemplo, algo más complejo, transformaremos la clase que vayamos a interferir. Pero como hemos indicado estas operaciones son complejas. Y para llevarlas a cabo más fácilmente haremos uso de la librería Byte Buddy.  

La idea de este ejemplo, es crear un Java Agent que nos permita medir cuantas veces se ha invocado un método determinado. Aparte, también filtraremos que clases queremos intervenir, evitando que se realicen tareas sobre clases que no nos interesan, como por ejemplo las de la JVM. En nuestro ejemplo intervendremos solo aquellas clases que extiendan de una clase abstracta. 

Primero implementamos el método premain

@Log4j2
public class CounterAgentExample {
  public static void premain(final String agentArgs, final Instrumentation inst) {
    log.info("Start from premain. agentArgs: " + agentArgs);
    Advice advice = Advice.to(CounterMeasureAdvice.class);
    new AgentBuilder.Default()
      .type(ElementMatchers.hasSuperType(ElementMatchers.isAbstract()))
      .transform((builder, type, classLoader, module) -> builder.visit(advice.on(ElementMatchers.isMethod())))
      .installOn(inst);
  }
}

El constructor AgentBuilder nos permitirá indicar que elementos queremos intervenir a través del método type y de la utilidad ElementMatchers. El método transform nos permitirá indica sobre que elemento de esa clase queremos realizar la transformación, en este caso sobre los métodos de las clases que cumplan hayan pasado el filtro. 

public class CounterMeasureAdvice {
  public final static Logger log = LogManager.getRootLogger();
  public static Map<String, Long> map = new HashMap<>();

  @Advice.OnMethodEnter
  public static void enter(@Advice.Origin final String origin) {
	log.info("CounterMeasureAdvice: Enter into: " + origin);
	if (map.containsKey(origin)) {
		map.put(origin, map.get(origin) + 1);
	} else {
		map.put(origin, 1L);
	}
	log.info(String.format("Origin %s invoked %d times", origin, map.get(origin)));
  }
}

Crearemos tambien un advice, que es un elemento asociado a la programación de aspecto, que nos permite ejecutar nuestro código en un punto específico. Los advice que creemos pueden ser de distinto tipo, en función de cuando queremos que se tengan en cuenta respecto a ese punto fijo: antes, o después de ese determinado punto. Para el ejemplo, ejecutaremos nuestro advice antes de acceder a los métodos que cumplan los filtros. 

public class ExampleThree extends AbstractClass {
  public static int fibonacci(final int n) {
	if (n > 1) {
		return fibonacci(n - 1) + fibonacci(n - 2); // función recursiva
	} else { // caso base
		return n;
	}
  }
  // java -javaagent:/path/to/agent.jar -cp *.jar the.main.ClassName
  public static void main(final String[] args) {
	System.out.println("Fibonacci de 5: " + fibonacci(5));
  }
}

Para probar nuestro Java Agent, ejecutaremos la clase que se muestra arriba. Indicando correctamente la librería del Java Agent y la clase que implementa dicho agente en el fichero MANIFEST. La salida será similar a esta:

[INFO ] 2021-03-04 [main]  - CounterMeasureAdvice: Enter into: public static int ExampleThree.fibonacci(int)
[INFO ] 2021-03-04 [main]  - Origin public static int ExampleThree.fibonacci(int) invoked 14 times
[INFO ] 2021-03-04 [main]  - CounterMeasureAdvice: Enter into: public static int ExampleThree.fibonacci(int)
[INFO ] 2021-03-04 [main]  - Origin public static int ExampleThree.fibonacci(int) invoked 15 times

Como podéis ver, el uso de Java Agent tiene muchas posibilidades. Incluso con la librería de Byte Buddy se pueden hacer cosas complejas de forma sencilla.



sábado, 17 de octubre de 2020

Gravietee: Introducción a creación de APIs

 Hoy vamos a ver Gravitee, una de las opciones open source que existen en el mercado para la gestión de APIs. En este post haremos un acercamiento básico sobre como montar o gestionar una API. 

Como backend utilizaremos ejemplos antiguos. Y en principio no nos centraremos en como debemos instalarlo. Aquí hay un docker-compose que será en el que nos basaremos que nos permitirá realizar el ejemplo. 

Lo primero es hablar un poco de Gravitee y sus componentes:

  • Management API: API de gestión. Nos permitirá realizar la administración de forma remota. Enlace a su documentación. 
  • Management UI: Interfaz gráfica para la administración. La cual usaremos mayormente en el ejemplo.
  • API Gateway: El Gateway encargado de realizar todo el trabajo: Redireccionar, comprobar los planes, etc. 
Ahora ya podemos comenzar a crear nuestra API. Para ello debemos acceder a la URL de la consola de administración a través del siguiente enlace. 


Como veis al entrar la primera vez no tendremos creado ninguna API, como es normal. Ahora procederemos a crearla. Cuando intentamos acceder por primera vez, podremos crear una API de tres formas:

  • Desde cero: Iremos paso a paso indicando como queremos que sea nuestra API
  • A través de un fichero Swagger que defina exteriormente como es nuestra API.
  • A través de una definición de API propia de Gravitee, que contendrá un mayor nivel de configuración sobre la misma. 
Nosotros haremos la API siempre paso a paso. Al darle a crear una API, entraremos en un tutorial que nos irá indicando como queremos configurar la API. Este tutorial se dividirá en las siguientes secciones:
  • General: Nos permitirá indicar el nombre, descripción, versión y context-path de nuestra API.
  • Gateway: Nos permitirá indicar el backend. En este caso: http://sandbox-camel:9090/book.
  • Plan: Este es un apartado importante. El cual nos permitirá indicar el tipo de acceso a la API, limitar el acceso a determinados recursos o especificar el tipo de subscripción que tendrá. En nuestro ejemplo utilizaremos un plan con seguridad Keyless y con el apartado Path Authorization el cual solo permita la ruta / y los métodos GET y DELETE. 
  • Doc: Nos permitirá asociar documentación a nuestra API.
  • Deployment: Indicará un resumen de la configuración actual y además nos permitirá crear la API y desplegarla con un simple click. 
Una vez creada nos redirigirá a una nueva sección con los detalles de la API, desde la cual podremos acceder a sus planes o subscripciones. Al ser una API Keyless no requerimos de subscripción y por tanto vamos a utilizarla ya directamente. Para que sea visible a todo el mundo y podamos verla debemos pulsar el enlace MAKE PUBLIC, en la nueva sección. 


Ya tendremos nuestra API desplegada y operativa en Gravitee. Y al ser Keyless, podemos probarla a través de una llamada GET a http://localhost:8082/book/ sin ningún otro parámetro. Pero recordemos que además le hemos indicado que solo podemos acceder a la ruta raíz. Por tanto si intentamos realizar la siguiente llamada,   http://localhost:8082/book/1, obtendremos este mensaje:

{
"message": "You're not allowed to access this resource",
"http_status_code": 403
}

Ahora vamos a ver un poco más el funcionamiento de los planes. Desde la sección actual iremos a la opción de menú Plans y lo editaremos. Realizando los siguientes cambios:
  • Restrictions: Añadir un límite de 5 llamadas al minuto. 
  • Restrictions: Añadir una cuota de 20 llamadas al día. 
  • Path Authorization - whitelist: Añadir el path pattern '/{book}' asociado a los métodos GET, PUT y POST. 
Ahora podremos llamar a todos los recursos de nuestro backend pero como mucho 5 veces al minuto y 20 veces al día. Por tanto si hacemos la llamada a http://localhost:8082/book/1 6 veces, obtendremos el siguiente mensaje:

{
"message": "Rate limit exceeded ! You reach the limit of 5 requests per 1 minutes",
"http_status_code": 429
}

Y si la realizamos más de 20 veces al día, este otro:

{
"message": "Quota exceeded ! You reach the limit of 20 requests per 1 days",
"http_status_code": 429
}

Como veís es facil crear políticas de acceso sobre nuestra API. Y además también habreís comprobado que despues de cualquier cambio sobre la API o el plan asociado, Gravitee te pedirá que la vuelvas a desplegar:


Ahora veremos como hacer una API que requiera una validación a través de una clave y por tanto este subscrita a una aplicación, la cual proveerá de la misma. Podemos elegir entre 3 tipos de claves: API Key, Oauth2 o JWT. Para nuestro ejemplo utilizaremos el más sencillo, API Key. 

Para aplicarlo debemos volver a la sección de planes de nuestra API y crear uno nuevo puesto que no podemos modificar la seguridad del plan anterior. Tras crearlo tendremos que publicarlo y volver a desplegar la API. 

Ahora procederemos a ir a la sección subscription y crear una subscripción entre el nuevo plan y la aplicación por defecto ya creada. Al crear la subscripción, veremos los detalles de la misma y la API Key. 



Si volvemos a probar la llamada que nos permite obtener un libro obtendremos la siguiente respuesta:

{
"message": "Unauthorized",
"http_status_code": 401
}

Para una respuesta correcta, deberemos añadir la cabecera X-Gravitee-Api-Key con el valor de nuestra clave. 

curl --location --request GET 'http://localhost:8082/book/1' \
--header 'X-Gravitee-Api-Key: 5bbdac7d-dc88-4102-9e35-d29a6476cefd'
 
Con esto habremos visto los detalles más importantes en la creación de APIs. Pero en el apartado de la izquierda podremos acceder a otras opciones de menú que nos aportarán mayor configuración:
  • Proxy: Permite modificar o añadir más, incluir cabeceras CORS, health check para verificar si el backend esta operativo o incluir una politica de failover que nos permita indicar el número máximo de llamadas a un backend antes de pasar al siguiente. 
  • Design: Nos permite realizar politicies antes o despues de las llamadas. Estas policies tendrán unas acciones asociadas. Y recursos y propiedades asociadas a estas policies. 
  • Analytics: Nos provee una visual sobre el uso de nuestra API. 
  • Audit: Nos indica todas las acciones de configuración que hemos realizado sobre nuestra API. 
  • Notifications: Nos permite añadir alertas en base a distintos eventos. 
  • Messages: Nos permite indicar como queremos recibir los mensajes. 
Espero que os haya servido como mini tutorial y acercamiento a este excelente software de gestión de APIs. También es importante indicar que aunque no esta incluido en el docker-compose, Gravitee cuenta con un portal del usuario donde se pueden visutalizar todas aquellas APIs que son públicas. 

lunes, 20 de julio de 2020

Apache Camel: REST CRUD y el componente SQL

Ya hicimos un post sobre Apache Camel con un mock como backend, que puedes ver aquí. Hoy veremos como cambiar ese mock por una BBDD real. 
Para empezar modificaremos las dependencias que necesitamos, tres cosas:
  • spring-boot-starter-jdbc: La librería que nos facilitará el uso de la bbdd y la creación del datasource.

  • mysql-connector-java: El driver de la BBDD.

  • camel-sql: El componente de Apache Camel que nos permitirá hacer consultas SQL. 

El siguiente paso será configurar el datasource que nos permita las conexiones con la base de datos. Esto lo podemos hacer en el fichero application.properties de Spring. 

spring.datasource.url=jdbc:mysql://localhost:3306/library?createDatabaseIfNotExist=true&autoReconnect=true&useSSL=false
spring.datasource.username=reader
spring.datasource.password=password
spring.datasource.platform=mysql
spring.datasource.initialization-mode=always

#CamelSqlGeneratedKeyRows
sql.insert=INSERT INTO BOOK (NAME, AUTHOR) VALUES (:#name, :#author)
sql.selectById=SELECT ID, NAME, AUTHOR FROM BOOK WHERE ID = :#id?outputType=SelectOne&outputClass=com.example.home.ApacheCamelRestExample.pojo.Book
sql.delete=DELETE FROM BOOK WHERE ID = :#id
sql.update=UPDATE BOOK SET NAME = :#name, AUTHOR = :#author WHERE ID = :#id

Como podeis ver tambien se han añadido las consultas que se usarán más adelante en el código.  

Vamos a empezar por un método sencillo, cambiando el mock que teníamos por una consulta SQL. Para ello utilizaremos el componente SQL de apache-camel. 

rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).route()
    .to("sql:SELECT ID, NAME, AUTHOR FROM BOOK").log("--- select all books ---");

Con el componente podremos realizar consultas directamente a BBDD haciendo uso del datasource configurado. Estas consultas podemos escribirlas directamente, o bien escribiendolas en el método o bien invocándolas desde el fichero properties con ayuda de las llaves.

rest().get("book/{id}").description("Details of an book by id").outType(Book.class)
    .produces(MediaType.APPLICATION_JSON_VALUE).route()
    .to("sql:{{sql.selectById}}").log("--- select a book ${body} ---");

En el caso anterior indicamos la salida con el método outType, lo que permitirá formatear la salida fácilmente. Para el caso de un método de tipo Insert o Delete en el cual no queremos devolver un payload podremos indicarlo con el método setBody y el código de la respuesta con setHeader.

rest().post("book").produces(MediaType.APPLICATION_JSON_VALUE).route()
    .routeId("postBookRoute").to("sql:{{sql.insert}}")
    .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(201)).setBody(constant(null));

En general es un ejemplo muy similar al que usábamos un mock por backend. Pero como vamos a usar JDBC y no JPA, las propiedades como pueden ser name o author, solo se se podrán obtener directamente desde la cabecera o el cuerpo del mensaje. Y por tanto deberemos quitar el método type, que convierte el payload de entrada en un objeto. 

rest().put("book/{id}").produces(MediaType.APPLICATION_JSON_VALUE).route()
    .choice().when()
        .simple("${header.id} < 1").bean(BookRouter.class, "negativeId")
    .otherwise()
        .to("sql:{{sql.update}}")
        .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
        .setBody(constant(null));

Un último truco, si queremos utilizar un pool de conexiones solo tendremos que indicarlo en el application.properties a través de la propiedad spring.datasource.type. Veremos el ejemplo con HikariCP por ejemplo:

spring.datasource.type=com.zaxxer.hikari.HikariDataSource