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)
问题原因分析
- 循环引用触发无限延迟加载:User继承自AbstractAuditable,而AbstractAuditable持有User类型的
lastModifiedBy延迟加载字段。调用User的getLastModifiedBy()会加载对应User实例,该实例又拥有自己的lastModifiedBy字段,进而触发新一轮加载,形成无限循环查询,最终导致栈溢出。 - 版本升级后的行为变更:Spring Boot 3.x依赖的Spring Framework 6.x和Hibernate 6.x对延迟加载代理的处理逻辑有调整。2.7版本中,Hibernate代理或Jackson序列化逻辑可能未触发这种循环;升级后,代理对象的方法调用会直接触发延迟加载,导致循环被激活。
- 接口默认方法的间接触发: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
相关产品推荐
相关产品推荐

