Azure Functions で Spring Cloud Functionを使う
Spring には、 Spring Cloud Function という関数指向なフレームワークが存在しています。これをAzure Functionsと組み合わせて使うことができるのですが、今回はそのあたりを試してみたいと思います。
Java で Azure Functions は、そこそこ書いているのですが、.NET と違いDI できなかったり、構成設定がお手軽でなかったりするのがイマイチだったりします。あとは Graceful shutdown や、Durable Functions などがサポートされてないこともですが。
今回試そうと思ったのは、 Azure Functions も Spring フレームワークに乗ってさえしまえば、D Iや構成設定の懸案が解消されるのでは無いかと思ったわけです。
Spring Cloud Function
Spring Cloud Function の説明は以下を参照しましょう。
ざっくり説明すると、規約に従って定義したBeanやクラスが、特別なことをせずに、そのままREST API になったりしてくれます。
例えば、以下のように Function
な、Beanを定義しておくと、それがそのままREST APIとなります。
@Bean
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
}
上記の例だと、String -> String の関数ですので、データをPOSTするか、繋げてパスに指定するとデータと勝手にバインドされます。
$ curl "http://localhost:8080/uppercase/abcdefg
ABCDEFG
また、Mono/Fluxを利用して Reactorによる非同期プログラムにも対応しています。Azure SDK も Reactorを利用している場合が多いので、相性が良いと思います。
@Bean
Function<Mono<String>, Mono<String>> uppercaseAsync() {
return value -> value.map(x -> x.toUpperCase());
}
これ以外にも、Functionを実装したクラスを用意し、関数が置かれている場所をspring.cloud.function.scan.packages=com.example.demo.functions
と設定しておくと、そのパッケージ内をスキャンして勝手にAPI化してくれます。
public class Echo implements Function<Mono<String>, Mono<String>> {
@Override
public Mono<String> apply(Mono<String> name) {
return name.map(x -> x + x);
}
}
思った以上に便利なので、普通の HttpTrigger くらいなら、これをWebApps にデプロイしてもいい気がしてきました。
Azure Functions で使ってみる
この Spring Cloud Function の フレームワークを Azure Functions にデプロイできるようにするライブラリが提供されています。Azure 以外にも GCP や AWS の labmda だったりもあるようです。
以下のドキュメントに詳細が書かれています(ちょっとドキュメントが書かれた日が古くてあまり頑張ってないのではないか疑惑が)
Azure での Spring Cloud Function の概要 | Microsoft Docs
HttpTriggerを定義するには、以下のように AzureSpringBootRequestHandler<T,R>
を実装したクラスを用意します。以下の場合 POJOとして、User
を入力としてバインドし、handleRequest
メソッドの返却値としtGreeting
を受け取るようなクラスを定義しています。簡単にいえば、単純な 入力 User
、出力 Greeting
という関数なわけです。
import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler;
public class HelloHandler extends AzureSpringBootRequestHandler<User, Greeting> {
public HttpResponseMessage execute(@HttpTrigger(name = "request", methods = { HttpMethod.GET, HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<User>> request,
@FunctionName("hello")
ExecutionContext context) {
context.getLogger().info("hello api is invoked.");
User user = request.getBody()
.filter((u -> u.getName() != null))
.orElseGet(() -> new User(
request.getQueryParameters()
.getOrDefault("name", "world")));
context.getLogger().info(greeting.toString());
context.getLogger().info("Greeting user name: " + user.getName());
return request
.createResponseBuilder(HttpStatus.OK)
.body(handleRequest(user, context))
.header("Content-Type", "application/json")
.build();
}
}
handleRequest
によって呼び出されるのは、別に定義 Function<T,R>
を実装したクラスとなります。Mono で囲っても良いし、Userそのものでも受け取ることもできます。同じシグネチャの関数が複数あると例外が出ると思います。
@Component
public class HelloFunction implements Function<Mono<User>, Mono<Greeting>> {
public Mono<Greeting> apply(Mono<User> mono) {
return mono.map(user -> new Greeting("Hello, " + user.getName() + "!"));
}
}
実行してリクエストを投げると、期待通りの結果が得られるでしょう。
$ curl http://localhost:7071/api/hello -d "{\"name\":\"statemachine\"}"
{
"message": "Hello, statemachine!"
}
DI してみる
正しくDIが動作するか、構成クラスを定義して、DIしてみましょう。
@Configuration
@ConfigurationProperties("myconfig")
public class MyConfig {
private String prefix;
public String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
}
コンストラクタインジェクションは使えないぽいので(例外がでる)、@Autowired
でDIします。
@Component
public class HelloFunction implements Function<Mono<User>, Mono<Greeting>> {
@Autowired
private MyConfig config;
public Mono<Greeting> apply(Mono<User> mono) {
return mono.map(user -> new Greeting(config.getPrefix() + ", " + user.getName() + "!"));
}
}
環境変数に、set myconfig.prefix=Good morning
とでもして、実行すると、ちゃんと MyConfig
クラスから値が取得できました。
$ curl http://localhost:7071/api/hello -d "{\"name\":\"statemachine\"}"
{
"message": "Good morning, statemachine!"
}
試した限りでは、リポジトリやサービスもDIできそうでした。
まとめ
Azure Functions + Spring Cloud Functionは、 Bean を定義しただけでREST API になるわけではないので、そのあたりの簡易性は失われてしまいますが、Azure Functions for Java にはない、Spring Boot 側のフレームワークが使えるようになるのはとても良い感じに使えそうです。
ただ、他のトリガとかでも使えるのかは分りません。HttpTriggerに特化しているので、時間が合ったら深掘りしてみたい感じです。
追伸
現バージョンですと AzureSpringBootRequestHandler
は、 @Deprecated
になっており FunctionInvoker
を使えと Javadoc に書かれているのですが、返却値側の Mono を正しく処理できていなくて、上のサンプル例ですと、 Greeting
が返るべきところ、 Mono<Greeting>
そのものが返ってきてしまいます。ちょっとバグっているのか仕様なのか分りませんが、時間があればIssueでも投げておきたいところです。
あまり使われてないのかなと思わざるを得ないバグでした。