《Spring 源码解读》

《Spring 源码解读》

傻瓜源码-内容简介

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

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

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

图片

第 1 章 阅读指南

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

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

    • ”企业应用 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

代码示例 1spring

public class PersionA {
    private PersionB pb;

    public PersionB getPb() {
        return pb;
    }

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

代码示例 2sql

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,天天给你答疑解惑,还能有第二份收入,这样的知识星球,难道你还要犹豫!

图片

相关文章
相关标签/搜索