Javaのバイトコードを解析した結果をクラス図やコールグラフで表現する
目的
Javaのバイトコードからクラスやメソッドの情報を抜き出してSqliteに記録します。
上記の記録した情報を用いてクラス図やコールグラフを記載します。
なお、doxygen + graphviz使える人は、それを使ったほうがいいです。
使用環境
・Windows10
・Java8
・bcel-6.3.1.
・sqlite-jdbc-3.27.2.1
・plantuml.jar
使用ライブラリの解説
bcel
BCEL API(バイトコードエンジニアリングライブラリ)は、静的分析および動的なJavaクラスファイルの作成のツールキットです。
FindBugはBCELを使用してクラスファイルから静的解析を行っています。
https://commons.apache.org/proper/commons-bcel/manual/introduction.html
サンプルコード
JavaSE BCEL
https://hondou.homedns.org/pukiwiki/index.php?JavaSE%20BCEL
またバイトコードのバイナリをどう解釈するかは以下のcodeToStringが参考になります。
https://github.com/llmhyy/commons-bcel/blob/master/src/main/java/org/apache/bcel/classfile/Utility.java
Sqlite-JDBC
SQLiteを操作するJDBCで下記のページからダウンロードできます。
ダウンロード
https://bitbucket.org/xerial/sqlite-jdbc/downloads/
サンプルコード
package sqlitesample;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.sqlite.Function;
public class SqliteSample {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("start-----");
// load the sqlite-JDBC driver using the current class loader
Class.forName("org.sqlite.JDBC");
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
PreparedStatement pstmt = null;
try
{
// create a database connection
// Connection connection = DriverManager.getConnection("jdbc:sqlite:C:/work/mydatabase.db");
// Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
connection = DriverManager.getConnection("jdbc:sqlite:sample.db");
connection.setAutoCommit(true);
statement = connection.createStatement();
statement.setQueryTimeout(30); // set timeout to 30 sec.
System.out.println("create table -----");
statement.executeUpdate("drop table if exists person");
statement.executeUpdate("create table person (id integer, name string)");
statement.executeUpdate("insert into person values(1, 'leo')");
statement.executeUpdate("insert into person values(2, 'yui')");
rs = statement.executeQuery("select * from person");
while(rs.next())
{
// read the result set
System.out.println("name = " + rs.getString("name"));
System.out.println("id = " + rs.getInt("id"));
}
rs.close();
rs = null;
System.out.println("update -----");
pstmt = connection.prepareStatement("update person set name = ? where id = ?");
pstmt.setString(1, "ole");
pstmt.setInt(2, 1);
pstmt.executeUpdate();
rs = statement.executeQuery("select * from person");
while(rs.next())
{
// read the result set
System.out.println("name = " + rs.getString("name"));
System.out.println("id = " + rs.getInt("id"));
}
rs.close();
rs = null;
System.out.println("Transactions -----");
connection.setAutoCommit(false);
statement.executeUpdate("insert into person values(3, 'zoo')");
rs = statement.executeQuery("select * from person");
while(rs.next())
{
// read the result set
System.out.println("name = " + rs.getString("name"));
System.out.println("id = " + rs.getInt("id"));
}
rs.close();
rs = null;
System.out.println("rollback -----");
connection.rollback();
rs = statement.executeQuery("select * from person");
while(rs.next())
{
// read the result set
System.out.println("name = " + rs.getString("name"));
System.out.println("id = " + rs.getInt("id"));
}
rs.close();
rs = null;
System.out.println("function -----");
Function.create(connection, "total", new Function() {
@Override
protected void xFunc() throws SQLException
{
int sum = 0;
for (int i = 0; i < args(); i++)
sum += value_int(i);
result(sum);
}
});
rs = statement.executeQuery("select total(1, 2, 3, 4, 5)");
while(rs.next())
{
// read the result set
System.out.println("total(1,2,3,4,5) = " + rs.getInt(1));
}
rs.close();
rs = null;
}
catch(SQLException e)
{
// if the error message is "out of memory",
// it probably means no database file is found
System.err.println(e.getMessage());
}
finally
{
try
{
if(rs != null)
{
rs.close();
}
if(pstmt != null)
{
pstmt.close();
}
if(statement != null)
{
statement.close();
}
if(connection != null)
{
connection.close();
}
}
catch(SQLException e)
{
// connection close failed.
System.err.println(e);
}
}
}
}
ユーザ定義関数とかも作成できます。
その他、SQLite固有の操作については下記のテストコードが参考になります。
https://github.com/xerial/sqlite-jdbc/tree/c7c5604bcc584460268abc9a64df2953fca788d3/src/test/java/org/sqlite
PlantUML
DSLといわれる言語でUMLを含めた以下の図を記載できます。
- シーケンス図
- ユースケース図
- クラス図
- アクティビティ図(古い文法はこちら)
- コンポーネント図
- 状態遷移図(ステートマシン図)
- オブジェクト図
- 配置図
- タイミング図
- ワイヤーフレーム
- アーキテクチャ図
- 仕様及び記述言語 (SDL)
- Ditaa
- ガントチャート
- マインドマップ
- WBS図(作業分解図)
- AsciiMath や JLaTeXMath による、数学的記法
公式でWebページから実際の記載を試せます。
RedmineのプラグインもあるのでWikiにテキストデータとして各種図を埋め込むことができます。
ダウンロード
http://plantuml.com/ja/download
サンプル
PlantUML Cheat Sheet
https://qiita.com/ogomr/items/0b5c4de7f38fd1482a48
plantuml.jarファイルを自分のJavaのプロジェクトに組み込むことで、自分のプログラムからPNGやSVGを出力することも可能です。
http://plantuml.com/ja/api
実験結果
https://github.com/mima3/BcelToSqlite
bcelを使用してSqliteを記録する
以下のコードではJarファイルを指定して、そこに格納されているclassファイルを解析してクラス、メソッド、フィールドの情報をSQLiteに記録しています。
BcelToSqlite.java
package bcelToSqlite;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import org.apache.bcel.Const;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.Type;
import org.apache.bcel.util.ByteSequence;
public class BcelToSqlite {
Connection connection = null;
PreparedStatement pstmt = null;
int nextClassId = 1;
int nextMethodId = 0x10000001;
int nextAnnotationId = 0x20000001;
/**
* 下記参考に実装
* https://hondou.homedns.org/pukiwiki/index.php?JavaSE%20BCEL
* @param args
* @throws Exception
*/
public static void main(String args[]) throws Exception
{
String srcPath;
srcPath = ".\\lib\\bcel-6.3.1.jar";
BcelToSqlite thisClass = new BcelToSqlite();
thisClass.startWalk(new File(srcPath));
System.out.println(srcPath + " -> output.sqlite");
}
private void executeSql(String sql) throws SQLException {
pstmt = connection.prepareStatement(sql);
pstmt.executeUpdate();
pstmt.close();
pstmt = null;
}
private void executeSql(String sql, Object args[]) throws SQLException {
pstmt = connection.prepareStatement(sql);
int ix = 1;
for (Object obj : args) {
try {
pstmt.setInt(ix, (int)Integer.parseInt(obj.toString()));
} catch (NumberFormatException ex) {
pstmt.setString(ix, obj.toString());
}
++ix;
}
pstmt.executeUpdate();
pstmt.close();
pstmt = null;
}
private void startWalk(File path) throws Exception {
try {
connection = DriverManager.getConnection("jdbc:sqlite:output.sqlite");
connection.setAutoCommit(true);
executeSql("drop table if exists class");
executeSql("create table class (id int primary key, name string , access_flags int, super_class_name string)");
executeSql("drop table if exists interface");
executeSql("create table interface (class_id int, interface_name string)");
executeSql("drop index if exists index_interface");
executeSql("create index index_interface on interface(class_id)");
executeSql("drop table if exists method");
executeSql("create table method (id int primary key, class_id int, name string, fullname string, access_flag int, return_type string, byte_code string)");
executeSql("drop table if exists method_parameter");
executeSql("create table method_parameter (method_id int, seq int, param_type string)");
executeSql("drop index if exists index_method_parameter");
executeSql("create index index_method_parameter on method_parameter(method_id)");
executeSql("drop table if exists method_depend");
executeSql("create table method_depend (method_id int, called_method string, opecode int)");
executeSql("drop index if exists index_method_depend");
executeSql("create index index_method_depend on method_depend(method_id)");
executeSql("drop table if exists field");
executeSql("create table field (id int primary key, class_id int, name string, fullname string, access_flag int, type string)");
executeSql("drop table if exists anotation");
executeSql("create table anotation (id int primary key, refid int, type string)");
executeSql("drop index if exists index_anotation");
executeSql("create index index_anotation on anotation(refid)");
connection.setAutoCommit(false);
dirWalk(path);
connection.commit();
}
finally
{
if(pstmt != null)
{
pstmt.close();
}
if(connection != null)
{
connection.close();
}
}
}
private void dirWalk(File path) throws Exception {
if (path.isDirectory()) {
for (File child : path.listFiles()) {
dirWalk(child);
}
} else if (path.getName().endsWith("jar") || path.getName().endsWith("zip")) {
jarWalk(path);
} else if (path.getName().endsWith("class")) {
JavaClass javaClass = new ClassParser(path.getAbsolutePath()).parse();
classWalk(javaClass);
}
}
private void jarWalk(File jarFile) throws Exception {
try (JarInputStream jarIn = new JarInputStream(new FileInputStream(jarFile));) {
JarEntry entry;
while ((entry = jarIn.getNextJarEntry()) != null) {
if (!entry.isDirectory()) {
String fileName = entry.getName();
if (fileName.endsWith("class")) {
JavaClass javaClass = new ClassParser(jarFile.getAbsolutePath(), fileName).parse();
classWalk(javaClass);
}
}
}
}
}
private void classWalk(final JavaClass javaClass) throws SQLException, ClassNotFoundException, IOException {
System.out.println(javaClass.getClassName());
executeSql(
"insert into class values(?, ?, ?, ?)",
new Object[] {
nextClassId,
javaClass.getClassName(),
javaClass.getAccessFlags(),
javaClass.getSuperclassName()
}
);
// メソッドの取得
final org.apache.bcel.classfile.Method[] methods = javaClass.getMethods();
for (org.apache.bcel.classfile.Method method : methods) {
methodWalk(nextClassId, javaClass, method);
}
Field[] fields = javaClass.getFields();
for (Field field : fields) {
fieldWalk(nextClassId, javaClass, field);
}
// インターフェイスの取得
for (JavaClass i : javaClass.getAllInterfaces()) {
if (i.getClassName().equals(javaClass.getClassName())) {
continue;
}
executeSql(
"insert into interface values(?, ?)",
new Object[] {
nextClassId,
i.getClassName()
}
);
}
// アノテーション
anotationWalk(nextClassId, javaClass.getAnnotationEntries());
if (nextClassId % 500 == 0) {
connection.commit();
}
// コミット
++nextClassId;
}
private void anotationWalk(final int refId, final AnnotationEntry[] annotations) throws SQLException {
for (AnnotationEntry a : annotations) {
executeSql(
"insert into anotation values(?, ?, ?)",
new Object[] {
nextAnnotationId,
refId,
a.getAnnotationType()
}
);
}
++nextAnnotationId;
}
private void methodWalk(final int classId, final JavaClass javaClass, final org.apache.bcel.classfile.Method method) throws SQLException, IOException {
String code = getCode(method);
executeSql(
"insert into method values(?, ?, ?, ?, ?, ?, ?)",
new Object[] {
nextMethodId,
classId,
method.getName(),
javaClass.getClassName() + "." + method.getName() + " " + method.getSignature(),
method.getAccessFlags(),
method.getReturnType().toString(),
code
}
);
int seq = 1;
for(Type p : method.getArgumentTypes()) {
executeSql(
"insert into method_parameter values(?, ?, ?)",
new Object[] {
nextMethodId,
seq,
p.toString()
}
);
++seq;
}
if (method.getCode() != null) {
ByteSequence stream = new ByteSequence(method.getCode().getCode());
for (int i = 0; stream.available() > 0 ; i++) {
analyzeCode(nextMethodId, stream, method.getConstantPool());
}
}
// アノテーション
anotationWalk(nextMethodId, method.getAnnotationEntries());
++nextMethodId;
}
private void fieldWalk(final int classId, final JavaClass javaClass, final org.apache.bcel.classfile.Field field) throws SQLException, IOException {
executeSql(
"insert into field values(?, ?, ?, ?, ?, ?)",
new Object[] {
nextMethodId,
classId,
field.getName(),
javaClass.getClassName() + "." + field.getName() + " " + field.getSignature(),
field.getAccessFlags(),
field.getType().toString()
}
);
// アノテーション
anotationWalk(nextMethodId, field.getAnnotationEntries());
++nextMethodId;
}
private boolean wide = false; /* The `WIDE' instruction is used in the
* byte code to allow 16-bit wide indices
* for local variables. This opcode
* precedes an `ILOAD', e.g.. The opcode
* immediately following takes an extra
* byte which is combined with the
* following byte to form a
* 16-bit value.
*/
/**
* 以下参考に実装
* commons-bcel/src/main/java/org/apache/bcel/classfile/Utility.java
* codeToString
* @param bytes
* @param constant_pool
* @throws IOException
* @throws SQLException
* @throws ClassFormatException
*/
public void analyzeCode(final int methodId, final ByteSequence bytes, final ConstantPool constant_pool) throws IOException, ClassFormatException, SQLException {
final short opcode = (short) bytes.readUnsignedByte();
int default_offset = 0;
int low;
int high;
int npairs;
int index;
int vindex;
int constant;
int[] match;
int[] jump_table;
int no_pad_bytes = 0;
int offset;
final boolean verbose = true;
final StringBuilder buf = new StringBuilder(Const.getOpcodeName(opcode));
/* Special case: Skip (0-3) padding bytes, i.e., the
* following bytes are 4-byte-aligned
*/
if ((opcode == Const.TABLESWITCH) || (opcode == Const.LOOKUPSWITCH)) {
final int remainder = bytes.getIndex() % 4;
no_pad_bytes = (remainder == 0) ? 0 : 4 - remainder;
for (int i = 0; i < no_pad_bytes; i++) {
byte b;
if ((b = bytes.readByte()) != 0) {
System.err.println("Warning: Padding byte != 0 in "
+ Const.getOpcodeName(opcode) + ":" + b);
}
}
// Both cases have a field default_offset in common
default_offset = bytes.readInt();
}
switch (opcode) {
/* Table switch has variable length arguments.
*/
case Const.TABLESWITCH:
low = bytes.readInt();
high = bytes.readInt();
offset = bytes.getIndex() - 12 - no_pad_bytes - 1;
default_offset += offset;
buf.append("\tdefault = ").append(default_offset).append(", low = ").append(low)
.append(", high = ").append(high).append("(");
jump_table = new int[high - low + 1];
for (int i = 0; i < jump_table.length; i++) {
jump_table[i] = offset + bytes.readInt();
buf.append(jump_table[i]);
if (i < jump_table.length - 1) {
buf.append(", ");
}
}
buf.append(")");
break;
/* Lookup switch has variable length arguments.
*/
case Const.LOOKUPSWITCH: {
npairs = bytes.readInt();
offset = bytes.getIndex() - 8 - no_pad_bytes - 1;
match = new int[npairs];
jump_table = new int[npairs];
default_offset += offset;
buf.append("\tdefault = ").append(default_offset).append(", npairs = ").append(
npairs).append(" (");
for (int i = 0; i < npairs; i++) {
match[i] = bytes.readInt();
jump_table[i] = offset + bytes.readInt();
buf.append("(").append(match[i]).append(", ").append(jump_table[i]).append(")");
if (i < npairs - 1) {
buf.append(", ");
}
}
buf.append(")");
}
break;
/* Two address bytes + offset from start of byte stream form the
* jump target
*/
case Const.GOTO:
case Const.IFEQ:
case Const.IFGE:
case Const.IFGT:
case Const.IFLE:
case Const.IFLT:
case Const.JSR:
case Const.IFNE:
case Const.IFNONNULL:
case Const.IFNULL:
case Const.IF_ACMPEQ:
case Const.IF_ACMPNE:
case Const.IF_ICMPEQ:
case Const.IF_ICMPGE:
case Const.IF_ICMPGT:
case Const.IF_ICMPLE:
case Const.IF_ICMPLT:
case Const.IF_ICMPNE:
buf.append("\t\t#").append((bytes.getIndex() - 1) + bytes.readShort());
break;
/* 32-bit wide jumps
*/
case Const.GOTO_W:
case Const.JSR_W:
buf.append("\t\t#").append((bytes.getIndex() - 1) + bytes.readInt());
break;
/* Index byte references local variable (register)
*/
case Const.ALOAD:
case Const.ASTORE:
case Const.DLOAD:
case Const.DSTORE:
case Const.FLOAD:
case Const.FSTORE:
case Const.ILOAD:
case Const.ISTORE:
case Const.LLOAD:
case Const.LSTORE:
case Const.RET:
if (wide) {
vindex = bytes.readUnsignedShort();
wide = false; // Clear flag
} else {
vindex = bytes.readUnsignedByte();
}
buf.append("\t\t%").append(vindex);
break;
/*
* Remember wide byte which is used to form a 16-bit address in the
* following instruction. Relies on that the method is called again with
* the following opcode.
*/
case Const.WIDE:
wide = true;
buf.append("\t(wide)");
break;
/* Array of basic type.
*/
case Const.NEWARRAY:
buf.append("\t\t<").append(Const.getTypeName(bytes.readByte())).append(">");
break;
/* Access object/class fields.
*/
case Const.GETFIELD:
case Const.GETSTATIC:
case Const.PUTFIELD:
case Const.PUTSTATIC:
index = bytes.readUnsignedShort();
buf.append("\t\t").append(
constant_pool.constantToString(index, Const.CONSTANT_Fieldref)).append(
verbose ? " (" + index + ")" : "");
executeSql(
"insert into method_depend values(?,?,?)",
new Object[] {
methodId,
constant_pool.constantToString(index, Const.CONSTANT_Fieldref),
opcode
}
);
break;
/* Operands are references to classes in constant pool
*/
case Const.NEW:
case Const.CHECKCAST:
buf.append("\t");
//$FALL-THROUGH$
case Const.INSTANCEOF:
index = bytes.readUnsignedShort();
buf.append("\t<").append(
constant_pool.constantToString(index, Const.CONSTANT_Class))
.append(">").append(verbose ? " (" + index + ")" : "");
executeSql(
"insert into method_depend values(?,?,?)",
new Object[] {
methodId,
constant_pool.constantToString(index, Const.CONSTANT_Class),
opcode
}
);
break;
/* Operands are references to methods in constant pool
*/
case Const.INVOKESPECIAL:
case Const.INVOKESTATIC:
index = bytes.readUnsignedShort();
final Constant c = constant_pool.getConstant(index);
// With Java8 operand may be either a CONSTANT_Methodref
// or a CONSTANT_InterfaceMethodref. (markro)
buf.append("\t").append(
constant_pool.constantToString(index, c.getTag()))
.append(verbose ? " (" + index + ")" : "");
executeSql(
"insert into method_depend values(?,?,?)",
new Object[] {
methodId,
constant_pool.constantToString(index, c.getTag()),
opcode
}
);
break;
case Const.INVOKEVIRTUAL:
index = bytes.readUnsignedShort();
buf.append("\t").append(
constant_pool.constantToString(index, Const.CONSTANT_Methodref))
.append(verbose ? " (" + index + ")" : "");
executeSql(
"insert into method_depend values(?,?,?)",
new Object[] {
methodId,
constant_pool.constantToString(index, Const.CONSTANT_Methodref),
opcode
}
);
break;
case Const.INVOKEINTERFACE:
index = bytes.readUnsignedShort();
final int nargs = bytes.readUnsignedByte(); // historical, redundant
buf.append("\t").append(
constant_pool
.constantToString(index, Const.CONSTANT_InterfaceMethodref))
.append(verbose ? " (" + index + ")\t" : "").append(nargs).append("\t")
.append(bytes.readUnsignedByte()); // Last byte is a reserved space
executeSql(
"insert into method_depend values(?,?,?)",
new Object[] {
methodId,
constant_pool.constantToString(index, Const.CONSTANT_InterfaceMethodref),
opcode
}
);
break;
case Const.INVOKEDYNAMIC:
index = bytes.readUnsignedShort();
buf.append("\t").append(
constant_pool
.constantToString(index, Const.CONSTANT_InvokeDynamic))
.append(verbose ? " (" + index + ")\t" : "")
.append(bytes.readUnsignedByte()) // Thrid byte is a reserved space
.append(bytes.readUnsignedByte()); // Last byte is a reserved space
executeSql(
"insert into method_depend values(?,?,?)",
new Object[] {
methodId,
constant_pool.constantToString(index, Const.CONSTANT_InvokeDynamic),
opcode
}
);
break;
/* Operands are references to items in constant pool
*/
case Const.LDC_W:
case Const.LDC2_W:
index = bytes.readUnsignedShort();
buf.append("\t\t").append(
constant_pool.constantToString(index, constant_pool.getConstant(index)
.getTag())).append(verbose ? " (" + index + ")" : "");
break;
case Const.LDC:
index = bytes.readUnsignedByte();
buf.append("\t\t").append(
constant_pool.constantToString(index, constant_pool.getConstant(index)
.getTag())).append(verbose ? " (" + index + ")" : "");
break;
/* Array of references.
*/
case Const.ANEWARRAY:
index = bytes.readUnsignedShort();
buf.append("\t\t<").append(
Utility.compactClassName(constant_pool.getConstantString(index,
Const.CONSTANT_Class), false)).append(">").append(
verbose ? " (" + index + ")" : "");
break;
/* Multidimensional array of references.
*/
case Const.MULTIANEWARRAY: {
index = bytes.readUnsignedShort();
final int dimensions = bytes.readUnsignedByte();
buf.append("\t<").append(
Utility.compactClassName(constant_pool.getConstantString(index,
Const.CONSTANT_Class), false)).append(">\t").append(dimensions)
.append(verbose ? " (" + index + ")" : "");
}
break;
/* Increment local variable.
*/
case Const.IINC:
if (wide) {
vindex = bytes.readUnsignedShort();
constant = bytes.readShort();
wide = false;
} else {
vindex = bytes.readUnsignedByte();
constant = bytes.readByte();
}
buf.append("\t\t%").append(vindex).append("\t").append(constant);
break;
default:
if (Const.getNoOfOperands(opcode) > 0) {
for (int i = 0; i < Const.getOperandTypeCount(opcode); i++) {
buf.append("\t\t");
switch (Const.getOperandType(opcode, i)) {
case Const.T_BYTE:
buf.append(bytes.readByte());
break;
case Const.T_SHORT:
buf.append(bytes.readShort());
break;
case Const.T_INT:
buf.append(bytes.readInt());
break;
default: // Never reached
throw new IllegalStateException("Unreachable default case reached!");
}
}
}
}
}
private String getCode(org.apache.bcel.classfile.Method method) {
if (method.getCode() == null) {
return "";
}
return method.getCode().toString();
}
}
bcelの解析結果からクラス図を作成する
bcelの解析結果を格納したSqliteからクラス図を作成しています。
SqliteToGraph.java
package sqliteToGraph;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.bcel.Const;
import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.SourceStringReader;
public class SqliteToGraph {
static final int MAX_MEMBER_SIZE = 10;
public class ClassData {
private int id;
private String name;
private String packageName;
private String className;
private int accessFlg;
private String superClassName;
public ClassData(ResultSet rs) throws SQLException {
id = rs.getInt("id");
name = rs.getString("name");
accessFlg = rs.getInt("access_flags");
superClassName = rs.getString("super_class_name");
int ix = name.lastIndexOf(".");
className = name.substring(ix + 1);
packageName = name.substring(0, ix);
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getPackageName() {
return packageName;
}
public String getClassName() {
return className;
}
public int getAccessFlg() {
return accessFlg;
}
public String getSuperClassName() {
return superClassName;
}
}
public static void main(String[] args) throws SQLException, IOException{
SqliteToGraph sg = new SqliteToGraph();
String dbPath = "..\\bcelToSqlite\\output.sqlite";
String path = "test_class.svg";
sg.parse(dbPath, path);
System.out.println("class:" + dbPath + "->" + path);
}
public void parse(String dbPath, String path) throws SQLException, IOException {
Connection connection = null;
ResultSet rs = null;
PreparedStatement pstmt = null;
PreparedStatement pstmtMethod = null;
StringBuilder sb = new StringBuilder();
try
{
// create a database connection
// Connection connection = DriverManager.getConnection("jdbc:sqlite:C:/work/mydatabase.db");
// Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
pstmt = connection.prepareStatement("select id, name, access_flags, super_class_name from class order by id");
rs = pstmt.executeQuery();
HashMap<Integer, ClassData> mapClass = new HashMap<Integer, ClassData>();
while(rs.next())
{
SqliteToGraph.ClassData data = new SqliteToGraph.ClassData(rs);
mapClass.put(data.id, data);
}
rs.close();
pstmt.close();
pstmt = null;
////
sb.append("@startuml\n");
sb.append("left to right direction\n");
pstmt = connection.prepareStatement("select id, name , access_flag, type from field where class_id = ?");
pstmtMethod = connection.prepareStatement("select distinct name , access_flag from method where class_id = ?");
for(Integer key : mapClass.keySet()) {
String prefix = "class";
if ((mapClass.get(key).getAccessFlg() & Const.ACC_INTERFACE) == Const.ACC_INTERFACE) {
prefix = "interface";
}
sb.append(" " + prefix +" \"" + mapClass.get(key).name + "\" {" + "\n");
// field
List<String> list = new ArrayList<String>();
pstmt.setInt(1, mapClass.get(key).id);
rs = pstmt.executeQuery();
while(rs.next())
{
list.add(rs.getString("name"));
}
for(int i = 0; i < list.size() ; ++i) {
sb.append(" " + list.get(i) + " \n");
if (i > MAX_MEMBER_SIZE) {
sb.append(" ... \n");
break;
}
}
rs.close();
list = new ArrayList<String>();
// method
pstmtMethod.setInt(1, mapClass.get(key).id);
rs = pstmtMethod.executeQuery();
while(rs.next())
{
if ((rs.getInt("access_flag") & Const.ACC_PUBLIC) == Const.ACC_PUBLIC) {
list.add(rs.getString("name"));
}
}
rs.close();
for(int i = 0; i < list.size() ; ++i) {
sb.append(" " + list.get(i) + "() \n");
if (i > MAX_MEMBER_SIZE) {
sb.append(" ...() \n");
break;
}
}
sb.append(" }\n");
if (checkSuperClassName(mapClass.get(key).getSuperClassName())) {
sb.append(" " + mapClass.get(key).getSuperClassName() + " <|-- " + mapClass.get(key).name + "\n");
}
}
pstmt.close();
pstmt = null;
pstmt = connection.prepareStatement("select class_id, interface_name from interface");
rs = pstmt.executeQuery();
while(rs.next())
{
if (checkSuperClassName(rs.getString("interface_name"))) {
sb.append(" " + rs.getString("interface_name") + " <|.. " + mapClass.get(rs.getInt("class_id")).name + "\n");
}
}
pstmt.close();
pstmt = null;
sb.append("@enduml\n");
writeSvg(sb.toString(), path);
}
finally
{
try
{
if(rs != null)
{
rs.close();
}
if(pstmt != null)
{
pstmt.close();
}
if(pstmtMethod != null)
{
pstmtMethod.close();
}
if(connection != null)
{
connection.close();
}
}
catch(SQLException e)
{
// connection close failed.
System.err.println(e);
}
}
}
private boolean checkSuperClassName(String superClassName) {
if (superClassName.startsWith("java.")) {
return false;
}
if (superClassName.startsWith("javax.")) {
return false;
}
return true;
}
private static void writeSvg(String source, String path) throws IOException {
SourceStringReader reader = new SourceStringReader(source);
final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Write the first image to "os"
@SuppressWarnings("deprecation")
String desc = reader.generateImage(os, new FileFormatOption(FileFormat.SVG));
os.close();
final String svg = new String(os.toByteArray(), Charset.forName("UTF-8"));
File out = new File(path);
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(out)));
pw.write(svg);
pw.close();
}
}
bcelの解析結果からコールグラフを作成する
bcelの解析結果を格納したSqliteから指定のメソッドのコールグラフを作成しています。
DependMethod.java
package sqliteToGraph;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.SourceStringReader;
public class DependMethod {
public static void main(String[] args) throws SQLException, IOException{
DependMethod dm = new DependMethod();
String dbPath = "..\\bcelToSqlite\\output.sqlite";
String path = "test_depend.svg";
String methodName = "org.apache.bcel.util.ClassPath.SYSTEM_CLASS_PATH";
dm.parse(dbPath, path, methodName);
System.out.println("depend:" + dbPath + "->" + path);
}
Connection connection = null;
PreparedStatement pstmtLike = null;
PreparedStatement pstmtEqual = null;
class TreeItem {
private String methodName;
private List<TreeItem> children = new ArrayList<TreeItem>();
public TreeItem(String m) {
methodName = m;
}
public List<TreeItem> GetChildren() {
return children;
}
}
List<TreeItem> root = new ArrayList<TreeItem>();
HashMap<String, TreeItem> map = new HashMap<String, TreeItem>();
List<String> rectangles = new ArrayList<String>();
public void parse(String dbPath, String path, String methodName) throws SQLException, IOException {
ResultSet rs = null;
try
{
connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
pstmtLike = connection.prepareStatement("select distinct method_depend.called_method from method inner join method_depend on method.id = method_depend.method_id where method_depend.called_method like ?");
pstmtLike.setString(1, "%" + methodName + "%");
rs = pstmtLike.executeQuery();
while(rs.next())
{
TreeItem item = new TreeItem(rs.getString("called_method"));
root.add(item);
map.put(item.methodName, item);
}
rs.close();
pstmtEqual = connection.prepareStatement("select distinct method.fullname as call_method from method inner join method_depend on method.id = method_depend.method_id where method_depend.called_method like ?");
for (TreeItem item : root) {
walkDependency(item.methodName);
}
StringBuilder sbDef = new StringBuilder();
StringBuilder sbArrow = new StringBuilder();
StringBuilder sb = new StringBuilder();
rectangles = new ArrayList<String>();
sb.append("@startuml\n");
for (TreeItem item : root) {
drawDependency(item, sbDef, sbArrow);
}
sb.append(sbDef.toString());
sb.append(sbArrow.toString());
sb.append("@enduml\n");
System.out.println(sb.toString());
writeSvg(sb.toString() + "\n" + sb.toString(), path);
}
finally
{
try
{
if(rs != null)
{
rs.close();
}
if(pstmtLike != null)
{
pstmtLike.close();
}
if(pstmtEqual != null)
{
pstmtEqual.close();
}
if(connection != null)
{
connection.close();
}
}
catch(SQLException e)
{
// connection close failed.
System.err.println(e);
}
}
}
void walkDependency(String calledMethod) throws SQLException {
pstmtEqual.setString(1, calledMethod);
ResultSet rs = pstmtEqual.executeQuery();
List<String> list = new ArrayList<String>();
while(rs.next())
{
String callMethod = rs.getString("call_method");
if (callMethod.equals(calledMethod)) {
// 再起呼び出し対策
continue;
}
list.add(callMethod);
if (!map.containsKey(callMethod)) {
TreeItem item = new TreeItem(callMethod);
map.put(item.methodName, item);
map.get(calledMethod).GetChildren().add(item);
}
}
rs.close();
for (String callMethod : list) {
walkDependency(callMethod);
}
}
void drawDependency(TreeItem item, StringBuilder sbDef, StringBuilder sbArrow) {
if (!rectangles.contains(item.methodName)) {
rectangles.add(item.methodName);
sbDef.append("rectangle \"" + item.methodName + "\" as " + makeAlias(item.methodName) + "\n");
}
for (TreeItem child : item.GetChildren()) {
sbArrow.append(makeAlias(item.methodName) + "<--" + makeAlias(child.methodName) + "\n");
drawDependency(child, sbDef, sbArrow);
}
}
private String makeAlias(String name) {
name = name.replaceAll("/", "_");
name = name.replaceAll(" ", "_");
name = name.replaceAll("<", "_");
name = name.replaceAll(">", "_");
name = name.replaceAll("\\$", "_");
name = name.replaceAll(";", "_");
name = name.replaceAll("\\(", "_");
name = name.replaceAll("\\)", "_");
name = name.replaceAll("\\[", "_");
name = name.replaceAll("\\]", "_");
return name;
}
private static void writeSvg(String source, String path) throws IOException {
SourceStringReader reader = new SourceStringReader(source);
final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Write the first image to "os"
@SuppressWarnings("deprecation")
String desc = reader.generateImage(os, new FileFormatOption(FileFormat.SVG));
os.close();
final String svg = new String(os.toByteArray(), Charset.forName("UTF-8"));
File out = new File(path);
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(out)));
pw.write(svg);
pw.close();
}
}
あとがき
DoxygenとGraphVizを使えば、こんなことしなくてもいいです。
Doxygenは結果をXMLを吐くこともできるので、解析も楽だと思います。
ただ、bcelはfindbug等のよくつかわれるツールに入っているので、インターネット禁止縛りプレイを楽しんでいるところでも、利用できます。
この時は、Sqliteでなくテキストに吐き出してからAccessにつっこんで使用するということをやっていました。
また、解析結果をDatabaseに格納しておくと、依存関係の調査等で役に立ったりします。