Slim3でのHot deploy時 ClassCastException

Google App Engine for Java & Slim3 /Google SDK : 1.4.2
開発サーバで以下のエラーが発生した。なにやら、Hotデプロイ時にエラーになっているらしい。


HTTP ERROR 500

Problem accessing /testdata. Reason:

If you use MemcacheService or JCache, use org.slim3.memcache.Memcache instead of it.
Or if a COOL class wants to access a HOT reloaded class, use CoolBridge.
COOL classes means classes located on "com.appspot.categoryeditor.cool" package or classes which Servlet Container manages like Servlets.
HOT reloaded classes means classes located on "com.appspot.categoryeditor" package except "com.appspot.categoryeditor.cool" package.
See http://sites.google.com/site/slim3appengine/hot-reloading

Caused by:

org.slim3.controller.HotReloadingRuntimeException: If you use MemcacheService or JCache, use org.slim3.memcache.Memcache instead of it.
Or if a COOL class wants to access a HOT reloaded class, use CoolBridge.
COOL classes means classes located on "com.appspot.categoryeditor.cool" package or classes which Servlet Container manages like Servlets.
HOT reloaded classes means classes located on "com.appspot.categoryeditor" package except "com.appspot.categoryeditor.cool" package.
See http://sites.google.com/site/slim3appengine/hot-reloading
at org.slim3.controller.HotReloadingFilter.createHotReloadingRuntimeException(HotReloadingFilter.java:250)
at org.slim3.controller.HotReloadingFilter.doHotReloading(HotReloadingFilter.java:231)
at org.slim3.controller.HotReloadingFilter.doFilter(HotReloadingFilter.java:187)
at org.slim3.controller.HotReloadingFilter.doFilter(HotReloadingFilter.java:157)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at com.google.appengine.api.blobstore.dev.ServeBlobFilter.doFilter(ServeBlobFilter.java:58)
※省略


Caused by: java.lang.ClassCastException: com.appspot.categoryeditor.model.User cannot be cast to com.appspot.categoryeditor.model.User
at com.appspot.categoryeditor.meta.UserMeta.getKey(UserMeta.java:50)
at org.slim3.datastore.DatastoreUtil.modelToEntity(DatastoreUtil.java:948)
at org.slim3.datastore.AsyncDatastoreDelegate.putAsync(AsyncDatastoreDelegate.java:1671)
at org.slim3.datastore.DatastoreDelegate.put(DatastoreDelegate.java:2174)
at org.slim3.datastore.DatastoreDelegate.put(DatastoreDelegate.java:2123)

メッセージ中に、

See http://sites.google.com/site/slim3appengine/hot-reloading

とあったので、その通りにページを確認してみた。

このページ中に以下の記述があった。

Classes that are under except cool package are HOT reloadable.

A HOT reloadable class can reference a HOT reloadable class and a COOL class. A COOL class can reference A COOL class, but cannot reference a HOT reloadable one.

ルートパッケージ配下(coolパッケージを除いた)のクラスはHOT reloadableクラスとなり、それ以外はCOOLクラスとなるらしい。また、COOLクラスからHOT reloadableクラスは参照できないらしい。今回はこの条件にひっかかってしまったと思われる。
ということでプログラムを確認してみると、見事に異なるパッケージ(testパッケージ)配下のクラスからHOT reloadableなクラスを呼びだしていた。
今回、テスト的にControllerクラスを作成する際、前に作ったテストクラスを利用したのだが、そもそもそれが問題だったらしい。処理をルートパッケージ配下のクラスに記述して呼び出したら問題なく動作した。

エラーがドンと出て少しビビったけれど、よく確認してみるとそんなに大した問題じゃなかった。。

Seasar2 S2AOP デフォルトで用意されているInterceptor使用例

traceInterceptor

java.util.Date() に対してtraceInterceptorを設定した例

app.docon

<?xml version="1.0" encoding="UTF-8"? >
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
"http://www.seasar.org/dtd/components24.dtd">
<components>
    <include path="aop.dicon" />
    <component class="java.util.Date" instance="prototype">
        <aspect pointcut="getTime">aop.traceInterceptor</aspect>
    </component>
</components>

mainメソッド

public static void main(String[] args) {
    S2Container container = S2ContainerFactory.create("app.dicon");
    Date d = (Date)container.getComponent(Date.class);
    d.getTime();
    Date d2 = (Date)container.getComponent(Date.class);
    d2.getTime();
}

traceThrowsInterceptor

例外発生を補足するためのInterceptor

app.docon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
"http://www.seasar.org/dtd/components24.dtd">
<components>
    <include path="aop.dicon" />
    <component class="sample.ThrowTest">
        <aspect pointcut="throwmethod">aop.traceThrowsInterceptor</aspect>
    </component>
</components>

例外発生テスト用クラス

package sample;
public class ThrowTest {
    public void throwmethod() throws Exception{
        if(true){
            throw new Exception("error test");
        }
    }
}


mainメソッド

S2Container container = S2ContainerFactory.create("app.dicon");
ThrowTest t = (ThrowTest)container.getComponent(ThrowTest.class);
t.throwmethod();

toStringInterceptor

オブジェクトのtoString処理を自動で行ってくれる
@ToStringアノテーションを指定することが可能

import org.seasar.framework.aop.annotation.ToString;

public class ToStringInterceptorTest {
    private String field1 = "field one";
    private String field2 = "field two";

    @ToString
    public String toString(){
        return null;
    }
}

mainメソッド

S2Container container = S2ContainerFactory.create("app.dicon");
ToStringInterceptorTest t =
  (ToStringInterceptorTest)container.getComponent(ToStringInterceptorTest.class);
System.out.println(t);

以下のように出力される

sample.ToStringInterceptorTest@224c47db[field1="field one",field2="field two"]

FizzBuzzをJavaでやってみる

有名なFizzBuzz問題というのを解いたことがなかったのでやってみた。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と,5の倍数のときは「Buzz」とプリントし,3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

ただ目的を達するだけではつまらないので、より汎用化した記述にしてみた。

public class FizzBuzzMaker {
    
    //このフィールドに対となる数値と文字列を格納しておく
    Map<Integer, String> table = new LinkedHashMap<Integer, String>();
    
    void addRecord(Integer i, String s){
        table.put(i, s);
    }
    
    //数値を文字列に変換する際はtableを見る
    String generateString(int i){
        StringBuilder sb = new StringBuilder();
        Set<Integer> keys = table.keySet();
        for (Integer key : keys) {
            if(i % key == 0) sb.append(table.get(key));
        }
        if(sb.length() == 0) sb.append(i);
        
        return sb.toString();
    }
    
    public static void main(String[] args) {
        FizzBuzzMaker fizzBuzzMaker = new FizzBuzzMaker();
        //数値と文字列を指定
        fizzBuzzMaker.addRecord(3, "Fizz");
        fizzBuzzMaker.addRecord(5, "Buzz");

        for (int i = 1; i <= 100; i++) {
            System.out.println(fizzBizzMaker.generateString(i));
        }
    }
    
}

この問題には、3とか5の特定の数値に限らず一つ共通化できるロジックがある。それは指定した数値の倍数だった場合、指定した数値の対となる文字列を出力するという点。その部分を汎用化すると上のような記述になった。

オブジェクトのフィールド"table" に数値と対になる文字列を格納し、判定をする際はそのテーブルを見て文字列に変換する形になる。

こうしておけば、仮に「7の倍数が来たときに"Kazz" という文字列を表示する。3と7の倍数の場合は"FizzKazz", 5と7の倍数の場合は"BuzzKazz", 3と5と7の倍数の場合は"FizzBuzzKazz" を表示する」とかいう条件が増えても、レコードを一つ追加するだけで対応できる。

    public static void main(String[] args) {
        FizzBuzzMaker fizzBuzzMaker = new FizzBuzzMaker();
        fizzBuzzMaker.addRecord(3, "Fizz");
        fizzBuzzMaker.addRecord(5, "Buzz");
        fizzBuzzMaker.addRecord(7, "Kazz");    //追加

        for (int i = 1; i <= 105; i++) {
            System.out.println(fizzBizzMaker.generateString(i));
        }
    }

出力結果

1
2
Fizz
4
Buzz
Fizz
Kazz
8
Fizz
Buzz
11
Fizz
13
Kazz
FizzBuzz
16
17
Fizz
19
Buzz
FizzKazz
22
23
Fizz
Buzz
26
Fizz
Kazz
29
FizzBuzz
31
32
Fizz
34
BuzzKazz
Fizz
37
38
Fizz
Buzz
41
FizzKazz
43
44
FizzBuzz
46
47
Fizz
Kazz
Buzz
Fizz
52
53
Fizz
Buzz
Kazz
Fizz
58
59
FizzBuzz
61
62
FizzKazz
64
Buzz
Fizz
67
68
Fizz
BuzzKazz
71
Fizz
73
74
FizzBuzz
76
Kazz
Fizz
79
Buzz
Fizz
82
83
FizzKazz
Buzz
86
Fizz
88
89
FizzBuzz
Kazz
92
Fizz
94
Buzz
Fizz
97
Kazz
Fizz
Buzz
101
Fizz
103
104
FizzBuzzKazz

FizzBuzzKazzは105まで出てこなかった。。

JavaScript独自プロファイラ

処理と処理の間の時間を計測する独自プロファイラを作成した。拡張可能なように、関数型コンストラクタにて実装。(jQueryクックブックの5-8を参考にした)

var profiler = function (my, spec){
	var that = {};
        my = my || {};

	var log = [];
	var first;
	var last;
	
	that.time = function(message, since){
		var now = +new Date();
		var seconds = ( now - (since || last )) /1000;
		log.push( seconds.toFixed(3) + ': ' + message );
		return last =+ new Date();
	};
	that.done = function (){
		that.time( 'total' , first);
		return log;
	};
	first = last = + new Date;
	return that;
};

実行する際は以下のようにすれば良い

var p = profiler();
//処理
p.time('first');
//処理
p.time('second');
//処理
p.time('third');
var log = p.done();
//※jQuery利用
$.each(log, function(i, v){
	$("#log").append(v + "<br />");
});

実行時のログ出力例

0.019: first
0.016: second
0.027: third
0.062: total

こちらに実際に実行出来るサンプルがあります。http://blog.livedoor.jp/tshimogaisho/archives/1148608.html

Chrome拡張AutoPatchWorkはEvernoteと相性が良い

Google Chromeの拡張ツールでAutoPatchWorkというソフトがあります。このツールとEvernoteとの相性がとても良いです。

このツールはオートページャー機能というのを持っています。Webページの端までスクロールすると、次のページを自動で探しに行き、そのページ挿入してくれます。
例えば、今見ているページがpage1/page2/page3といったように複数に分かれている場合に、page1の端までスクロールされると自動でpage2を探して挿入してくれます。下の画像はGoogleの検索結果のページの例です。通常は1ページ目までで終了するはずが、2ページ目が自動で挿入されています。

Webページで気に入った記事をEvernoteにクリップしたり保存したりというのはよくあると思います。その場合、こういった複数ページに分かれた記事を保存しようと思ったら、本来はページごとに切り替えてそれぞれのページを保存しなくてはいけません。
しかし、この拡張ツールを入れておけば、記事が1ページにまとめて表示されるため、Evernoteへの保存が一発でできます。EvernoteにWebの記事をよく保存する人にはオススメです。

Javaでの文字数カウント(サロゲートペア)に関する実験2

正規化できないアイヌ語表記用文字の扱いに関して
前回の記事で、サロゲートペアが含まれていた時の文字数カウントについて書いた。
Javaでの文字数カウント(サロゲートペア)に関する実験1
その中で対処方法の一つとして、合成文字(「か」+「゛」(濁点)のような)が含まれていた場合に、java.text.Normalizerを使って正規化する方法を紹介した。合成文字を単一の符号位置の文字に変換してしまうやり方である。

しかし、この方法だとまだ穴があった。
「か」+「゜」(丸) のようなアイヌ語表記用の片仮名や鼻濁音表記用の平仮名・片仮名が来た場合である。

これらのアイヌ語表記用の文字(25文字)はUnicode上に単一の符号位置が与えられていない。そのため正規化しようとしても当然できないということになる。

これらの文字が含まれることを考慮し、厳密に文字数をカウントしたい場合は、正規化ではなくjava.text.BreakIteratorを使う方法が確実だと言える。

いやしかし、文字数のカウントだけでこんなに色々書くことになるとは思わなかった・・・。それだけ文字コードというものが複雑なものだということかもしれない。
参考図書

Javaでの文字数カウント(サロゲートペア)に関する実験

プログラマのための文字コード技術入門」を読んで。
Stringの文字数をカウントする時、String#length()メソッドでは厳密に文字数をカウントできない場合があるという。

実験

実際にそのケースを試してみる。

本来5とカウントしたいところが、7とカウントされてしまった。これは、文字列の中にサロゲートペアに該当する文字が含まれているためである(1文字目と2文字目)。最初の2文字は「齟齬」(そご)ではなく、「齟齬」の異字体である。サロゲートペアの場合、1つの文字に対し1つのchar値が対応するわけではなく、2つのchar値が対応する形になる。String#length()はcharの数をカウントするため、この場合結果は7となってしまう。
そこでJDK1.5から追加されたString#codePointCount()メソッドを利用してカウントしてみる。これは、文字の符号位置の数をカウントするメソッドである。

問題なく5文字と表示された。
ただ、これではまだ正確に文字数を数えられないケースがある。文字列の中に結合文字が含まれていた場合である。例えば「か」と合成用濁点「゛」が組み合わさって1文字となるパターンの場合、「゛」の数もカウントされてしまうこととなる。

そこで、今度は文字列の区切りを検出するjava.text.BreakIteratorクラスを利用する。このクラスは、人が認識するテキストの境界位置を見つける機能を持つ。

合成文字の場合でも5文字とカウントされた。
また、文字列自体の構造を変えてしまうという手もある。

これは、JDK6から導入されたjava.text.Normalizerというクラスを利用して正規合成を行い、結合文字を1つの文字に変換している(「か」+「゛」を 「が」に変換)。
文字列の正規化に関してはこちらを参考 「Java SE 6完全攻略」第56回 文字列の正規化

おわりに

今まで文字数をカウントするのはString#length()メソッドでOKだと思っていた。実際にこれを使って文字数チェックしているプログラムを何度か見たことがある。ただ、サロゲートペアで問題が起きたというのは聞いたことがない。サロゲートペアになるのは非常に特殊でほとんど使われないような漢字であり、Vistaから導入された文字であるため、システムで入力されることはほとんど無いのではと思う。
もしシステムの入力文字数チェック処理でString#length()メソッドが使われていたとしても、可能な文字数が少なくなるわけで、オーバーしてしまうわけではない。そのためさほど問題にならないのではないかと思う。
ただ、サロゲートペアとなる文字が300文字近くあるというのも気持ち悪い。String#charAt()メソッドの場合も、サロゲートペアの場合、同様のことが起きる。システムとしてはサロゲートペアを考慮し厳密に対処した方が望ましいのではと思う。