動機
CDI で言うところの CDI.current().getBeans(...)
とか CDI.current().select(...)
とかそんな感じのことをやりたかった。
参考リファレンス
ドキュメントでは、やりたいことが書いてあるように見えたのはこのセクション
- 5.10.1. Using AspectJ to Dependency Inject Domain Objects with Spring - Spring Framework Core リファレンス
だったので、この記述に従って実装してみることにした。
環境
- 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
クラスのmyComponent
は null
のままなので、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.xml
へ spring-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 だと解釈するようだ)、その対応も行った。
<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.xml
に aspectj-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 つも使っている意味がわからない。