Session 事件、傾聽器


與 Session 相關的傾聽器有五個:HttpSessionListenerHttpSessionAttributeListenerHttpSessionBindingListenerHttpSessionActivationListener 與 Servlet 3.1 新增的 HttpSessionIdListener

如果想要在 HttpSession 物件建立或結束時,作些相對應動作,則可以實作 HttpSessionListener

package javax.servlet.http;

import java.util.EventListener;

public interface HttpSessionListener extends EventListener {
    public default void sessionCreated(HttpSessionEvent se) {}
    public default void sessionDestroyed(HttpSessionEvent se) {}
}

HttpSession 物件初始化或結束前,會分別呼叫 sessionCreated()sessionDestroyed() 方法,你可以透過傳入的 HttpSessionEvent 來取得 HttpSession,以針對會話物件作出相對應的建立或結束處理動作,Servlet 4.0 中,兩個方法都定義為預設方法,因此只要對感興趣的方法實作就可以了。例如:

package cc.openhome;
...
@WebListener()
public class SomeSessionListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        // ...
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // ...
    }
}

如果在實作 HttpSessionListener 的類別上標註 @WebListener,則容器在部署 應用程式時,會實例化該類別並註冊給應用程式。另一個方式是在 web.xml 如下設定:

<web-app...>
    ...
    <listener>
        <listener-class>cc.openhome.SomeSessionListener</listener-class>
    </listener>
   ...
<web-app>

來寫個 HttpSessionListener 的應用實例。假設你有個應用程式在使用者登入後會使用 HttpSession 物件來進行會話管理。例如:

package cc.openhome;

import java.io.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(
    urlPatterns = {"/login"},
    initParams = {
        @WebInitParam(name = "SUCCESS", value = "user"),
        @WebInitParam(name = "ERROR", value = "login.html")
    }
)
public class Login extends HttpServlet {
    private String SUCCESS_VIEW;
    private String ERROR_VIEW;

    @Override
    public void init() throws ServletException {
        super.init();
        SUCCESS_VIEW = this.getInitParameter("SUCCESS");
        ERROR_VIEW = this.getInitParameter("ERROR");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
        String name = request.getParameter("name");
        String passwd = request.getParameter("passwd");
        if("caterpillar".equals(name) && "123456".equals(passwd)) {
            request.changeSessionId();
            request.getSession().setAttribute("login", name);
            response.sendRedirect(SUCCESS_VIEW);
        }
        else {
            response.sendRedirect(ERROR_VIEW);
        }
    }
} 

這個 Servlet 在使用者驗證通過後,會取得 HttpSession 實例並設定屬性。如果你想要應用程式中,加上顯示目前已登入線上人數的功能,則可以實作 HttpSessionListener 介面。例如:

package cc.openhome;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

@WebListener()
public class LoginSessionListener implements HttpSessionListener {
    private static int count;

    public static int getCount() {
        return count;
    }

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        LoginSessionListener.count++;
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        LoginSessionListener.count--;
    }
}

LoginSessionListener 中有個靜態(static)變數,在每一次 HttpSession 建立時會遞增,而銷毀 HttpSession 時會遞減,也就是藉由統計 HttpSession 的實例,來作登入使用者的計數功能。你只要在想要顯示線上人數的頁面,使用 LoginSessionListner.getCount(),就可以取得目前的線上人數並顯示。

與會話物件相關 的傾聽器還有 HttpSessionAttributeListener,顧名思義,當在會話物件中加入屬性、移除屬性或替換屬性時,相對應的 attributeAdded()attributeRemoved()attributeReplaced() 方法就會被呼叫,並分別傳入 HttpSessionBindingEvent,在 Servlet 4.0 中,三個方法都被標示 default

package javax.servlet.http;

import java.util.EventListener;

public interface HttpSessionAttributeListener extends EventListener {
    public default void attributeAdded(HttpSessionBindingEvent se) {}
    public default void attributeRemoved(HttpSessionBindingEvent se) {}
    public default void attributeReplaced(HttpSessionBindingEvent se) {}
}

HttpSessionBindingEvent 有個 getName() 方法,可以取得屬性設定或移除時指定的名稱,而 getValue() 可以取得屬性設定或移除時的物件。

如果希望容器在部署應用程式時,實例化實作 HttpSessionAttributeListener 的類別並註冊給應用程式,則同樣也是在實作類別上標註 @WebListener

package cc.openhome;
...
@WebListener()
public class HttpSessionAttrListener implements HttpSessionAttributeListener {
    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        //...
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        //...
    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {
        //...
    }
}

另一個方式是在 web.xml 如下設定:

<web-app...>
    ...
    <listener>
        <listener-class>cc.openhome.HttpSessionAttrListener</listener-class>
    </listener>
   ...
<web-app>

如果有個即將加入 HttpSession 的屬性物件,希望在設定給 HttpSession 成為屬性或從 HttpSession 中移除時,可以收到 HttpSession 的通知,則可以讓該物件實作 HttpSessionBindingListener 介面。

package javax.servlet.http;

import java.util.EventListener;

public interface HttpSessionBindingListener extends EventListener {
    public default void valueBound(HttpSessionBindingEvent event) {}
    public default void valueUnbound(HttpSessionBindingEvent event) {}
}

這個介面是讓即將加入 HttpSession 的屬性物件實作,不需註釋或在 web.xml 中設定,當屬性物件被加入 HttpSession 或從中移除時,就會呼叫對應的 valueBound()valueUnbound() 方法,並傳入 HttpSessionBindingEvent 物件,可以透過該物件的 getSession() 取得 HttpSession 物件。

來介紹這個介面使用的一個範例。同樣地,你有個登入程式如下:

package cc.openhome;

import java.io.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(
    urlPatterns = {"/login"},
    initParams = {
        @WebInitParam(name = "SUCCESS", value = "user"),
        @WebInitParam(name = "ERROR", value = "login.html")
    }
)
public class Login extends HttpServlet {
    private String SUCCESS_VIEW;
    private String ERROR_VIEW;

    @Override
    public void init() throws ServletException {
        super.init();
        SUCCESS_VIEW = this.getInitParameter("SUCCESS");
        ERROR_VIEW = this.getInitParameter("ERROR");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
        String name = request.getParameter("name");
        String passwd = request.getParameter("passwd");
        if("caterpillar".equals(name) && "123456".equals(passwd)) {
            request.changeSessionId();
            request.getSession().setAttribute("user", new User(name));
            response.sendRedirect(SUCCESS_VIEW);
        }
        else {
            response.sendRedirect(ERROR_VIEW);
        }
    }
} 

當使用者輸入正確名稱與密碼時,首先會以使用者名稱來建立 User 實例,而後加入 HttpSession 中作為屬性。你希望 User 實例被加入成為 HttpSession 屬性時,可以自動從資料庫中載入使用者的其他資料,像是位址、照片等,並在日誌中記錄使用者登入的訊息,而當 HttpSession 失效,或者因使用者登出而 User 實例從 HttpSession 中移除時,在日誌中記錄使用者登出的訊息。你可以讓 User 類別實作 HttpSessionBindingListener 介面。例如:

package cc.openhome;
...
public class User implements HttpSessionBindingListener {
    private String name;
    private String other;

    public User(String name) {
        this.name = name;
    }

    public void valueBound(HttpSessionBindingEvent event) {
        this.other = name + ": query data from database...";
        Logger.getLogger(User.class.getName())
                        .log(Level.INFO, this.name + " login..", event);
    }
    public void valueUnbound(HttpSessionBindingEvent event) {
        Logger.getLogger(User.class.getName())
                        .log(Level.INFO, this.name + "logout..", event);
    }
    // 以下為一些Getter、Setter...
}

valueBound() 中,可以實作查詢資料庫的功能(也許是委託給一個負責查詢資料庫的服務物件),並補齊 User 物件中的相關資料,並且可以在 日誌中記錄使用者登入訊息。當 HttpSession 失效前會先移除屬性,或者你主動移除屬性時,則 valueUnbound() 方法會被呼叫,此時在日誌中留下使用者登出的訊息。

HttpSessionActivationListener 則定義了兩個方法 sessionWillPassivate()sessionDidActivate()

package javax.servlet.http;

import java.util.EventListener;

public interface HttpSessionActivationListener extends EventListener {
    public default void sessionWillPassivate(HttpSessionEvent se) {}
    public default void sessionDidActivate(HttpSessionEvent se) {}
}

絕大部份的情況下,幾乎不會使用到 HttpSessionActivationListener。在使用到分散式環境,應用程式的物件可能分散在多個 JVM 之中。當 HttpSession 要從一個 JVM 遷移至另一個 JVM 時,必須先在原本的 JVM 上序列化(Serialize)所有的屬性物件,在這之前若你的屬性物件有實作 HttpSessionActivationListener,就會呼叫 sessionWillPassivate() 方法,而 HttpSession 遷移至另一個 JVM 後,就會對所有屬性物件作反序列化,此時會呼叫 sessionDidActivate() 方法。

如果知道如何作物件的序列化,應該也知道,要可以序列化的物件必須實作 Serializable 介面。如果你的 HttpSession 屬性物件中,有些類別成員無法作序列化,則可以在 sessionWillPassivate() 方法中作些替代處理來保存該成員狀態,而在 sessionDidActivate() 方法中作些恢復該成員狀態的動作。

Servlet 3.1 新增 HttpSessionIdListener,只有一個方法 void sessionIdChanged(HttpSessionEvent event, String oldSessionId) 需要實作,實作類別可以標註 @WebListener,或者是在 web.xml 中設定,在 HttpSession 的 Session ID 發生變化時,會呼叫 sessionIdChanged() 方法。