bean是Spring中最核心的东西,那么我们先看bean的定义:
public class MyTestBean {
private String testStr = "testStr";
public String getTestStr() {
return testStr;
}
public void setTestStr(String testStr) {
this.testStr = testStr;
}
}
显然,bean并没有什么特别之处,接下来创建配置文件:
<?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 id="myTestBean" class="bean.MyTestBean"/>
</beans>
这样一来,我们就可以写测试代码了:
public static void main(String[] args) {
BeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("springApplication.xml"));
MyTestBean myTestBean = xmlBeanFactory.getBean("myTestBean", MyTestBean.class);
System.out.println(myTestBean.getTestStr());
}
直接使用BeanFactory作为容器对于Spring的使用并不常见,甚至XmlBeanFactory已经被废弃。在企业级开发中都会使用的是ApplicationContext,但是便于理解,我们从BeanFactory开始。
虽然我们可能没有看过Spring源码,但是我们肯定都用过Spring或者Springboot框架,我们应该猜的出来,这段测试代码无非就以下几点:
按照原始的思维方式,整个过程无非如此,但是作为一个风靡世界的源码显然不会这么简单。
我们打开spring-beans模块的源码可以发现,可以看到Spring用来巨多的代码来完成这个看似简单的功能。
首先我们需要梳理以下beans模块的结构,显然beans模块中我们只需要关注以下包:
在正式开始分析源码之前,有必要了解Spring中核心的两个类。
XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个bean加载的核心部分,是Spring注册以及加载bean的默认实现,而对于XmlBeanFactory与DefaultListableBeanFactory不同的地方是在XmlBeanFactory中使用了自定义的XMl读取器XmlBeanDefintionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口。下图为相关类图。
众所周知,UML类图是个好东西,可以帮助我们从全局角度了解某个类的脉络,而且在软件工程导论和软件需求分析中会考(刚考完…)。
首先,我们先简单地了解一下各个类的作用:
XmlBeanFactory对DefaultListableBeanFactory类进行了拓展,主要用于从XML文档中读取BEanDefinition,对于注册及获取bean都是继承的DefaultListableBeanFactory,唯独不同的个性化实现就是增加了XmlBeanDefinitionReader类型的reader属性,主要使用reader对资源文件进行读取和注册。
写到这里不由的想吐槽一下,这类也太多了吧!类名也太长了吧!
XML配置文件的读取是Spring中重要的功能(虽然现在已经很少使用XML配置文件了)
先了解XmlBeanDefinitonReader的脉络,在后续再深入了解。
接下来我们要开始详细探索Spring容器每个步骤的实现,首先要探究
BeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("springApplication.xml"));
这行代码背后的秘密,首先画一个时序图(画的好丑啊我。。。。)
Spring的配置文件读取是通过ClassPathResource进行封装的,比如ClassPathResource(“springApplication.xml”),那么ClassPathResource完成了什么功能呢?
在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(REULStreamHandler)来处理不同来源的资源的读取逻辑。Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口封装底层资源。
了解了配置文件封装相关的类以后,我们就可以开始debug看一下代码执行的流程。
BeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("springApplication.xml"));
1、我们在第一句上打断点,debug执行,步入ClassPathResource(“springApplication.xml”)
可以看到ClassPathResource只有一个参数的构造方法调用了两个参数的构造方法
public ClassPathResource(String path) {
this(path, (ClassLoader) null);
}
2、继续步入来到两个参数的构造方法:
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
Assert.notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
this.path = pathToUse;
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
显然这个这一步完成了ClassPathResource和classLoader的初始化,并将path中最前面的/删除。
3、我们步出后进入new XmlBeanFacorty(),可以看到XmlBeanFactory一个参数的构造方法调用了两个参数的构造方法:
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, null);
}
4、继续步入两个参数的构造方法,可以看到:
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
//parentBeanFactory为父类BeanFactory用于factory合并,可以为空,没太理解这
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
首先我们看第一句super(parentBeanFactory),我们继续步入,来到了父类DefaultListableBeanFactory,父类继续调用了父类的构造方法,来到AbstractAutowireCapableBeanFactory的构造方法,我们可以看到:
public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
this();
setParentBeanFactory(parentBeanFactory);
}
AbstractAutowireCapableBeanFactory一个参数的构造方法又调用了无参构造方法,我们步入看一眼无参构造:
public AbstractAutowireCapableBeanFactory() {
super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
if (IN_NATIVE_IMAGE) {
this.instantiationStrategy = new SimpleInstantiationStrategy();
}
else {
this.instantiationStrategy = new CglibSubclassingInstantiationStrategy();
}
}
这里有必要提及ignoreDependencyInterface方法,ignoreDependencyInterface主要功能是忽略给定接口的自动装配功能,什么意思呢?举例来说,当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。但是,某些情况下,B不会被初始化,其中一种情况就是实现了BeanNameAware接口,Spring是这么说的:忽略给定的依赖接口进行自动装配。
应用程序上下文通常将使用它来注册以其他方式解决的依赖关系,例如通过BeanFactoryAware的BeanFactory或通过ApplicationContextAware的ApplicationContext。
默认情况下,仅BeanFactoryAware接口被忽略。 要忽略其他类型,请为每种类型调用此方法。
5、回到XmlBeanFactory的两个参数的构造方法,显然this.reader.loadBeanDefinitions(resource);才是资源加载的真正实现,也是我们的分析的重点之一。
书接上一段,之前在XmlBeanFactory的构造方法中调用了XmlBeanDefinitionReader类型的reader提供的loadBeanDefinitions方法,显然,这句代码是整个资源加载的切入点。
public class XmlBeanFactory extends DefaultListableBeanFactory {
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
}
这里我又要用我的三脚猫画图技术画一张loadBeanDefinitions方法执行的时序图(时序图永远滴神!)
画完图感觉自己头都大了,果然分析源码是件痛苦的事!
从上面的时序图中我们尝试梳理整个处理过程:
1、首先看一下loadBeanDefinitions方法具体的实现过程。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
首先把Resource封装成了EncodedResource,那么它的作用是什么呢,通过名称我们可以大致推断,这个类主要是用于对资源文件的编码进行处理。Spring是这么告诉我们的:为给定Resource创建一个新的EncodedResource ,而不指定显式编码或Charset 。在EncodeResource中我们可以看一下getReader方法:
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
显然,getReader返回了一个有编码的InputStreamReader。
2、当构造好encodedResource后,再次转入了可复用方法 loadBeanDefinitions(同名参数类型不同的构造方法):
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
//通过属性来记录已经加载的资源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
//从encodedResource中拿出Resource,再从Resource中拿出InputStream
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//真正进入了逻辑核心部分
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
这个方法内部才是真正的数据准备阶段。所以我们在这里可以看出,之所以要对resource进行封装,是因为resource可能存在编码要求的情况。
3、然后我们步入核心部分doLoadBeanDefinitions(inputSource, encodedResource.getResource());
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}
代码是挺长,全是异常处理。。。
顺手步入doLoadDocument,它是这样的:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
运行到这里,其实只做了三件事:
这三个步骤支撑着整个Spring容器部分的实现,尤其事第3步对配置文件的解析,逻辑非常复杂,我们先从XML文件的验证模式看起。今天先看到这,明天继续。
XML文件的验证模式保证了XML文件的正确性,而比较常用的验证模式有两种:DTD和XSD。
两种验证模式这里不在做过多介绍,在我们的例子中:
<?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 class="com.qust.test.TestBean" id="testBean"/>
</beans>
使用了XSD验证模式。
在前面的代码中,我们可以看出Spring使用getValidationModeForResource方法来获取对应资源的验证模式。
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
//如果手动制定了验证模式则使用指定的验证模式
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
//如果未指定则自动检测
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
return VALIDATION_XSD;
}
而自动检测验证模式交给了detectValidationMode来处理:
protected int detectValidationMode(Resource resource) {
if (resource.isOpen()) {
throw new BeanDefinitionStoreException(
"Passed-in Resource [" + resource + "] contains an open stream: " +
"cannot determine validation mode automatically. Either pass in a Resource " +
"that is able to create fresh streams, or explicitly specify the validationMode " +
"on your XmlBeanDefinitionReader instance.");
}
InputStream inputStream;
try {
inputStream = resource.getInputStream();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
"Did you attempt to load directly from a SAX InputSource without specifying the " +
"validationMode on your XmlBeanDefinitionReader instance?", ex);
}
try {
return this.validationModeDetector.detectValidationMode(inputStream);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
resource + "]: an error occurred whilst reading from the InputStream.", ex);
}
}
detectValidationMode:
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
content = consumeCommentTokens(content);
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
return VALIDATION_AUTO;
}
}
private boolean hasDoctype(String content) {
return content.contains(DOCTYPE);
}
Spring检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。
经过了验证模式准备的步骤就可以进行Document加载了,XMLBeanFactoryReader类将文档读取的工作委托给了DocumentLoader来执行,这里的DocumentLoader是个接口,真正调用的是DefaultDocumentLoader,解析代码如下:
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
Spring在使用SAX解析XML文档的套路并没有什么特殊的地方,首先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析inputSource来返回Document对象。
这里有必要提及一下EntityResolver,对于参数entityResolver,传入的是通过getEntityResolver()方法获取的返回值:
//返回要使用的EntityResolver,如果未指定,则构建一个默认的解析器
protected EntityResolver getEntityResolver() {
if (this.entityResolver == null) {
// Determine default EntityResolver to use.
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader != null) {
this.entityResolver = new ResourceEntityResolver(resourceLoader);
}
else {
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}
EntityResolver的作用是项目本身提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD生命的过程。
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Trying to resolve XML entity with public ID [" + publicId +
"] and system ID [" + systemId + "]");
}
if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
int lastPathSeparator = systemId.lastIndexOf('/');
int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
if (dtdNameStart != -1) {
String dtdFile = DTD_NAME + DTD_EXTENSION;
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
}
try {
Resource resource = new ClassPathResource(dtdFile, getClass());
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isTraceEnabled()) {
logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
}
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
}
}
}
}
// Fall back to the parser's default behavior.
return null;
}
当把文件转换成Document后,接下来就到了提取及注册bean。当程序已经拥有了XML文档文件的Document实例对象时,会来到这个方法:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//创建BeanDefinitionDocumentReader以用于实际从XML文档读取bean定义。
//默认实现实例化指定的“ documentReaderClass”
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//加载统计前BeanDefinition的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
//加载及注册bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//记录本次加载的BeanDefinition个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
经过艰难险阻,我们终于到了核心逻辑的底部:
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
我们可以发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition注册。如果说以前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions算是真正地进行解析了,我们期待的核心部分真正地开始了(到目前位置,spring源代码与《Spring源码深度解析》一书中已经有了很多差别,但是具体流程没有变化)。
protected void doRegisterBeanDefinitions(Element root) {
//在递归的时候,跟踪父级delegate,新的递归调用引用上个方法的delegate(没懂什么意思。。。)
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
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)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
//解析前处理,留给子类实现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
//解析后处理,留给子类实现
postProcessXml(root);
this.delegate = parent;
}
1、创建delegate,创建代理,然后代理首先初始化一些默认的属性,DocumentDefaultsDefinition是存储默认配置的对象。
DEFAULT_LAZY_INIT_ATTRIBUTE = “default-lazy-init”;
DEFAULT_MERGE_ATTRIBUTE = “default-merge”;
DEFAULT_AUTOWIRE_ATTRIBUTE = “default-autowire”;
DEFAULT_DEPENDENCY_CHECK_ATTRIBUTE = “default-dependency-check”;
DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE = “default-autowire-candidates”;
DEFAULT_INIT_METHOD_ATTRIBUTE = “default-init-method”;
DEFAULT_DESTROY_METHOD_ATTRIBUTE = “default-destroy-method”
对profile处理完以后,就开始进行解析,我们发现preProcessXml()和postProcessXml()方法是空的!显然,这是设计模式中的模板方法模式(本人特别喜欢模板方法模式,个人认为扔个空方法留给子类实现是一件很酷的事0.0)。
Profile属性是一个用来配置生产环境和开发环境的属性,这样就可以方便的切换开发、部署环境。
处理了profile后就可以进行XMl的读取了,进入parseBeanDefinitions:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
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)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
BeanDefinitionParserDelegate(Bean定义解析器委托)
上面的代码看起来逻辑还是很清晰的,因为Spring的XML配置里面由两大类Bean声明,一个是默认的,如:
一种是自定义的,如:
这两种方式的读取及解析差别是非常大的,Spring判断是是哪一种声明,是通过使用node.getNamespaceURI()获取命名空间,并于Spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一致则认为是默认,反之则是自定义。如果是默认声明,则使用parseDefaultElement(ele, delegate)解析,反之则使用delegate.parseCustomElement(root)来解析。对于默认标签与自定义标签解析会在下一篇文章中进行探讨。写到最后想要吐槽一下CSDN,导入markdown文档是真的不好用,图片还得一张张上传,最后的格式两篇文章都乱掉了,不知道是bug还是什么,挺离谱的。