LoginSignup
4
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-09

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

サンプルコード

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

実験結果

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0