三级缓存破局:Spring 如何优雅解决循环依赖?

欢迎来到 Spring 容器的**"死结解开中心"**

在分布式系统中,我们害怕死锁;在代码逻辑中,我们害怕死循环。但在 Spring 的 Bean 容器里,**循环依赖(Circular Dependency)**却是一个被精心设计的"特性",而非 Bug。

生活化比喻

  • 循环依赖 = "鸡生蛋,蛋生鸡"
    A 说:"没有 B 我活不了。" B 说:"没有 A 我也活不了。"如果两人僵持不下,程序就死锁了(栈溢出)。
  • 三级缓存 = "半成品中转站" + "预言家工厂"
    Spring 的做法是:A 刚出生(实例化),还没穿衣服(属性填充),就先把自己的一张**"裸照"** (早期引用)或者**"穿着未来衣服的照片"** (代理对象)拍下来,存进中转站。
    当 B 需要 A 时,不用等 A 完全长大,直接去中转站拿这张照片先用着。等 B 长大了,A 再拿着 B 继续完成自己的成长。
  • 核心逻辑"先暴露半成品,再完成初始化"

今天,我们要深入 DefaultSingletonBeanRegistry 的核心,用源码和逻辑推导,彻底讲透为什么必须是三级缓存 ,以及AOP 代理在这个局中扮演的关键角色。

场景复现:成功 vs 失败

首先,我们用代码看看 Spring 能解决什么,不能解决什么

复制代码
@Component
class ServiceA {
    @Autowired // 字段注入
    private ServiceB serviceB;
    public void doA() { serviceB.doB(); }
}

@Component
class ServiceB {
    @Autowired // 字段注入
    private ServiceA serviceA;
    public void doB() { serviceA.doA(); }
}
// 结果:启动成功,运行正常。

场景二:构造器注入(失败)

Spring 直接报错:BeanCurrentlyInCreationException

复制代码
@Component
class ServiceA {
    private final ServiceB serviceB;
    // 构造器注入
    public ServiceA(ServiceB serviceB) { 
        this.serviceB = serviceB; 
    }
}

@Component
class ServiceB {
    private final ServiceA serviceA;
    // 构造器注入
    public ServiceB(ServiceA serviceA) { 
        this.serviceA = serviceA; 
    }
}
// 结果:启动报错!
// Reason: Requested bean is currently in creation: Is there an unresolvable circular reference?
报错堆栈

当你运行那个 ServiceA <-> ServiceB 的构造器循环依赖代码时,Spring 不会温柔地提示你,它会直接抛出 BeanCurrentlyInCreationException,并附带一段长长的堆栈:

复制代码
org.springframework.beans.factory.BeanCurrentlyInCreationException: 
Error creating bean with name 'serviceA': Requested bean is currently in creation: 
Is there an unresolvable circular reference?

    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
    // ... 省略中间调用 ...
    at com.example.ServiceA.<init>(ServiceA.java:14) // 看这里!A 的构造函数在等 B
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    // ...
    at com.example.ServiceB.<init>(ServiceB.java:14) // 看这里!B 的构造函数在等 A
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    // ... 无限递归直到栈溢出或 Spring 主动拦截 ...
    
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:65)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1328)

注意看堆栈中的 Requested bean is currently in creation

Spring 内部有一个 singletonsCurrentlyInCreation 的 Set,用来记录正在创建中的 Bean。

  1. 创建 A,把 "serviceA" 加入 Set。
  2. A 调构造器需要 B,去创建 B,把 "serviceB" 加入 Set。
  3. B 调构造器需要 A,发现 "serviceA" 已经在 Set 里了
  4. Spring 此时判断:这是构造器注入,没法提前暴露引用(因为还没实例化完),于是直接抛异常,阻止栈溢出。

so、为什么构造器注入无法解决?

  • 逻辑死锁 :构造器执行发生在实例化阶段 (第一步)。
    • 创建 A -> 调用 new A(B) -> 需要先创建 B。
    • 创建 B -> 调用 new B(A) -> 需要先创建 A。
    • 死循环:A 等 B,B 等 A,谁都无法迈出"实例化"这第一步。
    • 直接**"熔断机制",**报错也不能死循环
  • 无隙可乘 :Spring 解决循环依赖的核心是**"提前暴露早期引用"** 。但这一步发生在实例化之后,属性填充之前 。构造器注入连"实例化"都没完成,根本没有机会把"早期引用"放出去。

尽量避免构造器循环依赖。如果必须,使用 @Lazy 注解注入一个代理对象来打破僵局

核心地图:三级缓存的真面目

Spring 解决循环依赖的秘密武器,藏在 DefaultSingletonBeanRegistry 类的三个 Map 中

复制代码
// 1. 一级缓存:成品池
// 存放已经完全初始化好的 Bean(如果是 AOP Bean,这里存的是代理对象)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 2. 二级缓存:早期对象池
// 存放原始的 Bean 对象,或者提前生成的代理对象。
// 用于解决循环依赖,避免重复创建代理。
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 3. 三级缓存:工厂池 (核心中的核心)
// 存放 ObjectFactory<?>,这是一个函数式接口,用于按需生成早期引用。
// 关键点:它不存对象,存的是"创建对象的逻辑"。
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

灵魂拷问:为什么需要三级?两级不够吗?

很多教程说:"三级缓存是为了支持 AOP"。但具体是怎么支持的?如果只有两级会发生什么矛盾?

让我们通过逻辑推导来揭开谜底。

假设只有两级缓存(去掉 singletonFactories

流程推演

  1. A 实例化 (new A())。
  2. A 需要暴露早期引用 :因为没有三级缓存的工厂,我们必须立即 决定暴露什么对象放入二级缓存 earlySingletonObjects
    • 选择 1:暴露原始对象 A
      • B 依赖 A,从二级缓存拿到原始 A。
      • B 初始化完成。
      • A 继续初始化,走到后置处理器(AOP 环节)。
      • 问题 :A 需要被代理(比如加了 @Transactional)。Spring 创建了代理对象 ProxyA
      • 灾难 :B 手里拿的是原始 A ,而容器最终注册的是 ProxyA
      • 后果 :B 调用 A 的方法时,事务切面失效!因为 B 调用的不是代理对象。而且容器中出现了两个 A 的实例(一个是 B 持有的原始 A,一个是容器管理的 ProxyA),破坏了单例原则。
    • 选择 2:立即暴露代理对象 ProxyA
      • 后果:要么报错(重复代理),要么性能浪费(提前创建了可能不需要的代理,或者创建了两次)。
      • 灾难 :如果我们在第二步就创建了 ProxyA,而在正常的后置处理流程中,AnnotationAwareAspectJAutoProxyCreator 又会尝试创建一次代理。
      • 问题 :AOP 的创建通常依赖于 Bean 的完整状态(虽然大部分不依赖,但逻辑上 AOP 应该在初始化后期执行)。更严重的是,如果 A 没有发生循环依赖(即 B 不依赖 A),那么 A 会正常走完后置处理流程。
      • 在 A 刚实例化完(还没填充属性,还没执行 Aware 回调),就强制创建代理对象 ProxyA 放入二级缓存。
三级缓存的"神来之笔":延迟决策

Spring第三级缓存 singletonFactories存储是一个 ObjectFactory (Lambda 表达式)

核心逻辑

  • 实例化后 :我不立即创建代理,也不立即暴露原始对象。我只把一个**"制造早期引用的工厂"**放进去。
  • 只有当别人真正需要我时(发生循环依赖)
    • B 来找 A。
    • B 调用工厂的 getObject() 方法。
    • 就在这一刻 ,工厂内部执行 getEarlyBeanReference()
    • 判断 :A 是否需要 AOP?
      • :立即创建代理对象 ProxyA,放入二级缓存,返回 ProxyA
      • :返回原始对象 A,放入二级缓存。
  • 如果没有循环依赖
    • 工厂永远不会被调用。
    • A 正常走完所有流程,在标准的后置处理阶段创建代理。

结论

三级缓存的核心价值在于**"延迟初始化代理对象"** 。它解决了**"提前暴露"** 和**"AOP 代理时机"**之间的矛盾。

  • 它保证了:如果发生循环依赖,B 拿到的一定是正确的代理对象
  • 它保证了:如果不发生循环依赖,A 的代理对象只在标准流程中创建一次

源码深挖:doCreateBean 中的关键三步

第一步:实例化后,存入三级缓存

位置AbstractAutowireCapableBeanFactory.doCreateBean()

复制代码
// 1. 实例化 Bean (new A())
instanceWrapper = createBeanInstance(beanName, mbd, args);
Object exposedObject = instanceWrapper.getWrappedInstance();

// 2. 判断是否需要提前暴露(单例 + 允许循环依赖 + 正在创建中)
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));

if (earlySingletonExposure) {
    // 【关键】存入三级缓存
    // 注意:这里传入的是一个 Lambda (ObjectFactory),而不是对象本身!
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, exposedObject));
}

解读:此时 A 只是个空壳。Spring 并没有创建代理,只是注册了一个"承诺":如果有人要 A,我就执行这个 Lambda 给他!

第二步:别人来借,调用工厂,升入二级缓存

位置DefaultSingletonBeanRegistry.getSingleton() (被 doGetBean 调用)

当 B 需要 A 时,会调用 getSingleton("A", false) (allowEarlyReference=true)。

复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 查一级缓存 (成品)
    Object singletonObject = this.singletonObjects.get(beanName);
    
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 2. 查二级缓存 (早期对象)
        singletonObject = this.earlySingletonObjects.get(beanName);
        
        if (singletonObject == null && allowEarlyReference) {
            // 3. 查三级缓存 (工厂)
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            
            if (singletonFactory != null) {
                // 【核心时刻】调用工厂,获取早期引用
                singletonObject = singletonFactory.getObject();
                
                // 4. 从三级移除,放入二级
                this.singletonFactories.remove(beanName);
                //朋友,加急整了
                this.earlySingletonObjects.put(beanName, singletonObject);
            }
        }
    }
    return singletonObject;
}
  • 只有当 B 真的需要 A 时,singletonFactory.getObject() 才会被执行。
  • 在这个 Lambda 内部 (getEarlyBeanReference),Spring 会调用 SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference
  • 这正是 AnnotationAwareAspectJAutoProxyCreator 介入的地方!如果需要 AOP,现在就创建代理
  • 一旦创建,对象就从三级升到二级 ,保证后续再有人来借,拿到的都是同一个对象(避免重复创建代理)!!!🌟

第三步:自己完工,清理缓存,存入一级

位置AbstractAutowireCapableBeanFactory.doCreateBean() 结尾

复制代码
// A 完成了属性填充、Aware 回调、初始化方法...
// 现在到了后置处理阶段 (postProcessAfterInitialization)
// 如果之前因为循环依赖已经创建了代理,这里会直接返回二级缓存中的代理对象
// 如果之前没创建(无循环依赖),这里才正式创建代理

// 最后,将成品放入一级缓存
addSingleton(beanName, exposedObject); 
// addSingleton 内部会:
// 1. put into singletonObjects (一级)
// 2. remove from earlySingletonObjects (二级)
// 3. remove from singletonFactories (三级,以防万一)

迷你剧场:A (带事务) <-> B 的循环依赖之旅

假设 ServiceA@TransactionalServiceB 没有。A 依赖 B,B 依赖 A。

  1. 创建 A
    • new ServiceA() (原始对象)。
    • 发现 A 正在创建中,将工厂放入三级缓存
    • 工厂逻辑:if (needAOP) return createProxy(A); else return A;
  2. A 填充属性
    • 发现需要 ServiceB
  3. 创建 B
    • new ServiceB()
    • B 填充属性,发现需要 ServiceA
  4. B 寻找 A
    • 一级缓存?无。
    • 二级缓存?无。
    • 三级缓存?有工厂!
    • 调用工厂 getObject()
    • 关键时刻 :工厂检测到 A 有 @Transactional立即创建 A 的代理对象 ProxyA
    • ProxyA 放入二级缓存,移除三级缓存。
    • 返回 ProxyA 给 B。
    • B 持有 ProxyA。B 初始化完成,放入一级缓存。
  5. A 继续
    • A 拿到 B,完成属性填充。
    • A 执行 Aware 回调。
    • A 执行 @PostConstruct 等初始化方法。
    • A 进入后置处理 (postProcessAfterInitialization)
      • AnnotationAwareAspectJAutoProxyCreator 再次介入。
      • 它检查二级缓存,发现 A 已经在里面了(且是 ProxyA)。
      • 策略 :直接返回二级缓存中的 ProxyA不再创建新代理
    • A 初始化完成,放入一级缓存,清理二、三级。
  6. 结局
    • 容器中:ServiceA = ProxyA
    • B 中持有的:ServiceA = ProxyA
    • 完美一致!事务生效!

实战锦囊------如何优雅地"破解"死锁?

当遇到 Spring 无法自动解决的循环依赖(如构造器注入、多例 Prototype)时,我们该怎么办?这里有三种"手术方案"。

1、@Lazy 注解(最推荐的"作弊"手段)

@Lazy 会让 Spring 注入一个代理对象,而不是真实对象。只有当你第一次调用方法时,代理对象才会去容器里获取真实的 Bean。这就打破了构造时的"即时依赖"。

复制代码
@Component
class ServiceA {
    private final ServiceB serviceB;

    //  加上 @Lazy,注入的是 ServiceB 的代理
    public ServiceA(@Lazy ServiceB serviceB) { 
        this.serviceB = serviceB; 
    }
    
    public void doA() { 
        // 这里才真正触发 ServiceB 的初始化
        serviceB.doB(); 
    }
}
  • 效果:构造器瞬间完成,因为代理对象创建很快,不需要等待 B 完全初始化。
  • 适用场景 :无法修改架构,必须用构造器注入,且确定逻辑上不会在构造器内部直接调用 B 的方法。

2、实现 ApplicationContextAware 手动获取(下策)

先注入容器上下文,在需要的时候再去 getBean。这相当于把依赖查找从"编译期/启动期"推迟到"运行期"。这么说有点抽象,下面来看代码:

复制代码
@Component
class ServiceA implements ApplicationContextAware {
    private ApplicationContext context;
    private ServiceB serviceB;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.context = applicationContext;
    }

    public void doSomething() {
        //  用的时候再找,这时候 B 肯定已经好了
        if (serviceB == null) {
            serviceB = context.getBean(ServiceB.class);
        }
        serviceB.doB();
    }
}

代码耦合了 Spring API,不优雅,测试困难。除非万不得已,不推荐

架构重构(上上策)

环依赖往往是**设计 smell(代码异味)**的信号。说明这两个类职责耦合太紧。

  • 提取第三方类:把 A 和 B 共同依赖的逻辑提取出来,变成 C。A 依赖 C,B 依赖 C。C 不依赖 A 和 B。

  • 事件驱动:A 做完事发个事件,B 监听事件。解耦。

    // 重构前:A <-> B
    // 重构后:A -> C <- B
    @Component
    class CommonLogic { // C
    public void sharedMethod() {}
    }

    @Component
    class ServiceA {
    private final CommonLogic commonLogic; // 依赖 C
    // ...
    }

    @Component
    class ServiceB {
    private final CommonLogic commonLogic; // 依赖 C
    // ...
    }

验证实验------亲眼看见三级缓存的变化

我们来写一个测试,利用反射"黑入"Spring 容器,打印出三级缓存的内容,见证奇迹时刻

复制代码
@SpringBootTest
public class CircularDependencyTest {

    @Autowired
    private ApplicationContext context;

    @Test
    public void testCircularReferenceCache() throws Exception {
        // 1. 获取 DefaultSingletonBeanRegistry (通过反射访问私有字段)
        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory();
        Field singletonObjectsField = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects");
        Field earlySingletonObjectsField = DefaultSingletonBeanRegistry.class.getDeclaredField("earlySingletonObjects");
        Field singletonFactoriesField = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonFactories");
        
        singletonObjectsField.setAccessible(true);
        earlySingletonObjectsField.setAccessible(true);
        singletonFactoriesField.setAccessible(true);

        Map<String, Object> level1 = (Map<String, Object>) singletonObjectsField.get(factory);
        Map<String, Object> level2 = (Map<String, Object>) earlySingletonObjectsField.get(factory);
        Map<String, ObjectFactory<?>> level3 = (Map<String, ObjectFactory<?>>) singletonFactoriesField.get(factory);

        // 2. 触发 Bean 创建 (假设 ServiceA 和 ServiceB 有循环依赖)
        // 注意:如果是单例,启动时就已经创建好了。
        // 为了观察过程,我们可以尝试在启动钩子中打断点,或者这里只是打印最终状态
        
        System.out.println("=== 一级缓存 (成品池) ===");
        level1.keySet().forEach(k -> System.out.println(" [L1] " + k + " -> " + level1.get(k).getClass().getSimpleName()));

        System.out.println("\n=== 二级缓存 (早期对象池) ===");
        // 正常启动完成后,二级缓存应该是空的,因为循环依赖已解决,对象都升入 L1 了
        System.out.println("当前大小: " + level2.size() + " (正常应为 0)");

        System.out.println("\n=== 三级缓存 (工厂池) ===");
        // 正常启动完成后,三级缓存也应该是空的
        System.out.println("当前大小: " + level3.size() + " (正常应为 0)");
        
        //  想要看到非空状态?
        // 你需要在 AbstractAutowireCapableBeanFactory.doCreateBean 方法中打断点。
        // 当执行到 addSingletonFactory 后,但在 populateBean 之前,查看 level3,你会看到 ServiceA 的工厂!
    }
}
  • 启动完成后 :二、三级缓存通常是的。因为循环依赖一旦解决,对象就会晋升到一级缓存,临时缓存会被清理。
  • 调试技巧 :想看三级缓存里有东西?必须在 doCreateBean 方法里打断点 ,停在 addSingletonFactory 之后。那时你会发现 singletonFactories 里躺着那个神奇的 Lambda!

边界与禁忌------Spring 搞不定的场景

不要以为有了三级缓存就万事大吉。以下场景,三级缓存直接失效,踩中必死。

1. Prototype (多例) 作用域

现象 :如果 A 是 Singleton,B 是 Prototype,且互相依赖。
结果 :报错 BeanCurrentlyInCreationException
原因

  • Spring 的循环依赖缓存(一、二、三级)只针对 Singleton
  • Prototype Bean 每次请求都 new 一个,容器不缓存,也不管理其完整生命周期(特别是销毁)。
  • 既然不缓存,就没法"提前暴露引用"。B 每次要 A 都是新的,A 每次要 B 也是新的,死循环无解。

2. @Async 异步代理

现象 :A 和 B 互相依赖,且其中一个方法加了 @Async
结果 :可能报错,或者 AOP 失效。
原因

  • @Async 会生成代理对象。
  • 如果在循环依赖中,早期引用暴露的是原始对象,而最终需要的是异步代理对象。
  • 虽然三级缓存试图处理 AOP,但 @Async 的代理创建逻辑比较特殊(有时在更晚的阶段),容易导致 getEarlyBeanReference 拿到的对象和最终对象不一致。
  • 解决 :尽量避免在循环依赖链中使用 @Async,或者全部改为 @Lazy

3. 字段注入 vs 构造器注入的混合地狱

现象 :A 用构造器注入 B,B 用字段注入 A。
结果 :依然报错。
原因 :只要链条中任何一个环节是构造器注入,整个链条的"实例化"阶段就会卡死。木桶效应,最短的那块板(构造器)决定了死锁的发生。

复习时刻:深度回答

Q1: 为什么 Spring 解决循环依赖需要三级缓存?两级行不行

两级缓存不足以完美解决包含 AOP 代理 的循环依赖场景。

  1. 如果只有两级 :在 Bean 实例化后,我们必须立即决定向二级缓存放入"原始对象"还是"代理对象"。
    • 若放入原始对象 :当该 Bean 需要 AOP 时,后续流程会创建代理对象。这导致其他 Bean 持有的是原始对象,而容器中最终注册的是代理对象,造成单例不一致AOP 失效
    • 若放入代理对象 :必须在实例化后立即创建代理。但这违背了 Spring 的设计原则(代理应在后置处理阶段创建)。且如果该 Bean 没有 发生循环依赖,会导致代理对象被提前创建 甚至重复创建,造成资源浪费或逻辑错误。
  2. 三级缓存的优势
    • 第三级缓存存储的是 ObjectFactory(工厂函数),实现了延迟加载
    • 只有在真正发生循环依赖(其他 Bean 来索取)时,才调用工厂。
    • 工厂内部根据是否需要 AOP,动态决定是返回原始对象还是立即创建代理对象
    • 这样既保证了循环依赖时拿到的是正确的代理对象,又保证了无循环依赖时代理对象按正常流程创建,实现了逻辑的严密性性能的最优解

Q2: 构造器注入的循环依赖为什么无法解决?

Spring 解决循环依赖的核心机制是**"提前暴露早期引用"** ,这一步发生在 Bean 的实例化之后、属性填充之前

  • 字段/Setter 注入:依赖注入发生在属性填充阶段。此时 Bean 已经实例化,有机会将早期引用放入缓存,供对方使用。
  • 构造器注入 :依赖注入发生在实例化阶段 (构造函数执行时)。
    • 创建 A 需要调用 new A(b),此时 B 还不存在。
    • 为了创建 B,又需要调用 new B(a),此时 A 也还没实例化完成。
    • 由于连实例化都没完成,Spring 根本没有机会执行"提前暴露引用"的逻辑。
    • 这就形成了一个无法打破的死锁。
  • 解决方案 :使用 @Lazy 注解。Spring 会注入一个代理对象,实际调用时才去获取真实的 Bean,从而打破构造时的死锁。

Q3: 在循环依赖中,代理对象是什么时候创建的?

代理对象的创建时机取决于是否发生了循环依赖:

  1. 发生循环依赖时
    • 当 Bean A 实例化后,将工厂放入三级缓存。
    • 当 Bean B 依赖 A,从三级缓存获取工厂并调用 getObject() 时。
    • getEarlyBeanReference() 方法中,AnnotationAwareAspectJAutoProxyCreator提前创建 A 的代理对象,并将其放入二级缓存返回给 B。
  2. 未发生循环依赖时
    • 三级缓存的工厂不会被调用。
    • Bean A 正常执行完属性填充、初始化方法后,进入 postProcessAfterInitialization 阶段。
    • 此时,AnnotationAwareAspectJAutoProxyCreator 按照标准流程创建代理对象。

最后:

  1. 痛点:构造器注入会死锁,字段注入能活。
  2. 原理:三级缓存(成品、早期、工厂)。
  3. 核心 :第三级缓存的 ObjectFactory 实现了延迟决策 ,完美解决了AOP 代理提前暴露的时空矛盾。
  4. 实战
    • 能改架构就重构(提取公共类)。
    • 不能改就用 @Lazy(构造器注入神器)。
    • 千万别在 Prototype 或复杂 @Async 场景下硬搞循环依赖。
  5. 验证 :断点调试 doCreateBean,亲眼见证工厂的诞生。
相关推荐
小江的记录本1 小时前
【HashMap】HashMap 系统性知识体系全解(附《HashMap 面试八股文精简版》)
java·前端·后端·容器·面试·hash·哈希
yuuki2332331 小时前
【Linux】Linux基本指令 & 权限全解析
java·linux·服务器
⑩-1 小时前
Kafka 架构和工作原理?Kafka 如何保证高可用?
java·分布式·架构·kafka
大傻^1 小时前
Spring AI 2.0 生产部署指南:从 1.x 迁移、性能调优与云原生实践
人工智能·spring·云原生·springai
indexsunny1 小时前
互联网大厂Java面试实战:从Spring Boot到微服务与Kafka的深度探讨
java·spring boot·junit·kafka·mybatis·hibernate·microservices
Qlittleboy2 小时前
thinkphp如何配置模版缓存,来显著提高页面加载速度
缓存·php
BUG胡汉三2 小时前
Java内网代理访问HTTPS接口SSL证书不匹配
java·https·ssl
洛邙2 小时前
互联网大厂Java求职面试实录:Spring Boot与微服务实战解析
java·spring boot·缓存·微服务·面试·分布式事务·电商
java1234_小锋2 小时前
Java高频面试题:Spring框架中的单例bean是线程安全的吗?
java·数据库·spring