Spring Boot返回JSON时触发无限递归(StackOverflowError)求助
Spring Boot对象转JSON时出现无限递归栈溢出错误排查
问题描述
在Spring Boot项目中将对象转为JSON响应时,遇到如下错误:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)] with root cause
用户认为对象结构不会引发栈溢出(对象结构截图:对象结构截图),并提供了相关代码与配置:
控制器返回代码
@PostMapping(_endpointDirectory + "get/{id}") public ResponseEntity<Response<User>> get(@PathVariable(name = "id") long id) { ResponseEntity<Response<User>> response = UserControllerService.get_instance().get(id); return response; }
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.0.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>laustrup</groupId> <artifactId>BandwichPersistenceDebugging</artifactId> <version>0.0.1</version> <name>BandwichPersistenceDebugging</name> <description>BandwichPersistenceDebugging</description> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- Standard spring --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.0.1</version> </dependency> <!-- Database persistence --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> <scope>runtime</scope> </dependency> <!-- Additional --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-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>
Response类代码
package laustrup.bandwichpersistencedebugging.models; import laustrup.bandwichpersistencedebugging.utilities.Printer; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; /** * This object is meant for containing an element from backend to frontend. * It will also contain a message with a description of status and an error * boolean value, to determine whether the response is an error or not. * @param <E> The element that will be delivered to frontend. */ @NoArgsConstructor @ToString public class Response<E> { /** * The element that is determined of the Response type. * It is the element that will be delivered to frontend. */ @Getter private E _element; /** * An enum that is a status title of the situation. * Is used for determining the message. */ @Getter private StatusType _status; /** * This will be printed to inform the enduser of the situation * or even guide it through it. */ private String _message; /** * Will determine if it is an error or not. * Will be false if noting is set or the status is OK. */ @Getter private boolean _error = false; public Response(E element) { _element = element; _status = StatusType.OK; } public Response(E element, StatusType status) { _element = element; _status = status; _message = set_message(); } /** * Will set the message out of the status before getting it. * Also sets the error. * @return The described message. */ public String get_message() { return set_message(_status); } /** * Sets the message depending on the status. * Also sets the error. * @return The described message. */ public String set_message() { _message = new Status<E>(_status).describeMessageFor(_element); _error = set_error(); return _message; } /** * Sets the message depending on the status. * The status will be set as well. * Also sets the error. * @param status An enum that describes the status of the Response. * @return The described message. */ public String set_message(StatusType status) { _status = status; _error = set_error(); _message = new Status<E>(_status).describeMessageFor(_element, _status); return _message; } /** * Will set the error from the status situation. * Will only become true, if status is OK. * @return The described error. */ public boolean set_error() { _error = _status == StatusType.OK; return _error; } /** * Contains different status titles. * Is meant to describe the message and error of the Response. */ public enum StatusType { OK, UNKNOWN, NO_CONTENT, NOT_ACCEPTABLE, WRONG_PASSWORD, INVALID_PASSWORD_FORMAT } /** * A private class of Response, that will describe the message from the status. * @param <E> The element type of the Response. */ @NoArgsConstructor @ToString private class Status<E> { /** * The current status type of the Response. */ @Getter @Setter private StatusType _type; public Status(StatusType statusType) { _type = statusType; } /** * Describes a message depending on the status type. * @param element This element is being used to have values included in the message. * @param type The type of the Response status. * @return The described message. If switch default is reached or status is OK, * it will return an empty. */ public String describeMessageFor(E element, StatusType type) { _type = type; return _type == StatusType.OK ? new String() : describeMessage(element); } /** * Describes a message depending on the status type. * @param element This element is being used to have values included in the message. * @return The described message. If switch default is reached or status is OK, * it will return an empty. */ public String describeMessageFor(E element) { return _type == StatusType.OK ? new String() : describeMessage(element); } /** * Uses a switch case to describe a message. * @param element This element is being used to have values included in the message. * @return The described message. If switch default is reached or status is OK, * it will return an empty. */ private String describeMessage(E element) { switch (_type) { case NO_CONTENT -> { return "There wasn't found any matching element..."; } case NOT_ACCEPTABLE -> { return "That action is not allowed..."; } case WRONG_PASSWORD -> { return "Password is wrong..."; } case INVALID_PASSWORD_FORMAT -> { return "Password is not allowed... Please check the requirements."; } case UNKNOWN -> { return "Unknown issue for response..."; } default -> { Printer.get_instance().print("No message to write in response..."); return new String(); } } } } }
错误原因分析
无限递归的核心问题出在Response类的内部私有类Status上:
- 非静态内部类的隐式引用:
Status是Response的非静态内部类,每个Status实例都会自动持有外部Response类的引用。当Jackson序列化Response对象时:- 序列化
Response字段时,会调用get_message()方法,该方法会创建Status实例 - Jackson尝试序列化
Status实例时,发现它持有外部Response的引用,于是又回去序列化Response对象 - 如此循环往复,形成无限递归直到栈溢出
- 序列化
- Lombok注解放大问题:
Response和Status都添加了@ToString注解,序列化过程中可能触发toString方法调用,进一步加深递归循环。
解决方法
- 将
Status改为静态内部类:静态内部类不会持有外部类的引用,直接打破循环依赖:@NoArgsConstructor @ToString private static class Status<E> { // 原有代码保持不变 } - 优化消息生成逻辑:避免在
get_message()中每次创建Status实例,可提前初始化或缓存消息内容,减少不必要的对象创建。 - 可选:添加Jackson注解忽略循环引用:若不想修改类结构,可在
Status类的外部引用字段上添加@JsonIgnore注解,但这种方法不如静态内部类彻底。
内容的提问来源于stack exchange,提问作者Laustrup
相关产品推荐
相关产品推荐

