【小家Spring】Spring中@PropertySource和@ImportResource的区别,以及各自的实现原理解析(上)

简介: 【小家Spring】Spring中@PropertySource和@ImportResource的区别,以及各自的实现原理解析(上)

前言


@PropertySource和@ImportResource或许很多人都用过,并且都还没有用错。但是若真把他俩拿过来一起的时候,却有点傻傻分不清楚了。


是的,他俩都是向容器中导入Bean/属性信息,但是使用起来还是有很大的区别的,因此本文主要针对于他俩的区别,顺便从一定的原理的角度做一个解释,希望大家以后能区分开来。


在讲解之前,可以记住一个通用的的结论:


@PropertySource用于导入.properties的属性配置文件(能导入yaml吗,且继续往下看吧)

@ImportResource用于导入.xml的Bean信息的配置文件(能导入,properties吗,且继续看~)


@ImportResource


指示包含要导入的bean定义的一个或多个资源。它的功能比较像@Import注解,就是向容器内导入Bean。只是@ImportResource它导入的是一个xml配置文件,然后通过解析xml文件的方式再把解析好的Bean信息导入到Spring容器内。


我个人认为:这个注解它是Spring拿出来的一个过渡性产品,因为Spring3.0推荐使用全注解驱动后,所有的Bean都完全可以用注解来代替了。而Spring提供这个注解主要是为了向下兼容,便于老项目进行迁移。

其实使用XML是一种非常不好的选择,Java工程师就应该着眼于java应用上,而不是一会schema,一会DTD之类的


当然既然Spring提供了这个功能,有的时候还是非常有用的。比如当DUBBO还没有跟上注解只能使用xml的时候,这个导入注解就能发挥非常重要的作用了~


使用Demo


比如我在classpath下有这个xml文件:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="myPerson" class="com.fsx.bean.Person">
        <property name="name" value="fsx"/>
        <property name="age" value="18"/>
    </bean>
</beans>


在配置类上导入此资源:


@Configuration
@ImportResource(locations = "classpath:spring-beans.xml")
public class RootConfig {
}


单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class})
public class TestSpringBean {
    @Autowired
    private ApplicationContext applicationContext;
    @Test
    public void test1() {
        Object myPerson = applicationContext.getBean("myPerson");
        System.out.println(myPerson); // Person{name='fsx', age=18}
    }
}


这个myPerson这个Bean能够被我正常获取到。

那么它能够导入非xml文件吗???其实这个待我解释完它的原理后,这个问题就不攻自破了~


实现原理剖析


解析配置类、Bean定义的前部分原理这里就不在叙述了,还不太清楚的建议参见博文:

【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)


下面我们直接定位到解析@ImportResource注解的源码处:

class ConfigurationClassParser {
  ...
  @Nullable
  protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
      throws IOException {
      //1、解析嵌套内部类
      //2、解析@PropertySource  === 这是下面的内容 ====
    // 相当于拿到所有的PropertySource注解,注意PropertySources属于重复注解的范畴~~~
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
        sourceClass.getMetadata(), PropertySources.class,
        org.springframework.context.annotation.PropertySource.class)) {
      // 这个判断目前来说是个恒等式~~~  所以的内置实现都是子接口ConfigurableEnvironment的实现类~~~~
      // processPropertySource:这个方法只真正解析这个注解的地方~~~
      if (this.environment instanceof ConfigurableEnvironment) {
        processPropertySource(propertySource);
      } else {
        logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
            "]. Reason: Environment must implement ConfigurableEnvironment");
      }
    }
      //3、解析@ComponentScan
      //4、解析@Import
      //5、解析@ImportResource 
    //拿到这个注解~~~~~~~~~~~
    AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    if (importResource != null) {
      String[] resources = importResource.getStringArray("locations");
      // readerClass 这个在自定义规则也是非常重要的一块内容~~~~~
      Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
      for (String resource : resources) {
        // 显然它还支持${}这种方式去环境变量里取值的~~~比如spring-beans-${profie}.xml等
        String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
        // 此处仅仅是吧注解解析掉,然后作为属性添加到configClass里面去,还并不是它真正的执行时机~~~~~
        configClass.addImportedResource(resolvedResource, readerClass);
      }
    }
      //6、解析@Bean
      //7、解析接口default方法~~~ 也可以用@Bean标注
      //8、解析super class父类
  }
}


上面分析了,真正解析这个文件,然后把Bean定义加入到容器的行为:


class ConfigurationClassBeanDefinitionReader {
  // 从ConfigurationClass里面真正的加载Bean定义信息~~~
  private void loadBeanDefinitionsForConfigurationClass(
      ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    if (trackedConditionEvaluator.shouldSkip(configClass)) {
      String beanName = configClass.getBeanName();
      if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
        this.registry.removeBeanDefinition(beanName);
      }
      this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
      return;
    }
    // 如果这个配置类是被@Import的,那就第一个执行了~~~
    if (configClass.isImported()) {
      registerBeanDefinitionForImportedConfigurationClass(configClass);
    }
    // 加载标注了@Bean的~~
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
      loadBeanDefinitionsForBeanMethod(beanMethod);
    }
    //这个是我们今天关心的:解析@ImportedResource里面具体的Bean定义信息~~~
    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
  }
  private void loadBeanDefinitionsFromImportedResources(Map<String, Class<? extends BeanDefinitionReader>> importedResources) {
    Map<Class<?>, BeanDefinitionReader> readerInstanceCache = new HashMap<>();
    // 因为可以导入多个资源  所以这里遍历
    importedResources.forEach((resource, readerClass) -> {
      // Default reader selection necessary?
      // 从这里能够看出来,若我们自己没有指定BeanDefinitionReader,那它最终默认会采用XmlBeanDefinitionReader
      // ~~~~~这就是为什么默认情况下,只支持导入xml文件的原因~~~~~
      if (BeanDefinitionReader.class == readerClass) {
        if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) {
          // When clearly asking for Groovy, that's what they'll get...
          readerClass = GroovyBeanDefinitionReader.class;
        }
        else {
          // Primarily ".xml" files but for any other extension as well
          readerClass = XmlBeanDefinitionReader.class;
        }
      }
      BeanDefinitionReader reader = readerInstanceCache.get(readerClass);
      if (reader == null) {
        try {
          // Instantiate the specified BeanDefinitionReader
          // 拿到入有一个入参为BeanDefinitionRegistry的构造函数~~
          reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry);
          // Delegate the current ResourceLoader to it if possible
          if (reader instanceof AbstractBeanDefinitionReader) {
            AbstractBeanDefinitionReader abdr = ((AbstractBeanDefinitionReader) reader);
            abdr.setResourceLoader(this.resourceLoader);
            abdr.setEnvironment(this.environment);
          }
          readerInstanceCache.put(readerClass, reader);
        } catch (Throwable ex) {
          throw new IllegalStateException(
              "Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]");
        }
      }
      // TODO SPR-6310: qualify relative path locations as done in AbstractContextLoader.modifyLocations
      // 处理classpath:spring-beans.xml这种资源加载进来~~
      // 最终委托给的是`PathMatchingResourcePatternResolver`来加载这个资源,所以支持classpath*  也支持ant风格的通配符
      reader.loadBeanDefinitions(resource);
    });
  }
}


这样这个xml就会被解析完成,里面所有定义的Bean的定义信息就会被加载进容器里。


从源码中可以看出:默认情况下只支持导入xml格式的文件,并且要求遵循spring-beans.xsd。除非你在注解里可以自定义BeanDefinitionReader。它内置有三个实现类:

  1. PropertiesBeanDefinitionReader:一种简单的属性文件格式的bean definition解析器,提供以Map/Properties类型ResourceBundle类型定义的bean的注册方法。

 employee.(class)=MyClass       // bean is of class MyClass
 employee.(abstract)=true       // this bean can't be instantiated directly
 employee.group=Insurance       // real property
 employee.usesDialUp=false      // real property (potentially overridden)
 salesrep.(parent)=employee     // derives from "employee" bean definition
 salesrep.(lazy-init)=true      // lazily initialize this singleton bean
 salesrep.manager(ref)=tony     // reference to another bean
 salesrep.department=Sales      // real property
 techie.(parent)=employee       // derives from "employee" bean definition
 techie.(scope)=prototype       // bean is a prototype (not a shared instance)
 techie.manager(ref)=jeff       // reference to another bean
 techie.department=Engineering  // real property
 techie.usesDialUp=true         // real property (overriding parent value)
 ceo.$0(ref)=secretary          // inject 'secretary' bean as 0th constructor arg
 ceo.$1=1000000                 // inject value '1000000' at 1st constructor arg


这种方式我想说,其实我不想说什么~~~~~尴尬

2. GroovyBeanDefinitionReader:略

3. XmlBeanDefinitionReader:读取bean definition属性通过特定的xml文件。这个解析器在基于xml配置时候使用得非常之多,只是最终输给了时间几近被淘汰,此处也不用再举例了。


当然若都不满足你,你可以自己实现一个。(我相信99.99%都是没有必要的吧)。

需要特别注意的是:AnnotatedBeanDefinitionReader在基于注解的Spring项目中使用非常多,但它并不是BeanDefinitionReader的子类。它一般和ClassPathBeanDefinitionScanner一起使用


@ImportResource注解解释



// @since 3.0
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) // 它只能标注在类上
@Documented
public @interface ImportResource {
  // 路径支持${}这样动态取值~~~~   也支持ant风格的匹配  classpath*也是木有问题的
  @AliasFor("locations")
  String[] value() default {};
  @AliasFor("value")
  String[] locations() default {};
  // 上面说了,一般都不需要自定义,因为一般情况下我们都只会导入xml文件
  Class<? extends BeanDefinitionReader> reader() default BeanDefinitionReader.class;
}


需要特别注意的是,后缀名此处其实无所谓。比如你命名为spring-beans.txt也是没有问题的,但是需要保证里面的内容是xml格式的且遵循Spring Bean的schema:spring-beans.xsd就成~~ 这是需要注意的一点


关联知识

ConfigurationClassUtils里有这么一段代码:


abstract class ConfigurationClassUtils {
  static {
    candidateIndicators.add(Component.class.getName());
    candidateIndicators.add(ComponentScan.class.getName());
    candidateIndicators.add(Import.class.getName());
    candidateIndicators.add(ImportResource.class.getName());
  } 
}


可以看出标注为@ImportResource注解的Bean也会当作成一个配置类,只不过该配置类是Lite模式而已。关于什么叫Full模式什么叫Lite模式,他们有什么区别?请参考:

【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)

相关文章
|
4天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
15 0
|
1天前
|
安全 索引
【集合】03 Linkedlist原理深入解析
【集合】03 Linkedlist原理深入解析
6 0
|
1天前
|
Java Spring 容器
SpringBoot自动装配原理之@Import注解解析
SpringBoot自动装配原理之@Import注解解析
|
1天前
|
canal 缓存 关系型数据库
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
|
1天前
|
XML 人工智能 Java
Spring Bean名称生成规则(含源码解析、自定义Spring Bean名称方式)
Spring Bean名称生成规则(含源码解析、自定义Spring Bean名称方式)
|
2天前
|
安全 Java API
Spring工厂API与原理
Spring工厂API与原理
23 10
|
3天前
|
缓存 JavaScript 前端开发
|
4天前
|
SQL 分布式计算 资源调度
一文解析 ODPS SQL 任务优化方法原理
本文重点尝试从ODPS SQL的逻辑执行计划和Logview中的执行计划出发,分析日常数据研发过程中各种优化方法背后的原理,覆盖了部分调优方法的分析,从知道怎么优化,到为什么这样优化,以及还能怎样优化。
|
4天前
|
JSON Java Maven
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
8 0
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
|
4天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。

推荐镜像

更多