EJB 3.0(Public Draft)入門記 Simplified API Chapter3 その1

Chapter 3その1です。1日1Chapterといくつもりでしたが、ちょっと無理でした。JBossの勝手がよくわからないんです...。ということで3.3 Exceptionまでです。

Chapter3 Enterprise Bean Class and Business Interface

内容はエンタープライズBeanコンポーネントのタイプに共通な EJB 3.0 プログラミングモデルについてです。
Java Persistent API」ドキュメントで定義されるパーシステントエンティティは EJB 2.x のエンティティBeanと違ってエンタープライズコンポーネントではないらしいので、ここでいう「エンタープライズBeanコンポーネント」とはセッションBeanとメッセージ駆動型Beanを指しているんでしょう(たぶん)。

ところで、何を英字で書いて何をカタカナで書いて何を訳して書けばいいか迷うなぁ。

3.1 Enterprise Bean Class

EJB3.0APIを使うプログラミングモデルにおける開発者の作業は、主としてエンタープライズBeanクラスを定義しアノテーションをつけることだ、と言っています。

EJB2.1 以前のようにXMLのデプロイメント記述を書かなくてもいいよと暗に言いたいのかも。

3.1.1 Requirements for the Enterprise Bean Class

アノテーションで(デプロイメント記述使ってもOKですが)エンタープライズBeanクラスのタイプを指定しなければいけない、ということです。例として次のコードが載ってます。

@Stateful public class CartBean implements ShoppingCart {
  private float total;
  private Vector productCodes;
  public int someShoppingMethod(){...};
  ...
}

エンタープライズBeanクラスのタイプとして指定できるアノテーションには@Stateful以外に@Statelessや@MessageDrivenなどがあるようです。これらはchapter 4 以降で出てきます。

3.2 Business Interfaces

EJB 3.0 APIにおけるビジネスインターフェースはEJBObjectやEJBObjectLocalインタフェースではなく普通のJavaインタフェースだと言ってます。POJOってことですね。メッセージ駆動型Beanのビジネスインタフェースはメッセージのタイプによるみたい。たとえばJMSを使う場合はjavax.jms.MessageListenerがビジネスインタフェースのようです。

Beanクラスは複数のビジネスインタフェースを実装できます。そのときのルールは次のとおり。

  • Beanクラスが単一のインタフェースを実装する場合、そのインタフェースがビジネスインタフェースとなる。Beanクラス、インタフェース、デプロインメント記述のいずれにもRemoteの指定がされていなければビジネスインタフェースはローカルインタフェースとみなされる。
  • Beanクラスが2つ以上のインタフェースを持つ場合、Beanクラス、インタフェース、デプロインメント記述のいずれかにおいてRemoteかLoacalかが明示的に指定されなければいけない。ただし、次のインタフェースについてはBeanクラスが2つ以上のインタフェースをもつかどうかの判断から除く。
    • java.io.Serializable
    • java.io.Extenalizable
    • javax.ejbパッケージ内のインタフェース
  • ビジネスインタフェースはjavax.ejb.EJBObjectやjavax.ejb.EJBLocalObjectを拡張してはならない。

単一のインタフェースしかもたないときはローカルがデフォルトということね。単一のインタフェースしか持たない場合のjava.io.Serializableとかの扱いに触れられていないんだけどどうなるんでしょ。

例として次のようなコードがのってますがこれをJBossで動かしてみたいと思います。

@Stateless
public class CalculatorBean implements Calculator {
	public float add(int a, int b) {
		return a + b;
	}
	public float subtract(int a, int b) {
		return a - b;
	}
}

public interface Calculator {
	public float add(int x, int y);
	public float subtract(int x, int y);
}

環境はこんな感じです。

設定する環境変数JAVA_HOMEとJBOSS_HOME。JBossへのデプロイにはEclipseのAntを使います。build.xmlはこんな感じ。JBossEJB 3.0のサンプルにくっついたものを適当に修正しました。サンプルはhttp://www.jboss.com/docs/trailblazerにあります。

<?xml version="1.0"?>
<!-- ======================================================================= -->
<!-- JBoss build file                                                       -->
<!-- ======================================================================= -->
<project name="JBoss" default="run" basedir=".">

	<property environment="env" />
	<property name="src.dir" value="${basedir}/src" />
	<property name="jboss.home" value="${env.JBOSS_HOME}" />
	<property name="jboss.server.config" value="default" />
	<property name="build.dir" value="${basedir}/build" />
	<property name="build.classes.dir" value="${build.dir}/classes" />
	<property name="ejb3" value="study.ejb3" />

	<!-- Build classpath -->
	<path id="classpath">
		<!-- So that we can get jndi.properties for InitialContext -->
		<pathelement location="${basedir}" />
		<fileset dir="${jboss.home}/lib">
			<include name="**/*.jar" />
		</fileset>
		<fileset dir="${jboss.home}/server/${jboss.server.config}/lib">
			<include name="**/*.jar" />
		</fileset>
		<fileset dir="${jboss.home}/server/${jboss.server.config}/deploy/ejb3.deployer">
			<include name="*.jar" />
		</fileset>
		<fileset dir="${jboss.home}/server/${jboss.server.config}/deploy/jboss-aop-jdk50.deployer">
			<include name="*.jar" />
		</fileset>
		<pathelement location="${build.classes.dir}" />
	</path>

	<property name="build.classpath" refid="classpath" />

	<!-- =================================================================== -->
	<!-- Prepares the build directory                                        -->
	<!-- =================================================================== -->
	<target name="prepare">
		<mkdir dir="${build.dir}" />
		<mkdir dir="${build.classes.dir}" />
	</target>

	<!-- =================================================================== -->
	<!-- Compiles the source code                                            -->
	<!-- =================================================================== -->
	<target name="compile" depends="prepare">
		<javac srcdir="${src.dir}" destdir="${build.classes.dir}" debug="on" deprecation="on" optimize="off" includes="**">
			<classpath refid="classpath" />
		</javac>
		<copy todir="${build.classes.dir}">
			<fileset dir="${src.dir}">
				<include name="**/*.properties" />
				<include name="**/*.xml" />
				<exclude name="build.xml" />
			</fileset>
		</copy>
	</target>

	<target name="ejbjar" depends="compile">
		<delete file="build/${ejb3}" />
		<jar jarfile="build/${ejb3}">
			<fileset dir="${build.classes.dir}">
				<include name="**/*.class" />
			</fileset>
		</jar>
		<copy file="build/${ejb3}" todir="${jboss.home}/server/${jboss.server.config}/deploy" />
		<sleep milliseconds="500"/>
	</target>

	<!-- =================================================================== -->
	<!-- Run Java                                                            -->
	<!-- =================================================================== -->
	<target name="run" depends="ejbjar">
		<java classname="${java_type_name}" fork="yes" dir="${build.classes.dir}">
			<classpath refid="classpath" />
		</java>
	</target>

	<!-- =================================================================== -->
	<!-- Cleans up generated stuff                                           -->
	<!-- =================================================================== -->
	<target name="clean.db">
		<delete dir="${jboss.home}/server/${jboss.server.config}/data/hypersonic" />
	</target>

</project>

このbuild.xmlEclipseから実行してデプロイとClientの実行をいっしょにしてしまおうと思います。プロパティのjava_type_nameはEclipseの設定で指定したものです。設定は「Run」-> 「External Tool」からbuild.xmlの設定画面を開いて、propertiesタブの画面で「Add Property」ボタンを押し、NameとValueのとこ両方にjava_type_nameを入れときます。EclipseでClientのクラスを開いてbuild.xmlを実行するとClientのクラス名がjava_type_nameに入るのでrunターゲットでClientのクラスが実行できます。

次にEclipseに適当なプロジェクトを作ります。どれが必須かよくわかりませんけどbuild.xmlで指定したすべてのjarファイルにクラスパスが通るようにしときます。なんにしてjarファイル多すぎ。Java Build Pathの設定の画面でLibraryを作成しjarファイルを束ねて置くのがよさげです。それから上記のCalculatorBeanとCalculatorとそのClientのjavaファイルを適当なパッケージに作ります。Clientはこんな感じにします。

public class Client {
	public static void main(String[] args) throws Exception {
		InitialContext ctx = new InitialContext();
		Calculator calculator = (Calculator) ctx.lookup(Calculator.class
				.getName());
		System.out.println("100 - 1 = " + calculator.subtract(100, 1));
	}
}

あと、実行するにあったってjndi.propertyファイルをクラスパスに通しておきます。InitialContextがこのファイルを読み込んでくれるので(たぶん)、JNDIでEJBをlookupできるようになります。jndi.propertyファイルもEJB 3.0のサンプルにくっついています。一応内容をコピペしときます。

java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
java.naming.provider.url=localhost

環境がととのったらJBOSS_HOME\binの下のrun.batを起動します。

run -c default

JBossが起動したらEclipseでClient.javaを開いてbuild.xmlを実行します。コンパイル、デプロイ、Clientの実行が行われます。ちなみにデプロイされるアーカイブの拡張子は「ejb3」です。で、動きます? 私の環境では動かないんですけど。NullPointerExceptionがおきる。なんかBeanクラスが単一のインタフェースを実装する場合のルールが効いていないっぽい。
コメントのkoichikさんの指摘にあるように@Localを持ったEJBJBoss サーバ上から呼び出されなければいけませんでした。したがって、ルールが効いていないのではなくルールが効いている(@Localが指定されているとみなされている)からこそJBossサーバ外から起動できないと思われます。次回にでも@Localがちゃんと使えることを確認してみます。

いろいろやってみて次のいずれかの場合には動くことを確認しました。

  • インタフェースに@Remoteを使用する。(@Localでは駄目でした)
  • Beanクラスに@Remoteをアノテートしてクラスを明示的に指定する。(これも@Localでは駄目でした。クラスを明示的に指定しない場合も駄目です。)

JBossサーバ外からのアクセスなので次のように@Remoteを使えばいいことがわかりました。こんな感じ。Clientは変更してません。

@Remote
public interface Calculator {
	public float add(int x, int y);
	public float subtract(int x, int y);
}

@Stateless 
public class CalculatorBean implements Calculator {
	public float add(int a, int b) {
		return a + b;
	}
	public float subtract(int a, int b) {
		return a - b;
	}
}
public interface Calculator {
	public float add(int x, int y);
	public float subtract(int x, int y);
}

@Stateless 
@Remote({Calculator.class})
public class CalculatorBean implements Calculator {
	public float add(int a, int b) {
		return a + b;
	}
	public float subtract(int a, int b) {
		return a - b;
	}
}

要するに@Localが駄目です。何か勘違いしているところがあるかもしれません。まあしばらくは@Remoteで実行確認できればいいか。JBoss外からEJBを呼び出すときは@Remoteの指定がされていなければ駄目です。

あと実験してみたことと言えば

  • SerializableだけをimplementsしておくとSerializable.class.getName()でlookupしてBeanクラスを取得できる。でも、できても使いみちない...。
  • @Remoteって複数クラスを指定できるけど、指定してもJNDI名が解決できないと怒られる。JBossには@RemoteBindingアノテーションがあるけれどこういうJNDIを別個に指定するものといっしょにつかわないと駄目なのかなー。

この節はとりあえず以上。

3.3 Exception

ビジネスインタフェースは任意のアプリケーション例外をスローできるけどjava.rmi.RemoteExceptionをスローすべきではないとあります。プロトコルレベルの障害が生じたときはRemoteExceptionをラップしたEJBExceptionがEJBコンテナによってスローされるそうです。

Chapter 3 その2 に続きます。