Java
Maven2
AzureFunctions
maven-assembly-plugin

概要

azure functionsで、正式にjavaがサポートされたらしいので、ちょっとやってみます。
基本的には、下記の流れ通りでできましたが、途中何回かつまずいたので、つまずき内容を含めメモしておきます。
Java と Maven を使用して初めての関数を作成する (プレビュー)

環境

  • windows10
  • eclipse

まずはいろいろインストール

.NET CORE

↓からインストールします。
https://www.microsoft.com/net/learn/get-started/windows

node.js

インストーラ

『Windows Installer (.msi)』を選ぶ。
https://nodejs.org/en/download/

確認

インストールしたら、既にpathが通っている。
コマンドプロンプトで確認しよう。

> node -v
v8.9.4

Azure Functions Core Tools

Azure functionsをローカルで実行したり、デバッグしたりできるツールです。
node.jsをインストールしたので、npmを使ってインストールします。

>npm install -g azure-functions-core-tools@core
C:\Users\xxx\AppData\Roaming\npm\func -> C:\Users\xxx\AppData\Roaming\npm\node_modules\azure-functions-core-tools\lib\main.js
C:\Users\xxx\AppData\Roaming\npm\azurefunctions -> C:\Users\xxx\AppData\Roaming\npm\node_modules\azure-functions-core-tools\lib\main.js
C:\Users\xxx\AppData\Roaming\npm\azfun -> C:\Users\xxx\AppData\Roaming\npm\node_modules\azure-functions-core-tools\lib\main.js

azure-functions-core-tools@2.0.1-beta.22 postinstall C:\Users\xxx\AppData\Roaming\npm\node_modules\azure-functions-core-tools
node lib/install.js

[==================] Downloading Azure Functions Cli
+ azure-functions-core-tools@2.0.1-beta.22
added 46 packages in 8.164s

Azure CLI

名前の通り、Azureをコマンドラインで色々できるツールです。
下記を参考にインストール。
https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli-windows

プロジェクトを作成

コマンドプロンプトでeclipseのworkspaceに移り、下記コマンドを実行。
GUIで新規プロジェクトを作ってもいいと思います。

mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure -DarchetypeArtifactId=azure-functions-archetype

groupId等を色々聞いてくるので、好きな名前をつけよう。

Define value for property 'groupId': jp.suzuq
Define value for property 'artifactId': azure.functions
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1
Define value for property 'package' jp.suzuq: : jp.suzuq.azure.functions
Define value for property 'appName' azure.functions-20180222185249620: :
Define value for property 'appRegion' westus: : jp

さっそくビルド

プロジェクトができた段階で、簡単なfunctionクラスが作成されています。
まずはこれをビルド&デプロイして動作確認してみましょう。

mvn clean package

そのままやったら、下記のエラーでビルドが失敗した。

Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:0.1.10:package (package-functions) on project azure.functions: Execution package-functions of goal com.microsoft.azure:azure-functions-maven-plugin:0.1.10:package failed: A required class was missing while executing com.microsoft.azure:azure-functions-maven-plugin:0.1.10: package: com/microsoft/azure/serverless/functions/annotation/DocumentDBInput

azure-functions-maven-pluginが何かマズいみたいなので、バージョンを最新の0.2.1に変えてみたらうまくいきました。

pom.xml
                <plugin>
                    <groupId>com.microsoft.azure</groupId>
                    <artifactId>azure-functions-maven-plugin</artifactId>
                    <version>0.2.1</version>
                </plugin>

ローカルでテスト実行

Azureにデプロイする前に、ローカルで動かしてみます。

mvn azure-functions:run

コンソールに、functionsのロゴが表示されて…

[INFO] Starting running Azure Functions...

                  %%%%%%
                 %%%%%%
            @   %%%%%%    @
          @@   %%%%%%      @@
       @@@    %%%%%%%%%%%    @@@
     @@      %%%%%%%%%%        @@
       @@         %%%%       @@
         @@      %%%       @@
           @@    %%      @@
                %%
                %

下記の表示が出たら準備完了。

Http Functions:

    hello: http://localhost:7071/api/hello

指示された通り、curlで下記を実行してみます。
curlがなければ、何かhttpを投げられるツールを使ってください。

curl -w '\n' -d hoge http://localhost:7071/api/hello

コンソールにログが出力され、実行されている事がわかります。

[2018/05/02 9:51:28] Function started (Id=0aed5288-e5ff-47e2-868b-8455d7731b70)
[2018/05/02 9:51:28] Executing 'Functions.hello' (Reason='This function was programmatically called via the host APIs.', Id=0aed5288-e5ff-47e2-868b-8455d7731b70)
[2018/05/02 9:51:28] Java HTTP trigger processed a request.
[2018/05/02 9:51:28] Function "hello" (ID: 40832889-9057-4664-885e-c84407660a4a) invoked by Java Worker
[2018/05/02 9:51:28] Function completed (Success, Id=0aed5288-e5ff-47e2-868b-8455d7731b70, Duration=5ms)
[2018/05/02 9:51:28] Executed 'Functions.hello' (Succeeded, Id=0aed5288-e5ff-47e2-868b-8455d7731b70)

200番のレスポンスとともに、実行結果が返されました。

Hello, hoge'

デプロイ

いよいよデプロイしてみます。

azureにログイン

azure CLIを使ってログインしておきます。
PCでpowershellを開いて「az login」。

> az login
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXXXXXX to authenticate.

指示された通り、https://microsoft.com/devicelogin へ行ってpowershellのコンソールに表示されたコードを入力。
「az login」コマンドが終了して、ログインしたazureの情報が示されます。

[
  {
    "cloudName": "AzureCloud",
    "id": "xxxxxxxxxxxxxxxxxxxxxxx",
    "isDefault": true,
    "name": "Microsoft Azure",
    "state": "Enabled",
    "tenantId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "user": {
      "name": "xxxx@xxxxxxxxxxxxxxxxxxxxxx",
      "type": "user"
    }
  }
]

mavenでデプロイ

eclipseに戻って、下記mavenコマンドを実行します。

mvn azure-functions:deploy

なんかエラー出た…。

[ERROR] Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:0.2.1:deploy (default-cli) on project azure.functions: Status code 400, {"error":{"code":"LocationNotAvailableForResourceGroup","message":"The provided location 'jp' is not available for resource group. List of available regions is 'centralus,eastasia,southeastasia,eastus,eastus2,westus,westus2,northcentralus,southcentralus,westcentralus,northeurope,westeurope,japaneast,japanwest,brazilsouth,australiasoutheast,australiaeast,westindia,southindia,centralindia,canadacentral,canadaeast,uksouth,ukwest,koreacentral,koreasouth,francecentral'."}}

regionがどうこう言ってるので、pom.xmlのregionっぽい設定を「jp」から「japaneast」に変えてみる。

pom.xml
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <functionAppName>azure.functions-9999999.azurewebsites.net</functionAppName>
        <functionAppRegion>japaneast</functionAppRegion>
    </properties>

今度は違うエラーが。

[ERROR] Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:0.2.1:deploy (default-cli) on project azure.functions: The host name azure.functions-20180222185249620.azurewebsites.net is invalid. OnError while emitting onNext value: retrofit2.Response.class

function名を変えてみる。
function名は、azureにデプロイされるjarのファイル名にもなるので、ここでもう一度ビルドしておく。

pom.xml
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <functionAppName>testFunction</functionAppName>
        <functionAppRegion>japaneast</functionAppRegion>
    </properties>

またエラー。

[ERROR] Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:0.2.1:deploy (default-cli) on project azure.functions: {
[ERROR] "message": "An error has occurred.",
[ERROR] "exceptionMessage": "An error occurred when trying to create a controller of type 'DeploymentController'. Make sure that the controller has a parameterless public constructor.",
[ERROR] "exceptionType": "System.InvalidOperationException",
[ERROR] "stackTrace": "   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)\r\n   at System.Web.Http.Controllers.HttpControllerDescriptor.CreateController(HttpRequestMessage request)\r\n   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()",
[ERROR] "innerException": {
[ERROR] "message": "An error has occurred.",
[ERROR] "exceptionMessage": "The user name or password is incorrect.\r\n",
[ERROR] "exceptionType": "System.IO.IOException",
[ERROR] "stackTrace": "   at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n   at System.IO.Directory.InternalCreateDirectory(String fullPath, String path, Object dirSecurityObj, Boolean checkHost)\r\n   at System.IO.Directory.InternalCreateDirectoryHelper(String path, Boolean checkHost)\r\n   at System.IO.Directory.CreateDirectory(String path)\r\n   at System.IO.Abstractions.DirectoryWrapper.CreateDirectory(String path)\r\n   at Microsoft.Web.Deployment.WebApi.PathHelper.GetLogFilesFolderPath(IEnvironment environment)\r\n   at Microsoft.Web.Deployment.WebApi.DeploymentController..ctor()\r\n   at lambda_method(Closure )\r\n   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)"
[ERROR] }
[ERROR] }: OnError while emitting onNext value: retrofit2.Response.class

ちょっと何言ってるかわかんないので、そのままもう一度やってみたら、なぜかSUCCESS。

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 35.832 s
[INFO] Finished at: 2018-05-10T18:55:38+09:00
[INFO] Final Memory: 48M/551M
[INFO] ------------------------------------------------------------------------

azure portalでデプロイしたfunctionを確認しよう。
サイドメニューの「Function App」から。

azurePortal.png

実行してみる

↑の画像の右上「関数のURLの取得」からURLをコピーできる。
コピーしたURLにcurlからリクエストを投げてみましょう。

curl -w '\n' https://functionName.azurewebsites.net/api/hello -d AzureFunctions
Hello, AzureFunctions

無事動きました!

オリジナルのfunctionを作る

とりあえずデフォルトで用意されたfunctionは動いたので、オリジナルのやつを作っていきましょう。
さっき実行したのは、workspaceに自動生成されたこのプログラム。
これを参考に、自分の関数を作っていきます。

Function.java
package jp.suzuq.azure.functions;

import java.util.*;
import com.microsoft.azure.serverless.functions.annotation.*;
import com.microsoft.azure.serverless.functions.*;

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

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

        if (name == null) {
            return request.createResponse(400, "Please pass a name on the query string or in the request body");
        } else {
            return request.createResponse(200, "Hello, " + name);
        }
    }
}

タイマートリガーを作る

デフォルトのは、httpリクエストがトリガーとなって動くfunctionだったので、タイマーがトリガーとなって動くのを作ってみます。

TimerFunction.java
package jp.suzuq.azure.functions;

import com.microsoft.azure.serverless.functions.ExecutionContext;
import com.microsoft.azure.serverless.functions.annotation.FunctionName;
import com.microsoft.azure.serverless.functions.annotation.TimerTrigger;

public class TimerFunction {
    @FunctionName("timer")
    public void timer(
            @TimerTrigger(name = "timer", schedule = "0 */5 * * * *") String timerInfo,
            final ExecutionContext context) 
    {
        context.getLogger().info("timerInfo : " + timerInfo);

    }
}

メソッドの第一引数に@TimerTriggerアノテーションをつけてあげます。
んで、「schedule」にcron形式でスケジュールを記載します。これがfunctionの実行スケジュールになります。

ライブラリを追加

なんでjavaでAzure functionsを作りたかったかって、javaの色んなライブラリを使いたかったから。
社内mavenリポジトリに、javaの便利クラスがたくさんあって、普段の開発で使っているので、Azure functionsでも使えたらいいな、と思ったのです。

pom.xmlに追加してデプロイしてみます。

pom.xml
        <dependency>
            <groupId>jp.hoge</groupId>
            <artifactId>HogeCommon</artifactId>
            <version>1.20.0</version>
        </dependency>

動かしてみると、なぜかfunctionがタイムアウトになり、途中で強制終了してしまいます。
なぜタイムアウト?と色々悩みましたが、結局のところpomに追加したライブラリが、ビルド済モジュールに含まれていなかったようです。ならClassNotFoundExceptionを出してくれよ…

ビルド済モジュールは、eclipse workspaceのtargetフォルダを見るか、実際にデプロイされたものを見たければAzure portalからKuduでダウンロードできます。

スケッチ.png

スケッチ2.png

ビルドのしかたが悪かったという事で、下記を参考にpom.xmlを修正しました。
How to add dependency JAR in java azure functions

pom.xml
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-assembly-plugin</artifactId>
               <configuration>
                  <appendAssemblyId>false</appendAssemblyId>
                  <descriptorRefs>
                     <descriptorRef>jar-with-dependencies</descriptorRef>
                  </descriptorRefs>
                  <archive />
               </configuration>
               <executions>
                  <execution>
                     <id>make-assembly</id>
                     <phase>package</phase>
                     <goals>
                        <goal>single</goal>
                     </goals>
                  </execution>
               </executions>
            </plugin>

わ~い!動いた!
1528452015300.png

まだまだ色々試してみたいですが、とりあえずここまで。

感想

社内ライブラリをAzure Functionsで使う事ができて満足。
でもやっぱ色々と準備するのが面倒ですね。
簡単なfunctionなら、やっぱjavascriptで作るのが一番簡単。
javaは今回のように社内の既存資産を使いまわしたり、ローカルでデバッグできるのがいいですね。
用途によって使い分けたいと思います。

参考になったサイト

Java と Maven を使用して初めての関数を作成する (プレビュー)
javaでトリガーを設定する
Azure CLIインストール&ログイン(Windows版)
How to add dependency JAR in java azure functions