可移植、云就绪的 HTTP 会话

工程 | Josh Long | 2015年3月1日 | ...

适用于所有季节(和架构)的框架

Spring 处于一个有趣的平衡点。无论您在哪里运行它,它都能提供很多价值,并且——因为它构建在依赖注入层之上——它在底层和在其上运行的应用程序之间提供了自然的间接层。这种间接性通过解耦促进了代码的可移植性:您的应用程序代码不知道它正在使用的javax.sql.DataSource(或任何其他内容)来自哪里,无论是 JNDI 查找、环境变量,还是 Spring 提供的简单的全新 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,并且已经提供了几年的会话亲和性。直到去年年底,构建包的更改才开始为任何.war基于 Web 应用程序部署到 Apache Tomcat Web 服务器的默认独立配置提供会话复制。但是,此支持不使用组播网络。相反,它使用约定来配置任何绑定 Redis 后端服务以用于 Tomcat 容器的会话复制策略。使用像 Redis 这样的后端服务,或者可能是一些共享文件系统,确实是云中进行会话复制的唯一明智方法。

所有这些方法都有不同的权衡

  • 有些是特定于容器的,这意味着它们不能轻易地从一个环境迁移到另一个环境。
  • 它们可能会为运维带来额外的复杂性(如果您仍然有这个团队!),这会在应用程序和生产之间引入更多摩擦。
  • 它们可能会使用组播网络,这在云环境中效果不佳。
  • 它们可能依赖于像 Cloud Foundry Java 构建包这样的魔术,而该构建包只知道部署到独立 Apache Tomcat 的.war文件,而不是嵌入式.jar文件,或者实际上是 Jetty 等其他 Web 容器。
  • 所有这些其他要点都隐含着一个限制,即持久性策略不可插拔。组播不适合您?太好了,使用 Redis。Redis 不适合您,并且想要使用 Memcache 或其他不受支持的内容?哦……

引入 Spring Session

Spring Session 为所有这些问题提供了一个非常好的解决方案。它是标准 Servlet HTTP Session 抽象的包装器。它易于插入任何应用程序,无论它们是否基于 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?因为它真正地“具有 Web 规模”——查看这篇文章,了解 Twitter 如何将其扩展到 105TB 的 RAM、39MM QPS 和 10,000 多个实例,该文章来自High Scalability博客

为了使此示例能够工作,我已将以下 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 启动器会自动连接到在localhost上运行的 Redis 数据库。如果要将其指向特定位置,请使用各种spring.redis.*属性

此示例尽可能地精简:它只是确认数据是否正在写入 Redis 后端存储。在浏览器中通过localhost:8080/与 Web 应用程序交互后,打开您的redis-cli实用程序。第一个请求将触发一个唯一的会话,该会话将用于缓存uid值。同一浏览器会话的后续请求将看到相同的值。在redis-cli中输入keys *以查看已持久化的内容。

部署到 CloudFoundry

迁移到这个云环境可能会稍微复杂一些。如果您将应用程序部署到 Cloud Foundry,Cloud Foundry 构建包会自动将 Spring Boot 自动配置的RedisConnectionFactory替换为指向绑定到应用程序的 Redis 实例的RedisConnectionFactory。这在您运行在 Cloud Foundry 上,使用正确的构建包,并且应用程序中没有超过一个RedisConnectionFactory的情况下有效。

我将使用 Cloud Foundry 的manifest.yml来描述此应用程序部署到 Cloud Foundry 后应该是什么样子。在这种情况下,它至少需要一个名为redis-session的后端服务,该服务支持 Redis 数据库。我已经将此文件放在项目的根目录下,紧挨着我的 Maven pom.xml。请注意,此manifest.yml贡献了一个环境变量SPRING_PROFILES_ACTIVE,它将激活cloud Spring 配置文件。我们稍后会用到它。


---
applications:
- name: connectors
  memory: 512M
  instances: 1
  host: connectors-${random-word}
  domain: cfapps.io
  path: target/connectors.jar
  services:
    - redis-session
  env:
    SPRING_PROFILES_ACTIVE: cloud
    DEBUG: "true"
    debug: "true"

在推送应用程序之前,您需要创建一个 Redis 实例。我使用了以下命令在 Pivotal Web Services 上创建了一个简单的 Redis 实例(称为redis-session,我们在manifest.yml中引用它),然后推送了应用程序。

cf create-service rediscloud  25mb redis-session
cf push

在这个例子中,我可以在不进行任何修改的情况下部署应用程序,并且一切应该都能正常工作,因为只有一个绑定的后端服务和一个已知类型的 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 限定符注入它,如下所示

   // ..
   @Autowired
   @Qualifier("redis-session")
   RedisConnectionFactory rcf;

这种方法最简单,如果您只是添加 Spring Cloud 启动器依赖项,就会得到这种结果。如果您想显式配置服务,可以通过将以下属性添加到 Spring Boot 的src/main/resources/application.properties中来禁用 Spring Boot 启动器

spring.cloud.enabled=false

然后,显式使用 Spring Cloud PaaS 连接器。Bean 定义**仅**在cloud Spring 配置文件处于活动状态时才会生效。否则,Spring Boot 自动配置将生效(这正是您在本地运行时所期望的)。

package demo;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.Cloud;
import org.springframework.cloud.CloudFactory;
import org.springframework.cloud.service.common.RedisServiceInfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.util.UUID;

@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Bean
	@Profile("cloud")
	RedisConnectionFactory redisConnectionFactory() {
		CloudFactory cloudFactory = new CloudFactory();
		Cloud cloud = cloudFactory.getCloud();
		RedisServiceInfo redisServiceInfo = (RedisServiceInfo) cloud.getServiceInfo("redis-session");
		return cloud.getServiceConnector(redisServiceInfo.getId(),
                     RedisConnectionFactory.class, null);
	}
}

@RestController
class HelloRestController {

	@RequestMapping("/")
	String hello(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在文档和其他博文中对其中一些其他用例进行了精彩的介绍,因此我将在此处进行回顾

我很幸运在上周参加了关于 Spring Session 的网络研讨会。Rob向我介绍了一些**可能**包含在未来版本中的内容

  • 会话并发控制(“注销我的其他帐户”)
  • Spring Batch 和 Spring Integration 认领检查支持
  • 支持管理帐户 - 优化的持久性(超越 Java 序列化)
  • 更智能的可注入 Bean(与作为众所周知的请求属性公开的 Bean 相反,但可以通过其他方式作为 Spring MVC 参数等提供)

感谢 Rob 提供的所有精彩信息。

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部