领先一步
VMware 提供培训和认证,助您加速进步。
了解更多传统上,应用程序的定义取决于它们使用的核心技术。如果你正在构建一个Spring MVC应用程序,我们称之为“Java应用”。由于我们的应用程序主要由Java组件构成,我们倾向于固步自封,不怎么与其他技术打交道,直到被迫交互为止。我们设置基于Java的应用服务器,并且倾向于首先考虑用Java语言来解决应用程序中的问题,而不管Java是否是最佳选择。通常,维护多个应用程序运行时环境太困难了,所以我们仅仅是由于惯性而将自己限制在一个技术栈内。
Cloud Foundry颠覆了这种模式,因为它不再需要我们为了“各取所需”而使用不方便的工具。我们不再被迫将应用程序限制在一个类型中(“Java应用”或“Node应用”)。如果我们只需要高吞吐量、非阻塞式处理并支持XHR长轮询,我们可以为应用程序的这部分使用Node.js。如果我们希望利用Spring系列项目提供的灵活性和丰富的库支持,我们可以通过在应用程序的这部分使用Java来轻松实现。如果我们既需要一个用于缓存或事件总线的快速键值存储,又需要一个功能强大的文档存储来持久化数据,我们可以在同一个应用程序中同时使用它们,而无需担心单独设置和管理这些服务的后勤工作(或者将它们推给我们已经焦头烂额的运维团队)。
此外,无论是哪种类型的应用程序,只需从我们喜欢的Shell中执行一个“push”命令即可部署,这也无损于我们的优势。
那句老话说得好?“值得做的事情,就值得做得过分”?这个示例应用程序就是这种心态的典型代表!
这个应用程序包含几个组件:
第1项和第2项由同一个应用程序处理:即驱动Web前端的Node.js应用程序。第3项是一个标准的Spring MVC应用程序,它使用Spring Data系列项目提供的NoSQL支持,通过一个辅助类连接到Redis和MongoDB。
我们使用Node.js的原因是:a) 它轻量、快速且非阻塞;b) 它是Web世界的拉链帽衫(现在所有酷孩子都在穿)。
说真的,Node.js是部署Web前端的绝佳选择。我们使用它通过Socket.IO异步地将股票代码事件发送到浏览器,通过Mongoose MongoDB库发送到数据库,并通过应用程序的事件总线(在本例中是Redis)发送,以便在另一个应用程序中运行的代码可以消费。
这里的内容很多,我们将分块讲解。
在我们深入研究应用程序之前,我们需要讨论如何从Cloud Foundry环境中获取配置信息。“VCAP_SERVICES”环境变量中存储了一个JSON文档,其中包含了连接到已配置服务的所需主机名、端口、用户名和密码。市面上涌现出各种辅助工具来帮助开发人员使用这些配置值(或在本地运行时使用默认值)。我们在这里将要使用的Node.js模块可能不会完全反映出官方的Node.js Cloud Foundry运行时模块,因为我们在编写本文时该模块仍在开发中。
要在Cloud Foundry中运行时获取连接到MongoDB实例所需的配置信息,请像这样加载“cloudfoundry”模块:
var cf = require("cloudfoundry");
var mongoConfig = cf.getServiceConfig("ticker-analysis")
|| { username: "admin", password: "password", hostname: "localhost", port: 27017, db: "tickeranalysis" };
这会从VCAP_SERVICES环境变量中拉取我们的配置信息,或者提供一组默认值供本地运行时使用。
我们使用Mongoose MongoDB映射库来连接到我们的数据库。我们保存单独的股票代码事件,并读取由Spring MVC应用程序保存的事件。使用文档存储来持久化数据的优点在于它提供了完整的跨语言支持。我们可以使用Spring Data映射基础设施保存一个对象,之后在我们的Node.js应用程序中使用Mongoose读取该对象。
为了配置Mongoose库,我们需要定义我们的模型:
var mongoose = require("mongoose"),
Schema = mongoose.Schema,
ObjectId = Schema.ObjectId,
DocumentObjectId = mongoose.Types.ObjectId;
var TickerEvent = new Schema({
symbol: { type: String },
price: { type: Number },
volume: { type: Number }
});
mongoose.model('TickerEvent', TickerEvent);
var TickerSummary = new Schema({
_id: { type: String },
timestamp: { type: Number },
max: { type: Number },
min: { type: Number },
average: { type: Number },
volume: { type: Number }
});
mongoose.model('TickerSummary', TickerSummary);
Java端相应的域对象如下所示:
@Document(collection = "tickersummary")
public class Summary {
@Id
private final String symbol;
private final Long timestamp;
private Float total = new Float(0);
private Integer samples = 0;
private Float min = Float.MAX_VALUE;
private Float average = new Float(0);
private Float max = Float.MIN_VALUE;
private Integer volume = 0;
// Constructors, getters, and setters...
}
为了驱动Web前端,我们将使用Node.js的express.js Web框架。在此代码块中值得注意的是,我们使用了Cloud Foundry Node.js模块的一个特殊方法来告诉我们是否在云中运行。如果我们在云中运行,那么我们就不想像在开发环境中那样将异常转储到浏览器。
var express = require("express");
var app = express.createServer();
app.configure(function() {
// Standard express setup
app.use(express.methodOverride());
app.use(express.bodyParser());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
// Use the Jade template engine
app.set('view engine', 'jade');
app.set('running in cloud', cf.isRunningInCloud());
// Don't give away information about our environment in production
if(!cf.isRunningInCloud()) {
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
}
});
我们使用Socket.IO进行Ajax长轮询,将服务器端事件传递给监听的浏览器。由于Cloud Foundry尚处于beta阶段,它还不支持完整的WebSocket(但已在计划中)。为了实现这一点,我们将指定Socket.IO使用长轮询,因为我们知道否则动态路由基础设施将无法正常工作。我们还必须每10秒重置一次连接,以防止超时问题导致连接被中断。随着Cloud Foundry平台的演进,这可能会变得无关紧要。但目前,在使用Ajax推送与Cloud Foundry集成时,请记住这些注意事项。
var io = require("socket.io").listen(app, {
transports: ['xhr-polling'],
transportOptions: {
'xhr-polling': {duration: 10000}
}
});
为了生成实际的数据点,我们可以选择订阅任何公开可用的股票行情信息源。由于在这种情况下数据的构成并不重要,并且通过这样做,我们可以更好地展示跨运行时集成,因此我们将随机生成股票代码数据。
为了将这些事件发布到另一个监听应用程序,我们需要使用Redis的发布/订阅功能作为事件总线。在Node.js中执行此操作,我们将设置两个独立的Redis客户端实例。一个将用于监听发送到浏览器的事件,另一个将是出站的发布者客户端。
// Get our Cloud Foundry config information or default to localhost
var redisConfig = cf.getServiceConfig("ticker-stream")
|| { hostname: "localhost", port: 6379, password: false };
// Create Redis client instances
var redisClient = redis.createClient(redisConfig.port, redisConfig.hostname);
var redisPublisher = redis.createClient(redisConfig.port, redisConfig.hostname);
if(redisConfig.password) {
// Cloud Foundry Redis instances are secured with a password
redisClient.auth(redisConfig.password);
redisPublisher.auth(redisConfig.password);
}
redisClient.subscribe("ticker-stream");
redisClient.on("message", function(channel, json) {
var data = JSON.parse(json);
// Save this event to the database
var TickerEvent = db.model('TickerEvent', 'tickerdata');
var te = new TickerEvent({
symbol: data.symbol,
price: data.price,
volume: data.volume
});
te.save(function(err) {
if(err) {
throw(err);
}
});
// Broadcast this event to the browser
io.broadcast(json);
});
为了发送数据,我们有一个辅助方法,我们对其调用setTimeout,并设置一个3-7秒的随机等待时间。
var tickerSender;
function sendTickerEvent() {
var symbolInfo = {
symbol: getRandomSymbol(),
price: getRandomPrice(),
volume: getRandomVolume()
};
redisPublisher.publish("ticker-stream", JSON.stringify(symbolInfo));
// Call ourselves again after 3-7 seconds
tickerSender = setTimeout(sendTickerEvent, getRandomTimeout());
}
我们的Web应用程序的路由相当稀疏。我们需要渲染带有用于驱动UI的JavaScript魔法的主页,并提供一个用于从MongoDB获取摘要文档的路由,以便在用户单击股票代码链接时将其显示在页面的右侧。
app.get("/", function(req, resp) {
resp.render("home", {
pageTitle: "Ticker Analysis Sample"
});
});
app.get("/summary/:symbol", function(req, resp) {
var TickerSummary = db.model("TickerSummary", "tickersummary");
TickerSummary.findById(req.params.symbol, function(err, data) {
if(err) {
// Handle error
}
resp.send(JSON.stringify(data));
});
});
为了初始化我们的数据生成,我们需要确保我们的随机事件发射器正在运行。但是,由于我们不希望在没有人查看页面时数据库填满,所以我们将在用户首次访问我们的应用程序时启动事件发射器。在此之后,我们将一直让它运行,直到“tickerSender”超时被清除(如果需要,您可以添加一个路由来完成此操作)。
// Socket.IO-based Ticker Stream
io.on("connection", function(client) {
if(!tickerSender) {
// Start the ticker stream if one hasn't been already
sendTickerEvent();
}
});
为了告诉Express.js我们的应用程序应该在哪个端口上运行,我们需要读取环境变量VCAP_APP_PORT。我们的Cloud Foundry Node.js模块还有一个方法可以为我们完成此操作。所以我们的listen()调用如下所示:
app.listen(cf.getAppPort());
我们*可以*将此内容保留在同一个技术栈中,并在Node.js中处理摘要计算。但有时,出于非常好的业务原因,我们会在应用程序的某个部分使用Java/Spring组件。我们的目的是在此说明如何做到这一点,以便您可以选择最适合工作的工具。
您可能还记得,在处理Node.js部分时,我们在Cloud Foundry中运行时需要从环境中获取配置参数。我们的Spring应用程序也有同样的需求。由于已经有一个功能强大的Java版Cloud Foundry运行时库,我们将使用它来提取连接到已配置的MongoDB实例所需的片段。
我们需要做的第一件事是声明几个额外的命名空间。一个用于Cloud Foundry运行时,另一个用于MongoDB支持。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:cloud="http://schema.cloudfoundry.org/spring"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://schema.cloudfoundry.org/spring http://schema.cloudfoundry.org/spring/cloudfoundry-spring-0.6.xsd
http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd">
特别值得注意的是,我们将使用Spring 3.1,它仍处于预发布状态。您不必使用Spring 3.1就可以使用Cloud Foundry。但是,Spring 3.1的配置文件功能将使我们的配置更加容易。
为了配置我们的MongoDB连接,我们在本地运行时将使用
<!-- Use this when running locally -->
<beans profile="default">
<util:properties id="serviceProperties">
<prop key="ticker-analysis.db">tickeranalysis</prop>
<prop key="ticker-analysis.username">admin</prop>
<prop key="ticker-analysis.password">passwd</prop>
</util:properties>
<mongo:mongo id="mongo"/>
<bean id="redisConnectionFactory"
class="org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory"/>
</beans>
<!-- Use this when running in the cloud -->
<beans profile="cloud">
<cloud:service-properties id="serviceProperties"/>
<cloud:mongo id="mongo"/>
<cloud:redis-connection-factory id="redisConnectionFactory"/>
</beans>
<!-- MongoDB -->
<mongo:mapping-converter id="mappingConverter"/>
<bean id="mongoTemplate" class="org.springframework.data.document.mongodb.MongoTemplate"
p:username="#{serviceProperties['ticker-analysis.username']}"
p:password="#{serviceProperties['ticker-analysis.password']}">
<constructor-arg ref="mongo"/>
<constructor-arg name="databaseName" value="#{serviceProperties['ticker-analysis.db']}"/>
<constructor-arg name="defaultCollectionName" value="tickerdata"/>
<constructor-arg ref="mappingConverter"/>
</bean>
正如您可能已经注意到的,我们已配置服务的属性遵循convention SERVICE_NAME.PROPERTY_NAME。在此示例中,我有一个名为“ticker-analysis”的已配置MongoDB服务。
> vmc services
============== System Services ==============
... [omitted for brevity]
=========== Provisioned Services ============
+-----------------+---------+
| Name | Service |
+-----------------+---------+
| ticker-stream | redis |
| ticker-analysis | mongodb |
+-----------------+---------+
正如您现在可能猜到的,我的Redis连接也遵循类似的模式。
聪明的读者会立即想知道:“但是它怎么知道使用哪个配置文件呢?” 在我们的例子中,我们将使用一个ApplicationContextInitializer,它会根据是否可用适当的环境变量来设置我们的配置文件。
这是我们在运行时设置配置文件的全部内容,以便我们可以在开发期间使用“default”配置文件,在Cloud Foundry中运行时使用“cloud”配置文件:
public class CloudApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CloudEnvironment env = new CloudEnvironment();
if (env.getInstanceInfo() != null) {
// We're running in the cloud, set the profile accordingly
applicationContext.getEnvironment().setActiveProfiles("cloud");
}
else {
applicationContext.getEnvironment().setActiveProfiles("default");
}
}
}
我们将此ApplicationContextInitializer添加到我们的web.xml中以激活它:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>org.cloudfoundry.services.CloudApplicationContextInitializer</param-value>
</context-param>
</web-app>
我们的Spring层非常简单。我们有一个辅助类,它利用Spring Data Redis支持中的MessageListenerAdapter。每当Redis收到该事件的消息时,我们的bean就会被调用。在该处理程序内部,我们将使用Spring Data MongoDB支持将一个POJO映射到该文档,以便我们可以更新最小值、最大值和平均值。
public void handleMessage(String json) throws IOException {
// Use the Jackson ObjectMapper to turn a JSON document into a POJO
TickerEvent event = mapper.readValue(new StringReader(json), TickerEvent.class);
// Load the existing document or start a new one
Summary summ = mongoTemplate.findOne(query(where("_id").is(event.getSymbol())), Summary.class);
if (null == summ) {
summ = new Summary(event.getSymbol(), System.currentTimeMillis());
}
// Recalculate min, max, and average
summ.addTickerEvent(event);
// Save the modified document back
mongoTemplate.save(summ);
}
我们不需要将Spring层中的任何内容暴露给Web。它离线工作,不需要用户输入,并且不直接向Web客户端提供摘要数据。
即便如此,我们也可以放一个简单的Controller来让我们了解Java辅助类内部发生了什么。我们在示例应用程序中创建了这样一个类。
@Controller
@RequestMapping("/summaries")
public class SummariesController {
@Autowired
private SummaryService summaryService;
@RequestMapping(value = "/", method = RequestMethod.GET)
public @ResponseBody List<Summary> summaries() {
// Return all summaries
return summaryService.getSummaries();
}
@RequestMapping(value = "/{symbol}", method = RequestMethod.GET)
public @ResponseBody Summary summary(@PathVariable String symbol) {
// Return a specific summary document
return summaryService.getSummary(symbol);
}
}
在生产环境中,您不希望这样做。但在Cloud Foundry上开发并试图了解有时感觉像一个黑盒的内容时,放入几个暴露Spring层内部细节的控制器方法可能会有意义。
我不知道您怎么想,但我对许多示例和教程过于简单的性质感到有点厌烦。诚然,这个示例应用程序并不简单!这可能有点像喝消防水龙带,但目标是为您提供足够的内容,让您在探索Cloud Foundry并掌握云[海上]航行技巧时有事可做。
示例应用程序已在Cloud Foundry上运行
所有源代码都在GitHub上的Cloud Foundry示例存储库中
要快速掌握Cloud Foundry,您可以访问论坛:
全面的文档仍在不断变化,因为,说实话,Cloud Foundry平台也是如此。目前它有点像一个移动的目标。社区在github上维护一些Wiki页面,这应该会有所帮助。
前面提到的Node.js模块(用于更轻松地访问Cloud Foundry环境变量)实际上是示例应用程序的一部分,直到Node.js版的Cloud Foundry完全版发布为止。
祝您编码愉快!