How to create custom auditing entity listener that extends AuditingEntityListener

Viewed 2105

I have implemented auditing through Sping Data JPA but now I want to be able to control the update and create timestamps with custom logic. So I want to write a custom auditing entity listener that extends the Auditingentitylistener.

I have the following generic entity class where I register the custom auditing entity listener:

@MappedSuperclass
@Audited
@EntityListeners(CustomAuditingEntityListener.class)
public class AuditableEntity extends BaseEntity {

    @CreatedDate
    @Column(name = "created", nullable = false)
    private Date created;

    @CreatedBy
    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "created_by_id", foreignKey = @ForeignKey(name = "fk_entity_created_by_id"))
    private Account createdBy;

    @LastModifiedDate
    @Column(name = "last_updated", nullable = false)
    private Date lastUpdated;

    @LastModifiedBy
    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "last_updated_by_id", foreignKey = @ForeignKey(name = "fk_entity_last_updated_by_id"))
    private Account lastUpdatedBy;

    protected AuditableEntity() {
    }

    ...

}

The CustomAuditingEntityListener class is defined as

@Configurable
public class CustomAuditingEntityListener extends AuditingEntityListener {

    public CustomAuditingEntityListener() {
        super();
    }

    @Override
    @PrePersist
    public void touchForCreate(Object target) {
        if (//custom logic) {
            super.touchForCreate(target);
        }
    }

    @Override
    @PreUpdate
    public void touchForUpdate(Object target) {
        if (//custom logic) {
            super.touchForUpdate(target);
        }
    }

}

The custom auditing entity listener is called correctly but when the super.touchForCreate(target) or super.touchForUpdate(target) gets called the timestamps are not set because the handler in the AuditingEntityListener class is null.

This is the code of the AuditingEntityListener class:

package org.springframework.data.jpa.domain.support;

@Configurable
public class AuditingEntityListener implements ConfigurableObject {
    private ObjectFactory<AuditingHandler> handler;

    public AuditingEntityListener() {
        JoinPoint var2 = Factory.makeJP(ajc$tjp_1, this, this);
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        if (this != null && this.getClass().isAnnotationPresent(Configurable.class) && AnnotationBeanConfigurerAspect.ajc$if$bb0((Configurable)this.getClass().getAnnotation(Configurable.class))) {
            AnnotationBeanConfigurerAspect.aspectOf().ajc$before$org_springframework_beans_factory_aspectj_AbstractDependencyInjectionAspect$1$e854fa65(this);
        }

        if ((this == null || !this.getClass().isAnnotationPresent(Configurable.class) || !AnnotationBeanConfigurerAspect.ajc$if$bb0((Configurable)this.getClass().getAnnotation(Configurable.class))) && this != null && this.getClass().isAnnotationPresent(Configurable.class) && AbstractDependencyInjectionAspect.ajc$if$6f1(var1)) {
            AnnotationBeanConfigurerAspect.aspectOf().ajc$afterReturning$org_springframework_beans_factory_aspectj_AbstractDependencyInjectionAspect$2$1ea6722c(this);
        }

        if (!AnnotationBeanConfigurerAspect.ajc$if$bb0((Configurable)this.getClass().getAnnotation(Configurable.class)) && AbstractDependencyInjectionAspect.ajc$if$6f1(var2)) {
            AnnotationBeanConfigurerAspect.aspectOf().ajc$afterReturning$org_springframework_beans_factory_aspectj_AbstractDependencyInjectionAspect$2$1ea6722c(this);
        }

    }

    public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
        Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
        this.handler = auditingHandler;
    }

    @PrePersist
    public void touchForCreate(Object target) {
        if (this.handler != null) {
            ((AuditingHandler)this.handler.getObject()).markCreated(target);
        }

    }

    @PreUpdate
    public void touchForUpdate(Object target) {
        if (this.handler != null) {
            ((AuditingHandler)this.handler.getObject()).markModified(target);
        }

    }

    static {
        ajc$preClinit();
    }
}

Can someone explain how I can ensure that the AuditingHandler is not null and is set like it happens for the default AuditingEntityListener class?

1 Answers

With Spring 5.1 there is a rather straightforward way to autowire Spring components in the custom entity listener, so that the AuditingHandler can be used.

1. Declare the customer entity listener as a Spring component. Use constructor injection.

@Component
public class CustomAuditingEntityListener {

    private ObjectFactory<AuditingHandler> handler; 
    
    public CustomAuditingEntityListener(ObjectFactory<AuditingHandler> auditingHandler) {
        this.handler = auditingHandler;
    }

    @Override
    @PrePersist
    public void touchForCreate(Object target) {
        if (//custom logic) {
            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markCreated(target);
            }

        }
    }

    @Override
    @PreUpdate
    public void touchForUpdate(Object target) {
        if (//custom logic) {
            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markModified(target);
            }
        }
    }

}

2. Tell Hibernate to use Spring as the bean provider by setting the appropriate configuration property:

@Configuration
public class JpaConfig extends JpaBaseConfiguration {
    
    @Autowired
    private ConfigurableListableBeanFactory beanFactory;

    protected JpaConfig(DataSource dataSource, JpaProperties properties,
            ObjectProvider<JtaTransactionManager> jtaTransactionManager) {
        super(dataSource, properties, jtaTransactionManager);
    }

    @Override
    protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Override
    protected Map<String, Object> getVendorProperties() {
        Map<String, Object> properties = new HashMap<>();

        // This is the important line
        properties.put(org.hibernate.cfg.AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));

        return properties;
    }
}

--Edit-- Variation of 2. Alternatively the SpringBeanContainer can be configured this way, with the benefit of not losing the Hibernate properties that Spring Boot populates:

@Configuration
public class JpaConfig {

    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
            EntityManagerFactoryBuilder builder, ConfigurableListableBeanFactory beanFactory) {

        return builder.dataSource(dataSource) //
                .packages(BaseEntity.class) //
                .persistenceUnit("myunit") //
                .properties(Map.of(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory))) //
                .build();
    }
}
Related