问题 审核属性更改 - Spring MVC + JPA


我有一个班级客户。我希望能够审计这个类的属性的变化(不是整个类 - 只是它的属性)。

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。

我这样做的原因是我需要将这些更改存储到Action表下的DB中 - 因为我将来自不同实体的审核属性,我想将它们存储在一起,然后有能力在需要时获取它们。

我怎样才能做到这一点?

谢谢


7530
2017-08-29 14:43


起源

JPA不提供审核特定属性更改的方法。如果您使用Hibernate作为JPA提供程序,您可以编写自己的 拦截器,实施 onFlushDirty 方法,检查字段以查找哪些已更改,然后生成审核日志。 - manish
你能告诉我拦截器使用的例子吗?我使用Hibernate作为JPA提供程序 - Irakli
看到 官方文件。 - manish
我开始考虑Custom Anotations是Spring。但是,没有想出如何在那里正确地获得新旧实例。例如,我创建了anotation @CaptureChange,它将以我的确切方式开始捕获变更的过程。对我的解决方案来说,这实际上是个好主意吗? - Irakli
@JONIVar如下所示,我的答案可以使用AOP(Spring AOP或AspecJ编译时)处理自定义注释。这种方法比Hibernate Interceptors稍微复杂一点,但它是更灵活的解决方案,没有性能开销。 - Sergey Bespalov


答案:


Aop是正确的方法。您可以将AspectJ与字段一起使用 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 用于捕获字段集操作的切入点 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编译器能够对类进行字节码后处理。

applicationContext.xml中

<bean class="AuditAnnotationAspect" factory-method="aspectOf"/>

此外,您可能需要将方面实例添加到Spring Application Context以进行依赖项注入。

UPD: 这里 是这种AspectJ项目配置的一个例子


8
2017-09-19 04:36



这是回答问题的正确方法! - feeling unwelcome
@JarrodRoberson如果你贬低所有不好的答案那么赞成好的答案是公平的。 - Sergey Bespalov
我正在评论尝试的质量,我不确定它实际上是有用还是正确,但它比你正在做的答案更好的一个班轮评论,我试图给你一些改进的功劳。总而言之,这是一个 太宽泛 问题基本上是 发给我teh codez 这是最糟糕的问题。它也违反了 偏离主题:建议 所以我通常会对所有答案进行投票,以阻止任何人回答这样的事情。赏金是这仍然是开放的唯一原因。 - feeling unwelcome
@Sergey Bespalov - 您能举例说明如何将新旧对象转移到该anotation吗? - Irakli
@Purmarili 这里 是一个例子 - Sergey Bespalov


如果您使用的是Hibernate,则可以使用 Hibernate Envers,并定义自己的 RevisionEntity (如果你想合作的话 java.time 你需要Hibernate 5.x.在早期版本中,即使是自定义JSR-310实用程序也不能用于审计目的)

如果您没有使用Hibernate或想要使用纯JPA解决方案,那么您需要使用JPA编写自定义解决方案 EntityListeners 机制。


3
2017-08-29 15:23





我不确切知道“modifiedBy”属性是什么(应用程序的用户或其他客户端?),但忽略了这个属性,你可以捕获setter中所有属性的修改

(注意:更改setter实现或将其他参数添加到setter是不好的做法,这项工作应该使用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 


1
2017-08-29 16:09



你能给我一个AOP的例子吗? - Irakli
看到这个链接 five.agency/logging-with-spring-aop - SEY_91
这对我没有帮助。你能提出别的建议吗? - Irakli


AOP绝对是您案例的解决方案,我使用Spring AOP实现了类似的案例来保持实体修订。需要使用此解决方案的要点 周围 切入点。

另一种解决方案是使用 org.hibernate.Interceptororg.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("test@example.com");
    entityManager.persist(client);
    entityManager.flush();
}

将获得如下输出:

email was changed from null to test@example.com for 1

所以假设它可以替换为 Action 对象。

这是一个开源项目 JaVers - Java的对象审计和差异框架

JaVers是用于审核数据更改的轻量级Java库。

你可以看看这个项目。


1
2017-09-19 06:24



拦截器会对应用程序的性能产生多大影响? - Irakli
@JONIVar对不起,我没有为此做基准测试,实际上这取决于具体的拦截器实现。 - Liping Huang
@JONIVar可以帮你吗? - Liping Huang
我发现的是Interceptor以某种方式影响性能。所以我更喜欢其他解决方案。不管怎么说,还是要谢谢你 - Irakli


我希望您应该使用Audit属性覆盖实体的equals方法。 在DAO中,您只需使用您在实体内创建的equals方法将旧的instanceof实体与新实例进行比较。

您将能够识别这是否可审核。


1
2017-09-19 06:41