CLOVER🍀

That was when it all began.

Ubuntu Linux 20.04 LTSに、.NET 5.0/.NET Core 3.1をインストールしてみる

これは、なにをしたくて書いたもの?

Ubuntu Linuxに、.NET Coreをインストールしてみようかなと。

.NET Core

.NET自体の説明は、こちら。

.NET の概要 | Microsoft Docs

そして、.NET Coreについて。

2014 年に、Microsoft によって、.NET Framework のクロスプラットフォームのオープンソースの後継版の作成が開始されました。 この .NET の新しい実装には、バージョン 3.1 に達するまで .NET Core という名前が付けられていました。 .NET Core 3.1 以降の次のバージョンは .NET 5.0 であり、現在プレビュー段階です。

.NET の概要 / .NET Core と .NET 5

.NET Coreというのは、バージョン3.1までの名前ですが、クロスプラットフォームのオープンソース実装のことを指すようです。
.NET 5以降は、"Core"とは呼ばなくなるようですね。

で、.NET 5はプレビュー段階と上記のページに書いていますが、実際には2020年11月にリリースされているようなので…。

Announcing .NET 5.0 | .NET Blog

[速報]マイクロソフト「.NET 5」正式リリース。1つのフレームワークでWindows/Mac/Linuxのデスクトップ、サーバアプリ、Webアプリなどが開発可能に - Publickey

Microsoftが .NET 5 をリリース

今回は、.NET 5.0と.NET Core 3.1をインストールしてみたいと思います。

環境

今回の環境は、こちらです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Ubuntu Linux 20.04 LTSです。

Ubuntu Linuxに.NET 5.0をインストールする

Linuxへの.NETのインストールに関するドキュメントは、こちら。

Linux ディストリビューションに .NET をインストールする | Microsoft Docs

Ubuntu Linuxと.NETのサポートマトリクス。

Linux に .NET をインストールする / Ubuntu

インストール方法はLinuxディストリビューションごとにドキュメントがあり、Ubuntu Linuxについてはこちらです。

Ubuntu に .NET をインストールする - .NET | Microsoft Docs

サポートバージョンについても再度記載があります。

では、まずは.NET 5.0をインストールしてみます。

パッケージ署名キーの追加。

$ curl -O -L -s https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb

リポジトリ情報の更新と、必要なパッケージのインストール。

$ sudo apt update && sudo apt install -y apt-transport-https

インストール可能なパッケージに、.NETが含まれるようになっています。こちらはSDKです。

$ apt search dotnet-sdk
ソート中... 完了
全文検索... 完了  
dotnet-sdk-2.1/groovy 2.1.814-1 amd64
  Microsoft .NET Core SDK 2.1.814

dotnet-sdk-3.1/groovy 3.1.407-1 amd64
  Microsoft .NET Core SDK 3.1.407

dotnet-sdk-5.0/groovy 5.0.201-1 amd64
  Microsoft .NET SDK 5.0.201

ランタイムのみ。

$ apt search aspnetcore-runtime.*
ソート中... 完了
全文検索... 完了  
aspnetcore-runtime-2.1/focal 2.1.26-1 amd64
  Microsoft ASP.NET Core 2.1.26 Shared Framework

aspnetcore-runtime-3.1/focal 3.1.13-1 amd64
  Shared Framework for hosting of Microsoft ASP.NET Core applications. It is open source, cross-platform and is supported by Microsoft. We hope you enjoy using it! If you do, please consider joining the active community of developers that are contributing to the project on GitHub (https://github.com/aspnet/AspNetCore). We happily accept issues and PRs.

aspnetcore-runtime-5.0/focal 5.0.4-1 amd64
  Shared Framework for hosting of Microsoft ASP.NET Core applications. It is open source, cross-platform and is supported by Microsoft. We hope you enjoy using it! If you do, please consider joining the active community of developers that are contributing to the project on GitHub (https://github.com/dotnet/aspnetcore). We happily accept issues and PRs.

では、.NET 5.0のSDKをインストール。
※ランタイムのみのインストールも可能なようです

$ sudo apt install dotnet-sdk-5.0

インストールされました。

$ dotnet --version
5.0.201

--list-sdksでインストールされたSDKの一覧を、--list-runtimesでインストールされたランタイムの一覧を見ることが
できます。

$ dotnet --list-sdks
5.0.201 [/usr/share/dotnet/sdk]


$ dotnet --list-runtimes
Microsoft.AspNetCore.App 5.0.4 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 5.0.4 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

.NET Core 3.1をインストールする

続いて、.NET Core 3.1をインストールしてみます。

$ sudo apt install dotnet-sdk-3.1

.NET 5.0を先にインストールしているので、dotnetコマンドが示すバージョンは5.0となっています。

$ dotnet --version
5.0.201

ですが、SDKやランタイムとしては認識されています。

$ dotnet --list-sdks
3.1.407 [/usr/share/dotnet/sdk]
5.0.201 [/usr/share/dotnet/sdk]


$ dotnet --list-runtimes
Microsoft.AspNetCore.App 3.1.13 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 5.0.4 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.13 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.4 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

これで、.NET 5.0と.NET Core 3.1の両方がインストールされたことになります。

そして、この状態だとデフォルトは.NET 5.0が使われそうですね。

使ってみる

dotnetコマンドのヘルプを見てみましょう。

$ dotnet --help
.NET SDK (5.0.201)
使用法: dotnet [runtime-options] [path-to-application] [arguments]

.NET アプリケーションを実行します。

runtime-options:
  --additionalprobingpath <path>   調査ポリシーと調査対象アセンブリを含むパス。
  --additional-deps <path>         追加の deps.json ファイルへのパス。
  --depsfile                       <application>.deps.json ファイルへのパス。
  --fx-version <version>           アプリケーションを実行するために使用するインストール済み Shared Framework のバージョン。
  --roll-forward <setting>         フレームワーク バージョン (LatestPatch、Minor、LatestMinor、Major、LatestMajor、Disable) にロールフォワードします。
  --runtimeconfig                  <application>.runtimeconfig.json ファイルへのパス。

path-to-application:
  実行するアプリケーション .dll ファイルへのパス。

使用法: dotnet [sdk-options] [command] [command-options] [arguments]

.NET SDK コマンドを実行します。

sdk-options:
  -d|--diagnostics  診断出力を有効にします。
  -h|--help         コマンド ラインのヘルプを表示します。
  --info            .NET 情報を表示します。
  --list-runtimes   インストール済みランタイムを表示します。
  --list-sdks       インストール済み SDK を表示します。
  --version         使用中の .NET SDK バージョンを表示します。

SDK コマンド:
  add               .NET プロジェクトにパッケージまたは参照を追加します。
  build             .NET プロジェクトをビルドします。
  build-server      ビルドによって開始されたサーバーとやり取りします。
  clean             .NET プロジェクトのビルド出力をクリーンします。
  help              コマンド ラインのヘルプを表示します。
  list              .NET プロジェクトのプロジェクト参照を一覧表示します。
  msbuild           Microsoft Build Engine (MSBuild) コマンドを実行します。
  new               新しい .NET プロジェクトまたはファイルを作成します。
  nuget             追加の NuGet コマンドを提供します。
  pack              NuGet パッケージを作成します。
  publish           .NET プロジェクトを配置のために公開します。
  remove            .NET プロジェクトからパッケージまたは参照を削除します。
  restore           .NET プロジェクトに指定されている依存関係を復元します。
  run               .NET プロジェクトの出力をビルドして実行します。
  sln               Visual Studio ソリューション ファイルを変更します。
  store             指定されたアセンブリをランタイム パッケージ ストアに格納します。
  test              .NET プロジェクトに指定されているテスト ランナーを使用して、単体テストを実行します。
  tool              .NET のエクスペリエンスを向上するツールをインストールまたは管理します。
  vstest            Microsoft Test Engine (VSTest) コマンドを実行します。

バンドルされたツールからの追加コマンド:
  dev-certs         開発証明書を作成し、管理します。
  fsi               F# Interactive を開始するか、F# スクリプトを実行します。
  sql-cache         SQL Server キャッシュ コマンドライン ツール。
  user-secrets      開発ユーザーのシークレットを管理します。
  watch             ファイルが変更されたときにコマンドを実行するファイル ウォッチャーを起動します。

コマンドに関する詳細情報については、'dotnet [command] --help' を実行します。

ビルドなどは、このコマンドでできそうですね。

では、こちらのドキュメントに沿ってコンソールアプリケーション用のプロジェクトを作ってみたいと思います。

チュートリアル: .NET ツールを作成する - .NET CLI | Microsoft Docs

プロジェクトを作成するdotnet newコマンドを実行してみると、作成できるプロジェクトテンプレートが表示されるようです。

$ dotnet new
Templates                                     Short Name           Language    Tags                  
--------------------------------------------  -------------------  ----------  ----------------------
Console Application                           console              [C#],F#,VB  Common/Console        
Class library                                 classlib             [C#],F#,VB  Common/Library        
WPF Application                               wpf                  [C#]        Common/WPF            
WPF Class library                             wpflib               [C#]        Common/WPF            
WPF Custom Control Library                    wpfcustomcontrollib  [C#]        Common/WPF            
WPF User Control Library                      wpfusercontrollib    [C#]        Common/WPF            
Windows Forms (WinForms) Application          winforms             [C#]        Common/WinForms       
Windows Forms (WinForms) Class library        winformslib          [C#]        Common/WinForms       
Worker Service                                worker               [C#],F#     Common/Worker/Web     
Unit Test Project                             mstest               [C#],F#,VB  Test/MSTest           
NUnit 3 Test Project                          nunit                [C#],F#,VB  Test/NUnit            
NUnit 3 Test Item                             nunit-test           [C#],F#,VB  Test/NUnit            
xUnit Test Project                            xunit                [C#],F#,VB  Test/xUnit            
Razor Component                               razorcomponent       [C#]        Web/ASP.NET           
Razor Page                                    page                 [C#]        Web/ASP.NET           
MVC ViewImports                               viewimports          [C#]        Web/ASP.NET           
MVC ViewStart                                 viewstart            [C#]        Web/ASP.NET           
Blazor Server App                             blazorserver         [C#]        Web/Blazor            
Blazor WebAssembly App                        blazorwasm           [C#]        Web/Blazor/WebAssembly
ASP.NET Core Empty                            web                  [C#],F#     Web/Empty             
ASP.NET Core Web App (Model-View-Controller)  mvc                  [C#],F#     Web/MVC               
ASP.NET Core Web App                          webapp               [C#]        Web/MVC/Razor Pages   
ASP.NET Core with Angular                     angular              [C#]        Web/MVC/SPA           
ASP.NET Core with React.js                    react                [C#]        Web/MVC/SPA           
ASP.NET Core with React.js and Redux          reactredux           [C#]        Web/MVC/SPA           
Razor Class Library                           razorclasslib        [C#]        Web/Razor/Library     
ASP.NET Core Web API                          webapi               [C#],F#     Web/WebAPI            
ASP.NET Core gRPC Service                     grpc                 [C#]        Web/gRPC              
dotnet gitignore file                         gitignore                        Config                
global.json file                              globaljson                       Config                
NuGet Config                                  nugetconfig                      Config                
Dotnet local tool manifest file               tool-manifest                    Config                
Web Config                                    webconfig                        Config                
Solution File                                 sln                              Solution              
Protocol Buffer File                          proto                            Web/gRPC              

Examples:
    dotnet new mvc --auth Individual
    dotnet new xunit 
    dotnet new --help
    dotnet new classlib --help

consoleがコンソールアプリケーションのテンプレートです。

さらにヘルプを見てみます。

$ dotnet new console --help
使用法: new [options]

オプション:
  -h, --help          Displays help for this command.
  -l, --list          Lists templates containing the specified template name. If no name is specified, lists all templates.
  -n, --name          The name for the output being created. If no name is specified, the name of the output directory is used.
  -o, --output        Location to place the generated output.
  -i, --install       Installs a source or a template pack.
  -u, --uninstall     Uninstalls a source or a template pack.
  --interactive       Allows the internal dotnet restore command to stop and wait for user input or action (for example to complete authentication).
  --nuget-source      Specifies a NuGet source to use during install.
  --type              Filters templates based on available types. Predefined values are "project" and "item".
  --dry-run           Displays a summary of what would happen if the given command line were run if it would result in a template creation.
  --force             Forces content to be generated even if it would change existing files.
  -lang, --language   Filters templates based on language and specifies the language of the template to create.
  --update-check      Check the currently installed template packs for updates.
  --update-apply      Check the currently installed template packs for update, and install the updates.


Console Application (C#)
Author: Microsoft
Description: A project for creating a command-line application that can run on .NET Core on Windows, Linux and macOS
Options:                                                                             
  -f|--framework  The target framework for the project.                              
                      net5.0           - Target net5.0                               
                      netcoreapp3.1    - Target netcoreapp3.1                        
                  Default: net5.0                                                    

  --langVersion   Sets the LangVersion property in the created project file          
                  text - Optional                                                    

  --no-restore    If specified, skips the automatic restore of the project on create.
                  bool - Optional                                                    
                  Default: false                                                     

オプションを見てみると、作成するプロジェクトで使う.NETのバージョンなども指定できそうですね。

では、プロジェクトを作成します。

$ dotnet new console -n my-console-project
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on my-console-project/my-console-project.csproj...
  復元対象のプロジェクトを決定しています...
  path/to/my-console-project/my-console-project.csproj を復元しました (71 ms)。
Restore succeeded.

--nameまたは-nオプションでプロジェクト名を指定すると、指定された名前のディレクトリ内に必要なファイルが
作成されます。プロジェクト名を指定しない場合は、カレントディレクトリにファイルが生成されるようです。

プロジェクト内に移動。

$ cd my-console-project

作成されたプロジェクトファイル。

my-console-project.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <RootNamespace>my_console_project</RootNamespace>
  </PropertyGroup>

</Project>

生成されたプログラム。

Program.cs

using System;

namespace my_console_project
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

.NETのバージョンを出力するように、内容を変更してみましょう。

Program.cs

using System;

namespace my_console_project
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello .NET Core {0}!", Environment.Version.ToString());
        }
    }
}

実行。

$ dotnet run
Hello .NET Core 5.0.4!

動作しました。

今度は、ビルドして実行可能ファイルを作成してみましょう。

$ dotnet build
.NET 向け Microsoft (R) Build Engine バージョン 16.9.0+57a23d249
Copyright (C) Microsoft Corporation.All rights reserved.

  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  my-console-project -> /path/to/my-console-project/bin/Debug/net5.0/my-console-project.dll

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:01.01

作成されたようなので、実行。

$ bin/Debug/net5.0/my-console-project
Hello .NET Core 5.0.4!

OKですね。

次は、使用する.NETのバージョンを変更してみようかなと思います。

使用する .NET のバージョンを選択する - .NET | Microsoft Docs

こちらを見ると、プロジェクトファイルを変更してもよさそうですね。しかも、複数バージョン指定できそうです。

チュートリアル: .NET CLI を使用して .NET ツールを作成する / プロジェクトを作成する

今回は、2つの.NETバージョンを指定するようにしてみましょう。変更前のプロジェクトファイルはこちら。

my-console-project.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net5.0;netcoreapp3.1</TargetFrameworks>
    <RootNamespace>my_console_project</RootNamespace>
  </PropertyGroup>

</Project>

TargetFrameworkの部分を

    <TargetFramework>net5.0</TargetFramework>

TargetFrameworks(複数形)に変更して、使用する.NETのバージョンを;区切りで追加します。

    <TargetFrameworks>net5.0;netcoreapp3.1</TargetFrameworks>

ここで指定する名前は、こちらのページの「ターゲット フレームワーク モニカー(TFM)」で表現される名前の
ようですね。

SDK スタイル プロジェクトでのターゲット フレームワーク - .NET | Microsoft Docs

ビルド。

$ dotnet build
.NET 向け Microsoft (R) Build Engine バージョン 16.9.0+57a23d249
Copyright (C) Microsoft Corporation.All rights reserved.

  復元対象のプロジェクトを決定しています...
  /path/to/my-console-project/my-console-project.csproj を復元しました (83 ms)。
  my-console-project -> /path/to/my-console-project/bin/Debug/netcoreapp3.1/my-console-project.dll
  my-console-project -> /path/to/my-console-project/bin/Debug/net5.0/my-console-project.dll

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:01.64

各.NETバージョン向けの実行可能ファイルが生成されました。

確認。

$ bin/Debug/net5.0/my-console-project
Hello .NET Core 5.0.4!


$ bin/Debug/netcoreapp3.1/my-console-project
Hello .NET Core 3.1.13!

それぞれの.NETのバージョンで、ビルドできたようです。

ちなみに、このようにTargetFrameworksで複数の.NETのSDKを指定した状態だと、dotnet runだけでは実行できなくなります。

$ dotnet run
プロジェクトを実行できません
プロジェクトは複数のフレームワークを対象としています。'--framework' を使用して、実行するフレームワークを指定してください。

使用する.NETのバージョンを指定する必要があるようです。メッセージにあるように--frameworkまたは-fオプションで
実行する.NETを指定します。

$ dotnet run -f net5.0
Hello .NET Core 5.0.4!


$ dotnet run -f netcoreapp3.1
Hello .NET Core 3.1.13!

とりあえずは、複数バージョンの.NETをUbuntu Linuxにインストールして、実行できました、と。

Azure Storege向けのSpring Boot Starterを試してみる

これは、なにをしたくて書いたもの?

ローカルで、Azure StorageエミュレーターのAzuriteの使い方を調べたので、今度はJavaから使ってみます。

Azure Storageエミュレーター、Azuriteを試す - CLOVER🍀

Azureで使えるSpringライブラリがあるようなので、こちらを利用してみましょう。

Azure 上の Spring 統合のドキュメント | Microsoft Docs

AzureでのSpring、Spring Boot

AzureでのSpringに関する情報は、こちらのドキュメントを参照することになります。

Azure 上の Spring 統合のドキュメント | Microsoft Docs

Spring Bootについては、こちら。

Azure 向けの Spring Boot Starter | Microsoft Docs

ただ、このページの情報は古いようで、APIリファレンスのページを見ていくのが現時点では正解なようです。

Azure Spring Boot client library for Java | Microsoft Docs

ガイド本体の方は、Spring Initializrを使ってAzureに関する依存関係を含めることになっていますが、Spring Boot 2.4以降では
Spring Initializrでは指定できず、pom.xmlなどに直接依存関係を書くことになります。

Spring Cloud Azureのページを見てもAzure側のドキュメントに行くことになっていますし、主体はAzure側なんでしょうね。

Spring Cloud Azure

AzureでのSpringに関するソースコードは、Azure SDK for Javaのリポジトリに含まれています。

GitHub - Azure/azure-sdk-for-java: This repository is for active development of the Azure SDK for Java. For consumers of the SDK we recommend visiting our public developer docs at https://docs.microsoft.com/en-us/java/azure/ or our versioned developer docs at https://azure.github.io/azure-sdk-for-java.

現時点でのAzureでのSpring Boot Starterのバージョンが3.3.0なので、このバージョンで参照すると…、こちらですね。

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/spring

タグがバージョンで細かくたくさんあるので、どれを見るのが正解かイマイチわかっていませんが…。

ここを見ていると、実際に使うStarterのバージョンごとにタグを指定するのが、厳密な気はしますね。

For each package we release there will be a unique git tag created that contains the name and the version of the package to mark the commit of the code that produced the package. This tag will be used for servicing via hotfix branches as well as debugging the code for a particular preview or stable release version. Format of the release tags are <package-name>_<package-version>.

Azure SDK for Java / Release branches (Release tagging)

サンプルコードは、こちらにあります。

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot-samples

ですが、プロジェクト単位のサンプルコードは(存在する場合は)以下のルールに沿ったディレクトリを見るのが
正解のようです。

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/samples

AzureでのSpring Bootに関するソースコードは、こちらを見るとよいでしょう。
※Starterは、同じディレクトリ階層に並んでいます

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot

AutoConfigureはこちら。

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure

Spring Bootのサンプル。

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot/src/samples

あと、AzureのJava SDK本体の方も見ることになるでしょう。

Azure SDK for Java を使用する作業の開始 | Microsoft Docs

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk

APIリファレンスは、こちら。

Reference | Microsoft Docs

APIリファレンスページは、英語版を見た方が良さそうです。

ここまでが、AzureでのSpring Bootに関する情報でした。

Azure Storage向けのSpring Boot Starter

AzureでのSpring Bootでは、いくつかのリソースに対してのStarterが提供されています。

Azure Storage、Azure Key Vault、Azure Service Bus、Azure Cosmos DB、Azure Active Directory…などなど。

今回はAzure Storageを使うSpring Boot Starterを見ていきます。

Azure Storage 用の Spring Boot Starter の使用方法 | Microsoft Docs

ただ、こちらも実際の設定方法はAPIリファレンス側(英語)を見た方が良さそうです。

Azure Spring Boot Starter Storage client library for Java | Microsoft Docs

このページは、StarterのREADME.mdが元になっているようです。

https://github.com/Azure/azure-sdk-for-java/blob/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot-starter-storage/README.md

Azure Storageに関するAutoConfigureはこちら。

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/storage

あと、操作するのは結局Azure StorageのJava SDK APIなので、こちらも見ることになるでしょう。

Reference | Microsoft Docs

Azure Storage libraries for Java | Microsoft Docs

クイックスタート: Azure Blob Storage ライブラリ v12 - Java | Microsoft Docs

サンプルについては、Azure Storage向けのSpring Boot Starterに関するものはなさそうなので、Azure Java SDKで
Azure Storageを直接扱うサンプルを見ることになるでしょう。

azure-sdk-for-java/sdk/storage/azure-storage-blob at azure-spring-boot_3.3.0 · Azure/azure-sdk-for-java · GitHub

https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot_3.3.0/sdk/storage/azure-storage-blob/src/samples

では、情報をざっと眺めたところで使っていってみましょう。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-70-generic", arch: "amd64", family: "unix"

使用するAzuriteのバージョン。

$ npx azurite --version
3.11.0

プロジェクトの作成

Spring Initializrで、Spring Bootプロジェクトを作成します。依存関係は、Spring WebFluxにしました。
Azure Storageは、非同期のAPIを選択するとReactorを使うことになります。

$ curl -s https://start.spring.io/starter.tgz \
  -d dependencies=webflux,devtools \
  -d groupId=org.littlewings \
  -d artifactId=azure-spring-storege-example \
  -d packageName=org.littlewings.spring.azure \
  -d bootVersion=2.4.3 \
  -d javaVersion=11 \
  -d type=maven-project \
  -d baseDir=azure-spring-storege-example | tar zxvf -
$ cd azure-spring-storege-example

Azure向けのSpring Bootが依存しているSpring Bootのバージョンは2.4.3ですが、外部から異なるバージョンを指定されることも
想定しているようなので、現時点でのSpring Bootの最新版である2.4.4にしておきました。

https://github.com/Azure/azure-sdk-for-java/blob/azure-spring-boot_3.3.0/sdk/spring/azure-spring-boot/pom.xml#L31

そして、生成されたpom.xmlに以下の依存関係を追加します。

     <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>azure-spring-boot-starter-storage</artifactId>
            <version>3.3.0</version>
        </dependency>

これで、Azure Storage向けのSpring Boot Starterが使えるようになります。

自動生成されたソースコードは、今回は不要なので削除しておきしょう。

$ rm src/main/java/org/littlewings/spring/azure/DemoApplication.java src/test/java/org/littlewings/spring/azure/DemoApplicationTests.java

とりあえず、mainクラスのみ再作成。

src/main/java/org/littlewings/spring/azure/App.java

package org.littlewings.spring.azure;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

Azure Storage向けのSpring Boot Starterの設定を行う

まずは、Azure Storage向けのSpring Boot Starterの設定を行います。

こちらを参考に。

Azure Spring Boot Starter Storage client library for Java / Auto-configuration for Azure Blob storage

今回はBlob Storageのみを使うことにします。なので、Azuriteの情報を使ってこのように設定。

src/main/resources/application.properties

azure.storage.accountName=devstoreaccount1
azure.storage.accountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
azure.storage.blob-endpoint=http://127.0.0.1:10000/devstoreaccount1

これで、BlobServiceClientBuilderをDIできるようになります。

Azure Spring Boot Starter Storage client library for Java / Autowire the BlobServiceClientBuilder

存在するリソースであれば、Resouceと@ValueでDIできそうではありますが。

Azure Spring Boot Starter Storage client library for Java / Autowire a resource

今回はBlobServiceClientBuilderを使います。

Azure StorageにアクセスするRestControllerを書く

あとは、BlobServiceClientBuilderを使ったソースコードを書いていくだけです。

RestControllerから、BlobServiceClientBuilderを使うようにしましょう。また、APIは非同期の方を使います。

こんな感じで雛形を用意。

src/main/java/org/littlewings/spring/azure/BlobController.java

package org.littlewings.spring.azure;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;

import com.azure.storage.blob.BlobContainerAsyncClient;
import com.azure.storage.blob.BlobServiceAsyncClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.blob.models.ParallelTransferOptions;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
public class BlobController {
    BlobServiceClientBuilder blobServiceClientBuilder;

    BlobServiceAsyncClient serviceClient;

    public BlobController(BlobServiceClientBuilder blobServiceClientBuilder) {
        this.blobServiceClientBuilder = blobServiceClientBuilder;

        serviceClient = this.blobServiceClientBuilder.buildAsyncClient();
    }

    // ここに、メソッドを書く
}

BlobServiceClientBuilderをインジェクションして、BlobServiceAsyncClientを作成します。

続いて、Blobにアクセスするコード。URLは、{コンテナ名/Blob名`でアクセスするようにしています。といっても、コンテナの
階層的なパスを扱えうようにはしていませんけど。
それぞれ、Blobを取得、アップロード、削除するメソッドです。

    // blobs
    @GetMapping("{containerName}/{blobName}")
    public Mono<String> getBlob(@PathVariable String containerName, @PathVariable String blobName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .getBlobAsyncClient(blobName)
                .exists()
                .filter(Boolean::booleanValue)
                .map(v -> containerClient.getBlobAsyncClient(blobName))
                .flatMapMany(blobClient -> blobClient
                        .download()
                        .map(bytes -> {
                            byte[] byteArray = new byte[bytes.remaining()];
                            bytes.get(byteArray);
                            return new String(byteArray, StandardCharsets.UTF_8);
                        })
                )
                .next();
    }

    @PostMapping("{containerName}/{blobName}")
    public Mono<String> uploadBlob(@PathVariable String containerName, @PathVariable String blobName, @RequestBody String body) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        ParallelTransferOptions options = new ParallelTransferOptions();

        return containerClient
                .exists()
                .flatMap(exists -> !exists ? serviceClient.createBlobContainer(containerName) : Mono.just(containerClient))
                .map(c -> c.getBlobAsyncClient(blobName))
                .flatMap(blobClient -> blobClient.upload(Flux.just(ByteBuffer.wrap(body.getBytes(StandardCharsets.UTF_8))), options, true))
                .map(v -> String.format("blob uploaded, %s/%s", containerName, blobName));
    }

    @DeleteMapping("{containerName}/{blobName}")
    public Mono<String> deleteBlob(@PathVariable String containerName, @PathVariable String blobName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .exists()
                .filter(Boolean::booleanValue)
                .map(v -> containerClient.getBlobAsyncClient(blobName))
                .flatMap(blobClient -> blobClient.delete())
                .map(v -> String.format("blob deleted, %s/%s", containerName, blobName));
    }

Azure Storageを使うサンプルコードは、同期のものばかりだったのと、Springで扱うものはなかったのでちょっと頑張って
書いてみました。

クイックスタート: Azure Blob Storage ライブラリ v12 - Java | Microsoft Docs

非同期の方を使うコードは、Javadoc用のものがあるくらいでしょう。あ

https://github.com/Azure/azure-sdk-for-java/blob/azure-spring-boot_3.3.0/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobAsyncClientJavaDocCodeSnippets.java

わかりにくいですが、アップロードするコードのみ、コンテナが未存在の場合は作成するようにしています。

    @PostMapping("{containerName}/{blobName}")
    public Mono<String> uploadBlob(@PathVariable String containerName, @PathVariable String blobName, @RequestBody String body) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        ParallelTransferOptions options = new ParallelTransferOptions();

        return containerClient
                .exists()
                .flatMap(exists -> !exists ? serviceClient.createBlobContainer(containerName) : Mono.just(containerClient))
                .map(c -> c.getBlobAsyncClient(blobName))
                .flatMap(blobClient -> blobClient.upload(Flux.just(ByteBuffer.wrap(body.getBytes(StandardCharsets.UTF_8))), options, true))
                .map(v -> String.format("blob uploaded, %s/%s", containerName, blobName));
    }

この部分ですね。

                .flatMap(exists -> !exists ? serviceClient.createBlobContainer(containerName) : Mono.just(containerClient))

BlobContainerAsyncClient#createというメソッドもあるのですが、だいぶハマったのでこちらで…。

確認。

$ mvn spring-boot:run

アップロード。

$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/mycontainer/message.txt -d 'Hello World!!'
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 38

blob uploaded, mycontainer/message.txt


$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/mycontainer/message2.txt -d 'Hello Storage!!'
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 39

blob uploaded, mycontainer/message2.txt


$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/mycontainer2/foo.txt -d 'Foo'
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 35

blob uploaded, mycontainer2/foo.txt


$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/mycontainer2/hoge.txt -d 'Hoge'
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 36

blob uploaded, mycontainer2/hoge.txt


$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/mycontainer3/sample.txt -d 'Sample'
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 38

blob uploaded, mycontainer3/sample.txt

取得。

    @GetMapping("{containerName}/{blobName}")
    public Mono<String> getBlob(@PathVariable String containerName, @PathVariable String blobName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .getBlobAsyncClient(blobName)
                .exists()
                .filter(Boolean::booleanValue)
                .map(v -> containerClient.getBlobAsyncClient(blobName))
                .flatMapMany(blobClient -> blobClient
                        .download()
                        .map(bytes -> {
                            byte[] byteArray = new byte[bytes.remaining()];
                            bytes.get(byteArray);
                            return new String(byteArray, StandardCharsets.UTF_8);
                        })
                )
                .next();
    }
$ curl -i localhost:8080/mycontainer/message.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 13

Hello World!!


$ curl -i localhost:8080/mycontainer/message2.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 15

Hello Storage!!


$ curl -i localhost:8080/mycontainer2/foo.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 3

Foo


$ curl -i localhost:8080/mycontainer2/hoge.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 4

Hoge


$ curl -i localhost:8080/mycontainer3/sample.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 6

Sample

削除。

    @DeleteMapping("{containerName}/{blobName}")
    public Mono<String> deleteBlob(@PathVariable String containerName, @PathVariable String blobName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .exists()
                .filter(Boolean::booleanValue)
                .map(v -> containerClient.getBlobAsyncClient(blobName))
                .flatMap(blobClient -> blobClient.delete())
                .map(v -> String.format("blob deleted, %s/%s", containerName, blobName));
    }
$ curl -i -XDELETE localhost:8080/mycontainer2/hoge.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 0

Blobがない場合に、404にはしませんでした…。

$ curl -i localhost:8080/mycontainer2/hoge.txt
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 0

続いて、コンテナを扱うコードも書いてみましょう。こちらも、特に階層的なパスは意識していませんが。

    // containers
    @GetMapping
    public Mono<List<String>> containers() {
        return serviceClient
                .listBlobContainers()
                .map(blobContainerItem -> blobContainerItem.getName())
                .collectList();
    }

    @GetMapping("{containerName}")
    public Mono<List<String>> blobs(@PathVariable String containerName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .listBlobs()
                .map(blobItem -> blobItem.getName())
                .collectList();
    }

    @DeleteMapping("{containerName}")
    public Mono<String> deleteContainer(@PathVariable String containerName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .exists()
                .filter(Boolean::booleanValue)
                .flatMap(v -> containerClient.delete())
                .map(v -> String.format("container deleted, %s", containerName));
    }

現在のコンテナの一覧を取得。

    @GetMapping
    public Mono<List<String>> containers() {
        return serviceClient
                .listBlobContainers()
                .map(blobContainerItem -> blobContainerItem.getName())
                .collectList();
    }
$ curl -i localhost:8080
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 45

["mycontainer","mycontainer2","mycontainer3"]

コンテナ内に保持しているBlobの一覧を取得。

    @GetMapping("{containerName}")
    public Mono<List<String>> blobs(@PathVariable String containerName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .listBlobs()
                .map(blobItem -> blobItem.getName())
                .collectList();
    }
$ curl -i localhost:8080/mycontainer
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 30

["message.txt","message2.txt"]

コンテナを削除。

    @DeleteMapping("{containerName}")
    public Mono<String> deleteContainer(@PathVariable String containerName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .exists()
                .filter(Boolean::booleanValue)
                .flatMap(v -> containerClient.delete())
                .map(v -> String.format("container deleted, %s", containerName));
    }
$ curl -i -XDELETE localhost:8080/mycontainer3
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 0

指定したコンテナが削除されました、と。

$ curl -i localhost:8080
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 30

["mycontainer","mycontainer2"]

まとめ

Azure向けのSpring Boot Starterを使って、Azure Storageにアクセスしてみました。

けっこうてこずったのですが、AzureのSpringに関する情報はどこを見たらいいのか?というところと、Reactorに不慣れな
ところで苦労した感じですね…。

やりたいところまではできたので、良しとしましょう。

最後に、今回作成したAzure StorageにアクセスするRestControllerのソースコード全体を載せておきます。

src/main/java/org/littlewings/spring/azure/BlobController.java

package org.littlewings.spring.azure;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;

import com.azure.storage.blob.BlobContainerAsyncClient;
import com.azure.storage.blob.BlobServiceAsyncClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.blob.models.ParallelTransferOptions;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
public class BlobController {
    BlobServiceClientBuilder blobServiceClientBuilder;

    BlobServiceAsyncClient serviceClient;

    public BlobController(BlobServiceClientBuilder blobServiceClientBuilder) {
        this.blobServiceClientBuilder = blobServiceClientBuilder;

        serviceClient = this.blobServiceClientBuilder.buildAsyncClient();
    }

    // containers
    @GetMapping
    public Mono<List<String>> containers() {
        return serviceClient
                .listBlobContainers()
                .map(blobContainerItem -> blobContainerItem.getName())
                .collectList();
    }

    @GetMapping("{containerName}")
    public Mono<List<String>> blobs(@PathVariable String containerName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .listBlobs()
                .map(blobItem -> blobItem.getName())
                .collectList();
    }

    @DeleteMapping("{containerName}")
    public Mono<String> deleteContainer(@PathVariable String containerName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .exists()
                .filter(Boolean::booleanValue)
                .flatMap(v -> containerClient.delete())
                .map(v -> String.format("container deleted, %s", containerName));
    }

    // blobs
    @GetMapping("{containerName}/{blobName}")
    public Mono<String> getBlob(@PathVariable String containerName, @PathVariable String blobName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .getBlobAsyncClient(blobName)
                .exists()
                .filter(Boolean::booleanValue)
                .map(v -> containerClient.getBlobAsyncClient(blobName))
                .flatMapMany(blobClient -> blobClient
                        .download()
                        .map(bytes -> {
                            byte[] byteArray = new byte[bytes.remaining()];
                            bytes.get(byteArray);
                            return new String(byteArray, StandardCharsets.UTF_8);
                        })
                )
                .next();
    }

    @PostMapping("{containerName}/{blobName}")
    public Mono<String> uploadBlob(@PathVariable String containerName, @PathVariable String blobName, @RequestBody String body) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        ParallelTransferOptions options = new ParallelTransferOptions();

        return containerClient
                .exists()
                .flatMap(exists -> !exists ? serviceClient.createBlobContainer(containerName) : Mono.just(containerClient))
                .map(c -> c.getBlobAsyncClient(blobName))
                .flatMap(blobClient -> blobClient.upload(Flux.just(ByteBuffer.wrap(body.getBytes(StandardCharsets.UTF_8))), options, true))
                .map(v -> String.format("blob uploaded, %s/%s", containerName, blobName));
    }

    @DeleteMapping("{containerName}/{blobName}")
    public Mono<String> deleteBlob(@PathVariable String containerName, @PathVariable String blobName) {
        BlobContainerAsyncClient containerClient = serviceClient.getBlobContainerAsyncClient(containerName);

        return containerClient
                .exists()
                .filter(Boolean::booleanValue)
                .map(v -> containerClient.getBlobAsyncClient(blobName))
                .flatMap(blobClient -> blobClient.delete())
                .map(v -> String.format("blob deleted, %s/%s", containerName, blobName));
    }
}