2020年7月26日 星期日

解決 switchIfEmpty() 每次都被執行的問題

紀錄一下最近發現的有點意外的 Reactor 反應。

1
2
3
Mono.justOrEmpty(getXXX())
    .switchIfEmpty(fallbackXXX())
    ...

本來預期的行為是,getXXX() 會回覆一個 Optional 物件,當回覆的 Optionalempty 時,才會觸發 fallbackXXX() 去呼叫外部服務。但結果在測試時發現 fallbackXXX() 每次都必然會被執行,雖然最後結果會是對的,如果 getXXX() 有回覆時,走到後面的 stream 內容會是 getXXX() 回覆的東西,但這樣就等於每次執行時都會呼叫外部服務了,而且呼叫後拿到的結果還不一定會用到。

查了好一段時間之後,發現問題好像是出在 Java 本身的 method evaluation 行為上,然後解決方法是必須把 fallbackXXX()Mono.defer() 包裝起來,以達成讓 fallbackXXX() 推遲被執行的目的。

1
2
3
Mono.justOrEmpty(getXXX())
    .switchIfEmpty( Mono.defer(fallbackXXX()) )
    ...
參考資料
  1. Mono switchIfEmpty() is always called

2020年7月12日 星期日

Scala 的 HTTP 伺服器:http4s

在 Ubuntu 準備 sbt 環境

雖然覺得這個步驟蠻沒意義的,不過總之我又久違地在 Windows 上開了個 Ubuntu 20.04 的 container。

在安裝 sbt 之前,需要先加入 sbt 的 repository 到 apt 的來源。

1
2
3
echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list
sudo apt-get update

接著就是安裝 Java 與 sbt。

1
sudo apt-get install -y default-jre sbt

以我目前這個時間點來說,會裝出來的版本如下:

1
2
Java: 11.0.7
sbt: 1.3.13
產生 http4s 範例專案

執行以下的初始化指令,讓 sbt 產生一個 http4s 的範例小專案。

1
sbt new http4s/http4s.g8 -b 0.21

這個指令其實本來在 http4s 的 Quick Start 裡是還有指定 sbt 版本的 -sbt-version 1.3.12,不過這裡就把它拿掉,讓它直接用剛裝的版本了。產生過程會問一些問題,這裡基本上我全都用預設值了。

接著就可以來看看產生出來的檔案有哪些內容了:

build.sbt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
organization := "com.example"
name := "scala-test"
version := "0.0.1-SNAPSHOT"
scalaVersion := "2.12.4"
 
val Http4sVersion = "0.16.6a"
val Specs2Version = "4.0.2"
val LogbackVersion = "1.2.3"
 
libraryDependencies ++= Seq(
  "org.http4s"     %% "http4s-blaze-server"  % Http4sVersion,
  "org.http4s"     %% "http4s-circe"         % Http4sVersion,
  "org.http4s"     %% "http4s-dsl"           % Http4sVersion,
  "org.specs2"     %% "specs2-core"          % Specs2Version % "test",
  "ch.qos.logback" "logback-classic"      % LogbackVersion
)

HelloWorld.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.scalatest
 
import io.circe._
import org.http4s._
import org.http4s.circe._
import org.http4s.server._
import org.http4s.dsl._
 
object HelloWorld {
  val service = HttpService {
    case GET -> Root / "hello" / name =>
      Ok(Json.obj("message" -> Json.fromString(s"Hello, ${name}")))
  }
}

Server.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.scalatest
 
import scala.util.Properties.envOrNone
import org.http4s.server.blaze.BlazeBuilder
import org.http4s.util.ProcessApp
import scalaz.concurrent.Task
import scalaz.stream.Process
 
object Server extends ProcessApp {
  val port: Int = envOrNone("HTTP_PORT").fold(8080)(_.toInt)
 
  def process(args: List[String]): Process[Task, Nothing] = BlazeBuilder.bindHttp(port)
    .mountService(HelloWorld.service, "/")
    .serve
}

這樣就是個最簡單能跑的 Scala + http4s 的小專案了,可以透過 sbt run 啟動伺服器。執行後可以看到像是這樣的啟動訊息:

1
2
3
4
5
6
7
[info] Loading settings from plugins.sbt ...
[info] Loading project definition from G:\java\intellij\scala-http4s-test\project
[info] Loading settings from build.sbt ...
[info] Set current project to scala-test (in build file:/G:/java/intellij/scala-http4s-test/)
[info] Running com.example.scalatest.Server
[pool-6-thread-4] INFO  o.h.b.c.n.NIO1SocketServerGroup - Service bound to address /127.0.0.1:8080
[pool-6-thread-4] INFO  o.h.s.b.BlazeBuilder - http4s v0.16.6a on blaze v0.12.11 started at http://127.0.0.1:8080/

啟動完成後,就可以用瀏覽器連上 http://127.0.0.1:8080/ 了,例如範例的 REST API 網址會是 http://127.0.0.1:8080/hello/michael,因為輸入的 {name} 是 michael,所以就會獲得 {"message":"Hello, michael"} 的 JSON 回覆。

參考資料
  1. Installing sbt on Linux