はじめに
Spring Cloud Functionとは、関数(Function)によってビジネスロジックを実装し、実行環境に依存せず、同じコードでWebエンドポイント、ストリーム処理、タスクを実行できるようにするフレームワークです。
AWS LambdaやAzure Functionsなどのサーバーレス環境で動作させることができます。
要はSpringの便利機能を使いながらFunction開発できますというシロモノです。
この記事ではSpring Cloud Function × Azure Functionsを試してみます。
Mavenを使っているサンプルが多かったので、Gradleでプロジェクト作成したいと思います。
プロジェクトの作成
Spring InitializrでSpring Bootプロジェクトのひな型を作ります。
Project : Gradle Project
Language : Java
Packaging: Jar
Java : 11 or 8 (17ではデプロイできなかった: 2022/11/02 時点)
その他は任意
設定ファイル
Azure Functionsの機能を使えるようにするため、ビルド設定を修正します。
plugins {
id 'org.springframework.boot' version '2.7.5'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
+ id "com.microsoft.azure.azurefunctions" version "1.11.0"
id 'java'
}
group = 'azure.functions'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
+ compileJava.options.encoding = 'UTF-8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
+ implementation 'org.springframework.cloud:spring-cloud-function-adapter-azure:3.2.7'
+ implementation 'com.microsoft.azure.functions:azure-functions-java-library:2.1.0'
// ↓は任意
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
+ jar {
+ enabled = true
+ // jarファイル名を指定しないと、"${rootProject.name}-${version}-plan.jar"みたいなファイル名になって
+ // 実行時に「jarが見つからない」と怒られたので、明示的に指定
+ archiveFileName = "${rootProject.name}-${version}.jar"
+ manifest {
+ // メインクラスを指定
+ attributes 'Main-Class' : 'azure.functions.springcloudapp.SpringCloudApplication'
+ }
+}
+
+ // Azureへのデプロイ設定
+ azurefunctions {
+ resourceGroup = 'xxxxxxxxxxxxx' // リソースグループ名
+ appName = 'springcloudapp' // Functionアプリ名
+ region = 'eastus' // リージョン
+ appServicePlanName = 'xxxxxxxxxxxxx' // App Service Plan名
+ // ↑他にも色々リソースの指定ができます
+ runtime {
+ os = 'windows' // 'linux'にしたらなぜかデプロイできなかった
+ }
+ appSettings {
+ WEBSITE_RUN_FROM_PACKAGE = '1'
+ FUNCTIONS_EXTENSION_VERSION = '~4'
+ FUNCTIONS_WORKER_RUNTIME = 'java'
+ MAIN_CLASS = 'azure.functions.springcloudapp.SpringCloudApplication'
+ }
+ auth {
+ type = 'azure_cli'
+ }
+ // ローカルでデバッグするときは必要
+ localDebug = 'transport=dt_socket,server=y,suspend=n,address=5005'
+ deployment {
+ type = 'run_from_blob'
+ }
+ }
以下のファイルをプロジェクトルートに追加
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.*, 4.0.0)"
}
}
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "java",
"MAIN_CLASS": "azure.functions.springcloudapp.SpringCloudApplication",
"AzureWebJobsDashboard": ""
}
}
関数作成
大きく分けて、各トリガをハンドリングするためのHandlerと実際のロジックであるFunctionの2つを作成する必要があります。
Handlerでリクエストを受ける → Functionを呼び出す という流れになります。
Handler作成
トリガーをハンドリングするためにハンドラクラスを作成します。
ここではHttpTriggerを使用しています。
// FunctionInvokerというクラスを継承する
public class CreateTodoHandler extends FunctionInvoker<TodoDto, TodoDto> {
@FunctionName("saveTodo") // デプロイ時の関数アプリ名
public HttpResponseMessage saveTodo(
// @HttpTriggerにHTTPの受付情報を設定する
@HttpTrigger(
name = "req",
methods = { HttpMethod.POST },
route = "todos",
authLevel = AuthorizationLevel.FUNCTION
) HttpRequestMessage<TodoDto> request,
ExecutionContext context
) {
final TodoDto payload = request.getBody();
if (payload == null) {
return request.createResponseBuilder(HttpStatus.BAD_REQUEST).build();
}
// handleRequestメソッドを呼び出すと別で定義したFunctionが呼び出されて結果が返る
// @FunctionNameで指定した名前でDIコンテナ内のFunctionを検索する
final TodoDto todoCreated = handleRequest(payload, context);
return request
.createResponseBuilder(HttpStatus.CREATED)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(
HttpHeaders.LOCATION,
UriComponentsBuilder
.fromUri(request.getUri())
.path("/{id}")
.buildAndExpand(todoCreated.getId())
.toString()
)
.body(todoCreated)
.build();
}
}
Function作成
実際のロジック部分を実装します。
書き方は2通りあります。(個人的には1つ目が好み)
-
@Bean
を付与した、Function型を返却するメソッドを定義する -
@Component
を付与した、Funtionインターフェースの実装クラスを作る
※Function以外に、Supplier、Consumerも可
@Component
@RequiredArgsConstructor
public class TodoFunction {
// DIも使える
private final TodoService todoService;
// Handlerの@FunctionNameで指定した名前と同じ名前でDIコンテナに登録する
// デフォルトではメソッド名で登録されるので、メソッド名を合わせておけば@Beanで明示的に名前指定しなくてもOK
@Bean("saveTodo")
public Function<TodoDto, TodoDto> saveTodo() {
return (payload) -> {
return todoService.saveTodo(payload);
};
}
// 複数Functionを定義する場合はメソッドを追加する
}
// Handlerの@FunctionNameで指定した名前と同じ名前でDIコンテナに登録する
// デフォルトではクラス名(先頭は小文字)で登録される
// ※この場合だとクラス名が「SaveTodo」だったら@Componentで明示的に名前指定しなくてもOK
@Component("saveTodo")
@RequiredArgsConstructor
public class TodoFunction implements Function<TodoDto, TodoDto> {
private final TodoService todoService;
@Override
public TodoDto apply(TodoDto payload) {
return todoService.saveTodo(payload);
}
}
// 複数Functionを定義する場合はクラスを追加する
ローカルデバッグ
プロジェクトのルートディレクトリで以下のコマンドを実行。
./gradlew azureFunctionsRun
Azureへデプロイ
プロジェクトのルートディレクトリで以下のコマンドを実行。
./gradlew azureFunctionsDeploy
おまけ
ExecutionContextへのアクセス
HandlerのトリガーメソッドでExecutionContext
というのを受け取れます。
こいつは主にロギングの際に使うことになると思います。
context.getLogger().info("hogehoge");
このcontextをFunction側で使うには少し面倒ですがMessage
というのを使う必要があります。
結構Function側でログ出すことありそうなのに、ちょっと不便。
// Functionの入力をMessage(org.springframework.messaging.Message)にする
@Bean
public Function<Message<TodoDto>, TodoDto> saveTodo() {
return message -> {
// MessageのHeadersに"executionContext"というキーでExecutionContextがセットされてくる
ExecutionContext context =
ExecutionContext.class.cast(message.getHeaders().get("executionContext"));
context.getLogger().info("Invoke saveTodo");
return todoService.saveTodo(message.getPayload());
};
}
これをFunction毎に書くのは面倒なので、Functionインターフェースをラップしたインターフェースを作って共通化してみました。
public interface AzureFunction<I, O> extends Function<Message<I>, O> {
public static final String CONTEXT_HEADER_KEY = "executionContext";
@Override
default O apply(Message<I> message) {
ExecutionContext context =
ExecutionContext.class.cast(
message.getHeaders().get((CONTEXT_HEADER_KEY))
);
return apply(message.getPayload(), context);
}
/** handleRequestの引数とExecutionContextを受け取るメソッドを定義 */
O apply(I payload, ExecutionContext context);
}
使い方
@Bean
public AzureFunction<TodoDto, TodoDto> saveTodo() {
return (payload, context) -> {
context.getLogger().info("Invoke saveTodo");
return todoService.saveTodo(payload);
};
}
少しは使いやすくなった・・・(?)
ただし、Funtionの引数がPublisher(Mono、Flux)とかだと上手く型変換できませんでした。
もっとスマートなやり方があったら教えてください。
Function呼び出し時の型変換
HandlerのhandleRequest
メソッドでFunctionを呼び出すと、Functionの引数に合わせて自動的に型変換してくれたりします。
ざっくり確認してみたところ、以下のような感じでした。
入力時の型 (handleRequestの引数) |
変換後の型 (Functionの引数) |
変換可否 | 備考 |
---|---|---|---|
T | Collection<T> | ○ | 要素数1のCollectionに変換される |
T | Mono<T> | ○ | |
T | Flux<T> | ○ | 要素数1のFluxに変換される |
T | Message<T> | ○ |
Message::getPayload で入力値が取れる |
Collection<T> | T | ○ | Collectionの件数分Functionが実行されるhandleRequest の戻り値はArrayList<O1>になるただし、OがCollectionの場合はflatなリストになる |
Flux<T> | T | ○ | Fluxの件数分Functionが実行されるhandleRequest の戻り値は↑と同じ |
Message<T> | Message<T> | × | |
Message<T> | T | ○ |
Message::getPayload の値が渡される |
HttpRequestMessage<T> | HttpRequestMessage<T> | × | |
HttpRequestMessage<T> | T | ○ |
HttpRequestMessage::getBody の値が渡されるhandleRequest の戻り値はHttpResponseMessage になる |
HttpTrigger以外のトリガー
HttpTrigger以外のトリガーも普通に使えるようです。
以下のトリガーが使えるみたいです。
(Trigger自体はSpring Cloud Functionの機能ではなくAzure側が提供している)
- BlobTrigger
- TimerTrigger
- KafkaTrigger
- QueueTrigger
- WarmupTrigger
- EventHubTrigger
- EventGridTrigger
- ServiceBusTopicTrigger
- ServiceBusQueueTrigger
試しにTimerTrigger
とEventGridTrigger
を使ってみました。
- Function1がTimerTriggerでEventGridのイベントを発行
- Function2がEventGridのイベントを受け取って、Function3(API)に対してリクエスト
- Function3がHttpリクエストを受けてCosmos DBにデータ登録
というよくわからないものを作成。
public class TimerHandler extends FunctionInvoker<String, EventGridEvent> {
public static final String FUNCTION_NAME = "timer";
@FunctionName(FUNCTION_NAME)
public void timer(
// 1分毎に発火
@TimerTrigger(name = "timer", schedule = "* * * * *") String timerInfo,
@EventGridOutput(
name = "outputEvent",
topicEndpointUri = "MyEventGridTopicUriSetting",
topicKeySetting = "MyEventGridTopicKeySetting"
) OutputBinding<EventGridEvent> outputEvent,
ExecutionContext context
) {
// handleRequestの戻り値をoutputEventにセットしている
handleOutput(timerInfo, outputEvent, context);
}
}
@Component
@RequiredArgsConstructor
public class TimerFunction {
@Bean
public AzureFunction<String, EventGridEvent> timer() {
return (payload, context) -> {
context.getLogger().info("Invoke timer: " + payload);
final EventGridEvent document = new EventGridEvent();
document.setId(UUID.randomUUID().toString());
document.setEventType("MyCustomEvent");
document.setEventTime(DateTimeUtil.formatDateTime(LocalDateTime.now()));
document.setDataVersion("1.0");
document.setSubject("EventGridSample");
document.setData(new TodoDto(null, "Content", false, null));
return document;
};
}
}
public class EventGridHandler extends FunctionInvoker<EventSchema, TodoDto> {
public static final String FUNCTION_NAME = "eventGrid";
@FunctionName(FUNCTION_NAME)
public void eventGrid(
@EventGridTrigger(name = "event") EventSchema event,
ExecutionContext context
) {
final TodoDto output = handleRequest(event, context);
context.getLogger().info(output.toString());
}
}
@Component
@RequiredArgsConstructor
public class EventGridFunction {
private final RestTemplate restTemplate;
@Value("${FUNCTION_ENDPOINT}")
private String baseEndpoint;
@Value("${FUNCTION_KEY}")
private String functionKey;
@Bean
public AzureFunction<EventSchema, TodoDto> eventGrid() {
return (event, context) -> {
context.getLogger().info("Event: " + event);
// Function3 API 呼び出し
RequestEntity<TodoDto> request = RequestEntity
.post(baseEndpoint + "/todos?code={code}", functionKey)
.contentType(MediaType.APPLICATION_JSON)
.body(event.getData());
ResponseEntity<TodoDto> response = restTemplate.exchange(
request,
TodoDto.class
);
return response.getBody();
};
}
}
Function3はHandler作成、Function作成 で作成したもの。
ソース
まとめ
Spring Cloud Functionを使うことで、Springの機能を活かしてFunctionの開発ができるようになります。
個人的にはDIできるようになるのが嬉しいです。
HttpTrigger以外のトリガーも使えるので、色々試してみたいと思います。
(公式のライブラリもその辺りは開発中のようなので、今後デフォルトでDIできるようになるかもしれません)
参考文献
- Spring Cloud Function
- Azure での Spring Cloud Function の概要
- Azure Functions の Azure Event Grid トリガー
- AzureEventGridSimulator(EventGridのローカルデバッグ環境)
-
O
はFunctionの戻り値型 ↩