Spring IoC源码学习:@Autowire 详解

Spring IoC源码学习:invokeBeanFactoryPostProcessors详解

Spring IoC源码学习:registerBeanPostProcessors详解

Spring IoC源码学习:finishBeanFactoryInitialization详解

Spring IoC源码学习:getBean详解

Spring IoC源码学习:createBean详解(上)

Spring IoC源码学习:createBean详解(下)

Spring IoC源码学习:@Autowire 详解

Spring IoC源码学习:finishRefresh 详解

前言

==

在 Spring IoC:createBean 详解(上)代码块4.5 和 Spring IoC:createBean详解(下)代码块1代码块4的 7.1.1 我们遗留了一个解析——@Autowire 注解的解析。之所以单独提出来,是因为在我现在接触的项目中,使用 @Autowire 注解的比例非常高,可以说基本用过 Spring 的同学都接触过这个注解,重要性不言而喻。因此,单独拿出来,较详细的介绍一下。

本文只会单独介绍 @Autowire 的部分内容,具体 @Autowire 注解的完整过程,需要结合 Spring IoC:createBean 详解(上)代码块4.5 和 Spring IoC:createBean详解(下)代码块1代码块4的 7.1.1 去看。

如何使用

====

1.开启注解配置


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns=“http://www.springframework.org/schema/beans”

xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”

xmlns:context=“http://www.springframework.org/schema/context”

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.1.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-3.1.xsd">

<context:component-scan base-package=“com.joonwhee.open.demo.service”/>

context:annotation-config/

要使用 @Autowire 注解,可以通过 <context:component-scan /> 或 context:annotation-config/ 来开启,其中前者是包含了后者的效果的,因此现在一般都使用 <context:component-scan /> 即可。

2.在代码中使用


@Service

public class ConstructorServiceImpl implements ConstructorService {

// 1.属性注入

@Autowired

private UserService userService;

private final DemoService demoService;

// 2.构造函数注入

@Autowired

public ConstructorServiceImpl(DemoService demoService) {

this.demoService = demoService;

}

}

该代码中使用了目前最常见的两种注入方式:1)属性注入;2)构造函数注入。

以我自己为例,我周围最常用的是:属性注入,但是 Spring 团队建议使用的方式是:构造函数注入,当你使用属性注入时,鼠标移到属性上的 @Autowire 就可以看到如下图的提示,并且可以通过快捷键将属性注入直接修改成构造函数注入。

构造函数的常见优点是:

  1. 保证依赖不可变(final 关键字)

  2. 保证依赖不为空(省去了检查)

  3. 以完全初始化的状态返回到客户端(调用)代码

  4. 避免了循环依赖

  5. 提升了代码的可复用性

构造函数的常见缺点是:

  1. 构造函数会有很多参数。

  2. 有些类是需要默认构造函数的,一旦使用构造函数注入,就无法使用默认构造函数。

  3. 这个类里面的有些方法并不需要用到这些依赖。

这些优点我看了下,个人觉得还好,只要不瞎用,其实使用属性注入并不会有什么问题。至于使用哪一种,就看个人喜好了。

继承结构

====

@Autowire 注解的功能实现都是由 AutowiredAnnotationBeanPostProcessor 实现,AutowiredAnnotationBeanPostProcessor 的继承关系如下图:

源码解析

====

AutowiredAnnotationBeanPostProcessor 何时被注册到 BeanFactory?


在 Spring IoC:context:component-scan节点详解 中的代码块17就有 AutowiredAnnotationBeanPostProcessor 被注册到 BeanFactory 的代码,如下:

AnnotationConfigUtils#registerAnnotationConfigProcessors


查看该方法的上下文,请参考:Spring IoC:context:component-scan节点详解 中的代码块17

public static Set registerAnnotationConfigProcessors(

BeanDefinitionRegistry registry, Object source) {

DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);

if (beanFactory != null) {

if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {

// 1.设置dependencyComparator属性

beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);

}

if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {

// 2.设置autowireCandidateResolver属性(设置自动注入候选对象的解析器,用于判断BeanDefinition是否为候选对象)

beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());

}

}

Set beanDefs = new LinkedHashSet(4);

// 3.注册内部管理的用于处理@Configuration注解的后置处理器的bean

if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);

def.setSource(source);

// 3.1 registerPostProcessor: 注册BeanDefinition到注册表中

beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));

}

// 4.注册内部管理的用于处理@Autowired、@Value、@Inject以及@Lookup注解的后置处理器的bean

if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);

def.setSource(source);

beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));

}

// 5.注册内部管理的用于处理@Required注解的后置处理器的bean

if (!registry.containsBeanDefinition(REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition(RequiredAnnotationBeanPostProcessor.class);

def.setSource(source);

beanDefs.add(registerPostProcessor(registry, def, REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));

}

// 6.注册内部管理的用于处理JSR-250注解(例如@Resource, @PostConstruct, @PreDestroy)的后置处理器的bean

// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.

if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);

def.setSource(source);

beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));

}

// 7.注册内部管理的用于处理JPA注解的后置处理器的bean

// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.

if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition();

try {

def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME,

AnnotationConfigUtils.class.getClassLoader()));

}

catch (ClassNotFoundException ex) {

throw new IllegalStateException(

"Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);

}

def.setSource(source);

beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));

}

// 8.注册内部管理的用于处理@EventListener注解的后置处理器的bean

if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);

def.setSource(source);

beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));

}

// 9.注册内部管理用于生产ApplicationListener对象的EventListenerFactory对象

if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {

RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);

def.setSource(source);

beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));

}

return beanDefs;

}

在该代码的第4点注册了 AutowiredAnnotationBeanPostProcessor,具体看 registerAnnotationConfigProcessors 方法的调用方,可以看到 AnnotationConfigBeanDefinitionParser 和 ComponentScanBeanDefinitionParser,而这两个正是 <context:component-scan /> 和 context:annotation-config/ 的 bean 定义解析器。

构造函数注入时做了什么

===========

构造函数注入通常来说有两种:1)xml 配置注入;2)@Autowire 注解注入;本文只讨论 @Autowire 注解注入。

AutowiredAnnotationBeanPostProcessor 中重写的方法不多,直接找一下就可以找到跟构造函数相关的方法:determineCandidateConstructors,该方法被定义在 SmartInstantiationAwareBeanPostProcessor 接口中,主要作用是:确定要用于给定 bean 的候选构造函数。

在 Spring IoC 的过程中,调用的入口在 Spring IoC:createBean详解(上)中的代码块4.5,下面介绍下 determineCandidateConstructors 方法。

AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors


查看该方法的上下文,请参考:Spring IoC:createBean详解(上)中的代码块4.5

@Override

public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)

throws BeanCreationException {

// Let’s check for lookup methods here…

// @Lookup注解检查

if (!this.lookupMethodsChecked.contains(beanName)) {

try {

ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {

@Override

public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {

Lookup lookup = method.getAnnotation(Lookup.class);

if (lookup != null) {

LookupOverride override = new LookupOverride(method, lookup.value());

try {

RootBeanDefinition mbd = (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName);

mbd.getMethodOverrides().addOverride(override);

} catch (NoSuchBeanDefinitionException ex) {

throw new BeanCreationException(beanName,

“Cannot apply @Lookup to beans without corresponding bean definition”);

}

}

}

});

} catch (IllegalStateException ex) {

throw new BeanCreationException(beanName, “Lookup method resolution failed”, ex);

} catch (NoClassDefFoundError err) {

throw new BeanCreationException(beanName, “Failed to introspect bean class [” + beanClass.getName() +

“] for lookup method metadata: could not find class that it depends on”, err);

}

// 已经检查过的添加到lookupMethodsChecked

this.lookupMethodsChecked.add(beanName);

}

// Quick check on the concurrent map first, with minimal locking.

// 1.构造函数解析,首先检查是否存在于缓存中

Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);

if (candidateConstructors == null) {

// Fully synchronized resolution now…

// 2.加锁进行操作

synchronized (this.candidateConstructorsCache) {

// 3.再次检查缓存,双重检测

candidateConstructors = this.candidateConstructorsCache.get(beanClass);

if (candidateConstructors == null) {

// 存放原始的构造函数(候选者)

Constructor<?>[] rawCandidates;

try {

// 4.获取beanClass声明的构造函数(如果没有声明,会返回一个默认的无参构造函数)

rawCandidates = beanClass.getDeclaredConstructors();

} catch (Throwable ex) {

throw new BeanCreationException(beanName,

“Resolution of declared constructors on bean Class [” + beanClass.getName() +

“] from ClassLoader [” + beanClass.getClassLoader() + “] failed”, ex);

}

// 存放使用了@Autowire注解的构造函数

List<Constructor<?>> candidates = new ArrayList

// 存放使用了@Autowire注解,并且require=true的构造函数

Constructor<?> requiredConstructor = null;

// 存放默认的构造函数

Constructor<?> defaultConstructor = null;

// 5.遍历原始的构造函数候选者

for (Constructor<?> candidate : rawCandidates) {

// 6.获取候选者的注解属性

AnnotationAttributes ann = findAutowiredAnnotation(candidate);

if (ann == null) {

// 7.如果没有从候选者找到注解,则尝试解析beanClass的原始类(针对CGLIB代理)

Class<?> userClass = ClassUtils.getUserClass(beanClass);

if (userClass != beanClass) {

try {

Constructor<?> superCtor =

userClass.getDeclaredConstructor(candidate.getParameterTypes());

ann = findAutowiredAnnotation(superCtor);

} catch (NoSuchMethodException ex) {

// Simply proceed, no equivalent superclass constructor found…

}

}

}

// 8.如果该候选者使用了@Autowire注解

if (ann != null) {

if (requiredConstructor != null) {

// 8.1 之前已经存在使用@Autowired(required = true)的构造函数,则不能存在其他使用@Autowire注解的构造函数,否则抛异常

throw new BeanCreationException(beanName,

"Invalid autowire-marked constructor: " + candidate +

". Found constructor with ‘required’ Autowired annotation already: " +

requiredConstructor);

}

// 8.2 获取注解的require属性值

boolean required = determineRequiredStatus(ann);

if (required) {

if (!candidates.isEmpty()) {

// 8.3 如果当前候选者是@Autowired(required = true),则之前不能存在其他使用@Autowire注解的构造函数,否则抛异常

throw new BeanCreationException(beanName,

"Invalid autowire-marked constructors: " + candidates +

". Found constructor with ‘required’ Autowired annotation: " +

candidate);

}

// 8.4 如果该候选者使用的注解的required属性为true,赋值给requiredConstructor

requiredConstructor = candidate;

}

// 8.5 将使用了@Autowire注解的候选者添加到candidates

candidates.add(candidate);

} else if (candidate.getParameterTypes().length == 0) {

// 8.6 如果没有使用注解,并且没有参数,则为默认的构造函数

defaultConstructor = candidate;

}

}

// 9.如果存在使用了@Autowire注解的构造函数

if (!candidates.isEmpty()) {

// Add default constructor to list of optional constructors, as fallback.

// 9.1 但是没有使用了@Autowire注解并且required属性为true的构造函数

if (requiredConstructor == null) {

if (defaultConstructor != null) {

// 9.2 如果存在默认的构造函数,则将默认的构造函数添加到candidates

candidates.add(defaultConstructor);

} else if (candidates.size() == 1 && logger.isWarnEnabled()) {

logger.warn(“Inconsistent constructor declaration on bean with name '” + beanName +

"': single autowire-marked constructor flagged as optional - " +

"this constructor is effectively required since there is no " +

"default constructor to fall back to: " + candidates.get(0));

}

}

// 9.3 将所有的candidates当作候选者

candidateConstructors = candidates.toArray(new Constructor<?>[candidates.size()]);

} else if (rawCandidates.length == 1 && rawCandidates[0].getParameterTypes().length > 0) {

// 10.如果candidates为空 && beanClass只有一个声明的构造函数(非默认构造函数),则将该声明的构造函数作为候选者

candidateConstructors = new Constructor<?>[]{rawCandidates[0]};

} else {

// 11.否则返回一个空的Constructor对象

candidateConstructors = new Constructor<?>[0];

}

// 12.将beanClass的构造函数解析结果放到缓存

this.candidateConstructorsCache.put(beanClass, candidateConstructors);

}

}

}

// 13.返回解析的构造函数

return (candidateConstructors.length > 0 ? candidateConstructors : null);

}

6.获取候选者的注解属性,见代码块1详解

关于 8.1 和 8.3 的异常校验,说的简单点:在一个 bean 中,只要有构造函数使用了 “@Autowired(required = true)” 或 “@Autowired”,就不允许有其他的构造函数使用 “@Autowire”;但是允许有多个构造函数同时使用 “@Autowired(required = false)”。

代码块1:findAutowiredAnnotation


private AnnotationAttributes findAutowiredAnnotation(AccessibleObject ao) {

// 1.判断ao是否有被注解修饰

if (ao.getAnnotations().length > 0) {

// 2.检查是否有autowiredAnnotationTypes中的注解:@Autowired、@Value(@Value无法修饰构造函数)

for (Class<? extends Annotation> type : this.autowiredAnnotationTypes) {

// 3.拿到注解的合并注解属性,@Autowire在这边拿到,required=true(默认属性)

AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ao, type);

if (attributes != null) {

return attributes;

}

}

}

return null;

}

确定了要用于给定 bean 的候选构造函数后,之后的逻辑就是执行构造函数自动注入,该逻辑在 Spring IoC:createBean 详解(上)中的代码块5已经介绍。

属性注入时做了什么

=========

属性注入通常来说有两种:1)xml 配置注入;2)@Autowire 注解注入;本文只讨论 @Autowire 注解注入。

AutowiredAnnotationBeanPostProcessor 中跟属性注入有关的方法出口有两个:postProcessMergedBeanDefinition 和 postProcessPropertyValues。

1.postProcessMergedBeanDefinition 方法介绍


postProcessMergedBeanDefinition 被定义在 MergedBeanDefinitionPostProcessor 接口中,该方法的主要作用是:对指定 bean 的给定 MergedBeanDefinition 进行后置处理。

在 AutowiredAnnotationBeanPostProcessor 的实现中,主要是对使用了 @Autowire 注解的方法和属性进行预解析,并放到 injectionMetadataCache 缓存中,用于后续使用。

在 Spring IoC 的过程中,调用的入口在:Spring IoC:createBean详解(下)中的代码块1,下面介绍下 postProcessMergedBeanDefinition 方法。

AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition


查看该方法的上下文,请参考:Spring IoC:createBean详解(下)中的代码块1

@Override

public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {

if (beanType != null) {

// 1.在指定Bean中查找使用@Autowire注解的元数据

InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);

// 2.检查元数据中的注解信息

metadata.checkConfigMembers(beanDefinition);

}

}

1.在指定 bean 中查找使用 @Autowire 注解的元数据,见代码块2详解

2.检查元数据中的注解信息,见代码块4详解。

代码块2:findAutowiringMetadata


private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, PropertyValues pvs) {

// Fall back to class name as cache key, for backwards compatibility with custom callers.

// 1.设置cacheKey的值(beanName 或者 className)

String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());

// Quick check on the concurrent map first, with minimal locking.

// 2.检查beanName对应的InjectionMetadata是否已经存在于缓存中

InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);

// 3.检查InjectionMetadata是否需要刷新(为空或者class变了)

if (InjectionMetadata.needsRefresh(metadata, clazz)) {

synchronized (this.injectionMetadataCache) {

// 4.加锁后,再次从缓存中获取beanName对应的InjectionMetadata

metadata = this.injectionMetadataCache.get(cacheKey);

// 5.加锁后,再次检查InjectionMetadata是否需要刷新

if (InjectionMetadata.needsRefresh(metadata, clazz)) {

if (metadata != null) {

// 6.如果需要刷新,并且metadata不为空,则先移除

metadata.clear(pvs);

}

try {

// 7.解析@Autowired注解的信息,生成元数据(包含clazz和clazz里解析到的注入的元素,

// 这里的元素包括AutowiredFieldElement和AutowiredMethodElement)

metadata = buildAutowiringMetadata(clazz);

// 8.将解析的元数据放到injectionMetadataCache缓存,以备复用,每一个类只解析一次

this.injectionMetadataCache.put(cacheKey, metadata);

} catch (NoClassDefFoundError err) {

throw new IllegalStateException(“Failed to introspect bean class [” + clazz.getName() +

“] for autowiring metadata: could not find class that it depends on”, err);

}

}

}

}

return metadata;

}

7.解析 @Autowired 注解的信息,生成元数据,见代码块3详解

代码块3:buildAutowiringMetadata


private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {

// 1.用于存放所有解析到的注入的元素的变量

LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>();

Class<?> targetClass = clazz;

// 2.循环遍历

do {

// 2.1 定义存放当前循环的Class注入的元素(有序)

final LinkedList<InjectionMetadata.InjectedElement> currElements =

new LinkedList<InjectionMetadata.InjectedElement>();

// 2.2 如果targetClass的属性上有@Autowired注解,则用工具类获取注解信息

ReflectionUtils.doWithLocalFields(targetClass, new ReflectionUtils.FieldCallback() {

@Override

public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {

// 2.2.1 获取field上的@Autowired注解信息

AnnotationAttributes ann = findAutowiredAnnotation(field);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-hqLAXYqS-1711818156147)]

[外链图片转存中…(img-0dIVi0yz-1711818156148)]

[外链图片转存中…(img-qIA9RbtP-1711818156148)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

[外链图片转存中…(img-TBY1M1T6-1711818156148)]

[外链图片转存中…(img-jgQuKjPT-1711818156149)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值