spring 源码解析之 createBean

前言

经过了前一章的分析(spring 源码解析之 doGetBean)),我们是对获取 bean实例有了大致的理解,上篇主要说当Object sharedInstance = getSingleton(beanName)获取 bean 实例不为空时,如何从工厂获取最终的实例 bean。以及说了一下当 bean 不存在,bean 创建前做了一些准备,如获取父级BeanFactory:getParentBeanFactory、合并beangetMergedLocalBeanDefinition、以及 bean 依赖的处理mbd.getDependsOn()。然后本篇文章才是重头戏,当 bean 缓存不存在的时候,也就是还没创建时,bean 是如何创建的。接下来开卷!

源码解析

getSingleton

java 复制代码
 public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
       // 获取单例 bean
       Object singletonObject = this.singletonObjects.get(beanName);
       // bean 实例为空,则创建单例bean
       if (singletonObject == null) {
          // 判断,如果当前beanFactory正在被销毁则直接抛出异常,不允许创建单例bean
          if (this.singletonsCurrentlyInDestruction) {
             throw new BeanCreationNotAllowedException(beanName,
                   "Singleton bean creation not allowed while singletons of this factory are in destruction " +
                   "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
          }
          if (logger.isDebugEnabled()) {
             logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
          }
          // 做一些bean创建前的准备工作: 记录beanName 正在加载的状态(添加到 singletonsCurrentlyInCreation 缓存中),
          // 若bean已经正在加载,则抛出异常。为了解决循环引用的问题
          beforeSingletonCreation(beanName);
          boolean newSingleton = false;
          boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
          if (recordSuppressedExceptions) {
             this.suppressedExceptions = new LinkedHashSet<>();
          }
          try {
             singletonObject = singletonFactory.getObject();
             newSingleton = true;
          }
          catch (IllegalStateException ex) {
             // Has the singleton object implicitly appeared in the meantime ->
             // if yes, proceed with it since the exception indicates that state.
             singletonObject = this.singletonObjects.get(beanName);
             if (singletonObject == null) {
                throw ex;
             }
          }
          catch (BeanCreationException ex) {
             if (recordSuppressedExceptions) {
                for (Exception suppressedException : this.suppressedExceptions) {
                   ex.addRelatedCause(suppressedException);
                }
             }
             throw ex;
          }
          finally {
             if (recordSuppressedExceptions) {
                this.suppressedExceptions = null;
             }
             // 从singletonsCurrentlyInCreation删除正在记录创建中的beanName
             afterSingletonCreation(beanName);
          }
          // 添加到singletonObjects缓存中 从二级缓存 三级缓存中移除
          if (newSingleton) {
             addSingleton(beanName, singletonObject);
          }
       }
       return singletonObject;
    }
 }
  • this.singletonObjects.get(beanName) :再次尝试从缓存中获取bean,若获取到,就大吉大利直接返回。若为空,则需要创建 bean。
  • if (this.singletonsCurrentlyInDestruction) :未获取到检测bena是否正在销毁,若是则抛出异常
  • beforeSingletonCreation 方法中记录bean正在创建的状态将beanName添加到singletonsCurrentlyInCreation集合中)。在循环依赖时可根据此判断。
  • singletonObject = singletonFactory.getObject() :调用 ObjectFactory.getObject() 方法来实例化bean
  • afterSingletonCreation 方法移除bean正在夹杂的状态
  • addSingleton(beanName, singletonObject): 对各种缓存状态做处理。

beforeSingletonCreation

这个方法并非一个空方法,重点是!this.singletonsCurrentlyInCreation.add(beanName), 记录beanName 正在加载的状态(添加到 singletonsCurrentlyInCreation 缓存中)

java 复制代码
 protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
       throw new BeanCurrentlyInCreationException(beanName);
    }
 }

创建bean - createBean概述

这个方法这行代码才是重头:singletonObject = singletonFactory.getObject();这一行代码实则就是调用doGetBean下的 createBean

createBean是根据给定的bean名称(beanName)和根bean定义(RootBeanDefinition对象mbd)创建并初始化一个Bean实例,上面一系列的操作都是创建bean 做准备工作的。现在开始真正来创建 bean 了,下面看源码

java 复制代码
 protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
       throws BeanCreationException {
 ​
    if (logger.isTraceEnabled()) {
       logger.trace("Creating instance of bean '" + beanName + "'");
    }
    RootBeanDefinition mbdToUse = mbd;
 ​
    // Make sure bean class is actually resolved at this point, and
    // clone the bean definition in case of a dynamically resolved Class
    // which cannot be stored in the shared merged bean definition.
    // 1. 锁定class, 根据mdb和 beanName 解析出来 bean的class类型
    Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
    if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
       mbdToUse = new RootBeanDefinition(mbd);
       mbdToUse.setBeanClass(resolvedClass);
    }
 ​
    // Prepare method overrides.
    try {
       // 此方法的主要作用是验证和准备Bean的方法重写。
       // 这个方法会遍历Bean的所有方法,如果一个方法只被重载了一次,那么就设置overloaded为false,以避免参数类型的检查。因为这个检查很耗性能
       // 这样在后续调用的时候便可以直接使用找到的方法,而不需要进行方法的参数匹配了。此外,这个方法还可以提前对方法存在性进行验证。
       // 为了优化方法的调用,提高程序的运行效率。
       mbdToUse.prepareMethodOverrides();
    }
    catch (BeanDefinitionValidationException ex) {
       throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),
             beanName, "Validation of method overrides failed", ex);
    }
 ​
    try {
       // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
       // 调用BeanProcessors的方法来替代真正的实例
       // 给BeanPostProcessors一个返回代理而不是目标bean实例的机会
       // 如果处理器返回了代理,那么就直接返回代理,不再进行后续的初始化操作。
       // 这个方法可以让用户使用处理器来创建 bean 实例,不通过 spring 来创建 bean 实例。
       //InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation : 在bean初始化前调用
       //BeanPostProcessor#postProcessAfterInitialization : 在bean初始化后调用
       Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
       if (bean != null) {
          return bean;
       }
    }
    catch (Throwable ex) {
       throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
             "BeanPostProcessor before instantiation of bean failed", ex);
    }
 ​
    try {
       Object beanInstance = doCreateBean(beanName, mbdToUse, args);
       if (logger.isTraceEnabled()) {
          logger.trace("Finished creating instance of bean '" + beanName + "'");
       }
       return beanInstance;
    }
    catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
       // A previously detected exception with proper bean creation context already,
       // or illegal singleton state to be communicated up to DefaultSingletonBeanRegistry.
       throw ex;
    }
    catch (Throwable ex) {
       throw new BeanCreationException(
             mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);
    }
 }

可以看到,createBean 的整体流程大致如下:

  1. 解析并锁定Bean Class :确保在这一阶段解析出实际的Bean类,并在需要时克隆mbd以存储动态解析得到的Class(由于某些原因无法存储在共享合并的bean定义中)。具体做法是调用resolveBeanClass(mbd, beanName)方法。

    • resolveBeanClass(mbd, beanName)方法用于根据给定的RootBeanDefinition (mbd) 和 beanName 来获取实际的Bean类(即Java Class对象)
    • 判断条件resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null表示:
      • resolvedClass != null: 确保已经成功解析出一个Class对象。
      • !mbd.hasBeanClass(): 检查当前的RootBeanDefinition是否已经包含了一个已知的Class引用(例如,它可能是从XML或注解配置中读取的)。
      • mbd.getBeanClassName() != null: 确认RootBeanDefinition中包含了可用于查找类名的字符串信息。
      • 如果满足这些条件,则创建一个新的RootBeanDefinition实例(mbdToUse),将原始mbd的其他属性复制过来,并使用刚解析得到的Class对象(resolvedClass)来设置新的Bean定义的Class属性
    • 这样做的原因在于,原始的RootBeanDefinition可能存储的是一个类名字符串而不是Class对象本身,而在后续处理过程中,有时需要直接操作Class对象进行更高效的操作,比如反射调用、生成代理类等。
  2. 准备方法重写 :通过调用mbdToUse.prepareMethodOverrides()来验证和准备Bean的方法重写。这个过程会遍历Bean的所有方法,优化后续对方法的调用,提高运行效率,同时提前进行方法存在性验证。

  3. 应用BeanPostProcessor :在真正实例化Bean之前,提供一个机会让注册的InstantiationAwareBeanPostProcessorBeanPostProcessor通过resolveBeforeInstantiation(beanName, mbdToUse)方法返回代理对象替代目标Bean实例。如果处理器返回了非空代理对象,则直接返回该代理对象,不再继续执行后续的Bean初始化流程。

  4. 创建并初始化Bean实例 :如果没有BeanPostProcessor返回代理对象或者返回的对象为空,函数将调用doCreateBean(beanName, mbdToUse, args)方法来实际地创建并初始化Bean实例。此方法内部包含了依赖注入、属性填充以及调用初始化方法等一系列操作。

  5. 异常处理 :在整个创建过程中,函数会捕获并处理可能出现的任何异常,包括但不限于BeanCreationExceptionImplicitlyAppearedSingletonException。若出现异常,函数会抛出相应的异常信息,其中包含有关异常发生的上下文,以便于调试定位问题。 最终,当Bean实例成功创建并初始化完成后,函数返回该Bean实例。

prepareMethodOverrides

这里我又理解了好久,主要一开始看到这里说的方法重写就想到了方法重载与重写的那个,其实这里所说的是不一样的。

这里主要是理解lookup-methodreplaced-method 两个属性与@Override这玩意是不一样的。

  • lookup-method:这种注入方式主要用于注入方法返回结果,也就是说能通过配置方式替换方法返回结果。例如,如果有一个单例模式的bean引用了一个原型模式的bean,我们不希望被引用的原型模式bean被缓存,那么这个时候就需要用到lookup-method注入。
  • replace-method:这种注入方式可以实现方法主体或返回结果的替换。例如,如果你想改变某个方法的实现,你可以使用replace-method注入。
  • @Override:在Java中,当你想要修改继承自父类的某个方法时,可以在子类中重写这个方法。重写的方法必须与父类中的方法有相同的名称、参数列表和返回类型。使用@Override注解可以帮助编译器检查你的方法是否正确地重写了父类的方法。
  • lookup-method和replace-method:这两种方式是Spring框架提供的,用于在运行时动态地改变bean的行为,不知道大家是否记得上一篇(spring 源码解析之 doGetBean)中的AbstractBeanFactory#getObjectFromFactoryBean这个方法,这里就有提到过我们在缓存中获取到的 bean 都是初始状态的,并非最终状态,我们真正需要的是工厂 bean 中定义的 factory-method 方法中返回的 bean,而getObjectForBeanInstance就是完成这个工作的在前面 bean。

总的来说,@Override是在编译时确定的,它要求子类的方法必须与父类的方法完全相同。而Spring的lookup-method和replace-method是在运行时确定的,它们可以动态地改变方法的行为。

看一下源码

java 复制代码
 public void prepareMethodOverrides() throws BeanDefinitionValidationException {
    // Check that lookup methods exist and determine their overloaded status.
    // 判断是否有方法需要重写
    if (hasMethodOverrides()) {
       getMethodOverrides().getOverrides().forEach(this::prepareMethodOverride);
    }
 }
 protected void prepareMethodOverride(MethodOverride mo) throws BeanDefinitionValidationException {
    // 获取对应的类中的对应方法名的个数
    int count = ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName());
    // 等于0抛出异常。上面已经验证有方法需要覆盖,这里为0肯定错误
    if (count == 0) {
       throw new BeanDefinitionValidationException(
             "Invalid method override: no method with name '" + mo.getMethodName() +
             "' on class [" + getBeanClassName() + "]");
    }
    else if (count == 1) {
       // 标记 MethodOverride 暂未被覆盖,避免参数类型检查的开销
       // Mark override as not overloaded, to avoid the overhead of arg type checking.
       mo.setOverloaded(false);
    }
 }

在Spring配置中存在lookup-method和replace-method时,这两个配置在加载xml的时候就会统一存放在BeanDefinition中的methodOverrides属性里。当创建bean的时候,AbstractAutowireCapableBeanFactory类会调用prepareMethodOverride方法来准备方法的覆盖。

知道上面这些概念与MethodOverrides这个属性的来历就很简单了,如果没有需要覆盖的,就不做处理,如果有,则判断是否超过 1 个,等于 1 个就

mo.setOverloaded(false);这个方法主要就是验证并准备给定的方法覆盖。为了之后创建 bean 时,是否需要检测参数。因为在创建 bean 的时候,一个方法可能会有多种入参方式,而最终在实例化的时候就要确定使用哪个构造函数,这个检测就很耗性能,所以做一个标记位

resolveBeforeInstantiation

该方法主要是调用 InstantiationAwareBeanPostProcessor 来进行一些处理,这里实际上是给了用户一次代替Spring来创建bean的机会,代码实现上非常简单直接调用的后处理器方法,就是有几个概念这里要提一下的。

java 复制代码
 protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
    Object bean = null;
    if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
       // Make sure bean class is actually resolved at this point.
       // 当前类并非合成类 && 存在 BeanPostProcessor (后处理器)
       if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
          // 1. 获取目标类
          Class<?> targetType = determineTargetType(beanName, mbd);
          if (targetType != null) {
             // 2. 先用实例前的后处理器获取 bean
             bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
             if (bean != null) {
                // 3. 前置没有则再使用后处理器应用
                bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
             }
          }
       }
       mbd.beforeInstantiationResolved = (bean != null);
    }
    return bean;
 }

什么是合成类?什么是非合成类?为什么合成类不能使用BeanPostProcessor进行预处理?

  • 合成的含义是:这些构造体是由Java编译器引入的,并且在源代码中没有相应的构造体。这些合成构造体通常是编译器为了实现某些特性(如内部类访问外部类的私有成员)而生成的。例如,当AOP需要创建一个代理来拦截某个类的方法调用时,它可能会生成一个新的类(代理类),这个类在源代码中并不存在,因此它是合成的。这个代理类会包含一些额外的逻辑,比如在调用目标方法前后插入切面的代码。
  • 而非合成的则直接来自源代码
  • InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation()方法是在Bean实例化之前被调用的,它的主要目的是允许我们在Bean实例化之前进行一些自定义的处理。但是,对于合成的类,由于它们是由编译器生成的,因此我们可能无法在Bean实例化之前对它们进行有效的处理(这个属于处理器的知识,后面会讲)。

doCreateBean

见名知意,这是一个创建 bean 的方法,直接下源码

java 复制代码
 protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
       throws BeanCreationException {
 ​
    // Instantiate the bean.
    BeanWrapper instanceWrapper = null;
    // 如果是单例,先从缓存中移除
    if (mbd.isSingleton()) {
       instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    // 创建 bean 实例,并使用BeanWrapper包装器类来封装这个实例
    if (instanceWrapper == null) {
       instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    // 获取 bean 实例
    Object bean = instanceWrapper.getWrappedInstance();
    // 获取 bean 的类型
    Class<?> beanType = instanceWrapper.getWrappedClass();
    if (beanType != NullBean.class) {
       // 设置目标解析类型
       mbd.resolvedTargetType = beanType;
    }
 ​
    // Allow post-processors to modify the merged bean definition.
    // 对合并后的bean定义进行同步锁定,确保其仅被后处理器修改一次
    synchronized (mbd.postProcessingLock) {
       // 未经过后置处理器处理
       if (!mbd.postProcessed) {
          try {
             // 应用合并bean定义的后处理器,允许对bean的定义进行扩展或修改
             applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
          }
          catch (Throwable ex) {
             throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                   "Post-processing of merged bean definition failed", ex);
          }
          mbd.markAsPostProcessed();
       }
    }
 ​
    // Eagerly cache singletons to be able to resolve circular references
    // even when triggered by lifecycle interfaces like BeanFactoryAware.
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
          isSingletonCurrentlyInCreation(beanName));
    // 如果bean是单例且允许循环引用,在创建过程中就将bean放入单例缓存中,以解决可能存在的循环依赖问题
    if (earlySingletonExposure) {
       if (logger.isTraceEnabled()) {
          logger.trace("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
       }
       addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
 ​
    // Initialize the bean instance.
    // 暴露在缓存中的 bean 实例
    Object exposedObject = bean;
    try {
       // 注入属性
       populateBean(beanName, mbd, instanceWrapper);
       // 初始化bean实例
       exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
       if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) {
          throw bce;
       }
       else {
          throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex);
       }
    }
 ​
    // 处理早期单例曝光后的特殊情况
    if (earlySingletonExposure) {
       // 从缓存获取 bean 实例,此时缓存中的 bean 实例已经完成了初始化
       Object earlySingletonReference = getSingleton(beanName, false);
       if (earlySingletonReference != null) {
          // 如果暴露的 bean 实例与一开始的原始 bean 实例相同,那么将 exposeObject 替换为 从缓存中获取已经实例化完成的 bean
          if (exposedObject == bean) {
             exposedObject = earlySingletonReference;
          }
          // allowRawInjectionDespiteWrapping:允许在包装后仍然注入原始类型
          // hasDependentBean:有依赖当前 bean 的其他 bean
          else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
             // 获取依赖当前 bean 的其他 bean
             String[] dependentBeans = getDependentBeans(beanName);
             Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
             for (String dependentBean : dependentBeans) {
                // 移除给定bean名称的singleton实例(如果有的话),但前提是它没有用于类型检查以外的其他目的。
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                   // 能进来 则证明当前bean 的依赖 bean 还没有创建完成,还是使用的原始的 bean
                   actualDependentBeans.add(dependentBean);
                }
             }
             // 如果actualDependentBeans不为空,则证明当前 bean 还有依赖 bean 还没有创建完成,那么抛出异常
             if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException(beanName,
                      "Bean with name '" + beanName + "' has been injected into other beans [" +
                      StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                      "] in its raw version as part of a circular reference, but has eventually been " +
                      "wrapped. This means that said other beans do not use the final version of the " +
                      "bean. This is often the result of over-eager type matching - consider using " +
                      "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
             }
          }
       }
    }
 ​
    // Register bean as disposable.
    try {
       // 根据Scopse 注册bean
       registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    catch (BeanDefinitionValidationException ex) {
       throw new BeanCreationException(
             mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    }
 ​
    return exposedObject;
 }
 ​
 ============================
   
 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);
     }
   }
 }
  1. 处理单例缓存 :首先检查当前要创建的bean是否为单例模式(singleton)。如果是单例,尝试从factoryBeanInstanceCache中移除已存在的bean实例。

  2. 创建bean实例 :如果缓存中没有找到bean实例,则调用createBeanInstance方法创建新的bean实例,并使用BeanWrapper包装器类来封装这个实例,以便后续进行属性设置和访问。重点、很难,后面会说

  3. 类型判断与合并后的bean定义后处理获取bean的实际类型,并更新合并后的bean定义(RootBeanDefinition)中的目标类型信息。对合并后的bean定义进行同步锁定,确保其仅被后处理器修改一次。然后应用合并bean定义的后处理器,允许对bean的定义进行扩展或修改

  4. 早期单例曝光 :如果bean是单例且允许循环引用,在创建过程中就将bean放入单例缓存中,以解决可能存在的循环依赖问题。通过添加一个工厂方法到单例缓存中,该方法会在需要时返回bean的早期引用。在上一篇(spring 源码解析之 doGetBean)就有提到三级缓存相关的,这里就是保存至singletonFactories的地方

  5. 初始化bean实例 :使用populateBean方法完成bean属性的注入,即根据bean定义中的属性配置设置bean的字段值。调用initializeBean方法对bean执行初始化逻辑,包括自定义初始化方法的调用等。 重点、很难,后面会说

  6. 处理早期单例曝光后的特殊情况 :在bean已经初始化后,再次检查早期曝光的单例情况,处理可能出现的因为bean实际已经被包裹而引起的原始类型的循环引用问题。

    • 假设存在如下情况: BeanA和BeanB是两个单例bean,并且互相引用
    • 在创建BeanA的过程中,由于检测到BeanB还在创建中(处于"currently in creation"状态),因此Spring会将BeanB的一个早期引用(可能是原始类型代理对象)放入singleton缓存中,以解决循环引用问题。
    • 然后继续完成BeanA的实例化与初始化,接着开始BeanB的实例化过程。
    • 当BeanB最终被实例化并初始化完成后,此时需要确保所有依赖于BeanB的bean都使用的是最终初始化完成后的BeanB实例,而不是早期曝光的那个可能为代理或者不完整的实例
    • 所以在doCreateBean方法的末尾部分,Spring会再次检查这些早期曝光的单例bean。如果发现有其他bean在依赖于当前bean(如BeanB)的原始类型时,而实际上已经用到了早期曝光的代理对象,则直接 抛异常。
    • 这里的关键在于,在处理循环引用时,Spring必须确保无论何时从缓存中获取bean,都是其完整初始化过的版本,而不是为了打破循环引用而临时插入的早期引用对象。
  7. 注册可销毁bean:最后,如果bean定义中指定了销毁方法,则调用

至此,下一篇将详细解析 doCreateBean,到底是怎么一步步创建bean 的

相关推荐
Q_192849990617 分钟前
基于Spring Boot的个人健康管理系统
java·spring boot·后端
Q_192849990624 分钟前
基于Springcloud的智能社区服务系统
后端·spring·spring cloud
m0_748245171 小时前
Web第一次作业
java
小码的头发丝、1 小时前
Java进阶学习笔记|面向对象
java·笔记·学习
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
坊钰2 小时前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
chenziang12 小时前
leetcode hot100 LRU缓存
java·开发语言
会说法语的猪2 小时前
springboot实现图片上传、下载功能
java·spring boot·后端
码农老起2 小时前
IntelliJ IDEA 基本使用教程及Spring Boot项目搭建实战
java·ide·intellij-idea