首先,看源码这东西呀,要有耐心。
这里假设大家都用过spring这个框架了,没用过的,或者才涉足的,最好不要马上就去看源码,没啥意义。
1、配置文件读取
这里来个经典的例子,《spring源码深度解析》中的例子。
public static void main(String[] args){
XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
}
我们就从配置文件的读取开始往下看。
直接进入XmlBeanFactory的构造方法中,该类持有一个用于xml读取的XmlBeanDefinitionReader,重点在与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方法中,这里首先对配置文件的编码做了一下,在其调用getReader()方法时会根据设置的编码进行解码操作
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
/**
* 通过 charset 还是通过encoding其实底层都是一样的
* 底层会通过encoding这个字符串去获取到一个Charset对象
* @return
* @throws IOException
*/
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());
}
}
进入loadBeanDefinitions(new EncodedResource(resource))方法,代码上都写了注释了,就不在废话了
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 currentResources = this.resourcesCurrentlyBeingLoaded.get();
//若添加不成功,则抛异常--检测到encodingResource的循环加载-检查您的导入定义! ——> set返回false的也就因为重复添加,不清楚的可以去看下set的源码
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
// 将资源封装为InputSource供SAX解析XML
// SAX解析器将使用InputSource对象来确定如何读取XML输入。
// 如果有可用的字符流,则解析器将直接读取该流,而不考虑该流中发现的任何文本编码声明。
// 如果没有字符流,但是有字节流,则解析器将使用InputSource中指定的编码使用该字节流,否则(如果未指定编码)则使用算法自动检测字符编码,例如XML规范中的一个。
// 如果字符流或字节流均不可用,则解析器将尝试打开与系统标识符标识的资源的URI连接。
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
//设置inputSource的编码
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
//从当前正在加载的集合中移除encodedResource
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
点击进入doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法,屏蔽掉那些异常捕捉的,这里的代码其实很清晰,先解析XML,获取Document对象,在根据Document对象获取BeanDefinition对象,并注册。注册这个动作嘛,简单理解就是给放到一个map里面去。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
//加载并解析XML,获取到一个Document对象,使用的是SAX,相比于DOM,SAX是一种速度更快,更有效的方法。它逐行扫描文档,一边扫描一边解析。
//而DOM解析,是将整份xml文档读取到内存中,形成一个树状结构。若xml文件过大,可能内存溢出
Document doc = doLoadDocument(inputSource, resource);
//注册BeanDefinition:
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
...
}
进入doLoadDocument(inputSource, resource)方法,这里可能需要一些xml文档的知识。这里先补充下xml相关的基础知识。
//document加载器:负责XML文件的读取
private DocumentLoader documentLoader = new DefaultDocumentLoader();
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
2、 XML介绍
XML文件的约束,来判断你写的这个XML文档有没有按照我规定的格式去写。毕竟,XML叫可扩展标记语言,里面的标签是可以自定义的,所以,需要约束,防止别人乱写,解析报错。即规定XML中的标签,标签的层级关系,标签的位置,标签的属性等
XML有两种约束模式:DTD和XSD。
2.1、DTD约束
我们来看一份DTD约束的XML文件,这是一份被我简化掉的mybatis框架的mapper的xml映射文件
这里只介绍一下DTD约束的引入,对于具体的DTD约束的写法不做介绍。使用DTD约束需要在xml文件的头部输入以下信息
故,可通过“DOCTYPE ” 这个关键字来判断一份xml是否是DTD约束,若没有“DOCTYPE ” ,则为XSD约束。
mybatis映射文件:
select count(1) from tb_student
dtd约束文件:
2.1、XSD约束
以下是一份XSD约束的XML文件,XSD比DTD复杂很多,但也意味着它更强大,介绍几个概念就好。
xmlns="http://www.springframework.org/schema/beans" : 为这个XML文档设置了一个命名空间,这个名字随便起,保证唯一就行。
spring的XML配置文件
XSD约束文件:
3、继续配置文件的读取
有了上面对XML的了解,我们就可以继续往下看源码了。
//document加载器:负责XML文件的读取
private DocumentLoader documentLoader = new DefaultDocumentLoader();
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
这里我们先看loadDocument方法的第二个入参方法getEntityResolver()。该方法的作用其实就是在项目中去查找约束文件。
/**
* Return the EntityResolver to use, building a default resolver
* if none specified.
* EntityResolver :实体解析器,用来获取XML的约束
* 说人话就是,XML的约束,一般都是个url,例如http://www.springframework.org/schema/beans/spring-beans.xsd
* 这时候如果通过http调用去获取这个约束的话,由于网络原因,会比较耗时,甚至可能网络异常。所以,spring将这些约束都放在项目中了
* 通过这个 实体解析器 去获取,这样就不需要通过http调用了
*/
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;
}
我们接着看DelegatingEntityResolver这个类。其构造器中会创建两个处理器,分别是针对DTD和XSD的本地约束文件获取的。
入参的这个ClassLoader,其实就是为了加载本地文件的,不清楚的可以阅读我的另一篇文章《关于java中文件路径》
public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
//针对DTD约束的处理器
this.dtdResolver = new BeansDtdResolver();
//针对XSD约束的处理器
this.schemaResolver = new PluggableSchemaResolver(classLoader);
}
那具体是怎么个获取法呢,接着往下看。我们先看BeansDtdResolver。结合spring源码中dtd文件的存放位置,就比较清楚了
/**
*
*< ?xml version="1.0" encoding="UTF-8"?>
*< !DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
* 就上面这个例子 -> publicId = -//SPRING//DTD BEAN 2.0//EN
* systemId = http://www.springframework.org/dtd/spring-beans-2.0.dtd
* @param publicId
* @param systemId
* @return
* @throws IOException
*/
@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) {
//spring-beans.dtd
String dtdFile = DTD_NAME + DTD_EXTENSION;
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
}
try {
//getClass, 从当前类所在的包(包含当前包)及其子包里去找名为 "spring-beans.dtd"的文件
//在spring这里就是去 org/springframework/beans/factory/xml 路径下去找 spring-beans.dtd
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;
}
再来看下xsd的PluggableSchemaResolver。可以直接看getSchemaMappings()方法。说白了就是根据XML文档中的publicId跟systemId去META-INF/spring.schemas文件中,找到对应的地址映射。在根据这个地址,找到xsd文件。地址其实在上图的dtd文件下面那些。
/**
* < ?xml version="1.0" encoding="UTF-8"?>
*
*
* 就上面这个例子 -> publicId = http://www.springframework.org/schema/beans
* systemId = http://www.springframework.org/schema/beans/spring-beans.xsd
* Load the specified schema mappings lazily.
*/
private Map getSchemaMappings() {
Map schemaMappings = this.schemaMappings;
if (schemaMappings == null) {
synchronized (this) {
schemaMappings = this.schemaMappings;
if (schemaMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
}
try {
// 用类加载器获取资源 META-INF/spring.schemas
// spring.schemas文件的内容:xml的systemId与本地xsd的路径映射
// http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd = org/springframework/beans/factory/xml/spring-beans.xsd
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded schema mappings: " + mappings);
}
schemaMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
this.schemaMappings = schemaMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
}
}
}
}
return schemaMappings;
}
有了找到本地约束文件的方法后,我们就要决定到底是找dtd文件还是xsd文件了。让我们来看doLoadDocument方法的第3个入参getValidationModeForResource(resource)。还记得我们上面说的,使用什么约束文件,其实在XML文档的头部就声明好了。就是判断是否存在这个信息。spring其实就判断的是DOCTYPE 这个关键字。让我来看下具体方法。
这个方法的作用就是读取配置文件
/**
* xml的验证模式相关:xml文件,有两种约束形式,DTD跟XSD
* xml的约束即规定xml中的标签,标签的层级关系,标签的位置,标签的属性等
* DTD约束 例如: class下面有至少1个 元素
* 学生标签下可有(可无)名字,年龄,介绍三个元素且有序
* 对名字进行说明,PCDATA表示可解析的
*
*
* 使用的时候,在xml文件的开头,加入 < !DOCTYPE 文档根节点 SYSTEM "dtd文件路径">,这里<跟!间我加了个空格,实际是没有的
* XSD约束 例如:
*
*
*
*
*
*
*
*
*
*
*
* @param resource
* @return
*/
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
//如果手动指定了xml文件的验证模式则使用指定的验证模式
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
//如果没有指定验证模式,则使用自动检测 --> 其实就是判断文件中是否含有"DOCTYPE",有就是DTD,没有就XSD
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
//(⊙o⊙)… 这也行。。。
return VALIDATION_XSD;
}
进去detectValidationMode(resource)方法中会发现,真正确定验证模式的代码在this.validationModeDetector.detectValidationMode(inputStream)中。其中hasDoctype(content)中就是去判断是否包含DOCTYPE字样。
/**
* Detect the validation mode for the XML document in the supplied {@link InputStream}.
* Note that the supplied {@link InputStream} is closed by this method before returning.
* 简单来说就是判断这份xml文件中,是否包含 DOCTYPE ,是的话就是DTD,不然就是XSD
* 因为DTD约束的xml文件的格式如下:
* < ?xml version="1.0" encoding="UTF-8"?>
* < !DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
* @param inputStream the InputStream to parse
* @throws IOException in case of I/O failure
* @see #VALIDATION_DTD
* @see #VALIDATION_XSD
*/
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);
}
这些准备工作都准好后,就要开始解析XML文件了。
@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);
}
protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
throws ParserConfigurationException {
//获取JAXP文档解析器工厂
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(namespaceAware);
if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
factory.setValidating(true);
if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
// Enforce namespace aware for XSD...
//默认情况下,JAXP验证的不是XSD类型的文件,而是DTD类型的文件
//而在JAXP(JavaAPI for XML Processing),要开启XSD验证的话:
//1、设置namespaceAware=true; 默认情况下:false
//2、设置解析器的验证语言即 将http://java.sun.com/xml/jaxp/properties/schemaLanguage这个key的值,设置为http://www.w3.org/2001/XMLSchema
factory.setNamespaceAware(true);
try {
factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
}
catch (IllegalArgumentException ex) {
ParserConfigurationException pcex = new ParserConfigurationException(
"Unable to validate using XSD: Your JAXP provider [" + factory +
"] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
"Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
pcex.initCause(ex);
throw pcex;
}
}
}
return factory;
}
/**
* @param factory 用来创建文档解析器
* @param entityResolver 用来本地寻找DTD或XSD约束文件
* @param errorHandler 用来处理异常
* @return 文档解析器
* @throws ParserConfigurationException
*/
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
@Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
throws ParserConfigurationException {
DocumentBuilder docBuilder = factory.newDocumentBuilder();
if (entityResolver != null) {
docBuilder.setEntityResolver(entityResolver);
}
if (errorHandler != null) {
docBuilder.setErrorHandler(errorHandler);
}
return docBuilder;
}
到这里,读取XML文件的代码就结束了。至此,我们已经获得了XML文档的内容对象——Document对象了。后面要做的工作就是读取Document对象的内容,创建BeanDefinition对象,并注册。这部分我们下次再讲。
感谢阅读。