Play2 / sbt 操作指南

2,304 阅读3分钟

Play framework 抛弃了传统的 Java Web 框架模式,选择了「响应式」( Reactive ) 编程。Play 框架具备以下特点:

  • 响应式 ( Resposive ):快速响应用户行为。
  • 可伸缩 ( Scalable ):负载方面,Play能够很好的水平拓展。
  • 事件驱动 ( Event-driven ):基于事件 ( Action ) 模型实现 HTTP Server。

Play 是快速开发的利器,遵从ROR ( Ruby on Rails ) 的开发风格,支持使用 Scala 和 Java 开发,适用于小型的个人项目

👉想要了解更多特性。 | Play2 (Java/Scala)技術框架 (wisdomfish.org)

本文演示了以下内容的配置和安装:SBT,MongoDB,Play2。

1. 开发环境需求

  1. 要求 jdk 的版本在1.8+。
  2. 安装 sbt。
  3. Intellij IDEA 2018+ 版本需要安装 scala 插件。
  4. MongoDB 版本在 3.6 及以上。
  5. 项目 sdk 不要超过 2.13 版本。(有些插件没有及时更新)

2. 开发语言及版本选择

工具版本
开发环境Windows 10
jdk1.8.0_241
sdk(项目的)2.12.1
MongoDB3.6.x
sbt1.3.10
Play framework2.8.x
开发语言scala

play scala 目前没有对 Mysql 提供更好的支持,因此要意味着要基于原生的 jdbc 组件开发,因此本项目转用 MongDB 数据库进行开发。

3. Play 项目的基本格局

app                      → 项目源码目录
 └ controllers           → 控制层
 └ models                → 业务层
 └ views                 → 模板层
build.sbt                → 项目构建脚本
conf                     → 配置文件 (on classpath)
 └ application.conf      → 主配置文件
 └ routes                → 路由配置
public                   → 静态文件目录
 └ stylesheets           → CSS 文件
 └ javascripts           → Javascript 文件
 └ images                → Image 文件
project                  → sbt 配置文件
 └ build.properties      → 配置sbt版本
 └ plugins.sbt           → 配置sbt插件
lib                      → 第三方jar包依赖
logs                     → 日志目录
 └ application.log       → 默认日志文件
target                   → 构建时动态生成目录
 └ resolution-cache      → 依赖信息
 └ scala-2.xx
    └ api                → 生成的API文档
    └ classes            → 编译后的class文件
    └ routes             → 从routes文件生成的源码
    └ twirl              → 从模板文件生成的源码
 └ universal             → 项目打包目录
test                     → 测试目录
.gitignore               → git忽略列表配置

注意,sbt 不会主动编译第三方依赖 lib 内的 jar 包,如果需要使用内部开发的 jar 包,则需要上网上查询 assembly 命令相关用法。

4. sbt 安装并配置国内镜像

自行在 /conf 目录下创建一个 repo.properties 文件,更改镜像地址。有些论坛还会带上 maven-central 行,这会导致 sbt 仍然优先从 maven 仓库下载文件,所以需要将这行去除。sbt 在寻找依赖时,会依次按配置顺序从镜像中下载依赖。

[repositories]
local
huaweicloud-maven: https://repo.huaweicloud.com/repository/maven/
aliyun: https://maven.aliyun.com/nexus/content/groups/public/
mvn: https://repo1.maven.org/maven2/
sbt-plugin-repo: https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]

Windows 环境下则需要在 $SBT_PATH/sbt/conf/sbtconfig.txt 进行额外配置,指定 sbt 的下载路径。其中 C:/sbt 需要由实际的安装路径进行替换,然后通过 -Dsbt.repository.config 选项将刚才的镜像配置包括进来:

# sbt configuration file for Windows

# Set the java args

#-mem 1024 was added in sbt.bat as default

#-Xms1024m
#-Xmx1024m
#-Xss4M
#-XX:ReservedCodeCacheSize=128m

# Set the extra sbt options
-Dsbt.log.format=true
-Dsbt.boot.directory=C:/sbt/.sbt/boot
-Dsbt.ivy.home=C:/sbt/.ivy2
-Dsbt.global.base=C:/sbt/.sbt
-Dsbt.repository.config=C:/sbt/conf/repository.properties

在 IntelliJ IDEA 中配置这些 -Dsbt 打头的参数行。

sbt2idea.png

5. 在 IntelliJ IDEA 中创建 Play2 项目

推荐使用 LightBend Project Starter 来快速生成一个架构完备的 Play 框架。

playStart.gif

注意,截止 2020 年 5 月份为止,该 seed 使用的默认 Scala 版本是 2.13.1,有很多依赖工具并没有适配 Scala 2.13 版本,因此需要自行在 build.sbt 中将版本降低到2.12.x版本。

6. 关于 Play2

6.1 映射请求的基本原理

浏览器内输入 url 请求某服务器上的 Play2 服务(默认是 9000 端口)。程序会去 routes 文件中根据 url 映射到 controllers 文件夹下的指定 Controller 的方法去执行。(注:遵守sbt构建项目时约定俗成的项目结构)

GET        /doLogin               controllers.LoginController.doLogin

示例:用户输入 localhost:9000/doLogin,该请求被转发到 controllers.LoginController.doLogin 去处理。根据业务逻辑的不同,可以返回一个简单内容,也可以返回一个 html 视图。并且这个行为被称之为 Action

image-20200430225355746.png

6.2 Play 基于 Action 处理请求

Controller返回的Action实际上是一个处理Http请求的函数。这个函数描述了一个抽象的请求接收并处理的过程:接收Request,处理请求,并返回Response。

Action{Request=>
	//process this request.
    Response
}

Response可以分为以下几种,注释后表示的是对应的HTTP请求状态码:

Ok(response) // 200
Redirect(url) // 302
BadRequest(response) // 400
Unauthorized(response) // 401
Forbidden(response) // 403
NotFound(response) // 404
InternalServerError(response) // 500
ServiceUnavailable(response) // 503

6.3 Twirl 模板被映射成模板函数

views文件夹下面放置着xxx.scala.html命名的文件,称之为模板文件,基于Play内置的Twirl引擎来进行渲染。它们被映射为了同名的views.html.xxx函数(Scala将函数视作是一等公民)。

Controller 在处理完业务之后将 login.scala.html 模板返回,则它会像调用函数一样,将一些变量 ( 如 title ) 拼装到模板 ( 如 login.scala.html ) 后,返回给浏览器展示:

  def login() = Action {
    //=>login.scala.html,并将"请登录"赋值给了title变量。  
    Ok(views.html.login(title = "请登录"))
  }

而这个模板文件中存放着两类:等待动态插入的参数值(通过Controller在调用时传入参数),还有一个 html 文档。@ 后面对应着 views.html.login 的参数列表。不过这个函数的返回值比较特殊:它返回的是一个渲染 html 文件。可以像理解 Jsp 一样去理解它的工作流程。

@(title :String)
<!doctype html>
<html lang="en">
    <head>...</head>
    <body>...</body>
</html>

6.4 引用Play-Json包

根据官方文档的指示,引 play-json 包:📦Github Link

built.sbt 文件中添加依赖,并刷新 Refresh project

libraryDependencies += "com.typesafe.play" %% "play-json" % "2.8.1"

给出一个 Json 的简单实例:

//play-json创建一个Json对象。
val returnJson: JsObject = Json.obj(
    "name" -> "Lijunhu",
    "emails" -> "376781642@qq.com",
    "address" -> Json.obj(
        "province" -> "HeiLongJiang",
        "city" -> "Herbin"
    )
)

从这个变量中提取 emails 字段:

val emails :String = (returnJson \ "emails")
.getOrElse(Json.obj("ERR-EMAILS-VALUE"->"null")).toString()

6.5 尝试 case class 和 JsonObj 之间的相互转换

//命名空间 | namespace
import play.api.libs.json._

//创建一个case class实例。 | create an instance of User object.
val userObj = User("02","Kate","JavaScript")

//创建一个符合case class格式的Json。 | create an instance of Json object in User format.
val userJson = Json.obj("_id"->"03","userName"->"Tim","department"->"Java")

//将userJson 转化为 User实例。 | convert a User obj to User Json.
val objValue: JsResult[User] = Json.fromJson[User](userJson)

//将User 转化为符合类型的 Json. | convert a User Json to User object.
val jsValue: JsValue = Json.toJson[User](userObj)

7. 使用 MongoDB 作为数据库 ( Windows 平台 )

📦官方链接:MongoDB

下载.msi文件可以省去自行配置环境变量的过程。注意,如果本机已经装载了旧版本的 MongoDB 数据库,请额外留意环境变量。

本框架后面使用的 play-mongo 组件要求 MongoDB 的版本至少为3.6,在安装过程中还可以选择额外安装一个界面管理工具 MongoDBCompassCommunity

viewMon.gif

7.1 两种方式运行MongoDB服务

7.1.2 命令行下直接启动

在启动服务时指定数据的存储路径。

C:\mongodb\bin\mongod --dbpath c:\data\db

7.1.3 作为服务启动(推荐)

a. 需要创建一个mongodb.cfg文件,配置日志和数据的存储路径。

systemLog:
    destination: file
    path: c:\data\log\mongod.log
storage:
    dbPath: c:\data\db
  • 配置文件中不要使用Tab符号,使用空格。
  • :符号后面有空格
  • systemLog一项需要配置文件,而非目录。

b. 随后以管理员身份运行cmd,并输入:

C:\mongodb\bin\mongod.exe --config C:\mongodb\mongod.cfg --install
  • --config参数需要输入绝对路径,文件名以实际配置为准。

c. 以管理员身份执行以下命令(非管理员会出现拒绝访问:5的问题):

>net start MongoDB	# 开启服务
>net stop MongoDB	# 关闭服务
>mongod	--remove	# 移除此安装的服务。

7.2 检查 MongoDB 是否启动成功

在浏览器中输入:http://127.0.0.1:27017/。MongoDB 的默认端口为 27017。若服务已正常启动,则浏览器页面显示:

It looks like you are trying to access MongoDB over HTTP on the native driver port.

7.3 连接本机 MongoDB

简单运行以下命令:

mongo

如果连接成功,则命令行窗口显示:

MongoDB shell version: x.x.x connecting to: test

7.4 创建 MongoDB 用户

创建一个 root 级别用户 admin,和一个数据库用户 std,赋予对该数据库的任意读写 ( dbOwner ) 权限。

> db.createUser({user:"admin",pwd:"ljh20176714",roles:["root"]})
Successfully added user: { "user" : "admin", "roles" : [ "root" ] }

> db.createUser({user:"std",pwd:"ljh20176714",roles:[{role:"dbOwner",db:"studentDB"}]})
Successfully added user: {
        "user" : "std",
        "roles" : [
                {
                        "role" : "dbOwner",
                        "db" : "studentDB"
                }
        ]
}
>       

8. 使用 play mongo 搭建 MongDB 驱动

📦Github官网:Github tutorial

build.sbt当中添加以下依赖:

libraryDependencies += "cn.playscala" % "play-mongo_2.12" % "0.3.0"
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

注:当 sbt 反复提示 not found 错误时,除了切换镜像以外,要考虑到是否插件版本和目前的 Scala 版本不匹配。为了检查这一点,可以通过鼠标点击控制台显示的 repo1 镜像仓库链接查看支持的版本,然后进行适当调整。

随后,在 conf/application.conf 文件中,添加 MongoDB 设置。

# 配置数据库连接 | config your own dataBase source here.
mongodb.uri = "mongodb://localhost:27017/studentDB"

8.1 使用注解注入模型

有关该部分可参照 play mongo 官方的详细教程。

app/ 目录下建立 Module.scala,配置所有模型所在的目录在 app/models 下。

import cn.playscala.mongo.Mongo
import com.google.inject.AbstractModule

class Module extends AbstractModule{
  override def configure() :Unit= {
    Mongo.setModelsPackage("models")
  }
}

对每个模型进行注入。注解内的 common-person 对应 MongoDB 数据库中的集合 collection。Person 则是该集合下的每一个 Document

  • 使用 case 将这个伴生类声明为模板类,由底层来提供 apply , unapply 等伴生方法。

  • 注意,对每个 Model,推荐自行定义其 _id。这个字段用作 MongDB 的主键。如果不主动声明,则 MongoDB 会生成一个随机串。

package models

import cn.playscala.mongo.annotations.Entity

@Entity("common-person")
case class Person(_id :String,name:String,sno :String)

8.2 建立模型-Json的转换

app/models下创建包对象,提供 Json -> 模板类的**内部 ( 仅在 models 包下生效 ) **隐式转换:

package object models {
    implicit val personFormat = Json.format[Person]
}

9. Play 的并发编程

Play 2.x 框架的并发机制依赖于 Akka ( Spark 底层的通讯机制也是依赖 Akka 完成的 )。当你使用 mongo 驱动执行一条查询语句时,它返回的是一个Future[T]。它有点类似一个视图 view,只有线程真正的执行这条语句时,我们才能才得到内部的 T

因此为了等待某个线程为我们返回结果,我们使用 Await.result(..) 来阻塞它,

val maxAwaitTime = Duration(10000, "millis")

def studentList = Action {

    //注意!它返回的是一个Future[List]。
    //详情请参考Scala异步编程相关内容:Akka,Duration,Future.Await。
    val eventualObjects: Future[List[JsObject]] = mongo.collection[Student].find[JsObject]().list()

    //阻塞等待,并将最终结果返回。
    val studentList: List[JsObject] = Await.result(eventualObjects, maxAwaitTime)

    //Ok(JsArray(studentList))
    Ok(views.html.students(studentList))
}

10. 部署 Play2 框架

Play 框架可以通过插件打包成 war 包,然后部署在 Tomcat 服务器下。但是在这里我们练习如何用 sbt 工具进行部署。在此之前,我们首先需要先配置 play2 框架的 Secret 。如果没有配置此项,则 Play 2 框架在部署环境中启动会出错。

为什么要配置它?官网给出的解释是用于抵御 CSRF 攻击的 token,或者是用于加密工具中。有两种配置方式,第一种是直接在项目文件中的 conf/application.conf 添加(但官方并不推荐这种方式),另一种是在启动时作为参数添加。详细请参阅本小节附上的链接。

application.conf中配置以下行,密码值自行设定。

play.http.secret.key = "Your own secret"

额外的,如果对于运行在 Windows 平台的用户,最好在 build.sbt 处为你的项目添加 LauncherJarPlugin 插件。否则 cmd 在执行时可能会因为 *.bat 的参数过长而导致服务启动失败。

lazy val root = (project in file(".")).enablePlugins(PlayScala,LauncherJarPlugin)

只需要在项目根目录处执行 sbt dist 命令,sbt 便会自动地将 Play2 项目打包成一个 *.zip 压缩包。( 笔者使用 IntelliJ IDEA 开发,它在下方提供了一个 terminal 终端窗口。sbt 会将压缩包放入到..\target\universal目录下)。

我们只需要将这个压缩包提取出来,然后进入到 /bin 目录下,可以直接找到两个启动脚本:对于 Windows 环境,我们只需要直接打开 .bat 批处理文件;对于Linux 环境,我们 cd 到 bin 目录下执行它即可(这两个脚本文件都与你的项目名保持一致):

$ sudo ./{scalaProject}

额外提醒一点:在 Linux 环境下解压此压缩包时如果遇到了not in gzip format问题,则解压时的参数不要带上-z

$ sudo tar -xvf {yourZip} -C {path}

📦有关于配置Play Secret的官方说明

📦Play 2是一个全栈框架

📦解决Linux上解压jdk报错gzip: stdin: not in gzip format

11. 最简化版本的Github 链接

笔者基于 Play Scala Seed 生成了一个最“便携”版本的 Play-MongoDB 项目,没有任何额外的模型层,仅有基于一个简单 MongoDB 配置的MongoDBController

📦Play scala mongo seed

12. 参考文献

  1. OSCHINA:Play 2.0用户指南
  2. 博客园:Play2.6-Scala指南
  3. 码云:基于PlayFramework2.5.4的后端程序
  4. Playframework官方
  5. IntelliJ IDEA创建Play2项目的几种方式
  6. 国内的PlayScala社区
  7. Play For Scala 开发指南(2.6.x)
  8. 未来可能会出现的跨域问题
  9. 依赖注入
  10. eBooks:Play for Scala
  11. OnlineView:Play for Scala
  12. Windows环境下安装MongoDB服务
  13. 安装MongoDB时遇见的问题
  14. playFrameWork专栏
  15. MvnRepository
  16. Jnuit Class not found...
  17. CSRF防护
  18. Json与Txt相互转换