Spring 源码阅读 39:postProcessBeanFactory 对 @Configuration 配置的解析和处理(2)

简介: 本文通过阅读源码的方式分析了 Spring 的注解配置类后处理器对配置类进行增强处理的后半部分流程,也就是对配置类进行增强的逻辑。

基于 Spring Framework v5.2.6.RELEASE

接上篇:Spring 源码阅读 38:postProcessBeanFactory 对 @Configuration 配置的解析和处理(1)

概述

上篇介绍了后处理器的postProcessBeanFactory方法通过enhanceConfigurationClasses方法对配置类进行增强的前半部分,也就是从容器中获取并筛选出需要进行增强处理的配置类。本文将深入分析增强处理的原理。

处理过程

本文的分析内容对应到代码就是enhanceConfigurationClasses中的一下这部分:

ConfigurationClassEnhancerenhancer=newConfigurationClassEnhancer();
for (Map.Entry<String, AbstractBeanDefinition>entry : configBeanDefs.entrySet()) {
AbstractBeanDefinitionbeanDef=entry.getValue();
// If a @Configuration class gets proxied, always proxy the target classbeanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
// Set enhanced subclass of the user-specified bean classClass<?>configClass=beanDef.getBeanClass();
Class<?>enhancedClass=enhancer.enhance(configClass, this.beanClassLoader);
if (configClass!=enhancedClass) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with "+"enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
      }
beanDef.setBeanClass(enhancedClass);
   }
}

其中用到的configBeanDefs集合,就是筛选出的需要进行增强处理的配置类的集合。

在进入for循环之前,首先创建了一个 ConfigurationClassEnhancer 类型的对象enhancer,从名称可以看出,它用来对配置类进行增强处理。

for循环语句块中,首先会获取到当前遍历到的 BeanDefinition 对象,类型是 AbstractBeanDefinition。然后给其设置属性org.springframework.aop.framework.autoproxy.AutoProxyUtils.preserveTargetClass的值为true

然后获取到当前 BeanDefinition 的类型信息configClass,通过enhancerenhance方法,得到增强后的类型enhancedClass

最后,判断configClassenhancedClass不是同一个对象,将enhancedClass设置为BeanDefinition 新的类型信息。

可以看出,整个过程比较简单,而enhance方法是这里的核心方法,下面我们分析enhance方法的原理。

enhance方法

进入方法内部。

// org.springframework.context.annotation.ConfigurationClassEnhancer#enhancepublicClass<?>enhance(Class<?>configClass, @NullableClassLoaderclassLoader) {
if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Ignoring request to enhance %s as it has "+"already been enhanced. This usually indicates that more than one "+"ConfigurationClassPostProcessor has been registered (e.g. via "+"<context:annotation-config>). This is harmless, but you may "+"want check your configuration and remove one CCPP if possible",
configClass.getName()));
      }
returnconfigClass;
   }
Class<?>enhancedClass=createClass(newEnhancer(configClass, classLoader));
if (logger.isTraceEnabled()) {
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
configClass.getName(), enhancedClass.getName()));
   }
returnenhancedClass;
}

如果方法传入的configClass是 EnhancedConfiguration 类型,则说明已经被增强过,直接返回。如果不是的话,则通过newEnhancer方法,得到一个 Enhancer 对象,然后通过createClass方法,进行处理,并将结果返回。

先查看newEnhancer方法的处理逻辑。

// org.springframework.context.annotation.ConfigurationClassEnhancer#newEnhancer/*** Creates a new CGLIB {@link Enhancer} instance.*/privateEnhancernewEnhancer(Class<?>configSuperClass, @NullableClassLoaderclassLoader) {
Enhancerenhancer=newEnhancer();
enhancer.setSuperclass(configSuperClass);
enhancer.setInterfaces(newClass<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(newBeanFactoryAwareGeneratorStrategy(classLoader));
enhancer.setCallbackFilter(CALLBACK_FILTER);
enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
returnenhancer;
}

这里可以看出,Spring 对配置类的增强使用的是 CGLIB 的方式。这里给新创建的enhancer设置了一些信息,这些信息先留个印象,之后遇到了我们再回过来介绍。

创建好的enhancer会作为参数传入createClass方法中,我们进入createClass方法查看代码。

// org.springframework.context.annotation.ConfigurationClassEnhancer#createClassprivateClass<?>createClass(Enhancerenhancer) {
Class<?>subclass=enhancer.createClass();
// Registering callbacks statically (as opposed to thread-local)// is critical for usage in an OSGi environment (SPR-5932)...Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
returnsubclass;
}

这里通过刚刚创建的enhancer创建了增强的子类,在返回子类之前,通过 Enhancer 的registerStaticCallbacks方法为这个子类注册回调接口。常量CALLBACKS包含注册的回调接口。

// org.springframework.context.annotation.ConfigurationClassEnhancer#CALLBACKSprivatestaticfinalCallback[] CALLBACKS=newCallback[] {
newBeanMethodInterceptor(),
newBeanFactoryAwareMethodInterceptor(),
NoOp.INSTANCE};

下面我们逐个来看 BeanMethodInterceptor 和 BeanFactoryAwareMethodInterceptor 方法拦截器。

BeanMethodInterceptor

先看 BeanMethodInterceptor 拦截器,方法拦截器会根据isMatch方法筛选要拦截的方法。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#isMatch@OverridepublicbooleanisMatch(MethodcandidateMethod) {
return (candidateMethod.getDeclaringClass() !=Object.class&&!BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) &&BeanAnnotationHelper.isBeanAnnotated(candidateMethod));
}

由上面的代码可知,BeanMethodInterceptor 拦截的是有返回值、不是 BeanFactoryAware 接口的setBeanFactory方法,且被@Bean注解标记的方法。

拦截器的主要逻辑在它的intercept方法中。

image.png

代码量比较大,下面开始分析。

准备工作

ConfigurableBeanFactorybeanFactory=getBeanFactory(enhancedConfigInstance);

首先,通过getBeanFactory方法,从增强类实例中获取 BeanFactory 容器,进入方法查看源码。

privateConfigurableBeanFactorygetBeanFactory(ObjectenhancedConfigInstance) {
Fieldfield=ReflectionUtils.findField(enhancedConfigInstance.getClass(), BEAN_FACTORY_FIELD);
Assert.state(field!=null, "Unable to find generated bean factory field");
ObjectbeanFactory=ReflectionUtils.getField(field, enhancedConfigInstance);
Assert.state(beanFactory!=null, "BeanFactory has not been injected into @Configuration class");
Assert.state(beanFactoryinstanceofConfigurableBeanFactory,
"Injected BeanFactory is not a ConfigurableBeanFactory");
return (ConfigurableBeanFactory) beanFactory;
}

从源码中可以看出,容器对象就是增强配置类的$$beanFactory字段,这个字段是在创建增强类型的时候添加的,赋值的方式可以参考后文介绍的 BeanFactoryAwareMethodInterceptor 拦截器的逻辑。

再回到intercept方法中。

StringbeanName=BeanAnnotationHelper.determineBeanNameFor(beanMethod);

接下来,获取 BeanAnnotationHelper 的determineBeanNameFor方法,获取了被@Bean标记的方法对应的 Bean 名称。获取的代码如下:

// org.springframework.context.annotation.BeanAnnotationHelper#determineBeanNameForpublicstaticStringdetermineBeanNameFor(MethodbeanMethod) {
StringbeanName=beanNameCache.get(beanMethod);
if (beanName==null) {
// By default, the bean name is the name of the @Bean-annotated methodbeanName=beanMethod.getName();
// Check to see if the user has explicitly set a custom bean name...AnnotationAttributesbean=AnnotatedElementUtils.findMergedAnnotationAttributes(beanMethod, Bean.class, false, false);
if (bean!=null) {
String[] names=bean.getStringArray("name");
if (names.length>0) {
beanName=names[0];
         }
      }
beanNameCache.put(beanMethod, beanName);
   }
returnbeanName;
}

默认情况下,会把方法名作为 Bean 的名称,如果@Bean注解的name属性设置了值,则将其作为 Bean 的名称。

再看intercept方法中后续的代码。

// Determine whether this bean is a scoped-proxyif (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
StringscopedBeanName=ScopedProxyCreator.getTargetBeanName(beanName);
if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
beanName=scopedBeanName;
   }
}

如果方法通过@Scope指定了作用域代理,则对它的 Bean 名称进行处理。

接着往下看。

增强 FactoryBean

if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX+beanName) &&factoryContainsBean(beanFactory, beanName)) {
ObjectfactoryBean=beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX+beanName);
if (factoryBeaninstanceofScopedProxyFactoryBean) {
// Scoped proxy factory beans are a special case and should not be further proxied   }
else {
// It is a candidate FactoryBean - go ahead with enhancementreturnenhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
   }
}

if判断条件中,会根据 Bean 名称判断容器中是否有对应的 Bean 以及其对应的 FactoryBean 工厂 Bean,如果都有的话,则说明当前的 Bean 是一个实现了 FactoryBean 的类型。对于符合条件的 Bean,会从容器中获取其对应的 FactoryBean,也就是工厂实例,对其进行增强。

在增强之前,还会判断这个工厂实例是不是 ScopedProxyFactoryBean 类型,如果是的话,则跳过增强的逻辑。最终,增强的工作是通过enhanceFactoryBean方法完成的。

我们进入enhanceFactoryBean方法查看源码。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#enhanceFactoryBeanprivateObjectenhanceFactoryBean(finalObjectfactoryBean, Class<?>exposedType,
finalConfigurableBeanFactorybeanFactory, finalStringbeanName) {
try {
Class<?>clazz=factoryBean.getClass();
booleanfinalClass=Modifier.isFinal(clazz.getModifiers());
booleanfinalMethod=Modifier.isFinal(clazz.getMethod("getObject").getModifiers());
if (finalClass||finalMethod) {
if (exposedType.isInterface()) {
if (logger.isTraceEnabled()) {
logger.trace("Creating interface proxy for FactoryBean '"+beanName+"' of type ["+clazz.getName() +"] for use within another @Bean method because its "+                     (finalClass?"implementation class" : "getObject() method") +" is final: Otherwise a getObject() call would not be routed to the factory.");
            }
returncreateInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName);
         }
else {
if (logger.isDebugEnabled()) {
logger.debug("Unable to proxy FactoryBean '"+beanName+"' of type ["+clazz.getName() +"] for use within another @Bean method because its "+                     (finalClass?"implementation class" : "getObject() method") +" is final: A getObject() call will NOT be routed to the factory. "+"Consider declaring the return type as a FactoryBean interface.");
            }
returnfactoryBean;
         }
      }
   }
catch (NoSuchMethodExceptionex) {
// No getObject() method -> shouldn't happen, but as long as nobody is trying to call it...   }
returncreateCglibProxyForFactoryBean(factoryBean, beanFactory, beanName);
}

这个方法的逻辑比较清晰。首先会判断类定义和getObject方法的定义,如果二者至少有一个是final修饰的,那么就会进入第一个if语句块。

进入后,会判断@Bean方法的返回值,是不是一个接口类型。如果是的话,就通过createInterfaceProxyForFactoryBean方法创建动态代理并返回,如果不是的话,则直接返回factoryBean

这里我们进入createInterfaceProxyForFactoryBean方法查看一下细节。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#createInterfaceProxyForFactoryBeanprivateObjectcreateInterfaceProxyForFactoryBean(finalObjectfactoryBean, Class<?>interfaceType,
finalConfigurableBeanFactorybeanFactory, finalStringbeanName) {
returnProxy.newProxyInstance(
factoryBean.getClass().getClassLoader(), newClass<?>[] {interfaceType},
         (proxy, method, args) -> {
if (method.getName().equals("getObject") &&args==null) {
returnbeanFactory.getBean(beanName);
            }
returnReflectionUtils.invokeMethod(method, factoryBean, args);
         });
}

可以看到,这里创建代理使用的是JDK动态代理的方式,如果当前调用的方法是getObject方法且参数为空,那么则返回根据 Bean 名称从容器中获取的 Bean 实例。

回到enhanceFactoryBean方法中,如果最开始的if判断条件不成立,则通过createCglibProxyForFactoryBean方法创建动态代理,从名称可以看出,这里使用的是CGLIB的方式。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#createCglibProxyForFactoryBeanprivateObjectcreateCglibProxyForFactoryBean(finalObjectfactoryBean,
finalConfigurableBeanFactorybeanFactory, finalStringbeanName) {
Enhancerenhancer=newEnhancer();
enhancer.setSuperclass(factoryBean.getClass());
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setCallbackType(MethodInterceptor.class);
// Ideally create enhanced FactoryBean proxy without constructor side effects,// analogous to AOP proxy creation in ObjenesisCglibAopProxy...Class<?>fbClass=enhancer.createClass();
ObjectfbProxy=null;
if (objenesis.isWorthTrying()) {
try {
fbProxy=objenesis.newInstance(fbClass, enhancer.getUseCache());
      }
catch (ObjenesisExceptionex) {
logger.debug("Unable to instantiate enhanced FactoryBean using Objenesis, "+"falling back to regular construction", ex);
      }
   }
if (fbProxy==null) {
try {
fbProxy=ReflectionUtils.accessibleConstructor(fbClass).newInstance();
      }
catch (Throwableex) {
thrownewIllegalStateException("Unable to instantiate enhanced FactoryBean using Objenesis, "+"and regular FactoryBean instantiation via default constructor fails as well", ex);
      }
   }
   ((Factory) fbProxy).setCallback(0, (MethodInterceptor) (obj, method, args, proxy) -> {
if (method.getName().equals("getObject") &&args.length==0) {
returnbeanFactory.getBean(beanName);
      }
returnproxy.invoke(factoryBean, args);
   });
returnfbProxy;
}

这里的代码量虽然相对较多,但是代理的逻辑跟前面通过JDK动态代理实现的逻辑是一样的。

增强普通类型的 Bean

接着看intercept方法中后续的代码。

if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
// The factory is calling the bean method in order to instantiate and register the bean// (i.e. via a getBean() call) -> invoke the super implementation of the method to actually// create the bean instance.if (logger.isInfoEnabled() &&BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
logger.info(String.format("@Bean method %s.%s is non-static and returns an object "+"assignable to Spring's BeanFactoryPostProcessor interface. This will "+"result in a failure to process annotations such as @Autowired, "+"@Resource and @PostConstruct within the method's declaring "+"@Configuration class. Add the 'static' modifier to this method to avoid "+"these container lifecycle issues; see @Bean javadoc for complete details.",
beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
   }
returncglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}

判断条件中调用了isCurrentlyInvokedFactoryMethod方法,我们查看其判断逻辑。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#isCurrentlyInvokedFactoryMethodprivatebooleanisCurrentlyInvokedFactoryMethod(Methodmethod) {
MethodcurrentlyInvoked=SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
return (currentlyInvoked!=null&&method.getName().equals(currentlyInvoked.getName()) &&Arrays.equals(method.getParameterTypes(), currentlyInvoked.getParameterTypes()));
}

这里判断了给定的方法,是否是配置类中的工厂方法,且工厂方法就是当前的方法。如果是,则调用父类中的方法创建实例。

最后,如果当前调用的方法对应的 Bean 名称不能在 Spring 容器中找到 FactoryBean,而且也没有定义工厂方法,则会执行resolveBeanReference方法。

returnresolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);

进入resolveBeanReference方法。

image.png

这个方法虽然很长,但是仔细看就会发现逻辑很简单,就是使用 Bean 的名称,从容器中获取实例。

至此,BeanMethodInterceptor 的逻辑就介绍完了,下面看 BeanFactoryAwareMethodInterceptor。

BeanFactoryAwareMethodInterceptor

先看isMatch方法。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanFactoryAwareMethodInterceptor#isMatch@OverridepublicbooleanisMatch(MethodcandidateMethod) {
returnisSetBeanFactory(candidateMethod);
}
publicstaticbooleanisSetBeanFactory(MethodcandidateMethod) {
return (candidateMethod.getName().equals("setBeanFactory") &&candidateMethod.getParameterCount() ==1&&BeanFactory.class==candidateMethod.getParameterTypes()[0] &&BeanFactoryAware.class.isAssignableFrom(candidateMethod.getDeclaringClass()));
}

判断条件比较多,总结起来就是,判断当前的方法是不是 BeanFactoryAware 感知接口的setBeanFactory实现方法。

再看intercept方法。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanFactoryAwareMethodInterceptor#intercept@Override@NullablepublicObjectintercept(Objectobj, Methodmethod, Object[] args, MethodProxyproxy) throwsThrowable {
Fieldfield=ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD);
Assert.state(field!=null, "Unable to find generated BeanFactory field");
field.set(obj, args[0]);
// Does the actual (non-CGLIB) superclass implement BeanFactoryAware?// If so, call its setBeanFactory() method. If not, just exit.if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
returnproxy.invokeSuper(obj, args);
   }
returnnull;
}

首先会判断增强类中是否存在名称为$$beanFactory的字段(常量BEAN_FACTORY_FIELD的值),如果存在的话,就会为该字段赋值,这样,增强后的配置类就能回去到 BeanFactory 容器了。

这个$$beanFactory是怎么来的呢?

在之前介绍过的newEnhancer方法创建增强类的时候,有这样一行代码:

enhancer.setStrategy(newBeanFactoryAwareGeneratorStrategy(classLoader));

这里给增强类设置了一个策略,在 BeanFactoryAwareGeneratorStrategy 中对配置类进行增强时,添加了$$beanFactory字段。

// org.springframework.context.annotation.ConfigurationClassEnhancer.BeanFactoryAwareGeneratorStrategy#transform@OverrideprotectedClassGeneratortransform(ClassGeneratorcg) throwsException {
ClassEmitterTransformertransformer=newClassEmitterTransformer() {
@Overridepublicvoidend_class() {
declare_field(Constants.ACC_PUBLIC, BEAN_FACTORY_FIELD, Type.getType(BeanFactory.class), null);
super.end_class();
      }
   };
returnnewTransformingClassGenerator(cg, transformer);
}

最后的部分

到这里,enhance方法的全部逻辑就都介绍完了,我们再回到最初调用enhance方法的部分。

for (Map.Entry<String, AbstractBeanDefinition>entry : configBeanDefs.entrySet()) {
AbstractBeanDefinitionbeanDef=entry.getValue();
// If a @Configuration class gets proxied, always proxy the target classbeanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
// Set enhanced subclass of the user-specified bean classClass<?>configClass=beanDef.getBeanClass();
Class<?>enhancedClass=enhancer.enhance(configClass, this.beanClassLoader);
if (configClass!=enhancedClass) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with "+"enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
      }
beanDef.setBeanClass(enhancedClass);
   }
}

在for循环中,每获得一个配置类的增强类之后,就会将其对应的 BeanDefinition 类型信息设置为增强类型。

总结

本文分析了postProcessBeanFactory处理方法调用的enhanceConfigurationClasses方法对配置类进行增强处理的后半部分流程,也就是对配置类进行增强的逻辑。

目录
相关文章
|
6天前
|
缓存 测试技术 Android开发
深入了解Appium:Capability 高级配置技巧解析
Appium 提供多种进阶配置项以优化自动化测试,如 deviceName 作为设备别名,udid 确保选择特定设备,newCommandTimeout 设置超时时间,PRINT_PAGE_SOURCE_ON_FIND_FAILURE 在错误时打印页面源,以及测试策略中的 noReset、shouldTerminateApp 和 forceAppLaunch 控制应用状态和重启。这些配置可提升测试效率和准确性。
16 2
|
7天前
|
存储 弹性计算 固态存储
阿里云服务器配置选择指南,2024年全解析
阿里云服务器配置选择涉及CPU、内存、带宽和磁盘。个人开发者或中小企业推荐使用轻量应用服务器或ECS经济型实例,如2核2G3M配置,适合网站和轻量应用。企业用户则应选择企业级独享型ECS,如计算型c7、通用型g7,至少2核4G起,带宽建议5M,系统盘考虑SSD云盘或ESSD云盘以保证性能。阿里云提供了多种实例类型和配置,用户需根据实际需求进行选择。
|
12天前
|
分布式计算 DataWorks 调度
DataWorks操作报错合集之DataWorks配置参数在开发环境进行调度,参数解析不出来,收到了 "Table does not exist" 的错误,该怎么处理
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
25 0
|
13天前
|
分布式计算 大数据 数据处理
MaxCompute操作报错合集之在开发环境中配置MaxCompute参数进行调度,但参数解析不出来,如何解决
MaxCompute是阿里云提供的大规模离线数据处理服务,用于大数据分析、挖掘和报表生成等场景。在使用MaxCompute进行数据处理时,可能会遇到各种操作报错。以下是一些常见的MaxCompute操作报错及其可能的原因与解决措施的合集。
|
15天前
|
监控 安全 网络协议
|
17天前
|
canal 缓存 关系型数据库
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
|
17天前
|
XML 人工智能 Java
Spring Bean名称生成规则(含源码解析、自定义Spring Bean名称方式)
Spring Bean名称生成规则(含源码解析、自定义Spring Bean名称方式)
|
XML Java 数据格式
Spring零配置之@Configuration注解详解
Spring3.0之前要使用Spring必须要有一个xml配置文件,这也是Spring的核心文件,而Spring3.0之后可以不要配置文件了,通过注解@Configuration完全搞定。
241 0
Spring零配置之@Configuration注解详解
|
2月前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
53 0
|
2月前
|
缓存 安全 Java
Spring Boot 面试题及答案整理,最新面试题
Spring Boot 面试题及答案整理,最新面试题
138 0

推荐镜像

更多