前言
@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。它内置有三个实现类:
- 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)