SpringBoot 啟動(dòng)過程源碼分析

主要了解和學(xué)習(xí)下SpringBoot啟動(dòng)的大致原理是如何须揣,以及知道幾個(gè)注解的真正含義和用途是什么暮屡,SpringBoot就可以以SpringApplication.run(Bootstrap.class);這樣的一句代碼作為啟動(dòng)的入口

1奴艾、SpringApplication 對象實(shí)例化

SpringApplication 文件

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
      // 傳遞的source其實(shí)就是類Bootstrap
    return new SpringApplication(sources).run(args);
    // 實(shí)例化一個(gè)SpringApplication對象執(zhí)行run方法
}

實(shí)例化的時(shí)候又會(huì)執(zhí)行initialize 方法

private void initialize(Object[] sources) {
      // 這個(gè)source依舊是上文說的Bootstrap.class 類
    if (sources != null && sources.length > 0) {
        this.sources.addAll(Arrays.asList(sources));
        // 添加到source資源列表里面去
    }
    this.webEnvironment = deduceWebEnvironment();
    // 設(shè)置其是否為web環(huán)境
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 拆分為兩步谅海,一步是getSpringFactoriesInstances,再者就是set操作
    // set操作很簡單图筹,就是設(shè)置當(dāng)前對象的初始化對象以及監(jiān)聽器
    this.mainApplicationClass = deduceMainApplicationClass();
    // 通過堆棧信息卫漫,推斷 main方法的類對象為當(dāng)前的主程序類
}

private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };

private boolean deduceWebEnvironment() {
    for (String className : WEB_ENVIRONMENT_CLASSES) {
           // 遍歷包含上述兩個(gè)類名稱的數(shù)組
        if (!ClassUtils.isPresent(className, null)) {
               // 一旦發(fā)現(xiàn)不存在該類菲饼,就立即返回 deduce 推斷不是web環(huán)境
            return false;
        }
    }
    // 必須同時(shí)包含兩個(gè)類,才推斷出為web環(huán)境
    return true;
}

getSpringFactoriesInstances 方法操作

private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
        // 傳遞的type就是上面說的ApplicationContextInitializer.class以及ApplicationListener.class類
        // 類型以及參數(shù)目前都沒有具體指
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<String>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
            // 通過SpringFactoriesLoader 獲取對應(yīng)的名稱列赎,具體詳情可以看下面的代碼塊
            // 這點(diǎn)需要重點(diǎn)關(guān)注下:暝谩!包吝!
            // 結(jié)果就是返回一個(gè)set集合
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
            // 看樣子就是創(chuàng)建一個(gè)實(shí)例的集合
    AnnotationAwareOrderComparator.sort(instances);
    // 然后通過AnnotationAwareOrderComparator 的排序規(guī)則跪?qū)嵗线M(jìn)行排序
    // 排序就是看是否存在Order或者Priority注解饼煞,然后取得注解的值,排在集合前面
    return instances;
}

private <T> List<T> createSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
        Set<String> names) {
    List<T> instances = new ArrayList<T>(names.size());
    for (String name : names) {
           // 遍歷上面取到的name 集合
        try {
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            // 取到這個(gè)類名稱的類
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass
                    .getDeclaredConstructor(parameterTypes);
            // 獲取當(dāng)前類的符合當(dāng)前參數(shù)的構(gòu)造器
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            // 利用反射的方式生成具體的對象
            instances.add(instance);
        }
        catch (Throwable ex) {
            throw new IllegalArgumentException(
                    "Cannot instantiate " + type + " : " + name, ex);
        }
    }
    // 最后生成name映射的實(shí)例集合
    return instances;
}

SpringFactoriesLoader.loadFactoryNames 方法

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    // 傳遞的factoryClass 就是上面的ApplicationContextInitializer诗越、ApplicationListener.等
    String factoryClassName = factoryClass.getName();
    // 獲取類的全名稱
    try {
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        // 如果類加載器為null砖瞧,則使用系統(tǒng)默認(rèn)的方法,否則使用當(dāng)前傳遞的類加載器讀取
        // 當(dāng)前類加載器可以獲取到的所有文件路徑為“META-INF/spring.factories” 的地址
        
        List<String> result = new ArrayList<String>();
        while (urls.hasMoreElements()) {
              // 迭代遍歷url
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            // 讀取映射的spring.factories 文件的KV鍵值對嚷狞,存放到properties對象中
            String factoryClassNames = properties.getProperty(factoryClassName);
            // 類似于map一般块促,獲取對應(yīng)的值
            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
            // 對值使用逗號(hào)分隔,生成list感耙,然后去重添加到result
        }
        
        // 總結(jié)下來就是遍歷當(dāng)前類環(huán)境中的所有路徑為“META-INF/spring.factories”的文件
        // 讀取文件褂乍,然后獲取k為當(dāng)前類名稱的所有值,然后存儲(chǔ)到set中返回
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

到這里整個(gè)的initialize操作就已經(jīng)清楚了即硼,通過類加載器可獲取的所有為“META-INF/spring.factories” 的地址的文件內(nèi)容,然后獲取key為ApplicationContextInitializer.class以及ApplicationListener.class的類名稱的值集合
然后依次就行實(shí)例化屡拨,最后排序返回只酥,最后保存到當(dāng)前對象的初始化集合以及監(jiān)聽器集合中褥实,便于后續(xù)操作

需要注意到SpringFactoriesLoader.loadFactoryNames 后面很多地方都需要使用該方法去獲取相關(guān)內(nèi)容

當(dāng)然現(xiàn)在只是完成了SpringApplication構(gòu)造器里面的方法,還剩下后面的run(args)方法執(zhí)行

如下代碼塊就是SpringBoot的執(zhí)行過程(最后的套路依舊是Spring Framework的執(zhí)行策略)

2裂允、SpringApplication的run方法啟動(dòng)

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 記錄當(dāng)前服務(wù)開始啟動(dòng)
    ConfigurableApplicationContext context = null;
    // 上下文context损离,非常關(guān)鍵
    FailureAnalyzers analyzers = null;
    configureHeadlessProperty();
    // 給系統(tǒng)設(shè)置headless屬性值
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 就是通過SpringFactoriesLoader 獲取到所有SpringApplicationRunListener.class的對象
    // 其中args是用來進(jìn)行實(shí)例化SpringApplicationRunListener對應(yīng)的對象的構(gòu)造器參數(shù)
    // 最后返回listener是整個(gè)系統(tǒng)的監(jiān)聽器
    listeners.starting();
    // 監(jiān)聽器開始執(zhí)行
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 默認(rèn)程序參數(shù)
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        // 準(zhǔn)備運(yùn)行的環(huán)境上下文
        Banner printedBanner = printBanner(environment);
        // 打印banner,默認(rèn)輸出當(dāng)前springboot版本等內(nèi)容绝编,可以自定義設(shè)置文本或者圖片
        // 具體看下面的方法詳解
        context = createApplicationContext();
        // 創(chuàng)建SpringBoot最重要的上下文容器
        analyzers = new FailureAnalyzers(context);
        // 分析上下文出現(xiàn)問題的點(diǎn)僻澎,便于使用者可以直觀的發(fā)現(xiàn)問題出現(xiàn)在哪里
        // 其實(shí)套路類似,就是使用SpringFactoriesLoader獲取所有的FailureAnalyzer實(shí)例對象十饥,然后設(shè)置其bean工廠為context的bean工廠上下文
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        // 看名稱就是對context的前置準(zhǔn)備工作窟勃,細(xì)節(jié)在后面說
        refreshContext(context);
        // 切入到spring framework的方式去完成context內(nèi)容的裝載
        // 如果需要注冊終止鉤子,則注冊一個(gè)
        afterRefresh(context, applicationArguments);
        // 基本上認(rèn)為springboot所需的服務(wù)都加載完成逗堵,進(jìn)行最后的處理操作
        // 里面常用的就是CommandLineRunner
        listeners.finished(context, null);
        // 監(jiān)聽器的啟動(dòng)結(jié)束事件秉氧,
        stopWatch.stop();
        // 表示SpringBoot服務(wù)啟動(dòng)步驟完成,統(tǒng)計(jì)下啟動(dòng)時(shí)間等操作
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
              // 打印SpringBoot啟動(dòng)成功的消息蜒秤,例如 Started xxx in 12.4 seconds 等信息
        }
        return context;
    }
    catch (Throwable ex) {
        handleRunFailure(context, listeners, analyzers, ex);
        // 啟動(dòng)失敗了就會(huì)輸出Application startup failed 日志
        // 并且會(huì)輸出具體的錯(cuò)誤內(nèi)容信息
        throw new IllegalStateException(ex);
    }
}

private ConfigurableEnvironment prepareEnvironment(
        SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 如果當(dāng)前環(huán)境值不為null汁咏,直接返回
    // 否則根據(jù)上文推斷出的webEnvironment boolean 值 生成對象的環(huán)境對象
    // 當(dāng)為true的時(shí)候,生成StandardServletEnvironment
    // 否則生成的是StandardEnvironment
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
    if (!this.webEnvironment) {
          // 如果不是web的環(huán)境作媚,再對當(dāng)前的環(huán)境進(jìn)行包裝攘滩,生成一個(gè)新的運(yùn)行環(huán)境對象
        environment = new EnvironmentConverter(getClassLoader())
                .convertToStandardEnvironmentIfNecessary(environment);
    }
    return environment;
}

private Banner printBanner(ConfigurableEnvironment environment) {
     // 參數(shù)environment就是上面生成的環(huán)境對象
    if (this.bannerMode == Banner.Mode.OFF) {
          // 如果設(shè)置了banner關(guān)閉模式,則不進(jìn)行打印輸出操作
        return null;
    }
    ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader
            : new DefaultResourceLoader(getClassLoader());
    // 資源加載器生成
    SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
            resourceLoader, this.banner);
    // 后續(xù)使用SpringApplicationBannerPrinter 類的print進(jìn)行輸出操作
    if (this.bannerMode == Mode.LOG) {
          // 打印模式纸泡,如果是log則輸出到log中轰驳,否則輸出到終端中
        return bannerPrinter.print(environment, this.mainApplicationClass, logger);
    }
    return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
    // 大致操作就是先看是否存在自定義的圖片類型或者文字類型 banner,如果有就優(yōu)先確定banner對象
    // 否則就默認(rèn)使用SpringBootBanner的banner(這個(gè)里面就包含了常規(guī)的springboot輸出內(nèi)容)
    // 然后解析banner的資源弟灼,得出將要輸出的字符串內(nèi)容(利用日志直接輸出)级解,存儲(chǔ)到PrintedBanner
}

public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework."
        + "boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext";

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
        + "annotation.AnnotationConfigApplicationContext";

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            contextClass = Class.forName(this.webEnvironment
                    ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
            // 如果是web環(huán)境,則使用AnnotationConfigEmbeddedWebApplicationContext
            // 否則就使用AnnotationConfigApplicationContext
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                    "Unable create a default ApplicationContext, "
                            + "please specify an ApplicationContextClass",
                    ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
    // 直接通過類田绑,反射生成無構(gòu)造參數(shù)的對象勤哗,一般情況就是AnnotationConfigEmbeddedWebApplicationContext對象了
}

private void prepareContext(ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {
    // 傳遞上下文、環(huán)境掩驱、上下文參數(shù)等數(shù)據(jù)
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    // 前置處理context上下文芒划,包含了beanNameGenerator和resourceLoader
    // 其中beanNameGenerator 可以自定義規(guī)則約定bean的名稱功能
    applyInitializers(context);
    // 應(yīng)用ApplicationContextInitializer去初始化完成對context的操作
    // 具體的ApplicationContextInitializer對象就是在SpringApplication對象的構(gòu)造方法中實(shí)例化創(chuàng)建的
    // 可以給context添加額外的操作,同時(shí)也可以很方便的自定義完成自己需要的功能
    listeners.contextPrepared(context);
    // 執(zhí)行contextPrepared 上下文準(zhǔn)備工作的事件
    if (this.logStartupInfo) {
           // 日志啟動(dòng)標(biāo)志位欧穴,默認(rèn)為true
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
        // 明確當(dāng)前執(zhí)行的主函數(shù)log民逼,輸出SpringBoot的開始啟動(dòng)信息
    }

    // 注冊springApplicationArguments 這個(gè)bean到context中去
    context.getBeanFactory().registerSingleton("springApplicationArguments",
            applicationArguments);
    if (printedBanner != null) {
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
        // 同樣是注冊,打印早就完成了
    }

    // Load the sources
    Set<Object> sources = getSources();
    // 一般情況下這個(gè)source就是SpringBoot 啟動(dòng)的主類Class涮帘,注意不是實(shí)例對象
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[sources.size()]));
    // 把source也就是主類當(dāng)做bean拼苍,加載到spring的容器中
    listeners.contextLoaded(context);
    // 監(jiān)聽器的上下文導(dǎo)入完成事件 執(zhí)行
}

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<Object>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    // 從context獲取ApplicationRunner和CommandLineRunner 對象
    // 然后按照對應(yīng)的規(guī)則進(jìn)行排序
    for (Object runner : new LinkedHashSet<Object>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
        // 分別執(zhí)行各自的run方法
    }
    // 一般情況,我們?nèi)绻枰赟pringBoot加載完成后需要完成一些自定義操作就是注冊
    // ApplicationRunner或者CommandLineRunner 的bean對象调缨,然后自定義實(shí)現(xiàn)run方法即可
}

3疮鲫、總結(jié)

就SpringBoot的啟動(dòng)整個(gè)過程而已吆你,還是很清晰的,SpringBoot的套用SpringFramework的機(jī)制俊犯,為我們自定義實(shí)現(xiàn)功能提供了很好的便利妇多,整個(gè)的SpringBoot就是重新包裝了一個(gè)SpringFramework。

里面有一個(gè)點(diǎn)是SpringFactoriesLoader.loadFactoryNames燕侠,從Spring3.2加入的功能者祖,可以讀取META-INF/spring.factories文件需要的內(nèi)容數(shù)據(jù),例如SpringBoot中EnableAutoConfiguration也是充分使用了該功能實(shí)現(xiàn)的绢彤,后續(xù)也會(huì)針對該功能總結(jié)一篇學(xué)習(xí)筆記

更多關(guān)于Spring的內(nèi)容可以看看Spring 源碼學(xué)習(xí)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末七问,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子杖虾,更是在濱河造成了極大的恐慌烂瘫,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奇适,死亡現(xiàn)場離奇詭異坟比,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)嚷往,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門葛账,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人皮仁,你說我怎么就攤上這事籍琳。” “怎么了贷祈?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵趋急,是天一觀的道長。 經(jīng)常有香客問我势誊,道長呜达,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任粟耻,我火速辦了婚禮查近,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挤忙。我一直安慰自己霜威,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布册烈。 她就那樣靜靜地躺著戈泼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上矮冬,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天谈宛,我揣著相機(jī)與錄音次哈,去河邊找鬼胎署。 笑死,一個(gè)胖子當(dāng)著我的面吹牛窑滞,可吹牛的內(nèi)容都是我干的琼牧。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼哀卫,長吁一口氣:“原來是場噩夢啊……” “哼巨坊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起此改,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤趾撵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后共啃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體占调,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年移剪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了究珊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡纵苛,死狀恐怖剿涮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情攻人,我是刑警寧澤取试,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站怀吻,受9級特大地震影響瞬浓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烙博,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一瑟蜈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渣窜,春花似錦铺根、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春掂林,著一層夾襖步出監(jiān)牢的瞬間臣缀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工泻帮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留精置,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓锣杂,卻偏偏與公主長得像脂倦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子元莫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 今日體驗(yàn)赖阻,今天五一節(jié)后上班進(jìn)廠車輛比較多,有的車輛比較急踱蠢,配件也老配不齊耽誤了很多時(shí)間晚上加班把寶馬的大保養(yǎng)做完火欧,...
    王全峰閱讀 131評論 0 0
  • 不知道寫啥,所以啥也不寫了茎截。
    阿風(fēng)聽閱讀 163評論 0 0
  • 文/妙蛙種子君 圖/網(wǎng)絡(luò) (一) 他長得身長七尺五寸苇侵,兩耳垂肩,雙手過膝稼虎,目能自顧其耳衅檀,面如冠玉,唇若涂脂霎俩; 他長...
    解讀男人閱讀 653評論 0 1
  • 01 你長過青春痘嗎? 是不是覺得特丑特難看柳击? 每天再也不敢吃香喝辣通宵達(dá)旦猿推,更要命的是,特么連男(女)朋友也沒法...
    文海珀閱讀 504評論 2 6
  • 開源最佳實(shí)踐:Android平臺(tái)頁面路由框架ARouter Alibaba-ARouter 源碼分析筆記 ARou...
    Speronie閱讀 236評論 0 0