3

I have several entities that need to be audited. Auditing is implemented by using the following JPA event listener.

public class AuditListener {

    @PrePersist
    @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
    public void setCreatedOn(Auditable auditable) {
        User currentUser = getCurrentUser();
        Long entityId = auditable.getId();
        Audit audit;

        if (isNull(entityId)) {
            audit = getCreatedOnAudit(currentUser);
        } else {
            audit = getUpdatedOnAudit(auditable, currentUser);
        }

        auditable.setAudit(audit);
    }

    @PreUpdate
    @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
    public void setUpdatedOn(Auditable auditable) {
        User currentUser = getCurrentUser();
        auditable.setAudit(getUpdatedOnAudit(auditable, currentUser));
    }

    private Audit getCreatedOnAudit(User currentUser) {
        return Audit.builder()
                .userCreate(currentUser)
                .dateCreate(now())
                .build();
    }

    private Audit getUpdatedOnAudit(Auditable auditable, User currentUser) {
        AuditService auditService = BeanUtils.getBean(AuditService.class);
        Audit audit = auditService.getAudit(auditable.getClass().getName(), auditable.getId());
        audit.setUserUpdate(currentUser);
        audit.setDateUpdate(now());
        return audit;
    }

    private User getCurrentUser() {
        String userName = "admin";
        UserService userService = BeanUtils.getBean(UserService.class);
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (nonNull(auth)) {
            Object principal = auth.getPrincipal();
            if (principal instanceof UserDetails) {
                userName = ((UserDetails)principal).getUsername();
            }
        }
        return userService.findByLogin(userName);
    }
}

In a test environment(unit tests, e2e) for some tests I want to be able to manually set the audit.

Is that possible? I have previously tried to solve this problem with Spring AOP but unfortunately without success. I think, that Spring AOP could allow selectively set the audit by using various combinations in pointcuts:

Is there a way to selectively set auditing by using JPA features?

  • 1
    How about simply mocking/spying on `UserService` (using `@MockBean` or a simple bean definition override for the test context)? You should be able to override the creation/modification time in a similar manner, using `now(clock)` instead of `now()` and injecting the `Clock`, whose provider definition you then override for tests with a mock/fixed instant. BTW you don't need `BeanUtils.getBean(UserService.class)`, Spring supports dependency injection in JPA listeners – crizzis Sep 22 '19 at 08:16

1 Answers1

0

Solved as was suggested by Naros.

The logic for pre-persist and pre-update is moved to a separate injectable component, and AuditListener delegates execution to different implementations of this component, depending of the current active profile.

For Spring Boot 2.1.0:

Common interface:

public interface AuditManager {

    void performPrePersistLogic(Auditable auditable);

    void performPreUpdateLogic(Auditable auditable);
}

Listener for JPA callbacks:

@Component
@RequiredArgsConstructor
public class AuditListener {

    private final AuditManager auditManager;

    @PrePersist
    public void setCreatedOn(Auditable auditable) {
        auditManager.performPrePersistLogic(auditable);
    }

    @PreUpdate
    public void setUpdatedOn(Auditable auditable) {
        auditManager.performPreUpdateLogic(auditable);
    }
}

Implementations of the common interface, for test environment and local environment:

@RequiredArgsConstructor
public class AuditChanger implements AuditManager {

    private final UserService userService;
    private final AuditService auditService;

    @Override
    @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
    public void performPrePersistLogic(Auditable auditable) {
        // logic here
    }

    @Override
    @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
    public void performPreUpdateLogic(Auditable auditable) {
        // logic here        
    }   
}

public class AuditNoChanger implements AuditManager {

// mostly similar

...

Configuration that allows Spring perform injection of different implementation depending of the currently active profile:

@Configuration
public class AuditConfig {

    @Bean
    @Profile("e2e")
    public AuditManager getAuditNoChanger() {
        return new AuditNoChanger();
    }

    @Bean
    @Profile("local")
    public AuditManager getAuditChanger(AuditService auditService, 
            CurrentUserService currentUserService) {
        return new AuditChanger(auditService, currentUserService);
    }
}

Also need to allow beans overriding in *.yml file:

spring:
  main:
    allow-bean-definition-overriding: true