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文件吗???

实现原理剖析

下面我们直接定位到解析@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的注册方法。
  • GroovyBeanDefinitionReader
  • XmlBeanDefinitionReader:读取bean definition属性通过特定的xml文件。这个解析器在基于xml配置时候使用得非常之多

需要特别注意的是: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模式而已。

@PropertySource

Spring框架提供了PropertySource注解,目的是加载指定的属性文件。这个注解是非常具有实际意义的,特别是在SpringBoot环境下,意义重大。

由于SpringBoot默认情况下它会去加载classpath下的application.properties文件,所以我看大绝大多数开发者是这么干的:把所有的配置项都写在这一个配置文件里,这是非常不好的习惯,非常容易造成配置文件的臃肿,不好维护到最后的不能维护。

比如我们常见的一些配置:jdbc的、redis的、feign的、elasticsearch的等等。它们的边界都是十分清晰的,因此Spring提供给我们这个注解,能让我们很好的实现隔离性~~

此注解是Spring3.1后提供的,并不属于Spring Boot

使用DEMO

我有一个数据库的配置文件:jdbc.properties

## 配置db数据库相关信息
datasource.drivername=com.mysql.jdbc.Driver
datasource.username=vipkid_xb
datasource.password=jmdneyh4m2UT
datasource.url=jdbc:mysql://localhost:3316/test?zeroDateTimeBehavior=convertToNull

#### 连接池相关
datasource.maximum-pool-size=10
datasource.auto-commit=true
datasource.connection-test-query=SELECT 1
datasource.connectionTimeout=20000
datasource.maxLifetime=180000

我们可以这么使用它:采用Spring支持的@Value获取值

@Configuration
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {

    @Value("${datasource.username}")
    private String userName;
    @Value("${datasource.password}")
    private String password;
    @Value("${datasource.url}")
    private String url;


    // 此处只是为了演示 所以不用连接池了===========生产环境禁止这么使用==========
    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(userName);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }
}

单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JdbcConfig.class})
public class TestSpringBean {

    @Autowired
    private DataSource dataSource;

    @Test
    public void test1() throws SQLException {
        Connection connection = dataSource.getConnection();
        System.out.println(connection); com.mysql.jdbc.JDBC4Connection@6db66836
    }

}

能够正常获取到链接,说明配置生效~~~

其实大多数时候如果你是SpringBoot环境,我建议采用下面这种更优雅的方式,来处理某一类(请保证这一类拥有共同的前缀)属性值:@ConfigurationProperties

@Configuration
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
@ConfigurationProperties(prefix = "datasource")
public class JdbcConfig implements TransactionManagementConfigurer {

    private String username;
    private String password;
    private String url;


    public void setUsername(String username) {
        this.username = username;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public void setUrl(String url) {
        this.url = url;
    }

    // 此处只是为了演示 所以不用连接池了===========生产环境禁止这么使用==========
    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(username);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }
}

这样也是ok的。需要的注意的是:各个属性名和配置文件里的需要对应上。并且需要提供set方法。

另外还可以这么使用,直接把@ConfigurationProperties注解放在@Bean上,赋值极其方便。

@Configuration
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {

    @ConfigurationProperties(prefix = "datasource")
    @Bean
    public DataSource dataSource() {
        //dataSource.setUser(username);
        //dataSource.setPassword(password);
        //dataSource.setURL(url);
        return new MysqlDataSource();
    }
}

这样做极其优雅。但是需要注意的是MysqlDataSource里面对应的属性名称是什么。

SpringBoot下此种写法不区分大小写,驼峰,-,_等书写形式都是兼容的。但是你的字母必须对应上啊,比如上面的user你不能写成username了。比如我这样写:datasource.u-r-l=xxx也是能够被正常识别的

实现原理剖析

此处直接分析方法(该注解的解析时机还是非常早的)processPropertySource:

class ConfigurationClassParser {
    ...
    private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    	String name = propertySource.getString("name");
    	if (!StringUtils.hasLength(name)) {
    		name = null;
    	}
    	String encoding = propertySource.getString("encoding");
    	if (!StringUtils.hasLength(encoding)) {
    		encoding = null;
    	}
    
    	// 这里value代表这locations  我个人感觉  语义可以优化一下
    	String[] locations = propertySource.getStringArray("value");
    	Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
    	boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");    
    	Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    	// PropertySourceFactory接口,就是createPropertySource的工厂,Spring内部只有一个实现:DefaultPropertySourceFactory 
    	// 若你不指定默认就是DefaultPropertySourceFactory,否则给你new一个对象出来~(请保证有空的构造函数~)
    	PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
    			DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));    
    	for (String location : locations) {
    		try {
    			// 显然它也支持占位符,支持classpath*
    			String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
    			Resource resource = this.resourceLoader.getResource(resolvedLocation);
    			
    			// 调用factory的createPropertySource方法根据名字、编码、资源创建出一个PropertySource出来(实际是一个ResourcePropertySource)
    			addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
    		}
    		catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
    			// Placeholders not resolvable or resource not found when trying to open it
    			// 若它为true,那没找着就没找着,不会抛异常阻断程序的启动,需要注意~
    			if (ignoreResourceNotFound) {
    				if (logger.isInfoEnabled()) {
    					logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
    				}
    			}
    			else {
    				throw ex;
    			}
    		}
    	}
    }    
    // 把属性资源添加进来,最终全部要放进MutablePropertySources 里  这点非常重要~~~~ 这个时机
    private void addPropertySource(PropertySource<?> propertySource) {
    	String name = propertySource.getName();
    	
    	// 这个特别的重要,这个其实就是Spring处理配置文件优先级的原理,下面有个截图可以看到
    	// 因为这块特别重要,后面还会有专门章节分析~~~
    	// MutablePropertySources它维护着一个List<PropertySource<?>> 并且是有序的~~~
    	MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();    
    	// 此处若发现你的同名PropertySource已经有了,还是要继续处理的~~~而不是直接略过
    	if (this.propertySourceNames.contains(name)) {
    		// We've already added a version, we need to extend it
    		// 根据此name拿出这个PropertySource~~~~若不为null
    		// 下面就是做一些属性合并的工作~~~~~
    		PropertySource<?> existing = propertySources.get(name);
    		if (existing != null) {
    			PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
    					((ResourcePropertySource) propertySource).withResourceName() : propertySource);
    			if (existing instanceof CompositePropertySource) {
    				((CompositePropertySource) existing).addFirstPropertySource(newSource);
    			}
    			else {
    				if (existing instanceof ResourcePropertySource) {
    					existing = ((ResourcePropertySource) existing).withResourceName();
    				}
    				CompositePropertySource composite = new CompositePropertySource(name);
    				composite.addPropertySource(newSource);
    				composite.addPropertySource(existing);
    				propertySources.replace(name, composite);
    			}
    			return;
    		}
    	}    
    	// 这段代码处理的意思是:若你是第一个自己导入进来的,那就放在最末尾
    	// 若你不是第一个,那就把你放在已经导入过的最后一个的前一个里面~~~
    	if (this.propertySourceNames.isEmpty()) {
    		propertySources.addLast(propertySource);
    	} else {
    		String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
    		propertySources.addBefore(firstProcessed, propertySource);
    	}
    	this.propertySourceNames.add(name);
    }
    ...
}

比如这样子导入两次,但是名字不同:

@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config2", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {
    ...
}

最终结果为:

在这里插入图片描述

就这样,我们导入的属性值们,最终也放进了环境Environment里面。

@PropertySource注解解释

// @since 3.1  它也只能标注在类上面
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {

    // 该配置项PropertySource 的名字。若不指定 则用的是Resource#getDescription()
    // 示例:class path resource [jdbc.properties]
    String name() default "";
    // 配置文件地址。支持${...} placeholders。也支持classpath
    String[] value();
    // @since 4.0  默认是false就是强制要求文件必须存在的~
    boolean ignoreResourceNotFound() default false;
    // 可写:UTF-8
    String encoding() default "";
    // PropertySource的创建工厂,一般性而言,不要自己实现,用默认的即可
    Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

}

至于上面的言论:可不可以导入非properties文件呢?这里答案显然是否定的,只能是key-value形式的属性文件形式。

(显然yaml也是默认不支持的,除非你自己去实现,Spring是提供了这种扩展开口的)

关于Properties的读取加载,推荐工具类:org.springframework.core.io.support.PropertiesLoaderUtils。

Properties文件的使用小细节

logo.location=/image/logo/  
mail.host=webmaster@zlex.org  
site.name=zlex中文网站  
welcome=欢迎您,{0}!  

读取程序如下:

public static void main(String[] args) throws IOException {
    ClassPathResource resource = new ClassPathResource("my.properties");
    Properties properties = new Properties();
    InputStream inputStream = resource.getInputStream();
    properties.load(inputStream);
    properties.forEach((k, v) -> {
    	System.out.println(k + "=" + v);
    });
}

发现有中文乱码的

解决方案:

public static void main(String[] args) throws IOException {
    ClassPathResource resource = new ClassPathResource("my.properties");
    Properties properties = new Properties();
    InputStream inputStream = resource.getInputStream();
    //properties.load(inputStream);
    // 解决中文乱码问题  使用Reader (若直接使用inputStream,请把中文使用native2ascii.exe转换~~~)
    Reader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    properties.load(reader);
    properties.forEach((k, v) -> {
    	System.out.println(k + "=" + v);
    });
}

这样中文就没有乱码了。(请保证你的文件编码是utf-8)。

Properties 文件中可议使用:吗?

logo.location:/image/logo/  
mail.host:webmaster@zlex.org  
site.name:zlex中文网站  
welcome:欢迎您,{0}! 

最终结果:也是ok的。

=/:中间可以有N个空格吗?

logo.location:     /image/logo/  
mail.host      :webmaster@zlex.org  
site.name     :   zlex中文网站  
welcome:欢迎您,{0}! 

发现打印的结果一模一样。结论:中间有N个空格,都是无所谓的。

头、尾有空格呢?

 logo.location:/image/logo/
  mail.host:webmaster@zlex.org    
site.name:zlex中文网站
welcome:欢迎您,{0}!

/image/logo/后面有N个空格~~~

结论为:头部N个空格都没关系,但是,尾部的空格是会当作值的一部分的。这个特别特备引起注意,很多人在这里踩过坑,因为这个错误还非常的不好找~~~

 

参考:

 

posted @ 2021-12-12 12:04  残城碎梦  阅读(266)  评论(0编辑  收藏  举报