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)にブラウザーアクセスしてみると以下の表示がされました。

f:id:yuya_hirooka:20200320105901p:plain
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でリクエストを送ると以下のように返ってきました。

$ 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;
    }
}

リクエスト用のPOJOName.javaはリソースメソッドの引数として、バインディングの際にパブリックな引数なしコンストラクタが必要なようです(なかったときにエラーで落ちた)。

レスポンス用のPOJOGreet.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を突っ込むだけでバインディングを行ってくれるようです。
今回は利用していないですが、MessageBodyReaderMessageBodyWriterを実装すれば、カスタムなバインディングも行なうことが可能になると思われます。

それでは、アプリを再ビルド、起動を行い以下のコマンドを叩きリクエストを送ってみましょう。

$ 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のAPIJava EEのときからすでに存在したものばかりでしたが、個人的にあまりそちらを利用したことがなかったので、勉強になりました。
MicroProfileのその他のコンポーネントとかを見ていて思ったのですが、1から仕様を定義しているというよりは、すでに存在する有用そうな仕様をJavaの文脈で使えるようにまとめなおしたり、実装を行っているという印象を受けました。

参考資料