ノード・エッジモデルへの変換
抽出した関係情報はグラフ理論の ノード(頂点)= クラス/メソッド、 エッジ(辺)= 関係 というモデルに変換するのが扱いやすい。 このモデルはグラフDB(Neo4j 等)への入力にも、Jarviz JSONL への変換にも応用できる。
/** グラフのエッジ(呼び出し関係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 に対応)
) {}
💡 appSetName と appName の設計指針
appSetName はシステム全体の名前(例: "ECommerceSystem")、
appName はモジュールやマイクロサービスの名前(例: "order-service")に対応させると
Jarviz 3Dマップでクラスタリングが機能しやすい。
モノリスの場合は appName にパッケージ上位2階層(例: "com.example")を使うのも有効だ。
統合抽出クラスの実装
PART 03 の CallGraphExtractor を拡張し、
appSetName / appName の情報を付加したうえで CallEdge リストを生成する。
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 もこの形式を入力として受け取る。
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");
}
}
{"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 で出力してスプレッドシートで確認し、フィルタリングのチューニングを行うのが効率的だ。
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可視化する方法を解説する。