これは、なにをしたくて書いたもの?
Azure Functionsはローカルでも多少動かせるようなので、Javaで試してみようかな、と思いまして。
どういった雰囲気なのか、まずは知ってみようというのが目的です。
お題
Azure Functions(Java)を、HTTPトリガーとしてコマンドラインで作成して、ローカルで動かしてみることにします。
コマンド ラインから Java 関数を作成する - Azure Functions | Microsoft Docs
Azure FunctionsのJavaに関するAPIドキュメントは、こちらです。
Library for Azure Java Functions | Microsoft Docs
環境
今回の環境は、こちら。ローカルで動かすだけなら、Java、Maven、そして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を参照しています
作成されたプロジェクトに移動。
$ 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
これら、生成されるファイルの元は、こちらになります。
テストコードに目を戻してみましょう。なにかサーバーが立ち上がるというよりは、モックを使ってテストを行うのみの
ようです。
@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)を動かしてみました。こうやって簡単に実行できるのは良いですね。