0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フレームワークを作りながら、ServiceLoaderとアノテーションプロセッサーを使ってみました

Last updated at Posted at 2024-11-02

はじめに

現在、ドメインごとに依存関係を分離し、命名規則を強力に検査するDIフレームワークを作成しています。このフレームワークを開発する中で、ServiceLoaderAnnotation Processor を使用してみました。そこで、これらが何であるか、そしてどのように使用したかについて書いてみようと思います。

  • 韓国人として、日本語とコンピュータの勉強を同時に行うために、ここに文章を書いています
  • 翻訳ツールの助けを借りて書いた文章なので、誤りがあるかもしれません

状況

現在、私は命名規則を厳格に管理しています。

クラス名がルートドメインではなく、サブドメインに属する場合、必ず守るべき命名規則を定義しました。

基本的には以下のような命名規則が適用されます。

"Service", "Repository", "Controller", "Handler", "Manager"

これにより、次のように記述できます。

 import org.haroya.annotation.Container;

@Container(domain = "login")
public class LoginController {}

ルートドメインとして login を登録した場合、コンパイル時にクラスの命名をチェックし、

LoginService

login がドメインとして登録され、Service が命名規則に登録されている場合は通過し、

LoginUtil

login がドメインとして登録されていても、Util が命名規則に登録されていない場合はコンパイルエラーが発生するようにしたいと考えています。

しかし、以下のように

"Service", "Repository", "Controller", "Handler", "Manager"

このように基本的な命名規則だけを適用すると、開発者の自主性を過度に制限し、拡張性を妨げ、フレームワークにドメインが侵入的になる可能性があると考えました。

なぜランタイムでこの検査を行わないのか。

  1. 物理的に依存関係を分離したくありませんでした。(リソースの無駄、できるだけ軽量にしたい)
  2. 命名規則はコンパイル時にチェックするのが適切だと考えました
  3. アノテーションベースで動作すると、ユーザーがフローを追いにくくなるため、コンパイル時に検査して依存を避けたいと考えました

要件

  • 基本的な命名規則を提供する
  • ユーザーが、基本命名規則に加えて独自の命名規則を追加または削除できるようにする
  • すべてはコンパイル時に行われ、命名規則に違反したクラスはコンパイルエラーを発生させる

ServiceLoader

基本的にJavaのすべてのオブジェクトは実装に基づいて生成されます。

例を挙げると次のようになります。

image.png
次のようにインターフェースを定義し、

image.png

犬と牛のオブジェクトを鳴き声を含む形で再定義しました。

image.png
両方のオブジェクトで実装を定義し、

Animal aniaml = new 動物;

このように定義した結果、それぞれが正しく鳴き声を出すことを確認できました。

image.png

しかし、次のように定義してみてはどうだろうか。

Animal animal = new Animal();

image.png

image.png

しかし、次のように定義したらどうでしょうか?

このように、コンパイルエラーが発生することがわかります。
なぜなら、Javaではコンパイル時に具体的な実装でインスタンス(new)を生成しない場合、
コンパイルエラーが発生するためです。

これをランタイムで決定するようにするために、ServiceLoader を使用することができます。

image.png
次のようにresources/META_INF/services/{パッケージ名} に実装クラスのパスを指定しておくと、

ServiceLoader を通じてランタイムでどの実装クラスを使用するかを動的にロードすることができます。

image.png

次のように LazyLoading 方式で Cow インスタンスがロードされることを確認できます。

image.png
その後、同様に Dog をロードします。

ServiceLoader はインスタンスの生成を LazyLoading で行います。(情報のみを読み込み、実装クラスは必要な時点でロードします)

しかし、私がまとめた必須条件には以下のような条件がありました。

  • すべてはコンパイル時に行われ、命名規則に違反したクラスはコンパイルエラーを発生させること

これはドメインの分離を行うためのものであり、もしランタイムでロードされると、
開発者がエラーに気づくのが難しくなります。

また、この規則がドメインの領域にまで侵入することも望んでいませんでした(DDDの観点から)。

命名や規則がコード領域やランタイム領域に侵入しないようにするためには、
コンパイル条件 という条件を満たす必要がありました。

アノテーションプロセッサの使用

コンパイル時に処理を行うため、アノテーションプロセッサを使用しました。

resources/META-INF/javax.annotation.processing.Processor

上記のように定義すると、Processorとして登録され、javac のコンパイル時にフックとして呼び出されます。

これは round プロセスを経て、前のプロセスでロードされたすべてのオブジェクトを巡回し、コンパイル時に処理を行います。

詳細な javac コンパイルプロセスについては、以下の記事を参考にしてください。

image.png

上記のパスのようにフックするクラスをマッピングし、

image.png

次のようにインターフェースを構成しました。

  • initDomainTypeNames():命名規則に追加できます
  • removeDomainTypeNames():命名規則から削除できます

image.png

次のように基本提供のコンベンションを定義し、

image.png

image.png

次のように ConventionLoaderManager を作成し、順番にローダーを呼び出すようにしました。

image.png

image.png

次のように YAML でコンベンションを作成することもできます。

image.png

次のように、サービスローダーを使用して、ユーザーのJavaコードでもコンパイル時に検査できるようにしました。

image.png

これらすべては、アノテーションプロセッサである DomainNamingConventionProcessor がコンパイル時に実行します。

@SupportedAnnotationTypes("org.haroya.annotation.Container")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class DomainNamingConventionProcessor extends AbstractProcessor {

@SupportedAnnotationTypes:使用するアノテーションを指定します。
@SupportedSourceVersion:対応するJavaのバージョンを定義します。

Testコードで検証しよう

  • 基本的な命名規則を提供する
  • ユーザーが基本命名規則に加え、独自の命名規則を追加または削除できるようにする
  • すべてはコンパイル時に行われ、命名規則に違反したクラスはコンパイルエラーを発生させる

この3つの規則を検証するためのTestコードを作成しますが、JAVAC コンパイラーエラーをどのようにテストするかがわからず調べてみました。

testImplementation 'com.google.testing.compile:compile-testing:0.21.0'

このように、Googleが提供するライブラリを使用してコンパイルテストを行えば、より簡単にテストできるようです。

  • 基本的な命名規則を検査(テスト中の例)
    @Test
    @DisplayName("無効なサフィックスのテスト")
        void invalidSuffixTest() {
            JavaFileObject invalidClass = JavaFileObjects.forSourceString(
                    "test.UserInvalid",
                    """
                            package test;
                            import org.haroya.annotation.Container;
                            @Container
                            public class UserInvalid {}
                            """
            );

            Compilation compilation =
                    Compiler.javac()
                            .withProcessors(new DomainNamingConventionProcessor())
                            .compile(invalidClass);

            CompilationSubject.assertThat(compilation)
                    .hadErrorContaining("must end with one of these suffixes");
        }

次のように JavaFileObjectsCompilation を使用して、javac コンパイルを簡単にテストできました。

@Test
@DisplayName("YAML ベースの命名規則の追加が成功する")
void yamlConventionLoadTest() throws IOException {
    // テスト用の YAML ファイルを src/test/resources に作成
    Path resourcePath = Paths.get("src/test/resources/haroya.yml");
    String yamlContent = """
        naming:
          conventions:
            include:
              - Wow
        """;
    Files.write(resourcePath, yamlContent.getBytes());

    JavaFileObject domainClass = JavaFileObjects.forSourceString(
            "test.UserController",
            """
                    package test;
                    import org.haroya.annotation.Container;
                    @Container(domain = "user")
                    public class UserController {}
                    """
    );

    JavaFileObject testClass = JavaFileObjects.forSourceString(
            "test.UserWow",
            """
                    package test;
                    import org.haroya.annotation.Container;
                    @Container
                    public class UserWow {}
                    """
    );

    Compilation compilation =
            Compiler.javac()
                    .withProcessors(new DomainNamingConventionProcessor())
                    .compile(domainClass, testClass);

    CompilationSubject.assertThat(compilation).succeeded();
}

次のように Wow コンベンションも正常に登録できました。

リファレンス

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?