MicroProfileを調べてまとめる
はじめに
昨今はマイクロサービスが採用されるソフトウェアアーキテクチャとして、一般的になっているように感じます。マイクロサービスでは従来のようにアプリケーションを大きな一枚岩として作成せずアプリケーションの単位で分割し、それらのまとまりで、1つ以上のサービスを構築します。
Javaでサーバサイドのコードを書く際にJakarta EEなどの仕様が定義されています。ここで、Javaに置いてもマイクロサービスに有用な仕様があり、それがMicroProfileです。
最近、その名前を周りでよく耳にすることが増えた気がするので、この機会にまとめてみようかと思います。
この記事では以下ことをゴールとします。
- MicroProfileの概要を把握する
- HelloWorldとしていくつかのAPIを試してみる
MicroProfileとは
公式のトップページには以下のように紹介されています。
Optimizing Enterprise Java for a Microservices Architecture
MicroProfileのプロジェクトのミッションは「複数の実装を刷新し、基本的な部分に関して標準化することで、マイクロサービスに特化したEnterprise Javaのオープンフォーラムとなること」だそうです。 Jakarta EE(Java EE)とは別でEclipse Fandationの傘下でApache License, Version 2.0として開発が続けられています。
Jakarta EE(Java EE)との関係
MicroProfileはJavaEE8の策定が難航し、なかなか開発が進まなかった際に、ベンダー主導で立ち上げられました。ベンダー主導ではあるもののその目的は、「ベンダーに依存しないエンタープライズ・テクノロジーの開発を可能にすること」だったようです。もともとは、Java EEを補完するものとして、定義され、インキュベーターの役割になるとIBMのブログでは書かれています。 しかし、私がJJUGのナイトセミナーで聞いた話によると、そう簡単に統合していけるかを懐疑的であるような話も聞きました(この辺ソース探しても出てこなかった...)。今後どうなっていくかは情報を丁寧に追っていく必要がありそうです。
主要な開発ベンダー
MicroProfileを開発する主要なベンダーは以下のようになっています。
MicroProfile 3.3に含まれるコンポーネント
現在(2020年3月)最新のMicroProfile3.3には以下のコンポーネントが含まれています。
コンポーネント名 | バージョン | 説明 |
---|---|---|
MicroProfile Config | 1.4 | 設定を外部から変更することができるようにする機能を提供する。MicroProfile Configでは設定の変更を拾い上げすぐに反映させることが可能である。 |
MicroProfile Fault Tolerance | 2.1 | タイムアウト、リトライ、隔離、サーキットブレーカー、FallBack等々の機能を提供する |
MicroProfile Health | 2.2 | アプリケーションの状態を外部に公開するための機能を提供する |
MicroProfile JWT Authentication | 1.1 | JWTによる認証をサポートするAPIを提供する。 |
MicroProfile Metrics | 2.3 | JMXと同様のメトリクスをRESTFulなサービスから取得するための機能を提供する。 |
MicroProfile OpenAPI | 1.1 | API定義をOpenAPI v3 の仕様に準ずるドキュメントとして出力する機能を提供する。 |
MicroProfile OpenTracing | 1.3 | OpenTracingの仕様に準じたトレースを実現するための機能を提供する。JAX-RSの拡張APIを定義している。 |
MicroProfile Rest Client | 1.4 | JAX-RSのクライアントのラッパーでタイプセーフなサービスへのアクセスを実現する。 |
CDI | 2.0 | DIの機能を提供する |
Common Annotations | 1.3 | Java SEやEEで使われる様々なアノテーションを定義する |
JAX-RS | 2.1 | Restfulなアプリを作成するための機能を提供する。JavaEEから継承 |
JSON-B | 1.0 | JavaオブジェクトとJsonを相互に読み書きする機能を提供する |
JSON-P | 1.1 | StAX のようなストリーミングAPI と、DOMのようなオブジェクトモデル APIを提供する。 |
DBの仕様は定義されていないようで、QuarkusなどMicroProfileを利用するフレームワークでは独自に機能を追加している場合が多いようです。
HelloWorld
HelloWorldとして、MircoProfileの以下のAPIを使って簡単なAPIサーバを作成してみたいとおもいます。
環境
プロジェクトの作成
プロジェクトの作成方法には以下の2つがあるようです。
Starterはサンプルプロジェクトを作成してくれます。ビルドすると実行可能なJarが生成されるので別途ランタイムを準備しておく必要はありません。また、必要な設定などもしておいてくれるようです。 感覚として、spring initializrに近しいものっぽいです。
Archetypeの方はMvn Archetypeを使ってプロジェクトを生成します。.warアーカイブを作りたいときや、Jakarta EEと組み合わせるときに有用だそうです。
今回は、楽したい余計な複雑性を排除するために実行可能Jarとして作成可能な後者の方を利用して作成しようと思います。
MicroProfile Starterのサイトから適当な値を入力し、DownLoadボタンを押します。今回はMircoProfileのバージョンは3.2、RuntimeはPayara Micro Javaのバージョンは11を選択し、Examples for specifications
はHealth Checksだけチェックを入れました。ちなみに、選択にとくに深い意味は無いです。
生成されたプロジェクト
生成されたプロジェクト構成は以下のようになっています。
┌── pom.xml ├── readme.md └── src └── main ├── java │ └── dev │ └── hirooka │ └── helloworld │ └── helloworld │ ├── HelloController.java │ ├── HelloworldRestApplication.java │ └── health │ ├── ServiceLiveHealthCheck.java │ └── ServiceReadyHealthCheck.java ├── resources │ └── META-INF │ └── microprofile-config.properties └── webapp ├── WEB-INF │ └── beans.xml └── index.html
デフォルトでリソースクラスとHelthCkeck用のクラスが用意されているようです。
pom.xml
生成されたPomは以下のようになりました。
<?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>dev.hirooka.helloworld</groupId> <artifactId>helloworld</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.target>11</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> <maven.compiler.source>11</maven.compiler.source> <payaraVersion>5.194</payaraVersion> <final.name>helloworld</final.name> </properties> <dependencies> <dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>3.2</version> <type>pom</type> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>helloworld</finalName> </build> <profiles> <profile> <id>payara-micro</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <plugins> <plugin> <groupId>fish.payara.maven.plugins</groupId> <artifactId>payara-micro-maven-plugin</artifactId> <version>1.0.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>bundle</goal> </goals> </execution> </executions> <configuration> <payaraVersion>${payaraVersion}</payaraVersion> </configuration> </plugin> </plugins> </build> </profile> </profiles> </project>
依存として入っているのはorg.eclipse.microprofile
だけですね(てっきり、Health Checksの依存も入るのかと思ってた)。あとは、Payara MicroのMavenPluginが入れられています。
アプリの起動
一緒に生成されたreadme.md
を読むとmvn clean package
コマンドを叩き、java -jar target/helloworld-microbundle.jar
を叩くとアプリが起動するようです。実行してみてテストページを見てみます。
$ mvn clean package $ java -jar target/helloworld-microbundle.jar
デフォルトでテストページが容易されているみたいなので、そのページ(http://localhost:8080/index.html
)にブラウザーアクセスしてみると以下の表示がされました。
デフォルトでは8080ポートでアプリが起動するようですね。Health Checksを入れたからなのかそれ用のエンドポイントへのURLのリンクも用意されているみたいです。
なお、今後省略しますが、コードを書き換えるたびにビルドと起動をやり直しています(もしかしたら開発者用にHot Reloadの機能って提供されてたりするのかな...?)。
JAX-RSでWebAPIの口を作成する
アプリの起動ができたところで、ハンドラーを実装していこうと思います。
...と思いましたが前述したようにデフォルトでリソースクラスとアプリケーションサブクラスがすでに作られていますね。まずは読んでみましょう。コントローラーは以下のようになっていました。
HelloworldRestApplication.java
package dev.hirooka.helloworld.helloworld; import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; /** * */ @ApplicationPath("/data") @ApplicationScoped public class HelloworldRestApplication extends Application { }
HelloController
package dev.hirooka.helloworld.helloworld; import javax.inject.Singleton; import javax.ws.rs.GET; import javax.ws.rs.Path; /** * */ @Path("/hello") @Singleton public class HelloController { @GET public String sayHello() { return "Hello World"; } }
アプリケーションのサブクラスとHello World
の文字列を返すリソースクラスが書かれているようです。
/data/hello
にGetでアクセスすればHello World
と取得できます。
このリソースクラスに対してクエリストリングで名前を受け取って、受け取った名前に対して挨拶を返すリソースメソッドを追加してみたいと思います。
@Path("/hello") @Singleton public class HelloController { @GET public String sayHello() { return "Hello World"; } @GET @Path("someone") public String greetingToSomeone(@QueryParam("name") String name) { return "Hello, " + name; } }
$ curl localhost:8080/data/hello/someone?name=henohenomoheji Hello, henohenomoheji
まぁ、Java EEからあるJAX-RSを継承しているので、あまり、新規性は無いのかもしれないです。
JSON-BでJsonの値を取得できるようにする
続いて、JsonのBindingを行ってみます。MicroProfileではJsonのバインディングのAPIの仕様としてJSR 367: JavaTM API for JSON Binding (JSON-B)を採用しています。こちらもJax-RS同様にJava EEからの継承しています。
先程の挨拶を返すリソースメソッドを書き換えてJsonでのやり取りを行なうようにしてみたいと思います。まずはJsonをバインディングするPOJOを作成します。 リクエスト用にName.java
、レスポンスようにGreet.java
を用意します。
Name.java
public class Name { private String name; // リソースメソッドの引数として受け取る際に、public な引数なしコンストラクタが必要 public Name() {} public Name(String name) { this.name = name; } public void setName(String name) { this.name = name; } public String getName() { return name; } }
Greet.java
import javax.json.bind.annotation.JsonbTransient; public class Greet { private String greet; //レスポンスとして返してほしくない情報 @JsonbTransient private String internalInfo; public Greet(String greet, String internalInfo) { this.greet = greet; this.internalInfo = internalInfo; } public String getGreet() { return greet; } public void setGreet(String greet) { this.greet = greet; } public String getInternalInfo() { return internalInfo; } public void setInternalInfo(String internalInfo) { this.internalInfo = internalInfo; } }
リクエスト用のPOJO、Name.java
はリソースメソッドの引数として、バインディングの際にパブリックな引数なしコンストラクタが必要なようです(なかったときにエラーで落ちた)。
レスポンス用のPOJO、Greet.java
には内部情報を持つようのStringのローカル変数を持つようにしました。ここで、このインターナルな情報はレスポンスとして返してほしく無い情報であることを想定し、@JsonbTransient
アノテーションをつけました。
挨拶のリソースメソッドをJsonでのやり取りで行なう新しいリソースメソッドを追加します。
@Path("/hello") @Singleton public class HelloController { // 今まで書いたリソースメソッドは省略 @POST @Path("json")今までの @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Greet greetingWithJson(Name name) { Greet greet = new Greet("Hello, " + name.getName(), "Not returned info: Greeted at " + LocalDateTime.now()); System.out.println(greet.getInternalInfo()); return greet; } }
Jsonでのリクエストを受け、そしてレスポンスをJsonで返す場合に、それぞれ@Comsumes
アノテーションと@Produce
アノテーションを使ってコンテントタイプを"application/json"
に指定する必要があります。Json-bはJAX-RSの裏側で使われており、アプリケーション開発者は特に意識することなくリソースメソッドの引数と、レスポンスにそれぞれのPOJOを突っ込むだけでバインディングを行ってくれるようです。
今回は利用していないですが、MessageBodyReader
とMessageBodyWriter
を実装すれば、カスタムなバインディングも行なうことが可能になると思われます。
それでは、アプリを再ビルド、起動を行い以下のコマンドを叩きリクエストを送ってみましょう。
$ curl -XPOST -d '{"name": "henohenomoheji"}' -H "Content-Type: application/json" localhost:8080/data/hello/json {"greet":"Hello, henohenomoheji"}
Jsonでのやり取りをするWebAPIが完成しました。@JsonbTransient
で指定したinternalInfo
はレスポンスとして返ってきていないようです。
また、標準出力には以下の文字列が出力されていました。
Not returned info: Greeted at 2020-03-20T13:58:44.686383
CDIでDIしてみる
名前を受け取りHello, ${name}
という形式の文字列に変換し返すサービスを一つ作成しCDIしてみたいと思います。
まずはサービスクラスを作成します。
package dev.hirooka.helloworld.helloworld; import javax.inject.Named; @Named public class CreateHelloGreetService { public String createHelloGreet(String name){ return "Hello, " + name; } }
オブジェクト指向な設計的にも、Java的にもツッコミどころが多いサービスではあると思いますが、CDIを試してみたいだけなので、ご容赦ください。
CDIの使い方は深く解説しませんが、@Named
アノテーションをつけることでBeenとしてDIコンテナに管理されます。
コントローラークラスでこのサービスクラスをインジェクションします。
@Path("/hello") @Singleton public class HelloController { @Inject CreateHelloGreetService createHelloGreetService; @GET public String sayHello() { return "Hello World"; } @GET @Path("someone") public String greeting(@QueryParam("name") String name) { return createHelloGreetService.createHelloGreet(name); } @POST @Path("json") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Greet greetingWithJson(Name name) { Greet greet = new Greet("Hello, " + name.getName(), "Not returned info: Greeted at " + LocalDateTime.now()); System.out.println(greet.getInternalInfo()); return greet; } }
インジェクとには@Inject
アノテーションを用います。
アプリをリビルドして、再起動し以下のcurlを叩きます。
curl localhost:8080/data/hello/someone?name=henohenomoheji Hello, henohenomoheji
CDIが利用でき、Hello, henohenomoheji
の文字列が返ってきていることがわかります。
デフォルトで作成されているHealth Checksを読んでみる
Controller同様にいくつかのヘルスチェックの口もデフォルトで用意されていました。これもすでに作成されているクラスがあったので今回はそれを読んでみることにします。
ServiceLiveHealthCheck.java
import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.Liveness; import javax.enterprise.context.ApplicationScoped; @Liveness @ApplicationScoped public class ServiceLiveHealthCheck implements HealthCheck { @Override public HealthCheckResponse call() { return HealthCheckResponse.named(ServiceLiveHealthCheck.class.getSimpleName()).withData("live",true).up().build(); } }
ServiceReadyHealthCheck.java
import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.Readiness; import javax.enterprise.context.ApplicationScoped @Readiness @ApplicationScoped public class ServiceReadyHealthCheck implements HealthCheck { @Override public HealthCheckResponse call() { return HealthCheckResponse.named(ServiceReadyHealthCheck.class.getSimpleName()).withData("ready",true).up().build(); } }
これら2つのクラスはそれぞれアプリがRedyであることと、アプリが生きていいることを外部に公開するためのAPIを提供しているようです。
まず、注目すべきはどちらもHealthCheck
インターフェースを実装していることです。このインターフェースではcall()
メソッドが定義されており、HelthCheckResponse
を返すようになっています。またnamed()
で、チェックそのものに名前を付与し、withData()
で一緒に返すデータ、up
でアプリケーションの状態(UP
or DOWN
)を返すことができます。例えばServiceLiveHealthCheck
の呼び出しに対するレスポンスは以下のようになります。
{"status":"UP","checks":[{"name":"ServiceLiveHealthCheck","status":"UP","data":{"live":"true"}}]}
これが、MicroProfile HelthCheckの基本的な使い方のようです。
また、それぞれのクラスでは@Readiness
と@Liveness
アノテーションが付与されています。それぞれが付与されたHealth ChaeckのクラスがCDIのBeanとして登録されることで、 /health/live
と/heath/ready
から情報を取得することが可能となります。
今回自動生成されたクラスにはありませんでしたが、独自でカスタムしたHealth Checkを行いたい場合は@Health
アノテーションの利用も可能です。
@Health
アノテーションが付与されたHealthCkeckクラスは、/health
の呼び出しの際に検出されチェックロジックを実行、結果を統合して返すようになります。
感想
今回利用したMircoProfileのAPIはJava EEのときからすでに存在したものばかりでしたが、個人的にあまりそちらを利用したことがなかったので、勉強になりました。
MicroProfileのその他のコンポーネントとかを見ていて思ったのですが、1から仕様を定義しているというよりは、すでに存在する有用そうな仕様をJavaの文脈で使えるようにまとめなおしたり、実装を行っているという印象を受けました。