Spring Bootでprototypeスコープを使う方法あれこれ&ベンチマーク

今更感はありますが…。

Spring Boot(というよりSpring)でインジェクションされるコンポーネントのデフォルトのスコープはSingletonになります。
そのため、Singletonスコープのコンポーネントに対して、Singletonより短いスコープであるPrototypeコンポーネントをインジェクションしようと思った場合、@Scope("prototype") をつけてコンポーネントを定義してインジェクションさせるだけではうまくいきません。

prototypeスコープを使うには、いくつか方法があります。今回は4つの方法を試してみました。

なお、以下の本エントリでは、動作の説明のため、カウンタコンポーネントを利用します。

方法1: proxyMode = ScopedProxyMode.TARGET_CLASS を指定する

カウンタの定義。proxyMode を指定しているところがポイント。

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Counter1 {

    private int count;

    public int increment() {
        return count++;
    }
}

ロジックそのものは説明するまでもないですが、同じインスタンスに対して increment メソッドを呼ぶと、そのたびに、戻り値が0, 1, 2, …と1ずつ増えていくものです。

利用例(spring-boot-starter-webを使ったWebアプリを想定しています)

@RestController
public class Counter1Controller {

    @Autowired
    Counter1 counter1;

    @RequestMapping("counter1")
    public int counter1() {
        return counter1.increment();
    }
}

8080ポートでアプリを起動して、 http://localhost:8080/counter1 にアクセスすると、何度ブラウザをリロードしても、常に0が表示されます。 このことから、毎回新しいCounter1のインスタンスが参照されていることが分かります。

方法2: ApplicationContextからgetBeanで取得する

カウンタ。

@Component
@Scope(value = "prototype")
public class Counter2 {

    private int count;

    public int increment() {
        return count++;
    }
}

利用例。Counter2を直接@Autowiredでインジェクションせずに、 ApplicationContext#getBean で取得します。

@RestController
public class Counter2Controller {

    @Autowired
    ApplicationContext context;

    @RequestMapping("counter2")
    public int counter2() {
        Counter2 counter = context.getBean(Counter2.class);
        return counter.increment();
    }
}

アプリの動きは方法1と同じです。

方法3: @Lookupでnullを返す

カウンタ(方法2とまったく同じ)

@Component
@Scope(value = "prototype")
public class Counter3 {

    private int count;

    public int increment() {
        return count++;
    }
}

この方法の場合、新しくLookup用のサービスクラスを作ります。

@Component
public class Counter3Lookup {

    @Lookup
    public Counter3 lookup() {
        return null;
    }
}

Lookupメソッド経由で取得します。

@RestController
public class Counter3Controller {

    @Autowired
    Counter3Lookup lookup;

    @RequestMapping("counter3")
    public int counter3() {
        Counter3 counter = lookup.lookup();
        return counter.increment();
    }
}

方法4: JSR 330のjavax.inject.Providerを使う

ちょっと蛇足的ですが、一部Java EEAPIが利用できるので、それを利用します。 カウンタは方法2、方法3と同様。

@Component
@Scope(value = "prototype")
public class Counter4 {

    private int count;

    public int increment() {
        return count++;
    }
}

使う側は、 javax.inject.Provider#get メソッド経由で取得します。

@RestController
public class Counter4Controller {

    @Autowired
    Provider<Counter4> counterProvider;

    @RequestMapping("counter4")
    public int counter4() {
        Counter4 counter = counterProvider.get();
        return counter.increment();
    }
}

性能を比較

今回の記事のメイン。それぞれの方法について、実行時間的にどれくらいの差が現れるのかを簡単に計測してみました。

計測の条件をなるべく揃えるために、以下のように、方法1〜方法4までのカウンタを取得するためのコンポーネントを1枚かませてみます。

@Component
public class CounterProvider {

    @Autowired
    Counter1 counter1;

    @Autowired
    ApplicationContext context;

    @Autowired
    Counter3Lookup lookup;

    @Autowired
    Provider<Counter4> counterProvider;

    public Counter1 getCounter1() {
        return counter1;
    }

    public Counter2 getCounter2() {
        return context.getBean(Counter2.class);
    }

    public Counter3 getCounter3() {
        return lookup.counter3();
    }

    public Counter4 getCounter4() {
        return counterProvider.get();
    }
}

その上で、コンポーネントの取得&incrementメソッドをの呼び出しのセットを1,000,000呼び出すのに、どれくらいの時間がかかるのかを測ってみました。 以下のメソッドは方法1の場合ですが、他の方法についても同様に実施します。

    public int benchmarkCounter1() {
        long start = System.currentTimeMillis();

        int sum = 0;
        for (int i = 0; i < LIMIT; i++) {
            Counter1 c = counterProvider.getCounter1();
            sum += c.increment();
        }

        log.info("sum = {}, total = {} ms", sum, (System.currentTimeMillis() - start)); // sum はどれも 0

        return sum;
    }

結果

方法 実行時間
1: @ScopeproxyMode = ScopedProxyMode.TARGET_CLASS をつける 4,430ms
2: ApplicationContext#getBean 4,105ms
3: @Lookup 4,284ms
4: Provider#get 7,908ms

方法4だけが突出して遅いですね。
何度か試しても、この傾向は同じでした。

それ以外の方法はどれも大差なく proxyMode を使っても、顕著に遅くなる、ということはないようです。

まとめ

今回実行時間を比較してみることにした動機は、 方法1の場合に極端に処理が遅くなることがない、という裏付けをとることだったので、確認できてよかったです。

Spring Bootのアプリを作る上で、極力プロトタイプスコープのコンポーネントが必要ないようにはしているものの、どうしても必要な時は、1を使う、という方針でいこうかなーと思いました。

モダンJavaScript(ES6)で書いたコードがIEで動かなかったので対応ポイントをメモ

ES6で書いていたJavaScriptのコードがIE(※)で動かなかったので、その時に対応した内容のメモです。

IEのバージョンは9以上を想定してます。IE8、あるいはそれ以前となるとさらに面倒さが増すので。

各ブラウザの対応状況を知る

各ブラウザの対応状況を調べるには、以下のサイトが便利でした。

ECMAScript 6 compatibility table

「Show obsolete platforms」にチェックを入れると、サポート切れの古いブラウザについても状況を確認することができます。
なお、IE9以下は、ECMAScriptとして「6」ではなく「5」を選んでおかないと表示されません。ES6はIE10でも絶望的なのに、それより古いバージョンが対応してるわけないですもんね。。

Chromeが良くも悪くも手厚くサポートしているので、普段の動作確認をChromeでのみ行うのはリスクだなと実感。

前提条件

ES6のコードはBabelでトランスパイルして使っていました。 詳しく言うと、Babelはwebpackのloaderを使っており、各バージョンは以下の通りです。

  • babel-core (^6.5.2)
  • babel-loader (^6.2.3)
  • babel-preset-es2015 (^6.5.0)

とはいえ、今回の対応箇所は、webpack関係なく通用するものだと思います。

実際にやったこと

そもそもES6の新しい仕様をフルに使って書いていたわけではないため、実はそこまで手間はかかりませんでした。

Promiseを置き換え

原因)IEではネイティブでPromiseがサポートされていません。

対策)代替ライブラリとしてbluebirdを利用しました。

今回、webpackでビルドしていたため、webpack.ProvidePlugin を使うことで、既存のコードに手を加えることなく Promise をすべて bluebird を使うようにひねることができます。

webpack.config.js に以下を追加してあげればOKです。

  plugins: [
    new webpack.ProvidePlugin({  
      "Promise": "bluebird"  
    })
  ] 

Object.assign を置き換え

原因)Object.assign はオブジェクト同士をマージするメソッドですが、これもIEでは使えません。

対策)代替ライブラリとしてobject-assignを利用しました。

Object.assign を使ったコードをすべて置き換えて対応しました。

Object.assign(foo, bar);objectAssign(foo, bar);

console.log を置き換え

原因)IE9以前では、開発者ツールを起動していない場合、consoleがundefinedになります。

対策)代替ライブラリとしてConsole.log wrapperを使いました。

こちらも前述の Promise のように、単なる log メソッドとして呼び出せるようにしました。

webpack.config.js

  plugins: [
    new webpack.ProvidePlugin({  
      "Promise": "bluebird",
      "log": "consolelog"
    })
  ] 

を追加し、 console.log を使っている箇所を log に置き換えて対応しました。

ちなみに、consoleが undefined だったら自作のオレオレconsoleに差し替える、みたいな方法でももちろん可能なのですが、雑に作ってしまうと、開発者コンソールで見た時のログ出力行が、console.log を呼び出した行ではなく、全てオレオレconsoleの行になってしまう、みたいな残念なことになってしまうので気をつける必要があるかと思います。

forcedotcom/wscにプルリクエストを送ってみた

先日Qiitaにこんな記事を書きました。その続きです。

qiita.com

さて、変換仕様をwscを使ってアップロードするメソッドは2つあります。

  • void BulkConnection#createTransformationSpecFromStream(JobInfo, InputStream)
  • TransformationSpecRequest BulkConnection#createTransformationSpec(JobInfo)

Qiitaでは前者の方だけ紹介しました。
使い分けですが、それぞれのメソッドシグネチャを見ると想像がつく通り、CSVファイル(やその他何かしらのストリーム)がそのまま渡せる場合は前者を、プログラム的に変換仕様を渡したい場合は後者を使う、みたいな感じだと思います*1

後者の場合の利用例は以下のような感じ。

    TransformationSpecRequest specRequest = connection.createTransformationSpec(job);
    specRequest.addSpecRow("Name", "Full Name", "", "");
    specRequest.addSpecRow("Title", "Job Title", "", "");
    specRequest.addSpecRow("LeadSource", "Lead Source", "Import", "");
    specRequest.addSpecRow("Description", "", "Imported from XYZ.csv", "");
    specRequest.addSpecRow("Birthdate", "Date of Birth", "", "dd MMM yy");
    specRequest.completeRequest();

変換仕様は、データ項目の対応付け | Bulk API 開発者ガイド | Salesforce Developersでサンプルとして載っているspec.csvと同等のものです。

しかし、これがどうしてもエラーで動かず、最終的にwscのバグという結論に落ち着きました。
そこでコードを修正し、正常に期待した動きになるところまで確認したので、プルリクを送ってみました。

github.com

正直なところ、業務で使っているわけでもないので、別にマージされなくてもいいやと思ってはいるのですが、プルリクを送るまでの手順だけ、メモとして残しておこうと思います。
以下の流れです。
(追記:無事、マージされました。)

  1. forcedotcom/wsc を clone する
  2. clone してきたソースコードを修正する
  3. ビルド
  4. 動作確認用のコードにおいて、依存関係を修正し、Mavenリポジトリや(Salesforceからダウンロードしてきたものでなく)3. で自前ビルドしたjarを参照するようにする
  5. 期待する動作になることを確認
  6. プルリクを送る

このうち、 3. と 4. のところだけメモ。

3. wscのビルド

READMEの通りです(要Maven)。きちんと既存のテストがパスすることは確認します。

mvn clean package -Dgpg.skip

ビルドが成功すると、 target フォルダの下に force-wsc-35.2.6.jar のようなファイルが作られるので、これを使います。

4. 依存関係の修正

私はビルドにGradleを使っているので、元々、

dependencies {
    compile "com.force.api:force-wsc:35.2.7"
    compile "com.force.api:force-partner-api:35.0.1"
}

のようにしていたところを、

dependencies {
    compile files('/path/to/wsc/target/force-wsc-35.2.6.jar')
    compile ("com.force.api:force-partner-api:35.0.1") {
        transitive = false
    }
    compile 'org.antlr:ST4:4.0.7'
    compile 'org.codehaus.jackson:jackson-core-asl:1.9.13'
    compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.13'
}

のように修正します。force-partner-apiがforce-wscに依存関係を持っているので、推移的な依存関係を無効にしつつ(transitive = true)、ST4やらjacksonやらの必要なものだけ別途追加してます。

修正内容

ちなみに修正内容ですが、10行程度の修正の中で3〜4つのバグを直した(つもり)です。

  • 書き込んでいるCSVの内容が正しくない(先頭の項目の頭に余計なカンマが入ったり、改行されていなかったり)
  • completeRequestで処理の成否を判定する時に、常にリクエスト失敗と判定される
  • CSVしか扱えないのに、 Transport#connect でZIPフラグがtrueで処理されている

*1: 正直、プログラム中で変換仕様を組み立てたい場合でもByteArrayInputStreamあたりを渡せば事足りますが…。

5分くらいでわかるSwagger

最近身の回りでSwaggerを聞くことが多くなってきたと感じます。

上記記事でも言及されていますが、AWS API Gateway Importerでも、APIAPI GatewayにインポートするためのAPIの入力仕様としてSwaggerが使われています。
私もちょっと前から、AWS API Gatewayを使ってます。API GatewayAPIを作成するにあたり、GUIからポチポチAPIを作っていくのが辛いというのは多くの人の共通認識だと思うので、AWS謹製のインポータを使うのが今のところは最善の選択だと実感しています。

というわけで、Swaggerについての簡単なまとめ資料みたいなものを作りました。

Swaggerで調べると、Swagger codegenやSwagger UIをメインにした記事は多いのですが、Swagger Specificationの書き方そのものについて紹介している記事って少ないなぁと感じたので、その辺を意識して作りました。とはいえ、Swagger Specificationの構文はシンプルであり、公式ドキュメントも丁寧なので、5分くらいでざっくり分かる、というレベルにしています。

スライドでは触れていないですが、Swaggerの場合、'x-'で始まる独自の拡張項目を使うこともできて、AWS API Gateway Importerの場合、 x-amazon-apigateway-auth, x-amazon-apigateway-integration がそれに該当します。
これらについては、資料の趣旨が変わるので、別の機会にまとめようかと思います。

追記

Swaggerの周辺ツールとして、SpringのアノテーションからSwagger Specification(JSON)を生成するSpringFoxというものがあることを知りました。

Google Maps API v3で任意のDOMをマーカーにしてアニメーションで移動させたい

なぜかGoogle Maps APIを使うことがここ最近は増えました。

さて、Google Maps API v3を使って地図上にマーカーを表示させる場合、標準APIだけだと少し不足感を感じることがあります。
まず、標準APIで使えるのはデフォルトのピン(画像データ)や、指定した画像、SVG Path記法などに限られます。
また、アニメーションによってマーカーを移動させることはできません。標準で用意されているマーカーアニメーションもあるにはある ( Marker Animations)のですが、指定した位置にマーカーが降ってきてバウンドする、といったものなので、アニメーションによるマーカーの移動はできません。地図自体は panTo メソッドによって移動することができるのですが、マーカーだけをアニメーションでスムーズに移動することはできません。

Google Maps APIをサポートするライブラリ

というわけで、標準APIでできないことを可能にするために提供されているライブラリについて調査。

  • Google Maps API Libraries
    • Googleが出しているライブラリ郡です。GitHubのGoogle Maps OrganizationにはJavaPython用のクライアントなんかもあります。
    • 「任意のDOMをマーカーにする」というのは、まさにrich markerで実現可能です。
    • また、MarkerWithLabelというものもあります。マーカーにラベルを表示させるものですが、labelContentプロパティには任意のDOMを指定することができます。
  • SlidingMarker
    • Google謹製ではないですが、マーカーをアニメーションさせるためのライブラリ。marker-animateをベースに、移動後にマーカーがバウンドしないように改良されたものです。アニメーション処理自体はjQuey easing(animate)で行われているみたいですね。

結論

というわけで、SlidingMarkerとMarkerWithLabelを組み合わせることにしました。なぜこの組み合わせかというと、一番の理由は、そのまんまのデモが用意されているからですw

https://github.com/terikon/marker-animate-unobtrusive/blob/master/demo/markerwithlabelmove-sliding.html

webpackでビルドできるようにして使う

ここからは蛇足。

SlidingMarkerとMarkerWithLabelを使った別の理由として、SlidingMarkerとMarkerWithLabelのどちらもnpmとして提供されているのもポイントでした。最近はwebpackを使っているので、モジュールはBowerでなくnpmで提供されているものだけに集約したいのです。まぁ、js-rich-markerはbowerで提供されており、bowerで提供されているライブラリの依存解決もwebpackならできるため(Usage with Bower)、SlidingMarker + RichMarkerの組み合わせがベストっぽいですが…。

それはさておき、webpackでビルドすることを前提に、フロント用のJSもCommonJSスタイルで開発する際の手順ですが、2つポイントがありました。

  • 【ポイント1】デモで使われている markerwithlabel は、npmで提供されているものでなく、独自にカスタマイズされたもの(markerwithlabel.terikon.js)を使わなければならないです。
    • カスタマイズ版を使わないと、マーカーはきちんとアニメーションしますがラベルはアニメーションしないので、本体のマーカーが移動後に瞬間移動する、という動きになってしまいます。
    • すなわち、結局npmで提供されているmarkerwithlabelは使えませんでした…。
  • 【ポイント2】SlidingMarkerとMarkerWithLabelも共にgoogle.maps.Markerを拡張したものであるため、google.maps.MarkerをSligindMarkerでグローバルに置き換える必要があります。

MarkerWithLabelとしてmarkerwithlabel.terikon.jsを使うようにする

webpackの設定は webpack.config.js で記述します。ポイント的なところだけを抜粋すると以下の様なコンフィグになります。

var path = require('path');

var customMarkerWithLabelPath = path.join(__dirname,
  'node_modules',
  'marker-animate-unobtrusive',
  'vendor',
  'markerwithlabel.terikon.js');

module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    path: path.resolve(__dirname, 'public', 'js'),
    filename: '[name].js'
  },
  module: {
    loaders: [
      { test: customMarkerWithLabelPath, loader: 'exports?MarkerWithLabel' }
    ]
  },
  resolve: {
    root: __dirname,
    alias: {
      MarkerWithLabel: customMarkerWithLabelPath
    },
    extensions: ['', '.js', '.jsx', 'es6'],
    moduleDirectories: [
      'src',
      'node_modules'
    ]
  }
};

こちらを参考にしました。

2no.hatenablog.com

google.maps.MarkerをSligindMarkerで置き換える

SligingMarker.initializeGloballyを呼び出しても、webpackが色々と良きに計らってくれているため、MarkerWithLabel側が参照するgoogle.maps.Markerは、完全にSlidingMarkerに置き換わりません。
仕方なく、メインエントリ用のjsで以下のようにしました。

require('expose?$!expose?google.maps.Marker!marker-animate-unobtrusive'),

この辺りは、かなりバッドプラクティス感が漂っている気がします…。

そんなわけで、一応やりたいことはできました。SlidingMarker + RichMarkerの組み合わせができるかどうかに続きます(多分)

ちなみに、今回確認に使ったリポジトリはこちら(READMEすら書いていないですが、npm install 後に webpack でビルド後に npm start するとと動きます):

github.com

Spring Session利用時、Redis上のセッション情報をデバッグするツールを作った

Spring Bootによるアプリケーションでは、Spring Sessionという仕組みでRedisにセッション情報を保持させることができ、複数サーバ間でセッション情報を共有する仕組みを簡単に実現できます。

さて、デバッグのために、Redis上のセッション情報の中身を確認したいという状況があったのですが、セッション情報はシリアライズされているため、Redis CLIで取得した結果は読みやすいものとは言えず、もう少し読みやすい形で表示できるツールが欲しいと思いました。

そんなわけで、Spring Sessionに関連するデータをRedisから取得して、テーブル表示するツール(Spring Boot製のWebアプリケーション)を作ってみました。

ちなみに、デシリアライズに失敗した場合は、値を(無理やり)文字列化した値を表示します。

developとトピックブランチだけで運用してたらmasterを作ってなかったので作った

GitHubを使った開発フローで、本流であるdevelopと、プルリクを送るためのトピックブランチを使ってサイクルを回していました。

しばらくコミットを重ね、ようやくリリースに至り、masterブランチにdevelopをマージしようとしたところ、masterブランチを作ってなかったことに気づきました。

そんな時にやった手順。

$ git clone リポジトリのURL
$ git rm {すべてのファイル・フォルダ}
$ git checkout --orphan master
$ git commit --allow-empty "first commit"
$ git push origin master
  • 作業場所と別の場所にリポジトリをcloneします。
  • checkout時に --orphan オプションをつけると、空ブランチを作ることができます。
    ただ、既存(コミット済み)のファイルはunstageなファイルとして残るので、ブランチ作成前に削除しています(別の場所にcloneしたのはこのため)
  • 空コミットは --allow-empty オプションをつけて作成します。

改めてブログ記事にするとなんか面倒なことをしてる気もしますが、ともかくこれで、 "first commit" という1つの空コミットだけがある master ブランチをリモートに作成できます。

--orphan オプション便利ですね。GitHub Pagesを作る時なんかにも役立ちそうです。