Java Session 全面指南:原理、应用与实践(含 Spring Boot 实战)

这是一份详尽、实用且权威的 Java Session 全面指南,涵盖了从基础概念到在 Spring Boot 中的应用,并包含可直接运行的代码示例。


目录

  1. 理解会话(Session)

    • 1.1 为什么需要会话?
    • 1.2 会话的本质是什么?
    • 1.3 会话与 Cookie 的关系与区别
    • 1.4 会话的生命周期
  2. Java Web 中的会话管理:HttpSession

    • 2.1 HttpSession 接口详解
    • 2.2 创建和获取 HttpSession
    • 2.3 在会话中存储数据
    • 2.4 从会话中获取数据
    • 2.5 使会话失效
    • 2.6 监听会话事件:HttpSessionListener
    • 2.7 监听会话属性事件:HttpSessionAttributeListener
    • 2.8 会话超时配置
  3. Spring Boot 中的会话管理

    • 3.1 Spring Boot 对会话的自动化配置
    • 3.2 在 Controller 中使用 HttpSession
    • 3.3 使用 @SessionAttributes 注解
    • 3.4 使用 @SessionAttribute 注解
    • 3.5 在服务层或工具类中获取 HttpSession
    • 3.6 Spring Boot 中的会话超时配置
  4. 最佳实践与注意事项

    • 4.1 存储什么类型的数据?
    • 4.2 会话大小与性能考虑
    • 4.3 安全性考虑
    • 4.4 分布式环境下的会话管理 (简介)
    • 4.5 替代方案 (Token, JWT 等)
  5. 实战案例:可直接运行的 Spring Boot 应用

    • 5.1 案例一:用户登录状态管理
      • 5.1.1 场景描述
      • 5.1.2 核心代码实现 (Controller, Service)
      • 5.1.3 完整可运行代码结构
    • 5.2 案例二:购物车功能实现
      • 5.2.1 场景描述
      • 5.2.2 核心代码实现 (Cart 对象, Controller)
      • 5.2.3 完整可运行代码结构
    • 5.3 案例三:分布式会话管理 (使用 Spring Session + Redis)
      • 5.3.1 场景描述与为什么需要分布式会话
      • 5.3.2 依赖引入与配置
      • 5.3.3 核心代码实现 (与普通 Session 使用方式一致)
      • 5.3.4 完整可运行代码结构

1. 理解会话(Session)

1.1 为什么需要会话?

HTTP 协议本身是无状态的。这意味着服务器在处理完一个客户端请求后,就会"忘记"这个客户端。当下一个请求到来时,服务器无法直接知道这个新请求是否来自之前同一个用户。

例如:

  • 用户 A 登录了购物网站,添加了商品到购物车。
  • 用户 A 点击结算时,服务器需要知道这个请求来自 已经登录的 用户 A,并且需要知道用户 A 的购物车里有哪些商品。

如果 HTTP 是无状态的,服务器就无法将这些操作关联到同一个用户 A 身上。这就需要一种机制在多个请求之间保持用户的状态信息 。这就是 Session(会话) 要解决的问题。

1.2 会话的本质是什么?

会话(Session) 是服务器端用来跟踪特定用户 (或浏览器实例)的一种机制。服务器为每个用户(或浏览器会话)创建一个唯一的、服务器端存储的会话对象。这个对象就像一个临时的、用户专属的存储空间。

关键点:

  • 服务器端存储: 会话数据存储在服务器内存、数据库或缓存中。客户端通常只保存一个用于标识会话的 ID
  • 唯一标识: 每个会话有一个唯一的 ID (Session ID)。
  • 用户关联: 服务器通过 Session ID 将后续的请求与之前创建的会话关联起来。
  • 临时性: 会话通常只在用户与网站交互期间有效,一段时间不活动后会被销毁。

会话的实现通常依赖于 CookieURL 重写 来传递 Session ID。

  • 最常见方式 (Cookie):

    1. 用户第一次访问服务器(无 Session ID)。
    2. 服务器创建一个新的 HttpSession 对象,生成唯一的 Session ID。
    3. 服务器在 HTTP 响应中发送一个名为 JSESSIONID (或其他名称) 的 Cookie,其值就是这个 Session ID。
    4. 用户的浏览器保存这个 Cookie。
    5. 用户在后续的请求中,浏览器会自动将这个 JSESSIONID Cookie 包含在 HTTP 请求头中发送给服务器。
    6. 服务器收到请求,读取 JSESSIONID 的值,找到对应的 HttpSession 对象,从而获取或存储该用户的会话数据。
  • 关系:

    • Cookie 是传递 Session ID 的一种载体。Session ID 是钥匙,Cookie 是装钥匙的信封。
    • 会话数据本身存储在 Cookie 中(安全考虑和大小限制),只存储在服务器端。
  • 区别:

    • 存储位置: Session 数据在服务器,Cookie 数据在客户端浏览器。
    • 安全性: Session 相对更安全(敏感数据不直接暴露给客户端),Cookie 可能被窃取或篡改(需注意设置 HttpOnly, Secure 等属性)。
    • 大小限制: Session 理论上可以存储更多数据(受服务器资源限制),Cookie 有大小和数量限制(通常每个域名 4KB 左右)。
    • 生命周期: Session 生命周期由服务器配置(超时时间)或程序控制(显式销毁)。Cookie 的生命周期可由服务器设置(有效期)。

1.4 会话的生命周期

  1. 创建: 通常发生在用户首次与服务器交互,且程序调用 request.getSession()request.getSession(true) 时。如果请求中带有有效的 Session ID,则获取现有会话,不会创建新会话。
  2. 活动: 用户持续与服务器交互,Session ID 通过 Cookie 或 URL 在每个请求中传递。服务器在此期间可以向会话中添加、读取、修改或移除属性。
  3. 销毁:
    • 超时销毁 (Inactive Timeout): 最常见的销毁方式。服务器配置一个超时时间(例如 30 分钟)。如果用户在最后一次请求后,超过这个时间没有新的请求,服务器会自动销毁该会话,释放资源。
    • 显式销毁: 程序调用 session.invalidate() 方法主动销毁会话。通常在用户"注销"时调用。
    • 应用关闭/崩溃: 如果会话存储在服务器内存中,应用重启或崩溃会导致所有内存中的会话丢失。如果使用持久化存储(如数据库、Redis),会话可以存活得更久。
    • 浏览器关闭: 如果用于传递 Session ID 的 Cookie 是会话 Cookie (没有设置 max-ageexpires),那么关闭浏览器会删除这个 Cookie,导致服务器端的会话虽然还存在(直到超时),但用户再次访问时无法通过原来的 Session ID 找到它(服务器会创建一个新会话)。如果 Cookie 设置了有效期,关闭浏览器再打开,Cookie 仍然存在,可以找到原来的会话。

2. Java Web 中的会话管理:HttpSession

Java Servlet API 提供了 javax.servlet.http.HttpSession 接口来实现会话管理。

2.1 HttpSession 接口详解

HttpSession 接口定义了操作会话的核心方法:

  • String getId(): 获取此会话的唯一标识符 (Session ID)。
  • long getCreationTime(): 获取会话创建的时间,单位为自 1970 年 1 月 1 日午夜 (GMT) 以来的毫秒数。
  • long getLastAccessedTime(): 获取客户端最后一次发送与此会话相关的请求的时间。
  • void setMaxInactiveInterval(int interval): 设置客户端请求之间会话保持打开的最大时间间隔(以秒为单位)。负数表示会话永不过期(不推荐)。
  • int getMaxInactiveInterval(): 获取会话的最大不活动时间(秒)。
  • ServletContext getServletContext(): 获取会话所属的 ServletContext
  • Object getAttribute(String name): 返回绑定到此会话的指定名称的对象;如果没有该名称的对象,则返回 null
  • void setAttribute(String name, Object value): 将对象绑定到此会话,使用指定的名称。如果同名属性已存在,则替换它。value 可以是任何可序列化的 Java 对象。
  • void removeAttribute(String name): 从此会话中移除绑定到指定名称的对象。
  • Enumeration<String> getAttributeNames(): 返回一个 String 对象的 Enumeration,其中包含绑定到此会话的所有属性的名称。
  • void invalidate(): 使此会话无效并解除绑定到它的任何数据。

2.2 创建和获取 HttpSession

在 Servlet 的 doGet, doPost 等方法中,通过 HttpServletRequest 对象获取会话:

java 复制代码
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class MyServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 获取当前请求的会话。如果不存在,则创建一个新的会话。
        HttpSession session = request.getSession(); // 等同于 request.getSession(true)

        // 如果只是想获取现有会话,而不想创建新会话(例如检查用户是否已登录)
        HttpSession existingSession = request.getSession(false);
        if (existingSession != null) {
            // 存在现有会话
        } else {
            // 不存在现有会话
        }
    }
}
  • request.getSession()request.getSession(true): 如果请求没有有效的 Session ID,则创建 一个新的 HttpSession 对象并返回它。如果请求包含有效的 Session ID,则返回与该 ID 关联的现有会话。
  • request.getSession(false): 如果请求包含有效的 Session ID,则返回与该 ID 关联的现有 HttpSession 对象。否则,返回 null不会创建新会话。

2.3 在会话中存储数据

使用 setAttribute(String name, Object value) 方法将数据存储在会话中。value 应该是可序列化的对象(如果考虑集群或持久化存储)。

java 复制代码
session.setAttribute("username", "张三"); // 存储字符串
session.setAttribute("cart", myShoppingCart); // 存储自定义对象 (myShoppingCart 应实现 Serializable)
session.setAttribute("loginTime", new Date()); // 存储 Date 对象

2.4 从会话中获取数据

使用 getAttribute(String name) 方法从会话中检索数据。需要强制转换为正确的类型。

java 复制代码
String username = (String) session.getAttribute("username");
ShoppingCart cart = (ShoppingCart) session.getAttribute("cart");
Date loginTime = (Date) session.getAttribute("loginTime");

2.5 使会话失效

通常在用户注销时调用 invalidate() 方法。这会立即销毁会话对象,并移除其中存储的所有属性。服务器通常会发送一个过期时间为 0 的 JSESSIONID Cookie,指示浏览器删除它。

java 复制代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
    // 处理注销逻辑...
    HttpSession session = request.getSession(false);
    if (session != null) {
        session.invalidate(); // 销毁会话
    }
    // 重定向到登录页或首页...
}

2.6 监听会话事件:HttpSessionListener

实现 HttpSessionListener 接口可以监听会话的创建和销毁事件。

java 复制代码
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class MySessionListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        // 当一个新的 HttpSession 被创建时调用
        System.out.println("Session created: " + se.getSession().getId());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // 当一个 HttpSession 即将被销毁(超时或 invalidate)时调用
        System.out.println("Session destroyed: " + se.getSession().getId());
    }
}

需要在 web.xml 中注册监听器:

XML 复制代码
<listener>
    <listener-class>com.yourpackage.MySessionListener</listener-class>
</listener>

或者在支持注解的 Servlet 容器中(如 Tomcat 7+),使用 @WebListener

java 复制代码
@WebListener
public class MySessionListener implements HttpSessionListener {
    // ... 实现方法 ...
}

2.7 监听会话属性事件:HttpSessionAttributeListener

实现 HttpSessionAttributeListener 接口可以监听会话中属性的添加、移除和替换事件。

java 复制代码
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;

public class MySessionAttributeListener implements HttpSessionAttributeListener {

    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        // 当有属性被添加到会话中时调用
        System.out.println("Attribute added: " + event.getName() + " = " + event.getValue());
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        // 当有属性从会话中被移除时调用
        System.out.println("Attribute removed: " + event.getName());
    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {
        // 当会话中已有的属性被新值替换时调用
        System.out.println("Attribute replaced: " + event.getName() + " (old value: " + event.getValue() + ")");
        // 注意:event.getValue() 返回的是被替换的旧值!
    }
}

注册方式与 HttpSessionListener 相同(web.xml@WebListener)。

2.8 会话超时配置

会话超时时间可以通过多种方式配置:

  • web.xml 中配置 (适用于整个应用):

    XML 复制代码
    <session-config>
        <session-timeout>30</session-timeout> <!-- 单位:分钟 -->
    </session-config>
  • 在程序中动态设置 (针对特定会话):

    java 复制代码
    session.setMaxInactiveInterval(60 * 30); // 设置超时时间为 30 分钟 (1800 秒)
  • 在 Servlet 容器 (如 Tomcat) 的全局配置文件中设置 (如 conf/web.xml): 这会作为所有应用的默认值。


3. Spring Boot 中的会话管理

Spring Boot 简化了 Java Web 开发,包括会话管理。它自动配置了底层的 Servlet 容器(如 Tomcat, Jetty),并提供了便捷的注解来使用会话。

3.1 Spring Boot 对会话的自动化配置

  • Spring Boot 启动 Web 应用时,会自动配置 HttpSession 所需的环境。
  • 默认情况下,会话存储在 Servlet 容器的内存中。
  • Spring Boot 提供了 server.servlet.session.* 配置属性来定制会话行为(如超时时间、Cookie 设置)。

3.2 在 Controller 中使用 HttpSession

在 Spring MVC 的 @Controller@RestController 中,可以直接将 HttpSession 作为方法参数注入:

java 复制代码
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpSession;

@Controller
public class MyController {

    @GetMapping("/storeInSession")
    public String storeData(HttpSession session) {
        session.setAttribute("key", "value stored in Spring Boot session");
        return "redirect:/result";
    }

    @GetMapping("/getFromSession")
    public String getData(HttpSession session) {
        String value = (String) session.getAttribute("key");
        // ... 使用 value ...
        return "resultPage";
    }
}

3.3 使用 @SessionAttributes 注解

@SessionAttributes 注解用于在 Controller 级别 声明模型属性(Model Attributes)应该存储在会话中。这些属性会在多个请求之间保持(跨越同一个 Controller 的多个方法)。

java 复制代码
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

@Controller
@RequestMapping("/multiStepForm")
@SessionAttributes({"step1Data", "step2Data"}) // 指定哪些模型属性名要存入会话
public class MultiStepFormController {

    @ModelAttribute("step1Data") // 初始化模型属性 (可选)
    public Step1Data initStep1Data() {
        return new Step1Data();
    }

    @GetMapping("/step1")
    public String showStep1Form(@ModelAttribute("step1Data") Step1Data step1Data) {
        return "step1Form";
    }

    @PostMapping("/step1")
    public String processStep1(@ModelAttribute("step1Data") Step1Data step1Data) {
        // 处理 step1 数据。因为 step1Data 在 @SessionAttributes 中,它会自动保存到会话
        return "redirect:/multiStepForm/step2";
    }

    @GetMapping("/step2")
    public String showStep2Form(@ModelAttribute("step2Data") Step2Data step2Data) {
        return "step2Form";
    }

    @PostMapping("/step2")
    public String processStep2(@ModelAttribute("step2Data") Step2Data step2Data,
                               @ModelAttribute("step1Data") Step1Data step1Data, // 从会话获取 step1Data
                               SessionStatus sessionStatus) {
        // 处理 step2 数据,并结合 step1Data 完成整个表单
        // ...

        // 清除会话中由 @SessionAttributes 存储的属性 (可选)
        sessionStatus.setComplete(); // 清除 step1Data, step2Data
        return "confirmationPage";
    }
}

注意:

  • @SessionAttributes 指定的属性存储在会话中,但仅限于当前 Controller 内部的方法调用。
  • 使用 SessionStatus.setComplete() 可以清除这些会话属性。
  • 它不同于直接使用 HttpSessionsetAttribute/getAttribute

3.4 使用 @SessionAttribute 注解

@SessionAttribute 注解用于在方法参数上,指示参数值应该从会话中获取,而不是从请求参数或模型中获取。

java 复制代码
@GetMapping("/viewCart")
public String viewCart(@SessionAttribute("shoppingCart") ShoppingCart cart, Model model) {
    model.addAttribute("cartItems", cart.getItems());
    return "cartView";
}

这个注解通常用于访问那些不是通过 @SessionAttributes 声明,而是由你直接通过 session.setAttribute 或其他方式存储在会话中的属性。

3.5 在服务层或工具类中获取 HttpSession

有时需要在非 Controller 的组件(如 Service, Util)中访问会话。可以通过 HttpServletRequest 来间接获取 HttpSession

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@Service
public class MyService {

    public void doSomethingWithSession() {
        // 获取当前请求的 HttpServletRequest
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 获取 HttpSession (谨慎使用,确保当前线程在处理 Web 请求)
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object value = session.getAttribute("key");
            // ... 使用 value ...
        }
    }
}

重要提示: 这种方式依赖于 RequestContextHolder,它使用 ThreadLocal 来存储当前请求的属性。只有在处理 Web 请求的线程中调用这个方法才有效 (例如,在 Controller 调用的 Service 方法中)。在异步任务、定时任务或其他非请求线程中调用会失败(RequestContextHolder 获取不到请求)。

3.6 Spring Boot 中的会话超时配置

在 Spring Boot 的配置文件(application.propertiesapplication.yml)中设置:

properties 复制代码
# application.properties
server.servlet.session.timeout=30m # 设置会话超时为 30 分钟。单位: s(秒), m(分钟), h(小时), d(天)
yaml 复制代码
# application.yml
server:
  servlet:
    session:
      timeout: 30m

这个配置最终会调用 session.setMaxInactiveInterval()。它覆盖了 web.xml 中的设置。


4. 最佳实践与注意事项

4.1 存储什么类型的数据?

  • 推荐存储:
    • 用户标识符(如用户 ID、用户名)
    • 轻量级的用户状态信息(如登录状态、角色标识)
    • 临时性、非关键性数据(如购物车信息、多步骤表单的中间数据)
  • 避免存储:
    • 大量数据: 影响服务器性能(内存、序列化/反序列化开销)。考虑使用数据库或缓存。
    • 敏感数据: 如密码、信用卡号。即使存储在服务器端,也要确保会话存储本身的安全(加密、安全传输 Session ID)。
    • 频繁变化的业务数据: 更适合存储在数据库或缓存中。
    • 不可序列化的对象: 如果应用需要支持集群或持久化会话存储,存储在会话中的对象必须实现 java.io.Serializable 接口。

4.2 会话大小与性能考虑

  • 保持会话精简: 只存储必要的最小数据。移除不再需要的属性 (removeAttribute)。
  • 监控会话大小: 大型应用应监控会话数量和平均大小,防止内存溢出(OOM)。可以使用监听器记录会话创建时的属性信息(注意性能)。
  • 超时时间合理: 设置合适的超时时间,平衡用户体验(避免频繁重新登录)和服务器资源消耗(及时释放闲置会话)。
  • 考虑替代方案: 对于非常大的数据(如复杂的购物车),考虑存储在数据库或分布式缓存中,会话中只保存一个引用 ID。

4.3 安全性考虑

  • Session Fixation 攻击防护: 用户登录成功后,调用 session.invalidate() 销毁旧会话,再创建一个新会话 (request.getSession(true)),并赋予新的 Session ID。这可以防止攻击者预先获取一个有效 Session ID 并诱导用户使用它登录。

    java 复制代码
    // 登录成功后
    HttpSession oldSession = request.getSession(false);
    if (oldSession != null) {
        oldSession.invalidate();
    }
    HttpSession newSession = request.getSession(true); // 创建新会话
    newSession.setAttribute("user", authenticatedUser);
  • 安全传输 Session ID:

    • 确保 JSESSIONID Cookie 设置了 Secure 属性(仅通过 HTTPS 传输)。在 Spring Boot 中可通过 server.servlet.session.cookie.secure=true 配置。
    • 设置 HttpOnly 属性(防止 JavaScript 访问 Cookie 以减少 XSS 攻击风险)。Spring Boot 默认启用 HttpOnly
  • 验证会话数据: 从会话中获取数据时,不要完全信任其内容,特别是涉及权限或流程控制的数据。例如,检查用户 ID 是否与当前操作匹配。

  • 避免在 URL 中暴露 Session ID (URL 重写): 除非必要且已考虑安全风险,否则优先使用 Cookie 传输 Session ID。如果必须使用 URL 重写(如浏览器禁用 Cookie),需格外注意安全。

4.4 分布式环境下的会话管理 (简介)

当应用部署在多台服务器(集群)时,用户的不同请求可能被负载均衡器分发到不同的服务器实例。如果会话存储在单个服务器的内存中,用户后续请求如果落到另一台服务器,将无法找到之前的会话(导致用户需要重新登录)。

解决方案:

  • 粘性会话 (Sticky Session / Session Affinity): 配置负载均衡器,将同一用户的请求始终路由到同一台服务器。缺点:缺乏真正的容错性(该服务器宕机则会话丢失),负载可能不均。
  • 共享会话存储: 将会话数据存储在外部所有服务器都能访问的地方。
    • 数据库持久化: 将会话数据序列化后存储到数据库(如 MySQL)。性能相对较低。
    • 分布式缓存/内存存储: 使用如 Redis, Memcached, Hazelcast 等高性能分布式缓存存储会话数据。这是目前最流行的方案。
  • Spring Session 项目: 为 Spring 应用提供了透明的、支持多种存储(Redis, JDBC, Hazelcast 等)的分布式会话解决方案。它抽象了底层的 HttpSession 实现,使得代码使用 HttpSession 的方式几乎不变。我们将在案例三中演示。

4.5 替代方案 (Token, JWT 等)

对于 RESTful API 或前后端分离架构,基于 Cookie/Session 的状态管理可能不再适用。常用替代方案包括:

  • Token-Based Authentication (基于令牌的认证): 服务器在用户登录成功后生成一个令牌(Token)返回给客户端(通常在响应体中)。客户端在后续请求的 Authorization 头(如 Bearer <token>)中携带此令牌。服务器验证令牌有效性。令牌本身可以包含用户信息(如 JWT)或只是一个随机标识符(服务器需存储令牌与用户的映射关系)。
  • JWT (JSON Web Token): 一种开放标准 (RFC 7519),用于安全地在各方之间传输信息作为 JSON 对象。JWT 通常包含用户标识、有效时间等信息,并经过签名或加密。服务器验证签名即可信任其内容,无需在服务器端存储会话状态(无状态)。非常适合 RESTful API。注意 JWT 本身无法撤销(除非有效期很短或使用黑名单机制)。

5. 实战案例:可直接运行的 Spring Boot 应用

以下案例假设您已创建一个基本的 Spring Boot Web 项目 (使用 Spring Initializr, 选择 Web -> Spring Web 依赖)。

5.1 案例一:用户登录状态管理

5.1.1 场景描述

实现一个简单的用户登录功能。用户输入用户名密码(模拟验证),登录成功后记录用户信息到 Session。后续访问需要登录才能查看的页面时,检查 Session 中是否存在用户信息。提供注销功能销毁 Session。

5.1.2 核心代码实现
  1. User 类 (简化版):

    java 复制代码
    package com.example.sessiondemo.model;
    import java.io.Serializable;
    public class User implements Serializable { // 实现 Serializable 以备分布式存储
        private String username;
        // 省略构造函数、getter、setter
    }
  2. LoginController

    java 复制代码
    package com.example.sessiondemo.controller;
    import com.example.sessiondemo.model.User;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import javax.servlet.http.HttpSession;
    
    @Controller
    public class LoginController {
    
        // 模拟用户验证
        private boolean isValidUser(String username, String password) {
            // 实际应用中应查询数据库
            return "admin".equals(username) && "123456".equals(password);
        }
    
        @GetMapping("/login")
        public String loginPage() {
            return "login"; // 返回 login.html 视图
        }
    
        @PostMapping("/login")
        public String login(@RequestParam String username,
                            @RequestParam String password,
                            HttpSession session,
                            Model model) {
            if (isValidUser(username, password)) {
                // 登录成功,创建 User 对象存入 Session
                User user = new User();
                user.setUsername(username);
                session.setAttribute("user", user);
                return "redirect:/dashboard"; // 重定向到仪表盘
            } else {
                model.addAttribute("error", "Invalid username or password");
                return "login"; // 返回登录页并显示错误
            }
        }
    
        @GetMapping("/dashboard")
        public String dashboard(HttpSession session, Model model) {
            User user = (User) session.getAttribute("user");
            if (user == null) {
                // 用户未登录,重定向到登录页
                return "redirect:/login";
            }
            model.addAttribute("username", user.getUsername());
            return "dashboard"; // 返回 dashboard.html 视图
        }
    
        @GetMapping("/logout")
        public String logout(HttpSession session) {
            // 使会话失效
            session.invalidate();
            return "redirect:/login";
        }
    }
  3. 视图模板 (Thymeleaf 示例):

    • src/main/resources/templates/login.html

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <title>Login</title>
      </head>
      <body>
          <h1>Login</h1>
          <div th:if="${error}" th:text="${error}" style="color: red;"></div>
          <form th:action="@{/login}" method="post">
              Username: <input type="text" name="username"><br>
              Password: <input type="password" name="password"><br>
              <button type="submit">Login</button>
          </form>
      </body>
      </html>
    • src/main/resources/templates/dashboard.html

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <title>Dashboard</title>
      </head>
      <body>
          <h1>Welcome, <span th:text="${username}">User</span>!</h1>
          <p>This is your dashboard.</p>
          <a th:href="@{/logout}">Logout</a>
      </body>
      </html>
5.1.3 完整可运行代码结构
复制代码
src/
  main/
    java/
      com.example.sessiondemo/
        SessionDemoApplication.java // Spring Boot 主类
        controller/
          LoginController.java
        model/
          User.java
    resources/
      application.properties
      templates/
        login.html
        dashboard.html

5.2 案例二:购物车功能实现

5.2.1 场景描述

实现一个简单的购物车。用户可以将商品加入购物车,查看购物车内容。购物车信息存储在用户的 Session 中。

5.2.2 核心代码实现
  1. Product 类:

    java 复制代码
    package com.example.sessiondemo.model;
    import java.io.Serializable;
    public class Product implements Serializable {
        private Long id;
        private String name;
        private double price;
        // 省略构造函数、getter、setter
    }
  2. CartItem 类 (购物车项):

    java 复制代码
    package com.example.sessiondemo.model;
    import java.io.Serializable;
    public class CartItem implements Serializable {
        private Product product;
        private int quantity;
        // 省略构造函数、getter、setter
        public double getTotalPrice() {
            return product.getPrice() * quantity;
        }
    }
  3. ShoppingCart 类 (购物车):

    java 复制代码
    package com.example.sessiondemo.model;
    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.List;
    public class ShoppingCart implements Serializable {
        private List<CartItem> items = new ArrayList<>();
    
        public void addItem(Product product, int quantity) {
            for (CartItem item : items) {
                if (item.getProduct().getId().equals(product.getId())) {
                    item.setQuantity(item.getQuantity() + quantity);
                    return;
                }
            }
            items.add(new CartItem(product, quantity));
        }
    
        public List<CartItem> getItems() {
            return items;
        }
    
        public double getTotalAmount() {
            return items.stream().mapToDouble(CartItem::getTotalPrice).sum();
        }
    
        public void clear() {
            items.clear();
        }
    }
  4. ProductController (模拟商品列表):

    java 复制代码
    package com.example.sessiondemo.controller;
    import com.example.sessiondemo.model.Product;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import java.util.Arrays;
    import java.util.List;
    
    @Controller
    public class ProductController {
    
        @GetMapping("/products")
        public String listProducts(Model model) {
            // 模拟商品数据
            List<Product> products = Arrays.asList(
                new Product(1L, "Product A", 10.0),
                new Product(2L, "Product B", 20.0),
                new Product(3L, "Product C", 30.0)
            );
            model.addAttribute("products", products);
            return "productList";
        }
    }
  5. CartController

    java 复制代码
    package com.example.sessiondemo.controller;
    import com.example.sessiondemo.model.CartItem;
    import com.example.sessiondemo.model.Product;
    import com.example.sessiondemo.model.ShoppingCart;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import javax.servlet.http.HttpSession;
    
    @Controller
    public class CartController {
    
        @PostMapping("/cart/add")
        public String addToCart(@RequestParam Long productId,
                                @RequestParam int quantity,
                                HttpSession session) {
            // 实际应用中应根据 productId 从数据库获取 Product
            Product product = new Product(productId, "Product " + productId, productId * 10.0); // 模拟
    
            // 获取或创建购物车
            ShoppingCart cart = (ShoppingCart) session.getAttribute("cart");
            if (cart == null) {
                cart = new ShoppingCart();
                session.setAttribute("cart", cart);
            }
    
            cart.addItem(product, quantity);
            return "redirect:/cart"; // 重定向到购物车页面
        }
    
        @GetMapping("/cart")
        public String viewCart(HttpSession session, Model model) {
            ShoppingCart cart = (ShoppingCart) session.getAttribute("cart");
            model.addAttribute("cart", cart);
            return "cartView";
        }
    
        @PostMapping("/cart/clear")
        public String clearCart(HttpSession session) {
            ShoppingCart cart = (ShoppingCart) session.getAttribute("cart");
            if (cart != null) {
                cart.clear();
            }
            return "redirect:/cart";
        }
    }
  6. 视图模板:

    • src/main/resources/templates/productList.html

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <title>Products</title>
      </head>
      <body>
          <h1>Product List</h1>
          <table>
              <tr th:each="product : ${products}">
                  <td th:text="${product.name}"></td>
                  <td th:text="${product.price}"></td>
                  <td>
                      <form th:action="@{/cart/add}" method="post">
                          <input type="hidden" name="productId" th:value="${product.id}">
                          <input type="number" name="quantity" value="1" min="1">
                          <button type="submit">Add to Cart</button>
                      </form>
                  </td>
              </tr>
          </table>
          <a th:href="@{/cart}">View Cart</a>
      </body>
      </html>
    • src/main/resources/templates/cartView.html

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <title>Shopping Cart</title>
      </head>
      <body>
          <h1>Your Shopping Cart</h1>
          <table th:if="${cart != null and !cart.items.empty}">
              <tr>
                  <th>Product</th>
                  <th>Price</th>
                  <th>Quantity</th>
                  <th>Total</th>
              </tr>
              <tr th:each="item : ${cart.items}">
                  <td th:text="${item.product.name}"></td>
                  <td th:text="${item.product.price}"></td>
                  <td th:text="${item.quantity}"></td>
                  <td th:text="${item.totalPrice}"></td>
              </tr>
              <tr>
                  <td colspan="3">Total Amount:</td>
                  <td th:text="${cart.totalAmount}"></td>
              </tr>
          </table>
          <p th:if="${cart == null or cart.items.empty}">Your cart is empty.</p>
          <form th:action="@{/cart/clear}" method="post" th:if="${cart != null and !cart.items.empty}">
              <button type="submit">Clear Cart</button>
          </form>
          <a th:href="@{/products}">Continue Shopping</a>
      </body>
      </html>
5.2.3 完整可运行代码结构
复制代码
src/
  main/
    java/
      com.example.sessiondemo/
        SessionDemoApplication.java
        controller/
          LoginController.java // (可选,如果和案例一结合)
          ProductController.java
          CartController.java
        model/
          User.java // (可选)
          Product.java
          CartItem.java
          ShoppingCart.java
    resources/
      application.properties
      templates/
        login.html // (可选)
        dashboard.html // (可选)
        productList.html
        cartView.html

5.3 案例三:分布式会话管理 (使用 Spring Session + Redis)

5.3.1 场景描述与为什么需要分布式会话

假设应用部署在两个实例 (Instance A 和 Instance B) 上,由负载均衡器分发请求。用户 U1 在 Instance A 登录,Session 存储在 A 的内存中。用户 U1 的下一个请求被负载均衡器分发到 Instance B。Instance B 在自己的内存中找不到 U1 的 Session,导致 U1 需要重新登录。用户体验差。

解决方案: 使用 Spring Session 结合 Redis 实现分布式会话存储。所有服务器实例都从同一个 Redis 中读写会话数据。

5.3.2 依赖引入与配置
  1. 添加依赖 (pom.xml):

    XML 复制代码
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Session Data Redis -->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <!-- Spring Data Redis Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置 Redis 连接 (application.properties):

    properties 复制代码
    # Redis 服务器地址
    spring.redis.host=localhost
    # Redis 服务器端口
    spring.redis.port=6379
    # Redis 密码 (如果设置了)
    # spring.redis.password=yourpassword
    # Redis 数据库索引 (默认 0)
    spring.redis.database=0
    
    # 配置 Spring Session 存储类型为 Redis
    spring.session.store-type=redis
    # (可选) 配置 Session 在 Redis 中的过期时间 (秒), 会覆盖 server.servlet.session.timeout
    # spring.session.redis.flush-mode=on_save
    # spring.session.redis.time-to-live=1800
  3. 主类启用 Spring Session (SessionDemoApplication.java):

    java 复制代码
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
    
    @SpringBootApplication
    @EnableRedisHttpSession // 启用 Spring Session 并指定使用 Redis 存储 HttpSession
    public class SessionDemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(SessionDemoApplication.class, args);
        }
    }
5.3.3 核心代码实现 (与普通 Session 使用方式一致)

关键点: Spring Session 透明地替换了底层的 HttpSession 实现。这意味着你之前使用 HttpSession 的代码(如案例一、案例二中的 LoginControllerCartController几乎不需要修改! 它们仍然通过 request.getSession() 获取 Session,通过 setAttribute/getAttribute 存取数据。Spring Session 负责将这些操作桥接到 Redis 存储。

  • LoginControllerCartController 等代码保持案例一、案例二中的写法不变。
  • UserShoppingCart 等存储在 Session 中的对象必须实现 Serializable 接口,因为 Redis 存储需要序列化。
5.3.4 完整可运行代码结构

在案例一或案例二代码结构的基础上:

  1. 添加上述依赖到 pom.xml
  2. 添加 Redis 配置到 application.properties
  3. 在主类上添加 @EnableRedisHttpSession
  4. 确保所有存储在 Session 中的对象 (User, ShoppingCart, CartItem, Product) 都实现 java.io.Serializable
  5. 安装并运行 Redis 服务器 (localhost:6379)。

测试:

  1. 启动两个 Spring Boot 应用实例 (设置不同的端口 server.port=8081, server.port=8082 在配置文件中)。
  2. 通过负载均衡器 (或直接访问不同端口) 测试登录、购物车功能。用户在一个实例上的操作(如登录、加购),在另一个实例上访问时应能看到相同的状态。

总结

本指南详细介绍了 Java Session 的核心概念、HttpSession API 的使用、在 Spring Boot 中的集成方式、最佳实践以及分布式环境下的解决方案(Spring Session + Redis)。通过三个完整的实战案例,展示了 Session 在不同场景下的应用。希望这份指南能帮助您深入理解和有效地在项目中应用 Java Session。

相关推荐
devlei6 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
pshdhx_albert7 小时前
AI agent实现打字机效果
java·http·ai编程
沉鱼.447 小时前
第十二届题目
java·前端·算法
努力的小郑8 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
赫瑞8 小时前
数据结构中的排列组合 —— Java实现
java·开发语言·数据结构
Victor3568 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3569 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁9 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp9 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
周末也要写八哥9 小时前
多进程和多线程的特点和区别
java·开发语言·jvm