Created
December 11, 2011 04:59
-
-
Save ssmylh/1458455 to your computer and use it in GitHub Desktop.
http://unfiltered.databinder.net/Unfiltered.html の第6章~9章の自分的訳メモ(ver 0.5.1)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
『Matching and Responding』 | |
典型的なリクエスト/レスポンスが数行で手近に扱えます。 | |
この項ではシンプルなkey-valueストアを示すために、パターンマッチングとコンビネータ関数を適用します。 | |
■6.a Request Matchers | |
○Methods and Paths | |
UnfilteredはパスセグメントからHTTPメソッドとHTTPヘッダに、広い範囲の“request matchers” - リクエストに対して働く抽出子オブジェクト - を、 | |
供給します。 | |
アプリケーションはリクエストにレスポンスするかどうか、そしてどのようにレスポンスするかどうかを定義するために、 | |
“request matchers”を使用しましす。 | |
case GET(Path("/record/1")) => ... | |
このケースは「/record/1」のパスへのGETリクエストにマッチするでしょう。 | |
パスセグメントにマッチするには、一つ抽出子をネストします。 | |
case GET(Path(Seg("record" :: id :: Nil))) => ... | |
これはrecordパスの後に隣接する、いかなるid文字列にマッチします。 | |
(つまり、/record/id。idは任意の文字列) | |
SegはString型にマッチし、また典型的にPathパラメータにネストされます。 | |
Segは供給された文字列から文字列のリストを抽出し、スラッシュによりパスセグメントに分離します。 | |
○Reading Requests and Delayed Matching | |
上記のcase句はrecordを取得するためのリクエストにマッチします。 | |
それらをPUTするのはどうでしょう? | |
case req @ PUT(Path(Seg("record" :: id :: Nil))) => | |
val bytes = Body.bytes(req) | |
... | |
リクエストボディにアクセスすることは、一度だけ読み込むことが出来るストリームのように、 | |
一般的に副作用を持ちます。 | |
この理由により、ボディは複数回評価されうる“request matcher”からはアクセスされません。 | |
しかし、リクエストオブジェクトを操作するユーティリティ関数からアクセスされます。 | |
このケースでは、“req @”を使用しリクエストに参照をアサイン(束縛)し、 | |
そのボディが利用可能なメモリに収まるという仮定で、そのボディをバイト配列に読み込みました。 | |
予断ですが、このコードがマッチング表現においていくつかの繰り返しを導入していることは、多少わずらわしいです。 | |
case GET(Path(Seg("record" :: id :: Nil))) => ... | |
case req @ PUT(Path(Seg("record" :: id :: Nil))) => ... | |
代替案はそのメソッドにて、パスに最初にマッチすることです。 | |
case req @ Path(Seg("record" :: id :: Nil)) => req match { | |
case GET(_) => ... | |
case PUT(_) => ... | |
case _ => ... | |
} | |
このアプローチは重複したコードを排除しますが、 | |
異なるようにふるまうこと(動作が異なる)を理解するのも同様に重要です。 | |
元の“intent”部分関数はGETまたはPUTでないパスへのリクエストに対して、単に定義されていませんでした。 | |
最新の“intent”部分関数はいかなるパスへのリクエストにマッチし、 | |
従ってマッチ表現のある全メソッドに対して結果を返さなければいけません。 | |
重要なのは、リクエストメソッドにマッチすることを遅延させることは、 | |
“intent”部分関数を簡素化するということです。 | |
二つのcase句だったのは、今一つです。そして特に複雑なこと無く、 | |
パターンマッチングにDELETEのようなその他のメソッドのサポートを加えられるでしょう。 | |
これは注目に値します。なぜならば、定義されているということは、全ての“intent”はその評価の前に少なくとも一回は呼ばれる、からです。 | |
“intent”をより広く定義することにより、その複雑さを減らし、潜在的に実行時パフォーマンスを改善してきたのです。 | |
■6.b Response Functions | |
○Response Function and Combinators | |
典型的なリクエスト-レスポンスサイクルである“intent”にて、部分関数の戻り値はUnfilterdのResponseFunction型です。 | |
“response function”は一つのレスポンスオブジェクトを取り、多分それ(レスポンスオブジェクト)を変更し、 | |
同じレスポンスオブジェクトを返します。 | |
Unfilteredは一般的なレスポンス型のためのいくつかの“response function”を含んでいます。 | |
“record”の例を続けると、いくつかのケースで特定の文字列でレスポンスしたいかもしれません。 | |
case PUT(_) => | |
... | |
ResponseString("Record created") | |
また、このレスポンスにステータスコードを設定すべきです。 | |
幸い、これにもあらかじめ定義された関数があり、“response function”は簡単に構成されます。 | |
Unfilteredはこれをきれいに作るために、“chaining combinator”である“~>” をも提供します。 | |
case PUT(_) => | |
... | |
Created ~> ResponseString("Record created") | |
バイト配列があったならば、それら(バイト配列)は文字列を機能しやすくなるでしょう。 | |
case GET(_) => | |
... | |
ResponseBytes(bytes) | |
○Passing or Handling Errors | |
最後ですが、予期しない方法の為にいくつかの選択肢があります。 | |
一つの選択は、リクエストに応じてパスすることです。 | |
case _ => Pass | |
Passレスポンス関数は、あたかもリクエストがこの“intent”に定義されていなかったようにふるまう、“plan”への合図です。 | |
他のどの“plan”もそのリクエストにレスポンスしないならば、404エラーでレスポンスするかもしれません。 | |
しかし、予期しない方法であるこのパスへの全てのリクエストが、 | |
適切なレスポンスを受け取ることを確保した上で改善できます。 | |
case _ => MethodNotAllowed ~> ResponseString("Must be GET or PUT") | |
■6.c Silly Store | |
○Opening the Store | |
最後の数ページで概説された、“request matchers”と“response functions”を使うことで、 | |
ネイティブなkey-valueストアを構築する必要な全てのものが揃っています。 | |
import unfiltered.request._ | |
import unfiltered.response._ | |
object SillyStore extends unfiltered.filter.Plan { | |
@volatile private var store = Map.empty[String, Array[Byte]] | |
def intent = { | |
// まず絞る(reqにマッチした部分を束縛) | |
case req @ Path(Seg("record" :: id :: Nil)) => req match { | |
case GET(_) => | |
store.get(id).map(ResponseBytes).getOrElse { | |
NotFound ~> ResponseString("No record: " + id) | |
} | |
case PUT(_) => | |
SillyStore.synchronized { | |
store = store + (id -> Body.bytes(req)) | |
} | |
Created ~> ResponseString("Created record: " + id) | |
case _ => | |
MethodNotAllowed ~> ResponseString("Must be GET or PUT") | |
} | |
} | |
} | |
どうぞ、コンソールにペーストしてください。そこで、あなたのサーバーが8080ポートが利用可能でないならば、 | |
ポートを調整し、サーバーで"plan"を実行してください。 | |
unfiltered.jetty.Http.local(8080).filter(SillyStore).run() | |
methodlocalはanylocalのように、安全のため、ループバック・インタフェースのみにバインドされます。 | |
"SillyStore"は全く"Webスケール"ではありません。 | |
~省略~ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
『Application Structure』 | |
前項では簡単な“intent”関数を用いて、どのようにリクエストへレスポンスするかを示しました。 | |
Unfilteredが構築するブロックは把握しやすいですが、それら(構築するブロック)を用いて、 | |
あなたはより大きなアプリケーションをどのように構築しますか? | |
答えはScalaを使用することによって、です。 | |
Unfilteredはあなたのアプリケーション構造にイディオムを課しません。 | |
ただ、それは(Unfiltered)はあなたに合うように適用される、HTTPの翻訳層です。 | |
部分関数はWebアプリケーションにとってのほぼ一般的なエントリポイントではなく、 | |
このコンテキストでのそれら(部分関数)の多用は、初心者を混乱させるかもしれません、と述べました。 | |
この項では、アプリケーションデザインの決定を導く、いくつかの論理構造を調査します。 | |
■7.a Planning for Any-thing | |
Scalaの型システム(variance:変位)の闇のコーナーのおかげで、 | |
全ての“plan”で動作する“intent”を定義するのは簡単です。 | |
○Agnostic Intents(不可知的“intent”) | |
import unfiltered.request._ | |
import unfiltered.response._ | |
object Hello { | |
val intent = unfiltered.Cycle.Intent[Any, Any] { | |
case _ => ResponseString("Hello") | |
} | |
} | |
HelloオブジェクトはAny型のリクエストとレスポンスの基礎となる、“intent”を定義しています。 | |
結果として、“intent”は特定の、基礎となるリクエストとレスポンスの束縛を静的に待ち受けることはできません。 | |
どのリクエストとレスポンスでも動作する“intent”を作成したいので、これは理に適っています。 | |
○Specific Plans | |
次のステップは同じジェネリックな“intent”を異なる種類の“plan”に供給することです。 | |
object HelloFilter extends | |
unfiltered.filter.Planify(Hello.intent) | |
object HelloHandler extends | |
unfiltered.netty.cycle.Planify(Hello.intent) | |
通常、“plan”は実際に“servlet filter”または、“Netty handler”なので、 | |
あなたは個別に設定したサーバや、Unfilterdによって設定されたサーバで、それら(“plan”)を使うことができるでしょう。 | |
■7.b Just Kitting | |
Unfilteredはパターンマッチングを促すためにあなたにたくさんの抽出子をもたらし、 | |
そして、(Unfilteredは)一つのアプリケーションに向けていくらかの抽出子を作ることは役に立つかもしませんが、 | |
しばしば、あなたは広範囲の機能を(関数として)抜き出すことがしたいでしょう。 | |
こんな時は“kit”の出番です。 | |
○The GZip Kit | |
レスポンスのGzip圧縮をサポートする最善の方法を検討するとき、まず初めに“kit”の考えがありました。 | |
GZipのAccept-Encodingヘッダにマッチする抽出子は役立ち、また、 | |
“ResponseFunction”もレスポンスストリームを圧縮するのに役立ちますが、 | |
これらは毎回case句で繰り返さなければならないでしょう。 | |
GZip“kit”は完全な“intent”関数にて適用することができる、より高いレベルの抽象化となっています。 | |
“kit”はリクエストをまず検証し、もし適切ならば、“intent”によって提供されるレスポンス関数のチェーンに、 | |
圧縮関数を付加します。 | |
○GZip Kit Definition | |
このパートは“unfiltered-libary”で既に実行されていますが、 | |
今回、あなたはGZip“kit”の定義方法を知りたいでしょう。 | |
object GZip extends unfiltered.kit.Prepend { | |
def intent = Cycle.Intent[Any,Any] { | |
case Decodes.GZip(req) => | |
ContentEncoding.GZip ~> ResponseFilter.GZip | |
} | |
} | |
“plan”の“intent”関数とは違いますが、そのレスポンス関数が他の“intent”関数に付加される状態を定義しています。 | |
○GZip Usage | |
これはuser-agentがそのレスポンスをサポートするならば、 | |
とてもシンプルなそのレスポンスを圧縮する“plan”です。 | |
object EchoPlan extends unfiltered.filter.Plan { | |
def intent = unfiltered.kit.GZip { | |
case Path(path) => ResponseString(path) | |
} | |
} | |
○Do Kit Yourself | |
“kit”によって提供されるより高いレベルの抽象化は、 | |
一般的な問題だけでなく特定のアプリケーションの問題にも適用できます。 | |
検証を恐れないでください。もし一般的な問題を解決するものを作ろうとするならば、それを共有しましょう。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
『Identification and Cookies』 | |
リモートユーザはWebブラウザで対話する(人間・ユーザの)場合、 | |
アプリケーションはしばしばそれら(リモートユーザ)の同一性と設定を懸念します。 | |
この項はHTPPに対して定義される、関連するインタフェースをまとめています。 | |
■8.a Who’s Who | |
そのまま使用できるHTTPは、リクエストに名前とパスワードを特定する簡単な方法である、Basic認証を提供します。 | |
その認証情報は暗号化なしのリクエストヘッダーとして転送されるので、 | |
アプリケーションはあらゆる保護されるリソースのためにHTTPSを必要とすることにより、 | |
認証情報とメッセージボディの両方を安全にするべきです。 | |
以下に、ユーザ名とパスワードをBasic認証にて抽出し、誰でもゲートを通過させる前に認証情報を検証する、 | |
キットを定義します。 | |
ユーザの認証情報を検証する“Users”サービスを前提とします。 | |
case class Auth(users: Users) | |
extends unfiltered.kit.Prepend { | |
def intent = Cycle.Intent[Any, Any] { | |
case r => r match { | |
case BasicAuth(user, pass) if(users.authentic(user, pass)) => | |
Pass | |
case _ => Unauthorized ~> | |
WWWAuthenticate("""Basic realm="/"""") | |
} | |
} | |
} | |
この“kit”を適用することにより、クライアントアプリケーションにおいて、 | |
あらゆる“intent”の周りにBasic認証層を敷くことができます。 | |
case class App(users: Users) | |
extends unfiltered.filter.Plan { | |
def intent = Auth(users) { | |
case _ => ResponseString("Shhhh!") | |
} | |
} | |
また、どんな新聞記者にもパスワードを教えないでください。 | |
■8.a Remembrance of Things Past(過去のことの記憶) | |
Basic認証は一般的な認証にとって軽量な方法ですが、 | |
ユーザにセッションについての少々の情報を記憶しておくある場合はどうでしょう? | |
クッキーの出番です。 | |
認証されたアプリケーションを構築し、簡単なクッキー処理のサンプルを追加しましょう。 | |
import unfiltered.Cookie | |
case class App(users: Users) | |
extends unfiltered.filter.Plan { | |
def intent = Auth(users) { | |
case Path("/") & Cookies(cookies) => | |
ResponseString(cookies("pref") match { | |
case Some(Cookie(_, pref, _, _, _, _)) => | |
"you pref %s, don't you?" format pref | |
case _ => "no preference?" | |
}) | |
case Path("/prefer") & Params(p) => | |
// let's store it on the client | |
ResponseCookies(Cookie("pref", p("pref")(0))) ~> | |
Redirect("/") | |
case Path("/forget") => | |
ResponseCookies(Cookie("pref", "")) ~> | |
Redirect("/") | |
} | |
} | |
今、短くより洗練された基本的なアプリケーションがあるので、 | |
名前がjim、パスワードがj@mでそれをマウントしてみましょう。 | |
import unfiltered.jetty._ | |
object Main { | |
def main(args: Array[String]) { | |
jetty.Http(8080).filter(App(new Users { | |
def authentic(u: String, p: String) = | |
u == "jim" && p == "j@m" | |
})).run | |
} | |
} | |
あなたのブラウザで、 http://localhost:8080/を開き、 | |
ネイティブな認証ダイアログで迎えられるべきです(そのように動くはずです)。 | |
jimとj@mを入力し、 | |
一度認証されると、あなたはあなたの設定を質問する簡単なテキストを見るべきです(そのように動くはずです)。 | |
これはなんでしょう? | |
えぇ、あなたはまだサーバに何を設定するかを伝えなけらばなりません。 | |
アドレスバーで、以下のアドレスを入力してください。 | |
http://localhost:8080/prefer?pref=kittens | |
また、あなたの他の全ての設定も入力してください。 | |
今、あなたがhttp://localhost:8080/ にリクエストする度に、 | |
サーバはあなたのための設定を記憶しています。 | |
これは、クッキーが作用しています。 | |
もしあなたがあなたの記憶を変更したいならば、 | |
いつでも“prefer”のパスを新しいprefでリクエストできますし、 | |
または以下のアドレスを入力することにより、単に、サーバにそれを忘れるよう伝えることができます。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
『Netty Plans』 | |
ServletFilterと比較して、NettyへのUnfilteredのインタフェースは、 | |
Nettyの低レベルの性質を反映します。 | |
これは、プログラマによる多大な責務と詳細への注意を必ず必要としますが、 | |
アプリケーションに多大な柔軟性と力強さを与えます。 | |
もしあなたが挑戦する気があるまたは、単に新しい事をしたいならば、一つためしてみてください。 | |
■9.a Trying Netty | |
以前のコンソールハッキングのために使用されたJettyサーバと同じく、 | |
Nettyのために、スタータープロジェクトが必要です。 | |
もし、インストールされたg8コマンドラインツールがない場合、 | |
それを取得するまでそのページに戻ってください(そのページに戻って取得してください)。 | |
○Enter the Console | |
このステップでは若干の依存関係を取得し、ときどき特定のリポジトリは不安定です。 | |
なので、祈ってください。 | |
以下、省略。 | |
■9.b Execution and Exceptions | |
ちょうどJettyと同様に、数行のコードでNettyサーバを開始したのは、 | |
非常によい方法ですよね? | |
しかし一方、“Planify ”はコンソールでの実験に本当に便利だが、 | |
ライブサーバを保持し続けそうにないという多くの前提をなします。 | |
○A Less Simple Plan | |
“plan”が明示的に作成されたそれらのデフォルト値を持つように見えるか、見てみましょう。 | |
import unfiltered.netty._ | |
trait MyPlan extends cycle.Plan with | |
cycle.ThreadPool with ServerErrorResponse | |
Object Hello extends MyPlan { | |
def intent = { | |
case _ => ResponseString("hello world") | |
} | |
} | |
トレイトは“cycle.Plan”によって必要とされるメソッドを定義します。 | |
しかし何のために? | |
○Deferred Execution | |
Nettyの“handler”はI/Oワーカスレッド上で呼び出されます。 | |
いくつかのシナリオでは(実際にはこれも含みます)、 | |
それは(Nettyの“handler”)は、このスレッドでレスポンスの準備をするのにちょうどいいでしょう。 | |
もし処理がCPUバウンド(ブロッキング操作を実行しない)ならば、オーバーヘッド無しでの実行のために、 | |
“cycle.SynchronousExecution”トレイトを代わりにミックスインできます。 | |
しかし、典型的なWebアプリケーションはデータベースまたは何かにアクセスする必要があり、 | |
これをする典型的な方法はブロッキングコールを伴います。 | |
それが、“Planify”が“cycle.ThreadPool”トレイトを使用する理由です。“cycle.ThreadPool”トレイトは、 | |
キャッシュされたスレッドプールに“intent”部分関数のアプリケーションを遅延させます。 | |
○Deference has its Memory Limits(遅延処理にはメモリ制限があります) | |
不運にも、遅延実行において話の終わりではありません。 | |
アプリケーションは、また、メモリ不足を心配する必要があります。 | |
もしNettyがリクエストを受け入れできるのと同じ速さで、リクエストをリクエストハンドリングジョブをエグゼキュータキューに遅延させるならば、 | |
いかなるヒープの限界も非常に急激な上昇で越えてしまう可能性があります。 | |
その種の失敗を避けるために、制限されたメモリ量を消費し、 | |
その制限に達した時にくるリクエストをブロックする“plan”を備えるべきです。 | |
もし簡単なローカルな基本的な“plan”(上記のMyPlanのような)を定義してきたならば、 | |
メモリを認識するスレッドプールエグゼキュータに後で(理想的には、サーバがメモリ例外を投げる前に)カスタマイズできます。 | |
trait MyPlan extends cycle.Plan with | |
cycle.DeferralExecutor with cycle.DeferredIntent with | |
ServerErrorResponse { | |
def underlying = MyExecutor.underlying | |
} | |
object MyExecutor { | |
import org.jboss.netty.handler.execution._ | |
lazy val underlying = new MemoryAwareThreadPoolExecutor( | |
16, 65536, 1048576) | |
} | |
○Expecting Exceptions | |
“ServerErrorResponse ”トレイトは、また、 | |
あなたのアプリケーションが遅かれ早かれカスタマイズする必要があるであろう、振る舞いを実装します。 | |
そのトレイト(“ServerErrorResponse ”)をミックスインする代わりに、 | |
基本となる“plan”で直接“onException”を実装することもできます。 | |
これから始める場合は、提供される“ExceptionHandler”のソース | |
(https://github.com/unfiltered/unfiltered/blob/master/netty/src/main/scala/exceptions.scala)を参照してください。 | |
“ExceptionHandler”はスタックトレースを標準出力に出力したり、非常に簡潔なエラーレスポンスを提供します。 | |
通常、アプリケーションはアプリケーション自体のロガーにフックし、カスタムエラーページを提供、またはリダイレクトします。 | |
■9.c Going Asynchronous | |
“cycle.Plan”は伝統的なリクエスト・レスポンスのサイクルを実装する手法を提供しますが、 | |
“unfiltered-netty”では、また、リクエストに非同期でレスポンスするオプションがあります。 | |
他の非同期ライブラリと一緒に使用される時は、これは結果的により効果的なスレッドの使用し、より多くの同時接続のサポートします。 | |
○Other Asynchronous Libraries | |
幸運にも、最後の数ページで作成した、"nettyplayin"プロジェクトは既に第二の非同期ライブラリである、 | |
“dispatch-nio”に依存しています。“dispatch-nio”は他のサービスへのHTTPリクエストのためのクライアントとして動作します。 | |
“dispatch.nio.Http”と一緒に“async.Plan”を使用することで、リクエストを実行するのにかかる多くのミリ秒の間、 | |
スレッドを抱え込まずにリクエストを満たすように、他のHTTPサービスに問い合わせることができます。 | |
○Always Sunny in… | |
Googleには“Secret Weather API”があります。それらがオフラインになるまで使用してみましょう。 | |
import dispatch._ | |
val h = new nio.Http | |
def weather(loc: String) = | |
:/("www.google.com") / "ig/api" <<? Map("weather" -> loc) | |
コンソールにペーストし、以下のようにすることでレスポンスをプリントできます。 | |
h(weather("San+Francisco") >- { x => println(x) }) | |
あなたはレスポンスがプリントされる前に次のコマンドのためのプロンプトが表示されるのに気付くかもしれません。 | |
成功です! | |
○Taking the Temperature | |
今しなければいけない全てはサーバからこのサービスを利用することです。 | |
import unfiltered.response._ | |
import unfiltered.netty._ | |
val temp = async.Planify { | |
case req => | |
h(weather("San+Francisco") <> { reply => | |
val tempC = (reply \\ "temp_c").headOption.flatMap { | |
_.attribute("data") | |
}.getOrElse("unknown") | |
req.respond(PlainTextContent ~> | |
ResponseString(tempC + "°C")) | |
}) | |
} | |
Http(8080).plan(temp).run() | |
コンソールに全てをペーストすると常にサンフランシスコ(とにかくGoogle本社に最も近い都市名です。) | |
の温度を教えてくれるサーバが開始されるはずです。 | |
このサンプルをハードコーディングした環境で実行した場合、実際のやりとりに移れるように、 | |
“Dispatch”エグゼキュータをシャットダウンしてください。 | |
h.shutdown() | |
■9.d Asyncrazy Temperature Server | |
この全てを一緒に配置することで、もしあなたがWeb上に実際に配置したら、 | |
それはGoogleによって禁止されている、IPアドレスを効率的に取得するサーバを構築できます。 | |
しかし、ローカルでそれを実行するには全く問題なくステキです。 | |
そう思います。 | |
import dispatch._ | |
import unfiltered.request._ | |
import unfiltered.response._ | |
import unfiltered.netty._ | |
object Location extends | |
Params.Extract("location", Params.first ~> Params.nonempty) | |
object Temperature extends async.Plan with ServerErrorResponse { | |
val http = new nio.Http | |
def intent = { | |
case req @ GET(_) => | |
req.respond(view("", None)) | |
case req @ POST(Params(Location(loc))) => | |
http(:/("www.google.com") / "ig/api" <<? Map( | |
"weather" -> loc) <> { reply => | |
val tempC = for { | |
elem <- reply \\ "temp_c" | |
attr <- elem.attribute("data") | |
} yield attr.toString | |
req.respond(view(loc, tempC.headOption)) | |
} | |
) | |
} | |
def view(loc: String, temp: Option[String]) = Html( | |
<html> | |
<body> | |
<form method="POST"> | |
Location: | |
<input value={loc} name="location" /> | |
<input type="submit" /> | |
</form> | |
{ temp.map { t => <p>It's {t}°C in {loc}!</p> }.toSeq } | |
</body> | |
</html> | |
) | |
} | |
コンソールに全てを配置し、それを開始します。 | |
Http(8080).plan(Temperature).run() | |
あなたはどこの場所でも現在の温度を探すことができます。 | |
単に地名または郵便番号を入力するだけで、Googleがおそらくそれを(その場所の現在の温度)考えだしてくれるでしょう。 | |
世界中の興奮する場所の温度をチェックしたら、そのハンドラの“Dispatch”エグゼキュータをシャットダウンしてください。 | |
Temperature.http.shutdown() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment