以前、Azure FunctionsでAzure Blob Storageのコンテナー内にファイルを格納する処理を実施してみたが、今回はそのサンプルプログラムに、Azure Blob Storageのコンテナからファイルをダウンロードする処理を追加してみたので、共有する。
前提条件
下記記事の実装が完了していること。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
コントローラクラスの内容は以下の通りで、indexメソッドにAzure Blob Storageのコンテナからファイルをダウンロードする処理を、downloadメソッドにダウンロード処理を、それぞれ追加している。なお、Azure Blob Storageのコンテナからファイルをダウンロードする処理については、後述の「作成したサンプルプログラム(Azure Functions側)」にて記載する。
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.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 Blob Storageのコンテナからファイルをダウンロードする処理の戻り値と、ファイルデータの内容は、以下のクラスで定義している。
package com.example.demo; import java.util.ArrayList; import lombok.Data; @Data public class GetFileListResult { /** ファイルデータリスト */ private ArrayList<FileData> fileDataList; }
package com.example.demo; import lombok.Data; @Data public class FileData { /** ID */ private int id; /** ファイル名 */ private String fileName; /** ファイルパス */ private String filePath; /** ファイルデータ */ private byte[] fileData; }
また、画面のHTMLの内容は以下の通りで、Azure Blob Storageのコンテナからダウンロードしたファイルを一覧で表示している。
<!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> </body> </html>
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-blob-storage-download/demoAzureApp
作成したサンプルプログラム(Azure Functions側)の内容
作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
<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を利用した実装サンプルは、以下の記事を参照のこと。
pom.xmlには、以下のcommons-ioの設定を追加している。
<!-- commons-ioの設定 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency>
ファイルダウンロードを行うサービスクラスの内容は以下の通りで、Azure Storageのコンテナから取得したファイルデータを設定している。
package com.example.service; import java.io.InputStream; import java.util.ArrayList; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; 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()) { // ファイル名・ファイルパスを取得 String filePath = blobItem.getUri().toString(); String fileName = filePath.substring(filePath.lastIndexOf("/") + 1); // Blobデータを読み込み CloudBlockBlob blob = container.getBlockBlobReference(fileName); InputStream input = blob.openInputStream(); // ファイルデータを設定 FileData fileData = new FileData(); fileData.setId(index); fileData.setFileName(fileName); fileData.setFilePath(filePath); fileData.setFileData(IOUtils.toByteArray(input)); fileDataList.add(fileData); index++; } result.setFileDataList(fileDataList); } catch (Exception ex) { throw new RuntimeException(ex); } return result; } }
また、サービスクラスの入出力項目の内容は、以下の通り。「GetFileListResult.java」「FileData.java」の内容は、格納するパッケージ以外はApp Service側と同じ内容になっている。
package com.example.model; import lombok.Data; @Data public class FileData { /** ID */ private int id; /** ファイル名 */ private String fileName; /** ファイルパス */ private String filePath; /** ファイルデータ */ private byte[] fileData; }
package com.example.model; import lombok.Data; @Data public class GetFileListParam { }
package com.example.model; import java.util.ArrayList; import lombok.Data; @Data public class GetFileListResult { /** ファイルデータリスト */ private ArrayList<FileData> fileDataList; }
さらに、メインクラスとハンドラークラスの内容は、以下の通り。
package com.example; import java.util.function.Function; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import com.example.model.FileUploadParam; import com.example.model.FileUploadResult; import com.example.model.GetFileListParam; import com.example.model.GetFileListResult; import com.example.service.FileUploadService; import com.example.service.GetFileListService; @SpringBootApplication public class DemoAzureFunction { /** ファイルアップロードサービスクラスのオブジェクト */ @Autowired private FileUploadService fileUploadService; /** ファイルリスト取得サービスクラスのオブジェクト */ @Autowired private GetFileListService getFileListService; public static void main(String[] args) throws Exception { SpringApplication.run(DemoAzureFunction.class, args); } /** * ファイルアップロードを行い結果を返却する関数 * @return ファイルアップロードサービスクラスの呼出結果 */ @Bean public Function<FileUploadParam, FileUploadResult> fileUpload() { return fileUploadParam -> fileUploadService.fileUpload(fileUploadParam); } /** * ファイルリスト取得を行い結果を返却する関数 * @return ファイルリスト取得サービスクラスの呼出結果 */ @Bean public Function<GetFileListParam, GetFileListResult> getFileList(){ return getFileListParam -> getFileListService.getFileList(getFileListParam); } }
package com.example; import java.util.Optional; import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler; import com.example.model.GetFileListParam; import com.example.model.GetFileListResult; import com.microsoft.azure.functions.ExecutionContext; import com.microsoft.azure.functions.HttpMethod; import com.microsoft.azure.functions.HttpRequestMessage; import com.microsoft.azure.functions.HttpResponseMessage; import com.microsoft.azure.functions.HttpStatus; import com.microsoft.azure.functions.annotation.AuthorizationLevel; import com.microsoft.azure.functions.annotation.FunctionName; import com.microsoft.azure.functions.annotation.HttpTrigger; public class GetFileListHandler extends AzureSpringBootRequestHandler<GetFileListParam, GetFileListResult> { /** * HTTP要求に応じて、DemoAzureFunctionクラスのfileUploadメソッドを呼び出し、 * その戻り値をボディに設定したレスポンスを返す. * @param request リクエストオブジェクト * @param context コンテキストオブジェクト * @return レスポンスオブジェクト */ @FunctionName("getFileList") public HttpResponseMessage execute( @HttpTrigger(name = "request", methods = HttpMethod.POST , authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request , ExecutionContext context) { // handleRequestメソッド内でDemoAzureFunctionクラスのfileUploadメソッドを // 呼び出し、その戻り値をボディに設定したレスポンスを、JSON形式で返す return request.createResponseBuilder(HttpStatus.OK) .body(handleRequest(new GetFileListParam(), context)) .header("Content-Type", "text/json").build(); } }
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-blob-storage-download/demoAzureFunc
作成したサンプルプログラムの実行結果
サンプルプログラムをAzure App Service、Azure Functionsにデプロイして実行した結果は、以下の通り。
1) サンプルプログラム実行前のBlobコンテナーの内容は以下の通りで、3ファイルがアップロードされていることが確認できる。
2) Azure App ServiceのURL「https://azureappdemoservice.azurewebsites.net/」とアクセスすると、Blobコンテナー内のファイルが「アップロードファイルリスト」に表示されることが確認できる。
なお、Azure App ServiceのURLは、下記Azure App ServiceのURLから確認できる。
3) ファイルアップロードリストの「テスト.pdf」の「ダウンロード」ボタンを押下する。
4) 画面左下にダウンロードしたファイルが表示され、ダウンロードフォルダ下に「てすと.pdf」が出力されることが確認できる。
また、ダウンロードした「てすと.pdf」の中身は以下の通り。このファイルの中身は、アップロードしたファイルと同じものになっている。
要点まとめ
- Azure Blob Storageのコンテナからファイルをダウンロードする処理も、Azure FunctionsやAzure App Service内で実装できる。