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

Spring Boot2.7升级3.1.11后FetchType.LAZY引发栈溢出问题排查

升级Spring Boot 3.1.11 + Java 21后延迟加载导致栈溢出问题排查

将Spring Boot从2.7版本升级至3.1.11、Java从11升级至21后,仅调整了JPA语法和包路径,未修改其他业务代码,但应用出现延迟加载触发的栈溢出错误,该问题在升级前未出现。移除fetch = FetchType.LAZY可临时解决,但全局移除会引发内存溢出,需排查新版本变更点。

定位到的核心代码

public interface AuditEntity {
    User getLastModifiedBy();

    @JsonGetter("lastModifiedBy")
    default String getModifyUserName() {
        if (getLastModifiedBy() != null) 
            return getLastModifiedBy().getName();
        return null;
    }
}

public abstract class AbstractAuditable implements AuditEntity {
    @LastModifiedBy
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="LASTMODIFIEDBY_ID")
    protected User lastModifiedBy;

    @JsonIgnore
    public User getLastModifiedBy() {
        return lastModifiedBy;
    }
}

public class User extends AbstractAuditable {
    @NotNull 
    @Size(max=50)
    private String fname;
    public String getFname() {
        return fname;
    }
}

触发错误的示例代码

User user = service.getUser();
User user2 = user.getLastModifiedBy(); // 执行该行时出现栈溢出
user2.getFname();

使用的依赖配置

plugins {
    id 'org.springframework.boot' version '3.1.11'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java-library'
}
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-cache'
api 'org.springframework:spring-web'
api 'org.springframework.security:spring-security-core'
api 'com.oracle.database.jdbc:ojdbc11:21.13.0.0'
api 'jakarta.validation:jakarta.validation-api'
api 'jakarta.activation:jakarta.activation-api'
api 'com.jcraft:jsch:0.1.55'
api 'org.apache.httpcomponents.client5:httpclient5:5.2'

补充调试信息

  • 调试时查看User对象属性也会触发栈溢出,推测调试器访问属性时调用了getLastModifiedBy方法。
  • 开启SQL日志后发现,程序执行相关代码时会反复查询User表。

报错堆栈信息

jakarta.servlet.ServletException: Handler dispatch failed: java.lang.StackOverflowError
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1096)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at myservice.doFilter(ServletRequestFilter.java:44)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108)
    at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:117)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:83)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter.doFilterInternal(Saml2WebSsoAuthenticationRequestFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
    at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
    at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter.doFilterInternal(Saml2MetadataFilter.java:98)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:142)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82)

Caused by: java.lang.StackOverflowError: null
    at oracle.jdbc.driver.T4CTTIMsg.marshalTTCcode(T4CTTIMsg.java:311)
    at oracle.jdbc.driver.T4CTTIrxd.marshalBindDBA(T4CTTIrxd.java:223)
    at oracle.jdbc.driver.T4CTTIrxd.marshal(T4CTTIrxd.java:172)
    at oracle.jdbc.driver.T4C8Oall.marshalBinds(T4C8Oall.java:2246)
    at oracle.jdbc.driver.T4C8Oall.marshal(T4C8Oall.java:1406)
    at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:292)
    at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:511)
    at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:162)
Caused by: java.lang.StackOverflowError: null
    at oracle.jdbc.driver.T4CPreparedStatement.executeForDescribe(T4CPreparedStatement.java:1009)
    at oracle.jdbc.driver.OracleStatement.prepareDefineBufferAndExecute(OracleStatement.java:1270)
    at oracle.jdbc.driver.OracleStatement.executeMaybeDescribe(OracleStatement.java:1148)
    at oracle.jdbc.driver.OracleStatement.executeSQLSelect(OracleStatement.java:1660)
    at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1469)
    at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3760)
    at oracle.jdbc.driver.OraclePreparedStatement.executeQuery(OraclePreparedStatement.java:3935)
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeQuery(OraclePreparedStatementWrapper.java:1101)
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)

问题原因分析

  1. 循环引用触发无限延迟加载:User继承自AbstractAuditable,而AbstractAuditable持有User类型的lastModifiedBy延迟加载字段。调用User的getLastModifiedBy()会加载对应User实例,该实例又拥有自己的lastModifiedBy字段,进而触发新一轮加载,形成无限循环查询,最终导致栈溢出。
  2. 版本升级后的行为变更:Spring Boot 3.x依赖的Spring Framework 6.x和Hibernate 6.x对延迟加载代理的处理逻辑有调整。2.7版本中,Hibernate代理或Jackson序列化逻辑可能未触发这种循环;升级后,代理对象的方法调用会直接触发延迟加载,导致循环被激活。
  3. 接口默认方法的间接触发:AuditEntity中的默认方法getModifyUserName()会调用getLastModifiedBy(),若Jackson序列化或调试器访问属性时触发该方法,会间接引发循环加载。

解决方案

1. 打破循环引用(推荐)

在User类中重写getLastModifiedBy()并添加@JsonIgnore,避免序列化或调试器访问时触发循环:

public class User extends AbstractAuditable {
    @NotNull 
    @Size(max=50)
    private String fname;

    public String getFname() {
        return fname;
    }

    @Override
    @JsonIgnore
    public User getLastModifiedBy() {
        return super.getLastModifiedBy();
    }
}

2. 调整延迟加载触发逻辑

修改AuditEntity的默认方法,避免触发未初始化的代理对象加载:

@JsonGetter("lastModifiedBy")
default String getModifyUserName() {
    User user = getLastModifiedBy();
    // 检查是否为未初始化的Hibernate代理,避免触发加载
    if (user != null && !(user instanceof HibernateProxy)) {
        return user.getName();
    }
    return null;
}

3. 按需加载关联对象

在查询User时,通过JOIN FETCH按需加载lastModifiedBy,仅在业务需要时使用,避免全局EAGER加载:

@Query("SELECT u FROM User u JOIN FETCH u.lastModifiedBy WHERE u.id = :id")
User getUserWithLastModifiedBy(@Param("id") Long id);

4. 调试器配置调整

在IDE中设置调试器不自动加载延迟加载的关联对象,避免调试时触发循环。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 10:35:53