結論
以下のコードの実行結果からわかるように、引き継がれる。
@EnableAsync //非同期処理を有効化する @SpringBootApplication public class InheritApplication { public static void main(String[] args) { new SpringApplicationBuilder(InheritApplication.class).web(false).run(args); } @Bean public CommandLineRunner job(AsyncService service) { return i -> { service.exec(); }; } }
@Async //検証のため、抽象クラスにアノテーションをつける public abstract class AsyncService { public abstract void exec(); }
@Slf4j @Service public class AsyncServiceImpl extends AsyncService { @Override public void exec() { // 別スレッドで非同期に実行される場合、実行スレッドがmainスレッドじゃなくなる log.info("-----------------------"); log.info("exec"); log.info("-----------------------"); } }
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.0.2.RELEASE) 2018-06-01 20:03:23.290 INFO 3968 --- [ main] com.example.demo.InheritApplication : Starting InheritApplication on kimura-pc with PID 3968 (C:\Users\pbreh_000\Desktop\study\demo\target\classes started by pbreh_000 in C:\Users\pbreh_000\Desktop\study\demo) 2018-06-01 20:03:23.290 INFO 3968 --- [ main] com.example.demo.InheritApplication : No active profile set, falling back to default profiles: default 2018-06-01 20:03:23.337 INFO 3968 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@57175e74: startup date [Fri Jun 01 20:03:23 JST 2018]; root of context hierarchy 2018-06-01 20:03:24.269 INFO 3968 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2018-06-01 20:03:24.285 INFO 3968 --- [ main] com.example.demo.InheritApplication : Started InheritApplication in 1.235 seconds (JVM running for 1.712) 2018-06-01 20:03:24.285 INFO 3968 --- [ main] .s.a.AnnotationAsyncExecutionInterceptor : No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either 2018-06-01 20:03:24.300 INFO 3968 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl : ----------------------- 2018-06-01 20:03:24.300 INFO 3968 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl : exec 2018-06-01 20:03:24.300 INFO 3968 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl : ----------------------- 2018-06-01 20:03:24.300 INFO 3968 --- [ Thread-5] ...
理由
これだとあまり勉強にならないので、もう少し調べる。
検証環境
> java -version java version "1.8.0_25" Java(TM) SE Runtime Environment (build 1.8.0_25-b18) Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
前提知識
Springの初期化
Springの初期化フェーズは、大まかに以下の流れになっている。
参考 Spring Bean Life Cycle Tutorial
参考 Spring徹底入門
- Bean定義の読み込み
- BFPPの実行
- Bean生成、依存性の解決
- BPPの実行
AOP
参考 SpringでAOP
参考 5. Aspect Oriented Programming with Spring
アノテーションを処理するには
AOPの仕組みが利用される。具体的な処理の実装(Advice)は、主に、MethodInterceptorが利用される。例えば@Transactional
を処理するTransactionInterceptorクラスや@Async
を処理するAsyncExecutionInterceptorクラスといった感じ。今回は、@Async
を処理するAsyncExecutionInterceptorが実行されるまでの流れを調べてみる。
MethodInterceptorが実行されるまでの流れ
1. Configurationを読み込む
@EnableAsync
は以下のようになっている。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AsyncConfigurationSelector.class) public @interface EnableAsync { ...
@Import
で指定されたクラスはImportSelectorを継承したクラス。
参考 ImportSelector Javadoc
ImportSelectorは有効にするConfigurationを条件によって切り替えるためのクラスで、ここではAOPの処理をProxyで実現するかASPECTJで実現するかで、設定を切り替えている。
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> { ... @Override @Nullable public String[] selectImports(AdviceMode adviceMode) { switch (adviceMode) { case PROXY: return new String[] { ProxyAsyncConfiguration.class.getName() }; case ASPECTJ: return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME }; default: return null; } }
したがって、今回はProxyAsyncConfigurationが読み込まれる。
2. BPPを定義する
ProxyAsyncConfigurationクラスで、AsyncAnnotationBeanPostProcessorをBean登録する。
@Configuration @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public AsyncAnnotationBeanPostProcessor asyncAdvisor() { Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected"); AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor(); Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation"); if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) { bpp.setAsyncAnnotationType(customAsyncAnnotation); } if (this.executor != null) { bpp.setExecutor(this.executor); } if (this.exceptionHandler != null) { bpp.setExceptionHandler(this.exceptionHandler); } bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass")); bpp.setOrder(this.enableAsync.<Integer>getNumber("order")); return bpp;
3. BPPを実行する
BPPであるAsyncAnnotationBeanPostProcessorは、postProcessAfterInitializationメソッド内でAsyncAnnotationAdvisorを呼び出す。AsyncAnnotationAdvisorは、以下のようにAdviceとPointcutを持つ。
- Advice
- AnnotationAsyncExecutionInterceptorクラス
- 参考 AnnotationAsyncExecutionInterceptor
- Pointcut
- AnnotationMatchingPointcutクラス
- 参考 AnnotationMatchingPointcut Javadoc
public AsyncAnnotationAdvisor(@Nullable Executor executor, @Nullable AsyncUncaughtExceptionHandler exceptionHandler) { ... this.advice = buildAdvice(executor, this.exceptionHandler); this.pointcut = buildPointcut(asyncAnnotationTypes); } ... protected Advice buildAdvice(@Nullable Executor executor, AsyncUncaughtExceptionHandler exceptionHandler) { return new AnnotationAsyncExecutionInterceptor(executor, exceptionHandler); } ... protected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) { ComposablePointcut result = null; for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) { Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true); Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true); if (result == null) { result = new ComposablePointcut(cpc); } else { result.union(cpc); } result = result.union(mpc); } return (result != null ? result : Pointcut.TRUE); }
ということで、AnnotationMatchingPointcutが実行される仕組みがわかれば、抽象クラスに定義したアノテーションが引き継がれる仕組みもわかりそう。
先ほどのコードの、クラスに対するPointcutをnewしている部分について
Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
javadocには以下のように書かれており、親クラスやインタフェースまで再帰的にアノテーションがついているかを探索してくれることが明文化されている。
参考 AnnotationMatchingPointcut Javadoc
public AnnotationMatchingPointcut(java.lang.Class<? extends java.lang.annotation.Annotation> classAnnotationType, boolean checkInherited) Create a new AnnotationMatchingPointcut for the given annotation type. Parameters: classAnnotationType - the annotation type to look for at the class level checkInherited - whether to also check the superclasses and interfaces as well as meta-annotations for the annotation type
4. AnnotationMatchingPointcut の処理
AnnotationMatchingPointcutは条件に合致するクラスやメソッドをフィルタするが、フィルタ処理自体はAnnotationClassFilterやAnnotationMethodMatcherに処理を委譲している。
AnnotationClassFilterを見ると、アノテーションの検索処理はAnnotationUtilsに委譲していた。
public class AnnotationClassFilter implements ClassFilter { @Override public boolean matches(Class<?> clazz) { return (this.checkInherited ? (AnnotationUtils.findAnnotation(clazz, this.annotationType) != null) : clazz.isAnnotationPresent(this.annotationType)); }
AnnotationUtilsクラスでのアノテーションの検索は、以下のようにClassクラスのgetDeclaredAnnotationsメソッドを利用しながら、親クラスや継承元のアノテーションまで再帰的に探すことで実現していた。
@Nullable private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, Set<Annotation> visited) { try { A annotation = clazz.getDeclaredAnnotation(annotationType); if (annotation != null) { return annotation; } for (Annotation declaredAnn : clazz.getDeclaredAnnotations()) { Class<? extends Annotation> declaredType = declaredAnn.annotationType(); if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) { annotation = findAnnotation(declaredType, annotationType, visited); if (annotation != null) { return annotation; } } } } catch (Throwable ex) { handleIntrospectionFailure(clazz, ex); return null; } for (Class<?> ifc : clazz.getInterfaces()) { A annotation = findAnnotation(ifc, annotationType, visited); if (annotation != null) { return annotation; } } Class<?> superclass = clazz.getSuperclass(); if (superclass == null || Object.class == superclass) { return null; } // 再帰的に親クラスや継承元のアノテーションまで探索する return findAnnotation(superclass, annotationType, visited); }
なぜ再帰的に処理するのか
ClassクラスのgetDeclaredAnnotationsを試してみると
public class Main { public static void main(String[] args) { Stream.of(Children.class.getDeclaredAnnotations()) .forEach(System.out::println); System.out.println("--------------"); Stream.of(Parent.class.getDeclaredAnnotations()) .forEach(System.out::println); } } @Async @Service class Parent{} @Controller class Children extends Parent{}
指定したクラス自体のアノテーションしか取得できない。
@org.springframework.stereotype.Controller(value=) -------------- @org.springframework.scheduling.annotation.Async(value=) @org.springframework.stereotype.Service(value=) Process finished with exit code 0
したがって、親クラスまでたどろうと思うと、再帰的な処理が必要になる。 このとき、親クラスのアノテーションや、アノテーションの継承元のアノテーションを再帰的に探索すると、循環参照による無限ループの危険がある。が、ここはいい感じにAnnotationUtilsが処理してくれている。さすがSpringさん、アタマいい!
5. MethodInterceptor の処理
Pointcutで絞り込んだ箇所で、AsyncExecutionInterceptorが@Async
に関する処理をする。
検証のまとめ
親クラスやインタフェースに定義したアノテーションは(AnnotationMatchingPointcutが使われていれば)、引き継がれる。