Аудит Изменение свойств - 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, который вы создали внутри объекта.

Вы сможете узнать, является ли это аудитом или нет.