キーポイント
- KivaKit is a modular Java framework for developing microservices that requires a Java 11+ virtual machine but is compatible with Java 8 source code
- KivaKit provides base functionality for implementing applications, including command line parsing and application configuration
- KivaKit components are lightweight and communicate status information using a broadcaster / listener messaging system
- KivaKit mini-frameworks, including the conversion, validation, resource and logging mini-frameworks, consume and report status information via messaging
- KivaKit configures and runs Jetty, Jersey, Swagger and Apache Wicket to provide microservice interfaces in a consistent way
- Key KivaKit base classes are also available as stateful traits, or “mixins”
概要
KivaKitはマイクロサービス実装用に設計された、ApacheライセンスのオープンソースJavaフレームワークです。Java 11以降の仮想マシンが必要ですが、Java 8および9のプロジェクトとソース互換性があります。KivaKitは入念に統合された一連のミニフレームワークで構成されています。各ミニフレームワークは一貫した設計と独自のフォーカスを持っていて、他のフレームワークと組み合わせても、あるいは単独でも使用することができます。これらフレームワークが備える単純化された依存関係ネットワークが、KivaKitの優れたハイレベルビューを提供しています。
ミニフレームワークはそれぞれ、マイクロサービス開発において一般的に遭遇する、さまざまな問題に対処しています。この記事では、上図に示したミニフレームワークの概要と、それらが実際に使用されている方法の概略を説明します。
メッセージング
上図に示すように、KivaKitの中心にあるのはメッセージング(Messaging)です。メッセージングはステータスを監視可能なコンポーネントの構築に適しているため、クラウドベースの世界では有用な機能です。KivaKit内のオブジェクトの多くはAlert、Problem、WarningあるいはTraceといった、ステータスメッセージのブロードキャストやリッスンを行います。その大部分は、他のオブジェクトからのステータスメッセージをリッスンし、それに関心のあるダウンストリームのリスナに再ブロードキャストするリピータ(Repeater)です。これにより、終端リスナ(terminal listener)を持ったリスナチェーン(listener chain)が構成されます。
C -> B -> A
最終リスナとしては、一般的にはLoggerなどが使用されますが、チェーンの終端に複数のリスナを置くことも可能ですし、Listnerを実装したオブジェクトならば何でも動作します。例えばValidationミニフレームワークでは、ステータスメッセージはValidationIssuesクラスによってキャプチャされ、バリデーション成否の判定とともに、バリデーション失敗時の問題をユーザに提示するために使用されます。
上記のリスナチェーンであれば、CとBがRepeaterを、最終オブジェクトのAがListenerを、それぞれ実装することになります。チェーンの各クラス内では、リスナチェーンが次のように拡張されます。
listener.listenTo(broadcaster)
対象とするリスナ(複数も可能)にメッセージを送るために、共通メッセージタイプ毎のメソッドがBroadcasterから継承されています。
メッセージ |
目的 |
problem() |
何らかの問題が発生しており、対処が必要だが、現在のオペレーションに対して致命的ではない。 |
glitch() |
小さな問題が発生している。warningとは違ってglitchは、バリデーションエラーやデータ損失が発生したことを示す。またproblemとは違い、オペレーションの回復と継続が保証されている。 |
warning() |
修正すべき小さな問題が発生しているが、必ずしも注意を払う必要はない。 |
quibble() |
修正する必要のない、些細な問題が発生している。 |
announcement() |
オペレーションの重要なフェーズを告知する。 |
narration() |
オペレーション内の1ステップが開始あるいは完了したことを示す。 |
information() |
問題の通知ではなく、一般的に有用な情報。 |
trace() |
デバッグ時に使用する診断情報。 |
Broadcasterは、クラスやパッケージのパターンマッチングによって、コマンドラインからTraceメッセージをオンあるいはオフにするメカニズムも提供しています。
ミックスイン
KivakitにはRepeaterを実装する方法が2つあります。ひとつは単純なBaseRepeaterの拡張で、もうひとつはステートフルなトレートあるいはミックスインを使うことです。RepeatMixinインターフェースを実装することはBaseRepeaterの拡張と同じですが、前者は別のベースクラスを持っているクラスにも使用することができます。後述するComponentインターフェースでもこれと同じパターンが使用されています。BaseComponentを拡張できない場合には、代わりにComponentMixinを実装することが可能です。
Mixinインターフェースは、Java言語仕様の欠点を回避する手段を提供している、と言えます。このインターフェースは状態のルックアップを、パッケージプライベートなクラスであるMixinStateに委譲(delegation)することで機能します。MixinStateはMixinを実装したクラスの参照を使って、関連付けられた状態オブジェクト(state object)をIDハッシュマップから検索します。Mixinインターフェースはこのようになっています。
public interface Mixin
{
default <T> T state(Class<? extends Mixin> type, Factory<T> factory)
{
return MixinState.get(this, type, factory);
}
}
state()でthisの状態オブジェクトが見つからなかった場合は、指定されたファクトリメソッドを使って新たな状態オブジェクトを生成し、状態マップのミックスインによる関連付けを行います。例えば、私たちのRepeaterMixinインターフェースは次のようなものになります(簡略化のため大部分のメソッドは省略しています)。
public interface RepeaterMixin extends Repeater, Mixin
{
@Override
default void addListener(Listener listener, Filter<Transmittable> filter)
{
repeater().addListener(listener, filter);
}
@Override
default void removeListener(Listener listener)
{
repeater().removeListener(listener);
}
[...]
default Repeater repeater()
{
return state(RepeaterMixin.class, BaseRepeater::new);
}
}
ここでのaddListener()とremoveListener()の2メソッドは、それぞれがrepeater()を通じてBaseRepeater状態オブジェクトを取得した上で、メソッド呼び出しをそのオブジェクトに委譲します。ここから分かるように、KivaKitでのミックスインの実装はそれほど複雑なものではありませんが、
ミックスイン内の各メソッドの呼び出し時に状態マップの検索が必要な点に注意してください。通常はIDハッシュマップが十分な効果を持っていますが、一部のコンポーネントではこれがパフォーマンス上の問題になる可能性があります。ただし、パフォーマンス問題の常として、プロファイルによって否定されるまでは最もシンプルな方法を選択するのがベストでしょう。
コンポーネント
KivaKitのコンポーネント(Component)は一般的に、マイクロサービスの重要な部分である、と言っていいでしょう。BaseComponentの拡張を通じて(最も一般的なケース)、あるいはComponentMixinを実装することによって、メッセージへの容易なアクセス手段を提供します。Componentからの継承は、Repeaterから継承するリスナのリストを除けば、オブジェクトに状態を加えることはありません。このためコンポーネントは非常に軽量で、多数のインスタンスを生成しても問題にはなりません。ComponentはRepeaterでもあるので、前述のようなリスナチェーンを生成することも可能です。
メッセージアクセスの便宜に加えて、以下のような機能も提供します。
- オブジェクトの登録と検索
- オブジェクトのロードとアクセス設定
- パッケージリソースへのアクセス
これらの機能を個々に見ていきましょう。
オブジェクトの登録と検索
KivaKitでは、依存性注入(dependency injection)ではなく、サービスロケータ(service locator)デザインパターンを使用しています。Component内でのこのパターンの使用は単純で、ひとつのコンポーネントがregisterObject()で登録したオブジェクトを、別のコンポーネントがrequire()で検索できる、というものです。
Database database = [...]
registerObject(database);
[...]
var database = require(Database.class);
同一クラスの複数のインスタンスを登録する必要がある場合は、enum値を使って区別することができます。
enum Database { PRODUCTS, SERVICES }
registerObject(database, Database.PRODUCTS);
[...]
var database = require(Database.class, Database.SERVICES);
KivaKitでは、依存性注入を使用する場所にはすべて、代わりにregisterとrequiredを使用しています。
設定
KivaKitのコンポーネントは、require()メソッドを使って設定情報にも簡単にアクセスできます。
require(DatabaseSettings.class);
オブジェクトの登録時と同じように、イベント内に同じタイプの設定オブジェクトが複数ある場合、それらを区別するためにenumを使用することができます。
require(DatabaseSettings.class, Database.PRODUCTS);
設定情報を登録する方法はいくつかあります。
registerAllSettingsIn(Folder)
registerAllSettingsIn(Package)
registerSettingsObject(Object)
registerSettingsObject(Object, Enum)
KivaKit 1.0では、register.AllSettingsIn()メソッドでロードされる設定オブジェクトは、.propertiesファイルを使って定義されますが、将来的には、.jsonファイルなど他のソースからプロパティをロードするためのAPIが提供される予定です。インスタンス化する設定クラスの名称はclassプロパティで指定します。その他のプロパティは、インスタンス化されたオブジェクトから個々のプロパティとして取得することができます。各プロパティは、KivaKitコンバータを使ってオブジェクトに変換されます(後述)。
例えば、
DatabaseSettings.properties
class = com.mycompany.database.DatabaseSettings
port = database.production.mypna.com:3306
DatabaseSettings.java
public class DatabaseSettings
{
@KivaKitPropertyConverter(Port.Converter.class)
private Port port;
public Connection connect()
{
// Return connection to database on desired port
[...]
}
}
パッケージリソース
さまざまなリソースタイプを統合的に扱えるResoureミニフレームワークが用意されています。
- ファイル
- ソケット
- ZIPまたはJARファイルエントリ
- パッケージリソース
- HTTP応答
- 入力ストリーム
- 出力ストリーム
- […]
Resourceはストリームデータの読み取りが可能なComponentです。またWritableResourceはストリームデータの書き込みが可能なリソースです。Fileのメソッドの大半は、すべてのResourceでも利用可能ですが、一部のリソースタイプではサポートされないメソッドがいくつかあります。例えば、リソースがストリーミングされる場合は、sizeInByes()は実装できません。
KivaKitのFileは特殊なリソースです。新たなファイルシステムの追加を許可するには、サービスプロバイダインターフェース(SPI)を使用します。kivakit-extensionsプロジェクトでは、以下のファイルシステムの実装を提供しています。
- HDFSファイル
- S3オブジェクト
- GitHubリポジトリ(読み取り専用)
KivaKitのコンポーネントは、PackageResourceに簡単にアクセスできる手段を提供しています。リソースのカプセル化は、Apache Wicketのそれと同じように、コンポーネントのパッケージが機能するために必要なリソースを含んだサブパッケージを保持する形式で行います。これにより、単一のソースツリーからコンポネントを簡単にパッケージ化して利用することができます。Componentへの相対でパッケージリソースにアクセスする方法は、次のようなものになります。
public class MyComponent extends BaseComponent
{
[...]
var resource = listenTo(packageResource("data/data.txt"));
for (var line : resource.reader().lines())
{
}
}
パッケージ構造は次のようなものです。
├── MyComponent
└── data
└── data.txt
アプリケーション
KivaKitのApplicationは、スタートアップ、初期化、実行に関連するメソッドを持った、特別なComponentです。ServerはApplicationのサブクラスです。
KivaKitの最も一般的なアプリケーションはマイクロサービスですが、他のタイプのアプリケーション(デスクトップ、Web、ユーティリティ等)を実装することも可能です。マイクロサービスアプリケーションのコードの骨子はこのようになります。
public class MyMicroservice extends Server
{
public static void main(final String[] arguments)
{
new MyApplication().run(arguments);
}
private MyApplication()
{
super(MyProject());
}
@Override
protected void onRun()
{
[...]
}
}
ここでのmain()メソッドは、アプリケーションを生成した上で、Applicationベースクラスのrun()メソッドを、コマンドラインから渡された引数を使って呼び出します。次にマイクロサービスのコンストラクタが、Projectオブジェクトを上位クラスのコンストラクタに渡します。このオブジェクトはアプリケーションを含むプロジェクトや、それが依存する他のプロジェクトの初期化を行います。プログラム例に戻ると、今回のプロジェクトはこのようなものになります。
public class MyProject extends Project
{
private static Lazy<MyProject> project = Lazy.of(MyProject::new);
public static ApplicationExampleProject get()
{
return project.get();
}
protected ApplicationExampleProject()
{
}
@Override
public Set<Project> dependencies()
{
return Set.of(ResourceProject.get());
}
}
MyProjectのsingletonインスタンスをget()で取得することができます。MyProjectの依存関係はdependencies()を使って取得可能です。この例の場合のMyProjectは、kivakit-resourceミニフレームワーク用のProject定義であるResourceProjecrにのみ依存しています。ResourceProjectも独自の依存関係を持っています。KivaKitでは、onRun()がコールされる前に、すべての推移的プロジェクト依存関係が初期化されていることを保証しています。
デプロイメント
KivaKitアプリケーションには、"deployments"という名称のアプリケーション相対パッケージから、設定オブジェクトのリストを自動的にロードする機能があります。この機能は、マイクロサービスを特定の環境にデプロイする場合に便利です。アプリケーションの構造は次のようになっています。
├── MyMicroservice
└── deployments
├── development
│ ├── WebSettings.properties
│ └── DatabaseSettings.properties
└── production
├── WebSettings.properties
└── DatabaseSettings.properties
アプリケーションのコマンド行に"-deployment="というスイッチが渡された場合には、指定された名称のデプロイメントから設定がロードされます(この例ではdevelopmentまたはproduction)。パッケージされたデプロイメント設定を使うことにより、アプリケーションが非常にシンプルなものになるので、マイクロサービスでは特に便利です。
java -jar my-microservice.jar -deployment=development [...]
これにより、アプリケーションの内容を詳しく知らなくても、Dockerコンテナ内で簡単に実行できるようになります。
パッケージ化されたデプロイメント設定が望ましくない場合は、環境変数を設定して外部フォルダを使用することができます。
-DKIVAKIT_SETTINGS_FOLDERS=/Users/jonathan/my-microservice-settings
コマンド行解析
SwitchParsersのセット、および/またはArgumentParsersのリストを返すことで、コマンド行を解析することもできます。
public class MyMicroservice extends Application
{
private SwitchParser<File> DICTIONARY =
File.fileSwitchParser("input", "Dictionary file")
.required()
.build();
@Override
public String description()
{
return "This microservice checks spelling.";
}
@Override
protected void onRun()
{
var input = get(DICTIONARY);
if (input.exists())
{
[...]
}
else
{
problem("Dictionary does not exist: $", input.path());
}
}
@Override
protected Set<SwitchParser> switchParsers()
{
return Set.of(DICTIONARY);
}
}
この例では、switchParsers()が返すDICTIONARYスイッチパーザがKivaKitのコマンドライン解析に使用されます。onRun()メソッドでは、コマンド行で渡されたFile引数がget(DICTIONARY)で取り出されます。コマンド行に構文的な問題のある場合やバリデーションをパスしない場合には、KivaKitが自動的に問題を報告すると同時に、description()とスイッチ、および引数パーザから生成した使用方法のヘルプメッセージを表示します。
┏-------- COMMAND LINE ERROR(S) -----------
○ Required File switch -input is missing
┗------------------------------------------
KivaKit 1.0.0 (puffy telephone)
Usage: MyApplication 1.0.0 <switches> <arguments>
This microservice checks spelling.
Arguments:
None
Switches:
Required:
-input=File (required) : Dictionary file
スイッチパーザ
今回のサンプルアプリケーションでは、以下のコードでSwitchParserを構築しています。
private SwitchParser<File> INPUT =
File.fileSwitchParser("input", "Input text file")
.required()
.build();
File.fileSwitchParser()メソッドが返すスイッチパーザのビルダは、build()をコールする前に、いくつかのメソッドを使って特殊化することができます。
public Builder<T> name(String name)
public Builder<T> type(Class<T> type)
public Builder<T> description(String description)
public Builder<T> converter(Converter<String, T> converter)
public Builder<T> defaultValue(T defaultValue)
public Builder<T> optional()
public Builder<T> required()
public Builder<T> validValues(Set<T> validValues)
File.fileSwitchParser()の実装は、次のようなものになります。
public static SwitchParser.Builder<File> fileSwitchParser(String name, String description)
{
return SwitchParser.builder(File.class)
.name(name)
.converter(new File.Converter(LOGGER))
.description(description);
}
スイッチと引数はすべて型指定されたオブジェクトなので、builder(Class)メソッドはFile型のビルダを(type()メソッドを使って)生成します。fileSwitchParser()には指定された名称(name)と説明(description)が渡されます。StringオブジェクトからFileオブジェクトへの変換にはFile.Converterメソッドを使用します。
コンバータ
KivaKitには多数のコンバータが用意されていて、KivaKit内のさまざまな場所で使用することができます。コンバータは、あるタイプから別のタイプに変換する、再利用可能なオブジェクトです。生成が非常に簡単で、例外やnull値ないし空値といった共通的な問題を処理します。
public static class Converter extends BaseStringConverter<File>
{
public Converter(Listener listener)
{
super(listener);
}
@Override
protected File onToValue(String value)
{
return File.parse(value);
}
}
StringConverter.converter(String)を呼び出すことで、StringをFileに変換します。StringConverter.unconvert(File)は、Fileを文字列に戻します。変換中に発生した問題は、関心のあるリスナ(複数可能)にブロードキャストされます。変換に失敗した場合にはnullが返されます。
ここから分かるように、コンバータではリスナチェーンとは異なるアプローチを採用しています。コンバータは、ユーザによるlistenTo()呼び出しに頼るのではなく、コンストラクタの引数としてリスナを要求します。これによってすべてのコンバータが、少なくともひとつのリスナに対して、変換時の問題発生を報告できることが保証されるのです。
バリデーション
前述したコマンドラインの解析コードでは、kivakit-validationミニフレームワークを使ってスイッチと引数を検証していました。このミニフレームワークのもうひとつの一般的なユースケースは、マイクロサービスに対するWebアプリケーションのユーザインターフェースのドメインオブジェクトを検証することです。
Validatableクラスは次のインターフェースを実装します。
public interface Validatable
{
/**
* @param type The type of validation to perform
* @return A {@link Validator} instance
*/
Validator validator(ValidationType type);
}
このメソッドを実装するために、BaseValidatorを匿名でサブクラス化することができます。BaseValidatorは状態の一貫性チェックや、問題と警告のブロードキャストなど、便利なメソッドを提供してくれます。KivaKitはこれらのメッセージをValidationIssuesオブジェクトでキャプチャします。その後はValidatableインターフェースのデフォルトメソッドを使って、この状態を問い合わせることが可能です。使用方法は次のようになります。
public class User implements Validatable
{
String name;
[...]
@Override
public Validator validator(ValidationType type)
{
return new BaseValidator()
{
@Override
protected void onValidate()
{
problemIf(name == null, "User must have a name");
}
};
}
}
public class MyComponent extends BaseComponent
{
public void myMethod()
{
var user = new User("Jonathan");
if (user.isValid(this))
{
[...]
}
}
}
ここではバリデーションからのメッセージを、Userオブジェクトが有効かどうかを判定するためにキャプチャしています。同じメッセージがMyComponentのリスナにもブロードキャストされ、ログ記録やユーザインターフェースへの表示といったことが行われます。
ロギング
KivaKitのLoggerは、受信したすべてのメッセージをログ記録するメッセージリスナです。ベースApplicationクラスには、コンポーネントからアプリケーションレベルに浮き上がってきたメッセージをログするロガーがあります。各コンポーネントからアプリケーションに至るまではリスナチェーンでつながっているので、アプリケーションとそのすべてのコンポーネントまでロガーを用意する必要はない、ということになります。
最も単純なロガーはConsoleLoggerです。基本的な部分のみに限定すると、ConsoleLoggerとその関連クラスは、おおよそ次のような構成になっています(下記UML図を参照してください)。
public class ConsoleLogger extends BaseLogger
{
private Log log = new ConsoleLog();
@Override
protected Set<Log> logs()
{
return Sets.of(log);
}
}
public class BaseLogger implements Logger
{
void onMessage(final Message message)
{
log(message);
}
public void log(Message message)
{
[...]
for (var log : logs())
{
log.log(entry);
}
}
}
public class ConsoleLog extends BaseTextLog
{
private Console console = new Console();
@Override
public synchronized void onLog(LogEntry entry)
{
console.printLine(entry.message().formatted());
}
}
BaseLogger.log(Message)メソッドは、与えられたメッセージにコンテキスト情報を追加してLogEntryに変換し、それをLogs()で取得したログリスト内の各Logに渡します。ConsoleLoggerの場合は、ConsoleLogの単一インスタンスが返されます。ConsoleLogはLogEntryをコンソールに書き込みます。
KivaKitには、コマンドラインからの新たなロガーの動的な追加と設定が可能なSPIがあります。KivaKitが提供するロガーは以下のようなものです。
- ConsoleLog
- EmailLog
- FileLog
WebとREST
kivakit-extensionsプロジェクトには、マイクロサービスの実装で使用されることの多いJetty、Jersey、Swagger、Apache Wicketの必要最低限なサポートが含まれています。これらのミニフレームワークはすべて統合されているため、マイクロサービスのRESTとWebアクセスを提供するJettyサーバを起動するようなことは非常に簡単です。
@Override
protected void onRun()
{
final var port = (int) get(PORT);
final var application = new MyRestApplication();
// and start up Jetty with Swagger, Jersey and Wicket.
listenTo(new JettyServer())
.port(port)
.add("/*", new JettyWicket(MyWebApplication.class))
.add("/open-api/*", new JettySwaggerOpenApi(application))
.add("/docs/*", new JettySwaggerIndex(port))
.add("/webapp/*", new JettySwaggerStaticResources())
.add("/webjar/*", new JettySwaggerWebJar(application))
.add("/*", new JettyJersey(application))
.start();
}
ここでのJettyServerはJersey、Wicket、Swaggerを一貫的なAPIで統合するので、コードがクリアで簡潔なものになります。ほとんどの場合はこれで十分です。
結論
KivaKitはバージョン1.0になったばかりですが、Telenavでは10年以上にわたって使用されています。フィードバックやバグ報告、機能上のアイデア、資料、テストやコードのコントリビューションなど、オープンソースコミュニティからのインプットは大歓迎です。
詳細を確認するためには、以下のリソースが役に立つでしょう。
リソース |
説明 |
ライセンス |
|
関連プロジェクト |
|
開発者向けセットアップ Blog |
|
GitHub |
|
コード |
git clone https://github.com/Telenav/kivakit.git |
Eメール |
|
|
|
著者について
Jonathan Locke氏は1996年からJavaに携わっています。氏はかつて、Sun Microsystems Java Teamのメンバでした。オープンソース作家としての氏は、Apache Wicket WebフレームワークやJava UMLドキュメンテーションツールLexakaiの作者です。現在はTelenavでプリンシパルソフトウェアアーキテクトの職にあります。