Code Climate + Circle CI でRubyプロジェクトのコードカバレッジを計測する手順
Code Climate + Circle CI でRubyプロジェクトのコードカバレッジを計測する手順です。 試したのはCircle CIですが、テストが実行できる環境であればTravis CIやJenkinsでも同じ仕組みでできるはず。
概要
- テストに SimpleCov を仕込み、コードカバレッジを計測
SimpleCov のレポーターに codeclimate-test-reporter を追加して、結果を Code Climate に送信
→ Code Climate でカバレッジが集計されるようになります。あとは、Circle CIなりTravis CIなりでテストを実行すればOK
簡単ですね。
1.テストに SimpleCov を仕込んでカバレッジを計測する
SimpleCov をbundlerでインストールします。
Gemfileに以下を追加して、
gem 'simplecov', :require => false, :group => :test
インストール。
$ bundle install
テストの先頭に、SimpleCovの設定を行うコードを追加します。
require 'simplecov' # カバレッジレポートの出力先を指定 # Circle CIで実行する場合は、ビルド成果物置き場に、 # ローカルで実行する場合は、 ./build/coverage に作成します。 dir = File.join(ENV['CIRCLE_ARTIFACTS'] || 'build', 'coverage') SimpleCov.coverage_dir(dir) SimpleCov.start do # /vendor/,/spec/ を集計対象から除外 add_filter '/vendor/' add_filter '/spec/' end
テストを実行すれば、 ./build/coverage/index.html にカバレッジレポートが作成されるはず。
2. SimpleCov に codeclimate-test-reporter を追加する
Gemfileに一行追加して、bundler でインストール
gem 'codeclimate-test-reporter', :require => false, :group => :test
$ bundle install
SimpleCov の設定部分を以下のように変更。レポーターを追加します。
require 'simplecov' require 'codeclimate-test-reporter' # ★追加 dir = File.join(ENV['CIRCLE_ARTIFACTS'] || 'build', 'coverage') SimpleCov.coverage_dir(dir) SimpleCov.start do add_filter '/vendor/' add_filter '/spec/' # ★追加 formatter SimpleCov::Formatter::MultiFormatter[ SimpleCov::Formatter::HTMLFormatter, CodeClimate::TestReporter::Formatter ] end
3. Circle CIに 環境変数を追加
最後に、環境変数 CODECLIMATE_REPO_TOKEN を設定します。 これをしないと、レポーターを追加しても 結果の送信はされません。
まず、CODECLIMATE_REPO_TOKEN の値を Code Climate の Settings ページから取得します。(プロジェクトごとに異なります。)
Circle CIのページの右上にある、Project Settingsをクリック。
Enviroment variables に移動し、
CODECLIMATE_REPO_TOKEN を設定します。
あとは、Circle CIでテストを実行すると、Code Climate のページでコードカバレッジが見られるようになるはず。
参考: バッジを貼る
Code Climate の Settings ページに各種形式のURLがあるので、コピペして貼り付ければOK
キャッシュしたデータが消える!?prefork型HTTPサーバーUnicornでドはまりしたメモ
HTTPサーバーUnicornを使っていてドはまりしたのでメモ。
主に、Unicornで動かすRackアプリでの変数スコープとpreload_appについて。
サマリ
- Uncornは、RackアプリケーションのためのHTTPサーバーです。
- HTTPリクエストを、メインプロセスからforkした子プロセスで処理するアーキテクチャが特徴。
- スレッドとかは使いません。
- forkした子プロセスは、それぞれ別のメモリ空間を持つので、
- HTTPリクエストの処理間で、状態(変数の値)が共有されません。
- ある処理で変数の値を変更しても、別の子プロセスで処理しているHTTPリクエストでは変数値は変わりません。
- ただし、子プロセスが同じ場合は、状態が引き継がれます。(処理が終わった子プロセスは、次のHTTPリクエストで使いまわされます)
- 状態が共有されないので、スレッドセーフティを考慮した実装にする必要もありません。
- もし、メモリリークしても、子プロセスを再作成すればOK。
- HTTPリクエストの処理間で、状態(変数の値)が共有されません。
ドはまりした状況を再現する
↓のような、オンメモリキャッシュを作っていました。
# coding: utf-8 require 'sinatra/base' require 'singleton' # キャッシュ class Cache include Singleton def initialize @store = {} @mutex = Mutex.new end def create_or_get(key, &generator) @mutex.synchronize { unless @store.include? key puts "cache is not exists. key=#{key}" @store[key] = generator.call else puts "cache exists. key=#{key} value=#{@store[key]}" end @store[key] } end end class App < Sinatra::Base get '/' do Cache.instance.create_or_get(:date) { DateTime.now.iso8601 } end end
リクエストを何回か投げてみます。
cache is not exists. key=date 10.0.2.2 - - [14/Feb/2015 12:32:18] "GET / HTTP/1.1" 200 25 0.0587 cache exists. key=date value=2015-02-14T12:32:18+09:00 10.0.2.2 - - [14/Feb/2015 12:32:19] "GET / HTTP/1.1" 200 25 0.0021 cache exists. key=date value=2015-02-14T12:32:18+09:00 10.0.2.2 - - [14/Feb/2015 12:32:21] "GET / HTTP/1.1" 200 25 0.0084 cache is not exists. key=date ★ 10.0.2.2 - - [14/Feb/2015 12:32:22] "GET / HTTP/1.1" 200 25 0.0234 cache exists. key=date value=2015-02-14T12:32:22+09:00 10.0.2.2 - - [14/Feb/2015 12:32:26] "GET / HTTP/1.1" 200 25 0.0071
キャッシュされたはずのデータが、、消えてる・・・!?・・・って、Unicornの仕組みを考慮すれば、そりゃそうなんですが。
恥ずかしながら、その辺の仕組みを知らずに何気なく使っていた私は、ドはまりしました。
そもそもforkとは何か
fork(フォーク)とは、プロセスのコピーを生成するものである。
...
forkが呼び出されると、子プロセスのためのアドレス空間が新たに作成される。子プロセスのアドレス空間には親プロセスが持っていた全セグメントのコピーがあるが、コピーオンライト機能によって実際の物理メモリの確保は遅延される
- forkとは、プロセスのコピーを生成するもの
- forkしてできた子プロセスは、独自のアドレス空間を持つ。
- また、子のアドレス空間には、親プロセスが持っていた全セグメントのコピーが作成される。
- この時、メモリを全コピーするとコストが高いので、コピーオンライトの仕組み提供されている。
- fork時に、実際にはメモリの値をコピーせず、親子で共有する。
- 子プロセスがメモリを書き換えたときに、そのページだけコピーを作成して使用する。
ふーむ。
preload_app
さて、この仕組みだとすべてのプロセスがRuby VMをごっそり持つので、
-
- メモリたくさん食うんじゃね?
- 子プロセスごとにRackアプリのロードが走って遅いんじゃね?
とか思うわけですが、このあたりの問題を回避する仕組みとして、preload_app(の設定)があります。
preload_app=trueにすると、メインプロセス内でアプリのロードが行われ、fork時にまるまる複製されるようになります。なので、子プロセス側でのロードは発生しません。また、コピーオンライトの仕組みでメモリも大部分が共有されるので、実メモリの使用量も少なくて済みます。
ということで、検証。
ロードに時間がかかる状態をエミュレーションするアプリを作ります。
# coding: utf-8 require 'sinatra/base' require 'singleton' puts "start loading." sleep 5 puts "end loading." class LazyApp < Sinatra::Base get '/' do "Lazy" end end
preload_app=true で起動すると、子プロセスのforkの前に1度だけロードが走ります。
I, [2015-02-14T14:42:14.329315 #8117] INFO -- : Refreshing Gem list start loading. end loading. I, [2015-02-14T14:42:19.411606 #8117] INFO -- : listening on addr=0.0.0.0:5000 fd=9 I, [2015-02-14T14:42:19.411817 #8117] INFO -- : worker=0 spawning... I, [2015-02-14T14:42:19.413196 #8117] INFO -- : worker=1 spawning... I, [2015-02-14T14:42:19.413949 #8144] INFO -- : worker=0 spawned pid=8144 I, [2015-02-14T14:42:19.415228 #8117] INFO -- : worker=2 spawning... I, [2015-02-14T14:42:19.416395 #8117] INFO -- : master process ready I, [2015-02-14T14:42:19.416847 #8144] INFO -- : worker=0 ready I, [2015-02-14T14:42:19.417557 #8149] INFO -- : worker=2 spawned pid=8149 I, [2015-02-14T14:42:19.418379 #8147] INFO -- : worker=1 spawned pid=8147 I, [2015-02-14T14:42:19.421307 #8149] INFO -- : worker=2 ready I, [2015-02-14T14:42:19.421429 #8147] INFO -- : worker=1 ready
preload_app=false だと、forkされたプロセスの数だけ、最初にロードが走ります。
I, [2015-02-14T14:45:24.395364 #8154] INFO -- : listening on addr=0.0.0.0:5000 fd=9 I, [2015-02-14T14:45:24.395553 #8154] INFO -- : worker=0 spawning... I, [2015-02-14T14:45:24.397392 #8154] INFO -- : worker=1 spawning... I, [2015-02-14T14:45:24.398156 #8181] INFO -- : worker=0 spawned pid=8181 I, [2015-02-14T14:45:24.398442 #8181] INFO -- : Refreshing Gem list I, [2015-02-14T14:45:24.398412 #8154] INFO -- : worker=2 spawning... I, [2015-02-14T14:45:24.399575 #8184] INFO -- : worker=1 spawned pid=8184 I, [2015-02-14T14:45:24.399857 #8184] INFO -- : Refreshing Gem list I, [2015-02-14T14:45:24.401218 #8154] INFO -- : master process ready I, [2015-02-14T14:45:24.401716 #8187] INFO -- : worker=2 spawned pid=8187 I, [2015-02-14T14:45:24.402049 #8187] INFO -- : Refreshing Gem list start loading. start loading. start loading. end loading. end loading. end loading. I, [2015-02-14T14:45:29.529195 #8184] INFO -- : worker=1 ready I, [2015-02-14T14:45:29.533661 #8187] INFO -- : worker=2 ready I, [2015-02-14T14:45:29.541946 #8181] INFO -- : worker=0 ready
resqueで非同期分散処理を試す
resqueは非同期処理の仕組みを提供するライブラリです。
- Webアプリで、画像変換とかメールの一括送信のような重たい処理を行うときに使ったりします。
- Railsに組み込んで使うこともできる模様。
- 複数ノードでの分散処理にも対応。
- 管理コンソールがついているので、処理ノード(worker)の状態や、積まれた処理の数を簡単に確認できるもポイント。
サンプルのタスクを作って、複数ノードで分散処理するところまで試してみました。
構成
docker上に構築します。用意したコンテナは以下。
- redis
- resqueではバックエンドにredisを使います。
- resque-manager
- resqueの管理コンソール(webアプリ)を動かすコンテナ
- resque-console
- resqueで行わせる処理(job)を置くコンテナです。
- resqueへの処理の登録もこのコンテナから行います。
- resque-worker x2
- 非同期処理を実行するコンテナです。2つ用意します。
図にするとこんな感じ。
- すべてのコンテナからredisにアクセスできるようにします。
- resque-consoleにjobを置き、さらにData Volume Containerにして、resque-workerからもjobを参照できるようにします。
- jobは、タスクの登録時および実行時の両方で必要です。
- workerの実行時に必要なRakefileも、resque-consoleに置いて共有します。
1. redis コンテナを作る
まず、redisコンテナを作ります。
$ mkdir -p ~/work/redis $ vi ~/work/redis/Dockerfile
Dockerfileの中身は以下。
FROM centos:centos7 RUN yum -y update; yum clean all RUN yum -y install epel-release; yum -y install redis; yum clean all EXPOSE 6379 CMD ["redis-server"]
- centos7をベースに使います。
- redisをインストールして、起動します。
Dockerfileを保存したら、Dockere Imageをビルドして実行。
$ sudo docker build --rm -t='redis' redis $ sudo docker run -d --name=redis redis
- -d オプションを指定して、バックエンドで実行します。
- redisへのアクセスはコンテナ内からのみできればOKなので、ホスト側のポートとのマッピングはしていません。
2. resque-manager コンテナを作る
次にresque-manager コンテナを作ります。
$ mkdir -p ~/work/resque-manager $ vi ~/work/resque-manager/Dockerfile
Dockerfileは以下。
FROM centos:centos7 RUN yum -y update; yum clean all RUN yum -y install ruby; yum clean all RUN gem install resque EXPOSE 3000 CMD resque-web -p 3000 -r redis -F
- rubyとresqueのgemをインストールします。
- コンテナ起動時にresqueの管理コンソールを起動するように指定します。
- 「-p 3000」で使用するポートを3000に変更。
- 「-r redis」でredisノードを指定します。
- コンテナ起動時に、「--link redis:redis」を指定することで、resque-managerコンテナの /etc/hosts にredisコンテナのアドレスが追加され接続できるようになる仕組み。(詳しくはこちら)なお、上記の例ではportは省略しています。
- 「-F」でフォアグラウンドで実行するようにします。
ビルドして実行。
$ sudo docker build --rm -t='resque-manager' resque-manager $ sudo docker run -d -p 3000:3000 --link redis:redis --name=resque-manager resque-manager
- 「-p 3000:3000」で、ホストのポート3000へのアクセスをゲストのポート3000にマッピングするようにしています。
起動したら、ブラウザから http://127.0.0.1:3000 にアクセス。resqueの管理コンソールが表示されるはず。
3. resque-console コンテナを作る
管理コンソールの起動が確認できたら、resque-console コンテナを作ります。
$ mkdir -p ~/work/resque-console $ vi ~/work/resque-console/Dockerfile
Dockerfile:
FROM centos:centos7 RUN yum -y update; yum clean all RUN yum -y install ruby; yum clean all RUN gem install resque VOLUME ["/resque-data"] CMD ["/bin/bash"]
- jobの登録時に必要になので、rubyとresqueをインストールします。
- 「VOLUME ["/resque-data"]」で data volume を追加。
- ここにjobの定義ファイルやwoker起動用Rakefileを置いて、他のコンテナと共有します。
ビルドして実行。
$ sudo docker build --rm -t='resque-console' resque-console $ sudo docker run -ti --link redis:redis --name=resque-console resque-console
起動後のコンソールから、jobファイルを作成します。
# cd /resque-data # vi job.rb
job.rb:
class Job @queue = :default def self.perform(time) puts "start #{time}" sleep time puts "end #{time}" end end
- 非同期で行う処理の定義です。
- perform メソッドが処理の本体になります。
- @queue でキューの名前を指定します。
jobが用意できたので、resqueに登録してみます。
add_jobs.rb:
require 'resque' require_relative 'job' Resque.redis="redis" # 接続先とするredisを指定 10.times {|n| Resque.enqueue( Job, n ) }
実行すると、jobが登録されます。
# ruby add_jobs.rb
workerはまだないので、実行はされません。
4. workerの起動時に使用するRakefileを作る
resque-workerコンテナをつくる前にworker起動用のRakefileを用意しておきます。
- workerはRake Taskの形で提供されているので、Rakefileを作る必要があります。
- といっても、job.rbとresque/tasksをrequireするだけですけど。
- あと、接続先とするredisもこの中で指定します。
- Rakefileはresque-consoleコンテナの /resque-data に置き、resque-worker x2 と共有します。
- アクセスできるようにするための設定は後述。
require_relative 'job' require 'resque/tasks' Resque.redis="redis"
5. resque-worker コンテナを作る
最後にworkerコンテナを作ります。
$ mkdir -p ~/work/resque-worker $ vi ~/work/resque-worker/Dockerfile
Dockerfile:
FROM centos:centos7 RUN yum -y update; yum clean all RUN yum -y install ruby; yum clean all RUN yum -y install rake; yum clean all RUN gem install resque CMD TERM_CHILD=2 QUEUE=default rake -f /resque-data/Rakefile resque:work
- ruby,resque と rake も必要なので追加でインストールします。
- CMDで、起動時にresqueのworkerを起動します。
ビルドして実行。別の名前で2つ実行します。
$ sudo docker build --rm -t='resque-worker' resque-worker $ sudo docker run -d --link redis:redis --volumes-from resque-console --name="resque-worker1" resque-worker $ sudo docker run -d --link redis:redis --volumes-from resque-console --name="resque-worker2" resque-worker
- 「--volumes-from resque-console」でresque-consoleに追加した「/resque-data」にアクセスできるようにしています。
管理コンソールをリロードすると、workerが2つ起動しているはず。
jobの実行も始まります。
Rakeの基本的な使い方まとめ
Rakeの基本的な使い方のまとめです。
- インストールから、Rakefileの書き方、組み込みライブラリの使い方まで。
- 忘れたときに見返す用に。
Rakeって何?
rakeツールのインストールと使い方
rakeコマンド
よく使いそうなものをいくつか。
$ rake <タスク名> # 指定のタスクを実行。 $ rake <パラメータキー>=<値> <タスク名> # パラメータ指定ありで、指定のタスクを実行。 $ rake -h # 使い方を表示。 $ rake -T # 定義されているタスク一覧を表示
Rakefileの書き方
Task
Rakeで行う処理は「タスク」としてRakefileで定義されます。タスクは以下の要素から成ります。
- 名前
- タスクを識別するための名前。rakeコマンドで実行するタスクを指定する場合などに使用します。
- アクション
- タスクで行われる一連の処理です。省略可です。
- 事前タスク
- 前提として実行するタスクの一覧です。指定された事前タスクは、タスクの実行前に実行されていなければ実行されます。
- パラメータ
- タスクに渡されるパラメータ名です。rakeコマンドの引数で
タスクは「task」メソッドで定義できます。
## 書き方 # task <タスク名> , [<パラメータ名>, <パラメータ名> ... ] => [<前提タスク名>,<前提タスク名> ... ] do # # アクション # end # "hello" を表示するだけのタスク task :hello do puts 'hello' end # "hello"の後に"world"を出力するタスク task :hellow_world=>[:hello] do puts 'world' end # アクションなしのタスク task :all=>[:hellow_world, :echo] #パラメータを受け付けるタスク task :echo, [:message] => [:hello] do |t, args| # ブロックへのパラメータとしてタスクオブジェクトとパラメータが渡される。 puts args.message end
タスクに説明を付ける
「desc」で、次に作成するタスクの説明を設定できます。
desc "タスクの説明です。" task :hoge
説明を付与しておくと、「-T」オプションの一覧で表示されます。
$ rake -T (in xx/Rakefile) rake hoge # タスクの説明です。
指定のファイル/ディレクトリがあれば実行しないタスクを作る
「file」メソッドでタスクを定義すると、タスク名が示すファイルが存在しない場合のみ実行するタスク(File Task)を作成できます。「.classを作ったら消されるまで再コンパイルしない」といった場合に使えます。
# foo.txtがない場合のみ実行されるタスク file "foo.txt" do puts "create foo.txt" open("./foo.txt", "w"){|f| f << "test." } end
同様に、ディレクトリがない場合のみ作成するタスクも作成できます。こちらはアクションは指定できません。
# var/hoge ディレクトリがない場合のみ、ディレクトリを作るタスク。 # アクションは指定できない。 directory "var/hoge"
名前に対応するタスクが存在しない場合のタスクを指定する
「rule」メソッドで、名前に対応するタスクが存在しない場合に実行されるタスクを定義できます。
# *.txtが存在しない場合、作成するタスク rule /.*\.txt/ do |t| puts "create #{t.name}" open("#{t.name}", "w"){|f| f << "test." } end
rubyの「method_missing」的な機能ですかね。ただ、ruleで定義したタスクはFile Task扱い(=タスク名が示すファイルが存在しない場合のみ実行される)のでご注意。
名前空間
「namespace」で、任意の名前空間の中にタスクを定義できます。
# ネームスペースの中にタスクを定義する。 namespace :test do task :hello do puts 'hello' end task :hello_world=>[:hello] do puts 'world' end end
タスクをグループ化したいときに。実行時には「<ネームスペース>:<タスク名>」の形式で指定します。
$ rake test:hello_world (in xx/Rakefile) hello world
組み込みタスクを使う
よく使うタスクがいくつか組み込みで用意されていて、さくっと使えるようになっています。
成果物を削除する
clean/clobberタスクは
require 'rake/clean'
で定義されるタスクです。
- clean
- Remove any temporary products. (ビルドの一時作成物を削除する?)
- デフォルトでは["**/*~", "**/*.bak", "**/core"]を削除するので、他に消したいものがあれば、定数「CLEAN」に追加します。
require 'rake/clean' CLEAN << "*.txt"
- clobber
- Remove any generated file.(ビルドの作成物を削除する?)
- cleanで消されるファイル+定数「CLOBBER」のファイルを削除します。
- cleanとの違いはいまいちわからん。
rdocを作る
RDocTaskでrdocを生成するタスクをさくっと定義できます。
# 「lib/**/*.rb」のrdocを生成するタスク require 'rake/rdoctask' Rake::RDocTask.new do |rd| # RDocTask 内で rdoc,rdoc_clobber,rerdocが定義される。 rd.rdoc_dir = 'rdocs' rd.rdoc_files = FileList["lib/**/*.rb"] rd.options << '-charset=UTF-8 ' end
これで、
$ rake -T (in xx/Rakefile) rake clobber_rdoc # Remove rdoc products rake rdoc # Build the rdoc HTML Files rake rerdoc # Force a rebuild of the RDOC files
が定義され、実行できるようになります。
$ rake rdoc (in xx/Rakefile) rm -r rdocs test.rb: c.... Generating HTML... Files: 1 Classes: 1 Modules: 0 Methods: 4 Elapsed: 0.717s
Gemを作る、UnitTestを実行する .. etc ..
その他、未確認ですが、gemを作ったり、UnitTestを実行したりするタスクもrdocのようなユーティリティでさくっと定義できるみたいです。詳細はrdocで。
補足: 参考にしたサイトなど
- rake.rubyforge.org
- rake のAPIなど。Rakefileの書式等についても解説があります。
- docs.rubyrake.org
- チュートリアルやサンプル。
一定期間経過するとログアウト状態になる問題に対応したsbiclientをプレリリース
tana さんよりご報告頂いた、「起動後一定期間経過するとレート情報が取得できなくなってしまう」問題に対応したsbiclientをプレリリースします。
→Github - unageanu / sbiclient
→Gemcutter.org - sbiclient
原因
ログイン後一定期間経過すると、強制的にログアウト状態にされることが原因のようです。明確な時間は不明ですが、こちらで試した限りでは1時間程度でログアウト状態になりました。定期的にレート情報を取得しているにもかかわらず、です。
対応
ログアウトによりレート情報が取得できない場合、再ログインする処理を追加して対応します。
ステータス
動作確認中ですが、試してみたい方は以下の手順で更新できます。
$ gem update sbiclient
jijiのrestartもお忘れなく。不具合等あればご報告いただければ幸いです。
Javaメソッドの呼び出し元をツリー表示するスクリプト
JavaClassを使って指定メソッドの呼び出し元を探索しツリー表示するスクリプトを書いてみました。
- 引数で指定されたパス以下の*.class,および*.jarに含まれるクラスファイルを解析し、
- メソッドの呼び出しコードを収集。
- 収集した情報を再帰的に探索し、呼び出し元メソッドをツリー形式で出力します。
使い方
$ ./caller.rb <クラスが置かれたディレクトリorJarファイル> <呼び出し元を探すメソッド>
java.Util.ArrayList#indexOf(Ljava/lang/Object;)I
- クラスが置かれたディレクトリ or Jarファイルは、「;」区切りで複数指定可能です。
具体例
↓のようなクラスがあったとして、
package com.example.caller; public class CallSample { static class A { void aaa() { new B().bbb(); } void aaa2() { new C().ccc(); } void aaa3() { aaa2(); } } static class B { void bbb() { new C().ccc(); } } static class C { void ccc() {} } }
「C#ccc()」の呼び出し元を探索すると、
$ ./caller.rb "../lib;../classes" "com.example.caller.CallSample\$C#ccc()V"
以下が出力されます。
com.example.caller.CallSample$C#ccc()V ├com.example.caller.CallSample$A#aaa2()V │└com.example.caller.CallSample$A#aaa3()V └com.example.caller.CallSample$B#bbb()V └com.example.caller.CallSample$A#aaa()V
実装
次のとおりです。
#!/usr/bin/ruby require "rubygems" require "zip/zip" require "javaclass" require "kconv" # クラスプール class ClassPool def initialize @classes = {} @classes_r = {} end def <<( jc ) jc.methods.each {|m| key = jc.name + "#" + m.name + m.descriptor @classes[key] ||= [] next unless m.attributes.key? "Code" m.attributes["Code"].codes.each {|code| case code.opcode when 0xB6,0xB7,0xB8,0xB9 m = jc.get_constant( code.operands[0].value ) add_caller( key, m.class_name.name + "#" + m.name_and_type.name + m.name_and_type.descriptor ) when 0xB2,0xB3,0xB4,0xB5 m = jc.get_constant( code.operands[0].value ) add_caller( key, m.class_name.name + "." + m.name_and_type.name ) end } } end attr_reader :classes attr_reader :classes_r private def add_caller( from, to ) @classes[from] ||= [] @classes[from] << to @classes_r[to] ||= [] @classes_r[to] << from end end #ノード class Node def initialize( name ) @name = name @children = [] end def <<(child) @children << child end def to_s( indent="" ) str = indent.dup str << name.to_s << "\n" child_indent = indent.gsub(/├/, "│").gsub(/└/, " ") @children.each_index {|i| next if children[i] == nil tmp = child_indent + ( i >= children.length-1 ? "└" : "├" ) str << children[i].to_s( tmp ) } return str end attr :name, true attr :children, true end #呼び出し元を収集する。 def collect( pool, start, stop=[], node=nil, checked=[] ) node ||= Node.new(start.to_s) return node unless pool.classes_r.key? start pool.classes_r[start].each {|caller| next if checked.find{|i| i==caller } next if stop && stop.find{|i| caller =~ i } checked.push start.to_s node << collect( pool, caller, stop, Node.new( caller ), checked ) checked.pop } return node end # クラス情報を収集 pool = ClassPool.new JavaClass::Utils.each_class( *ARGV[0].split(";") ){|jc| pool << jc } # 呼び出し元をツリー表示 puts collect(pool, ARGV[1]).to_s
なお、「JavaClass」のGemはGemcutterにアップロードしたので、そちらから取得できます。同名のGemがすでにあったので「unageanu-javaclass」として登録しています。
$ gem install unageanu-javaclass
「require」するときは「require 'javaclass'」です。
require 'javaclass'
JSON-RPCで転送するJavaオブジェクトをさくっと作成するユーティリティ の続き
昨日作成したJSON-RPCで転送するJavaオブジェクトをさくっと作成するユーティリティですが、Ruby→JSONへのシリアライズだけじゃなく、JSON→Rubyへのデシリアライズもサポートしてればさらに便利じゃね?と思い立って実装してみた。API呼び出しの結果として返されたハッシュが定義済みのJavaクラスであればそのインスタンスに変換して返します。
require 'rubygems' require 'uri' require 'httpclient' require 'json/lexer' require 'json/objects' module JsonRpc # クライアント class Client def initialize( name, host="http://localhost:8080", proxy=ENV["http_proxy"] ) @client = HTTPClient.new( proxy, "JsonClientLib") @client.set_cookie_store("cookie.dat") @name = name @host = host @id = 0 end def method_missing( name, *args ) body = "{ 'id':#{@id+=1},method:#{(@name+"."+name.to_s).to_json},params:#{args.to_json}}" result = @client.post(@host, body ) json = JSON::Lexer.new(result.content).nextvalue if json["error"] raise json["error"]["msg"] else JsonRpc.to_java_class(json["result"]) end end end #JSON-RPCで送付するJavaクラスを定義する。 #java_class:: 対応するJavaクラスのFQCN #attrs:: Javaクラスの属性一覧 #return:: Javaクラス def self.define( java_class, *attrs ) cl = Class.new(JsonRpc::JavaClass) {|klass| @java_class = java_class @attrs = attrs def self.java_class ; @java_class; end def self.attrs; @attrs; end def initialize( *args ) attrs = self.class.attrs args.each_index {|i| break if i >= attrs.length instance_variable_set("@" + attrs[i].to_s, args[i]) } end attrs.each {|a| attr a, true} } @@registory[java_class] = cl return cl end # 定義したJavaClass @@registory = {} # ハッシュをJavaClassにデシリアライズする。 def self.to_java_class( obj ) if obj.kind_of?( Hash ) hash = obj.inject({}){|r,i| r[i[0]] = to_java_class(i[1]); r } if ( obj["javaClass"] && @@registory.key?(obj["javaClass"]) ) java_class = @@registory[obj["javaClass"]] args = java_class.attrs.map {|a| hash[to_jar_attr_name( a )] } return java_class.new(*args) else return hash end elsif obj.kind_of?( Array ) return obj.map {|i| to_java_class( i ) } else return obj end end def self.to_jar_attr_name( name ) tmp = name.to_s.split(/_/) return ([tmp[0]] + tmp[1..-1].map{|i| i.capitalize}).join end # Javaクラスの基底クラス class JavaClass def to_hash h = {"javaClass"=>self.class.java_class} attrs = self.class.attrs attrs.each {|attr| java_attr_name = JsonRpc.to_jar_attr_name( attr ) h[java_attr_name] = instance_variable_get("@" + attr.to_s) } return h end def to_json to_hash.to_json end end end
利用例は以下。
# Kittenオブジェクトを定義 Kitten = JsonRpc::define( "test.Kitten", :id, :name, :age ) # クライアント作成 client = JsonRpc::Client.new( "dao", "http://localhost:8888/json" ) # API呼び出し。 p client.put( Kitten.new( nil, "mii", 1 ))
実行結果です。
#<Kitten:0x7fd308f4 @name="mii", @age=1, @id={"kind"=>"Kitten", "javaClass"=>"com.google.appengine.api.datastore.Key", "id"=>4}>