タグ「GlassFish」が付けられているもの

Introduction to migrate GlassFish to Payara Server

Payara 4.1.1.171 will be released soon. (I think it's several days after.) All features are fixed and it seems to prepare for release now.

Well, recently some article was posted to Payara Blog. their are continuing series on alternatives for commercial Oracle GlassFish features, in other words, GlassFish 3.x w/commertial support to Payara Server migration.The series is following five articles at this time. See the detail if you're interested in.

  1. GlassFish to Payara Server Migration - Migrating away from the Load Balancer Configurator Plugin
  2. GlassFish to Payara Server Migration - Domain Administration Server Backup & Recovery
  3. GlassFish to Payara Server Migration: Hazelcast as a Coherence ActiveCache Alternative
  4. GlassFish to Payara Server Migration: Replacing the Monitoring Scripting Client
  5. GlassFish to Payara Server Migration - migrating away from the Oracle Access Manager integration

In general, almost commertial features of Oracle GlassFish 3.x can be replaced by Payara Server or tools in Operating Systems (e.g. cron), but some of them is difficult. (e.g. online backup of domain) Payara Team seems to plan to implement alternative features, so the situation may be better than now.

Payara is better and better each releases. At first, let's try to use Payara!

Payara の asadmin を日本語化する

この記事は Payara Advent Calendar 2016 の 2 日目です。昨日分は「そのプログラム、Payara なら動きますよ」です。

Payara には多国語化対応のパッケージが用意されています。それを利用すると GUI の管理コンソールが日本語表示になるなど、英語の苦手な日本人にとっては嬉しいものです。ダウンロードサイト上部のメニューから "All Downloads" をクリックすると (リンク先は http://www.payara.fish/all_downloads です) 多国語化対応パッケージを入手することができます。

多国語化対応パッケージで日本語化されるのは管理コンソールだけで、asadmin については日本語化されません。ただし、Payara を構成する JAR ファイルを手動で移動 (またはコピー) することで asadmin を強制的に日本語化する方法があります。以下に手順を示しますが、Payara の構成ファイルを操作するため動作しなくなる可能性もあります。あくまで自己責任でお願いします。

Step 1 : Payara Server を完全に停止した状態にする。

Step 2 : payara41/glassfish/modules に存在する以下の 3 ファイルを、payara41/glassfish/lib/asadmin に移動 (またはコピー) する。

  • cli-optional-l10n.jar
  • cluster-cli-l10n.jar
  • server-mgmt-l10n.jar

Step 3 : Payara Server を起動する。asadmin start-domain のメッセージが日本語化されているはず。

上記で移動 (またはコピー) した 3 ファイルは、本来 payara41/glassfish/lib/asadmin 以下に存在すべきファイルですが、Payara が GlassFish 4.0 から引きずっているバグ (公式にはバグと認められていない) により payara41/glassfish/modules 以下に配置されています。Payara を構成する JAR ファイルのうち末尾に "-l10n" が付加されているものは、付加されていないモジュールに対する多国語化パッチであり、モジュール本体と多国語化パッチが同じディレクトリ上に配置されることではじめてローカライズが行われる仕組みとなっています。上記 3 ファイルは asadmin の多国語化パッチですが、既定でモジュール本体とは異なるディレクトリにあるため、結果として asadmin のローカライズが行われない状態になっているのです。

このあたりのことは Payara (GlassFish) のモジュラー・システムが深く関わっています。参考までに、以前の発表資料 (20 分版) を添付します。


この件については、一度 Payara の国際化対応に関する Issue に関連づけて私が修正を試みたのですが、該当箇所のソースコードがスパゲティ状態でとうとう手が付けられませんでした (さすがにこの時ばかりは GlassFish 4.0 の開発時に該当箇所をいじった人間を恨みましたが...)。多分、今後もなかったことにされ続けることでしょう。

この記事は GlassFish Advent Calendar 2014 のために書きためておいた記事の一部です。従って、内容は 2014 年 12 月下旬時点のものとなりますのでご注意ください。


今回は、GlassFish のコンポーネントのバージョンの調べ方についてお話します。GlassFish 4.1 の開発中に新しい Promoted build がリリースされた際、筆者は何度かコンポーネントのアップデート状況をブログ記事にしています。今回はそのソースとなるもののご紹介です。

GlassFish Core のソースコードは、Java.net 内の SVN リポジトリにホストされています。筆者が確認しているのは、trunk または tags/{tag} 以下にある main/nucleus/pom.xml および main/appserver/pom.xml です。それぞれの pom.xml の properties 要素に "component-name.version" という名前の子要素があり、これが各コンポーネントのバージョン番号を保持しています。例えば、Jersey であれば nucleus/pom.xml の jersey.version、Tyrus であれば appserver/pom.xml の tyrus.version といった具合です。調べるには GlassFish の構成に関する知識が必要となりますので、主な Java EE コンポーネントがどの要素に対応しているのかを表にまとめてみました。

nucleus にある Java EE コンポーネント
要素名コンポーネント
stax-api.version StAX
servlet-api.version Servlet
grizzly.version Grizzly (サーブレット・コンテナ)
javax.el-api.version EL
javax.el.version EL
jax-rs-api.spec.version JAX-RS API 仕様
jax-rs-api.impl.version JAX-RS API 実装
jersey.version Jersey (JAX-RS 実装)
jackson.version Jackson (Jersey が用いる JSON-B 実装)
jettison.version Jettison (Jersey が用いる JSON-B 実装)
mail.version JavaMail API/実装
javax.annotation-api.version Common Annotations
appserver にある Java EE コンポーネント
要素名コンポーネント
jsp-api.version JSP
jsp-impl.version JSP 実装
jstl-api.version JSTL
jstl-impl.version JSTL 実装
javax.ejb-api.version EJB
javax.interceptor-api.version EJB Interceptor
javax.transaction-api.version JTA
jms-api.version JMS
mq.version OpenMQ (JMS 実装)
javax.faces-api.version JSF (例)2.2
mojarra.version Mojarra (JSF 実装)
javax-persistence-api.version JPA
eclipselink.version EclipseLink (JPA 実装)
javadb.version Java DB (埋め込み DB)
jaxws-api.version JAX-WS
webservices.version Metro (JAX-WS 実装)
jsr181-api.version JSR 181 API
jaxb-api.version JAXB
jaxb.version JAXB 実装
stax2-api.version StAX
javax.inject.version DI
cdi-api.version CDI
weld.version Weld (DI/CDI 実装)
websocket-api.version WebSocket API
tyrus.version Tyrus (WebSocket API 実装)
jsonp-api.version JSON-P
jsonp-ri.version JSON-P 実装
jsonp-jaxrs.version JSON-P = JAX-RS 連携
concurrent-api.version Concurrent Utilities for JEE
concurrent.version Concurrent Utilities for JEE 実装
javax.batch-api.version jBatch
com.ibm.jbatch-ri-spi.version jBatch RI (by IBM)
com.ibm.jbatch-runtime-all.version jBatch ランタイム (by IBM)

以上の情報だけを毎回ブラウザで確認するのは面倒なので、筆者は JavaFX と SVNKit を用いて簡単なツール https://github.com/btnrouge/gfversions を作り、それを調査に使っていました。

なお、今回使用したツールは、過去記事「GlassFishのコンポーネントのバージョンを一覧するツール」でも触れていました。こちらはツールの紹介だけにとどめていますが、今回の情報と合わせることで新しい展開があるかもしれません。

GlassFish の Server-Sent Events 対応

将来 Java EE に導入される新機能として Server-Sent Event (SSE) が提案されています。これはその名が示す通り、サーバー側からクライアントに対して通知メッセージを送信 (Push) するものです。文献によっては単に「サーバー Push」とだけ書かれている場合があります。SSE のベースは Comet などのサーバー Push 技術で、HTML5 と HTTP/2 の組み合わせにより最適な形で規格化されるようです。

サーバーからクライアントへメッセージを送信 (Push) する技術としては他に WebSocket があり、Java API では既に JSR 356 として規格化され Java EE 7 の一部にもなっています。SSE はプロトコル切り替えを用いる WebSocket と異なり HTTP プロトコルの範囲内でサーバー Push を実現する技術で、WebSocket より制約は多くなるものの (例: テキストデータしか送信できない)、単純なサーバーからクライアントへの通知のようなユースケースでは有効と考えられます。

GlassFish 4 では JAX-RS 2.0 実装である Jersey 2.x が独自拡張として SSE を実装しており、GlassFish 4.0.1 build 5 以降 (厳密には Jersey 2.8 以降) ではデフォルトで有効になっています。詳細は Jersey のドキュメントを参照して頂きたいのですが、Jersey の独自拡張を用いた SSE のサーバー側実装は、以下のようなコードになります。

@Path("events")
public class SseResource {
 
  @GET
  @Produces(SseFeature.SERVER_SENT_EVENTS)
  public EventOutput getServerSentEvents() {
    EventOutput eventOutput = new EventOutput();
    ...
    return eventOutput;
  }
}

もし、Java EE で SSE が採用され、かつ JAX-RS の機能として実現した場合には、Jersey の独自拡張に近い形になるでしょう。ただし、JSR 339/370 (JAX-RS 2.0/2.1) の Expert Group 内では Bill Burke (RESTEasy のリード) が Action-based MVC に続き導入に難色を示していることと、Servlet 4.0 で HTTP/2 に本格対応する構想があることから、JAX-RS に SSE 対応の仕様が追加されるのかは不透明な状況です。

今回の記事は、あくまで参考程度に。

Payara のアプリケーション・ロギング

Payara Blog に "TROUBLESHOOTING YOUR JAVA EE APPLICATIONS" という記事が投稿されています。投稿者は Payara Team (開発メンバー) の Mike Croft です。

この記事では、主に Payara の管理コンソール (GUI) を用いたトラブルシューティングに必要な管理コンソール上でのログ設定と、Payara を JMX 経由で VisualVM からモニタリングする方法について取り上げています。また、Payara 4.1.1.161 から搭載された新機能「Slow SQL Logging」と「Healthcheck Service」の簡単な使い方についても触れています。

英文ですが、GlassFish/Payara のログ設定と JMX モニタリングのイントロダクション的な資料ですので、参考にされると良いかと思います。

第十二回 #渋谷java にて発表をしてきました。昨年夏の GlassFish Internals #1 : Introduction to Nucleus に続く、一応シリーズものになります。今回は GlassFish のモジュラー・アーキテクチャがテーマです。テーマ故に難易度はかなり高めに設定していて、GlassFish の初級コミッタレベル、あるいは Payara のコミッタレベルの内容になっています。

内容自体は会場の圧倒的多数を置き去りにするハイレベルなものですが、本当のことを言うと glassfish4/glassfish/modules/*.jar と glassfish4/glassfish/lib/*.jar の違いが理解できれば発表内容の 6~7 割程度は理解したことになります。残り 2~3 割は分かっていなくても GlassFish エバンジェリストを名乗れます (そのくらい難しい内容です)。

まず、発表内容の訂正ですが、Felix Gogo は GlassFish 4.0 以降にはバンドルされており、さらに asadmin に統合されています。GlassFish 3.x では発表通り別配布となっています。GlassFish のドメイン起動中に asadmin osgi help と入力することでヘルプを参照できます。リンク先がソースコードのため適切とは言いがたいですが https://svn.java.net/svn/glassfish~svn/trunk/main-docs-l10n/man/nucleus/osgi-platforms/osgi-cli-remote/src/main/resources/org/glassfish/osgi/cli/remote/ja/osgi.1 に日本語のヘルプがあります。

発表内容だけでは GlassFish の起動直後は数個しか OSGi バンドルが Active になっていないような印象を受けますが、実際には約 300 のバンドルのうち 100 弱が Active になります (これは GlassFish 4.1 Full Platform の場合であり、バージョンやディストリビューション等により変動します)。その多くは Auto-start bundles にカテゴライズされている Java EE 実装で、他のバンドルと依存関係にあるため Active になっているものも少なくありません。むしろ約 300 あるバンドルのうち 3 分の 2 が Lazy Loading となることに注目してください。

通常、ここまで高度な内容を取り上げることはしないのですが、今回は実験的にコミッタ級の難易度で発表をしてみました。予想通り皆さんを置き去りにするような結果となりましたが (これも織り込み済み)、今回の発表は以下のことだけ覚えておいていただければ OK です。

  • glassfish4/glassfish/modules -- OSGi 管理のモジュールが配置されている
  • glassfish4/glassfish/lib -- 非 OSGi 管理のモジュールが配置されている

※modules と lib は構造上、クラスローダーが異なります。例えば JDBC ドライバは lib に配置しないと機能しません (JDBC ドライバは通常、非 OSGi のため)。

HK2 の使い方(後編)

この記事は GlassFish Advent Calendar 2014 の 21 日目です。昨日は「HK2 の使い方(前編)」です。本日は HK2 で利用可能な様々な DI について見てゆきます。

1. @Inject によるインジェクション

@Inject によるインジェクション(JSR 330)が可能です。

package jp.coppermine.samples.hk2.inject;

import javax.inject.Inject;

import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {
  
  @Inject
  private Hello hello;
  
  public String greet() {
    return hello.say();
  }
}

この DI を実現するには、ServiceLocator に対して以下のようなバインドを追加するか、

// Obtain the DynamicConfiguration object for binding a service to a contract.
DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
DynamicConfiguration config = dcs.createDynamicConfiguration();

// binding a service to a contract.
config.bind(BuilderHelper.link(Greeter.class).build());
config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).build());
config.commit();

あるいは META-INF/hk2-locator/default に以下を含めます。

[jp.coppermine.samples.hk2.inject.Greeter]S

[jp.coppermine.samples.hk2.inject.HelloImpl]S
contract={jp.coppermine.samples.hk2.inject.Hello}

同じ要領で Provider インジェクション(下記)も可能です。

package jp.coppermine.samples.hk2.provider;

import javax.inject.Inject;
import javax.inject.Provider;

import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {
  
  @Inject
  private Provider<hello> greeting;
  
  public String greet() {
    return greeting.get().say();
 }
}

さらに、コンストラクタ・インジェクションやメソッド・インジェクション(Setter インジェクション)もサポートされています。上記 Greeter クラスをコンストラクタ・インジェクションを用いて書き換えた例を以下に示します。

package jp.coppermine.samples.hk2.inject;

import javax.inject.Inject;

import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {

  private Hello hello;
  
  @Inject
  public Greeter(Hello hello) {
    this.hello = hello;
  }
  
  public String greet() {
    return hello.say();
  }
}

2. @Named を用いた Service のインジェクション

Contract に 対して複数の Service がある場合、@Named で修飾して(JSR 330)インジェクション対象の Service を選択することができます。また、IterableProvider を用いて対象となるすべての Service を取得することも可能です。

まず、Contract と Service の関連づけをすべて手動で行う場合のコードを示します。@Named を解決するため BuilderHelper#named(String) メソッドを用いていることに注目してください。

package jp.coppermine.samples.hk2.named;

import org.glassfish.hk2.api.DynamicConfiguration;
import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;
import org.glassfish.hk2.utilities.BuilderHelper;

public class ManualAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
    ServiceLocator locator = factory.create("default");
    
    // Obtain the DynamicConfiguration object for binding a service to a contract.
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    DynamicConfiguration config = dcs.createDynamicConfiguration();
    
    // binding a service to a contract.
    config.bind(BuilderHelper.link(Greeter.class).build());
    config.bind(BuilderHelper.link(Greeter.class).build());
    config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).build());
  
  config.bind(BuilderHelper.link(HelloJaImpl.class).to(Hello.class).named("Japanese").build());
  
  config.bind(BuilderHelper.link(HelloDeImpl.class).to(Hello.class).named("German").build());
  
  config.bind(BuilderHelper.link(HelloFrImpl.class).to(Hello.class).named("France").build());
  
  config.bind(BuilderHelper.link(HelloItImpl.class).to(Hello.class).named("Italian").build());
  
  
config.bind(BuilderHelper.link(HelloEsImpl.class).to(Hello.class).named("Spanish").build());
  
  config.bind(BuilderHelper.link(HelloBrPtImpl.class).to(Hello.class).named("Portuguese").build());
  
  config.bind(BuilderHelper.link(HelloZhChImpl.class).to(Hello.class).named("Chinese").build());
  
  config.bind(BuilderHelper.link(HelloZhTwImpl.class).to(Hello.class).named("TraditionalChinese").build());
  
  config.bind(BuilderHelper.link(HelloKoImpl.class).to(Hello.class).named("Korean").build());
    config.commit();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}

続いて、META-INF/hk2-locator/default を用いる例を示します。

package jp.coppermine.samples.hk2.named;

import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;

public class AutomaticAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}
[jp.coppermine.samples.hk2.named.Greeter]S

[jp.coppermine.samples.hk2.named.HelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}

[jp.coppermine.samples.hk2.named.HelloJaImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Japanese

[jp.coppermine.samples.hk2.named.HelloDeImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=German

[jp.coppermine.samples.hk2.named.HelloFrImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=French

[jp.coppermine.samples.hk2.named.HelloItImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Italian

[jp.coppermine.samples.hk2.named.HelloEsImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Spanish

[jp.coppermine.samples.hk2.named.HelloBrPtImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Portuguese

[jp.coppermine.samples.hk2.named.HelloZhChImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Chinese

[jp.coppermine.samples.hk2.named.HelloZhTwImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=TraditionalChinese

[jp.coppermine.samples.hk2.named.HelloKoImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.English}
name=Korean

META-INF/hk2-locator/default ファイル側には、Contract を指定する contract 属性に加え、@Named を特定する named 属性があることに注目してください。

最後に、Greeter のコードを見てみます。

package jp.coppermine.samples.hk2.named;

import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;

import org.glassfish.hk2.api.IterableProvider;
import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {
  
  @Inject
  private IterableProvider<Hello> hello greetings;
  
  @Named("Japanese")
  @Inject
  private Hello hello;
  
  public List<string> getAll() {
    List<string> list = new ArrayList<>();
    greetings.forEach(e -> list.add(e.say()));
    return list;
  }
  
  public String greet() {
    return hello.say();
  }
}

IterableProvider<Hello> とラップした形式でインジェクションを行うことで、対象となるすべての Service をイテレータで取得できるようになります。

3. Qualifier を用いた Service のインジェクション

@Named アノテーションの代わりに Qualifier を付加する(JSR 330)ことでも同じようなことが可能です。

まず、Contract と Service の関連づけをすべて手動で行う場合のコードを示します。Qualifier を解決するため BuilderHelper#qualifiedBy(Annotation) メソッドを用いていることに注目してください。また今回は、Annotation のインスタンスを取得するために Qualifiers インタフェースの static メソッドを作成しました。アノテーションのインスタンスは警告を無視してアノテーションの実装クラスを作成して取得する方法もありますが、今回のように既に付加されているアノテーションを Class#getAnnotations() メソッドで取得して絞り込む方法も使えます。

package jp.coppermine.samples.hk2.qualifier;

import org.glassfish.hk2.api.DynamicConfiguration;
import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;
import org.glassfish.hk2.utilities.BuilderHelper;

import jp.coppermine.samples.hk2.qualifier.qualifiers.Chinese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.France;
import jp.coppermine.samples.hk2.qualifier.qualifiers.German;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Italian;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Japanese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Korean;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Portuguese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Qualifiers;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Spanish;
import jp.coppermine.samples.hk2.qualifier.qualifiers.TraditionalChinese;

public class ManualAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
    ServiceLocator locator = factory.create("default");
    
    // Obtain the DynamicConfiguration object for binding a service to a contract.
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    DynamicConfiguration config = dcs.createDynamicConfiguration();
    
    // binding a service to a contract.
    config.bind(BuilderHelper.link(Greeter.class).build());
    config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).build());
    config.bind(BuilderHelper.link(HelloJaImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Japanese.class)).build());
    config.bind(BuilderHelper.link(HelloDeImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(German.class)).build());
    config.bind(BuilderHelper.link(HelloFrImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(France.class)).build());
    config.bind(BuilderHelper.link(HelloItImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Italian.class)).build());
    config.bind(BuilderHelper.link(HelloEsImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Spanish.class)).build());
    config.bind(BuilderHelper.link(HelloBrPtImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Portuguese.class)).build());
    config.bind(BuilderHelper.link(HelloZhChImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Chinese.class)).build());
    config.bind(BuilderHelper.link(HelloZhTwImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(TraditionalChinese.class)).build());
    config.bind(BuilderHelper.link(HelloKoImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Korean.class)).build());
    config.commit();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}

続いて、META-INF/hk2-locator/default を用いる例を示します。

package jp.coppermine.samples.hk2.qualifier;

import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;

public class AutomaticAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}
[jp.coppermine.samples.hk2.qualifier.Greeter]S

[jp.coppermine.samples.hk2.qualifier.HelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}

[jp.coppermine.samples.hk2.qualifier.HelloJaImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Japanese}

[jp.coppermine.samples.hk2.qualifier.HelloDeImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.German}

[jp.coppermine.samples.hk2.qualifier.HelloFrImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.French}

[jp.coppermine.samples.hk2.qualifier.HelloItImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Italian}

[jp.coppermine.samples.hk2.qualifier.HelloEsImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Spanish}

[jp.coppermine.samples.hk2.qualifier.HelloPtImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Portuguese}

[jp.coppermine.samples.hk2.qualifier.HelloZhChImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Chinese}

[jp.coppermine.samples.hk2.qualifier.HelloZhTwImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Chinese}

[jp.coppermine.samples.hk2.qualifier.HelloKoImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Korean}

@Named を解決したときには named 属性を追加しましたが、Qualifier を解決する場合には qualifier 属性を追加し、Qualifier の完全クラス名を Contract と同様の形式で記述します。

その他のクラスについては、@Named の場合とほぼ同じ(@Named を Qualifier に置き換えただけ)ですので省略します。

4. まとめ

HK2 は GlassFish の基盤であると同時に、JSR 330 ベースの DI も提供しています。事前に Inhabitants Generator を用いてクラスの依存関係をファイル(META-INF/hk2-locator/service-locator-name)に出力する必要はありますが、その分だけ動的にクラスを操作する量が少なく、他のライブラリとのコンフリクトを起こしにくいとも言えます。Java EE においては、DI は CDI (Weld)の一機能として利用することが大半だと思われますが、純粋な DI (JSR 330)の選択肢として、GlassFish や Jersey には HK2 があることを覚えておくと、いつか役に立つ日が来るかもしれません。

前回と今回のサンプルは https://github.com/khasunuma/hk2-sample にあります。

明日は @kikutaro_ さんです。

HK2 の使い方(前編)

この記事は GlassFish Advent Calendar 2014 の 20 日目です。昨日は「GlassFish 4.1 のロギング」です。

9 日目の記事「GlassFish 4.1でJAX-RS=CDI連携の単体テストを行う方法」にて、HK2 による DI を併用した Jersey Test Framework の使い方についてご紹介しました。今回は本日と明日の 2 回に分けて、HK2 そのものについて見てゆきたいと思います。

1. HK2 の歴史

HK2 はもともと、JSR 277: Java Module System をベースとした簡易なモジュールシステムとして開発がスタートしました。JSR 277 は端的に言うと、JAR の代わりに Java Application Module(JAM)という新しいモジュールと JAM を支援する仕組みを導入しようという試みでした。JAM には JAR が持っていなかったバージョンや依存関係のメタ情報等を保持することになっていました。また、これとは別に JSR 294: Improved Modularity Support in the Java Programming Language というモジュールの動的管理を行う仕組みも提案され、JSR 277 と JSR 294 の組み合わせも想定されていましたが、JSR 291: Dynamic Component Support for Java SE という、当時台頭し始めていた OSGi を Java SE 上で扱えるようにする別規格の提案によって、JSR 277 および JSR 294 は「保留」となりました。もっとも、これらを保留に追い込んだ JSR 291 自身も本当に JSR として規格化すべきだったのか疑問視する声があり(実際に JSR 提出から承認まで終始一貫して当時の Sun に反対され続けた異例の JSR でした)、さらにはリファレンス実装が一般公開されていないなどの問題がありました。

こうして Java 標準のモジュール管理システムの整備が遅れている間に、Groovy を DSL として依存関係を解決する Gradle が人気を集めるようになり、Equinox や Apache Felix など OSGi ランタイム(これらは JSR 291 の実装でもあります)による動的モジュール管理が普及しました。そういった背景の中で、Java SE 7 の仕様として「Project Jigsaw」が提案されました。Project Jigsaw は提案当時ですら未決事項が多く、Java SE 7 と Java SE 8 の 2 度に渡り先送りされ、Java SE 9 への導入を目標として JSR 376: Java Platform Module System という新しい JSR で再提案されました。JSR 376 は過去に保留となっていた JSR 277 および JSR 294 の仕様も取り込んだ形となる見込みで、OpenJDK 9 では従来の rt.jar に相当する部分が既に JSR 376 ベースのモジュールに変更されています。

HK2 は Java のモジュール管理システムに関する仕様が二転三転する中で、GlassFish の OSGi ランタイム上で動作するライブラリとしての位置を確立しました。GlassFish のモジュールは最低限 OSGi バンドルでなければなりませんが、主要モジュールはさらに HK2 を共通基盤として構築されています。

2. HK2 とは?

前章では HK2 の歴史について振り返ってみましたが、ここでは現在の HK2 の仕様に照らし合わせて、HK2 がどのような役割を示すものかをまとめてみます。

2.1. モジュール管理システムとしての役割

HK2 が当初から持っていた役割です。モジュールのロード、依存性管理、バージョン管理などを引き受けます。GlassFish v3 以降においては、モジュールのロードに OSGi を使うことができます(Java SE API の ServiceLoader を利用したプラグインのロードに対応します)。

HK2 草創期によく使われた簡単なサンプルがあります。GlassFish v3 を以下のたった 1 コマンドで実行できる、というものです。

C:\Users\hasunuma>java -jar C:\glassfish4\glassfish\modules\glassfish.jar

このコマンドには、クラスパスの指定がありません。glassfish.jar は OSGi bundle としてビルドされ(glassfish.jar のソースコードを見るとはっきりします)、.jar の中で依存関係を解決しているため、クラスパスを明示する必要がないのです。glassfish.jar の依存モジュールもまた OSGi bundle であればそちらでも依存関係を解決しているため、カスケード式に依存関係が解決され、最終的にクラスパスの指定なしで GlassFish を起動することができるのです。

さらに HK2 では、クラスローダーを跨いだ依存関係の解決もできます。以下をご覧ください。

runtime-network-of-class-loaders.png

これは川口耕介さんがかつて GlassFish v3 の新しいアーキテクチャを解説する際に使用した(と思われる)資料から引用したものです。

モジュール管理システムとしての HK2 については、後日改めてご紹介したいと考えています。

2.2. DI コンテナとしての役割

HK2 は JSR 330 に準拠した DI コンテナとして動作させることができます。JSR 330 の RI である Google Guice と密に連携させることも可能です。

Java SE 環境に限って言えば、HK2 と Guice ではエントリ・ポイントの記述が大きく異なります。これは Weld SE やその他 JSR 330 実装についても言えることです。ただし、一度 DI コンテナが稼働すれば、@Inject アノテーションで DI を実現できるのはどの実装でも変わりません。

DI コンテナとしての HK2 については、この後の章と次回でご紹介します。

3. HK2 による DI について

3.1. Service と Contract

HK2 における DI でまず押さえておきたいのは Service と Contract です。いずれも POJO ですが、クラスレベル・アノテーションが @Service か @Contract かの違いがあります。

Service は実際に処理を行う具象クラスです。多くの場合、Contract となるインタフェースの実装または抽象クラスのサブクラスとして実装しますが、必ずしも Contract を必要としているわけではありません。このあたりは、インタフェースなしで EJB がインジェクトできるのと同じ感覚です。Service には @Service アノテーションを付加します。

Contract はインジェクション・ポイントとなるインタフェースまたは抽象クラスです。これらの実装またはサブクラスがインジェクト可能な Service となります。

@Contract
public interface Foo {
  // ...
}

@Service
public interface FooImpl implements Foo {
  // ...
}

@Service
public class Bar {
  // ...
}

以上に簡単な例を挙げました。ここには Service である FooImpl と Bar があります。FooImpl は Contract である Foo の実装で、インジェクション・ポイント Foo または FooImpl にインジェクトされます。Bar は Contract がない Service で、Bar 自身がインジェクション・ポイントとなります。

HK2 では、Service を Contract にマッピングする作業が必要です。この方法には、コードに記述する方法と、設定ファイルで指定する方法があります。設定ファイルについては HK2 に付属するユーティリティで自動生成します。ただし、今回はクラス数が少ない上に、設定ファイルをブラックボックス化して説明したくないため、設定ファイルを手動で記述する方法をご紹介します。

3.2. 最も簡単な例

まず初めに、最も簡単な例を示します。この例は Contract の Message インタフェースと Service の MessageImpl クラスを用意し、Message ← MessageImpl の DI を実現します。このくらいの規模ではエントリ・ポイントのオーバーヘッドの比率が非常に大きくなりますが、HK2 の DI を利用するために必要な手順を学習する上で必要なことですので、頭の片隅に置いておいてください。

package jp.coppermine.samples.hk2.instance;

import org.jvnet.hk2.annotations.Contract;

@Contract
public interface Message {
  String get();
}
package jp.coppermine.samples.hk2.instance;

import org.jvnet.hk2.annotations.Service;

@Service
public class MessageImpl implements Message {

  @Override
  public String get() {
    return "Hello, world";
  }

}

まず、ServiceLocator を取得する必要があります。ServiceLocator は登録されている Service の中から適切なものをインスタンス化する役目を持ちます。ServiceLocator は ServiceLocatorFactory から取得することができます。

ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
ServiceLocator locator = factory.create("default");

ServiceLocator には生成時に JVM で一意となるような任意の名前を付けます。ここでは "default" という名前の ServiceLocator を生成しています。

次に、ServiceLocator に対して Service を登録する必要があります。コードで Service を登録するには DynamicConfiguration を使用します。DynamicConfiguration は DynamicConfigurationService から取得します。さらに DynamicConfigurationService は ServiceLocator から取得します。すべての ServiceLocator には ServiceLocator 自身、DynamicConfigurationService、デフォルトの InjectionResolver 実装(JSR 330 リゾルバ、@Inject の解決に用いる)の 3 つの Service が登録されています。

DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
DynamicConfiguration config = dcs.createDynamicConfiguration();

こうして取得した DynamicConfiguration を用いて Service と Contract の関連づけを行っていきます。

config.bind(BuilderHelper.link(MessageImpl.class).to(Message.class).build());
config.commit();

最後の commit メソッド実行で、すべての関連づけが完了し、有効となります。

以上をまとめます。

package jp.coppermine.samples.hk2.instance;

import org.glassfish.hk2.api.DynamicConfiguration;
import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;
import org.glassfish.hk2.utilities.BuilderHelper;

public class ManualAppMain {

  public static void main(String[] args) throws Exception {
    // Obtain a ServiceLocator object
    ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
    ServiceLocator locator = factory.create("default");
    
    // Obtain the DynamicConfiguration object for binding a service to a contract.
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    DynamicConfiguration config = dcs.createDynamicConfiguration();
    
    // binding a service to a contract.
    config.bind(BuilderHelper.link(MessageImpl.class).to(Message.class).build());
    config.commit();
    
    // Obtain the value from a Message object and output it
    Message message = locator.getService(Message.class);
    System.out.println(message.get());
  }

}

今回は 1 組だけの関連づけでしたが、@Inject で DI を行うとこのような Service と Contract の対応がいくつも発生し、そのすべてに対して bind メソッドで関連づけを行わなければなりません。HK2 の DI は、Service と Contract の対応付けを静的に記述する必要があり、そこが Weld 等との違いとなります。HK2 には Inhabitants Generator というツールが用意されており、クラスパスを走査して Service と Contract の対応を洗い出し、関連づけのファイルを自動生成します。このファイルは META-INF/hk2-locator/service-locator-mame に配置することで機能します(Inhabitants Generator ではそこまでを自動化することもできます)。今回は Inhabitants Generator の出力形式を知るために、META-INF/hk2-locator/default ファイルをエディタで作成してみましょう(繰り返しになりますが、このファイルは本来、自動生成するものです)。

[jp.coppermine.samples.hk2.instance.MessageImpl]S
contract={jp.coppermine.samples.hk2.instance.Message}

最初の行に Service クラスの完全名を [ ]S で囲んで記述します。行末の 'S' は忘れないようにしてください。2 行目以降にはその Service に結びつく属性が続きます。上記の例では Contract の完全名を記述しています。Contract へのインジェクションを行う場合は contract 属性は必須です。

では、Service と Contract の関連づけを META-INF/hk2-locator/default で行うコードを示します。<

package jp.coppermine.samples.hk2.instance;

import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;

public class PopulateAppMain {

  public static void main(String[] args) throws Exception {
    // Obtain a ServiceLocator object
    ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
    ServiceLocator locator = factory.create("default");
    
    // Obtain the DynamicConfiguration object for binding a service to a contract.
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    dcs.getPopulator().populate();
    
    // Obtain the value from a Message object and output it
    Message message = locator.getService(Message.class);
    System.out.println(message.get());
  }

}

先ほどのコードとの相違は、DynamicConfiguration の bind メソッドと commit メソッドを呼び出していたところが、DynamicConfigurationService の getPopulator メソッドと、それで取得した Populator クラスの populate メソッドの呼び出しに置き換わったことです。先ほどのコードでは Service が増えると bind メソッドの呼び出しが大量に発生しますが、こちらのコードでは bind に相当する情報がすべてファイルにありますので、コードは増加しません。

以上の処理は HK2 のエントリ・ポイントとして頻繁に発生しうるので、便利な書き方が用意されています。

package jp.coppermine.samples.hk2.instance;

import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;

public class AutomaticAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
    
    // Obtain the value from a Message object and output it
    Message message = locator.getService(Message.class);
    System.out.println(message.get());
  }

}

この方法では、ServiceLocator の取得から Service と Contract の関連づけまですべてを 1 行で行えます。また、この方法では ServiceLocator 名を指定していませんが、その場合は "default" という名前が使用されます。前のコードの ServiceLocator 名を "default" としていたのは、筆者がこの命名規約を知っていたからです。

4. まとめ

今回は HK2 の歴史と概要、そしてプログラミングを行う上で知っておきたいエントリ・ポイントの書き方についてお話しました。明日は HK2 が提供する様々な DI についてご紹介します。

今回のサンプルは https://github.com/khasunuma/hk2-sample にあります。

GlassFish 4.1 のロギング

この記事は GlassFish Advent Calendar 2014、19 日目です。昨日は「GlassFishの更新ツール」です。今回は GlassFish 4.1 のログ出力についてお話しします。

GlassFish 4.1 は JDK 標準の java.util.logging を使用してサーバ・ログをファイル(デフォルトでは domain-dir/logs/server.log)にログを出力します。サーバ・ログの内容は syslog (Linux または Solaris)やコンソールに出力することも可能です。

ログに関する各種設定は構成ごとに行います。スタンドアロン構成の場合は server-config、クラスタ構成の場合は cluster-name-config に行います。設定の変更は管理コンソールまたは asadmin で行いますが、ロガーが java.util.logging であるため logging.properties を直接編集することも可能です(推奨はしません)。なお、logging.properties は domain-dir/config/logging.properties または domain-dir/config/cluster-name-config/logging.properties にあります。

ロガーの設定項目については、管理コンソールのオンラインヘルプの説明がよくまとまっているのでこれを参照するとよいでしょう(英語が苦手という方は、GlassFish 4.1 の日本語化を行ってください)。GlassFish 4.0 で追加された項目が少しわかりにくいので解説します。

設定項目に Console Logging Format と Log File Logging Format があり、いずれも選択肢が ULF と ODF の 2 つで、Console のデフォルトが ULF、Log File のデフォルトが ODF になっているはずです。GlassFish 4.0 以降では、server.log の内容の出力先によってフォーマットを変えることが可能で、かつデフォルトでは変えています。

ULF は UniformLogFormatter によるフォーマットを意味します。 GlassFish 3.x までの標準ログフォーマットです。ULF の例を以下に示します。

[#|2014-12-11T09:33:40.460+0900|INFO|glassfish 4.1|javax.enterprise.logging|_ThreadID=15;_ThreadName=RunLevelControllerThread-1418258020378;_TimeMillis=1418258020460;_LevelValue=800;_MessageID=NCLS-LOGGING-00009;|
Running GlassFish Version: GlassFish Server Open Source Edition 4.1 (build 13)|#]

ODL は Oracle Diagnostics Logging によるフォーマットを意味します。Oracle Fusion Middleware の標準ログフォーマットで、GlassFish 4.0 以降は server.log ファイルへの出力フォーマットのデフォルトとして選択されています。ODL の例を以下に示します。

[2014-12-11T09:31:26.890+0900] [glassfish 4.1] [INFO] [NCLS-LOGGING-00009] [javax.enterprise.logging] [tid: _ThreadID=15 _ThreadName=RunLevelControllerThread-1418257886808] [timeMillis: 1418257886890] [levelValue: 800] [[
Running GlassFish Version: GlassFish Server Open Source Edition 4.1 (build 13)]]

Console Logging Formatter は、サーバ・ログをコンソールに出力する時のフォーマッタです。コンソールへの出力を有効にした場合のコンソール出力、IDE から起動した時のログ・コンソール出力などに適用されます(java -jar glassfish.jar で GlassFish Core を直接起動した場合もこちらになります)。

Log File Logging Formatter は、サーバ・ログをファイル(server.log)に出力する場合のフォーマッタです。Embedded Server として起動した場合のログにもこちらが適用されます。

GlassFish は概ねモジュール単位でロガーを管理しており、モジュール単位でログレベルを調整できるように配慮しています。GlassFish が管理するロガーは追加または削除することが可能です。ロガーとログレベルは構成により差があるため、管理コンソールまたは asadmin で確認してください(asadmin ではロガーの一覧に list-loggers、設定されているログレベル一覧に list-log-levels サブコマンドをそれぞれ使用します)。

アプリケーションでログを出力する場合は、Logger#getLogger(String) で最も相応しいロガーを取得(なければ作成)してそれを利用するようにします。なお、GlassFish ではルートロガーのログレベルがデフォルトで INFO になっているため、個別のロガーでより細かいログレベルを指定しても INFO レベルのログまでしか出力されません。Logger#setLevel(Lebel) で個別のログレベルを制御したい場合には、個別のログレベルが必要最小限に抑えられていることを確認したうえでルートロガーまたは上位ロガー javax のレベルを ALL に設定します。なお、ルートロガーのレベルを変更する場合は logging.properties を直接編集する必要があります。

GlassFish 上でアプリケーションを動作させると、そのアプリケーションが System.out で出力した内容もサーバ・ログに出力されます。GlassFish のログ出力仕様では Java EE アプリケーションの System.out と System.err をルートロガーにリダイレクトするようになっています。つまり、java.util.logging の使い方がわからなくても、System.out.println を使えばとりあえずサーバ・ログへの出力はできます(すべてルートロガー経由で出力されるためあまりお勧めはできませんが)。

最後に、GlassFish 4.1 の開発終盤に JBoss Logging がバンドルされたことに触れておきましょう。GlassFish 4.0.1 build 6 より CDI 1.2 に対応した Weld 2.2.1.Final がバンドルされ(最終的には Weld 2.2.2.Final に変更されました)、その依存モジュールとして JBoss Logging 3.1.3 GA も追加されました。あくまで Weld 内部のロギング用途であり、アプリケーションでの利用を意図したものではないと考えられます。

明日は GlassFish の基盤を構成する HK2 についてお話する予定です。

GlassFish 4.1 の更新ツール

この記事は GlassFish Advent Calendar 2014 の 18 日目です。昨日は「パスワード・エイリアスの使い方」です。

GlassFish 4.1 にはモジュールを追加・更新・削除するための更新ツールが 3 種類用意されています。管理コンソール、updatetool ユーティリティー、pkg コマンドの 3 つがそれで、GlassFish v3 Prelude の頃、現在の形に落ち着きました。今回は 3 つの更新ツールを取り上げてみます。

1. 管理コンソール

おなじみの管理コンソールでモジュールの追加、インストール済みモジュールと更新の有無の確認が可能です。

1.1. 使い方

起動直後のダッシュボードの「Update Center」、または左ペインの「更新ツール」から行けます。

「使用可能なアドオン」タブでは、現在 GlassFish にインストールされていないモジュールの一覧が表示され、必要なものを選択してインストールすることが可能です。

available-addon.png

「使用可能な更新」タブでは、インストール済みのモジュールでアップデート可能なもののリストが表示されます。実際のアップデートは updatetool ユーティリティーまたは pkg コマンドで行ってください。

available-updates.png

「インストール済みコンポーネント」タブでは、アップデート可否に関わらず現在 GlassFish にインストールされているモジュールの一覧が表示されます。このタブではあくまでモジュール一覧の確認だけであり、実際の操作(更新または削除)は updatetool ユーティリティーまたは pkg コマンドで行います。

installed-components.png

「構成」タブは他とは異色で、更新ツールの動作全般に関わる設定を行います。

configuration.png

1.2. 注意点

管理コンソールではモジュールの追加は可能ですが更新と削除はできないため、モジュールの更新と削除には updatetool ユーティリティーまたは pkg コマンドを使用する必要があります。また、他の 2 つに比べて非常に処理が重たいので注意が必要です。

2. updatetool ユーティリティー

GUI の更新ツールで、管理コンソールとは独立して動作します。初回起動に先立ち必須コンポーネント(Python ランタイム等)をダウンロードする必要があります。

2.1. updatetool の使い方

as-install-parent/bin/updatetool を起動します。

モジュールを追加する場合は左ペインの「入手可能なアドオン」を選択し、右ペイン上段から追加したいモジュールを選択しツールバーの「インストール」をクリックします。その後にライセンス確認などのウィザードが始まりますので、その指示に従ってください。

updatetool-1.png

モジュールの更新は、左ペインで「入手可能なアップデート」を選択するほかはモジュールの追加と同様の手順です。

モジュールの削除は、左ペインで「インストール済みのコンポーネント」を選択して右ペイン上段から削除したいモジュールを選択しツールバーの「削除」をクリックします。

updatetool-2.png

2.2. 注意点

updatetool ユーティリティーは GlassFish 稼働中でも使用できますが、モジュールの更新と削除については GlassFish 停止中でないとできない場合が多いです。

3. pkg コマンド

コマンドラインの更新ツールで、IPS(Image Packaging System)と呼ばれるパッケージ管理システムの CLI クライアントです。pkg コマンド自体は Solaris 11 以降に搭載されているものと基本的には同じですが、扱うパッケージの種類が異なります。3 種類の更新ツールの中では最も軽快に動作します。GUI の更新ツール同様、初回起動に先立ち必須コンポーネント(Python ランタイム等)をダウンロードする必要があります。

実を言うと、updatetool ユーティリティーは IPS の GUI クライアントに当たります。また、管理コンソールには IPS の一部機能にアクセスするためのコードが含まれています。

3.1. 使い方

この節は過去記事「GlassFish Internal」の一部を再編集したものです。当時の最新であった GlassFish 3.1.2.2 を例に話を進めていますが、内容は GlassFish 4.0 以降でも共通です。

pkg コマンドは、繰り返しになりますが IPS(Image Packaging System)の CLI クライアントです。IPS は OpenSolaris と Solaris 11 以降で採用されたパッケージ管理システムで、GlassFish v3 以降でも標準で採用されています。IPS は OpenSolaris が大々的に採用したため OpenSolaris プロジェクトで開発されたものと思われがちですが、実際には OpenMQ でプロトタイプが開発され、それが OpenSolaris に取り込まれ、発展したものです。なお、Linux にも同名のパッケージ管理ツールが存在しますが、それとは別物なので注意が必要です。

pkg コマンドは as-install-parent/bin/pkg にあります。ただし、先述の通り GlassFish 出荷時は pkg コマンドの一部がインストールされていません。そのため pkg コマンドの初回起動時に不足するモジュールのインストールを案内されるためそれに従います。

これはライセンスの関係で Python を GlassFish にバンドルできないためです(再配布条件によるもので、ユーザーが一緒に使う場合は問題ない)。同様のケースとして NetBeans とJUnit、Eclipse と SVNKit などがあります。

pkg コマンドの準備が整ったら、まず、インストールされているパッケージの一覧を表示してみましょう。コマンドラインから pkg list とだけ入力します。

C:\glassfish3\bin>pkg list
NAME (PUBLISHER)                              VERSION         STATE      UFIX
felix                                         4.0.2-0         installed  ----
glassfish-cluster                             3.1.2.2-5       installed  ----
glassfish-cluster-l10n                        3.1.2-18        installed  ----
glassfish-common                              3.1.2.2-5       installed  ----
glassfish-common-l10n                         3.1.2-18        installed  ----
glassfish-corba-base                          3.1.0-32        installed  ----
glassfish-ejb-lite                            3.1.2.2-5       installed  ----
glassfish-ejb-lite-l10n                       3.1.2-18        installed  ----
glassfish-grizzly                             1.9.50-1        installed  ----
glassfish-grizzly-full                        1.9.50-1        installed  ----
glassfish-gui                                 3.1.2.2-5       installed  ----
glassfish-gui-l10n                            3.1.2-18        installed  ----
glassfish-ha                                  3.1.2.2-5       installed  ----
glassfish-hk2                                 3.1.2.2-5       installed  ----
glassfish-javahelp                            2.0.2-1         installed  ----
glassfish-jca                                 3.1.2.2-5       installed  ----
glassfish-jca-l10n                            3.1.2-18        installed  ----
glassfish-jcdi                                3.1.2.2-5       installed  ----
glassfish-jdbc                                3.1.2.2-5       installed  ----
glassfish-jdbc-l10n                           3.1.2-18        installed  ----
glassfish-jmx                                 3.1.2.2-5       installed  ----
glassfish-jpa                                 3.1.2.2-5       installed  ----
glassfish-jsf                                 2.1.6-4         installed  ----
glassfish-jta                                 3.1.2.2-5       installed  ----
glassfish-jts                                 3.1.2.2-5       installed  ----
glassfish-jts-l10n                            3.1.2-18        installed  ----
glassfish-management                          3.1.2.2-5       installed  ----
glassfish-nucleus                             3.1.2.2-5       installed  ----
glassfish-nucleus-l10n                        3.1.2-18        installed  ----
glassfish-osgi                                4.0.2-0         installed  ----
glassfish-osgi-http                           3.1.2.2-5       installed  ----
glassfish-registration                        3.1.2.2-5       installed  ----
glassfish-upgrade                             3.1.2.2-5       installed  ----
glassfish-upgrade-l10n                        3.1.2-18        installed  ----
glassfish-web                                 3.1.2.2-5       installed  ----
glassfish-web-incorporation                   3.1.2.2-5       installed  ----
glassfish-web-l10n                            3.1.2-18        installed  ----
glassfish-web-profile                         3.1.2.2-5       installed  ----
javadb-client                                 10.8.1.2        installed  ----
javadb-common                                 10.8.1.2        installed  ----
javadb-core                                   10.8.1.2        installed  ----
jersey                                        1.11.1-1.0      installed  ----
pkg                                           1.122.2-56.2852 installed  ----
pkg-java                                      1.122-56.2852   installed  ----
pkg-toolkit-incorporation                     2.3.5-56.2852   installed  ----
python2.4-minimal                             2.4.6.0-56.2852 installed  ----
shoal                                         1.6.21-0        installed  ----

これは GlassFish 3.1.2.2 Web Profile のほぼ標準状態(pkg のために python2.4-minimal などが追加されている)です。

IPS は pkg コマンドなどのクライアントと、「リポジトリ」と呼ばれるサーバーで構成されます。「リポジトリ」にはシステムにインストール可能なパッケージがホスティングされており、pkg コマンドでパッケージを検索→ダウンロード&インストールという流れになります。通常、「リポジトリ」は配布元の公開サーバー上に存在し、HTTP 経由でパッケージをダウンロードすることになります。

IPS のパッケージには依存関係が定義されており、必要なパッケージも同時にダウンロード&インストールすることができます。また、不要なパッケージを削除するときも依存関係を参照し、他のパッケージから呼び出されるものは削除できないような仕組みになっています。

IPS では「リポジトリ」によってホスティングされているパッケージの品揃えが左右されます。ホスティングされているパッケージが同一の「リポジトリ」も存在しますが(ミラーサイト)、多くの場合は「リポジトリ」ごとに異なるため、必要に応じて「リポジトリ」を切り替えて使用します。例えば、GlassFish 3.1.2.2 のコミュニティ版では以下の「リポジトリ」を使用し、いずれかのリポジトリに存在するパッケージをダウンロード&インストールするように構成されています。

  • contrib.glassfish.org (http://pkg.glassfish.org/v3/contrib/)
  • contrib.glassfish.oracle.com (http://pkg.oracle.com/glassfish/v3/contrib/)
  • stable.glassfish.org (http://pkg.glassfish.org/v3/stable/) ←主なパッケージはここ

なお、GlassFish 4.1 では、以下の 2 つの「リポジトリ」が設定されています。GlassFish 4.1 ではこちらの「リポジトリ」が使用されます。

  • release.glassfish.org (http://pkg.glassfish.org/4/release/)
  • release.native.glassfish.org (http://pkg.glassfish.org/4/native/release/)

未インストールのパッケージも含めた一覧は pkg list -a で確認できます。

C:\glassfish3\bin>pkg list -a
NAME (PUBLISHER)                              VERSION         STATE      UFIX
ant (contrib.glassfish.org)                   1.7.1-0.6       known      ----
felix                                         4.0.2-0         installed  ----
glassfish-ant-tasks (contrib.glassfish.org)   3.1.2.2-5       known      ----
glassfish-appclient                           3.1.2.2-5       known      ----
glassfish-appclient-l10n                      3.1.2-18        known      ----
glassfish-cluster                             3.1.2.2-5       installed  ----
glassfish-cluster-l10n                        3.1.2-18        installed  ----
glassfish-cluster-util (contrib.glassfish.org) 1.0-0.0         known      ----
glassfish-cmp                                 3.1.2.2-5       known      ----
glassfish-cmp-l10n                            3.1.2-18        known      ----
glassfish-common                              3.1.2.2-5       installed  ----
glassfish-common-full                         3.1.2.2-5       known      ----
glassfish-common-full-l10n                    3.1.2-18        known      ----
glassfish-common-l10n                         3.1.2-18        installed  ----
glassfish-corba                               3.1.0-32        known      ----
glassfish-corba-base                          3.1.0-32        installed  ----
glassfish-dcom (contrib.glassfish.org)        1.0.0-0.2       known      ----
glassfish-ejb                                 3.1.2.2-5       known      ----
glassfish-ejb-l10n                            3.1.2-18        known      ----
glassfish-ejb-lite                            3.1.2.2-5       installed  ----
glassfish-ejb-lite-l10n                       3.1.2-18        installed  ----
glassfish-full-incorporation                  3.1.2.2-5       known      ----
glassfish-full-profile                        3.1.2.2-5       known      ----
glassfish-generic-ra                          2.1-0.0         known      ----
glassfish-grizzly                             1.9.50-1        installed  ----
glassfish-grizzly-full                        1.9.50-1        installed  ----
glassfish-gui                                 3.1.2.2-5       installed  ----
glassfish-gui-l10n                            3.1.2-18        installed  ----
glassfish-ha                                  3.1.2.2-5       installed  ----
glassfish-hk2                                 3.1.2.2-5       installed  ----
glassfish-javahelp                            2.0.2-1         installed  ----
glassfish-jca                                 3.1.2.2-5       installed  ----
glassfish-jca-l10n                            3.1.2-18        installed  ----
glassfish-jcdi                                3.1.2.2-5       installed  ----
glassfish-jdbc                                3.1.2.2-5       installed  ----
glassfish-jdbc-l10n                           3.1.2-18        installed  ----
glassfish-jms                                 3.1.2.2-5       known      ----
glassfish-jms-l10n                            3.1.2-18        known      ----
glassfish-jmx                                 3.1.2.2-5       installed  ----
glassfish-jpa                                 3.1.2.2-5       installed  ----
glassfish-jsf                                 2.1.6-4         installed  ----
glassfish-jta                                 3.1.2.2-5       installed  ----
glassfish-jts                                 3.1.2.2-5       installed  ----
glassfish-jts-l10n                            3.1.2-18        installed  ----
glassfish-management                          3.1.2.2-5       installed  ----
glassfish-monitoring-dtrace (contrib.glassfish.oracle.com) 3.1.2.2-5       known      ----
glassfish-monitoring-dtrace-l10n (contrib.glassfish.oracle.com) 3.1.2-18       known      ----
glassfish-nucleus                             3.1.2.2-5       installed  ----
glassfish-nucleus-l10n                        3.1.2-18        installed  ----
glassfish-osgi                                4.0.2-0         installed  ----
glassfish-osgi-feature-pack (contrib.glassfish.org) 3.1.2.2-5       known      ----
glassfish-osgi-gui (contrib.glassfish.org)    3.1.2.2-5       known      ----
glassfish-osgi-http                           3.1.2.2-5       installed  ----
glassfish-osgi-incorporation (contrib.glassfish.org) 3.1.2.2-5       known      ----
glassfish-registration                        3.1.2.2-5       installed  ----
glassfish-scripting                           3.1-41          known      ----
glassfish-scripting-l10n                      3.0.1-20.1      known      ----
glassfish-upgrade                             3.1.2.2-5       installed  ----
glassfish-upgrade-l10n                        3.1.2-18        installed  ----
glassfish-verifier                            3.1.2.2-5       known      ----
glassfish-verifier-l10n                       3.1.2-18        known      ----
glassfish-web                                 3.1.2.2-5       installed  ----
glassfish-web-incorporation                   3.1.2.2-5       installed  ----
glassfish-web-l10n                            3.1.2-18        installed  ----
glassfish-web-profile                         3.1.2.2-5       installed  ----
grails (contrib.glassfish.org)                1.1.2-1.0       known      ----
hibernate (contrib.glassfish.org)             3.5.0-0.3       known      ----
javadb-client                                 10.8.1.2        installed  ----
javadb-common                                 10.8.1.2        installed  ----
javadb-core                                   10.8.1.2        installed  ----
javadb-demo                                   10.6.2.1-1      known      ----
javadb-doc                                    10.6.2.1-1      known      ----
javadb-javadoc                                10.6.2.1-1      known      ----
jersey                                        1.11.1-1.0      installed  ----
jersey-docs-and-examples                      1.9-1.18        known      ----
jmaki (contrib.glassfish.org)                 1.8.1-2.0       known      ----
jruby (contrib.glassfish.org)                 1.4.0-1.0       known      ----
jruby-gems (contrib.glassfish.org)            2.3.5-1.0       known      ----
jython-container (contrib.glassfish.org)      0.5.6-1.1       known      ----
jython-runtime (contrib.glassfish.org)        2.5.1-1.0       known      ----
metro                                         2.2-13.1        known      ----
mq-bin-exe                                    4.5.2-3.4       known      ----
mq-bin-sh                                     4.5.2-3.4       known      ----
mq-capi                                       4.5.2-3.4       known      ----
mq-config-gf                                  4.5.2-3.4       known      ----
mq-core                                       4.5.2-3.4       known      ----
mq-docs                                       4.5.2-3.4       known      ----
mq-locale                                     4.5.2-3.4       known      ----
mq-server                                     4.5.2-3.4       known      ----
nss-libs                                      3.13.1-1.0      known      ----
nss-nspr-jss-src                              3.13.1-1.0      known      ----
nss-utils                                     3.13.1-1.0      known      ----
pkg                                           1.122.2-56.2852 installed  ----
pkg-extra-tools                               0.2.0-56.2852   known      ----
pkg-java                                      1.122-56.2852   installed  ----
pkg-toolkit                                   2.3.5-56.2852   known      ----
pkg-toolkit-incorporation                     2.3.5-56.2852   installed  ----
python2.4-minimal                             2.4.6.0-56.2852 installed  ----
shoal                                         1.6.21-0        installed  ----
shoal-l10n                                    1.6.17-0        known      ----
sun-javaee-engine                             3.0-74.2        known      ----
updatetool                                    2.3.5-56.2852   known      ----
wxpython2.8-minimal                           2.8.10.1-56.2852 known      ----

"installed" がインストール済み、"known" が未インストール(インストール可能)という意味です。現在の環境はWeb Profileのため、JMS(mq-*) や JAX-WS(metro) が入っていないことがわかります。

パッケージをインストールする場合には、pkg install <パッケージ名> と入力します。依存パッケージがあればそれも同時にインストールされます。例えば metro (JAX-WS 実装)をインストールする場合には以下のようにします。

C:\glassfish3\bin>pkg install metro

Web ProfileをFull Profileに切り替える場合も同様の操作です。

C:\glassfish3\bin>pkg install glassfish-full-profile

この glassfish-full-profile というパッケージは本体を持たず、Full Profileに必要なすべてのパッケージに依存しています。Java EE 6 Full Profile のとりまとめ役を果たします。glassfish-web-profile も同様に、Web Profileに必要なすべてのパッケージに依存しています。なお、glassfish-full-profile の依存パッケージの1つに glassfish-web-profile があり、Web Profile は Full Profile のサブセットであることが、IPS 上からも明らかになります。

パッケージを削除する場合には、pkg uninstall <パッケージ名> と入力します。先にも述べたように、削除しようとするパッケージに依存するパッケージが存在する場合には削除できません。

C:\glassfish3\bin>pkg uninstall metro

参考まで、今回の環境(Web Profile)で glassfish-web-profile を削除してもインストール済みのパッケージは削除されません。glassfish-web-profile はすべてのパッケージに依存していますが、glassfish-web-profile に依存するパッケージは存在しないからです。

pkg コマンドや IPS に関する詳細については、Solaris 11 および OpenSolaris の文献が充実しているのでそちらを参照して下さい。

3.2. 注意点

基本的には updatetool ユーティリティーに準じます。

4. まとめ

GlassFish の更新ツールは、基本となるのは updatetool ユーティリティーと pkg コマンドです。どちらを選択するかはケース・バイ・ケースによります。管理コンソールはモジュールの追加をサポートしますが、どちらかというと管理コンソールとして他の情報と合わせて現在インストールされているモジュールを確認する意味合いの方が強いようです。

明日は GlassFish のロギングについてお話します。

パスワード・エイリアスの使い方

この記事は GlassFish Advent Calendar 2014、17 日目の記事です。昨日は「JAX-RS (Jersey) Client をプロキシ経由で使う方法」です。今回は使い方によってはセキュリティレベルを向上させることができるパスワード・エイリアスについて、基本的な管理方法ご紹介します。

1. パスワード・エイリアスの概要

GlassFish にはパスワード・エイリアスという機能が備わっています。パスワード・エイリアスとは、パスワードを暗号化して保存し別名(エイリアス)で参照できるようにしたものです。

Java EE の設定項目の中には、パスワードを項目中に直接記述しなければならないものがあります。具体的には、JDBC 接続プールの接続文字列に含まれるパスワード、JavaMail セッションのプロパティで設定する SMTP/POP3/IMAP サーバのパスワード等がそれに該当します。これらを GlassFish の管理コンソールや asadmin から設定する場合には、パスワードを直接記述する代わりに当該パスワードのパスワード・エイリアスを指定することができます(パスワード・エイリアスは事前に作成しておく必要があります)。

2. パスワード・エイリアスの管理

2.1. パスワード・エイリアスの作成

パスワード・エイリアスの作成は、管理コンソールまたは asadmin の create-password-alias サブコマンドで行います。

管理コンソールからパスワード・エイリアスを追加する場合には、左ペインから「ドメイン」を選択し、「パスワード・エイリアス」タブを選択して指示に従います。

asadmin-create-password-alias.jpg

asadmin を使用する場合は、以下の手順に従います。単に asadmin create-password-alias を実行した場合はエイリアス名とパスワードを対話式で入力します。

asadmin create-password-alias
Enter the value for the aliasname operand> jdbc-password
エイリアス・パスワードを入力してください>
エイリアス・パスワードをもう一度入力してください>
Command create-password-alias executed successfully.

asadmin create-password-alias alias-name とした場合はパスワードのみ対話式で入力します。いずれの場合もパスワードは GlassFi h内のキーストアに暗号化して保存され、保護されます。

C:\glassfish4\bin>asadmin create-password-alias imap-password
エイリアス・パスワードを入力してください>
エイリアス・パスワードをもう一度入力してください>
Command create-password-alias executed successfully.

2.2. パスワード・エイリアスの一覧

登録したパスワード・エイリアスは管理コンソールまたは asadmin で一覧表示することができます。管理コンソールの場合は、「パスワード・エイリアス」タブ自体が一覧表示を兼ねています。

asadmin-list-password-aliases.jpg

asadmin を使用する場合は、サブコマンド list-password-aliases を使用します。

C:\glassfish4\bin>asadmin list-password-aliases
imap-password
jdbc-password
Command list-password-aliases executed successfully.

2.3. パスワード・エイリアスの削除

パスワード・エイリアスを削除するには管理コンソールまたは asadmin の delete-password-alias サブコマンドを使用します。

管理コンソールからパスワード・エイリアスを削除する場合は、一覧から削除したいパスワード・エイリアスを選択して[削除]ボタンをクリックします(スクリーンショットは省略)

asadmin を使用する場合は、以下の手順に従います。単純に delete-password-alias と入力した場合は削除するパスワード・エイリアス名を対話式で入力します。

C:\glassfish4\bin>asadmin delete-password-alias
Enter the value for the aliasname operand> imap-password
Command delete-password-alias executed successfully.

asadmin delete-password-alias alias-name とした場合は特にプロンプトが表示されることなくパスワード・エイリアスが削除されます。

C:\glassfish4\bin>asadmin delete-password-alias jdbc-password
Command delete-password-alias executed successfully.

2.4. その他のトピック

今回は触れませんでしたが、パスワード・エイリアスには他にもいくつかのトピックがあります。ここではその一部をご紹介します。

  • これは asadmin でのみ可能な操作ですが、登録済みのパスワード・エイリアスのパスワードを更新することができます。詳細は「asadmin help update-password-alias」で検索してください。
  • 今回紹介した方法は、管理コンソールを使用する方法と、asadmin の対話モードを利用する方法でした。この他にもあらかじめファイルに保存したパスワードを asadmin で処理し、非対話モードでパスワード・エイリアスを作成する方法があります。

3. パスワード・エイリアスの使い方

パスワード・エイリアスを使用する場合は、パスワードを記述すべき箇所で ${alias=alias-name} とします。パスワード・エイリアスは必要なだけいくらでも作成することができます。パスワード・エイリアスが使える箇所はあくまで GlassFish の管轄にある設定ファイル内(言い換えると管理コンソールで設定できる項目)に限られます。プログラム内のソースコードやリソースファイル、設定ファイル等に対しては使用することはできません。

なお、asadmin でパスワードエイリアスを含む項目を設定する場合は、設定値をクォーテーションで囲む必要が出てきますので注意してください。

パスワード・エイリアスを作成・削除する場合はドメイン管理サーバ(DAS)が起動している必要があります。また、各パスワード・エイリアスはそれを作成したドメイン内で管理され、また有効となります。

4. まとめ

サードパーティー製のフレームワークを多用しリソースを個別に管理している場合は、パスワード・エイリアスは効果を発揮できませんが、リソースの管理をサーバに集中させる場合は、パスワードの暗号化とラベル付けの2つのメリットが得られます。

明日は GlassFish の更新ツールについてお話する予定です。

この記事は GlassFish Advent Calendar 2014 の 16 日目です。前日は「GlassFish 4.1 開発ウラ話」でした。本日は小ネタの部類に入りますが、世間ではあまり採用されていないアプローチのため、参考になるかと思います。

JAX-RS 2.0 で導入された Client API は有益ですが、HTTP プロキシサーバへの対応が仕様化されていません。ここでは Jersey を例にプロキシに対応させる方法を考察します(他の実装では適応できないことをあらかじめお断りしておきます)。

方法1 システムプロパティを設定する

Jersey Client のデフォルトでは内部で HTTP 通信に HttpURLConnection を利用します。したがって、同クラスに対してプロキシ情報を伝えるシステムプロパティ [http.proxyHost ; http.proxyPort ; httpNonProxyHosts] はそのまま有効です。

方法2 ConnectorProvider を指定する

Jersey Client は、HTTP 接続を提供する部分を ConnectorProvider と呼び、必要であれば差し替えることができるようになっています。デフォルトでは HttpURLConnection を利用する HttpUrlConnectorProvider となっています。今回は HttpUrlConnectorProvider を利用し、Proxy を設定した HttpURLConnection を使う例を示します。

Apache HttpClient を接続に用いる ApacheConnectorProvider を利用する方法(Jersey 2.5 以降)もあります。こちらの事例は多いようなので、興味があれば調べてみてください。

では、コード例を以下に示します。

Proxy proxy = new Proxy(Type.HTTP, new InetSocketAddress(PROXY_HOST, PROXY_PORT);

ClientConfig config = new ClientConfig();
HttpUrlConnectorProvider connectorProvider = new HttpUrlConnectorProvider();
connectorProvider.connectionFactory(url -> (HttpURLConnection) url.openConnection(proxy));
config.connectorProvider(connectorProvider);

Client client = ClientBuilder.newClient(config);
// ...

今回示した Proxy 設定済みの HttpURLConnection を使用する方法は、追加のライブラリを導入しなくても使用できる方法のため、個人的にはお勧めの方法です。


明日は使われているのかどうかいまいち分からない、パスワートエイリアスについてご紹介する予定です。

GlassFish 4.1 開発ウラ話

この記事は GlassFish Advent Calendar 2014、15 日目です。昨日は「GlassFish アプリケーション・デプロイメント Tips」と題して、アプリケーションのデプロイに関する Tips をご紹介しました。本日は趣を変え、2013 年夏から 2014 年 9 月までの GlassFish 4.0.1/4.1 開発中に起こったいくつかの問題を取り上げ、GlassFish 開発チームが問題をどのように解決したのかをお話したいと思います。本文中では GlassFish 4.0.1 と GlassFish 4.1 の表記が混在していますが、build 1 から build 10 までが GlassFish 4.0.1、build 11 から build 13/GA までが GlassFish 4.1 というバージョン番号になっています。

問題1: Jersey と Tyrus のバージョンアップが頻発した

GlassFish 4.0 には、JAX-RS 2.0 実装として Jersey 2.0 が、また WebSocket 1.0 実装として Tyrus 1.0 がそれぞれバンドルされていました。この Jersey と WebSocket が GlassFish 4.0.1 の開発期間中にバージョンがどんどん上がってしまい(Jersey 2.0→2.10.4、Tyrus 1.0→1.8.1)、仕様までアップデートされるに至って、GlassFish 4.0.1 RC1 が出た直後にバージョン番号を GlassFish 4.1 へリナンバリングすることになってしまいました。仕様がアップデートしたものとしては他にも CDI が該当しますが、CDI 1.2 対応が決定されるまで実装は Weld 2.0.3.Final に固定されていたため、トリガーはやはり Jersey と Tyrus(特に Jersey)の急激なバージョンアップだったと思います。この話は上の方では周知事項だったようですが、末端まで情報が届いておらず、突然の発表のような形になってしまいました。

問題2: CDI 1.2 にアップデートしたら TCK で不合格となった

GlassFish 4.0.1 開発中に JSR 346 がアップデートされ、CDI 1.1 から CDI 1.2 になりました。GlassFish 4.0.1 でも CDI 実装を Weld 2.0.3.Final(CDI 1.1) から Weld 2.2系(CDI 1.2)にアップデートしましたが、Java EE 7の TCK で不合格となりました。リファレンス実装が TCK 不合格になる異常事態に対し、開発チームは CDI が関連する箇所の見直しを行うとともに、TCK にもバグがないか確認を行いました。担当者間では、最悪 TCK の修正まで覚悟していたようです。CDI 実装を当時最新の Weld 2.2.2.Final にアップデート、EJB コンテナや Jersey にも CDI 連携で TCK をパスできなくなるバグがいくつか発見され修正しました。それでも TCK には合格しませんでしたが、同時に原因を Jersey に絞り込むこともできました。GlassFish 4.1 は最終的に Jersey 2.10 系を採用することになりましたが、TCK に合格するまで Jersey 2.10 のバグを修正し続けました。GlassFish 4.1 GA が採用したJersey 2.10.4 は、GlassFish 4.1 の TCK不合格問題に対処するためだけにビルドされたものと言っても過言ではありません。その証拠に、Jersey 2.10.4 よりも先に次バージョン Jersey 2.11 がリリースされていたのです。

問題3: Mac OS X 10.10 (Yosemite) で起動に失敗した

GlassFish 4.0.1 の開発と同時期にプレビュー版が公開されていた Mac OS X 10.10 (Yosemite) で GlassFish 4.0.1 が起動できない問題が発覚しました。当時の現行バージョンであった Mac OS X 10.9 (Mavericks) では問題が発生していなかったこと、GlassFish関係者に Mac OS X ユーザーがいたこと(例えば Java EE エヴァンジェリストの Reza Rahman)から、重点を置いて問題の調査が行われました。結果、GlassFish が起動時に呼び出していた Mac OS X 付属のスクリプトが Yosemite で予告なく変更され、それが原因で GlassFish の起動に失敗していたことが明らかになりました。GlassFish 4.0 では Mac OS X 提供のスクリプトを呼び出していた箇所を、GlassFish 4.0.1 build 10 以降では Mavericks のスクリプトをベースに必要な呼び出しをハードコーディングするように変更することで Yosemite 対応を実現しました。

実はこれには後日談があって、Mac OS X 上の JDK 8 に限っては上記の他にもいくつかの不具合がありました。この問題はリリースまでに解決が必須でありながら、長い間原因が分かりませんでした。しかし、JDK 8 Update 20 のプレビュー版が安定してきた頃、互換性検証のためJDK 8 Update 20 上で GlassFish 4.0.1 を実行したところ、問題が再現しなくなったのです。開発チームでは、原因はJDK 8 Update 11のバグと結論付け、必須とする JDK 8 を JDK 8 Update 5 以降から JDK 8 Update 20 以降に変更することで対処しました。こうした経緯から、実は Windows や Linux では JDK 8 Update 11 でも動作します。

問題4: Java DB が起動できなくなった

GlassFish には標準のデータベースとして Java DB を同梱しており、asadmin にて Java DB の起動および停止ができるように連携が強化されています。ところが、ある時点を境に Java DB が起動できない問題が発生しました。

原因は、JDK 7 および JDK 8 のセキュリティー機能の強化により、Java DB が通信ポートとして使用していた TCP 1527 が標準でブロックされるようになったためです。JDK 側のポリシーファイルを修正することで一時対応を行うことは可能でしたが、GlassFish 開発チームが採った選択は、JDK のセキュリティー強化にも対応した新バージョン Java DB 10.10.2.0 にアップデートすることでした。

なお、この問題は Java DB をネットワークモードで起動したときのみ発生するもので、埋め込みモードで使用する場合には発生しません。


明日は「JAX-RS (Jersey) Client をプロキシ経由で使う方法」を予定しています。

この記事は GlassFish Advent Calendar 2014、14 日目の記事です。昨日は「GlassFish 4.1で始めるWebサービス&MQ通信ー番外編2:Server-Sent Events」です。本日は GlassFish にアプリケーションをデプロイする上で知っておくと便利な機能についてお話します。

基本的には .war でデプロイ

GlassFish の機能というよりは Java EE 6 以降の仕様ですが、ほとんどのアプリケーションは .war にパッケージできます。Java EE 5 まではサーブレットなどの Web アプリケーションを .war、バックエンドの EJB を .jar(ejb-jar)にパッケージし、必要であればこれらをさらに .ear にパッケージしていました。現在では一部例外を除き EJB も .war に含めることができるため、アプリケーション全体を .war でパッケージした方が手数は減ります。そもそも CDI の機能強化により EJB で実装しなければならない箇所自体が減ってきています。

.war にパッケージできない EJB の代表例は、JAX-WS のエンドポイントを EJB で構成した場合です。なお、JAX-WS のエンドポイントは CDI 1.1 にも対応していません(CDI 1.0 には対応)ので、beans.xml の省略や厳密なスコープ管理を適用することもできません。

EJB 化 した JAX-WS のエンドポイントが .war にデプロイできない件については、一昨日の「GlassFish 4.1で始めるWebサービス&MQ通信―(2)JAX-WS」にも記載がありますので、必要に応じて参照してください。

デプロイ先はドメイン管理サーバ(DAS)

スタンドアロン構成・クラスタ構成を問わず、アプリケーションのデプロイ先はターゲットのドメインのドメイン管理サーバ(DAS)です。デプロイ方法は管理コンソール・asadmin・autodeploy・REST backend のいずれでも構いません。クラスタ構成の場合はさらに DAS を含む各ノードにアプリケーションが展開されます(クラスタが巨大な場合は相当時間がかかります)。クラスタ構成時にノード管理サーバに対してデプロイして各ノードに展開するのはごく当然な動きなのですが(例えば WebLogic も同様で、管理サーバにデプロイしたアプリケーションが各管理対象サーバに展開されます)、GlassFish は DAS 自身がクラスタのノード(localhost-domain1 という CONFIG ノード)であり、またクラスタ構成を採らなくても単独ノードとして稼働するため、見かけ分かりづらいところがあります。からくり自体はとてもシンプルなのですが。

同一のサーバでもドメインが異なる場合は、各ドメインのDASに対してそれぞれデプロイする必要があります。同一サーバにドメインが複数ある場合、ドメインごとにノード(DAS またはインスタンスノード)が存在しています。単一のサーバに複数のノードが存在するのは不自然に思えるかもしれませんが、GlassFish の実行環境は DAS を中心とするドメインを単位としているため、このような構成になります。

なお、第 3 の実行形態である埋め込みサーバの場合は少し特殊で、ドメインを構成せず DAS もインスタンスノードも存在しません。埋め込みサーバに対しては専用の API 経由でアプリケーションをデプロイします。埋め込みサーバは構成や設定方法こそ独自ですが、実行するアプリケーションに制約はありません。

アプリケーション・バージョニングの利用

GlassFish はアプリケーションの複数のバージョンをデプロイし必要に応じて切り替えて使用するアプリケーション・バージョニングをサポートします。デプロイ可能なバージョンの数には特に制限はありませんが、切り替え作業そのものは手動になります。

アプリケーション・バージョニングは製品により仕様が異なり、例えば WebLogic では新旧 2 バージョンに限定されるものの、一時的に新旧 2 バージョン同時稼働させシステムを停止させずにバージョン切り替えを行うことができます(ただし、クラスローダがメモリリークする可能性があるため、本番機でメンテナンス時間を取れなかった場合に限って利用すべきです)。

GlassFish でアプリケーションのバージョニングを行う場合は、デプロイおよびアンデプロイには asadmin を使用し、アプリケーション名に「:バージョン」を付加します。例えば sampleapp というアプリケーションの場合は、sample:1、sample:2、... のようになります。バージョンには英数字と一部の記号(ハイフン、アンダーバー、ピリオドを使用できます。複数のバージョンをデプロイした場合、0 または 1 のバージョンが有効になります。アプリケーションの特定のバージョンを、

  • 有効化する場合: asadmin enable application-name:version
  • 無効化する場合: asadmin disable application-name:version

とします。あるバージョンを有効化する際、既に別のバージョンが有効になっていた場合は、現在有効になっているバージョンは無効化されます。別の表現をするならば : より前の名前が共通しているアプリケーションは、そのうちのどれか1つのみ有効になります。

GlassFish のアプリケーション・バージョニングは、実質的に複数のアプリケーションをデプロイすることと同じであるため、メモリの少ない環境では容易に OutOfMemoryError が発生する可能性があることに注意してください(WebLogic と異なり、クラスローダのメモリリークはそれほど心配しなくて良いと思います)。

マニュアルには asadmin だけでなく管理コンソールからでもアプリケーション・バージョニングを使用できるとの記述がありますが、確実に動作するのはデプロイのみで、アンデプロイや有効化・無効化は動作しないか、仮に動作したとしても不安定です。また、IDE からのデプロイではアプリケーション・バージョニングは利用できません。従って、現状では asadmin 経由のデプロイ操作がアプリケーション・バージョニングを利用する唯一の確実な方法となります。

ちなみに、アプリケーション・バージョニングを利用する場合でも、デプロイするアプリケーションの各バージョンは一応独立したアプリケーションとして GlassFish 上に存在することになります。従って、デプロイとアンデプロイはアプリケーション名とバージョンを合わせた名称をキーとして行う必要があります。

明日は 2013 年夏から GlassFish 4.1(旧称 GlassFish 4.0.1) の開発を追いかけてきたからこそ話せる「GlassFish 4.1 開発ウラ話」についてお話します。「ウラ話」と言っても過剰な期待は禁物です。ではまた。

この記事は GlassFish Advent Calendar 2014 の 13 日目です。昨日は「GlassFish 4.1で始めるWebサービス&MQ通信―番外編:WebSocket」です。本日は番外編その 2 として、GlassFish 4.1 がサポートする Server-Sent Events(SSE)について触れようと思います。

GlassFish 4.1 では、JAX-RS 実装 Jersey がいくつかの独自拡張機能を提供しています。Jersey 独自拡張機能としては MVC Framework(JAX-RS 2.0/2.1 での標準化を Bill Burke に拒否されたため、別仕様の JSR 371 として標準化が始まりました)、multipart メッセージ(ファイルアップロード等)対応などがあり、Jersey 2.3 からは SSE サポートが加わりました。Jersey 2.8 からは SSE サポートはデフォルトで有効となっています(GlassFish 4.1 はさらに新しい Jersey 2.10.4 をバンドルしているため、当然ながらデフォルトで SSE を扱うことができます)。

Jersey の SSE サポートについては、本日の第九回 #渋谷java にて発表しましたので、概要は発表資料を参照してください。

Jersey SSE サポートの詳細については、Jersey Users Guide, Chapter. 14 にサンプル付きで記載がありますので、こちらもご覧ください。

SSE 対応は JAX-RS 2.1 新機能として JSR 370 にて提案されています。しかし、残念ながら下記の理由からおそらく採用されることはないでしょう。

  • Java EE 8 の SSE サポートはほぼ確定とみられるが、どのコンポーネントに入れるべきかについて意見がまとまっていない。JAX-RS 2.1 の他にも、HTTP/2 の Server Push とあわせて Servlet 4.0(JSR 369)に入れる案もあり、その他 Web の API である JSF 2.3(JSR 372)、MVC 1.0(JSR 371)にも余地はあり得る(それが適切か否かは別として)。もっとも、JSP での SSE 対応はないと思われるが。
  • JSR 370 では、例によって Bill Burke が SSE サポートに難色を示している。MVC の時のような初めから提案を全否定し議論にすら応じないほどの拒絶は見られないが、最終的には Bill Burke がスペックリードの意見を押し切って廃案にする可能性が高い。

念のために書き添えておくと、Bill Burke は JBoss の JAX-RS 実装である RESTEasy のリードであり、JAX-RS の初期からエキスパート・グループの一員を務める JAX-RS の第一人者です。「JavaによるRESTfulシステム構築」(O'Reilly、2010)という JAX-RS に関する良著を執筆した人物でもあります。RESTEasy の独自拡張機能には彼の思想が色濃く反映されており、Jersey のそれとは全く異なるものになっています。余談ですが、筆者自身は JSR 370 における Bill Burke の態度が気に入らなくて(以下略)

SSE については以上です。では、また明日お目にかかりましょう。


補足:「JavaによるRESTfulシステム構築」は、JAX-RS を深く知るためには是非手元に置いておきたい一冊です。邦訳は JAX-RS 1.1 ベースですが、JAX-RS 2.0 を理解するための基礎知識を固める上で非常に有益です。Bill Burke に対する個人的な感情を抜きにして、筆者が自信を持ってお薦めできる参考書です。

この記事は GlassFish Advent Calendar 2014、12 日目の記事です。昨日は「GlassFish 4.1で始めるWebサービス&MQ通信―(3)JAX-RS」です。本日は、「GlassFish 4.1で始めるWebサービス&MQ通信」の番外編として、WebSocket API と Tyrus について取り上げます。

Java EE 7 の一部としてリリースされた WebSocket API の標準仕様である JSR 356 は、これまでのサーバー/サーブレットコンテナ独自の WebSocket 実装は大きく異なる高水準 API に仕上がっています。JAX-RS に類似したモダンなインタフェース、データ型を意識させないエンコード/デコード機能、そして Java EE だけでなく Java SE でも利用できる幅広い適用範囲など、多くのプログラマーがわくわくするような仕掛けが用意されています。

今回は WebSocket API 1.0 の概要をご紹介するとともに、WebSocket 1.1 で追加された新機能についても触れてみようと思います。

この記事は Java EE Advent Calendar 2013 の 16 日目「JSR 356―Java標準のWebSocket API」を WebSocket 1.1 仕様を前提に改訂したものです。

1. WebSocket とは?

WebSocket は Web サーバーとクライアントの間で双方向通信を行うためのプロトコルです。主な用途は、従来 REST や Long Polling で半ば強引に実現していたサーバー Push 通信で、従来のやり方に比べてオーバーヘッドがはるかに小さいという特徴があります。WebSocket は当初 HTML5 の一部として仕様が検討され、数度に渡るドラフト改訂の末に RFC 6455 という独立した規格として成立しました。

WebSocket は早期ドラフトの段階から注目を集め、Java においてもいくつかの Java EE サーバーやサーブレットコンテナが独自に WebSocket を実装してきました。そして 2011 年 12 月に RFC 6455 が発行され、Java EE 7 でも HTML5 とともに WebSocket API の標準化を表明しました。JCP でも JSR 356 として WebSocket API の標準化を進め、2013 年 6 月の Java EE 7 と同時にリファレンス実装である Tyrus をリリースしました。現在では Java EE 7 サーバはもとより、その他のアプリケーション・サーバ(WebLogic 12.1.3、Jetty など)でも標準の WebSocket をサポートしています。

WebSocket API を策定した JSR 356 では、2014 年 8 月にメンテナンスリリースである WebSocket 1.1 をリリースしました。WebSocket 1.1 はそれまでの WebSocket 1.0 と大きくは変わりませんが、イベントハンドラ周りで Java 8 のラムダ式が使いやすいように API の追加が行われています。GlassFish 4.1 では WebSocket 1.1 を採用しています。

2. WebSocket の仕組み

WebSocket では、Web サーバーとクライアントのハンドシェイクに HTTP(または HTTPS)が用いられますが、その後はプロトコルを WebSocket に切り替えてサーバー・クライアントが双方向かつリアルタイムにデータを送受信するようになります。

REST に対して WebSocket が優れている点は、REST がポーリングのたびに HTTP セッションを確立しているのに対して(頻度の高いポーリングでは、このオーバーヘッドは無視できないレベルに達します)、WebSocket は一旦プロトコルが切り替わった後はクライアント・サーバーのいずれかがクローズしない限り WebSocket のセッションが継続していることです。これが REST より WebSocket の方が軽量であると言われる所以でもあります。

メモリ消費量は REST よりも WebSocket の方が結果的に大きくなる傾向にあるようです。WebSocket ではメモリ消費を増大させるファクターが複数存在するため、メモリ解放のタイミングも REST より難しいと考えられます。

Java では、WebSocket は Servlet 3.1 の上位に位置する仕様です。WebSocket に必須であるプロトコル切り替えのリクエストとレスポンス HTTP 101 をサポートするのが Servlet 3.1 以降となっているためです。

3. WebSocket API によるプログラミング

WebSocket API は、JAX-RS に似たアノテーション・ベースのプログラミングをサポートします(アノテーションを用いないプログラミング・スタイルもサポートしますが、個人的にあまり好きではないので今回は割愛します)。主な構成要素としては次のようなものがあります。

  • クライアント・エンドポイント
  • サーバー・エンドポイント
  • セッション
  • エンコーダ(オプション)
  • デコーダ(オプション)

以下に概念図を示します。

jsr356-overview.png

3.1. クライアント・エンドポイント

WebSocket の開始リクエストを投げる側です。JavaScript の WebSocket API が主に想定されますが、WebSocket API では Java SE/EE でクライアント・エンドポイントを実装する方法も提供されます。

最初に、JavaScriptによるクライアント・エンドポイントの全体構造を以下に示します。

// サーバー・エンドポイントの URI
var uri = "ws://localhost:8080/wsapp/hello";

// WebSocket の初期化
var websocket = new WebSocket(uri);

// イベントハンドラの設定
websocket.onopen = function(event) {
  // セッション確立時の処理
};
websocket.onmessage = function(event) {
  // メッセージ受信時の処理
}
websocket.onerror = function(event) {
  // エラー発生時の処理
}
websocket.onclose = function(event) {
  // セッション解放時の処理
}

次に Java SE/EE によるクライアント・エンドポイントの全体構造を以下に示します。以下に示す以外にも追加で受け取ることが出来る要素(例えば OnMessage ハンドラで Session オブジェクトを受け取る、など)があります。

@ClientEndPoint
public HelloClient {
  @OnOpen
  public void onOpen(Session session) {
    // セッション確立時の処理
  }
  
  @OnMessage
  public void onMessage(String message) {
    // メッセージ受信時の処理
  }
  
  @OnError
  public void onError(Throwable t) {
    // エラー発生時の処理
  }
  
  @OnClose
  public void onClose(Session session) {
    // セッション解放時の処理
  }
}

上記のクライアント・エンドポイントは以下のように Java のコードに組み込むことで機能するようになります。

// 初期化のため WebSocket コンテナのオブジェクトを取得する
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
// サーバー・エンドポイントの URI
URI uri = URI.create("ws://localhost:8080/wsapp/hello");
// サーバー・エンドポイントとのセッションを確立する
Session session = container.connectToServer(new HelloClient, uri);

3.2. サーバー・エンドポイント

クライアント・エンドポイントからの要求を受ける側です。JSR 356 策定以前から、サーバーサイド Java がその対象としてきたものです。JSR 356 で規定された WebSocket API ではアノテーションを使用して簡単に実装することが出来ます。サーバー・エンドポイントの全体構造を以下に示しますが、クライアント・エンドポイントとほぼ同じです。

Web アプリケーションの場合はクライアント・エンドポイントを JavaScript で記述するため、Java 側ではサーバー・エンドポイントのみを実装することになります。なお、サーバー・エンドポイントでは JAX-RS のリソース・クラス同様、CDI の Injection(つまり @Inject)が使用可能です。

@ServerEndPoint("/hello")
public HelloServer {
  @OnOpen
  public void onOpen(Session session) {
    // セッション確立時の処理
  }
  
  @OnMessage
  public void onMessage(String message) {
    // メッセージ受信時の処理
  }
  
  @OnError
  public void onError(Throwable t) {
    // エラー発生時の処理
  }
  
  @OnClose
  public void onClose(Session session) {
    // セッション解放時の処理
  }
}

3.3. セッション

WebSocket のセッションそのものを表します。セッションにデータを設定すると、WebSocket 通信を介してエンドポイント(サーバーまたはクライアント)に送信され、エンドポイントのイベントハンドラが呼び出され、データが取得できる仕組みになっています。

送信側は、セッションから取得できる「リモート・エンドポイント」と呼ばれるオブジェクトの setXXX というメソッドに値を渡すことで、データを送信できます。

// 送信側
// session は javax.websocket.Sessionのオブジェクト
session.getBasicRemote().sendText("Hello");

受信側は、エンドポイントのクラスに OnMessage ハンドラを定義しておくと、データ受信時に呼び出され、引数に値が設定されます。

// 受信側
@OnMessage
public void onMessage(Session session, String text) {
  // text には "Hello" が設定されている
  System.out.printf("Receive: %s\n");
  // 受信側でもsession経由で返信ができる
  session.getRemoteBasic().sendText("Hi");
}

OnMessage ハンドラの戻り値を使用すると、上記のコードはさらに簡単になります。

@OnMessage
public String onMessage(String text) {
  // text には "Hello" が設定されている
  System.out.printf("Receive: %s\n");
  // 戻り値がそのまま送信データになる (Session参照不要)
  return "Hi";
}

3.4. エンコーダ・デコーダ

WebSocket で扱えるデータは本来テキスト(Java では String)かバイナリ(Java では byte[] または InputStream/OutputStream)だけです。従来の独自実装の WebSocket でもこの制約に縛られていました(例えば古い Tomcat の WebSocket では CharBuffer と ByteBuffer を使用します)。現在の WebSocket API ではその制約を取り払う、あるギミックが仕込まれています。

WebSocket API では任意のデータ型からテキストまたはバイナリに変換するエンコーダー、その逆を行うデコーダーを実装し、エンドポイントに適用することで、Java のデータ型をそのまま送受信するイメージでプログラミングすることが可能です。エンコーダー・デコーダーにデータ型変換処理を集約できるとともに、積極的な再利用で開発の効率が向上するはずです。筆者は個人的にエンコーダ・デコーダの存在が WebSocket API (JSR 356) 最大の魅力だと感じています。

以下にエンコーダとデコーダの実装例(一部)を示します。この例では JavaBean を JAXB で XML に変換して WebSocket で送受信することになりますが、SOAP エンベロープ級の大規模 XML スキーマでもない限り JSON で表現できますので、実際には JSON に変換した方が効率的です(サンプルが XML なのは、手元にあったエンコーダ/デコーダがこれしかなかったから。単なる手抜きですごめんなさい)。

public class TweetEncoder implements Encoder.Text<Tweet> {
  // ...
  @Ovrride
  public String encode(Tweet tweet) throws EncodeException {
    Writer writer = new StringWriter();
    JAXB.marshal(tweet, writer);
    return writer.toString();
  }
}
public class TweetDecoder implements Decoder.Text<Tweet> {
  // ...
  @Override
  public Tweet decode(String s) throws DecodeException {
    return JAXB.unmarshal(new StringReader(s), Tweet.class);
  }
}

エンコーダとデコーダは、@ClientEndPoint および @ServerEndPoint に設定することで機能します。以下に例を示します。

@ServerEndPoint(value = "/hello",
  decoders = { TweetDecoder.class },
  encoders = { TweetEncoder.class })
public class HelloEndPoint {
  /* OnMessage ハンドラ以外は省略 */
  @OnMessage
  public void onMessage(Session session, Tweet tweet) {
    // Tweet クラスのオブジェクトとして受信可能
    System.out.println(tweet);
    
    // Tweet クラスのオブジェクトのまま送信可能
    Tweet anotherTweet = new Tweet();
    session.getRemoteBasic().sendObject(anotherTweet);
  }
}

エンコーダ・デコーダを設定すると、送受信データに任意の Java データ型を使用できるようになります。WebSocket プロトコルを巧妙に抽象化することにより、JAX-RS 等と変わりない使い勝手を実現しています。

3.5. WebSocket 1.1 の新機能

WebSocket 1.0 から WebSocket 1.1 の変更点は、javax.websocket.Session インタフェースに 2 つのメソッドが追加されたことだけです。追加されたメソッドは、MessageHandler = セッションで送受信されるデータのハンドラを登録する Session#addMessageHandler(MessageHandler) の 2 つのオーバーロード・メソッドです(MessageHandler については今回特に触れませんが、この節で変更点についてのみ取り上げます)。

MessageHandler インタフェースは、自身のサブインタフェースである MessageHandler.Partial インタフェースおよび MessageHandler.Whole インタフェースを包含しています。実は、MessageHandler.Partial と MessageHandler.Whole はともに関数インタフェースであり、Java SE 8 で導入された Lambda 式を使うことで簡単に表現できます。WebSocket 1.0 ではハンドラを MessageHander というインタフェースにまとめて登録していましたが、Lambda 式が使える状況下ではハンドラを MessageHander.Partial または MessageHandler.Whole として登録した方が記述が簡潔になります。実際には継承関係と包含関係を同時に持つため型判定に工夫が必要になり、新たに 2 つのオーバーロード・メソッドを追加しました。これが WebSocket 1.1 となります。WebSocket 1.1 で Session インタフェースに追加される addMessageHandler メソッドのシグネチャは以下の通りです。

  • <T> void addMessageHandler(Class<T> clazz, MessageHandler.Partial<T> handler)
  • <T> void addMessageHandler(Class<T> clazz, MessageHandler.Whole<T> handler)

これで、メッセージハンドラを以下のように登録できるようになります。

// Add MessageHander.Partial
session.addMessageHandler(String.class, (partialMessage, last) -> { ... });

// Add MessageHandler.Whole
session.addMessageHandler(String.class, message -> { ... });

4. WebSocket アプリケーションのデプロイ

サーバサイドの WebSocket アプリケーションは、原則として .war にパッケージしてデプロイします。一般的な .war のデプロイと大きな相違点はありませんが、@ServerEndPoint で指定した URI は、コンテキストルートの直下にマッピングされます。コンテキストルート下に @ApplicationPath で指定したルート URI がマッピングされ、さらに下に @Path で指定した URI 階層がマッピングされる JAX-RS とはその点が異なります。URI マッピングという点では、JAX-RS よりもむしろ Servlet に近いと言えるでしょう。

クライアントサイドは実装によってデプロイ方法はまちまちです。HTML5 の場合は任意の Web サーバになります。Java SE で実装した場合はスタンドアロンの Java アプリケーションになります。

5. まとめ

今回は WebSocket 1.0/1.1 について、サンプルプログラムとデプロイ方法についてご紹介しました。リファレンス実装であるTyrusは、特定のサーバーに依存せず、Java SE 環境だけでも利用可能な、使いやすいものに仕上がっています。WebSocket は今後、Server-Sent Events とともに注目を浴びるであろう技術だけに、基礎をしっかりと押さえておきたいものです。

明日は、第九回 #渋谷java の発表内容を中心にお話します。

この記事は GlassFish Advent Calendar 2014、10 日目の記事です。昨日は「GlassFish 4.1で始めるWebサービス&MQ通信ー(2)JAX-WS」です。今回は「GlassFish 4.1で始めるWebサービス&MQ通信」の第 3 回として、JAX-RS 2.0 と Jersey について取り上げます。

GlassFish 4.1 の JAX-RS 2.0 は、正式には JAX-RS 2.0 rev.A と呼ばれる仕様です。これは JSR 339 のメンテナンスリリースとして 2014 年 10 月にリリースされたもので、これ以降の Java EE 7 実装は厳密には JAX-RS 2.0 ではなく JAX-RS 2.0 rev.A をサポートすることになります。メンテナンスリリースといっても、実態は JAX-RS API 実装のバグと Javadoc の誤記(いわゆる「仕様バグ」)を修正しただけです。

現時点では GlassFish 4.1 と WildFly 8.2 が JAX-RS 2.0 rev.A をサポートしています。

JAX-RS 2.0 は JAX-RS 1.1(JSR 311)にいくつかの追加仕様を盛り込んだものです。追加された仕様は JAX-RS 1.1 実装の独自 API を整理し、標準化したものとなっています。追加された仕様には以下のようなものがあります。

  • Client API
  • Filter および Interceptor
  • CDI 連携
  • Bean Validation 連携(BV 側もメソッド・バリデーションを追加
  • 非同期処理(クライアントおよびサーバ)対応
  • Content Negotiation 対応

それ以外については JAX-RS 1.1 の仕様を引き継いでおり、基本的なコンセプトも全く変わっていません。

以下、過去資料へのリンクです。まずは JAX-RS 1.1 の概要に加え JAX-RS 2.0 の内容を先取りした発表資料です(Client API の一部クラス名が発表当時から変更になっています)。

資料に含まれていない JAX-RS 2.0 の新機能については、過去のブログ記事「JAX-RS 2.0 ことはじめ」を参照してください。

もう 1 つの資料は JAX-RS の使いこなしを目的に作成したものです。JAX-RS 1.1 と JAX-RS 2.0、および JAX-RS 実装固有機能がすべて混ざっています(区別できるようにはしていますが)。

GlassFish 4.1 では、Jersey 2.0 が抱えていた大量のバグ(例えばこういうのとか)が一斉に淘汰されています。ひょっとしたらまだ変なところが残っているかもしれませんが、普通に使っている分には問題なく、とても安定して動作します。

「西のうらがみ、東のはすぬま」と称されている(かどうかはわかりませんが)ほど、あちこちで JAX-RS の話をしてきているので、過去資料をまとめるだけで JAX-RS の回はおしまいです。

これ以上、私にどうしろと言うのですか!

(画像は自粛)

明日は番外編として WebSocket 1.1 と Tyrus について取り上げる予定です。

この記事は GlassFish Advent Calendar 2014、10 日目の記事です。昨日は「GlassFish 4.1でJAX-RS=CDI連携の単体テストを行う方法」です。本日は、一昨日より始めた連載「GlassFish 4.1で始めるWebサービス&MQ通信」の第 2 回で、JAX-WS 2.2 と Metro について取り上げます。

1. はじめに

JAX-WS は、大雑把に言うと HTTP でリモート・プロシージャ・コール(RPC)を実現する方法です。JAX-WS は最初のバージョンが 2.0 で、バージョン 1.x に相当するものは JAX-RPC という別の仕様でした。

リモート・プロシージャ・コールのプロトコルとしては、DCOM(分散 DOM、別名 ActiveX)、CORBA、Java RMI、RMI-IIOP(CORBA 上で Java RMI を動かすもの)などがあります。これらは分散コンピューティングがもてはやされた時期にやたら使われていましたが、プロトコル自体が複雑で、送受信されるデータは独自のバイナリ、さらにはベンダーごとに振るまいが異なるなど、実はかなりの地雷でした。それに対して Microsoft が HTTP とその上で送受信する XML でリモート・プロシージャ・コールを実現する SOAP という規格を開発し、従来の方式よりシンプルで確実、パフォーマンスも良かったことから W3COASIS で標準化され、「Web サービス」として普及しました。現在では「Web サービス」というと「RESTful Web サービス」を指すことが多くなりましたが、いわゆる「SOAP Web サービス」も健在です。

Java でも SOAP による通信を行うための API として JAX-RPC を策定し、J2EE 1.4 に JAX-RPC 1.1 が含まれました。しかし、Java EE 5 で Ease of Development(かんたん開発)を推し進めた結果、JAX-RPC がアノテーションベースで大幅に拡張され(この時の副産物の 1 つが JAXB です)、名称も JAX-WS 2.0 に変更されました。Java EE 6 および Java EE 7 にはアップデートされた JAX-WS 2.2 が含まれています。ここで注意したいところは、Java EE 6 から Java EE 7 にかけて JAX-WS のバージョンが上がっていないことです。Java EE 7 では JMS 2.0、JAX-RS 2.0 といった大物アップデートが存在する一方、JAX-WS 2.2 は Java EE 6 から据え置きとなっています(このことが実はある欠点に結びついてきます)。

実は SOAP でもベンダー間によって拡張部分の解釈が異なり、相互接続性に問題がある場合もあります。GlassFish 4.1 に関して言えば、JAX-WS 実装である Metro は .NET Framework 3.5 との相互接続性が(開発時の Sun と Microsoft のテストによって)保証されています。

Metro は純粋な JAX-WS 実装として新たに開発されたもので、動作も高速な部類に入ります。Metro は SOAP Web サービスの Java 実装系としては後発の部類に入り、Apache Axis 2 などに見られるような JAX-WS がカバーしていない手段(すなわち JAX-WS 策定時点で非標準のもの)は排除する形で設計されています。また、入出力についても JAXB に一本化されています。JAX-WS 自体は JAXB との組み合わせが前提のため問題ありませんが、古くからある実装では互換性維持のため JAX-RPC 時代の方式を利用できる場合もあるようです。

GlassFish だけでなく Java SE 6 以降の JDK/JRE にも Metro は含まれており(クライアント API を中心としたサブセットといくつかのツールがバンドルされています)、JDK だけでも Web サービスを作成・公開できるようになっています。

WebLogic も最新の 12c では JAX-WS 実装として Metro(あるいはその拡張)を採用しているようです。

2. JAX-WS で Web サービスを公開する

JAX-WS で Web サービスを公開するのは非常に簡単(サービスの公開だけなら JAX-RS より楽)です。以下のクラス 1 つを .war に含めて GlassFish にデプロイしてください。

package jp.coppermine.samples.wssample;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService
public class Sample {
  
  private String message;
  
  @WebMethod
  public String sayHello(String name) {
    return "Hello, " + name;
  }
}

クラスには @WebService、メソッドには @WebMethod を付加するのが鍵です。もしメソッドの戻り値が void の場合は @Oneway も付加します。

動作確認はブラウザで行います。URL は .war のコンテキストルートを wssample とすると、クラス名の末尾に Service を加えて http://localhost:8080/wssample/SampleService とします。

webservice.png

また、その Web サービスがどのようなメソッドを提供しているのかを表す WSDL は、同様に http://localhost:8080/wssample/SampleService?wsdl で取得できます。

<!--
 Published by JAX-WS RI (http://jax-ws.java.net). RI's version is Metro/2.3.1-b419 (branches/2.3.1.x-7937; 2014-08-04T08:11:03+0000) JAXWS-RI/2.2.10-b140803.1500 JAXWS-API/2.2.11 JAXB-RI/2.2.10-b140802.1033 JAXB-API/2.2.12-b140109.1041 svn-revision#unknown. 
-->
<!--
 Generated by JAX-WS RI (http://jax-ws.java.net). RI's version is Metro/2.3.1-b419 (branches/2.3.1.x-7937; 2014-08-04T08:11:03+0000) JAXWS-RI/2.2.10-b140803.1500 JAXWS-API/2.2.11 JAXB-RI/2.2.10-b140802.1033 JAXB-API/2.2.12-b140109.1041 svn-revision#unknown. 
-->
<definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://wssample.samples.coppermine.jp/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://wssample.samples.coppermine.jp/" name="SampleService">
  <types>
    <xsd:schema>
      <xsd:import namespace="http://wssample.samples.coppermine.jp/" schemaLocation="http://localhost:8080/wssample/SampleService?xsd=1"/>
    </xsd:schema>
  </types>
  <message name="sayHello">
    <part name="parameters" element="tns:sayHello"/>
  </message>
  <message name="sayHelloResponse">
    <part name="parameters" element="tns:sayHelloResponse"/>
  </message>
  <portType name="Sample">
    <operation name="sayHello">
      <input wsam:Action="http://wssample.samples.coppermine.jp/Sample/sayHelloRequest" message="tns:sayHello"/>
      <output wsam:Action="http://wssample.samples.coppermine.jp/Sample/sayHelloResponse" message="tns:sayHelloResponse"/>
    </operation>
  </portType>
  <binding name="SamplePortBinding" type="tns:Sample">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
    <operation name="sayHello">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
  <service name="SampleService">
    <port name="SamplePort" binding="tns:SamplePortBinding">
      <soap:address location="http://localhost:8080/wssample/SampleService"/>
    </port>
  </service>
</definitions>

この Web サービスにアクセスする側、すなわちクライアントは、WSDL を読み取って Web サービスが提供しているメソッドの名前や入出力形式を判別します。

3. クライアントを作成する

一般に SOAP Web サービスのクライアントは、前述のように WSDL の記述に沿って入力 XML メッセージの作成と出力 XML メッセージの解析を行います。JAX-WS では XML の入出力そのものは隠蔽され、入出力データを含む必要な設定項目を Java で記述するだけで Web サービスを利用することができます。設定項目の多くは WSDL からの転記であるため、WSDL からプロキシと呼ばれるコードを生成して、それを利用することで実装の手間を大幅に省くことができます。

3.1. プロキシを用いる場合

WSDL からプロキシを生成するには、wsimport というツールを使用します。wsimport は GlassFish に含まれる(as-install/bin/wsimport)ほか、JDK にも付属しています。

GlassFish の wsimport はスクリプトですが、Windows 版 JDK の wsimport は .exe になっています。ただし、いずれも JAX-WS の WsImport ツールのラッパーです。

wsimport はコマンドラインから以下のように実行します。

C:\ws\client>wsimport -keep http://localhost:8080/wssample/SampleService?wsdl

wsimport には引数に WSDL の URL を指定すればプロキシを構成するコードを自動で生成します。ただし、そのままではクラスファイルのみ生成する(ソースファイルは削除してしまう)ため、オプション -keep を指定してソースファイルを残すようにします。あるいはオプション -s sourceDir と -d classDir を指定してソースファイルとクラスファイルを別々の場所に格納することでソースファイルを残すこともできます。

今回のサンプルの WSDL から生成されるプロキシのソースファイルは以下の通りです。

Sample サービス

ソースファイル名概要
ObjectFactory.java JAXB 連携のために必要なクラス
package-info.java パッケージの説明(@XmlSchema が指定されている)
Sample.java Sample サービスのポート
SampleService.java Sample サービス
SayHello.java メソッド sayHello への入力(引数)
SayHelloResponse.java メソッド sayHello からの出力(戻り値)

プロキシを用いて Web サービスを呼び出すコードの例を以下に示します。

SampleService service = new SampleService();
Sample port = service.getSamplePort();

final String input = "world";
String output = port.sayHello(input);

System.out.println(output);

説明のため、わざと冗長なコードにしました。実際の Web サービスでは上記 input と output のクラスは JAXB で XML と相互変換可能なクラスになります。今回は非常に単純な例なので文字列リテラルとなりました。

なお、コンテナ(サーブレットコンテナまたは EJB コンテナ、Web サービスも含む)で実行されるコードでは、@WebServiceRef アノテーションを用いたサービスとポートのインジェクションがサポートされます。以下に例を示します。

@WebServlet("/wsclient")
public WebServiceClientServlet extends HttpServlet {
  @WebServiceRef
  private SampleService service;
  
  @Override
  protected void doGet(HttpRequest request, HttpResponse response) throws ServletException, IOException {
    // ...
    Sample port = service.getPort(Sample.class);
    String output = port.sayHello("world");
    // ...
  }
}
@WebServlet("/wsclient")
public WebServiceClientServlet extends HttpServlet {
  @WebServiceRef(SampleService.class)
  private Sample port;
  
  @Override
  protected void doGet(HttpRequest request, HttpResponse response) throws ServletException, IOException {
    // ...
    String output = port.sayHello("world");
    // ...
  }
}

[2014-12-10 注記] 仕様 (@WebServiceRef を規定している JSR 181) では、上記のようなリソース・インジェクションが可能とされていますが、同一ドメインに Web サービスとそのクライアントをデプロイすると XML namespace が衝突してしまうようです。詳細が分かり次第、追記します。

3.2. プロキシを用いない場合

プロキシを用いないクライアントは、プロキシが行っていた処理をすべて自分で実装する必要があります。手間がかかる方法なので第一選択肢とはなりませんが、一方でプロキシ自動生成では実現できない非同期通信を行うことが可能です。

サービスの取得は、以下のようなコードになります。実際には SampleService のスーパークラスである Service のインスタンスを取得することになります。

URL url = new URL("http://localhost:8080/wssample/SampleService?wsdl");
QName serviceName = new QName("http://"http://wssample.samples.coppermine.jp/", "SampleService");
Service service = Service.create(url, serviceName);

次にポートを取得し、メソッドを実行します。自動生成されたプロキシのコードを見る限り、ポートの取得は簡単そうに見えます。

Sample sample = service.getPort(new QName("http://wssample.samples.coppermine.jp/", "SamplePort"), Sample.class);

しかし、Sample の中身を見るとそれはただの幻想だったことがわかります。以下に自動生成されたコードを示します(編集の都合でタブ幅だけは変更してあります)。

package jp.coppermine.samples.wssample;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.ws.Action;
import javax.xml.ws.RequestWrapper;
import javax.xml.ws.ResponseWrapper;


/**
 * This class was generated by the JAX-WS RI.
 * JAX-WS RI 2.2.9-b130926.1035
 * Generated source version: 2.2
 * 
 */
@WebService(name = "Sample", targetNamespace = "http://wssample.samples.coppermine.jp/")
@XmlSeeAlso({
  ObjectFactory.class
})
public interface Sample {


  /**
   * 
   * @param arg0
   * @return
   *     returns java.lang.String
   */
  @WebMethod
  @WebResult(targetNamespace = "")
  @RequestWrapper(localName = "sayHello", targetNamespace = "http://wssample.samples.coppermine.jp/", className = "jp.coppermine.samples.wssample.SayHello")
  @ResponseWrapper(localName = "sayHelloResponse", targetNamespace = "http://wssample.samples.coppermine.jp/", className = "jp.coppermine.samples.wssample.SayHelloResponse")
  @Action(input = "http://wssample.samples.coppermine.jp/Sample/sayHelloRequest", output = "http://wssample.samples.coppermine.jp/Sample/sayHelloResponse")
  public String sayHello(
      @WebParam(name = "arg0", targetNamespace = "")
      String arg0);

}

Service#getPort(Class) と Sample インタフェースのメタ情報から、Sample の実装クラスを動的に生成し、その過程で SOAP のプロトコル・バインディング(メソッドの入力(リクエスト)と出力(レスポンス)の関連づけ)を行っているようです。手動でプロトコル・バインディングを行うには、BindingProvider インタフェースを使用しますが、実際にはサブインタフェース Dispatch の実装を Service から取得できるため、こちらを使用します。

では、Dispatch オブジェクトを取得するコードを以下に示します。

QName portName = new QName("http://wssample.samples.coppermine.jp/", "SamplePort");
JAXBContext context = JAXBContext.newInstance("jp.coppermine.samples.wssample");
Dispatch<Object> dispatch = service.createDispatch(portName, context, Service.Mode.PAYLOAD);

続いて、入力 SayHello クラスと出力 SayHelloResponse を作成し、Web サービスを実行します。まず SayHello を以下に示します。これは自動生成されたコードと同等です。

package jp.coppermine.samples.wssample;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "sayHello", propOrder = { "arg0" })
public class SayHello {
  
  protected String arg0;
  
  public String getArg0() {
    return arg0;
  }
  
  public void setArg0(String arg0) {
    this.arg0 = arg0;
  }
}

SayHelloResponse を以下に示します。これも自動生成されたものと同等です。

package jp.coppermine.samples.wssample;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "sayHelloResponse", propOrder = { "_return" })
public class SayHelloResponse {
  
  @XmlElement(name = "return")
  protected String _return;
  
  public String get_return() {
    return _return;
  }
  
  public void set_return(String value) {
    this._return = value;
  }
}

Web サービス実行までを以下に示します。

SayHello sayHello = new SayHello();
sayHello.setArg0("world");

ObjectFactory factory = new ObjectFactory();
JAXBElement<SayHello> request = factory.createSayHello(sayHello);

なお、ObjectFactory は自動生成されたコードと同じものを使用します。最低限必要な部分を以下に示します。

package jp.coppermine.samples.wssample;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {

  private final static QName _SayHello_QNAME = new QName("http://wssample.samples.coppermine.jp/", "sayHello");
  private final static QName _SayHelloResponse_QNAME = new QName("http://wssample.samples.coppermine.jp/", "sayHelloResponse");
  
  @XmlElementDecl(namespace = "http://wssample.samples.coppermine.jp/", name = "sayHello")
  public JAXBElement createSayHello(SayHello value) {
    return new JAXBElement(_SayHello_QNAME, SayHello.class, null, value);
  }
  
  @XmlElementDecl(namespace = "http://wssample.samples.coppermine.jp/", name = "sayHelloResponse")
  public JAXBElement createSayHelloResponse(SayHelloResponse value) {
    return new JAXBElement(_SayHelloResponse_QNAME, SayHelloResponse.class, null, value);
  }
}

JAX-WS は入力・出力とも JAXB で変換する前提となっています。今回は単純な文字列の送受信のため、これを XML 要素にラップする必要があります。そこで ObjectFactory クラスが必要となるわけです。

入力を設定し Web サービスを実行するところまでのコードを示してきたので、残りの Web サービスを実行して出力(戻り値)を受け取るコードを以下に示します。

JAXBElement<SayHelloResponse> message = (JAXBElement<SayHelloResponse>) dispatch.invoke(request);
SayHelloResponse response = message.getValue();

Dispatch<Object>#invoke(Object) の戻り値は Object のため、JAXBElement<SayHelloResponse> でキャストしています。

ここまでのコード(SayHello、SayHelloResponse、ObjectFactory の各クラス実装を除く)をまとめます。

URL url = new URL("http://localhost:8080/wssample/SampleService?wsdl");
QName serviceName = new QName("http://wssample.samples.coppermine.jp/", "SampleService");
Service service = Service.create(url, serviceName);

QName portName = new QName("http://wssample.samples.coppermine.jp/", "SamplePort");
JAXBContext context = JAXBContext.newInstance("jp.coppermine.samples.wssample");
Dispatch<Object> dispatch = service.createDispatch(portName, context, Service.Mode.PAYLOAD);

SayHello sayHello = new SayHello();
sayHello.setArg0("world");

ObjectFactory factory = new ObjectFactory();
JAXBElement<SayHello> request = factory.createSayHello(sayHello);

@SuppressWarnings("unchecked")
JAXBElement<SayHelloResponse> message = (JAXBElement<SayHelloResponse>) dispatch.invoke(request);
SayHelloResponse response = message.getValue();

System.out.println(response.get_return());

自動生成されてプロキシを使わなかった場合、コードは複雑になります。さらに自動生成から完全に逃れられるわけではなく、入出力(SayHello、SayHelloResponse)や ObjectFactory は原則として自動生成されたものを利用することになります(今回は自動生成されたコードをもとに手書きしましたが)。

一方で、自動生成されたプロキシを使わないことで、処理が WSDL の記述内容に従っていることがわかります。今回は送受信のペイロードのみを API に渡しましたが、Service#createDispatch メソッドの引数指定によっては、その外側の SOAP エンベロープを含めることもできます。また Dispatch には invoke メソッド以外に invokeAsync メソッドが用意されており、こちらを使うことで非同期通信を行うことができます(結果をリスナーで受け取る形になります)。非同期通信については今回触れませんが、Dispatch などの低水準 API を用いることで可能であることは覚えておいて損はないでしょう。

4. JAX-WS の抱える問題点

4.1. クライアント作成の煩雑さ

JAX-WS のクライアントは作成に手間がかかります。自動生成されたプロキシを用いる場合は WSDL から生成されたコードを取り込んで呼び出すだけですが、それでも JAX-RS に比べると複雑です。他の RPC プロトコルに比べるとシンプルとはいえ SOAP の仕様もそれなりに複雑ですので、そこは致し方ないでしょう。むしろ、JAX-WS で Web サービスが非常に簡単に公開できてしまうことを評価すべきです。

4.2. EJB エンドポイントのパッケージング

JAX-WS のエンドポイント(サービスクラス、@WebService を付加したクラス)は、EJB(Stateless Bean)として定義することができます。しかし、EJB にしたエンドポイントは .war に含めることができません(.war に含めても Web サービスとして認識しません)。Java EE 7 ではほとんどの EJB は .war に含めることができますが、JAX-WS のエンドポイントは数少ない例外となります。

4.3. CDI 連携

JAX-WS は CDI 1.0 と連携しますが、CDI 1.1 には対応できません。JAX-WS と CDI を組み合わせる場合は beans.xml を省略できず、http://java.sun.com/xml/ns/javaee/beans_1_0.xsd スキーマで定義するか、中身を空にしなければなりません。また、エンドポイントに対してスコープ宣言のアノテーション(@ApplicationScoped、@SessionScoped、@RequestScoped、@ConversationScoped、@Dependent)は付加できません。

5. まとめ

JAX-WS は Java で SOAP 通信を行う上で唯一とも言える手段で、少なくともサービス側の記述は非常に簡潔に行えます。クライアントの実装は手間がかかりますが、リソース・インジェクションの使えるところでは多少楽にはなります。ただ、Java EE 7 になって他の API に比べて若干取り残された感があり、特に今後成長が見込まれる CDI との連携に不備があるなど、不安材料も抱えています。

明日は JAX-RS 2.0 と Jersey について取り上げる予定です。

参考文献

この記事は GlassFish Advent Calendar 2014 の 9 日目です。昨日は「GlassFish 4.1で始めるWebサービス&MQ通信―(1)JMS」です(都合により 8 日目と 9 日目を入れ替えました)。

今回は GlassFish 4.1 向けに JAX-RS の単体テストを行う上で知っておくと便利なテクニックについてお話します。GlassFish 4.1 向け、と銘打ってはいますが、Jersey 2.5.1 をバンドルしている WebLogic 12c (12.1.3) でもライブラリの依存関係解決方法は異なるものの、主要なコードは適用できるはずです。

GlassFish の JAX-RS 実装である Jersey には、Jersey Test Framework と呼ばれるテストツールが備わっており、JUnit と組み合わせることでリソースクラスの単体テストを行うことができます。

JAX-RS 2.0 では CDI との連携が追加され、リソースクラス内で @Inject アノテーションを用いた DI が可能になりました。しかし Jersey Testing Framework は CDI をサポートしないため、そのままではテストを実行することができません。Weld SE を利用して JUnit に CDI サポートを追加するライブラリも存在しますが、Jersey Test Frameworkと相性が悪いようで組み合わせることができません。

幸いなことに、Jersey は依存ライブラリー HK2 の機能を利用することで、手動で @Inject を解決することができます。今回は HK2 の機能を利用してCDIと連携するリソースクラスの単体テストを行う方法をご紹介します。HK2 は GlassFish の主要な構成要素であるため IDE がパスを解決さえできれば他に何も要らないというメリットがあります。また、HK2 による @Inject の解決は Google Guice のbind と似ていますので、Guice の利用経験があれば容易に理解できるでしょう。

参考まで、Jersey 2.x と CDI (Weld 2.x) の連携には HK2 が関わっています。Jersey 2.x の場合 JAX-RS Client API を使うだけで HK2 をはじめ多くのライブラリが一緒に付いてくるのは、このような構成によるものです。

では、具体例を見てみましょう。事前準備として、pom.xml に以下の依存関係を追加しておいてください。

<dependencies>
  ...
  <!-- JUnit -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-core</artifactId>
    <version>1.3</version>
    <scope>test</scope>
  </dependency>
  
  <!-- Jersey Test Framework -->
  <dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>2.10.4</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jsonp-jaxrs</artifactId>
    <version>1.0</version>
    <scope>test</scope>
  </dependency>
  
  <!-- HK2 -->
  <dependency>
    <groupId>org.glassfish.hk2</groupId>
    <artifactId>hk2-api</artifactId>
    <version>2.3.0-b07</version>
  </dependency>
  ...
</dependencies>

Eclipse を利用している場合は、Eclipse 側のビルド・パス解決で JUnit のライブラリを含めてしまうことも多いため、pom.xml に JUnit の依存関係は入れなくても動きます。同じことは HK2 にも当てはまります。ただし、他の IDE でテストを実行することも考慮してサボらずに依存関係を明記します。

この例では jsonp-jaxrs(JSON-P の JAX-RS 連携モジュール)を依存関係に含めています。ドキュメントにはなかったのですが、これがないと jersey-test-framework-provider-grizzly2 が動作しなかったため、おまじないのつもりで追加しています。ドキュメントと実装のどちらが間違っているのかは不明ですが。

次にテスト対象のリソースクラスです。このクラスは Twitter 検索実行のファサードとなるもので、クエリー・パラメーターに検索文字列と最大取得件数を指定しています。

package jp.coppermine.examples.twitter;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;

import java.util.List;

import javax.enterprise.context.RequestScoped;
import javax.validation.constraints.NotNull;
import javax.ws.rs.DefaultValue;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import jp.coppermine.examples.twitter.Tweet;

@Path("/twitter")
@RequestScoped
public class TwitterResource {
  
  // TweetFinder の実装が Twitter 検索を実行する。
  // 検索結果は TwitterFinder#search(String, int) で取得する。
  @Inject
  private TweetFinder finder;
  
  @GET
  @Path("/search")
  @Produces({APPLICATION_XML, APPLICATION_JSON})
  public List<Tweet> search(@NotNull @QueryParam("q") String keyword, @DefaultValue("15") @QueryParam("count") int count) {
    List<Tweet> tweets = finder.search(keyword, count);
    return tweets.size() > count ? tweets.subList(0, count) : tweets;
  }
}

なお、上記 TweetFinder は以下のようなインタフェースです。

package jp.coppermine.examples.twitter;

import java.util.List;

import jp.coppermine.examples.twitter.Tweet;

public interface TweetFinder {
  
  List<Tweet> search(String keyword, int count);

}

今回のテストでは実際に Twitter API を呼び出す Stateless Bean 実装ではなく、あらかじめ用意したテストデータを返すモックアップ(TweetFinderMock)を使うこととします。

続いて、リソースクラスの単体テストクラスを以下に示します。

package jp.coppermine.example.twitter;

import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.util.List;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;

import jp.coppermine.example.twitter.Tweet;

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

public class TwitterResourceTest extends JerseyTest {
  
  /**
   * DI の解決ルールを記述したクラス
   */
  public static class TestBinder extends AbstractBinder {
    @Override
    protected void configure() {
      bind(TweetFinderMock.class).to(TweetFinder.class);
    }
  }
  
  /**
   * Application クラス(またはそのサブクラス)を返す。
   * ここでは Application のサブクラスである ResourceConfig クラスを用いて
   * TestBinder とテスト対象のリソースクラス(TwitterResource)を関連づけ、
   * TwitterResource の DI を解決する。
   */
  @Override
  protected Application configure() {
    return new ResourceConfig().register(new TestBinder()).register(TwitterResource.class);
  }
  
  @Test
  public void testSearch_count99() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").queryParam("count", 99).request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(99));
  }
  
  @Test
  public void testSearch_count100() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").queryParam("count", 100).request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(100));
  }
  
  @Test
  public void testSearch_count101() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").queryParam("count", 101).request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(101));
  }
  
  @Test
  public void testSearch_defaultCount() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(15));
  }
  
  @Test
  public void testSearch_noKeyword() {
    Response response = target("twitter").path("search").queryParam("count", 100).request().get();
    assertThat(response.getStatus(), is(400));
  }
}

ポイントは、HK2 API の AbstractBinder のサブクラス TestBinder を作成して DI の関連づけルールを記述しておくことと、Application のサブクラスである ResourceConfig クラスを利用して TestBinder とテスト対象の TwitterResource クラスを関連づけ DI を解決しているところです。この辺りの仕組みは Google Guice と類似していますが、bind の source と destination の順序が異なるなどの差異があります。また、今回の例にはありませんが、実装とインタフェースの結びつけだけでなく、実装を取得するファクトリーとインタフェースの結びつけを記述することもできます。

最近は様々なテストツールが登場し、テストの効率が向上しています。しかし、すべての開発現場においてそれらのツールを導入できるとは限りません。そのような時に、今回ご紹介した付属ライブラリだけによる実現方法は有力な選択肢になることでしょう。

本稿執筆に当たっては、「Playing with JerseyTest (Jersey 2.5.1 and DI)」および筆者が同記事を元に実務で実装したテストクラスを参考にしました。

明日は「GlassFish 4.1で始めるWebサービス&MQ通信―(2)JAX-WS」を予定しています。

この記事は Java EE Advent Calendar 2014 および GlassFish Advent Calendar 2014 の 8 日目です。昨日は @yamadamn さんの「スレッドダンプから見るWebLogic Serverの世界」(Java EE) および @backpaper0 さんの「よく使うasadminのコマンドを紹介する」(GlassFish)です。

本日は、「GlassFish 4.1で始めるWebサービス&MQ通信」の第 1 回で、JMS 2.0 と OpenMQ について取り上げます。このシリーズでは、Java EE 7 の文献ではあまり取り上げられないアプリケーションのデプロイとその前準備について取り上げていきます。第 2 回は明後日の予定で JAX-WS と Metro を取り上げます。第 3 回は 12 月 11 日で JAX-RS と Jersey を取り上げる予定です。

1. はじめに

今回の主役である JMS(Java Messaging Service)は、Java で MQ(Message Queue)通信を利用するための API です。MQ 通信とは、送信側と受信側の間にキューを設けて、メッセージを非同期に送受信する方式を言います。MQ 通信を実現するミドルウェアとしては IBM の WebSphere MQ が歴史・実績とも抜きんでており、世界中の大規模システムで利用されています。その他にも多数の製品があり、GlassFish には OpenMQ という製品がバンドルされています。こう言った製品を総称して MoM(Message-oriented Middleware)と言います(もっとも、現場ではこんな格好を付けた呼び方はあまりせず、単に MQ と呼ぶことが多いです)。

さて、MQ の何がそんなに嬉しいかというと、(キューが動作していれば)受信側の都合を考えずにメッセージを送信できることに尽きます。例えば、送信側が 24x7 稼働のオンラインシステムで、受信側が平日夜間しか動作しないバッチシステムの場合を考えてみましょう。MQ がなければ、オンラインシステム側がバッチシステムの稼働時間に合わせてメッセージ送信のタイミングを調整しなければなりません。しかし MQ があればオンラインシステムは任意のタイミングでメッセージを送信すれば良く、バッチシステムも自身の稼働時間中にメッセージを受信すれば良いのです。このように MQ は送受信双方の特性や性能が大きく違うシステム間連携で利用されます。

JMS を利用すると、Java (主に Java EE)アプリケーションで MQ を簡単に扱うことができます。また、利用する MQ 製品(WebSphere MQ、OpenMQ、Active MQ、HornetQ など)を問わず統一された API で MQ 通信を利用できます。

今回は GlassFish 4.1 における JMS と OpenMQ の基本的な使い方についてご紹介します。JMS には 2 種類の通信方式、Point-to-Point と Publish-Subscribe があり用途によって使い分けますが、ここでは基本的な MQ の使い方である Point-to-Point を取り上げます。

2. GlassFish と OpenMQ

GlassFish 4.1 は JMS の最新規格である JMS 2.0 をサポートし、MQ 製品である OpenMQ をバンドルしています(ただし、Web Profile には JMS と OpenMQ は含まれません)。

JMS ではキューを管理するサービスをブローカといいます。GlassFish を起動すると TCP ポート 7676 でブローカーが 1 つ起動します。このブローカは GlassFish 管理ブローカ(GlassFish-managed broker)と呼ばれる特殊な OpenMQ ブローカで、サービスの制御を GlassFish が管理します。GlassFish にバンドルされている OpenMQ はデフォルトで GlassFish 管理ブローカとして動作するようになっています。

まずは GlassFish を起動して、ブローカの動作を確認してみましょう。as-install-parent/mq/bin 以下に OpenMQ 一式が配置されています。今回はこの中から imqcmd コマンドを利用します。

imgcmd list bkr でブローカの一覧を表示します。ブローカが動作しているかどうかも確認できます。デフォルトのユーザー名とパスワードはともに admin です。

C:\glassfish4\mq\bin>imqcmd list bkr
Username: admin
Password:
Listing all the brokers in the cluster that the following broker is a member of:


-------------------------
Host         Primary Port
-------------------------
localhost    7676

Cluster ID
Cluster is Highly Available   false

-------------------------------
      Address           State
-------------------------------
192.168.xxx.xxx:7676  OPERATING

Successfully listed brokers in the cluster.

次にキューを作成します。キューの作成も imqcmd コマンドで行えます。myQueue という名前のキューを作成してみましょう。

C:\glassfish4\mq\bin>C:\glassfish4\mq\bin>imqcmd create dst -t q -n myQueue
Username: admin
Password:
Creating a destination with the following attributes:

Destination Name    myQueue
Destination Type    Queue

On the broker specified by:

-------------------------
Host         Primary Port
-------------------------
localhost    7676

Successfully created the destination.

imqcmd の create dst サブコマンドでキューを作成できます。オプション -n に名前(ここでは myQueue)を指定し、オプション -t にキューを示す q を指定します。

参考までに、Publish-Subscribe で使用するトピック(Topic)という特殊なキューを作成する場合はオプション -t にトピックを示す t を指定します。

なお、キューの削除には destroy dst サブコマンドを使用します。オプションは create dst と同様です。先に作成した myQueue を削除する場合は以下のようにします。

C:\glassfish4\mq\bin>imqcmd destroy dst -t q -n myQueue
Username: admin
Password:
Destroying the destination where:

------------------------------------
Destination Name    Destination Type
------------------------------------
myQueue             Queue

On the broker specified by:

-------------------------
Host         Primary Port
-------------------------
localhost    7676

Are you sure you want to destroy this destination? (y/n)[n] y

Successfully destroyed the destination.

キューの作成と削除は、imqadmin(Message Queue Administration Console)という GUI でも実行できます。今回は imqadmin の説明は割愛します。

キューが作成できたら、GlassFish の管理コンソールで設定を行いましょう。設定箇所は[リソース]>[JMS リソース]以下にある、[接続ファクトリ]と[宛先リソース]です。

まずは接続ファクトリから見てゆきましょう。

connection-factories.png

GlassFish には localhost のブローカに接続するための jms/__defaultConnectionFactory があらかじめ定義されており、特に指定しない場合この接続ファクトリが使用されます。独自の接続ファクトリを利用するケースについては、後の方でご紹介します。

次に宛先リソースを見てみましょう。

destination-resources.png

こちらは初期状態では何も定義されていません。ここに先ほど作成した myQueue を追加してみましょう。

create-destination.png

設定項目は、以下のようにします。

  • JNDI 名: jms/myQueue (アプリケーションから参照するための任意の名前)
  • 物理宛先名: myQueue (先に作成した myQueue を指定する
  • リソース・タイプ: javax.jms.Queue (キューであることを示す)

これで GlassFish 上のアプリケーションから myQueue に対してアクセスする準備が整いました。

実のところ、先にキューを作成しなくても、物理宛先名で指定された名前のキューが自動的に作成されます。しかし、JMS の背後にある OpenMQ を全く意識せずに使い続けると意図しないキューが作成される、あるいは意図したキューが作成されないなどのトラブルを引き起こしかねないため、敢えて OpenMQ の imqcmd コマンドでキューを作成・削除する方法をご紹介しました。

3. MQ の送受信を行うアプリケーションを作成する

ここからは、JMS を利用した非常に簡単なアプリケーションを作成します。

このアプリケーションは、文字列をキューに送信する機能と、キューから文字列を受信する機能だけを持ちます。今回、きちんとした UI は用意せず、JAX-RS のリソースに対し HTTP クライアントで直接アクセスする仕様とします。

最初に、JAX-RS のリソースクラスを示します。なお、ルート URI は /app、リソースの URI は /queue とし、キューへの送信には POST、キューからの受信には GET を使うものとします。

package jp.coppermine.samples.mq;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

@Path("/queue")
@RequestScoped
public class MqTestResource {
  
  @Inject
  private Sender sender;
  
  @Inject
  private Receiver receiver;
  
  @POST
  public void send(@FormParam("message") String message) {
    sender.send(message);
  }
  
  @GET
  public String receive() {
    return receiver.receive();
  }
}

キューへの送信は Sender クラス、キューからの受信は Receiver クラスが行います。この後ソースコードを示しますがいずれもステートレス・セッション Bean(EJB)です。

参考まで、リソースクラスに付加された @RequestScoped アノテーションは、このクラスのスコープを明示します。CDI 1.1 では beans.xml を省略できますが、その場合にはリソースクラスのスコープを明示しなければ @Inject が有効になりません(beans.xml の bean-discovery-mode が annotated と見なされる)。詳細については、上妻さん(@n_agetsu)のブログ記事「Java EE環境におけるCDIのデフォルト化」を参照してください。

続いて Sender クラスです。このクラスはキューへの送信を担当します。

package jp.coppermine.samples.mq;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.jms.Destination;
import javax.jms.JMSConnectionFactory;
import javax.jms.JMSContext;

@Stateless
public class Sender {
  
  @Inject
  private JMSContext context;
  
  @Resource(name = "jms/myQueue")
  private Destination myQueue;
  
  public void send(String message) {
    context.createProducer().send(myQueue, message);
  }
}

JMSContext context; に対して @Inject アノテーションを付加することで、デフォルトの接続ファクトリに基づいた JMSContext を取得することができます。Destination myQueue; に対して @Resource(name = "jms/myQueue) アノテーションを付加して JNDI 解決を行います。その上で JMSContext のオブジェクトから JMSProducer のオブジェクトを取得し、JMSProducer#send(Destination, String); でキューにメッセージを送信します。send メソッドはいくつかのオーバーロード・メソッドが存在し、メッセージのクラスを選択できます。

Sender と Receiver は、今回は EJB で作成しましたが、この規模であれば EJB である必要はありません。ただし、実際の MQ 通信では送受信とデータベースアクセスが連動するケースも多く見られるため、そのような場合は EJB を使用した方がトランザクション管理が完結になります。

以上で送信部は完了です。昔の JMS はいくつもの前処理と例外処理を必要とし、正直筆者も避けていたのですが、JMS 2.0 ではわずかな前処理があるだけです(例外は JPA 同様すべてランタイム例外になっている)。

最後に Receiver クラスを示します。このクラスはキューからメッセージを受信します。

package jp.coppermine.samples.mq;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.jms.Destination;
import javax.jms.JMSConnectionFactory;
import javax.jms.JMSContext;

@Stateless
public class Receiver {
  
  @Inject
  private JMSContext context;
  
  @Resource(name = "jms/myQueue")
  private Destination myQueue;
  
  public String receive() {
    return context.createConsumer(myQueue).receiveBody(String.class, 1000L);
  }
}

基本的な部分は Sender クラスと同じです。ただし、JMSContext のオブジェクトから JMSConsumer のオブジェクトを取得し、JMSConsumer#receiveBody(Class<T>, long); を呼び出しているところが異なります。receive 系メソッドも何種類かあります。ここで使用したのはメッセージのクラスとタイムアウト時間を指定する形式です。

では、実行してみましょう。まず、HTTP クライアントを用いてメッセージ "Hello, world" をキューに送信します。今回は HTTP クライアントに HTTP4e を使用しました。

send.png

実行後はキューにメッセージが 1 件入っているはずです。imqcmd でキューの状態を確認してみましょう。list dst サブコマンドを使用します。

C:\glassfish4\mq\bin>imqcmd list dst
Username: admin
Password:
Listing all the destinations on the broker specified by:

-------------------------
Host         Primary Port
-------------------------
localhost    7676

-----------------------------------------------------------------------------------------------------
   Name     Type    State      Producers        Consumers                      Msgs
                            Total  Wildcard  Total  Wildcard  Count  Remote  UnAck  InDelay  Avg Size
-----------------------------------------------------------------------------------------------------
mq.sys.dmq  Queue  RUNNING  0      -         0      -         0      0       0    0        0.0
myQueue     Queue  RUNNING  0      -         0      -         1      0       0    0        152.0

Successfully listed destinations.

myQueue の Count(件数)が 1 になっていることが確認できました。

次に HTTP クライアントを用いてキューからメッセージを取得します。

receive.png

先ほどと同様に imqcmd でキューの状態を確認しましょう。

C:\glassfish4\mq\bin>imqcmd list dst
Username: admin
Password:
Listing all the destinations on the broker specified by:

-------------------------
Host         Primary Port
-------------------------
localhost    7676

-----------------------------------------------------------------------------------------------------
   Name     Type    State      Producers        Consumers                      Msgs
                            Total  Wildcard  Total  Wildcard  Count  Remote  UnAck  InDelay  Avg Size
-----------------------------------------------------------------------------------------------------
mq.sys.dmq  Queue  RUNNING  0      -         0      -         0      0       0    0        0.0
myQueue     Queue  RUNNING  0      -         0      -         0      0       0    0        0.0

Successfully listed destinations.

myQueue の Count が 0 になっていることがわかります。

4. その他のトピックス

4.1. 他のホストからキューにアクセスする

前章で作成したアプリケーションは、あくまで同一ホスト上のアプリケーションによる MQ 通信でした。しかし現実には、異なるホスト上のアプリケーションが MQ で通信するケースがほとんどです。異なるホスト上のキューに接続する場合は、接続ファクトリを新たに定義する必要があります。

例として、GlassFish 4.1 をインストールした 2 つのホストを用意し、うち一方の OpenMQ ブローカにキューを作成して利用するケースを挙げます。ここではキューを作成するホストを lysithea、作成しないホストを himalia とします。

まず、lysithea と himalia の双方に lysithea のブローカにアクセスするための接続ファクトリを作成します。

create-connection-factory1.png

一般設定は以下のようにします。

  • JNDI 名: jms/myConnectionFactory (アプリケーションから参照するための任意の名前)
  • リソース・タイプ: javax.jms.ConnectionFactory

さらに、以下のプロパティを追加します。

create-connection-factory2.png

  • addressList = lysithea.voyager.coppermine.jp (lyshitea のホスト名、ここでは lysithea の FQDN が左記の通りであると仮定する

また、lyshitea のみキュー myQueue を作成し、lyshitea と himalia の双方の宛先リソースに myQueue を追加します。

さらにアプリケーションの JMSContext オブジェクト取得箇所を以下のように書き換えます。

@Inject
@JMSConnectionFactory("jms/myConnectionFactory")
JMSContext context;

このようにすることで、以降 jms/myConnectionFactory が指し示す先の myQueue、すなわち lysithea の myQueue にアクセスするようになります。

余談ですが、上記の設定を行った場合は himalia 側に myQueue が自動作成されることはありません。接続ファクトリを明示することで lysithea 側に作成された myQueue を左記に参照する(=himalia 側で myQueue を作成する必要はないと判断する)からです。

4.2. キュー間のメッセージ送受信

例えば、次のような通信は可能でしょうか?

lysithea の app → lysithea のキュー → himalia のキュー → himalia の app

JMS の文献ではこのような構成例はあまり見かけません。WebSphere MQ では良く採用されている構成なのですが。JMS で上記の構成を実現するには JMS ブリッジを使用します。JMS ブリッジでは例えば lysithea のブローカが自身のキューと himalia のキューをマッピングするようなことが可能です。OpenMQ には imqbridgemgr というブリッジ管理のコマンドが用意されています。JMS ブリッジについては今回は触れません。

5. まとめ

今回は JMS 2.0 の Point-to-Point 通信にターゲットを絞り、代わりに実行環境の GlassFish と OpenMQ の操作を交えながら、サンプルアプリケーションを作成してみました。JMS には Publish-Subscribe 通信という 1 対 N の通信方式を採ることもでき、EJB の Message-Driven Beans を用いるとメッセージの到着をトリガーにして何らかの処理を実行するアプリケーションを容易に実現できます。JAX-RS のような手軽さや汎用性はありませんが、システム間連携の要所で効果を発揮してくれる有力な API と言えるでしょう。

※今回作成したサンプルは GitHub https://github.com/btnrouge/mq-test にあります。

明日の Java EE Advent Calendar 2014 は hikaruworld@github さんです。GlassFish Advent Calendar 2014 は明日が「GlassFish 4.1でJAX-RS=CDI連携の単体テストを行う方法」、明後日が「GlassFish 4.1で始めるWebサービス&MQ通信―(2)JAX-WS」を予定しています。