Jakarta EE 11(Servlet 6.1)で増えてたHttpSession.Accessorを使ってみる

Jakarta EE 11(Servlet 6.1)でHttpSessionに対する新しい制約が増えていました。

HttpSessionをHttpRequestのスコープ外から使用する事が禁止されました。 そのため、外部から使用できるようにHttpSession.Accessorというインターフェースが増えています。

spring.pleiades.io

業務要件などにより特定の条件で一括してログアウトしたい場合とかのためにHttpSessionの一覧を取っておきたい場合があったのですが、GCとかで不都合があったんでしょうねぇ。。。

ということで、試してみようと思います。

使用したのは Apache Tomcat/11.0.1 です。

Rootにアクセスしてセッションを作成するクラス。

import java.io.IOException;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/")
public class Root extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getSession(true);
        resp.getWriter().write("accessed!");
    }

}

Listenerを作ってAccessorをキャッシュします。ついでに古い機能が動くかどうかを試すためにHttpSessionもキャッシュします。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSession.Accessor;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;

@WebListener
public class MySessionListener implements HttpSessionListener {

    public static List<Accessor> accessors = Collections.synchronizedList(new ArrayList<>()); //正しいやり方
    public static List<HttpSession> sessions = Collections.synchronizedList(new ArrayList<>()); //今後Invalidになる

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        accessors.add(se.getSession().getAccessor());
        sessions.add(se.getSession());
    }
        //sessionDestroyedでListから取り除くのは省略
}

/listには一覧をループしてIDを表示するようなプログラムを書いてます。

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/list")
public class GetSessionList extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        final PrintWriter writer = resp.getWriter();
        writer.write("Accessor:");
        MySessionListener.accessors.forEach(t -> t.access(s -> writer.write(s.getId()+"\r\n")));
        writer.write("HttpSession:");
        MySessionListener.sessions.forEach(s -> writer.write(s.getId()+"\r\n"));
    }
}

適当にcurlを使って数回リクエストを投げます。

curl http://localhost:8080/
curl http://localhost:8080/
curl http://localhost:8080/
curl http://localhost:8080/

で、/listにアクセスします。

Accessor:A3B1369126D75435EEE5FB975B91C5C7
F9EC68EC8447FF7EE0948561B867ABBC
5C74C432AB5199D634F282D149C5A5FD
2F4E054EDF06AF15DC43E6132A8F74C9
HttpSession:A3B1369126D75435EEE5FB975B91C5C7
F9EC68EC8447FF7EE0948561B867ABBC
5C74C432AB5199D634F282D149C5A5FD
2F4E054EDF06AF15DC43E6132A8F74C9

同じセッションオブジェクトにアクセスできてそうです。

また、現時点のTomcatでは、少なくともSessionが生きている段階において、HttpSessionを直接キャッシュして使用しても即座にエラーになることはなさそうです。

ただ、このやり方でAccessorを経由してHttpSessionにアクセスしてSessionを無効化してもHttpServletRequest#logout()は呼び出せていないため、SSOを使用している場合でのログアウトには問題が発生することがあります。また、仕組み的にHttpSessionからHttpServletRequestオブジェクトにアクセスすることは出来ません。IDPの方での手当を考えないとですかねぇ。