Spring Bootで コンポーネント外からコンポーネントを取得したかった
   5 min read

動機

CDI で言うところの CDI.current().getBeans(...) とか CDI.current().select(...)とかそんな感じのことをやりたかった。

参考リファレンス

ドキュメントでは、やりたいことが書いてあるように見えたのはこのセクション

だったので、この記述に従って実装してみることにした。

環境

  • Spring Boot 2.1.5
  • Java11
  • Lombok 使用

ゴール

次のようなコードを動かしたい。最終的なインジェクションのやり方はともかく、 new MyPojo() で生成したインスタンス(つまり非 Spring コンポーネント)で、Spring コンポーネントである MyComponent を使いたい。

d7436a7cd8f4d2ef49707c21b488bdadd9e5fac1:

@SpringBootApplication
public class AspectjApplication implements CommandLineRunner {

    public static void main(final String[] args) {
        SpringApplication.run(AspectjApplication.class, args);
    }

    @Override
    public void run(final String... args) throws Exception {
        System.out.println(new MyPojo().getText());
    }

    public static class MyPojo {
        @Autowired
        private MyComponent myComponent;

        @Getter
        @Setter
        private String greetingText = "Hello, ";

        public String getText() {
            return getGreetingText() + myComponent.getName();
        }
    }

    @Component
    public static class MyComponent {
        public String getName() {
            return this.getClass().getSimpleName();
        }
    }
}

なお、このまま実行すると、 MyPojoクラスのmyComponentnull のままなので、NPE が発生する。

作業ログ

結果のソース:

@EnableSpringConfigured @Configurable 付与

前述 Spring Framework リファレンスに記載されている通り、アノテーションを付与した。

494691a12b1ae303f3d51caa08b83ccb85923b9e:

+@EnableSpringConfigured
 @SpringBootApplication
 public class AspectjApplication implements CommandLineRunner {

@@ -20,6 +23,7 @@ public class AspectjApplication implements CommandLineRunner {
         System.out.println(new MyPojo().getText());
     }

+    @Configurable
     public static class MyPojo {
         @Autowired
         private MyComponent myComponent;

依存関係追加

同じくリファレンスに記載されている通り pom.xmlspring-aspects を依存関係に追加した。
a0b2455bb5b4e5d0937899de20180a482475a023:

+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-aspects</artifactId>
+    </dependency>

AspectJ アノテーションプロセッシング

おそらく上で追加したアノテーションをコンパイル時に何かするのだろう、と探したところ aspectj-maven-pluginというものがあったので usage の通り pom.xml へ追記した。

fc552678d14a5c01f7ea33b6df09453823456510:

   <build>
     <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>aspectj-maven-plugin</artifactId>
+        <version>1.11</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compile</goal>       <!-- use this goal to weave all your main classes -->
+              <goal>test-compile</goal>  <!-- use this goal to weave all your test classes -->
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
       <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>

この状態でコンパイルを実行すると次のエラーが発生した:

[ERROR] Failed to execute goal org.codehaus.mojo:aspectj-maven-plugin:1.11:compile (default) on project aspectj: Execution default of goal org.codehaus.mojo:aspectj-maven-plugin:1.11:compile failed: Plugin org.codehaus.mojo:aspectj-maven-plugin:1.11 or one of its dependencies could not be resolved: Could not find artifact com.sun🛠jar:11.0.2 at specified path /home/yuki/.sdkman/candidates/java/11.0.2-open/../lib/tools.jar

aspectj-maven-plugin の Java11 対応

当 plugin の GitHub Issues/PR のページを見てみると、Java11 に対して未対応のようだった。
対応した fork version を作成されている方がいたのでこれを用いることとした。

また、AJC(AspectJ Compiler の略か?)にターゲット Java バージョンを明示する必要があったので(デフォルトだと 1.4 だと解釈するようだ)、その対応も行った。

21a76a79d9..3f0abe043:

   <build>
     <plugins>
       <plugin>
-        <groupId>org.codehaus.mojo</groupId>
+        <groupId>com.nickwongdev</groupId>
         <artifactId>aspectj-maven-plugin</artifactId>
-        <version>1.11</version>
+        <version>1.12.1</version>
         <executions>
           <execution>
             <goals>
@@ -61,6 +61,11 @@
             </goals>
           </execution>
         </executions>
+        <configuration>
+          <source>${java.version}</source>
+          <target>${java.version}</target>
+          <complianceLevel>${java.version}</complianceLevel>
+        </configuration>
       </plugin>
       <plugin>
         <groupId>org.springframework.boot</groupId>

Lombok との組み合わせ対応: .java に対する AspectJ 適用スキップ

ここまでの状態でコンパイルを実行すると次のエラーとなる:

[ERROR] Failed to execute goal com.nickwongdev:aspectj-maven-plugin:1.12.1:compile (default) on project aspectj: AJC compiler errors:
[ERROR] error at return getGreetingText() + myComponent.getName();
[ERROR]        ^^
[ERROR] /home/yuki/Documents/repos/java/hello-java/spring/aspectj/src/main/java/com/github/yukihane/spring/aspectj/AspectjApplication.java:36:0::0 The method getGreetingText() is undefined for the type AspectjApplication.MyPojo

ここで指摘されている getGreetingText() メソッドは、Lombok の @Getter アノテーションによって生成されるはずのメソッドだ。 AJC compiler はこれが見えないと言っている。
当然だ。.java上には存在しない。

ググると対策が提示されていた。

ただしなぜこれで上手く行くのか説明は無いので自分なりの解釈をここに書いておく。

AspectJ が weaving を行い得るタイミングは 3 つあるらしい。このうち今回関係しているのは、コンパイル時の話なので、最初の 2 つだ。
Chapter 5. Load-Time Weaving - The AspectJ Development Environment Guide:

  • Compile-time weaving is the simplest approach. When you have the source code for an application, ajc will compile from source and produce woven class files as output. The invocation of the weaver is integral to the ajc compilation process. The aspects themselves may be in source or binary form. If the aspects are required for the affected classes to compile, then you must weave at compile-time. Aspects are required, e.g., when they add members to a class and other classes being compiled reference the added members.
  • Post-compile weaving (also sometimes called binary weaving) is used to weave existing class files and JAR files. As with compile-time weaving, the aspects used for weaving may be in source or binary form, and may themselves be woven by aspects.
  • Load-time weaving (LTW) is simply binary weaving defered until the point that a class loader loads a class file and defines the class to the JVM. To support this, one or more “weaving class loaders”, either provided explicitly by the run-time environment or enabled through a “weaving agent” are required.

リンク先 Stack Overflow の回答で行っているのは、compile-time weaving をスキップすることで Lombok が getter を生成する前の.javaファイルを AJC が見ることを回避し、ただしスキップしたが.classに対する post-compile weaving は強制している、ということなのだろう。

差分は長い割に回答リンク先と変わらないので記載省略。
リンク: 4ca1f64cc4174bb3c41932f6a08b3997779a782a

weaving 時の問題対処

さて、AspectJ 適用をコンパイル後に先送りしてしまったのでここからはmvn clean compileでなくmvn clean process-classesを実行する必要がある。
早速実行してみると新しいエラーが出る。

[ERROR] Failed to execute goal com.nickwongdev:aspectj-maven-plugin:1.12.1:compile (default-compile) on project aspectj: AJC compiler errors:
[ERROR] error can't determine superclass of missing type org.springframework.transaction.interceptor.TransactionAspectSupport
[ERROR] when batch building BuildConfig[null] #Files=0 AopXmls=#0
[ERROR]  [Xlint:cantFindType]

これもググったら回答があった:

ただし最も upvoted されている回答は何を言っているのかさっぱり理解できない。

何にせよ今回トランザクションに関わることは行っていないし、所詮は lint のメッセージなのでXlint オプションでエラーレベルを下げて放置することにした。

62ff366f3755cfd882b15bf6b2b8a4b49807b065:

               <weaveDirectories>
                 <weaveDirectory>${project.build.directory}/classes</weaveDirectory>
               </weaveDirectories>
+              <Xlint>warning</Xlint>
             </configuration>

警告メッセージ対応

ここまでで mvn clean process-classes は正常終了するようになった。ただし、いくつかの warning が残っているのでそれらを対処した。

couldn’t find aspectjrt.jar on classpath

aspectjrtを依存関係に追加した。

5311573ab6b8dd45cf921e9d234e9e4fdd51e3a7:

       <groupId>org.springframework</groupId>
       <artifactId>spring-aspects</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.aspectj</groupId>
+      <artifactId>aspectjrt</artifactId>
+    </dependency>

     <dependency>
       <groupId>org.springframework.boot</groupId>

bad version number found in /home/yuki/.m2/repository/org/aspectj/aspectjrt/1.9.4/aspectjrt-1.9.4.jar expected 1.9.2 found 1.9.4

aspectj-maven-plugin が利用すべきバージョンを明示した。

e63eb7ce811d4081eded526d1310bcea33532c09:

             </aspectLibrary>
           </aspectLibraries>
         </configuration>
+        <dependencies>
+          <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjtools</artifactId>
+            <version>1.9.4</version>
+          </dependency>
+        </dependencies>
       </plugin>
       <plugin>
         <groupId>org.springframework.boot</groupId>

実行

mvn clean spring-boot:run

で所望の結果が得られた:

Hello, MyComponent

結果的に、ソースコードは当初の想定通りで、@Autowiredにコンポーネントがインジェクションされるような形で扱えた。

おまけ

Eclipse IDE の対応

pom.xmlaspectj-maven-plugin(fork でなくオリジナルの方)を設定すると m2e プラグインだったり AJDT だったり?をインストールしてくれようとするのだが、AJDT のリンクが死んでいるようでインストール全体が失敗する。
AspectJ プロジェクトページからの AJDT リンクも死んでいる。

結果、Eclipse IDE が実行するコンパイルでは weaving されない(ので別途mvn process-classesを実行する必要がある)。

顧客が本当に欲しかったもの


@Component
public class ApplicationContextProvider implements ApplicationContextAware {

    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    private static class Holder {
        private static final Holder SINGLETON = new Holder();

        private ApplicationContext applicationContext;
    }

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
        Holder.SINGLETON.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return Holder.SINGLETON.applicationContext;
    }
}

Spring get current ApplicationContext - Stack Overflow の回答コードを参考にしたんだけど、原文がインナークラス 2 つも使っている意味がわからない。