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

5年目の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 ::        (v1.5.6.RELEASE)
...
2017-08-06 16:02:52.514  INFO 8224 --- [           main] com.example.demo.InheritApplication      : Started InheritApplication in 1.498 seconds (JVM running for 1.811)
// AsyncServiceクラスのexecメソッドの実行スレッドが変わっているため、@Asyncが有効になっている
2017-08-06 16:02:52.519  INFO 8224 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl        : -----------------------
2017-08-06 16:02:52.519  INFO 8224 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl        : exec
2017-08-06 16:02:52.519  INFO 8224 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl        : -----------------------
...

これだとあまり勉強にならないので、もう少し調べる。

アノテーションを処理するには

そもそもアノテーションを処理するには主に、MethodInterceptorという仕組みが使われる。例えば@Transactionalを処理するTransactionInterceptorクラスや@Asyncを処理するAsyncExecutionInterceptorクラスといった感じ。

このMethodInterceptorはAOPのAdviceになっていて、これを実行するタイミングはPointcutで制御される。ざっくりいうと、Adviceが処理したい内容(トランザクション管理するとか、別スレッドで実行するとか、そういうやつ)で、Pointcutは実行されるタイミング(パッケージ配下は実行する、とか、アノテーションついてたら実行する、とかそういうやつ)。

11. Aspect Oriented Programming with Spring

MethodInterceptorを設定する流れ

具体例として、@Asyncを処理するAsyncExecutionInterceptorが設定されるまでの流れを調べる。
バージョンは 1.5.6.RELEASE

まずはConfigurationを読み込む

@EnableAsyncの定義は以下のようになっている。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
...

@Importで指定されたクラスはImportSelectorを継承したクラスで、ImportSelectorは有効にするConfigurationを条件によって切り替えるためのクラスらしい。
ImportSelector (Spring Framework 4.3.10.RELEASE API)

AOPの処理をProxyで実現するかASPECTJで実現するかで、設定を切り替えているっぽい。Proxyの場合はProxyAsyncConfigurationが読み込まれる。

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
    ...
    @Override
    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;
        }
    }

@EnableAsyncから読み込まれるConfiguration内で@Asyncを有効にする設定を書くことで、@EnableAsyncがついているときのみ@Asyncを有効にできる。

BPPを定義する

Springの初期化フェーズは大まかに、Bean定義の読み込み -> BFPPの実行 -> Bean生成、依存性の解決 -> BPPの実行 の流れになっている。
howtodoinjava.com

ここらへんを日本語で詳しく知りたい人は「Spring徹底入門」がわかりやすいですよ!(お金もらってないのにステマ
www.shoeisha.co.jp

今回のBPPはAsyncAnnotationBeanPostProcessorで、このBPPが実行される際にAsyncExecutionInterceptorをAdviceとして埋め込む。

先ほどのImportSelectorによって読み込まれた@Configurationクラス内で、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);
        }
        ...
        return bpp;
     }

BPPを実行する

Bean登録されたAsyncAnnotationBeanPostProcessorは、postProcessAfterInitializationメソッド内でAsyncAnnotationAdvisorを呼び出す。

このAsyncAnnotationAdvisorは、AdviceとしてAnnotationAsyncExecutionInterceptorクラスを、PointcutとしてAnnotationMatchingPointcutクラスを用いる。

特にPointcutについて抜粋する。

    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 = AnnotationMatchingPointcut.forMethodAnnotation(asyncAnnotationType);
            if (result == null) {
                result = new ComposablePointcut(cpc);
            }
            else {
                result.union(cpc);
            }
            result = result.union(mpc);
        }
        return result;
    }

PointcutとしてAnnotationMatchingPointcutが利用されている。このクラスのJavadocには以下のように書かれている。

Simple Pointcut that looks for a specific Java 5 annotation being present on a class or method.

AnnotationMatchingPointcut (Spring Framework 4.3.10.RELEASE API)


ということで、AnnotationMatchingPointcutが実行される仕組みがわかれば、抽象クラスに定義したアノテーションが引き継がれた仕組みもわかりそう。

先ほどのコードの、クラスに対するPointcutをnewしている部分について

    Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);

javadocには以下のように書かれている。
AnnotationMatchingPointcut (Spring Framework 4.3.10.RELEASE API)

public AnnotationMatchingPointcut(Class 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 explicitly check the superclasses and interfaces for the annotation type as well (even if the annotation type is not marked as inherited itself)

継承先のクラスまでアノテーションがついているかを探索してくれるっぽい。

なるほど。じゃあ抽象クラスに定義したアノテーションも引き継がれるな!安心した!

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メソッドを利用しながら、親クラスや継承元のアノテーションまで再帰的に探すことで実現していた。

  private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, Set<Annotation> visited) {
      try {
          Annotation[] anns = clazz.getDeclaredAnnotations();
          for (Annotation ann : anns) {
              if (ann.annotationType() == annotationType) {
                  return (A) ann;
              }
          }
          for (Annotation ann : anns) {
              if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
                  A annotation = findAnnotation(ann.annotationType(), annotationType, visited);
                  if (annotation != null) {
                      return annotation;
                  }
              }
          }
            // 再帰的に親クラスや継承元のアノテーションまで探索する

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=)

親クラスのアノテーションや、アノテーションの継承元のアノテーション再帰的に探索すると循環参照による無限ループの危険があるはずだが、いい感じにAnnotationUtilsは処理してくれる。さすがSpringさん、アタマいい。

@EnableAsyncの雑学

コードリーディングしてる最中に知らなかった機能に気づいたのでメモる。

@EnableAsyncのannotation属性に指定すれば、@Async以外のアノテーションも非同期のためのアノテーションに指定できる。

@EnableAsync(annotation = MyAsync.class)
@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();
        };
    }
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAsync {
}
@MyAsync //独自アノテーションをつける
public abstract class AsyncService {
    public abstract void exec();
}

結論

抽象クラスに定義したアノテーションは引き継がれる。(少なくともPointcutとしてAnnotationMatchingPointcutが使われていれば)