谷本 心 in せろ部屋 このページをアンテナに追加 RSSフィード

2016-01-06

[]AWS Lambda + Javaは、なぜ1回目と3回目の処理が重いのか?

以前のエントリーで、AWS LambdaでJavaを使ってDynamoDBを呼び出した際に、初回起動にとても時間が掛かったという話を書きました。

http://d.hatena.ne.jp/cero-t/20160101/1451665326


今回は、この辺りの原因をもう少し追求してみます。


なぜ1回目と3回目のアクセスが遅いのか?

AWS Lambdaの中身はよく知りませんが、おそらく、アップロードしたモジュールTomcatみたいなコンテナとして起動させて、外部からコールしているんだろうと予想しました。それであれば、2回目以降のアクセスが早くなることは理解ができます。

ただ、1回目と3回目だけが極端に遅くて、2回目、4回目以降は早くなるというところは腑に落ちません。


その辺りを調べるべく、staticなカウンタを使って、値がどんな風に変化するかを調べてみました。

こんなソースコードです。

public class Pid {
    static AtomicLong counter = new AtomicLong();

    public String myHandler() {
        long count = counter.incrementAndGet();
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println("Name: " + name);
        System.out.println("Count: " + count);
        return "SUCCESS";
    }
}

出力された結果は、次のようになりました。

回数NameCount
1回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal1
2回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal2
3回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal1
4回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal2
5回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal3
6回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal4
7回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal5
8回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal3
9回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal4
10回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal5
11回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal6
12回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal7

サーバIPアドレスが2種類あり、それぞれのサーバで、1から順番にカウントアップしていることが分かります。

なるほど、2台のサーバでロードバランシングしているのだと。そのため、それぞれのサーバの初回起動である、1回目と3回目の処理に時間が掛かるのですね。なかなか納得いく結果でした。

ちなみにロードバランシングは毎回このような結果になるわけではなく、1回目と2回目がそれぞれ別のサーバに行く(=処理に時間が掛かる)こともあります。


どんなコンテナを使っているのか?

先ほど「Tomcatみたいなコンテナ」を使っているんじゃないかと推測しましたが、実際、どんなコンテナを使っているのでしょうか。スレッドダンプを取って、確かめてみました。


こんなコードです。

public class StackTrace {
    public String myHandler() {
        new Exception("For stack trace").printStackTrace();
        Arrays.stream(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true))
                .forEach(System.out::println);
        return "SUCCESS";
    }
}

結果、こうなりました。

java.lang.Exception: For stack trace
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:8)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:434)
	at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:365)
	at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:967)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:231)
	at lambdainternal.AWSLambda.<clinit>(AWSLambda.java:59)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:93)

"Signal Dispatcher" Id=4 RUNNABLE

"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@19bb089b
	at java.lang.Object.wait(Native Method)
(略)

"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@4563e9ab
	at java.lang.Object.wait(Native Method)
(略)

"main" Id=1 RUNNABLE
	at sun.management.ThreadImpl.dumpThreads0(Native Method)
	at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:446)
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:9)
(略)

何やらシンプルな独自コンテナを使っているみたいです。何度か実行してみても結果は同じでした。

ソースコードがないので推測になりますが、アップロードされたzipをロードして起動する独自コンテナがあり、外部からAPIコールされた際にAWSLambdaクラスあたりが処理を受け取って、zip内のハンドラを呼び出しているのでしょう。


なぜコンストラクタで処理すると早いの?

そういえば、もう一つ、謎な挙動がありました。

それは、Credentialsを取るという重めの処理をコンストラクタで実行すれば、処理時間がかなり短くなるというものです。


ハンドラの中でCredentialsを取ると、実測値で24秒ぐらい、課金対象値で22秒ぐらいでした。

コンストラクタでCredentialsを取っておくと、実測値で8秒ぐらい、課金対象値で6秒ぐらいでした。

ここでいう実測値とは、手元のストップウォッチを使って計測したという意味です。


ここから推測できることは、コンストラクタは事前に処理されていて、そこは課金対象外になるのかも知れません。


・・・あれ、それなら、コンストラクタで重い処理をがっつり走らせて、ハンドラでその結果を取り出せば、課金額を抑えられるじゃないですか?

ということで、ハンドラの中で10秒スリープする場合と、コンストラクタスリープした場合の比較をしてみました。


こんな2つのクラスで試してみます。

public class Wait1 {
    static long origin = System.currentTimeMillis();

    public String myHandler() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "SUCCESS";
    }
}
public class Wait2 {
    static long origin = System.currentTimeMillis();

    public Wait2() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果は、、、


Wait1(ハンドラの中でsleep)

回数Before waitAftre wait課金対象値実測値
1回目3901039010100ms10秒
2回目3871038710100ms10秒
3回目209173091710100ms10秒
4回目688527885210100ms10秒

きっちり10秒sleepして、課金対象値もそのオーバーヘッド分ぐらい。ストップウォッチで計測した値も同じく10秒ぐらいになりました。


Wait2(コンストラクタでsleep)

回数Before waitAftre wait課金対象値実測値
1回目3381033814800ms25秒
2回目--100ms1秒以下
3回目2611035814800ms25秒
4回目--100ms1秒以下

えーっ、sleepは10秒だったのに、なぜか15秒分ぐらい課金されてしまい、ストップウォッチで計測すると25秒と、えらく時間が掛かりました。これは謎な挙動です。

2回目や4回目ではインスタンス生成が終わっているので、Before waitやAfter waitが出力されず、処理がすぐに終わるというのは納得ですが。


どうして、こんなことが起きるんでしょうか。

不思議に思って、CloudWatch Logsのログを確認してみると・・・

Before wait: 17 
START RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f Version: $LATEST 
Before wait: 249 
After wait: 10250 
Called: 14408
END RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f 
REPORT RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f	Duration: 14531.57 ms	Billed Duration: 14600 ms Memory Size: 128 MB Max Memory Used: 27 MB	

最初のBefore waitの後にAfter waitがなく、Lambdaの処理がSTARTした後に再度Before waitが呼ばれ、After waitした後に、4秒ほど待ってから、ハンドラ処理が実行されてCalledが呼ばれていました。

なるほど、つまりこういうことでしょうか。


1. コンストラクタが実行され、10秒sleepの途中でタイムアウトして強制的に処理が止められ、インスタンス生成を諦めた(プロセスごと破棄された?)

2. 改めてコンストラクタが実行され、10秒sleepした。

3. AWS Lambda内の処理か何かで4秒ぐらい処理が掛かった。

4. ハンドラが実行された。

5. 2〜4の間が課金対象となり、15秒弱となった。

6. ストップウォッチで計測した時間は1から5までの間なので、25秒弱となった。


要するに、コンストラクタで重い処理を行うような悪いことを考える人への対策として、コンストラクタは一定時間で(おそらく10秒きっかりで)タイムアウトして、いったんプロセスは破棄される。

その後、改めてコンストラクタの処理がタイムアウト関係なく実行されたうえで、AWS Lambdaの内部処理と、ハンドラ処理が行われ、すべての処理が課金対象となる、ということころでしょうか。


コンストラクタの処理が短い場合は、どうなるの?

ということで、sleepの時間を短くして、再挑戦してみます。

public class Wait3 {
    static long origin = System.currentTimeMillis();

    public Wait3() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(2000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果。

回数Before WaitAfter wait課金対象値実測値
1回目--100ms3秒
2回目--100ms1秒以内
3回目--100ms1秒以内

CloudWatch Logsでの出力

Before wait: 25 
After wait: 2026 
START RequestId: xxx Version: $LATEST 
Called: 2389 
END RequestId: xxx 
REPORT RequestId: xxx	Duration: 97.52 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	 
START RequestId: yyy Version: $LATEST 
Called: 16385 
END RequestId: yyy 
REPORT RequestId: yyy	Duration: 6.04 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	

今度はSTARTの前に、きちんとBefore waitもAfter waitも出力され、ハンドラ処理のみが課金対象となっていました。


まとめ

ここまでの話をまとめると、AWS Lambdaは・・・


1. 複数台のサーバで処理されるため、それぞれのサーバでの初回起動時には処理時間が掛かる。

2. 独自のコンテナを利用して、モジュールデプロイしている。

3. コンストラクタの処理が軽い場合は、ハンドラ内の処理だけが課金対象となる。

4. コンストラクタの処理が重い場合は、コンストラクタの処理 + 5秒弱 + ハンドラ内の処理が課金対象となる。


ということですね。


・・・とは言え、Credentialsの処理をコンストラクタで行った場合に、実測値まで早くなる辺りは、少しだけ不可解です。というか、Credentialsの取得処理が重いこと自体が不可解なのですが。

この辺りはもう少し追試験をしてみれば解析できそうですが、長くなるので、今回はこの辺りまでにしたいと思います。


いやー、Lambdaさん、なかなかよく考えられてますね!

2016-01-01

[]AWS LambdaでJavaNode.jsとGoの簡易ベンチマークをしてみた。

あけましておめでとうございます!

現場でいつも締め切りに追われデスマーチ化している皆様方におかれましては、年賀状デスマーチ化していたのではないかと憂慮しておりますが、いかがお過ごしですか。

エンジニアの鑑みたいな僕としましては、年始の挨拶はSNSでサクッと済ませ、年末年始は紅白など見ながらAWS Lambdaのソースコードを書いていました。


ということで、Lambda。

Lambdaを書く時に最初に悩むのは、どの言語を選択するか、なのです。


まず手を出すのは、サクッと書けるNode.js

ただNode.jsの非同期なプログラミングになかなか馴染めず、わからん殺しをされてうんざりしていました。みんなどうしているのよとtwitterで問いかけたところ @ からPromiseを使うべしと教わり、なるほど確かにこれは便利ですナと思いつつも、これが標準で使えないぐらいLambdaのNode.jsが古いところにまたうんざりしました。


では手に馴染んでるJavaを使ってみたらどうかと思ったら、メモリイーターだし、なんとなくパフォーマンスが悪い感じでした。詳しいことは後ほどベンチマーク結果で明らかになるわけですが。


それなら消去法で残ったPythonなのですが、僕マジPython触ったことないレベルであり、これは若者たちに任せることにしているので、Pythonも選択肢から消えて。


本来、Lambdaみたいな用途にはGoが向いているはずなのだけど、いつ使えるようになるんだろうなぁなどと思って調べてみたら、Node.jsからGoを呼び出すというテクを見つけて、こりゃいいやとなりました。

http://blog.0x82.com/2014/11/24/aws-lambda-functions-in-go/


ただGoを呼ぶにしてもNode.jsを経由するためのオーバーヘッドはあるわけで、じゃぁ実際にどれぐらいパフォーマンス影響があるのか、調べてみることにしました。


処理の中身

簡単に試したいだけだったので、数行で済む程度のごく簡単な処理を書いてベンチマークすることにしました。


1. 引数で渡されたJSONパースして、中身を表示する

2. DynamoDBから1件のデータを取得する


当初は1だけだったのですが、あまり性能に差が出なかったので2を追加した感じです。そんな経緯のベンチマークなので、実装も雑ですが、とりあえずソースをGitHubに置いておきました。

https://github.com/cero-t/lambda-benchmark


実行結果
回数Java(1)Java(2)Node.jsGo
1回目218006700900600
2回目13007300800500
3回目19000500200500
4回目10001300200400
5回目500200400500
6回目400100200500
7回目400300400500

時間はいずれもミリ秒のBilled duration


Java(1) : メモリ192MB。処理中にAmazonDynamoDBClientを初期化

Java(2) : メモリ192MB。処理前に(コンストラクタで)AmazonDynamoDBClientを初期化

Node.js : メモリ128MB

Go : メモリ128MB


メモリの実使用量は

Java : 68MB

Node.js : 29MB

Go : 15MB

でした。


考察など

正味の話、Lambdaで行っている処理などほとんどないので、性能的には大差ないかと思っていたのですが、思ったより特性が出ました。これは処理時間というよりは、関連ライブラリのロード時間な気がしますね。


Javaは最初の数回が20秒、実装を改善しても7秒とかなり遅かったのですが、ウォーミングアップが終わった後は200msぐらいまで早くなりました。呼ばれる頻度が高い処理であれば良いのでしょうけど、たまに呼ばれるような処理では、この特性はネックになる気がします。


ちなみにJavaだけは192MBで計測しましたが、最初にメモリ128MBで試したところ、30秒ぐらいで処理が強制的に中断されたり、60秒でタイムアウトするなど、計測になりませんでした。こういう所を見ても、Javaを使う時にはメモリを多め(CPU性能はメモリと比例して向上)にしなくてはいけない感じでした。


Node.jsも少しはウォーミングアップで性能が変わりますが、最初から性能は良い感じです。


Goを使った場合は性能が安定しており、ウォーミングアップしても性能が変わりません。メモリ使用量が少ないのも良いですね。Node.jsから呼び出す際のオーバーヘッドがあるせいか、性能的にはウォーミングアップ後のJavaNode.jsに一歩劣る感じでした。


なお、Pythonの実装をしてくれる人がいらっしゃれば、プルリクしていただければ、データを追加したいと思います。


結論

繰り返しになりますが、雑なベンチマークなので特性の一部しか掴んでないと思います。

そもそもJavaだけメモリが1.5倍になっている(=CPU性能も1.5倍になっている)ので公平ではないですし。


ただ「たまに行う処理」を「少ないリソース」で行うという観点では、Goで実装するのが良さそうです。GoはそのうちLambdaでも正式サポートされそうですしね。

きちんとしたベンチマークは、また別の機会にじっくり行ってみたいと思います。


ということで、Goを使う大義名分ができたベンチマークでした!

See you!

2014-12-15

[][]SqlTemplateっていうJdbcTemplateのラッパーを作ってみました。

「SQLが書きたいんや!」という想いのもと、

Spring Bootと組み合わせて簡単に使える、

JdbcTemplateのラッパーライブラリを勢いで作ってみました。


GitHubに置いています。

https://github.com/cero-t/sqltemplate


JdbcTemplate / NamedParameterJdbcTemplateをベースにして、

 1. SQLファイルが使えること

 2. Date and Time APIに対応すること

 3. publicフィールドに対応すること

 4. APIが今風であること

の4つを目的にして作りました。


それならMirageでいいんじゃね? という想いは消えませんが、

Spring標準機能のみを使うことによる、政治的な使いやすさを取りました。


作りましたって言っても、ただのラッパーですので

ソースコードはすっごく小さくて、空行とコメントを入れても600行ぐらいしかありません。

ジェバンニでなくとも一晩でやってくれるぐらいのサイズです。


利用イメージ

exampleのプロジェクトも作っておきました。

https://github.com/cero-t/sqltemplate/tree/master/sqltemplate-example


使う側のソースコードは、こんな感じになります。

@Component
public class SampleProcess {
    @Autowired
    SqlTemplate query;

    public void process() {
        List<Emp> emps = query.forList("sql/selectAll.sql", Emp.class);
        emps.forEach(e -> System.out.println(e.ename));

        Emp emp = query.forObject("sql/selectByEmpno.sql", Emp.class, 7839);
        System.out.println(emp.ename);

        Map<String, Object> condition = new HashMap<>();
        condition.put("deptno", 30);
        condition.put("job", "SALESMAN");
        emps = query.forList("sql/selectByCondition.sql", Emp.class, condition);
        emps.forEach(e -> System.out.println(e.ename));
    }
}

forObjectで1件検索、forListで複数件検索。

第一引数がSQLファイル名で、第二引数が戻り値の型、

第三引数以降がSQLにバインドするパラメータです。


ちなみにIntelliJを使っていると、ファイル名にカーソルをあわせて

Ctrl (Command) + クリックでSQLファイルを開けるのが嬉しいですね。


SQLファイルは、こんな感じになります。

select
    *
from
    emp
inner join dept
    on emp.deptno = dept.deptno
where
    dept.deptno = :deptno
    and emp.job = :job

第三引数に指定したMapやEntityの値を、SQLのパラメータとしてバインドします。

内部的にはNamedParameterJdbcTemplateに処理を委譲しているだけです。


名前を指定せずに ? を使うこともできます。

select
    *
from
    emp
where
    empno = ?

第三引数以降に指定した任意の数の基本型(String、Date、Number)をバインドします。

こちらは内部的にJdbcTemplateに委譲しているだけです。


使い方

使うための設定は一つだけ。

@Configurationアノテーションをつけたクラスに

SqlTemplateを返すメソッドを作り、@Beanアノテーションをつけます。

@Bean
SqlTemplate sqlTemplate(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
    return new SqlTemplate(jdbcTemplate, namedParameterJdbcTemplate);
}

この初期化の仕方は @ がpullリクエストで教えてくれました。

ありがとう!


ちなみにSqlTemplateのコンストラクタの第三引数には、

SQLファイルを読み込む際のテンプレートエンジンを指定することができます。

たとえばここで2-way SQLパーサーを指定すれば、

2-way SQLにも対応できるというスンポーです。


現時点でもFreeMarkerを使うことができるようにしているんですが、

一度も動作確認してないので、動くかどうか分かりません。てへぺろ。


いまあるAPI一覧

用意したメソッドの一覧は、以下になります。

<T> T forObject(String fileName, Class<T> clazz, Object... args)
<T> T forObject(String fileName, Class<T> clazz, Map<String, Object> params)
<T> T forObject(String fileName, Class<T> clazz, Object entity)

<T> List<T> forList(String fileName, Class<T> clazz, Object... args)
<T> List<T> forList(String fileName, Class<T> clazz, Map<String, Object> params)
<T> List<T> forList(String fileName, Class<T> clazz, Object entity)

int update(String fileName, Map<String, Object> params)
int update(String fileName, Object entity)
int update(String fileName, Object... args)

戻り値をMapにする「forMap」とか

実案件では欠かせない「batchUpdate」は、まだ作っていません。

委譲するだけなので、作っちゃえばいいんですけどね。


制限事項的なやつ

とりあえずコンセプト実証した程度なので、色々できません。

 1. 上に書いた通り、forMapとbatchUpdateがありません。

 2. JSR-310を使っているので、Java8でしか動きません。

 3. publicフィールドのないgetter/setterベースのJavaBeansは使えません。

 4. パッケージ名が変です。

 5. mvnリポジトリに置いてないです。

 6. README.mdちゃんと書け。


ひとまずは「こんなコンセプトでサクッとできたよ! 」っていう位置づけです。

ご自由に参考にしてください!

2011-12-05

[]いいから聞け! 俺が文字コードについて教えてやるよ Advent Calendar 特別編

長らく更新の止まっている「いいから俺文字コード」シリーズですが、

このたび、Java Advent Calendarの一環として復活させました!

Java Advent Calendarって?

本エントリーはJava Advent Calendarの5日目です。

Java Advent Calendarについては、以下のサイトをご覧ください。

http://atnd.org/events/22434


前の4日目は @akirakoyasu さんの「SDKで身近になるAmazon Web Service」

http://www.akirakoyasu.net/2011/12/04/easily-use-aws-through-sdk/

S3、SimpleDB、SESの使い方をサンプルコードつきで紹介しています。


次の6日目は @shuji_w6e さんの「JUnit のセカイ」

http://d.hatena.ne.jp/shuji_w6e/20111205/1323098690

同じ事前条件(前準備)のテストケースを内部クラスとしてまとめる方法が目ウロコでした。


では、文字コードの話に入りましょう。

今回は特別編として、Javaの開発案件で特に頻発する問題や

陥りやすい問題についてフォーカスして、お送りします。

問題1:開発中は問題なかったのに、本番環境では急に文字化けが起きるようになりました!

新人くん「開発中は問題なかったのに、本番環境で急にファイルが文字化けしたと連絡があったんです!」

先輩社員「結合試験の環境では、どうだったの?」

新人くん「えっと、ファイル出力は試してません・・・」

先輩社員「なんで?」

新人くん(だって先輩、やれって言わなかったじゃないですか・・・)

先輩社員「え、何か言った?」

いきなり気まずい雰囲気から始まった「いいから俺文字コード」ですが(しつこい)

この問題が起きる原因は、十中八九、デフォルトエンコーディングの違いによるものでしょう。


開発環境はWindowsを利用し、結合試験や本番環境ではLinuxを利用するというのは

よくある組み合わせだと思います(最近は開発環境にMacが増え始めましたが)


ファイルや文字列のデフォルトエンコーディングとして、

Windows環境では「Windows-31j」が利用され、

Linux環境では「UTF-8」や「euc-jp」が利用されるため、

結果的に、開発環境と結合試験環境で生成されるファイルのエンコードが

変わってしまうという現象が発生しうるのです。


そのため、特にサーバサイドJavaの開発においては、

「デフォルトエンコーディングを使わない」ことをルールづけたほうが安全です。


デフォルトエンコーディングを利用するクラスやメソッドとしては

以下のものが、よく使われています。

  • new String()
  • String#getBytes()
  • FileReader
  • FileWriter
  • new InputStreamReader()
  • new OutputStreamWriter()

このうち、

FileReaderとFileWriterは使わずに、FileInputStreamとInputStreamReaderで代用する、

それ以外のメソッドやコンストラクタでは、必ずエンコーディングやcharsetを指定するようにすれば、

開発環境と結合試験や本番環境で、結果が異なるということがなくなるでしょう。


ちなみに私は、Checkstyleの正規表現チェック機能を使って

エンコーディングの指定漏れを検出しています。

問題2:なぜか「〜」ニョロだけ文字化けします!

新人くん「なんか、特定の文字だけが化けてしまって、解決しないんですが・・・」

先輩社員「特定の文字って?」

新人くん「ニョロです」

先輩社員「あぁ、全角チルダね」

新人くん「え、何かご存知なんですか?」

先輩社員「あぁ、よく化けるよ」

キング・オブ・文字化けと言えば、この、チルダ(ニョロ)でしょう。

「〜」と「〜」

\uFF5Eと、\u301C


Windowsでは「綺麗なニョロ」と「汚いニョロ」と呼べば何となく伝わりますが、

Macだと同じ形に見えるので、より発見が困難ですね。


さて、

Javaでは(厳密に言えば、Java1.4.1以降では)

「Windows-31j」と「Shift_JIS」は少しだけ異なった範囲の文字を扱っており、

特に記号の扱いが異なっています。


問題のニョロについて言うと、

Windowsで普通に入力できる「〜」(\uFF5E)は「Windows-31j」の文字であり、

Macで普通に入力できる「〜」(\u301C)は「Shift_JIS」の範囲にある文字なのです。


「問題1」の最後に示したクラスやメソッドの引数に「Shift_JIS」を指定している

Windowsでよく入力する方の「〜」(\uFF5E)を扱うことができず、

ただの「?」、半角クエスチョンに文字化けしてしまうのです。


対策としては、

システム全体として、エンコーディングをUTF-8に統一するのがオススメです。


ただ、どうしても案件都合でShift_JISを選ばなければいけない場合は、

基本的には「Windowsを優遇」しましょう。


JavaソースコードやJSPファイルなどを検索して

「Shift-JIS」となっている箇所を「Windows-31j」に置換していけば、

きっとそれで問題は解決するでしょう。


、、、ただ、IEがWindows-31jという文字列を認識できない、という問題があるため

HTMLのMETAタグに指定するcharsetだけは、「Shift_JIS」のままにしておいてください。


この件については「IE Windows-31j」で検索してくださいね。

問題3:Windows-31jの文字だけ許容したいから、getBytes("Windows-31j")でチェックしますた!

新人くん「Windows-31jの文字だけ許容したいんですが、どうすればいいですか?」

先輩社員「getBytesして、バイト値がWindows-31jの範囲に一致するかどうか判定すれば良いんじゃない?」

新人くん「やってみます」

・・・

新人くん「なんか期待通りに動かないんですが・・・」

私自身、いくつかの案件で

「Windows-31jで扱える範囲内の文字列だけを許容したい」という

仕様を要望されたことがあります。


この要件は「JIS第四水準の漢字」や「サロゲートペア」などの

機種によっては対応していない文字の入力を防ぐことが目的なのです。


# 一方で ① や ㍼ のようなWindows固有文字については、

# イマドキのMacやケータイでも見えるから問題ない、という判断です。


さて、この要件自体の是非はさておき、

このチェック処理のために以下のようなコードが使われることがあります。

private boolean isWindows31j(String str) {
	byte[] bytes = str.getBytes();
	for (byte b : bytes) {
		if (isWindows31byte(b) == false) { // Windows31jの文字コード範囲かのチェック
			return false;
		}
	}

	return true;
}
// 実際は上位バイトと下位バイトのチェックをするので、もう少し違うコードになる。

残念ながら、この文字チェック処理では、

決して除外文字を検出することができず、常にtrueを返し続けます。


なぜか?

Windows-31jで扱えない文字がstrに入っていた場合、

getBytesした時点で、その文字は "?" を示す 0x3F に変換されてしまうのです。


たとえば「問題2」で話題にした、Mac用の「〜」(\u301C)も、

getBytesした時点で "?" というただの半角クエスチョンマークになり、

isWindows31byteメソッドの中でWindows-31jの文字と識別されてしまい、

「これはWindows-31jの文字だ」と誤判定されてしまうのです。


では、どうチェックするのが良いのか?

私は以下の処理でチェックすることをオススメしています。

byte[] bytes = str.getBytes(encoding);
return str.equals(new String(bytes, encoding));

詳細については、過去のエントリを参照してください。

http://d.hatena.ne.jp/cero-t/20100204/1265302329


フォーマットをバリデーションする際には、

「変換して、さらに逆変換した文字列が、元の文字列と一致するかどうか」

というチェックするのが王道だと、私は考えています。

問題4:せろさんの言う通りチェックしたのに、JIS第四水準の文字が通っちゃったんですけど!

新人くん「なんか、せろ部屋とかいうblogを見て文字チェックのコードを書いたんですが」

先輩社員「聞いたことないな」

新人くん「それを実際のWebアプリに組み込んだんですが、やっぱり第四水準の文字が通っちゃうんですよ」

先輩社員「そのサイトの説明が間違ってるんじゃないの?」

失敬な!


さておき、Webアプリ開発においては、

もう一つ追加で気をつけなければいけない事があります。

それが、文字の「数値文字参照」です。


たとえばWebブラウザで、charsetが「euc-jp」のHTMLページを開き、

入力ボックスに「euc-jp」の範囲外の文字を入力してsubmitした場合、

Webブラウザは、その文字を「数値文字参照」という形式に変換することがあります。

たとえば「❶」を「&#10102;」という文字列変換して送信してくるのです。


「&#10102;」という文字列の、文字を一つひとつ見れば、

もちろんWindows-31jの範囲内ですから、

「問題3」に書いたチェック処理ではtrueを返してしまい、通ってしまうわけですね。


この問題への対策としては、数値文字参照を含む(可能性のある)文字列について

一度、数値文字参照から文字への変換を行なう必要があります。


サンプルコードは長くなるので割愛します。

Googleで「数値文字参照 java」で検索してください。

問題5:サロゲートペア対策が必要だと、上の方から言われました。

※WindowsXPをご利用の方で、この「𠀋」という文字列が見えない場合、

 以降の章を楽しむために、このパッチを導入することをオススメします。

http://www.microsoft.com/ja-jp/windows/products/windowsvista/jp_font/jis04/default.aspx

先輩社員「お客に、サロゲートペアの対策をしろって言われたんだよねー」

新人くん「サロゲートペアって何ですか?」

先輩社員「1つの文字を2文字で表すヤツなんだけどね、Windows Vistaからサポートされて、Java5でも対応してるはずなんだけど」

新人くん「なんだか面倒臭さそうなことは、分かりました」

2006〜7年頃、つまり、Windows Vistaが出てくる頃に話題になり、

その後は誰も触れなくなってしまった用語、サロゲートペア。

「サロゲートペアとは一体何だったのか」というスレとか、立ってそうですよね。


サロゲートペアの詳細な話は割愛しますが、

Javaにおいては「char2つで、1つの文字を表現する」文字だと考えれば良いでしょう。


サンプルコードで見てみましょう。

String str = "𠀋";

System.out.println(str.length());

System.out.println(str.toCharArray().length);

このコードを実行すると

2

2

と表示されるのが、サロゲートペアなのです。


開発案件で「サロゲートペアに対策してください」と言われた場合、

多くの場合が「禁止してください」という意味でした。


禁止するだけなら、Character#isXxxSurrogateメソッドを使うと簡単です。

char[] chars = str.toCharArray();
for (char c : chars) {
	if (Character.isLowSurrogate(c) || Character.isHighSurrogate(c)) {
		return true;
	}
}

return false;

このコードでtrueが返ればサロゲートペアを含んでいるためエラーとし、

falseが返れば問題なしと判定してやれば良いのです。

問題6:サロゲートペアを許容しろと、上の方から言われました。

新人くん「お客様から、やっぱりサロゲートペアを許容して欲しいと言われたんですけど」

先輩社員「えっ、前は禁止って言ってたのに?」

新人くん「そうなんですが、やはり使う人がいるだろうし、WindowsXPを使う人も減ってきたということで」

先輩社員「ちゃんと押し返して来いよなー」

新人くん(そんな事言うなら、打ち合わせにちゃんと参加してくださいよ)

先輩社員「え、何か言った?」

さて、最後の問題は、サロゲートペアを許容する場合の話です。

そもそも、サロゲートペアを許容するとは、どういう意味なのでしょうか?


「問題5」では「𠀋」という文字が「2文字」として扱われると説明しましたが、

人間の目から見れば、どう見ても「1文字」です。

サロゲートペアを許容するためには、この差異をなくす必要があります。


たとえば「最大100文字」まで許容する入力ボックスに、

サロゲートペアを51文字入力した場合、

Javaとしては「102文字」として扱ってしまい、文字数オーバーとなりますが

人間の目には「100文字に到達していないのにエラーになった」ように見えるのです。

# 実際には、こんなことをする人がいないため、サロゲートペアはあまり問題になっていないのでしょう。


つまり、サロゲートペアを許容するポイントは、

特に「バリデーション処理を適切にすること」にあると、私は考えています。


見た目の文字数を取得するためには、String#codePointCountメソッドを利用します。

String str = "あ𠀋𠀋𠀋あ";

System.out.println(str.length());

System.out.println(str.codePointCount(0, str.length()));

この処理を実行すると

8

5

と表示されます。この「5」が見た目と一致しますね。


これまで長さのバリデーションを行なう際には

String#lengthメソッドを利用してきたと思いますが、

サロゲートペアに対応するためには

String#codePointCountメソッドを利用して判定を行なう必要があるのです。


また、サロゲートペアをRDBMSなどに保存する場合には

RDBMS側が対応しているかどうか(Unicodeの4byte対応)など、

周辺のミドルウェアがサロゲートペアに対応しているかどうかも

必ず確認すべきでしょうね。

まとめ

  • この連載は「いいから俺文字コード」シリーズって言うらしい。
  • 意図せず、デフォルトエンコーディングを使ってしまわないように気をつけろ!
  • Windows-31jとShift_JISは、全く別ものだから! そもそも、UTF-8でいいならそっちで!
  • getBytesしてから文字範囲チェックするのって、間違ってるから!
  • 数値文字参照に気をつけろ! 冷や汗かいてるだろ、そこのお前。お前のことだよ!
  • サロゲートペアなんて危険なもの、禁止にしてしまえ!
  • サロゲートペアに対応するなら、バリデーションでcodePointCountを使うのが肝だ!
  • この連載は「いいから俺文字コード」シリーズって言うらしい。

大事なことは、2回言いました。

2009-03-15

[]考えなしに肥大化する定数クラス。

よく定数クラスというものを見かける。

大体はXxxConstantsという名前で、public static finalなフィールドをたくさん持つクラス。


あるいは、定数クラス自身をinterfaceとして定義しておいて、

値を利用するクラスで、implementsするという手法も見かける。


初めて見た時には、便利な手法だと思ったけど、

その後、ひどい定数クラスを目にすることが少なくなかった。


定数クラスは、疎結合の考え方と全く合わないと僕は思う。


具体的に、悪い例を見ながら話していく。

public class XxxConstants {
	public static final String EOL = "\r\n";
	public static final String ENCODING = "UTF-8";
	public static final int HOGE_X = 480;
	public static final int HOGE_Y = 640;
	public static final int HOGE_INTERVAL = 20;
	public static final int FUGA_X = 40;
	public static final int FUGA_Y = 60;
	public static final int CONFIG_FILE = "/usr/xxx.properties";
	public static final int FOO_OPTION_HORIZONTAL = 1;
	public static final int FOO_OPTION_VERTICAL = 2;
	public static final int FOO_OPTION_LINEAR = 4;
	public static final int FOO_OPTION_DASHED = 8;
	public static final int MSG_ID_001 = "APP001";
	public static final int MSG_ID_002 = "APP002";
	public static final int MSG_ID_003 = "APP003";
}

なにか特別なアプリケーションを意識して書いたわけではないけど、

大体の定数クラスというものには、こんな定数が定義されている。


環境の定数や、ウィンドウのサイズ、ファイル名、形状のオプション、ログコード、

まさにごった煮だが、定数クラスは、得てしてこんな風に肥大化してしまいやすい。


まずこの肥大化が、最初の間違い。

クラスは「一言で表せるもの」ぐらいの粒度で作るべきだけど、

上の例だと「定数を定義するクラス」っていう曖昧な言葉でしか説明できない。

せめて、定数の種類ごとにクラスを分けるなどすることが必要だ。

環境の定数ならEnvironmentクラス、ログコードならLogCodeクラス、という風に。


定数クラスを作ってしまうと、その配下の多くのクラスは、定数クラスに依存する。

機能分割ができそうなのに、定数クラスに依存してしまうせいで、JARを分けられないこともある。

だから、機能や目的ごとに定数クラスを分割しておくべきだ。


ちなみにログコードを「MSG_ID_001 = "APP001"」みたいに定義するのは最悪だ。

利用する側では、XxxLogger.log(MSG_ID_001) となるが、何をロギングするのか全く分からない。

せめて、定数名を「MSG_USER_NOT_FOUND」とか「MSG_ID_001_USER_NOT_FOUND」など、

意味が推測できるものにした方が良い。


あと、たまに、特定の1クラス(仮にClassAとする)からしか使わない値も

定数クラスに定義することがあるけど、そんな場合、

大体は、ClassA自身の定数にしておいが方が、疎結合になって使いやすい。


「定数クラスとしてまとまっていた方が分かりやすい」っていう意見もあるけど、

だからといって全てをまとめるのは、設計を放棄していることに他ならない。


変更の頻度、変更のライフサイクル(稼動中か再起動時か)、変更する人は誰か、

をしっかり考えてから、設定ファイルか定数クラスか、各クラスに定義するかを

よく考えるべきだと思う。


次に、オプション。これはよく使われる手法で、SWTなんかでも、

Table table = new Table(shell,SWT.MULTI|SWT.FULL_SELECTION|SWT.BORDER);

こんな風に書いたりする。

これはこれで便利だと思ったんだけど、どのオプションが有効なのか分からず、そこで手が止まる。

某商用プロダクトでは、こんなオプションが数百個あって、

どれが実際に使えるのかを理解するために、随分と時間を掛けてしまった。


対策としては、Java5以降ならEnumと可変長引数を利用した方が良い。

要するに、コンストラクタの定義を、↓のようにする。

public Table(Composite shell, TableOptions... options) {
	// コンストラクタの処理
}

ここでTableOptionsとは、Tableのオプションを示すEnum。

こうしておけば、開発環境の自動補完だけでサクサクと開発ができる。

まぁEnumはそのためにあるんだから、当たり前か。


では、Java1.4以前ならどうするか。

せめて、クラスに対応した定数クラスを作るべきだと思う。


コンストラクタは、変わらないままだけど、

public Table(Composite shell, int options) {
	// コンストラクタの処理
}

引数にはSWTという定数クラスを使わせるんじゃなくて、

Tableクラスの定数か、TableOptionsクラスの定数を使わせる

そうすることで、少なくとも利用できるオプションが明確になる。


もちろん、自前でEnum風なことをしても良い、

public Table(Composite shell, TableOptions[] options) {
	// コンストラクタの処理
}

けど、さすがにこれはちょっとヤリスギだと思うし、やったことはない。


最後に、定数クラスをinterfaceにするのは、やめて欲しい。

public static finalで意味は十分に通じるし、

インスタンスを作られたくないなら、privateコンストラクタを書けば良い。


interfaceでないものを、interfaceとして利用するのは、

余計な混乱を生むし、若手に対する教育としても決して良いものではないと思う。


つらつら書いてきたけど、要するに、僕は定数クラスが嫌いだってこと。

定数クラスを見かけたら、何とかして削除できないかを考えるようにしている。