这是一份详尽、实用且权威的 Java Session 全面指南,涵盖了从基础概念到在 Spring Boot 中的应用,并包含可直接运行的代码示例。
目录
-
理解会话(Session)
- 1.1 为什么需要会话?
- 1.2 会话的本质是什么?
- 1.3 会话与 Cookie 的关系与区别
- 1.4 会话的生命周期
-
Java Web 中的会话管理:
HttpSession- 2.1
HttpSession接口详解 - 2.2 创建和获取
HttpSession - 2.3 在会话中存储数据
- 2.4 从会话中获取数据
- 2.5 使会话失效
- 2.6 监听会话事件:
HttpSessionListener - 2.7 监听会话属性事件:
HttpSessionAttributeListener - 2.8 会话超时配置
- 2.1
-
Spring Boot 中的会话管理
- 3.1 Spring Boot 对会话的自动化配置
- 3.2 在 Controller 中使用
HttpSession - 3.3 使用
@SessionAttributes注解 - 3.4 使用
@SessionAttribute注解 - 3.5 在服务层或工具类中获取
HttpSession - 3.6 Spring Boot 中的会话超时配置
-
最佳实践与注意事项
- 4.1 存储什么类型的数据?
- 4.2 会话大小与性能考虑
- 4.3 安全性考虑
- 4.4 分布式环境下的会话管理 (简介)
- 4.5 替代方案 (Token, JWT 等)
-
实战案例:可直接运行的 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 完整可运行代码结构
- 5.1 案例一:用户登录状态管理
1. 理解会话(Session)
1.1 为什么需要会话?
HTTP 协议本身是无状态的。这意味着服务器在处理完一个客户端请求后,就会"忘记"这个客户端。当下一个请求到来时,服务器无法直接知道这个新请求是否来自之前同一个用户。
例如:
- 用户 A 登录了购物网站,添加了商品到购物车。
- 用户 A 点击结算时,服务器需要知道这个请求来自 已经登录的 用户 A,并且需要知道用户 A 的购物车里有哪些商品。
如果 HTTP 是无状态的,服务器就无法将这些操作关联到同一个用户 A 身上。这就需要一种机制在多个请求之间保持用户的状态信息 。这就是 Session(会话) 要解决的问题。
1.2 会话的本质是什么?
会话(Session) 是服务器端用来跟踪特定用户 (或浏览器实例)的一种机制。服务器为每个用户(或浏览器会话)创建一个唯一的、服务器端存储的会话对象。这个对象就像一个临时的、用户专属的存储空间。
关键点:
- 服务器端存储: 会话数据存储在服务器内存、数据库或缓存中。客户端通常只保存一个用于标识会话的 ID。
- 唯一标识: 每个会话有一个唯一的 ID (Session ID)。
- 用户关联: 服务器通过 Session ID 将后续的请求与之前创建的会话关联起来。
- 临时性: 会话通常只在用户与网站交互期间有效,一段时间不活动后会被销毁。
1.3 会话与 Cookie 的关系与区别
会话的实现通常依赖于 Cookie 或 URL 重写 来传递 Session ID。
-
最常见方式 (Cookie):
- 用户第一次访问服务器(无 Session ID)。
- 服务器创建一个新的
HttpSession对象,生成唯一的 Session ID。 - 服务器在 HTTP 响应中发送一个名为
JSESSIONID(或其他名称) 的 Cookie,其值就是这个 Session ID。 - 用户的浏览器保存这个 Cookie。
- 用户在后续的请求中,浏览器会自动将这个
JSESSIONIDCookie 包含在 HTTP 请求头中发送给服务器。 - 服务器收到请求,读取
JSESSIONID的值,找到对应的HttpSession对象,从而获取或存储该用户的会话数据。
-
关系:
- Cookie 是传递 Session ID 的一种载体。Session ID 是钥匙,Cookie 是装钥匙的信封。
- 会话数据本身不存储在 Cookie 中(安全考虑和大小限制),只存储在服务器端。
-
区别:
- 存储位置: Session 数据在服务器,Cookie 数据在客户端浏览器。
- 安全性: Session 相对更安全(敏感数据不直接暴露给客户端),Cookie 可能被窃取或篡改(需注意设置
HttpOnly,Secure等属性)。 - 大小限制: Session 理论上可以存储更多数据(受服务器资源限制),Cookie 有大小和数量限制(通常每个域名 4KB 左右)。
- 生命周期: Session 生命周期由服务器配置(超时时间)或程序控制(显式销毁)。Cookie 的生命周期可由服务器设置(有效期)。
1.4 会话的生命周期
- 创建: 通常发生在用户首次与服务器交互,且程序调用
request.getSession()或request.getSession(true)时。如果请求中带有有效的 Session ID,则获取现有会话,不会创建新会话。 - 活动: 用户持续与服务器交互,Session ID 通过 Cookie 或 URL 在每个请求中传递。服务器在此期间可以向会话中添加、读取、修改或移除属性。
- 销毁:
- 超时销毁 (Inactive Timeout): 最常见的销毁方式。服务器配置一个超时时间(例如 30 分钟)。如果用户在最后一次请求后,超过这个时间没有新的请求,服务器会自动销毁该会话,释放资源。
- 显式销毁: 程序调用
session.invalidate()方法主动销毁会话。通常在用户"注销"时调用。 - 应用关闭/崩溃: 如果会话存储在服务器内存中,应用重启或崩溃会导致所有内存中的会话丢失。如果使用持久化存储(如数据库、Redis),会话可以存活得更久。
- 浏览器关闭: 如果用于传递 Session ID 的 Cookie 是会话 Cookie (没有设置
max-age或expires),那么关闭浏览器会删除这个 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> -
在程序中动态设置 (针对特定会话):
javasession.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()可以清除这些会话属性。 - 它不同于直接使用
HttpSession的setAttribute/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.properties 或 application.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:
- 确保
JSESSIONIDCookie 设置了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 核心代码实现
-
User类 (简化版):javapackage com.example.sessiondemo.model; import java.io.Serializable; public class User implements Serializable { // 实现 Serializable 以备分布式存储 private String username; // 省略构造函数、getter、setter } -
LoginController:javapackage 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"; } } -
视图模板 (Thymeleaf 示例):
-
src/main/resources/templates/login.htmlhtml<!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.htmlhtml<!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 核心代码实现
-
Product类:javapackage com.example.sessiondemo.model; import java.io.Serializable; public class Product implements Serializable { private Long id; private String name; private double price; // 省略构造函数、getter、setter } -
CartItem类 (购物车项):javapackage 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; } } -
ShoppingCart类 (购物车):javapackage 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(); } } -
ProductController(模拟商品列表):javapackage 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"; } } -
CartController:javapackage 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"; } } -
视图模板:
-
src/main/resources/templates/productList.htmlhtml<!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.htmlhtml<!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 依赖引入与配置
-
添加依赖 (
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> -
配置 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 -
主类启用 Spring Session (
SessionDemoApplication.java):javaimport 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 的代码(如案例一、案例二中的 LoginController、CartController)几乎不需要修改! 它们仍然通过 request.getSession() 获取 Session,通过 setAttribute/getAttribute 存取数据。Spring Session 负责将这些操作桥接到 Redis 存储。
LoginController、CartController等代码保持案例一、案例二中的写法不变。User、ShoppingCart等存储在 Session 中的对象必须实现Serializable接口,因为 Redis 存储需要序列化。
5.3.4 完整可运行代码结构
在案例一或案例二代码结构的基础上:
- 添加上述依赖到
pom.xml。 - 添加 Redis 配置到
application.properties。 - 在主类上添加
@EnableRedisHttpSession。 - 确保所有存储在 Session 中的对象 (
User,ShoppingCart,CartItem,Product) 都实现java.io.Serializable。 - 安装并运行 Redis 服务器 (localhost:6379)。
测试:
- 启动两个 Spring Boot 应用实例 (设置不同的端口
server.port=8081,server.port=8082在配置文件中)。 - 通过负载均衡器 (或直接访问不同端口) 测试登录、购物车功能。用户在一个实例上的操作(如登录、加购),在另一个实例上访问时应能看到相同的状态。
总结
本指南详细介绍了 Java Session 的核心概念、HttpSession API 的使用、在 Spring Boot 中的集成方式、最佳实践以及分布式环境下的解决方案(Spring Session + Redis)。通过三个完整的实战案例,展示了 Session 在不同场景下的应用。希望这份指南能帮助您深入理解和有效地在项目中应用 Java Session。