SpringBoot 3.X系での AzureFunctionsのTimerTrigger実装方法

Spring Boot3.X系 でAzure FunctionsのTimer Triggerを実装する手順を示す。

プロジェクトの作成は以下の記事参照。

olafnosuke.hatenablog.com

SpringBoot2.X系の実装方法は以下の記事参照。

olafnosuke.hatenablog.com


TimerTriggerの起動時のパラメータを保持するクラスの作成

TimerTriggerはトリガーされると下記の形式のパラメータを渡してくるので、このパラメータを保持するためのクラスを作成する。

※TimerTriggerは関数アプリの再起動や、デプロイ時にトリガーされてしまうことがあり、予期していないタイミングで処理が実行されてしまうことがある。
※予期しないタイミングでの処理実行を防ぐために、一部パラメータの値を使用して制御を業務処理の実行前に制御を行う。

{
    "Schedule":{
        "AdjustForDST": true
    },
    "ScheduleStatus": {
        "Last":"2024-01-15T10:15:00+00:00",
        "LastUpdated":"2024-01-15T10:16:00+00:00",
        "Next":"2024-01-15T10:15:00+00:00"
    },
    "IsPastDue":false
}

Schedule内のパラメータ用クラス

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "AdjustForDST" })
@ToString
public class Schedule {

    /**
     * {@code true}:タイムゾーンに基づくサマータイムに合わせて調整される。<br>
     * {@code false}:常に標準時刻が維持される。
     */
    @JsonProperty("AdjustForDST")
    public boolean adjustForDST;
}

ScheduleStatus内のパラメータ用クラス

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "Last", "Next", "LastUpdated" })
@ToString
public class ScheduleStatus {

    /** 前回実行日時 */
    @JsonProperty("Last")
    public String last;

    /** 前回 ScheduleStatus を更新した時点での、次回実行予定日時 */
    @JsonProperty("Next")
    public String next;

    /** 前回 ScheduleStatus を更新した日時 */
    @JsonProperty("LastUpdated")
    public String lastUpdated;
}

パラメータ全体のクラス

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "Schedule", "ScheduleStatus", "IsPastDue" })
@ToString
public class TimerInfo {

    /** サマータイムの調整 */
    @JsonProperty("Schedule")
    @Valid
    public Schedule schedule;

    /** 前回実行日時と次回実行予定日時 */
    @JsonProperty("ScheduleStatus")
    @Valid
    public ScheduleStatus scheduleStatus;

    /**
     * {@code true} 現在の関数の呼び出しがスケジュールより遅い場合<br>
     * {@code false} 現在の関数の呼び出しがスケジュール通りの場合
     */
    @JsonProperty("IsPastDue")
    public boolean isPastDue;
}

Functionクラスの作成

Functionクラスには、ファンクションごとに行いたい処理を記述する。
FunctionクラスはHandlerクラスで呼び出し処理を実装するため、DIコンテナに登録されるように「@Componentアノテーションをクラスに付与している。

/**
 * TimerTriggerの処理
 */
@Component
@Slf4j
public class TimerFunction implements Function<TimerInfo, String> {

    /**
     * {@inheritDoc}
     */
    @Override
    public String apply(TimerInfo t) {
        log.info("かきくけこ");
        return t + "!!";
    }
}

Handlerクラスの作成

AzureFunctionの実装用に追加した「spring-cloud-function-adapter-azure」「spring-cloud-starter-function-web」のバージョン3.X系→4.x系の変更に伴い、Functionクラスを呼び出す処理の実装方法が変更となった。
※3.X系では「FunctionInvoker」を継承して、Functionクラスを呼び出す処理を実装していたが、「FunctionInvoker」が4.X系では非推奨となったため。

4.X系ではHandlerクラスもDIコンテナに登録し、HandlerクラスにFunctionクラスをDIする。
クラス内に定義した「@FunctionName」アノテーションを付与したメソッドの中で、Functionクラスを任意のタイミングで実行するように処理を記述する。

意図しないタイミングでのFunction起動に対する処理について

TimerTriggerはAzure Blob Storage上で前回起動時間や次回起動時間などを管理している。
※このためTimerTriggerの実装にAzureBlobStorageは必須
関数アプリ自体を長く止めていた場合などでは、関数アプリを再起動した際に、AzureBlobStorage上のスケジュールをTimerTriggerが参照したときにFunctionの次回実行予定日時を過ぎてしまっている場合があり、この時に予期していないタイミングで処理が実行されてしまう場合がある。

isPastDue」パラメータは「次回起動予定時間」と比較して今が過去日かどうかを確認するパラメータである。
このパラメータが「true」の場合、予定時刻よりも遅れてTimerTriggerがトリガーされたということが分かるので、この場合に業務ロジックを行わないように処理を分岐すれば、意図しないタイミングでの処理実行は避けられる。

/**
 * サンプルTimer Trigger
 */
@Component
@Slf4j
public class TimerHandler {

    private TimerFunction timerFunction;

    public TimerHandler(TimerFunction timerFunction) {
        this.timerFunction = timerFunction;
    }

    @FunctionName("timer")
    public String timerTrigger(
          @TimerTrigger(name = "timer", schedule = "0 */1 * * * *") String timerinfo,
          ExecutionContext context) throws JsonMappingException, JsonProcessingException {
      // 起動時のパラメータを引数で受け取る際に、作成したクラスで受けても、入れ子にしたクラスに対してマッピングが上手くいかない。
      // 引数では1回文字列で受けて、処理の中で手動でObjectMapperを使用してマッピングしている
      TimerInfo info = new ObjectMapper().readValue(timerinfo, TimerInfo.class);

      // 意図したタイミングでの起動かどうかを確認する処理
      if (info.isPastDue) {
          log.info("業務処理をスキップします");
          return null;
      }

      // 業務ロジック呼び出し
      return this.timerFunction.apply(info);
  }

}

トリガーの設定

トリガーの設定はアノテーションで行う。@FunctionName("timer")を付与したメソッドのパラメーターにcom.microsoft.azure.functions.annotation.TimerTriggerを付与する。
アノテーションの属性については以下の通り。

属性名 説明
name リクエストやリクエストボディのファンクションコードで使用される変数名。必須
dataType パラメータ値をどのように扱うかを定義する。以下の値が設定可能。
"":値を文字列として取得し、POJOにデシリアライズしようとする(デフォルト
string:常に文字列として値を取得する
binary:値をバイナリデータとして取得し、byte[]にデシリアライズしようとする
schedule cronでファンクション実行のタイミングを設定する。必須
{second} {minute} {hour} {day} {month} {day-of-week}形式

スケジュールの各項目の概要と設定可能な値は以下の通り。

項目 概要 設定可能な値
second 0-59
minute 0-59
hour 時間 0-23
day 1-31
month 1-12
day-of-week 曜日 0-6(0が日曜日)

スケジュールの設定例をいくつか示す。

スケジュール 設定
1月の毎週月曜日の午前9時30分 0 30 9 * 1 1
平日の午前9時30分 0 30 9 * * 1-5
5分間隔 0 */5 * * * *

CRON式で使用する既定のタイムゾーンUTC。別のタイムゾーンに基づくCRON式を使うには、Function App用にWEBSITE_TIME_ZONEという名前のアプリ設定を追加する。参考
ただしローカルで実行する場合はシステムのタイムゾーンとなる。

// 記述例
public String timerTrigger(
      @TimerTrigger(name = "timer", schedule = "0 */1 * * * *") String timerinfo,
      ExecutionContext context) {
}

ローカルでの動作確認

Timer Triggerの起動には、Azure Blob Storageが必要となる。。
今回は、ローカル環境で使用できるAzure Blob StorageのエミュレータであるAzuriteを使用する例を示す。

Azuriteの導入

コマンドプロンプトで以下のコマンドを実行する。

npm install -g azurite

インストール完了後にc:\\azuriteディレクトリを作成し、以下のコマンドでAzuriteを起動する

azurite --silent --location c:\\azurite --debug c:\\azurite\\debug.log

起動すると以下のようなログが表示される

C:>azurite --silent --location c:\\azurite --debug c:\\azurite\\debug.log
Azurite Blob service is starting at http://127.0.0.1:10000
Azurite Blob service is successfully listening at http://127.0.0.1:10000
Azurite Queue service is starting at http://127.0.0.1:10001
Azurite Queue service is successfully listening at http://127.0.0.1:10001
Azurite Table service is starting at http://127.0.0.1:10002
Azurite Table service is successfully listening at http://127.0.0.1:10002

起動中のAzuriteはCtrl + cで停止できる。

ファンクションの設定

ファンクションプロジェクトのlocal.settings.jsonを開き、AzureWebJobsStorageUseDevelopmentStorage=trueと設定する。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    "MAIN_CLASS":"jp.co.sample.functions.Application"
  }
}

ファンクションの処理の中でAzureBlobStorageに対して処理を行っている場合、以下の接続設定を利用する。 以下はAzuriteの接続設定である。

設定
アカウントキー Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
アカウント名 devstoreaccount1
Blobエンドポイント http://127.0.0.1:10000/devstoreaccount1

Azurite起動中はMicrosoft Azure Storage Explorerの「ローカルで接続済み」->「ストレージアカウント」->「エミュレーター」で起動中のエミュレータの操作をすることも可能。

ファンクション動作確認

ファンクションの作成完了後にプロジェクトをビルドする。

gradlew azureFunctionsPackage

以下のコマンドでAzuriteを起動する

azurite --silent --location c:\azurite --debug c:\azurite\debug.log

ファンクションを起動する。

gradlew azureFunctionsRun

起動に成功すると以下のようにファンクションの一覧が表示される。

> Task :azureFunctionsRun
Azure Function App's staging directory found at: C:\workspace\functions\build\azure-functions\java-template-func

Azure Functions Core Tools
Core Tools Version:       4.0.5198 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.21.1.20667

Functions:

        timer: timerTrigger

起動後にスケジュールに記載したタイミングでファンクションが実行される。


任意のタイミングでファンクションを実行する

任意のタイミングでファンクションを実行したい場合は以下のURLに対してPOSTリクエストを送信する。 http://localhost:7071/admin/functions/{ファンクション名}
その際、POSTデータには以下の形式のデータを記載する。

{
    "Schedule":{
        "AdjustForDST": true
    },
    "ScheduleStatus": {
        "Last":"2024-01-15T10:15:00+00:00",
        "LastUpdated":"2024-01-15T10:16:00+00:00",
        "Next":"2024-01-15T10:15:00+00:00"
    },
    "IsPastDue":false
}

TimerTriggerを任意で呼び出すPOSTリクエスト