akka 簡介
by Miguel Lopez
由Miguel Lopez
Akka HTTP路由簡介 (An introduction to Akka HTTP routing)
Akka HTTP’s routing DSL might seem complicated at first, but once you get the hang of it you’ll see how powerful it is.
Akka HTTP的路由DSL乍一看似乎很復雜,但是一旦掌握了它,您就會發現它的功能強大。
In this tutorial we will focus on creating routes and their structure. We won’t cover parsing to and from JSON, we have other tutorials that cover that topic.
在本教程中,我們將重點介紹創建路線及其結構。 我們將不介紹與JSON之間的解析,也有涉及該主題的其他教程 。
什么是指令? (What are directives?)
One of the first concepts we’ll find when learning server-side Akka HTTP (there’s a client-side library as well) is directives.
當學習服務器端Akka HTTP(也有一個客戶端庫)時,我們會發現第一個概念是指令 。
So, what are they?
那是什么
You can think of them as building blocks, Lego pieces if you will, that you can use to construct your routes. They are composable, which means we can create directives on top of other directives.
您可以將它們視為積木,也可以視作樂高積木,用于構建路線。 它們是可組合的,這意味著我們可以在其他指令之上創建指令。
If you want a more in-depth reading, feel free to check out Akka HTTP’s official documentation.
如果您想更深入地閱讀,請隨時查閱Akka HTTP的官方文檔 。
Before moving on, let’s discuss what we’ll build.
在繼續之前,讓我們討論一下我們將要構建的內容。
類博客API (Blog-like API)
We’ll create a sample of a public facing API for a blog, where we will allow users to:
我們將為博客創建一個面向公眾的API樣本,我們將允許用戶執行以下操作:
- query a list of tutorials 查詢教程列表
- query a single tutorial by ID 通過ID查詢單個教程
- query the list of comments in a tutorial 查詢教程中的評論列表
- add comments to a tutorial 在教程中添加評論
The endpoints will be:
端點將是:
- List all tutorials GET /tutorials
- Create a tutorial GET /tutorials/:id
- Get all comments in a tutorial GET /tutorials/:id/comments
- Add a comment to a tutorial POST /tutorials/:id/comments
We will only implement the endpoints, no logic in them. This way we’ll learn how to create this structure and the common pitfalls when starting with Akka HTTP.
我們將只實現端點,而沒有邏輯。 這樣,我們將學習從Akka HTTP開始如何創建此結構以及常見的陷阱。
項目設置 (Project Setup)
We’ve created a repo for this tutorial, in it you’ll find a branch per each section that requires coding. Feel free to clone it and use it as a base project or even just change between branches to look at the differences.
我們已經為本教程創建了一個倉庫 ,其中每個需要編碼的部分都會找到一個分支。 隨意克隆它并將其用作基礎項目,甚至只是在分支之間進行更改以查看差異。
Otherwise, create a new SBT project, and then add the dependencies in the build.sbt
file:
否則,請創建一個新的SBT項目,然后在build.sbt
文件中添加依賴build.sbt
:
name := "akkahttp-routing-dsl"
version := "0.1"
scalaVersion := "2.12.7"
val akkaVersion = "2.5.17" val akkaHttpVersion = "10.1.5"
libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-actor" % akkaVersion, "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test, "com.typesafe.akka" %% "akka-stream" % akkaVersion, "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test, "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test, "org.scalatest" %% "scalatest" % "3.0.5" % Test )
We added Akka HTTP and its dependencies, Akka Actor and Streams. And we will also use Scalatest for testing.
我們添加了Akka HTTP及其依賴項,Akka Actor和Streams。 我們還將使用Scalatest進行測試。
列出所有教程 (Listing all the tutorials)
We’ll take a TDD approach to build our directive hierarchy, creating the tests first to make sure when don’t break our routes when adding others. Taking this approach is quite helpful when starting with Akka HTTP.
我們將采用TDD方法來構建指令層次結構,首先創建測試以確保添加其他路由時不中斷我們的路由。 從Akka HTTP開始時,采用這種方法非常有幫助。
Let’s start with our route to listing all the tutorials. Create a new file under src/test/scala
(if the folders don't exist, create them) named RouterSpec
:
讓我們從列出所有教程的路線開始。 在src/test/scala
下創建一個名為RouterSpec
的新文件(如果文件夾不存在,請創建它們):
import akka.http.scaladsl.testkit.ScalatestRouteTest import org.scalatest.{Matchers, WordSpec}
class RouterSpec extends WordSpec with Matchers with ScalatestRouteTest {
}
WordSpec
and Matchers
are provided by Scalatest, and we'll use them to structure our tests and assertions. ScalatestRouteTest
is a trait provided by Akka HTTP's test kit, it will allow us to test our routes in a convenient way. Let's see how we can accomplish that.
WordSpec
和Matchers
是由Scalatest提供,我們將用它們來構建我們的測試和斷言。 ScalatestRouteTest
是Akka HTTP測試工具提供的特征,它將使我們能夠以方便的方式測試路由。 讓我們看看我們如何做到這一點。
Because we’re using Scalatest’s WordSpec, we’ll start by creating a scope for our Router
object that we will create soon and the first test:
因為我們使用的是Scalatest的WordSpec ,所以我們將從為我們的Router
對象創建一個范圍開始,這將是我們即將創建的第一個測試:
"A Router" should { "list all tutorials" in { } }
Next, we want to make sure can send a GET request to the path /tutorials
and get the response we expect, let's see how we can accomplish that:
接下來,我們要確保可以將GET請求發送到路徑/tutorials
并獲得我們期望的響應,讓我們看一下如何實現:
Get("/tutorials") ~> Router.route ~> check { status shouldBe StatusCodes.OK responseAs[String] shouldBe "all tutorials" }
It won’t even compile because we haven’t created our Router
object. Let's do that now.
它甚至不會編譯,因為我們還沒有創建Router
對象。 現在開始吧。
Create a new Scala object under src/main/scala
named Router
. In it we will create a method that will return a Route
:
在src/main/scala
下創建一個名為Router
的新Scala對象。 在其中,我們將創建一個將返回Route
:
import akka.http.scaladsl.server.Route
object Router {
def route: Route = ???
}
Don’t worry too much about the ???
, it's just a placeholder to avoid compilation errors temporarily. However, if that code is executed, it'll throw a NotImplementedError
as we'll see soon.
不用擔心???
,這只是暫時避免編譯錯誤的占位符。 但是,如果執行了該代碼,它將拋出NotImplementedError
,我們將很快看到。
Now that our tests and project are compiling, let’s run the tests (Right-click the spec and “Run ‘RouterSpec’”).
現在我們的測試和項目正在編譯,讓我們運行測試(右鍵單擊spec并“運行'RouterSpec'”)。
The test failed with the exception we were expecting, we haven’t implemented our routes. Let’s begin!
測試失敗,除了我們所期望的例外,我們還沒有實現我們的路線。 讓我們開始!
創建上市路線 (Creating the listing route)
By looking into the official documentation we see that the route begins with the path
directive. Let's mimic what they're doing and build our route:
通過查看官方文檔,我們發現路由以path
指令開頭。 讓我們模仿他們在做什么,并建立我們的路線:
import akka.http.scaladsl.server.{Directives, Route}
object Router extends Directives {
def route: Route = path("tutorials") { get { complete("all tutorials") } }}
Seems reasonable, let’s run our spec. And it passes, great!
似乎合理,讓我們運行我們的規范。 它過去了,太好了!
For reference, our entire RouterSpec
now looks like:
作為參考,我們的整個RouterSpec
現在看起來像:
import akka.http.scaladsl.model.StatusCodesimport akka.http.scaladsl.testkit.ScalatestRouteTestimport org.scalatest.{Matchers, WordSpec}class RouterSpec extends WordSpec with Matchers with ScalatestRouteTest { "A Router" should { "list all tutorials" in { Get("/tutorials") ~> Router.route ~> check { status shouldBe StatusCodes.OK responseAs[String] shouldBe "all tutorials" } } }}
通過ID獲取單個教程 (Getting a single tutorial by ID)
Next, we will allow our users to retrieve a single tutorial.
接下來,我們將允許我們的用戶檢索單個教程。
Let’s add a test for our new route:
讓我們為新路線添加一個測試:
"return a single tutorial by id" in { Get("/tutorials/hello-world") ~> Router.route ~> check { status shouldBe StatusCodes.OK responseAs[String] shouldBe "tutorial hello-world" }}
We expect to get back a message that includes the tutorial ID.
我們希望獲得一條包含教程ID的消息。
The test will fail because we haven’t created our route, let’s do that now.
由于我們尚未創建路線,因此測試將失敗,讓我們現在開始。
From the same resource we used earlier to base our route on, we can see how we can place multiple directives at the same level in the hierarchy using the ~
directive.
從之前使用的相同資源開始,我們可以看到如何使用~
指令將多個指令放置在層次結構中的同一級別上。
We will have to nest path
directives because need another segment after the /tutorials
route for the tutorial ID. In the documentation they use IntNumber
to extract a number from the path, but we'll use a string and for that we use can Segment
instead.
我們將必須嵌套path
指令,因為在/tutorials
路由后需要另一個段來獲取教程ID。 在文檔中,他們使用IntNumber
從路徑中提取數字,但是我們將使用字符串,為此,我們可以使用Segment
。
Our route looks like:
我們的路線如下:
def route: Route = path("tutorials") { get { complete("all tutorials") } ~ path(Segment) { id => get { complete(s"tutorial $id") } }}
Let’s run the tests. And you should get a similar error:
讓我們運行測試。 而且您應該得到類似的錯誤:
Request was rejectedScalaTestFailureLocation: RouterSpec at (RouterSpec.scala:17)org.scalatest.exceptions.TestFailedException: Request was rejected
What’s going on?!
這是怎么回事?!
Well, a request is rejected when it doesn’t match our directive hierarchy. This is one of the things that got me when starting.
好吧,當請求與我們的指令層次結構不匹配時,該請求將被拒絕。 這是開始時讓我著迷的事情之一。
Now is probably a good time to look into how these directives match the incoming request as it goes through the hierarchy.
現在可能是研究這些指令如何與傳入請求通過層次結構進行匹配的好時機。
Different directives will match different aspects of an incoming request, we’ve seen path
and get
, one matches the URL of the request and the other the method. If a request matches a directive it will go inside it, if it doesn't it will continue to the next one. This also tells us that order matters. If it doesn't match any directive the request is rejected.
不同的指令將匹配傳入請求的不同方面,我們已經看到path
和get
,一個匹配請求的URL,另一個匹配方法。 如果一個請求與一個指令相匹配,它將進入其中,如果不匹配,它將繼續到下一個指令。 這也告訴我們訂單很重要。 如果它與任何指令都不匹配,則請求被拒絕。
Now that we now that our request is not matching our directives, let’s start looking into why.
現在,我們的請求與指令不匹配,讓我們開始研究原因。
If we look the documentation for the path
directive (Cmd + Click on Mac) we'll find:
如果我們查看path
指令的文檔(Cmd +在Mac上單擊),我們會發現:
/** * Applies the given [[PathMatcher]] to the remaining unmatched path after consuming a leading slash. * The matcher has to match the remaining path completely. * If matched the value extracted by the [[PathMatcher]] is extracted on the directive level. * * @group path */
So, the path
directive has to match exactly the path, meaning our first path
directive will only match /tutorials
and never /tutorials/:id
.
因此, path
指令必須與路徑完全匹配,這意味著我們的第一個path
指令將僅匹配/tutorials
而不會匹配/tutorials/:id
。
In the same PathDirectives
trait that contains the path
directive we can see another directive named pathPrefix
:
在包含path
指令的同一PathDirectives
特性中,我們可以看到另一個名為pathPrefix
指令:
/** * Applies the given [[PathMatcher]] to a prefix of the remaining unmatched path after consuming a leading slash. * The matcher has to match a prefix of the remaining path. * If matched the value extracted by the PathMatcher is extracted on the directive level. * * @group path */
pathPrefix
matches only a prefix and removes it. Sounds like this is what we're looking for, let's update our routes:
pathPrefix
僅匹配前綴,并將其刪除。 聽起來這就是我們想要的,讓我們更新路線:
def route: Route = pathPrefix("tutorials") { get { complete("all tutorials") } ~ path(Segment) { id => get { complete(s"tutorial $id") } }}
Run the tests, and… we get another error. ?
運行測試,然后…我們得到另一個錯誤。 ?
"[all tutorials]" was not equal to "[tutorial hello-world]"ScalaTestFailureLocation: RouterSpec at (RouterSpec.scala:18)Expected :"[tutorial hello-world]"Actual :"[all tutorials]"
Looks like our request matched the first get
directive. It now matches the pathPrefix
, and because it also is a GET request it will match the first get
directive. Order matters.
看起來我們的請求與第一個get
指令匹配。 現在,它與pathPrefix
匹配,并且由于它也是一個GET請求,因此它將與第一個get
指令匹配。 順序很重要。
There are a couple of things we can do. The simplest solution would be to move the first get
request to the end of the hierarchy, however, we would have to remember this or document it. Not ideal.
我們可以做幾件事。 最簡單的解決方案是將第一個get
請求移至層次結構的末尾,但是,我們必須記住此要求或將其記錄下來。 不理想。
Personally, I prefer avoiding such solutions and instead make the intend clear through code. If we look in the PathDirectives
trait from earlier, we'll find a directive called pathEnd
:
就我個人而言,我更喜歡避免此類解決方案,而是通過代碼明確意圖。 如果我們從較早的版本開始查看PathDirectives
特性,我們將找到一個名為pathEnd
的指令:
/** * Rejects the request if the unmatchedPath of the [[RequestContext]] is non-empty, * or said differently: only passes on the request to its inner route if the request path * has been matched completely. * * @group path */
That’s exactly what we want, so let’s wrap our first get
directive with pathEnd
:
這正是我們想要的,所以讓我們用pathEnd
包裝第一個get
指令:
def route: Route = pathPrefix("tutorials") { pathEnd { get { complete("all tutorials") } } ~ path(Segment) { id => get { complete(s"tutorial $id") } }}
Run the tests again, and… finally, the tests are passing! ?
再次運行測試,……最后,測試通過了! ?
列出教程中的所有評論 (Listing all comments in a tutorial)
Let’s put into practice what we learned about nesting routes by taking it a bit further.
讓我們進一步實踐嵌套路由所學到的知識。
First the test:
首先測試:
"list all comments of a given tutorial" in { Get("/tutorials/hello-world/comments") ~> Router.route ~> check { status shouldBe StatusCodes.OK responseAs[String] shouldBe "comments for the hello-world tutorial" }}
It’s a similar case as before: we know we’ll need to place a route next to another one, which means we need to:
這與之前的情況類似:我們知道我們需要在另一條路線旁邊放置一條路線,這意味著我們需要:
change the
path(Segmenter)
topathPrefix(Segmenter)
將
path(Segmenter)
更改為pathPrefix(Segmenter)
wrap the first
get
with thepathEnd
directive用
pathEnd
指令包裝第一個get
place the new route next to the
pathEnd
將新路線放置在
pathEnd
Our routes end up looking like:
我們的路線最終看起來像:
def route: Route = pathPrefix("tutorials") { pathEnd { get { complete("all tutorials") } } ~ pathPrefix(Segment) { id => pathEnd { get { complete(s"tutorial $id") } } ~ path("comments") { get { complete(s"comments for the $id tutorial") } } }}
Run the tests, and they should pass! ?
運行測試,它們應該通過! ?
在教程中添加評論 (Adding comments to a tutorial)
Our last endpoint is similar to the previous, but it will match POST requests. We’ll use this example to see the difference between implementing and testing a GET request versus a POST request.
我們的最后一個端點與先前的端點相似,但是它將匹配POST請求。 我們將使用此示例查看實現和測試GET請求與POST請求之間的區別。
The test:
考試:
"add comments to a tutorial" in { Post("/tutorials/hello-world/comments", "new comment") ~> Router.route ~> check { status shouldBe StatusCodes.OK responseAs[String] shouldBe "added the comment 'new comment' to the hello-world tutorial" }}
We’re using the Post
method instead of the Get
we've been using, and we're giving it an additional parameter which is the request body. The rest is familiar to us now.
我們使用的是Post
方法而不是我們一直使用的Get
方法,并且為其提供了一個附加參數,即請求正文。 其余的現在對我們來說已經很熟悉了。
To implement our last route, we can refer to the documentation and look at how it’s usually done.
要實現我們的最后一條路線,我們可以參考文檔并查看它通常是如何完成的。
We have a post
directive just as we have a get
one. To extract the request body we need two directives, entity
and as
, to which we supply the type we expect. In our case it's a string.
我們有一個post
指令,就像我們有一個get
指令。 為了提取請求主體,我們需要兩個指令, entity
和as
,我們向它們提供期望的類型。 在我們的例子中,它是一個字符串。
Let’s give that a try:
讓我們嘗試一下:
post { entity(as[String]) { comment => complete(s"added the comment '$comment' to the $id tutorial") }}
Looks reasonable. We extract the request body as a string and use it in our response. Let’s add it to our route
method next to the previous route we worked on:
看起來很合理。 我們將請求主體提取為字符串,并在響應中使用它。 讓我們將其添加到我們之前處理過的路由旁邊的route
方法中:
def route: Route = pathPrefix("tutorials") { pathEnd { get { complete("all tutorials") } } ~ pathPrefix(Segment) { id => pathEnd { get { complete(s"tutorial $id") } } ~ path("comments") { get { complete(s"comments for the $id tutorial") } ~ post { entity(as[String]) { comment => complete(s"added the comment '$comment' to the $id tutorial") } } } }}
If you’d like to learn how to parse Scala classes to and from JSON we’ve got tutorials for that as well.
如果您想學習如何在JSON中解析Scala類以及從JSON解析出Scala類, 我們也有相應的教程 。
Run the tests, and they should all pass.
運行測試,它們都應該通過。
結論 (Conclusion)
Akka HTTP’s routing DSL might seem confusing at first, but after overcoming some bumps it just clicks. After a while it’ll come naturally and it can be very powerful.
Akka HTTP的路由DSL乍一看似乎令人困惑,但是在克服了一些麻煩之后,只需單擊一下。 一段時間后,它會自然而然地變得強大。
We learned how to structure our routes, but more importantly, we learned how to create that structure guided by tests which will make sure we don’t break them at some point in the future.
我們學會了如何構造路線,但更重要的是,我們學會了如何在測試的指導下建立這種結構,以確保我們在將來的某個時候不會破壞它們。
Even though we only worked on four endpoints, we ended up with a somewhat complicated and deep structure. Stay tuned and we’ll explore different ways to simplify our routes and make them more manageable!
即使我們僅在四個端點上工作,但最終還是有一個復雜而深入的結構。 請繼續關注,我們將探索各種方法來簡化路線并使其更易于管理!
Learn how to build REST APIs with Scala and Akka HTTP with this step-by-step free course!
通過此分步免費課程,了解如何使用Scala和Akka HTTP構建REST API!
Originally published at www.codemunity.io.
最初在www.codemunity.io上發布。
翻譯自: https://www.freecodecamp.org/news/an-introduction-to-akka-http-routing-697b00399cad/
akka 簡介