«1. Обзор
В этом руководстве мы увидим, как настроить и использовать JaVers в простом приложении Spring Boot для отслеживания изменений сущностей.
2. JaVers
При работе с изменяемыми данными у нас обычно есть только последнее состояние сущности, хранящееся в базе данных. Как разработчики, мы тратим много времени на отладку приложения, поиск в лог-файлах события, изменившего состояние. Это становится еще сложнее в производственной среде, когда систему использует множество разных пользователей.
К счастью, у нас есть отличные инструменты, такие как JaVers. JaVers — это структура журнала аудита, которая помогает отслеживать изменения сущностей в приложении.
Использование этого инструмента не ограничивается только отладкой и аудитом. Его можно успешно применять для анализа, применения политик безопасности и ведения журнала событий.
3. Настройка проекта
Прежде всего, чтобы начать использовать JaVers, нам нужно настроить репозиторий аудита для сохранения моментальных снимков сущностей. Во-вторых, нам нужно настроить некоторые настраиваемые свойства JaVers. Наконец, мы также расскажем, как правильно настроить наши модели предметной области.
Но стоит отметить, что JaVers предоставляет параметры конфигурации по умолчанию, поэтому мы можем начать использовать его практически без настройки.
3.1. Зависимости
Во-первых, нам нужно добавить стартовую зависимость JaVers Spring Boot в наш проект. В зависимости от типа постоянного хранилища у нас есть два варианта: org.javers:javers-spring-boot-starter-sql и org.javers:javers-spring-boot-starter-mongo. В этом руководстве мы будем использовать стартер Spring Boot SQL.
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-boot-starter-sql</artifactId>
<version>5.14.0</version>
</dependency>
Поскольку мы собираемся использовать базу данных H2, давайте также включим эту зависимость:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
3.2. Настройка репозитория JaVers
JaVers использует абстракцию репозитория для хранения коммитов и сериализованных сущностей. Все данные хранятся в формате JSON. Поэтому, возможно, лучше использовать хранилище NoSQL. Однако для простоты мы будем использовать экземпляр H2 в памяти.
По умолчанию JaVers использует реализацию репозитория в памяти, и если мы используем Spring Boot, дополнительная настройка не требуется. Кроме того, при использовании стартеров Spring Data JaVers повторно использует конфигурацию базы данных для приложения.
JaVers предоставляет два стартовых пакета для стеков сохраняемости SQL и Mongo. Они совместимы с Spring Data и по умолчанию не требуют дополнительной настройки. Однако мы всегда можем переопределить компоненты конфигурации по умолчанию: JaversSqlAutoConfiguration.java и JaversMongoAutoConfiguration.java соответственно.
3.3. Свойства JaVers
JaVers позволяет настроить несколько параметров, хотя в большинстве случаев достаточно стандартных значений Spring Boot.
Давайте переопределим только один, newObjectSnapshot, чтобы мы могли получать моментальные снимки только что созданных объектов:
javers.newObjectSnapshot=true
3.4. Конфигурация домена JaVers
JaVers внутренне определяет следующие типы: Entities, Value Objects, Values, Containers и Primitives. Некоторые из этих терминов взяты из терминологии DDD (Domain Driven Design).
Основной целью наличия нескольких типов является предоставление различных алгоритмов сравнения в зависимости от типа. У каждого типа есть соответствующая стратегия сравнения. Как следствие, при неправильной настройке классов приложений мы получим непредсказуемые результаты.
Чтобы указать JaVers, какой тип использовать для класса, у нас есть несколько вариантов:
-
Явно — первый вариант — явно использовать методы register* класса JaversBuilder — второй способ — использовать аннотации Неявно — JaVers предоставляет алгоритмы для автоматического определения типов на основе отношений классов. По умолчанию — по умолчанию JaVers будет обрабатывать все классы как ValueObjects
В этом руководстве мы настроим JaVers явно, используя метод аннотации.
Самое замечательное, что JaVers совместим с аннотациями javax.persistence. В результате нам не нужно будет использовать специфичные для JaVers аннотации к нашим сущностям.
4. Образец проекта
Теперь мы собираемся создать простое приложение, которое будет включать в себя несколько доменных объектов, которые мы будем проверять.
«4.1. Модели доменов
Наш домен будет включать в себя магазины с продуктами.
Давайте определим объект Store:
@Entity
public class Store {
@Id
@GeneratedValue
private int id;
private String name;
@Embedded
private Address address;
@OneToMany(
mappedBy = "store",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Product> products = new ArrayList<>();
// constructors, getters, setters
}
Обратите внимание, что мы используем аннотации JPA по умолчанию. JaVers сопоставляет их следующим образом:
-
@javax.persistence.Entity сопоставляется с @org.javers.core.metamodel.annotation.Entity @javax.persistence.Embeddable сопоставляется с @org.javers.core.metamodel. аннотация.ValueObject.
Встраиваемые классы определяются обычным образом:
@Embeddable
public class Address {
private String address;
private Integer zipCode;
}
4.2. Репозитории данных
Для аудита репозиториев JPA JaVers предоставляет аннотацию @JaversSpringDataAuditable.
Давайте определим StoreRepository с этой аннотацией:
@JaversSpringDataAuditable
public interface StoreRepository extends CrudRepository<Store, Integer> {
}
Кроме того, у нас будет ProductRepository, но без аннотации:
public interface ProductRepository extends CrudRepository<Product, Integer> {
}
Теперь рассмотрим случай, когда мы не используем Spring Data репозитории. Для этой цели в JaVers есть еще одна аннотация уровня метода: @JaversAuditable.
Например, мы можем определить метод сохранения продукта следующим образом:
@JaversAuditable
public void saveProduct(Product product) {
// save object
}
Кроме того, мы можем даже добавить эту аннотацию прямо над методом в интерфейсе репозитория:
public interface ProductRepository extends CrudRepository<Product, Integer> {
@Override
@JaversAuditable
<S extends Product> S save(S s);
}
4.3. Author Provider
У каждого зафиксированного изменения в JaVers должен быть свой автор. Более того, JaVers поддерживает Spring Security из коробки.
В результате каждая фиксация выполняется определенным аутентифицированным пользователем. Однако для этого урока мы создадим действительно простую пользовательскую реализацию интерфейса AuthorProvider:
private static class SimpleAuthorProvider implements AuthorProvider {
@Override
public String provide() {
return "Baeldung Author";
}
}
И в качестве последнего шага, чтобы заставить JaVers использовать нашу пользовательскую реализацию, нам нужно переопределить bean-компонент конфигурации по умолчанию: ~~ ~
@Bean
public AuthorProvider provideJaversAuthor() {
return new SimpleAuthorProvider();
}
5. Аудит JaVers
Наконец, мы готовы провести аудит нашего приложения. Мы будем использовать простой контроллер для диспетчеризации изменений в нашем приложении и получения журнала коммитов JaVers. Кроме того, мы также можем получить доступ к консоли H2, чтобы увидеть внутреннюю структуру нашей базы данных:
Мы используем EventListener для заполнения нашей базы данных некоторыми продуктами:
5.1. Начальная фиксация
Когда объект создается, JaVers сначала делает фиксацию типа INITIAL.
Давайте проверим снимки после запуска приложения:
В приведенном выше коде мы запрашиваем у JaVers снимки для класса Store. Если мы сделаем запрос к этой конечной точке, мы получим результат, подобный приведенному ниже:
@EventListener
public void appReady(ApplicationReadyEvent event) {
Store store = new Store("Baeldung store", new Address("Some street", 22222));
for (int i = 1; i < 3; i++) {
Product product = new Product("Product #" + i, 100 * i);
store.addProduct(product);
}
storeRepository.save(store);
}
Обратите внимание, что приведенный выше снимок включает все продукты, добавленные в магазин, несмотря на отсутствующую аннотацию для интерфейса ProductRepository.
По умолчанию JaVers проверяет все связанные модели сводного корня, если они сохраняются вместе с родителем.
Мы можем указать JaVers игнорировать определенные классы, используя аннотацию DiffIgnore.
@GetMapping("/stores/snapshots")
public String getStoresSnapshots() {
QueryBuilder jqlQuery = QueryBuilder.byClass(Store.class);
List<CdoSnapshot> snapshots = javers.findSnapshots(jqlQuery.build());
return javers.getJsonConverter().toJson(snapshots);
}
Например, мы можем аннотировать поле products аннотацией объекта Store:
[
{
"commitMetadata": {
"author": "Baeldung Author",
"properties": [],
"commitDate": "2019-08-26T07:04:06.776",
"commitDateInstant": "2019-08-26T04:04:06.776Z",
"id": 1.00
},
"globalId": {
"entity": "com.baeldung.springjavers.domain.Store",
"cdoId": 1
},
"state": {
"address": {
"valueObject": "com.baeldung.springjavers.domain.Address",
"ownerId": {
"entity": "com.baeldung.springjavers.domain.Store",
"cdoId": 1
},
"fragment": "address"
},
"name": "Baeldung store",
"id": 1,
"products": [
{
"entity": "com.baeldung.springjavers.domain.Product",
"cdoId": 2
},
{
"entity": "com.baeldung.springjavers.domain.Product",
"cdoId": 3
}
]
},
"changedProperties": [
"address",
"name",
"id",
"products"
],
"type": "INITIAL",
"version": 1
}
]
Следовательно, JaVers не будет отслеживать изменения продуктов, происходящих из объекта Store.
5.2. Обновить фиксацию
Следующим типом фиксации является фиксация UPDATE. Это наиболее ценный тип фиксации, поскольку он представляет изменения состояния объекта.
Давайте определим метод, который будет обновлять объект магазина и все продукты в магазине:
@DiffIgnore
private List<Product> products = new ArrayList<>();
Если мы запустим этот метод, мы получим следующую строку в выводе отладки (в случае Количество одинаковых продуктов и магазинов):
Поскольку JaVers успешно сохранил изменения, давайте запросим снимки для продуктов:
Мы получим предыдущие коммиты INITIAL и новые коммиты UPDATE: ~~ ~
Здесь мы можем увидеть всю информацию о внесенных нами изменениях.
public void rebrandStore(int storeId, String updatedName) {
Optional<Store> storeOpt = storeRepository.findById(storeId);
storeOpt.ifPresent(store -> {
store.setName(updatedName);
store.getProducts().forEach(product -> {
product.setNamePrefix(updatedName);
});
storeRepository.save(store);
});
}
Стоит отметить, что JaVers не создает новых подключений к базе данных. Вместо этого он повторно использует существующие соединения. Данные JaVers фиксируются или откатываются вместе с данными приложения в той же транзакции.
11:29:35.439 [http-nio-8080-exec-2] INFO org.javers.core.Javers - Commit(id:2.0, snapshots:3, author:Baeldung Author, changes - ValueChange:3), done in 48 millis (diff:43, persist:5)
5.3. Изменения
@GetMapping("/products/snapshots")
public String getProductSnapshots() {
QueryBuilder jqlQuery = QueryBuilder.byClass(Product.class);
List<CdoSnapshot> snapshots = javers.findSnapshots(jqlQuery.build());
return javers.getJsonConverter().toJson(snapshots);
}
JaVers записывает изменения как атомарные различия между версиями объекта. Как видно из схемы JaVers, отдельной таблицы для хранения изменений нет, поэтому JaVers вычисляет изменения динамически как разницу между снапшотами.
{
"commitMetadata": {
"author": "Baeldung Author",
"properties": [],
"commitDate": "2019-08-26T12:55:20.197",
"commitDateInstant": "2019-08-26T09:55:20.197Z",
"id": 2.00
},
"globalId": {
"entity": "com.baeldung.springjavers.domain.Product",
"cdoId": 3
},
"state": {
"price": 200.0,
"name": "NewProduct #2",
"id": 3,
"store": {
"entity": "com.baeldung.springjavers.domain.Store",
"cdoId": 1
}
}
}
Давайте обновим цену товара:
«
«Затем давайте запросим JaVers об изменениях:
Вывод содержит измененное свойство и его значения до и после:
Чтобы определить тип изменения, JaVers сравнивает последующие снимки обновлений объекта. В приведенном выше случае, когда мы изменили свойство объекта, мы получили тип изменения PROPERTY_VALUE_CHANGED.
public void updateProductPrice(Integer productId, Double price) {
Optional<Product> productOpt = productRepository.findById(productId);
productOpt.ifPresent(product -> {
product.setPrice(price);
productRepository.save(product);
});
}
5.4. Shadows
@GetMapping("/products/{productId}/changes")
public String getProductChanges(@PathVariable int productId) {
Product product = storeService.findProductById(productId);
QueryBuilder jqlQuery = QueryBuilder.byInstance(product);
Changes changes = javers.findChanges(jqlQuery.build());
return javers.getJsonConverter().toJson(changes);
}
Более того, JaVers предоставляет еще один вид проверяемых сущностей, который называется Shadow. Тень представляет собой состояние объекта, восстановленное из моментальных снимков. Эта концепция тесно связана с Event Sourcing.
[
{
"changeType": "ValueChange",
"globalId": {
"entity": "com.baeldung.springjavers.domain.Product",
"cdoId": 2
},
"commitMetadata": {
"author": "Baeldung Author",
"properties": [],
"commitDate": "2019-08-26T16:22:33.339",
"commitDateInstant": "2019-08-26T13:22:33.339Z",
"id": 2.00
},
"property": "price",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": 100.0,
"right": 3333.0
}
]
Существует четыре различных области действия теней:
Мелкие — тени создаются из моментального снимка, выбранного в запросе JQL. Дочерний объект-значение — тени содержат все дочерние объекты-значения, принадлежащие выбранным сущностям. — тени создаются из всех снимков, связанных с выбранными объектами Deep+ — JaVers пытается восстановить полные графы объектов с (возможно) всеми загруженными объектами.
Давайте воспользуемся областью видимости Child-value-object и получим тень для одного хранилища:
В результате мы получим сущность store с объектом Address value:
-
Чтобы получить продукты в результате, мы можем применить Commit-deep scope.
6. Заключение
@GetMapping("/stores/{storeId}/shadows")
public String getStoreShadows(@PathVariable int storeId) {
Store store = storeService.findStoreById(storeId);
JqlQuery jqlQuery = QueryBuilder.byInstance(store)
.withChildValueObjects().build();
List<Shadow<Store>> shadows = javers.findShadows(jqlQuery);
return javers.getJsonConverter().toJson(shadows.get(0));
}
В этом уроке мы увидели, как легко JaVers интегрируется, в частности, со Spring Boot и Spring Data. В целом, для установки JaVers практически не требуется никаких настроек.
{
"commitMetadata": {
"author": "Baeldung Author",
"properties": [],
"commitDate": "2019-08-26T16:09:20.674",
"commitDateInstant": "2019-08-26T13:09:20.674Z",
"id": 1.00
},
"it": {
"id": 1,
"name": "Baeldung store",
"address": {
"address": "Some street",
"zipCode": 22222
},
"products": []
}
}
В заключение можно сказать, что у JaVers могут быть разные приложения, от отладки до сложного анализа.
Полный проект для этой статьи доступен на GitHub.
«
To conclude, JaVers can have different applications, from debugging to complex analysis.
The full project for this article is available over on GitHub.