Аудит Изменение свойств - Spring MVC + JPA
У меня есть класс Client. Я хочу, чтобы иметь возможность проверять изменения свойства этого класса (не весь класс - только его свойства).
public class Client {
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
private Branch companyBranch;
на самом деле это очень легко проверить весь объект с @Audited аннотацией.
Но я хочу проверить эти изменения, используя мою структуру классов.
вот мой желаемый класс результата:
public class Action {
private String fieldName;
private String oldValue;
private String newValue;
private String action;
private Long modifiedBy;
private Date changeDate;
private Long clientID;
результат должен выглядеть следующим образом:
fieldName + "было изменено с" + oldValue + "на" + newValue + "для" clientID + "на" modifiedBy;
- mobileNumber был изменен с 555 на 999 для Билла Гейтса Джорджем.
Причина, по которой я делаю это, заключается в том, что мне нужно сохранить эти изменения в БД под таблицей действий - потому что я буду одитингом свойств из разных сущностей, и я хочу сохранить их вместе, а затем иметь возможность их получить, когда я необходимость.
Как я могу это сделать?
Спасибо
Ответы
Ответ 1
Aop - правильный путь. Вы можете использовать AspectJ с полем point set()
для ваших нужд. С аспектом before
вы можете извлечь необходимую информацию для заполнения объекта Action.
Также вы можете использовать пользовательский класс Annotation @AopAudit
для обнаружения классов, которые вы хотите провести аудит. Вы должны определить такую аннотацию в своем пути к классам и разместить ее под целевыми классами, которые вы хотите проверить.
Этот подход может выглядеть следующим образом:
AopAudit.java
@Retention(RUNTIME)
@Target(TYPE)
public @interface AopAudit {
}
Client.java
@AopAudit
public class Client {
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
}
AuditAnnotationAspect.aj
import org.aspectj.lang.reflect.FieldSignature;
import java.lang.reflect.Field;
public aspect FieldAuditAspect {
pointcut auditField(Object t, Object value): set(@(*.AopAudit) * *.*) && args(value) && target(t);
pointcut auditType(Object t, Object value): set(* @(*.AopAudit) *.*) && args(value) && target(t);
before(Object target, Object newValue): auditField(target, newValue) || auditType(target, newValue) {
FieldSignature sig = (FieldSignature) thisJoinPoint.getSignature();
Field field = sig.getField();
field.setAccessible(true);
Object oldValue;
try
{
oldValue = field.get(target);
}
catch (IllegalAccessException e)
{
throw new RuntimeException("Failed to create audit Action", e);
}
Action a = new Action();
a.setFieldName(sig.getName());
a.setOldValue(oldValue == null ? null : oldValue.toString());
a.setNewValue(newValue == null ? null : newValue.toString());
}
}
Это аспект AspectJ, который определяет auditField
pointcut для захвата операций набора полей и логики before
для создания объекта Audit
.
Чтобы включить AspectJ Compile Time Weaving
, вы должны сделать следующее в случае Maven
:
pom.xml
...
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
</dependencies>
...
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.6</version>
<configuration>
<showWeaveInfo>true</showWeaveInfo>
<source>${java.source}</source>
<target>${java.target}</target>
<complianceLevel>${java.target}</complianceLevel>
<encoding>UTF-8</encoding>
<verbose>false</verbose>
<XnoInline>false</XnoInline>
</configuration>
<executions>
<execution>
<id>aspectj-compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>aspectj-compile-test</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
Эта конфигурация Maven
позволяет компилятору AspectJ, который делает posttecode последующей обработкой ваших классов.
applicationContext.xml
<bean class="AuditAnnotationAspect" factory-method="aspectOf"/>
Также вам может потребоваться добавить экземпляр аспекта в Spring Контекст приложения для инъекции зависимостей.
UPD:
Здесь приведен пример такой конфигурации проекта AspectJ
Ответ 2
Если вы используете Hibernate, вы можете использовать Hibernate Envers и определить свой собственный RevisionEntity
(Если вы хотите работать с java.time
вам понадобится Hibernate 5.x. В более ранних версиях даже пользовательские утилиты JSR-310 не будут работать для целей аудита)
Если вы не используете Hibernate или хотите иметь чистое решение JPA, вам нужно будет написать свое собственное решение с помощью JPA EntityListeners
.
Ответ 3
Я точно не знаю, что такое атрибут "modifiedBy" (пользователь приложения или другой клиент?), но игнорируя это, вы можете уловить модификацию всех атрибутов в установщике
(Примечание: изменение настройки сеттера или добавление других параметров в сеттер - это плохая практика, эта работа должна выполняться с помощью LOGGER или AOP):
public class Client {
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
private Branch companyBranch;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn("client_ID");
List<Action> actions = new ArrayList<String>();
public void setFirstName(String firstName,Long modifiedBy){
// constructor Action(fieldName, oldValue, newValue ,modifiedBy)
this.actions.add(new Action("firstName",this.firstName,firstName,modifiedBy));
this.firstName=firstName;
}
//the same work for lastName,email,mobileNumber,companyBranch
}
Примечание. Лучшее и правильное решение - использовать LOGGER или AOP
Ответ 4
AOP абсурдно - это решение для вашего дела, я применил аналогичный случай с Spring АОП, чтобы сохранить ревизии сущности. Для этого решения необходимо использовать вокруг pointcut.
Другое решение - использовать org.hibernate.Interceptor
, org.hibernate.EmptyInterceptor
должно быть соответствующим расширением, я пишу некоторые простые коды для имитации его (возьмите коды клиента):
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
// getter and setter
}
Реализация Interceptor
public class StateChangeInterceptor extends EmptyInterceptor {
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
if (entity instanceof Client) {
for (int i = 0; i < propertyNames.length; i++) {
if (currentState[i] == null && previousState[i] == null) {
return false;
} else {
if (!currentState[i].equals(previousState[i])) {
System.out.println(propertyNames[i] + " was changed from " + previousState[i] + " to " + currentState[i] + " for " + id);
}
}
}
}
return true;
}
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
return super.onSave(entity, id, state, propertyNames, types);
}
}
Зарегистрируйте inceptor, я использую Spring boot, поэтому просто добавьте его в application.properties
spring.jpa.properties.hibernate.ejb.interceptor=io.cloudhuang.jpa.StateChangeInterceptor
Вот тест
@Test
public void testStateChange() {
Client client = new Client();
client.setFirstName("Liping");
client.setLastName("Huang");
entityManager.persist(client);
entityManager.flush();
client.setEmail("[email protected]");
entityManager.persist(client);
entityManager.flush();
}
Будет выводиться как:
email was changed from null to tes[email protected] for 1
Предположим, что его можно заменить объектами Action
.
И вот проект с открытым исходным кодом JaVers - проверка объектов и структура diff для Java
JaVers - это легкая библиотека Java для проверки изменений ваших данных.
Вы можете посмотреть этот проект.
Ответ 5
Я бы предпочел, чтобы вы переопределили метод equals вашего объекта с помощью свойства Audit.
И в DAO вы просто сравниваете старый экземпляр экземпляра с новым экземпляром, используя метод equals, который вы создали внутри объекта.
Вы сможете узнать, является ли это аудитом или нет.