SIer だけど技術やりたいブログ

Spring 抽象クラスに設定したアノテーションは引き継がれるか?

結論

以下のコードの実行結果からわかるように、引き継がれる。

@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徹底入門

  1. Bean定義の読み込み
  2. BFPPの実行
  3. Bean生成、依存性の解決
  4. 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を持つ。

   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が使われていれば)、引き継がれる。