以前、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内で実装できる。





