Javers es una librería nacida en 2015 cuyo objetivo principal es auditar cambios en nuestros datos. Es open-source (bajo licencia Apache) y bastante ligero, lo cual resulta interesante para aplicar a nuestro proyectos.

Por necesidades de negocio puede ser necesario tener un histórico de los distintos estados por los que ha pasado nuestro objeto o simplemente para tener una auditoría de datos, y Javers nos provee de varios tipos de utilidades que nos ayudarán a realizar estas operaciones y consultarlas de forma intuitiva y sencilla.

¿Qué nos ofrece Javers?

JaVers es una librería de Java de código abierto y ligera para auditar los cambios en los datos de nuestra aplicación.

Nos ofrece un framework preparado para proporcionar un seguimiento de auditoría de sus objetos Java (entidades, POJO, objetos de datos).

Al desarrollar una aplicación, generalmente nos concentramos en el estado actual de los objetos. Pero puede darse el caso de que necesitemos conocer el estado anterior de dicho objeto.

Por ejemplo, imaginemos que queremos tener un histórico con todos los cambios de precio de un producto. O simplemente queremos tener una auditoría de quien ha realizado determinados cambios en la descripción, stock etc del mismo producto. En estos casos, Javers es claro candidato para facilitarnos esta tarea.

La configuración es fácil, y además solo se necesita conocer algunos datos de alto nivel sobre el modelo de datos. Para ello se utilizan algunas nociones básicas siguiendo la terminología DDD (Entidad, Object Value).

Configuración

Configuración para Maven

Si nuestro gestor de construcción de proyecto es Maven debemos de importar la dependencia Javers de la siguiente manera:

<dependency>
    <groupId>org.javers</groupId>
    <artifactId>javers-core</artifactId>
    <version>5.6.1</version>
</dependency>

Configuración para Gradle

Si nuestro gestor de construcción de proyecto es Gradle debemos de importar la dependencia Javers así:

compile 'org.javers:javers-core:5.6.1'

Propósito y ejemplo práctico

Si vamos a utilizar Javers para auditar datos, tenemos que elegir la implementación de repositorio adecuada. Por ejemplo, si estás usando MongoDB agrega:

Gradle

compile 'org.javers:javers-persistence-mongo:5.6.1'

Maven

<dependency>
    <groupId>org.javers</groupId>
    <artifactId>javers-persistence-mongo</artifactId>
    <version>5.6.1</version>
</dependency>

Respecto a las compatibilidades de versiones, Javers está escrito con Java 8 desde su versión 3, con lo cual si aún estamos utilizando Java 7 la última versión compatible es la 2.9.3.

A continuación vamos a proceder con un caso práctico, ya que de esta manera se verá mejor cuales son los pasos a dar en una primera aproximación con suficiente entidad.

Ejemplo práctico

Para nuestro proyecto particular vamos a introducir estas dependencias para integrar Javers con Spring Boot (aquí puedes ver un ejemplo).

<dependency>
   <groupId>org.javers</groupId>
   <artifactId>javers-core</artifactId>
   <version>${javers.version}</version>
</dependency>
<dependency>
   <groupId>org.javers</groupId>
   <artifactId>javers-persistence-mongo</artifactId>
   <version>${javers.version}</version>
</dependency>
<dependency>
   <groupId>org.javers</groupId>
   <artifactId>javers-spring-boot-starter-mongo</artifactId>
   <version>${javers.version}</version>
</dependency>

Lo primero que haremos es crear una clase de configuración para Javers.

Tenemos dos beans denominados AuthorProvider y CommitPropertiesProvider. Estos dos beans son requeridos para el llamado “Auto audit aspect” (puedes verlo aquí). Para ambos, las implementaciones predeterminadas son creadas por el iniciador JaVers:

Para AuthorProvider: si Javers detecta Spring Security en el classpath, la implementación será la que provee SpringSecurityAuthorProvider. De lo contrario, JaVers crea un MockAuthorProvider que devuelve autor "desconocido". En nuestro caso lo hemos modificado para que se sepa quien es el autor del cambio.

Para CommitPropertiesProvider cada commit con Javers puede tener una o más propiedades cambiadas,y este bean puede ser útil para realizar consultas. En nuestro caso no lo vamos a utilizar pero se implementaría de la siguiente manera:

@Bean
public CommitPropertiesProvider commitPropertiesProvider() {
   return new CommitPropertiesProvider() {
       @Override
       public Map<String, String> provide() {
           return ImmutableMap.of("key", "ok");
       }
   };
}

@Configuration
public class JaversAuditConfiguration {

   private static final String AUTHOR = "paradigma";
   @Bean
   public CommitPropertiesProvider createAuditCommitPropertiesProvider() {
       return new EmptyPropertiesProvider();
   }

   @Bean
   @Primary
   public AuthorProvider createAuditAuthorProvider() {
       return new AuthorProvider() {
           @Override
           public String provide() {
               return AUTHOR ;
           }
       };
   }

}

A continuación en nuestra clase Application importamos dos componentes:

  1. La clase de configuración creada por nosotros (JaversAuditConfiguration).
  2. La clase JaversMongoAutoConfiguration (propio de javers-spring-boot-starter-mongo) donde, de forma autoconfigurable, se inicializan todos los repositorios de Mongo para Javers y establecer cierta configuración por defecto para seguridad, etc.

En el yml que tengamos definido como recurso deberemos de poner algo así:

spring:

 output:
   ansi:
     --md-var-hashtag- Activamos los colorines al arranque
     enabled: ALWAYS
 data:
   mongodb:
     uri: mongodb://app:app@127.0.0.1:27017/poc

Por otro lado, en nuestra capa de servicio de persistencia tendremos que inyectar las propiedades Javers y AuthorProvider.

Creación de registro. Tendremos una clase que dará de alta un registro en la colección Mongo y auditaremos llamando al método createAndCommitAuditChange.

En este método crearemos un mapa inmutable donde estableceremos la key del modelo y haremos commit con el autor (definido en el fichero de configuración), el objeto de modelo y las commitProperties del mapa anteriormente generado.

@Service
public class ToyPersistenceServiceImpl implements ToyPersistenceService {

  private static final Logger LOG = LoggerFactory.getLogger(ToyPersistenceServiceImpl.class);
  public static final String MODEL_KEY_COMMIT_KEY = "modelKey";

  @Autowired
  private ToyRepository repository;

  @Autowired
  private ToyPersistenceServiceTransformer transformer;

  @Autowired
  private Javers javers;

  @Autowired
  private AuthorProvider authorProvider;


  @Override
  public void createToy(ToyPersistenceIDTO toyPersistenceIDTO) {
     Validate.notNull(toyPersistenceIDTO, "toyPersistenceIDTO null not allowed");

     LOG.debug("Query para crear el toy con id {}", toyPersistenceIDTO.getToy().getModelKey());

     try {

        ToyMO result = repository.insert(toyPersistenceIDTO.getToy());

        createAndCommitAuditChange(result);

     } catch (DuplicateKeyException dke) {
        LOG.error("Error al insertar: Modelo duplicado", dke);
        throw new CustomValidatorCodeErrorException("Error");
     }
  }

private void createAndCommitAuditChange(ToyMO toyMO) {

  String modelKey = Optional.ofNullable(toyMO).map(ToyMO::getModelKey)
     .orElse(Strings.EMPTY);

  Map<String, String> commitProperties = ImmutableMap.of(MODEL_KEY_COMMIT_KEY, modelKey);

  javers.commit(authorProvider.provide(), toyMO, commitProperties);

}

De esta manera hemos generado lo siguiente en Mongo:

Colección con el objeto que hemos dado de alta.

Se crean dos colecciones nuevas de forma automática.

Es una cabecera que se crea por defecto con el número de operaciones realizadas.

Ahora vamos a realizar un actualización del objeto y vemos cómo se comporta. Para ello, tenemos este método que realiza la operación de actualización.

Inicialmente comprobaremos que el objeto y la clave no son vacíos, extraemos el objeto del modelo a través de un find, y seteamos el id para persistir los cambios con esta clave.

@Override
public ToyPersistenceODTO updateToy(String modelKey, ToyUpdatePersistenceIDTO toyUpdatePersistenceIDTO) { //@on

  Validate.notBlank(modelKey, "modelKey blank not allowed");
  Validate.notNull(toyUpdatePersistenceIDTO, "toyUpdatePersistenceIDTO null not allowed");

  if (!StringUtils.equals(modelKey, toyUpdatePersistenceIDTO.getToy().getModelKey())) {
     throw new CustomValidatorCodeErrorException();
  }

  ToyMO originalToy = repository.findToyMoByModelKey(modelKey);
  if (originalToy == null) {
     throw new CustomValidatorCodeErrorException();
  }

  toyUpdatePersistenceIDTO.getToy().setId(originalToy.getId());

  ToyMO toy = repository.save(toyUpdatePersistenceIDTO.getToy());

  createAndCommitAuditChange(toy);

  return transformer.toToyPersistenceODTO(toy);
}


private void createAndCommitAuditChange(ToyMO toyMO) {

  String modelKey = Optional.ofNullable(toyMO).map(ToyMO::getModelKey)
     .orElse(Strings.EMPTY);

  Map<String, String> commitProperties = ImmutableMap.of(MODEL_KEY_COMMIT_KEY, modelKey);

  javers.commit(authorProvider.provide(), toyMO, commitProperties);

}

¿Cuáles son los cambios en las colecciones de auditoría?

Y ahora la pregunta sería: ¿cómo muestro esta información para darle valor al usuario? Para ello, tendremos que montar una Jql (aquí puedes ver algunos ejemplos).

Inicialmente lo que haremos es crear una JqlQuery con los parámetros indicados en el siguiente fragmento de código, donde indicaremos la clase del modelo, las fechas “desde-hasta” de los cambios, y ejecutando la consulta invocando al siguiente método. javers.findChanges(query).

Posteriormente mapearemos la respuesta a un DTO custom donde mapearemos la salida para que sea lo más legible posible en los transformadores (serviceChangeToResponse).

private static final String MODEL_KEY_PROPERTY = "modelKey";

@Autowired
private Javers javers;


@Override
public AuditPersistenceODTO findChanges(ToyChangesAuditPersistenceIDTO idto) {
   JqlQuery colorChangesQuery = QueryBuilder.byClass(ToyMO.class)
           .from(idto.getChangesFrom())
           .to(idto.getChangesTo())
           .withCommitProperty(MODEL_KEY_PROPERTY, idto.getModelKey())
           .withChangedProperty(idto.getPropertyToShow())
           .withChildValueObjects()
           .build();

return find(colorChangesQuery);
}

private AuditPersistenceODTO find(JqlQuery query) {

   Changes changes = javers.findChanges(query);

   LOG.debug(changes.prettyPrint());

   return AuditPersistenceODTO.builder()
           .auditChanges(changes)
           .build();
}


Con el output DTO:

@Data
@GenerateCoverage
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuditPersistenceODTO {

  private Changes auditChanges;

}

Y para devolver algo legible en la salida, utilizamos un transformador (interesante ver ValueChange de javers-core).

@Override
public ToyChangesRSDTO toToyChangesRSDTO(ToyChangeODTO toyChangeODTO) {
  List<ToyChangeRSDTO> responseChanges = toyChangeODTO.getToyChanges().stream()
        .filter(ValueChange.class::isInstance)
        .map(ValueChange.class::cast)
        .map(this::serviceChangeToResponse)
        .collect(Collectors.toList());

  return ToyChangesRSDTO.builder()
        .changes(responseChanges)
        .build();

}

private ToyChangeRSDTO serviceChangeToResponse(ValueChange change) {


  return ToyChangeRSDTO.builder()
        .author(change.getCommitMetadata().map(CommitMetadata::getAuthor).orElse(null))
        .commitId(change.getCommitMetadata().map(CommitMetadata::getId).map(id -> id.valueAsNumber().doubleValue()).orElse(null))
        .changeDate(change.getCommitMetadata().map(CommitMetadata::getCommitDate).map(date -> OffsetDateTime.of(date, ZoneOffset.UTC)).orElse(null))
        .statusChangedFrom(change.getLeft().toString())
        .statusChangedTo(change.getRight().toString())
        .modelKey(change.getCommitMetadata().map(CommitMetadata::getProperties).map(map -> map.get(MODEL_KEY_COMMIT_KEY)).orElse(null))
        .build();
}

¿Qué obtenemos en la salida? Algo tan legible y útil como esto:

{
    "changes": [
        {
            "modelKey": "Exin",
            "statusChangedFrom": "Blanco",
            "statusChangedTo": "Rojo",
            "changeDate": "2019-07-11T10:17:20.47Z",
            "author": "paradigma",
            "commitId": 2
        }
    ]
}

Conclusiones

Como vemos, Javers es una librería fácil de usar que nos provee de herramientas que nos permiten visibilizar y trazar los cambios sobre los objetos teniendo una foto clara de los distintos estados por los que ha ido pasando dicho objeto.

Esto se suele utilizar para proyectos donde tengamos que hacer una auditoría de nuestro dominio y que además nos puede facilitar otro tipo de requisitos (para un PATCH incremental por ejemplo).

Os dejo en este repositorio el código fuente por si queréis probarlo en vuestras casas y podéis explorar las posibilidades que ofrece (en la carpeta doc tenéis un postman para realizar vuestras pruebas).

Dada que la documentación de Javers explica bastante bien sus distintas posibilidades, puedes encontrar más información en este link. Echadle un vistazo y probad a ver que os parece.

Cuéntanos qué te parece.

Enviar.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.