結論
以下のコードの実行結果からわかるように、引き継がれる。
@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が使われていれば)、引き継がれる。