依存関係の追加
Maven または Gradle に JavaParser を追加します。
<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>
dependencies {
implementation 'com.github.javaparser:javaparser-core:3.26.1'
implementation 'com.github.jsqlparser:jsqlparser:4.9'
}
ソースファイルのパース
JavaParser の StaticJavaParser.parse() でソースファイルを読み込み、
CompilationUnit(ファイル全体のASTルート)を取得します。
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() メソッドをオーバーライドすることで、
目的のノードが現れたときだけ処理を差し込めます。
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候補リストを取得します。
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) で対応済みですが、
典型的なコードパターンと合わせて確認しておきます。
// ① メソッド内ローカル変数(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() の判定ロジックをもう少し強化します。
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() でソースツリーを再帰走査します。
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句など複数テーブルが絡むケースも丁寧に扱います。