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

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上:

  1. 非静态内部类的隐式引用:Status是Response的非静态内部类,每个Status实例都会自动持有外部Response类的引用。当Jackson序列化Response对象时:
    • 序列化Response字段时,会调用get_message()方法,该方法会创建Status实例
    • Jackson尝试序列化Status实例时,发现它持有外部Response的引用,于是又回去序列化Response对象
    • 如此循环往复,形成无限递归直到栈溢出
  2. Lombok注解放大问题:Response和Status都添加了@ToString注解,序列化过程中可能触发toString方法调用,进一步加深递归循环。

解决方法

  • 将Status改为静态内部类:静态内部类不会持有外部类的引用,直接打破循环依赖:
    @NoArgsConstructor @ToString
    private static class Status<E> {
        // 原有代码保持不变
    }
    
  • 优化消息生成逻辑:避免在get_message()中每次创建Status实例,可提前初始化或缓存消息内容,减少不必要的对象创建。
  • 可选:添加Jackson注解忽略循环引用:若不想修改类结构,可在Status类的外部引用字段上添加@JsonIgnore注解,但这种方法不如静态内部类彻底。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 15:55:15