Stub con Jetty embebido en Test con JUnit 5


En esta entrada mostraré un pequeño ejemplo para utilizar al servidor Jetty embebido durante las pruebas (con JUnit 5) para cuando necesitemos un Stub de algún servicio fuera de nuestro alcance.

1. Las dependencias:

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-webapp</artifactId>
        <version>9.4.28.v20200408</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.6.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.6.0</version>
        <scope>test</scope>
    </dependency>
2. La clase para definir los handlers, lanzar y detener el servidor Jetty en las pruebas:
import java.net.URI;
import java.nio.file.Path;
import java.util.concurrent.ThreadLocalRandom;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.PathResource;

public class JettyStub {
  private Server server;

  public URI uri() throws Exception {
    crearServer(ThreadLocalRandom.current().nextInt(10000, 20000));
    server.setStopAtShutdown(true);
    server.start();
    return server.getURI();
  }

  public void stop() throws Exception {
    server.stop();
  }

  private void crearServer(int port) throws Exception {   
    server = new Server(port);
    
    // definir recursos desde el sistema de archivos
    final var rh = new ResourceHandler();
    rh.setDirectoriesListed(true);
    rh.setBaseResource(new PathResource(
        Path.of(System.getProperty("user.dir")).resolve("target/test-classes")));
    
    // definir servlets
    final var sh = new ServletHandler();        
    sh.addServletWithMapping(new ServletHolder(new HelloServlet()), "/hello/*");
    
    // agregar los handlers
    server.setHandler(new HandlerList(rh, sh));
  }
}
en esta clase agregué un handler para contenido estático desde el sistema de archivos y un servlet que responde en el path /hello/*:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {
  public void doGet(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
    res.getOutputStream().print("""
        {"hola": "chao"}""");
  }
}
el servlet responderá un JSON y en el contenido estático también existe un archivo llamado demo.json (carpeta resources de test) con el siguiente contenido:
{"a":"b","c":"d"}
3. Las pruebas son las siguientes:
public class StubJettyTest {
  private static final JettyStub JS = new JettyStub();
  /** uri en la que responde el server. */
  private static URI uri;

  @BeforeAll
  public static void beforeAll() throws Exception {
    uri = JS.uri();
  }

  @AfterAll
  public static void afterAll() throws Exception {
    JS.stop();
  }

  @Test
  public void obtengoElContenidoEsperadoDesdeServlet() throws MalformedURLException {
    WebClient client = new WebClient();     
    var json = client.getContent(uri.resolve("/hello").toURL());
    assertEquals("""
        {"hola": "chao"}""", json);
    json = client.getContent(uri.resolve("/hello?a=b").toURL());
    assertEquals("""
        {"hola": "chao"}""", json);
  }
  
  @Test
  public void obtengoElContenidoEsperadoDesdeArchivo() throws MalformedURLException {
    WebClient client = new WebClient();  
    String json = client.getContent(uri.resolve("demo.json").toURL());
    assertEquals("""
            {"a":"b","c":"d"}""", json);
  }
}
y eso es todo, de esta simple manera podemos tener los stubs de los servicios que necesitamos durante nuestras pruebas 😌.

En Github está el ejemplo junto a otros.

Codersrank - TOP DEVELOPERS IN JAVA [UPDATE]

Ahora estoy en el puesto número 1 en codersrank, así que dejaré las capturas para el recuerdo 👻:

  • Chile + Java = 1





  • Chile + Santiago + Java = 1




¿y cual será el motivo si mi puntaje no ha cambiado? 😀


GraalVM + Java 11 + AWS Lambda + PostgreSQL + DynamoDB + Docker

En esta ocasión mostraré la travesía que fue conseguir compilar un jar con GraalVM y que luego corra en AWS Lambda.

¿Por qué hacerlo?
En la actualidad, el cold start de lanzar un jar en un lambda es alto, varios segundos (en node puede ser medio segundo o menos), pero luego de eso cada invocación tiene un tiempo de respuesta bueno, son solo algunos milisegundos. Entonces mi motivación es hacer cada vez el cold start menor y si mejora el tiempo de respuesta de las siguientes invocaciones es aún mejor 😉.

¿Que se necesita?
  1. Java 11
  2. Docker
  3. Maven
  4. Acceso a Lambdas en AWS

¿Cómo hacerlo?
Esta es la parte entretenida, en un principio fue compleja pero luego de entenderla fue divertido.
  1. Para poder lanzar el jar compilado hay que utilizar AWS Lambdas con un custom runtime.
  2. El custom runtime tiene un comportamiento que debe ser seguido y se describe en este link y acá. En resumen: existe una URL para obtener un evento, otra para indicar si hay un error al iniciar, también una para enviar la respuesta del evento y la última para notificar error al procesar el evento. Considerar que la aplicación debe mantenerse en ejecución esperando nuevos eventos.
  3. Antes de poder tener el jar compilado hay que compilarlo... Para eso usé GraalVM con Docker.
Con esas mínimas consideraciones comenzamos...

El siguiente código (en un proyecto usando maven) es lo mínimo necesario para cumplir: 
public class App {

    public static void main(String[] args) throws MalformedURLException, IOException {

        while (true) {
            final var url = new java.net.URL(
                    new StringBuilder("http://")
                            .append(System.getenv("AWS_LAMBDA_RUNTIME_API"))
                            .append("/2018-06-01/runtime/invocation/next")
                            .toString());

            final var conn = url.openConnection();
            final var requestid = conn.getHeaderField("Lambda-Runtime-Aws-Request-Id");

            try (final var is = conn.getInputStream()) {
                int lee;
                while ((lee = is.read()) != -1) {
                    System.out.print((char) lee);
                }
            }

            ((HttpURLConnection) conn).disconnect();
            final var ok = new java.net.URL(
                    new StringBuilder("http://")
                            .append(System.getenv("AWS_LAMBDA_RUNTIME_API"))
                            .append("/2018-06-01/runtime/invocation/")
                            .append(requestid).append("/response")
                            .toString());

            final var responsehttp = (HttpURLConnection) ok.openConnection();
            responsehttp.setRequestMethod("POST");
            responsehttp.setDoOutput(true);
            responsehttp.connect();

            try (final var os = responsehttp.getOutputStream()) {
                os.write("ok".getBytes());
                os.flush();
            }

            try (final var is = responsehttp.getInputStream()) {
                int lee;
                while ((lee = is.read()) != -1) {
                    System.out.print((char) lee);
                }
            }
            responsehttp.disconnect();
        }
    }
}

Explicación del código:
  • Usa el while(true) para quedar corriendo mientras espera nuevos eventos.
  • Crea la URL para acceder a los eventos.
  • Cuando recibe un evento obtiene el Lambda-Runtime-Aws-Request-Id (necesario para luego enviar la respuesta, está descrito en los links de arriba).
  • Lee desde el InputStream los datos del evento recibido y los muestra.
  • Crea la URL para enviar la respuesta.
  • Escribe ok como respuesta al evento recibido (en un POST).
  • Lee el InputStream del POST enviado (necesario para poder recibir un nuevo evento).
  • Vuelve a esperar un evento.
  • Usa AWS_LAMBDA_RUNTIME_API para obtener la ip y puerto al que se le envía la respuesta y se obtienen los eventos.
Luego de eso hay que generar el jar con mvn package y continuar con la compilación usando GraalVM y Docker. El siguiente es el Dockerfile:

FROM oracle/graalvm-ce:19.3.1-java11 as graalvm
COPY . /app
WORKDIR /app
RUN gu install native-image
RUN native-image --no-server -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:+AddAllCharsets -H:EnableURLProtocols=http,https --enable-all-security-services -H:+JNI -H:+TraceServiceLoaderFeature -H:+StackTrace  -jar target/lambda-graal-1.0-SNAPSHOT.jar

FROM frolvlad/alpine-glibc
COPY --from=graalvm /app/lambda-graal-1.0-SNAPSHOT /app/bootstrap

Lanzarlo con:
docker build -t demo .

Ahora que tenemos la imagen hay que extraer el binario generado con GraalVM y dejarlo ejecutable (el nombre tiene que ser bootstrap): 
docker run --rm demo cat /app/bootstrap > bootstrap
chmod +x bootstrap

Luego generamos el zip para subirlo a AWS Lambda:
zip function.zip bootstrap

Y eso es todo!

Para crear una aplicación nativa PERO más compleja usando ServiceLoader, DynamoDB y PostgreSQL Necesité lo siguiente:
  • Hay que tener el archivo libsunec.so y cacerts.
  • Ocupé el plugin Shade para generar un uber-jar.
  • En la dependencia de DynamoDB quité apache-client y netty-nio-client.
  • Usé url-connection-client como dependencia (por eso quité los anteriores).
  • Agregué slf4j-simple para evitar el error UnresolvedElementException con org.slf4j.impl.StaticLoggerBinder.
  • Generé el archivo scripts.sh para realizar todo el proceso.
  • Usé la variable de ambiente _HANDLER que entrega lo que se escribe en la consola de AWS :

  • Creé una interface para servicios (LambdaService) y otra para los handlers (LambdaHandler).
  • La clase que se escribe en el handler de la consola de AWS tiene que ser un LambdaHandler para que sea invocado (es el nombre de la clase, no incluye el método).
  • Luego de configurar la lambda con 128mb de memoria, 5 segundos de timeout y subir el zip, el resultado es el siguiente:

El resultado es excelente 😊 !!!

El código queda disponible en GitHub 😎 y los enlaces que me ayudaron fueron:







Codersrank - TOP DEVELOPERS IN JAVA

Hoy solo quiero dejar las capturas de pantalla de Codersrank en las cuales tengo el agrado de aparecer 😊, no se por cuanto tiempo estaré en el ranking, así que las guardo de recuerdo 😊.


  • Filtro: Chile + Santiago + Java = 1


  • Filtro: Chile + Java = 6





















llegar al 1 en Chile será un poco dificil...



Buenas Practicas creando AWS Lambdas con Java

En esta entrada recopilaré buenas practicas que he aprendido al momento de crear Lambdas en AWS con Java y Maven. Algunas son mis recomendaciones y otras son desde AWS (sobre todo como evitar el cold start). Comenzamos:

  • Todo lo que pueda ser estático tiene que ser estático, de esta manera serán reutilizados en los siguientes request que se realicen en la misma instancia del Lambda.
  • inicializar todo lo que puedan en el handler del lambda en un bloque estático: 
    public class LambdaHandler implements RequestStreamHandler {
     
      static {
         // acá
      }
    
      @Override
      public void handleRequest(InputStream is, OutputStream os, Context ctx) throws IOException {
    
      }
    }
    Hay que hacer esto porque cuando se instancia la clase hay más poder de computo que el configurado como límite para la lambda, si hay conexiones a DynamoDB hay que instanciar la conexión y hacer una Query (cualquiera) lo importante es que se instancien todas las dependencias. Lo mismo ocurre para conexiones con RDS o cualquier otro recurso.
  • Utilizar CloudFormation o Serverless para automatizar nuestra solución.
  • No ocupar el plugin shade de maven para crear el Jar (no me gusta 😊), prefiero dejar dentro del jar una carpeta lib con todas las librerías la cual quedará disponible cuando se ejecute en el entorno de AWS:
    <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-dependency-plugin</artifactId>
       <version>3.1.1</version>
       <executions>
          <execution>
             <id>copy-dependencies</id>
             <phase>prepare-package</phase>
             <goals>
                <goal>copy-dependencies</goal>
             </goals>
             <configuration>
                <outputDirectory>${project.build.directory}/classes/lib</outputDirectory>
                <overWriteReleases>false</overWriteReleases>
                <overWriteSnapshots>false</overWriteSnapshots>
                <overWriteIfNewer>true</overWriteIfNewer>
                <includeScope>runtime</includeScope>
             </configuration>
          </execution>
       </executions>
    </plugin>
  • Utilizar el logger de log4j por su versatilidad:
    <dependencies>
       <dependency>
          <groupId>com.amazonaws</groupId>
          <artifactId>aws-lambda-java-log4j2</artifactId>
          <version>1.1.0</version>
       </dependency>
       <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-core</artifactId>
          <version>2.12.1</version>
       </dependency>
       <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-api</artifactId>
          <version>2.12.1</version>
       </dependency>
    </dependencies>
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration packages="com.amazonaws.services.lambda.runtime.log4j2">
       <Appenders>
          <Lambda name="Lambda">
             <PatternLayout>
                <pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n</pattern>
             </PatternLayout>
          </Lambda>
       </Appenders>
       <Loggers>
          <Root level="info">
             <AppenderRef ref="Lambda" />
          </Root>
       </Loggers>
    </Configuration>
    private static final Logger LOGGER = LogManager.getLogger(LambdaHandler.class);
  • Usar sam para ejecuciones locales del lambda:
    sam local invoke NombreFuncion -t src/main/aws/template.yml
  • Para testing usar testcontainers con localstack, tendremos los servicios de AWS en nuestro ambiente de pruebas (es genial 😍):
       <dependency>
          <groupId>org.testcontainers</groupId>
          <artifactId>testcontainers</artifactId>
          <version>${test.containers}</version>
          <scope>test</scope>
       </dependency>
       <dependency>
          <groupId>org.testcontainers</groupId>
          <artifactId>localstack</artifactId>
          <version>${test.containers}</version>
          <scope>test</scope>
       </dependency>
  • No usar frameworks somo spring, jersey o similares, no es necesario (salvo que quieran utilizar graalvm para compilarlos), las lambdas deberían ser simples (muy personal, nada en contra de los frameworks)
El código de ejemplo está en Github 😇

Java + Cognito: Pre Token Generation Lambda Trigger

En esta ocasión dejaré el código necesario para poder generar la respuesta en un Lambda de AWS que se ejecute como un Pre Token Generation Lambda Trigger, no es ninguna explicación de que es Lambda o Cognito, lo hago porque no encontré en el universo un ejemplo que explicara el formato de la respuesta usando Java, usando NodeJS habían cientos incluso desde la misma documentación de AWS...

Si alguien pasa por el mismo problema y llega acá, este es el código:

public class PreTokenGenerationLambdaTrigger implements RequestStreamHandler {
  private static final ObjectMapper OM;

  static {
    OM = new ObjectMapper();
    OM.enable(SerializationFeature.INDENT_OUTPUT);
    ServiceLoader.load(LambdaServicio.class).forEach(LambdaServicio::cargar);
  }

  @Override
  public void handleRequest(InputStream is, OutputStream os, Context ctx) throws IOException {
    final var request = OM.readTree(is);
    final var response = (ObjectNode) request;
    OM.writeValue(os, response); 
  }

}
El problema se solucionó con lo siguiente:
OM.enable(SerializationFeature.INDENT_OUTPUT);
Cognito reclamaba que no entendía la salida generada desde el Lambda, y con esa opción de Jackson se corrige. Ahora se pueden agregar atributos personalizados antes de generar el Token 😏