我在创建表时发现了一个与Spring Boot和JPA相关的bug。当有一个序列化实体与一个内置序列化类相关联,而该内置序列化类又拥有另一个内置序列化类,并且最终该类也有一个序列化实体时,Spring Boot 无法启动服务器,JPA 也不会创建数据库表。相反,它会卡在“HikariPool-1:开始完成”。
图片在这里:(https://github.com/spring-projects/spring-boot/assets/97984278/67b8353c-d805-4e94-ab1b-d71ab0b45456)
我与 Spring Boot 团队的一名成员交谈(https://github.com/spring-projects/spring-boot/issues/38701),他告诉我这可能是与 spring data jpa、hibernate 相关的错误或 hikari,所以他让我在这里提出一个问题。
问题似乎在于实体与嵌入类和实体的这种串行关联。如果您想运行并检查问题以验证这是否确实是 Spring Boot(因此无法启动)或 JPA 或 Hikari 的问题,我将不胜感激。
示例有点长,但这是为了确保您按照我测试的方式进行测试。 (我尝试减少很多以使其更简单)。
<?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>
# 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
version: '3.1'
services:
db:
image: mysql:8.0.32
restart: always
environment:
MYSQL_DATABASE: bug_db
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
@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;
}
}
public interface AccountRepository extends JpaRepository<Account, Long>{
Optional<Account> findByUsername(String username);
}
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;
}
}
@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
}
@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
}
@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
}
public interface AccountSecurityQuestionRepository extends JpaRepository<AccountSecurityQuestion, Long> {
}
@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
}
public interface SecurityQuestionRepository extends JpaRepository<SecurityQuestion, Long> {
}
我尝试过各种解决方案,例如清理 Maven 存储库、本地运行、使用 Docker、删除并重新创建数据库,甚至重命名数据库。奇怪的是,只有当我删除一个嵌入类与另一个嵌入类的关联时,它才起作用。`
我通过删除序列化内置类与序列化实体的关联解决了这个问题,并且它起作用了。
显然,JPA 或 Hibernate 在遇到具有序列化嵌入类的序列化实体时,在启动表创建过程时会遇到困难,而这些嵌入类又涉及序列化实体。结果Spring Boot卡住了,无法启动