Search code examples
spring-boothibernatespring-data-jpahikaricp

Bug in spring boot initialization, when serialized entity has serialized built-in classes. HikariPool-1: Start Completed


I found a bug related to Spring Boot and JPA when creating tables. When there is a serialized entity associated with a built-in serialized class, which in turn has another built-in serialized class, and finally, this class has a serialized entity, Spring Boot fails to start the server, and JPA doesn't create the database tables. Instead, it gets stuck at `HikariPool-1: Start Completed.

Image here: (https://github.com/spring-projects/spring-boot/assets/97984278/67b8353c-d805-4e94-ab1b-d71ab0b45456)

I talked to a member of the spring boot team (https://github.com/spring-projects/spring-boot/issues/38701) and he told me that maybe it's a bug related to spring data jpa, hibernate or hikari and so he asked me to create a question here.

The problem seems to be with this serial association of entities with embedded classes and entities. If you would like to run and check the issue to verify whether this is indeed a problem with either Spring Boot (which fails to start because of this) or JPA or Hikari, I would appreciate it.

  1. Spring version: 3.2.0 or less;
  2. MySQL 8.0.32 or more;
  3. Java 18 or more;

The example is a little long, but it is to ensure that you will test it the same way I tested it. (I tried to reduce it a lot to make it simpler).

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.2.0</version>
            <relativePath /> <!-- lookup parent from repository -->
        </parent>
    
        <groupId>com.test</groupId>
        <artifactId>test-api</artifactId>
        <version>1.0</version>
        <name>test-backend</name>
        <description>Test</description>
    
        <properties>
            <java.version>18</java.version>
        </properties>
    
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
    
            <dependency>
                <groupId>com.mysql</groupId>
                <artifactId>mysql-connector-j</artifactId>
                <version>8.0.32</version>
                </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
 
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-test</artifactId>
                <scope>test</scope>
            </dependency>
  
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>

application.properties:

# MySQL database configuration
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/bug_db?allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true&useTimezone=true&serverTimezone=UTC&useSSL=false

spring.datasource.username=root
spring.datasource.password=root
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql=true

docker-compose.yml

version: '3.1'

services:
  db:
    image: mysql:8.0.32
    restart: always
    environment:
      MYSQL_DATABASE: bug_db
      MYSQL_ROOT_PASSWORD: root
    ports:
      - "3306:3306"

Account.java

@Entity
@Table(name = "accounts", uniqueConstraints = { @UniqueConstraint(columnNames = { "username" }) })
public final class Account implements UserDetails {

    /** The serialVersionUID. */
    private static final long serialVersionUID = 221625420706334299L;

    /** The unique identifier for the account. */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /** The user name for authentication. */
    @Column(nullable = false, unique = true)
    @NotBlank(message = "The username cannot be blank")
    private String username;

    /**
     * The password for authentication. */
    @Column(name = "password", nullable = false)
    @JsonIgnore
    @NotBlank(message = "The password cannot be blank")
    private String password;

    /** The information of the account holder. */
    @Embedded
    @Valid
    private AccountHolderInformation holderInformation;

    /** Indicates whether it is account non expired. False by default. */
    @Column(columnDefinition = "boolean default false", nullable = false)
    private boolean isAccountNonExpired;

    /** Indicates whether it is account non locked. False by default. */
    @Column(columnDefinition = "boolean default false", nullable = false)
    private boolean isAccountNonLocked;

    /** Indicates whether it is enabled. False by default. */
    @Column(columnDefinition = "boolean default false", nullable = false)
    private boolean isEnabled;

    /** The role of the account in the system. */
    @Column(name = "role", nullable = false)
    @Enumerated(EnumType.STRING)
    private Role role;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public AccountHolderInformation getHolderInformation() {
        return holderInformation;
    }

    public void setHolderInformation(AccountHolderInformation holderInformation) {
        this.holderInformation = holderInformation;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (this.role == Role.ROLE_ADMIN) {
            return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));
        } else {
            return List.of(new SimpleGrantedAuthority("ROLE_USER"));
        }
    }

    @Override
    public boolean isAccountNonExpired() {
        return isAccountNonExpired;
    }

    public void setAccountNonExpired(boolean isAccountNonExpired) {
        this.isAccountNonExpired = isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return isAccountNonLocked;
    }

    public void setAccountNonLocked(boolean isAccountNonLocked) {
        this.isAccountNonLocked = isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return isEnabled;
    }

    public void setEnabled(boolean isEnabled) {
        this.isEnabled = isEnabled;
    }

}

AccountRepository.java

public interface AccountRepository extends JpaRepository<Account, Long>{
      Optional<Account> findByUsername(String username);
}

Role.java

public enum Role {
    ROLE_ADMIN("admin"),
    ROLE_USER("user");

    private final String key;

    private Role(String key) {
        this.key = key;
    }

    public String getRole() {
        return key;
    }

}

AccountHolderInformation.java

@Embeddable
public final class AccountHolderInformation implements Serializable {

    /**
     * The serialVersionUID.
     */
    private static final long serialVersionUID = 4089056018657825205L;

    /** The first name of the account holder. */
    @Column(nullable = false)
    @NotBlank(message = "The name cannot be blank")
    private String name;

    /** (Optional) The last name or surname of the account holder. */
    @Column
    @Length
    private String surname;

    /** The security information of the account holder. */  
    @Embedded
    @Valid
    private AccountHolderSecurityInformation securityInformation;

        //getters and setters

}

AccountHolderSecurityInformation.java

@Embeddable
public final class AccountHolderSecurityInformation implements Serializable {

    /**
     * The serialVersionUID.
     */
    private static final long serialVersionUID = 3585858950258340583L;

    /** The first security question to confirm the identity of an account holder. */
    @JsonIgnore
    @OneToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, mappedBy = "account")
    private AccountSecurityQuestion securityQuestionOne;

     //getters and setters
}

AccountSecurityQuestion.java

@Entity
@Table(name = "accounts_security_questions")
public final class AccountSecurityQuestion implements Serializable {

    /** The serialVersionUID. */
    private static final long serialVersionUID = -8188615055579913942L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JoinColumn(name = "account_id", nullable = false)
    @JsonIgnore
    @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, optional = false) 
    private Account account;

    @ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, optional = false) 
    @JoinColumn(name = "security_question_id", nullable = false)
    private SecurityQuestion securityQuestion;

    @JsonIgnore
    @NotBlank(message = "The answer cannot be blank")
    private String answer;

        //getters and setters
}

AccountSecurityQuestionRepository.java

public interface AccountSecurityQuestionRepository extends JpaRepository<AccountSecurityQuestion, Long> {
}

SecurityQuestion.java

@Entity
@Table(name = "security_questions")
public final class SecurityQuestion implements Serializable {

    /** The serialVersionUID. */
    private static final long serialVersionUID = -6788149456783476682L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @NotBlank(message = "The question cannot be blank")
    private String question;

     //getters and setters

}

SecurityQuestionRepository.java

public interface SecurityQuestionRepository extends JpaRepository<SecurityQuestion, Long> {
}

I've tried various solutions, such as cleaning the Maven repository, running locally, using Docker, deleting and recreating the database, and even renaming the database. Strangely, it worked only when I removed the association from one embedded class to another.`


Solution

  • I solved the problem by removing the associations of the serialized built-in classes with the serialized entity and it worked.

    Apparently, JPA or Hibernate encounters difficulties starting the table creation process when it encounters a serialized entity with serialized embedded classes, and these embedded classes, in turn, involve serialized entities. As a result, Spring Boot gets stuck and fails to start