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'
これで一区切りとします。
「1日1エントリ」のルールで続けてきたこのブログですが、今日の記事で連続投稿1000日目となります。
1000日続けてみた感想ですが、
- 技術的知識は(当たり前ですが)だいぶ増えましたよ。また、このブログ自体も個人的なデータベースとして日々役立っていますよ。
- 新しい技術情報を意識して探すようになった。というか、「毎日更新」なので日々新しい何かを探してないとネタが尽きる・・・。
- 「"1000日毎日コード書く"と決めて実績できる人」らしいということが解った。
といったところは良かったかなと思っています。
さて、今後ですが「1日1エントリ」での更新は一旦終了としたいと思います。諸般の事情で毎日更新するのが難しくなってきたというのもありますが、以下の思惑もあります。
- 記事のクオリティを多少なりとも何とかしたい
- 時間がなくてほとんど推敲しないまま投稿してしまったり、
- 本来はまとめて1エントリとしたほうが解りやすいような記事も、つい分割して小出しにしてしまったりする。
- ルールを廃して、忙しい時には無理をしない代わりに、クオリティを多少なりとも向上していきたい。
- 地味な作業が進まない
- やってもネタに繋がらなそうな地味な作業(clickclient_scrapのクリック証券デモ取引対応のテストとか)が、ついつい後回しに・・・。申し訳ない・・。
- ネタがないときに、だらだらとネットを彷徨っている時間がもったいない。
ブログ自体は継続しますので、今後ともよろしくお願いします。