基于最新Spring 5.x,对于基于XML的Spring IoC容器初始化过程中的setConfigLocations设置容器配置信息方法的源码进行了详细分析,最后给出了比较详细的方法调用时序图!
Spring 5.x源码解析系列的第一篇文章,主要介绍了Spring源码的一些学习思路,以及从最基础容器的开始,对于基于XML的Spring IoC容器ClassPathXmlApplicationContext的初始化过程中的setConfigLocations设置容器配置信息方法的源码进行了详细分析,最后给出了比较详细的方法调用时序图!
Spring IoC容器初始化源码 系列文章
Spring IoC容器初始化源码(1)—setConfigLocations设置容器配置信息
Spring IoC容器初始化源码(2)—prepareRefresh准备刷新、obtainFreshBeanFactory加载XML资源、解析<beans/>标签
Spring IoC容器初始化源码(3)—parseDefaultElement、parseCustomElement解析默认、扩展标签,registerBeanDefinition注册Bean定义
Spring IoC容器初始化源码(4)—<context:component-scan/>标签解析、spring.components扩展点、自定义Spring命名空间扩展点
Spring IoC容器初始化源码(5)—prepareBeanFactory、invokeBeanFactoryPostProcessors、registerBeanPostProcessors方法
Spring IoC容器初始化源码(6)—finishBeanFactoryInitialization实例化Bean的整体流程以及某些扩展点
Spring IoC容器初始化源码(7)—createBean实例化Bean的整体流程以及构造器自动注入
Spring IoC容器初始化源码(8)—populateBean、initializeBean实例化Bean以及其他依赖注入
< context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析
三万字的ConfigurationClassPostProcessor配置类后处理器源码深度解析
基于JavaConfig的AnnotationConfigApplicationContext IoC容器初始化源码分析
文章目录
- Spring IoC容器初始化源码 系列文章
- 1 Spring源码学习概述
- 1 IoC容器概述
- 3 从头开始
- 4 ClassPathXmlApplicationContext容器初始化
- 5 时序图与小结
1 Spring源码学习概述
Spring作为一个优秀框架,经受住了时间和众多开发者的考验,它的Bug相比于其他框架非常少,使用起来也非常简单,对于普通开发者,完全可以不必关心它到底是怎么实现的,特别是Spring Boot出现之后,Spring的使用更加简单,对外隐藏了更多的实现细节。这也是目前众多框架发展的趋势,因为大部分的开发者使用一个框架时,往往更加关注它能解决什么问题以及上手的难易程度!
通常,看框架源码往往不会直接提升我们的业务能力,看源码的目的更多的是学习它的设计思路,设计模式,然后举一反三,再看看自己写的业务代码,是否有什么可改进和优化的地方,系统架构设计是否合理,是否具有可扩展性。
这种优秀框架的源码量非常非常非常的多,我们不能和看Java核心代码那样一行一行的分析,必须要有自己的学习策略。
首先,Spring源码从哪里学起呢?有一种比较好的方法是,从你刚接触到的代码开始学习。所以,这就是我们从传统Spring Framework而不是Spring Boot开始学习的好处,那就是在学习源码的时候,能够更加好进行对照和过渡。因为Spring Boot是对于Spring Framework等其他项目进行的一个更加高级的封装,这无形中增加了我们的学习源码的难度。
在看源码之前,应该先学会如何简单的使用,Spring框架的核心就是IoC与AOP,此前我们已经学习了IoC与AOP的基于XML和注解的基本使用(Spring 5.x 学习),然而在学习过程中我们肯定有很多疑惑,比如容器到底是怎么创建的,bean是怎么加载的?现在我们来简单学习一下与它们相关的源码,同时介绍一些Spring提供的扩展点以及在此前入门学习过程中漏掉的一些知识。
1 IoC容器概述
所谓控制反转IoC,简单的说就是我们将对象创建、依赖注入的权力交给Spring,我们不用关心对象和依赖什么时候创建以及到底在什么地方,由Spring帮我们创建、管理对象,在需要的时候进行依赖注入。我们交给Spring的管理的对象,被称为bean,bean对象存放的地方,被称为IoC容器,或者bean工厂。上面案例中的ClassPathXmlApplicationContext类,就是IoC容器的一种实现。
非web环境下IoC容器核心类的uml类图结构如下:
可以看到相关的类、结构非常的多,但是目前,我们只需要简单关注ApplicationContext与BeanFactory。
BeanFactory是一个IoC容器的超级接口,它是Spring框架发展的早期出现的,功能比较单一,仅仅定义了对单个bean的获取,对bean的作用域判断,获取bean类型,获取bean别名的功能。实际上第一篇文章我们也使用过早期IoC容器的实现,比如DefaultListableBeanFactory、XmlBeanFactory,由于功能比较单一,并且它管理的bean都是默认懒加载的,现在基本不再使用,除非某些对内存有严格要求的应用才会考虑。
ApplicationContext是后来出现的IoC容器接口,从uml类图中也能发现它继承了BeanFactory。ApplicationContext可以看作BeanFactory的一个增强升级版,它继承了BeanFactory的全部功能,负责实例化、配置和组装bean,同时从uml类图中也能看出来它实现了其他接口,添加了国际化消息(MessageSource)、事件发布(ApplicationEvent、ApplicationListener)、BeanPostProcessor与BeanFactoryPostProcessor的自动注册、支持web应用的上下文实现(XmlWebApplicationContext)、多个资源访问(ResourceLoader)、与Spring AOP直接集成等等新功能。ApplicationContext常被称为Spring核心上下文,它管理的bean都是默认饿加载的。
ApplicationContext提供了许多IoC容器的实现。ClassPathXmlApplicationContext和FileSystemXMLApplicationContext,他们是两兄弟,通过加载配置文件来启动Spring的,只不过一个是从程序类路径加载一个是从系统路径加载。AnnotationConfigApplicationContext则是专门从注解进行加载配置的。还有一个XmlWebApplicationContext,它是专门为web开发所准备的,用于通过监听器启动并加载web根目录下的配置文件信息。
实际上ClassPathXmlApplicationContext和FileSystemXmlApplicationContext的功能一样,只是在加载配置的时候有区别:
- 对于 ClassPathXmlApplicationContext,默认就是加载项目的 classpath 路径下面的配置文件,可以不加上“classpath:”前缀;但是如果要使用绝对路径,就必须需要加上“file:”前缀,这是绝对路径。
- 对于 FileSystemXmlApplicationContext,默认就是加载文件系统下面的配置文件,可以不加“file:”,有盘符绝对路径的算作项目的根目录。但是如果要使用 classpath 路径,就必须需要加上“asspath:”前缀,这是相对路径。
其它出现在uml类图中的类或者接口:
- ListableBeanFactory:提供可以一次性获取容器中的全部bean的功能。
- HierarchicalBeanFactory:支持继承关系,提供父容器的访问功能。
- MessageSource:支持国际化功能。
- ApplicationEventPublisher:事件发布功能。
- EnvironmentCapable:获取Environment的功能,即获取应用运行时的环境。
- ResourcePatternResolver:提供解析资源文件策略,比如classpath*的前缀支持。
- ResourceLoader:获取资源的超级接口。
- DefaultResourceLoader:ResourceLoader接口的默认实现,用来获取资源。
- Lifecycle:监听容器生命周期时间,比如start、stop信号。
- AutoCloseable:JDK的接口,实现AutoCloseable接口的类的实例,将其放到try后面(我们称之为:带资源的try语句),在try结束的时候,会自动将这些资源关闭(调用close方法)。
- Closeable:JDK的接口,实现了Closeable接口的类的对象可以被关闭。比如数据源或流。
- ConfigurableApplicationContext:提供配置应用程序上下文(ApplicationContext)的功能。
- AbstractApplicationContext:抽象应用程序上下文,实际上大部分的IOC容器初始化工作都是在该类中完成的,比如refresh()方法,但是也提供了可扩展方法留给子类实现。
- Aware:感知接口,用于辅助访问IoC容器的资源,其实现通常为XXAware,即表示对XX感知,从容器获取XX资源。
- BeanNameAware:用于获得到容器中Bean的名称。
- AbstractRefreshableApplicationContext:支持beanFactory的刷新。
- InitializingBean:为bean提供了一种初始化方法,类似于init-method(但不一样)。
- AbstractRefreshableConfigApplicationContext:指定配置文件加载路径。
- AbstractXmlApplicationContext:解析bean的定义。
- ClassPathXmlApplicationContext:IoC容器的实现,实际上仅仅提供简单的构造函数,具体的功能由它的父类实现了。
当然,还有很多相关类、接口、组件没有出现在上面的uml类图之中,实际上上面的每一接口和类也都足以单独成文讲解。
本次,我们主要学习一种IoC容器即ClassPathXmlApplicationContext的初始化,并对途中遇到的组件进行介绍。源码学习都是循序渐进的过程,所以说不要看着这么多的组件就害怕了,慢慢来就行了。
3 从头开始
现在让我们回到最开始学习Spring的时候!
所需Spring核心依赖:
<properties>
<spring-framework.version>5.2.8.RELEASE</spring-framework.version>
</properties>
<dependencies>
<!--spring 核心组件所需依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
</dependencies>
一个简单的类定义:
/**
* @author lx
*/
public class FirstSpringSource {
public FirstSpringSource() {
System.out.println("FirstSpringSource init");
}
public void methodCall(){
System.out.println("methodCall");
}
}
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-->
<bean class="com.spring.source.FirstSpringSource" name="firstSpringSource"/>
</beans>
使用Spring:
@Test
public void firstSpringSource() {
//初始化容器
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
//获取bean实例
FirstSpringSource firstSpringSource = ac.getBean("firstSpringSource", FirstSpringSource.class);
//使用
firstSpringSource.methodCall();
}
结果:
FirstSpringSource init
methodCall
到此,我们回顾了一下比较原始的Spring的使用,现在我们从ClassPathXmlApplicationContext的初始化,即IoC容器的初始化开始学习源码!
4 ClassPathXmlApplicationContext容器初始化
4.1 ClassPathXmlApplicationContext构造器
在案例中,首先就是初始化ClassPathXmlApplicationContext容器,因此,我们从ClassPathXmlApplicationContext的构造器开始,其实ClassPathXmlApplicationContext的源码很“简单”,里面都是一系列的构造器,这些构造器相互调用,最后会调用一个具有全部参数的构造器。
ClassPathXmlApplicationContext的构造器用于完成IoC容器的初始化,实际上都是调用的父类的方法,容器初始化的代码基本上都是被它的父类所完成的,构造器的方法调用主要有三步:
- super(parent):设置父上下文容器(默认为null),以及设置resource资源加载器和解析器;
- setConfigLocations:设置本地配置文件信息,处理替换${}占位符;
- refresh:刷新上下文,真正的创建容器,这是核心方法。
因此,ClassPathXmlApplicationContext容器的初始化源码学习可以从这三个方法入手,本次我们讲学习前两步,最后一个refresh核心步骤的源码将在第二篇文章中介绍!
/**
* 使用给定的父级创建新的ClassPathXmlApplicationContext,从给定的XML文件加载配置。
*
* @param configLocations 资源文件位置数组
* @param refresh 是否自动刷新上下文、加载所有 bean 定义和创建所有单例bean。
* true 是,默认为true;false 否,在进一步配置上下文后手动刷新。
* @param parent 父上下文容器,默认null
* @throws BeansException 如果上下文创建失败
*/
public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
/*1 设置父上下文容器,默认为null*/
super(parent);
/*
* 2 创建环境对象ConfigurableEnvironment,
* 处理替换ClassPathXmlApplicationContext传入的字符串中${}占位符
*/
setConfigLocations(configLocations);
/*是否刷新上下文*/
if (refresh) {
/*3 刷新上下文,这是核心方法*/
refresh();
}
}
4.2 super(parent) 设置父容器
super(parent)方法主要目的有两个:
- 设置资源模式解析器(resourcePatternResolver)和资源加载器(resourceLoader),为后面的资源(配置文件)解析做准备。
- 设置父上下文容器,将父级容器运行时环境合并到当前(子)容器运行时环境,默认没有父上下文容器,因此不需要关心。
super(parent)方法明显是调用了父类的构造器,我们应该向上追踪!
它的直接父级就是AbstractXmlApplicationContext:
/**
* ClassPathXmlApplicationContext的直接父类AbstractXmlApplicationContext的构造器
*/
public AbstractXmlApplicationContext(@Nullable ApplicationContext parent) {
//调用父类AbstractRefreshableConfigApplicationContext的构造器
super(parent);
}
继续向上是AbstractRefreshableConfigApplicationContext:
/**
* AbstractRefreshableConfigApplicationContext的构造器
*/
public AbstractRefreshableConfigApplicationContext(@Nullable ApplicationContext parent) {
//调用父类AbstractRefreshableApplicationContext的构造器
super(parent);
}
继续向上是AbstractRefreshableApplicationContext:
/**
* AbstractRefreshableApplicationContext的构造器
*/
public AbstractRefreshableApplicationContext(@Nullable ApplicationContext parent) {
//调用父类AbstractApplicationContext的构造器
super(parent);
}
最终调用调用AbstractApplicationContext的构造器:
/**
* 最后调用AbstractApplicationContext的构造器
*/
public AbstractApplicationContext(@Nullable ApplicationContext parent) {
//创建一个没有父级的新抽象应用程序上下文,并设置resourcePatternResolver
this();
//设置父级上下文
setParent(parent);
}
this()用于创建一个没有父级的新抽象应用程序上下文,以及设置resourcePatternResolver:
/**
* this()用于创建一个没有父级的新抽象应用程序上下文,设置resourcePatternResolver
*/
public AbstractApplicationContext() {
//对resourcePatternResolver 变量赋值,获取一个资源模式解析器
this.resourcePatternResolver = getResourcePatternResolver();
}
/**
* 位于AbstractApplicationContext中的方法
* 获取一个资源模式解析器
*/
protected ResourcePatternResolver getResourcePatternResolver() {
//将AbstractApplicationContext自身作为ResourceLoader传递给了PathMatchingResourcePatternResolver;
//返回一个PathMatchingResourcePatternResolver对象,提供了以classpath开头的Ant路径通配符加载资源获得Resource的方式
return new PathMatchingResourcePatternResolver(this);
}
/**
* 创建一个新的PathMatchingResourcePatternResolver
*
* @param resourceLoader 资源加载器
*/
public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
//对resourceLoader变量赋值,实际上就是AbstractApplicationContext对象
//因为AbstractApplicationContext继承了DefaultResourceLoader,它就是一个资源加载器
this.resourceLoader = resourceLoader;
}
setParent(parent)用于设置此应用程序上下文的父级,将父级容器运行时环境合并到当前(子)容器运行时环境,默认没有父上下文容器,因此不需要关心。
/**
* 设置此应用程序上下文的父级。
*
* @param parent 父级上下文,默认null
*/
@Override
public void setParent(@Nullable ApplicationContext parent) {
//对parent变量赋值,默认为null
this.parent = parent;
//如果不为null,那么设置父容器的环境变量Environment,即整个应用运行时的环境
if (parent != null) {
//获取父容器的环境对象
Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {
//获取子容器自己的环境对象,没有就创建并赋值给environment变量
//随后调用merge方法,将父级容器运行时环境合并到当前(子)容器运行时环境。
getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
}
}
}
4.3 setConfigLocations设置容器配置信息
setConfigLocations方法是在AbstractRefreshableConfigApplicationContext类中实现的。
在ClassPathXmlApplicationContext构造器中,我们传入了配置文件的路径字符串数组,这里的路径字符串中实际上是可以使用占位符的,用来传递系统和JVM中的环境变量,而setConfigLocations方法就是用来解析路径字符串中的${… : …}占位符的,并将它们解析替换成对应的值。
/**
* 位于AbstractRefreshableConfigApplicationContext类中的属性
* 配置文件路径字符串数组
*/
@Nullable
private String[] configLocations;
/**
* 位于AbstractRefreshableConfigApplicationContext类中的实现
* <p>
* 设置此应用程序上下文(application context)的配置文件位置
*
* @param locations 传递的配置文件路径字符串数组
*/
public void setConfigLocations(@Nullable String... locations) {
//如果locations不为null
if (locations != null) {
//Assert是Spring框架提供的的一个工具类,用于各种参数格式校验,这里是校验数组元素不能为null
Assert.noNullElements(locations, "Config locations must not be null");
//初始化configLocations数组,长度为传入参数数组的length
this.configLocations = new String[locations.length];
//遍历locations数组
for (int i = 0; i < locations.length; i++) {
//取出每一个配置路径字符串,并尝试替换字符串中的环境变量占位符,然后将结果设置到configLocations数组的对应位置中
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
//如果locations为null
else {
//那么configLocations也设置为null
this.configLocations = null;
}
}
4.3.1 resolvePath解析路径
在setConfigLocations中,会遍历我们传递每一个配置文件路径,然后使用resolvePath(locations[i]).trim()进行解析,该方法也是用来替换每一个路径字符串中的环境变量占位符${… : …}。
resolvePath方法同样位于AbstractRefreshableConfigApplicationContext类中。首先调用getEnvironment获取当前容器的Environment环境变量对象,然后通过Environment.resolveRequiredPlaceholders方法来解析路径字符串中的占位符。
/**
* 位于AbstractRefreshableConfigApplicationContext类中的实现
* <p>
* 解析给定的路径字符串,必要时将占位符替换为相应的环境属性值。
*
* @param path 传递的文件路径
* @return 解析后的的文件路径
*/
protected String resolvePath(String path) {
//1 getEnvironment方法获取(没有就创建)当前上下文的可配置环境变量对象(StandardEnvironment类型)
//2 随后resolveRequiredPlaceholders方法替换字符串path中的${}占位符
return getEnvironment().resolveRequiredPlaceholders(path);
}
4.3.1.1 getEnvironment获取环境变量对象
getEnvironment方法位于AbstractApplicationContext类中,用于获取当前上下文的可配置的环境变量对象environment。
该方法被调用时,会判断如果没有environment为null,那么就调用createEnvironment创建一个StandardEnvironment类型的环境变量对象,然后赋值给environment属性。
/**
* AbstractApplicationContext类中的实现
* 获取当前上下文的环境变量ConfigurableEnvironment
*/
@Override
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
//没有指定,则创建
this.environment = createEnvironment();
}
return this.environment;
}
/**
* AbstractApplicationContext类中的实现
* 创建一个StandardEnvironment类型的环境变量,可以配置
*/
protected ConfigurableEnvironment createEnvironment() {
return new StandardEnvironment();
}
4.3.1.1.1 StandardEnvironment 标准环境对象
StandardEnvironment类相关的uml类图如下:
这里的类还不算多,咱们一起来简单看看,它们都是干什么的,切忌死记硬背!
最顶级的接口是org.springframework.core.env.PropertyResolver接口,又称“属性解析器”。该接口用于对于底层属性源进行属性解析,提供了通过属性key获取属性value以及解析占位符等基本功能;
org.springframework.core.env.ConfigurablePropertyResolver接口继承了PropertyResolver接口,该接口扩了PropertyResolver的功能,如设置的占位符格式、配合org.springframework.core.convert.ConversionService用来转换获取到的属性value的类型等功能。
org.springframework.core.env.AbstractPropertyResolver是ConfigurablePropertyResolver接口的抽象实现类,实现了ConfigurablePropertyResolver接口的所有抽象方法,是用于针对任何基础属性源解析属性的抽象基类,定义了默认占位符属性格式“${…:…}”,以及其他比如转换服务属性、必备属性。
org.springframework.core.env.PropertySourcesPropertyResolver类继承了AbstractPropertyResolver抽象类,作为非web环境下的PropertyResolver体系的默认实现也是唯一实现也是,它将PropertySources(类型为PropertySource)属性源集合作为属性来源,通过顺序遍历每一个PropertySource属性源,返回第一个找到(不为null)的属性key对应的属性value。
org.springframework.core.env.Environment接口作为当前应用运行环境的抽象,抽象出了应用程序环境的两个关键点:“profiles“和”properties”。
properties是指一些应用中的一些属性,它们可能来外部properties配置文件、系统环境变量、JVM环境变量、JNDI、Servlet 上下文参数、临时(ad-hoc)属性对象、Maps等。Environment接口继承了PropertyResolver接口,继承了对属性进行解析、替换等操作的功能。
profiles是指一套配置 ,Environment接口新增了获取、检测所使用的“profile”的功能。一个的Sping项目通常有多套运行环境test、uat、prod……,每一套运行环境可能有不同的配置,它们被称为一个个的profile,在运行时我们可以指定激活某些profile环境,该环境下的配置就会生效,而其他环境下的配置信息则不会生效,在项目中我们只需要配置就行了,而底层的源码都是由Environment及其子接口的方法控制。
org.springframework.core.env.ConfigurableEnvironment继承了Environment接口,从名字也能看出来(Spring的类命名却是非常好理解,值得学习),它是一个可配置的Environment,提供了修改和设置profiles,以及获取系统属性、系统环境变量的一系列方法。
org.springframework.core.env.AbstractEnvironment作为ConfigurableEnvironment抽象实现类,实现了全部抽象方法并且为子类留下了可扩展自定义属性来源的方法customizePropertySources,提供了profile以及系统相关属性的属性名的定义,以及提供了相应的存储容器的定义。后面我们马上就会看到的这些属性名,说不定你此前就真的见过,或者用过!
org.springframework.core.env.StandardEnvironment继承了AbstractEnvironment抽象类,作为非web环境下的默认标准实现也是唯一实现(web环境下的实现是StandardServletEnvironment)。重写了customizePropertySources方法,在创建时就将JVM系统属性和系统环境属性存入资源容器中。
4.3.1.1.2 new StandardEnvironment
下面来看看相关的重点源码,即new StandardEnvironment标准环境对象的时候发生了什么!
这里,我们要从父类AbstractEnvironment开始看,因为子类实例化时会首先调用父类的构造器!在AbstractEnvironment中,你可以看到如下属性:
//-------------AbstractEnvironment中的一系列属性和方法-------------
/**
* 是否不允许Spring访问系统环境变量的属性名称.
* 属性值默认为false,表示允许,
* 设置为true,表示不允许,即从不允许通过System.getenv()获取系统环境变量。
*/
public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
/**
* 要设置为指定活动配置文件的属性名称
* 属性值可以使用,分隔
*/
public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
/**
* 要设置为指定默认情况下处于活动状态的配置文件的属性名称。
* 属性值可以使用,分隔
*/
public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";
/**
* 如果未显式设置默认配置文件名称,并且未显式设置活动配置文件名称,则默认情况下使用“default”作为自动激活配置文件名。
*/
protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";
/**
* 活动Profile名集合
*/
private final Set<String> activeProfiles = new LinkedHashSet<>();
/**
* 默认Profile名集合
*/
private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());
/**
* 可修改属性源,实际类型为MutablePropertySources。可以看作一个列表,用来存放PropertySource属性源
*/
private final MutablePropertySources propertySources = new MutablePropertySources();
/**
* 可配置的属性解析器,实际类型为PropertySourcesPropertyResolver类型,用于解析属性源中的属性
*/
private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
如果使用过Spring Boot开发项目,那么我们对里面的一些属性名一定不会陌生,Spring Boot能直接配置spring.profiles.active、spring.profiles.default等等属性来设置spring profile环境,只不过目前这个setConfigLocations阶段还不需要用到这几个属性,后面就会用到了。
另外,一些容器比如propertySources 、propertyResolver等都会在该类构造器被调用时被同时初始化。我们再来看看AbstractEnvironment的构造器,子类StandardEnvironment在初始化时默认会调用父类构造器,这才是我们目前应该关心的:
/**
* AbstractEnvironment的构造器
* 子类StandardEnvironment在初始化时默认会调用父类构造器
*/
public AbstractEnvironment() {
customizePropertySources(this.propertySources);
}
/**
* AbstractEnvironment的方法
* 该方法被子类StandardEnvironment重写了,因此会调用子类重写的方法
*/
protected void customizePropertySources(MutablePropertySources propertySources) {
}
4.3.1.1.3 customizePropertySources添加属性源
然后我们再来看看StandardEnvironment的相关扩展属性定义以及重写的customizePropertySources方法,该方法用于子类自定义属性来源:
//-------------StandardEnvironment中的一系列属性和方法-------------
/**
* 系统环境属性源名称
* 内部的属性键值对通过System.getenv()方法获取
*/
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/**
* JVM系统属性 属性源名称
* 内部的属性键值对通过System.getProperties()方法获取
*/
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
/**
* 向资源列表加载JVM 系统属性属性和系统环境属性
* systemProperties属性优先于systemEnvironment属性
*/
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
//先加载systemProperties,后加载systemEnvironment,因此spring 容器在遍历查找环境属性配置时会优先从System Properties中查找
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
到这里,我们能够明白,getEnvironment方法在获取环境变量对象时,还会默认将JVM系统属性和系统环境属性加载到属性源集合中,由此,我们可以轻易的联想到,实际上这两个属性源就是后面resolveRequiredPlaceholders方法在解析占位符的时候用来查询属性的属性源。
4.3.1.2 resolveRequiredPlaceholders严格解析占位符
在获取到environment环境变量对象之后,将调用environment(StandardEnvironment)的resolveRequiredPlaceholders方法,实际上该方法同样是AbstractEnvironment类中的方法:
/**
1. StandardEnvironment对象调用resolveRequiredPlaceholders方法,实际上是位于父类AbstractEnvironment类中的方法
2. <p>
3. 解析必须的给定文本中的占位符,默认占位符语法规则为 ${...}
4. 5. @param text 原始的字符串文本
6. @return 已解析的字符串
7. @throws IllegalArgumentException 如果给定文本为null
*/
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
//继续调用属性解析器的resolveRequiredPlaceholders方法解析、替换给定文本中的${}占位符(默认)
//propertyResolver属性解析器在创建环境对象时就被创建了,并且被设置了JVM和系统属性源
return this.propertyResolver.resolveRequiredPlaceholders(text);
}
该方法继续调用propertyResolver(PropertySourcesPropertyResolver)的resolveRequiredPlaceholders方法,实际上该方法同样是PropertySourcesPropertyResolver的父类AbstractPropertyResolver中实现的方法,该方法主要做两件事:
- 创建一个属性占位符解析辅助对象PropertyPlaceholderHelper,这个对象就是真正用来解析占位符的对象;
- 调用doResolvePlaceholders方法,传入文本以及helper对象,用于解析占位符;
/**
* 该方法是位于PropertySourcesPropertyResolver的父类AbstractPropertyResolver中的方法
* <p>
* 解析必须的给定文本中的占位符,默认占位符语法规则为 ${...}
*
* @param text 原始的字符串文本
* @return 已解析的字符串
* @throws IllegalArgumentException 如果给定文本为null
*/
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
//创建一个属性占位符解析辅助对象 PropertyPlaceholderHelper
if (this.strictHelper == null) {
this.strictHelper = createPlaceholderHelper(false);
}
//调用doResolvePlaceholders方法,解析占位符
return doResolvePlaceholders(text, this.strictHelper);
}
4.3.1.2.1 createPlaceholderHelper创建解析器
createPlaceholderHelper是AbstractPropertyResolver中的方法,用于创建默认的PropertyPlaceholderHelper对象。
PropertyPlaceholderHelper专门用于将字符串里的占位符,替换为对应的值。对应的类就是一个独立的类,没有任何的继承关系,不依赖其他类,我们自己也可以可以单独使用。
/**
* AbstractPropertyResolver中的方法
* <p>
* 创建属性占位符辅助对象
*
* @param ignoreUnresolvablePlaceholders 指示是否应忽略无法解析的占位符,true 忽略 false 抛出异常
* @return
*/
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
//调用PropertyPlaceholderHelper的构造器
//传递默认的占位符解析格式 前缀"${" 后缀"}" 占位符变量和默认值的分隔符":"
//ignoreUnresolvablePlaceholders=true,即在无法解析占位符的时候忽略
return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
this.valueSeparator, ignoreUnresolvablePlaceholders);
}
可以看到createPlaceholderHelper方法还是比简单的,直接调用PropertyPlaceholderHelper的构造器,然后传递AbstractPropertyResolver类自己的属性作为PropertyPlaceholderHelper的属性,同时设置不忽略没有解析到的占位符,即如果没有解析到value则抛出异常!
//-------------AbstractPropertyResolver中的一系列占位符相关属性-------------
//使用了SystemPropertyUtils系统属性工具类中的常量值
/**
* 占位符开头的前缀
*/
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
/**
* 占位符结尾的后缀
*/
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
/**
* 占位符变量和关联的默认值之间的分隔字符(如果有)
*/
@Nullable
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
SystemPropertyUtils系统属性工具类定义了默认占位符的语法“${… : …}”,并且提供了现成的静态helper对象,这个工具类,我们也可以单独使用:
//-------------SystemPropertyUtils系统属性工具类中的常量值-------------
/**
* Prefix for system property placeholders: "${".
*/
public static final String PLACEHOLDER_PREFIX = "${";
/**
* Suffix for system property placeholders: "}".
*/
public static final String PLACEHOLDER_SUFFIX = "}";
/**
* Value separator for system property placeholders: ":".
*/
public static final String VALUE_SEPARATOR = ":";
调用的PropertyPlaceholderHelper的构造器以及相关属性如下:
//-------------PropertyPlaceholderHelper相关属性和构造器-------------
private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<>(4);
static {
wellKnownSimplePrefixes.put("}", "{");
wellKnownSimplePrefixes.put("]", "[");
wellKnownSimplePrefixes.put(")", "(");
}
/**
* 占位符前缀,默认为"${"
*/
private final String placeholderPrefix;
/**
* 占位符后缀,默认为"}"
*/
private final String placeholderSuffix;
/**
* 简单的占位符前缀,默认为"{",嵌套的占位符中有效比如${xx{yy}}
*/
private final String simplePrefix;
/**
* 占位符变量和默认值的分隔符,默认为":"
*/
@Nullable
private final String valueSeparator;
/**
* 是否忽略无法解析的占位符,默认false,将会抛出异常,如果为true,那么跳过这个占位符
*/
private final boolean ignoreUnresolvablePlaceholders;
/**
* 创建属性占位符辅助对象 设置占位符的解析格式 默认就是 ${...:...}
*
* @param placeholderPrefix 表示占位符开头的前缀
* @param placeholderSuffix 表示占位符结尾的后缀
* @param valueSeparator 占位符变量和关联的默认值之间的分隔字符(如果有)
* @param ignoreUnresolvablePlaceholders 指示是否应忽略无法解析的占位符,true 忽略 false 抛出异常
*/
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {
//设置相关占位符属性
Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
this.placeholderPrefix = placeholderPrefix;
this.placeholderSuffix = placeholderSuffix;
String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
this.simplePrefix = simplePrefixForSuffix;
} else {
this.simplePrefix = this.placeholderPrefix;
}
this.valueSeparator = valueSeparator;
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
}
现在我们能明白,createPlaceholderHelper在创建属性占位符解析器帮助类的时候,同时设置了默认的占位符语法格式“${… : …}”。其实我们还能想到,Spring作为一个优秀的框架,肯定给我们留有“后门”,或许我们可以自定义占位符格式(对于setConfigLocations中的占位符解析格式不能自定义,只能是${…:…},对于自己加载的properties配置文件的占位符属性的解析才能自定义)!
4.3.1.3 doResolvePlaceholders解析占位符
创建了helper对象之后,继续调用AbstractPropertyResolver中的doResolvePlaceholders方法,这个方法是干什么的呢?为什么不直接调用helper的方法呢?
/**
* AbstractPropertyResolver中的方法
* <p>
* 实际上内部调用属性占位符辅助对象的replacePlaceholders方法,解析占位符
*/
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
//可以看到,使用了Java8的新特性:方法引用,因此Spring 5.x需要的JDK版本至少是JDK1.8
return helper.replacePlaceholders(text, this::getPropertyAsRawString);
//这个方法引用有点绕,表示resolvePlaceholder方法调用当前PropertySourcesPropertyResolver对象的getPropertyAsRawString方法
//使用普通匿名内部类格式如下:
//return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {
// @Override
// public String resolvePlaceholder(String key) {
// return getPropertyAsRawString(key);
// }
//});
}
通过源码我们才能明白,内部实际上就是通过helper调用的方法,但是在参数中我们能发现这里使用了一个非常巧妙的方法引用this::getPropertyAsRawString。
helper.replacePlaceholders的第二个参数要求一个PlaceholderResolver实例,这个PlaceholderResolver作为一个函数式策略接口,有一个用于将提取出来的占位符解析为对应属性值的resolvePlaceholder抽象方法。
/**
* 位于PropertyPlaceholderHelper类中的PlaceholderResolver接口
* 用于解析占位符名字的策略接口
*/
@FunctionalInterface
public interface PlaceholderResolver {
/**
* 将提供的占位符名称解析为替换值。
* @param placeholderName 要解析的占位符的名称
* @return 被替换的值,如果无法替换,则返回null
*/
@Nullable
String resolvePlaceholder(String placeholderName);
}
这在里,这个接口的实现是调用PropertySourcesPropertyResolver对象的getPropertyAsRawString方法,因此可以根据Java8的上下文推导(因为调用doResolvePlaceholders方法的实际类型就是一个PropertySourcesPropertyResolver对象),从而改写成为方法引用。
使用普通匿名内部类格式如下,这样看来,使用方法引用是不是简单得多?
return helper.replacePlaceholders(text, new
PropertyPlaceholderHelper.PlaceholderResolver() {
@Override
public String resolvePlaceholder(String key) {
return getPropertyAsRawString(key);
}
});
4.3.1.3.1 replacePlaceholders替换占位符
终于到了最终的方法了,这个而方法就是用来真正解析、替换占位符的方法,位于PropertyPlaceholderHelper类中。
/**
1. PropertyPlaceholderHelper类中的方法
2. <p>
3. 替换所有的占位符为对应的值
4. 5. @param value 包含要替换的占位符的值
6. @param placeholderResolver 占位符解析器
7. @return 替换之后的值
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
//value不能为null
Assert.notNull(value, "'value' must not be null");
//继续调用parseStringValue方法
return parseStringValue(value, placeholderResolver, null);
}
可惜,里面还调用了parseStringValue方法,这个方法才是“真正”的用于解析占位符的方法,大概步骤就是:
- 首先递归的调用parseStringValue方法解析占位符key,中途将解析出来的占位符变量添加到visitedPlaceholders临时集合中,用于防止循环占位符引用,直到最里面的占位符变量placeholder;
- 然后从里向外调用placeholderResolver.resolvePlaceholder(placeholder)方法,通过占位符变量,从属性源中获取对应的属性值propVal,可能获取不到,那么判断是否有默认值,有的话尝试解析、获取默认值,同样赋给propVal;
- 如果获取到的propVal值(无论是属性值还是默认值)不为null;
- 那么继续对这个propVal值进行递归的调用parseStringValue方法解析value中的占位符;
- 直到value中也没有了占位符,此时才进行占位符的替换:使用获取到的值替换给定文本中的占位符;随后获取下一个占位符的索引,从visitedPlaceholders集合移除已解析的占位符变量,继续下一次循环;
- 如果获取到的propVal值为null,这就是解析失败的情况,那么判断是否忽略失败,如果是的话,那么解析下一个占位符,同时将当前占位符整体作为值,如果不能忽略,那么抛出异常。
简单的说:占位符可以嵌套,因此必须递归到最里面的占位符开始解析;同时,解析出来的对应的值(属性值或者默认值)也可能嵌套包含占位符,因此必须对值也进行递归的解析。
/**
* PropertyPlaceholderHelper类中的方法
* <p>
* 最终调用的方法,递归的解析字符串中的占位符
*
* @param value 包含要替换的占位符的值
* @param placeholderResolver 占位符解析器
* @param visitedPlaceholders 访问过的占位符,用于递归向后推进,同时避免递归解析导致的死循环
* @return 替换之后的值
*/
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
//获取第一个占位符前缀索引值startIndex
int startIndex = value.indexOf(this.placeholderPrefix);
//如果startIndex为-1,表示没有占位符,不需要继续解析,直接返回原值
if (startIndex == -1) {
return value;
}
//到这里,表示存在占位符,开始解析
//先创建一个StringBuilder,传入value
StringBuilder result = new StringBuilder(value);
/*如果startIndex不为-1,那么一直循环,直到将全部占位符都解析完毕或者抛出异常*/
while (startIndex != -1) {
//获取对应占位符结束位置索引
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
//获取开始索引和结束索引之间的占位符变量
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
//添加到已解析占位符集合中
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
//如果没有添加成功,这说明出现了同名的的嵌套占位符,这类似于循环引用,那么抛出异常
//主要出现在value的递归解析中,后面会有案例
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
//递归调用parseStringValue,用于分析占位符中的占位符……,这里是用于分析key
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
//到这里表示递归完毕,占位符中没有占位符了
//找到最底层的占位符变量之后,调用placeholderResolver的resolvePlaceholder(placeholder)方法,根据lambda表达式
//实际上就是调用PropertySourcesPropertyResolver对象的getPropertyAsRawString方法,然后又会调用
//PropertySourcesPropertyResolver.getProperty方法中通过占位符变量找出对应的值
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
//如果没找到(值为null),并且默认值分隔符不为null,那么尝试获取默认值
if (propVal == null && this.valueSeparator != null) {
//分隔符的起始索引
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
//实际的占位符变量
String actualPlaceholder = placeholder.substring(0, separatorIndex);
//默认值
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
//尝试从属性源中查找占位符变量对应的属性值
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
//如果没找到,那么就使用默认值了
if (propVal == null) {
propVal = defaultValue;
}
}
}
//到这一步,如果值不为null,说明解析到了值,无论是默认值还是属性值
if (propVal != null) {
//继续递归调用parseStringValue,用于分析占位符的值中的占位符……,这里是用于分析value
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
//到这里表示递归完毕,占位符的值中没有占位符了
//这里将占位符替换成获取到的值
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
//获取下一个占位符的起始索引
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
//如果值为null,并且默认值分隔符为null,这就是解析失败的情况,判断是不是忽略,如果是的话,那么解析下一个占位符,同时将当前占位符整体作为值
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
//如果不能忽略,那么抛出异常
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
//移除已解析的占位符变量
visitedPlaceholders.remove(originalPlaceholder);
} else {
//出现问题,没找到后缀,下一次循环直接退出
startIndex = -1;
}
}
//返回解析后的结果
return result.toString();
}
4.3.1.3.2 getPropertyAsRawString获取字符串属性值
我们此前说过placeholderResolver.resolvePlaceholder(placeholder)方法,实际上是调用的PropertySourcesPropertyResolver类中的getPropertyAsRawString方法的逻辑,现在一起来看看该方法!
/**
* PropertySourcesPropertyResolver类中的方法
* <p>
* 将指定的属性检索为原始字符串,即不解析嵌套占位符。
*
* @param key 占位符变量
* @return 解析结果
*/
@Override
@Nullable
protected String getPropertyAsRawString(String key) {
//调用getProperty方法
return getProperty(key, String.class, false);
}
/**
* PropertySourcesPropertyResolver类中的方法
* <p>
* 通过key获取属性值
*
* @param key 占位符变量,即属性key
* @param targetValueType 返回值类型
* @param resolveNestedPlaceholders 是否解析嵌套占位符
* @return 解析后的值
*/
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
//如果属性源不为null,在调用getEnvironment方法获取环境对象的时候propertySources就被初始化了,肯定是不为null的
//并且还通过customizePropertySources方法被设置了系统(systemEnvironment)和JVM(systemProperties)属性源
if (this.propertySources != null) {
//propertySources实现了Iterable,是一个可迭代的对象,相当于一个列表
//每一个列表元素代表一个属性源,这个属性源就相当于一个map,存放的是属性键值对
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
//获取属性源里key对应的value
Object value = propertySource.getProperty(key);
//选用第一个不为null的匹配key的属性值
if (value != null) {
//如果需要递归解析value中的嵌套占位符比如${},并且value属于String类型
if (resolveNestedPlaceholders && value instanceof String) {
//那么递归解析
value = resolveNestedPlaceholders((String) value);
}
//记录日志
logKeyFound(key, propertySource, value);
//调用父类AbstractPropertyResolver中的方法:如有必要,将给定值转换为指定的目标类型并返回
return convertValueIfNecessary(value, targetValueType);
}
}
}
//记录日志
if (logger.isTraceEnabled()) {
logger.trace("Could not find key '" + key + "' in any property source");
}
//propertySources为null,那么直接返回null,因为没有任何属性源
return null;
}
简单的说,就是遍历propertySources属性源集合,尝试获取属性源里key对应的value,选用第一个不为null的匹配key的属性值。因此,如果有同样的配置,那么先加载systemProperties,后加载systemEnvironment,因此spring 容器在遍历查找环境属性配置时是顺序遍历的,而systemProperties属性源被最先放入集合之中。
4.3.1.3.2.1 convertValueIfNecessary属性值类型转换
找到value之后,最后会调用convertValueIfNecessary方法,这个方法用于将字符串的value 转换为指定的目标类型。该方法同样是PropertySourcesPropertyResolver的父类AbstractPropertyResolver实现的方法:
/**
* 类AbstractPropertyResolver中的方法
* <p>
* 如有必要,将给定值转换为指定的目标类型。
*
* @param value 原始属性值
* @param targetType 属性检索的指定目标类型
* @return 转换后的值,如果不需要转换则返回原始值
*/
@SuppressWarnings("unchecked")
@Nullable
protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
//如果指定目标类型为null,那么返回原始值
if (targetType == null) {
return (T) value;
}
//获取Spring转换服务对象,这也是一个组件,用于转换类型
ConversionService conversionServiceToUse = this.conversionService;
//如果为null
if (conversionServiceToUse == null) {
// Avoid initialization of shared DefaultConversionService if
// no standard type conversion is needed in the first place...
//是否等于给定类型,一般都不相等,除了字符串类型
if (ClassUtils.isAssignableValue(targetType, value)) {
return (T) value;
}
//调用DefaultConversionService的静态方法获取共享的转换服务实例,这里是单例模式的懒汉模式应用
conversionServiceToUse = DefaultConversionService.getSharedInstance();
}
//实际上就是调用DefaultConversionService的convert方法,将会查找适合的转换器,并尝试转换
return conversionServiceToUse.convert(value, targetType);
}
内部将会使用DefaultConversionService提供的一系列默认转换器,进行该类型转换,而这里的DefaultConversionService就是一个单例模式的应用:
//-------------DefaultConversionService相关属性和构造器-------------
/**
* 保存的单例实例
*/
@Nullable
private static volatile DefaultConversionService sharedInstance;
/**
* 构造器
*/
public DefaultConversionService() {
//在构造器中添加超过20个默认的通用转换器
addDefaultConverters(this);
}
/**
* 返回共享的默认值转换服务实例,根据需要懒加载,单例模式-懒汉模式的应用
*
* @return 共享的转换服务实例
*/
public static ConversionService getSharedInstance() {
DefaultConversionService cs = sharedInstance;
if (cs == null) {
//加锁
synchronized (DefaultConversionService.class) {
cs = sharedInstance;
if (cs == null) {
//调用构造器
cs = new DefaultConversionService();
sharedInstance = cs;
}
}
}
return cs;
}
DefaultConversionService提供了三十几个转换器:
如果不满足,我们也可以实现GenericConverter接口,自定义转换器然后存入进去即可,在这里,在setConfigLocations方法中,返回值都是String的,因此不需要其他转换器。
4.4 相关案例
4.4.1 查看系统和JVM环境变量
前面说过,系统环境和JVM环境变量分别使用System.getenv()和System.getProperties()可以获取,那么我们来看看里面到底有哪些属性!
/**
* 查看JVM系统属性和系统环境属性的值
*/
@Test
public void test() {
//系统环境属性
Map<String, String> map = System.getenv();
System.out.println(map);
//JVM系统属性
Properties properties = System.getProperties();
System.out.println(properties);
}
从结果看起来,这些属性确实比较底层:
{USERDOMAIN_ROAMINGPROFILE=DESKTOP-8Q842HN, LOCALAPPDATA=C:\Users\lx\AppData\Local, PROCESSOR_LEVEL=6, USERDOMAIN=DESKTOP-8Q842HN, ………………}
{java.runtime.name=Java(TM) SE Runtime Environment, sun.boot.library.path=C:\Java\jdk1.8.0_144\jre\bin, java.vm.version=25.144-b01, ………………}
4.4.2 设置环境变量
系统环境变量会影响一个系统平台的所有程序,不可在Java程序中设置和修改,JVM环境变量只会影响一个JVM实例,可以在Java程序中设置和修改。我们使用System.setProperty(key,value)修改和设置JVM环境变量。
/**
* 设置环境变量
*/
@Test
public void setProp() {
System.setProperty("x","config");
//初始化容器
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-${x}.xml");
}
debug测试,即可看到我们设置的环境变量:
4.4.3 循环环境变量引用
由于getProperty方法会递归解析环境变量,因此发生循环引用时就造成了死循环,因此Spring的处理是直接抛出异常。
循环环境变量引用通常是key和value的相互指向造成的。
@Test
public void circularPlaceholderReference() {
//config key 对的value为${config},在解析value时又会找到config key,造成循环变量引用…………
System.setProperty("config", "${config}");
//初始化容器
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-${config}.xml");
}
运行之后会抛出:
java.lang.IllegalArgumentException: Circular placeholder reference 'config' in property definitions
4.4.4 设置profile
我们可以在JVM环境变量中设置AbstractEnvironment类中的属性,比如设置spring.profiles.active来设置激活的profile!
三个配置文件spring-config.xml,spring-config-dev.xml,spring-config-uat.xml:
<!--spring-config.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-->
<bean class="com.spring.source.FirstSpringSource" name="firstSpringSource">
<property name="str" value="不设置profile"/>
</bean>
</beans>
<!--spring-config-dev.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" profile="dev">
<!--dev环境-->
<!--定义bean-->
<bean class="com.spring.source.FirstSpringSource" name="firstSpringSource">
<property name="str" value="dev"/>
</bean>
</beans>
<!--spring-config-uat.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" profile="uat">
<!--uat环境-->
<!--定义bean-->
<bean class="com.spring.source.FirstSpringSource" name="firstSpringSource">
<property name="str" value="uat"/>
</bean>
</beans>
测试类:
/**
* @author lx
*/
public class FirstSpringSource {
public FirstSpringSource() {
System.out.println("FirstSpringSource init");
}
public void methodCall(){
System.out.println("methodCall");
}
public String str;
public void setStr(String str) {
this.str = str;
}
@Override
public String toString() {
return "FirstSpringSource{" +
"str='" + str + '\'' +
'}';
}
}
测试:
@Test
public void profile() {
System.setProperty("config", "config");
//设置激活的profile
System.setProperty("spring.profiles.active", "uat");
//初始化容器
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-${config}.xml",
"spring-${config}-dev.xml", "spring-${config}-uat.xml");
System.out.println(ac.getBean("firstSpringSource"));
}
设置不同的activeProfile可以得到不同的对象:
FirstSpringSource init
FirstSpringSource{str='uat'}
你也可以激活多个profile,使用,分隔,同名的bean不会报错,但是会覆盖,通常后面解析的配置会覆盖前面解析配置。
5 时序图与小结
setConfigLocations
用于解析我们传入的路径字符串,将其中的${}占位符替换成对应的值,查找的属性源范围就是JVM环境和系统环境两个属性原,占位符可以嵌套,value中也可以存在占位符,如果存在循环占位符,那么抛出异常,如果占位符变量没有找到对应的值,那么同样抛出异常。
super(parent)就不说了比较简单,下面是setConfigLocations 的时序图:
刚开始学习Spring源码,我们就接触到了调用链路很深的setConfigLocations方法,但这就是Spring的特点,因为Spring具有非常强的扩展性,每一个功能点基本上都有对应的接口和实现,并且它的源码调用链虽然很深,但是难度并不是很高,真正的代码量也不是很多,如果理清了类与类之间的关系,那么应该不难理解。还有一点值得我们学的就是Spring对于类和接口的命名,能够让人大概的明白某个类或者接口是干什么的,这就是所谓的见其名知其意!
另外,Spring框架的源码具有很强的复用性,本次学习的这些类和方法并不是只有在setConfigLocations方法中会使用到,比如我们学习的Spring容器环境变量解析中,对于占位符的解析功能,在其他地方也会用到,比如@Value中的${}、XML中的${},只不过它们的配置源是从外部properties文件中加载的,但是解析占位符的原理都是一致的,后面我们会学习到,这些占位符的解析,虽然具有不同的调用链路,具有不同的“起点”,但是最终它们都会指向PropertyPlaceholderHelper类的parseStringValue方法,其他的比如自动转换类型服务,也都是指向convertValueIfNecessary方法。
另外要知道,仅仅Spring springframework就有几十万代码量(包括注释)!某些类或者方法,我们只需要知道它是做什么的就行了,对于源码该放弃的就放弃,不要太钻牛角尖了,不然就再也出不来了!
总结一下,本次学习了基于XML的ClassPathXmlApplicationContext IoC容器初始化的前两步:
super(parent)
:设置父容器,将父级容器运行时环境合并到当前(子)容器运行时环境,默认没有父上下文容器,因此不需要关心。同时还会设置资源模式解析器(resourcePatternResolver)和资源加载器(resourceLoader),为后面的资源(配置文件)解析做准备。setConfigLocations
:解析配置文件路径字符串,替换${… : …}占位符为真实的值,同样是为后面的资源(配置文件)解析做准备。
实际这前两步都是辅助步骤,最后一步refresh方法才是容器真正的初始化方法,包括配置文件加载、bean解析、bean初始化等等复杂的步骤,refresh方法我们留到下一篇文章再说!
相关文章:
https://spring.io/
Spring 5.x 学习
Spring 5.x 源码
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!