Spring源码分析2 — XML配置文件的解析流程
来源:互联网 发布:gta5低特效优化补丁 编辑:程序博客网 时间:2024/06/05 18:52
1 介绍
创建并初始化spring容器中,关键一步就是读取并解析spring XML配置文件。这个过程比较复杂,本文将详细分析整个流程。先看涉及到的关键类。
XmlWebApplicationContext:web应用的默认Spring容器
XmlBeanDefinitionReader:读取XML并解析xml文件
DocumentLoader:文件先被读取为了原始的输入流InputStream,然后封装为InputSource。DocumentLoader加载inputSource,解析后得到Document对象
Document:代表一个XML或者HTML标记文件,包含docType,各种element节点等。
BeanDefinition:XML中bean在spring容器中的表示。Document会被解析为BeanDefinition。在Bean创建和初始化中它们会大展拳脚。
BeanDefinitionDocumentReader:解析Document中的节点元素Element,转换为BeanDefinition,并注册他们到BeanDefinition注册表中。默认实现类为DefaultBeanDefinitionDocumentReader
BeanDefinitionParserDelegate:实际解析Document中的节点元素,采用了代理模式。
2 流程
2.1 obtainFreshBeanFactory
初始化spring容器中的refresh()方法中,会调用obtainFreshBeanFactory()方法,它是读取并解析spring xml配置文件的入口。详细过程可以参看上一篇文章。下面从这个方法开始分析。
// obtainFreshBeanFactory 加载spring XML配置文件protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { // 关键所在,后面分析 refreshBeanFactory(); // log之类的东西,不是很关键了 ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (logger.isDebugEnabled()) { logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); } return beanFactory;}protected final void refreshBeanFactory() throws BeansException { // BeanFactory已存在,则先销毁它 if (hasBeanFactory()) { destroyBeans(); closeBeanFactory(); } try { // new DefaultListableBeanFactory,创建容器,设置id,个性化配置等 DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); customizeBeanFactory(beanFactory); // 加载xml配置文件,具体子ApplicationContext会实现它。不同子类实现会不同。重点节点,后面分析 loadBeanDefinitions(beanFactory); synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); }}
2.2 loadBeanDefinitions
下面我们来分析web应用中默认的spring容器,也就是XmlWebApplicationContext,中的loadBeanDefinitions。
通过XmlBeanDefinitionReader来读取xml配置文件。
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { // 创建XmlBeanDefinitionReader,用它来读取XML配置文件 XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); // 配置beanDefinitionReader的环境和属性 beanDefinitionReader.setEnvironment(getEnvironment()); // ApplicationContext也继承了ResourceLoader接口 beanDefinitionReader.setResourceLoader(this); // entityResolver在parse时会用到 beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); // 初始化beanDefinitionReader,子类可以实现这个方法,做一些个性化配置和初始化 initBeanDefinitionReader(beanDefinitionReader); // 开始load xml文件,这一步开始才是真正读取XML文件了。前面都是做一些环境配置之类的事情 loadBeanDefinitions(beanDefinitionReader);}
loadBeanDefinitions()中先创建beanDefinitionReader,然后配置它的环境,设置成员属性,最后才是真正干活的,也就是读取XML配置文件。我们来看真正读取XML的这一步。
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException { // 获取XML配置文件的地址,还记得前面讲到过的web.xml中的contextConfigLocation元素吧,它指明了XML配置文件的地址。如果web.xml中没有配置,则读取默认的地址,参看后面分析 String[] configLocations = getConfigLocations(); if (configLocations != null) { // 遍历读取每个配置文件 for (String configLocation : configLocations) { reader.loadBeanDefinitions(configLocation); } }}
2.3 getConfigLocations()
我们先分析下getConfigLocations方法, 它会获取到spring XML配置文件的地址。
// 获取配置文件地址,从web.xml中读取。读取不到,则使用默认地址protected String[] getConfigLocations() { return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations());}// 获取默认的xml配置文件地址public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";protected String[] getDefaultConfigLocations() { if (getNamespace() != null) { // 设置了容器namespace时,返回/WEB-INF/ + ApplicationContext的namespace + .xml return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX}; } else { // 没有设置namespace时,直接返回 /WEB-INF/applicationContext.xml return new String[] {DEFAULT_CONFIG_LOCATION}; }}
我们先获取XML文件地址,然后再读取XML文件,下面重点分析reader如何读取xml文件的
2.4 XmlBeanDefinitionReader.loadBeanDefinitions()
// XmlBeanDefinitionReader读取XML文件public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { // 一段log,省略 // 将Resource对象添加到hashSet中,不是很关键 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 { // 获取Resource对象的输入流 InputStream inputStream = encodedResource.getResource().getInputStream(); try { // 将inputStream封装到InputSource对象中,并设置编码格式 InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } // 加载封装好的inputSource对象,读取XML配置文件。关键步骤,后面分析 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(); } }}
XmlBeanDefinitionReader做了一些成员变量设置后,获取传入的Resource对象的输入流,封装成InputSource后,开始真正解析配置文件。下面来看doLoadBeanDefinitions()如何解析XML文件。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { // 加载并解析XML文件,解析方案为社区通用方法,不是Spring所特有的 Document doc = doLoadDocument(inputSource, resource); return registerBeanDefinitions(doc, resource); } // 各种异常处理,省略 catch (BeanDefinitionStoreException ex) { }}
先利用documentLoader加载XML配置文件,然后注册beans。
2.4.1 doLoadDocument 加载xml文件,将InputSource输入流转换为Document对象
// 加载XML配置文件protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { // 获取entityResolver,errorHandler, validationMode,namespaceAware,它们都有默认值,也可以由用户来设置 // entityResolver: 解析器,默认ResourceEntityResolver // errorHandler: 解析XML时错误处理,默认SimpleSaxErrorHandler // validationMode: xml验证模式,默认VALIDATION_XSD // namespaceAware: XML命名空间是否敏感,默认false return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware());}// DefaultDocumentLoader的loadDocument方法public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { // 创建DocumentBuilderFactory,对应多个实现类,默认com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); if (logger.isDebugEnabled()) { logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]"); } // 创建DocumentBuilder,通过factory创建 DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); // 解析输入流,并返回一个Document对象。解析采用的是通用的DocumentBuilderImpl对象,使用DomParser解析xml文件。解析这一步是通用的,不是Spring特有的方法。比较复杂,不展开了。只要知道通过parse后得到了Document对象就可以了。 return builder.parse(inputSource);}
2.4.2 registerBeanDefinitions 将读取XML后的Document转换为BeanDefinition
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { // 反射创建documentReader实例,默认为DefaultBeanDefinitionDocumentReader BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); // 获取容器中当前beans数量,已经注册的BeanDefinition会存储在一个Map中,获取Map的size即可。 int countBefore = getRegistry().getBeanDefinitionCount(); // 注册beanDefinition,这是关键所在,后面分析 documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); // 返回本次注册的数量 return getRegistry().getBeanDefinitionCount() - countBefore;}// 反射创建documentReader,默认为DefaultBeanDefinitionDocumentReaderprivate Class<?> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() { return BeanDefinitionDocumentReader.class.cast(BeanUtils.instantiateClass(this.documentReaderClass)); }
registerBeanDefinitions的作用就是将上一步中,输入流转换为的Document对象,转换为BeanDefinition对象。主要工作在documentReader.registerBeanDefinitions()中,下面来分析。
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; logger.debug("Loading bean definitions"); // root为<beans />标签 Element root = doc.getDocumentElement(); doRegisterBeanDefinitions(root);}// 采用代理进行解析,代理为BeanDefinitionParserDelegateprotected void doRegisterBeanDefinitions(Element root) { // 创建代理 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)) { return; } } } // 解析前的处理,DefaultBeanDefinitionDocumentReader没有实现它,子类可以实现,来扩展功能 preProcessXml(root); // 解析root内的XML标签,如<import> <alias> <bean>等 parseBeanDefinitions(root, this.delegate); // 解析后的处理,同样没有实现它,子类可以实现。 postProcessXml(root); this.delegate = parent; }
registerBeanDefinitions() 解析下的XML标签,通过BeanDefinitionParserDelegate代理来进行。具体工作在parseBeanDefinitions()中。这里是本篇文章中比较关键的地方。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { if (delegate.isDefaultNamespace(root)) { // 获取<beans>的子节点 NodeList nl = root.getChildNodes(); // 遍历子节点 for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { // 子节点是Element对象,默认的子节点,如<import>都是Element对象 Element ele = (Element) node; if (delegate.isDefaultNamespace(ele)) { // 在默认的命名空间url中的元素,是默认定义好的节点,采用parseDefaultElement方法解析 parseDefaultElement(ele, delegate); } else { // 用户自定义的命名空间url中的元素,采用parseCustomElement方法解析 delegate.parseCustomElement(ele); } } } } else { // 子节点不是标准的Element元素,比如用户自定义的,采用parseCustomElement方法解析 delegate.parseCustomElement(root); }}
parseBeanDefinitions()方法会循环遍历的子节点。如果是默认命名空间内的Element,则采用parseDefaultElement()方法解析,否则采用parseCustomElement()方法。DefaultElement包括、、、嵌套的,其余都为CustomElement,如。
2.4.2.1 parseDefaultElement() 解析DefaultElement
public static final String IMPORT_ELEMENT = "import";public static final String ALIAS_ATTRIBUTE = "alias";public static final String BEAN_ELEMENT = "bean";public static final String NESTED_BEANS_ELEMENT = "beans";private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { // 解析<import> if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { importBeanDefinitionResource(ele); } // 解析<alias> else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { processAliasRegistration(ele); } // 解析<bean> else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate); } // 解析<beans> else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // recurse doRegisterBeanDefinitions(ele); }}
下面我们重点分析下processBeanDefinition(),因为这个和spring息息相关。这个过程十分复杂,所以我们不进行很细致的分析,抓住主要流程就OK了。也不建议非要去了解每行代码做了什么事情,避免过度陷入其中,而忽略了主流程。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { // 解析Element为BeanDefinition,这是重点,后面详细分析 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try { // 将BeanDefinition注册到BeanDefinitionMap中,key为beanName BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, ex); } // 发送注册的消息,相应的监听器就会收到并处理消息了 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); }}
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { // 获取bean的id属性 String id = ele.getAttribute(ID_ATTRIBUTE); // 获取bean的name属性 String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); // 解析name属性,支持多个name List<String> aliases = new ArrayList<>(); if (StringUtils.hasLength(nameAttr)) { String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } // id属性赋值到beanName变量中,注意不是name属性。如果没有id属性,则使用name属性的第一个值 String beanName = id; if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { beanName = aliases.remove(0); } // 校验beanname的唯一性,这也是为啥id属性值必须唯一的原因 if (containingBean == null) { checkNameUniqueness(beanName, aliases, ele); } // 解析bean节点为GenericBeanDefinition,后面详细分析 AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); // 后面不是很重要了 if (beanDefinition != null) { if (!StringUtils.hasText(beanName)) { try { if (containingBean != null) { beanName = BeanDefinitionReaderUtils.generateBeanName( beanDefinition, this.readerContext.getRegistry(), true); } else { beanName = this.readerContext.generateBeanName(beanDefinition); // Register an alias for the plain bean class name, if still possible, // if the generator returned the class name plus a suffix. // This is expected for Spring 1.2/2.0 backwards compatibility. String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { aliases.add(beanClassName); } } } catch (Exception ex) { error(ex.getMessage(), ele); return null; } } String[] aliasesArray = StringUtils.toStringArray(aliases); return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); } return null;}
public static final String PARENT_ATTRIBUTE = "parent";public static final String CLASS_ATTRIBUTE = "class";public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); // 获取class和parent属性 String className = null; if (ele.hasAttribute(CLASS_ATTRIBUTE)) { className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); } String parent = null; if (ele.hasAttribute(PARENT_ATTRIBUTE)) { parent = ele.getAttribute(PARENT_ATTRIBUTE); } try { // 反射实例化bean为GenericBeanDefinition AbstractBeanDefinition bd = createBeanDefinition(className, parent); // 解析节点中其他属性,如scope, singleton等。后面详细分析 parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); // 解析子节点meta属性,如果有的话 parseMetaElements(ele, bd); // 解析子节点lookup-method属性 parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); // 解析子节点replaced-method属性 parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); // 解析constructor-arg属性 parseConstructorArgElements(ele, bd); // 解析property属性 parsePropertyElements(ele, bd); // 解析qualifier属性 parseQualifierElements(ele, bd); bd.setResource(this.readerContext.getResource()); bd.setSource(extractSource(ele)); return bd; } // 各种异常处理 catch (ClassNotFoundException ex) { error("Bean class [" + className + "] not found", ele, ex); } ... return null;}
// 反射实例化,创建GenericBeanDefinition对象public static AbstractBeanDefinition createBeanDefinition( @Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException { GenericBeanDefinition bd = new GenericBeanDefinition(); bd.setParentName(parentName); if (className != null) { if (classLoader != null) { bd.setBeanClass(ClassUtils.forName(className, classLoader)); } else { bd.setBeanClassName(className); } } return bd;}
// 解析<bean>中的各种属性,比如scope,lazy-init等public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { // 解析singleton if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele); } // 解析scope else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); } else if (containingBean != null) { // Take default from containing bean in case of an inner bean definition. bd.setScope(containingBean.getScope()); } // 解析abstract if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); } // 解析lazy-init String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); if (DEFAULT_VALUE.equals(lazyInit)) { lazyInit = this.defaults.getLazyInit(); } bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); // 解析autowire String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); bd.setAutowireMode(getAutowireMode(autowire)); // 解析depends-on if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) { String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE); bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS)); } // 解析autowire-candidate String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE); if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) { String candidatePattern = this.defaults.getAutowireCandidates(); if (candidatePattern != null) { String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern); bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName)); } } else { bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate)); } // 解析primary if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) { bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE))); } // 解析init-method if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) { String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE); if (!"".equals(initMethodName)) { bd.setInitMethodName(initMethodName); } } else if (this.defaults.getInitMethod() != null) { bd.setInitMethodName(this.defaults.getInitMethod()); bd.setEnforceInitMethod(false); } // 解析destroy-method if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE); bd.setDestroyMethodName(destroyMethodName); } else if (this.defaults.getDestroyMethod() != null) { bd.setDestroyMethodName(this.defaults.getDestroyMethod()); bd.setEnforceDestroyMethod(false); } // 解析factory-method if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) { bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE)); } // 解析factory-bean if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) { bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE)); } return bd;}
2.4.2.2 parseCustomElement() 解析CustomElement
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { String namespaceUri = getNamespaceURI(ele); if (namespaceUri == null) { return null; } // 根据命名空间url获取具体的NamespaceHandler,比如<context:component-scan>对应的就是用户自定义的ContextNamespaceHandler。 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); if (handler == null) { error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); return null; } // 交给用户自定义的NamespaceHandler来解析用户自定义的CustomElement return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));}
parseCustomElement()首先根据自定义标签的命名空间,生成具体的NamespaceHandler。一般要由用户自己定义。然后调用parse方法进行解析,这个也是用户自定义的。这里就不展开分析了。
3 流程图
转载至:http://blog.csdn.net/u013510838/article/details/75092612
- Spring源码分析2 — XML配置文件的解析流程
- Spring源码分析3 — spring XML配置文件的解析流程
- Spring源码分析-配置文件的解析(二)
- Spring源码浅析 -- XML配置文件的载入与解析
- Spring源码浅析 -- XML配置文件的载入与解析
- Mybatis3源码分析(三):解析mapper的xml配置文件
- Mybatis3源码分析(三):解析mapper的xml配置文件
- spring源码-2-xml标签的解析
- Spring源码分析:Bean加载流程概览及配置文件读取
- Spring 源码分析:Bean 加载流程概览及配置文件读取
- Spring源码解析——配置文件读取相关的类
- spring源码解析-从xml配置文件中获取bean
- Spring IOC 源码分析-xml配置文件加载-注册
- ibatis源码分析—配置文件解析(2)
- Spring xml配置文件头解析
- Spring xml配置文件头解析
- Spring xml配置文件头解析
- Spring xml配置文件头解析
- java 枚举
- 《Windows核心编程》读书笔记二十章 DLL高级技术
- java中的pass方法
- BBB学习(三):SD卡安装系统镜像(1):安装流程
- 如何设计出用户喜爱的API
- Spring源码分析2 — XML配置文件的解析流程
- CreateCompatibleDC 与 CreateCompatibleBitmap 小小结
- Oracle Sqlplus参数设置
- mysql_把符合条件的某列的多条数据合并为一条
- 171211之Oracle闪回操作
- 多线程练习
- html开启摄像头进行拍照摄像
- duilib文本消息
- js数组的操作详解