Spring Bootアプリケーションの開発を効率化するツールとして、DevToolsというツールがある。
DevToolsを利用すると、ソースコードを修正したタイミングで、Spring Bootアプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。
今回は、Azure App ServiceとAzure Functionsの両方でDevToolsを利用してみたので、その手順を共有する。
前提条件
下記記事の実装が完了していること。
また、以下のように、STSの「プロジェクト」メニューで「自動的にビルド」にチェックが入っていること。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
pom.xml(追加分)の内容は以下の通りで、DevToolsの設定を追加している。
1 2 3 4 5 | <!-- DevToolsの設定 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> |
コントローラクラスの内容は以下の通りで、初期表示を行うindexメソッドで、appServiceMessage・azureFunctionsMessageの値を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 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の値を表示する処理を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <!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側でエラーを防ぐためである。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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の設定を追加している。
1 2 3 4 5 | <!-- DevToolsの設定 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> |
GetFileListServiceクラスの内容は以下の通りで、azureFunctionsMessageの値を設定する処理を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | 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アプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。