Spring中的环境(Environment)>
这小结内容对应官网中的1.13小节
在前面的ApplicationContext的继承关系中我们知道ApplicationContext这个接口继承了一个EnvironmentCapable接口,而这个接口的定义非常简单,如下
public interface EnvironmentCapable { Environment getEnvironment(); }
可以看到它只是简单的提供了一个获取Environment对象的方法,那么这个Environment对象是做什么的呢?
1、什么是环境(Environment)?
它其实代表了当前Spring容器的运行环境,比如JDK环境,系统环境;每个环境都有自己的配置数据,如System.getProperties()可以拿到JDK环境数据、System.getenv()可以拿到系统变量,ServletContext.getInitParameter()可以拿到Servlet环境配置数据。Spring抽象了一个Environment来表示Spring应用程序环境配置,它整合了各种各样的外部环境,并且提供统一访问的方法。
2、接口定义
public interface Environment extends PropertyResolver { // 获取当前激活的Profile的名称 String[] getActiveProfiles(); // 获取默认的Profile的名称 String[] getDefaultProfiles(); @Deprecated boolean acceptsProfiles(String... profiles); // 判断指定的profiles是否被激活 boolean acceptsProfiles(Profiles profiles); } public interface PropertyResolver { // 当前的环境中是否包含这个属性 boolean containsProperty(String key); //获取属性值 如果找不到返回null @Nullable String getProperty(String key); // 获取属性值,如果找不到返回默认值 String getProperty(String key, String defaultValue); // 获取指定类型的属性值,找不到返回null @Nullable <T> T getProperty(String key, Class<T> targetType); // 获取指定类型的属性值,找不到返回默认值 <T> T getProperty(String key, Class<T> targetType, T defaultValue); // 获取属性值,找不到抛出异常IllegalStateException String getRequiredProperty(String key) throws IllegalStateException; // 获取指定类型的属性值,找不到抛出异常IllegalStateException <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException; // 替换文本中的占位符(${key})到属性值,找不到不解析 String resolvePlaceholders(String text); // 替换文本中的占位符(${key})到属性值,找不到抛出异常IllegalArgumentException String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; }
我们可以看到,Environment接口继承了PropertyResolver,而Environment接口自身主要提供了对Profile的操作,PropertyResolver接口中主要提供了对当前运行环境中属性的操作,如果我们去查看它的一些方法的实现可以发现,对属性的操作大都依赖于PropertySource。
所以在对Environment接口学习前,我们先要了解Profile以及PropertySource
3、Profile
我们先看官网上对Profile的介绍:
从上面这段话中我们可以知道
- Profile是一组逻辑上的Bean的定义
- 只有这个Profile被激活时,这组Bean才会被注册到容器中
- 我们既可以通过注解的方式来将Bean加入到指定的Profile中,也可以通过XML的形式
- Environment主要决定哪个Profile要被激活,在没有激活的Profile时要使用哪个作为默认的Profile
注解方式(@Profile)
1、简单使用
@Component @Profile("prd") public class DmzService { public DmzService() { System.out.println("DmzService in prd"); } } @Component @Profile("dev") public class IndexService { public IndexService(){ System.out.println("IndexService in dev"); } } public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); ac.register(ProfileConfig.class); //ac.getEnvironment().setActiveProfiles("prd"); //ac.refresh(); // 输出 DmzService in prd ac.getEnvironment().setActiveProfiles("dev"); ac.refresh(); // 输出 IndexService in dev }
在上面的例子中,我们给两个组件(DmzService,IndexService)配置了不同的profile,可以看到当我们利用Environment激活不同的Profile时,可以分别只创建不同的两个类。
在实际生产环境中,我们往往会将"prd","dev"这种代表环境的标签放到系统环境变量中,这样依赖于不同系统的同一环境变量,我们就可以将应用程序运行在不同的profile下。
2、结合逻辑运算符使用
有时间我们某一组件可能同时要运行在多个profile下,这个时候逻辑运算符就派上用场了,我们可以通过如下的三种运行符,对profile进行逻辑运算
- !: 非,只有这个profile不被激活才能生效
- &: 两个profile同时激活才能生效
- |: 只要其中一个profile激活就能生效
比如在上面的例子中,我们可以新增两个类,如下:
@Component @Profile("dev & qa") public class LuBanService { public LuBanService(){ System.out.println("LuBanService in dev & qa"); } } @Component("!prd") public class ProfileService { public ProfileService(){ System.out.println("ProfileService in !prd"); } } public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); ac.register(ProfileConfig.class); // ac.getEnvironment().setActiveProfiles("prd"); // ac.refresh(); // 输出 DmzService in prd // ac.getEnvironment().setActiveProfiles("dev"); // ac.refresh(); // 输出 IndexService in dev ac.getEnvironment().setActiveProfiles("dev","qa"); ac.refresh();// 输出IndexService in dev //LuBanService in dev & qa //ProfileService in !prd }
为了编码的语义,有时候我们会将不同的profile封装成不同的注解,如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Profile("production") public @interface Production { }
有时候可能我们要将多个Bean同时置于某个profile下,这个时候每个Bean添加一个@Profile注解显得过去麻烦,这个时候如果我们是采用@Bean方式申明的Bean,可以直接在配置类上添加@Profile注解,如下(这里我直接取官网中的例子了,就不做验证了):
@Configuration public class AppConfig { @Bean("dataSource") @Profile("development") public DataSource standaloneDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } @Bean("dataSource") @Profile("production") public DataSource jndiDataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } }
3、注意一种特殊的场景
如果我们对使用了@Bean注解的方式进行了重载,那么要求所有重载的方法都在同一个@Profile下,否则@Profile的语义会被覆盖。什么意思呢?大家看下面这个Demo
public class A { public A() { System.out.println("independent A"); } public A(B b) { System.out.println("dependent A with B"); } } public class B { } @Configuration public class ProfileConfig { @Bean @Profile("dev") public A a() { return new A(); } @Bean @Profile("prd") public A a(B b) { return new A(b); } @Bean @Profile("prd | dev") public B b() { return new B(); } } public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); ac.register(ProfileConfig.class); ac.getEnvironment().setActiveProfiles("dev"); ac.refresh(); // 输出:dependent A with B }
我们明明激活的是dev这个profile,为什么创建的用的带参的构造函数呢?这是因为Spring在创建Bean时,方法的优先级高于Profile,前提是方法的参数在Spring容器内(在上面的例子中,如果我们将B的profile限定为dev,那么创建的A就会是通过空参构造创建的)。这里暂且不多说,大家知道有这种场景存在即可。在后面分析源码时我们会介绍,这里涉及到Spring对创建Bean的方法的推断(既包括构造函数也包括factroyMethod)。
XML方式
<!--在beans标签中指定profile属性即可--> <beans profile="development" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="..."> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans>
XML方式不多赘述,大家有需要可以自行研究
4、PropertySource
通过我们的Environment对象,除了能操作profile对象之外,通过之前的继承结构我们知道,他还能进行一些关于属性的操作。而这些操作是建立在Spring本身对运行环境中的一些属性文件的抽象而来的。抽象而成的结果就是PropertySource。
接口定义
public abstract class PropertySource<T> { protected final String name;//属性源名称 protected final T source;//属性源(比如来自Map,那就是一个Map对象) public String getName(); //获取属性源的名字 public T getSource(); //获取属性源 public boolean containsProperty(String name); //是否包含某个属性 public abstract Object getProperty(String name); //得到属性名对应的属性值 // 返回一个ComparisonPropertySource对象 public static PropertySource<?> named(String name) { return new ComparisonPropertySource(name); } }
除了上面定义的这些方法外,PropertySource中还定义了几个静态内部类,我们在下面的UML类图分析时进行介绍
UML类图
从上图中可以看到,基于PropertySource子类主要可以分为两类,一类是StubPropertySource,另一类是EnumerablePropertySource。而StubPropertySource这一类都是申明于PropertySource中的静态内部类。这两个类主要是为了完成一些特殊的功能而设计的。
StubPropertySource:这个类主要起到类似一个占位符的作用,例如,一个基于ServletContext的PropertySource必须等待,直到ServletContext对象对这个PropertySource所在的上下文可用。在这种情况下,需要用到StubPropertySource来预设这个PropertySource的位置跟顺序,之后在上下文刷新时期,再用一个ServletContextPropertySourc来进行替换
ComparisonPropertySource:这个类设计的目的就是为了进行比较,除了hashCode(),equals(),toString()方法能被调用外,其余方法被调用时均会抛出异常
而PropertySource的另外一些子类,都是继承自EnumerablePropertySource的,我们先来看EnumerablePropertySource这个类对其父类PropertySource进行了哪些补充,其定义如下:
public abstract class EnumerablePropertySource<T> extends PropertySource<T> { public EnumerablePropertySource(String name, T source) { super(name, source); } protected EnumerablePropertySource(String name) { super(name); } // 复写了这个方法 @Override public boolean containsProperty(String name) { return ObjectUtils.containsElement(getPropertyNames(), name); } // 新增了这个方法 public abstract String[] getPropertyNames(); }
这个类跟我们PropertySource的区别在于:
- 复写了containsProperty这个方法
- 新增了一个getPropertyNames方法
并且我们可以看到,再containsProperty这个方法中调用了getPropertyNames,这么做的理由是什么呢?为什么它不直接使用父类的containsProperty方法而要自己复写一个呢?我们对比下父类的实现:
public boolean containsProperty(String name) { return (getProperty(name) != null); }
结合这个类上的一段javadoc,如下:
A {@link PropertySource} implementation capable of interrogating its
underlying source object to enumerate all possible property name/value
pairs. Exposes the {@link #getPropertyNames()} method to allow callers
to introspect available properties without having to access the underlying
source object. This also facilitates a more efficient implementation of
{@link #containsProperty(String)}, in that it can call {@link #getPropertyNames()}
and iterate through the returned array rather than attempting a call to
{@link #getProperty(String)} which may be more expensive. Implementations may
consider caching the result of {@link #getPropertyNames()} to fully exploit this
performance opportunity.
Spring设计这个类的主要目的是为了,让调用者可以不访问其中的Source对象但是能判断这个PropertySource中是否包含了指定的key,所以它多提供了一个getPropertyNames,同时这段javadoc还指出,子类的实现应该考虑去缓存getPropertyNames这个方法的返回值去尽可能的压榨性能。
接下来,我们分别看一看它的各个实现类
- MapPropertySource
MapPropertySource的source来自于一个Map,这个类结构很简单,这里不说。
用法如下:
public static void main(String[] args) { Map<String,Object> map=new HashMap<>(); map.put("name","wang"); map.put("age",23); MapPropertySource source_1=new MapPropertySource("person",map); System.out.println(source_1.getProperty("name"));//wang System.out.println(source_1.getProperty("age"));//23 System.out.println(source_1.getName());//person System.out.println(source_1.containsProperty("class"));//false }
- ResourcePropertySource
source是一个Properties对象,继承自MapPropertySource。与MapPropertySource用法相同
- ServletConfigPropertySource
source为ServletConfig对象
- ServletContextPropertySource
source为ServletContext对象
- SystemEnvironmentPropertySource
继承自MapPropertySource,它的source也是一个map,但来源于系统环境。
- CompositePropertySource
内部可以保存多个PropertySource
private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>();
取值时依次遍历这些PropertySource
PropertySources
我们在阅读PropertySource源码上,会发现在其上有一段这样的javaDoc解释,其中提到了
{@code PropertySource} objects are not typically used in isolation, but rather through a {@link PropertySources} object, which aggregates property sources and in conjunction with a {@link PropertyResolver} implementation that can perform precedence-based searches across the set of {@code PropertySources}.
也就是说,PropertySource通常都不会单独的使用,而是通过PropertySources对象。
- 接口定义
public interface PropertySources extends Iterable<PropertySource<?>> { default Stream<PropertySource<?>> stream() { return StreamSupport.stream(spliterator(), false); } boolean contains(String name); @Nullable PropertySource<?> get(String name); }
这个接口由于继承了Iterable接口,所以它的子类也具备了迭代能力。
- 唯一子类
public class MutablePropertySources implements PropertySources { private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>(); ...... }
这个类最大的特点就是,持有了一个保存PropertySource的CopyOnWriteArrayList集合。并且它其余提供的方法,都是在往集合中增删PropertySource。
5、PropertyResolver
在之前的Environment的接口定义中我们知道,Environment接口继承了PropertyResolver接口,接下来我们再来关注下这个接口的定义
接口定义
public interface PropertyResolver { // 当前的环境中是否包含这个属性 boolean containsProperty(String key); //获取属性值 如果找不到返回null @Nullable String getProperty(String key); // 获取属性值,如果找不到返回默认值 String getProperty(String key, String defaultValue); // 获取指定类型的属性值,找不到返回null @Nullable <T> T getProperty(String key, Class<T> targetType); // 获取指定类型的属性值,找不到返回默认值 <T> T getProperty(String key, Class<T> targetType, T defaultValue); // 获取属性值,找不到抛出异常IllegalStateException String getRequiredProperty(String key) throws IllegalStateException; // 获取指定类型的属性值,找不到抛出异常IllegalStateException <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException; // 替换文本中的占位符(${key})到属性值,找不到不解析 String resolvePlaceholders(String text); // 替换文本中的占位符(${key})到属性值,找不到抛出异常IllegalArgumentException String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; }
UML类图
它的实现类主要有两种:
1.各种Resolver:主要是PropertySourcesPropertyResolver
2.各种Environment
PropertySourcesPropertyResolver使用示例
MutablePropertySources sources = new MutablePropertySources(); sources.addLast(new MapPropertySource("map", new HashMap<String, Object>() { { put("name", "wang"); put("age", 12); } }));//向MutablePropertySources添加一个MapPropertySource PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); System.out.println(resolver.containsProperty("name"));//输出 true System.out.println(resolver.getProperty("age"));//输出 12 System.out.println(resolver.resolvePlaceholders("My name is ${name} .I am ${age}."));
关于Environment实现主要分为两种
1.StandardEnvironment,标准环境,普通Java应用时使用,会自动注册2.System.getProperties()和 System.getenv()到环境
StandardServletEnvironment:标准Servlet环境,其继承了StandardEnvironment,Web应用时使用,除了StandardEnvironment外,会自动注册ServletConfig(DispatcherServlet)、ServletContext及有选择性的注册JNDI实例到环境
总结
在这篇文章中,我们学习了ApplicationContext的部分知识,首先我们知道ApplicationContext继承了5类接口,正由于继承了这五类接口,所以它具有了以下这些功能:
- MessageSource,主要用于国际化
- ApplicationEventPublisher,提供了事件发布功能
- EnvironmentCapable,可以获取容器当前运行的环境
- ResourceLoader,主要用于加载资源文件
- BeanFactory,负责配置、创建、管理Bean,IOC功能的实现主要就依赖于该接口子类实现。
在上文,我们分析学习了国际化,以及Spring中环境的抽象(Environment)。对于国际化而言,首先我们要知道国际化到底是什么?简而言之,国际化就是为每种语言提供一套相应的资源文件,并以规范化命名的方式保存在特定的目录中,由系统自动根据客户端语言选择适合的资源文件。其次,我们也一起了解了java中的国际化,最后学习了Spring对java国际化的一些封装,也就是MessageSource接口
对于Spring中环境的抽象(Environment)这块内容比较多,主要要知道Environment完成了两个功能
- 为程序运行提供不同的剖面,即Profile
- 操作程序运行中的属性资源
整个Environment体系可以用下图表示
对上图的解释:
1.Environment可以激活不同的Profile而为程序选择不同的剖面,一个Profile其实就是一组Spring中的Bean
2.Environment继承了PropertyResolver,从而可以操作程序运行时中的属性资源。而PropertyResolver的实现依赖于PropertySource,同时PropertySource一般不会独立使用,而是被封装进一个PropertySources对象中。
/