Java EEサーバーが重くてテスト不能というイメージはもう過去の話かもしれない

Java EE 5まではいろいろな面で生産性が低かったと言わざるを得ないところがあった

今まで仕事上、Java EEのサーバーを実行基盤として用いるさまざまなシステムの開発に関わってきましたが、JavaEE(古くはJ2EE)のサーバーというと経験上

  • xmlの設定ファイルの記述がきわめて面倒
  • J2EE1.4までは、EJBを使った場合Pojoとしてサービスやエンティティを作成できない
  • サーバーの再起動にものすごく時間がかかる
  • ライセンス料が高い
  • サーバーを気軽にダウンロードして試せない

というような非常に悪いイメージがあったというのが正直なところでした。
それゆえ、JavaをSEとEEに分類するのは今では無意味になってきている? - 達人プログラマーを目指してでも紹介したように、Seasar2やSpringといった軽量コンテナというしくみが登場し、事実上EJBコンテナの機能はほとんど利用せず、単にデプロイ・実行のための基盤としてのみアプリケーションサーバーを利用するというケースも少なくありませんでした。中には、EJBの使用が会社の標準規約として上から押し付けられているようなケースもありますが、そのような場合でも裏でDIコンテナを併用し、Pojoとしてロジックを作成することで、開発時はTomcatなどの少しでも軽量なサーバーで開発ができるようにするといったような涙ぐましい努力も何度かしてきた経験があります。その上、ただでさえ重たいアプリケーションサーバーですが、一般にプログラマーに割り当てられる開発PCのスペックは残念ながら極めて低いという場合が多く、重たいサーバーをローカルで起動するともなれば、実際何分も待たされるということも珍しいことではありません。
EoDをうたい文句として2006年に登場したJava EE5準拠のサーバーであれば、形式的にEJBPojoとして作成することができるため、先に述べた問題点のうち最初の二つは解消しているように見え、生産性が大きく向上するように思われました。少なくとも記述すべきコードや設定ファイルの分量が激減することは確かなのです。しかしながら、実際にJava EE5で簡単になったのは表面上のAPIの部分だけなのであって、アプリケーションサーバー自体は以前のバージョンに比べてより巨大で重たくなったという印象を受けるケースがほとんどではないかと思います。
さらに、サーバーの重さに加えてJava EE5の問題点としては、EJBのコンテナ上でのテストを自動化することが意外に困難ということを忘れてはなりません。EJB3Pojoであるため、少なくとも次世代のモックフレームワークであるJMockitの基本的な使い方 - 達人プログラマーを目指してなどのモックライブラリーを利用して単体試験することは比較的容易に行えます。しかし、実際にデータベースと接続してトランザクション中でJPAのエンティティを検索するような試験を作成する仕組みを作ることは実はなかなか困難だったのです。一方、SpringやSeasar2などの軽量コンテナを利用する場合、DIコンテナ上での結合試験を自動化する仕組みが提供されているため、このような試験の自動化はきちんとした知識とスキルが身についていれば難しいことではありません。
このように、軽量コンテナを活用した開発モデルと比較してJavaEE 5標準の機能をフルに活用した開発は、正直なところ開発生産性が高いとは言えないところがあったと思います。Java EEサーバーを使う必要がないのであれば、あえて使いたくなかったというのが開発者の立場としては本音だったのです。

Java EEはとっくの昔にオワコンなのか?

JavaEE 5の生産性が期待に反して低かったという事実もあるのでしょうか、少なくとも日本ではJava EE準拠の仕様を使った開発というのはあまり人気がありません。それどころか、海外でもJava EEの人気は低いのではないかという意見すら聞かれるほどです。
Java EE関連技術が人気ないのは日本だけでなく世界的な傾向? - Togetter
その一方、Java EE6に準拠した最新のGlassfishは、きわめて軽量で使い勝手がよく、世界各国でも使われているという意見もあります。そこで、実際にGlassfishを使った場合EJBコンテナ上での試験自動化がどのように行えるのかを実際に調べてみることにしました。

埋め込みEJBコンテナを使ったEJBの試験の自動化

EJBという名前のせいですごく開発が面倒なイメージが定着してしまっているところがありますが、前バージョンのEJB3からはPojoとして設定ファイルなしで作成できるようになっただけでなく、最新バージョンのEJB3.1ではインターフェースの定義すら不要になっています。実際、EJBを定義することは普通のクラスを定義することとなんら変わるところがなく、単に@Statelessというアノテーションを付けるだけで非常に簡単に行うことができます。これだけで、JTAのグローバルトランザクションの中でJPAを使ってデータベースを処理したり、SOAPやRESTのWebサービスとして公開するなど便利な機能を利用することができるようになります。実際にHello WorldEJBは以下のように定義できます。

package com.github.ryoasai.embeddedjee6.ejb;

import javax.ejb.Stateless;

@Stateless
public class HelloService {

    public String sayHello(String name) {
        return "Hello " + name + "!";
    }
}

次に、インメモリで埋め込みのGlassfishを起動し、このBeanをデプロイした上で呼び出す試験は以下のように記述することができます。ここで、Glassfish固有のAPIに依存していない点に注意してください。Java EE6では標準でインメモリのEJBコンテナを起動するための標準APIが提供されているからです。

package com.github.ryoasai.embeddedjee6.ejb;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EmbeddedEJBWithGlassfishTest {
    Logger log = LoggerFactory.getLogger(EmbeddedEJBWithGlassfishTest.class);
    Map<String, Object> containerProps = new HashMap<String, Object>();

    EJBContainer container;

    @Before
    public void setUp() throws Exception {
        containerProps.put(EJBContainer.MODULES, getDeployTargets());
        container = EJBContainer.createEJBContainer(containerProps); // EJBコンテナを作成しモジュールをデプロイ
    }

    private File[] getDeployTargets() {
        File baseDir = new File(System.getProperty("user.dir"));

        File[] files = { 
            new File(baseDir, "../embedded-jee6-test/target/classes"), 
        };

        return files;
    }

    @After
    public void tearDown() throws Exception {
        if (container != null) {
            container.close();
        }

        log.debug("EmbeddedTest completed.");
    }

    @Test
    public void test() throws Exception {
        Context ic = container.getContext();

        log.debug("Looking up HelloService EJB.");
        HelloService helloService = (HelloService) ic
                .lookup("java:global/classes/HelloService"); // JNDIツリーからEJBをルックアップ
        log.debug("Invoking HelloService [" + helloService + "]");

        assertThat(helloService.sayHello("Test"), is("Hello Test!"));
        log.debug("HelloService tested successfully");
    }
}

以上のテストを実行すると、以下のようなログが得られ、実際にテストにパスすることが確認できました。テストの実行のために、Glassfishの起動とデプロイを行っているため多少時間はかかりますが、私の自宅のごく平均的なPC(メモリ4G、Core2-Quad 2.66GHz、普通のHDD)で4秒弱で試験が完了しました。この程度の速度であれば、軽量なDIコンテナを起動して試験するのと時間的には大差はありません。

2011/05/21 21:58:00 org.glassfish.ejb.embedded.EJBContainerProviderImpl getValidFile
致命的: EJB6004:Specified application server installation location [C:\Users\Ryo\.m2\repository\org\glassfish\extras\glassfish-embedded-all\domains\domain1] does not exist.
2011/05/21 21:58:00 com.sun.enterprise.v3.server.CommonClassLoaderServiceImpl findDerbyClient
情報: Cannot find javadb client jar file, derby jdbc driver will not be available by default.
2011/05/21 21:58:01 com.sun.enterprise.v3.services.impl.GrizzlyService createNetworkProxy
情報: Network listener http-listener on port 0 disabled per domain.xml
2011/05/21 21:58:01 com.sun.enterprise.v3.services.impl.GrizzlyService createNetworkProxy
情報: Network listener https-listener on port 0 disabled per domain.xml
2011/05/21 21:58:01 com.sun.enterprise.v3.server.AppServerStartup run
情報: GlassFish Server Open Source Edition 3.1 (java_re-private) startup time : Embedded (711ms), startup services(330ms), total(1,041ms)
2011/05/21 21:58:01 org.glassfish.admin.mbeanserver.JMXStartupService$JMXConnectorsStarterThread run
情報: JMXStartupService: JMXConnector system is disabled, skipping.
2011/05/21 21:58:01 org.glassfish.ejb.embedded.EJBContainerImpl deploy
情報: [EJBContainerImpl] Deploying app: C:\Users\Ryo\embedded-jee6-test\embedded-jee6-test-glassfish\..\embedded-jee6-test\target\classes
2011/05/21 21:58:02 com.sun.enterprise.security.SecurityLifecycle <init>
情報: SEC1002: Security Manager is OFF.
2011/05/21 21:58:02 com.sun.enterprise.security.SecurityLifecycle onInitialization
情報: SEC1010: Entering Security Startup Service
2011/05/21 21:58:02 com.sun.enterprise.security.PolicyLoader loadPolicy
情報: SEC1143: Loading policy provider com.sun.enterprise.security.jacc.provider.SimplePolicyProvider.
2011/05/21 21:58:02 com.sun.enterprise.security.auth.realm.Realm doInstantiate
情報: SEC1115: Realm [admin-realm] of classtype [com.sun.enterprise.security.auth.realm.file.FileRealm] successfully created.
2011/05/21 21:58:02 com.sun.enterprise.security.auth.realm.Realm doInstantiate
情報: SEC1115: Realm [file] of classtype [com.sun.enterprise.security.auth.realm.file.FileRealm] successfully created.
2011/05/21 21:58:02 com.sun.enterprise.security.auth.realm.Realm doInstantiate
情報: SEC1115: Realm [certificate] of classtype [com.sun.enterprise.security.auth.realm.certificate.CertificateRealm] successfully created.
2011/05/21 21:58:02 com.sun.enterprise.security.SecurityLifecycle onInitialization
情報: SEC1011: Security Service(s) Started Successfully
2011/05/21 21:58:02 com.sun.ejb.containers.BaseContainer initializeHome
情報: Portable JNDI names for EJB HelloService : [java:global/classes/HelloService!com.github.ryoasai.embeddedjee6.ejb.HelloService, java:global/classes/HelloService]
2011/05/21 21:58:02 org.jboss.weld.bootstrap.WeldBootstrap <clinit>
情報: WELD-000900 SNAPSHOT
2011/05/21 21:58:02 org.hibernate.validator.util.Version <clinit>
情報: Hibernate Validator null
2011/05/21 21:58:02 org.hibernate.validator.engine.resolver.DefaultTraversableResolver detectJPA
情報: Instantiated an instance of org.hibernate.validator.engine.resolver.JPATraversableResolver.
***************************
2011/05/21 21:58:03 org.glassfish.admin.mbeanserver.JMXStartupService shutdown
情報: JMXStartupService and JMXConnectors have been shut down.
2011/05/21 21:58:03 com.sun.enterprise.v3.server.AppServerStartup stop
情報: Shutdown procedure finished
2011/05/21 21:58:03 AppServerStartup run
情報: [Thread[GlassFish Kernel Main Thread,5,main]] exiting

jeeunitとCDIを利用してもっと簡単に単体試験を書けるようにする

このように埋め込みEJBコンテナを使えばEJBの単体試験が行えることが分かったのですが、spring-testなどの環境と比較すると

  • 単体テストクラス自身にDIが行えないため、わざわざJNDIからEJBをルックアップするのが面倒
  • 埋め込みGlassfishの起動と停止が面倒
  • 一回のサーバー起動で複数のテストケースを実行するためには工夫がいる*1

といった問題があります。この点に関しては、JUnitを独自に拡張して上記の処理を隠ぺいする仕組みを自分で作ればよいのですが、自作しなくても以下のライブラリーを利用することができます。
Google Code Archive - Long-term storage for Google Code Project Hosting.
これを利用すると、テストクラスは以下のように非常に簡単に記述できるようになります。

package com.github.ryoasai.embeddedjee6.ejb;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import javax.inject.Inject;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.googlecode.jeeunit.JeeunitRunner;

@RunWith(JeeunitRunner.class)
public class JeeUnitTest {

    Logger log = LoggerFactory.getLogger(JeeUnitTest.class);
    
    @Inject
    HelloService helloService;

    @Test
    public void test() throws Exception {
        assertThat(helloService.sayHello("Test"), is("Hello Test!"));
        log.debug("HelloService tested successfully");
    }
}

ポイントは埋め込みGlassfishが持つCDIコンテナの機能を流用することで@Injectにより、EJBを直接テストクラスに対してインジェクションできるようになっているということですね。ここまで単純になれば、Springを使った開発と比べて開発すべきコードの分量だけでなく、テスト容易性でもまったく互角といえると思います。その上、Glassfishの場合はSpringのように設定ファイルでライブラリーの組み合わせを記述しなくても、デフォルトでJTAJPAなどの機能を利用することもできるのです。

テストを自動化できるのはEJBだけではない

今のところ、JavaEEとして標準化されているのはEJBコンテナの部分に限られますが、Glassfish固有のAPIを利用すれば、サーブレットなどのレイヤも含めて完全にインメモリでサーバーを起動して試験を自動化することが可能です。サーブレットコンテナであれば、古くからJettyなどを利用してインメモリでサーバーを起動することができましたが同様の機能をGlassfishでも利用することができます。埋め込みGlassfishを利用したテストクラスの作成については、以下のコードが参考になります。
GlassFish

まとめ

今回は埋め込みGlassfishを利用してEJBをコンテナ上で簡単に呼び出すことができるということについて紹介しました。以前のバージョンまでと比較して

  • 作成すべきクラスや設定ファイルの分量
  • 単体試験の容易性
  • コンテナの起動の速さ

において軽量のDIコンテナと比較してまったく遜色がないレベルであるということが確認できました。確かに、Java EEは標準であるため、Android開発やNoSQLデータベースの活用といった最新技術を利用しようとするとどうしてもSpringのようなOSSの方が先を行っていることは否定できないところがありますが、普通に枯れたWeb+RDB技術を使って社内システムを開発するといったようなケースにおいては、Java EEの開発も捨てたものではないのではないかと感じました。
EJBJava EEサーバーは重くて使い物にならない」と考えてしまうのは、少なくとも最新のGlassfishを使う限りにおいては当てはまりませんし、そのように最初から決めつけて選択肢を狭めてしまうのは、むしろ「老害」的とすら言えるのではないかと思われます。
なお、今回のサンプルコードについては設定も含めてGithub上にアップしてあります。
GitHub - ryoasai/embedded-jee6-test: Test samples with embedded Java EE 6 servers.

*1:テスト実行の高速化のためには一般にこのテクニックは重要ですが、副作用の影響を受けることもあるため、もろ刃の剣というところはあります。