Extract class and method information from Java bytecode and record it in Sqlite. Describe the class diagram and call graph using the information recorded above.
If you can use doxygen + graphviz, you should use it.
・ Windows 10 ・ Java8 ・ Bcel-6.3.1. ・ Sqlite-jdbc-3.27.2.1 ・ Plantuml.jar
bcel The BCEL API (Bytecode Engineering Library) is a toolkit for static analysis and the creation of dynamic Java class files. FindBug uses BCEL to do static analysis from class files.
https://commons.apache.org/proper/commons-bcel/manual/introduction.html
JavaSE BCEL https://hondou.homedns.org/pukiwiki/index.php?JavaSE%20BCEL
Also, the following codeToString will be helpful for how to interpret the bytecode binary. https://github.com/llmhyy/commons-bcel/blob/master/src/main/java/org/apache/bcel/classfile/Utility.java
Sqlite-JDBC You can download it from the following page with JDBC that operates SQLite.
download 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);
}
}
}
}
You can also create user-defined functions. In addition, the following test code will be helpful for SQLite-specific operations. https://github.com/xerial/sqlite-jdbc/tree/c7c5604bcc584460268abc9a64df2953fca788d3/src/test/java/org/sqlite
PlantUML You can write the following figure including UML in a language called DSL.
You can officially try the actual description from the web page. There is also a Redmine plugin, so you can embed various figures as text data in the Wiki.
official: http://plantuml.com/ja/
download http://plantuml.com/ja/download
PlantUML Cheat Sheet https://qiita.com/ogomr/items/0b5c4de7f38fd1482a48
You can also output PNG or SVG from your program by embedding the plantuml.jar file in your Java project. http://plantuml.com/ja/api
https://github.com/mima3/BcelToSqlite
The following code specifies a Jar file, parses the class file stored in it, and records the class, method, and field information in 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;
/**
*Implemented with reference to the following
* 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()
}
);
//Get method
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);
}
//Get interface
for (JavaClass i : javaClass.getAllInterfaces()) {
if (i.getClassName().equals(javaClass.getClassName())) {
continue;
}
executeSql(
"insert into interface values(?, ?)",
new Object[] {
nextClassId,
i.getClassName()
}
);
}
//Annotation
anotationWalk(nextClassId, javaClass.getAnnotationEntries());
if (nextClassId % 500 == 0) {
connection.commit();
}
//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());
}
}
//Annotation
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()
}
);
//Annotation
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.
*/
/**
*Implemented for reference below
* 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();
}
}
I am creating a class diagram from Sqlite that stores the analysis result of bcel.
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();
}
}
A call graph of the specified method is created from Sqlite that stores the analysis result of bcel.
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)) {
//Countermeasures against recall calls
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();
}
}
With Doxygen and GraphViz, you don't have to do this. I think Doxygen is easy to parse because it can also spit out the result as XML.
However, since bcel is included in commonly used tools such as findbug, it can be used even where you are enjoying internet ban binding play. At this time, I was doing it by spitting it out in text instead of Sqlite and then sticking it in Access.
Also, storing the analysis results in the Database is useful for investigating dependencies.
Recommended Posts