Session的显式创建和销毁流程 - 高飞网
1510 人阅读

Session的显式创建和销毁流程

2017-07-28 02:09:46

    Session的创建分为隐式创建和显式创建,隐式创建对开发者是透明的,我们不关心它的什么时候创建,什么时候销毁,也不知道他是如何使用的。但是显示创建却需要我们自己去维护它的生命周期。这很类似于Java的GC之于C语言的malloc和free。相似的,销毁也有显式销毁和隐式销毁之分。

一、创建流程

    Servlet的API给我们提供了两个接口,用以创建session。

/**
     * Returns the current <code>HttpSession</code> associated with this request
     * or, if there is no current session and <code>create</code> is true,
     * returns a new session.
     * <p>
     * If <code>create</code> is <code>false</code> and the request has no valid
     * <code>HttpSession</code>, this method returns <code>null</code>.
     * <p>
     * To make sure the session is properly maintained, you must call this
     * method before the response is committed. If the container is using
     * cookies to maintain session integrity and is asked to create a new
     * session when the response is committed, an IllegalStateException is
     * thrown.
     * 
     * @param create
     *            <code>true</code> to create a new session for this request if
     *            necessary; <code>false</code> to return <code>null</code> if
     *            there's no current session
     * @return the <code>HttpSession</code> associated with this request or
     *         <code>null</code> if <code>create</code> is <code>false</code>
     *         and the request has no valid session
     * @see #getSession()
     */
    public HttpSession getSession(boolean create);

    /**
     * Returns the current session associated with this request, or if the
     * request does not have a session, creates one.
     * 
     * @return the <code>HttpSession</code> associated with this request
     * @see #getSession(boolean)
     */
    public HttpSession getSession();

   

    上面的两个重载接口,都可以用来创建session,不同点在于,第一个可以控制,当与request请求关联的session还没有时,是自动创建一个新的session,还是不创建;第二个则没有可选项,当前request中没有关联时,直接就创建一个新的session。接下来看看源码是不是这么实现的

org.apache.catalina.connector.RequestFacade

@Override
public HttpSession getSession(boolean create) {

    if (request == null) {
        throw new IllegalStateException(
                        sm.getString("requestFacade.nullRequest"));
    }

    if (SecurityUtil.isPackageProtectionEnabled()){
        return AccessController.
            doPrivileged(new GetSessionPrivilegedAction(create));
    } else {
        return request.getSession(create);
    }
}

@Override
public HttpSession getSession() {

    if (request == null) {
        throw new IllegalStateException(
                        sm.getString("requestFacade.nullRequest"));
    }

    return getSession(true);
}

    

    从上面的代码可以看到,getSession其实是转换了一下getSession(Boolean create)而已,默认的不存在就创建一个。我们着重看一下getSession(Boolean create)这个方法。首选判断下request是否为空,为空就抛出异常,这个不必说。然后判断包是不加了访问保护,如果加了就有特权读取,执行GetSessionPrivilegedAction中的run方法。(关于特权访问,可以参考:关于AccessController.doPrivileged)。最终都是调用的request.getSession(create)。接下来继续看代码:


    /**
     * Return the session associated with this Request, creating one
     * if necessary and requested.
     *
     * @param create Create a new session if one does not exist
     */
    @Override
    public HttpSession getSession(boolean create) {
        Session session = doGetSession(create);
        if (session == null) {
            return null;
        }

        return session.getSession();
    }


    该方法只是个简单的封装,继续跟进去看doGetSession(create)。


    protected Session doGetSession(boolean create) {

        // There cannot be a session if no context has been assigned yet
        Context context = getContext();
        if (context == null) {
            return (null);
        }

        // Return the current session if it exists and is valid
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            return (session);
        }

        // Return the requested session if it exists and is valid
        Manager manager = context.getManager();
        if (manager == null) {
            return null;        // Sessions are not supported
        }
        if (requestedSessionId != null) {
            try {
                session = manager.findSession(requestedSessionId);
            } catch (IOException e) {
                session = null;
            }
            if ((session != null) && !session.isValid()) {
                session = null;
            }
            if (session != null) {
                session.access();
                return (session);
            }
        }

        // Create a new session if requested and the response is not committed
        if (!create) {
            return (null);
        }
        if ((context != null) && (response != null) &&
            context.getServletContext().getEffectiveSessionTrackingModes().
                    contains(SessionTrackingMode.COOKIE) &&
            response.getResponse().isCommitted()) {
            throw new IllegalStateException
              (sm.getString("coyoteRequest.sessionCreateCommitted"));
        }

        // Re-use session IDs provided by the client in very limited
        // circumstances.
        String sessionId = getRequestedSessionId();
        if (requestedSessionSSL) {
            // If the session ID has been obtained from the SSL handshake then
            // use it.
        } else if (("/".equals(context.getSessionCookiePath())
                && isRequestedSessionIdFromCookie())) {
            /* This is the common(ish) use case: using the same session ID with
             * multiple web applications on the same host. Typically this is
             * used by Portlet implementations. It only works if sessions are
             * tracked via cookies. The cookie must have a path of "/" else it
             * won't be provided to for requests to all web applications.
             *
             * Any session ID provided by the client should be for a session
             * that already exists somewhere on the host. Check if the context
             * is configured for this to be confirmed.
             */
            if (context.getValidateClientProvidedNewSessionId()) {
                boolean found = false;
                for (Container container : getHost().findChildren()) {
                    Manager m = ((Context) container).getManager();
                    if (m != null) {
                        try {
                            if (m.findSession(sessionId) != null) {
                                found = true;
                                break;
                            }
                        } catch (IOException e) {
                            // Ignore. Problems with this manager will be
                            // handled elsewhere.
                        }
                    }
                }
                if (!found) {
                    sessionId = null;
                }
                sessionId = getRequestedSessionId();
            }
        } else {
            sessionId = null;
        }
        session = manager.createSession(sessionId);

        // Creating a new session cookie based on that session
        if ((session != null) && (getContext() != null)
               && getContext().getServletContext().
                       getEffectiveSessionTrackingModes().contains(
                               SessionTrackingMode.COOKIE)) {
            Cookie cookie =
                ApplicationSessionCookieConfig.createSessionCookie(
                        context, session.getIdInternal(), isSecure());

            response.addSessionCookieInternal(cookie);
        }

        if (session == null) {
            return null;
        }

        session.access();
        return session;
    }


    好了,这里是session创建的核心区域。

  1. 获取Servlet Context(Servlet上下文),没有则返回null;
  2. 当前session非空无效时,返回null,非空有效时,返回当前session;
  3. 从上下文中得到session pool manager(会话池管理器),为空则返回;
  4. 当requestedSessionId存在时(可能是从cookies中获取或url重写获取),根据sessionid从管理器中查看所对应的session是否存在,如果存在而且有效,就返回session,同时更新session的访问信息(如时间、访问次数);
  5. 上面的代码片段都是从“已存在”中获取的,下面的代码(create为true),将尝试获取新的session。
  6. 如果response已经提交session,则直接返回null;
  7. 获取sessionid(过程忽略),调用管理器的createSession(sessionId)方法,创建session。逐级跟入:
    o->manager.createSession(sessionId)
    -->org.apache.catalina.ha.session.DeltaManager.createSession(String sessionId)
    -->org.apache.catalina.ha.session.DeltaManager.createSession(String sessionId, boolean distribute)
    -->org.apache.catalina.session.ManagerBase.createSession(String sessionId),下面贴一下ManagerBase.createSession的代码段:
    /**
     * Construct and return a new session object, based on the default
     * settings specified by this Manager's properties.  The session
     * id specified will be used as the session id.  
     * If a new session cannot be created for any reason, return 
     * <code>null</code>.
     * 
     * @param sessionId The session id which should be used to create the
     *  new session; if <code>null</code>, a new session id will be
     *  generated
     * @exception IllegalStateException if a new session cannot be
     *  instantiated for any reason
     */
    @Override
    public Session createSession(String sessionId) {
        
        if ((maxActiveSessions >= 0) &&
                (getActiveSessions() >= maxActiveSessions)) {
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }
        
        // Recycle or create a Session instance
        Session session = createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(this.maxInactiveInterval);
        String id = sessionId;
        if (id == null) {
            id = generateSessionId();
        }
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            sessionCreationTiming.add(timing);
            sessionCreationTiming.poll();
        }
        return (session);

    }

    上面的代码挑重点说一下,首选判断session活跃个数是否超过限制,如果超过,就抛出一个session个数超限的异常,否则就创建一个空的Session,然后设置这个session是新的,有效的,创建时间为当前,设置超时时间,设置sessionId。然后将session计数器+1。

    8.  创建完session以后,将sessionId写入到cookies中,然后更新session访问信息。默认的key值是JSESSIONID,可以在org.apache.catalina.util.SessionConfig。


    以上就是对session显式创建流程的源码分析。


二、销毁流程

    session不能无限制的创建,当不用的时候,要将其显式的销毁掉,尤其是显式创建的session,如果不主动的去销毁它,而是等待它自己“老死”,将耗费系统的大量资源,甚至导致严重的FullGC问题。servletAPI为了我提供了下面的方法去销毁session


    /**
     * Invalidates this session then unbinds any objects bound to it.
     *
     * @exception IllegalStateException
     *                if this method is called on an already invalidated session
     */
    public void invalidate();

    从字面上理解,就是废弃这个session,然后将绑定在它上面的对象都解绑。我们看一下具体是怎么实现的,打开void javax.servlet.http.HttpSession.invalidate()


    /**
     * Invalidates this session and unbinds any objects bound to it.
     *
     * @exception IllegalStateException if this method is called on
     *  an invalidated session
     */
    @Override
    public void invalidate() {

        if (!isValidInternal())
            throw new IllegalStateException
                (sm.getString("standardSession.invalidate.ise"));

        // Cause this session to expire
        expire();

    }

    如果这个Session本身是无效的session,操作将引发异常,否则就进入expire()方法,这将使session结束生命周期。


    /**
     * Perform the internal processing required to invalidate this session,
     * without triggering an exception if the session has already expired.
     *
     * @param notify Should we notify listeners about the demise of
     *  this session?
     */
    public void expire(boolean notify) {

        // Check to see if session has already been invalidated.
        // Do not check expiring at this point as expire should not return until
        // isValid is false
        if (!isValid)
            return;

        synchronized (this) {
            // Check again, now we are inside the sync so this code only runs once
            // Double check locking - isValid needs to be volatile
            // The check of expiring is to ensure that an infinite loop is not
            // entered as per bug 56339
            if (expiring || !isValid)
                return;

            if (manager == null)
                return;

            // Mark this session as "being expired"
            expiring = true;

            // Notify interested application event listeners
            // FIXME - Assumes we call listeners in reverse order
            Context context = (Context) manager.getContainer();

            // The call to expire() may not have been triggered by the webapp.
            // Make sure the webapp's class loader is set when calling the
            // listeners
            ClassLoader oldTccl = null;
            if (context.getLoader() != null &&
                    context.getLoader().getClassLoader() != null) {
                oldTccl = Thread.currentThread().getContextClassLoader();
                if (Globals.IS_SECURITY_ENABLED) {
                    PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                            context.getLoader().getClassLoader());
                    AccessController.doPrivileged(pa);
                } else {
                    Thread.currentThread().setContextClassLoader(
                            context.getLoader().getClassLoader());
                }
            }
            try {
                Object listeners[] = context.getApplicationLifecycleListeners();
                if (notify && (listeners != null)) {
                    HttpSessionEvent event =
                        new HttpSessionEvent(getSession());
                    for (int i = 0; i < listeners.length; i++) {
                        int j = (listeners.length - 1) - i;
                        if (!(listeners[j] instanceof HttpSessionListener))
                            continue;
                        HttpSessionListener listener =
                            (HttpSessionListener) listeners[j];
                        try {
                            context.fireContainerEvent("beforeSessionDestroyed",
                                    listener);
                            listener.sessionDestroyed(event);
                            context.fireContainerEvent("afterSessionDestroyed",
                                    listener);
                        } catch (Throwable t) {
                            ExceptionUtils.handleThrowable(t);
                            try {
                                context.fireContainerEvent(
                                        "afterSessionDestroyed", listener);
                            } catch (Exception e) {
                                // Ignore
                            }
                            manager.getContainer().getLogger().error
                                (sm.getString("standardSession.sessionEvent"), t);
                        }
                    }
                }
            } finally {
                if (oldTccl != null) {
                    if (Globals.IS_SECURITY_ENABLED) {
                        PrivilegedAction<Void> pa =
                            new PrivilegedSetTccl(oldTccl);
                        AccessController.doPrivileged(pa);
                    } else {
                        Thread.currentThread().setContextClassLoader(oldTccl);
                    }
                }
            }

            if (ACTIVITY_CHECK) {
                accessCount.set(0);
            }

            // Remove this session from our manager's active sessions
            manager.remove(this, true);

            // Notify interested session event listeners
            if (notify) {
                fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
            }

            // Call the logout method
            if (principal instanceof GenericPrincipal) {
                GenericPrincipal gp = (GenericPrincipal) principal;
                try {
                    gp.logout();
                } catch (Exception e) {
                    manager.getContainer().getLogger().error(
                            sm.getString("standardSession.logoutfail"),
                            e);
                }
            }

            // We have completed expire of this session
            setValid(false);
            expiring = false;

            // Unbind any objects associated with this session
            String keys[] = keys();
            if (oldTccl != null) {
                if (Globals.IS_SECURITY_ENABLED) {
                    PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                            context.getLoader().getClassLoader());
                    AccessController.doPrivileged(pa);
                } else {
                    Thread.currentThread().setContextClassLoader(
                            context.getLoader().getClassLoader());
                }
            }
            try {
                for (int i = 0; i < keys.length; i++) {
                    removeAttributeInternal(keys[i], notify);
                }
            } finally {
                if (oldTccl != null) {
                    if (Globals.IS_SECURITY_ENABLED) {
                        PrivilegedAction<Void> pa =
                            new PrivilegedSetTccl(oldTccl);
                        AccessController.doPrivileged(pa);
                    } else {
                        Thread.currentThread().setContextClassLoader(oldTccl);
                    }
                }
            }
        }
    }

    下面详细分析session在设置过期的过程:

  1. 如果session是无效的,或者如果session正在设置过期进程中,则直接返回,这是对线程并发的安全处理;
  2. 如果有session监听器,则遍历监听器,触发sessionDestroyed事件。设置session访问次数为0;
  3. 将Session从Manager中移除(Session保存在ManagerBase.sessions中,这是一个ConcurrentHashMap)。
  4. 设置session为失效状态,正在设置过期标识 expring为false;
  5. 解除绑定在session上的对象,这些对象保存在Session.attributes上,这也是一个ConcurrentHashMap。


总结:

    以上从源码的角度,分析的session的显式创建和销毁流程,希望对读者有所帮助,同时笔者将继续撰文,分析session的隐式创建。





还没有评论!
54.167.243.214