Spring容器启动之扫描加载类实现原理
整理代码过程中极为痛苦,我要坚持。
你的坚持,终将美好
spring容器启动会执行AbstractApplicationContext#refresh()方法,该方法是spring启动核心方法,下面主要整理下spring容器在启动时,是如何将项目里面的类,扫描到并注册到spring容器中。
先看下refresh()方法概要时序图
本章主要讲述的是1.2步骤中,扫描并注册Bean的过程,先看下1.2源码
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
/**
* 注意下面这个方法,该方法就是扫描项目中的Bean,并将Bean注入到BeanDefinition中。
*/
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
return beanFactory;
}
接下来进入refreshBeanFactory方法源码
@Override
protected final void refreshBeanFactory() throws BeansException {
/**
* 查看是否启动的临时容器,存在就销毁关闭容器。为下面创建容器做准备。
*/
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
//创建容器并设置容器标示
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
/**
* 定制容器,里面是设置是否允许BeanDefinition重写和循环引用。默认false
*/
customizeBeanFactory(beanFactory);
/**
* 创建一个容器后,通过容器去加载BeanDefinition
*/
loadBeanDefinitions(beanFactory);
this.beanFactory = beanFactory;
}
}
进入loadBeanDefinitions(beanFactory) 源码
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
//为BeanFactory创建一个XmlBeanDefinitionReader实例,
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
//设置BeanDifinitionReader环境遍历、加载资源器、资源解析器。
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
/**
*初始化Reader,校验模式,自动校验or不校验。为后续的解析docment做初始化,配置文件xml是按照DTD还是XSD方式解析。默认spring按照XSD配置文件,
* XSD与DTO区别:DTD :xml文件会有一行以<!DOCTYPE 开头的标示 eg:<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
* XSD的xml文件是没有那个标示。
*/
initBeanDefinitionReader(beanDefinitionReader);
/**
* 通过BeanDefinitionReader去加载bean定义。开始读取配置文件循环的获取配置文件里面的信息
*/
loadBeanDefinitions(beanDefinitionReader);
}
/**
* 通过XmlBeanDefinitionReader加载BeanDefinition
*
*/
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}
@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int counter = 0;
for (String location : locations) {
counter += loadBeanDefinitions(location);
}
return counter;
}
上述方法中:getConfigResource()是从ClassPathXmlApplicationContext构造器传入的指定配置文件位置,进而将配置文件读取到Resource中。
getConfigLocations() 是从AbstractRefreshableConfigApplicationContext构造器传入指定配置文件,如果没有设置文件路径,默认取 /WEB-INF/applicationContext.xml
之后将配置源循环调用
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
//删除一段不重要代码
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
int loadCount = loadBeanDefinitions(resources);
if (actualResources != null) {
for (Resource resource : resources) {
actualResources.add(resource);
}
}
return loadCount;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
Resource resource = resourceLoader.getResource(location);
int loadCount = loadBeanDefinitions(resource);
if (actualResources != null) {
actualResources.add(resource);
}
return loadCount;
}
}
真正的开始读取文件文件源码
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
注意try/catch里面的这个方法doLoadBeanDefinitions();Spring源码方法已do开始就代表真正去做某些事情,在这里就是真正的加载BeanDefinitions。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}
//删除了解析失败捕捉异常的catch
}
上述方法通过文件流以及xml类型去获取document(xml中的文本内容),registerBeanDefinitions() 方法就开始注册document中定义的属性内容
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
//注册文档中属性
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//记录本次注册bean的个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
终于到了解析标签的阶段了。doRegisterBeanDefinitions()
protected void doRegisterBeanDefinitions(Element root) {
//下面两段代码是创建BeanDefinition解析代表,并且解析初始化<beans>标签
//例如 获取beans标签中default-lazy-init、default-init-method等等属性,如果没有设置,spring会将这些属性设置默认值。具体默认属性请看BeanDefinitionParserDelegate类中的常量
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
//判断xml文件是否是默认的spring文件类型。即beans 为http://www.springframework.org/schema/beans
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
}
//前置处理方法,空方法,为子类提供前置方法。
preProcessXml(root);
//解析<beans>标签里面的标签
parseBeanDefinitions(root, this.delegate);
//后置处理方法,空方法,为子类提供后置方法。
postProcessXml(root);
this.delegate = parent;
}
接下来是开始解析beans标签中的属性方法源码
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
/**
* 检查xml文件root节点,是否是默认解析方式。
* 默认 http://www.springframework.org/schema/beans。
*/
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
/**
* 开始循环子节点一一解析。
*/
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
/**
* spring默认的几个标签。import/alias/bean/beans
* 只有上述的几个标签是走默认的解析方式。
*/
parseDefaultElement(ele, delegate);
}
else {
/**
* 定制标签解析方式。
*/
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
解析spring默认标签元素 parseDefaultElement()
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
//解析<import>标签
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
//解析<alias>标签
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
//解析<bean>标签
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
//解析<beans>标签
doRegisterBeanDefinitions(ele);
}
}
从上述源码中可以看出spring支持四个默认标签import/alias/bean/beans。
接下来分别看下上述解析4个标签的源代码 ,先看下解析import都做了什么事情?
/**
* Parse an "import" element and load the bean definitions
* from the given resource into the bean factory.
*/
protected void importBeanDefinitionResource(Element ele) {
//<import resource="classpath*:spring/spring-core.xml"/>
//获取import标签上的resource属性,如果为空,跳过。
String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
if (!StringUtils.hasText(location)) {
getReaderContext().error("Resource location must not be empty", ele);
return;
}
// Resolve system properties: e.g. "${user.dir}"
//解析resource属性上的表达式。
location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);
Set<Resource> actualResources = new LinkedHashSet<>(4);
// Discover whether the location is an absolute or relative URI
//默认加载路径方式为相对路径。
boolean absoluteLocation = false;
try {
//判断得出import标签引用的路径是绝对路径还是相对路径
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
}
// Absolute or relative?
if (absoluteLocation) {
// 绝对路径加载配置文件的方式。
try {
int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
}
}
else {
// No URL -> considering resource location as relative to the current file.
//相对路径加载方式。
try {
int importCount;
Resource relativeResource = getReaderContext().getResource().createRelative(location);
if (relativeResource.exists()) {
importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
actualResources.add(relativeResource);
}
else {
String baseLocation = getReaderContext().getResource().getURL().toString();
importCount = getReaderContext().getReader().loadBeanDefinitions(
StringUtils.applyRelativePath(baseLocation, location), actualResources);
}
}
}
Resource[] actResArray = actualResources.toArray(new Resource[0]);
getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}
先会判断是否为标签,importBeanDefinitionResource()方法主要负责将引入的配置文件再次通过XmlBeanDefinitionReader#loadBeanDefinitions()方法将这些配置文件再次加载进来,源码中会根据import标签中的路径是绝对路径还是相对路径分别走不同的加载文件方式。最后都会重新用相应reader#loadBeanDefinitions()方法去加载配置文件,达到加载import配置文件里面的Bean类定义以及相关信息。