Spring源码分析:bean加载流程

news2024/11/24 20:45:07

7b2663d67506b10df9cc11971a332081.jpeg

背景

在Spring中,Bean的加载和管理是其核心功能之一,包括配置元数据解析、Bean定义注册、实例化、属性填充、初始化、后置处理器处理、完成创建和销毁等步骤。

源码入口

AbstractBeanFactory#doGetBean

884e11e7c022917f72c3d6bf8566f7ba.png

具体源码流程如下:

56bd983a68cf1beba09cb1d7973c1343.png

bean加载流程(#getBean方法)分析:

1、提取beanName

  • 提取FactoryBean修饰符,比如name=“&aa”,那么就要去掉&,使得name=“aa”

  • 或者,提取指定的beanName,比如别名A->B的bean则返回B

2、从缓存中加载单例

创建单例时,为避免循环依赖,不等完全bean创建,就将创建bean的ObjectFactory提早曝光;即,ObjectFactory放到缓存Map中,一旦下一个bean需要依赖上个bean,就直接使用 ObjectFactory即可。

  • singletonObjects:key=BeanName,value=构造完成的bean实例

  • earlySingletonObjects:key=BeanName,value=构造完成的bean实例,用来解决循环依赖问题

  • singletonFactories:key=BeanName,value=ObjectFactory工厂实例

三级缓存实现原理

当Spring容器创建Bean时,首先会从singletonObjects缓存中查找Bean实例,如果能找到则直接返回。

  • 如果在singletonObjects缓存中没有找到Bean实例,则从earlySingletonObjects缓存中查找,如果找到了一个未完成初始化的Bean实例,则将其返回,并在后续的处理中完成初始化。

  • 如果在earlySingletonObjects缓存中也没有找到Bean实例,则从singletonFactories缓存中查找是否存在创建Bean实例的工厂对象,如果存在,则通过工厂对象创建一个新的Bean实例,并将工厂对象从singletonFactories缓存中移除。

  • 如果在singletonFactories缓存中也没有找到工厂对象,则需要创建一个新的工厂对象,并放到singletonFactories缓存中,以便后续使用。

  • 创建Bean实例时,如果Bean存在循环依赖,那么Spring框架会先创建一个用于创建Bean实例的工厂对象,放到singletonFactories缓存中。然后通过工厂对象创建一个新的Bean实例,并将其放到earlySingletonObjects缓存中,以便后续处理。

下面是DefaultSingletonBeanRegistry源码

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);


/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);


/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);


@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   Object singletonObject = this.singletonObjects.get(beanName);
   // 【1】
   if (singletonObject == null 
       && isSingletonCurrentlyInCreation(beanName)) {
     // 【2】
      synchronized (this.singletonObjects) {
          // 【3】
         singletonObject = this.earlySingletonObjects.get(beanName);
         if (singletonObject == null && allowEarlyReference) {
             // 【4】
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                // 【5】
               singletonObject = singletonFactory.getObject();
               // 【6】
               this.earlySingletonObjects.put(beanName, singletonObject);
               // 【7】
               this.singletonFactories.remove(beanName);
            }
         }
      }
   }
   return singletonObject;


}

代码分析:

  • 【1】isSingletonCurrentlyInCreation 判断是否循环依赖

  • 【2】用synchronized 锁住singleObjects一级缓存全局变量,确保线程安全,因为有3个全局变量要修改的

  • 【3】从 earlySingletonObjects 二级缓存取 object

    • 如果object不为空,说明bean正在被创建,则跳出来

    • 如果object为空,说明bean没在被创建,需要执行下面逻辑

  • 【4】从 singletonFactories 三级缓存取 ObjectFactory(某些方法提前初始化了ObjectFactory策略到 singletonFactories 中)

    • 【5】如果object不为空,说明已经有预设好的ObjectFactory,可以实例化它:通过预设的getBean ObjectFactory@getObject() 创建bean实例

    • 【6】记录到 earlySingletonObjects 中,说明此bean正在被创建

    • 【7】earlySingletonObjects 和 singletonFactories是互斥的,因此要remove调

3、多例对象-是否创建中检测

if (isPrototypeCurrentlyInCreation(beanName)) {
   throw new BeanCurrentlyInCreationException(beanName);
}

单例才检测循环依赖,多例抛异常。

4、检测parentBeanFactory

BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
   // Not found -> check parent.
   String nameToLookup = originalBeanName(name);
   if (parentBeanFactory instanceof AbstractBeanFactory) {
      return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
            nameToLookup, requiredType, args, typeCheckOnly);
   }
   else if (args != null) {
      // Delegation to parent with explicit args.
      return (T) parentBeanFactory.getBean(nameToLookup, args);
   }
   else if (requiredType != null) {
      // No args -> delegate to standard getBean method.
      return parentBeanFactory.getBean(nameToLookup, requiredType);
   }
   else {
      return (T) parentBeanFactory.getBean(nameToLookup);
   }
}

缓存没有数据,直接转到父类工厂上去加载工厂方法

5、解析Defination类的类型转换,换成可用的BeanDefination

final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

6、寻找递归解决bean依赖,并加载和初始化这些依赖 -- 递归思想哦!

String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
   for (String dep : dependsOn) {
      if (isDependent(beanName, dep)) {
         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
               "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
      }
      registerDependentBean(dep, beanName);
      try {
         getBean(dep);
      }
      catch (NoSuchBeanDefinitionException ex) {
         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
               "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
      }
   }
}

7、正式根据scope去创建bean

bean创建的特别之处

Spring 解决循环依赖依靠的是 Bean 的 " 中间态 " 这个概念,而这个中间态指的是已经实例化但还没初始化的状态 —>半成品。

实例化的过程又是通过构造器创建的,如果 A 还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。

由于 spring 中的 bean 的创建过程为先实例化 再初始化 ( 在进行对象实例化的过程中不必赋值 ) 将实例化好的对象暴露出去, 供其他对象调用 , 然而使用构造器注入 , 必须要使用构造器完成对象的初始化的操作 ,就会陷入死循环的状态。

单例scope:实例化bean

if (mbd.isSingleton()) {
   sharedInstance = getSingleton(beanName, () -> {
      try {
         return createBean(beanName, mbd, args);
      }
      catch (BeansException ex) {
         // Explicitly remove instance from singleton cache: It might have been put there
         // eagerly by the creation process, to allow for circular reference resolution.
         // Also remove any beans that received a temporary reference to the bean.
         destroySingleton(beanName);
         throw ex;
      }
   });
   bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

代码分析:

  • 实例化bean:createBean -》

    • doCreateBean -》,参考下面的源码分析【1】

      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

    • createBeanInstance,参考下面的源码分析【2】

  • getSingleton:beanName是一个参数,另一个匿名函数则是ObjectFactory<?> singletonFactory,即构造bean的工厂方法

    • singletonObjects:添加经过了实例化+初始化的bean实例

    • singletonFactories:移除

    • earlySingletonObjects:移除

    • addSingleton(beanName, singletonObject):三级缓存的清理赋值

【1】源码addSingletonFactory:创建bean实例

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
      isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
   addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}


protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
   Assert.notNull(singletonFactory, "Singleton factory must not be null");
   synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
         this.singletonFactories.put(beanName, singletonFactory);
         this.earlySingletonObjects.remove(beanName);
         this.registeredSingletons.add(beanName);
      }
   }
}

代码分析:该方法用于添加单例工厂,主要功能包括:

  • 检查传入的单例工厂是否为null,若为null则抛出异常。

  • 同步锁住singletonObjects确保线程安全。

  • 若singletonObjects中没有beanName对应的对象,则将singletonFactory放入singletonFactories映射中,并移除earlySingletonObjects中的对应项,最后将beanName加入registeredSingletons集合。

这也侧面说明了,bean还没有完全初始化,就把工厂方法提前暴露到了singletonFactories

【2】源码createBeanInstance:bean实例化
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
   // Make sure bean class is actually resolved at this point.
   Class<?> beanClass = resolveBeanClass(mbd, beanName);


   if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
      throw new BeanCreationException(mbd.getResourceDescription(), beanName,
            "Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
   }


   Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
   if (instanceSupplier != null) {
      return obtainFromSupplier(instanceSupplier, beanName);
   }


   if (mbd.getFactoryMethodName() != null) {
      return instantiateUsingFactoryMethod(beanName, mbd, args);
   }


   // Shortcut when re-creating the same bean...
   boolean resolved = false;
   boolean autowireNecessary = false;
   if (args == null) {
      synchronized (mbd.constructorArgumentLock) {
         if (mbd.resolvedConstructorOrFactoryMethod != null) {
            resolved = true;
            autowireNecessary = mbd.constructorArgumentsResolved;
         }
      }
   }
   if (resolved) {
      if (autowireNecessary) {
         return autowireConstructor(beanName, mbd, null, null);
      }
      else {
         return instantiateBean(beanName, mbd);
      }
   }


   // Candidate constructors for autowiring?
   Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
   if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
         mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
      return autowireConstructor(beanName, mbd, ctors, args);
   }


   // Preferred constructors for default construction?
   ctors = mbd.getPreferredConstructors();
   if (ctors != null) {
      return autowireConstructor(beanName, mbd, ctors, null);
   }


   // No special handling: simply use no-arg constructor.
   return instantiateBean(beanName, mbd);
}

代码分析:

createBeanInstance 用于创建 Bean 的实例,负责根据不同的条件和配置来实例化 Bean,并处理相关的异常情况:

  1. 解析并验证 Bean 类。

  2. 如果允许,尝试从 Supplier 获取实例。

  3. 若配置了工厂方法,则通过工厂方法实例化。

  4. 否则,根据参数情况选择构造器自动装配或直接实例化。

初始化bean:填充属性

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean#populateBean

@SuppressWarnings("deprecation")  // for postProcessPropertyValues
protected void populateBean(
    String beanName, RootBeanDefinition mbd, 
    @Nullable BeanWrapper bw) {
    //【1】
   if (bw == null) {
      if (mbd.hasPropertyValues()) {
         throw new BeanCreationException(
               mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
      }
      else {
         return;
      }
   }


    //【2】
   if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
         if (bp instanceof InstantiationAwareBeanPostProcessor) {
            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
            if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
               return;
            }
         }
      }
   }


   PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);


    //【3】
   int resolvedAutowireMode = mbd.getResolvedAutowireMode();
   if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
      MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
      // Add property values based on autowire by name if applicable.
      if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
         autowireByName(beanName, mbd, bw, newPvs);
      }
      // Add property values based on autowire by type if applicable.
      if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
         autowireByType(beanName, mbd, bw, newPvs);
      }
      pvs = newPvs;
   }


   boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
   boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);


   PropertyDescriptor[] filteredPds = null;
   if (hasInstAwareBpps) {
      if (pvs == null) {
         pvs = mbd.getPropertyValues();
      }
      //【4】
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
         if (bp instanceof InstantiationAwareBeanPostProcessor) {
            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
            PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
            if (pvsToUse == null) {
               if (filteredPds == null) {
                  filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
               }
               pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
               if (pvsToUse == null) {
                  return;
               }
            }
            pvs = pvsToUse;
         }
      }
   }


   if (needsDepCheck) {
      if (filteredPds == null) {
         filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
      }
      //【5】
      checkDependencies(beanName, mbd, filteredPds, pvs);
   }


   if (pvs != null) {
       //【6】
      applyPropertyValues(beanName, mbd, bw, pvs);
   }
}

代码分析:

  1. 【1】检查BeanWrapper是否为空,若为空且存在属性值,则抛出异常;否则跳过属性填充。

  2. 【2】若存在实例化需要的后置处理器,则调用它们对bean进行处理。

  3. 【3】根据自动装配模式(按名或按类型)添加属性值。

  4. 【4】调用实例化后置处理器处理属性值。

  5. 【5】检查依赖关系并进行依赖检查。

  6. 【6】应用最终的属性值到bean上

多例scope:实例化bean

else if (mbd.isPrototype()) {
   // It's a prototype -> create a new instance.
   Object prototypeInstance = null;
   try {
      beforePrototypeCreation(beanName);
      prototypeInstance = createBean(beanName, mbd, args);
   }
   finally {
      afterPrototypeCreation(beanName);
   }
   bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

代码分析:

  • createBean:流程参考单例scope的创建,是类似的。

8、通用:初始化bean & 注入依赖

之前已经实例化过bean了,现在则是完成注入属性;属性注入的顺序是,优先通过autowireByName,然后才是autowireByType。

int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
   MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
   // Add property values based on autowire by name if applicable.
   if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
      autowireByName(beanName, mbd, bw, newPvs);
   }
   // Add property values based on autowire by type if applicable.
   if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
      autowireByType(beanName, mbd, bw, newPvs);
   }
   pvs = newPvs;
}

9、再执行当前bean的initializeBean初始化

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
   if (System.getSecurityManager() != null) {
      AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
         invokeAwareMethods(beanName, bean);
         return null;
      }, getAccessControlContext());
   }
   else {
      invokeAwareMethods(beanName, bean);
   }


   Object wrappedBean = bean;
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
   }


   try {
      invokeInitMethods(beanName, wrappedBean, mbd);
   }
   catch (Throwable ex) {
      throw new BeanCreationException(
            (mbd != null ? mbd.getResourceDescription() : null),
            beanName, "Invocation of init method failed", ex);
   }
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
   }


   return wrappedBean;
}

该Java函数initializeBean用于初始化一个Bean:

  1. 初始化前

    1. 根据系统安全设置,以特权操作方式或直接调用invokeAwareMethods方法,即Aware接口方法调用

    2. 执行BeanPostProcessor接口的postProcessBeforeInitialization方法,即Bean应用前置处理器

  2. 初始化时

  • 调用InitializingBean接口的afterPropertiesSet方法,完成初始化

  1. BeanPostProcessor接口的applyBeanPostProcessorsBeforeInitialization

  2. 调用初始化方法,对bean 进行初始化,若失败则抛出异常。

  3. 执行BeanPostProcessor接口的postProcessAfterInitialization方法,即对Bean应用后置处理器。

初始化后

  1. DisposableBean接口的destroy方法执行(有必要的话)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2207974.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

怎么利用商品详情API接口实现数据获取与应用?

在当今数字化的商业时代&#xff0c;高效获取和利用商品数据对于企业和开发者来说至关重要。商品详情 API 接口为我们提供了一种便捷的方式来获取丰富的商品信息&#xff0c;从而实现各种有价值的应用。本文将深入探讨如何利用商品详情 API 接口实现数据获取与应用。 一、商品…

信号转导的风暴中心:ERK1/2

前 言 ERK1/2是RAF-MEK-ERK信号通路的关键组成部分&#xff0c;在Thr202、Tyr204位点被磷酸化从而激活&#xff0c;进而激活多种与细胞增殖、分化、迁移和血管生成相关的底物&#xff08;超过160种&#xff09;。因此ERK1/2的(Thr202, Tyr204)/(Thr185, Tyr187)磷酸化是ERK激…

从SQL Server过渡到PostgreSQL:理解模式的差异

前言 随着越来越多的企业转向开源技术&#xff0c;商业数据库管理员和开发者也逐渐面临向PostgreSQL迁移的需求。 虽然SQL Server和PostgreSQL共享许多数据库管理系统&#xff08;RDBMS&#xff09;的基本概念&#xff0c;但它们在处理某些结构上的差异可能会让人感到困惑&…

利用Spring Boot实现医疗病历的B2B平台集成

第5章 系统实现 5.1 管理员角色 5.1.1 医院管理 管理员可以在医院管理界面对医院信息进行添加&#xff0c;修改&#xff0c;删除&#xff0c;查询操作。医院管理页面的运行结果如图5-1所示&#xff1a; 图5-1医院管理界面 5.1.2 医院注册 管理员可以在医院注册界面对医院信息…

【LeetCode】动态规划—1312. 让字符串成为回文串的最少插入次数(附完整Python/C++代码)

动态规划—1312. 让字符串成为回文串的最少插入次数 题目描述前言基本思路1. 问题定义目标&#xff1a;举例&#xff1a; 2. 理解问题和递推关系动态规划思路&#xff1a; 3. 解决方法动态规划方法伪代码&#xff1a; 4. 进一步优化5. 小总结 Python代码Python代码解释&#xf…

基于Spring Boot的医疗病历B2B平台开发策略

第4章 系统设计 4.1 系统总体设计 系统不仅要求功能完善&#xff0c;而且还要界面友好&#xff0c;因此&#xff0c;对于一个成功的系统设计&#xff0c;功能模块的设计是关键。由于本系统可执行的是一般性质的学习信息管理工作&#xff0c;本系统具有一般适用性&#xff0c;其…

Java项目:148 基于springboot的校友管理系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 校友管理系统项目说明 ​ 本系统是一个学校与毕业生的交流平台。方便校友们了解母校的最新动态&#xff0c;同学的情况&#xff1b;同时学校也可以通过平台了解…

<<迷雾>> 第11章 全自动加法计算机(2)--5 比特存储器 示例电路

可以读/写单个5位二进制数的存储器. info::操作说明 将多个比特单元组合的结果, 整体操作流程类似, 只是可同时读取多位 注: D0~D4 处没有引入写入测试开关, 读者可仿照前面自行引入 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhttps://book.xiaogd.n…

【React】React18核心源码解读

前言 本文使用 React18.2.0 的源码&#xff0c;如果想回退到某一版本执行git checkout tags/v18.2.0即可。如果打开源码发现js文件报ts类型错误请看本人另一篇文章&#xff1a;VsCode查看React源码全是类型报错如何解决。 阅读源码的过程&#xff1a; 下载源码 观察 package…

MySQL 【日期】函数大全(二)

DATE_ADDDATE_FORMATDATE_SUBDATEDIFFDAYDAYNAMEDAYOFMONTHDAYOFWEEK 1、DATE_ADD DATE_ADD(date, value) &#xff1a;在指定的日期/时间上加上指定的时间间隔加并返回新的日期/时间。 DATE_ADD(date, value) DATE_ADD(date, INTERVAL value unit) date&#xff1a;需要操作…

Qt-系统处理键盘按键相关事件(58)

目录 描述 使用 单个按键 组合键 描述 Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时&#xff0c;键盘事件便会触发 Qt 中对按键事件进行了封装&#xff0c;QShortCut 就是封装出来的&#xff0c;这里我们介绍底层的函数 使用 单个按键…

如何将csdn文章导出为pdf

前言 在csdn上浏览文章的时候我发现有的文章支持pdf导出&#xff0c;但是有的文章不支持pdf导出&#xff0c;为了解决能将csdn上所有文章都能以pdf格式导出遂作此文。 正文 先上代码&#xff1a; (function(){use strict;var contentBox $("div.article_content")…

智能 AI 应用为什么需要知识库

智能 AI 应用为什么需要知识库 上回我们讲到如何在 Dify 上搭建企业知识库&#xff0c;并引入大语言模型应用中&#xff0c;实现企业内部知识助手。 使用 Dify 快速搭建企业内部知识助手。 其中提到的企业知识库&#xff0c;正是 “检索增强生成(Retrieval-Augmented Generat…

开放式耳机哪个品牌音质好?开放式耳机排行榜10强推荐!

耳机在我们的日常生活中扮演着重要角色&#xff0c;无论是上班路上还是运动时&#xff0c;它们都能帮助我们放松并增强安全感。选择一款合适的耳机很关键&#xff0c;开放式耳机因其设计&#xff0c;在不同场合都适用&#xff0c;特别是在运动时&#xff0c;它们提供了稳固而舒…

【笔记】Day2.2.1表设计说明

主键ID一般使用bigint类型 运送类型 使用比int更小的tinyint类型 eg&#xff1a;普快代表1 特快代表2&#xff08;没写反&#xff09; 关联城市 varchar 2代表京津冀 3代表江浙沪 4代表川渝 首重和续重都有小数点 故使用double 轻抛系数都为整数 故使用int 创建时间和修改…

基于SpringBoot+Vue的体育馆场地预约系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

【测试】自动化——概念篇

自动化测试 自动化概念 什么是自动化&#xff1f; 自动化操作在生活中处处可见&#xff0c;自动化洒水机、自动洗手液、超市自动闸门。 自动化测试——>自动的测试软件&#xff08;减少人力和时间的消耗&#xff0c;提高软件的测试质量&#xff09;。 人力&#xff1a;2…

自定义实现项目进度图

目录 0.背景 1. 演示效果 2.实现原理 2.1 处理表头数据&#xff08;日期对应的是星期几&#xff09; 2.2 获取项目数据 2.3 合并单元格 3.源码 0.背景 目遇到一个展示项目进度的需求&#xff0c;类似甘特图的效果&#xff0c;不同的是&#xff0c;每一个项目都有计划和实…

MySQL:基于Spring监听Binlog日志

binlog的三种模式 MySQL 的二进制日志&#xff08;binlog&#xff09;有三种不同的格式&#xff0c;通常被称为 binlog 模式。这三种模式分别是 Statement 模式、Row 模式和Mixed 模式。 Statement 模式&#xff1a; 在 Statement 模式下&#xff0c;MySQL 记录每个会更改数…

代码随想录训练营Day30 | 491.递增子序列 | 46.全排列 | 47.全排列 II

学习文档&#xff1a;代码随想录 (programmercarl.com) 学习视频&#xff1a;代码随想录算法公开课 | 最强算法公开课 | 代码随想录 (programmercarl.com) Leetcode 491. 非递减子序列 题目描述 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列…