Am using Java 1.8, Spring Boot, REST, JPA to create a Spring Boot REST Microservice API which has the following cardinality with its Entity Relationship:
Owner can have many Cars. Cars only have one Owner.
Am able to create and view Owners through my REST Web Service.
Everytime I try to create a Car with an associated Owner, it populates the database’s row correctly, but seems like it keeps recurses infinitely causes a Stack Overflow error (see below).
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=
Owner entity:
@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. }
Car entity:
@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. }
OwnerRepository:
@Repository public interface OwnerRepository extends JpaRepository<Owner, Long> { }
CarRepository:
@Repository public interface CarRepository extends JpaRepository<Car, Long> { }
OwnerService:
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(); } }
CarService:
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; } }
OwnerController:
@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); } }
CarController:
@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); } }
Whereas, I am able to create new owners (and view them in the database & also view them by calling getAllOwners via curl / Postman), by passing this as the request body:
{ "owner": "John Doe" }
Inside the database car_db.owner
:
------------------------------------- |id | name | ------------------------------------- |1 | John Doe | -------------------------------------
There is something wrong when I try to create a brand new car for an owner, by using this REST call /cars/{ownerId}
:
POST http://localhost:8080/car-api/cars/1
with the following request body:
{ "make": "Honda", "model": "Accord" "year": 2020 }
It inserts it properly inside MySQL database’s car_db.car
table like this:
--------------------------------------- |id | make | model | year | owner_id| --------------------------------------- |1 | Honda | Accord | 2020 | 1 | ---------------------------------------
Is there something I am doing inside CarServiceImpl.createCar() method
its causing bi-directional relationship to break?
Creates a Stack Over Flow : null execption:
-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.StackOverflowError] with root cause java.lang.StackOverflowError: 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)
The wierd this is that despite this is the Stack Trace that comes every single time I create a new Car, everything is fine in the database (the insert is there inside the car table with the correct ownerId inside the row) and I am able to view the JSON response payload when I do either of these GET requests:
GET http://localhost:8080/owners/1
Yields:
{ "name": "John Doe", "cars": [ { "make": "Honda", "model": "Accord", "year": 2020 } ] }
GET http://localhost:8080/owners
Yields:
[ { "name": "John Doe", "cars": [ { "make": "Honda", "model": "Accord", "year": 2020 } ] } ]
Why am I getting this Stack Overflow Error despite all the GETs and the database inserts are working?
Answer
JPA has nothing to do with this error. Look at the stack trace – your Car#toString()
prints its Owner
. While Owner#toString()
prints its collection of Cars.
So when something in your code calls toString()
on one of these objects – it causes an infinite chain of invocations which ends only when the max depth of the thread’s stack is achieved causing StackOverflowError.
Usually in toString()
we only want to print primitives/ValueObjects from current class. If we start printing associated Entities as well – this will cause lazy fields to be initialized.