スポンサーリンク

Javaプロジェクトで,クラスの依存関係をグラフ化するツール (Graphvizでソースコードのパッケージをサブグラフ化する)

クラスの依存関係のグラフを,Javaのソースコードから自動生成するようなWSHバッチを試作した。



四角で囲ったのがパッケージ。

点線の丸がインタフェース。

import, extend, implementsなどをいっしょくたにして,とにかく「使っているか」「呼び出しているか」だけに注目しているので,Judeで出力できるようなUMLのクラス図とはちょっと種類が異なる。

JudeやDoxygenでできること

Judeには,パッケージのフォルダ階層を再帰的に辿ってクラス図を生成する機能はない。


Judeでパッケージ内のみでのクラス図生成をしたい場合は,ソースコードをインポートしたのち,プロジェクトビューで右クリック→クラス図を自動生成 を選ぶ。

judeでクラス図の自動生成
http://d.hatena.ne.jp/suusuke/2008060...


UMLツールレビュー オープンソース、無償モデリングツールの実力派
http://www.atmarkit.co.jp/im/carc/ser...

ソースコードを読み込ませてみよう。

・・・対象となるJavaソースコードも.java(複数可)でしか指定できず、フォルダで再帰的に指定できないのもつらい。

かかった時間は25秒ほど。パッケージごとに「クラス図の自動生成」をしてみる。それなりに問題ないクラスが生成される。


パッケージ内でコラボレーション図を出したいなら,Doxygenを使うのもいい。
Javadoc風のドキュメントの一部としてクラス関係図を得られる。

KNOPPIXにプログラムをインストールする(その2) doxygenとgraphviz
http://www.h2.dion.ne.jp/~miyawaki/kn...

ここで作ったバッチ

パッケージ階層を再帰的に辿る事ができれば,プロジェクト全体像を一度に描写できて便利な場合がある。


以下はそのバッチのソースコード。(300行ほど)

graph.js (こちらからダウンロードできます。)

/*
	javaのクラス依存関係をグラフ化するスクリプト
	
	usage: 
		・Graphvizをインストールし,binにPATHを通しておく
		・コマンドプロンプトから
			wscript graph.js Javaプロジェクトのルートフォルダパス
*/


var file_basename = "class_relation";
var ws = WScript.CreateObject("Wscript.Shell");
var fillcolor = "#FFFFFF";

// 引数からプロジェクトルートをget
var args = WScript.Arguments;
if( args.length < 1 )
{
	WScript.Echo("引数がありません。");
	WScript.Quit();
}
var project_root = args.Unnamed(0);
	// http://homepage3.nifty.com/aya_js/wsh/wsh302.htm


// クラス名の辞書(パッケージ名.* のような物も含む)
var CDict = function(){ this.arr = new Array(); };
CDict.prototype = {
	arr : null,
	// 特定の番号のクラスを取得
	get : function ( index ){
		return this.arr[ index ]
	},
	// 全クラス数
	getlen : function(){
		return this.arr.length;
	},
	// クラスを追加
	update : function ( key ){
		for( var i = 0, len = this.getlen(); i < len; i ++ )
		{
			if( this.get(i) == key ) return;
		}
		this.arr.push( key );
	},
	// このキーを持っているか
	haskey : function( key ){
		for( var i = 0, len = this.getlen(); i < len; i ++ )
		{
			if( this.get(i) == key ) return true;
		}
		return false;
	}
};
var dic = new CDict(); // クラス完全名の辞書(インタフェースを含む)
var intdic = new CDict(); // インタフェース名の辞書
var pacdic = new CDict(); // 枠で囲うべきパッケージ名の辞書


// import 文を抜き出して配列に格納
/*
 subof[インデックス番号] = {
	from : 読み込む完全クラス名,
	to : 読み込まれる完全クラス名
	};
*/
var subof = new Array();
ws.CurrentDirectory = project_root;
var proc = ws.Exec("findstr /s \"^import\" *.java");
var res = proc.StdOut.ReadAll().split("\r\n");
for( var i = 0 ; i < res.length; i ++ )
{
	if( res[i].length < 1 )
	{
		break;
	}
	
	// ファイル名からクラス完全名を作成
	var class_from = res[i].split(":")[0].replace( ".java", "" ).replace( /\\/g, "." );
	
	// import文からクラス完全名を作成
	var class_to = res[i].split(" ")[1].replace( ";", "" );
	
	// 関係として記録
	var obj = {};
	obj.from = class_from;
	obj.to = class_to;
	subof.push( obj );
	
	// クラスとして記録
	dic.update( class_from );
	dic.update( class_to );
}


// インタフェースを列挙
var proc_i = ws.Exec("findstr /s /r /c:\"^public interface .*\" *.java");
var res_i = proc_i.StdOut.ReadAll().split("\r\n");
for( var i = 0 ; i < res_i.length; i ++ )
{
	if( res_i[i].length < 1 )
	{
		break;
	}

	// ファイル名からインタフェース完全名を作成
	var iname = res_i[i].split(":")[0].replace( ".java", "" ).replace( /\\/g, "." );
	
	// インタフェースとして記録
	intdic.update( iname );
}
var proc_i = ws.Exec("findstr /s /r /c:\"class .* implements \" *.java");
var res_i = proc_i.StdOut.ReadAll().split("\r\n");
for( var i = 0 ; i < res_i.length; i ++ )
{
	if( res_i[i].length < 1 )
	{
		break;
	}

	// 実装しているクラス名を抽出
	var cname = res_i[i].split(":")[0].replace( ".java", "" ).replace( /\\/g, "." );

	// インタフェース完全名
	var iname = res_i[i].split("implements ")[1].split( /( |{)/ )[0];
	var pname_arr = cname.split(".");
	pname_arr.pop();
	iname = pname_arr.join(".") + "." + iname;
	
	// インタフェースとして記録
	intdic.update( iname );
	
	// 関係を記録
	if( ! hasarrow( cname, iname ) )
	{
		var obj = {};
		obj.from = cname;
		obj.to = iname;
		subof.push( obj );
	}
}


// 完全クラス名からパッケージ分類を作成 
//    パッケージの階層は区別して別扱いする
//    もし*の下に,ファイル名が明示的に判明しているクラスが無ければ,それは無視
// 枠で囲うべきパッケージ名に所属しているクラスを一つ保管しておく
var elem_of_pac = {};
for( var i = 0, len = dic.getlen(); i < len; i ++ )
{
	var cname = dic.get(i);
	// もしクラス完全名が.*で終わらないなら,その直上のパッケージは枠で囲うべきなので保管
	if( ! cname.match( /\.\*$/ ) )
	{
		var pname = cname.split(".");
		pname.pop(); // popは元の配列を破壊し,末尾の削除された要素を返す
		pname = pname.join(".");
		pacdic.update( pname );

		// パッケージに属するクラスを一つだけ保管
		elem_of_pac[ pname ] = cname;
	}
}


// ファイル書き出し
var file_to = file_basename + ".dot";
var file_png = file_basename + ".png";
var ForWriting = 2; // 書き込み(上書き)
var fso_w = WScript.CreateObject( "Scripting.FileSystemObject" );
if( fso_w.FileExists( file_to ) )
{
	fso_w.DeleteFile( file_to );
}
fso_w.CreateTextFile( file_to );
var txt_w = fso_w.OpenTextFile( file_to,   ForWriting );


// グラフ宣言を書く
txt_w.WriteLine("digraph \"class_relation\" {");
txt_w.WriteLine(" graph [compound = true, ranksep = 1.0, nodesep = 0.75];");
	// http://homepage3.nifty.com/kaku-chan/graphviz/chapter_008.html#3
	// http://homepage3.nifty.com/kaku-chan/graphviz/chapter_006.html#2
txt_w.WriteLine(" node [style = filled, fillcolor = \"" + fillcolor + "\"];");
	// http://homepage3.nifty.com/kaku-chan/graphviz/chapter_004.html#3
txt_w.WriteLine(" edge [color = \"#777777\"];");
	// http://homepage3.nifty.com/kaku-chan/graphviz/chapter_005.html
// サブグラフとして,枠で囲うべきパッケージを指定
for( var i = 0, len = pacdic.getlen(); i < len; i ++ )
{
	var pname = pacdic.get(i);
	txt_w.WriteLine(
		" subgraph cluster" 
		+ pname.replace( /\./g, "_" ) 
		+ " {\r\n"
		+ " label = \""
		+ pname
		+ "\";\r\n"
		+ " bgcolor = \"#eeeeff\";"
	);

	// このパッケージ名の直下に属するクラスをサブグラフの要素として全列挙
	for( j = 0, clen = dic.getlen(); j < clen; j ++ )
	{
		var cname = dic.get(j);
		var arr_cname = cname.split(".");
		var cname_last_elem = arr_cname.pop();
		var pname_of_class = arr_cname.join(".");
		if(
			( pname == pname_of_class )
			&&
			( cname_last_elem != "*" )
		)
		{
			// クラスを書く(ラベル指定)
			txt_w.WriteLine(
				" \""
				+ cname
				+ "\" [label=\""
				+ f2c( cname )
				+ "\""
				// インタフェースなら点線
				+ (
					intdic.haskey( cname ) 
						? ", style=dashed, fillcolor=\"" + fillcolor + "\""
						: ""
				)
				+ "];"
			);
			// 点線 http://homepage3.nifty.com/kaku-chan/graphviz/chapter_004.html#3
		}
	}
	
	txt_w.WriteLine(" }");
}


// 矢印を書く

// クラスからクラスへ(宛先が.*で終わらないもの)
for( var i = 0; i < subof.length; i ++ )
{
	if( ! subof[i].to.match( /\.\*$/g ) )
	{
		txt_w.WriteLine( 
			" \""
			+ subof[i].from
			+ "\" -> \"" 
			+ subof[i].to
			+ "\";"
		);
	}
}

// クラスからパッケージへ(宛先が.*で終わるもの)
for( var i = 0; i < subof.length; i ++ )
{
	var pname = subof[i].to.replace(".*", "");
	if( subof[i].to.match( /\.\*$/g ) && pacdic.haskey( pname ) )
	{
		txt_w.WriteLine( 
			" \""
			+ subof[i].from
			+ "\" -> \"" 
			+ elem_of_pac[ pname ]
			+ "\" [lhead=cluster"
			+ pname.replace( /\./g, "_" )
			+ "];"
		);
	}
}


txt_w.WriteLine("}");
txt_w.Close();

// グラフ化実行
ws.Run( "dot -Tpng -o " + file_png + " " + file_to );
ws.Run( file_png );


// 完全名からクラス名のみ抜き出す
function f2c( str )
{
	if( str.match(/\.\*/g) )
	{
		return str;
	}
	var arr = str.split(".");
	return arr[arr.length - 1];
}


// 矢印が既に伸びているか
function hasarrow( cfrom, cto )
{
	for( var i = 0; i < subof.length; i ++ )
	{
		if( ( subof[i].from == cfrom ) && ( subof[i].to == cto ) )
		{
			return true;
		}
	}
	return false;
}

そして,このJScriptファイルをコマンドプロンプトから呼び出して実行するbatファイル。

graph.bat

wscript graph.js ここにパッケージのルートフォルダ(jpの上のフォルダ)を書く


両者を同じディレクトリ上に置いて,BATをダブルクリック。

そうすると,冒頭のような図が出力され,表示される。


なお,事前にGraphviz(グラフ描画ツール)をダウンロードし,binにPATHを通しておく。

ダウンロード
http://www.graphviz.org/

概説

アルゴリズムは単純で,import文やinterfaceの宣言を探しているだけ。構文解析まではしていない。

同一パッケージ内に存在するクラスは,本来ならimport文を記述する必要なく素で呼び出せる。
このグラフ上で関係が記述されるためには,わざわざimportしなければならない。
小さなクラスが小さなパッケージ群に散在している状況が理想。



パッケージ名ごとにサブグラフとして枠で囲う部分は,ちょっと工夫している。

Graphvizでサブグラフを描く場合,subgraphのブロック中には「要素」だけを書けばよくて,要素間の「関係」はsubgraphの外に書いてもよい。

例えば:

digraph g {
	graph [compound = true];
	subgraph cluster1{
		label = "c1";
		"a";
		"b";
		"c";
	}
	subgraph cluster2{
		label = "c2";
		"d";
		"e";
		"f";
	}
	"a"->"b";
	"a"->"c";
	"d"->"b";
	"c"->"f" [lhead=cluster2];
}

subgraphの中には要素の宣言だけを書いた。

これを s.dot で保存して,コマンドラインから

dot -Tpng -o s.png s.dot

と打ちこむ。すると



こんなグラフ画像が生成される。


参考:

サブグラフに向かって矢印を引く方法:サブグラフを端点とするエッジ
http://homepage3.nifty.com/kaku-chan/...

なお,サブグラフ名をclusterという名前で始めると,自動的に枠が付く。


補足

形式的方法論の奴隷になってはいけない:

丸と矢印は貧弱な師匠です。…

広げると千畳にもなりそうなクラス図…を持って打ち合わせに出向いたとしても,それらは…,単なる紙なのです。


アンドリュー・ハント著,「達人プログラマー」,7章