ノード・エッジモデルへの変換

抽出した関係情報はグラフ理論の ノード(頂点)= クラス/メソッドエッジ(辺)= 関係 というモデルに変換するのが扱いやすい。 このモデルはグラフDB(Neo4j 等)への入力にも、Jarviz JSONL への変換にも応用できる。

Java — グラフモデルのデータクラス定義
/** グラフのエッジ(呼び出し関係1件) */
public record CallEdge(
    String appSetName,    // アプリグループ名  例: "MySystem"
    String appName,       // モジュール/サービス名  例: "order-service"
    String sourceClass,   // 呼び出し元クラス  例: "com.example.OrderService"
    String sourceMethod,  // 呼び出し元メソッド
    String targetClass,   // 呼び出し先クラス
    String targetMethod   // 呼び出し先メソッド
) {}

/** グラフのノード(クラス1件) */
public record ClassNode(
    String fullyQualifiedName,  // FQCN  例: "com.example.OrderService"
    String packageName,          // パッケージ
    String simpleName,           // 短縮名
    String moduleName            // モジュール (appName に対応)
) {}

💡 appSetNameappName の設計指針

appSetName はシステム全体の名前(例: "ECommerceSystem")、 appName はモジュールやマイクロサービスの名前(例: "order-service")に対応させると Jarviz 3Dマップでクラスタリングが機能しやすい。 モノリスの場合は appName にパッケージ上位2階層(例: "com.example")を使うのも有効だ。

統合抽出クラスの実装

PART 03 の CallGraphExtractor を拡張し、 appSetName / appName の情報を付加したうえで CallEdge リストを生成する。

Java — 統合抽出クラス(CallEdgeBuilder)
import org.objectweb.asm.*;
import java.io.*;
import java.util.*;
import java.util.jar.*;

public class CallEdgeBuilder {

    private final String appSetName;
    private final String appName;
    private final List<CallEdge> edges = new ArrayList<>();

    /** @param appSetName システム全体名  @param appName モジュール名 */
    public CallEdgeBuilder(String appSetName, String appName) {
        this.appSetName = appSetName;
        this.appName    = appName;
    }

    public void analyzeJar(String jarPath) throws IOException {
        try (JarFile jar = new JarFile(jarPath)) {
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.getName().endsWith(".class")) {
                    try (InputStream is = jar.getInputStream(entry)) {
                        analyzeClass(new ClassReader(is));
                    }
                }
            }
        }
    }

    private void analyzeClass(ClassReader cr) {
        cr.accept(new ClassVisitor(Opcodes.ASM9) {

            private String currentClass;

            @Override
            public void visit(int version, int access, String name,
                    String sig, String superName, String[] ifaces) {
                currentClass = name.replace('/', '.');
            }

            @Override
            public MethodVisitor visitMethod(int access, String name,
                    String desc, String sig, String[] exceptions) {
                final String srcMethod = name;
                return new MethodVisitor(Opcodes.ASM9) {
                    @Override
                    public void visitMethodInsn(int opcode, String owner,
                            String mName, String mDesc, boolean isInterface) {
                        String targetClass = owner.replace('/', '.');
                        // 標準ライブラリ除外(必要に応じてカスタマイズ)
                        if (isLibraryClass(targetClass)) return;

                        edges.add(new CallEdge(
                            appSetName, appName,
                            currentClass, srcMethod,
                            targetClass,  mName
                        ));
                    }
                };
            }
        }, 0);
    }

    private static boolean isLibraryClass(String className) {
        return className.startsWith("java.")
            || className.startsWith("javax.")
            || className.startsWith("sun.")
            || className.startsWith("org.springframework.")
            || className.startsWith("com.fasterxml.");
    }

    public List<CallEdge> build() { return Collections.unmodifiableList(edges); }
}

JSONL形式での出力

JSONL(JSON Lines)は 1行 = 1JSON オブジェクト というフォーマットだ。 大量データを行単位でストリーム処理しやすく、Jarviz もこの形式を入力として受け取る。

Java — JSONL 出力(Jackson 使用)
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.util.List;

public class JsonlWriter {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    public static void write(List<CallEdge> edges, String outputPath) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath))) {
            for (CallEdge edge : edges) {
                // CallEdge の各フィールドを Jarviz 期待フォーマットにマップ
                var node = MAPPER.createObjectNode()
                    .put("appSetName",    edge.appSetName())
                    .put("appName",       edge.appName())
                    .put("sourceClass",   edge.sourceClass())
                    .put("sourceMethod",  edge.sourceMethod())
                    .put("targetClass",   edge.targetClass())
                    .put("targetMethod",  edge.targetMethod());
                bw.write(MAPPER.writeValueAsString(node));
                bw.newLine();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        CallEdgeBuilder builder = new CallEdgeBuilder("ECommerceSystem", "order-service");
        builder.analyzeJar("target/order-service.jar");
        List<CallEdge> edges = builder.build();
        write(edges, "coupling.jsonl");
        System.out.println("出力完了: " + edges.size() + " エッジ → coupling.jsonl");
    }
}
coupling.jsonl の出力例
{"appSetName":"ECommerceSystem","appName":"order-service","sourceClass":"com.example.OrderService","sourceMethod":"createOrder","targetClass":"com.example.InventoryClient","targetMethod":"checkStock"}
{"appSetName":"ECommerceSystem","appName":"order-service","sourceClass":"com.example.OrderService","sourceMethod":"createOrder","targetClass":"com.example.PaymentGateway","targetMethod":"charge"}
{"appSetName":"ECommerceSystem","appName":"order-service","sourceClass":"com.example.OrderRepository","sourceMethod":"save","targetClass":"com.example.db.DataSource","targetMethod":"getConnection"}
{"appSetName":"ECommerceSystem","appName":"inventory-service","sourceClass":"com.example.InventoryService","sourceMethod":"checkStock","targetClass":"com.example.CacheClient","targetMethod":"get"}

CSV形式での中間出力と確認

大量の JSONL を目視確認するのは難しい。 まず CSV で出力してスプレッドシートで確認し、フィルタリングのチューニングを行うのが効率的だ。

Java — CSV 出力
public class CsvWriter {

    public static void write(List<CallEdge> edges, String outputPath) throws IOException {
        try (PrintWriter pw = new PrintWriter(new FileWriter(outputPath))) {
            pw.println("appSetName,appName,sourceClass,sourceMethod,targetClass,targetMethod");
            for (CallEdge e : edges) {
                pw.printf("%s,%s,%s,%s,%s,%s%n",
                    e.appSetName(), e.appName(),
                    e.sourceClass(), e.sourceMethod(),
                    e.targetClass(), e.targetMethod());
            }
        }
    }
}

フィルタリングのコツ

無加工の JAR を解析すると数千〜数万エッジが生成される。 ノイズを減らすフィルタリングが可視化品質に直結する。

フィルタ対象実装のヒント
標準ライブラリ(java.*)targetClass.startsWith("java.") で除外
Lombok生成メソッドsourceMethod.startsWith("get") || startsWith("set")で除外
テストクラスsourceClass.contains("Test") で除外
内部クラスsourceClass.contains("$") で除外(オプション)
同一クラス内呼び出しsourceClass.equals(targetClass) で除外(モジュール間分析に集中)

次の PART 05 への布石

ここで作成した coupling.jsonl がそのまま Jarviz の入力になる。 PART 05 では JSONL の各フィールドの意味と3Dマップへのマッピング、 そして Java 以外の言語でも同じ JSONL を生成して3D可視化する方法を解説する。