Spring--三级缓存机制

news2024/9/20 10:32:24

一、什么是三级缓存

就是在Bean生成流程中保存Bean对象三种形态的三个Map集合,如下:

// 一级缓存Map 存放完整的Bean(流程跑完的)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

// 二级缓存Map 存放不完整的Bean(只实例化完,还没属性赋值、初始化)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);

// 三级缓存Map 存放一个Bean的lambda表达式(也是刚实例化完)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);

用来解决什么问题?

这个大家应该熟知了,就是循环依赖

什么是循环依赖?

就像下面这样,AService 中注入了BService ,而BService 中又注入了AService ,这就是循环依赖

@Service
public class AService {

    @Resource
    private BService bService;
}

@Service
public class BService {

    @Resource
    private AService aService;
}

二、Bean的加载源码

我们先通过getBean()流程图,来了解Spring的getBean()方法的工作流程,接着根据这个工作流程一步一步的阅读源码
在这里插入图片描述

//在spring中我们平时用到的getbean()这个方法实际上是调用的AbstractBeanFactory这个抽象工厂中得getbean方法
public Object getBean(String name) throws BeansException {
//看源码 我们首先看其返回值   如下返回得是doGetBean这个方法
        return this.doGetBean(name, null, null, false);
        }
//接下来我们看这个doGetBean这个方法
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {

 //获取name对应的真正beanName
//有这么几种情况 传入得参数有可能是某个参数得别名,也有可能是FactoryBean的name  //根据具体得实例去解析最终的得name
final String beanName = this.transformedBeanName(name);

        Object bean;

      //   在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖
       //  Spring创建bean的原则是不等bean创建完成就会将创建bean的ObjectFactory提前曝光(将对应的ObjectFactory加入到缓存)
      //   一旦下一个bean创建需要依赖上一个bean,则直接使用ObjectFactory对象
        // 获取单例
        Object sharedInstance = this.getSingleton(beanName);
        if (sharedInstance != null && args == null) {
        // 实例已经存在
        if (logger.isDebugEnabled()) {
	        if (this.isSingletonCurrentlyInCreation(beanName)) {
	        	logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");
	        } else {
	        	logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
	        }
        }
        // 返回对应的实例
        bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, null);
        } else {
	        // 单例实例不存在
	        if (this.isPrototypeCurrentlyInCreation(beanName)) {
	         // 只有在单例模式下才会尝试解决循环依赖问题
	         // 对于原型模式,如果存在循环依赖,也就是满足this.isPrototypeCurrentlyInCreation(beanName),抛出异常
	        throw new BeanCurrentlyInCreationException(beanName);
        }

        // 获取parentBeanFactory实例
        BeanFactory parentBeanFactory = this.getParentBeanFactory();
        // 如果在beanDefinitionMap中(即所有已经加载的类中)不包含目标bean,则尝试从parentBeanFactory中获取
        if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {
        	String nameToLookup = this.originalBeanName(name);  // 获取name对应的真正beanName,如果是factoryBean,则加上“&”前缀
	        if (args != null) {
	        // 递归到BeanFactory中寻找
	        	return (T) parentBeanFactory.getBean(nameToLookup, args);
	        } else {
	        	return parentBeanFactory.getBean(nameToLookup, requiredType);
	        }
        }

        // 如果不仅仅是做类型检查,标记bean的状态已经创建,即将beanName加入alreadyCreated集合中
        if (!typeCheckOnly) {
        	this.markBeanAsCreated(beanName);
        }

        try {
		 //将存储XML配置的GenericBeanDefinition实例转换成RootBeanDefinition实例,方便后续处理
		 // 如果存在父bean,则同时合并父bean的相关属性
		final RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
        // 检查bean是否是抽象的,如果是则抛出异常
        this.checkMergedBeanDefinition(mbd, beanName, args);

        // 加载当前bean依赖的bean
        String[] dependsOn = mbd.getDependsOn();
        if (dependsOn != null) {
        // 存在依赖,递归实例化依赖的bean
        for (String dep : dependsOn) {
        if (this.isDependent(beanName, dep)) {
        // 检查dep是否依赖beanName,从而导致循环依赖
        throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
        }
        // 缓存依赖调用
        this.registerDependentBean(dep, beanName);
        this.getBean(dep);
        }
        }

        // 完成加载依赖的bean后,实例化mbd自身
        if (mbd.isSingleton()) {
        // scope == singleton
        sharedInstance = this.getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
        try {
        return createBean(beanName, mbd, args);
        } catch (BeansException ex) {
        // 清理工作,从单例缓存中移除
        destroySingleton(beanName);
        throw ex;
        }
        }
        });
        bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        } else if (mbd.isPrototype()) {
        // scope == prototype
        Object prototypeInstance;
        try {
        // 设置正在创建的状态
        this.beforePrototypeCreation(beanName);
        // 创建bean
        prototypeInstance = this.createBean(beanName, mbd, args);
        } finally {
        this.afterPrototypeCreation(beanName);
        }
        // 返回对应的实例
        bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
        } else {
        // 其它范围得实例
        String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
        if (scope == null) {
        throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
        }
        try {
        Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
        @Override
        public Object getObject() throws BeansException {
        beforePrototypeCreation(beanName);
        try {
        return createBean(beanName, mbd, args);
        } finally {
        afterPrototypeCreation(beanName);
        }
        }
        });
        // 返回对应的实例
        bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
        } catch (IllegalStateException ex) {
        throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex);
        }
        }
        } catch (BeansException ex) {
        cleanupAfterBeanCreationFailure(beanName);
        throw ex;
        }
        }

        // 检查需要的类型是否符合bean的实际类型,对应getBean时指定的需要类型
        if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
        try {
        // 执行类型转换,转换成对应的类型
        return this.getTypeConverter().convertIfNecessary(bean, requiredType);
        } catch (TypeMismatchException ex) {
        if (logger.isDebugEnabled()) {
        logger.debug("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", ex);
        }
        throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
        }
        }
        到这里才是真正意义上返回一个bean而已
        return (T) bean;
        }

三、三级缓存详解

不管你了不了解源码,我们先看一下Bean的生成流程,看看三级缓存是在什么地方有调用,就三个地方:

  1. Bean实例化前会先查询缓存,判断Bean是否已经存在
  2. Bean属性赋值前会先向三级缓存中放入一个lambda表达式,该表达式执行则会生成一个半成品Bean放入二级缓存
  3. Bean初始化完成后将完整的Bean放入一级缓存,同时清空二、三级缓存

接下来我们一个一个看!
在这里插入图片描述

3.1 Bean实例化前

AbstractBeanFactory.doGetBean

Bean实例化前会从缓存里面获取Bean,防止重复实例化
在这里插入图片描述

DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)

我们看看这个获取的方法逻辑:

  • 从一级缓存获取,获取到了,则返回
  • 从二级缓存获取,获取到了,则返回
  • 从三级缓存获取,获取到了,则执行三级缓存中的lambda表达式,将结果放入二级缓存,清除三级缓存
public Object getSingleton(String beanName) {
    return this.getSingleton(beanName, true);
}

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从一级缓存中获取Bean 获取到了则返回 没获取到继续
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
        // 从二级缓存中获取Bean  获取到了则返回 没获取到则继续
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            // 加一把锁防止 线程安全 双重获取校验
            synchronized(this.singletonObjects) {
                // 从一级缓存中获取Bean 获取到了则返回 没获取到继续
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    // 从二级缓存中获取Bean  获取到了则返回 没获取到则继续
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 从三级缓存中获取 没获取到则返回
                        ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            // 获取到了 执行三级缓存中的lambda表达式
                            singletonObject = singletonFactory.getObject();
                            // 并将结果放入二级缓存
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            // 从三级缓存中移除
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }

    return singletonObject;
}

3.2 属性赋值/注入前

AbstractAutowireCapableBeanFactory.doCreateBean
在这里插入图片描述
DefaultSingletonBeanRegistry.addSingletonFactory

这里就是将一个lambda表达式放入了三级缓存,我们需要去看一下这个表达式是干什么的!!

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized(this.singletonObjects) {
        // 一级缓存中不存在的话 
        if (!this.singletonObjects.containsKey(beanName)) {
            // 将lambda表达式放入三级缓存
            this.singletonFactories.put(beanName, singletonFactory);
            // 清除二级缓存 
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

AbstractAutowireCapableBeanFactory.getEarlyBeanReference

该方法说白了就是会判断该Bean是否需要被动态代理,两种返回结果:

  • 不需要代理,返回未属性注入、未初始化的半成品Bean
  • 需要代理,返回未属性注入、未初始化的半成品Bean的代理对象
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && this.hasInstantiationAwareBeanPostProcessors()) {
        Iterator var5 = this.getBeanPostProcessors().iterator();
        // 遍历后置处理器
        while(var5.hasNext()) {
            BeanPostProcessor bp = (BeanPostProcessor)var5.next();
            // 找到实现SmartInstantiationAwareBeanPostProcessor接口的
            // 该接口getEarlyBeanReference方法什么时候会执行?
            // AOP动态代理的时候 该方法执行就是判断该Bean是否需要被代理
            // 需要代理则会创建代理对象返回
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor)bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    // 这个Object有两种情况,一是实例化后的半成品Bean,二是半成品Bean动态代理后的代理对象
    return exposedObject;
}

注意:这里只是把lambda表达式放入了三级缓存,如果不从三级缓存中获取,这个表达式是不执行的,一旦执行了,就会把半成品Bean或者半成品Bean的代理对象放入二级缓存中了

3.3初始化后

AbstractBeanFactory.doGetBean

这里注意啊,这个getSingleton方法传参传了个lambda表达式,这个表达式内部就是Bean的实例化过程,初始化完成后,是要需要执行这个getSingleton方法的
在这里插入图片描述
DefaultSingletonBeanRegistry.getSingleton(beanName, singletonFactory)

这个方法与上面那个不一样,重载了

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
     
        synchronized(this.singletonObjects) {
            // 第一次进来这里获取肯定为null
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
               // 省略................
                try {
                    // 注意啊,这个就是执行外面那个传参的lambda表达式
                    // 所以这里才会跳到createBean方法那里去执行
                    singletonObject = singletonFactory.getObject();
                    newSingleton = true;
                } 
                // 省略................
                finally {
                    if (recordSuppressedExceptions) {
                        this.suppressedExceptions = null;
                    }
                    this.afterSingletonCreation(beanName);
                }
                // 到了这说明Bean创建完了
                if (newSingleton) {
                    // 这里就会把Bean放入一级缓存中了 同时清除二、三级缓存
                    this.addSingleton(beanName, singletonObject);
                }
            }
            return singletonObject;
        }
    }

DefaultSingletonBeanRegistry.addSingleton

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);
  }
}

总结

整个过程就三个地方跟缓存有关,我们假设现在要实例化A这个Bean,看看缓存是怎么变化的:

  • 实例化前,获取缓存判断(三个缓存中肯定没有A,获取为null,进入实例化流程)
  • 实例化完成,属性注入前(往三级缓存中放入了一个lambda表达式,一、二级为null)
  • 初始化完成(将A这个Bean放入一级缓存,清除二、三级缓存)

以上则是单个Bean生成过程中缓存的变化!!

四、怎么解决的循环依赖

上面我们把Bean流程中利用缓存的三个重要的点都找出来了,也分析了会带来什么变化,接下来看看是怎么解决的循环依赖,我们看个图就懂了:

以A注入B,B注入A为例:

A属性注入前就把lambda表达式放入了第三级缓存,所以B再注入A的时候会从第三级缓存中找到A的lambda表达式并执行,然后将半成品Bean放入第二级缓存,所以此时B注入的只是半成品的A对象,B创建完成后返回给A注入,A继续初始化,完成创建。

注意: B注入的半成品A对象只是一个引用,所以之后A初始化完成后,B这个注入的A就随之变成了完整的A
在这里插入图片描述
从上述看第三级缓存是用来提前暴露Bean对象引用的,所以解决了循环依赖,但是第二级缓存的这个半成品Bean对象干嘛的呢?

假设A同时注入了B和C,B和C又都注入了A,这时A注入B,实例化B的过程和上述是一样的,但随后还会注入C,那这个C在注入A的时候还会有第三级缓存用吗?没了吧,所以它就只能用第二级缓存的半成品Bean对象了,同样也是引用而已

五、不用三级缓存不行吗

可能很多小伙伴得到的答案就是不行,而且答案是因为不确定这个Bean是不是代理对象,所以搞了个lambda表达式?答案真的是这样吗??

我们分析一下:AOP动态代理在没有循环依赖的时候是在哪里执行的?Bean初始化后!有循环依赖的时候是在属性赋值前,中间就间隔了一个属性注入对吧,没错,在属性注入的时候注入的是原始对象的引用还是代理对象的引用这个很重要,但是属性注入会影响AOP的结果吗?是否AOP创建代理对象和切面有关,和属性注入无关,所以我们完全可以在属性注入之前就知道这个Bean是代理对象还是非代理对象,就像下面这样,我不将表达式放入第三级缓存了,而是直接执行,将结果放入第二级缓存
在这里插入图片描述
这样可不可以?可以吧,这样用二级缓存就解决了,但是在一个对象没有属性赋值、初始化前就创建代理对象是有风险的!像这么做不管有没有产生循环依赖,只要有AOP动态代理对象的产生就有一分风险,这么做是得不偿失的,所以有了三级缓存,三级缓存是只有在循环依赖以及AOP动态代理同时产生时才会有风险。可以说是因为存在循环依赖所以被迫的导致Bean对象提前的暴露了引用!!! 所以这下懂了吧

至于为什么多例、构造器注入这两种情况解决不了循环依赖就很简单了:

循环依赖的解决原理是在对象实例化后提前暴露了引用,而这两种情况都还没实例化呢

六、总结

  • 一级缓存:用于存储被完整创建了的bean。也就是完成了初始化之后,可以直接被其他对象使用的bean。
  • 二级缓存:用于存储半成品的Bean。也就是刚实例化但是还没有进行初始化的Bean
  • 三级缓存:三级缓存存储的是工厂对象(lambda表达式)。工厂对象可以产生Bean对象提前暴露的引用(半成品的Bean或者半成品的代理Bean对象),执行这个lambda表达式,就会将引用放入二级缓存中

经过以上的分析,现在应该懂了吧:

循环依赖是否一定需要三级缓存来解决? 不一定,但三级缓存会更合适,风险更小

二级缓存能否解决循环依赖? 可以,但风险比三级缓存更大

第二级缓存用来干嘛的? 存放半成品的引用,可能产生多对象循环依赖,第三级缓存产生引用后,后续的就可以直接注入该引用

多例、构造器注入为什么不能解决循环依赖? 因为循环依赖的原理的实例化后提前暴露的引用,这两种情况还没实例化

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

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

相关文章

51单片机——LED灯控制

1、LED介绍 中文名&#xff1a;发光二极管 外文名&#xff1a;Light Emitting Diode 简称&#xff1a;LED 用途&#xff1a;照明、广告灯、指引灯、屏幕 2、LED原理图 电阻在原理图上标注为1k&#xff0c;表示这是1千欧的电阻&#xff0c;实际在电路板上的表示是102 102解…

HarmonyOs应用权限申请,system_grant和user_grant区别。本文附头像上传申请user-grant权限代码示例

HarmonyOs应用权限申请&#xff0c;system_grant和user_grant区别。本文附头像上传申请user-grant权限代码示例 system_grant&#xff08;系统授权&#xff09; system_grant指的是系统授权类型&#xff0c;在该类型的权限许可下&#xff0c;应用被允许访问的数据不会涉及到用户…

【大数据算法】一文掌握大数据算法之:排序链表搜索的亚线性算法。

排序链表搜索的亚线性算法 1、引言2、平面图直径问题的亚线性算法2.1 定义2.2 核心原理2.2.1 跳表2.2.2 跳跃搜索2.2.3 分块搜索 2.3 应用场景2.4 算法公式2.5 代码示例 3、总结 1、引言 小屌丝&#xff1a;鱼哥&#xff0c;这茶味道怎么样&#xff1f; 小鱼&#xff1a;嗯&am…

计算机毕业设计选题推荐-保险业务管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

[CUDA编程] --- cuda线程模型

1 核函数 先看一个cuda版本的hello world #include <stdio.h>__global__ void helloworld() {printf("hello world\n"); }int main() {helloworld()<<<1, 1>>>();cudaDeviceSynchronize();return 0; }这里helloworld()<<<1, 1>…

旅行达人必备!有道翻译和这三款神器,轻松走遍世界

在如今的全球化和科技迅猛发展的时代&#xff0c;翻译工具在我们的日常生活中发挥着越来越重要的作用。在各种格式数据的翻译当中&#xff0c;我们就可以发现各种类型的翻译工具纷纷崭露头角。今天就分享三款除了有道翻译外的好用翻译工具&#xff0c;希望可以解决大家翻译的需…

虚幻5|暴击攻击和释放技能,造成伤害

玩家数据的Actor组件制作&#xff1a;虚幻5|制作玩家血量&#xff0c;体力-CSDN博客 造成伤害时&#xff0c;显示暴击及暴击字体颜色和未暴击的字体颜色&#xff0c;还有释放技能连击 一.编辑暴击数据 1.打开之前创建的玩家数据Actor组件 创建一个浮点变量&#xff0c;命名…

从法律风险的角度来看,项目经理遇到不清楚或不明确问题时的处理

大家好&#xff0c;我是不会魔法的兔子&#xff0c;在北京从事律师工作&#xff0c;日常分享项目管理风险预防方面的内容。 序言 在项目开展过程中&#xff0c;有时候会遇到一些不清楚或不明确的状况&#xff0c;但碍于项目进度的紧迫性&#xff0c;不得不硬着头皮做决策&…

喜羊羊教你(如何应对突发的技术故障和危机?)

开发团队如何应对突发的技术故障和危机&#xff1f; 在数字化时代&#xff0c;软件服务的稳定性至关重要。、8月19日下午&#xff0c;网易云音乐疑似出现服务器故障&#xff0c;网页端出现502 Bad Gateway 报错&#xff0c;且App也无法正常使用。 怀疑了自己的电脑、自己的手…

OpenStack 常见模块(二)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

MFC工控项目实例之七点击下拉菜单弹出对话框

承接专栏《MFC工控项目实例之六CFile添加菜单栏》 1、在SEAL_PRESSUREDlg.h文件中添加代码 class CSEAL_PRESSUREDlg : public CDialog { ...afx_msg void OnTypeManage(); ... } 2、在SEAL_PRESSUREDlg.cpp文件中添加代码 BEGIN_MESSAGE_MAP(CSEAL_PRESSUREDlg, CDialog)//…

如何使用ssm实现基于Java的学生信息管理系统的设计与实现

TOC ssm165基于Java的学生信息管理系统的设计与实现jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&a…

python-随机序列(赛氪OJ)

[题目描述] 小理的作业太多了&#xff0c;怎么也做不完。 小理的数学作业由 T 张试卷组成&#xff0c;每张试卷上有 n 个数 a1..n​ &#xff0c;小理需要算出这些数的极差和方差。极差是一个整数&#xff0c;方差是一个浮点数&#xff0c;要求保留到小数点后 3 位。虽然题目很…

iPhone 手机使用技巧:iPhone 数据恢复软件

无论是由于意外删除、系统崩溃还是软件更新&#xff0c;丢失 iPhone 上的数据都是一场噩梦。从珍贵的照片到重要的工作文件&#xff0c;这种损失可能会让人感到毁灭性。值得庆幸的是&#xff0c;几个 iPhone 数据恢复软件选项可以帮助您找回丢失的文件。这些工具提供不同的功能…

大学数据库系统原理 Mysql数据库实验记录

软件版本说明&#xff1a; 1.Mysql数据库&#xff1a;sql server8.0 2.命令实现使用以及数据库可视化查看&#xff1a;Navicat 16 #不用Mysql Command Line 的原因是不喜欢那个黑框&#xff0c;也不常用&#xff0c;使用Navicat的MYSQL命令列界面是一样的 另外说明 实现相同…

Junit单元测试笔记

常用mock类框架 在软件测试和开发过程中&#xff0c;Mock框架扮演着至关重要的角色&#xff0c;它们允许开发者模拟对象的行为&#xff0c;以便在不需要实际依赖的情况下进行测试。以下是一些常用的Mock框架&#xff1a; MockitoPowerMockEasyMockJMockSpock 初始化mock/spy…

解决ONENOTE复制文字到外部为图片(Ditto)

默认情况下&#xff0c;在ONENOTE中记录的文字&#xff0c;在复制粘贴到外部时&#xff0c;会成为一张图片格式 如下图这段文字&#xff0c;粘贴到QQ中变为了图片 解决办法&#xff1a;安装Ditto Ditto下载链接 点击Download下载 双击安装.exe&#xff0c;选择安装路径后&…

JVM上篇:内存与垃圾回收篇-07-方法区

笔记来源&#xff1a;尚硅谷 JVM 全套教程&#xff0c;百万播放&#xff0c;全网巅峰&#xff08;宋红康详解 java 虚拟机&#xff09; 文章目录 7. 方法区7.1. 栈、堆、方法区的交互关系7.2. 方法区的理解7.2.1. 方法区在哪里&#xff1f;7.2.2. 方法区的基本理解7.2.3. HotSp…

编译 wolfssl 库

wolfssl github: https://github.com/wolfSSL/wolfssl 编译 .lib 或者 .dll wolfssl 很好的提供了 win32 的工程》sln 文件 这样就不用折腾 CMakeLists 文件了&#xff0c;使用 Visual Studio 打开 sln 文件后&#xff0c;设置好 Static 编译库即可&#xff0c;开箱即用 编译 .…

项目开始后,拒绝客户提出的新需求是否会违约?

大家好&#xff0c;我是不会魔法的兔子&#xff0c;在北京从事律师工作&#xff0c;日常分享项目管理风险预防方面的内容。 序言 当一个项目已经开展后&#xff0c;对于项目组而言&#xff0c;最难以忍受的可能要数需求突然发生变化了&#xff0c;尤其是在项目已经进行一半或…