めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

JBoss AS 7.1のドメイン構成とEJB3.1のリモート呼び出し

2012/04/16 追記:コンテナ間のEJB呼び出しのSASL認証設定に対応しました。

やること

JBoss AS 7.1では、複数ノードをシングルコンソールで管理する「ドメイン構成」が利用できます。

ここでは、ドメイン構成を利用して、2ノードでコンテナを起動した上で、EJB3.1のリモート呼び出しのサンプルを実行してみます。Jave EE6のEJB3.1では、手軽にEJBが取り扱えることが実感できます。

システム構成

全体構成はこんな感じ。

ドメイン管理のための通信は、管理ネットワークを通じて行います。

ドメイン構成では、各ノードに複数の「サーバ」(コンテナのこと)を起動して、複数ノードの複数サーバを任意にまとめた「サーバグループ」を作成します。アプリケーションのデプロイは、サーバグループ単位で行います。ここでは、「Web_Application_Group」と「EJB_Container_Group」を作成しています。

各ノードはOS(RHEL6.2)の導入まで終わっているものとします。簡単のために、iptablesは停止しています。SELinuxはtargeted modeで有効化しています。

JDKは、RHEL6.2に同梱のOpenJDKです。

# rpm -qa | grep openjdk
java-1.6.0-openjdk-1.6.0.0-1.41.1.10.4.el6.x86_64
# java -version
java version "1.6.0_22"
OpenJDK Runtime Environment (IcedTea6 1.10.4) (rhel-1.41.1.10.4.el6-x86_64)
OpenJDK 64-Bit Server VM (build 20.0-b11, mixed mode)

両ノードの名前解決は/etc/hostsで行なっています。

# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.122.41	node01
192.168.122.42	node02

192.168.3.41	node01m
192.168.3.42	node02m

JBoss AS 7.1の導入

「JBoss-7.1.1.Final Brontes.」を使用します。ダウンロードはこちらから。まずは、node01、node02に共通の作業です。

JBoss AS 7.1のtar.gzを展開します。

# cd /opt
# tar -xvzf /root/work/jboss-as-7.1.1.Final.tar.gz 
# cd /root
# ln -s /opt/jboss-as-7.1.1.Final jboss

デフォルトで用意されているサーバ、サーバグループの定義は消しておきます。

「~/jboss/domain/configuration/domain.xml」の下記のタグを削除します。

    <server-groups>
<!-- ここから -->
        <server-group name="main-server-group" profile="full">
            <jvm name="default">
                <heap size="64m" max-size="512m"/>
            </jvm>
            <socket-binding-group ref="full-sockets"/>
        </server-group>
        <server-group name="other-server-group" profile="full-ha">
            <jvm name="default">
                <heap size="64m" max-size="512m"/>
            </jvm>
            <socket-binding-group ref="ha-sockets"/>
        </server-group>
<!-- ここまで -->
    </server-groups>

「~/jboss/domain/configuration/host.xml」の下記のタグを削除します。

    <servers>
<!-- ここから -->
        <server name="server-one" group="main-server-group">
            <!-- Remote JPDA debugging for a specific server
            <jvm name="default">
              <jvm-options>
                <option value="-Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n"/>
              </jvm-options>
           </jvm>
           -->
        </server>
        <server name="server-two" group="main-server-group" auto-start="true">
            <!-- server-two avoids port conflicts by incrementing the ports in
                 the default socket-group declared in the server-group -->
            <socket-bindings port-offset="150"/>
        </server>
        <server name="server-three" group="other-server-group" auto-start="false">
            <!-- server-three avoids port conflicts by incrementing the ports in
                 the default socket-group declared in the server-group -->
            <socket-bindings port-offset="250"/>
        </server>
<!-- ここまで -->
    </servers>

各ノードの「host名」を指定します。これは、OSのhostnameと異なっても構いません。(のはず。。。)

~/jboss/domain/configuration/host.xml

<host name="host01" xmlns="urn:jboss:domain:1.2">

「host01」の部分は各ノードにあわせて指定してください。ここでは、node01を「host01」、node02を「host02」として指定します。

「~/jboss/domain/configuration/host.xml」でListenするアドレスを指定します。

    <interfaces>
        <interface name="management">
            <inet-address value="${jboss.bind.address.management:node01m}"/>
        </interface>
        <interface name="public">
            <inet-address value="${jboss.bind.address:0.0.0.0}"/>
        </interface>
        <interface name="unsecure">
            <inet-address value="${jboss.bind.address.unsecure:0.0.0.0}"/>
        </interface>
    </interfaces>

"management"には、管理ネットワークに接続したNICを指定して、管理ネットワークからの接続のみを許可しています。「node01m」の部分は、各ノードにあわせて変えてください。(IPアドレスを直書きしてもかまいません。)

次は、node01のみでの作業です。管理コンソールに接続するユーザを登録します。

# ~/jboss/bin/add-user.sh 

What type of user do you wish to add? 
 a) Management User (mgmt-users.properties) 
 b) Application User (application-users.properties)
(a): 

Enter the details of the new user to add.
Realm (ManagementRealm) : 
Username : adminuser
Password : 
Re-enter Password : 
About to add user 'adminuser' for realm 'ManagementRealm'
Is this correct yes/no? yes
Added user 'adminuser' to file '/opt/jboss-as-7.1.1.Final/standalone/configuration/mgmt-users.properties'
Added user 'adminuser' to file '/opt/jboss-as-7.1.1.Final/domain/configuration/mgmt-users.properties'

# ~/jboss/bin/add-user.sh 

What type of user do you wish to add? 
 a) Management User (mgmt-users.properties) 
 b) Application User (application-users.properties)
(a): 

Enter the details of the new user to add.
Realm (ManagementRealm) : 
Username : host02
Password : 
Re-enter Password : 
About to add user 'host02' for realm 'ManagementRealm'
Is this correct yes/no? yes
Added user 'host02' to file '/opt/jboss-as-7.1.1.Final/standalone/configuration/mgmt-users.properties'
Added user 'host02' to file '/opt/jboss-as-7.1.1.Final/domain/configuration/mgmt-users.properties'

管理コンソールを使用するためのユーザ(adminuser)に加えて、node02の"host名"である「host02」もユーザとして登録しています。これは、node02がドメインに参加するために必要になります。

次は、node02のみでの作業です。ドメインのマスタノードを「node01」に指定して、管理接続のためのユーザ/パスワードを指定します。

~/jboss7/domain/configuration/host.xml

<host name="host02" xmlns="urn:jboss:domain:1.2">
    <management>
        <security-realms>
            <security-realm name="ManagementRealm">
                <server-identities>
                    <secret value="aG9nZWhvZ2U="/>
                </server-identities>
(snip)
    <domain-controller>
        <remote host="node01m" port="9999" security-realm="ManagementRealm"/>
    </domain-controller>

上記の「」には、先に登録した「host02」ユーザのパスワードをBASE64にエンコードしたものを指定します。domain-contorllerタグの「」には、ドメインマスタのIPアドレスを指定します。ここでは、管理ネットワーク側のNICのIPを指定しています。(domain-contorllerタグに「」を指定すると、このノードがドメインマスタになります。デフォルトはこれになっています。)

ちなみに、BASE64エンコードは次のコマンドで可能です。

# echo -n "hogehoge" | base64
aG9nZWhvZ2U=

JBossの起動と構成

いよいよJBossを起動します。それぞれのノードで次を実行します。

# ~/jboss/bin/domain.sh

フォアグラウンドで起動ログがずらずら流れてきます。両ノードの起動が完了して、node01のログに次のメッセージが表示されれば、両ノードによるドメインが無事に構成されています。

[Host Controller] 14:27:47,081 INFO  [org.jboss.as.domain] (domain-mgmt-handler-thread - 1) JBAS010918: Registered remote slave host "host02", JBoss AS 7.1.1.Final "Brontes"

Webブラウザで「http://192.168.3.41:9990/console」を開いて、先に設定した「adminuser」でログインするとドメインの管理コンソールが開きます。

はじめに、サーバグループを定義します。画面右上の「Profile」→画面左の「Server Groups/Group Configurations」を選択して、次の2種類のサーバグループを定義します。

Name: EJB_Container_Group
Profile: default
Socket Binding: standard-sockets

Name: EJB_Container_Group
Profile: default
Socket Binding: standard-sockets

次に、サーバ(コンテナ)を定義します。画面右上の「Server」→画面左の「Server Configurations」を選択します。画面左上の「Host:」プルダウンから、host01を選択して、次のサーバを定義します。

Name: host01_server01
Server Group: Web_Application_Group
Port Offset: 0
Auto Start?:「チェック」

同じく、host02を選択して、次のサーバを定義します。

Name: host02_server01
Server Group: EJB_Container_Group
Port Offset: 0
Auto Start?:「チェック」

EJBサンプルアプリの作成

この内容は、こちらのドキュメントとこちらのディスカッションを参考にしています。

まず、EclipseでJavaプロジェクト「GreeterEJB」を作成して、プロジェクト・プロパティの「プロジェクト・ファセット」に「EJBモジュール」を追加します。クラスパスには「JREシステム・ライブラリー」と「JBoss 7.1 Runtime」あたりを追加しておきます。

EJB3.1で、EJBをつくるときは、最初にクライアントに見せるインターフェース(ビジネスインターフェース)を定義します。

/src/ejb/GreetingServiceBusiness.java

package ejb;

public interface GreetingServiceBusiness {
    public String greet(String name);

    @javax.ejb.Remote
    public interface Remote extends GreetingServiceBusiness {
    }

    @javax.ejb.Local
    public interface Local extends GreetingServiceBusiness {
    }
}

ローカルアクセス(同一のコンテナ内からの参照渡しでのアクセス)とリモートアクセス(異なるコンテナからの値渡しでのアクセス)用に2種類のインターフェースが必要ですが、ここでは、GreetingServiceBusinessインターフェースのインナーインターフェースとしてまとめています。「@javax.ejb.Remote」「@javax.ejb.Local」のアノテーションによって、それぞれ、リモートアクセス用とローカルアクセス用のビジネスインターフェースとして、コンテナから自動的に認識されます。

続いて、EJBの実装クラスを書きます。

/src/ejb/GreetingServiceBean.java

package ejb;

import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import ejb.GreetingServiceBusiness;

@Stateless(name = "GreetEJB")
public class GreetingServiceBean
        implements GreetingServiceBusiness.Local, GreetingServiceBusiness.Remote {

    @Resource
    private SessionContext context;

    @Override
    public String greet(String name) {
        System.out.println(context.getInvokedBusinessInterface().toString());
        return "Hello, " + name + "!";
    }
}

先に定義した2種類のビジネスインターフェースを実装します。アノテーション「@Stateless(name = "GreetEJB")」により、コンテナから、Stateless Beanとして自動的に認識されます。ここで指定したnameは、JNDIでLookupする際のEJB名になります。ここではサンプルとして、@Resourceアノテーションにより、SessionContextのインジェクトも行なっています。

EJBの作成はこれだけです。xmlファイルを書かずに、アノテーションだけでEJBを構成することができました。Eclipseから「GreeterEJB.jar」として、Exportしておきます。

続いて、このEJBを利用するサーブレットを作成します。まず、Eclipseで動的Webプロジェクト「HelloWorld」を作成します。

EJBを利用するアプリケーションには、該当EJBのインターフェースクラスが必要です(実装クラスはもちろん不要)。先に作成した「/src/ejb/GreetingServiceBusiness.java」をプロジェクトにコピーします。

サーブレットのクラスを作成します。

src/web/HelloWorldServlet.java

package web;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import ejb.GreetingServiceBusiness;

@WebServlet("/greeting")
public class HelloWorldServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private static final String JNDI_NAME = "ejb:/GreeterEJB/GreetEJB!ejb.GreetingServiceBusiness$Remote";

    public HelloWorldServlet() {
        super();
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
        GreetingServiceBusiness greeter = null;
        String message = "Hello, World!";

        String name = (String) request.getParameter("name");
        if (name != null) {
            try {
                final Hashtable<String, String> jndiProperties = new Hashtable<String, String>();
                jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
                final Context context = new InitialContext(jndiProperties);
                greeter = (GreetingServiceBusiness) context.lookup(JNDI_NAME);
            } catch (NamingException e) {
                throw new RuntimeException(e);
            }
            message = greeter.greet(name);
        }

        ServletOutputStream out = response.getOutputStream();
        out.println("<html><head><title>EJB Test</title></head>");
        out.println("<body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }
}

アノテーション「@WebServlet("/greeting")」により、コンテナからサーブレットとして認識されます。「HelloWorld/greeting」が対応するURLになります。

サーブレットの中身は単純で、nameパラメータが指定されている場合に、GreetEJBを呼び出して表示するメッセージを構成します。

EJBを呼び出すコードは、本質的には次の5行です。

private static final String JNDI_NAME = "ejb:/GreeterEJB/GreetEJB!ejb.GreetingServiceBusiness$Remote";
final Hashtable<String, String> jndiProperties = new Hashtable<String, String>();
jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
final Context context = new InitialContext(jndiProperties);
greeter = (GreetingServiceBusiness) context.lookup(JNDI_NAME);

JNDI名に「ejb://!<ビジネスインターフェースクラス>」を指定して、lookupするだけで、直接にEJBインスタンス(正確には、そのProxy)を取得して使用することができます。

jndiPropetiesで、JBoss環境に固有のURL_PKG_PREFIXESを使用しています。これをコードの外に出したい場合は、クラスパスの通った場所に下記のプロパティファイルを配置します。

jndi.properties

java.naming.factory.url.pkgs=org.jboss.ejb.client.naming

この場合は、下記のように、jndiPropertiesを指定せずにContextの取得が可能です。

//                final Hashtable<String, String> jndiProperties = new Hashtable<String, String>();
//                jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
//                final Context context = new InitialContext(jndiProperties);
                final Context context = new InitialContext();
                greeter = (GreetingServiceBusiness) context.lookup(JNDI_NAME);

EJBモジュールとサーブレットモジュールが同一コンテナにある場合は、これだけでOKです。作成した2つのプロジェクトをEclipse上でローカルのJBossサーバにデプロイすると、次のように動作確認ができます。

$ curl http://localhost:8080/HelloWorld/greeting
<html><head><title>EJB Test</title></head>
<body>
<h1>Hello, World!</h1>
</body></html>

$ curl http://localhost:8080/HelloWorld/greeting?name=enakai
<html><head><title>EJB Test</title></head>
<body>
<h1>Hello, enakai!</h1>
</body></html>

これらのモジュールを異なるコンテナで実行する場合は、何らかの方法で、接続先のEJBコンテナを指定する必要があります。これには、サーブレットモジュール側の準備とコンテナ(JBoss)側の準備が必要です。

まず、サーブレットモジュールには、META-INFの下に次のxmlを用意します。

META-INF/jboss-ejb-client.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-ejb-client xmlns="urn:jboss:ejb-client:1.0">
    <client-context>
        <ejb-receivers>
            <remoting-ejb-receiver outbound-connection-ref="remote-ejb-connection"/>
        </ejb-receivers>
    </client-context>
</jboss-ejb-client>

これは、コンテナ(JBoss)上で定義された「remote-ejb-connection」で指定されるEJBコンテナにアクセスするという指定です。(これを作成すると、必要な設定がなされていない、ローカルのJBossではサーブレットが実行できなくなるので注意してください。)これでサーブレット・モジュールも完成です。Eclipseから「HelloWorld.war」として、Exportしておきます。

続いて、JBoss側の設定です。CLIを利用すると動的変更もできるようですが、ここでは、一旦、両ノードのJBossをCtrl+Cで停止して、設定ファイルを直接変更します。

まず、ドメインマスタのdomain.xmlに次の設定を追加します。

domain.xml

    <socket-binding-groups>
        <socket-binding-group name="standard-sockets" default-interface="public">
(snip)
            <outbound-socket-binding name="mail-smtp">
                <remote-destination host="localhost" port="25"/>
            </outbound-socket-binding>
<!-- ここから -->
            <outbound-socket-binding name="remote-ejb">
                <remote-destination host="node02" port="4447"/>
            </outbound-socket-binding>
<!-- ここまで -->
        </socket-binding-group>

これは、リモートのEJBコンテナのホストネームとポートを「remote-ejb」という名前で定義しています。「socket-binding-group」には、「name」アトリビュートが異なるいくつか種類があります。サーバグループを定義する際に指定した「Socket Binding: standard-sockets」に対応しますので、サーバグループで指定したもの(この例では「standard-sockets」)に対して設定を追加します。

さらに使用する設定プロファイルの「」に次の設定を追加します。

            <subsystem xmlns="urn:jboss:domain:pojo:1.0"/>
            <subsystem xmlns="urn:jboss:domain:remoting:1.1">
                <connector name="remoting-connector" socket-binding="remoting" security-realm="ApplicationRealm"/>
<!-- ここから -->
                <outbound-connections>
                    <remote-outbound-connection name="remote-ejb-connection" outbound-socket-binding-ref="remote-ejb" security-realm="ejb-security-realm" username="ejbuser">
                        <properties>
                            <property name="SASL_POLICY_NOANONYMOUS" value="false"/>
                            <property name="SSL_ENABLED" value="false"/>
                        </properties>
                    </remote-outbound-connection>
                </outbound-connections>
<!-- ここまで -->
            </subsystem>
            <subsystem xmlns="urn:jboss:domain:resource-adapters:1.0"/>

設定プロファイルは、サーバグループを定義する際に指定した「Profile: default」の部分に相当します。サーブレット側で指定した「remote-ejb-connection」という名前がこれによって、先の「remote-ejb」と紐付きます。

この時、上記の設定に2種類の「security-realm」が登場している事に注意します。タグの「security-realm="ejb-security-realm" username="ejbuser"」は、ユーザ「ejbuser」が、接続元コンテナのセキュリティレルム「ejb-security-realm」の認証情報を持って、EJBコンテナに接続に行くという意味です。一方、タグの「security-realm="ApplicationRealm"」は、接続先のEJBコンテナのセキュリティレルムになります。

したがって、接続先EJBコンテナ(node02)の「ApplicationRealm」にユーザ「ejbuser」とパスワードを登録して、そのパスワードを接続元コンテナ(node01)の「ejb-security-realm」の認証情報として埋め込んでおく必要があります。

具体的には次の手順になります。

まず、node02で、ApplicationRealmにejbuserを登録します。

# ./jboss/bin/add-user.sh 

What type of user do you wish to add? 
 a) Management User (mgmt-users.properties) 
 b) Application User (application-users.properties)
(a): b

Enter the details of the new user to add.
Realm (ApplicationRealm) : 
Username : ejbuser
Password : 
Re-enter Password : 
What roles do you want this user to belong to? (Please enter a comma separated list, or leave blank for none) : 
About to add user 'ejbuser' for realm 'ApplicationRealm'
Is this correct yes/no? yes
Added user 'ejbuser' to file '/opt/jboss-as-7.1.1.Final/standalone/configuration/application-users.properties'
Added user 'ejbuser' to file '/opt/jboss-as-7.1.1.Final/domain/configuration/application-users.properties'

この時セットしたパスワードをBASE64に変換したものを、node01のhost.xmlに下記のように仕込みます。

    <management>
<!-- ここから -->
        <security-realms>
            <security-realm name="ejb-security-realm">
                <server-identities>
                    <secret value="YXNkZmhvZ2U="/>
                </server-identities>
            </security-realm>
<!-- ここまで -->
            <security-realm name="ManagementRealm">

モジュールのデプロイ

作成した「GreetingEJB.jar」と「HelloWorld.war」をデプロイします。

各ノードのdomain.shを起動した後に、JBossの管理コンソールを開きなおして、画面右上の「Runtime」→画面左の「Deployments/Manage Deployments」を選択します。ここで、画面上の「Deployment Content」タブから、それぞれのモジュールをアップロードします。

アップロードしたモジュールの「Add to Groups」ボタンで、それぞれのモジュールをサーバグループに割り当てます。ここでは、「GreetingEJB.jar」を「EJB_Container_Group」に、「HelloWorld.war」を「Web_Application_Group」にそれぞれ割り当てます。これで、node01でサーブレットが起動して、node02でEJBが起動する状態になります。

次のように動作確認ができます。

$ curl http://192.168.122.41:8080/HelloWorld/greeting
<html><head><title>EJB Test</title></head>
<body>
<h1>Hello, World!</h1>
</body></html>

$ curl http://192.168.122.41:8080/HelloWorld/greeting?name="E.Nakai"
<html><head><title>EJB Test</title></head>
<body>
<h1>Hello, E.Nakai!</h1>
</body></html>