複数モジュールに分かれたJavaプロジェクトで、IDEから手動でJavadocを生成する運用を続けていると、モジュールが増えるたびに生成漏れが発生します。Pythonでパッケージ検出から生成・警告チェックまでを自動化し、CIに乗せた際の実装をまとめています。
はじめに
前回の記事ではJavaDocの記述ルールを整理した。 ルールを守って書けても、生成そのものを人手で行っていると「更新を忘れる」「一部のモジュールだけ生成し忘れる」といった運用ミスが起きやすい。
この記事ではPythonのsubprocessからjavadocコマンドを呼び出し、以下を実現するスクリプトを組み立てる。
- ソースディレクトリを再帰的に走査し、パッケージ一覧を自動収集する
- エンコーディング・出力先を指定して
javadocを一括実行する - 標準出力・エラーをログファイルに保存する
- 警告・エラー件数を検出し、多すぎる場合は異常終了させる
- GitHub Actions上でCIとして実行する
前提環境
javadocコマンドはJDKに同梱されている。JREだけの環境では実行できないため、まずjavadoc -versionでPATHが通っているか確認する。
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側で受け取れる。
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ファイルを再帰的に走査し、
ソースルートからの相対ディレクトリをドット区切りのパッケージ名に変換すれば自動化できる。
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と揃えるのが基本 |
-private | privateメンバーまで含めて生成する(既定は public/protected のみ) |
-quiet | 進捗メッセージを抑制し、警告・エラーだけを出力する |
-Xdoclint:all,-missing | HTML構文チェック等を有効化しつつ、Javadoc未記載の警告だけ除外する |
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で受け取った内容をファイルへ保存し、ビルドアーティファクトとして残す。
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のゲートとして使う。
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本のスクリプトにまとめると、以下のようになる。
#!/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件でもあればワークフローを失敗させられる。
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.runにjavadocコマンドとオプションのリストを渡し、capture_output=True, text=Trueで結果を受け取る |
| パッケージ自動検出 | Path.rglob("*.java")と相対パスからパッケージ名を組み立てれば手書きの列挙が不要になる |
| 文字コード | -encoding -docencoding -charsetを揃えて指定し、文字化けを防ぐ |
| 警告検出 | 終了コードだけでは不十分。出力テキストを正規表現でスキャンし、件数をCIのゲートにする |
| CI連携 | GitHub Actionsでpull request時に実行し、生成物をartifactとして残すとレビューが効率化する |