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

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!

2015-12-24

[][]JMXで情報を取得する時のベンチマーク

JMHを使って、JMX経由でMBeanの情報を取る際のパフォーマンスを測定してみた。

ベンチマークソースコードはこちら。

https://github.com/cero-t/Benchmarks/blob/master/src/main/java/ninja/cero/benchmark/JmxBenchmark.java


ベンチマーク環境はMacBook Pro Late 2013 (Core i5 2.4GHz) で、

他のアプリなども立ち上げっぱなしの環境なのでノイズは多めでだけど、

傾向を見たいだけなのであまり気にせず。


VirtualMachine.attachのパフォーマンス

VMオンデマンドアタッチする際のパフォーマンス。


No1 : VirtualMachine.attacheしてからシステムプロパティを取ってdetachする

No2 : キャッシュしていたVirtualMachineを使ってシステムプロパティを取得する

@Benchmark
public void no1_vmAttach() throws Exception {
    VirtualMachine vm = VirtualMachine.attach(PID);
    vm.getSystemProperties();
    vm.detach();
}

@Benchmark
public void no2_vmCachedGetProperties() throws Exception {
    vm.getSystemProperties();
}

結果

BenchmarkModeCntScoreErrorUnits
JmxBenchmark.no1_vmAttachthrpt10653.834± 108.790ops/s
JmxBenchmark.no2_vmCachedGetPropertiesthrpt102330.529± 270.268ops/s

アタッチありは1.5msec程度、キャッシュした場合は0.4msec程度。

ということで、アタッチに掛かる時間は1msec程度と推定。割とでかい。


JMXConnectorFactory.connectのパフォーマンス

続いて、VMに対するJMX接続を行う際のパフォーマンス。


No3 : JMXConnectorの取得処理と、クローズ処理をする

No4 : キャッシュしていたJMXConnectorを使って、MBeanServerConnectionの取得とMbean情報を取得する

public JMXConnector no3_vmCachedGetConnector() throws Exception {
    String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");

    if (connectorAddress == null) {
        String agent = vm.getSystemProperties().getProperty("java.home") + File.separator + "lib" + File.separator + "management-agent.jar";
        vm.loadAgent(agent);
        connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
    }

    JMXServiceURL serviceURL = new JMXServiceURL(connectorAddress);
    JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceURL);
    jmxConnector.close();
}

@Benchmark
public void no4_connectorCachedGetMBeanCount() throws Exception {
    MBeanServerConnection connection = connector.getMBeanServerConnection();
    connection.getMBeanCount();
}

結果

BenchmarkModeCntScoreErrorUnits
JmxBenchmark.no3_vmCachedGetConnectorthrpt10612.652± 95.547ops/s
JmxBenchmark.no4_connectorCachedGetMBeanCountthrpt109047.803± 1480.741ops/s

JMXの接続と切断は1.6msec程度。おおまかVMに対するアタッチと同じぐらい。

接続したあとの、MBeanServerへの接続とMBean情報取得は0.1msecぐらいで、これは無視できる小さい。


ThreadMXBeanからThreadCountを取るパフォーマンス

今回の主目的はこれ。

ThreadMXBeanを使って情報を取るのと、

MBeanServerConnection.getAttributeで名前を指定して情報を取るのと、どっちが早いか。


No5 : MBeanServerConnection.getAttributeの名前指定でThreadCountを取得する

No6 : ThreadMXBeanを取得してから、getThreadCountで取得する

No7 : キャッシュしていたThreadMXBeanから、getThreadCountで取得する

@Benchmark
public void no5_connectorCachedThreadCount() throws Exception {
    MBeanServerConnection connection = connector.getMBeanServerConnection();
    Object count = connection.getAttribute(new ObjectName(ManagementFactory.THREAD_MXBEAN_NAME), "ThreadCount");
    sum += (Integer) count;
} 

@Benchmark
public void no6_connectorCachedGetThreadCount() throws Exception {
    MBeanServerConnection connection = connector.getMBeanServerConnection();
    ThreadMXBean threadBean = ManagementFactory.newPlatformMXBeanProxy(
            connection, ManagementFactory.THREAD_MXBEAN_NAME, ThreadMXBean.class);
    sum += threadBean.getThreadCount();
}

@Benchmark
public void no7_beanCachedGetThreadCount() throws Exception {
    sum += threadMXBean.getThreadCount();
}

結果

BenchmarkModeCntScoreErrorUnits
JmxBenchmark.no5_connectorCachedThreadCountthrpt108199.887± 1269.164ops/s
JmxBenchmark.no6_connectorCachedGetThreadCountthrpt102662.147± 585.627ops/s
JmxBenchmark.no7_beanCachedGetThreadCountthrpt108148.967± 1705.518ops/s

ThreadMXBeanを毎回取る(No6)は明らかにパフォーマンスが悪いけど、

ThreadMXBeanをキャッシュしている限りは、ThreadMXBeanから情報を取るのと、

MBeanServerConnection.getAttributeで取ることに性能差はなし。


まとめ

1. VirtualMachineへのattach/detachは時間が掛かるので、キャッシュすべき

2. JMX接続の確立/切断は時間が掛かるので、キャッシュすべき

3. MBeanServerへの接続は時間が掛からないので、無理にキャッシュしなくてよい(close処理もないのでリソース管理もしてない?)

4. MBeanServerConnection.getAttributeでもThreadMXBeanを使っても性能差はないので、ThreadMXBeanを無理にキャッシュしなくてよい


ベンチマークソースコード

https://github.com/cero-t/Benchmarks/blob/master/src/main/java/ninja/cero/benchmark/JmxBenchmark.java


現場からは以上です。

2015-12-12

[Java]Stream APIをつくろう

最近、上司からのパワハラではなく、部下からの「部下ハラ」というものがあると聞きますが、

それって要するにバックプレッシャー型ハラスメントでありリアクティブであるよなと思うこの頃ですが、

皆さんいかがお過ごしでしょうか。


さて、順調な滑り出しからスタートした本エントリーは Java Advent Calenda 2015 の12日目になります。


前日は最年少(?)OpenJDKコミッタの @ さんによる

DeprecatedがJDK9で変わるかもしれない件(JEP 277: Enhanced Deprecation)

明日は日本唯一のJava Championの @ さんのエントリーです。

なかなかエラいところに挟まれてしまったなという感じです!


では本題、

今日のテーマは「Stream APIをつくろう」です。


Stream APIって、使う側がよくフォーカスされるのですが、

作る側がどうなっているのか、あるいはその使いどころはどこなのか、というのはあまり情報がないように思います。


今日はその作る側にフォーカスしたいと思います。

少し長いエントリーですが、ゆっくりとおつきあいください。


おしながき

1. なぜStream APIを作りたいのか

2. お題:ディレクトリの直下にあるテキストファイルをまとめて Stream<String> として扱う

3. そもそもStream APIってどうやって作るの?

4. お題:AWS S3にあるテキストファイルをまとめて Stream<String> として扱う

5. まとめ、そして次回予告


1. なぜSteam APIを作りたいのか

もともとのきっかけは、AWSのS3上にある大量のログファイルを

elasticsearchに流し込む処理を書いたことでした。


ファイルサイズが膨大なのはもちろんのこと、ファイル数自体も大量ですから、

処理対象となるファイルをリストアップするだけでも大変という状況です。

なのでファイルリストを少しずつ作りつつ、

ファイルを開いて少し読んでは流し込むという、逐次処理をしなくてはいけません。


Java7までなら、うんたらHandlerを渡して処理をグルグル回すところですが

Java8なので、せっかくだからStream APIを使うことにしてみました。


ただ先にも書いたように、Stream APIを提供する側の処理はあまり情報がなかったため

@さんや@さん、@さんに色々と質問しながら勉強しました。

皆さん、ありがとうございました。今日はその辺りをまとめたいと思います。


ちなみに言葉の定義としてはStream APIを作るんじゃなくて

Stream APIのProducerを作るっていう方が正しいとは思うんですが

ぱっと見での分かりやすさのためにこうしました。


2. お題:ディレクトリの直下にあるテキストファイルをまとめて Stream<String> として扱う

まず手始めに、ローカルファイルにあるファイルを処理する方法から考えます。

特定のディレクトリの直下にテキストファイルが大量にあり、

ファイル名などは別に表示しなくても良いから、

テキストファイルを読み込んで、一行ずつ処理したいというシチュエーションです。


Files.listとFiles.linesをどう組み合わせるの?

ディレクトリ内にあるファイルの一覧を Stream<Path> として取得するのは Files.list メソッド、

ファイルを読み込んで Stream<String> として取得するのは Files.lines メソッドが使えます。

では、これを組み合わせる時は、どうしたら良いんでしょうか。


変換はmapメソッドだったよなーと思いつつ、こんな事をやると。

Stream<Stream<String>> stream = Files.list(path)
        .map(p -> {
            try {
                return Files.lines(p);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
stream.forEach(System.out::println);

Stream<Stream<String>> になり、恐ろしく期待外れの結果になります。

java.util.stream.ReferencePipeline$Head@421faab1
java.util.stream.ReferencePipeline$Head@2b71fc7e
java.util.stream.ReferencePipeline$Head@5ce65a89

ではどうするかというと、そう、みんな大好きflatMapです。

try (Stream<String> stream = Files.list(path)
        .flatMap(p -> {
            try {
                return Files.lines(p);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        })) {
    stream.forEach(System.out::println);
}

こうすれば無事に結果をStream<String>として取り出すことができます。


出力例はこんな感じ。

This is file1.
Hello world.
This is file 2.
This is file3.
Good bye world.

ちなみにFiles.listで作ったStreamはきちんとcloseする必要があるので

try-with-resourcesを使うのがオススメです。


flatMapは毎回closeを呼び出してくれる

ちなみにflatMapがスグレモノなところは、

flatMapメソッドの中で作ったStreamを、毎回必ずcloseしてくれるところです。


上の例ではFiles.linesを利用しているので、別にcloseする必要はないのですが、

たとえばBufferedReaderやInputStreamなどを作った場合などには、closeする必要があります。

そういう時は、flatMapメソッド内で作るStreamにonCloseを書いておけば、毎回呼び出されるのです。

try (Stream<String> stream = Files.list(path)
        .flatMap(s -> {
            try {
                BufferedReader reader = Files.newBufferedReader(s);
                return reader.lines().onClose(() -> { // (1)
                    try {
                        reader.close(); // (2)
                        System.out.println("closed."); // (3)
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        })) {
    stream.forEach(System.out::println);
}

(1) でStreamを作ったところにonCloseを書いて

(2) でBufferedReaderをcloseしています。

This is file1.
Hello world.
closed.
This is file 2.
closed.
This is file3.
Good bye world.
closed.

(3) で書いた「closed.」が3回表示され、きちんと毎回closeされていることが分かります。


この章のまとめ

1. Streamの要素からStreamに変換する時は、flatMapだ!

2. flatMapで作ったStreamは、きちんとcloseしてくれるぞ!


3. そもそもStream APIってどうやって作るの?

前の章ではflatMapが便利だという知見を得ました。

次は、そもそもStream APIってどうやって作れば良いのかを考えます。


恥ずかしながら全然知らなかったので、twitterで聞いてみたところ

@さんと @さんから、

Iteratorを作って、SpliteratorにしてStreamSupporに渡すと良いと教わりました。


この件については、BufferedReader.linesの実装が分かりやすすぎるので引用したいと思います。

public Stream<String> lines() {
    Iterator<String> iter = new Iterator<String>() { // (1)
        String nextLine = null;

        @Override
        public boolean hasNext() {
            if (nextLine != null) {
                return true;
            } else {
                try {
                    nextLine = readLine();
                    return (nextLine != null);
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
        }

        @Override
        public String next() {
            if (nextLine != null || hasNext()) {
                String line = nextLine;
                nextLine = null;
                return line;
            } else {
                throw new NoSuchElementException();
            }
        }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
            iter, Spliterator.ORDERED | Spliterator.NONNULL), false); // (2)
}

(1) Iteratorを実装して

(2) Spliteratorsを使ってSpliteratorに変換し、StreamSupportを使ってStreamに変換しています。

これがStreamを作る時の、ひとつの王道のようですね。


この章のまとめ

1. ルークよ、Iteratorを使え


4. お題:AWS S3にあるテキストファイルをまとめて Stream<String> として扱う

Streamのつくりかたが分かったところで、

次は、AWS S3にあるテキストファイルを処理する方法を考えます。

と、その前に、

AWSになじみのない方がここで読んでしまうことを防ぐために説明したいのですが、

この章の目的は、AWS S3の操作がしたいのではなく、

Java標準APIでStream APIを作れないようなパターンで、どのように操作できるかを考えることです。


なのでAWSだけでなく、たとえばDBや外部Webサーバなどでも同じパターンが使えますので、

AWSはあくまで一例として捉えてもらえればと思います。


今回利用するクラス

まず簡単に、AWS S3からファイルを取ってくる処理を実装するために必要なクラスを紹介します。


ちなみにAWS S3とはファイルストレージのサービスで、

ブラウザからのファイルアップロード / ダウンロードができる他にも

コマンドラインツール(AWS CLI)や、AWS SDK(AWS SDK for Javaなど)から操作ができます。

今回はAWS SDK for Javaを使うことにします


それで、使うクラスは以下の通りです。

  • AmazonS3Client
    • AWS S3にアクセスするためのクライアント。通信するくせにclose不要なやつ。
  • S3ObjectSummary
    • S3のファイル情報を持つクラス。Javaで言うPathやFileクラスに相当する。
  • ObjectListing
    • ファイル一覧取得結果のメタ情報を持つクラス。ここから List<S3ObjectSummary> を取り出すことができる。

あんまり多くないですね。


AWS S3へのアクセスを試す

まず簡単に、AWS S3にJavaからアクセスする方法を書いておきます。

型などがはっきり分かるよう、Stream APIを使わずに書きます。

AmazonS3Client client = new AmazonS3Client();
ObjectListing listing = client.listObjects("cero.ninja.public", "advent2015java"); // (1)
List<S3ObjectSummary> list = listing.getObjectSummaries(); // (2)

for (S3ObjectSummary s : list) {
    S3Object object = client.getObject(s.getBucketName(), s.getKey()); // (3)
    try (InputStream content = object.getObjectContent(); // (4)
         BufferedReader reader = new BufferedReader(new InputStreamReader(content))) {
        String line = reader.readLine();
        while (line != null) {
            System.out.println(line); // (5)
            line = reader.readLine();
        }
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

(1)「cero.ninja.public」という名前のバケットにある「advent2015java」というフォルダから情報を取り

(2) ファイル情報をListとして取り出して

(3) ファイルの中身を読み込んで

(4) InputStreamを取り出して

(5) 標準出力に表示しています。


簡単でしょう?

ただ、実は (1) のメソッド呼び出しは最大1000件までしか取得できないという制限があります。

1001件以降のデータを取るためには、別のメソッドを使う必要があります。

List<S3ObjectSummary> allList = new ArrayList<>(); // (6)

AmazonS3Client client = new AmazonS3Client();
ObjectListing listing = client.listObjects("cero.ninja.public", "advent2015java"); // (7)
List<S3ObjectSummary> list = listing.getObjectSummaries();

while (list.isEmpty() == false) {
    allList.addAll(list); // (8)
    listing = client.listNextBatchOfObjects(listing); // (9)
    list = listing.getObjectSummaries();
}

(6) Listを別に用意しておいて

(7) 最初の1000件を読み込み

(8) ファイル情報を (6) のListに入れながら

(9) 次の1000件を読む、を繰り返す


こんな風になります。

ただこれがたとえば10万件、100万件となってくると、Listがメモリを過剰に消費しますし

そもそも実際の処理を始めるまでのオーバーヘッドが大きくなりすぎます。


じゃぁwhileの中で処理しちゃえばいいやんか、という話になってきます。

AmazonS3Client client = new AmazonS3Client();
ObjectListing listing = client.listObjects("cero.ninja.public", "advent2015java");
List<S3ObjectSummary> list = listing.getObjectSummaries();

while (list.isEmpty() == false) {
    list.stream().forEach(this::doSomething); // (10)
    listing = client.listNextBatchOfObjects(listing);
    list = listing.getObjectSummaries();
}

(10) 取得した1000件を使ってすぐに処理を始めます。


これはこれで正解だと思いますしシンプルで良いのですが、

「S3から情報を取るクラスに処理を渡す」よりも

「S3から情報を取ってStreamにする」ほうが、何かと取り回しが楽なのですよね。


AWSへのアクセスをStreamにしよう

ということで、ここまで学んだ知識を利用して、

AWS S3にあるテキストファイルをまとめて Stream<String> にする方法を考えます。


まずはファイル一覧を取ってくる部分を、Iteratorにします。

public class S3Iterator implements Iterator<InputStream> {
    AmazonS3Client client;
    String bucketName;
    String key;

    ObjectListing listing;
    List<S3ObjectSummary> cache; // (1)

    public S3Iterator(String bucketName, String key) {
        this.client = new AmazonS3Client();
        this.bucketName = bucketName;
        this.key = key;
    }

    void load() {
        if (cache != null && cache.size() > 0) {
            return;
        }

        if (listing == null) {
            listing = client.listObjects(bucketName, key); // (2)
        } else {
            listing = client.listNextBatchOfObjects(listing); // (3)
        }

        cache = listing.getObjectSummaries(); // (4)
    }

    @Override
    public boolean hasNext() {
        if (cache != null && cache.size() > 0) {
            return true;
        }

        load();
        return cache.size() > 0;
    }

    @Override
    public InputStream next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }

        S3ObjectSummary summary = cache.remove(0); // (5)
        return client.getObject(summary.getBucketName(), summary.getKey()).getObjectContent(); // (6)
    }
}

(1) 読み込んだ1000件のファイル情報を保持しておくcacheを作っておき

(2) 初回はlistObjectsを使って読み込み

(3) 2回目以降はlistNextBatchOfObjectsを使って読み込みます。

(4) 読み込んだファイル情報はcacheに入れておき

(5) removeしながら取り出します。

(6) また、S3ObjectSummaryのStreamにはせず、InputStreamにしてから返します。

このようにすれば、Iteratorの外側でAmazonS3Clientを利用する必要がなくなるためです。


そして、このIteratorからStreamを作り、さらにflatMapを使ってStream<String>に集約しましょう。

Stream<InputStream> fileStream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
        new S3Iterator("cero.ninja.public", "advent2015java"), Spliterator.NONNULL), false); // (7)

Stream<String> stream = fileStream.flatMap(in -> { // (8)
    BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
    return reader.lines() // (9)
            .onClose(() -> { // (10)
            reader.close();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    });
});

stream.forEach(System.out::println); // (11)

(7) IteratorからSpliteratorを作り、そこからStreamに変換します。これでファイル一覧が Stream<InputStream> になります。

(8) InputStreamから読み込んだ内容を集約するために、flatMapを使って

(9) BufferedReader.linesで作ったStream<String>を集約します。

(10) ここでonCloseを書いておくと、BufferedReaderが毎回きちんとcloseされます。flatMapさまさま。

(11) 最後に標準出力への書き出しをしていますが、もちろんこの Stream<String> はどのように扱うこともできます。


という感じで、AWS S3上にあるテキストファイルを Stram<String> にして

処理することができるようになりました。


gzipやzipだったらどうするの?

ちなみに大量のテキストファイルが存在する場合、たとえばそれがWebサーバのログだったりすると

たいていにおいてgzip圧縮されているかと思います。

そういう時は、読み込む時にGZIPInputStreamを使うと良いでしょう。


先に出した例とあまり変わらず、GZIPInputStreamを挟んだだけの形となります。

Stream<String> stream = fileStream.flatMap(in -> {
    BufferedReader reader;
    try {
        reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(in), StandardCharsets.UTF_8)); // (12)
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
    return reader.lines()
            .onClose(() -> {
                try {
                    reader.close();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
});

(12) のところにGZIPInputStreamを挟みました。


では、読み込む対象が、複数のファイルを持つようなzipファイルだった場合はどうすれば良いでしょうか。

実はこれが割と難敵で、zipで処理を回せるようなIteratorをもう一つ作る必要があります。


ZipInputStreamにStream<InputStream>を返すようなAPIを作ってくれれば良いのにと思うんですが

(ZipFileやFileSystemsならStreamを作りやすいんですが、ZipInputStreamからは作れない!)

そのあたりは自前で少し実装する必要があります。

さすがにエントリーが長くなってきたので、今回は割愛します。


Spliteratorを直接作っても良い

@さんから、Iteratorを実装するよりも

AbstractSpliteratorを実装したSpliteratorを作ったほうが楽だよという知見をもらったので

それを使ったサンプルも掲載しておきます。

public class S3Spliterator extends Spliterators.AbstractSpliterator<InputStream> {
    AmazonS3Client client;
    String bucketName;
    String key;

    List<S3ObjectSummary> cache;
    ObjectListing listing;

    public S3Spliterator(String bucketName, String key) {
        super(Long.MAX_VALUE, 0);
        this.client = new AmazonS3Client();
        this.bucketName = bucketName;
        this.key = key;
    }

    @Override
    public boolean tryAdvance(Consumer<? super InputStream> action) {
        if (cache == null) {
            listing = client.listObjects(bucketName, key);
            cache = listing.getObjectSummaries();
        } else if (cache.isEmpty()) {
            listing = client.listNextBatchOfObjects(listing);
            cache = listing.getObjectSummaries();
        }

        if (cache.isEmpty()) {
            return false;
        }

        S3ObjectSummary summary = cache.remove(0);
        S3ObjectInputStream in = client.getObject(summary.getBucketName(), summary.getKey()).getObjectContent();
        action.accept(in);
        return true;

    }
}

tryAdvanceメソッドをひとつ作れば良いだけですので、

hasNextやnextを実装する時のように、状態がどうなるかよく分からなくなって混乱することもありません。

確かに、Streamを作ることだけが目的となる場合は、こちらを使った方が楽になりそうですね。


念のため、これを使った処理も書いておきます。

Stream<InputStream> fileStream = StreamSupport.stream(
        new S3Spliterator("cero.ninja.public", "advent2015java"), false);

Stream<String> stream = fileStream.flatMap(in -> { // 以下略

StreamSupportに直接渡すだけで済む、ということですね。


この章のまとめ

1. 「1000件ずつ読み出して処理」みたいなやつは、IteratorにしてStreamにすれば処理しやすいよ!

2. AbstractSpliteratorを継承すると、Spliteratorを作りやすいよ!


5. まとめ、そして次回予告

では今回のまとめです。

今回のエントリーでは、Streamを作るところと、

その使いどころとしてAWS S3上の大量ファイルを例に挙げて説明しました。


Iteratorを作り、Spliteratorにして、そこからStreamを作る、

また、Stream.flatMapを使って複数リソースをシームレスに処理することができました。


Stream.flatMapを使うときちんとリソースのcloseができるのは、便利さがじわじわ効いてくる系のやつで、

たとえばStreamの代わりにIteratorを取り回そうとすると、リソースのcloseタイミングで困ることになります。

そういう点でも、Streamというのは取り回しやすい形なのかなと思います。


さて、長くなってきたので、今回のエントリーはここまでにしたいと思います。


でも実はもう一つ、言及したかったテーマがあります。

それが「Hot ProducerとCold Producer」というお話です。


JavaのStream APIは、どちらかと言えば「既に存在するListやMap」などに対する処理を

簡潔に書くところがクローズアップされているように思います。

これはCold Producer、あるいはCold Streamと呼ばれているそうです。


一方で、HTTPリクエストや、JMS、AMQP、あるいはTwitterのstreamなど、

いわゆるイベントを受け取るようなもの、あるいは無限Streamになるようなものは

Hot Producer、Hot Streamと呼ばれます。


JavaではHot Producerを扱う場合には、Listenerを実装する形が一般的ですが

Streamとして処理することもできるはずです。


Reactive StreamsがJava9に入るとか、バックプレッシャー型がどうしたと言われている中で、

このHot ProducerをStream APIを使って処理するような考え方が、

Javaにおいても、今後、重要になってくるはずです。

次回は、その辺りを考えてみたいと思います。


Stay metal,

See you!

2012-12-10

[]若作りするためのJavaコード

このエントリーは Java Advent Calendar 2012 の10日目として書きました。

なんか色々あって、公開が2日ぐらい遅れてしまってごめんなさい(><)


前日は、Hideki Kishida (@quicy) さんの「Xtend の Lambda とストリーム処理」です。

http://legacy-style.blogspot.jp/2012/12/xtend-lambda.html


翌日は、Katsumi Kokuzawa (@kokuzawa) さんの「WiiRemoteJで遊ぼう on OSX 10.8.7」です。

http://kokuzawa.github.com/blog/2012/12/11/wiiremotejdeyou-bou-on-osx-10-dot-8-7/


どちらも今風ですね!


そんな中、若干、加齢臭が気になり始めたこの頃の私としては、

若さアピールのためのJavaプログラミングとかちょっと気にしたりするわけですよね。

イマドキのプログラミングスタイルにしなきゃいかん、的なね。


そんな同世代、あるいはもう少し上の、プログラマの定年を超えたJavaエンジニアの皆さんにも

業界に入ったばかりで、古い定石など知らないJavaプログラマの皆さんにも、

ちょっと見てもらいたい、「若作りするためのJavaコード」です。


要するに、イマ風のコードを書くための注意点、ですね。

ランキングづけしていきましょう。


まずは第三位から。

第三位 : no native2ascii, no life

Javaではメッセージの外部化や国際化対応などのために、

メッセージをまとめたリソースファイルを作成して

Propertiesクラスを使って読み込むという事を、よくやります。


皆さんも一度ならず経験があると思いますが、こんな感じですよね。

public static void main(String[] args) throws IOException {
	Properties props = new Properties();
	InputStream in = Main.class.getResourceAsStream("/messages.properties");
	try {
		props.load(in);
	} finally {
		in.close();
	}
	System.out.println(props.getProperty("message"));
}

そして、ここで読み込むリソースファイルはUnicode変換しておく必要があるため

native2asciiコマンドや、それに相応する開発環境、プラグインなどを利用していたかと思います。

message=\u30c6\u30b9\u30c8

なんかもうこの一連の流れが、おっさんなわけですね。


「native2asciiが許されるのは、Java5までだよねー!」


みたいな、意外と元ネタが十分に知られていなくて、ネタだけ先行しているようなものは

けっこう滑るので注意が必要なわけですが、

Java6以降ならメッセージは別に日本語のままでも良いんです。

message=テスト

というのも、Properties#loadメソッドにReaderクラスを渡せるようになったからです。

public static void main(String[] args) throws IOException {
	Properties props = new Properties();
	InputStream in = Main.class.getResourceAsStream("/messages.properties");
	Reader reader; 
	try {
		// readerを使ってCharsetを明示!
		reader = new InputStreamReader(in, "UTF-8");
		props.load(reader);
	} finally {
		in.close();
	}
	System.out.println(props.getProperty("message"));
}

Java5までは、CharsetのないInputStreamしか利用できませんでしたが

Java6には、Charsetを指定したReaderを利用できるようになった所がポイントです。

というか、こんなの最初から用意しとけよと誰しも思っているでしょうけどね。


ちなみにPropertiesクラスを使っているフレームワーク側が

ReaderではなくInputStreamを使っていれば、結局Unicode変換しなきゃいけないんですけどね!

第二位 : リソースはfinallyでクローズする?

第二位は、「リソースはfinallyでクローズする」という鉄則です。

もう何年間も鉄則だ、定石だと教わってきました。

第三位に書いたソースも、きちんとその鉄則をきちんと守っています。素晴らしい!


「と思うやんかー? finally不要なんよー。」


テレビCMを文字ったネタは、意外とテレビを見ない昨今は滑りやすいので、これまた注意が必要で

どちらかと言えば、ネットの流行語を取り入れた方が良いわけですが、最近はこんな風に書きます。

public static void main(String[] args) throws IOException {
	Properties props = new Properties();
	try (InputStream in = Main.class.getResourceAsStream("/messages.properties");
			Reader reader = new InputStreamReader(in, "UTF-8");) {
		props.load(reader);
	}
	System.out.println(props.getProperty("message"));
}

Java7以降はtry-with-resourcesを使うことでリソースが自動的にクローズされるため、

わざわざfinallyでクローズする必要はありません。


もう十年以上も言われ続けてきたfinallyでのクローズも、いよいよおさらばですね!

第一位 : pubilc static void main(String[] args)

ところで、mainメソッドの宣言と言えば、

pubilc static void main(String[] args)

皆さん、こうですよね。


たまに、おいたしちゃって

pubilc static void main(String args[])

こんな形になっている宣言もあったりしますが、まぁこんなもんですよね。


でも、最近の若者は、こんな風に書くらしいですよ?

public static void main(String... args)

_人人人人人人人人人人人_

> 突然の可変長引数! <

 ̄^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄


テンポが悪いと滑る感じにすらならないため、大けがをしかねないわけですが

確かにmainメソッドは、まさに可変長引数がぴったり合うところです。

長年 String[] args と宣言してくると、なかなか気づかなかったりしますよね。


いまだEclipseでのmainメソッドの自動生成は String[] args なわけですが

各IDEでどんなmainメソッドを作っているか調べてみたら、

ちょっとしたイマドキ判定ができるかも知れませんね!

まとめ

Before

public static void main(String[] args) throws IOException {
	Properties props = new Properties();
	InputStream in = Main.class.getResourceAsStream("/messages.properties");
	try {
		props.load(in);
	} finally {
		in.close();
	}
	System.out.println(props.getProperty("message"));
}

After

public static void main(String... args) throws IOException {
	Properties props = new Properties();
	try (InputStream in = Main.class.getResourceAsStream("/messages.properties");
			Reader reader = new InputStreamReader(in, "UTF-8");) {
		props.load(reader);
	}
	System.out.println(props.getProperty("message"));
}

・元ネタは知らないけど、ネタだけ知っている、というものは滑りやすい。

・テレビCMネタは、テレビを見ていない人が多い昨今は多用を避けること。

・突然の◯◯! は意外性とともにテンポ良く出すこと。


トラシューネタとか言っておいて、

全然違うネタを出して、ごめんなさいね!

今度また連載にでもしますよ!

おまけ

少しだけ宣伝ですが、

日経ソフトウエアの「Javaのイケてるコード、残念なコード」という連載にて

1月号、2月号の2回に分けて

「そのコーディングスタイルはもう古い、Javaの新定石を学ぶ」を執筆しました。

http://www.acroquest.co.jp/company/press/2012/1125p1.html

http://ec.nikkeibp.co.jp/item/backno/SW1176.html


今回書いたような内容を含む、Java5からJava7あたりまでに導入された機能を使った

イマドキの書き方を紹介していますので、ぜひそちらもご覧ください m(_ _)m

2010-10-04

[]JavaOne2010まとめ テクニカル編

JavaOne2010が閉幕してから約一週間。

そろそろ落ち着いてきたので、JavaOneまとめを書く。


まずは、技術面で印象的だったことなどから。


Oracleはクラウドに向かう。

これまでの否定的な態度から一転して、

ラリーエリソンはクラウドを雄弁に語っていた。


今までは「クラウドと言うが、ただのWebアプリに過ぎない」と言っていた

否定的な態度は、Salesforceに対してぶつけつつ、

Amazon EC2のようなサービスこそがクラウドであり、

Oracleもそこを目指すと語った。


ところで、Oracle Open WorldのKeynoteで発表されたExalogicだけでなく

Coherenceというキーコンポーネントも元々Oracleは持っており

いつでもクラウドを推進できたはず。


ラリーエリソンの態度を一変させたのは、この時代の流れなのか、

Sunという「ハードウェアベンダ」を手に入れたためなのか、

ちょっと私には分からないんだけど。

HotRockitが今後の主流になる。

「Oracle's Java Virtual Machine Strategy」というセッションが面白かった。

http://d.hatena.ne.jp/cero-t/20100923/1285343999


HotSpot(旧Sun Java)に、

JRockit(旧BEA Java)の機能をマージした

通称「HotRockit」が、来年中に登場する予定。


いまHotSpotを使っている人たちは、

恐らく何の迷いもなく(あるいは気づくことなく)HotRockitに乗り換えるはずで、

今後、HotRockitがトップシェアになっていくはず。


特にJRockit Mission ControlやJRockit Flight Recorderといった

モニタリングツール、トラブルシューティングツールが

HotSpotでも使えるようになる所が最大の見どころ。


この辺りのツールに目がない人は、

今のうちからJRockitのツールを触っておくと良さそう。

Future Java : Yet Another Javaが求められている?

もう一つ面白かったのが

「The Next Big Java Virtual Machine Language」というセッション。

http://d.hatena.ne.jp/cero-t/20100920/1285149540


スピーカーのサイトでも詳細がまとめられていたので、そちらも紹介。

http://www.jroller.com/scolebourne/entry/the_next_big_jvm_language1


セッションも割と人が集まっており、

やはり「Javaの次」を求めている人達が増えてきていると感じる。


上に書いたセッションでは、「Java8(やJava9)がそうなったら、どうだろう」

という締めくくり方であったが、ちょっと僕はそうは思わない。

(どうもスピーカー自身もそうは思ってないみたいだけど)


Oracle自身がJavaのライバル言語を産み出すメリットはないし、

オープンソース戦略に打って出ることも、ちょっと考えにくい。


Next Big JVM Languageを提供するのは、やっぱりGoogleじゃないかな。

あるいは傑出した個人かも知れないけど。

誰かが動き出さなければ、いつまでも絵に描いた餅のままになってしまう。

JavaSE : やっとJava7が登場する。

いよいよ来年、本当に、今度こそ、Java7が登場しそう。

2011年半ばにJava7がリリースされ、2012年後半にJava8がリリースされる。


モジュラリティやクロージャーはJava8に回ったけど

Invoke DynamicなんかはJava7に入ってくる。


Invoke Dynamicは、特に動的言語の性能や、クラスのHotSwap辺りに影響が大きいので

Java7登場後の、周辺プロダクトのリリースが楽しみになる。


ところで、

ここ数年間のJavaの停滞感を、今後Oracleがどう払拭するかは、とても見もの。


たとえば、JCPとの連携をきちんと復活させ、

コミュニティから意見を広く集め直すとか?


悪い方の予想で言えば、(歩みを遅める理由になった)OpenJDKに対して

全くリソースを割かないようにして、事実上ストップさせるとか。

JavaEE : 次が見えてこない。JavaVE観点からの機能追加があるか?

JavaSEと違って、次の姿が見えてこないのがJavaEE。

もっと言えば(いつも言ってるけど)世の中がどれだけJavaEEに

期待しているのか、私にはよく分からない。


次が見えてこないJavaEEだけど、

個人的には、JRockit Virtual Edition(VE)の存在が、

JavaEEに影響するんじゃないかと思ってる。


OS不在で動くVEだからこそ、

「OSありの時にはできたけど、VEになってから出来なくなったこと」

なんてことが出てきて、その一部がJavaEEの機能に対するニーズになるとか。


仮にIBMなどのJVMベンダもVEに追従してきたら、

将来的には「JavaEE = JavaVE」ぐらいのイメージで

語られるようになっても、不思議じゃないかな。

JavaME : 奇策で逆転満塁ホームランを狙う以外ない。

将来が見えないどころか、なくなってしまうんじゃないかと思うのが、JavaME。

iPhone / Androidに対して、Oracle / Javaがどう打って出るのか

という話が聞けることを期待していたんだけど、全く何も出てこなかった。

HTML5周りの話が多少出てたけど、期待できるもんじゃない。


このままでは、iPhoneやAndroidには引き離されるばかりなので

(台数だけで言えば、まだJavaMEの方が相当上回ってるだろうけど)

追いついていくためには、何か奇策が必要。


たとえば、今から規格を統一し直して、

Java搭載ケータイで共通的に使えるアプリストアを構築するとか、

いっそAppleやGoogleに売ってしまうだとか。まぁあり得ないよね。


ただ、なんか大掛かりの手を打たない限りは、

ただガラケーでの既得権益を得るだけの

つまらないビジネスになってしまうんじゃないかな。