依存関係の追加

Maven または Gradle に JavaParser を追加します。

XML — pom.xml(Maven)
<dependency>
    <groupId>com.github.javaparser</groupId>
    <artifactId>javaparser-core</artifactId>
    <version>3.26.1</version>
</dependency>

<!-- SQL解析(次のPARTで使用)-->
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.9</version>
</dependency>
Groovy — build.gradle(Gradle)
dependencies {
    implementation 'com.github.javaparser:javaparser-core:3.26.1'
    implementation 'com.github.jsqlparser:jsqlparser:4.9'
}

ソースファイルのパース

JavaParser の StaticJavaParser.parse() でソースファイルを読み込み、 CompilationUnit(ファイル全体のASTルート)を取得します。

Java — 基本的なパース
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;

import java.nio.file.Path;

public class SourceParser {

    public CompilationUnit parse(Path sourceFile) throws Exception {
        return StaticJavaParser.parse(sourceFile);
    }
}

💡 文字コードの指定

日本語コメントが含まれるソースでは StaticJavaParser.parse(sourceFile, StandardCharsets.UTF_8) のように文字コードを明示するとパース失敗を防げます。

VoidVisitorAdapterで走査

ASTの走査には VoidVisitorAdapter を継承したビジタークラスを作ります。 各ノード型に対応する visit() メソッドをオーバーライドすることで、 目的のノードが現れたときだけ処理を差し込めます。

Java — ビジター基本構造
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.body.FieldDeclaration;

import java.util.ArrayList;
import java.util.List;

/**
 * JavaソースのASTを走査してSQL候補文字列を収集するビジター
 */
public class SqlCandidateVisitor extends VoidVisitorAdapter<List<String>> {

    /** 文字列リテラルを検出 */
    @Override
    public void visit(StringLiteralExpr n, List<String> collector) {
        super.visit(n, collector);
        String value = n.asString();
        if (isSqlCandidate(value)) {
            collector.add(value);
        }
    }

    /** フィールド宣言を検出(定数SQLに対応) */
    @Override
    public void visit(FieldDeclaration n, List<String> collector) {
        super.visit(n, collector);
        n.getVariables().forEach(var ->
            var.getInitializer().ifPresent(init -> {
                if (init instanceof StringLiteralExpr) {
                    String value = ((StringLiteralExpr) init).asString();
                    if (isSqlCandidate(value)) {
                        collector.add(value);
                    }
                }
            })
        );
    }

    private boolean isSqlCandidate(String s) {
        if (s == null || s.isBlank()) return false;
        String upper = s.trim().toUpperCase();
        return upper.startsWith("SELECT")
            || upper.startsWith("INSERT")
            || upper.startsWith("UPDATE")
            || upper.startsWith("DELETE")
            || upper.startsWith("MERGE");
    }
}

文字列リテラルからSQL抽出

ビジターをCompilationUnitに適用してSQL候補リストを取得します。

Java — ビジターの適用
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class SqlExtractor {

    /**
     * 単一ソースファイルからSQL候補を抽出する
     *
     * @param sourceFile 対象Javaファイルのパス
     * @return SQL候補文字列リスト
     */
    public List<String> extract(Path sourceFile) throws Exception {
        CompilationUnit cu = StaticJavaParser.parse(sourceFile);
        List<String> candidates = new ArrayList<>();
        new SqlCandidateVisitor().visit(cu, candidates);
        return candidates;
    }

    public static void main(String[] args) throws Exception {
        SqlExtractor extractor = new SqlExtractor();
        List<String> sqls = extractor.extract(
            Path.of("src/main/java/com/example/OrderRepository.java")
        );
        sqls.forEach(System.out::println);
    }
}
実行結果例
SELECT * FROM orders WHERE user_id = ?
INSERT INTO orders (user_id, total) VALUES (?, ?)
UPDATE orders SET status = ? WHERE id = ?
DELETE FROM order_items WHERE order_id = ?

定数フィールドからSQL抽出

実際のコードでは SQL を定数として定義するパターンが多くあります。 ビジターの visit(FieldDeclaration) で対応済みですが、 典型的なコードパターンと合わせて確認しておきます。

Java — 対応する典型パターン
// ① メソッド内ローカル変数(StringLiteralExpr で検出)
public List<User> findAll() {
    String sql = "SELECT id, name FROM users";
    return jdbcTemplate.query(sql, ...);
}

// ② クラス定数(FieldDeclaration で検出)
private static final String FIND_BY_ID =
    "SELECT id, name FROM users WHERE id = ?";

// ③ メソッド引数に直接渡す(StringLiteralExpr で検出)
jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?", name, id);

// ④ 複数行文字列(Java 15+ テキストブロック)
String sql = """
    SELECT u.id, u.name, o.total
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE u.active = true
    """;
// ↑ テキストブロックも StringLiteralExpr として走査される

SQL判定フィルター

文字列リテラルすべてを対象にすると、エラーメッセージや設定値も混入します。 isSqlCandidate() の判定ロジックをもう少し強化します。

Java — フィルター強化版
private static final Pattern SQL_START =
    Pattern.compile(
        "^\\s*(SELECT|INSERT\\s+INTO|UPDATE|DELETE\\s+FROM|MERGE)\\b",
        Pattern.CASE_INSENSITIVE | Pattern.DOTALL
    );

private boolean isSqlCandidate(String s) {
    if (s == null || s.length() < 10) return false;
    return SQL_START.matcher(s).find();
}

⚠️ 誤検出について

SELECT から始まるログメッセージ文字列が稀に存在します。誤検出した場合は次のPARTのJSqlParserでパースを試み、パース失敗したものを除外することで自動的に取り除けます。

複数ファイルの処理

プロジェクト全体を対象にするには、Files.walk() でソースツリーを再帰走査します。

Java — プロジェクト全体スキャン
import java.nio.file.*;
import java.util.*;

public class ProjectScanner {

    /**
     * srcRoot 配下のすべての .java ファイルをスキャンし、
     * クラス名 → SQL候補リスト のマップを返す
     */
    public Map<String, List<String>> scan(Path srcRoot) throws Exception {
        SqlExtractor extractor = new SqlExtractor();
        Map<String, List<String>> result = new LinkedHashMap<>();

        try (var stream = Files.walk(srcRoot)) {
            stream.filter(p -> p.toString().endsWith(".java"))
                  .forEach(javaFile -> {
                      try {
                          List<String> sqls = extractor.extract(javaFile);
                          if (!sqls.isEmpty()) {
                              // クラス名はファイル名から取得(簡易版)
                              String className = javaFile.getFileName()
                                  .toString().replace(".java", "");
                              result.put(className, sqls);
                          }
                      } catch (Exception e) {
                          System.err.println("Parse error: " + javaFile + " - " + e.getMessage());
                      }
                  });
        }
        return result;
    }
}

💡 クラス名の正確な取得

上記はファイル名からクラス名を取る簡易実装です。より正確には CompilationUnit.getType(0).getNameAsString() で定義されたクラス名を取得できます。パッケージ名を含むFQCNにしたい場合は cu.getPackageDeclaration() と組み合わせてください。

次の章では…

PART 04 では抽出したSQL文字列を JSqlParser に通し、テーブル名の取得と操作種別(C/R/U/D)の判別を実装します。FROM句・INTO句・JOIN句など複数テーブルが絡むケースも丁寧に扱います。

→ PART 04 — テーブル名・操作種別の判別へ