文章目录

  • 1. bean标签的解析及注册
    • 1.1 解析BeanDefinition
      • 1.1.1 创建用于承载属性的 BeanDefinition
        • 1.1.1.1 AbstractBeanDefinition
        • 1.1.1.2 GenericBeanDefinition
      • 1.1.2 解析各种属性
      • 1.1.3 解析子元素 meta
      • 1.1.4 解析子元素 lookup-method
      • 1.1.5 解析子元素 replaced-method
      • 1.1.6 解析子元素 constructor-arg
      • 1.1.7 解析子元素 property
      • 1.1.8 解析子元素 qualifier
    • 1.2 解析默认标签中的自定义标签元素
    • 1.3 注册解析到的BeanDefinition
      • 1.3.1 通过beanName注册BeanDefinition
      • 1.3.2 通过别名注册BeanDefinition
    • 1. 4 解析及注册完成,通知监听器
  • 2. 嵌入式beans标签的解析
  • 3. import标签的解析
  • 4. alias标签的解析
  • 5. 整体流程思维导图

默认标签的解析

接着上文Spring——1. BeanFactory容器的初始化,在Spring中,XML配置里面有两大类型的Bean声明:默认类型Bean 和 自定义类型Bean;对于不同的类型解析的方式差别很大,这里先进行默认类型标签的解析。

DefaultBeanDefinitionDocumentReader.java

parseDefaultElement(ele, delegate);
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {// import标签的解析;// 当项目比较庞大的时候,需要进行分模块,可以使用import来导入其他模块的配置文件// <import resource="dependency-lookup-context.xml"/>importBeanDefinitionResource(ele);}else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {// alias标签的解析;// 在定义bean时就指定所有的别名并不总是恰当的,有时候期望能在当前位置为在别处定义的bean引入别名;// <bean id="user" class="org...."/>// <alias name="user" alias="user-bgy1,user-bgy2"/>processAliasRegistration(ele);}else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {// bean标签的解析和注册// <bean id="" class="" />processBeanDefinition(ele, delegate);}else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {// 嵌入式 beans标签的解析;递归调用beans的解析过程// <beans>//     <bean></bean>// </beans>// recursedoRegisterBeanDefinitions(ele);}
}

1. bean标签的解析及注册

这四种标签的解析中,bean标签的解析是最为核心的地方,也是最复杂的,所以先从bean标签的解析开始。

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {// 委托BeanDefinitionParserDelegate 类的 parseBeanDefinitionElement 方法进行元素解析,返回BeanDefinitionHolder 类型的实例;// BeanDefinitionHolder:Holder for a BeanDefinition with name and aliases.// bdHolder实例已经包含了配置文件中配置的各种属性了(class、name、id、alias)BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {// 如果需要的话,就对BeanDefinition进行装饰/*** 适用场景:* 当spring中的bean使用的是默认的标签配置,但是其中的子元素是自定义配置(子元素,不是以bean的形式存在,是一个属性)** <bean id="test" class="test.MyClass">*     <mybean:user username="aaa"/>* </bean>*/bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);// 解析、装饰 完成后,对于得到的BeanDefinition已经可以满足后续的使用要求了,只需要进行注册了;// 对解析后的 BeanDefinition 进行注册try {// 注册操作委托给了 BeanDefinitionReaderUtils// Register the final decorated instance.BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to register bean definition with name '" +bdHolder.getBeanName() + "'", ele, ex);}// 发出响应事件,通知相关的监听器,这个bean的解析和注册 已经完成了// Send registration event.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));}
}

代码上的注释基本都解释了每个步骤的内容,下面来具体看看各个步骤的实现。

1.1 解析BeanDefinition

BeanDefinitionParserDelegate.java

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {return parseBeanDefinitionElement(ele, null);
}
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {// 获取idString id = ele.getAttribute(ID_ATTRIBUTE);// 获取nameString 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));}String beanName = id;if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {beanName = aliases.remove(0);if (logger.isTraceEnabled()) {logger.trace("No XML 'id' specified - using '" + beanName + "' as bean name and " + aliases + " as aliases");}}if (containingBean == null) {checkNameUniqueness(beanName, aliases, ele);}// 解析标签其他属性,并封装到 GenericBeanDefinition/*** 在这里是完成,从 XML文档 到 GenericBeanDefinition 的转换,也就是说* XML中的所有配置,都可以在 GenericBeanDefinition 的实例中找到对应的配置属性*/AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);if (beanDefinition != null) {if (!StringUtils.hasText(beanName)) {// 如果beanName不存在,就根据spring中提供的命名规则为当前的bean生成对应的beanNametry {if (containingBean != null) {beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true);} else {// 生成beanNamebeanName = 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);}}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);// 将获取到的信息封装到 BeanDefinitionHolder 实例中return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);}return null;

这个方法中完成了以下工作:

  1. 获取到元素的id以及name属性,并且检查beanName是否已经被使用了;
  2. 解析元素的其他所有属性,封装到 GenericBeanDefinition的实例中;即把XML配置文件转换成Java对象的过程;
  3. 检测到如果这个bean没有指定beanName,就使用默认规则为这个bean生成一个beanName;
    • 将这些解析到的结果封装到 BeanDefinitionHolder中;

进一步查看步骤2中,对XML配置文件其他属性的解析过程:

public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean) {this.parseState.push(new BeanEntry(beanName));String className = null;// 解析class属性if (ele.hasAttribute(CLASS_ATTRIBUTE)) {className = ele.getAttribute(CLASS_ATTRIBUTE).trim();}String parent = null;// 解析parent属性if (ele.hasAttribute(PARENT_ATTRIBUTE)) {parent = ele.getAttribute(PARENT_ATTRIBUTE);}try {// 创建用于承载属性的 AbstractBeanDefinition类型的 GenericBeanDefinition(<bean>元素标签在容器中的内部表示形式)AbstractBeanDefinition bd = createBeanDefinition(className, parent);// 硬编码解析默认bean的各种属性parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);// 提取descriptionbd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));/*** <bean id="user" class="org.springframework.beans.example.bean.User">*     <meta key="testStr" value="aaa"/>* </bean>**/// 解析子元素metaparseMetaElements(ele, bd);// 解析子元素 lookup-method 属性// 获取器注入:获取器注入是一种特殊的方法注入,它是把一个方法声明为返回某种类型的bean,但实际要返回的bean是在配置文件里配置的;parseLookupOverrideSubElements(ele, bd.getMethodOverrides());// 解析子元素 replaced-method属性// 方法替换:可以在运行时使用新的方法替换现有的方法(不但可以动态地替换返回实体bean,还能动态地更改原有方法的逻辑)parseReplacedMethodSubElements(ele, bd.getMethodOverrides());// 解析子元素 constructor-arg/*** <bean id="testBean" class="org.TestBean>*     <constructor-arg>*         <value>a</value>*     </constructor-arg>* </bean>*/parseConstructorArgElements(ele, bd);// 解析 Property子元素/*** <bean id="testBean" class="org.TestBean>*     <property name="testStr" value="aaa"/>* </bean>** 或** <bean id="testBean" class="org.TestBean>*     <property name="testList">*         <list>*             <value>aa</value>*             <value>bb</value>*         </list>*     </property>* </bean>** property中必须含有name属性*/parsePropertyElements(ele, bd);// 解析 Qualifier子元素/*** spring允许通过Qualifier指定注入bean的名称** <bean id="myTestBean" class="bean.MyTestBean">*     <qualifier type="org.Springframework.beans.factory.annotation.Qualifier" value="qf"/>* </bean>** Qualifier中必须含有type属性*/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);} 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;
}

bean标签中的所有属性都在这里被解析完成,下面继续看一些复杂标签的属性解析:

1.1.1 创建用于承载属性的 BeanDefinition

BeanDefinition是一个接口,抽象类 AbstractBeanDefinition实现了这个接口;AbstractBeanDefinition是配置文件 元素标签在容器中的内部表现形式。元素标签中有 class、scope、lazy-init等属性,AbstractBeanDefinition中则提供了相应的 beanClass、scope、lazyInit属性,AbstractBeanDefinition和中的属性是一一对应的。

1.1.1.1 AbstractBeanDefinition

AbstractBeanDefinition.java

public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessorimplements BeanDefinition, Cloneable {// bean的作用范围,对应bean属性 scope@Nullableprivate String scope = SCOPE_DEFAULT;// 是否是抽象,对应bean属性 abstractprivate boolean abstractFlag = false;// 是否延迟加载,对应bean属性 lazy-init@Nullableprivate Boolean lazyInit;// 自动注入模式,对应bean属性 autowireprivate int autowireMode = AUTOWIRE_NO;// 依赖检查,3.0 后弃用此属性private int dependencyCheck = DEPENDENCY_CHECK_NONE;// 用来表示一个bean的实例化依靠另一个bean先实例化,对应bean属性 depend-on@Nullableprivate String[] dependsOn;// autowire-candidate 属性设置为false,容器在查找自动装配对象时,将不考虑该bean;// 即它不会被考虑作为其他bean自动装配的候选者,但是该bean本身还是可以使用自动装配来注入其他 bean// 对应bean属性 autowire-candidateprivate boolean autowireCandidate = true;// 自动装配时出现多个bean后选择,将作为首选者,对应bean属性 primaryprivate boolean primary = false;// 用于记录Qualifier,对应bean属性 子元素qualifiersprivate final Map<String, AutowireCandidateQualifier> qualifiers = new LinkedHashMap<>();@Nullableprivate Supplier<?> instanceSupplier;// 允许访问非公开的构造器和方法,程序设置private boolean nonPublicAccessAllowed = true;// 是否以一种宽松的模式解析构造函数,默认为trueprivate boolean lenientConstructorResolution = true;// 对应bean属性 factory-bean/*** 用法:* <bean id="instanceFactoryBean" class="example.chapter3.InstanceFactoryBean"/>* <bean id="currentTime" factory-bean="instanceFactoryBean" factory-method="createTime"/>*/@Nullableprivate String factoryBeanName;// 对应bean属性 factory-method@Nullableprivate String factoryMethodName;// 记录构造函数注入属性,对应bean属性 子元素 constructor-arg@Nullableprivate ConstructorArgumentValues constructorArgumentValues;// 记录普通属性集合,对应bean属性 子元素 property@Nullableprivate MutablePropertyValues propertyValues;// 方法重写的持有者,记录lookup-method、replaced-method元素,对应bean属性 子元素 lookup-method、replaced-methodprivate MethodOverrides methodOverrides = new MethodOverrides();// 初始化方法,对应bean属性 init-method@Nullableprivate String initMethodName;// 销毁方法,对应bean属性 destroy-method@Nullableprivate String destroyMethodName;// 是否执行 init-method,程序设定private boolean enforceInitMethod = true;// 是否执行 destroy-method,程序设定private boolean enforceDestroyMethod = true;// 是否是用户定义的而不是应用程序本身定义的,创建AOP的时候为true,程序设置private boolean synthetic = false;private int role = BeanDefinition.ROLE_APPLICATION;// bean的描述信息@Nullableprivate String description;// 这个bean定义的资源@Nullableprivate Resource resource;//省略其他方法
}

抽象类AbstractBeanDefinition有三种具体实现:RootBeanDefinition、ChildBeanDefinition、GenericBeanDefinition。其中RootBeanDefinition是常用的实现类,对应一般性的元素标签;GenericBeanDefinition是2.5版本后新加入的实现类,是一站式服务类,基本实现了所有属性;在XML配置文件中可以定义父和子,父用RootBeanDefinition表示,子用ChildBeanDefinition表示;

1.1.1.2 GenericBeanDefinition

GenericBeanDefinition.java

public class GenericBeanDefinition extends AbstractBeanDefinition {@Nullableprivate String parentName;// 忽略其他方法
}

Spring通过 BeanDefinition将配置文件中的配置信息转换成了容器的内部表示形式。因此,要解析属性首先就要创建用于承载属性的实例,也就是创建GenericBeanDefinition类型的实例

protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName)throws ClassNotFoundException {return BeanDefinitionReaderUtils.createBeanDefinition(parentName, className, this.readerContext.getBeanClassLoader());
}

BeanDefinitionReaderUtils.java

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) {// 如果classLoader不为空,就使用传入的classLoader同一虚拟机加载类对象,否则只是记录 classNamebd.setBeanClass(ClassUtils.forName(className, classLoader));}else {bd.setBeanClassName(className);}}return bd;
}

在这里如果传入的classLoader不为空,就直接使用这个classLoader根据解析出来的className通过反射创建对应的Class对象;如果为空,就只是设置className。

1.1.2 解析各种属性

在创建了bean信息的承载实例后,就可以进行bean信息的各种属性的解析了:

public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName,@Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) {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.// 在嵌入 BeanDefinition情况下,且没有单独制定scope属性,则使用父类默认的属性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 (isDefaultValue(lazyInit)) {lazyInit = this.defaults.getLazyInit();}// 如果没有设置lazy-init属性,或者被设置为其他字符,都会被设置为falsebd.setLazyInit(TRUE_VALUE.equals(lazyInit));// 解析 autowire属性String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE);// 设置自动装配的模式bd.setAutowireMode(getAutowireMode(autowire));// 解析 dependency-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 (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));}// 解析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;
}

在这个方法里面,Spring对bean的所有属性进行一一的解析,解析之后设置到标签的属性承载实例 GenericBeanDefinition中。

1.1.3 解析子元素 meta

parseMetaElements(ele, bd);

元数据meta的使用:

<bean id="user" class="org.springframework.beans.example.bean.User"><meta key="testStr" value="aaa"/>
</bean>
标签并不会体现在User的属性中,而是一个额外的声明,当需要使用里面的信息的时候,可以通过 BeanDefinition的getAttribute(key)方法进行获取。

回到代码:

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;// 提取metaString key = metaElement.getAttribute(KEY_ATTRIBUTE);String value = metaElement.getAttribute(VALUE_ATTRIBUTE);// 使用key value构造BeanMetadataAttributeBeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value);attribute.setSource(extractSource(metaElement));attributeAccessor.addMetadataAttribute(attribute);}}
}

1.1.4 解析子元素 lookup-method

parseLookupOverrideSubElements(ele, bd.getMethodOverrides());

子元素 lookup-method不是很常用,但是在某些时候也是非常有用的属性,通常称它为 获取器注入

获取器注入是一种特殊的方法注入,它是把一个方法声明为返回某种类型的bean;但实际上要返回的具体bean是在xml配置文件里配置的,这个方法就可以用在设计有些可插拔的功能上

使用示例:

  1. 首先创建一个父类:
public class User {public void showMe() {System.out.println("i am user.");}
}
  1. 创建一个子类并覆盖父类的showMe()方法:
public class Teacher extends User {@Overridepublic void showMe() {System.out.println("i am teacher.");}
}
  1. 创建一个调用类,使用获取器注入:
public abstract class LookupTest {// 返回一个User类型的bean,但可以有User的不同实现类public abstract User getUser();public static void main(String[] args) {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:lookup-method-context.xml");LookupTest lookupTest = (LookupTest)applicationContext.getBean("lookupTest");User user = lookupTest.getUser();user.showMe();}
}

到这里整个测试方法已经完成了,但是会有一个疑问:这个抽象类的抽象方法都没有被实现,怎么可以直接调用呢?

原因就是需要使用Spring提供的获取器注入,xml配置文件为:

<?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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="lookupTest" class="org.bgy.spring.study.spring.bean.definition.lookup.method.LookupTest"><lookup-method name="getUser" bean="teacher"/></bean><bean id="teacher" class="org.bgy.spring.study.spring.bean.definition.lookup.method.Teacher"/></beans>

在这个配置文件中,使用了 lookup-method 子元素,这个配置的功能就是动态地将teacher的bean,作为getUser()方法的返回值,实现了把bean注入到LookupTest中
此时控制台会打印:i am teacher.

如果业务变更了,我们需要有不同的User来实现不同的showMe()的逻辑的时候,我们可以增加新的逻辑类:

public class Student extends User {@Overridepublic void showMe() {System.out.println("i am student.");}
}

同时修改配置文件:

<?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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="lookupTest" class="org.bgy.spring.study.spring.bean.definition.lookup.method.LookupTest"><lookup-method name="getUser" bean="student"/></bean><bean id="teacher" class="org.bgy.spring.study.spring.bean.definition.lookup.method.Teacher"/><bean id="student" class="org.bgy.spring.study.spring.bean.definition.lookup.method.Student"/>
</beans>

此时控制台会打印:i am student.

回到代码:

public void parseLookupOverrideSubElements(Element beanEle, MethodOverrides overrides) {NodeList nl = beanEle.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);// 仅在spring默认bean的子元素下,且为 <lookup-method>时有效if (isCandidateElement(node) && nodeNameEquals(node, LOOKUP_METHOD_ELEMENT)) {Element ele = (Element)node;// 获取要修饰的方法String methodName = ele.getAttribute(NAME_ATTRIBUTE);// 获取配置中要返回的 beanString beanRef = ele.getAttribute(BEAN_ELEMENT);// 构造一个 LookupOverride 属性(也是MethodOverride属性)LookupOverride override = new LookupOverride(methodName, beanRef);override.setSource(extractSource(ele));// 记录在了BeanDefinition的 methodOverride属性中overrides.addOverride(override);}}
}

1.1.5 解析子元素 replaced-method

parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

方法替换:Spring可以在运行时用新的方法替换现有的方法;不但可以动态地替换返回的bean类型,还能动态地更改原有方法的逻辑。

使用示例:

  1. 先创建一个带有changeMe()方法的测试类,并且增加main()方法进行测试:
public class ReplacedTest {public void changeMe() {System.out.println("change me.");}public static void main(String[] args) {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:replaced-method-context.xml");ReplacedTest replacedTest = (ReplacedTest)applicationContext.getBean("replacedTest");replacedTest.changeMe();}
}
  1. 在需要改变changeMe()方法的逻辑的时候,创建一个替换类:
public class MethodReplacerTest implements MethodReplacer {@Overridepublic Object reimplement(Object obj, Method method, Object[] args) throws Throwable {System.out.println("changed.");return null;}
}
  1. 在xml文件中进行配置,使替换类生效:
<?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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="replacedTest" class="org.bgy.spring.study.spring.bean.definition.replaced.method.ReplacedTest"><replaced-method name="changeMe" replacer="methodReplacerTest"/></bean><bean id="methodReplacerTest" class="org.bgy.spring.study.spring.bean.definition.replaced.method.MethodReplacerTest"/>
</beans>

此时控制台会打印:changed.

使用了标签,把replacedTest的bean的changeMe()方法的返回值和具体的实现逻辑,动态替换成了methodReplacerTest的bean中的重载方法。

回到代码:

public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) {NodeList nl = beanEle.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);// 仅在spring默认bean的子元素下,且为 <replaced-method>时有效if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) {Element replacedMethodEle = (Element)node;// 获取要替换的旧的方法String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE);// 获取对应的新的替换方法String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE);// 构造一个 ReplaceOverride 属性(也是MethodOverride属性)ReplaceOverride replaceOverride = new ReplaceOverride(name, callback);// Look for arg-type match elements.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));// 记录在了BeanDefinition的 methodOverride属性中overrides.addOverride(replaceOverride);}}

这里基本就只比上面的子元素 lookup-method 多了一个记录参数的步骤;最终都是把 methodOverride 记录在BeanDefinition中。

1.1.6 解析子元素 constructor-arg

parseConstructorArgElements(ele, bd);

xml中使用构造函数注入,应该是使用最多的方式之一,所以对构造函数的解析是非常常用的,但是也是非常复杂的。

使用示例:

  1. 首先创建一个有两个属性的bean类:
public class ConstructorArg {private String name;private String age;public ConstructorArg() {}public ConstructorArg(String name, String age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAge() {return age;}public void setAge(String age) {this.age = age;}
}
  1. 使用xml对这个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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="constructorArg" class="org.bgy.spring.study.spring.bean.definition.constructor.arg.ConstructorArg"><!-- 通过index属性--><constructor-arg index="0" value="bgy"><!-- <value>bgy</value> --></constructor-arg><!-- 通过name属性--><constructor-arg name="age" value="25"><!-- <value>25</value> --></constructor-arg></bean>
</beans>

这样就可以实现,对constructorArg这个bean自动寻找对应这两个参数的构造函数,并在初始化的时候将设置的参数传入进去。

回到代码:

public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) {NodeList nl = beanEle.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);// 遍历所有子元素,提取所有的 constructor-arg,进行解析if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) {// 解析 constructor-argparseConstructorArgElement((Element)node, bd);}}
}

跟进:

public void parseConstructorArgElement(Element ele, BeanDefinition bd) {// 提取index属性String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE);// 提取type属性String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE);// 提取name属性String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);// 如果配置了 index属性if (StringUtils.hasLength(indexAttr)) {try {int index = Integer.parseInt(indexAttr);if (index < 0) {error("'index' cannot be lower than 0", ele);} else {try {this.parseState.push(new ConstructorArgumentEntry(index));// 解析ele对应的属性元素Object value = parsePropertyValue(ele, bd, null);// 创建一个 ConstructorArgumentValues.ValueHolder 实例,用于封装解析出来的元素;ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);// 并将type、name、index等属性一起封装到ConstructorArgumentValues.ValueHolder中if (StringUtils.hasLength(typeAttr)) {valueHolder.setType(typeAttr);}if (StringUtils.hasLength(nameAttr)) {valueHolder.setName(nameAttr);}valueHolder.setSource(extractSource(ele));if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) {error("Ambiguous constructor-arg entries for index " + index, ele);} else {// 添加到当前BeanDefinition的ConstructorArgumentValues的 indexedArgumentValues属性中bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder);}} finally {this.parseState.pop();}}} catch (NumberFormatException ex) {error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele);}// 如果没有index属性则忽略此属性,自动寻找} else {try {this.parseState.push(new ConstructorArgumentEntry());// 解析ele对应的属性元素Object value = parsePropertyValue(ele, bd, null);// 创建一个 ConstructorArgumentValues.ValueHolder 实例,用于封装解析出来的元素;ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);// 并将type、name、index等属性一起封装到ConstructorArgumentValues.ValueHolder中if (StringUtils.hasLength(typeAttr)) {valueHolder.setType(typeAttr);}if (StringUtils.hasLength(nameAttr)) {valueHolder.setName(nameAttr);}valueHolder.setSource(extractSource(ele));// 添加到当前BeanDefinition的ConstructorArgumentValues的 genericArgumentValues属性中bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder);} finally {this.parseState.pop();}}
}

在这个方法中:

  1. 提取constructor-arg上必要的元素(index、type、name);
  2. 判断是否指定了index属性,如果指定了先判断index不小于0;
  3. 解析constructor-arg的子元素;
  4. 使用 ConstructorArgumentValues.ValueHolder类型来封装解析出来的元素;将type、name、index等属性都set进ConstructorArgumentValues.ValueHolder中;
  5. 如果指定了index,就把index和valueHolder添加到当前的BeanDefinition的 ConstructorArgumentValues 的 indexedArgumentValues 属性中
  6. 如果没有指定index,就把ValueHolder添加到当前的 BeanDefinition的 ConstructorArgumentValues 的 genericArgumentValues 属性中

可以看到,对于是否指定index,Spring的处理流程是不同的,关键在于属性信息的保存位置是indexedArgumentValues 还是 genericArgumentValues。

进一步了解解析constructor-arg的子元素:

public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) {String elementName = (propertyName != null ?"<property> element for property '" + propertyName + "'" :"<constructor-arg> element");// Should only have one child element: ref, value, list, etc.// 一个属性只能对应一种类型:ref、value、list等NodeList nl = ele.getChildNodes();Element subElement = null;for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);// 如果是 description或者meta就不处理if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&!nodeNameEquals(node, META_ELEMENT)) {// Child element is what we're looking for.if (subElement != null) {error(elementName + " must not contain more than one sub-element", ele);} else {subElement = (Element)node;}}}// 提取 <constructor-arg>标签中的 ref和value属性,以便于根据规则验证正确性;boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);// 不能同时有 ref和value;// 或有ref或者value的时候,不能有子元素if ((hasRefAttribute && hasValueAttribute) || ((hasRefAttribute || hasValueAttribute) && subElement != null)) {error(elementName +" is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele);}// ref 属性的处理if (hasRefAttribute) {String refName = ele.getAttribute(REF_ATTRIBUTE);if (!StringUtils.hasText(refName)) {error(elementName + " contains empty 'ref' attribute", ele);}// 使用RuntimeBeanReference封装对应的 ref名称, 如 <constructor-arg ref="a">RuntimeBeanReference ref = new RuntimeBeanReference(refName);ref.setSource(extractSource(ele));return ref;} else if (hasValueAttribute) { // value属性的处理// 使用TypedStringValue 封装对应的 value, 如 <constructor-arg value="a">TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));valueHolder.setSource(extractSource(ele));return valueHolder;} else if (subElement != null) {// 解析子元素/*** <constructor-arg>*     <map>*         <entry key="key" value="value"/>*     </map>* </constructor-arg>*/return parsePropertySubElement(subElement, bd);} else {// Neither child element nor "ref" or "value" attribute found.// 既没有 ref有没有value,也没有子元素,会报错error(elementName + " must specify a ref or value", ele);return null;}
}

这个方法不止用于解析constructor-arg标签的元素,还可以用于解析其他的property元素;如果用于解析constructor-arg元素的时候,propertyName为null。

解析步骤:

  1. 一个constructor-arg标签对应于构造函数中的一个属性,一个属性只能有一种类型(ref、value、list)。
    先遍历constructor-arg下所有子元素,如果是description或者meta属性,直接跳过不处理;否则记录下子元素,如果有多个,抛出error错误。
  2. 提取constructor-arg标签上的ref和value属性,并且根据规则验证其正确性;规则:
    1. 同时既有ref属性又有value属性,错误;
    2. 存在ref属性或者value属性的时候,又有子元素,错误;
  3. 根据constructor-arg标签指定的属性情况,进行处理:
    1. 如果是指定了ref属性,使用 RuntimeBeanReference 封装ref对应的bean名称,如:,并且返回RuntimeBeanReference;
    2. 如果是指定了value属性,使用TypedStringValue封装value对应的具体指,如:,并且返回使用TypedStringValue封装value对应的具体指;
    3. 如果都没有指定,且subElement不为null,则是使用了子元素的方式进行指定,则需要解析子元素;
    4. 如果都不满足,抛出error错误;

继续跟进对于各种子元素的分类处理:

public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) {return parsePropertySubElement(ele, bd, null);
}public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) {if (!isDefaultNamespace(ele)) {// 如果不是默认命名空间,调用解析自定义元素的方法进行解析;(会根据自定义的解析器进行解析)return parseNestedCustomElement(ele, bd);} else if (nodeNameEquals(ele, BEAN_ELEMENT)) {// 如果子元素的bean,则又递归调用bean标签的解析过程;BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd);if (nestedBd != null) {nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd);}return nestedBd;} else if (nodeNameEquals(ele, REF_ELEMENT)) { // 对ref子元素的解析// A generic reference to any name of any bean.String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE);boolean toParent = false;if (!StringUtils.hasLength(refName)) {// A reference to the id of another bean in a parent context.refName = ele.getAttribute(PARENT_REF_ATTRIBUTE);toParent = true;if (!StringUtils.hasLength(refName)) {error("'bean' or 'parent' is required for <ref> element", ele);return null;}}if (!StringUtils.hasText(refName)) {error("<ref> element contains empty target attribute", ele);return null;}RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent);ref.setSource(extractSource(ele));return ref;} else if (nodeNameEquals(ele, IDREF_ELEMENT)) { // 对idref子元素的解析return parseIdRefElement(ele);} else if (nodeNameEquals(ele, VALUE_ELEMENT)) { // 对value子元素的解析return parseValueElement(ele, defaultValueType);} else if (nodeNameEquals(ele, NULL_ELEMENT)) { // 对null子元素的解析// It's a distinguished null value. Let's wrap it in a TypedStringValue// object in order to preserve the source location.TypedStringValue nullHolder = new TypedStringValue(null);nullHolder.setSource(extractSource(ele));return nullHolder;} else if (nodeNameEquals(ele, ARRAY_ELEMENT)) { // 解析array子元素return parseArrayElement(ele, bd);} else if (nodeNameEquals(ele, LIST_ELEMENT)) { // 解析list子元素return parseListElement(ele, bd);} else if (nodeNameEquals(ele, SET_ELEMENT)) { // 解析set子元素return parseSetElement(ele, bd);} else if (nodeNameEquals(ele, MAP_ELEMENT)) { // 解析map子元素return parseMapElement(ele, bd);} else if (nodeNameEquals(ele, PROPS_ELEMENT)) { // 解析props子元素return parsePropsElement(ele);} else {error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele);return null;}
}

解析步骤:

  1. 首先判断是不是自定义子元素,如果是的话就调用parseNestedCustomElement()方法进行解析,这个方法里面会获取到自定义元素的自定义解析器NamespaceHandler(下一章会讲)进行解析;
  2. 如果子元素是bean,则又会递归调用bean标签的解析步骤;
  3. 剩下的就是对ref、value、list…等元素的解析;

总的来说,在这个方法中,实现了所有支持的子元素的解析处理,到这里构造函数的解析流程基本完成。

1.1.7 解析子元素 property

parsePropertyElements(ele, bd);

xml中使用setter方法注入,也是使用得最多的方式之一。
使用示例:

<bean id="people" class="org.bgy.spring.study.spring.bean.definition.property.People"><property name="name" value="bgy"/><property name="age" value="25"/>
</bean>

现在来看一下对于setter方法注入的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);// 遍历所有元素,解析property属性的元素if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {parsePropertyElement((Element)node, bd);}}
}

跟进:

public void parsePropertyElement(Element ele, BeanDefinition bd) {// 获取配置元素中 name 的值String propertyName = ele.getAttribute(NAME_ATTRIBUTE);if (!StringUtils.hasLength(propertyName)) {error("Tag 'property' must have a 'name' attribute", ele);return;}this.parseState.push(new PropertyEntry(propertyName));try {// 不允许多次对同一属性进行配置if (bd.getPropertyValues().contains(propertyName)) {error("Multiple 'property' definitions for property '" + propertyName + "'", ele);return;}// 解析ele对应的属性元素Object val = parsePropertyValue(ele, bd, propertyName);// 创建一个 PropertyValue 实例,用于封装解析出来的元素PropertyValue pv = new PropertyValue(propertyName, val);// 可能存在 meta,解析metaparseMetaElements(ele, pv);pv.setSource(extractSource(ele));// 添加到当前BeanDefinition的MutablePropertyValues的 propertyValueList属性中bd.getPropertyValues().addPropertyValue(pv);} finally {this.parseState.pop();}
}

解析步骤:

  1. 这里先通过BeanDefinition的processedProperties属性判断当前property属性是否已经被解析过了,如果解析过了直接抛出error异常;
  2. 根据propertyName来解析property元素;
  3. 把解析出来的元素封装到PropertyValue中;
  4. 记录到过BeanDefinition的processedProperties属性中;

这里你会发现,解析元素的的时候,使用的是我们上面的parsePropertyValue方法,只是propertyName参数不是null了,而是property标签的propertyName;所以解析方式跟上面一样。

1.1.8 解析子元素 qualifier

parseQualifierElements(ele, bd);

qualifier属性的使用:

  • 在Spring中,进行自动注入的时候,Spring容器中匹配的bean的数量必须有且只有一个;如果按照类型匹配,可能出现两个相同类型的bean,这个时候Spring容器就会抛出 NoUniqueBeanDefinitionException的异常,并且指出期望的是单个匹配的bean。
    这个时候,就可以使用qualifier标签来手动指定匹配的bean:
    <bean id="myTestBean" class="bean.MyTestBean"><qualifier type="org.Springframework.beans.factory.annotation.Qualifier" value="qf"/>
    </bean>
    
  • 不过实际中,我们接触更多的是使用 @Qualifier注解的形式,特别是使用@Autowired注解时;详细示例可见:@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);// 遍历所有元素,解析qualifier属性的元素if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ELEMENT)) {parseQualifierElement((Element)node, bd);}}
}

跟进:

public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) {// 获取配置元素中 type的值String typeName = ele.getAttribute(TYPE_ATTRIBUTE);if (!StringUtils.hasLength(typeName)) {error("Tag 'qualifier' must have a 'type' attribute", ele);return;}this.parseState.push(new QualifierEntry(typeName));try {AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(typeName);qualifier.setSource(extractSource(ele));String value = ele.getAttribute(VALUE_ATTRIBUTE);if (StringUtils.hasLength(value)) {qualifier.setAttribute(AutowireCandidateQualifier.VALUE_KEY, value);}NodeList nl = ele.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ATTRIBUTE_ELEMENT)) {Element attributeEle = (Element)node;String attributeName = attributeEle.getAttribute(KEY_ATTRIBUTE);String attributeValue = attributeEle.getAttribute(VALUE_ATTRIBUTE);if (StringUtils.hasLength(attributeName) && StringUtils.hasLength(attributeValue)) {BeanMetadataAttribute attribute = new BeanMetadataAttribute(attributeName, attributeValue);attribute.setSource(extractSource(attributeEle));qualifier.addMetadataAttribute(attribute);} else {error("Qualifier 'attribute' tag must have a 'name' and 'value'", attributeEle);return;}}}// 添加到当前BeanDefinition的 qualifiers属性中bd.addQualifier(qualifier);} finally {this.parseState.pop();}
}

这里主要就是创建一个AutowireCandidateQualifier类的实例qualifier,然后把解析到的各个属性封装到这个实例中,最终把这个类的实例添加到BeanDefinition的 qualifiers属性中。

接着,设置资源,返回BeanDefinition:

bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));
return bd;

至此,我们完成了整个对XML文档到 GenericBeanDefinition的转换过程,也就是说,XML文档中的所有配置都可以在 GenericBeanDefinition的实例中找到对应的配置。

回到这里把BeanDefinition封装到BeanDefinitionHolder中,并返回BeanDefinitionHolder的实例bdHolder;返回之后,要做的是:如果需要的话,就对BeanDefinition进行装饰:

DefaultBeanDefinitionDocumentReader.java

BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {// 如果需要的话,就对BeanDefinition进行装饰/*** 适用场景:* 当spring中的bean使用的是默认的标签配置,但是其中的子元素是自定义配置(子元素,不是以bean的形式存在,是一个属性)** <bean id="test" class="test.MyClass">*     <mybean:user username="aaa"/>* </bean>*/bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);// 省略其他部分}
}

适用场景在源码注解中也已经写了:XML文件中的标签使用的是默认标签,但是其中的子元素使用了自定义的标签元素(这个自定义类型不是以Bean的形式出现的,而是以属性的形式出现的),这个时候就需要再对子元素的自定义标签进行解析。

1.2 解析默认标签中的自定义标签元素

bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);

BeanDefinitionParserDelegate.java

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

这里的第三个参数是父类BeanDefinition(作用为使用父类的scope属性),当对某个嵌套配置进行分析时,这里需要传递父类的BeanDefinition;这里解析的是顶层配置,所以传递null。

public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) {BeanDefinitionHolder finalDefinition = originalDef;// Decorate based on custom attributes first.NamedNodeMap attributes = ele.getAttributes();// 遍历所有的属性,看看是否有适合于修饰的属性for (int i = 0; i < attributes.getLength(); i++) {Node node = attributes.item(i);finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);}// Decorate based on custom nested elements.NodeList children = ele.getChildNodes();// 遍历所有的子元素,看看是否有适合修饰的子元素for (int i = 0; i < children.getLength(); i++) {Node node = children.item(i);if (node.getNodeType() == Node.ELEMENT_NODE) {finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);}}return finalDefinition;
}

在上面的代码中,分别对这个自定义元素的所有属性 以及子节点进行了decorateIfRequired,继续跟进:

public BeanDefinitionHolder decorateIfRequired(Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) {// 获取自定义标签的命名空间String namespaceUri = getNamespaceURI(node);// 对于非默认标签进行修饰(对于默认标签的处理是直接跳过的,因为默认标签到这里已经被处理完了,这里只对自定义的标签或者bean的自定义属性处理)if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) {// 根据命名空间找到对应的处理器NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);if (handler != null) {// 进行修饰BeanDefinitionHolder decorated = handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd));if (decorated != null) {return decorated;}} else if (namespaceUri.startsWith("http://www.springframework.org/schema/")) {error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node);} else {// A custom namespace, not to be handled by Spring - maybe "xml:...".if (logger.isDebugEnabled()) {logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]");}}}return originalDef;
}

在这里首先获取自定义标签的命名空间,并且只处理非默认命名空间的标签(因为默认标签的解析到这里是已经被处理完了的);然后根据自定义标签的命名空间,找到对应的自定义解析器NamespaceHandler进行解析。(跟这里的解析一样)

1.3 注册解析到的BeanDefinition

来到这里,对于XML配置文件,解析也解析完了装饰也装饰完了,这个时候的BeanDefinition已经可以满足后续的使用要求了,现在只需要再对BeanDefinition进行注册:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {// 委托BeanDefinitionParserDelegate 类的 parseBeanDefinitionElement 方法进行元素解析,返回BeanDefinitionHolder 类型的实例;// BeanDefinitionHolder:Holder for a BeanDefinition with name and aliases.// bdHolder实例已经包含了配置文件中配置的各种属性了(class、name、id、alias)BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {// 如果需要的话,就对BeanDefinition进行装饰/*** 适用场景:* 当spring中的bean使用的是默认的标签配置,但是其中的子元素是自定义配置(子元素,不是以bean的形式存在,是一个属性)** <bean id="test" class="test.MyClass">*     <mybean:user username="aaa"/>* </bean>*/bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);// 解析、装饰 完成后,对于得到的BeanDefinition已经可以满足后续的使用要求了,只需要进行注册了;// 对解析后的 BeanDefinition 进行注册try {// 注册操作委托给了 BeanDefinitionReaderUtils// Register the final decorated instance.BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to register bean definition with name '" +bdHolder.getBeanName() + "'", ele, ex);}// 发出响应事件,通知相关的监听器,这个bean的解析和注册 已经完成了// Send registration event.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));}
}BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());

BeanDefinitionReaderUtils.java

public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {// 使用beanName做唯一注册标识// Register bean definition under primary name.String beanName = definitionHolder.getBeanName();// 使用beanName的方式 注册 BeanDefinitionregistry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());// Register aliases for bean name, if any.// 遍历注册所有的别名String[] aliases = definitionHolder.getAliases();if (aliases != null) {for (String alias : aliases) {// 通过别名的方式 注册BeanDefinitionregistry.registerAlias(beanName, alias);}}
}

在上面的代码中,把解析得到的BeanDefinition注册到了BeanDefinitionRegistry类型的实例registry中,而对于BeanDefinition的注册分为两个部分:通过beanName的方式注册 以及 通过别名的方式注册。

1.3.1 通过beanName注册BeanDefinition

void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException;

DefaultListableBeanFactory.java

//---------------------------------------------------------------------
// Implementation of BeanDefinitionRegistry interface
//---------------------------------------------------------------------
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {Assert.hasText(beanName, "Bean name must not be empty");Assert.notNull(beanDefinition, "BeanDefinition must not be null");if (beanDefinition instanceof AbstractBeanDefinition) {try {/*** 注册前的最后一次校验,这个校验不同于之前的XML文件校验* 主要是对于AbstractBeanDefinition 属性中的methodOverrides校验* 校验methodOverrides 是否与工厂方法并存,或者methodOverrides对应的方法根本不存在*/((AbstractBeanDefinition) beanDefinition).validate();}catch (BeanDefinitionValidationException ex) {throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,"Validation of bean definition failed", ex);}}// 使用了 ConcurrentHashMap,线程安全BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);// 如果这个beanName已经注册了if (existingDefinition != null) {// 如果不允许被覆盖,直接抛出异常if (!isAllowBeanDefinitionOverriding()) {throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);}else if (existingDefinition.getRole() < beanDefinition.getRole()) {// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTUREif (logger.isInfoEnabled()) {logger.info("Overriding user-defined bean definition for bean '" + beanName +"' with a framework-generated bean definition: replacing [" +existingDefinition + "] with [" + beanDefinition + "]");}}else if (!beanDefinition.equals(existingDefinition)) {if (logger.isDebugEnabled()) {logger.debug("Overriding bean definition for bean '" + beanName +"' with a different definition: replacing [" + existingDefinition +"] with [" + beanDefinition + "]");}}else {if (logger.isTraceEnabled()) {logger.trace("Overriding bean definition for bean '" + beanName +"' with an equivalent definition: replacing [" + existingDefinition +"] with [" + beanDefinition + "]");}}// 加入map缓存,注册BeanDefinitionthis.beanDefinitionMap.put(beanName, beanDefinition);}// 这个beanName没有被注册else {if (hasBeanCreationStarted()) {// Cannot modify startup-time collection elements anymore (for stable iteration)synchronized (this.beanDefinitionMap) {// 加入map缓存,注册BeanDefinitionthis.beanDefinitionMap.put(beanName, beanDefinition);List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);updatedDefinitions.addAll(this.beanDefinitionNames);updatedDefinitions.add(beanName);this.beanDefinitionNames = updatedDefinitions;removeManualSingletonName(beanName);}}else {// Still in startup registration phase// 加入map缓存,注册BeanDefinitionthis.beanDefinitionMap.put(beanName, beanDefinition);this.beanDefinitionNames.add(beanName);removeManualSingletonName(beanName);}this.frozenBeanDefinitionNames = null;}if (existingDefinition != null || containsSingleton(beanName)) {// 重置beanName对应的缓存resetBeanDefinition(beanName);}else if (isConfigurationFrozen()) {clearByTypeCache();}
}

主要步骤:

  1. 最后再对BeanDefinition进行一次校验,这里校验的主要是对于 methodOverrides 属性的校验:
    • methodOverrides属性和工厂方法一起存在时,抛出异常;
    • 检查methodOverrides对应的方法不存在时,抛出异常;
  2. beanName已经注册的情况下,如果设置了不允许bean的覆盖,则抛出异常;否则直接覆盖并加入map缓存,即注册BeanDefinitionthis.beanDefinitionMap.put(beanName, beanDefinition);
  3. beanName没有被注册的情况下,使用同步锁进行加入map缓存,,即注册BeanDefinitionthis.beanDefinitionMap.put(beanName, beanDefinition);
  4. 清除解析之前的beanName对应的缓存;

1.3.2 通过别名注册BeanDefinition

void registerAlias(String name, String alias);

SimpleAliasRegistry.java

public void registerAlias(String name, String alias) {Assert.hasText(name, "'name' must not be empty");Assert.hasText(alias, "'alias' must not be empty");synchronized (this.aliasMap) {// 如果beanName与alias相同的话,不记录alias,并删除对应的aliasif (alias.equals(name)) {this.aliasMap.remove(alias);if (logger.isDebugEnabled()) {logger.debug("Alias definition '" + alias + "' ignored since it points to same name");}}else {String registeredName = this.aliasMap.get(alias);// 如果这个alias已经被注册了if (registeredName != null) {// 并且注册的beanName是当前要注册的beanName,直接返回,不需要重复注册if (registeredName.equals(name)) {// An existing alias - no need to re-registerreturn;}// 并且注册的beanName不是当前的,也就是说被指向了别的beanName;// 并且还不允许覆盖的话,抛出异常if (!allowAliasOverriding()) {throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +name + "': It is already registered for name '" + registeredName + "'.");}if (logger.isDebugEnabled()) {logger.debug("Overriding alias '" + alias + "' definition for registered name '" +registeredName + "' with new target name '" + name + "'");}}// alias循环检查checkForAliasCircle(name, alias);// 加入map,注册aliasthis.aliasMap.put(alias, name);if (logger.isTraceEnabled()) {logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'");}}}
}

主要步骤:

  1. 如果beanName与alias相同的话则不需要处理,并且删除原有aliasMap缓存中的alias;
  2. 如果这个alias已经被注册过了,分为两种情况:
    • 注册的beanName就是当前的beanName,则不需要重复注册,直接返回;
    • 如果注册的beanName不是当前的beanName,再判断是否允许覆盖,如果不允许则抛出异常;
  3. alias的循环检查:若 A -> B 存在时,如果再出现 A -> C -> B 的情况,则抛出异常;
  4. 把alias和对应的beanName注册到aliasMap缓存中;

1. 4 解析及注册完成,通知监听器

getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));

这里的实现只是为了扩展,目前Spring中没有对这个事件做任务逻辑的处理;当开发人员需要对注册BeanDefinition事件进行监听时,可以通过注册监听器的方式,并将处理逻辑写入监听器中。

2. 嵌入式beans标签的解析

回到开头,我们对配置文件的解析包括了:import标签、alias标签、bean标签、beans标签;通过上面的内容我们已经完成了对最复杂的bean标签的解析,接下就继续对beans标签的解析。

嵌入式beans标签的使用示例:

<?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/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd"><bean></bean><beans><bean></bean></beans>
</beans>

DefaultBeanDefinitionDocumentReader.java

doRegisterBeanDefinitions(ele);protected void doRegisterBeanDefinitions(Element root) {// 专门处理解析// BeanDefinitionParserDelegate 用于解析 xml bean 的状态委托类BeanDefinitionParserDelegate parent = this.delegate;this.delegate = createDelegate(getReaderContext(), root, parent);// 在这里如果是默认的命名空间,即:http://www.springframework.org/schema/beansif (this.delegate.isDefaultNamespace(root)) {// 处理profile属性(可以配置多套不同的开发环境,便于切换)// 获取bean节点是否定义了profile属性,String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);if (StringUtils.hasText(profileSpec)) {// profile可以同时指定多个String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);// We cannot use Profiles.of(...) since profile expressions are not supported// in XML config. See SPR-12458 for details.// 判断每一个profile是否都符合环境变量中所定义的,如果不符合则不会浪费性能去解析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;}}}// 解析前处理,留给子类实现(空方法,面向继承设计的,如果继承自DefaultBeanDefinitionDocumentReader的类,// 需要在Bean解析前后做一些处理的话,就可以重写这两个方法)preProcessXml(root);// 正式解析 BeanDefinitionparseBeanDefinitions(root, this.delegate);// 解析后处理,留给子类实现(空方法,面向继承设计的,如果继承自DefaultBeanDefinitionDocumentReader的类,// 需要在Bean解析前后做一些处理的话,就可以重写这两个方法)postProcessXml(root);this.delegate = parent;
}

可以看到在这里调用了上一篇文章Spring——1. BeanFactory容器的初始化中 2.2.1 解析BeanDefinitions 讲到的doRegisterBeanDefinitions()方法,其实就是递归调用注册解析BeanDefinitions的过程,等于把本篇文章的所有流程再走一遍。

3. import标签的解析

在比较庞大的Spring项目中,如果整个项目使用一个Spring的配置文件,那么编写和维护起来都会特别麻烦。
Spring提供了import的方法,可以使得用户对于项目中的进行分模块编写配置文件,然后使用import方法导入到 applicationContext.xml 配置文件中:

<?xml version="1.0" encoding="gb2312"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans><import resource="customerContext.xml" /><import resource="manageContext.xml" /><import resource="systemContext.xml" />
</beans>

如果以后有新模块的加入,就可以简单的修改这个配置文件进行导入;这样简化了配置文件的维护,并且使得项目模块化,易于管理。

回到代码看看Spring是如何解析import标签的:

importBeanDefinitionResource(ele);protected void importBeanDefinitionResource(Element ele) {// 获取resource属性所表示的路径String location = ele.getAttribute(RESOURCE_ATTRIBUTE);if (!StringUtils.hasText(location)) {// 如果不存在resource,不做任何处理getReaderContext().error("Resource location must not be empty", ele);return;}// 解析路径中的系统属性,格式如 ${user.dir}// Resolve system properties: e.g. "${user.dir}"location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);Set<Resource> actualResources = new LinkedHashSet<>(4);// 判断location是相对URI还是绝对URI// Discover whether the location is an absolute or relative URIboolean absoluteLocation = false;try {absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();}catch (URISyntaxException ex) {// cannot convert to an URI, considering the location relative// unless it is the well-known Spring prefix "classpath*:"}// Absolute or relative?if (absoluteLocation) {// 如果是绝对URI,则直接根据地址加载对应的配置文件try {int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);if (logger.isTraceEnabled()) {logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]");}}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to import bean definitions from URL location [" + location + "]", ele, ex);}}else {// No URL -> considering resource location as relative to the current file.try {int importCount;// Resource存在多个实现类,如 ClassPathResource、FileSystemResource等;// 而每个resource的createRelative方式实现都不一样,所以这里先使用子类的方法尝试解析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);}if (logger.isTraceEnabled()) {logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]");}}catch (IOException ex) {getReaderContext().error("Failed to resolve current resource location", ele, ex);}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]", ele, ex);}}// 解析后进行监听器激活处理Resource[] actResArray = actualResources.toArray(new Resource[0]);getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}

主要步骤:

  1. 获取resource属性中指定的资源路径location,如果没有指定则直接返回不做任何处理;
  2. 解析指定的路径中的系统属性,因为可以通过"${user.dir}"等方式直接设置路径值;
  3. 判断location是绝对路径还是相对路径;
  4. 如果是绝对路径,则递归调用XML文档的解析过程,进行另一个XML文档的解析;
  5. 如果是相对路径,则直接根据这个相对路径创建出一个相对资源relativeResource,并判断这个资源是否存在:
    • 如果存在,也是递归调用XML文档的解析过程;
    • 如果不存在,则拿到父XML文档的路径,并且跟相对路径拼接出来一个绝对路径,再递归调用XML文档的解析过程;
  6. 解析完成后,通知监听器;

这里的递归调用XML文档的解析过程,其实就是根据location获取到resource,然后调用上一篇文章中讲的 2. 加载BeanDefinitions 步骤进行解析:

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {ResourceLoader resourceLoader = getResourceLoader();if (resourceLoader == null) {throw new BeanDefinitionStoreException("Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");}if (resourceLoader instanceof ResourcePatternResolver) {// Resource pattern matching available.try {Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);int count = loadBeanDefinitions(resources);if (actualResources != null) {Collections.addAll(actualResources, resources);}if (logger.isTraceEnabled()) {logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");}return count;}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 count = loadBeanDefinitions(resource);if (actualResources != null) {actualResources.add(resource);}if (logger.isTraceEnabled()) {logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");}return count;}
}

4. alias标签的解析

在对bean进定义的时候,除了使用id属性来指定名称之外,还可以提供多个名称,Spring中使用alias标签来实现这个功能。alias标签中所有的名称都指向同一个bean,在某些情况下提供别名非常有用,比如让应用的每个组件都能更容易地对公用组件进行引用

使用方式:

  1. 直接使用bean标签中的name属性:
<bean id="user" name="user1,user2" class="org...."/>
  1. 单独使用alias标签来指定别名:
<bean id="user" class="org...."/>
<alias name="user" alias="user1,user2"/>

一个具体的使用示例:
模块A在XML配置文件中定义了一个名为 componentA的DataSource类型的bean;但模块B想在自己的XML文件中以componentB来引用此bean;模块C又要想在自己的XML文件中以componentC来引用此bean;这样的话就可以这样配置:

<bean id="componentA" class="org...."/>
<alias name="componentA" alias="componentB"/>
<alias name="componentA" alias="componentC"/>

这样一来,各个模块都可以通过唯一的名字来引用同一个数据源,而互不干扰,结构和使用都更清晰。

回到代码:

processAliasRegistration(ele);protected void processAliasRegistration(Element ele) {// 获取nameString name = ele.getAttribute(NAME_ATTRIBUTE);// 获取 aliasString alias = ele.getAttribute(ALIAS_ATTRIBUTE);boolean valid = true;// 必须要有nameif (!StringUtils.hasText(name)) {getReaderContext().error("Name must not be empty", ele);valid = false;}// 必须要有aliasif (!StringUtils.hasText(alias)) {getReaderContext().error("Alias must not be empty", ele);valid = false;}// name和alias都有的情况下,验证通过,valid为trueif (valid) {try {// 注册alias,将alias与beanName组成一对注册到registry中getReaderContext().getRegistry().registerAlias(name, alias);}catch (Exception ex) {getReaderContext().error("Failed to register alias '" + alias +"' for bean with name '" + name + "'", ele, ex);}// 注册alias之后,通知监听器做相应的处理getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));}
}

这里主要就是先对name和alias做验证,并且在name和alias都存在的情况下才去解析;解析的过程,就是上面bean中 通过别名注册BeanDefinition 的过程。

5. 整体流程思维导图

最后附上一个整体流程的思维导图:Spring容器初始化体系,本篇文章对应其中的 默认标签的解析 部分。

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 【Python】Pandas DataFrame 一维表二维表的转换

    目录一、stack & unstackunstack 将一维表转换为二维表stack 将二维表转换为一维表二、pivot & meltpivot 将一维表转换为二维表melt将二维表转换为一维表Tips用pandas处理数据&#xff0c;我们经常获取到的是从数据库或者excel中获取的一维表。而常常需要重排&#xf…...

    2024/5/9 11:24:32
  2. 个人使用:Spring中整合mybatis使用的pom.xml、applicationContext.xml、mybatis.xml文件

    pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 …...

    2024/5/10 0:07:43
  3. 长沙本地4条美食街,名气不大,但很好吃

    长沙本地4条美食街,名气不大,但很好吃长沙美食街以上是长沙的美食街&#xff0c;你去过吗&#xff0c;你有时间一定要去看看。转自&#xff1a;[啦啦问答 -专业的旅游知识问答平台](http://www.676339.com/)长沙美食街 许多朋友正儿八经的吃货。俗话说&#xff0c;吃是一种福气…...

    2024/5/10 2:57:26
  4. 【LeetCode-哈希表】128.最长连续序列

    原题连接&#xff1a; https://leetcode-cn.com/problems/longest-consecutive-sequence/ class Solution {public int longestConsecutive(int[] nums) {int size nums.length;if (size < 1){return size;}// 存在可能重复的元素&#xff0c;而我们注意到题目中要求是不重…...

    2024/5/9 13:43:41
  5. Pytorch中画feature map热力图

    1.画某层所有feature maps求和后的热力图 # 1.1 获取feature maps features ... # 尺度大小&#xff0c;如&#xff1a;torch.Size([1,80,45,45]) # 1.2 每个通道对应元素求和 heatmap torch.sum(features, dim1) # 尺度大小&#xff0c; 如torch.Size([1,45,45]) max_val…...

    2024/5/9 13:25:52
  6. 04. MySQL表_插入查询更新删除数据

    对表中数据的操作一般分为四类, 常记做 "CURD": C: 创建&#xff08;Create&#xff09;U: 更新&#xff08;Update&#xff09;R: 读取&#xff08;Retrieve&#xff09;D: 删除&#xff08;Delete&#xff09; 1. INSERT 插入 完整的 insert 语句为: INSERT INT…...

    2024/4/26 10:10:03
  7. java中的进制转换

    最近在做项目重构&#xff0c;C语言的项目重构成java项目&#xff0c;并且使用gradle构建成微服务项目放到云平台上运行。看到c代码中的进制转换&#xff0c;突然发现java中的进制转换都是已经封装好了。 Integer num 10;String binary Integer.toBinaryString(num);//10进制转…...

    2024/3/23 10:56:42
  8. JavaWeb——jQuery的常用操作(1)

    jQueryjQuery简介jQuery的核心函数 $()$()函数的常用参数jQuery对象jQuery 选择器基本选择器层级选择器过滤选择器基本过滤器内容过滤器属性过滤器表单过滤器表单对象过滤器jQuery元素筛选jQuery简介 含义&#xff1a;jQuery&#xff0c;是简化了的JavaScript&#xff0c;也就…...

    2024/5/5 14:14:18
  9. 家庭网关斐讯 K3 基础环境篇

    导读 斐讯 k3 说实话挺看重它时尚优雅的外观&#xff0c;对于普通家用来说&#xff0c;它的配置足够支撑&#xff0c;比如易雾君用它搭建家庭内网远程接入服务、nfs服务、自动加密备份百度网盘等&#xff0c;稳定运行至今。 固件选择 易雾君的 k3 为非三星内存颗粒的版本&am…...

    2024/5/2 0:17:07
  10. 洛谷---P1656 炸铁路---强连通分量---Tarjin

    题目描述 A 国派出将军uim&#xff0c;对 B 国进行战略性措施&#xff0c;以解救涂炭的生灵。 B 国有 n 个城市&#xff0c;这些城市以铁路相连。任意两个城市都可以通过铁路直接或者间接到达。 uim 发现有些铁路被毁坏之后&#xff0c;某两个城市无法互相通过铁路到达。这样…...

    2024/4/24 22:39:12
  11. cmd中javac和java使用及注意事项--暨运行第一个简单的java程序

    一、简述&#xff1a; cmd中&#xff0c;执行java命令与javac命令的区别&#xff1a; javac&#xff1a;是编译命令&#xff0c;将java源文件编译成.class字节码文件。 例如&#xff1a;javac hello.java 将生成hello.class文件。 java&#xff1a;是运行字节码文件的工具…...

    2024/5/4 0:10:32
  12. Loss

    Summary of Paper “WHAT’S IN A LOSS FUNCTION FOR IMAGE CLASSIFICATION?” Different losses and regularizers achieve broadly similar accuracies. Although the acuuracy differences are larger enough to be meaningful in some contexts, the largerst difference…...

    2024/4/26 14:05:53
  13. 【Xcode】报错:iPhone is not available. Please reconnect the device

    macOS Version 10.15.7 (Build 19H2)Xcode 11.7 (16142) 其实这个问题&#xff0c;每年升级 Xcode 都会遇到&#xff0c; 每次都要 baidu google stackOverflow 一下&#xff0c; 这次就记录一下解决思路吧。问题&#xff1a; Xcode 最近几个月运行的设备是 iOS 13.7&#xf…...

    2024/3/19 23:09:03
  14. java设计模式之桥接模式

    桥接(Bridge)是用于把抽象化与现实化解耦&#xff0c;使得二者可以独立变化。这种类型的设计模式属于结构型模式&#xff0c;它通过提供抽象化和实现化之间的桥接结构&#xff0c;来实现二者的解耦。 这种模式涉及到一个作为桥接的接口&#xff0c;使得实体类的功能独立于接口…...

    2024/4/25 15:15:28
  15. mac下国内安装Homebrew教程

    mac下国内安装Homebrew教程 Homebrew是一款包管理工具&#xff0c;目前支持macOS和linux系统。主要有四个部分组成: brew、homebrew-core 、homebrew-cask、homebrew-bottles。 名称说明brewHomebrew 源代码仓库homebrew-coreHomebrew 核心源homebrew-cask提供 macOS 应用和大…...

    2024/4/25 15:30:27
  16. Edgar后端学习--复现一个热乎的json、ajax、重定向相关的bug以及引起的一些思考

    躺了很长时间&#xff0c;起来也不想学习。好久也咩更新了&#xff0c;开了音响&#xff0c;开干&#xff01; 接上回jsp小项目&#xff0c;现在进程多是业务上的逻辑的设计。前端就是DOM元素的增加、修改&#xff0c;用一用模态框之类的。纯原生代码&#xff0c;注解还是热乎…...

    2024/3/19 23:08:59
  17. 2020年下半年最新:字节跳动“算法面试题汇总”让你面试没压力

    注&#xff1a;本场面试在疫情期间三月份拿到的字节offer 基本条件 本人是底层 211本科,无科研经历,但是有一些项目经历,在国内监控行业某头部企业做过一段时间的实习。想着投一下字节,可以积累一下面试经验和为春招做准备.投了简历之后,过了一段时间,HR 就打电话跟我约时间,在…...

    2024/4/23 11:15:01
  18. 连表查询(初始)

    跨表查询 1.内连接 插入 源码参考 -- 查询数据 select * from department; select * from employee; select * from student;-- 通过内连接跨表查询老师的信息 select employee.eid,employee.name,department.name from employee inner join department on employee.depar…...

    2024/4/24 1:33:31
  19. 青岛这些景区门票优惠来了!快看有你想去的吗?

    随着青岛市文化旅游惠民季的启动&#xff0c;2020年11月至2021年2月&#xff0c;全市将以门票减免、文化旅游产品特惠等系列优惠措施&#xff0c;带市民游客感受冬游青岛的别样魅力。其中&#xff0c;在部分国有景区免费和执行淡季门票价格的基础上&#xff0c;更多景区陆续推出…...

    2024/3/23 10:56:38
  20. mysql 数据库的基本管理

    ###### 1.数据库的介绍 ###### 1.什么是数据库 数据库就是个高级的表格软件 2.常见数据库 Mysql Oracle mongodb db2 sqlite sqlserver ....... 3.Mysql (SUN -----> Oracle) 4.mariadb ###### 2.mariadb的安装 ###### dnf install mariadb-server.x86_64 -y #####…...

    2024/3/23 10:56:39

最新文章

  1. 3月空气净化器市场数据分析,热门品牌排行榜揭晓!

    三月上旬以来&#xff0c;中国空气净化器行业的规模持续扩大&#xff0c;市场规模和消费需求也在不断提升&#xff0c;消费者对高质量空气的需求增加。智能化是当前空气净化器市场的一个重要发展方向&#xff0c;这类产品集成了空气过滤、监测等功能&#xff0c;满足了现代消费…...

    2024/5/10 2:57:22
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/9 21:23:04
  3. MySql数据库从0-1学习-第三天多表设计学习

    项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种: 一对多(多对一)多对多一对一 一对多 需求:根据需求,完成部门和员工表的设计 一对多,很多人会使用外键,…...

    2024/4/30 1:33:11
  4. 谷粒商城实战(008 缓存)

    Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第151p-第p157的内容 简介 数据库承担落盘&#xff08;持久化&#xff09;工作 拿map做缓存 这种是本地缓存&#xff0c;会有一些问题 分布…...

    2024/5/10 0:17:43
  5. node.js常用指令

    1、node&#xff1a;启动 Node.js REPL&#xff08;交互式解释器&#xff09;。 node 2、node [文件名]&#xff1a;执行指定的 JavaScript 文件。 node app.js 3、npm init&#xff1a;初始化一个新的 Node.js 项目&#xff0c;生成 package.json 文件。 此命令会创建一个…...

    2024/5/9 21:46:27
  6. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/5/10 1:36:26
  7. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/5/9 7:40:42
  8. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/5/9 2:44:26
  9. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/5/10 2:07:45
  10. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/5/9 3:15:57
  11. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/5/9 5:40:03
  12. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/5/9 7:40:40
  13. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/5/10 2:07:43
  14. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/5/10 2:07:43
  15. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/5/9 4:12:16
  16. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/5/9 7:40:35
  17. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/5/9 19:47:07
  18. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/5/9 7:40:34
  19. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/5/10 2:07:41
  20. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/5/9 5:02:59
  21. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/5/9 4:31:45
  22. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/5/9 16:54:42
  23. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/5/10 1:31:37
  24. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/5/9 6:36:49
  25. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/5/9 4:33:29
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57