You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot初始化Bug:序列化实体关联内嵌类致启动停滞

Spring Boot + JPA 数据库表创建死锁问题:嵌套内嵌类关联实体导致启动失败

发现Spring Boot与JPA在创建数据库表时存在异常:当实体关联序列化内嵌类,该内嵌类又关联另一个序列化内嵌类,最终这个内嵌类再关联回实体时,Spring Boot服务器启动失败,JPA无法创建数据库表,进程卡在HikariPool-1: Start Completed。

已与Spring Boot团队成员沟通,对方判断该问题可能涉及Spring Data JPA、Hibernate或Hikari,建议进一步排查。

问题似乎出在实体与多层嵌套内嵌类之间的序列化关联结构上,可通过以下复现代码验证问题归属。

环境版本要求

  • Spring版本:3.2.0及以下
  • MySQL版本:8.0.32及以上
  • Java版本:18及以上

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> {
}

已尝试多种排查手段:清理Maven仓库、本地直接运行、Docker容器部署、删除重建数据库、重命名数据库等。仅在移除某一层内嵌类之间的关联后,启动失败问题才得以解决。

内容的提问来源于stack exchange,提问作者AlefMemTav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 14:22:01