StackOverflowError with Hibernate when trying to implement JWT Security

Viewed 94

I am new to this topic and I keep getting "java.lang.StackOverflowError: null" When I try and authorize. I get Internal server error in postman and in the console I get the stackoveflowerror. This is my AuthenticationController:


   @PostMapping("/signup")
   public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
       if (userService.existsByEmail(signUpRequest.getEmail())) {
           throw new BadRequestException("email already exists");
       }
       User user = UserMapper.INSTANCE.registerRequestoUser(signUpRequest);
       user.setPassword(passwordEncoder.encode(user.getPassword()));
       Optional<Role> optionalRole = roleService.getByName("user");

       if (optionalRole.isPresent()) {
           Role userRole = optionalRole.get();
           user.addRole(userRole);
           Optional<User> optionalUser = userService.create(user);
           if (optionalUser.isPresent()) {
               User result = optionalUser.get();
               URI location = ServletUriComponentsBuilder
                       .fromCurrentContextPath().path("/api/v1/users/email/{email}")
                       .buildAndExpand(result.getEmail()).toUri();

               return ResponseEntity.created(location).body("User registered successfully");
           }

       }
       return (ResponseEntity<?>) ResponseEntity.badRequest();
   }

My SignUpRequest class:


public class SignUpRequest {
   @NotBlank
   private String firstName;

   @NotBlank
   private String lastName;

   @NotBlank
   @Email
   private String email;

   @NotBlank
   @Size(min = 8, max = 20)
   private String password;

   private Set<String> roles;

My user entity and my role entity:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User extends BaseEntity {

    private String firstName;

    private String lastName;

    @Email(message = "Email should be valid")
    private String email;

    @Size(min = 3, max = 100, message
            = "password must be between 3 and 50 characters")
    private String password;

    @OneToMany(mappedBy = "user",
            cascade = {CascadeType.PERSIST, CascadeType.MERGE,
                    CascadeType.DETACH, CascadeType.REFRESH})
    private Set<Appointment> appointments;


    @ManyToMany(targetEntity = Role.class,
            cascade = {CascadeType.ALL},
            fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_role",
            joinColumns = {@JoinColumn(name = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "role_id")}
    )
    private Set<Role> roles;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role extends BaseEntity {

    @NotNull
    private String name;

    @ManyToMany(mappedBy = "roles")
    private Set<User> users;

    public String  getName() {
        return name;
    }

And here is my postman request

{
        "firstName":"name",
        "lastName":"name",
        "email": "an@gmail.com",
        "password": "thepassword123",
        "roles": ["user"]

}

Here is a piece from the stack trace:

at at com.project.rushhour.entity.User.hashCode(User.java:15) ~[classes/:na]
...
at at com.project.rushhour.entity.Role.hashCode(Role.java:15) ~[classes/:na]
...


at com.project.rushhour.entity.User.hashCode(User.java:15) ~[classes/:na]
...
at com.project.rushhour.entity.Role.hashCode(Role.java:15) ~[classes/:na]
...


at com.project.rushhour.entity.User.hashCode(User.java:15) ~[classes/:na]
...
at com.project.rushhour.entity.Role.hashCode(Role.java:15) ~[classes/:na]
...


I think the error is somewhere around when the program is trying to parse the roles, but not sure exactly what is the problem. The things for jwt are set up correctly, it's just the logic that is wrong

1 Answers

The reason of the problem is, that you have relations from User to Role and from Role to user. But you have not explicitly defined hashCode method. You use @Data annotation. By default Lombok generates hashCode method that includes all the properties of the entity.

hashCode is normally calculated as follows:

h = hashCode(property1);
h = h*31 + hashCode(property2);
h = h*31 + hashCode(property3);
h = h*31 + hashCode(property4);
h = h*31 + hashCode(property5);
...

When hashCode is called on User, if calculates hash codes of properties firstName, lastName, etc. Then also add the hash code of the property roles. Calculation of hash code of roles includes calculation of hash code of every Role element.

But calculation of hash code Role includes calculation of hash code of the property users, which calls hashCode() on every User element. And as we see above, hash code of User will again coll hash code of Role. etc. Until the stack capacity is used. Then you get StackOverflowError.

How can you avoid it? Exclude recursion from hashCode() method. Either implement hashCode() manually. Or tell Lombok to exclude relation based properties from hashCode().

I would prefer that Lombok still generates everything for me. So I would tell Lombok to ignore property roles in the User entity and property users in the Role entity when generating hashCode() and equals(). So your code would look as follows:

public class User extends BaseEntity {

    private String firstName;

    private String lastName;

    ...
    @EqualsAndHashCode.Exclude
    private Set<Role> roles;

    ...
}

public class Role extends BaseEntity {

    ...    
    private String name;

    ...
    @EqualsAndHashCode.Exclude
    private Set<User> users;

    ...

}

Why is annotation called EqualsAndHashCode? Because hashCode() and equals() should always be changed synchronously to fulfill so called hashCode - equals contract.

Related