spring-boot-2.0.3不一樣系列之源碼篇 - run方法(四)之prepareContext,絕對有值得你看的地方

来源:https://www.cnblogs.com/youzhibing/archive/2018/09/28/9697825.html
-Advertisement-
Play Games

前言 此系列是針對springboot的啟動,旨在於和大家一起來看看springboot啟動的過程中到底做了一些什麼事。如果大家對springboot的源碼有所研究,可以挑些自己感興趣或者對自己有幫助的看;但是如果大家沒有研究過springboot的源碼,不知道springboot在啟動過程中做了些 ...


前言

  此系列是針對springboot的啟動,旨在於和大家一起來看看springboot啟動的過程中到底做了一些什麼事。如果大家對springboot的源碼有所研究,可以挑些自己感興趣或者對自己有幫助的看;但是如果大家沒有研究過springboot的源碼,不知道springboot在啟動過程中做了些什麼,那麼我建議大家從頭開始一篇一篇按順序讀該系列,不至於從中途插入,看的有些懵懂。當然,文中講的不對的地方也歡迎大家指出,有待改善的地方也希望大家不吝賜教。老規矩:一周至少一更,中途會不定期的更新一些其他的博客,可能是springboot的源碼,也可能是其他的源碼解析,也有可能是其他的。

  路漫漫其修遠兮,吾將上下而求索!

  github:https://github.com/youzhibing

  碼雲(gitee):https://gitee.com/youzhibing

前情回顧

  大家還記得上篇博文講了什麼嗎,或者說大家知道上篇博文講了什麼嗎。這裡幫大家做個簡單回顧:

    創建web應用上下文,對其部分屬性:reader、scanner、beanFactory進行了實例化;reader中實例化了屬性conditionEvaluator;scanner中添加了兩個AnnotationTypeFilter:一個針對@Component,一個針對@ManagedBean;beanFactory中註冊了8個註解配置處理器的Bean。應用上下文類型實際上是AnnotationConfigServletWebServerApplicationContext,beanFactory的類型是DefaultListableBeanFactory,這兩個類型的類圖大家重點看下,既是上篇博文的重點,也是接下來系列博客的基點。創建上下文的過程其實還創建了environment,本文中會涉及到environment,大家請留意。

    通過createApplicationContext方法之後,context的包含的主要內容如下:

prepareContext

  先欣賞下我們的戰績,看看我們對run方法完成了多少的源碼解讀

/**
 * Run the Spring application, creating and refreshing a new
 * {@link ApplicationContext}.
 * @param args the application arguments (usually passed from a Java main method)
 * @return a running {@link ApplicationContext}
 */
public ConfigurableApplicationContext run(String... args) {
    // 秒錶,用於記錄啟動時間;記錄每個任務的時間,最後會輸出每個任務的總費時
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // spring應用上下文,也就是我們所說的spring根容器
    ConfigurableApplicationContext context = null;
    // 自定義SpringApplication啟動錯誤的回調介面
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 設置jdk系統屬性java.awt.headless,預設情況為true即開啟
    configureHeadlessProperty();
    // 獲取啟動時監聽器(EventPublishingRunListener實例)
    SpringApplicationRunListeners listeners = getRunListeners(args)
    // 觸發ApplicationStartingEvent事件,啟動監聽器會被調用,一共5個監聽器被調用,但只有兩個監聽器在此時做了事
    listeners.starting(); 
    try {
        // 參數封裝,也就是在命令行下啟動應用帶的參數,如--server.port=9000
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 準備環境:1、載入外部化配置的資源到environment;2、觸發ApplicationEnvironmentPreparedEvent事件
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        // 配置spring.beaninfo.ignore,並添加到名叫systemProperties的PropertySource中;預設為true即開啟
        configureIgnoreBeanInfo(environment);
        // 列印banner圖
        Banner printedBanner = printBanner(environment);
        // 創建應用上下文,並實例化了其三個屬性:reader、scanner和beanFactory
        context = createApplicationContext();
        // 獲取異常報道器,即載入spring.factories中的SpringBootExceptionReporter實現類
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        // 準備上下文,本文重點
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}
View Code

  前菜

    exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);

      getSpringFactoriesInstances這個方法在之前已經講過,就是載入META-INF/spring.factories中指定類型的bean集合。如下圖

    SpringBootExceptionReporter是一個回調介面,用於支持對SpringApplication啟動錯誤的自定義報告。

    先根據SpringBootExceptionReporter獲取FailureAnalyzers的全限定類名,實例化FailureAnalyzers的時候,再次調用SpringFactoriesLoader.loadFactoryNames方法獲取類型為FailureAnalyzer的名稱列表,然後再根據名稱列表實例化bean列表。

    bean列表創建好之後,設置bean列表中滿足條件的bean的beanFactory和environment,同時也將部分bean應用到context的environment和beanFactory中,代碼如下

private void prepareFailureAnalyzers(List<FailureAnalyzer> analyzers,
        ConfigurableApplicationContext context) {
    for (FailureAnalyzer analyzer : analyzers) {
        prepareAnalyzer(context, analyzer);
    }
}

private void prepareAnalyzer(ConfigurableApplicationContext context,
        FailureAnalyzer analyzer) {
    if (analyzer instanceof BeanFactoryAware) {
        ((BeanFactoryAware) analyzer).setBeanFactory(context.getBeanFactory());
    }
    if (analyzer instanceof EnvironmentAware) {
        ((EnvironmentAware) analyzer).setEnvironment(context.getEnvironment());
    }
}
View Code

    其中NoSuchBeanDefinitionFailureAnalyer bean的setBeanFactory方法

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory);
    this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    this.metadataReaderFactory = new CachingMetadataReaderFactory(
            this.beanFactory.getBeanClassLoader());
    // Get early as won't be accessible once context has failed to start
    this.report = ConditionEvaluationReport.get(this.beanFactory);        // 往beanFactory中註冊autoConfigurationReport
}
View Code

      往beanFactory中註冊一個名叫autoConfigurationReport的單例bean(類型是ConditionEvaluationReport),這個bean用於後面自動配置條件評估的詳情報告與日誌記錄。

    exceptionReporters 獲取成功後,我們來看看beanFactory的變化

  正餐

    prepareContext內容不多,源代碼如下

private void prepareContext(ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {
    // 設置上下文的environment
    context.setEnvironment(environment);
    // 應用上下文後處理
    postProcessApplicationContext(context);
    // 在context refresh之前,對其應用ApplicationContextInitializer
    applyInitializers(context);
    // 上下文準備(目前是空實現,可用於拓展)
    listeners.contextPrepared(context);
    // 列印啟動日誌和啟動應用的Profile
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }

    // Add boot specific singleton beans
    context.getBeanFactory().registerSingleton("springApplicationArguments",
            applicationArguments);                                // 向beanFactory註冊單例bean:命令行參數bean
    if (printedBanner != null) {
        // 向beanFactory註冊單例bean:banner bean
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
    }

    // Load the sources
    Set<Object> sources = getAllSources();                        // 獲取全部資源,其實就一個:SpringApplication的primarySources屬性
    Assert.notEmpty(sources, "Sources must not be empty");        // 斷言資源是否為空
    // 將bean載入到應用上下文中
    load(context, sources.toArray(new Object[0]));
    // 向上下文中添加ApplicationListener,並廣播ApplicationPreparedEvent事件
    listeners.contextLoaded(context);
}
View Code

    我們逐個方法來看

    context.setEnvironment(environment)

/**
 * {@inheritDoc}
 * <p>
 * Delegates given environment to underlying {@link AnnotatedBeanDefinitionReader} and
 * {@link ClassPathBeanDefinitionScanner} members.
 */
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
    super.setEnvironment(environment);            // 設置context的environment
    this.reader.setEnvironment(environment);    // 實例化context的reader屬性的conditionEvaluator屬性
    this.scanner.setEnvironment(environment);    // 設置context的scanner屬性的environment屬性
}
View Code

      將context中相關的environment全部替換成SpringApplication中創建的environment。還記得這篇中的疑問嗎,引申下就是:之前我們的應用中有兩個environment,一個在context中,一個在SpringApplication中。經過此方法後,就只會存在SpringApplication中的environment了,而context中的原environment會被回收

    postProcessApplicationContext(context);

/**
 * Apply any relevant post processing the {@link ApplicationContext}. Subclasses can
 * apply additional processing as required.
 * @param context the application context
 */
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
    if (this.beanNameGenerator != null) {
        context.getBeanFactory().registerSingleton(
                AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
                this.beanNameGenerator);
    }
    if (this.resourceLoader != null) {
        if (context instanceof GenericApplicationContext) {
            ((GenericApplicationContext) context)
                    .setResourceLoader(this.resourceLoader);
        }
        if (context instanceof DefaultResourceLoader) {
            ((DefaultResourceLoader) context)
                    .setClassLoader(this.resourceLoader.getClassLoader());
        }
    }
}
View Code

      上下文後處理。SpringApplication子類可以根據需要應用其他處理。

      由於當前SpringApplication實例的屬性:beanNameGenerator和resourceLoader都為null,所以此方法目前相當於什麼也沒做。此方法可能是我們定製SpringApplication所用。

    applyInitializers(context);

/**
 * Apply any {@link ApplicationContextInitializer}s to the context before it is
 * refreshed.
 * @param context the configured ApplicationContext (not refreshed yet)
 * @see ConfigurableApplicationContext#refresh()
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void applyInitializers(ConfigurableApplicationContext context) {
    for (ApplicationContextInitializer initializer : getInitializers()) {
        // 解析當前initializer實現的ApplicationContextInitializer的泛型參數
        Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
                initializer.getClass(), ApplicationContextInitializer.class);
        // 斷言context是否是requiredType的實例
        Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
        // 向context應用初始化器
        initializer.initialize(context);
    }
}
View Code

      在context refresh之前應用ApplicationContextInitializer到context中。還記得SpringApplication的屬性initializers嗎,不記得的可以點這裡

      一共6個initializer,他們的initialize方法都被調用,源代碼就不跟了,上圖中已經進行了展示,我們總結下

      DelegatingApplicationContextInitializer

        environment沒有context.initializer.classes配置項,所以相當於沒有做任何事。

        如果配置了context.initializer.classes,獲取其值(逗號分隔的initializer列表字元串),轉換成class列表,根據classes列表進行實例化獲取initializer實例列表,再對每個initializer實例調用initialize方法。

        DelegatingApplicationContextInitializer相當於context.initializer.classes的代理,最終還是會執行到被代理的initializer的initialize方法
      ContextIdApplicationContextInitializer

        設置application id:從environment中獲取spring.application.name配置項的值,並把設置成application id,若沒有配置spring.application.name,則取預設值application;

        將application id封裝成ContextId對象,註冊到beanFactory中。

      ConfigurationWarningsApplicationContextInitializer

        向上下文註冊了一個BeanFactoryPostProcessor:ConfigurationWarningsPostProcessor實例;

        實例化ConfigurationWarningsPostProcessor的時候,也實例化了它的屬性Check[] checks,check中只有一個類型是ComponentScanPackageCheck的實例。
      ServerPortInfoApplicationContextInitializer

        向上下文註冊了一個ApplicationListener:ServerPortInfoApplicationContextInitializer對象自己;

        ServerPortInfoApplicationContextInitializer實現了ApplicationListener<WebServerInitializedEvent>,所以他本身就是一個ApplicationListener。
      SharedMetadataReaderFactoryContextInitializer

        向context註冊了一個BeanFactoryPostProcessor:CachingMetadataReaderFactoryPostProcessor實例。
      ConditionEvaluationReportLoggingListener

        將上下文賦值給自己的屬性applicationContext;

        向上下文註冊了一個ApplicationListener:ConditionEvaluationReportListener實例;

        從beanFactory中獲取名為autoConfigurationReport的bean賦值給自己的屬性report。

    listeners.contextPrepared(context);

      還記得SpringApplicationRunListeners中listeners屬性嗎,沒錯,裡面就一個EventPublishingRunListener對象。

      調用EventPublishingRunListener的contextPrepared,發現其是空實現。

      也就是相當於啥事也沒做。

    load(context, sources.toArray(new Object[0]));

      創建了一個BeanDefinitionLoader對象;BeanDefinitionLoader作為AnnotatedBeanDefinitionReader,XmlBeanDefinitionReader和ClassPathBeanDefinitionScanner的門面,從底層源載入bean定義,包括XML和JavaConfig;

      能被載入的source類型包括:Class、Resource、Package和CharSequence四種,每種類型的載入方式也不一樣,Class用AnnotatedBeanDefinitionReader處理、Resource用XmlBeanDefinitionReader處理、Package用ClassPathBeanDefinitionScanner,而CharSequence則比較特殊了,它按Class、Resource、Package的順序處理,哪種處理成功就按哪種處理(CharSequence方式貌似很少用,反正我還沒用過);

      而目前我們的source只有一個:class com.lee.shiro.ShiroApplication,是class類型;先判斷ShiroApplication是否有被component註解修飾,很顯然是(SpringBootApplication註解中包含component註解),那麼AnnotatedBeanDefinitionReader來處理:將com.lee.shiro.ShiroApplication封裝成一個名叫ShiroApplication的BeanDefinition對象,並將其註冊到了beanFactory的BeanDefinitionMap中。

    listeners.contextLoaded(context);

      還記得SpringApplication的屬性listeners嗎,不記得的可以點這裡。將這些ApplicationListener註冊到了上下文中,具體包括ConfigFileApplicationListener,AnsiOutputApplicationListener,LoggingApplicationListener,ClasspathLoggingApplicationListener,BackgroundPreinitializer,DelegatingApplicationListener,ParentContextCloserApplicationListener(實現了ApplicationContextAware介面;將上下文賦值給了屬性context,相當於有了上下文的引用),ClearCachesApplicationListener,FileEncodingApplicationListener,LiquibaseServiceLocatorApplicationListener,EnableEncryptablePropertiesBeanFactoryPostProcessor。

      廣播ApplicationPreparedEvent事件,並觸發對應的事件。過濾出匹配事件的監聽器可以查看這裡,一共過濾出5個監聽器,他們的onApplicationEvent方法會被調用,具體做瞭如下事情:

        ConfigFileApplicationListener

          向context註冊了一個BeanFactoryPostProcessor:PropertySourceOrderingPostProcessor實例;該實例後面會對我們的property sources進行重排序,另外該實例擁有上下文的引用。

        LoggingApplicationListener

          向beanFactory中註冊了一個名叫springBootLoggingSystem的單例bean,也就是我們的日誌系統bean。

        BackgroundPreinitializer

          目前什麼也沒做

        DelegatingApplicationListener

          目前什麼也沒做

        EnableEncryptablePropertiesBeanFactoryPostProcessor

          僅僅列印了一句debug日誌,相當於什麼也沒做

  甜點

    一開始還以為本文內容不會多,但分析分析著,發現內容不少。不管我們是吃撐了還是沒吃飽,都來點甜點收尾。

    一般一個單例對象註冊到beanFactory中,beanFactory會有2個屬性都添加此單例對象信息:singletonObjects、registeredSingletons

      Map<String, Object> singletonObjects = new ConcurrentHashMap<>(),key是bean name,value是單例對象

      Set<String> registeredSingletons = new LinkedHashSet<>(),存放的是bean name

    一般一個bean定義註冊到beanFactory中是,beanFactory也會有2個屬相會添加此bean定義信息:beanDefinitionMap、beanDefinitionNames 

      List<String> beanDefinitionNames = new ArrayList<>(),beanDefinition的名稱列表
      Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(),key是beanDefinition的名稱,value是beanDefinition對象

    另外beanFactory中Set<String> manualSingletonNames = new LinkedHashSet<>,按註冊順序存放手動註冊的單例的名稱。

    load方法,我會放到另一篇博文中重點分析;load負責載入bean定義資源,應該是挺重要的,而本文卻講的比較粗糙,我們一起期待吧。

 

    有時候,不是對手有多強大,只是我們不敢去嘗試;勇敢踏出第一步,你會發現自己比想象中更優秀!誠如海因斯第一次跑進人類10s大關時所說:上帝啊,原來那扇門是虛掩著的!

總結

  1、上文中的load

    就是載入bean定義資源,支持4種方式:Class、Resource、Package和CharSequence。

    Class:註解形式的Bean定義;AnnotatedBeanDefinitionReader負責處理。

    Resource:一般而言指的是xml bean配置文件,也就是我們在spring中常用的xml配置。xml的載入大家可以去閱讀《Spring源碼深度解析》。說的簡單點就是:將xml的bean定義封裝成BeanDefinition並註冊到beanFactory的BeanDefinitionMap中;XmlBeanDefinitionReader負責處理。

    Package:以掃包的方式掃描bean定義; ClassPathBeanDefinitionScanner負責處理。

    CharSequence:以先後順序進行匹配Class、Resource或Package進行載入,誰匹配上了就用誰的處理方式處理。

    當然還支持Groovy形式的Bean定義,有興趣的朋友可以自行去跟下源代碼。

    springboot鼓勵用java類實現java bean定義,所以springboot應用中,我們一般只需要關註Class方式、Package方式即可。

  2、prepareContext到底做了什麼    

    1、將context中的environment替換成SpringApplication中創建的environment
    2、將SpringApplication中的initializers應用到context中
      設置application id,並將application id封裝成ContextId對象,註冊到beanFactory中
      向context的beanFactoryPostProcessors中註冊了一個ConfigurationWarningsPostProcessor實例
      向context的applicationListeners中註冊了一個ServerPortInfoApplicationContextInitializer實例
      向context的beanFactoryPostProcessors中註冊了一個CachingMetadataReaderFactoryPostProcessor實例
      向context的applicationListeners中註冊了一個ConditionEvaluationReportListener實例

    3、載入兩個單例bean到beanFactory中

      向beanFactory中註冊了一個名叫springApplicationArguments的單例bean,該bean封裝了我們的命令行參數;
      向beanFactory中註冊了一個名叫springBootBanner的單例bean。

    4、載入bean定義資源
      資源文件只有SpringApplication的primarySources集合,裡面就一個資源類:com.lee.shiro.ShiroApplication;
      將該資源封裝成了名叫ShiroApplication的BeanDefinition對象,並將其註冊到了beanFactory的BeanDefinitionMap中。
    5、將SpringApplication中的listeners註冊到context中,並廣播ApplicationPreparedEvent事件
      總共11個ApplicationListener註冊到了context的applicationListeners中;
      ApplicationPreparedEvent事件的監聽器一共做了兩件事
        向context的beanFactoryPostProcessors中註冊了一個PropertySourceOrderingPostProcessor實例
        向beanFactory中註冊了一個名叫springBootLoggingSystem的單例bean,也就是我們的日誌系統bean

    context中主要是三個屬性增加了內容:beanFactory、beanFactoryPostProcessors和applicationListeners,到目前為止,context的內容如下

參考

  《Spring源碼深度解析》

  Spring Boot Reference Guide  

  Spring boot 源碼


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 描述: 本文主要是講,通過css+js實現網頁中的【返回頂部】功能。 實現代碼: HTML: CSS: JS: ...
  • 簡稱 js防連點 var flag = true; $(".yzm>span").click(function(){ if(!flag){ return false } flag = false; var time = 60; var timer = setInterval(function(){ ...
  • 前段時間,公司在項目上用到了xhEditor編輯器來給用戶做一個上傳圖片的功能當時做的時候覺得很有意思,想想 基本的用戶圖片上傳到自己伺服器,還有點小占地方; 後來....然後直接上傳到阿裡雲 。接下來就是基本操作: 首先,引入官方提供的js庫 註:xhEditor插件下載官網:https://xh ...
  • TSAD的來源: TSAD由Open-test、Open-stor、Open-api、Open-dev四大系統組成,提供API測試平臺Open-test;測試通過版本可發佈服務倉庫Open-stor,倉庫抽離單一服務,其他產品部可任意裝配服務;根據業務需求將服務開放到Open-api與Open-de ...
  • 教程:高能:語句結構都是由關鍵字開頭,用冒號結束! 一:語句結構for <variable> in <sequence>: <statements>else: # else可有可無 <statements>二:基本規則 (1)使用縮進來劃分語句塊,相同縮進數的語句在一起組成一個語句塊。 (2)seq ...
  • ![](https://img2018.cnblogs.com/blog/711958/201809/711958-20180928091826555-1354813331.jpg) ...
  • ![](https://img2018.cnblogs.com/blog/711958/201809/711958-20180928091434379-573436458.jpg) ...
  • 原文出自: "http://cmsblogs.com" import 標簽解析完畢了,再看 Spring 中最複雜也是最重要的標簽 bean 標簽的解析過程。 在方法 中,如果遇到標簽 為 bean 則調用 方法進行 bean 標簽解析,如下: 整個過程分為四個步驟 1. 調用 進行元素解析,解析過 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...