便携式、云就绪的 HTTP 会话
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
 - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
 截止目前, 星球 内专栏累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3700+ 小伙伴加入学习 ,欢迎点击围观
  春天走的是一条有趣的路线。无论您在哪里运行它,它都能提供很多价值,而且 - 因为它建立在依赖注入层上 - 它在底层和运行在它之上的应用程序之间提供了一种自然的间接。这种间接通过解耦提高了代码的可移植性:您的应用程序代码不知道它使用的
  
   javax.sql.DataSource
  
  (或其他)句柄来自哪里,无论是 JNDI 查找、环境变量还是提供的简单的新 bean由春天。 Spring 之上的这种解耦和丰富的功能工具箱支持各种用例——批处理、集成、流处理、Web 服务、微服务、操作、Web 应用程序、安全性等——使 Spring 成为开发人员的合理选择部署到(有时是嵌入式的)Web 容器,如 Apache Tomcat 或 Eclipse Jetty,应用服务器,如 WebSphere 和 WildFly,以及云运行时,如 Google App Engine、Heroku、OpenShift 和(我个人最近最喜欢的)Cloud Foundry。这种可移植性
  
   也
  
  使得将大多数(合理编写的!)应用程序从应用程序服务器转移到更轻的 Web 容器并最终转移到云中变得容易。
 
美中不足的(有状态的)苍蝇
所以有什么问题?为什么要写这个博客?
但是,对于使用 HTTP 会话的应用程序,情况并不理想。扩展 HTTP 会话是事情的进展——请原谅 HTTP 会话术语双关 语 。您会看到您的应用程序需要两件事来扩展 HTTP 会话:会话亲和力和会话复制。会话亲和性(或 粘性会话 )意味着对集群 Web 应用程序的请求将被路由到最初发出 HTTP 会话 cookie 的节点。如果该应用程序实例应脱机,则会话复制可确保相关状态在另一个节点上可用。客户端可以无缝地路由到那里,保留会话状态的所有概念。在流行的容器中配置 HTTP 会话复制并不 难 。这是关于 使用 Apache Tomcat 进行设置的页面,这是关于如何使用 Jetty 进行设置的页面。典型的会话复制策略涉及使用多播网络来通知集群中的其他节点状态更改。会话亲和力和会话复制在只有几个节点的小型环境中运行良好。除非您使用的是嵌入式 Web 容器,否则配置 HTTP 会话复制是另一件需要在容器中配置的事情,不受应用程序的控制。
没关系,云会修复它,对吗?
  您可能会认为——如果不出意外的话——一旦将您的应用程序移动到云中,这种配置会变得更容易、更可预测,但实际上它可能会更痛苦!多播网络在大多数云环境中都是禁忌,包括在 Amazon Web Services 中。即使在更高级别、更以应用程序为中心的平台即服务环境(如 Heroku 或 Cloud Foundry)中,会话复制也不是那么容易。例如,Heroku
  
   不提供会话关联和会话复制
  
  。这个限制是可以理解的:除了多播网络中的限制,应用程序应该——尽可能——最小化服务器端状态。请记住,Heroku 将应用程序的 RAM 限制为 512MB!如果您不尝试将备用 RAM 用作数据库或持久层,这就足够了! Cloud Foundry 就其本身而言,服务于更大的开发人员社区,并在各种数据中心内部运行,因此它必须更加实用。例如,Pivotal Web Services(运行
  
   Cloud Foundry
  
  )为应用程序提供 1GB 的 RAM,并且几年来一直提供会话亲和性。它直到去年年底才提供会话复制,当时对 buildpack 的更改
  
   为部署到 Apache Tomcat Web 服务器的默认独立配置的
  
  任何基于
  
   .war
  
  的 Web 应用程序启用了会话复制。不过,此支持不使用多播网络。相反,它使用配置任何绑定 Redis 支持服务的约定,以便与 Tomcat 容器的会话复制策略一起使用。使用像 Redis 这样的支持服务,或者可能是一些共享文件系统,是在云中进行会话复制的唯一明智的方法。
 
所有这些方法都有不同的权衡:
- 有些是特定于容器的,这意味着它们不会轻易地从一个环境迁移到另一个环境。
 - 他们可能会为操作引入额外的复杂性(如果你还有那个团队!)并且这会在应用程序和生产之间引入更多的摩擦
 - 他们可能使用在云环境中无法正常工作的多播网络
 - 
   他们可能依赖像 Cloud Foundry Java buildpack 这样的
   
    魔法
   
   ,它只知道部署到独立 Apache Tomcat 的
   
.war,而不是嵌入式.jar或 Jetty 等其他 Web 容器。 - 对所有这些其他点的隐含限制是持久性策略不可插入。多播不适合你?太好了,使用Redis。 Redis 不适合您并且想使用 Memcache 或其他不受支持的东西?哦…
 
进入春季会议
  Spring Session 为所有这些问题提供了一个非常好的解决方案。它是标准 Servlet HTTP 会话抽象的包装器。很容易插入任何应用程序,无论它们是否基于 Spring。它充当 HTTP 会话前面的一种代理,将请求转发到策略实现。开箱即用,有一个支持使用
  
   java.util.Map<K,V>
  
  实现和另一个直接与 Redis 一起使用的实现。使用
  
   java.util.Map<K,V>
  
  的实现起初听起来并不那么有趣,但请记住,所有您最喜欢的分布式数据网格(Pivotal GemFire、Hazelcast、Oracle Coherence 等)都可以为您提供对由数据网格内存支持的
  
   Map
  
  实现的引用。
 
Redis 特定的实现利用了 Redis 中的一些效率(如果可用)。让我们看看使用 Redis 设置一个非常简单的 Spring Session 应用程序。为什么是 Redis?因为它是合法的“网络规模”—— 在 高可扩展性 博客上查看这篇关于 Twitter 如何使用它扩展到 105TB RAM、39MM QPS 和 10,000 多个实例的帖子 !
为了让这个示例正常工作,我将以下 Maven 依赖项添加到 一个简单的 Spring Boot 项目 中。
- 
   
org.springframework.boot:spring-boot-starter-redis:1.2.0.RELEASE - 
   
org.springframework.boot:spring-boot-starter-web:1.2.0.RELEASE - 
   
org.springframework.session:spring-session-data-redis:1.0.0.RELEASE 
这是一个简单的示例应用程序:
 package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
	SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloRestController {
@RequestMapping("/")
String uid(HttpSession session) {
	UUID uid = (UUID) session.getAttribute("uid");
	if (uid == null) {
		uid = UUID.randomUUID();
	}
	session.setAttribute("uid", uid);
	return uid.toString();
}
}
 
  在运行它之前,请确保您将一个干净的 Redis 数据库专用于此应用程序。例如,您可以使用
  
   FLUSHDB
  
  重置当前数据库。 Spring Boot Redis starter 自动连接到在
  
   localhost
  
  上运行的 Redis 数据库。如果您想将它指向特定的地方,请使用
  
   各种
   
    spring.redis.*
   
   properties
  
  。
 
  该示例尽可能简单:它只是确认数据正在写入 Redis 后备存储。在浏览器中与
  
   localhost:8080/
  
  Web 应用程序交互后,打开您的
  
   redis-cli
  
  实用程序。第一个请求将触发一个唯一的会话,该会话将用于缓存
  
   uid
  
  值。同一浏览器会话的后续请求将看到相同的值。在
  
   redis-cli
  
  中输入
  
   keys *
  
  以查看已保留的内容。
 
部署到 CloudFoundry
  迁移到这个云可能有点棘手。如果您将其部署到 Cloud Foundry,Cloud Foundry buildpack 将自动将 Spring Boot 自动配置的
  
   RedisConnectionFactory
  
  替换为指向绑定到应用程序的 Redis 实例的
  
   RedisConnectionFactory
  
  。
  
   如果
  
  您在 Cloud Foundry 上运行,使用正确的 buildpack,并且您的应用程序中没有多个
  
   RedisConnectionFactory
  
  ,则此方法有效。
 
  我将使用 Cloud Foundry
  
   manifest.yml
  
  来描述此应用程序在部署到 Cloud Foundry 时的外观。在这种情况下,它至少需要一个支持 Redis 数据库的名为
  
   redis-session
  
  的支持服务。我已将此文件放在我的项目的根目录中,在我的 Maven
  
   pom.xml
  
  旁边。请注意,此
  
   manifest.yml
  
  提供了一个环境变量
  
   SPRING_PROFILES_ACTIVE
  
  ,它将激活
  
   cloud
  
  Spring 配置文件。我们稍后会用到它。
 
 package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
	SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloRestController {
@RequestMapping("/")
String uid(HttpSession session) {
	UUID uid = (UUID) session.getAttribute("uid");
	if (uid == null) {
		uid = UUID.randomUUID();
	}
	session.setAttribute("uid", uid);
	return uid.toString();
}
}
 
  在推送应用程序之前,您需要创建一个 Redis 实例。我使用以下咒语在 Pivotal Web Services 上创建一个简单的 Redis 实例(称为
  
   redis-session
  
  ,我们在
  
   manifest.yml
  
  中引用它)然后推送应用程序。
 
 package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
	SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloRestController {
@RequestMapping("/")
String uid(HttpSession session) {
	UUID uid = (UUID) session.getAttribute("uid");
	if (uid == null) {
		uid = UUID.randomUUID();
	}
	session.setAttribute("uid", uid);
	return uid.toString();
}
}
 我可以按原样部署应用程序,在这个示例中应该一切正常,只有一个绑定的支持服务和一个已知类型的 bean。
您可以使用 Spring Cloud PaaS 连接器来简化显式配置和使用云管理的 Redis 支持服务的工作。在这种新安排中,我们将使用 Spring 配置文件来保持在 Cloud Foundry 上运行的显式配置。像这样添加 Spring Cloud PaaS 连接器:
   org.springframework.cloud
  
  
   spring-boot-starter-cloud-connectors
  
  
   1.2.0.RELEASE
  
 
  这将使 Spring Boot 自动装配它知道的每个绑定支持服务类型的实例。如果有一个带有服务 ID
  
   redis-session
  
  Redis 数据库,那么它可以使用常规的 Spring 限定符注入,如下所示:
 
 package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
	SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloRestController {
@RequestMapping("/")
String uid(HttpSession session) {
	UUID uid = (UUID) session.getAttribute("uid");
	if (uid == null) {
		uid = UUID.randomUUID();
	}
	session.setAttribute("uid", uid);
	return uid.toString();
}
}
 
  这种方法是最简单的,如果您只需添加 Spring Cloud 启动器依赖项,您就会得到这种方法。如果要显式配置服务,请通过将以下属性添加到 Spring Boot
  
   src/main/resources/application.properties
  
  来禁用 Spring Boot starter:
 
 package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
	SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloRestController {
@RequestMapping("/")
String uid(HttpSession session) {
	UUID uid = (UUID) session.getAttribute("uid");
	if (uid == null) {
		uid = UUID.randomUUID();
	}
	session.setAttribute("uid", uid);
	return uid.toString();
}
}
 
  然后,明确使用 Spring Cloud PaaS 连接器。 bean 定义
  
   仅
  
  在
  
   cloud
  
  Spring 配置文件处于活动状态时才处于活动状态。否则,Spring Boot 自动配置将启动(这是您在本地运行时想要的)。
 
 package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
	SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class HelloRestController {
@RequestMapping("/")
String uid(HttpSession session) {
	UUID uid = (UUID) session.getAttribute("uid");
	if (uid == null) {
		uid = UUID.randomUUID();
	}
	session.setAttribute("uid", uid);
	return uid.toString();
}
}
 但是等等,还有更多……
  这篇文章的重点是研究如何轻松地在本地环境或云中为 Spring 应用程序获取可扩展的 HTTP 会话。我
  
   不
  
  建议您再次开始用 JSF 页面图塞满 HTTP 会话!如果您需要一个过期的、可扩展的、短暂的存储来存储轻量级的业务状态——比如安全令牌——那么 Spring Session 可以提供帮助。由于 Spring Session 位于您的应用程序和 HTTP 会话之间,因此它可以在 Servlet
  
   HttpSession
  
  之上提供一些其他有用的抽象。
  
   Spring Security 和 Spring Session 的负责人 Rob Winch
  
  在文档和其他博客文章中谈论其中一些其他用例做得非常出色,所以我将在这里回顾一下:
 
- 您可以轻松 实现“切换用户” 功能(有点像 Google 帐户)。查看 文档中令人惊叹的文章以了解更多信息 。
 - Spring Session 了解您的 Spring websocket 流量并将正确地延续 HTTP 会话。这避免了 websocket 标准 的 问题 ,即没有实用的方法从 websocket 流量中延续 HTTP 会话。查看 文档中令人惊叹的文章以了解更多信息 。
 - 您可以为所有事情拉动旋钮和杠杆,包括用于将客户端请求关联到服务器端会话状态的机制: HTTP 标头 ?饼干?还有别的吗?标头方法非常酷 ,如果与 Spring Security 一起使用以保护 REST 服务,可以为您提供一种穷人的 OAuth 访问令牌 。查看 文档中令人惊叹的文章以了解更多信息 。
 
我很幸运上周在春季会议上做了一个网络研讨会。 Rob 给了我 411 一些 可能 在未来版本中的东西:
- 会话并发控制(“让我退出我的其他账户”)
 - Spring Batch 和 Spring Integration 声明检查支持
 - 支持管理帐户 - 优化的持久性(超越 Java 序列化),
 - 更智能、可注入的 bean(与公开为众所周知的请求属性但可以作为 Spring MVC 参数等提供的 beans 相对)
 
谢谢罗布提供的所有重要信息。