Spring-源码解读

48次阅读

共计 13957 个字符,预计需要花费 35 分钟才能阅读完成。

《Spring 源码解读》

傻瓜源码 - 内容简介

傻瓜源码 - 内容简介
????【职场经验】(持续更新)
精编短文:如何成为值钱的 Java 开发 - 指南

如何日常学习、如何书写简历、引导面试官、系统准备面试、选择 offer、提高绩效、晋升 TeamLeader…..
????【源码解读】(持续更新) <br/>1. 源码选材:Java 架构师必须掌握的所有框架和类库源码<br/>2. 内容大纲:按照“企业应用 Demo”讲解执行源码:总纲“阅读指南”、第一章“源码基础”、第二章“相关 Java 基础”、第三章“白话讲源码”、第四章“代码解读”、第五章“设计模式”、第六章“附录 - 面试习题、相关 JDK 方法、中文注释可运行源码项目”
3. 读后问题:粉丝群答疑解惑
已收录:HashMap、ReentrantLock、ThreadPoolExecutor、《Spring 源码解读》、《Dubbo 源码解读》…..
????【面试题集】(持续更新)<br/>1. 面试题选材:Java 面试常问的所有面试题和必会知识点 <br/>2. 内容大纲:第一部分”注意事项“、第二部分“面试题解读”(包括:”面试题“、”答案“、”答案详解“、“实际开发解说”)
3. 深度 / 广度:面试题集中的答案和答案详解,都是对齐一般面试要求的深度和广度
4. 读后问题:粉丝群答疑解惑
已收录:Java 基础面试题集、Java 并发面试题集、JVM 面试题集、数据库 (Mysql) 面试题集、缓存 (Redis) 面试题集 …..
????【粉丝群】(持续更新) <br/>收录:阿里、字节跳动、京东、小米、美团、哔哩哔哩等大厂内推
???? 作者介绍:Spring 系源码贡献者、世界五百强互联网公司、TeamLeader、Github 开源产品作者
???? 作者微信:wowangle03(企业内推联系我)

  加入我的粉丝社群,阅读更多内容。从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入!

第 1 章 阅读指南

  • 本书基于 Spring 5.0.x(5.0.16.BUILD-SNAPSHOT)版本。
  • 本书根据”企业应用 Demo“解读源码。
  • 本书建议分为两个学习阶段,掌握了第一阶段,再进行第二阶段;

    • 第一阶段,理解章节“源码解读”前的所有内容。即掌握 IT 技能:熟悉 Spring 原理。
    • 第二阶段,理解章节“源码解读”(包括源码解读)之后的内容。即掌握 IT 技能:精读 Spring 源码。
  • 建议按照本书内容顺序阅读(内容前后顺序存在依赖关系)。
  • 阅读过程中,如果遇到问题,记下来,后面不远的地方肯定有解答。
  • 阅读章节“源码解读”时,建议获得中文注释源码项目配合本书,Debug 进行阅读学习。
  • 源码项目中的注释含义;

    • ”企业应用 Demo“在源码中,会标注“// Spring Demo”。
    • 在源码中的不易定位到的主线源码,会标注“// tofix 主线”。
  • 以下注释的源码,暂时不深入讲解:

    • 在执行“企业应用 Demo”过程中,没有执行到的源码(由于遍历空集合、if 判断),会标注“/* Demo 不涉及 /”。
    • 在执行”企业应用 Demo“过程中,有用变量的数据转换方法,输入值和输出值相同(由于遍历空集合、if 判断没有处理数据),会标注“/* 无效果 /”。
    • 从头到尾都是空的变量(包括不包含元素的集合),会标注“/* 空变量 /”。
    • 有被赋值的变量,但“企业应用 Demo”运行过程中没有使用到该变量,会标注”/* 无用逻辑 /“。
    • 不是核心逻辑,并且不影响源码理解,会标注”/* 非主要逻辑 /“。
    • 锁、异常处理逻辑、非空校验、日志打印没有标注注释。

第 2 章 Spring 实战

2.1 源码本地构建

  1. 下载作者详细中文注释后的 Spring 源码;
  2. 保证本地已经安装 jdk(最好 1.8)、gradle(最好 4.4.1 版本);
  3. 进入项目根目录,打开 cmd 或者 git 命令行,输入 ./gradlew :spring-oxm:compileTestJava 进行编译,如果中途编译失败,就重复编译几次;
  4. 然后使用 Idea 导入项目,使用 gradle 进行 import;
  5. 全局搜索“// Spring Demo”,运行“企业应用 Demo”。

2.2 基础入门 Demo

代码示例 1

public class PersionA {
    private PersionB pb;

    public PersionB getPb() {return pb;}

    public void setPb(PersionB pb) {this.pb = pb;}
}

代码示例 2

public class PersionB {
    private PersionA pa;

    public PersionA getPa() {return pa;}

    public void setPa(PersionA pa) {this.pa = pa;}
}

代码示例 3 application.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
>
    <bean id="persionA" class="org.springframework.demo.PersionA">
        <property name="pb" ref="persionB"/>
    </bean>
    <bean id="persionB" class="org.springframework.demo.PersionB">
        <property name="pa" ref="persionA"/>
    </bean>

</beans>

代码示例 4 启动 Spring

public class Test {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:applicationContext.xml");
        PersionA persion = (PersionA) context.getBean("persionA");
        System.out.println(persion.getPb());
    }
}

2.3 企业应用 Demo

  暂时由“基础入门 Demo”代替,”企业应用 Demo“以及配套的第 2 版《Spring 源码解读》正在修改中 …。

第 3 章 相关 Java 基础

3.1 实例化接口


代码示例 1 接口

public interface Person {public abstract void eat();
}

代码示例 2 实例化接口

public class Demo {public static void main(String[] args) {
        // 使用 Lambda 直接创建 Persion 接口实例
        Person p1 = () -> System.out.println("eat something!");
        // 打印结果:eat something!p1.eat();}
}

  直接实例化接口适用于在多个不同的调用场合,抽象方法会有不同实现逻辑的场景。反过来想,如果不使用 Lambda 创建 Persion 接口实例,想要在不同调用场合,执行不同实现逻辑,就必须为每个场合定义一个实现了接口的类,然后在实现类的方法里实现对应逻辑。这样比较,使用 Lambda 表达式直接创建接口实例是不是大大简化了代码呢!

第 4 章 源码基础

4.1 导读

   1. Spring 中的对象种类

  在”企业应用 Demo“中,有涉及到两种对象:Pojo、Bo;

  • Pojo(plian ordinary java object):仅包含属性以及属性的 get、set、add、remove、is、has 方法的对象;
  • Bo(business object):就是封装着业务逻辑的对象。

4.2 ClassPathXmlApplicationContext

  ClassPathXmlApplicationContext(Bo),继承自 AbstractRefreshableConfigApplicationContext,调用构造函数实例化的同时,启动了 Spring;在“企业应用 Demo”中主要负责创建 DefaultListableBeanFactory 工厂对象和 XmlBeanDefinitionReader 对象来执行逻辑。

代码示例 重要成员变量

public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {
    
    // 继承自 AbstractRefreshableConfigApplicationContext
    // configLocations 表示 application.xml 配置文件的路径,可以配置多个配置文件,例:["classpath*:applicationContext.xml"]
    private String[] configLocations;
    
    // 继承自 AbstractRefreshableApplicationContext
    // beanFactory 表示 DefaultListableBeanFactory 实例,总的来说就是用于生成 Bean 的工厂。比如:生成目标对象(例:PersionA)。private DefaultListableBeanFactory beanFactory;

4.3 XmlBeanDefinitionReader

  XmlBeanDefinitionReader(Bo),简称:XmlBean 定义读取器;负责读取 application.xml 配置文件,解析成 Resource 实例(Spring 定义的,是用来封装文件资源的类),再根据 Resource 实例获取到的 inputStream 输入流,转化成 Document 实例;然后交由 DefaultBeanDefinitionDocumentReader 对象进行下一步操作。

代码示例 重要成员变量

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    // resourceLoader 表示 ClassPathXmlApplicationContext 实例,负责读取 applicaiton.xml 文件为 Resource 实例
    private ResourceLoader resourceLoader;

    // documentLoader 负责将代表 Resource 的 inputStream 输入流转换成 Document 对象
    private DocumentLoader documentLoader = new DefaultDocumentLoader();

    // resourcesCurrentlyBeingLoaded 保存当前线程加载了的 application.xml 配置文件(包括 <import/> 标签引进的 xml 资源),用来检查 <import/> 造成的循环导入
    private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded =
            new NamedThreadLocal<>("XML bean definition resources currently being loaded");

    // 继承自 AbstractBeanDefinitionReader
    // registry 表示 DefaultListableBeanFactory 实例,XmlBeanDefinitionReader 是通过构造函数获取到并持有的这个对象,是为了向后续逻辑传递下去
    private final BeanDefinitionRegistry registry;

4.4 DefaultBeanDefinitionDocumentReader

  DefaultBeanDefinitionDocumentReader(Bo),简称:Bean 定义 Document 读取器;负责读取 Document 实例中的节点信息,如:< bean/>、< import/> 等;然后交由 BeanDefinitionParserDelegate 对象进行下一步操作。

代码示例 重要成员变量

public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {

    // readerContext 表示 XmlReaderContext 实例,Xml 读取器上下文
    // 作用:为了将 XmlBeanDefinitionReader 实例、Resource 实例、DefaultListableBeanFactory 实例等 统一封装到 XmlReaderContext 里,向后传递使用
    private XmlReaderContext readerContext;

    // delegate 代表 BeanDefinitionParserDelegate 实例;负责将 Document 对象(例如:< bean/>)装载为 GenericBeanDefinition
    private BeanDefinitionParserDelegate delegate;

4.5 BeanDefinitionParserDelegate

  BeanDefinitionParserDelegate(Bo),简称:Bean 定义解析代理;负责将 Document 对象(例如:< bean/>)装载成 GenericBeanDefinition 对象;然后交由 DefaultListableBeanFactory 对象进行下一步操作。

代码示例 重要成员变量

public class BeanDefinitionParserDelegate {

    // readerContext 表示 XmlReaderContext 实例,上文提过,是 Xml 读取器上下文
    // 用于从 XmlReaderContext 里获取 DefaultListableBeanFactory 实例等对象)private final XmlReaderContext readerContext;

4.6 GenericBeanDefinition

  GenericBeanDefinition(Pojo),简称:通用 Bean 定义;是用来装载 < bean/> 配置的实体类。

代码示例 重要成员变量

public class GenericBeanDefinition extends AbstractBeanDefinition {

    // beanClass 对应 <bean class=""/> 中的 class 值,一开始会被赋值为 class 设置的字符串,后面会被赋值为解析后的 Class 对象
    private volatile Object beanClass;

    // 继承自 AbstractBeanDefinition
    // propertyValues 是保存 <property/> 信息的实体类
    private MutablePropertyValues propertyValues;

4.7 BeanDefinitionHolder

  BeanDefinitionHolder(Pojo),简称:Bean 定义持有者;负责持有 GenericBeanDefinition 对象(也就是说 GenericBeanDefinition 对象是 BeanDefinitionHolder 一个成员变量),在“企业应用 Demo”中,只起到数据传输的作用。

  BeanDefinitionHolder 的必要性和 BeanNameAware 相关;”企业应用 Demo“不涉及。

代码示例 重要成员变量

public class BeanDefinitionHolder implements BeanMetadataElement {

    // beanDefinition 表示 GenericBeanDefinition 实例;用于装载从 application.xml 配置文件中解析出来的 < bean/> 
    private final BeanDefinition beanDefinition;

    // beanName 表示 <bean/> 在 Spring 中的名字,一般为 <bean id=""/> 中的 id 值
    private final String beanName;

4.8 DefaultListableBeanFactory

  DefaultListableBeanFactory(Bo);总的来说就是生成 Bean 的工厂。在”企业应用 Demo“中,主要负责根据 GenericBeanDefinition 对象,生成目标对象(例:PersionA)并注入属性值,放到 Map<String, Object> singletonObjects 里保存起来,供应用程序使用。

代码示例 重要成员变量

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
        implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

    // beanDefinitionNames 用于存放从 application.xml 文件中解析出来的 beanName(一般为 <bean/> 标签中的 id 值),按注册顺序排列
    private volatile List<String> beanDefinitionNames = new ArrayList<>(256);
    
    // 数据结构:{beanName -> GenericBeanDefinition 对象}, 用于后续逻辑根据 beanName 获取 GenericBeanDefinition 实例来用
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

    // 继承自 AbstractBeanFactory
    // 数据结构:{beanName -> RootBeanDefinition 实例} 缓存, 用于防止重复创建 beanName 的 RootBeanDefinition
    private final Map<String, RootBeanDefinition> mergedBeanDefinitions = new ConcurrentHashMap<>(256);

    // 继承自 DefaultSingletonBeanRegistry
    // singletonsCurrentlyInDestruction 用于标记 Spring 是否处在销毁单例的过程,默认为 false;如果设置为 true,创建单例 bean 的时候,就会抛出 BeanCreationNotAllowedException 异常
    private boolean singletonsCurrentlyInDestruction = false;
    
    /**
     * 
     * 以下成员变量,参考下一节“循环依赖”,进行理解
     *
     */

    // 继承自 DefaultSingletonBeanRegistry
    // singletonsCurrentlyInCreation 中存储的 beanName 都是处于创建过程中的
    // 当该目标对象创建完成后,会将对应的 beanName 从 singletonsCurrentlyInCreation 集合中剔除掉
    // 作用:主要用于解决 IOC 循环依赖的问题
    private final Set<String> singletonsCurrentlyInCreation =
            Collections.newSetFromMap(new ConcurrentHashMap<>(16));

    // 继承自 DefaultSingletonBeanRegistry
    // 数据结构:{beanName -> 单例 bean 对象(例:PersionA 实例)},也被称为一级缓存;// 添加场景:在“企业应用 Demo”中,当 application.xml 中定义的 bean 对象被完全实例化后(完全实例化是指实例化并注入属性值后),则会被放入本缓存中
    // 作用:单例 bean 会被放到这个缓存里,当应用系统想要得到 Spring 管理的 <bean/> 时(例:context.getBean("persion");),就可以直接从这个缓存里获取
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    // 继承自 DefaultSingletonBeanRegistry
    // 数据结构:{beanName —> ObjectFactory 接口的实现类实例} , 也称为三级缓存
    // ObjectFactory 是生产目标对象的工厂接口;不同场景下,生成对象的逻辑不同,所以这里使用了工厂接口(比如:生成 AOP 代理对象和通过构造函数生成普通对象等)// 添加场景:在“企业应用 Demo”中,当 bean 对象(例:PersionA 实例)实例化后,没有注入属性值之前,会放入本缓存;// 移除场景:在“企业应用 Demo”中,当 bean 对象完全实例化后,则会从本缓存中剔除掉
    // 作用:在“企业应用 Demo”中,singletonObjects 存放的 value 值是 lambda 表达式创建的 ObjectFactory 接口实例;// singletonFactories 是创建目标对象过程中,用于存放处于中状态对象的临时容器
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    
    // 继承自 DefaultSingletonBeanRegistry
    // 数据结构:{beanName -> bean 对象(例:PersionA 实例)},是提前曝光的单例对象缓存,也称为二级缓存;// 添加场景:在“企业应用 Demo”中,当 bean 对象(例:PersionA 实例)实例化后,发现成员变量指定了其它 Spring 管理的 <bean/> 对象(例:PersionB 实例),并且这个 <bean/> 对象(例:PersionB 实例)存在于 singletonsCurrentlyInCreation,也存在于三级缓存中,就会把 PersionB 对象从三级缓存中移除,放到二级缓存里;(如果未发生过循环依赖的场景,二级缓存从始至终没有存在过值)// 移除场景:在“企业应用 Demo”中,当 bean 对象完全实例化后,则会从本缓存中剔除掉
    // 作用:用于解决 IOC 循环依赖的问题
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

4.9 循环依赖

1. 问题

  如果 persionA 依赖 persionB,persionB 依赖 persionA,Spring 是如何初始化 PersionA 和 PersionB 的?(这个现象称之为”循环依赖“)

2. 答案

persionA persionB
1. 要开始创建 persionA 对象之前,向 singletonsCurrentlyInCreation 添加该对象的对应 beanName(旨在标记 persionA 处于’创建中‘)
2. 创建完 persionA 对象后,再把 persionA 对象存在三级缓存中,再准备开始注入属性
3. 注入属性时,发现依赖 persionB,要创建 persionB 对象
总结:这时,persionA 对象的 pb 属性为空;三级缓存包含 persionA
1. 要开始创建 persionB 对象之前,向 singletonsCurrentlyInCreation 添加该对象的对应 beanName(旨在标记 persionB 处于’创建中‘)
2. 创建完 psersionB 对象后,再把 persionB 对象存在三级缓存里,再准备开始注入属性
2. 注入属性时,发现依赖 persionA 对象
3. 创建 persionA,发现 singletonsCurrentlyInCreation 存有相应 beanName(PersionA 处于 ’ 创建中 ’),并且保存在三级缓存里,则直接从三级缓存中移除 PersionA 对象,然后放到二级缓存里
4. 将 persionA 对象注入到 persionB 对象的 pa 属性里
5. 从三级缓存中删掉 persionB 对象,放到一级缓存里
总结:这时,persionB 对象的 pa 属性不为空,但是 pa 属性 (persionA) 的 pb 属性为空;二级缓存包含 persionA,一级缓存包含 persionB
4. 获得 persionB 对象,放到 pb 属性里
5. 从二级缓存中删掉 persionA 对象,放到一级缓存里
总结:这时,persionA 对象的 pb 属性不为空,pb 属性的 pa 属性也不为空;一级缓存包含 persionA 和 persionB

4.10 RootBeanDefinition

  RootBeanDefinition(Pojo);< bean/> 有继承的能力(< bean parent=””/>),所以 Spring 会进行对父子 < bean/> 进行合并操作,最后合并成 RootBeanDefinition 实例,区别于 GenericBeanDefinition 实例。

代码示例 重要成员变量

public class RootBeanDefinition extends AbstractBeanDefinition {

    // beanClass 表示 <bean class="org.springframework.PersionA"/> 中的 class 值,一开始为 "org.springframework.PersionA" 字符串,然后会解析为 PersionA Class 对象,再 set 到当前属性里
    private volatile Object beanClass;

    // 继承自 AbstractBeanDefinition
    // propertyValues 表示对应 < property/> 信息的实体类
    private MutablePropertyValues propertyValues;

    // scope 表示 <bean/> 的单例模式;scope 默认为 "",当 Spring 发现值是空字符串时,并且用户没有指定,就会将值修改为"singleton"(单例),也就是说 Spring 的 <bean/> 默认是单例的。private String scope = "";

4.11 MutablePropertyValues

  MutablePropertyValues(Pojo),在 Spring 中,用于封装 < property/> 信息的实体类。

代码示例 重要成员变量

public class MutablePropertyValues implements PropertyValues, Serializable {

    // propertyValueList 表示 <property/> 集合(集合中的一个元素对应一个 <property/> 标签)private final List<PropertyValue> propertyValueList;

4.12 PropertyValue

  PropertyValue(Pojo),在 Spring 中,对应 < property/> 的实体类,保存了 < property/> 的配置信息,比如属性名、属性值等;这里 Spring 之所以不使用 Map 这种键值对类,是因为自定义类有更强的扩展性。

代码示例 重要成员变量

public class PropertyValue extends BeanMetadataAttributeAccessor implements Serializable {

    // name 表示 <property/> 中的 name 属性
    private final String name;

    // value 表示 <property/> 指定的值;例:<property name="persionB" ref="pB">,value 一开始为指代 ref 的 RuntimeBeanReference 对象;后续逻辑将 RuntimeBeanReference 对象解析为 PersionB 对象,重新覆盖到 value 属性上
    private final Object value;

    // source 是 PropertyValue 实例;在将 RuntimeBeanReference 对象解析为 PersionB 对象之前和之后,分别会使用两个 PropertyValue 对象去装载,这个 source 属性就是之前的 PropertyValue 对象(例:<property name="persionB" ref="pB">)private Object source;

    // conversionNecessary 表示是否转换 <Property/> 指定的值,默认为空(表示有必要进行转换),Spring 就会查找用户自定义的转换器进行转换,当发现没有转换器,就会将 conversionNecessary 设置为 false,不需要转换
    volatile Boolean conversionNecessary;

4.13 RuntimeBeanReference

  RuntimeBeanReference(Pojo),在 Spring 中,用于装载 < property ref=””/> 中 ref 指定的值。

代码示例 重要成员变量

public class RuntimeBeanReference implements BeanReference {

    // beanName 表示 ref 的值
    private final String beanName;

4.14 BeanWrapperImpl

  BeanWrapperImpl(Bo),持有目标对象(例:PersionA 对象),可以对其设置 / 获取属性的描述信息,比如:查询只读 / 可写属性等。”企业应用 Demo”中,主要负责给 < bean/> 实例化后的目标对象(例:PersionA 对象)注入的 < property/> 所配置的值。

代码示例 重要成员变量

public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper {

    // 继承自 AbstractNestablePropertyAccessor
    // wrappedObject 表示 <bean/> 所代表的目标对象,例:PersionA 对象
    Object wrappedObject;

    // 私有类
    // BeanPropertyHandler 表示通用属性描述器,用于保存 < property/> 中的相关信息(例:属性所属类 Class、属性对应 get/set 方法 Method 对象等);”企业应用 Demo”中,主要用于从本对象获取相应属性的 set 方法 Method 对象
    private class BeanPropertyHandler extends PropertyHandler{...}

4.15 BeanWrapperImpl$BeanPropertyHandler

  BeanWrapperImpl$BeanPropertyHandler(Bo)($ 表示 BeanPropertyHandler 是 BeanWrapperImpl 中的内部类),Bean 属性处理器,负责管理 < property/> 属性;”企业应用 Demo”中,主要负责利用反射,调用 setter 方法,对 < bean/> 设置 < property /> 指定的值。

代码示例 重要成员变量

public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper {
    
private class BeanPropertyHandler extends PropertyHandler {

    // GenericTypeAwarePropertyDescriptor 实例,继承自 PropertyDescriptor(来自第三方 jar 包),可以获得类的信息,比 setter 方法、setter 方法的参数类型等
    private final PropertyDescriptor pd;

4.16 GenericTypeAwarePropertyDescriptor

  GenericTypeAwarePropertyDescriptor(Pojo),通用属性描述器,用于保存 < property/> 中的相关信息(例:属性所属类 Class、属性的对应 get/set 方法 Method 对象等);”企业应用 Demo”中,主要用于从本对象获取相应属性的 set 方法 Method 对象。

代码示例 重要成员变量

final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor {

    // beanClass 表示 <property/> 的所属类的 Class 对象(例:PersionA.Class)private final Class<?> beanClass;

    // 继承自 PropertyDescriptor(第三方 jar)// writeMethodRef 记录了 <bean/> 对象(例:PersionA)的 set<property/> 的 Method 对象,用于获取 set 方法反射设置属性值
    private final MethodRef writeMethodRef = new MethodRef();

    // writeMethod 表示 setter 方法的 Method 对象
    private final Method writeMethod;

    // propertyType 表示 <property/> 指代值的 Class 对象
    private Class<?> propertyType;

<br/>

加入我的粉丝社群,阅读全部内容

  从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入,这样的知识星球,难道你还要犹豫!

正文完
 0