¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
Raúl Martínez 30/09/2019 Cargando comentarios…
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.
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).
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>
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'
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:
compile 'org.javers:javers-persistence-mongo:5.6.1'
<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.
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:
En el yml que tengamos definido como recurso deberemos de poner algo así:
spring:
output:
ansi:
# 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
}
]
}
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.
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.
Cuéntanos qué te parece.