Azure基本

Azure App ServiceとAzure Functionsの共通クラスを別プロジェクトに取り出してみた

これまでこのブログで取り上げてきたAzureのサンプルプログラムの中には、Azure App ServiceとAzure Functionsで共用できるクラスがいくつかある。今回は、Azure App ServiceとAzure Functionsで共通のクラスを別プロジェクトに取り出してみたので、その手順を共有する。

前提条件

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

Azure FunctionsでAzure Blob Storageのコンテナからファイルをダウンロードしてみた以前、Azure FunctionsでAzure Blob Storageのコンテナー内にファイルを格納する処理を実施してみたが、今回は...

なお、今回共通クラスとして取り出すのは、Azure App Serviceの場合、以下の赤枠のクラスとなる。
前提条件

やってみたこと

  1. 共通クラス格納用プロジェクトの作成
  2. 共通クラス格納用プロジェクトへのソースコード配置
  3. 共通クラス格納用プロジェクトをローカルMavenリポジトリへインストール
  4. Azure App Service側プロジェクトの編集
  5. Azure Functions側プロジェクトの編集
  6. サンプルプログラムの実行

共通クラス格納用プロジェクトの作成

共通クラス格納用プロジェクトは、lombokを利用できるよう、Mavenプロジェクトとして作成する。その手順は、以下の通り。

1) STSを起動し、パッケージ・エクスプローラーで右クリックし、「新規」メニューから「プロジェクト」を選択する。
共通クラス格納用プロジェクトの作成_1

2)「Mavenプロジェクト」を選択し、「次へ」ボタンを押下する。
共通クラス格納用プロジェクトの作成_2

3)「シンプルなプロジェクトの作成」をチェックし、「次へ」ボタンを押下する。
共通クラス格納用プロジェクトの作成_3

4) グループId、アーティファクトIdを指定し、「完了」ボタンを押下する。ここでは「demoAzureCommon」という名前の共通クラス格納用プロジェクトを作成するため、アーティファクトIdに「demoAzureCommon」を指定している。
共通クラス格納用プロジェクトの作成_4

5) 以下のように、「demoAzureCommon」という名前のMavenプロジェクトが作成されたことが確認できる。
共通クラス格納用プロジェクトの作成_5

6) pom.xmlを、lombokを追加するため、以下のように編集する。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>demoAzureCommon</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <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>
  </properties>
  <dependencies>
    <!-- lombokを利用するための設定 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>

7) pom.xmlを編集した後で、Mavenプロジェクトを更新すると、以下のように、lombokが追加されていることが確認できる。
共通クラス格納用プロジェクトの作成_7



共通クラス格納用プロジェクトへのソースコード配置

先ほど作成したMavenプロジェクトに、Azure App ServiceとAzure Functionsで共用するクラスを格納する。その手順は、以下の通り。

1) パッケージを追加するため、共通クラス格納用プロジェクトの「src/main/java」を選択し右クリックし、「新規」メニューから「パッケージ」を選択する。
共通クラス格納用プロジェクトのソース追加_1

2) パッケージ名「com.example.model」を指定し、「完了」ボタンを押下する。
共通クラス格納用プロジェクトのソース追加_2

3) 以下のように、指定したパッケージが作成されたことが確認できる。
共通クラス格納用プロジェクトのソース追加_3

4) 以下の赤枠のように、App ServiceとAzure Functions共通で利用するクラスを追加する。
共通クラス格納用プロジェクトのソース追加_4



共通クラス格納用プロジェクトをローカルMavenリポジトリへインストール

共通クラス格納用プロジェクトを他のプロジェクトから参照できるようにするために、ローカルMavenリポジトリへインストールする。その手順は、以下の通り。

1) 共通クラス格納用プロジェクトを選択し右クリックし、「実行」メニューから「Maven clean」を選択する。
ローカルMavenリポジトリへインストール_1

2) コンソールに以下の実行結果が表示され、「BUILD SUCCESS」と表示されることを確認する。
ローカルMavenリポジトリへインストール_2

3) 共通クラス格納用プロジェクトを選択し右クリックし、「実行」メニューから「Maven install」を選択する。
ローカルMavenリポジトリへインストール_3

4) コンソールに以下の実行結果が表示され、「BUILD SUCCESS」と表示されることを確認する。
ローカルMavenリポジトリへインストール_4

5) 「Maven install」を実行した後は、ローカルMavenリポジトリ(.m2フォルダ以下)に、以下の赤枠の「(共通クラス格納用プロジェクト名)-(バージョン).jar」というJarファイルが作成されていることが確認できる。
ローカルMavenリポジトリへインストール_5



Azure App Service側プロジェクトの編集

Azure App Service側プロジェクトから、先ほど作成した共通クラス格納用プロジェクトを参照できるようにする。その手順は、以下の通り。

1) demoAzureApp内のpom.xmlに、先ほど作成した共通クラス格納用プロジェクトの定義を追加し、Mavenプロジェクトを更新する。

<!-- 共通クラス格納用プロジェクトの設定 -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>demoAzureCommon</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

2) 共通クラス格納用プロジェクトに含まれていたクラスを削除する。
AppServiceの編集_2

3) 以下のように、ソース編集時にフォルダパスを変えた「com.example.model」フォルダ下のクラスが表示されるようになることが確認できる。
AppServiceの編集_3

4) コンパイルエラーを解消し、ソース変更後のフォルダ構成は、以下の通り。
AppServiceの編集_4

また、編集したコントローラクラスの内容は、以下の通り。

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.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.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
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.FileUploadResult;
import com.example.model.GetFileListResult;
import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
public class DemoController {

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

    /** ObjectMapperオブジェクト */
    @Autowired
    private ObjectMapper objectMapper;

    /** 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関数を呼び出す
        ResponseEntity<String> response = restTemplate.exchange(
            demoAzureFuncBase + "getFileList", HttpMethod.POST,
            new HttpEntity<>(headers), String.class);

        // 取得したファイルリストをModelとセッションに設定する
        GetFileListResult getFileListResult = null;
        try {
            getFileListResult = objectMapper.readValue(
                response.getBody(), GetFileListResult.class);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        model.addAttribute("fileDataList", getFileListResult.getFileDataList());
        session.setAttribute("fileDataList", getFileListResult.getFileDataList());

        model.addAttribute("message"
            , "アップロードするファイルを指定し、アップロードボタンを押下してください。");
        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(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関数を呼び出すための引数を設定する
        MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
        try {
            map.add("fileName", uploadFile.getOriginalFilename());
            map.add("fileData", IOUtils.toByteArray(uploadFile.getInputStream()));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        HttpEntity<MultiValueMap<String, Object>> entity 
            = new HttpEntity<>(map, headers);

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

        // ファイルアップロード処理完了のメッセージを設定する
        FileUploadResult fileUploadResult = null;
        try {
            fileUploadResult = objectMapper.readValue(
                response.getBody(), FileUploadResult.class);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        // メイン画面へ遷移
        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;
    }

}



Azure Functions側プロジェクトの編集

Azure Functions側プロジェクトから、先ほど作成した共通クラス格納用プロジェクトを参照できるようにする。その手順は以下の通りで、Azure App Serviceの場合と同じように実行できる。

1) Azure Functions側プロジェクトは、共通クラスとパッケージ名・フォルダ名が同一のため、あらかじめ共通クラスと重複するファイルを選択し削除する。
AzureFunctionsの編集_1

2) コンパイルエラーが出るが、そのまま、demoAzureFunc内のpom.xmlに、先ほど作成した共通クラス格納用プロジェクトの定義を追加し、Mavenプロジェクトを更新する。

<!-- 共通クラス格納用プロジェクトの設定 -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>demoAzureCommon</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

3) コンパイルエラーが消え、以下のようなフォルダ構成になることが確認できる。
AzureFunctionsの編集_3

なお、修正したソースコード全体の内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-common-project/

<2021年4月13日 追記>
spring-cloud-function-dependenciesのバージョンは、2021年3月16日にリリースしたバージョン3.1.2を利用すると、1つのAzure Functions内に複数のファンクションを含む場合の不具合が解消できている。


その場合、Handlerクラスの継承するクラスを「AzureSpringBootRequestHandler」クラスから「FunctionInvoker」クラスに変更する。


spring-cloud-function-dependenciesの3.1.2を利用した実装サンプルは、以下の記事を参照のこと。

spring-cloud-function-dependenciesのバージョンを最新(3.1.2)にしてみたこれまでこのブログで取り上げてきたAzure Functionsのサンプルプログラムでは、spring-cloud-function-d...

サンプルプログラムの実行

サンプルプログラムの実行結果は、以下の各記事の「作成したサンプルプログラムの実行結果」と同じになる。

Azure Blob Storageのコンテナー内にファイルを格納するプログラムを作成してみた以前、JavaでSpring Bootフレームワークを利用してファイルをアップロードするプログラムを作成したことがあったが、ファイルのア...
Azure FunctionsでAzure Blob Storageのコンテナからファイルをダウンロードしてみた以前、Azure FunctionsでAzure Blob Storageのコンテナー内にファイルを格納する処理を実施してみたが、今回は...

要点まとめ

  • Azure App ServiceとAzure Functionsで共通のクラスは、別のMavenプロジェクトとして取り出し、Azure App Service側プロジェクト、Azure Functions側プロジェクトそれぞれから参照可能にできる。