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。

相关推荐
Amumu121382 小时前
Vue Router(二)
java·前端
念越3 小时前
数据结构:栈堆
java·开发语言·数据结构
千寻技术帮3 小时前
10333_基于SpringBoot的家电进存销系统
java·spring boot·后端·源码·项目·家电进存销
dear_bi_MyOnly3 小时前
【多线程——线程状态与安全】
java·开发语言·数据结构·后端·中间件·java-ee·intellij-idea
jiaguangqingpanda3 小时前
Day36-20260204
java·开发语言
tb_first3 小时前
万字超详细苍穹外卖学习笔记4
java·spring boot·笔记·学习·spring·mybatis
努力写代码的熊大3 小时前
c++异常和智能指针
java·开发语言·c++
山岚的运维笔记4 小时前
SQL Server笔记 -- 第15章:INSERT INTO
java·数据库·笔记·sql·microsoft·sqlserver
Yvonne爱编码4 小时前
JAVA数据结构 DAY5-LinkedList
java·开发语言·python