作为一名顽固的后端开发人员,每当我考虑在JVM平台上使用某些UI构建Web应用程序时,我都会感到害怕。 这是有原因的:拥有JSF , Liferay , Grails等等的经验,...我不再想走这条路了。 但是,如果有需要,真的有选择的余地吗? 我发现了一个我认为很棒的游戏 : Play Framework 。
Play Framework建立在JVM之上,可以毫不费力地使用Java或Scala创建Web应用程序。 它提供的宝贵和与众不同的区别:静态编译(甚至用于页面模板),易于入门和简洁( 此处有更多信息)。
为了演示Play Framework的出色之处,我想分享一下我开发简单Web应用程序的经验。 假设我们有几台主机,并且希望实时(在图表上)观察每台主机上的CPU使用情况。 当听到“实时”消息时,这可能意味着不同的意思,但是对于我们的应用程序而言,这意味着:使用WebSockets将数据从服务器推送到客户端。 尽管Play Framework支持纯Java API,但我将使用一些Scala代替,因为它使代码非常紧凑和清晰。
让我们开始吧! 下载Play Framework (撰写本文时,最新版本为2.1.1)后,让我们通过输入以下内容来创建应用
play new play-websockets-example
并选择Scala作为主要语言。 难怪在这里:如今这是一种非常标准的方法,对吗?
准备好我们的应用程序后,下一步就是创建一些起始网页。 Play Framework使用基于Scala的自己的类型安全模板引擎,它具有几个非常简单的规则,并且非常容易上手。 这是views / dashboard.scala.html的示例:
@(title: String, hosts: List[Host])
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
<script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript">
<script src="@routes.Assets.at("javascripts/highcharts.js")" type="text/javascript">
</head>
<body>
<div id="hosts">
<ul class="hosts">
@hosts.map { host =>
<li>
<a href="#" onclick="javascript:show( '@host.id' )"><b>@host.name</b></a>
</li>
}
</ul>
</div>
<div id="content">
</div>
</body>
</html>
<script type="text/javascript">
function show( hostid ) {
$("#content").load( "/host/" + hostid,
function( response, status, xhr ) {
if (status == "error") {
$("#content").html( "Sorry but there was an error:" + xhr.status + " " + xhr.statusText);
}
}
)
}
</script>
除了有趣的结构(这是很好的描述了轿跑车在这里 ),它看起来非常像一个JavaScript代码的常规HTML。 该网页的结果是浏览器中主机的简单列表。 每当用户单击特定主机时,都会从服务器获取另一个视图(使用旧伙伴AJAX ),并在主机的右侧显示。 这是第二个(也是最后一个)模板views / host.scala.html :
@(host: Host)( implicit request: RequestHeader )
<div id="content">
<div id="chart">
<script type="text/javascript">
var charts = []
charts[ '@host.id' ] = new Highcharts.Chart({
chart: {
renderTo: 'chart',
defaultSeriesType: 'spline'
},
xAxis: {
type: 'datetime'
},
series: [{
name: "CPU",
data: []
}]
});
</script>
</div>
<script type="text/javascript">
var socket = new WebSocket("@routes.Application.stats( host.id ).webSocketURL()")
socket.onmessage = function( event ) {
var datapoint = jQuery.parseJSON( event.data );
var chart = charts[ '@host.id' ]
chart.series[ 0 ].addPoint({
x: datapoint.cpu.timestamp,
y: datapoint.cpu.load
}, true, chart.series[ 0 ].data.length >= 50 );
}
</script>
它看起来像是一个片段,而不是一个完整HTML页面,该页面只有一个图表,并通过侦听器打开WebSockets连接。 在Highcharts和jQuery的巨大帮助下,JavaScript编程对于后端开发人员而言从未像现在这样容易。 此时,UI部分已完成。 让我们继续到后端。
首先,让我们定义路由表,该表仅包含三个URL,默认情况下位于conf / routes :
GET / controllers.Application.index
GET /host/:id controllers.Application.host( id: String )
GET /stats/:id controllers.Application.stats( id: String )
定义好视图和路线后,就该填充最后一个最有趣的部分了,这些控制器将所有部分粘合在一起(实际上,只有一个控制器, controllers / Application.scala )。 这是一个将索引操作映射到由views / dashboard.scala.html模板化的视图的代码段,就这么简单:
def index = Action {
Ok( views.html.dashboard( "Dashboard", Hosts.hosts() ) )
}
对此操作的解释听起来像是这样:返回成功的响应代码,并使用两个参数title和hosts渲染模板views / dashboard.scala.html作为响应主体。 处理/ host /:id的动作看起来几乎相同:
def host( id: String ) = Action { implicit request =>
Hosts.hosts.find( _.id == id ) match {
case Some( host ) => Ok( views.html.host( host ) )
case None => NoContent
}
}
这是在models / Hosts.scala中定义的Hosts对象。 为了简单起见,主机列表是硬编码的:
package models
case class Host( id: String, name: String )
object Hosts {
def hosts(): List[ Host ] = {
return List( new Host( "h1", "Host 1" ), new Host( "h2", "Host 2" ) )
}
}
无聊的部分结束了,让我们继续最后但并非最不重要的实现:使用WebSockets服务器推送主机的CPU统计信息。 如您所见, / stats /:id URL已经映射到控制器操作,因此让我们看一下它的实现:
def stats( id: String ) = WebSocket.async[JsValue] { request =>
Hosts.hosts.find( _.id == id ) match {
case Some( host ) => Statistics.attach( host )
case None => {
val enumerator = Enumerator
.generateM[JsValue]( Promise.timeout( None, 1.second ) )
.andThen( Enumerator.eof )
Promise.pure( ( Iteratee.ignore[JsValue], enumerator ) )
}
}
}
这里没有太多代码,但是如果您对Play Framework中的 WebSockets感到好奇,请单击此链接 。 这两行乍看起来可能有些怪异,但是一旦您阅读了文档并了解了Play Framework的基本设计原理,它就会看起来更加熟悉和友好。 Statistics对象是真正完成任务的对象,让我们看一下代码:
package models
import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
import akka.actor.ActorRef
import akka.actor.Props
import akka.pattern.ask
import akka.util.Timeout
import play.api.Play.current
import play.api.libs.concurrent.Akka
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.iteratee.Enumerator
import play.api.libs.iteratee.Iteratee
import play.api.libs.json.JsValue
case class Refresh()
case class Connect( host: Host )
case class Connected( enumerator: Enumerator[ JsValue ] )
object Statistics {
implicit val timeout = Timeout( 5 second )
var actors: Map[ String, ActorRef ] = Map()
def actor( id: String ) = actors.synchronized {
actors.find( _._1 == id ).map( _._2 ) match {
case Some( actor ) => actor
case None => {
val actor = Akka.system.actorOf( Props( new StatisticsActor(id) ), name = s"host-$id" )
Akka.system.scheduler.schedule( 0.seconds, 3.second, actor, Refresh )
actors += ( id -> actor )
actor
}
}
}
def attach( host: Host ): Future[ ( Iteratee[ JsValue, _ ], Enumerator[ JsValue ] ) ] = {
( actor( host.id ) ? Connect( host ) ).map {
case Connected( enumerator ) => ( Iteratee.ignore[JsValue], enumerator )
}
}
}
与往常一样,由于Scala的简洁性,没有太多的代码,但是发生了很多事情。 由于我们可能有数百个主机,因此合理地为每个主机分配自己的工作线程(而不是线程),或者更确切地说,为自己的参与者 。 为此,我们将使用另一个令人称奇的库Akka 。 上面的代码片段只是为主机创建一个actor ,或者使用已经创建的actor的注册表中的现有actor 。 请注意,该实现已相当简化,并省略了重要细节。 正确方向的想法是使用主管和其他高级概念来代替同步阻止。 还值得一提的是,我们希望将actor安排为预定任务:我们要求actor系统每3秒向actor发送一条“ 刷新”消息。 这意味着图表也将每三秒钟更新一次新值。
因此,当创建主机的actor时,我们向他发送一条消息Connect,通知正在建立新的连接。 当收到响应消息Connected时 ,我们从该方法返回,并且此时将通过WebSockets建立连接。 请注意,我们有意使用Iteratee.ignore [JsValue]忽略了来自客户端的任何输入。
这是StatisticsActor实现:
package models
import java.util.Date
import scala.util.Random
import akka.actor.Actor
import play.api.libs.iteratee.Concurrent
import play.api.libs.json.JsNumber
import play.api.libs.json.JsObject
import play.api.libs.json.JsString
import play.api.libs.json.JsValue
class StatisticsActor( hostid: String ) extends Actor {
val ( enumerator, channel ) = Concurrent.broadcast[JsValue]
def receive = {
case Connect( host ) => sender ! Connected( enumerator )
case Refresh => broadcast( new Date().getTime(), hostid )
}
def broadcast( timestamp: Long, id: String ) {
val msg = JsObject(
Seq(
"id" -> JsString( id ),
"cpu" -> JsObject(
Seq(
( "timestamp" -> JsNumber( timestamp ) ),
( "load" -> JsNumber( Random.nextInt( 100 ) ) )
)
)
)
)
channel.push( msg )
}
}
CPU统计信息是随机生成的, 参与者仅每3秒将其作为简单的JSON对象广播一次。 在客户端,JavaScript代码解析此JSON并更新图表。 这是Mozilla Firefox中两个主机( 主机1和主机2)的外观:
最后,我个人迄今为止对Play Framework所做的工作感到非常兴奋。 仅花了几个小时即可上手,又花了几个小时使事情按预期进行。 来自运行应用程序的错误报告和反馈周期绝对很棒,这要归功于Play Framework团队及其周围的社区。 我还有很多东西要学,但是值得去做。 请在GitHub上找到完整的源代码。