LoginSignup
0

More than 1 year has passed since last update.

Azure Functions で Spring Cloud Functionを使う

Posted at

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 の説明は以下を参照しましょう。

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でも投げておきたいところです。

あまり使われてないのかなと思わざるを得ないバグでした。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0