IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring源码分析:Bean加载分析之BeanDefinition -> 正文阅读

[Java知识库]Spring源码分析:Bean加载分析之BeanDefinition

(一)序言

Spring源码分析容器篇中提到将spring中的bean解析并注入到beanFactory中,当时因为容器篇幅太长仅仅是一笔带过,本文是对spring加载bean中生成BeanDefinition的详细分析,也是笔者对spring深入学习的过程,文中有出错的地方,还望大家能够及时指出。

(二)Spring生成BeanDefinition

本次入口分析是Spring容器篇中第二步obtainFreshBeanFactory#…#loadBeanDefinitions方法,obtainFreshBeanFactory方法是包含了创建容器和解析配置并生成BeanDefinition,创建容器是由DefaultListableBeanFactory直接创建,而解析配置文件是本文重点分析的对象。

1、Spring Bean解析入口

Spring源码的风格是将一切比较复杂的方法进行封装和抽象,便于其他地方可调用,然而真正意义上在干活的,也是我们所关心的是do-*开头的方法才是具体实现的地方,loadBeanDefinitions(resource)->loadBeanDefinitions(encodedResource)->doLoadBeanDefinitions,代码如下:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));//进行编码封装配置资源
}
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
	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!");
	}
	try (InputStream inputStream = encodedResource.getResource().getInputStream()) {//获取资源输入流
		InputSource inputSource = new InputSource(inputStream);//流转化
		if (encodedResource.getEncoding() != null) {
			inputSource.setEncoding(encodedResource.getEncoding());
		}
		return doLoadBeanDefinitions(inputSource, encodedResource.getResource());//开始调用真正解析BeanDefinition
	}catch (IOException ex) {
		throw new BeanDefinitionStoreException(
				"IOException parsing XML document from " + encodedResource.getResource(), ex);
	}finally {
		currentResources.remove(encodedResource);
		if (currentResources.isEmpty()) {
			this.resourcesCurrentlyBeingLoaded.remove();
		}
	}
}

在spring进行一系列的包装、封装和转化操作后,进行调用doLoadBeanDefinitions对配置文件进行解析,首先是先对文件进行文档化Document转化,然后对doc进行标签解析并调用registerBeanDefinitions方法,主要就是根据配置文件中的各种标签和属性,进行封装BeanDefinition,代码如下:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
          throws BeanDefinitionStoreException {
      try {
          Document doc = doLoadDocument(inputSource, resource);//文件转化为文档类,底层是转化包,此处不多解释
          int count = registerBeanDefinitions(doc, resource);//生成BeanDefinition,重点分析
          if (logger.isDebugEnabled()) {
              logger.debug("Loaded " + count + " bean definitions from " + resource);
          }
          return count;
      //后面一连串的catch是捕获异常,便于spring能精确提示出解析异常
      }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);
      }
  }

根据上述方式中转化后的文档类进行注册BeanDefinition,创建读取对象,然后根据读取对象进行封装,如下:

 public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
      BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();//创建读取器
      int countBefore = getRegistry().getBeanDefinitionCount();//统计已存在的BeanDefinition数量
      documentReader.registerBeanDefinitions(doc, createReaderContext(resource));//解析doc并生成BeanDefinition
      return getRegistry().getBeanDefinitionCount() - countBefore;//此次文件解析的数量
  }

spring源码风格,概念封装,实际干活的是doRegisterBeanDefinitions,一笑而过,继续看码:

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
	this.readerContext = readerContext;
	doRegisterBeanDefinitions(doc.getDocumentElement());
}
protected void doRegisterBeanDefinitions(Element root) {
	this.delegate = createDelegate(getReaderContext(), root, parent);//创建委托类对象
	if (this.delegate.isDefaultNamespace(root)) {
		String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);//是否有profile属性设置,个性化属性生效
		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);//核心点,真正开始处理各种标签和bean
	postProcessXml(root);//解析后子类重写,开放性设计,默认为空实现
	this.delegate = parent;
}

望眼欲穿的点终于被发现,parseBeanDefinitions生成各种BeanDefinition的入口,分为自定义和默认标签解析,接着看:

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)) {//spring默认的定义空间,则使用默认的解析如配置文件指定的bean
					parseDefaultElement(ele, delegate);//普通的import、alias、bean、beans中被定义的bean
				}else{
					delegate.parseCustomElement(ele);//采用自定义的,比如采用tx定义命名空间、jdbc命名等等
				}
			}
		}
	}else{
		delegate.parseCustomElement(root);
	}
}

2、Spring Bean内置标签解析

Spring支持普通的import、alias、bean、beans中被定义的bean,本文主要是分析配置文件XMl定义的bean解析,注解方式实现的bean会放到后面分析Springboot实现方式时去重点分析,原理其实相差不多,直接上干货,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别名bean处理
		processAliasRegistration(ele);
	}else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {//bean标签处理(也是配置文件中使用比较常用的,重点分析)
		processBeanDefinition(ele, delegate);
	}else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {//beans标签处理
		doRegisterBeanDefinitions(ele);
	}
}

(1)bean标签解析——processBeanDefinition方法

在Spring配置文件中,比较核心、常见的也是bean标签定义Bean,同时最复杂的也是processBeanDefinition(重点分析),代码如下:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    //返回BeanDefinitionHolder对象,包含基本bean属性、元素
	BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
	if (bdHolder != null) {//判断bdHolder对象不为空时会判断是否存在子节点,持续解析
		bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
		try {
			//将bdHolder注册到容器中
			BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
		}catch (BeanDefinitionStoreException ex) {
			getReaderContext().error("Failed to register bean definition with name '" +
					bdHolder.getBeanName() + "'", ele, ex);
		}
		//发送事件通知,通知想关的监听器,表明bean加载完毕
		getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
	}
}

Spring的bean标签属性或元素解析,交由BeanDefinitionParserDelegate类来具体解析,parseBeanDefinitionElement代码如下:

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
    return parseBeanDefinitionElement(ele, null);
}

bean标签具体解析内容:id、name和多个name名称、别名alias、beanName唯一性检查,生成beanDefinition(实际上是GenericBeanDefinition生成),beanName是否命名等步骤,最后生成使用生成的beanDefinition来封装BeanDefinitionHolder实例,逻辑代码如下:

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
	String id = ele.getAttribute(ID_ATTRIBUTE);//id属性获取
	String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);//name属性获取
	List<String> aliases = new ArrayList<>();
	if (StringUtils.hasLength(nameAttr)) {//别名处理
		String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
		aliases.addAll(Arrays.asList(nameArr));
	}
	String beanName = id;
	if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {//去除别名中第一个beanName值
		beanName = aliases.remove(0);
		if (logger.isTraceEnabled()) {
			logger.trace("No XML 'id' specified - using '" + beanName +
					"' as bean name and " + aliases + " as aliases");
		}
	}
	//检查beanName唯一性
	if (containingBean == null) {
		checkNameUniqueness(beanName, aliases, ele);
	}
	//使用GenericBeanDefinition封装beanDefinition的各种属性
	AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
	if (beanDefinition != null) {
		if (!StringUtils.hasText(beanName)) {
			try {
			    //beanName不存在,则采用spring默认的命名规则
				if (containingBean != null) {//当前BeanDefinition已存在,则使用generateBeanName方法默认生成
					beanName = BeanDefinitionReaderUtils.generateBeanName(
							beanDefinition, this.readerContext.getRegistry(), true);
				}else{
				    //不存在containingBean时则使用Spring自身定义的beanName和CLassName的相关约束
					beanName = this.readerContext.generateBeanName(beanDefinition);
					String beanClassName = beanDefinition.getBeanClassName();
					if (beanClassName != null &&
							beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
							!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
						aliases.add(beanClassName);
					}
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Neither XML 'id' nor 'name' specified - " +
							"using generated bean name [" + beanName + "]");
				}
			}catch (Exception ex) {
				error(ex.getMessage(), ele);
				return null;
			}
		}
		String[] aliasesArray = StringUtils.toStringArray(aliases);
		return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
	}
	return null;
}

(2)bean元素解析——parseBeanDefinitionElement方法(核心)

Spring对bean标签的属性采用硬编码处理方式,分别使用了以下八个核心点来处理,如下①②③④⑤⑥⑦⑧所示:

public AbstractBeanDefinition parseBeanDefinitionElement(
		Element ele, String beanName, @Nullable BeanDefinition containingBean) {
	this.parseState.push(new BeanEntry(beanName));
	String className = null;
	if (ele.hasAttribute(CLASS_ATTRIBUTE)) {//处理bean标签中的属性class
		className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
	}
	String parent = null;
	if (ele.hasAttribute(PARENT_ATTRIBUTE)) {//处理bean标签中的属性parent
		parent = ele.getAttribute(PARENT_ATTRIBUTE);
	}
	try {
		//使用GenericBeanDefinition来生成AbstractBeanDefinition实例,代码如①
		AbstractBeanDefinition bd = createBeanDefinition(className, parent);
		//spring内置处理bean属性的硬编码处理逻辑,代码如②
		parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
		bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));//description属性处理
		parseMetaElements(ele, bd);//解析元数据如③
		parseLookupOverrideSubElements(ele, bd.getMethodOverrides());//解析lookup-method如④
		parseReplacedMethodSubElements(ele, bd.getMethodOverrides());//解析replaced-method如⑤
		parseConstructorArgElements(ele, bd);//解析构造函数参数如⑥
		parsePropertyElements(ele, bd);//解析property子元素如⑦
		parseQualifierElements(ele, bd);//解析qualifier子元素如⑧
		bd.setResource(this.readerContext.getResource());
		bd.setSource(extractSource(ele));
		return bd;
	}
	catch (ClassNotFoundException ex) {
		error("Bean class [" + className + "] not found", ele, ex);
	}
	catch (NoClassDefFoundError err) {
		error("Class that bean class [" + className + "] depends on not found", ele, err);
	}
	catch (Throwable ex) {
		error("Unexpected failure during bean definition parsing", ele, ex);
	}
	finally {
		this.parseState.pop();
	}

	return null;
}

①创建AbstractBeanDefinition实例

GenericBeanDefinition类创建具体实例,具体代码如下

protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName)
			throws ClassNotFoundException {
    return BeanDefinitionReaderUtils.createBeanDefinition(
            parentName, className, this.readerContext.getBeanClassLoader());
}
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;
}

②spring内置硬编码处理

bean属性:singleton、scope、abstract、lazy、autowire、depends_on、autowire_candidate、primary、init_method、destroy_method、factory_method和factory_bean,具体代码如下:

 public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName,
  			BeanDefinition containingBean, AbstractBeanDefinition bd) {
  	//处理singleton属性
      if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) {
          error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele);
      }else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) {//处理scope属性
          bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
      }else if (containingBean != null) {
          bd.setScope(containingBean.getScope());
      }
      //处理abstract属性
      if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) {
          bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE)));
      }
      //处理lazy懒加载属性
      String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
      if (isDefaultValue(lazyInit)) {
          lazyInit = this.defaults.getLazyInit();
      }
      bd.setLazyInit(TRUE_VALUE.equals(lazyInit));
      //处理自动注入属性autowire
      String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE);
      bd.setAutowireMode(getAutowireMode(autowire));
      if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {//处理depends_on依赖
          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 (isDefaultValue(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));
      }
      //处理bean的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);
          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;
  }

③解析元数据

代码如下:

 public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) {
    NodeList nl = ele.getChildNodes();
     for (int i = 0; i < nl.getLength(); i++) {
         Node node = nl.item(i);
         if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) {
             Element metaElement = (Element) node;
             String key = metaElement.getAttribute(KEY_ATTRIBUTE);
             String value = metaElement.getAttribute(VALUE_ATTRIBUTE);
             BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value);
             attribute.setSource(extractSource(metaElement));
             attributeAccessor.addMetadataAttribute(attribute);
         }
     }
 }

④解析lookup-method

代码如下:

 public void parseLookupOverrideSubElements(Element beanEle, MethodOverrides overrides) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, LOOKUP_METHOD_ELEMENT)) {
			Element ele = (Element) node;
			String methodName = ele.getAttribute(NAME_ATTRIBUTE);
			String beanRef = ele.getAttribute(BEAN_ELEMENT);
			LookupOverride override = new LookupOverride(methodName, beanRef);
			override.setSource(extractSource(ele));
			overrides.addOverride(override);
		}
	}
}

⑤解析replaced-method

代码如下:

 public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) {
			Element replacedMethodEle = (Element) node;
			String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE);//name属性
			String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE);//回调方法
			ReplaceOverride replaceOverride = new ReplaceOverride(name, callback);
			List<Element> argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, ARG_TYPE_ELEMENT);
			for (Element argTypeEle : argTypeEles) {
				String match = argTypeEle.getAttribute(ARG_TYPE_MATCH_ATTRIBUTE);
				match = (StringUtils.hasText(match) ? match : DomUtils.getTextValue(argTypeEle));
				if (StringUtils.hasText(match)) {
					replaceOverride.addTypeIdentifier(match);
				}
			}
			replaceOverride.setSource(extractSource(replacedMethodEle));
			overrides.addOverride(replaceOverride);
		}
	}
}

⑥解析构造函数参数

代码如下:

public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) {
			parseConstructorArgElement((Element) node, bd);
		}
	}
}

⑦解析property子元素

代码如下:

public void parsePropertyElements(Element beanEle, BeanDefinition bd) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {
			parsePropertyElement((Element) node, bd);
		}
	}
}

⑧解析qualifier子元素

代码如下:

public void parseQualifierElements(Element beanEle, AbstractBeanDefinition bd) {
	NodeList nl = beanEle.getChildNodes();
	for (int i = 0; i < nl.getLength(); i++) {
		Node node = nl.item(i);
		if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ELEMENT)) {
			parseQualifierElement((Element) node, bd);
		}
	}
}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-05 11:04:09  更:2022-05-05 11:07:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:36:13-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码