1. 背景
我们的项目早期是采用Spring Roo进行构建的,但是当业务越来越复杂,后端人员越来越多的时候分工特别不方便;并且大家都不太习惯使用Spring Roo CLI进行项目开发,所以经过了一波去Roo化后回归到了SSH架构;后来由于项目的环境配置管理特别不方便,我们发现Spring Boot对配置文件的管理特别符合我们的需求以及我们后期要把项目进行微服务化发现Spring Cloud同样也是一个不错的选择,但项目业务体系比较庞大需要一步一步去做,要把项目进行调整过渡慢慢的适应于Spring Boot那一套进行开发;所以需要移植Spring Boot已经存在的和我们迫切需要的一些优秀特征。
2. 说明
把配置文件从Properties换成YAML需要导入第三方YAML的核心库,Spring仅仅只是做为一个管理去使用它,但真正的YAML解析还是需要靠这个第三方库;具体Spring是如何使用YAML来进行管理配置文件的可以参考Spring第三方配置文档。
3. 环境版本
Spring 4.2.6.RELEASE、Snakeyaml 1.17
4. SnakeYaml的MAVEN片段
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.17</version>
</dependency>
5. 从Spring Boot移植的依赖类(稍修过)
这里所涉及到四个类文件,在自己项目里面新建包名路径为“org.springframework.env”(最好直接使用我这个名称,不需要额外导包之类的),然后把下面四个类拷贝至新建的包中就好了。
5.1 PropertySourceLoader
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.env;
import java.io.IOException;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Strategy interface located via {@link SpringFactoriesLoader} and used to load a
* {@link PropertySource}.
*
* @author Dave Syer
* @author Phillip Webb
*/
public interface PropertySourceLoader {
/**
* Returns the file extensions that the loader supports (excluding the '.').
* @return the file extensions
*/
String[] getFileExtensions();
/**
* Load the resource into a property source.
* @param name the name of the property source
* @param resource the resource to load
* @param profile the name of the profile to load or {@code null}. The profile can be
* used to load multi-document files (such as YAML). Simple property formats should
* {@code null} when asked to load a profile.
* @return a property source or {@code null}
* @throws IOException if the source cannot be loaded
*/
PropertySource<?> load(String name, Resource resource, String profile)
throws IOException;
}
5.2 ArrayDocumentMatcher
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.env;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher;
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
import org.springframework.util.StringUtils;
/**
* Matches a document containing a given key and where the value of that key is an array
* containing one of the given values, or where one of the values matches one of the given
* values (interpreted as regexes).
*
* @author Dave Syer
*/
public class ArrayDocumentMatcher implements DocumentMatcher {
private final String key;
private final String[] patterns;
public ArrayDocumentMatcher(final String key, final String... patterns) {
this.key = key;
this.patterns = patterns;
}
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey(this.key)) {
return MatchStatus.ABSTAIN;
}
Set<String> values = StringUtils
.commaDelimitedListToSet(properties.getProperty(this.key));
if (values.isEmpty()) {
values = Collections.singleton("");
}
for (String pattern : this.patterns) {
for (String value : values) {
if (value.matches(pattern)) {
return MatchStatus.FOUND;
}
}
}
return MatchStatus.NOT_FOUND;
}
}
5.3 SpringProfileDocumentMatcher
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.env;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Properties;
import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher;
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
import org.springframework.core.env.Environment;
/**
* {@link DocumentMatcher} backed by {@link Environment#getActiveProfiles()}. A YAML
* document matches if it contains an element "spring.profiles" (a comma-separated list)
* and one of the profiles is in the active list.
*
* @author Dave Syer
*/
public class SpringProfileDocumentMatcher implements DocumentMatcher {
private static final String[] DEFAULT_PROFILES = new String[] { "^\\s*$" };
private String[] activeProfiles = new String[0];
public SpringProfileDocumentMatcher() {
}
public SpringProfileDocumentMatcher(String... profiles) {
addActiveProfiles(profiles);
}
public void addActiveProfiles(String... profiles) {
LinkedHashSet<String> set = new LinkedHashSet<String>(
Arrays.asList(this.activeProfiles));
Collections.addAll(set, profiles);
this.activeProfiles = set.toArray(new String[set.size()]);
}
@Override
public MatchStatus matches(Properties properties) {
String[] profiles = this.activeProfiles;
if (profiles.length == 0) {
profiles = DEFAULT_PROFILES;
}
return new ArrayDocumentMatcher("spring.profiles", profiles).matches(properties);
}
}
5.4 SpringYamlPropertiesFactoryBean
package org.springframework.env;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.YamlProcessor;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
/**
* Spring Yaml配置信息处理类
* <p><b>参考SpringBoot内容进行移植</b></p>
* <p>http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html</p>
* <p>具体查看: 24.6.2 Exposing YAML as properties in the Spring Environment</p>
* @author cary
* @date 2016/11/2
*/
public class SpringYamlPropertiesFactoryBean implements FactoryBean<Properties>, InitializingBean, EnvironmentAware {
private boolean singleton = true;
private Properties properties;
private Resource resource;
private Environment environment;
@Override
public Properties getObject() throws Exception {
return this.properties;
}
@Override
public Class<?> getObjectType() {
return Properties.class;
}
@Override
public boolean isSingleton() {
return this.singleton;
}
@Override
public void afterPropertiesSet() throws Exception {
if (isSingleton()) {
if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
this.properties = new Properties();
loadDefault();
for (int i = 0; i < environment.getActiveProfiles().length; i++) {
String profile = environment.getActiveProfiles()[i];
Processor processor = new Processor(resource, profile);
this.properties.putAll(processor.process());
}
//让VM参数可以覆盖YAML
this.properties.putAll(System.getProperties());
System.getProperties().putAll(this.properties);
}
}
}
protected void loadDefault(){
Processor processor = new Processor(resource, null);
this.properties.putAll(processor.process());
}
public void setResource(Resource resource){
this.resource = resource;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* {@link YamlProcessor} to create a {@link Map} containing the property values.
* Similar to {@link YamlPropertiesFactoryBean} but retains the order of entries.
*/
private static class Processor extends YamlProcessor {
Processor(Resource resource, String profile) {
if (profile == null) {
setMatchDefault(true);
setDocumentMatchers(new SpringProfileDocumentMatcher());
}
else {
setMatchDefault(false);
setDocumentMatchers(new SpringProfileDocumentMatcher(profile));
}
setResources(new Resource[] { resource });
}
@Override
protected Yaml createYaml() {
return new Yaml(new StrictMapAppenderConstructor(), new Representer(),
new DumperOptions(), new Resolver() {
@Override
public void addImplicitResolver(Tag tag, Pattern regexp,
String first) {
if (tag == Tag.TIMESTAMP) {
return;
}
super.addImplicitResolver(tag, regexp, first);
}
});
}
// public Map<String, Object> process() {
// final Map<String, Object> result = new LinkedHashMap<String, Object>();
// process(new MatchCallback() {
// @Override
// public void process(Properties properties, Map<String, Object> map) {
// result.putAll(getFlattenedMap(map));
// }
// });
// return result;
// }
public Properties process() {
final Properties result = new Properties();
process(new MatchCallback() {
@Override
public void process(Properties properties, Map<String, Object> map) {
result.putAll(properties);
}
});
return result;
}
}
}
6. 配置applicationContext.xml
<!--
如果需要针对不同环境设置,VM参数设置参考: -Dspring.profiles.active=production
-->
<bean id="yamlPropertiesFactoryBean" class="org.springframework.env.SpringYamlPropertiesFactoryBean"
p:resource="classpath:META-INF/spring/application.yml"/>
<context:property-placeholder properties-ref="yamlPropertiesFactoryBean" />
7. 简易各环境配置示例
新建一个文件“application.yml”存放到项目的“resources/META-INF/spring”中(目录位置其实可以随意,只要在第6点中把位置配置对就好了),加入以下内容到文件中,启动的时候设置JVM参数“-Dspring.profiles.active=development”即可看到效果。
application:
name: '默认名称'
---
spring:
profiles: development
application:
name: '测试名称'
---
spring:
profiles: production
application:
name: '生产名称'
---
8. 最后注意
在项目中具体怎么去使用Spring配置属性的相关的代码我就不贴出来了(和原始的使用方式一样);如果项目启动JVM中没有设置“-Dspring.profiles.active”变量参数的就会使用默认的信息。