eclipse+Jetty開発環境の作成

Herokuに手を出してからというものの、eclipse+Jettyで開発することが多くなりました。

JettyのおもしろいところはAPIを使って起動用クラスを作ることで、普通のJavaアプリとして起動できることです。

これが便利なところは

などですかね。

同じことはGlassfishでも出来るんですが、Jettyは依存JARが少なくサイズが小さいのでお手軽ですし、GlassfishAPIが安定していなくって起動用クラスの作成に苦労するんですよね。


そんなわけで今回はeclipse+Jettyのお手軽開発環境を構築する手順をまとめてみました。

例として「sample-web」というプロジェクトを作ってみましょう。

前提

  • Mavenが導入済みであること

http://maven.apache.org

私の環境(MacOS X Lion 10.7.5)にはデフォルトでVer3.0.3が入っていました。

言葉について

コマンドライン」という言葉は、Windowsなら「コマンドプロンプト」を指しMacなら「ターミナル」を指します。



では始めましょう。

eclipseMavenを連携させるための準備

コマンドラインで次のコマンドを実行します。

mvn -Declipse.workspace=<eclipseのworkspaceのパス> eclipse:add-maven-repo

このコマンドでM2_REPOという変数がeclipseに作られます。
ですのであるworkspaceに対して一度だけ実行すればOKです。

作業フォルダに移動

コマンドラインで次のコマンドを実行します。

cd <eclipseプロジェクトを置くフォルダ>

eclipseプロジェクトを置くフォルダは、通常だとworkspaceと同じ場所が多いでしょう。

プロジェクトの原型作成

コマンドラインで次のコマンドを実行します。

mvn archetype:create -DgroupId=jabara -DartifactId=sample-web -DarchetypeArtifactId=maven-archetype-webapp

groupIdには適当な名前を指定して下さい。
このコマンドでsample-webディレクトリが作られ、その中にpom.xmlが作られます。

コマンドラインでsample-webフォルダに移動

コマンドラインで次のコマンドを実行します。

cd sample-web

プロジェクトにJetty設定追加

pom.xmlを好きなエディタで開いての下に次を追記します。

<!-- 組み込みJettyの実行に必要 -->
<dependency>
    <groupId>org.eclipse.jetty.aggregate</groupId>
    <artifactId>jetty-all-server</artifactId>
    <version>8.1.4.v20120524</version>
</dependency>
<dependency>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>jsp-2.1-glassfish</artifactId>
    <version>2.1.v20100127</version>
</dependency>

<!-- 組み込みJettyにServletAPI3.0のアノテーションを解釈させるために必要 -->
<dependency>
    <groupId>asm</groupId>
    <artifactId>asm-commons</artifactId>
    <version>3.3.1</version>
</dependency>

プロジェクトのJavaのバージョンを6にする

pom.xmlsample-webの下に次を追記します。

<plugins>
    <plugin>
        <inherited>true</inherited>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
            <source>1.6</source>
            <target>1.6</target>
        </configuration>
    </plugin>
</plugins>

バージョンを7にしたい場合は「1.6」を「1.7」すればOKです。

eclipseプロジェクトの設定を追加

コマンドラインで次のコマンドを実行します。これでeclipseにインポートできるようになります。

mvn eclipse:eclipse

初回はいろんなライブラリをダウンロードするので時間がかかります

プロジェクトをeclipseにインポート

eclipseを起動し、メニューの「ファイル」→「インポート」→「既存プロジェクトをワークスペースへ」を使ってsample-webフォルダをインポートします。
詳しい手順は次のサイトを参考にして下さい。
http://www.javadrive.jp/eclipse3/project/index4.html

ソースフォルダを作成

なぜかソースフォルダが足りません。これではJettyの起動用クラスを作れないので、作成します。
「src/main/java」フォルダをソースフォルダとして作成して下さい。


デフォルト出力フォルダの変更

mvnの設定をそのまま使うと@WebServlet(など)の付いたクラスを自動的で探す機能が動作しません。
そこで「sample-web/src/main/webapp/WEB-INF/classes」に変更します。

なお、この設定は

mvn eclipse:eclipse

コマンドを実行する度に施す必要があります。依存ライブラリを追加したときなど、このコマンドを実行することはよくあります。その時にこの設定を忘れるとWebアプリが動作しなくなるので、注意が必要です。
もっと良い方法がきっとあると思うのですが、まだ調べがついていません。


ここまでの設定のスクリーンショットです。

Jetty起動用クラス作成

src/main/java直下に次のクラスを作ります。

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.TagLibConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;

/**
 * @author jabaraster
 */
public class SampleWebStarter {
    /**
     * @param pArgs -
     * @throws Exception -
     */
    public static void main(final String[] pArgs) throws Exception {
        final int port = getWebPort();
        final String webappDirLocation = "src/main/webapp/"; //$NON-NLS-1$

        final Server server = new Server(port);
        final WebAppContext context = new WebAppContext();
        context.setConfigurations(new Configuration[] { //
        new AnnotationConfiguration() //
                , new WebXmlConfiguration() //
                , new WebInfConfiguration() //
                , new TagLibConfiguration() //
                , new PlusConfiguration() //
                , new MetaInfConfiguration() //
                , new FragmentConfiguration() //
                , new EnvConfiguration() //
        });
        context.setContextPath("/"); //$NON-NLS-1$
        context.setDescriptor(webappDirLocation + "/WEB-INF/web.xml"); //$NON-NLS-1$
        context.setResourceBase(webappDirLocation);
        context.setParentLoaderPriority(true);

        server.setHandler(context);
        server.start();
        server.join();
    }

    private static int getWebPort() {
        final String webPort = System.getenv("PORT"); //$NON-NLS-1$
        if (webPort == null || webPort.isEmpty()) {
            return 8080;
        }
        return Integer.parseInt(webPort);
    }
}

ポートの決定には一工夫しています。
Herokuで動作させることを考慮し、環境変数PORTを見るようにしています。もし環境変数PORTが空なら、8080を決め打ちで使います。

なおクラスのpackage宣言を省略している(default packageに所属させている)のには深い意味はありません。


Jetty起動

SampleWebStarterをeclipseから起動します。
1秒くらいで起動します。速い。

動作確認

ブラウザで次のURLにアクセスします。
http://localhost:8080/

Hello World!」が表示されれば成功!

Servletを作ってみる

ついでにServletAPI3.0を使ってServletを作ってみましょう。
src/main/javaの下に次のJavaクラスを作ります。

package servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns={"/sample"})
public class SampleServlet  extends HttpServlet{

    protected void doGet(final HttpServletRequest pReq, final HttpServletResponse pResp) throws ServletException, IOException {
        pResp.setContentType("text/plain");
        pResp.getWriter().print("Hello World!");
    }
}

もしこのServletのソースに「Syntax error, annotations are only available if source level is 1.5 or greater」というエラーが起きた場合、「プロジェクトのJavaのバージョンを6にする」の手順の設定を追加して下さい。

Servletの動作確認

ブラウザで次のURLにアクセスします。
http://localhost:8080/sample

Hello World!」が表示されれば成功!

JerseyでJSONを扱うときは注意

JAX-RSは素晴らしい仕様です。

しかし、その参照実装であるJerseyでJSONを扱うときには注意が必要です。
けっこうイヤな動きをします。

Listオブジェクトをエンコードしてくれない

トップレベルのオブジェクトがListの場合、JSONにしてくれません。
他のオブジェクトのプロパティとしてListを持たせる分にはOKです。

Listの要素数JSONでの型が変わる

  • 素数が0のとき⇒null
  • 素数が1のとき⇒要素そのもの
  • 素数が2以上のとき⇒配列

となります。

・・・この仕様が嬉しい人いたら教えて下さい。

サンプル

文字列のListをJSONで返すコードを書いてみます。

直感的な動作は

return Arrays.<String>asList();

のときは

[ ]
return Arrays.<String>asList("a");

のときは

["a"]
return Arrays.<String>asList("a", "b");

のときは

["a", "b"]

となることでしょう。

実際にやってみます。
まず、JerseyはListを扱えないのでラップするクラスを作ります。この時点で信じられないめんどくささです。

    @XmlRootElement
    @XmlAccessorType(XmlAccessType.FIELD)
    static class StringList {

        @XmlElement
        List<String> list;

        StringList() {
        }

        StringList(final List<String> pList) {
            this.list = pList;
        }
    }

内部クラスとして作ったのでクラス宣言にstaticが付いています。
あと、引数なしのコンストラクタはプログラムからは使わないけど必要です。

さてStringのListを返して、JSONがどうなるかを見てみます。

return new StringList(Arrays.<String>asList());

のときは

null

・・・は?null?トップレベルのオブジェクトがなくなったぞ?

return new StringList(Arrays.<String>asList("a"));

のときは

{"list":"a"}

ここ要注意!listプロパティの値が単なる文字列になってます!Listはどこに消えたんだ?!


return new StringList(Arrays.<String>asList("a", "b"));

のときは

{"list":["a","b"]}

ようやく期待する形(に近い形)でJSONが得られました。


対策・JsonMessageBodyWriterを作る

素数JSON側の型が変わるのは、JavaScriptで処理するときのことを考えるとかなり使いにくいです。
そこでJSONICを使って自前でMessageBodyWriterを作り、JSON側では常に配列となるようにします。

import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;

import net.arnx.jsonic.JSON;

/**
 * @author jabaraster
 */
@Provider
public class JsonMessageBodyWriter implements MessageBodyWriter<Object> {

    @SuppressWarnings({ "unused", "javadoc" })
    @Override
    public long getSize(final Object pT, final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) {
        return -1;
    }

    @SuppressWarnings({ "unused", "javadoc" })
    @Override
    public boolean isWriteable(final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) {
        return MediaType.APPLICATION_JSON_TYPE.isCompatible(pMediaType);
    }

    @SuppressWarnings({ "unused", "javadoc" })
    @Override
    public void writeTo( //
            final Object pT //
            , final Class<?> pType //
            , final Type pGenericType //
            , final Annotation[] pAnnotations //
            , final MediaType pMediaType //
            , final MultivaluedMap<String, Object> pHttpHeaders //
            , final OutputStream pEntityStream //
    ) throws IOException, WebApplicationException {

        JSON.encode(pT, pEntityStream, true);
    }
}

超シンプル。なんと3つのメソッド全てが、たった1行の実装です。
だけど期待通りの結果が得られます。
その上読みやすい形でJSONが得られます(通信量増えるけど)。

Jerseyのバージョン

jersey-server及びjersey-jsonのバージョンは、共に1.8でした。

java.util.List#subListの誤った用法

java.util.List#subListメソッドの使い方がまちがっていたためにメモリリークを犯してしまっていた、という話です。


画像の各ピクセルの色を抽出してある法則に並べ替えた後に、先頭の10個の要素を返す、というメソッドを作るとしましょう。
このとき

List<Color> cs = ...
Collections.sort(cs, COMPARATOR);
return cs.subList(0, 10);

と書いてしまうと問題が生じます。csがメモリリークを起こすのです。


java.util.List#subListのJavadocによると、このメソッドはあるリストの特定の範囲を操作したいときの面倒なインデックス計算を省略するためにあるようです。
例えばJavadocの例ですが

list.subList(from, to).clear();

と書くとのfromからtoの範囲の要素を削除できます。

ここで重要なのは、subListメソッドが返すListオブジェクトを操作することで元のListオブジェクト(この例だとlist変数に格納されたListオブジェクト)が変更されている、ということです。
つまり、subListメソッドが返すListオブジェクトは元のListオブジェクトを参照しているということになります。


これを踏まえて冒頭に挙げたコードを見てみます。

List<Color> cs = ...
Collections.sort(cs, COMPARATOR);
return cs.subList(0, 10);

cs変数が参照しているListオブジェクトはメソッドから抜けると不要になるにかかわらず、subListメソッドが返すListオブジェクトからずっと参照され続けるので、メモリリークを起こすわけですね。

これを防ぐには

List<Color> cs = ...
Collections.sort(cs, COMPARATOR);
return new ArrayList<Color>(cs.subList(0, 10));

と書く必要があります。


Javaのコレクションの中でも最も出番の多いであろうjava.util.Listですが、油断すると落とし穴にはまりますね。。。

WicketのAjaxでJavaScriptを動かす

Wicketで、Ajaxで何か処理した後にクライアント(ブラウザ)側でJavaScriptを実行するには
AjaxRequestTarget#appendJavaScript(CharSequence)を使います。

便利。


ちなみにAjaxRequestTargetにはfocusComponent(Component)というメソッドがあって、Ajax処理が完了した後にフォーカスを当てる場所を指定することができます。

便利。

WicketのHTMLテンプレートはまりどころ

WicketはHTMLのテンプレートにHTMLを使います。
このことの恩恵は非常に大きくて、HTMLでローカルで確認したレイアウトをそのままアプリで使えるわけです。
もちろん他にもHTMLをテンプレートに使えるフレームワークはありますが、Wicketのおもしろいところはパネルによる部品化や継承によるデザインと機能の共通化が可能で、それでいてローカルでHTMLを見てもデザインが壊れない工夫がされている、というところだと思います。

さて便利なWicketのHTMLテンプレート機能ですが、はまりどころがあります。
継承や部品化によって消える部分に、Wicketが動的に処理するタグを含めてはならない、ということです。

例えば

<wicket:remove>
  <div wicket:id="somePanel">
  </div>
</wicket:remove>

のようにしてはならん、ということですね。

消える部分にWicketが処理する部分があると、Wicketはテンプレートをどう処理したらいいか分からなくなるので、当然と言えば当然なんですが、wicket:idを付けなくてもWicketが処理を加えるタグの場合、このミスに気付きにくいです。
例えば<a>タグのhref属性や<form>のaction属性がそうです。

たとえタグにwicket:idを付けていなくても、これらの属性に.htmlで終わるファイルを記述していると、WicketはそのHTMLを検索して表示しようとします。
だからここがで削除される対象になっていると、Wicketは「ほんとに消しちゃっていいの?何か表示するつもりなんだよね?」と言ってエラーにしてくれるわけです。

Wicketを通さなくても画面レイアウトや遷移を確かめられるようにHTMLを作っていると、特にこれにはまると思います。というかはまりました。かなりの長時間。
Wicketを使うときは気を付けて下さい。

JavaEEでConnectionを使うときはclose()を忘れずに

最近Glassfish v3.0.1 を使ったJavaEE+Wicketで開発しているのですがはまった出来事があります。
それはjava.sql.Connectionのクローズもれです。

Java1年生かと見紛うばかりの初歩的なミスですが、なぜか「JavaEEのConnectionはclose()しなくてもOK」という思い込みがあったんですよね。

それどころかclose()してしまうとトランザクションが終了してしまうので、close()してはいけないとさえ思っていました。

JTAを使うと、Connectionのクローズのタイミングとトランザクションが終わるタイミングとは切り離されるとのこと。
自分の知識は生兵法なんだなぁ、と反省しきりです。