Spring 框架中的 @Enable* 注解是怎样实现的?

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 概述Spring 的项目中,我们经常会使用 @Enable 开头的注解到配置类中,添加了这种注解之后,便会开启一些功能特性。常用的注解如 @EnableWebMvc、@EnableTransactionManagement、@EnableAsync、@EnableScheduling 等等。

概述


Spring 的项目中,我们经常会使用 @Enable 开头的注解到配置类中,添加了这种注解之后,便会开启一些功能特性。常用的注解如 @EnableWebMvc、@EnableTransactionManagement、@EnableAsync、@EnableScheduling 等等。集成到 Spring 的第三方的框架中,我们也经常会看到这样的注解,如阿里巴巴开源的缓存框架 jetcache,便需要使用注解 @EnableCreateCacheAnnotation 开启缓存能力。本篇将尝试对其实现进行分析,并实现一个自己的 @Enable* 注解。


@Enable* 注解实现分析


以 @EnableScheduling 注解为例,其源码如下。


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}


除了最常用的 Java 提供的元注解,这个 @EnableScheduling 使用了 @Import 注解,很明显 @Enable* 就是利用 @Import 注解实现的。查看 @Import 源码如下。


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
  /**
   * {@link Configuration @Configuration}, {@link ImportSelector},
   * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
   */
  Class<?>[] value();
}


@Import 可以标注在类型上,只有一个类型为 Class 的 value 属性,根据注释我们可以看到,value 属性值可以是 @Configuration 配置类、ImportSelector、ImportBeanDefinitionRegistrar 或者常规组件类。Spring 读取到这几个类型时的处理如下。


@Configuration 配置类:Spring 对配置类进行解析。

ImportSelector:实例化该接口实现类,提供元数据给接口中的方法,将返回的类型作为配置类进行解析。

ImportBeanDefinitionRegistrar:实例化该类型,提供元数据给接口中的方法,由用户自己控制注入哪些 bean。

常规类:将该类作为 bean 进行注入。

value 值为配置类的时候 Spring 会对配置类进行解析,值为常规组件类的时候 Spring 会把组件类作为 Spring 的 bean 进行注入,这两个相对比较容易理解。下面对剩下的两个类型进行分析。


ImportSelector 定义

ImportSelector 接口定义如下。


public interface ImportSelector {
  /**
   * 选择并返回哪些类应该被导入,导入的类将作为配置类再次进行解析
   * AnnotationMetadata 是 @Configuration 配置类的元数据
   */
  String[] selectImports(AnnotationMetadata importingClassMetadata);
  /**
   * 排除导入的类
   * @since 5.2.4
   */
  @Nullable
  default Predicate<String> getExclusionFilter() {
    return null;
  }
}


ImportSelector#selectImports 方法中的参数 AnnotationMetadata 是使用 @Import 注解的配置类的元数据,返回值将作为配置类再次进行解析。如果返回值是实现 BeanFactoryPostProcessor 或者 BeanPostProcessor 接口的组件,我们将有机会在 Spring 应用上下文或 bean 的生命周期中做一些其他的操作。如果返回值是配置类,我们还能根据 @Conditional 注解根据条件注入一些 bean 。@Conditional 注解的使用见 Spring 条件注解 @Conditional 使用及其底层实现 。


ImportSelector#getExclusionFilter 方法则用于从导入的类中排除我们不想导入的类。


ImportBeanDefinitionRegistrar 定义

ImportBeanDefinitionRegistrar 定义如下。


public interface ImportBeanDefinitionRegistrar {
  // 注册 BeanDefinition
  default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
      BeanNameGenerator importBeanNameGenerator) {
    registerBeanDefinitions(importingClassMetadata, registry);
  }
  // 注册 BeanDefinition
  default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  }
}


相比 ImportSelector 只能获取配置类的元数据,ImportBeanDefinitionRegistrar 接口提供了更为灵活的方法,还能收到参数 BeanDefinitionRegistry、BeanNameGenerator 。 BeanDefinitionRegistry 可以方便我们获取已注册的 BeanDefinition,注册自己定义的 BeanDefinition。BeanNameGenerator 则用于生成 bean 的名称。


@Import 实现分析


通过上面的描述,我们知道 @Enable* 注解是通过 @Import 实现的,而 @Import 的处理在 Spring 中的配置类解析过程中。解析 @Import 注解的核心代码位置为 ConfigurationClassParser#processImports,源码如下。


  /**
   * 处理 @Import 注解
   *
   * @param configClass             标注或元标注 @Import 的配置类
   * @param currentSourceClass      配置类对应的 SourceClass
   * @param importCandidates        @Import 注解 value 属性值集合或 ImportSelector.selectImports 返回值
   * @param exclusionFilter         不要处理配置类的过滤器
   * @param checkForCircularImports 是否检查循环 @Import
   */
  private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
      Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
      boolean checkForCircularImports) {
    if (importCandidates.isEmpty()) {
      return;
    }
    if (checkForCircularImports && isChainedImportOnStack(configClass)) {
      this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
    } else {
      this.importStack.push(configClass);
      try {
        for (SourceClass candidate : importCandidates) {
          // @Import value 属性值是 ImportSelector.class
          if (candidate.isAssignable(ImportSelector.class)) {
            // Candidate class is an ImportSelector -> delegate to it to determine imports
            Class<?> candidateClass = candidate.loadClass();
            ImportSelector selector = ParserStrategyUtils
                .instantiateClass(candidateClass, ImportSelector.class,
                    this.environment, this.resourceLoader, this.registry);
            Predicate<String> selectorFilter = selector.getExclusionFilter();
            if (selectorFilter != null) {
              exclusionFilter = exclusionFilter.or(selectorFilter);
            }
            if (selector instanceof DeferredImportSelector) {
              // @Import value 属性值是 DeferredImportSelector.class
              this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
            } else {
              String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
              Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames,
                  exclusionFilter);
              // 递归处理 ImportSelector 返回的配置类
              processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter,
                  false);
            }
          } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
            // Candidate class is an ImportBeanDefinitionRegistrar ->
            // delegate to it to register additional bean definitions
            Class<?> candidateClass = candidate.loadClass();
            ImportBeanDefinitionRegistrar registrar =
                ParserStrategyUtils
                    .instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                        this.environment, this.resourceLoader, this.registry);
            configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
          } else {
            // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
            // process it as an @Configuration class
            this.importStack.registerImport(
                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
            processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
          }
        }
      } catch (BeanDefinitionStoreException ex) {
        throw ex;
      } catch (Throwable ex) {
        throw new BeanDefinitionStoreException(
            "Failed to process import candidates for configuration class [" +
                configClass.getMetadata().getClassName() + "]", ex);
      } finally {
        this.importStack.pop();
      }
    }
  }


processImports 方法整体结构较为清晰,对 @Import 注解的 value 属性值进行循环处理。


首先判断是否为 ImportSelector 类型,如果是则 调用 ImportSelector#selectImports 方法,并将取到的类型作为配置类,进行递归解析 @Import。

否则如果类型为 ImportBeanDefinitionRegistrar,则对其实例化,并添加到 importBeanDefinitionRegistrars 中,后面将统一进行处理。

否则将导入的类作为 bean 进行注册,然后作为配置类递归进行处理。


实现自己的 @Enable* 注解


下面尝试实现一个 @EnableHelloWorld ,配置类上使用了该注解后将自动注入一个 HelloWorld 的 bean。代码如下。


public class HelloWorld {
    public void sayHello(){
        System.out.println("hello,world");
    }
}
public class HelloWorldSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{HelloWorld.class.getName()};
    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(HelloWorldSelector.class)
public @interface EnableHelloWorld {
}
@EnableHelloWorld
@Configuration
public class Application {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.scan("com.zzuhkp");
        context.refresh();
        context.getBean(HelloWorld.class).sayHello();
    }
}


程序执行后成功打印出“hello,world”,表明自定义 @Enable* 成功生效。


总结


@Enable* 注解通过 @Import 注解实现。@Import 注解在配置类解析过程中进行处理。通过 @Import 可以方便的注入自定义的 bean 。

目录
相关文章
|
4天前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
|
4天前
|
Java 数据安全/隐私保护 Spring
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
|
1天前
|
安全 前端开发 Java
随着企业应用复杂度提升,Java Spring框架以其强大与灵活特性简化开发流程,成为构建高效、可维护应用的理想选择
随着企业应用复杂度提升,Java Spring框架以其强大与灵活特性简化开发流程,成为构建高效、可维护应用的理想选择。依赖注入使对象管理交由Spring容器处理,实现低耦合高内聚;AOP则分离横切关注点如事务管理,增强代码模块化。Spring还提供MVC、Data、Security等模块满足多样需求,并通过Spring Boot简化配置与部署,加速微服务架构构建。掌握这些核心概念与工具,开发者能更从容应对挑战,打造卓越应用。
7 1
|
4天前
|
运维 Java Nacos
Spring Cloud应用框架:Nacos作为服务注册中心和配置中心
Spring Cloud应用框架:Nacos作为服务注册中心和配置中心
|
4天前
|
XML Java Maven
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
这篇文章是Spring5框架的入门到实战教程,介绍了Spring5的新功能——整合日志框架Log4j2,包括Spring5对日志框架的通用封装、如何在项目中引入Log4j2、编写Log4j2的XML配置文件,并通过测试类展示了如何使用Log4j2进行日志记录。
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
|
4天前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
|
4天前
|
XML 数据库 数据格式
Spring5入门到实战------14、完全注解开发形式 ----JdbcTemplate操作数据库(增删改查、批量增删改)。具体代码+讲解 【终结篇】
这篇文章是Spring5框架的实战教程的终结篇,介绍了如何使用注解而非XML配置文件来实现JdbcTemplate的数据库操作,包括增删改查和批量操作,通过创建配置类来注入数据库连接池和JdbcTemplate对象,并展示了完全注解开发形式的项目结构和代码实现。
Spring5入门到实战------14、完全注解开发形式 ----JdbcTemplate操作数据库(增删改查、批量增删改)。具体代码+讲解 【终结篇】
|
4天前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式
|
4天前
|
Java API Spring
Spring5入门到实战------1、Spring5框架概述、入门案例
这篇文章是Spring5框架的入门教程,概述了Spring框架的核心概念和特点,并通过一个创建普通Java类的案例,详细演示了从下载Spring核心Jar包、创建配置文件、编写测试代码到运行测试结果的完整流程,涵盖了Spring IOC容器的使用和依赖注入的基本用法。
Spring5入门到实战------1、Spring5框架概述、入门案例
|
4天前
|
设计模式 Java 测试技术
公司为何禁止在SpringBoot中使用@Autowired注解?
【8月更文挑战第15天】在Spring Boot的广泛应用中,@Autowired注解作为依赖注入的核心机制之一,极大地简化了Bean之间的装配过程。然而,在某些企业环境下,我们可能会遇到公司政策明确禁止或限制使用@Autowired注解的情况。这一决策背后,往往蕴含着对代码质量、可维护性、测试便利性以及团队开发效率等多方面的考量。以下将从几个方面深入探讨这一决定的合理性及替代方案。
12 0