Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

StackOverflowException in spring-data-jpa app with spring-security AuditorAware

I have a really nasty StackOverflowException in my spring backend, that I need help with. This is not going to be solved easily. I really hope to find some help here.

Most parts of my backend work. I can query my REST interface for models, they are nicely returned by spring-hateoas, GET, PUT and POST operations work. But one exception: When I try to update an existing DelegationModel, then I run into an endless StackOverflowException.

Here is my DelegetionModel.java class. Please mark, that delegation model actually doesn't have any property annotated with @CreatedBy!

@Entity
@Data
@NoArgsConstructor
@RequiredArgsConstructor(suppressConstructorProperties = true)  //BUGFIX:  https://jira.spring.io/browse/DATAREST-884
@EntityListeners(AuditingEntityListener.class)  // this is necessary so that UpdatedAt and CreatedAt are handled.
@Table(name = "delegations")
public class DelegationModel {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  public Long id;

  /** Area that this delegation is in */
  @NonNull
  @NotNull
  @ManyToOne
  public AreaModel area;

  /** reference to delegee that delegated his vote */
  @NonNull
  @NotNull
  @ManyToOne
  public UserModel fromUser;

  /** reference to proxy that receives the delegation */
  @NonNull
  @NotNull
  @ManyToOne
  public UserModel toProxy;

  @CreatedDate
  @NotNull
  public Date createdAt = new Date();

  @LastModifiedDate
  @NotNull
  public Date updatedAt = new Date();

}

As described in the Spring-data-jpa doc I implemented the necessary AuditorAware interface, which loads the UserModel from the SQL DB. I would have expected that this AuditorAware interface is only called for models that have a field annotated with @CreatedBy.

@Component
public class LiquidoAuditorAware implements AuditorAware<UserModel> {
  Logger log = LoggerFactory.getLogger(this.getClass());  // Simple Logging Facade 4 Java

  @Autowired
  UserRepo userRepo;    

  @Override
  public UserModel getCurrentAuditor() {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      if (authentication == null || !authentication.isAuthenticated()) {
        log.warn("Cannot getCurrentAuditor. No one is currently authenticated");
        return null;
      }
      User principal = (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
      UserModel currentlyLoggedInUser = userRepo.findByEmail(principal.getUsername());   // <<<<======= (!)
      return currentlyLoggedInUser;
    } catch (Exception e) {
      log.error("Cannot getCurrentAuditor: "+e);
      return null;
    }
  }
}

Now I update a DelegationModel in my UserRestController. The functional "Scrum User Story" here is:

As a user I want to be able to store a delegation so that I can forward my right to vote to my proxy.

@RestController
@RequestMapping("/liquido/v2/users")
public class UserRestController {

  [...]

  @RequestMapping(value = "/saveProxy", method = PUT, consumes="application/json")
  @ResponseStatus(HttpStatus.CREATED)
  public @ResponseBody String saveProxy(
                      @RequestBody Resource<DelegationModel> delegationResource,
                      //PersistentEntityResourceAssembler resourceAssembler,
                      Principal principal)            throws BindException
  {
    [...]
    DelegationModel result = delegationRepo.save(existingDelegation);
    [...]
  }

  [...]         

}

For some reason, that I cannot see, this actualy calls the AuditorAware implementation above. The problem is now, that my LqiuidoAuditorAware implementation is called again and again in and endless loop. It seems that the query for the UserModel inside LiquidoAuditorAware.java calls the LiquidoAuditorAware again. (Which is unusual, because that is only a read operation from the DB.)

Here is the full ThreadDump as a Gist

All the code can by found in this github repo

I'd really apriciate any help here. I am searching in the dark :-)

like image 593
Robert Avatar asked Dec 23 '22 20:12

Robert


1 Answers

The reason for the behavior you see is that the AuditorAware implementation is called from within a JPA @PrePersist/@PreUpdate callback. You now issue a query by calling findByEmail(…), which triggers the dirty-detection again, which in turn causes the flushing to be triggered and thus the invocation of the callbacks.

The recommended workaround is to keep an instance of the UserModel inside the Spring Security User implementation (by looking it up when the UserDetailsService looks up the instance on authentication), so that you don't need an extra database query.

Another (less recommended) workaround could be to inject an EntityManager into the AuditorAware implementation, call setFlushMode(FlushModeType.COMMIT) before the query execution and reset it to FlushModeType.AUTO after that, so that the flush will not be triggered for the query execution.

like image 60
Oliver Drotbohm Avatar answered Dec 28 '22 07:12

Oliver Drotbohm