【问题标题】:Audit Property change - Spring MVC+ JPA审计属性更改 - Spring MVC+ JPA
【发布时间】:2017-01-05 15:30:42
【问题描述】:

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

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 + "to" + newValue + "for" clientID +"by" modifiedBy;

  • George 将比尔·盖茨的 mobileNumber 从 555 更改为 999。

我这样做的原因是我需要将此更改存储到操作表下的数据库中 - 因为我将审核来自不同实体的属性,并且我想将它们存储在一起,然后能够在我获取它们时需要。

我该怎么做?

谢谢

【问题讨论】:

  • JPA 不提供审计对特定属性的更改的方法。如果您使用 Hibernate 作为 JPA 提供程序,您可以编写自己的 Interceptor,实现 onFlushDirty 方法,检查字段以查找更改的字段,然后生成审核日志。
  • 你能给我举个拦截器使用的例子吗?我使用 Hibernate 作为 JPA 提供程序
  • 我开始考虑Custom Annotations is Spring。但是无法弄清楚如何正确获取旧实例和新实例。例如,我创建注释 @CaptureChange,它将以我的确切方式开始捕获 change 的过程。这对我的解决方案来说真的是个好主意吗?
  • @JONIVar 从我下面的答案中可以看出,自定义注释可以使用 AOP(Spring AOP 或 AspecJ 编译时)进行处理。这种方法比 Hibernate 拦截器稍微复杂一些,但它是更灵活的解决方案,没有性能开销。

标签: java spring jpa audit-logging


【解决方案1】:

Aop 是正确的方法。您可以根据需要将 AspectJ 与字段 set() 切入点一起使用。使用before aspect,您可以提取必要的信息来填充 Action 对象。

您还可以使用自定义类注释@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 以进行依赖注入。

更新: Here 就是这种 AspectJ 项目配置的一个例子

【讨论】:

  • @JarrodRoberson 如果你对所有不好的答案投了反对票,那么对好的答案投赞成票是公平的。
  • 我在评论尝试的质量,我不确定它是否真的有用或正确,但它比你正在做的答案要好,我想给你一些归功于改进。综上所述,这是一个太宽泛的问题,基本上是send me teh codez,这是最糟糕的问题。它也违反了题外话:建议,所以我通常会否决所有答案,以阻止任何人回答这样的问题。赏金是它仍然开放的唯一原因。
  • @Sergey Bespalov - 你能给我举个例子如何将新旧对象转移到那个注解吗?
  • 据我了解,您的目标不是新旧对象版本,您希望使用有关属性更改的信息填充 Action 对象。我的示例已经展示了如何创建包含所需信息的 Action 对象。
  • @Purmarili here 是一个例子
【解决方案2】:

如果您使用 Hibernate,您可以使用 Hibernate Envers,并定义您自己的 RevisionEntity(如果您想使用 java.time,您将需要 Hibernate 5.x。在早期版本中,甚至自定义 JSR-310 实用程序不适用于审计目的)

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

【讨论】:

    【解决方案3】:

    我不知道“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

    【讨论】:

    • 你能给我举个AOP的例子吗?
    • 这对我没有帮助。您能提出其他建议吗?
    【解决方案4】:

    AOP 绝对是您案例的解决方案,我使用 Spring AOP 实现了类似的案例以保留实体修订。此解决方案的一个要点是需要使用 around 切入点。

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

    会得到如下输出:

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

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

    这是一个开源项目JaVers - object auditing and diff framework for Java

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

    你可以看看这个项目。

    【讨论】:

    • Interceptor 对应用程序性能的影响有多大?
    • @JONIVar 对不起,我没有为此做基准测试,实际上这取决于具体的拦截器实现。
    • @JONIVar 这对你有帮助吗?
    • 我发现 Interceptor 以某种方式影响性能。所以我更喜欢其他一些解决方案。还是谢谢
    【解决方案5】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-08-22
      • 1970-01-01
      • 1970-01-01
      • 2014-11-10
      • 2016-06-02
      • 2017-03-07
      • 2015-01-20
      • 2019-01-27
      相关资源
      最近更新 更多