Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】How we integrate best practices in Java

2025年09月17日に「Dev.to」が公開したITニュース「How we integrate best practices in Java」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

複雑化したJavaアプリケーションコードを、DI(Dependency Dependency Injection)フレームワークGuiceで刷新した。新しいアノテーション処理モジュールをDI前提で設計し、既存コードも段階的にリファクタリング。これによりコードの結合度を下げ、拡張性を高めた結果、保守性や機能追加のしやすさが大幅に向上した。

出典: How we integrate best practices in Java | Dev.to公開日:

ITニュース解説

長年にわたり開発されてきたJava製静的アナライザーのアプリケーションを、現代のベストプラクティスに合わせて改善した物語だ。

プロジェクトは7年前にJava 8が登場した頃から始まり、その間に開発チームが何度も入れ替わり、一時的な停滞期も経験した。その結果、コードベースには多くの問題が蓄積されていた。具体的には、どこからでも呼び出せるstaticメソッドが大量に存在し、一つの処理だけでなく複数の役割を担うような長々としたメソッド名(例:「doThisAndThisBeforeThat」)が使われ、アプリケーション全体で共有されるグローバルな状態が乱用され、本来一つだけであるべきシングルトンパターンが至る所に散見され、コード本体よりもコメントが長くなるような箇所も多かった。さらに、コードスタイルもJavaの標準から逸脱し、C#やC++の書き方が混在している状態だった。

まず、コードスタイルの乱れを修正する作業から始めた。これはGoogle Java Styleをベースに、インデントをスペース4つにするなど業界標準的な変更を加え、自動フォーマットツールを活用することで比較的容易に解決できた。しかし、アプリケーションの根幹であるアーキテクチャの変更は、もっと困難な課題だと考えられたため、必要性が生じた際に段階的に変更していく方針を採用した。

その「必要性」はすぐに訪れた。「taint module」(汚染データ検出モジュール)の開発を開始するにあたり、より高度なアノテーションメカニズムが必要となったのだ。既存のアナライザーが使用していたアノテーションメカニズムはC++で書かれたネイティブな部分に依存しており、JavaアナライザーがC++アナライザーの内部ライブラリを共有していることに起因していた。このC++製のネイティブ部分はデバッグが非常に困難であり、チームにC++の知識がないことも大きな障害となっていた。

さらに、ユーザーが定義するアノテーション(JSON形式)をサポートする必要性が浮上し、古いネイティブモジュールを完全に置き換える時が来たと判断された。そこで、新しいアノテーションモジュールを純粋なJavaで書き直すことを決定した。

同時に、アプリケーション全体をより堅牢で保守しやすいものにするため、DI(Dependency Injection:依存性注入)フレームワークの導入も決断した。これにより、新しいモジュールだけでなく、既存のメインモジュールもDIに対応するよう全面的に改修する必要が生じた。これが、今回のアーキテクチャ変更の二つの大きな柱となった。

なぜDIが必要なのか。DIは、オブジェクト間の依存関係を外部(DIコンテナ)で管理することで、コードの結合度を低減し、再利用性やテストのしやすさを向上させる技術である。Javaの世界ではSpring FrameworkのIoC(Inversion of Control:制御の反転)コンテナとして広く普及している。

しかし、このアナライザーはウェブアプリケーションではなく、コマンドラインインターフェース(CLI)アプリケーションであるため、Springのような大規模なフレームワークは不要な依存関係を多数持ち込み、過剰に重すぎると判断された。必要なのはIoCコンテナの機能だけだったため、Googleが開発した軽量なDIフレームワーク「Guice(グース)」が選択された。GuiceはSpringのような余分な機能を持たず、CLIアプリケーションに適していた。Daggerも候補には挙がったが、実行時に依存関係を解析する必要があるため、反射(reflection)の使用が問題ないデスクトップアプリケーションであるGuiceが採用された。

Guiceの基本的な使い方を簡単に見てみよう。依存関係を定義するには、まずAbstractModuleクラスを継承したモジュールクラスを作成し、その中のconfigureメソッドで設定を行う。例えば、bind(AnnotationProcessor.class);と書くことで、AnnotationProcessorクラスのインスタンスをDIコンテナが管理するように指示できる。アプリケーションの起動時にはGuice.createInjector(new AnnotationModule());のようにインジェクターを作成し、injector.getInstance(AnnotationProcessor.class);を呼び出すことで、必要なAnnotationProcessorのインスタンスを取得できる。

AnnotationProcessorが他のクラス(例えばDependency)に依存している場合、そのコンストラクタに@Injectアノテーションを付与する。するとGuiceは、AnnotationProcessorのインスタンスを作成する際に、Dependencyのインスタンスを自動的に探し出して注入してくれる。もちろん、Dependencyクラスもモジュール内でバインドしておく必要がある。インターフェースとその実装クラスをバインドする場合は、bind(MyInterface.class).to(MyInterfaceImpl.class);のように記述する。また、@Providesアノテーションをメソッドに付与することで、より複雑な初期化ロジックを持つオブジェクトを提供することも可能である。

新しいモジュールは、このGuiceを基盤として設計された。目標は、解析対象のソースコードモデルを走査し、ルールに基づいて各要素にアノテーションを付加することだった。この新しいモジュールは、アノテーションプロバイダー、アノテーションプロセッサー、そしてインタラクションサービスという主要なクラス群で構成される。

ユーザー定義のアノテーションを扱うために、「JSONアノテーションパーサー」モジュールも作成され、解析、検証、データ変換を担当する。また、C++のデータフロー解析で使われていた機能を実現するため、「宣言型アノテーションのための内部DSL(Domain Specific Language)」も開発された。これにより、例えばjava.sql.Statementクラスのメソッド呼び出しを「SQLインジェクションのシンク」としてマークアップするような複雑なルールを、簡潔に記述できるようになった。

これらのモジュール間の連携には、GuiceのMultibinderという機能が活用された。これは、アプリケーション内の複数の場所から同一の型のインスタンスのコレクションに要素を追加できるようにする仕組みである。アノテーションプロバイダーのリストを生成するためにMultibinderを用いることで、グローバルな状態に依存せず、かつモジュール作成時にリストを固定しつつも、後から簡単に拡張できる柔軟なアーキテクチャを実現した。

具体的には、AnnotationModule内でMultibinder.newSetBinderを宣言し、@ProvidesIntoSetアノテーションを付与したメソッドを通じて、他のモジュールがそれぞれのアノテーションプロバイダーをこのセットに追加できるようにした。これにより、AnnotationProcessorは、どのアノテーションを使用するか、あるいはそれらがどこから来るのかを知る必要がなくなり、責任の分離が促進された。UserAnnotationProcessingModuleのようなユーザーアノテーション処理モジュールは、AnalyzerModuleのプラグインのように振る舞い、アナライザーがサポートするアノテーション形式を決定できるようになる。例えば、将来的にネットワークからアノテーションをロードする機能が必要になった場合でも、新しいモジュールを追加するだけで容易に拡張可能だ。

次に、長年の開発で蓄積されたレガシーなメインモジュールの改修に取り掛かった。このモジュールはDIコンテナを想定して設計されていなかったため、非常に長い依存関係の連鎖やシングルトンパターンの過剰な使用が見られた。まず、既存のコンポーネント間の関係を図に描き出し、どの部分を分離できるか、DIによって改善できるかを検討した。

DIを導入したことで、コンポーネントが直接必要としない依存関係をコンストラクタ引数で渡す必要がなくなった。また、動的にルールをロードするような機能など、一部のクラスが事実上GuiceのProviderのような役割を担っていた箇所は、DIの仕組みに合わせて置き換えるのが容易だった。

ユーザーアノテーションをロードするメカニズムもMultibinderを使って作成した。ユーザーデータに関わるため、エラー処理は専用のモジュールで行う必要がある。AnnotationModuleはアノテーションの生成や処理方法に集中し、エラーが発生した場合の具体的な対応は、より上位のコアモジュールに委ねることで、責任が明確に分離された。これにより、コアモジュールはCLI引数からアノテーションファイルのパスを読み込んだり、プロジェクト内の特定のディレクトリからJSONファイルを自動ロードしたりするロジックを担い、エラーが発生した際にはアナライザーの警告(V019警告)として報告するといった処理を実行する。このような分離のおかげで、将来的に警告の出力先をレポートやGUIから標準出力に変更するといったことも容易になる。

これらの取り組みの結果、アプリケーションのコードはより理解しやすくなり、コア部分はエンタープライズレベルの標準に準拠した依存性管理によって強化された。新しいモジュールの設計には多くの時間と労力を費やしたが、これは単に動くものを作るだけでなく、将来的な機能拡張や未考慮のシナリオが発生した際に、エラーや開発時間が指数関数的に増加するのを防ぐための重要な投資だった。この改善により、アナライザーはより堅牢で強力なツールへと進化を遂げた。

関連コンテンツ

関連IT用語