Javaのバイトコードを解析した結果をクラス図で表現してみる

Table of Content

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を使用してクラスファイルから静的解析を行っています。

bcel.png

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/

ダウンロード
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からクラス図を作成しています。
plantUML.png

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から指定のメソッドのコールグラフを作成しています。

planguml2.png

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に格納しておくと、依存関係の調査等で役に立ったりします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です