Spring官网阅读(十一)ApplicationContext详细介绍(上)(2)

简介: Spring官网阅读(十一)ApplicationContext详细介绍(上)(2)

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的介绍:

微信图片_20221112193921.png

从上面这段话中我们可以知道


  1. Profile是一组逻辑上的Bean的定义
  2. 只有这个Profile被激活时,这组Bean才会被注册到容器中
  3. 我们既可以通过注解的方式来将Bean加入到指定的Profile中,也可以通过XML的形式
  4. 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类图

微信图片_20221112194453.png

从上图中可以看到,基于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的区别在于:


  1. 复写了containsProperty这个方法
  2. 新增了一个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

微信图片_20221112195615.png

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体系可以用下图表示

微信图片_20221112195829.png

对上图的解释:


1.Environment可以激活不同的Profile而为程序选择不同的剖面,一个Profile其实就是一组Spring中的Bean

2.Environment继承了PropertyResolver,从而可以操作程序运行时中的属性资源。而PropertyResolver的实现依赖于PropertySource,同时PropertySource一般不会独立使用,而是被封装进一个PropertySources对象中。


/

相关文章
|
3月前
|
Java Spring
Spring 源码阅读 72:基于 CGLIB 的 AOP 代理的原理(2)- 拦截器的查找与执行
【1月更文挑战第7天】本文分析了基于 CGLIB 的 AOP 代理如何查找和执行拦截器链,其主要的逻辑在 DynamicAdvisedInterceptor 的intercept方法执行。
35 1
|
2月前
|
Java 应用服务中间件 Spring
Spring5源码(50)-SpringMVC源码阅读环境搭建
Spring5源码(50)-SpringMVC源码阅读环境搭建
40 0
|
3月前
|
缓存 Java Spring
Spring 源码阅读 66:基于 JDK 的 AOP 代理如何获取拦截器链(4)- 将 Advice 封装为拦截器
【1月更文挑战第1天】本文分析了 Advice 被封装成 MethodInterceptor 的过程,Spring AOP 用到的五种 Advice 中,有些本身就是 MethodInterceptor 的实现类,而有些需要通过适配器的封装。
41 0
|
1月前
|
Java 测试技术 数据库连接
【Spring源码解读!底层原理高级进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨
【Spring源码解读!底层原理高级进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨
|
3月前
|
Java Spring
Spring 源码阅读 71:基于 CGLIB 的 AOP 代理的原理(1)- DynamicAdvisedInterceptor 分析
【1月更文挑战第6天】本文分析了基于 CGLIB 的 AOP 代理对象,是通过一个 DynamicAdvisedInterceptor 类型的 Callback 来完成 AOP 增强逻辑处理的,DynamicAdvisedInterceptor 通过实现 MethodInterceptor 接口的intercept方法来处理 AOP 增强逻辑。下一篇,将重点分析这个方法的原理。
53 7
|
1月前
|
XML Java 开发者
【Spring源码解读 底层原理高级进阶】【上】探寻Spring内部:BeanFactory和ApplicationContext实现原理讲解
【Spring源码解读 底层原理高级进阶】【上】探寻Spring内部:BeanFactory和ApplicationContext实现原理讲解
|
3月前
|
XML Java 数据格式
Spring5源码(26)-ApplicationContext容器refresh过程简析
Spring5源码(26)-ApplicationContext容器refresh过程简析
37 0
|
3月前
|
XML Java 开发者
Spring 源码的阅读心得
【1月更文挑战第12天】最近花了很多时间去阅读Spring框架核心部分的源码,本文将分享一些阅读的思路和心得,分享给想阅读源码但是不知道如何下手或者读不下来的小伙伴。
62 1
|
3月前
|
缓存 Java Spring
Spring 源码阅读 75:@EnableAsync 分析
【1月更文挑战第10天】本文以 @EnableAsync 作为切入点,分析了 Spring 开启基于注解的异步任务特性的原理。
35 0
|
3月前
|
缓存 Java 数据库连接
Spring 源码阅读 74:BeanFactoryTransactionAttributeSourceAdvisor 分析
【1月更文挑战第9天】本文通过对 BeanFactoryTransactionAttributeSourceAdvisor 类的分析,了解了 Spring 是如何通过 AOP 来完成事务的管理的,本文的内容需要你对 Spring 的 AOP 的实现原理有一定的了解。
46 0