便携式、云就绪的 HTTP 会话

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前, 星球 内第2个项目《仿小红书(微服务架构)》正在更新中。第1个项目:全栈前后端分离博客项目已经完结,演示地址:http://116.62.199.48/。采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 255 小节,累计 39w+ 字,讲解图:1716 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 1300+ 小伙伴加入,欢迎点击围观

春天走的是一条有趣的路线。无论您在哪里运行它,它都能提供很多价值,而且 - 因为它建立在依赖注入层上 - 它在底层和运行在它之上的应用程序之间提供了一种自然的间接。这种间接通过解耦提高了代码的可移植性:您的应用程序代码不知道它使用的 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 在文档和其他博客文章中谈论其中一些其他用例做得非常出色,所以我将在这里回顾一下:

    我很幸运上周在春季会议上做了一个网络研讨会。 Rob 给了我 411 一些 可能 在未来版本中的东西:

    • 会话并发控制(“让我退出我的其他账户”)
    • Spring Batch 和 Spring Integration 声明检查支持
    • 支持管理帐户 - 优化的持久性(超越 Java 序列化),
    • 更智能、可注入的 bean(与公开为众所周知的请求属性但可以作为 Spring MVC 参数等提供的 beans 相对)

    谢谢罗布提供的所有重要信息。