Spring Boot內嵌Tomcat session超時問題


最近讓Spring Boot內嵌Tomcat的session超時問題給坑了一把。

在應用中需要設置session超時時間,然后就習慣的在application.properties配置文件中設置如下,

server.session.timeout=90

這里把超時時間設置的短些,主要想看看到底有沒有起作用(不能設值30min然后再看吧,那樣太不人道了)。結果沒起作用,百度下發現Spring Boot 2后,配置變成如下,

server.servlet.session.timeout=90

但結果依然不起作用,后來就斷斷續續的懵了逼的找問題原因,各種百度,google,最后覺得還是看源代碼吧,順便也學習下。

1. 既然是Session超時時間問題,那就看看對Session的實現 - StandardSession

其中有isValid()方法

    /**
     * Return the <code>isValid</code> flag for this session.
     */
    @Override
    public boolean isValid() {

        if (!this.isValid) {
            return false;
        }

        if (this.expiring) {
            return true;
        }

        if (ACTIVITY_CHECK && accessCount.get() > 0) {
            return true;
        }

        if (maxInactiveInterval > 0) {
            int timeIdle = (int) (getIdleTimeInternal() / 1000L);
            if (timeIdle >= maxInactiveInterval) {
                expire(true);
            }
        }

        return this.isValid;
    }

看了下,這里的 timeIdle >= maxInactiveInterval就是觸發session超時的判斷,滿足則調用 expire(true)。那么問題就來了,什么時候調用isValid()?

2. 后台肯定有定時調用isValid()的線程

查看調用isValid()的相關類如下,StandardManager和ManagerBase入了法眼了。

StandardManager中的注解表明是用來讓所有存活的session過期的,應該是在web容器銷毀時調用的,所以就只看 ManagerBase

        // Expire all active sessions
        Session sessions[] = findSessions();
        for (int i = 0; i < sessions.length; i++) {
            Session session = sessions[i];
            try {
                if (session.isValid()) {
                    session.expire();
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
            } finally {
                // Measure against memory leaking if references to the session
                // object are kept in a shared field somewhere
                session.recycle();
            }
        }

ManagerBase,注解表明是我們想要的,接下來看調用processExpires()的類。還是ManagerBase。

    /**
     * Invalidate all sessions that have expired. */
    public void processExpires() {

        long timeNow = System.currentTimeMillis();
        Session sessions[] = findSessions();
        int expireHere = 0 ;

        if(log.isDebugEnabled())
            log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
        for (int i = 0; i < sessions.length; i++) {
            if (sessions[i]!=null && !sessions[i].isValid()) {
                expireHere++;
            }
        }
        long timeEnd = System.currentTimeMillis();
        if(log.isDebugEnabled())
             log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
        processingTime += ( timeEnd - timeNow );

    }

調用processExpires()

    /**
     * Frequency of the session expiration, and related manager operations.
     * Manager operations will be done once for the specified amount of
     * backgroundProcess calls (ie, the lower the amount, the most often the
     * checks will occur).
     */
    protected int processExpiresFrequency = 6;
    /**
     * {@inheritDoc}
     * <p>
     * Direct call to {@link #processExpires()}
     */
    @Override
    public void backgroundProcess() {
        count = (count + 1) % processExpiresFrequency;
        if (count == 0)
            processExpires();
    }

看到backgroundProcess()方法名就知道離真理不遠了。其調用如下,在StandardContext類中,

    @Override
    public void backgroundProcess() {

        if (!getState().isAvailable())
            return;

        Loader loader = getLoader();
        if (loader != null) {
            try {
                loader.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.loader", loader), e);
            }
        }
        Manager manager = getManager();
        if (manager != null) {
            try {
                manager.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.manager", manager),
                        e);
            }
        }
        WebResourceRoot resources = getResources();
        if (resources != null) {
            try {
                resources.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.resources",
                        resources), e);
            }
        }
        InstanceManager instanceManager = getInstanceManager();
        if (instanceManager instanceof DefaultInstanceManager) {
            try {
                ((DefaultInstanceManager)instanceManager).backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.instanceManager",
                        resources), e);
            }
        }
        super.backgroundProcess();
    }

但是還沒有看到線程的創建,繼續查看調用,ContainerBase.ContainerBackgroundProcessor

    /**
     * Private thread class to invoke the backgroundProcess method
     * of this container and its children after a fixed delay.
     */
    protected class ContainerBackgroundProcessor implements Runnable 
                while (!threadDone) {
                    try {
                        Thread.sleep(backgroundProcessorDelay * 1000L);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                    if (!threadDone) {
                        processChildren(ContainerBase.this);
                    }
                }

看到曙光了!看來后台線程每隔 backgroundProcessorDelay * processExpiresFrequency (s)來判斷session是否過期。

默認值:

backgroundProcessorDelay  = 30s

ServerProperties.class
     /**
         * Delay between the invocation of backgroundProcess methods. If a duration suffix
         * is not specified, seconds will be used.
         */
        @DurationUnit(ChronoUnit.SECONDS)
        private Duration backgroundProcessorDelay = Duration.ofSeconds(30);

processExpiresFrequency = 6

所以默認情況下后台線程每隔3min去判斷session是否超時。這樣我之前設置server.servlet.session.timeout=90s,沒辦法看到效果的。

另外還要注意后台對timeout的處理以min為單位,即90s在后台會認為是1min的。

TomcatServletWebServerFactory.class

    private long getSessionTimeoutInMinutes() {
        Duration sessionTimeout = getSession().getTimeout();
        if (isZeroOrLess(sessionTimeout)) {
            return 0;
        }
        return Math.max(sessionTimeout.toMinutes(), 1);
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM