CLOVER🍀

That was when it all began.

Azure Functions(Java)をローカルで動かしてみる

これは、なにをしたくて書いたもの?

Azure Functionsはローカルでも多少動かせるようなので、Javaで試してみようかな、と思いまして。

どういった雰囲気なのか、まずは知ってみようというのが目的です。

お題

Azure Functions(Java)を、HTTPトリガーとしてコマンドラインで作成して、ローカルで動かしてみることにします。

コマンド ラインから Java 関数を作成する - Azure Functions | Microsoft Docs

Azure FunctionsのJavaに関するAPIドキュメントは、こちらです。

Library for Azure Java Functions | Microsoft Docs

環境

今回の環境は、こちら。ローカルで動かすだけなら、JavaMaven、そしてAzure Functions Core Toolsがインストールされていれば
よさそうです。

$ func -v
3.0.3388


$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /home/kazuhira/.sdkman/candidates/maven/current
Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-67-generic", arch: "amd64", family: "unix"

Azure Functions Javaプロジェクトを作成する

こちらのドキュメントを見ながら、Azure Functionsをローカルで動かすためのプロジェクトを作成します。

クイックスタート: コマンド ラインから Azure に Java 関数を作成する / ローカル関数プロジェクトを作成する

Mavenアーキタイプで作成するようです。今回は、こんな感じで指定しました。

$ mvn archetype:generate \
 -DarchetypeGroupId=com.microsoft.azure \
 -DarchetypeArtifactId=azure-functions-archetype \
 -DgroupId=org.littlewings \
 -DartifactId=hello-java-function \
 -Dversion=0.0.1-SNAPSHOT \
 -Dpackage=org.littlewings.azure.function \
 -DappRegion=japaneast \
 -Dtrigger=HttpTrigger \
 -DjavaVersion=11 \
 -DinteractiveMode=false

アーキタイプは、azure-functions-archetypeを指定します。作成する関数のタイプは、HTTPトリガーです。

作成している様子。

[INFO] --------------------------------[ pom ]---------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:3.2.0:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:3.2.0:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO] 
[INFO] 
[INFO] --- maven-archetype-plugin:3.2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] Archetype [com.microsoft.azure:azure-functions-archetype:1.35] found in catalog remote
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: azure-functions-archetype:1.35
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.littlewings
[INFO] Parameter: artifactId, Value: hello-java-function
[INFO] Parameter: version, Value: 0.0.1-SNAPSHOT
[INFO] Parameter: package, Value: org.littlewings.azure.function
[INFO] Parameter: packageInPathFormat, Value: org/littlewings/azure/function
[INFO] Parameter: resourceGroup, Value: java-functions-group
[INFO] Parameter: package, Value: org.littlewings.azure.function
[INFO] Parameter: appName, Value: $(artifactId)-$(timestamp)
[INFO] Parameter: javaVersion, Value: 11
[INFO] Parameter: groupId, Value: org.littlewings
[INFO] Parameter: artifactId, Value: hello-java-function
[INFO] Parameter: appServicePlanName, Value: java-functions-app-service-plan
[INFO] Parameter: trigger, Value: HttpTrigger
[INFO] Parameter: appRegion, Value: japaneast
[INFO] Parameter: version, Value: 0.0.1-SNAPSHOT
[INFO] Parameter: docker, Value: false
[INFO] Executing META-INF/archetype-post-generate.groovy post-generation script

Azure Functions Archetypeは、1.35です。

プロジェクト作成時にどのようなパラメーターを指定できるかは、こちらを見ればよさそうです。
※1.35のタグがなかったので、1.33を参照しています

azure-maven-archetypes/azure-functions-archetype at azure-functions-archetype-1.33 · microsoft/azure-maven-archetypes · GitHub

作成されたプロジェクトに移動。

$ cd hello-java-function

ディレクトリ内に作成されたファイルは、こんな感じになっています。

$ tree -a
.
├── .gitignore
├── host.json
├── local.settings.json
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── org
    │           └── littlewings
    │               └── azure
    │                   └── function
    │                       └── Function.java
    └── test
        └── java
            └── org
                └── littlewings
                    └── azure
                        └── function
                            ├── FunctionTest.java
                            └── HttpResponseMessageMock.java

13 directories, 7 files

pom.xmlを少し抜粋してみましょう。

プロパティ。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>11</java.version>
        <azure.functions.maven.plugin.version>1.9.2</azure.functions.maven.plugin.version>
        <azure.functions.java.library.version>1.4.0</azure.functions.java.library.version>
        <functionAppName>hello-java-function-20210324223813016</functionAppName>
        <stagingDirectory>${project.build.directory}/azure-functions/${functionAppName}</stagingDirectory>
    </properties>

依存関係。compileスコープは、azure-functions-java-libraryだけなんですね。

    <dependencies>
        <dependency>
            <groupId>com.microsoft.azure.functions</groupId>
            <artifactId>azure-functions-java-library</artifactId>
            <version>${azure.functions.java.library.version}</version>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.23.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

プラグインの設定。

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-functions-maven-plugin</artifactId>
                <version>${azure.functions.maven.plugin.version}</version>
                <configuration>
                    <!-- function app name -->
                    <appName>${functionAppName}</appName>
                    <!-- function app resource group -->
                    <resourceGroup>java-functions-group</resourceGroup>
                    <!-- function app service plan name -->
                    <appServicePlanName>java-functions-app-service-plan</appServicePlanName>
                    <!-- function app region-->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-regions for all valid values -->
                    <region>japaneast</region>
                    <!-- function pricingTier, default to be consumption if not specified -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-pricing-tiers for all valid values -->
                    <!-- <pricingTier></pricingTier> -->
                    
                    <!-- Whether to disable application insights, default is false -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details for all valid configurations for application insights-->
                    <!-- <disableAppInsights></disableAppInsights> -->
                    <runtime>
                        <!-- runtime os, could be windows, linux or docker-->
                        <os>windows</os>
                        <javaVersion>11</javaVersion>
                        <!-- for docker function, please set the following parameters -->
                        <!-- <image>[hub-user/]repo-name[:tag]</image> -->
                        <!-- <serverId></serverId> -->
                        <!-- <registryUrl></registryUrl>  -->
                    </runtime>
                    <appSettings>
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~3</value>
                        </property>
                    </appSettings>
                </configuration>
                <executions>
                    <execution>
                        <id>package-functions</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <overwrite>true</overwrite>
                            <outputDirectory>${stagingDirectory}</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${project.basedir}</directory>
                                    <includes>
                                        <include>host.json</include>
                                        <include>local.settings.json</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.1</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${stagingDirectory}/lib</outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                            <includeScope>runtime</includeScope>
                            <excludeArtifactIds>azure-functions-java-library</excludeArtifactIds>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!--Remove obj folder generated by .NET SDK in maven clean-->
            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>obj</directory>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>
        </plugins>

作成されたソースコード

src/main/java/org/littlewings/azure/function/Function.java

package org.littlewings.azure.function;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

import java.util.Optional;

/**
 * Azure Functions with HTTP Trigger.
 */
public class Function {
    /**
     * This function listens at endpoint "/api/HttpExample". Two ways to invoke it using "curl" command in bash:
     * 1. curl -d "HTTP Body" {your host}/api/HttpExample
     * 2. curl "{your host}/api/HttpExample?name=HTTP%20Query"
     */
    @FunctionName("HttpExample")
    public HttpResponseMessage run(
            @HttpTrigger(
                name = "req",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS)
                HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Parse query parameter
        final String query = request.getQueryParameters().get("name");
        final String name = request.getBody().orElse(query);

        if (name == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
        } else {
            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }
}

テストコードは、テストコードそのものとモックがあります。

src/test/java/org/littlewings/azure/function/FunctionTest.java

package org.littlewings.azure.function;

import com.microsoft.azure.functions.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.*;
import java.util.logging.Logger;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;


/**
 * Unit test for Function class.
 */
public class FunctionTest {
    /**
     * Unit test for HttpTriggerJava method.
     */
    @Test
    public void testHttpTriggerJava() throws Exception {
        // Setup
        @SuppressWarnings("unchecked")
        final HttpRequestMessage<Optional<String>> req = mock(HttpRequestMessage.class);

        final Map<String, String> queryParams = new HashMap<>();
        queryParams.put("name", "Azure");
        doReturn(queryParams).when(req).getQueryParameters();

        final Optional<String> queryBody = Optional.empty();
        doReturn(queryBody).when(req).getBody();

        doAnswer(new Answer<HttpResponseMessage.Builder>() {
            @Override
            public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
                HttpStatus status = (HttpStatus) invocation.getArguments()[0];
                return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
            }
        }).when(req).createResponseBuilder(any(HttpStatus.class));

        final ExecutionContext context = mock(ExecutionContext.class);
        doReturn(Logger.getGlobal()).when(context).getLogger();

        // Invoke
        final HttpResponseMessage ret = new Function().run(req, context);

        // Verify
        assertEquals(ret.getStatus(), HttpStatus.OK);
    }
}

`src/test/java/org/littlewings/azure/function/HttpResponseMessageMock.java

package org.littlewings.azure.function;

import com.microsoft.azure.functions.*;

import java.util.Map;
import java.util.HashMap;

/**
 * The mock for HttpResponseMessage, can be used in unit tests to verify if the
 * returned response by HTTP trigger function is correct or not.
 */
public class HttpResponseMessageMock implements HttpResponseMessage {
    private int httpStatusCode;
    private HttpStatusType httpStatus;
    private Object body;
    private Map<String, String> headers;

    public HttpResponseMessageMock(HttpStatusType status, Map<String, String> headers, Object body) {
        this.httpStatus = status;
        this.httpStatusCode = status.value();
        this.headers = headers;
        this.body = body;
    }

    @Override
    public HttpStatusType getStatus() {
        return this.httpStatus;
    }

    @Override
    public int getStatusCode() {
        return httpStatusCode;
    }

    @Override
    public String getHeader(String key) {
        return this.headers.get(key);
    }

    @Override
    public Object getBody() {
        return this.body;
    }

    public static class HttpResponseMessageBuilderMock implements HttpResponseMessage.Builder {
        private Object body;
        private int httpStatusCode;
        private Map<String, String> headers = new HashMap<>();
        private HttpStatusType httpStatus;

        public Builder status(HttpStatus status) {
            this.httpStatusCode = status.value();
            this.httpStatus = status;
            return this;
        }

        @Override
        public Builder status(HttpStatusType httpStatusType) {
            this.httpStatusCode = httpStatusType.value();
            this.httpStatus = httpStatusType;
            return this;
        }

        @Override
        public HttpResponseMessage.Builder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        @Override
        public HttpResponseMessage.Builder body(Object body) {
            this.body = body;
            return this;
        }

        @Override
        public HttpResponseMessage build() {
            return new HttpResponseMessageMock(this.httpStatus, this.headers, this.body);
        }
    }
}

あとは、設定ファイルがあります。こちらは、ローカルで動かすための設定ファイルのようです。

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}

Azure Functions Core Tools の操作 / ローカル設定ファイル

こちらは、Azureにデプロイした時に使われる設定ファイルのようですね。今回は使わないことになります。

host.json

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  } 
}

Azure Functions 2.x の host.json のリファレンス | Microsoft Docs

これら、生成されるファイルの元は、こちらになります。

https://github.com/microsoft/azure-maven-archetypes/tree/azure-functions-archetype-1.33/azure-functions-archetype/src/main/resources/archetype-resources

テストコードに目を戻してみましょう。なにかサーバーが立ち上がるというよりは、モックを使ってテストを行うのみの
ようです。

    @Test
    public void testHttpTriggerJava() throws Exception {
        // Setup
        @SuppressWarnings("unchecked")
        final HttpRequestMessage<Optional<String>> req = mock(HttpRequestMessage.class);

        final Map<String, String> queryParams = new HashMap<>();
        queryParams.put("name", "Azure");
        doReturn(queryParams).when(req).getQueryParameters();

        final Optional<String> queryBody = Optional.empty();
        doReturn(queryBody).when(req).getBody();

        doAnswer(new Answer<HttpResponseMessage.Builder>() {
            @Override
            public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
                HttpStatus status = (HttpStatus) invocation.getArguments()[0];
                return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
            }
        }).when(req).createResponseBuilder(any(HttpStatus.class));

        final ExecutionContext context = mock(ExecutionContext.class);
        doReturn(Logger.getGlobal()).when(context).getLogger();

        // Invoke
        final HttpResponseMessage ret = new Function().run(req, context);

        // Verify
        assertEquals(ret.getStatus(), HttpStatus.OK);
    }

テストを実行してみます。

$ mvn test

成功しました。

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.littlewings.azure.function.FunctionTest
3月 24, 2021 10:51:56 午後 org.littlewings.azure.function.Function run
情報: Java HTTP trigger processed a request.
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.636 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

では、Azure Functionsをローカルで起動してみましょう。

クイックスタート: コマンド ラインから Azure に Java 関数を作成する / 関数をローカルで実行する

まずは、パッケージングします。

$ mvn package

パッケージングをせずにAzure Functionsをローカルで実行しようとすると、「最初にmvn packageしてください」とエラーに
なります。

[ERROR] Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:1.9.2:run (default-cli) on project hello-java-function: Stage directory not found. Please run mvn package first. -> [Help 1]

起動。

$ mvn azure-functions:run

こんな表示が出ます。

[INFO] Azure Function App's staging directory found at: /path/to/hello-java-function/target/azure-functions/hello-java-function-20210324223813016
[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85fbd0bcfc8d743ff7d509122ae 
Function Runtime Version: 3.0.15371.0

[2021-03-24T14:03:33.760Z] Worker process started and initialized.

Functions:

    HttpExample: [GET,POST] http://localhost:7071/api/HttpExample

For detailed output, run func with --verbose flag.
[2021-03-24T14:03:38.852Z] Host lock lease acquired by instance ID '000000000000000000000000C3CC5E65'.

情報としては、こちらと同じですね。

Azure Functions Core Tools の操作 / 関数をローカルで実行する

関数の定義がこのようになっているので、nameをパラメーターとして受け取れるようです。

    @FunctionName("HttpExample")
    public HttpResponseMessage run(
            @HttpTrigger(
                name = "req",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS)
                HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Parse query parameter
        final String query = request.getQueryParameters().get("name");
        final String name = request.getBody().orElse(query);

        if (name == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
        } else {
            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }

確認してみます。

QueryStringで指定する場合。

$ curl -i http://localhost:7071/api/HttpExample?name=Isono
HTTP/1.1 200 OK
Date: Wed, 24 Mar 2021 14:09:47 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

Hello, Isono

HTTPボディとして指定する場合。

$ curl -i -XPOST http://localhost:7071/api/HttpExample -d name=Isono
HTTP/1.1 200 OK
Date: Wed, 24 Mar 2021 14:18:53 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

Hello, name=Isono

Mavenアーキタイプで作成した関数そのままですが、まずは動かせました。

自分で関数を作ってみる

せっかくなので、自分で関数を作成してみましょう。

こんな感じの関数を作成。

src/main/java/org/littlewings/azure/function/MyFunction.java

package org.littlewings.azure.function;

import java.util.Optional;
import java.util.logging.Logger;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.BindingName;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

public class MyFunction {
    @FunctionName("MyHttpFunction")
    public HttpResponseMessage run(@HttpTrigger(name = "req", methods = HttpMethod.GET, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
                                   @BindingName("firstName") String firstName,
                                   @BindingName("lastName") String lastName,
                                   ExecutionContext context) {
        Logger logger = context.getLogger();

        logger.info("firstName = " + firstName);
        logger.info("lastName = " + lastName);

        return request.createResponseBuilder(HttpStatus.OK).body(String.format("Hello %s %s!!", firstName, lastName)).build();
    }
}

関数名はMyHttpFunctionとし、パラメーターはバインディングとして受け取るようにしました。

    @FunctionName("MyHttpFunction")
    public HttpResponseMessage run(@HttpTrigger(name = "req", methods = HttpMethod.GET, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
                                   @BindingName("firstName") String firstName,
                                   @BindingName("lastName") String lastName,
                                   ExecutionContext context) {

Azure Functions のトリガーとバインド | Microsoft Docs

QueryStringを@BindingNameアノテーションで受け取るのに、参考にしたのはこちら。

Library for Azure Java Functions / Metadata

その他、リクエストパラメーターを扱う方法はこちらのドキュメントを見るのが良さそうです。JSONなどについても
記載があります。

Azure Functions の HTTP トリガー | Microsoft Docs

ちなみに、ログはjava.util.loggingを使うようです。

Azure Functions の Java 開発者向けガイド / 実行コンテキスト

        Logger logger = context.getLogger();

        logger.info("firstName = " + firstName);
        logger.info("lastName = " + lastName);

レスポンスの作成。

        return request.createResponseBuilder(HttpStatus.OK).body(String.format("Hello %s %s!!", firstName, lastName)).build();

起動してみましょう。

$ mvn package -DskipTests=true
$ mvn azure-functions:run

認識する関数が、ひとつ増えます。

[INFO] Azure Function App's staging directory found at: /path/to/hello-java-function/target/azure-functions/hello-java-function-20210324223813016

[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85fbd0bcfc8d743ff7d509122ae 
Function Runtime Version: 3.0.15371.0

[2021-03-24T14:35:38.728Z] Worker process started and initialized.

Functions:

    HttpExample: [GET,POST] http://localhost:7071/api/HttpExample

    MyHttpFunction: [GET] http://localhost:7071/api/MyHttpFunction

For detailed output, run func with --verbose flag.
[2021-03-24T14:35:43.822Z] Host lock lease acquired by instance ID '000000000000000000000000C3CC5E65'.

確認。

$ curl -i 'http://localhost:7071/api/MyHttpFunction?firstName=Katsuo&lastName=Isono'
HTTP/1.1 200 OK
Date: Wed, 24 Mar 2021 14:36:11 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

Hello Katsuo Isono!!

OKですね。

まとめ

ローカルで、Azure Functions(Java)を動かしてみました。こうやって簡単に実行できるのは良いですね。