Network.Qiita作りました!
去年暮れから仲間と4人でコツコツ作っていた、HaskellによるQiita APIラッパーをついにリリースしました!
配布サイトはこちら
http://qiita.com/items/a0e4d1a105b12467850f
こういうライブラリを公開するのは初めてです。
誰かの役に立つと嬉しいですね〜
eclipse+Jetty開発環境の作成
Herokuに手を出してからというものの、eclipse+Jettyで開発することが多くなりました。
JettyのおもしろいところはAPIを使って起動用クラスを作ることで、普通のJavaアプリとして起動できることです。
これが便利なところは
などですかね。
同じことはGlassfishでも出来るんですが、Jettyは依存JARが少なくサイズが小さいのでお手軽ですし、GlassfishはAPIが安定していなくって起動用クラスの作成に苦労するんですよね。
そんなわけで今回はeclipse+Jettyのお手軽開発環境を構築する手順をまとめてみました。
例として「sample-web」というプロジェクトを作ってみましょう。
eclipseとMavenを連携させるための準備
コマンドラインで次のコマンドを実行します。
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が作られます。
プロジェクトに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.xmlの
<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秒くらいで起動します。速い。
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にする」の手順の設定を追加して下さい。
JerseyでJSONを扱うときは注意
JAX-RSは素晴らしい仕様です。
しかし、その参照実装であるJerseyでJSONを扱うときには注意が必要です。
けっこうイヤな動きをします。
サンプル
文字列の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を通さなくても画面レイアウトや遷移を確かめられるようにHTMLを作っていると、特にこれにはまると思います。というかはまりました。かなりの長時間。
Wicketを使うときは気を付けて下さい。
JavaEEでConnectionを使うときはclose()を忘れずに
最近Glassfish v3.0.1 を使ったJavaEE+Wicketで開発しているのですがはまった出来事があります。
それはjava.sql.Connectionのクローズもれです。
Java1年生かと見紛うばかりの初歩的なミスですが、なぜか「JavaEEのConnectionはclose()しなくてもOK」という思い込みがあったんですよね。
それどころかclose()してしまうとトランザクションが終了してしまうので、close()してはいけないとさえ思っていました。
JTAを使うと、Connectionのクローズのタイミングとトランザクションが終わるタイミングとは切り離されるとのこと。
自分の知識は生兵法なんだなぁ、と反省しきりです。