领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多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 这样的后端服务,或者可能是一些共享文件系统,确实是云中进行会话复制的唯一明智方法。
所有这些方法都有不同的权衡
.war
文件,而不是嵌入式.jar
文件,或者实际上是 Jetty 等其他 Web 容器。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 *
以查看已持久化的内容。
迁移到这个云环境可能会稍微复杂一些。如果您将应用程序部署到 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向我介绍了一些**可能**包含在未来版本中的内容
感谢 Rob 提供的所有精彩信息。