【微服务】spring循环依赖深度解析

news2025/1/24 11:45:44

目录

一、循环依赖概述

1.2 spring中的循环依赖

二、循环依赖问题模拟

2.1 循环依赖代码演示

2.2 问题分析与解决

2.2.1 使用反射+中间容器

三、spring循环依赖问题解析

3.1 spring中的依赖注入

3.1.1 field属性注入

3.1.2 setter方法注入

3.1.3 构造器注入

3.2 spring中不同的循环依赖解决方案

3.2.1 spring中bean的生命周期

3.2.2 循环依赖处理时机

四、spring 三级缓存解决方案

4.1 前置准备

4.2 三级缓存源码分析过程

4.2.1 代码调试技巧

4.3 为什么使用三级缓存?

4.4 spring循环依赖解决方案小结

五、写在文末


一、循环依赖概述

循环依赖,叫做循环引用,指一个或者多个bean对象之间互相引用,最后形成一种类似环形的依赖关系,循环依赖的大致情形如下几种:

从上图不难发现,循环依赖其实就是一个逻辑上的闭环,像中间的那张图,bean-A中注入bean-B,创建bean-A的时候就会去容器中查找bean-B,发现没有,就会去创建bean-B,而创建bean-B的时候,发现又注入了bean-A,于是又去容器检查bean-A......,接下来就会不断的循环重复上面的过程。有心的同学似乎发现,这个是不是有点像死锁了呢?其实循环依赖就是一个死循环的过程。

1.2 spring中的循环依赖

在使用spring进行编码时,可能你会见到下面这样的写法,这里有两个被spring容器管理的类UserService和RoleService,由于加了@Service注解,都可以注册到bean容器中;

其中UserService中注入了RoleService

@Service
public class UserService {

    @Autowired
    private RoleService roleService;

}

RoleService中注入了UserService

@Service
public class UserService {

    @Autowired
    private RoleService roleService;

}

这样的写法就形成了相互引用的循环依赖,这种互相注入引用的方式是我们平时开发中使用最多的,原因是这种方式使用起来非常简单,代码看起来也很简洁。

二、循环依赖问题模拟

2.1 循环依赖代码演示

在下面这段代码中,有两个类,在第一个类CircularTest1实例化对象时,需要创建CircularTest2的类对象,同时CircularTest2类实例化时,也需要CircularTest1类的创建,这样就形成了循环依赖。

public class CircularTestError {

    public static void main(String[] args) {
        new CircularTest1();
    }
}
class CircularTest1{
    private CircularTest2 circularTest2 = new CircularTest2();
}

class CircularTest2{
    private CircularTest1 circularTest1 = new CircularTest1();
}

运行这段代码,最终会看到栈溢出的错误

上面的案例就是最基本的循环依赖场景,你需要我,我需要你,然后就报错了。而且上面的这种设计情况我们是没有办法解决的。那么针对这种场景我们应该要怎么设计呢?这个是关键!

2.2 问题分析与解决

深入分析这个问题时,首先要明确的一点就是如果这个对象A还没创建成功,在创建的过程中要依赖另一个对象B,而另一个对象B也是在创建中要依赖对象A,这种肯定是无解的。

此时我们需要转换思路,先把A创建出来,但是还没有完成初始化操作,也就是这是一个半成品的对象,然后在赋值的时候先把A暴露出来,然后创建B,让B创建完成后找到暴露的A完成整体的实例化,这时再把B交给A完成A的后续操作,从而揭开了循环依赖的密码。整个过程如下图所示:

2.2.1 使用反射+中间容器

明白了上面两个对象之间循环依赖的原理,可以尝试引入一个中间容器,比如这里可以选择map来解决这个问题,完整的代码如下:

import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CircularTestHalf {

    // 保存提前暴露的对象,也就是半成品的对象
    private final static Map<String,Object> singletonObjects = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception{
        System.out.println(getBean(CircularTest1.class).getCircularTest2());
        System.out.println(getBean(CircularTest2.class).getCircularTest1());
    }

    private static <T> T getBean(Class<T> beanClass) throws Exception{
        //1.获取类对象对应的名称
        String beanName = beanClass.getSimpleName().toLowerCase();
        // 2.根据名称去 singletonObjects 中查看是否有半成品的对象
        if(singletonObjects.containsKey(beanName)){
            return (T) singletonObjects.get(beanName);
        }
        // 3. singletonObjects 没有半成品的对象,那么就反射实例化对象
        Object obj = beanClass.newInstance();
        // 还没有完整的创建完这个对象就把这个对象存储在了 singletonObjects中
        singletonObjects.put(beanName,obj);
        // 属性填充来补全对象
        Field[] declaredFields = obj.getClass().getDeclaredFields();
        // 遍历处理
        for (Field field : declaredFields) {
            field.setAccessible(true); // 针对private修饰
            // 获取成员变量 对应的类对象
            Class<?> fieldClass = field.getType();
            // 获取对应的 beanName
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            // 给成员变量赋值 如果 singletonObjects 中有半成品就获取,否则创建对象
            field.set(obj,singletonObjects.containsKey(fieldBeanName)?
                    singletonObjects.get(fieldBeanName):getBean(fieldClass));
        }
        return (T) obj;
    }
}

在这段代码中,我们引入了一个中间容器map,map在这里的作用就是,通过反射创建A对象的实例,但是还没有初始化(填充字段等属性)之前,作为一个中间态的容器存放A、B两个未实例化的对象,通俗理解就是所谓的“半成品”对象容器,以对象A来说,在反射创建对象之后进行属性值填充的时候发现B对象没有,这时就要去做对象B的创建,此时对象B也重复A的过程,但不同的是,由于上一步对象A初始创建后放到了MAP中,此时A就可以拿到并填充到自己的属性里面了,这样就不会陷入无限的死循环里面了。

运行上面的程序,可以发现通过这种方式就解决了这个问题

总结这种方案的解决流程如下图所示

三、spring循环依赖问题解析

在谈到spring中循环依赖的时候,很多同学第一反应就是只要A中注入B,B中注入A,肯定就会出现循环依赖问题。其实这种理解是片面的。需要首先弄清楚spring中的几种依赖注入的方式。

3.1 spring中的依赖注入

依赖注入是Spring IOC(控制反转)模块的一个核心概念,DI (Dependency Injection):依赖注入是指在 Spring IOC 容器创建对象的过程中,将所依赖的对象通过配置进行注入,我们可以通过依赖注入的方式来降低对象间的耦合度。

这里的配置注入可以基于XML配置文件也可以基于注解配置,当下注解配置开发是主流,所以在这里主要讨论基于注解的注入方式,基于注解的常规注入方式通常有三种:

  • 基于field属性注入

  • 基于setter方法注入

  • 基于构造器注入

下面分别来看下这几种常用的注入方式。

3.1.1 field属性注入

这种方式是平时开发中使用最多的,原因是这种方式使用起来非常简单,代码也比较简洁。

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
  
}

3.1.2 setter方法注入

这种方式使用的不是很多

@Service
public class UserService {
    private UserDAO userDAO;
    
    @Autowired
    public void setUserDAO(serDAO userDAO) {
        this.userDAO = userDAO;
    }
}

3.1.3 构造器注入

@Service
public class UserService {
  private UserDAO userDAO;
    
    @Autowired //构造器注入
    public UserService(UserDAO userDAO) {
        this.userDAO = userDAO;
    }
}

补充说明:

事实上,项目中如果存在大量的Bean的循环依赖,其实是Bean对象职责划分不明确、代码质量不高的表现,SpringBoot在后续的版本(大概是是2.6.X的某个版本)中默认把循环依赖给禁用了!从2.6版本开始,如果你的项目里还存在循环依赖,SpringBoot将拒绝启动!

如果你的版本高于2.6,仍然需要保留允许循环依赖,需要手动在配置文件中开启

spring:
  main:
    allow-circular-references: true

3.2 spring中不同的循环依赖解决方案

基于上面的探讨,在不同的依赖注入模式下, spring中也对应着不同的循环依赖解决方案,如下图

以设值注入,即属性注入的方式为例进行说明,看看spring中是如何解决循环依赖问题的 。

3.2.1 spring中bean的生命周期

在搞清楚spring是如何解决循环依赖的问题之前,有必要搞清楚spring中bean的完整生命周期,因为只有搞清生命周期中各个阶段要做的事情,才知道spring为何要这样解决,spring生命周期如下图所示

基于上面的案例,结合对spring的理解,对象的生命周期的核心就两个

1、创建对象;

2、对象属性填

3.2.2 循环依赖处理时机

基于前面案例的了解,我们知道肯定需要在调用构造方法方法创建完成后再暴露对象,在Spring中提供了三级缓存来处理这个事情,对应的处理节点如下图:

对应到源码中具体处理循环依赖的流程如下:

上面就是在Spring的生命周期方法中和循环依赖出现相关的流程了。那么源码中的具体处理是怎么样的呢?接下来从源码来详细看下。

四、spring 三级缓存解决方案

通过上面的分析我们基本了解了一个情况就是,如果出现了循环依赖,在spring的bean的生命周期方法中,由两个重要的节点,第一是提前暴露对象A,第二需提供一个中间存储的容器存放提前暴露的对象A,spring的设计中,即采用了一种叫做三级缓存的思想完美解决了这个问题。

4.1 前置准备

为了方便下文我们对源码的分析有一个更深入的理解,建议从源码debug进去分析,可以一步步追根溯源,搞清三级缓存的底层原理。

在spring工程中创建两个业务类,UserService和RoleService,两个类分别互相引用,代码如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private RoleService roleService;

}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class RoleService {

    @Autowired
    private UserService userService;

}

4.2 三级缓存源码分析过程

在spring源码中,找到DefaultSingletonBeanRegistry这个类,在该类中使用了3个map作为三级缓存,代码如下:

public class DefaultSingletonBeanRegistry ... {
  //1、最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存"
  Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
  //2、早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存"
  Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
  //3、单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存"
  Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}

我们知道,在项目启动时会进行Bean加载注入到Spring容器中,当创建某个Bean时发现引用依赖于另一个Bean,就会进行依赖查找,就会来到顶层接口BeanFactory的#getBean()方法,所以接下来看看AbstractBeanFactory的#doGetBean()的实现逻辑,发现首先会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回

Object sharedInstance = getSingleton(beanName);

这个#getSingleton()是在DefaultSingletonBeanRegistry实现的,详细代码如下:

@Nullable
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    // 从单例缓存中加载bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 单例缓存中没有获取到bean,同时bean在创建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      // 从 earlySingletonObjects 获取
      singletonObject = this.earlySingletonObjects.get(beanName);
      // 还是没有加载到bean,并且允许提前创建
      if (singletonObject == null && allowEarlyReference) {
        // 对单例缓存map加锁
        synchronized (this.singletonObjects) {
          // Consistent creation of early reference within full singleton lock
          // 再次从单例缓存中加载bean
          singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null) {
            // 再次从 earlySingletonObjects 获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null) {
              // 从 singletonFactories 中获取对应的 ObjectFactory
              ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
              if (singletonFactory != null) {
                // 获得 bean
                singletonObject = singletonFactory.getObject();
                // 添加 bean 到 earlySingletonObjects 中
                this.earlySingletonObjects.put(beanName, singletonObject);
                // 从 singletonFactories 中移除对应的 ObjectFactory
                this.singletonFactories.remove(beanName);
              }
            }
          }
        }
      }
    }
    return singletonObject;
  }

这个方法就是从三级缓存中获取Bean对象,不难发现这里先从一级缓存singletonObjects中查找,如果没找到,接着从二级缓存earlySingletonObjects,还是没找到的话最终会去三级缓存singletonFactories中查找,需要注意的是如果在三级缓存中找到,就会从三级缓存升级到二级缓存了。所以,二级缓存存在的意义,就是缓存三级缓存中的 ObjectFactory 的 #getObject() 方法的执行结果,提早曝光的单例 Bean 对象。

getSingleton()返回空就会接着执行AbstractBeanFactory的#doGetBean()的下面逻辑

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

可以看到,如果配置的是原型模式,bean循环依赖直接报错,对于单例模式下的bean,循环依赖Spring通过三级缓存提前曝光bean来解决,因为单例bean在整个容器中就一个,但原型模式每次都会创建一个新的bean,无法使用缓存解决,所以直接报错了。

经过一系列代码之后还是没有找到当前需要的bean,就会执行如下创建bean的逻辑:

// 上面的缓存中没找到,需要根据不同的模式创建
// bean实例化
// Create bean instance.
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.
      // 显式从单例缓存中删除 Bean 实例
      // 因为单例模式下为了解决循环依赖,可能他已经存在了,所以销毁它
      destroySingleton(beanName);
      throw ex;
    }
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

最终来到了AbstractAutowireCapableBeanFactory的#createBean(),真正执行逻辑实现的是#doCreateBean()方法:

// Eagerly cache singletons to be able to resolve circular references
    // even when triggered by lifecycle interfaces like BeanFactoryAware.
    // <4> 解决单例模式的循环依赖
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
      if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
            "' to allow for resolving potential circular references");
      }
      // 提前将创建的 bean 实例加入到 singletonFactories 中
      // 这里是为了后期避免循环依赖
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

这里将创建的bean工厂对象加入到 singletonFactories 三级缓存中,用来生成半成品的bean并放入到二级缓存,提前曝光bean,意味着别的bean引用它的时候,依赖查找就可以在前面的#getSingleton()中拿到当前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);
    }
  }
}

通过代码可以看到,singletonFactories 这个三级缓存是解决 Spring Bean 循环依赖的重要所在。这段代码发生在 #createBeanInstance(...) 方法之后,也就是说这个 bean 其实已经被创建出来了,但它还不完美(没有进行属性填充和初始化),但对其他依赖它的对象而言已经可以使用了(可以根据对象引用定位到堆中对象)。所以 Spring 在这个时候将该对象提前曝光出来,可以被其他对象所使用。

当然也需要注意到addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))的() -> getEarlyBeanReference(beanName, mbd, bean)匿名函数调用,使用lambda方式生成一个ObjectFactory对象放到三级缓存中,提前曝光的是ObjectFactory对象,在被注入时才在ObjectFactory.getObject方式内实时生成代理对象,也就是调用#getEarlyBeanReference()进行实现的。

// 这里如果当前Bean需要aop代理增强,就是这里生成代理Bean对象的
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
  Object exposedObject = bean;
  if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
      if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
        SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
        exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
  }
  return exposedObject;
}

这就是为什么 Spring 需要额外增加 singletonFactories 三级缓存的原因,目的是为了解决 Spring 循环依赖情况下Bean存在动态代理的情况,否则循环注入到别人的 Bean 就是原始的,而不是经过动态代理的

4.2.1 代码调试技巧

在上文中,我们使用了两个互相引用依赖的类,可以直接在上面的DefaultSingletonBeanRegistry类中,找到addSingleton这个方法,然后再在 synchronized 代码块中打上断点(注意设置断点的条件),重启项目时,即可进入到singletonObjects的put方法中。

当断点进入到这个地方时,从断点的执行链路可以知道互相依赖的两个bean的完整创建过程

4.3 为什么使用三级缓存?

这里有个值得思考的问题,为什么要包装一层ObjectFactory对象存入三级缓存,上面说是为了解决Bean对象存在aop代理情况。如果直接生成代理对象半成品Bean放入二级缓存,这样就可以不用三级缓存了,这么说使用三级缓存的意义在哪里呢?

首先需要明确一点:正常情况下(没有循环依赖),Spring都是在创建好成品Bean之后才创建对应的代理对象。为了处理循环依赖,Spring有两种选择:

1)不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入;

2)不提前创建好代理对象,在出现循环依赖被其他对象注入时,才实时生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建;

显然spring使用了三级缓存,选择第二种方案,为什么呢?原因如下:

如果要使用二级缓存解决循环依赖,意味着Bean在构造完后就创建代理对象,这样违背了Spring设计原则。Spring结合AOP跟Bean的生命周期,是在Bean创建完全之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

经过实例化,初始化、属性赋值等操作之后,bean对象已经是一个完整的实例了,最终会调用DefaultSingletonBeanRegistry的#addSingleton()将完整bean放入一级缓存singletonObjects。

protected void addSingleton(String beanName, Object singletonObject) {
  synchronized (this.singletonObjects) {
    this.singletonObjects.put(beanName, singletonObject);
    this.singletonFactories.remove(beanName);
    this.earlySingletonObjects.remove(beanName);
    this.registeredSingletons.add(beanName);
  }
}

到这里一个完整的Bean已经存入Spring容器中,就可以被其他对象注入使用了。

4.4 spring循环依赖解决方案小结

以上全部就是Spring对单例bean循环依赖的解决方案,核心就是使用三级缓存提前暴露bean对象,对于两个互相引用循环依赖的bean,Spring解决过程如下:

  • 通过构建函数创建A对象(A对象是半成品,还没注入属性和调用init方法);

  • A对象需要注入B对象,发现缓存里还没有B对象,将半成品对象A放入半成品缓存;

  • 通过构建函数创建B对象(B对象是半成品,还没注入属性和调用init方法);

  • B对象需要注入A对象,从半成品缓存里取到半成品对象A;

  • B对象继续注入其他属性和初始化,之后将完成品B对象放入完成品缓存;

  • A对象继续注入属性,从完成品缓存中取到完成品B对象并注入;

  • A对象继续注入其他属性和初始化,之后将完成品A对象放入完成品缓存;

五、写在文末

本文详细总结了spring循环依赖问题的解决方案,并通过源码分析详细探讨了spring如何使用三级缓存解决在单例bean模式下的解决过程,循环依赖的解决思想具有很好的借鉴意义,在日常工作中也很有指导作用,有兴趣的同学可以借此深入研究,本篇到此结束,感谢观看!

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

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

相关文章

Python中字符串列表的相互转换详解

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python编程中&#xff0c;经常会遇到需要将字符串列表相互转换的情况。这涉及到将逗号分隔的字符串转换为列表&#xff0c;或者将列表中的元素连接成一个字符串。本文将深入讨论这些情景&#xff0c;并提供丰富…

JAVA网络编程——BIO、NIO、AIO深度解析

I/O 一直是很多Java同学难以理解的一个知识点&#xff0c;这篇帖子将会从底层原理上带你理解I/O&#xff0c;让你看清I/O相关问题的本质。 1、I/O的概念 I/O 的全称是Input/Output。虽常谈及I/O&#xff0c;但想必你也一时不能给出一个完整的定义。搜索了谷哥欠&#xff0c;发…

吉利护航,宣称比友商“更懂车”,魅族造车的底气与底色

继小米、华为后&#xff0c;又一家手机厂商宣布跨界造车。 在近日举办的2023魅族秋季无界生态发布会上&#xff0c;星纪魅族集团&#xff08;下称“魅族”&#xff09;董事长兼CEO沈子瑜宣布&#xff0c;魅族正式进入汽车市场&#xff0c;将在2024年第一季度启动“DreamCar共创…

CoreDNS实战(十一)-分流与重定向

本文主要介绍了目前CoreDNS服务在外部域名递归结果过程中出现的一些问题以及使用dnsredir插件进行分流和alternate插件进行重试优化的操作。 1 自建DNS服务现状 一般来说&#xff0c;无论是bind9、coredns、dnsmasq、pdns哪类dns服务器&#xff0c;我们自建的监听在UDP53端口…

微信怎么自动跟圈?怎么一键转发好友的朋友圈?

做私域、微商的小伙伴们每天需要发许多朋友圈来推广产品&#xff0c;一个人手上就有好几个微信的话&#xff0c;每个微信都要发朋友圈的话&#xff0c;非常麻烦。有没有一键转发同步好友朋友圈的功能呢&#xff1f; 朋友圈互动 1&#xff09;查看朋友圈&#xff1a;可通过昵称…

智能优化算法应用:基于梯度算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于梯度算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于梯度算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.梯度算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

识别和修复网站上损坏链接的最佳实践

如果您有一个网站&#xff0c;我们知道您花了很多时间在它上面&#xff0c;以使其成为最好的资源。如果你的链接不起作用&#xff0c;你的努力可能是徒劳的。您网站上的断开链接可能会以两种方式损害您的业务&#xff1a; 它们对企业来说是可怕的&#xff0c;因为当消费者点击…

接上文 IBM ServeRAID M1015阵列卡 支持RAID5

模块到了&#xff0c;由于着急测试没直接拍照&#xff0c;就是一个跟指甲盖大小的模块&#xff0c;直接安装到阵列卡U1接口上&#xff0c;&#xff0c;不知道U1是哪个位置的参考前文&#xff0c;安装到机器上之后的图片如下 启动服务器&#xff0c;进入WebBIOS&#xff0c;选择…

Databend 如何利用 GPT-4 进行质量保证

背景 在数据库行业&#xff0c;质量是核心要素。 Databend 的应用场景广泛&#xff0c;特别是在金融相关领域&#xff0c;其查询结果的准确性对用户至关重要。因此&#xff0c;在快速迭代的过程中&#xff0c;如何确保产品质量&#xff0c;成为我们面临的重大挑战。 随着 Da…

微信小程序基础bug

1.苹果11手机小程序请求数据不显示 设置-》隐私-》分析与改进-》开启 ”与开发者共享“ 2.<navigator>组件回退delta不成功 tabBar 页面是不能实现后退的效果的. 因为, 当我们跳转到 tabBar 页面&#xff0c;会关闭其他所有非tabBar 页面,所以当处于 tabBar 页面时, 无…

Spring Cache【娓娓道来】

目录​​​​​​​ 1.自我介好&#x1f633;&#x1f633;&#x1f633; 2.常用注解 &#x1f495;&#x1f495;&#x1f495; 3.EnableCaching&#x1f926;‍♂️&#x1f926;‍♂️&#x1f926;‍♂️ 4.CachePut&#x1f937;‍♀️&#x1f937;‍♀️&#x1f93…

7.上传project到服务器及拉取服务器project到本地、更新代码冲突解决

1.上传project到SVN服务器 1.在eclipse中&#xff0c;从show view里调出SVN资源库视图 2.在SVN资源库窗口的空白位置右键选择新建资源库位置 3.填好服务器的地址 4.资源库导入成功,SVN资源库视图下出现导入的资源库 5.新建project 6.写好project的初始版本 7.右键project --&…

C++基础 -46- 类的静态函数成员

类的静态函数成员可以不创建类直接访问 #include "iostream"using namespace std;class base {public:static void show(){cout << "hello world" << endl;} };int main() {base::show(); }类的静态函数成员不能访问非静态成员 class base…

GPT实现开放式世界游戏实践【生化危机】

最近开始研究如何基于GPT构建一个游戏引擎&#xff0c;于是先从简单的文字游戏开始探索。 从最简单的选择机制、故事机制&#xff0c;完善成一个包括天气、事件、技能、属性、伙伴、建造系统的-生化危机版文字游戏-。 我唯一的体验是&#xff1a;AI游戏&#xff0c;大有可为! …

【LVS实战】04 LVS+Keepalived实现负载均衡高可用

一、介绍 Keepalived 是一个用于 Linux 平台的高可用性软件。它实现了虚拟路由器冗余协议 (VRRP) 和健康检查功能&#xff0c;可以用于确保在多台服务器之间提供服务的高可用性。Keepalived 可以检测服务器的故障&#xff0c;并在主服务器宕机时&#xff0c;自动将备份服务器提…

音视频之旅 - 基础知识

图像基础知识 像素 像素是图像的基本单元&#xff0c;一个个像素就组成了图像。你可以认为像素就是图像中的一个点。在下面这张图中&#xff0c;你可以看到一个个方块&#xff0c;这些方块就是像素 分辨率 图像&#xff08;或视频&#xff09;的分辨率是指图像的大小或尺寸。…

Python读写txt文件数据

&#x1f388; 博主&#xff1a;一只程序猿子 &#x1f388; 博客主页&#xff1a;一只程序猿子 博客主页 &#x1f388; 个人介绍&#xff1a;爱好(bushi)编程&#xff01; &#x1f388; 创作不易&#xff1a;如喜欢麻烦您点个&#x1f44d;或者点个⭐&#xff01; &#x1f…

OkGo导入失败解决办法

jcenter()maven { url "https://jitpack.io" }再同步就可以了

【3DsMax】制作简单的骨骼动画

效果 步骤 首先准备4个板子模型展开放置好 添加一个4段的骨骼 选中其中的一块板子添加蒙皮命令 在蒙皮的参数面板中&#xff0c;设置每块板子对应哪块骨骼 设置好后你可以发现此时就已经可以通过骨骼来控制模型了 接下来就可以制作动画 点击左下角“时间配置”按钮 设置一下动…

二叉树OJ题之三

哈喽伙伴们&#xff0c;有一段时间没更新博客了&#xff0c;主要是这段时间要准备学校的期末考试&#xff0c;所以没有把部分时间分给博客&#xff0c;今天我们一起去接着看二叉树递归有关的OJ题&#xff0c;今天我们要学习的是 判断相同的树&#xff0c;力扣题目--100 &…