spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖 (下)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖 (下)

6. 增加三级缓存



三级缓存有什么作用呢? 这个问题众说纷纭, 有说代理, 有说AOP. 其实AOP的问题可以用二级缓存来解决. 下面就来看看AOP如何用二级缓存解决.


创建AOP动态代理 (不是耦合的, 采用解耦的, 通过BeanPostProcessor bean的后置处理器来创建). 之前讲过, 如下图


在初始化之后, 调用Bean的后置处理器去创建的AOP的动态代理

1187916-20201106103849714-2078147201.png


如上图. 我们在创建bean 的时候, 会有很多Bean的后置处理器BeanPostProcessor. 如果有AOP, 会在什么时候创建呢? 在初始化以后, 调用BeanPostProcessor创建动态代理.

结合上面的代码, 我们想一想, 其实在初始化以后创建动态代理就晚了. 为什么呢? 因为, 如果有循环依赖, 在初始化之后才调用, 那就不是动态代理. 其实我们这时候应该在实例化之后, 放入到二级缓存之前调用


面试题: 在创建bean的时候, 在哪里创建的动态代理, 这个应该怎么回答呢?

很多人会说在初始化之后, 或者在实例化之后.


其实更严谨的说,有两种情况: 第一种是在初始化之后调用 . 第二种是出现了循环依赖, 会在实例化之后调用


我们上面说的就是第二种情况. 也就是说,正常情况下是在初始化之后调用的, 但是如果有循环依赖, 就要在实例化之后调用了.

 

下面来看看如何在二级缓存加动态代理.

首先, 我们这里有循环依赖, 所以将动态代理放在实例化之后,

/**
     * 获取bean, 根据beanName获取
     */
    public static Object getBean(String beanName) throws Exception {
        // 增加一个出口. 判断实体类是否已经被加载过了
        Object singleton = getSingleton(beanName);
        if (singleton != null) {
            return singleton;
        }
        /**
         * 第一步: 实例化
         * 我们这里是模拟, 采用反射的方式进行实例化. 调用的也是最简单的无参构造函数
         */
        RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);
        Class<?> beanClass = beanDefinition.getBeanClass();
        // 调用无参的构造函数进行实例化
        Object instanceBean = beanClass.newInstance();
        /**
         * 创建AOP动态代理 (不是耦合的, 采用解耦的, 通过BeanPostProcessor bean的后置处理器得来的.  之前讲过,
         * 在初始化之后, 调用Bean的后置处理器去创建的AOP的动态代理 )
         */
        instanceBean = new JdkProxyBeanPostProcessor().getEarlyBeanReference(instanceBean, "instanceA");
        /**
         * 第二步: 放入到二级缓存
         */
        earlySingletonObjects.put(beanName, instanceBean);
        /**
         *  第三步: 属性赋值
         *  instanceA这类类里面有一个属性, InstanceB. 所以, 先拿到 instanceB, 然后在判断属性头上有没有Autowired注解.
         *  注意: 这里我们只是判断有没有Autowired注解. spring中还会判断有没有@Resource注解. @Resource注解还有两种方式, 一种是name, 一种是type
          */
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field declaredField: declaredFields) {
            // 判断每一个属性是否有@Autowired注解
            Autowired annotation = declaredField.getAnnotation(Autowired.class);
            if (annotation != null) {
                // 设置这个属性是可访问的
                declaredField.setAccessible(true);
                // 那么这个时候还要构建这个属性的bean.
                /*
                 * 获取属性的名字
                 * 真实情况, spring这里会判断, 是根据名字, 还是类型, 还是构造函数来获取类.
                 * 我们这里模拟, 所以简单一些, 直接根据名字获取.
                 */
                String name = declaredField.getName();
                /**
                 * 这样, 在这里我们就拿到了 instanceB 的 bean
                 */
                Object fileObject = getBean(name);
                // 为属性设置类型
                declaredField.set(instanceBean, fileObject);
            }
        }
        /**
         * 第四步: 初始化
         * 初始化就是设置类的init-method.这个可以设置也可以不设置. 我们这里就不设置了
         */
      //   正常动态代理创建的时机
        /**
         * 第五步: 放入到一级缓存
         */
        singletonObjects.put(beanName, instanceBean);
        return instanceBean;
    }

这里只是简单模拟了动态代理.


我们知道动态代理有两个地方. 如果是普通类动态代理在初始化之后执行, 如果是循环依赖, 那么动态代理是在实例化之后.

 

上面在实例化之后创建proxy的代码不完整, 为什么不完整呢, 因为没有判断是否是循环依赖.

 

我们简单模拟一个动态代理的实现.


public class JdkProxyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {
    /**
     * 假设A被切点命中 需要创建代理  @PointCut("execution(* *..InstanceA.*(..))")
     * @param bean the raw bean instance
     * @param beanName the name of the bean
     * @return
     * @throws BeansException
     */
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        // 假设A被切点命中 需要创建代理  @PointCut("execution(* *..InstanceA.*(..))")
        /**
         * 这里, 我们简单直接判断bean是不是InstanceA实例, 如果是, 就创建动态代理.
         * 这里没有去解析切点, 解析切点是AspectJ做的事.
         */
        if (bean instanceof InstanceA) {
            JdkDynimcProxy jdkDynimcProxy = new JdkDynimcProxy(bean);
            return jdkDynimcProxy.getProxy();
        }
        return bean;
    }
}

这里直接判断, 如果bean是InstanceA的实例, 那么就调用bean的动态代理.  动态代理的简单逻辑就是: 解析切面, 然后创建类, 如果类不存在就新增, 如果存在则不在创建, 直接取出来返回.

 

在来看看动态代理,放在实例化之后. 创建AOP, 但是, 在这里创建AOP动态代理的条件是循环依赖.


问题1: 那么如何判断是循环依赖呢?

二级缓存中bean不是null.

如果一个类在创建的过程中, 会放入到二级缓存, 如果完全创建完了, 会放入到一级缓存, 然后删除二级缓存. 所以, 如果二级缓存中的bean只要存在, 就说明这个类是创建中, 出现了循环依赖.


问题2: 什么时候判断呢?

应该在getSingleton()判断是否是循环依赖的时候判断. 因为这时候我们刚好判断了二级缓存中bean是否为空.


/**
     * 判断是否是循环引用的出口.
     * @param beanName
     * @return
     */
    private static Object getSingleton(String beanName) {
        // 先去一级缓存里拿,如果一级缓存没有拿到,去二级缓存里拿
        if (singletonObjects.containsKey(beanName)) {
            return singletonObjects.get(beanName);
        } else if (earlySingletonObjects.containsKey(beanName)){
            /**
             * 第一次创建bean是正常的instanceBean. 他并不是循环依赖. 第二次进来判断, 这个bean已经存在了, 就说明是循环依赖了
             * 这时候通过动态代理创建bean. 然后将这个bean在放入到二级缓存中覆盖原来的instanceBean.
             */
            Object obj = new JdkProxyBeanPostProcessor()
                    .getEarlyBeanReference(earlySingletonObjects.get(beanName), beanName);
            earlySingletonObjects.put(beanName, obj);
            return earlySingletonObjects.get(beanName);
        } else {
            return null;
        }
    }

这样我们在循环依赖的时候就完成了AOP的创建. 这是在二级缓存里创建的AOP,

问题3: 那这是不是说就不需要三级缓存了呢?

那么,来找问题.  这里有两个问题:

问题1: 我们发现在创建动态代理的时候, 我们使用的bean的后置处理器JdkProxyBeanPostProcessor.这有点不太符合规则,

     因为, spring在getBean()的时候并没有使用Bean的后置处理器, 而是在createBean()的时候才去使用的bean的后置处理器.

问题2: 如果A是AOP, 他一直都是, 最开始创建的时候也应该是. 使用这种方法, 结果是第一次创建出来的bean不是AOP动态代理.

 

对于第一个问题: 我们希望在实例化的时候创建AOP, 但是具体判断是在getSingleton()方法里判断. 这里通过三级缓存来实现. 三级缓存里面放的是一个接口定义的钩子方法. 方法的执行在后面调用的时候执行.

 

对于第二个问题: 我们的二级缓存就不能直接保存instanceBean实例了, 增加一个参数, 用来标记当前这个类是一个正在创建中的类. 这样来判断循环依赖.

 

下面先来看看创建的三个缓存和一个标识


// 一级缓存
    private static Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
    // 二级缓存: 为了将成熟的bean和纯净的bean分离. 避免读取到不完整的bean.
    private static Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
    // 三级缓存:
    private static Map<String, ObjectFactory> singletonFactories = new ConcurrentHashMap<>();
    // 循环依赖的标识---当前正在创建的实例bean
    private static Set<String> singletonsCurrectlyInCreation = new HashSet<>();

然后在来看看循环依赖的出口

/**
     * 判断是否是循环引用的出口.
     * @param beanName
     * @return
     */
    private static Object getSingleton(String beanName) {
        //先去一级缓存里拿
        Object bean = singletonObjects.get(beanName);
        // 一级缓存中没有, 但是正在创建的bean标识中有, 说明是循环依赖
        if (bean == null && singletonsCurrectlyInCreation.contains(beanName)) {
            bean = earlySingletonObjects.get(beanName);
            // 如果二级缓存中没有, 就从三级缓存中拿
            if (bean == null) {
                // 从三级缓存中取
                ObjectFactory objectFactory = singletonFactories.get(beanName);
                if (objectFactory != null) {
                    // 这里是真正创建动态代理的地方.
                    Object obj = objectFactory.getObject();
                    // 然后将其放入到二级缓存中. 因为如果有多次依赖, 就去二级缓存中判断. 已经有了就不在再次创建了
                    earlySingletonObjects.put(beanName, obj);
                }
            }
        }
        return bean;
    }

这里的逻辑是, 先去一级缓存中拿, 一级缓存放的是成熟的bean, 也就是他已经完成了属性赋值和初始化. 如果一级缓存没有, 而正在创建中的类标识是true, 就说明这个类正在创建中, 这是一个循环依赖. 这个时候就去二级缓存中取数据, 二级缓存中的数据是何时放进去的呢, 是后面从三级缓存中创建动态代理后放进去的. 如果二级缓存为空, 说明没有创建过动态代理, 这时候在去三级缓存中拿, 然后创建动态代理. 创建完以后放入二级缓存中, 后面就不用再创建.

 

完成的代码如下:

package com.lxl.www.circulardependencies;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class MainStart {
    private static Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
    // 一级缓存
    private static Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
    // 二级缓存: 为了将成熟的bean和纯净的bean分离. 避免读取到不完整的bean.
    private static Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
    // 三级缓存:
    private static Map<String, ObjectFactory> singletonFactories = new ConcurrentHashMap<>();
    // 循环依赖的标识---当前正在创建的实例bean
    private static Set<String> singletonsCurrectlyInCreation = new HashSet<>();
    /**
     * 读取bean定义, 当然在spring中肯定是根据配置 动态扫描注册的
     *
     * InstanceA和InstanceB都有注解@Component, 所以, 在spring扫描读取配置类的时候, 会把他们两个扫描到BeanDefinitionMap中.
     * 这里, 我们省略这一步, 直接将instanceA和instanceB放到BeanDefinitionMap中.
     */
    public static void loadBeanDefinitions(){
        RootBeanDefinition aBeanDefinition = new RootBeanDefinition(InstanceA.class);
        RootBeanDefinition bBeanDefinition = new RootBeanDefinition(InstanceB.class);
        beanDefinitionMap.put("instanceA", aBeanDefinition);
        beanDefinitionMap.put("instanceB", bBeanDefinition);
    }
    public static void main(String[] args) throws Exception {
        // 第一步: 扫描配置类, 读取bean定义
        loadBeanDefinitions();
        // 第二步: 循环创建bean
        for (String key: beanDefinitionMap.keySet()) {
            // 第一次: key是instanceA, 所以先创建A类
            getBean(key);
        }
        // 测试: 看是否能执行成功
        InstanceA instanceA = (InstanceA) getBean("instanceA");
        instanceA.say();
    }
    /**
     * 获取bean, 根据beanName获取
     */
    public static Object getBean(String beanName) throws Exception {
        // 增加一个出口. 判断实体类是否已经被加载过了
        Object singleton = getSingleton(beanName);
        if (singleton != null) {
            return singleton;
        }
        // 标记bean正在创建
        if (!singletonsCurrectlyInCreation.contains(beanName)) {
            singletonsCurrectlyInCreation.add(beanName);
        }
        /**
         * 第一步: 实例化
         * 我们这里是模拟, 采用反射的方式进行实例化. 调用的也是最简单的无参构造函数
         */
        RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);
        Class<?> beanClass = beanDefinition.getBeanClass();
        // 调用无参的构造函数进行实例化
        Object instanceBean = beanClass.newInstance();
        /**
         * 第二步: 放入到三级缓存
         * 每一次createBean都会将其放入到三级缓存中. getObject是一个钩子方法. 在这里不会被调用.
         * 什么时候被调用呢?
         * 在getSingleton()从三级缓存中取数据, 调用创建动态代理的时候
         */
        singletonFactories.put(beanName, new ObjectFactory() {
            @Override
            public Object getObject() throws BeansException {
                return new JdkProxyBeanPostProcessor().getEarlyBeanReference(earlySingletonObjects.get(beanName), beanName);
            }
        });
        //earlySingletonObjects.put(beanName, instanceBean);
        /**
         *  第三步: 属性赋值
         *  instanceA这类类里面有一个属性, InstanceB. 所以, 先拿到 instanceB, 然后在判断属性头上有没有Autowired注解.
         *  注意: 这里我们只是判断有没有Autowired注解. spring中还会判断有没有@Resource注解. @Resource注解还有两种方式, 一种是name, 一种是type
         */
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field declaredField: declaredFields) {
            // 判断每一个属性是否有@Autowired注解
            Autowired annotation = declaredField.getAnnotation(Autowired.class);
            if (annotation != null) {
                // 设置这个属性是可访问的
                declaredField.setAccessible(true);
                // 那么这个时候还要构建这个属性的bean.
                /*
                 * 获取属性的名字
                 * 真实情况, spring这里会判断, 是根据名字, 还是类型, 还是构造函数来获取类.
                 * 我们这里模拟, 所以简单一些, 直接根据名字获取.
                 */
                String name = declaredField.getName();
                /**
                 * 这样, 在这里我们就拿到了 instanceB 的 bean
                 */
                Object fileObject = getBean(name);
                // 为属性设置类型
                declaredField.set(instanceBean, fileObject);
            }
        }
        /**
         * 第四步: 初始化
         * 初始化就是设置类的init-method.这个可以设置也可以不设置. 我们这里就不设置了
         */
        /**
         * 第五步: 放入到一级缓存
         *
         * 在这里二级缓存存的是动态代理, 那么一级缓存肯定也要存动态代理的实例.
         * 从二级缓存中取出实例, 放入到一级缓存中
         */
        if (earlySingletonObjects.containsKey(beanName)) {
            instanceBean = earlySingletonObjects.get(beanName);
        }
        singletonObjects.put(beanName, instanceBean);
        return instanceBean;
    }
    /**
     * 判断是否是循环引用的出口.
     * @param beanName
     * @return
     */
    private static Object getSingleton(String beanName) {
        //先去一级缓存里拿,
        Object bean = singletonObjects.get(beanName);
        // 一级缓存中没有, 但是正在创建的bean标识中有, 说明是循环依赖
        if (bean == null && singletonsCurrectlyInCreation.contains(beanName)) {
            bean = earlySingletonObjects.get(beanName);
            // 如果二级缓存中没有, 就从三级缓存中拿
            if (bean == null) {
                // 从三级缓存中取
                ObjectFactory objectFactory = singletonFactories.get(beanName);
                if (objectFactory != null) {
                    // 这里是真正创建动态代理的地方.
                    Object obj = objectFactory.getObject();
                    // 然后将其放入到二级缓存中. 因为如果有多次依赖, 就去二级缓存中判断. 已经有了就不在再次创建了
                    earlySingletonObjects.put(beanName, obj);
                }
            }
        }
        return bean;
    }
}

1187916-20201108073933562-1940526219.png


下面就我们的代码分析一下:


第一种情况: 没有循环依赖

第二种情况: 有循环依赖

第三种情况: 有多次循环依赖

我们模拟一个循环依赖的场景, 覆盖这三种情况.

1187916-20201108074451802-402797875.png

用代码表示

类A

package com.lxl.www.circulardependencies;
import org.springframework.beans.factory.annotation.Autowired;
public class A {
    @Autowired
    private B b;
    @Autowired
    private C c;
}

类B

package com.lxl.www.circulardependencies;
import org.springframework.beans.factory.annotation.Autowired;
public class B {
    @Autowired
    private A a;
    @Autowired
    private B b;
}

类C

package com.lxl.www.circulardependencies;
import org.springframework.beans.factory.annotation.Autowired;
public class C {
    @Autowired
    private A a;
}


其中类A刚好匹配AOP的切面@PointCut("execution(* *..A.*(..))")

 

下面分析他们的循环依赖关系.

此时beanDefinitionMap中有三个bean定义. 分别是A, B, C

1. 先解析类A, 根据上面的流程.

  1) 首先调用getSingleton, 此时一级缓存, 二级缓存都没有, 正在创建标志也是null. 所以, 返回的是null

  2) 标记当前类正在创建中

  3) 实例化

  4) 将A放入到三级缓存, 并定义动态代理的钩子方法

  5) 属性赋值. A有两个属性, 分别是B和C. 都带有@Autowired注解, 先解析B.

  6) A暂停, 解析B

2. 解析A类的属性类B

  1) 首先调用getSingleton, 此时一级缓存, 二级缓存都没有, 正在创建标志也是null. 所以, 返回的是null  

  2) 标记当前类正在创建中

  3) 实例化

  4) 将B放入到三级缓存, 并定义动态代理的钩子方法

  5) 属性赋值. B有两个属性, 分别是A和C. 都带有@Autowired注解, 先解析A. 在解析C

  6) B暂停, 解析A

3. 解析B类的属性A 

  1) 首先调用getSingleton, 此时一级缓存中这个属性为null, 正在创建中标志位true, 二级缓存为空, 从三级缓存中创建动态代理, 然后判断是否符合动态代理切面要求, A符合. 所以通过动态代理创建A的代理bean放入到二级缓存.返回实例bean.


  2) A此时已经存在了, 所以, 直接返回

4. 解析B类的属性C

  1) 首先调用getSingleton, 此时一级缓存, 二级缓存都没有, 正在创建标志也是null. 所以, 返回的是null  

  2) 标记当前类C正在创建中

  3) 实例化

  4) 将C放入到三级缓存, 并定义动态代理的钩子方法

  5) 属性赋值. C有一个属性, 是A. 带有@Autowired注解, 先解析A

  6) C暂停, 解析A

5. 解析C中的属性A

  1) 首先调用getSingleton()方法, 此时一级缓存中没有, 标志位为true, 二级缓存中已经有A的动态代理实例了, 所以,直接返回.

  2) A此时已经在存在, 直接返回

6. 继续解析B类的属性C

  1) 接着第4步往下走

  2) 初始化类C

  3) 将类C放入到一级缓存中. 放之前去二级缓存中取, 二级缓存中没有. 所以, 这里存的是C通过反射构建的instanceBean

7. 继续解析A类的属性类B

  1) 接着第2步往下走

  2) 初始化类B

  3) 将类B放入到一级缓存中. 放之前去二级缓存中取.二级缓存中没有, 所以, 这里存的是B通过反射构建的instanceBean

  4) 构建结束,返回

8. 解析A类的属性类C

  1) 首先调用getSingleton()方法, 此时一级缓存中已经有了类C, 所以直接返回

9. 继续解析A类

  1) 接着第1步往下走

  2) 初始化类A

  3) 将A放入到一级缓存中. 放之前判断二级缓存中有没有实例bean, 我们发现有, 所以, 取出来放入到A的一级缓存中.

  4) 构建bean结束, 返回

10. 接下来构建beanDefinitionMap中的类B

  1) 首先调用getSingleton()方法, 此时一级缓存中已经有了类B, 所以直接返回

11. 接下来构建beanDefinitionMap中的类C

  1) 首先调用getSingleton()方法, 此时一级缓存中已经有了类C, 所以直接返回

至此整个构建过程结束.

 

总结:



再来感受一下三级缓存的作用:

一级缓存: 用来存放成熟的bean. 这个bean如果是切入点, 则是一个动态代理的bean,如果不是切入点, 则是一个普通的类

二级缓存: 用来存放循环依赖过程中创建的动态代理bean.

三级缓存: 用来存放动态代理的钩子方法. 用来在需要构建动态代理类的时候使用.


相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
15 2
|
19天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
9天前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
35 9
|
14天前
|
缓存 监控 Java
|
1月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
404 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
162 2
|
3月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
8天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
20 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
5天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
15 2