Spring Bootアプリケーションの開発を効率化するツールとして、DevToolsというツールがある。
DevToolsを利用すると、ソースコードを修正したタイミングで、Spring Bootアプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。
今回は、Azure App ServiceとAzure Functionsの両方でDevToolsを利用してみたので、その手順を共有する。
前提条件
下記記事の実装が完了していること。
また、以下のように、STSの「プロジェクト」メニューで「自動的にビルド」にチェックが入っていること。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
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
作成したサンプルプログラム(共通クラス)の内容
作成したサンプルプログラム(共通クラス)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
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
作成したサンプルプログラム(Azure Functions側)の内容
作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
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
作成したサンプルプログラムの実行結果
作成したサンプルプログラムをローカル環境で実行した結果は、以下の通り。
1) Azure Functionsのメインクラス「DemoAzureFunction.java」を選択し右クリックし、「実行」メニューから「Spring Boot アプリケーション」を選択する。
2) 以下のように、コンソール上でAzure Functionsが、ポート番号:8085で起動したことが確認できる。また、コンソールログで[ main]と出力されていた箇所が、[ restartedMain]と出力されていることが確認できる。
3) Azure App Serviceのメインクラス「DemoAzureAppApplication.java」を選択し右クリックし、「実行」メニューから「Spring Boot アプリケーション」を選択する。
4) 以下のように、コンソール上でAzure App Serviceが、ポート番号:8084で起動したことが確認できる。また、コンソールログで[ main]と出力されていた箇所が、[ restartedMain]と出力されていることが確認できる。
5)「http:// (ホスト名):(App Serviceのポート番号)」とアクセスすると、以下の初期表示画面が表示されることが確認できる。
6) コントローラクラスの、以下の赤枠の部分を「Azure App Serviceのメッセージ1」から「Azure App Serviceのメッセージ2」に修正する。
7) コントローラクラスを変更後のAzure App Serviceのコンソールログの内容は以下の通りで、DevToolsにより、Azure App ServiceのSpring Bootアプリケーションが再起動されたことが確認できる。
8)「http:// (ホスト名):(App Serviceのポート番号)」と再度アクセスすると、以下の赤枠のように、App Serviceのメッセージが「Azure App Serviceのメッセージ2」に変更されていることが確認できる。
9) サービスクラスの、以下の赤枠の部分を「Azure Functionsのメッセージ1」から「Azure Functionsのメッセージ2」に修正する。
10) サービスクラスを変更後のAzure Functionsのコンソールログの内容は以下の通りで、DevToolsにより、Azure FunctionsのSpring Bootアプリケーションが再起動されたことが確認できる。
11)「http:// (ホスト名):(App Serviceのポート番号)」と再度アクセスすると、以下の赤枠のように、Azure Functionsのメッセージが「Azure Functionsのメッセージ2」に変更されていることが確認できる。
要点まとめ
- Spring Bootアプリケーションの開発で、DevToolsというツールを利用すると、Spring Bootアプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。