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アプリケーションを再起動しなくてもアプリケーションを再起動してくれるため、ソースコード修正後の確認を素早く行うことができる。





