これまでこのブログで取り上げてきたAzureのサンプルプログラムの中には、Azure App ServiceとAzure Functionsで共用できるクラスがいくつかある。今回は、Azure App ServiceとAzure Functionsで共通のクラスを別プロジェクトに取り出してみたので、その手順を共有する。
前提条件
下記記事の実装が完了していること。
なお、今回共通クラスとして取り出すのは、Azure App Serviceの場合、以下の赤枠のクラスとなる。
やってみたこと
- 共通クラス格納用プロジェクトの作成
- 共通クラス格納用プロジェクトへのソースコード配置
- 共通クラス格納用プロジェクトをローカルMavenリポジトリへインストール
- Azure App Service側プロジェクトの編集
- Azure Functions側プロジェクトの編集
- サンプルプログラムの実行
共通クラス格納用プロジェクトの作成
共通クラス格納用プロジェクトは、lombokを利用できるよう、Mavenプロジェクトとして作成する。その手順は、以下の通り。
1) STSを起動し、パッケージ・エクスプローラーで右クリックし、「新規」メニューから「プロジェクト」を選択する。
2)「Mavenプロジェクト」を選択し、「次へ」ボタンを押下する。
3)「シンプルなプロジェクトの作成」をチェックし、「次へ」ボタンを押下する。
4) グループId、アーティファクトIdを指定し、「完了」ボタンを押下する。ここでは「demoAzureCommon」という名前の共通クラス格納用プロジェクトを作成するため、アーティファクトIdに「demoAzureCommon」を指定している。
5) 以下のように、「demoAzureCommon」という名前のMavenプロジェクトが作成されたことが確認できる。
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が追加されていることが確認できる。
共通クラス格納用プロジェクトへのソースコード配置
先ほど作成したMavenプロジェクトに、Azure App ServiceとAzure Functionsで共用するクラスを格納する。その手順は、以下の通り。
1) パッケージを追加するため、共通クラス格納用プロジェクトの「src/main/java」を選択し右クリックし、「新規」メニューから「パッケージ」を選択する。
2) パッケージ名「com.example.model」を指定し、「完了」ボタンを押下する。
3) 以下のように、指定したパッケージが作成されたことが確認できる。
4) 以下の赤枠のように、App ServiceとAzure Functions共通で利用するクラスを追加する。
共通クラス格納用プロジェクトをローカルMavenリポジトリへインストール
共通クラス格納用プロジェクトを他のプロジェクトから参照できるようにするために、ローカルMavenリポジトリへインストールする。その手順は、以下の通り。
1) 共通クラス格納用プロジェクトを選択し右クリックし、「実行」メニューから「Maven clean」を選択する。
2) コンソールに以下の実行結果が表示され、「BUILD SUCCESS」と表示されることを確認する。
3) 共通クラス格納用プロジェクトを選択し右クリックし、「実行」メニューから「Maven install」を選択する。
4) コンソールに以下の実行結果が表示され、「BUILD SUCCESS」と表示されることを確認する。
5) 「Maven install」を実行した後は、ローカルMavenリポジトリ(.m2フォルダ以下)に、以下の赤枠の「(共通クラス格納用プロジェクト名)-(バージョン).jar」というJarファイルが作成されていることが確認できる。
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) 共通クラス格納用プロジェクトに含まれていたクラスを削除する。
3) 以下のように、ソース編集時にフォルダパスを変えた「com.example.model」フォルダ下のクラスが表示されるようになることが確認できる。
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側プロジェクトは、共通クラスとパッケージ名・フォルダ名が同一のため、あらかじめ共通クラスと重複するファイルを選択し削除する。
2) コンパイルエラーが出るが、そのまま、demoAzureFunc内のpom.xmlに、先ほど作成した共通クラス格納用プロジェクトの定義を追加し、Mavenプロジェクトを更新する。
<!-- 共通クラス格納用プロジェクトの設定 --> <dependency> <groupId>com.example</groupId> <artifactId>demoAzureCommon</artifactId> <version>0.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency>
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を利用した実装サンプルは、以下の記事を参照のこと。
サンプルプログラムの実行
サンプルプログラムの実行結果は、以下の各記事の「作成したサンプルプログラムの実行結果」と同じになる。
要点まとめ
- Azure App ServiceとAzure Functionsで共通のクラスは、別のMavenプロジェクトとして取り出し、Azure App Service側プロジェクト、Azure Functions側プロジェクトそれぞれから参照可能にできる。