JPA 一对多双向堆栈溢出问题

Posted

技术标签:

【中文标题】JPA 一对多双向堆栈溢出问题【英文标题】:JPA One To Many Bi-Directional Stack Overflow Issue 【发布时间】:2020-06-19 18:26:15 【问题描述】:

我正在使用 Java 1.8、Spring Boot、REST、JPA 创建一个 Spring Boot REST 微服务 API,它的实体关系具有以下基数:

Owner can have many Cars.
Cars only have one Owner.

能够通过我的 REST Web 服务创建和查看所有者。

每次我尝试创建具有关联所有者的汽车时,它都会正确填充数据库的行,但似乎它会无限循环导致堆栈溢出错误(见下文)。


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>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.myapi</groupId>
    <artifactId>car-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>car-api</name>
    <description>Car REST API</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

src/main/resources/applications.properties:

server.servlet.context-path=/car-api
server.port=8080
server.error.whitelabel.enabled=false

# Database specific
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/car_db?useSSL=false
spring.datasource.ownername=root
spring.datasource.password=

所有者实体:

@Entity
@Table(name = "owner")
public class Owner 

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

    @NotNull
    private String name;


    @OneToMany(cascade = CascadeType.ALL,
                fetch = FetchType.LAZY,
                mappedBy = "owner")
    private List<Car> cars = new ArrayList<>();

    public Owner() 
    

    // Getter & Setters omitted for brevity.

汽车实体:

@Entity
@Table(name="car")
public class Car 

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

    String make;
    String model;
    String year;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id", nullable = false)
    private Owner owner;

    // Getter & Setters omitted for brevity.


所有者存储库:

@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> 


汽车存储库:

@Repository
public interface CarRepository extends JpaRepository<Car, Long> 


所有者服务:

public interface OwnerService 

    boolean createOwner(Owner owner);

    Owner getOwnerByOwnerId(Long ownerId);

    List<Owner> getAllOwners();



OwnerServiceImpl:

@Service
public class OwnerServiceImpl implements OwnerService 


    @Autowired
    OwnerRepository ownerRepository;

    @Autowired
    CarRepository carRepository;

    @Override
    public List<Owner> getAllOwners() 
        return ownerRepository.findAll();
    

    @Override
    public boolean createOwner(Owner owner) 
        boolean created = false;
        if (owner != null) 
            ownerRepository.save(owner);
            created = true;
        
        return created;
    

    @Override
    public Owner getOwnerByOwnerId(Long ownerId) 
        Optional<Owner> owner = null;
        if (ownerRepository.existsById(ownerId)) 
            owner = ownerRepository.findById(ownerId);
        
        return owner.get();
    


汽车服务:

public interface CarService 

    boolean createCar(Long ownerId, Car car);


CarServiceImpl:

@Service
public class CarServiceImpl implements CarService 

    @Autowired
    OwnerRepository ownerRepository;

    @Autowired
    CarRepository carRepository;

    @Override
    public boolean createCar(Long ownerId, Car car) 
        boolean created = false;
        if (ownerRepository.existsById(ownerId)) 
            Optional<Owner> owner = ownerRepository.findById(ownerId);
            if (owner != null) 
                List<Car> cars = owner.get().getCars();
                cars.add(car);
                owner.get().setCars(cars);
                car.setOwner(owner.get());
                carRepository.save(car);
                created = true;
            
        
        return created;
    




所有者控制器:

@RestController
public class OwnerController 


    private HttpHeaders headers = null;

    @Autowired
    OwnerService ownerService;

    public OwnerController() 
        headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
    

    @RequestMapping(value =  "/owners" , method = RequestMethod.POST, produces = "APPLICATION/JSON")
    public ResponseEntity<Object> createOwner(@Valid @RequestBody Owner owner) 
        boolean isCreated = ownerService.createOwner(owner);
        if (isCreated) 
            return new ResponseEntity<Object>(headers, HttpStatus.OK);
        
        else 
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        
    


    @RequestMapping(value =  "/owners" , method = RequestMethod.GET, produces = "APPLICATION/JSON")
    public ResponseEntity<Object> getAllOwners() 
        List<Owner> owners = ownerService.getAllOwners();

        if (owners.isEmpty()) 
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        
        return new ResponseEntity<Object>(owners, headers, HttpStatus.OK);
    


    @RequestMapping(value =  "/owners/ownerId" , method = RequestMethod.GET, produces = "APPLICATION/JSON")
    public ResponseEntity<Object> getOwnerByOwnerId(@PathVariable Long ownerId) 
        if (null == ownerId || "".equals(ownerId)) 
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        
        Owner owner = ownerService.getOwnerByOwnerId(ownerId);
        return new ResponseEntity<Object>(owner, headers, HttpStatus.OK);
    



汽车控制器:

@RestController
public class CarController 

    private HttpHeaders headers = null;

    @Autowired
    CarService carService;

    public VehicleController() 
        headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
    

    @RequestMapping(value =  "/cars/ownerId" , method = RequestMethod.POST, produces = "APPLICATION/JSON")
    public ResponseEntity<Object> createVehicleBasedOnOwnerId(@Valid @RequestBody Car car, Long ownerId) 
        boolean isCreated = carService.createCar(ownerId, vehicle);
        if (isCreated) 
            return new ResponseEntity<Object>(headers, HttpStatus.OK);
        
        else 
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        
    


然而,我可以创建新所有者(并在数据库中查看它们,还可以通过 curl / Postman 调用 getAllOwners 来查看它们),方法是将其作为请求正文传递:


    "owner": "John Doe"


数据库内部car_db.owner

-------------------------------------
|id | name                          |
-------------------------------------  
|1  | John Doe                      |
-------------------------------------  

当我尝试使用此 REST 调用 /cars/ownerId 为车主创建全新汽车时出现问题:

POST http://localhost:8080/car-api/cars/1

请求正文如下:


    "make": "Honda",
    "model": "Accord"
    "year": 2020

它将它正确地插入到 MySQL 数据库的 car_db.car 表中,如下所示:

---------------------------------------
|id | make  | model  | year | owner_id|
---------------------------------------  
|1  | Honda | Accord | 2020 |     1   |
---------------------------------------  

我在CarServiceImpl.createCar() method 内部做了什么导致双向关系中断?

创建一个堆栈溢出:空执行:

-03-08 01:43:20,106 ERROR org.apache.juli.logging.DirectJDKLog [http-nio-8080-exec-1] Servlet.service() for servlet [dispatcherServlet] in context with path [/car-api] threw exception [Handler dispatch failed; nested exception is java.lang.***Error] with root cause
java.lang.***Error: null
    at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:512)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:141)
    at com.myapi.model.Car.toString(Car.java:87)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
    at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at com.myapi.model.Owner.toString(Owner.java:105)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at com.myapi.model.Car.toString(Car.java:87)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
    at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at com.myapi.model.Owner.toString(Owner.java:105)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at com.myapi.model.Car.toString(Car.java:87)
    at java.base/java.lang.String.valueOf(String.java:2788)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
    at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
    at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)

奇怪的是,尽管这是每次我创建新车时都会出现的堆栈跟踪,但数据库中的一切都很好(插入在 car 表中,行内有正确的 ownerId)而且我当我执行以下任一 GET 请求时,能够查看 JSON 响应负载:

GET http://localhost:8080/owners/1

产量:


    "name": "John Doe",
    "cars": [
       
           "make": "Honda",
           "model": "Accord",
           "year": 2020
       
    ]


GET http://localhost:8080/owners

产量:

[
   
      "name": "John Doe",
      "cars": [
         
             "make": "Honda",
             "model": "Accord",
             "year": 2020
         
      ]
   
]

尽管所有 GET 和数据库插入都正常工作,为什么我会收到此 Stack Overflow 错误?

【问题讨论】:

错误是因为车主有车。每辆车都有主人。再说那个主人有车。所以它会进入递归并给你***错误。在您的 Car 实体中,使用 JsonIgnore 注释标记 ManyToOne 关系。 我可以在 createCar 方法中看到的另一个合乎逻辑的问题是:您正在检查所有者是否存在。然后制作新的汽车集合并将新车设置为这个集合。因此,所有以前的汽车都将被清除。您可能想将新车添加到现有的汽车收藏中。 也许this article 会有所帮助。 @AmitB10 - 非常感谢,但 Car 实体中的 @JsonIgnore 注释仍然会导致 *** 跟踪。这现在有效(我可以看到 cars JSON 数组被填充在所有者中,通过我的 GET 查看 JSON 响应),但错误仍然出现在日志中。设置实体时我可能做错了什么? 有谁知道为什么这不起作用? 【参考方案1】:

JPA 与此错误无关。查看堆栈跟踪 - 您的 Car#toString() 打印出它的 Owner。而Owner#toString() 打印它的汽车集合。

因此,当您的代码中的某些内容在其中一个对象上调用 toString() 时 - 它会导致无限的调用链,该调用链仅在达到线程堆栈的最大深度时才会结束,从而导致 ***Error。

通常在toString() 中,我们只想打印当前类中的原语/ValueObjects。如果我们也开始打印关联实体 - 这将导致惰性字段被初始化。

【讨论】:

是的,这是因为 IntelliJ IDEA 使用 "cars=" + cars 自动生成了 toString() 方法,一旦我从 toString() 方法中删除了此字段,一切正常。【参考方案2】:

在我的 Owner 类中,这是 IntelliJ IDEA 为我的 toString() 方法生成的内容:

public String toString() 
  return "Owner" +
              "id=" + id +
              "name=" + name + 
              ", cars=" + cars +
        '';

当我取出包含", cars=" + cars +的那行时,变成了:

public String toString() 
  return "Owner" +
              "id=" + id +
              "name=" + name +
        '';


成功了!

【讨论】:

以上是关于JPA 一对多双向堆栈溢出问题的主要内容,如果未能解决你的问题,请参考以下文章

在堆栈大小和可能的溢出方面是不是可能有太多方法?

函数 堆栈溢出

怎么防止堆栈溢出

c++堆栈溢出问题?

怎么解决 LINUX 堆栈溢出内存的问题

缓冲区溢出是与程序堆栈相关的唯一可能的错误吗?