【小家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)

@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里面对应的属性名称是啥。比如此处为user、password、URL,因此为了对应上你需要做出如下修改才能生效。(如何修改此处省略)
建议:这种方式一般还是用于框架内部(比如我自己写的框架就用到了它,挺方便也好懂),而不是外部使用(因为约定得太多了,不太好太强的约束到使用者,当然我觉得也没啥,规范应该人人遵守

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

另外,本文重点是@PropertySource而非@ConfigurationProperties~~~~~~

实现原理剖析

上面已经贴出了入口,此处直接分析方法(该注解的解析时机还是非常早的)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是提供了这种扩展开口的)

总结

我个人认为这两个注解还是比较有用的,特别是@PropertySource注解在我们分模块开发时候有非常大的作用,它能让你的代码更清晰,配置更隔离,减少出错

说到配置我啰嗦一句:其实任何事都一样,隔离性、自治性都是需要保证的,这样后续才好维护。比如我看到非常多的小伙伴配置@Bean的时候,根本不考虑到底配置在哪个@Confiuration配置类下呢?随便乱扔,虽然可能能正常work,但是若真出了问题,可以说将是灾难性的,会付出很大的代价去查找~~~ 希望小伙伴们能保持一个良好的分层、隔离的编码习惯

我对它体会最深处是我在之前某家公司的时候,对臃肿的的配置文件的重构。当时我还是一个刚好1年经验的java新生,完全不懂的原理,但我知道这么做很好,哈哈~~~

还有就是Spring暴露出来给我们使用的API,我认为都是很有必要去学习、了解的。虽然它有很多也是重复发明轮子,但是人家有这个能力并且你可以很方便的使用它(至少比JavaEE方便),因此熟悉后,这方面的开发效率会大大的提升,最终受益者也是你自己个

小知识点:@Order注解对@Bean、以及@Service都是不能生效的(控制不了Bean的顺序),若你想在Spring环境下控制Filter的顺序,请使用其它方式(SpringBoot可以很友好的控制Servlet三大组件的顺序,这个以后再说~

使用Properties读取xml文件

其实这也是我的一个意外收获。在看SpringBoot源码的时候有这么一个类:

public class PropertiesPropertySourceLoader implements PropertySourceLoader {
	@Override
	public String[] getFileExtensions() {
		return new String[] { "properties", "xml" };
	}
}

发现它不仅加载properties,也加载xml文件。可见Spring团队对jdk底层是非常非常熟悉的。

勾起兴趣后,mark下这个:使用Properties也能读取xml文件。
Demo如下:
xml文件内容(请注意:是有DTD要求的,并且可以看到是sun官方的DTD):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">

<!-- properties标签下只有这comment和entry两个值 -->
<properties>
    <comment>系统配置</comment>
    <entry key="logo.location"><![CDATA[/image/logo/]]></entry>
    <entry key="mail.host"><![CDATA[webmaster@zlex.org]]></entry>
    <entry key="site.name"><![CDATA[zlex中文网站]]></entry>
    <entry key="welcome"><![CDATA[欢迎您,{0}!]]></entry>
</properties>

读取:

    public static void main(String[] args) throws IOException {
        ClassPathResource resource = new ClassPathResource("my.xml");
        Properties properties = new Properties();
        properties.loadFromXML(resource.getInputStream());
        
        System.out.println(properties); //{logo.location=/image/logo/, site.name=zlex中文网站, welcome=欢迎您,{0}!, mail.host=webmaster@zlex.org}
    }

可以正常读取到内容。此xml内容同下面properties文件。xml没有中文乱码问题,这是它一大优秀的地方

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

备注:loadFromXML()方法JDK5提供。平时我们使用中还是推荐使用properties文件,但这个可以炫技用,哈哈

关于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);
        });
    }

发现是有中文乱码的。

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

1、解决中文乱码问题: 使用Reader读取

    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)
2、Properties 文件中可议使用:吗?
形如下面,使用:配置。

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

最终结果:也是ok的
3、=/:中间可以有N个空格吗?
形如这样:

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

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

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

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

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

以上为关于properties配置文件的一些使用细节,希望能帮助到大家。虽然遵循规范是最好,但是若别人使用了一些非工整格式,比如使用:,比如多空格啥的,你要知道咋回事~~这就是高手


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
"Access denied for user 'root'@'127.0.0.1' (using password: NO)" 表示使用root用户在本地主机上连接数据库时,没有提供密码。这个错误通常发生在需要使用密码进行身份验证的情况下,但用户没有提供密码。数据库会拒绝访问,并显示该错误信息。 解决这个问题的方法是提供正确的密码。你可以尝试以下几个步骤来解决这个问题: 1. 确保你已经为root用户设置了正确的密码。如果你还没有设置密码,可以使用以下命令在MySQL设置密码: ``` ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_password'; ``` 将`your_password`替换为你想要设置的密码。 2. 确保你在连接数据库时提供了正确的密码。你可以在连接字符串指定密码,或者在连接时通过提示输入密码。确保密码与你在第一步设置的密码匹配。 3. 检查你的数据库配置文件是否正确。有时候,配置文件的密码可能不正确导致访问被拒绝。你可以检查配置文件与root用户相关的密码设置,并确保其正确。 如果你按照以上步骤操作仍然无法解决问题,你可以尝试重置root用户的密码,并重新进行设置。同时,确保你的数据库服务器正在运行并且允许来自本地主机的连接。 希望这些信息能够帮助你解决问题!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [navicat,idea连接示1045 - Access denied for user ‘root‘@‘127.0.0.1‘ (using password:Yes)](https://blog.csdn.net/weixin_44446230/article/details/126862377)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [mysql-8.0.20-macos10.15-x86_64.tar.gz](https://download.csdn.net/download/long4512524/12427861)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值