Records preview en JDK 14 ea

Hoy escribiré un ejemplo del uso del nuevo type declaration Record en una versión del JDK 14 
Early-Access Builds.

¿Que es un Record?


Es una nueva declaración de tipo, similar a una clase con las siguientes características (descritas en el JEP 359):

  • tiene un nombre y un state description que define sus componentes.
  • cada componente del estado tiene un método de acceso público con el mismo nombre del componente.
  • tiene un constructor público con la misma firma que el state description en el cual se inicializa cada campo desde el argumento.
  • implementa equals y hashCode, en donde 2 records serán iguales si son del mismo tipo y contienen el mismo estado.
  • implementa toString con la representación de todos sus componentes.
  • cada componente del state description es private final.
  • no pueden extender otras clases.
  • no pueden tener otros campos de instancia, solo los del state description.
  • si se definen otros campos tienen que ser static.
  • un record es final y no puede ser abstracto.
  • puede implementar interfaces.
  • su body es opcional.
  • como es preview, puede cambiar en el futuro.

Creando un Record

Un ejemplo de record es el siguiente:
record Persona(int edad, String nombre) {}
y eso es todo 👽 tenemos nuestro record Persona que en el state description define los componentes edad y nombre. Para instanciarlo realizamos lo siguiente:
final var p1 = new Persona(12, "Persona 1");
ahora p1 tiene edad = 12 y nombre = "Persona 1", y los métodos para acceder a ellos son:
p1.edad();
p1.nombre();

Y al mostrar el contenido con System.out obtenemos lo siguiente:
Persona[edad=12, nombre=Persona 1]

Compilación

  • descargar la JDK 14 ea y asociarlo al JAVA_HOME:
    export JAVA_HOME=/path/jdk-14
  • agregar la opción para que maven tenga habilitada la opción preview al ejecutar la clase main:
    export MAVEN_OPTS=--enable-preview
  • configurar el plugin de maven con --enable-preview en la compilación:
    <plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <version>3.8.1</version>
     <configuration>
         <release>14</release>
         <compilerArgs>--enable-preview</compilerArgs&gt
     </configuration>
    </plugin>
Eso es todo, el código fuente está en Github y se ejecuta con:
mvn compile exec:java



JPA-Hibernate + POJO + Projection + ConstructorResult + NativeQuery + Testcontainers + PostgreSQL


En esta ocasión mostraré un ejemplo usando Java SE 11 de como mapear una clase no entidad (un POJO) a partir de los datos que existen en la base de datos y que representan a una entidad utilizando ConstructorResult, Projection y una construcción "manual" con JPA. Las pruebas serán realizadas usando Testcontainers junto a Postgresql como base de datos.

El procedimiento es el siguiente (se omiten los metodos/imports/anotaciones que no son relevantes y usaré lombok solo para getter/setter):

1- La Entidad
Representa la entidad que mantendrá datos en la base de datos y de la cual extraeremos algunos de sus campos para nuestro pojo.
@Entity
@Table(schema = "jpa", name = "persona")
public class Persona {
    @Id
    private int id;
    private String nombre;
    private String apellido;
}


2- El POJO
Requiere tener un constructor con los argumentos que la query utilizará:
public class PersonaPOJO {
    private String nn;
    private String aa;
    
    public PersonaPOJO() {}

    public PersonaPOJO(String nn, String aa) {
        this.nn = nn;
        this.aa = aa;
    }
}
Los atributos pueden tener el nombre que estimen conveniente.


3- Definir el ConstructorResult
@SqlResultSetMapping(name = "personapojo",
  classes = {
    @ConstructorResult(targetClass = PersonaPOJO.class,
      columns = {
        @ColumnResult(name = "nn", type = String.class),
        @ColumnResult(name = "aa", type = String.class)})})

personapojo: es el nombre que utilizaremos para hacer referencia a él
targetClass: nuestra clase objetivo (el POJO)
columns: definicion del resultado de la query a las propiedades que serán mapeadas a nuestro POJO usando el constructor

Para que sea reconocida en nuestro contexto de persistencia, personapojo tiene que ser escanenada por Hibernate y como PersonaPOJO.java no es una entidad no sería analizada si la definimos en ella. Por este motivo la dejaremos en Persona:

@SqlResultSetMapping(name = "personapojo",
  classes = {
    @ConstructorResult(targetClass = PersonaPOJO.class,
      columns = {
        @ColumnResult(name = "nn", type = String.class),
        @ColumnResult(name = "aa", type = String.class)})})
@Entity
@Table(schema = "jpa", name = "persona")
public class Persona {
    @Id
    private int id;
    private String nombre;
    private String apellido;
}
4- persistence.xml

El archivo persistence.xml hay que dejarlo dentro de META-INF con el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.2"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="em"
                      transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
    </persistence-unit>
</persistence>
Definimos el nombre de nuestra unidad de persistencia (em) e indicamos con RESOURCE_LOCAL que nosotros administramos la creación de los EntityManager y que las transacciones serán administrados localmente y no por alguna implementacion JTA (es lo que necesitamos para nuestro ejemplo).

5- Contexto de Persistencia
Lo primero que hay que hacer es crear el EntityManagerFactory:
private static EntityManagerFactory emf
    = Persistence.createEntityManagerFactory("em", cargarPropiedades());

private static Map<String, String> cargarPropiedades() {
      return Map.of(
        "javax.persistence.jdbc.driver", System.getProperty("jdbc_driver"),
        "javax.persistence.jdbc.url", System.getProperty("jdbc_url"),
        "javax.persistence.jdbc.user", System.getProperty("jdbc_user"),
        "javax.persistence.jdbc.password", System.getProperty("jdbc_password"),
        "hibernate.hbm2ddl.auto", "validate"
      );
}

usando el metodo cargarPropiedades() definimos programaticamente como será el acceso a la base de datos y no utilizamos el archivo persistence.xml para este propósito.

Luego para obtener el EntityManager:
public static EntityManager obtener() {
 return emf.createEntityManager();
}


6- Test

Para los tests usaré el siguiente script que permitirá tener datos precargados:
create schema jpa;

create table jpa.persona (
   id int primary key,
   nombre varchar default null,
   apellido varchar default null
);

insert into jpa.persona (id, nombre, apellido) values (1, 'Sebastián', 'Ávila');
Ahora las pruebas... El objetivo es sacar los datos disponibles para Persona (puede ser cualquier dato, no es necesario que esté asociado a la entidad) y usarlos para construir una instancia de PersonaPOJO (no es @Entity) utilizando JPA:
// obtener el contexto de persistencia
final var em = ContextoPersistencia.obtener();

// obtener una entidad que ya existe en la base de datos (solo por entretención)
final var persona = em.find(Persona.class, 1);
validarPersona(persona);

// Método 1: utilizar ConstructorResult junto a NativeQuery
List<PersonaPOJO> personas = em
  .createNativeQuery("select nombre nn, apellido aa from jpa.persona", "personapojo")
  .getResultList();
assertThat(personas.size()).isEqualTo(1);
validarPersona(personas.get(0));

// Método 2: utilizando JPQL y el constructor del POJO para la proyección
personas = em
  .createQuery("select new com.sebastian.pojojpa.dominio.PersonaPOJO(p.nombre, p.apellido) from Persona p")
  .getResultList();
validarPersona(personas.get(0));

// Método 3: obtener un List<Object[]> y hacer la transformación “manual”
final List<Object[]> obj = em
  .createNativeQuery("select nombre nn, apellido aa from jpa.persona")
  .getResultList();
validarPersona(obj.get(0));

em.close();
Los métodos de validación:
private void validarPersona(final Object[] persona) {
    final var pj = new PersonaPOJO();
    pj.setNn((String) persona[0]);
    pj.setAa((String) persona[1]);
    validarPersona(pj);
}
    
private void validarPersona(final Persona p) {
    assertThat(p).isNotNull();
    assertThat(p.getId()).isEqualTo(1);
    assertThat(p.getNombre()).isEqualTo("Sebastián");
    assertThat(p.getApellido()).isEqualTo("Ávila");
}

private void validarPersona(final PersonaPOJO p) {
    assertThat(p).isNotNull();
    assertThat(p.getNn()).isEqualTo("Sebastián");
    assertThat(p.getAa()).isEqualTo("Ávila");
}

Estructura del proyecto:



Y eso es todo☺el código está disponible en GitHub para ver el detalle de la implementación ✌.


Redis Streams: xadd + xread

Esta ocasión mostraré un ejemplo de Redis Streams, con los metodos xadd y xread utilizando la librería Lettuce

Generar Mensajes

El Stream tendrá el nombre mensajes y cada mensaje enviado tendrá un UUID y un número indicando la cantidad de mensajes enviados:
    final var cliente = RedisClient.create("redis://localhost:6379");
    try (final var con = cliente.connect()) {
      final var cmd = con.sync();
      int i = 0;
      while (i < 500_000) {
        final var res =
            cmd.xadd("mensajes", Map.of(UUID.randomUUID().toString(), 
                                        String.valueOf(i)));
        i++;
        LOGGER.info("enviados: {} - id: {}", i, res);
        Thread.sleep(1_000);
      }
    }
    cliente.shutdown(); 

Recibir Mensajes
El cliente lee un mensaje a la vez y recuerda el último ID leido para preguntar por los nuevos mensajes y se bloquea 5 segundos esperando mensajes:
    final var cliente = RedisClient.create("redis://localhost:6379");
    String ultimo = "0";
    boolean leyendo = true;
    var cuentaMensajes = 0;
    try (final var con = cliente.connect()) {
      final var cmd = con.sync();
      while (leyendo) {
        final var mensajes = cmd.xread(
              XReadArgs.Builder.block(Duration.ofSeconds(5)).count(1),
              StreamOffset.from("mensajes", ultimo));
        cuentaMensajes += mensajes.size();
        LOGGER.info("leidos: {}", cuentaMensajes);
        for (final var m : mensajes) {
          LOGGER.info("{}", m.getId());
          ultimo = m.getId();
        }
      }
    }
    cliente.shutdown(); 
El código es bastante simple (quitando los extras como la pausa y los comentarios quedarian menos lineas) y cumple el objetivo de mostrar lo sencillo que es utilizar xadd y xread con Lettuce para Streams del poderoso Redis 💪.

El código queda disponible en GitHub 🙃.



Spring Boot + Spring Security + React + Keycloak

Ahora mostraré como utilizar Spring Boot, Spring Security y Keycloak para tener recursos protegidos accediendo a ellos utilizando React 😉.

Configurar Keycloak

Configurar Spring Boot

Configurar React

 

  • Keycloak


Keycloak se puede obtener utilizando docker o desde su página como Standalone server distribution (previamente en este enlace había indicado como obtenerlo y lanzarlo). 
Luego de lanzarlo estará disponible localmente y procedemos a realizar su configuración (en mi caso visitando http://localhost:8282/):

Si es la primera vez configuramos el usuario administrador. Una vez ingresado entramos a la aplicación:

Seleccionamos Add realm y definimos su nombre, en este ejemplo es: sso


Luego vamos a Clients y presionamos Create para agregar dos clientes: cliente-uno para Spring Boot y cliente-dos para React, en ambos definimos su Root URL y Web Origins (NO olvidar presionar el boton save cuando realizamos modificaciones, web origins puede ser más estricto, es * para el ejemplo):



Luego creamos el cliente-dos en forma similar al cliente-uno pero especificando la URL en la que se encontrará React (no olvidar presionar save luego de realizar las modificaciones):

Ahora creamos un Role para el Realm sso (en este ejemplo es privilegiado):

Luego creamos un usuario (sebastian) y lo asociamos al rol que creamos:




  • Spring Boot

La primera clase es la configuración de Keycloak:
@Configuration
@EnableWebSecurity
public class KeycloakSecurity extends KeycloakWebSecurityConfigurerAdapter {

  @Autowired
  public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception {
    final var kap = keycloakAuthenticationProvider();
    kap.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
    auth.authenticationProvider(kap);
  }

  @Bean
  public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
    return new KeycloakSpringBootConfigResolver();
  }

  @Bean
  @Override
  protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
  }

  @Override
  protected void configure(final HttpSecurity http) throws Exception {
    super.configure(http);
    http.cors().and().csrf().disable().authorizeRequests().antMatchers("/personas*")
        .hasRole("privilegiado").anyRequest().permitAll();
  }
}

En el método configure() especificamos que todos los request a /personas* tienen que tener el rol privilegiado.

Y la clase para lanzar Spring Boot:
@SpringBootApplication
@RequestMapping
@CrossOrigin(allowCredentials="true")
public class SbKeycloakApplication {

  @GetMapping(path = "/")
  public ResponseEntity saludo() {
    return ResponseEntity.ok(new Persona(3, "tres"));
  }

  @GetMapping(path = "/personas")
  public ResponseEntity> customers(final Principal principal, final Model model) {
    final var personas = new ArrayList();
    personas.add(new Persona(1, "uno"));
    personas.add(new Persona(2, "dos"));
    return ResponseEntity.ok(personas);
  }

  public static void main(final String[] args) {
    SpringApplication.run(SbKeycloakApplication.class, args);
  }

}
Para no extender el ejemplo quedan los metodos de recursos en la misma clase:
  • request a / no requieren autorizacion
  • request a /personas requieren autorizacion
y las propiedades de configuracion:
keycloak.principal-attribute=preferred_username
keycloak.auth-server-url=http://localhost:8282/auth
keycloak.realm=sso
keycloak.resource=cliente-uno
keycloak.public-client=true


Si lanzamos la aplicación y accedemos al http://localhost:8080 obtenemos lo siguiente:

es de acceso libre. Ahora accedemos al http://localhost:8080/personas y no podemos acceder inmediatamente:


 tenemos que especificar el usuario y clave asociados al rol privilegiado y accedemos a los recursos:




Así tenemos nuestra aplicacion Spring Boot protegida con Keycloak.

  • React

El ultimo paso es crear la vista:
  • propagará las credenciales al backend (spring boot)
  • permitirá acceder al inicio (público) y a los recursos protegidos (keycloak)
Mostraré las partes relacionadas con la seguridad y todo el resto estará disponible en Github.

  • dentro de la carpeta public tiene que quedar un archivo keycloak.json con el siguiente contenido (que puede variar dependiendo de la instalación):
    {
      "realm": "sso",
      "auth-server-url": "http://localhost:8282/auth",
      "ssl-required": "external",
      "resource": "cliente-dos",
      "public-client": true,
      "confidential-port": 0,
      "enable-cors": true
    }
    
 el cual se obtiene desde keycloak (Clients/cliente-dos/installation/keycloak OIDC JSON):


  • Ahora creamos los metodos relacionados con obtener los datos desde el backend en el componente Protegido.js:
    componentDidMount() {
        const keycloak = Keycloak('/keycloak.json');
        keycloak.init({onLoad: 'login-required'}).then(authenticated => {
          this.setState({ keycloak: keycloak, authenticated: authenticated });
          this.cargarDatosProtegidos();
        })
      }
    
      authorizationHeader() {
        if(!this.state.keycloak) return {};
        return {
          headers: {
            "Authorization": "Bearer " + this.state.keycloak.token
          }
        };
      }
    
      cargarDatosProtegidos() {
        fetch('http://localhost:8080/personas', this.authorizationHeader())
        .then(response => response.json())
        .then(result => this.setState({datos: result}))
        .catch(error => console.log(error));
      }
    
      render() {
        if (this.state.keycloak) {
          if (this.state.authenticated) 
             return ...codigo-contenido-autorizado...; 
           else return ...codigo-error...
        }
        return ...codigo-no-autorizado...;
      }
    }
    
(en github el código esta completo)
  • cuando se carga el componente carga los datos de acceso a keycloak e intenta cargar los datos protegidos del backend, si no tiene las credenciales se mostrará la pagina de autenticación del keycloak, luego se agrega al header del request y se accede a los recursos protegidos:



  •  También está el componente Logout.js que realiza el logout en keycloak:
    logout() {
      this.props.history.push('/');
      this.props.keycloak.logout();
    }
    

Y eso es todo 😌, está keycloak configurado, recursos de spring boot protegidos y la vista con React hermosa 😖. Ahora podemos continuar creando microservicios seguros 😁.

Enlaces del código fuente:

Spring Boot con Redis para almacenar JSON y no la serialización predefinida

En esta entrada mostraré como utilizar Spring Boot y Redis con el objetivo de almacenar los objetos como JSON y no como la forma de serialización de Java por defecto que tiene RedisTemplate. Los test están con Testcontainers, Junit 5 y AssertJ.


  • Lo primero es la clase que nos convoca, la configuración de RedisTemplate:
@Configuration
public class RedisConfig {

  @Autowired
  private ConfigurableApplicationContext context;

  @Bean
  @Primary
  public RedisTemplate redisTemplate(JedisConnectionFactory conn) {
    RedisTemplate template = new RedisTemplate<>();
    template.setConnectionFactory(conn);
    template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return template;
  }

  @Bean
  public JedisConnectionFactory redisConnectionFactory(@Value("${redis.host}") String host,
          @Value("${redis.puerto}") int puerto) {
    RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, puerto);
    return new JedisConnectionFactory(config);
  }
}

  • En la clase anterior, lo que importa para el objetivo del ejemplo es lo siguiente:
    template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    
    Acá definimos que en redis el valor será un JSON y las claves serán String.
  • La clase que usaré para la prueba es la siguiente:
    public class Persona {
    
      private int id;
      private String nombre;
    
      public int getId() {
        return id;
      }
    
      public void setId(int id) {
        this.id = id;
      }
    
      public String getNombre() {
        return nombre;
      }
    
      public void setNombre(String nombre) {
        this.nombre = nombre;
      }
    
    }
  • La clase de entrada para la aplicación:
    @SpringBootApplication
    public class RedisStringApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(RedisStringApplication.class, args);
      }
    }
  • Y la clase de Test:
    @SpringBootTest
    @ContextConfiguration(initializers = {RedisStringJsonTests.Initializer.class})
    public class RedisStringJsonTests {
      private static String redisHost;
      private static int redisPuerto;
      
      @Rule
      public static GenericContainer cont = new GenericContainer<>("redis:5.0.3-alpine")
              .withExposedPorts(6379);
    
      @AfterAll
      static void detenerRedis() {
        cont.stop();
      }
    
      @BeforeAll
      static void iniciarRedis() {
        cont.start();
      }
    
      static class Initializer
              implements ApplicationContextInitializer {
    
        @Override
        public void initialize(ConfigurableApplicationContext ctx) {
          redisHost = cont.getContainerIpAddress();
          redisPuerto = cont.getFirstMappedPort();
          TestPropertyValues.of(
                  "redis.puerto=" + redisPuerto,
                  "redis.host=" + redisHost
          ).applyTo(ctx.getEnvironment());
        }
      }
    
      @Autowired
      private RedisTemplate redis;
    
      @Test
      public void validarRegistroJson() throws IOException {
        assertThat(redis).isNotNull();
        var hash = redis.opsForHash();
        final Persona p = new Persona();
        final var claveRedis = "redis-clave";
        final var claveRegistro = "clave";
        p.setId(1);
        p.setNombre("nombre");
        hash.put(claveRedis, claveRegistro, p);
        var x = (Persona) hash.get(claveRedis, claveRegistro);
        assertThat(x.getId()).isEqualTo(1);
        assertThat(x.getNombre()).isEqualTo("nombre");
        
        Jedis jedis = new Jedis(redisHost, redisPuerto);
        final var recuperado = jedis.hget(claveRedis, claveRegistro);
        var personaRecuperada = new ObjectMapper().readValue(recuperado, Persona.class);
        assertThat(personaRecuperada).isEqualToComparingFieldByField(p);
      }
    }
  • El Test consiste en lo siguiente:
    • Utiliza testcontainers para ejecutar redis
    • Define propiedades para acceder a Redis al inicializar los beans
    • Utiliza RedisTemplate para guardar un objeto y luego recuperarlo
    • Recuperar el valor json guardado para transformarlo en una instancia de la clase
  • El test demuestra que se puede guardar y recuperar el objeto y también se realiza una nueva conexion para recuperar el String y transformarlo para compararlo con la instancia que se guardó.
  • Para confirmar que se guardó como JSON la instancia al ser serializada, usando los datos que entrega testcontainers accederé al redis del contenedor para validar que se guardó como queremos (breakpoint en el código para conocer el puerto 😁):

Eso es todo, el código queda disponible en Github 👀

Usando Redis en Pruebas [versión 2]

Previamente había comentado como utilizar Redis en pruebas mediante redis embebido, pero en esta ocasión utilizaremos Testcontainers, que como lo dicen en su página, podemos utilizar cualquier cosa que corra en docker en nuestros tests 😮.

Comencemos

Lo primero es incluir en nuestro pom la dependencia a testcontainers:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.11.2</version>
</dependency>

También incluimos otras librerías de test y el cliente redis:
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.11.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0-m1</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.26</version>
    <scope>test</scope>
</dependency>

Ahora en nuestra clase de test escribimos lo siguiente:
@Rule
public GenericContainer redis = new GenericContainer<>("redis:5.0.3-alpine").withExposedPorts(6379);

@Test
public void clientePuedeObtenerConexion() {
    var cliente = new Jedis(redis.getContainerIpAddress(), redis.getFirstMappedPort());
    cliente.set("hola", "chao");
    Assertions.assertThat(cliente.get("hola")).isEqualTo("chao");
}

Lo que hace es iniciar la imagen redis:5.0.3-alpine en un contenedor y define el puerto interno PERO lo expone en el primer puerto que encuentra. Para cambiar este comportamiento hacemos lo siguiente exponiendo el puerto local 10000:


final Consumer config = e -> 
e.withPortBindings(new PortBinding(Ports.Binding.bindPort(10000),
new ExposedPort(containerExposedPort)));

@Rule
public GenericContainer redis = new GenericContainer("redis:5.0.3-alpine")
                .withExposedPorts(containerExposedPort)
                .withCreateContainerCmdModifier(config);


nuestro cliente puede ahora conectarse al puerto 10.000.

El código completo está disponible en Github 😃.




Servicios: Invertir las dependencias utilizando el sistema de módulos

Hoy quiero mostrar un ejemplo del uso de servicios utilizando módulos. Esta característica está presente desde Java 9 y nos permite invertir la dirección de las dependencias dejando al sistema de módulos obtener las implementaciones que serán utilizadas y nuestro modulo consumidor solo tiene que interactuar con tipos abstractos.

Creando Servicios

Comenzamos con la siguiente figura:



El Consumidor solo tiene conocimiento de la interface Servicio, la cual utiliza en tiempo de ejecución llamando al método identificar en cada una de las implementaciones que recibe, en este caso serán ServicioA y ServicioB. Lo entretenido del sistema de módulos es que nos entrega esta funcionalidad, podemos crear los módulos que provean los servicios, agregarlos al module path y serán provistos a la clase consumidora. La siguiente imagen muestra los módulos que usaremos:
  


Si es necesario agregar mas servicios generamos nuevos módulos que implementan la interface Servicio y los dejamos en el module path.

Carga de los Servicios

El sistema de módulos se encarga de entregar las implementaciones del servicio al consumidor y lo hace a través de la clases ServiceLoader y Provider. Lo que tenemos que hacer para que puedan ser utilizadas nuestras Implementaciones es lo siguiente:

  • Exportar el paquete que contiene el tipo abstracto que representa al servicio (export), en nuestro caso la interface Servicio se encuentra en el paquete com.sebastian.servicios.port y luego indicamos con uses que la utilizaremos como un servicio:


  • En los módulos que implementan el servicio requerimos el módulo que contiene la interface e indicamos lo siguiente:
    provides com.sebastian.servicios.port.Servicio with com.sebastian.productor.a.ServicioA;
    
    • provides está acompañado del servicio que implementamos.
    • with indica cual es la clase que está entregando el servicio.
 




  • Para crear el servicio en los módulos proveedores hay que cumplir uno de los siguientes requisitos:
    • La clase tiene que tener un constructor público sin argumentos e implementar la interface del servicio.
    • Tener una clase factory con un método público, estático, sin parámetros que se llame provider que retorne la implementación del servicio.
  • finalmente la clase consumidora que desconoce los servicios que utilizará tiene el siguiente código:

    • Utiliza el método ServiceLoader::load indicándole cual es el servicio requerido (Servicio.class).
    • Llama al método Provider::get para obtener las implementaciones que encontró, las guarda en una lista y llama al método identificar en cada una de las referencias.
  • Al lanzar la aplicación obtenemos el siguiente resultado: 

compilar con:
mvn clean package
lanzar con:
java --module-path mods --module services.consumidor/com.sebastian.servicios.Consumidor
Conclusiones

  • tenemos disponible la inversión de las dependencias directamente desde el sistema de módulos sin requerir librerías adicionales
    • Sin la inversión de dependencias tenemos la siguiente imagen:

    • Acá el Consumidor conoce explícitamente cuales son sus dependencias (requires en el module-info para cada servicio)
    • Si hay que agregar una nueva implementación del servicio el Consumidor tiene que ser modificado
  • Desacoplamos el servicio de sus implementaciones
  • Se pueden modificar o reemplazar componentes sin interferir entre módulos

Eso es todo por hoy 😁, en GitHub está el código!

NOTA: El código en GitHub incluye interacción con servicios pre modulos ✌ (servicios C y D)