Troubleshooting stack overflow error

Keywords: Programming Java Apache SQL Mybatis

The cause of stack overflow

Before solving the stack overflow problem, we first need to know the causes of stack overflow, which are mainly as follows:

  1. Whether there is recursive call or not, cyclic dependent call
  2. Is there a large number of cycles or dead cycles
  3. Too many global variables
  4. Local variables are too large, such as array, List, Map data

Problem phenomenon

A very old interface (which hasn't been moved in the past year) reported stack overflow after running on line for a period of time. Other interfaces can provide services normally. Error log:

java.lang.StackOverflowError
	org.springframework.web.servlet.DispatcherServlet.triggerAfterCompletionWithError(DispatcherServlet.java:1303)
	org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:977)
	org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
	org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
	org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
	org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
	com.wlqq.etc.deposit.web.filter.WebContextFilterDev.doFilter(WebContextFilterDev.java:78)
	org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
	org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
	com.wlqq.library.httpcommons.sso.filter.SSOSessionFilter.doFilter(SSOSessionFilter.java:95)
	org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
	org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
	org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
	org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	io.opentracing.contrib.web.servlet.filter.TracingFilter.doFilter(TracingFilter.java:187)
root cause java.lang.StackOverflowError
	java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
	java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
	java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
	java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
	java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
	java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
...

Solving process

review code

First of all, I review ed the business code of the interface according to the above stack overflow reasons, but unfortunately, I didn't find any problems, such as no dead cycle, circular dependency, large local variables, etc.

From the log point of view, the error log appears in the dispatcher servlet. Generally speaking, the dispatcher servlet doesn't see any problems, because if it's a framework problem, it shouldn't appear in this interface. So for this interface, I also looked at his query statement, corresponding to Mybatis's resultMap, etc. There's no problem. It's just a simple query statement. resultMap is not nested, and return entities are not nested. It's normal.

Local reproduction

Because this is an online environment, our troubleshooting is very limited, and the stack overflows, and we really don't know what commands and tools can be used, so I run the code locally, and use JMeter tool to test the interface. Sure enough, the same problem occurs locally, and I'm relieved to be able to reproduce it locally, because the truth is close to us.

Using Breakpoints

I broke a breakpoint in DispatcherServlet's wrong position, and I got nothing after debug stack came out, because stack information was exactly the same as the information on the line. This information cannot be located even if the problem is reported in any line of the dispatcher servlet code.

Then I move the breakpoint to line 1309 of the Collections. Through continuous attempts, I see the debug stack:

...
get:1309, Collections$UnmodifiableList (java.util) [7]
get:1309, Collections$UnmodifiableList (java.util) [6]
get:1309, Collections$UnmodifiableList (java.util) [5]
get:1309, Collections$UnmodifiableList (java.util) [4]
get:1309, Collections$UnmodifiableList (java.util) [3]
get:1309, Collections$UnmodifiableList (java.util) [2]
get:1309, Collections$UnmodifiableList (java.util) [1]
handleResultSets:159, DefaultResultSetHandler (org.apache.ibatis.executor.resultset)
query:63, PreparedStatementHandler (org.apache.ibatis.executor.statement)
query:78, RoutingStatementHandler (org.apache.ibatis.executor.statement)
doQuery:62, SimpleExecutor (org.apache.ibatis.executor)
queryFromDatabase:303, BaseExecutor (org.apache.ibatis.executor)
query:154, BaseExecutor (org.apache.ibatis.executor)
query:102, CachingExecutor (org.apache.ibatis.executor)
query:82, CachingExecutor (org.apache.ibatis.executor)
invoke:-1, GeneratedMethodAccessor98 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
proceed:49, Invocation (org.apache.ibatis.plugin)
intercept:85, PageInterceptor (com.wlqq.etc.deposit.common.interceptor)
invoke:61, Plugin (org.apache.ibatis.plugin)
query:-1, $Proxy66 (com.sun.proxy)
selectList:120, DefaultSqlSession (org.apache.ibatis.session.defaults)
selectList:113, DefaultSqlSession (org.apache.ibatis.session.defaults)
invoke:-1, GeneratedMethodAccessor100 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:386, SqlSessionTemplate$SqlSessionInterceptor (org.mybatis.spring)
selectList:-1, $Proxy49 (com.sun.proxy)
selectList:205, SqlSessionTemplate (org.mybatis.spring)
executeForMany:122, MapperMethod (org.apache.ibatis.binding)
execute:64, MapperMethod (org.apache.ibatis.binding)
invoke:53, MapperProxy (org.apache.ibatis.binding)
queryOpenCardOrders:-1, $Proxy132 (com.sun.proxy)
queryOpenCardOrders:1506, OpenCardServiceImpl (com.wlqq.etc.deposit.service.impl)
invoke:-1, GeneratedMethodAccessor113 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeJoinpointUsingReflection:302, AopUtils (org.springframework.aop.support)
invoke:201, JdkDynamicAopProxy (org.springframework.aop.framework)
queryOpenCardOrders:-1, $Proxy154 (com.sun.proxy)
queryOpenCardOrders:90, OpenCardOrderController (com.wlqq.etc.deposit.web.controller)
invoke:-1, OpenCardOrderController$$FastClassBySpringCGLIB$$1a780c6e (com.wlqq.etc.deposit.web.controller)
invoke:204, MethodProxy (org.springframework.cglib.proxy)
invokeJoinpoint:717, CglibAopProxy$CglibMethodInvocation (org.springframework.aop.framework)
proceed:157, ReflectiveMethodInvocation (org.springframework.aop.framework)
proceed:85, MethodInvocationProceedingJoinPoint (org.springframework.aop.aspectj)
doAround:62, RequestLogAOP (com.wlqq.etc.deposit.web.filter)
invoke:-1, GeneratedMethodAccessor112 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeAdviceMethodWithGivenArgs:621, AbstractAspectJAdvice (org.springframework.aop.aspectj)
invokeAdviceMethod:610, AbstractAspectJAdvice (org.springframework.aop.aspectj)
invoke:68, AspectJAroundAdvice (org.springframework.aop.aspectj)
proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework)
proceed:85, MethodInvocationProceedingJoinPoint (org.springframework.aop.aspectj)
doAround:67, ValidateArgsAOP (com.wlqq.library.validate.aop)
invoke:-1, GeneratedMethodAccessor111 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeAdviceMethodWithGivenArgs:621, AbstractAspectJAdvice (org.springframework.aop.aspectj)
invokeAdviceMethod:610, AbstractAspectJAdvice (org.springframework.aop.aspectj)
invoke:68, AspectJAroundAdvice (org.springframework.aop.aspectj)
proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework)
invoke:92, ExposeInvocationInterceptor (org.springframework.aop.interceptor)
proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework)
intercept:653, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)
queryOpenCardOrders:-1, OpenCardOrderController$$EnhancerBySpringCGLIB$$6931dba9 (com.wlqq.etc.deposit.web.controller)
invoke:-1, GeneratedMethodAccessor110 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
doInvoke:221, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:137, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:110, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:806, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:729, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:959, DispatcherServlet (org.springframework.web.servlet)
doService:893, DispatcherServlet (org.springframework.web.servlet)
processRequest:970, FrameworkServlet (org.springframework.web.servlet)
doPost:872, FrameworkServlet (org.springframework.web.servlet)
service:648, HttpServlet (javax.servlet.http)
service:846, FrameworkServlet (org.springframework.web.servlet)
service:729, HttpServlet (javax.servlet.http)
internalDoFilter:292, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:78, WebContextFilterDev (com.wlqq.etc.deposit.web.filter)
invokeDelegate:346, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:262, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:95, SSOSessionFilter (com.wlqq.library.httpcommons.sso.filter)
invokeDelegate:346, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:262, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:85, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
doFilter:187, TracingFilter (io.opentracing.contrib.web.servlet.filter)
internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
invoke:212, StandardWrapperValve (org.apache.catalina.core)
invoke:106, StandardContextValve (org.apache.catalina.core)
invoke:502, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:141, StandardHostValve (org.apache.catalina.core)
invoke:79, ErrorReportValve (org.apache.catalina.valves)
invoke:616, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:88, StandardEngineValve (org.apache.catalina.core)
service:528, CoyoteAdapter (org.apache.catalina.connector)
process:1099, AbstractHttp11Processor (org.apache.coyote.http11)
process:670, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
doRun:2508, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:2497, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

Seeing that this deug stack is similar to our error reporting, I looked at the List object again, and directly prompted the stack overflow:

Through the figure above, I confirm that I have found the right location, and then find the source of the list according to the debug stack.

I found that this list is mybatis's resultMaps. In the defaultresultsethandler ා handleresultsets method, the resultMaps also reported stack overflow. The resultMaps came from mappedStatement, so we just need to find the source of mappedStatement.

In the defaultsqlsession ා selectlistobject, rowbounds) method, I found the source of MappedStatement, which is a cache object obtained directly from the configuration object of Mybatis.

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

Through the breakpoint information, I found that the resultMaps property of the ms object is normal. And I was surprised to find that this ms object is not the same as the mappedStatement object that we reported the error later, so I guess the code later changed the mappedStatement. Then I checked the debug stack and found that in the paging plug-in, in order to implement paging, it would change the mappedStatement object.

Root cause of problem

From the above, we locate the getPageStatement() method in the paging plug-in and change the mappedStatement of Mybatis. Here is the source code. Let's see how to modify it:

@Intercepts({@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageInterceptor implements Interceptor {
    private static final int MAPPED_STATEMENT_INDEX = 0;
    private static final int PARAMETER_INDEX = 1;
    private static final int ROWBOUNDS_INDEX = 2;
    private static final String sql = "sql", SQLSOURCE_STRING = "sqlSource";
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private static final Map<String, Builder> BUILDER_MAP = new HashMap<String, Builder>();
    //Processing SQL
    public static final SqlParser sqlParser = new SqlParser();

    @SuppressWarnings({"unchecked", "rawtypes"})
    @Override
    public Object intercept(final Invocation invocation) throws Throwable {
        final Object[] queryArgs = invocation.getArgs();
        final MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
        final BoundSql boundSql = ms.getBoundSql(queryArgs[PARAMETER_INDEX]);
        final Object paramObj = boundSql.getParameterObject();
        Page<?> page = null;
        if (paramObj instanceof MapperMethod.ParamMap) {    //If multi parameter
            for (Object value : ((MapperMethod.ParamMap) paramObj).values()) {
                if (value instanceof Page) {
                    page = (Page<?>) value;
                    break;
                }
            }
        }

        if (paramObj instanceof Page) {    //If the parameter is a single page object
            page = (Page<?>) paramObj;
        }

        if (page != null) {
            int count = getCount(((Executor) invocation.getTarget()).getTransaction().getConnection(), boundSql, paramObj, ms);
            page.setTc(count);
            if (count != 0) {
                queryArgs[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
                queryArgs[MAPPED_STATEMENT_INDEX] = getPageStatement(ms, boundSql, page);
                page.setDatas((List) invocation.proceed());
            }
            return page.getDatas();
        }
        return invocation.proceed();
    }

    private static final MappedStatement getPageStatement(MappedStatement ms, BoundSql boundSql, Page<?> page) {
        String id = ms.getId();
        Builder builder = BUILDER_MAP.get(id);
        if (builder == null) {
            builder = new Builder(ms.getConfiguration(), ms.getId(), new ExtSqlSource(boundSql), ms.getSqlCommandType());
            builder.resource(ms.getResource());
            builder.fetchSize(ms.getFetchSize());
            builder.statementType(ms.getStatementType());
            builder.keyGenerator(ms.getKeyGenerator());
            if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
                StringBuffer keyProperties = new StringBuffer();
                for (String keyProperty : ms.getKeyProperties()) {
                    keyProperties.append(keyProperty).append(",");
                }
                keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
                builder.keyProperty(keyProperties.toString());
            }

            builder.timeout(ms.getTimeout());

            builder.parameterMap(ms.getParameterMap());

            builder.resultMaps(ms.getResultMaps());
            builder.resultSetType(ms.getResultSetType());

            builder.cache(ms.getCache());
            builder.flushCacheRequired(ms.isFlushCacheRequired());
            builder.useCache(ms.isUseCache());
            BUILDER_MAP.put(id, builder);
        }

        ms = builder.build();

        MetaObject.forObject(boundSql, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                .setValue(sql, getPageSql(boundSql.getSql(), page));

        MetaObject.forObject(ms, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                .setValue(SQLSOURCE_STRING, new ExtSqlSource(boundSql));
        return ms;
    }

    private static final int getCount(Connection connection, BoundSql boundSql, Object paramObj,
                                      MappedStatement mappedStatement) {
        int count = 0;
        ResultSet rs = null;
        PreparedStatement countStmt = null;
        try {
            final String countSql = getCountSql(boundSql.getSql());
            countStmt = connection.prepareStatement(countSql);
            final DefaultParameterHandler handler = new DefaultParameterHandler(mappedStatement, paramObj, boundSql);
            handler.setParameters(countStmt);
            rs = countStmt.executeQuery();
            if (rs.next()) {
                count = rs.getInt(1);
            }

        } catch (SQLException e) {
            throw new SystemException("SQL invalid", e);
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (countStmt != null) {
                    countStmt.close();
                }
            } catch (SQLException e) {
                //throw new SystemException("SQL invalid", e);
                e.printStackTrace();
            }

        }
        return count;
    }

    private static String getCountSql(String originalSql) {    //count sql
        return sqlParser.getSmartCountSql(originalSql);
    }


    private static String getPageSql(String originalSql, Page<?> page) {
        return originalSql + " limit " + page.getStart() + "," + page.getPs();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties props) {
    }


    private static class ExtSqlSource implements SqlSource {
        BoundSql boundSql;

        protected ExtSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}

We can see that the implementation principle of the Mybatis paging plug-in is to implement paging by modifying the SQL statement in the MappedStatement object every time. This code caches the MappedStatement.Builder object, and builds the MappedStatement object through the MappedStatement.Builder ා build() object. The first error point appears here. It directly uses HashMap to cache objects. HashMap is thread unsafe. If it was before jdk1.7, there would be a circular call during the expansion of HashMap, resulting in stack overflow. Here, you should use ConcurrentHashMap to cache. But our problem is not caused by HashMap, because we are using JDK 1.8, and there is no expansion during my pressure test.

So I have a look at the source code of mappedstatement.builder ා build() method. The code is as follows:

public MappedStatement build() {
    assert mappedStatement.configuration != null;
    assert mappedStatement.id != null;
    assert mappedStatement.sqlSource != null;
    assert mappedStatement.lang != null;
    mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
    return mappedStatement;
}

Through this code, I find that the first part is full of judgment, and the last part is to decorate resultMaps. Collections.unmodifiableList is mainly used to turn our list into a list that cannot be modified. The source code is as follows:

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ?
            new Collections.UnmodifiableRandomAccessList<>(list) :
            new Collections.UnmodifiableList<>(list));
}

Seeing this code, I just thought I found the source, but looking at the source code, I was disappointed. This code is too normal, but I decorated the original list, and then shielded some modification methods.

So I went back to the MappedStatement.Builder source code and found a key point. Our MappedStatement is built using the Builder mode. Each Builder ` ` ` object will build a MappedStatement ` `. The source code is as follows:

public static class Builder {
    private MappedStatement mappedStatement = new MappedStatement();

    public MappedStatement build() {
        assert mappedStatement.configuration != null;
        assert mappedStatement.id != null;
        assert mappedStatement.sqlSource != null;
        assert mappedStatement.lang != null;
        mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
        return mappedStatement;
    }
}

Through this code, I find that the same mappedStatement object returned by every call to mappedStatement.builder ා build() method is not the same as we think, but different objects will be returned by every build() method. This leads to the second error point of this plug-in. In the pageinterceptor ා getpagestatement() method, there is the following code:

private static final MappedStatement getPageStatement(MappedStatement ms, BoundSql boundSql, Page<?> page) {
        String id = ms.getId();
        Builder builder = BUILDER_MAP.get(id);
        if (builder == null) {... }

        ms = builder.build();

        MetaObject.forObject(boundSql, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                .setValue(sql, getPageSql(boundSql.getSql(), page));

        MetaObject.forObject(ms, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                .setValue(SQLSOURCE_STRING, new ExtSqlSource(boundSql));
        return ms;
    }

ms is the same object every time. In order to implement paging, we have changed the sql of the object. In the case of concurrency, because the same shared variable is modified at the same time, the data will be disordered in subsequent paging. But this mistake has nothing to do with the problem we need to locate this time.

But through the analysis, I'm sure the problem must be in this line of code

ms = builder.build();

So I went back to the source code of build():

public MappedStatement build() {
    ...
    mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
    return mappedStatement;
}

There is only one line of valid code. Through the above analysis, we find that mappedStatement is the same object every time we build it. Then every time we build(), this code will decorate itself once. The source code is as follows:

mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);

If the number of requests is large, this line of code is constantly decorating itself. The effect is as follows:

Collections.unmodifiableList(
        Collections.unmodifiableList(
                Collections.unmodifiableList(
                        Collections.unmodifiableList(
                                Collections.unmodifiableList(
                                        Collections.unmodifiableList(
                                                Collections.unmodifiableList(
                                                        ...)))))));

When the level reaches a certain number, when we call the get method of this list, the call chain will be too long, and then the method stack will burst, resulting in stack overflow. Here we find the root of the problem.

Solution

  1. The root cause of the problem is that we cache the MappedStatement.Builder object. After we remove the cache, the code returns to normal.
  2. Instead of creating a new MappedStatement, we directly modify the sql statement of the original MappedStatement by adding limit?,?, and finally the paging information is passed in through parameters.

Backward question answer

  1. Why did this problem work well before, only to be found a few years later today? This is because we used to publish this service very frequently, resulting in the level of decoration being cleared after each publishing.
  2. Why is there a problem with this only interface? This is because this interface is the most visited one among paging query interfaces, so it is the first one to have problems.
  3. Why are only one or two machines on the line having problems? This is because the load of the machine in question is higher. When the time comes, these machines will have problems first.

summary

  1. The causes of stack overflow are basically those listed above, but we will consciously avoid these problems in the process of programming, so the possibility of stack overflow on the line is very small, but once it occurs, it is not easy to troubleshoot. We need to calm down and analyze slowly. We will always find the root cause of the problem, but the process is a little painful.
  2. It is not recommended to do plug-in development of Mybatis without fully understanding the operation principle of Mybatis.
  3. Just because you haven't touched the code doesn't mean the system won't have problems.

Posted by Smasher on Mon, 25 Nov 2019 22:03:43 -0800