複数モジュールに分かれたJavaプロジェクトで、IDEから手動でJavadocを生成する運用を続けていると、モジュールが増えるたびに生成漏れが発生します。Pythonでパッケージ検出から生成・警告チェックまでを自動化し、CIに乗せた際の実装をまとめています。

はじめに

前回の記事ではJavaDocの記述ルールを整理した。 ルールを守って書けても、生成そのものを人手で行っていると「更新を忘れる」「一部のモジュールだけ生成し忘れる」といった運用ミスが起きやすい。

この記事ではPythonのsubprocessからjavadocコマンドを呼び出し、以下を実現するスクリプトを組み立てる。

  • ソースディレクトリを再帰的に走査し、パッケージ一覧を自動収集する
  • エンコーディング・出力先を指定してjavadocを一括実行する
  • 標準出力・エラーをログファイルに保存する
  • 警告・エラー件数を検出し、多すぎる場合は異常終了させる
  • GitHub Actions上でCIとして実行する

前提環境

javadocコマンドはJDKに同梱されている。JREだけの環境では実行できないため、まずjavadoc -versionでPATHが通っているか確認する。

Shell — 環境確認
javadoc -version
# javadoc 17.0.9

python3 --version
# Python 3.11.6

想定するJavaプロジェクトのソース構成は以下の通り。src配下の階層がそのままパッケージ構造に対応する一般的なMaven/Gradleレイアウトを前提にする。

project/
  src/main/java/
    com/example/app/
      Application.java
    com/example/app/service/
      OrderService.java
    com/example/app/repository/
      OrderRepository.java

基本:subprocessでjavadocコマンドを呼ぶ

最小構成では、対象パッケージ名を直接指定してsubprocess.runに渡すだけでよい。 capture_output=Trueで標準出力・標準エラーをPython側で受け取れる。

Python — 最小構成
import subprocess

result = subprocess.run(
    [
        "javadoc",
        "-d", "build/javadoc",
        "-sourcepath", "src/main/java",
        "-encoding", "UTF-8",
        "com.example.app",
        "com.example.app.service",
    ],
    capture_output=True,
    text=True,
)

print("returncode:", result.returncode)
print(result.stdout)
print(result.stderr)

💡 text=Trueで文字列として受け取る

text=True(Python 3.7+)を指定しないとstdout/stderrはバイト列になる。日本語を含むログを扱うためencoding="utf-8"を明示するとより安全になる:subprocess.run(..., text=True, encoding="utf-8")

パッケージを自動検出して一括生成する

パッケージ名を手書きで羅列するのはモジュールが増えると破綻する。pathlib.Path.rglob.javaファイルを再帰的に走査し、 ソースルートからの相対ディレクトリをドット区切りのパッケージ名に変換すれば自動化できる。

Python — discover_packages()
from pathlib import Path


def discover_packages(source_root: Path) -> list[str]:
    """source_root 以下から package を持つディレクトリをパッケージ名に変換する。"""
    packages = set()
    for java_file in source_root.rglob("*.java"):
        rel_dir = java_file.parent.relative_to(source_root)
        if rel_dir == Path("."):
            continue  # デフォルトパッケージは対象外
        package_name = ".".join(rel_dir.parts)
        packages.add(package_name)
    return sorted(packages)


# 使用例
packages = discover_packages(Path("src/main/java"))
print(packages)
# ['com.example.app', 'com.example.app.repository', 'com.example.app.service']

⚠️ ディレクトリ名とpackage宣言のズレは検出できない

この方式はディレクトリ構造からパッケージ名を推測するだけで、.javaファイル内のpackage宣言そのものは読んでいない。標準的なMaven/Gradleレイアウトなら一致するが、独自レイアウトのプロジェクトではpackage宣言を正規表現で読み取る実装に切り替えること。

オプション指定 — エンコーディング・出力先・除外

よく使うjavadocオプションを整理する。文字コード関連は日本語プロジェクトで特にトラブルになりやすい。

オプション役割
-d <dir>生成したHTMLの出力先ディレクトリ
-sourcepath <dir>パッケージ名を解決するためのソースルート
-encoding <enc>ソースファイルの文字コード。ソースがUTF-8ならUTF-8を指定
-docencoding <enc> / -charset <enc>生成するHTMLの文字コード。-encodingと揃えるのが基本
-privateprivateメンバーまで含めて生成する(既定は public/protected のみ)
-quiet進捗メッセージを抑制し、警告・エラーだけを出力する
-Xdoclint:all,-missingHTML構文チェック等を有効化しつつ、Javadoc未記載の警告だけ除外する
Python — build_javadoc_command()
def build_javadoc_command(
    source_root: Path,
    output_dir: Path,
    packages: list[str],
    encoding: str = "UTF-8",
    exclude: list[str] | None = None,
) -> list[str]:
    exclude = exclude or []
    target_packages = [p for p in packages if p not in exclude]

    return [
        "javadoc",
        "-d", str(output_dir),
        "-sourcepath", str(source_root),
        "-encoding", encoding,
        "-docencoding", encoding,
        "-charset", encoding,
        "-quiet",
        *target_packages,
    ]

標準出力・エラーのキャプチャとログ保存

CI環境ではコンソール出力が流れて後から確認しづらい。capture_output=Trueで受け取った内容をファイルへ保存し、ビルドアーティファクトとして残す。

Python — ログ保存
result = subprocess.run(cmd, capture_output=True, text=True)

log_path = Path("javadoc.log")
log_path.write_text(result.stdout + result.stderr, encoding="utf-8")

print(f"ログを保存しました: {log_path}")

生成結果の警告・エラー検出

javadocコマンドは構文エラーが無ければ警告があっても終了コード0を返すことが多い。 警告を見逃さないために、出力テキストを正規表現でスキャンして件数を数え、CIのゲートとして使う。

Python — count_issues()
import re


def count_issues(output_text: str) -> tuple[int, int]:
    warnings = len(re.findall(r"^warning:", output_text, re.MULTILINE))
    errors = len(re.findall(r"^error:", output_text, re.MULTILINE))
    return warnings, errors


warnings, errors = count_issues(result.stdout + result.stderr)
print(f"警告: {warnings}件 / エラー: {errors}件")

if errors > 0:
    raise SystemExit("javadoc生成でエラーが発生しました")

ここまでの要素を1本のスクリプトにまとめると、以下のようになる。

Python — generate_javadoc.py(全体)
#!/usr/bin/env python3
"""javadocを一括生成し、警告・エラーを検出するスクリプト。"""
import argparse
import re
import subprocess
import sys
from pathlib import Path


def discover_packages(source_root: Path) -> list[str]:
    packages = set()
    for java_file in source_root.rglob("*.java"):
        rel_dir = java_file.parent.relative_to(source_root)
        if rel_dir == Path("."):
            continue
        packages.add(".".join(rel_dir.parts))
    return sorted(packages)


def build_javadoc_command(
    source_root: Path,
    output_dir: Path,
    packages: list[str],
    encoding: str,
) -> list[str]:
    return [
        "javadoc",
        "-d", str(output_dir),
        "-sourcepath", str(source_root),
        "-encoding", encoding,
        "-docencoding", encoding,
        "-charset", encoding,
        "-quiet",
        *packages,
    ]


def count_issues(output_text: str) -> tuple[int, int]:
    warnings = len(re.findall(r"^warning:", output_text, re.MULTILINE))
    errors = len(re.findall(r"^error:", output_text, re.MULTILINE))
    return warnings, errors


def main() -> int:
    parser = argparse.ArgumentParser(description="javadoc一括生成スクリプト")
    parser.add_argument("source", type=Path, help="Javaソースのルートディレクトリ")
    parser.add_argument("-o", "--output", type=Path, default=Path("build/javadoc"))
    parser.add_argument("-e", "--encoding", default="UTF-8")
    parser.add_argument("--log", type=Path, default=Path("javadoc.log"))
    parser.add_argument("--max-warnings", type=int, default=-1, help="許容する警告件数。-1は無制限")
    args = parser.parse_args()

    packages = discover_packages(args.source)
    if not packages:
        print(f"パッケージが見つかりませんでした: {args.source}", file=sys.stderr)
        return 1

    args.output.mkdir(parents=True, exist_ok=True)
    cmd = build_javadoc_command(args.source, args.output, packages, args.encoding)

    print(f"対象パッケージ数: {len(packages)}")
    result = subprocess.run(cmd, capture_output=True, text=True)

    combined = result.stdout + result.stderr
    args.log.write_text(combined, encoding="utf-8")

    warnings, errors = count_issues(combined)
    print(f"警告: {warnings}件 / エラー: {errors}件(詳細は {args.log} を参照)")

    if result.returncode != 0 or errors > 0:
        print("javadoc生成に失敗しました。", file=sys.stderr)
        return 1

    if args.max_warnings >= 0 and warnings > args.max_warnings:
        print(f"警告が許容件数({args.max_warnings})を超えました。", file=sys.stderr)
        return 1

    print(f"生成完了: {args.output}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
実行例
$ python3 generate_javadoc.py src/main/java -o build/javadoc --max-warnings 0
対象パッケージ数: 3
警告: 0件 / エラー: 0件(詳細は javadoc.log を参照)
生成完了: build/javadoc

GitHub ActionsへのCI組み込み

ローカルで動くスクリプトができたら、pull request作成時に自動実行してドキュメント漏れを検知する。 --max-warnings 0を指定すれば、警告が1件でもあればワークフローを失敗させられる。

YAML — .github/workflows/javadoc.yml
name: JavaDoc Check

on:
  pull_request:
    paths:
      - 'src/main/java/**'

jobs:
  javadoc:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Generate JavaDoc
        run: |
          python3 generate_javadoc.py src/main/java \
            -o build/javadoc \
            --max-warnings 0

      - name: Upload JavaDoc artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: javadoc-html
          path: build/javadoc

artifactとしてHTMLを残すとレビューが楽になる

upload-artifactで生成済みHTMLをCIの実行結果からダウンロードできるようにしておくと、レビュワーがローカルでjavadocを実行し直す必要がなくなる。if: always()にしておけば、警告で失敗した場合でも生成物を確認できる。

動作確認環境: Python 3.11 / JDK 17 / GitHub Actions ubuntu-latest

まとめ

この記事のポイントを整理する。

テーマ要点
基本の呼び出し subprocess.runjavadocコマンドとオプションのリストを渡し、capture_output=True, text=Trueで結果を受け取る
パッケージ自動検出 Path.rglob("*.java")と相対パスからパッケージ名を組み立てれば手書きの列挙が不要になる
文字コード -encoding -docencoding -charsetを揃えて指定し、文字化けを防ぐ
警告検出 終了コードだけでは不十分。出力テキストを正規表現でスキャンし、件数をCIのゲートにする
CI連携 GitHub Actionsでpull request時に実行し、生成物をartifactとして残すとレビューが効率化する