Play for Scala-Define Http interface
更新日期:
- 1. Benefits of good URL design
- 2. Controllers—the interface between HTTP and Scala
- 2.1. Controller classes and action methods
- 2.2. HTTP and the controller layer’s Scala API
- 2.3. Action composition
- 3. Routing HTTP requests to controller actions
- 3.1. Router configuration
- 3.2. Matching URL path parameters that contain forward slashes
- 3.3. Constraining URL path parameters with regular expressions
- 3.4. Binding HTTP data to Scala objects
- 4. Generating HTTP calls for actions with reverse routing
- 5. Generating a response
Benefits of good URL design
If you don’t think changing the URL matters, then this is probably a good time to read Cool URIs Don’t Change, which Tim Berners-Lee wrote in 1998 (http://www.w3.org/Provider/Style/URI.html), adding to his 1992 WWW style guide, which is an important part of the documentation for the web itself.
ervlet API URL mapping is too limited to handle even our first three example URLs, because it only lets you match URLs exactly, by prefix or by file extension. What’s missing is a notion of path parameters that match variable segments of the URL, using URL templates:
1 2 | /product/{ean}/edit /product/(\d+)/edit |
Here are several benefits of good URL design:
- A consistent public API —The URL scheme makes your application easier to understand by providing an alternative machine-readable interface.
- The URLs don’t change—Avoiding implementation-specifics makes the URLs sta- ble, so they don’t change when the technology does.
- Short URLs—Short URLs are more usable; they’re easier to type or paste into other media, such as email or instant messages.
Controllers—the interface between HTTP and Scala
Controllers are the application components that handle HTTP requests for application resources identified by URLs.
In Play, you use controller classes to make your application respond to HTTP requests for URLs, such as the product catalog URLs:
1 2 3 | /products /product/5010255079763 /product/5010255079763/edit |
Controller classes and action methods
We’ll start by defining a Products controller class, which will contain four action methods for handling different kinds of requests: list, details , edit, and update
A controller is a Scala object that’s a subclass of play.api.mvc.Controller
,
which provides various helpers for generating actions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package controllers import play.api.mvc.{Action, Controller} object Products extends Controller { def list(pageNumber: Int) = Action { NotImplemented } def detail(ean: Long) = Action { NotImplemented } def edit(ean: Long) = Action { NotImplemented } def update(ean: Long) = Action { NotImplemented } } |
GROUP CONTROLLERS BY MODEL ENTITY
Create one controller for each of the key entities in your application’s high-level data model. For example, the four key entities—Product, Order, Warehouse, and User—might correspond to a data model with more than a dozen entities. In this case, it’d probably be a good idea to have four controller classes: Products, Orders, Warehouses, and Users.
In Play, each controller is a Scala object that defines one or more actions. Play uses an object instead of a class because the controller doesn’t have any state; the controller is used to group some actions.
DON’T DEFINE A var IN A CONTROLLER OBJECT
Each action is a Scala function that takes an HTTP request and returns an HTTP result.
Request[A] => Result
The controller layer is therefore the mapping between stateless HTTP requests and responses and the object-oriented model. In MVC terms, controllers process events (HTTP requests in this case), which can result in updates to the model. Controllers are also responsible for rendering views.
HTTP and the controller layer’s Scala API
Play models controllers, actions, requests, and responses as Scala traits in the
play.api.mvc
package—the Scala API for the controller layer.
MVC API traits and classes correspond to HTTP concepts and act as wrappers for the corresponding HTTP data:
play.api.mvc.Cookie
—An HTTP cookie: a small amount of data stored on the client and sent with subsequent requestsplay.api.mvc.Request
—An HTTP request: HTTP method, URL, headers, body, and cookiesplay.api.mvc.RequestHeader
—Request metadata: a name-value pairplay.api.mvc.Response
—An HTTP response, with headers and a body; wraps a Play Resultplay.api.mvc.ResponseHeader
—Response metadata: a name-value pair
Play controllers use the following concepts in addition to HTTP concepts:
play.api.mvc.Action
— A function that processes a client Request and returns a Resultplay.api.mvc.Call
— An HTTP request: the combination of an HTTP method and a URLplay.api.mvc.Content
— An HTTP response body with a particular content typeplay.api.mvc.Controller
— A generator for Action functionsplay.api.mvc.Flash
— A short-lived HTTP data scope used to set data for the next requestplay.api.mvc.Result
— The result of calling an Action to process a Request, used to generate an HTTP responseplay.api.mvc.Session
— A set of string keys and values, stored in an HTTP cookie
Action composition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def list = Action { // Check authentication. // Check for a cached result. // Process request... // Update cache. } // 可以这样写 def list = Authenticated { Cached { Action { // Process request... } } } |
This example uses Action to create an action function that’s passed as a parameter to Cached, which returns a new action function. This, in turn, is passed as a parameter to Authenticated, which decorates the action function again.
Routing HTTP requests to controller actions
Once you have controllers that contain actions, you need a way to map different request URLs to different action methods.
In Play, mapping the combination of an HTTP method and a URL to an action method is called routing.
Router configuration
The great thing about this approach is that your web application’s URLs—its public HTTP interface—are all specified in one place, which makes it easier for you to maintain a consistent URL design.
The routes file structure is line-based: each line is either a blank line, a comment line, or a route definition. A route definition has three parts on one line, separated by whitespace.
conf/routes
1 2 3 4 5 6 7 8 9 10 11 | GET / controllers.Products.home() GET /products controllers.Products.list() GET /products controllers.Products.list(page: Int ?= 1) ## Option[Int] GET /products controllers.Products.list(page: Int = 1) ## Int GET /product/:ean controllers.Products.details(ean: Long) GET /product/:ean/edit controllers.Products.edit(ean: Long) |
The benefit of this format is that you can see your whole URL design in one place, which makes it more straightforward to manage than if the URLs were specified in many different files.
Matching URL path parameters that contain forward slashes
/photo/5010255079763.jpg
/photo/customer-submissions/5010255079763/42.jpg
/photo/customer-submissions/5010255079763/43.jpg
1 2 3 4 5 | ## 这样上面的配置不会匹配 斜杠(slash) GET /photo/:file controllers.Media.photo(file: String) ## 必须这样 GET /photo/*file controllers.Media.photo(file: String) |
Constraining URL path parameters with regular expressions
为如下的url做匹配
/product/5010255079763
/product/paper-clips-large-plain-1000-pack
正则表达式要写在<>
里面
1 2 3 | GET /product/$ean<\d{13}> controllers.Products.details(ean: Long) GET /product/:alias controllers.Products.alias(alias: String) |
Binding HTTP data to Scala objects
Play, along with other modern web frameworks such as Spring MVC, improves on treating HTTP request parameters as strings by performing type conversion before it attempts to call your action method.
Here’s what happens when Play’s router handles the request PUT /product/5010255079763
- The router matches the request against configured routes and selects the route:
PUT /product/:ean controllers.Products.update(ean: Long)
- The router binds the ean parameter using one of the type-specific binders—in this case, the Long binder converts 5010255079763 to a Scala Long object
- The router invokes the selected route’s Products.update action, passing
5010255079763L
as a parameter.
If you send an HTTP request for /product/x
,
the binding will fail because x isn’t a number, and Play
will return an HTTP response with the 400 (Bad Request)
status code and an error page
Generating HTTP calls for actions with reverse routing
In addition to mapping incoming URL requests to controller actions, a Play application can do the opposite: map a particular action method invocation to the corresponding URL.
Hardcoded URLs
The interesting part is what happens next, after the product is deleted. Let’s suppose that after deleting the product, we want to show the updated product list. We could render the product list page directly, but this exposes us to the double-submit problem: if the user “reloads” the page in a web browser, this could result in a second call to the delete action, which will fail because the specified product no longer exists.
The standard solution to the double-submit problem is the redirect-after-POST pattern: after performing an operation that updates the application’s persistent state, the web application sends an HTTP response that consists of an HTTP redirect.
1 2 3 4 | def delete(ean: Long) = Action {
Product.delete(ean)
Redirect("/proudcts")
}
|
This looks like it will do the job, but it doesn’t smell too nice because we’ve hardcoded the URL in a string.
解决办法如下
Reverse routing
You can do reverse routing by writing Scala code.
1 2 3 4 | def delete(ean: Long) = Action {
Product.delete(ean)
Redirect(routes.Products.list())
}
|
Keeping these two points in mind:
- Routing is when URLs are routed to actions—left to right in the routes file
- Reverse routing is when call definitions are “reversed” into URL s—right to left
1 2 | scala> val call = controllers.routes.Products.list()
scala> val (method, url) = (call.method, call.url)
|
Generating a response
An HTTP response consists of an HTTP status code, optionally followed by response headers and a response body. Play gives you total control over all three, which lets you craft any kind of HTTP response you like, but it also gives you a convenient API for handling common cases.
Debugging HTTP responses
To use cURL, use the --request
option to specify the HTTP method and
--include
to include HTTP response headers in the output
1 | curl --request GET --include http://localhost:9000/products |
Response body
The response body will consist of this representation, in some particular format.
- Plain text—Such as an error message, or a lightweight web service response
- HTML —A web page, including a representation of the resource as well as application user-interface elements, such as navigation controls
- JSON —A popular alternative to XML that’s better suited to Ajax applications
- XML —Data accessed via a web service
- Binary data—Typically nontext media such as a bitmap image or audio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // plain text def version = Action { Ok("Version 2.0") } // html def index = Action { Ok(views.html.index()) } // json def json = Action { import play.api.libs.json.Json val success = Map("status" -> "success") val json = Json.toJson(success) Ok(json) } // scala.xml.NodeSeq def xml = Action { Ok(<status>success</status>) } |
BINARY DATA
In Play, returning a binary result to the web browser is the same as serving other formats: as with XML and JSON, pass the binary data to a result type.
条形码生成
1 2 3 | val appDependencies = Seq(
"net.sf.barcode4j" % "barcode4j" % "2.0"
)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | def ean13Barcode(ean: Long, mimeType: String): Array[Byte] = { import java.io.ByteArrayOutputStream import java.awt.image.BufferedImage import org.krysalis.barcode4j.output.bitmap.BitmapCanvasProvider import org.krysalis.barcode4j.impl.upcean.EAN13Bean val BarcodeResolution = 72 val output: ByteArrayOutputStream = new ByteArrayOutputStream val canvas: BitmapCanvasProvider = new BitmapCanvasProvider(output, mimeType, BarcodeResolution, BufferedImage.TYPE_BYTE_BINARY, false, 0) val barcode = new EAN13Bean() barcode.generateBarcode(canvas, String valueOf ean) canvas.finish output.toByteArray } // GET /barcode/:ean controllers.Products.barcode(ean: Long) def barcode(ean: Long) = Action { import java.lang.IllegalArgumentException val MimeType = "image/png" try { val imageData: Array[Byte] = ean13Barcode(ean, MimeType) Ok(imageData).as(MimeType) } catch { case e: IllegalArgumentException => BadRequest("Could not generate bar code. Error: " + e.getMessage) } } |
HTTP status codes
The simplest possible response that you might want to generate consists of only an HTTP status line that describes the result of processing the request.
1 2 3 4 5 6 7 8 | def list = Action { request => NotImplemented } // 等同于 def list = Action { new Status(501) } |
NotImplemented is one of many HTTP status codes that are defined in the
play.api.mvc.Controller
class via the play.api.mvc.Results
trait.
Response headers
In addition to a status, a response may also include response headers: metadata that instructs HTTP clients how to handle the response
HTTP/1.1 501 Not Implemented
Content-Length: 0
1 2 3 4 | Status(FOUND).withHeaders(LOCATION -> url) val url = routes.Products.details(product.ean).url Created.withHeaders(LOCATION -> url) |
SETTING THE CONTENT TYPE
Every HTTP response that has a response body also has a Content-Type header, whose value is the MIME type that describes the response body format.
Play automatically sets the content type for supported types, such as text/html when rendering an HTML template, or text/plain when you output a string response.
1 2 3 4 5 6 7 8 9 | val json = """{ "status": "success" }""" Ok(json).withHeaders(CONTENT_TYPE -> "application/json") // 也可以这样 Ok("""{ "status": "success" }""").as("application/json") // JSON is defined in the play.api.http.ContentTypes trait // which Controller extends. Ok("""{ "status": "success" }""").as(JSON) |
Play sets the content type automatically for some more types:
Play selects text/xml
for
scala.xml.NodeSeq
values, and application/json
for play.api.libs.json.JsValue
values.
SESSION DATA
1 2 3 4 5 6 7 | Ok(results).withSession( request.session + ("search.previous" -> query) ) Ok(results).withSession( request.session - "search.previous" ) |
The session is implemented as an HTTP session cookie, which means that its total size is limited to a few kilobytes.
DON’T CACHE DATA IN THE SESSION COOKIE Don’t try to use session data as a cache to improve performance by avoiding fetching data from server-side persistent storage. Apart from the fact that session data is limited to the 4 KB of data that fits in a cookie, this will increase the size of subsequent HTTP requests, which will include the cookie data, and may make performance worse overall.
The canonical use case for session cookies is to identify the currently authenticated user. You can load user-specific data from a persistent data model instead.
The session Play cookie is signed using the application secret key as a salt to pre- vent tampering.
FLASH DATA
Displaying a message when handling the next request, after a redirect, is such a common use case that Play provides a special session scope called flash scope.
Flash scope works the same way as the session, except that any data that you store is only available when processing the next HTTP request, after which it’s automatically deleted.
1 2 3 4 5 6 7 | // 设置 Redirect(routes.Products.flash()).flashing( "info" -> "Product deleted!" ) // 获取 val message = request.flash("info") |
SETTING COOKIES
Cookies store small amounts of data in an HTTP client, such as a web browser on a specific computer.
If you do need to use cookies, you can use the Play API to create cookies and add them to the response, and to read them from the request.
AVOID USING COOKIES
Serving static content
Not everything in a web application is dynamic content: a typical web application also includes static files, such as images, JavaScript files, and CSS stylesheets. Play serves these static files over HTTP the same way it serves dynamic responses: by routing an HTTP request to a controller action.
USING THE DEFAULT CONFIGURATION
Put files and folders inside your application’s public/
folder and access
them using the URL path /assets
, followed by the path relative to public.
public/images/favicon.png
访问 http://localhost:9000/assets/images/favicon.png
1 | <link href="/assets/images/favicon.png" rel="shortcut icon" type="image/png"/> |
conf/routes
的默认配置
1 2 3 | GET /assets/*file controllers.Assets.at(path="/public", file) GET /images/*file controllers.Assets.at(path="/public/images", file) GET /styles/*file controllers.Assets.at(path="/public/styles", file) |
USING AN ASSET’S REVERSE ROUTE
1 2 3 4 5 | <link href="@routes.Assets.at("images/favicon.png")" rel="shortcut icon" type="image/png"> <link href="@routes.Assets.at("/public/images", "favicon.png")" rel="shortcut icon" type="image/png"> |
CACHING AND ETAGS
In addition to reverse routing, another benefit of using the assets controller is its builtin caching support, using an HTTP Entity Tag (ET ag). This allows a web client to make conditional HTTP requests for a resource so that the server can tell the client it can use a cached copy instead of returning a resource that hasn’t changed.
Once it has an ET ag value, an HTTP client can make a conditional request, which means “only give me this resource if it hasn’t been modified since I got the version with this ET ag.”
If-None-Match: 978b71a4b1fef4051091b31e22b75321c7ff0541
When this header is included in the request, and the favicon.png file hasn’t been modified (it has the same ETag value), then Play’s assets controller will return the fol- lowing response, which means “you can use your cached copy”:
HTTP/1.1 304 Not Modified
Content-Length: 0
COMPRESSING ASSETS WITH GZIP
HTTP compression is a feature of modern web servers and web clients that helps address page sizes by sending compressed versions of resources over HTTP .
The way this works is that the web browser indicates that it can handle a com-
pressed response by sending an HTTP request header such as Accept-Encoding:gzip
that specifies supported compression methods.
In Play, HTTP compression is transparently built into the assets controller, which can automatically serve a compressed version of a static file, if it’s available, and if gzip is supported by the HTTP client. This happens when all of the following are true:
- Play is running in prod mode (production mode is explained in chapter 9); HTTP compression isn’t expected to be used during development.
- Play receives a request that’s routed to the assets controller.
- The HTTP request includes an Accept-Encoding: gzip header.
- The request maps to a static file, and a file with the same name but with an
additional
.gz
suffix is found.
所以必须要先运行
1 | gzip --best < ui.js > ui.js.gz |