[Cloud] Java EE based microservice on Oracle Cloud with Payara Micro

原文はこちら。
https://community.oracle.com/community/cloud_computing/oracle-cloud-developer-solutions/blog/2017/03/09/java-ee-based-microservice-on-application-container-cloud-with-payara-micro

(訳注)
  1. 元記事がやや古い(2017/03/09)ため、この当時はこのようなやり方を使った、とご理解ください。
  2. @khasunuma さんから、以下のコメントをいただきました。どうもありがとうございます!
    • 2017/06/14時点での最新のPayaraを使うと、Mavenプラグインは独自のものが用意されている
    • Mavenを使わず、コマンドラインからでもUber JARを作成できる
このエントリでは、Payara Microを使って、Java EEベースのマイクロサービスを構築する方法をご紹介します。
Payara Micro
http://www.payara.fish/payara_micro
Oracle Cloud (PaaS) Stackの以下のサービスを活用します。
  • Developer Cloud service
    コードのホスト(Git Repository)、および(他のOracle PaaSサービスとの統合による) Continuous Integration & Continuous Deployment機能の提供 
  • Application Container Cloud service
    Java EE マイクロサービスを実行するためのスケーラブルなaPaaS

Overview

Payara Micro?

Payara Microとは、マイクロサービススタイルのアプリケーションを構築するためのJava EEベースのソリューションです。少々説明しますと・・・
  • Java EE
    Payara MicroはJava EE Web Profile標準ならびにWeb Profileに含まれないその他の仕様(例えばBatch、Concurrency Utilitiesなど)もサポートします。
  • It’s a library
    これらの機能は全てカプセル化されたJARファイルとして利用できます。

Development model

Payara Microは複数のデプロイメントスタイルを選択できます。
  • WAR
    Java EEアプリケーションをWARファイルにパッケージし、以下のかたちでPayara Microとともに起動します。
    java –jar payara-micro-<version>.jar --deploy mystocks.war
  • Embedded mode
    ライブラリあので、Javaアプリケーション内にAPIを使って埋め込むことができます。
  • Uber JAR
    Payara MicroはMavenをサポートしているので、 exec pluginを使い、fat JARとしてPayara MicroライブラリとともにWARファイルをパッケージします。
    exec:javaプラグイン
    http://www.mojohaus.org/exec-maven-plugin/java-mojo.html
このエントリでは、fat JARの方法を使うことにします。

Benefits

潜在的なメリットは以下のようです。
  • Microservices friendly
    ライブラリとしてJava EEを使えるため、アプリケーション内で簡単に利用でき、柔軟な方法(WAR+JAR、もしくはただのfat JAR)でパッケージできます。また、PaaS、コンテナベースのプラットフォームといった複数の環境で実行できます。
  • Leverage Java EE skill set
    JAX-RS、JPA、EJB、CDIといったJava EE仕様の知識を活用できます。

About the sample application

JAX-RSやEJB、CDI、WebSocketといったAPIを使うシンプルなJava EEアプリケーションです。これはNYSEの株価の追跡に役立つアプリケーションです。
  • 利用者はNASDAQに上場している株価をシンプルなRESTインターフェースでチェックできます。
  • リアルタイムの株価追跡も可能ですが、この機能はOracle (ORCL) に対してのみ有効です。
ハイレベル図と背景をまとめておきます。
  • EJBスケジューラがORCLを定期的にチェックして株価を取得し、CDIイベントを発行します。(CDI Event Observerとしてマークされた)WebSocketコンポーネントがそのイベントを受け取り、接続済みのクライアントに対し最新の価格をアップデートします。
  • JAX-RS RESTエンドポイントを使ってオンデマンドで任意の企業の株価を取得します。これは(双方向、完全二重型のWebSocketインタラクションとは異なる)典型的なリクエスト-レスポンスベースのHTTPインタラクションです。 

Code

では、関連する部分のコードを見ていきましょう(簡単にするためにimport文は省略しています)。

RealTimeStockTicker.java
@ServerEndpoint("/rt/stocks") 
public class RealTimeStockTicker { 
 
 
    //stores Session (s) a.k.a connected clients 
    private static final List<Session> CLIENTS = new ArrayList<>(); 
 
    /**
     * Connection callback method. Stores connected client info
     *
     * @param s WebSocket session
     */ 
    @OnOpen 
    public void open(Session s) { 
        CLIENTS.add(s); 
        Logger.getLogger(RealTimeStockTicker.class.getName()).log(Level.INFO, "Client connected -- {0}", s.getId()); 
    } 
 
    /**
     * pushes stock prices asynchronously to ALL connected clients
     *
     * @param tickTock the stock price
     */ 
    public void broadcast(@Observes @StockDataEventQualifier String tickTock) { 
        Logger.getLogger(RealTimeStockTicker.class.getName()).log(Level.INFO, "Event for Price {0}", tickTock); 
        for (final Session s : CLIENTS) { 
            if (s != null && s.isOpen()) { 
                /**
                 * Asynchronous push
                 */ 
                s.getAsyncRemote().sendText(tickTock, new SendHandler() { 
                    @Override 
                    public void onResult(SendResult result) { 
                        if (result.isOK()) { 
                            Logger.getLogger(RealTimeStockTicker.class.getName()).log(Level.INFO, "Price sent to client {0}", s.getId()); 
                        } else { 
                            Logger.getLogger(RealTimeStockTicker.class.getName()).log(Level.SEVERE, "Could not send price update to client " + s.getId(), 
                                    result.getException()); 
                        } 
                    } 
                }); 
            } 
        } 
    } 
 
    /**
     * Disconnection callback. Removes client (Session object) from internal
     * data store
     *
     * @param s WebSocket session
     */ 
    @OnClose 
    public void close(Session s) { 
        CLIENTS.remove(s); 
        Logger.getLogger(RealTimeStockTicker.class.getName()).log(Level.INFO, "Client discconnected -- {0}", s.getId()); 
    } 
} 
StockDataEventQualifier.java
/**
 * Custom CDI qualifier to stamp CDI stock price CDI events
 * 
 */ 
@Qualifier 
@Retention(RUNTIME) 
@Target({METHOD, FIELD, PARAMETER, TYPE}) 
public @interface StockDataEventQualifier { 
} 
StockPriceScheduler.java
/** 
 * Periodically polls the Google Finance REST endpoint using the JAX-RS client 
 * API to pull stock prices and pushes them to connected WebSocket clients using 
 * CDI events 
 * 
 */  
@Singleton  
@Startup  
public class StockPriceScheduler {  
  
  
    @Resource  
    private TimerService ts;  
    private Timer timer;  
  
  
    /** 
     * Sets up the EJB timer (polling job) 
     */  
    @PostConstruct  
    public void init() {  
        /** 
         * fires 5 secs after creation 
         * interval = 5 secs 
         * non-persistent 
         * no-additional (custom) info 
         */  
        timer = ts.createIntervalTimer(5000, 5000, new TimerConfig(null, false)); //trigger every 5 seconds  
        Logger.getLogger(StockPriceScheduler.class.getName()).log(Level.INFO, "Timer initiated");  
    }  
  
  
    @Inject  
    @StockDataEventQualifier  
    private Event<String> msgEvent;  
  
  
    /** 
     * Implements the logic. Invoked by the container as per scheduled 
     * 
     * @param timer the EJB Timer object 
     */  
    @Timeout  
    public void timeout(Timer timer) {  
        Logger.getLogger(StockPriceScheduler.class.getName()).log(Level.INFO, "Timer fired at {0}", new Date());  
        /** 
         * Invoked asynchronously 
         */  
        Future<String> tickFuture = ClientBuilder.newClient().  
                target("https://www.google.com/finance/info?q=NASDAQ:ORCL").  
                request().buildGet().submit(String.class);  
  
  
        /** 
         * Extracting result immediately with a timeout (3 seconds) limit. This 
         * is a workaround since we cannot impose timeouts for synchronous 
         * invocations 
         */  
        String tick = null;  
        try {  
            tick = tickFuture.get(3, TimeUnit.SECONDS);  
        } catch (InterruptedException | ExecutionException | TimeoutException ex) {  
            Logger.getLogger(StockPriceScheduler.class.getName()).log(Level.INFO, "GET timed out. Next iteration due on - {0}", timer.getNextTimeout());  
            return;  
        }  
          
        if (tick != null) {  
            /** 
             * cleaning the JSON payload 
             */  
            tick = tick.replace("// [", "");  
            tick = tick.replace("]", "");  
  
  
            msgEvent.fire(StockDataParser.parse(tick));  
        }  
  
  
    }  
  
  
    /** 
     * purges the timer 
     */  
    @PreDestroy  
    public void close() {  
        timer.cancel();  
        Logger.getLogger(StockPriceScheduler.class.getName()).log(Level.INFO, "Application shutting down. Timer will be purged");  
    }  
}  
RESTConfig.java
/**
 * JAX-RS configuration class
 * 
 */ 
@ApplicationPath("api") 
public class RESTConfig extends Application{ 
     
}
StockDataParser.java
/**
 * A simple utility class which leverages the JSON Processing (JSON-P) API to filter the JSON 
 * payload obtained from the Google Finance REST endpoint and returns useful data in a custom format
 * 
 */ 
public class StockDataParser { 
     
    public static String parse(String data){ 
         
        JsonReader reader = Json.createReader(new StringReader(data)); 
                JsonObject priceJsonObj = reader.readObject(); 
                String name = priceJsonObj.getJsonString("t").getString(); 
                String price = priceJsonObj.getJsonString("l_cur").getString(); 
                String time = priceJsonObj.getJsonString("lt_dts").getString(); 
 
        return (String.format("Price for %s on %s = %s USD", name, time, price)); 
    } 
} 

A note on packaging

開発の観点から、前述の通り、通常のWARベースのJava EEアプリケーションをfat JARとしてPayara Microのコンテナとともにパッケージします。
コンテナにアプリケーションをデプロイするのではなく、アプリケーションとともにコンテナをパッケージングする点にご注意ください。

Payara MicroライブラリにJava EE APIは存在するので、Java EE APIはコンパイル時のみ必要です(scope = provided)
<dependency> 
    <groupId>javax</groupId> 
    <artifactId>javaee-api</artifactId> 
    <version>7.0</version> 
    <scope>provided</scope> 
</dependency> 
Mavenプラグインを使ってfat JARを生成します
<plugin> 
    <groupId>org.codehaus.mojo</groupId> 
    <artifactId>exec-maven-plugin</artifactId> 
    <version>1.5.0</version> 
    <dependencies> 
        <dependency> 
            <groupId>fish.payara.extras</groupId> 
            <artifactId>payara-micro</artifactId> 
            <version>4.1.1.164</version> 
        </dependency> 
    </dependencies> 
    <executions> 
        <execution> 
            <id>payara-uber-jar</id> 
            <phase>package</phase> 
            <goals> 
                <goal>java</goal> 
            </goals> 
            <configuration> 
                <mainClass>fish.payara.micro.PayaraMicro</mainClass> 
                <arguments> 
                    <argument>--deploy</argument> 
                    <argument>${basedir}/target/${project.build.finalName}.war</argument> 
                    <argument>--outputUberJar</argument>                                                   
                    <argument>${basedir}/target/${project.build.finalName}.jar</argument> 
                </arguments> 
                <includeProjectDependencies>false</includeProjectDependencies> 
                <includePluginDependencies>true</includePluginDependencies> 
                <executableDependency> 
                    <groupId>fish.payara.extras</groupId> 
                    <artifactId>payara-micro</artifactId> 
                </executableDependency> 
            </configuration> 
        </execution> 
    </executions> 
</plugin>

Setting up Continuous Integration & Deployment

以下の章でOracle Developer Cloud Serviceで実施した構成について取り扱います。

Project & code repository creation

以下のエントリのProject & code repository creationの章をご覧になるか、詳細についてはサービスのドキュメントをご覧ください。
Tracking JUnit test results in Developer Cloud service
https://community.oracle.com/community/cloud_computing/oracle-cloud-developer-solutions/blog/2016/10/05/junit-testing-using-oracle-developer-cloud
Oracle® Cloud Using Oracle Developer Cloud Service
Creating a Project
http://docs.oracle.com/cloud/latest/devcs_common/CSDCS/GUID-3317B279-A9C0-4566-A289-BD651A89D7B5.htm#GUID-7B30C8EC-6CDA-4F14-9791-8AE3BB3E8343

Configure source code in Git repository

ローカルシステムから先ほど作成したDeveloper CloudのGitリポジトリにプロジェクトをPushします。
Oracle® Cloud Using Oracle Developer Cloud Service
Pushing an Existing Local Git Repository to an Empty Oracle Developer Cloud Service Git Repository
http://docs.oracle.com/cloud/latest/devcs_common/CSDCS/GUID-B4C03296-8497-4356-8C74-2031D1FB96FC.htm#CSDCS-GUID-A33E83CE-845C-4393-8C93-936527033715
この操作はコマンドラインから実施しますが、事前にGitクライアントをローカルマシンにインストールしておく必要があります。Gitを使ってもいいですし、お好みのものを使うことができます。
Git
https://git-scm.com/downloads
cd <project_folder>  
git init   
git remote add origin <developer_cloud_git_repo>   
//e.g. https://john.doe@developer.us.oraclecloud.com/developer007-foodomain/s/developer007-foodomain-project_2009/scm/sample.git//john.doe@developer.us.oraclecloud.com/developer007-foodomain/s/developer007-foodomain-project_2009/scm/sample.git    
git add .   
git commit -m "first commit"   
git push -u origin master  //Please enter the password for your Oracle Developer Cloud account when prompted

Configure build

新しいジョブを作成しましょう。


JDKを選択します。


Continuous Integration (CI)

Git リポジトリを選択します。


ビルドトリガーを設定します。このビルドジョブは、(git pushなどの)Gitリポジトリの更新に応じて呼び出されます。


ビルドステップを追加します。
  • Maven : WARとfat JAR作成のためのビルドステップ
  • Execute Shell : 必要なデプロイメント・ディスクリプタ(Application Container Cloudではmanifest.jsonが必要です)とともにアプリケーションJARをパッケージングするステップ


以下はコマンドの例です。
zip -j accs-payara-micro.zip target/mystocks.jar manifest.json  
manifest.json の例です。
{ 
    "runtime": { 
        "majorVersion": "8" 
    }, 
    "command": "java -jar mystocks.jar --port $PORT --noCluster", 
    "release": { 
        "build": "23022017.1202", 
        "commit": "007", 
        "version": "0.0.1" 
    }, 
    "notes": "Java EE on ACC with Payara Micro" 
} 
デプロイ可能なZipファイルをアーカイブするためのビルド後のアクションを有効化します。


Execute Build

デプロイメントの構成前に、デプロイメントの構成が参照可能なアーティファクトを生成するためにビルドを呼び出す必要があります。


ビルドが完了した後、以下のことが可能になっています。
  • ビルドログの確認
  • アーカイブ済みのアーティファクトの確認
ログ


アーティファクト


Continuous Deployment (CD) to Application Container Cloud

新たにデプロイメント用のConfigurationを作成します。


  • 必要事項を入力し、Deployment targetを構成します
  • Application Container Cloudインスタンスを構成します
  • 最後の確認ページの自動デプロイメントオプションを構成します
最終的に以下のような構成になっているはずです。


確認画面


Application Container Cloudのアプリケーションを確認します。


Test the CI/CD flow

ちょっとコードを変更し、Developer Cloud ServiceのGitリポジトリにPushしてみましょう。すると以下を確認できるはずです。
  • 自動的にビルドが呼び出され、成功したら
  • 自動的にデプロイメントプロセスが呼び出され
  • つづいて新しいバージョンのアプリケーションをApplication Container Cloudに再デプロイする

Test the application

  • 特定の企業の株価をチェックする場合、GETでURLをつつきます。以下はその例です(このURLはサンプルで、実際にデプロイした環境にあわせる必要があります)。
    https://acc-p-m-mydomain.apaas.em1.oraclecloud.com/mystocks/api/stocks?ticker=AAPL
  • リアルタイムフィードをサブスクライブする場合、WebSocketクライアントを使って指定のエンドポイントURLにアクセスします。以下はその例です。
    wss://acc-p-m-mydomain.apaas.em1.oraclecloud.com/mystocks/rrt/stocks
Chomeブラウザにプラグインとしてインストールできるクライアントを使うことをお勧めします。例えば、Simple WebSocket Clientなどがよいでしょう。
Simple WebSocket Client
https://chrome.google.com/webstore/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=en

0 件のコメント:

コメントを投稿