Azure基本

Azure App ServiceとAzure Functionsの両方でDevToolsを利用してみた

Spring Bootアプリケーションの開発を効率化するツールとして、DevToolsというツールがある。

DevToolsを利用すると、ソースコードを修正したタイミングで、Spring Bootアプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。

今回は、Azure App ServiceとAzure Functionsの両方でDevToolsを利用してみたので、その手順を共有する。

前提条件

下記記事の実装が完了していること。

Azure App ServiceとAzure Functionsの両方をSTSで起動してみたこれまでこのブログで、Azure Functionsの起動を「mvn azure-functions:run」コマンドで行ってきたが、A...

また、以下のように、STSの「プロジェクト」メニューで「自動的にビルド」にチェックが入っていること。
前提条件

作成したサンプルプログラム(App Service側)の内容

作成したサンプルプログラム(App Service側)の構成は以下の通り。
サンプルプログラムの構成(AppService側)
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。

pom.xml(追加分)の内容は以下の通りで、DevToolsの設定を追加している。

<!-- DevToolsの設定 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
</dependency>

コントローラクラスの内容は以下の通りで、初期表示を行うindexメソッドで、appServiceMessage・azureFunctionsMessageの値を追加している。

package com.example.demo;

import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.thymeleaf.util.StringUtils;

import com.example.model.FileData;
import com.example.model.FileUploadParam;
import com.example.model.FileUploadResult;
import com.example.model.GetFileListParam;
import com.example.model.GetFileListResult;

@Controller
public class DemoController {

    /** RestTemplateオブジェクト */
    @Autowired
    private RestTemplate restTemplate;

    /** application.propertiesからdemoAzureFunc.urlBaseの値を取得 */
    @Value("${demoAzureFunc.urlBase}")
    private String demoAzureFuncBase;

    /**
     * ファイルリストを取得し、メイン画面を初期表示する.
     * @param model   Modelオブジェクト
     * @param session HttpSessionオブジェクト
     * @return メイン画面
     */
    @GetMapping("/")
    public String index(Model model, HttpSession session) {
        // Azure FunctionsのgetFileList関数を呼び出すためのヘッダー情報を設定する
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Azure FunctionsのgetFileList関数を呼び出すための引数を設定する
        HttpEntity<GetFileListParam> entity 
            = new HttpEntity<>(new GetFileListParam(), headers);

        // Azure FunctionsのgetFileList関数を呼び出す
        ResponseEntity<GetFileListResult> response 
            = restTemplate.exchange(demoAzureFuncBase + "getFileList",
                HttpMethod.POST, new HttpEntity<>(entity), GetFileListResult.class);
        GetFileListResult getFileListResult = response.getBody();

        model.addAttribute("fileDataList", getFileListResult.getFileDataList());
        session.setAttribute("fileDataList", getFileListResult.getFileDataList());

        model.addAttribute("message"
            , "アップロードするファイルを指定し、アップロードボタンを押下してください。");
        
        // DevToolsの実行確認を行うためのメッセージを設定
        model.addAttribute("appServiceMessage", "Azure App Serviceのメッセージ1");
        model.addAttribute("azureFunctionsMessage"
            , getFileListResult.getAzureFunctionsMessage());
                
        return "main";
    }

    /**
     * ファイルダウンロード処理.
     * @param id       ID
     * @param response HttpServletResponse
     * @param session  HttpSessionオブジェクト
     * @return 画面遷移先(nullを返す)
     */
    @RequestMapping("/download")
    public String download(@RequestParam("id") String id
            , HttpServletResponse response, HttpSession session) {
        // セッションからファイルリストを取得する
        @SuppressWarnings("unchecked")
        ArrayList<FileData> fileDataList 
            = (ArrayList<FileData>) session.getAttribute("fileDataList");
        FileData fileData = fileDataList.get(Integer.parseInt(id) - 1);

        // ファイルダウンロードの設定を実施
        // ファイルの種類は指定しない
        response.setContentType("application/octet-stream");
        response.setHeader("Cache-Control", "private");
        response.setHeader("Pragma", "");
        response.setHeader("Content-Disposition",
            "attachment;filename=\"" 
           + getFileNameEncoded(fileData.getFileName()) + "\"");

        // ダウンロードファイルへ出力
        try (OutputStream out = response.getOutputStream()) {
            out.write(ArrayUtils.toPrimitive(fileData.getFileData()));
            out.flush();
        } catch (Exception e) {
            System.err.println(e);
        }

        // 画面遷移先はnullを指定
        return null;
    }

    /**
     * ファイルデータをAzure Blob Storageに登録する.
     * @param uploadFile         アップロードファイル
     * @param model              Modelオブジェクト
     * @param redirectAttributes リダイレクト先に渡すパラメータ
     * @return メイン画面
     */
    @PostMapping("/upload")
    public String add(@RequestParam("upload_file") MultipartFile uploadFile
        , Model model, RedirectAttributes redirectAttributes) {
        // ファイルが未指定の場合はエラーとする
        if (uploadFile == null || StringUtils.isEmptyOrWhitespace(
                uploadFile.getOriginalFilename())) {
            redirectAttributes.addFlashAttribute("errMessage"
                 , "ファイルを指定してください。");
            return "redirect:/";
        }

        // Azure FunctionsのfileUpload関数を呼び出すためのヘッダー情報を設定する
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Azure FunctionsのfileUpload関数を呼び出すための引数を設定する
        FileUploadParam fileUploadParam = new FileUploadParam();
        try {
            fileUploadParam.setFileName(uploadFile.getOriginalFilename());
            fileUploadParam.setFileData(ArrayUtils.toObject(
                IOUtils.toByteArray(uploadFile.getInputStream())));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        HttpEntity<FileUploadParam> entity 
            = new HttpEntity<>(fileUploadParam, headers);

        // Azure FunctionsのfileUpload関数を呼び出す
        ResponseEntity<FileUploadResult> response 
            = restTemplate.exchange(demoAzureFuncBase + "fileUpload",
                HttpMethod.POST, entity, FileUploadResult.class);
        FileUploadResult fileUploadResult = response.getBody();

        // メイン画面へ遷移
        model.addAttribute("message", fileUploadResult.getMessage());
        return "redirect:/";
    }

    /**
     * ファイル名をUTF-8でエンコードする.
     * 
     * @param filePath ファイル名
     * @return エンコード後のファイル名
     */
    private String getFileNameEncoded(String fileName) {
        String fileNameAft = "";
        if (!StringUtils.isEmptyOrWhitespace(fileName)) {
            try {
                // ファイル名をUTF-8でエンコードして返却
                fileNameAft = URLEncoder.encode(fileName, "UTF-8");
            } catch (Exception e) {
                System.err.println(e);
                return "";
            }
        }
        return fileNameAft;
    }

}

HTMLファイルの内容は以下の通りで、画面下にappServiceMessage・azureFunctionsMessageの値を表示する処理を追加している。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>メイン画面</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data" th:action="@{/upload}">
        <th:block th:if="!${#strings.isEmpty(message)}">
            <span th:text="${message}">メッセージ</span><br/><br/>
        </th:block>
        <th:block th:if="!${#strings.isEmpty(errMessage)}">
            <font color="#FF0000"><span th:text="${errMessage}">
                エラーメッセージ</span></font><br/><br/>
        </th:block>
        ファイル : <input type="file" name="upload_file" /><br/><br/>
        <input type="submit" value="アップロード" />
    </form>
    <br/><br/>
    アップロードファイルリスト:<br/>
    <table border="1" cellpadding="5">
        <tr>
            <th>ID</th>
            <th>ファイル名</th>
            <th>ファイルパス</th>
            <th>ファイルダウンロード</th>
        </tr>
        <tr th:each="obj : ${fileDataList}">
            <td th:text="${obj.id}"></td>
            <td th:text="${obj.fileName}"></td>
            <td th:text="${obj.filePath}"></td>
            <td>
                <!-- ダウンロードボタンを表示 -->
                <form action="#" method="get"
                      th:action="@{/download(id=${'__${obj.id}__'})}"
                      th:method="download" >
                    <input type="hidden" name="_method" value="download" />
                    <input type="submit" value="ダウンロード" />
                </form>
            </td>
        </tr>
    </table>
    <br/><br/><hr/><br/>
    <th:block th:if="!${#strings.isEmpty(appServiceMessage)}">
        App Serviceのメッセージ: <span th:text="${appServiceMessage}">
            App Serviceのメッセージ</span><br/><br/>
    </th:block>
    <th:block th:if="!${#strings.isEmpty(azureFunctionsMessage)}">
        Azure Functionsのメッセージ: <span th:text="${azureFunctionsMessage}">
            Azure Functionsのメッセージ</span>
    </th:block>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-app-service-functions-devtools/demoAzureApp



Code VillageはJavaScriptを中心としたサポート体制が充実したプログラミングスクールだったJavaScriptや、JavaScriptのフレームワーク「React」「Vue」を中心にオンラインで学習できるプログラミングスクール...

作成したサンプルプログラム(共通クラス)の内容

作成したサンプルプログラム(共通クラス)の構成は以下の通り。
サンプルプログラムの構成(共通クラス)
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。

GetFileListResultクラスの内容は以下の通りで、azureFunctionsMessageを追加すると共に、Serializableインタフェースを実装している。なお、Serializableインタフェースを実装しているのは、DevToolsによる再起動により発生するApp Service側でエラーを防ぐためである。

package com.example.model;

import java.io.Serializable;
import java.util.ArrayList;
import lombok.Data;

@Data
public class GetFileListResult implements Serializable {
    
    /** シリアライズバージョンを設定 */
    private static final long serialVersionUID = -2696400144403338648L;
    
    /** ファイルデータリスト */
    private ArrayList<FileData> fileDataList;
    
    /** Azure Functionsのメッセージ */
    private String azureFunctionsMessage;
    
}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-app-service-functions-devtools/demoAzureCommon



freelance hubを利用して10万件を超える案件情報からJava Spring案件を検索してみたfreelance hubは、レバテックフリーランスやフリエン(furien)を始めとした多くのフリーランスエージェントの案件をまとめて...

作成したサンプルプログラム(Azure Functions側)の内容

作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
サンプルプログラムの構成(AzureFunctions側)
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。

pom.xml(追加分)の内容は以下の通りで、DevToolsの設定を追加している。

<!-- DevToolsの設定 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
</dependency>

GetFileListServiceクラスの内容は以下の通りで、azureFunctionsMessageの値を設定する処理を追加している。

package com.example.service;

import java.io.InputStream;
import java.util.ArrayList;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.example.DemoFuncConst;
import com.example.model.FileData;
import com.example.model.GetFileListParam;
import com.example.model.GetFileListResult;
import com.microsoft.azure.storage.CloudStorageAccount;
import com.microsoft.azure.storage.blob.CloudBlobClient;
import com.microsoft.azure.storage.blob.CloudBlobContainer;
import com.microsoft.azure.storage.blob.CloudBlockBlob;
import com.microsoft.azure.storage.blob.ListBlobItem;

@Service
public class GetFileListService {

    /** Azure Storageのアカウント名 */
    @Value("${azure.storage.accountName}")
    private String storageAccountName;

    /** Azure Storageへのアクセスキー */
    @Value("${azure.storage.accessKey}")
    private String storageAccessKey;

    /** Azure StorageのBlobコンテナー名 */
    @Value("${azure.storage.containerName}")
    private String storageContainerName;

    /**
     * ファイルリスト取得処理を行うサービス.
     * @param getFileListParam ファイル取得用Param
     * @return ファイルリスト取得サービスクラスの呼出結果
     */
    public GetFileListResult getFileList(GetFileListParam getFileListParam) {
        GetFileListResult result = new GetFileListResult();

        // ファイルアップロード処理
        try {
            // Blobストレージへの接続文字列
            String storageConnectionString = "DefaultEndpointsProtocol=https;" 
                    + "AccountName=" + storageAccountName + ";" 
                    + "AccountKey=" + storageAccessKey + ";";

            // ストレージアカウントオブジェクトを取得
            CloudStorageAccount storageAccount 
                = CloudStorageAccount.parse(storageConnectionString);

            // Blobクライアントオブジェクトを取得
            CloudBlobClient blobClient = storageAccount.createCloudBlobClient();

            // Blob内のコンテナーを取得
            CloudBlobContainer container 
                = blobClient.getContainerReference(storageContainerName);

            // Blob情報を取得し、戻り値に設定する
            ArrayList<FileData> fileDataList = new ArrayList<>();
            int index = 1;
            
            for (ListBlobItem blobItem : container.listBlobs(
                    DemoFuncConst.BLOB_FILE_PATH, true)) {
                // ファイル名・ファイルパスを取得
                String filePath = blobItem.getUri().toString();
                String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);

                // Blobデータを読み込み
                CloudBlockBlob blob = container.getBlockBlobReference(
                    DemoFuncConst.BLOB_FILE_PATH + fileName);
                InputStream input = blob.openInputStream();

                // ファイルデータを設定
                FileData fileData = new FileData();
                fileData.setId(index);
                fileData.setFileName(fileName);
                fileData.setFilePath(filePath);
                fileData.setFileData(ArrayUtils.toObject(IOUtils.toByteArray(input)));
                fileDataList.add(fileData);
                index++;
            }
            result.setFileDataList(fileDataList);
            
            // DevToolsの実行確認を行うための、Azure Functionsからのメッセージを設定
            result.setAzureFunctionsMessage("Azure Functionsのメッセージ1");

        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        return result;
    }
}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-app-service-functions-devtools/demoAzureFunc



「FlexClip」はテンプレートとして利用できる動画・画像・音楽などが充実した動画編集ツールだったテンプレートとして利用できるテキスト・動画・画像・音楽など(いずれも著作権フリー)が充実している動画編集ツールの一つに、「FlexCli...

作成したサンプルプログラムの実行結果

作成したサンプルプログラムをローカル環境で実行した結果は、以下の通り。

1) Azure Functionsのメインクラス「DemoAzureFunction.java」を選択し右クリックし、「実行」メニューから「Spring Boot アプリケーション」を選択する。
サンプルプログラムの実行結果_1

2) 以下のように、コンソール上でAzure Functionsが、ポート番号:8085で起動したことが確認できる。また、コンソールログで[ main]と出力されていた箇所が、[ restartedMain]と出力されていることが確認できる。
サンプルプログラムの実行結果_2

3) Azure App Serviceのメインクラス「DemoAzureAppApplication.java」を選択し右クリックし、「実行」メニューから「Spring Boot アプリケーション」を選択する。
サンプルプログラムの実行結果_3

4) 以下のように、コンソール上でAzure App Serviceが、ポート番号:8084で起動したことが確認できる。また、コンソールログで[ main]と出力されていた箇所が、[ restartedMain]と出力されていることが確認できる。
サンプルプログラムの実行結果_4

5)「http:// (ホスト名):(App Serviceのポート番号)」とアクセスすると、以下の初期表示画面が表示されることが確認できる。
サンプルプログラムの実行結果_5

6) コントローラクラスの、以下の赤枠の部分を「Azure App Serviceのメッセージ1」から「Azure App Serviceのメッセージ2」に修正する。
サンプルプログラムの実行結果_6

7) コントローラクラスを変更後のAzure App Serviceのコンソールログの内容は以下の通りで、DevToolsにより、Azure App ServiceのSpring Bootアプリケーションが再起動されたことが確認できる。
サンプルプログラムの実行結果_7

8)「http:// (ホスト名):(App Serviceのポート番号)」と再度アクセスすると、以下の赤枠のように、App Serviceのメッセージが「Azure App Serviceのメッセージ2」に変更されていることが確認できる。
サンプルプログラムの実行結果_8

9) サービスクラスの、以下の赤枠の部分を「Azure Functionsのメッセージ1」から「Azure Functionsのメッセージ2」に修正する。
サンプルプログラムの実行結果_9

10) サービスクラスを変更後のAzure Functionsのコンソールログの内容は以下の通りで、DevToolsにより、Azure FunctionsのSpring Bootアプリケーションが再起動されたことが確認できる。
サンプルプログラムの実行結果_10

11)「http:// (ホスト名):(App Serviceのポート番号)」と再度アクセスすると、以下の赤枠のように、Azure Functionsのメッセージが「Azure Functionsのメッセージ2」に変更されていることが確認できる。
サンプルプログラムの実行結果_11

要点まとめ

  • Spring Bootアプリケーションの開発で、DevToolsというツールを利用すると、Spring Bootアプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。