通俗理解spring源碼(五)—— 解析及註冊BeanDefinitions

来源:https://www.cnblogs.com/xiaohang123/archive/2020/05/03/12822669.html
-Advertisement-
Play Games

通俗理解spring源碼(五)—— 解析及註冊BeanDefinitions 上節講到瞭如何獲取document,當把文件轉換為document後,接下來的提取及註冊bean就是我們的重頭戲。 protected int doLoadBeanDefinitions(InputSource input ...


通俗理解spring源碼(五)—— 解析及註冊BeanDefinitions

  上節講到瞭如何獲取document,當把文件轉換為document後,接下來的提取及註冊bean就是我們的重頭戲。

    protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

        try {
            //從資源文件轉換為document對象
            Document doc = doLoadDocument(inputSource, resource);
            //解析document,並註冊beanDefiniton到工廠中
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (SAXParseException ex) {
            throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                    "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
        }
        catch (SAXException ex) {
            throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                    "XML document from " + resource + " is invalid", ex);
        }
        catch (ParserConfigurationException ex) {
            throw new BeanDefinitionStoreException(resource.getDescription(),
                    "Parser configuration exception parsing XML from " + resource, ex);
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(resource.getDescription(),
                    "IOException parsing XML document from " + resource, ex);
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(resource.getDescription(),
                    "Unexpected exception parsing XML document from " + resource, ex);
        }
    }

  在xmlBeanDefinitionReader的doLoadBeanDefinitions方法中,將document對象交給registerBeanDefinitions方法,返回本次載入的BeanDefinition個數。

    public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        //創建預設的documentReader,即DefaultBeanDefinitionDocumentReader,用來解析document
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        //獲取註冊中心,記錄統計前的BeanDefinition載入個數
        int countBefore = getRegistry().getBeanDefinitionCount();
        //載入及註冊BeanDefinition
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        //記錄本次載入的BeanDefinition個數
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

  這裡首先會createReaderContext(resource),代碼很簡單。

    public XmlReaderContext createReaderContext(Resource resource) {
        return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
                this.sourceExtractor, this, getNamespaceHandlerResolver());
    }

  這裡重點是將this對象,也就是當前的xmlBeanDefinitionReader對象放進去了,所以XmlReaderContext相當於一個上下文,方便數據的傳遞,類似於ServletContext,SecurityContext等。

  然後將document和上下文對象交給documentReader.registerBeanDefinitions方法。

    public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
        this.readerContext = readerContext;
        doRegisterBeanDefinitions(doc.getDocumentElement());
    }

  此處引用了之前的上下文,然後調用doRegisterBeanDefinitions,在spring中,很多以"do"開頭的方法名就是真正幹活的方法。

  這裡獲取了document的根元素,進入documentReader.doRegisterBeanDefinitions方法

    protected void doRegisterBeanDefinitions(Element root) {
        // Any nested <beans> elements will cause recursion in this method. In
        // order to propagate and preserve <beans> default-* attributes correctly,
        // keep track of the current (parent) delegate, which may be null. Create
        // the new (child) delegate with a reference to the parent for fallback purposes,
        // then ultimately reset this.delegate back to its original (parent) reference.
        // this behavior emulates a stack of delegates without actually necessitating one.
        BeanDefinitionParserDelegate parent = this.delegate;
        //委托給delegate解析
        this.delegate = createDelegate(getReaderContext(), root, parent);

        //判斷當前Beans節點是否是預設命名空間
        if (this.delegate.isDefaultNamespace(root)) {
            //獲取beans節點的profile屬性
            String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                //可以使用逗號或分號將當前beans標簽指定為多個profile類型
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                // We cannot use Profiles.of(...) since profile expressions are not supported
                // in XML config. See SPR-12458 for details.
                //判斷當前beans標簽的profile是否被激活
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                "] not matching: " + getReaderContext().getResource());
                    }
                    return;
                }
            }
        }
        //解析前處理,留給子類實現
        preProcessXml(root);
        //真正的解析過程
        parseBeanDefinitions(root, this.delegate);
        //解析後處理,留給子類實現
        postProcessXml(root);

        this.delegate = parent;

   可以先根據我的註釋看看這個方法到底是在幹啥,此處documentReader對象引用了一個BeanDefinitionParserDelegate,又將解析過程委托給了delegate 處理,在parseBeanDefinitions(root, this.delegate)方法中實現。

  方法一開頭,便有一大段英文註釋,大致意思就是說,任何內嵌的beans標簽,將會導致該方法的遞歸調用,為了正確的傳播和保留<beans>標簽的default屬性,追蹤當前delegate(即父delegete,可能為null),每次都會創建一個新的delegate(即子delegate),引用父delegate,而這個子delegate下次又會作為父delegate。

  腦瓜子是不是有點嗡嗡的???但是大概也能明白這裡就是為了處理beans標簽的default屬性的。

  因為我們在配置xml文件的時候,是可以在根beans標簽中嵌套beans標簽的(雖然這樣的寫法很少,一般是寫在另外一個xml文件中,然後通過import標簽導入,但實際上效果是一樣的),類似這樣:

<?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" default-autowire="byType">
    <beans profile="test" default-autowire="byType">
        <bean id="user" class="cn.zxh.po.User" >
            <property name="name" value="張三"/>
        </bean>
    </beans>
    <beans profile="dev" default-autowire="constructor">
        <bean id="user" class="cn.zxh.po.User">
            <property name="name" value="李四"/>
        </bean>
    </beans>
</beans>

  理論上沒有棧溢出的情況下beans內部應該是可以無限嵌套beans的(不一定正確,還沒有試過),後面會講到每次解析到beans標簽都會進入到該方法中,所以該方法可能會遞歸調用,每次都會創建delegate,對應一個beans標簽,根據父Delegate來決定當前Delegate的預設屬性。

1、創建Delegate

        BeanDefinitionParserDelegate parent = this.delegate;
        this.delegate = createDelegate(getReaderContext(), root, parent);

  createDelegate點進入看看。

    protected BeanDefinitionParserDelegate createDelegate(
            XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) {

        BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
        //根據父delegate的預設屬性,初始化當前beans的預設屬性
        delegate.initDefaults(root, parentDelegate);
        return delegate;
    }

   首先會實例化一個BeanDefinitionParserDelegate對象,該對象引用了之前的上下文readerContext,並且還引用了一個DocumentDefaultsDefinition。

    private final DocumentDefaultsDefinition defaults = new DocumentDefaultsDefinition();
    public BeanDefinitionParserDelegate(XmlReaderContext readerContext) {
        Assert.notNull(readerContext, "XmlReaderContext must not be null");
        this.readerContext = readerContext;
    }

   DocumentDefaultsDefinition保存了根節點的預設配置屬性值,比如說default-lazyInit,default-autowire,default-initMethod,default-destroyMethod等,這幾種屬性應該都用過吧,如果該beans標簽下的bean沒有配置這些屬性,就會使用beans標簽的預設配置。

  所以在這裡,delegate引用了一個DocumentDefaultsDefinition,將來在解析各個bean標簽時會起作用,然後調用initDefaults(root, parentDelegate),就是根據父delegate,初始化它自身的DefaultsDefinition。

  在initDefaults方法中大概就是採用子配置優先的原則給DocumentDefaultsDefinition屬性賦值的,具體就不帶大家細看了,不然很容易從入門到放棄,反正經過幾層方法的調用,最終進入到這個方法中,這裡僅僅貼一下代碼。

    protected void populateDefaults(DocumentDefaultsDefinition defaults, @Nullable DocumentDefaultsDefinition parentDefaults, Element root) {
        String lazyInit = root.getAttribute(DEFAULT_LAZY_INIT_ATTRIBUTE);
        if (isDefaultValue(lazyInit)) {
            // Potentially inherited from outer <beans> sections, otherwise falling back to false.
            lazyInit = (parentDefaults != null ? parentDefaults.getLazyInit() : FALSE_VALUE);
        }
        defaults.setLazyInit(lazyInit);

        String merge = root.getAttribute(DEFAULT_MERGE_ATTRIBUTE);
        if (isDefaultValue(merge)) {
            // Potentially inherited from outer <beans> sections, otherwise falling back to false.
            merge = (parentDefaults != null ? parentDefaults.getMerge() : FALSE_VALUE);
        }
        defaults.setMerge(merge);

        String autowire = root.getAttribute(DEFAULT_AUTOWIRE_ATTRIBUTE);
        if (isDefaultValue(autowire)) {
            // Potentially inherited from outer <beans> sections, otherwise falling back to 'no'.
            autowire = (parentDefaults != null ? parentDefaults.getAutowire() : AUTOWIRE_NO_VALUE);
        }
        defaults.setAutowire(autowire);

        if (root.hasAttribute(DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE)) {
            defaults.setAutowireCandidates(root.getAttribute(DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE));
        }
        else if (parentDefaults != null) {
            defaults.setAutowireCandidates(parentDefaults.getAutowireCandidates());
        }

        if (root.hasAttribute(DEFAULT_INIT_METHOD_ATTRIBUTE)) {
            defaults.setInitMethod(root.getAttribute(DEFAULT_INIT_METHOD_ATTRIBUTE));
        }
        else if (parentDefaults != null) {
            defaults.setInitMethod(parentDefaults.getInitMethod());
        }

        if (root.hasAttribute(DEFAULT_DESTROY_METHOD_ATTRIBUTE)) {
            defaults.setDestroyMethod(root.getAttribute(DEFAULT_DESTROY_METHOD_ATTRIBUTE));
        }
        else if (parentDefaults != null) {
            defaults.setDestroyMethod(parentDefaults.getDestroyMethod());
        }

        defaults.setSource(this.readerContext.extractSource(root));
    }

2、判斷當前profile是否被激活

  我們知道spring支持配置多個profile,可在beans標簽的profile屬性配置,然後在運行前動態指定spring.active.profile的。在java項目中,可以配置系統屬性System.setProperty("spring.profiles.active","test"),web項目中配置ServletContext上下文參數指定,springboot中也可以通過spring.active.profile指定。

        //判斷當前Beans節點是否是預設命名空間
        if (this.delegate.isDefaultNamespace(root)) {
            //獲取beans節點的profile屬性
            String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                //可以使用逗號或分號將當前beans標簽指定為多個profile類型
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                // We cannot use Profiles.of(...) since profile expressions are not supported
                // in XML config. See SPR-12458 for details.
                //判斷當前beans標簽的profile是否被激活
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                "] not matching: " + getReaderContext().getResource());
                    }
                    return;
                }
            }
        }

   知道了怎麼用,就容易多了,首先得到beans標簽的指定的profile數組,與指定的spring.active.profile對比,符合條件的話改beans才會被載入。

  首先通過getReaderContext().getEnvironment(),獲取StandardEnvironment。這裡的getReaderContext()就是獲取的剛剛說的XmlReaderContext上下文,再從上下文中的得到XmlBeanDefinitionReader初始化時引用的StandardEnvironment。

  下麵是XmlBeanDefinitionReader繼承自抽象父類的構造方法。

    protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        this.registry = registry;

        // Determine ResourceLoader to use.
        if (this.registry instanceof ResourceLoader) {
            this.resourceLoader = (ResourceLoader) this.registry;
        }
        else {
            this.resourceLoader = new PathMatchingResourcePatternResolver();
        }

        // Inherit Environment if possible
        if (this.registry instanceof EnvironmentCapable) {
            this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
        }
        else {
            this.environment = new StandardEnvironment();
        }
    }

   這裡的StandardEnvironment初始化會載入當前的系統變數和環境變數,是對系統變數和環境變數的封裝。在StandardEnvironmen繼承自父類的構造方法中,會調用customizePropertySources方法

private final MutablePropertySources propertySources = new MutablePropertySources()
protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(
                new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(
                new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

  該方法將當前系統變數和環境變數保存在其propertySources屬性中。

public class MutablePropertySources implements PropertySources {

    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
    /**
     * Create a new {@link MutablePropertySources} object.
     */
    public MutablePropertySources() {
    }
}

  而MutablePropertySources 有一個List屬性,保存多個屬性來源。

  也就是說,StandardEnvironment初始化完成時,就會載入系統變數和環境變數,然後這裡會調用acceptsProfiles(specifiedProfiles)方法判定當前beans標簽的profile是否應該被載入,遍歷給定的profiles數組,只要有一個被指定為spring.active.profile就返回true。

    public boolean acceptsProfiles(String... profiles) {
        Assert.notEmpty(profiles, "Must specify at least one profile");
        for (String profile : profiles) {
            if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
                if (!isProfileActive(profile.substring(1))) {
                    return true;
                }
            }
            else if (isProfileActive(profile)) {
                return true;
            }
        }
        return false;
    }
    protected boolean isProfileActive(String profile) {
        validateProfile(profile);
        Set<String> currentActiveProfiles = doGetActiveProfiles();
        return (currentActiveProfiles.contains(profile) ||
                (currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
    }

  重點是獲取spring.active.profile的方法。

    protected Set<String> doGetActiveProfiles() {
        synchronized (this.activeProfiles) {
            if (this.activeProfiles.isEmpty()) {
                //從propertySources中獲取,key為spring.active.profile
                String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
                if (StringUtils.hasText(profiles)) {
                    setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
                            StringUtils.trimAllWhitespace(profiles)));
                }
            }
            return this.activeProfiles;
        }
    }

3、解析根節點parseBeanDefinitions

  經過了委托類的創建,spring.active.profile判斷返回true的beans標簽,才會進入到parseBeanDefinitions中被解析

    protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        //判定是否是預設標簽
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    Element ele = (Element) node;
                    if (delegate.isDefaultNamespace(ele)) {
                        //預設標簽的解析
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        //自定義標簽的解析
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            //自定義標簽的解析
            delegate.parseCustomElement(root);
        }
    }

  看到這裡應該就一目瞭然了,判定root節點以及其子節點是否是預設標簽,預設標簽和自定義標簽有不同的解析方式,除了beans、bean、alias和import四種標簽外,都是自定義標簽,自定義標簽需要實現一些介面和配置。如果是預設標簽,進入parseDefaultElement方法。

    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
        //解析import標簽
        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
            importBeanDefinitionResource(ele);
        }
        //解析alias標簽
        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
            processAliasRegistration(ele);
        }
        //解析bean標簽
        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
            processBeanDefinition(ele, delegate);
        }
        //解析beans標簽
        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
            // recurse
            doRegisterBeanDefinitions(ele);
        }
    }

  具體的解析過程下一章會講到,在這裡,看看解析beans標簽的時候,是不是又會調用doRegisterBeanDefinitions方法?還記得嗎?這正是對之前該方法會遞歸調用的解釋,再貼一遍代碼吧。

    protected void doRegisterBeanDefinitions(Element root) {
        // Any nested <beans> elements will cause recursion in this method. In
        // order to propagate and preserve <beans> default-* attributes correctly,
        // keep track of the current (parent) delegate, which may be null. Create
        // the new (child) delegate with a reference to the parent for fallback purposes,
        // then ultimately reset this.delegate back to its original (parent) reference.
        // this behavior emulates a stack of delegates without actually necessitating one.
        BeanDefinitionParserDelegate parent = this.delegate;
        //委托給delegate解析
        this.delegate = createDelegate(getReaderContext(), root, parent);

        //判斷當前Beans節點是否是預設命名空間
        if (this.delegate.isDefaultNamespace(root)) {
            //獲取beans節點的profile屬性
            String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                //可以使用逗號或分號將當前beans標簽指定為多個profile類型
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                // We cannot use Profiles.of(...) since profile expressions are not supported
                // in XML config. See SPR-12458 for details.
                //判斷當前beans標簽的profile是否被激活
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                "] not matching: " + getReaderContext().getResource());
                    }
                    return;
                }
            }
        }
        //解析前處理,留給子類實現
        preProcessXml(root);
        //真正的解析過程
        parseBeanDefinitions(root, this.delegate);
        //解析後處理,留給子類實現
        postProcessXml(root);

        this.delegate = parent;
    }

 

  走的太遠,不要忘記為什麼出發!

 

  參考:spring源碼深度解析

 


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

-Advertisement-
Play Games
更多相關文章
  • 有的時候 SpringMVC 框架提供的視圖解析器不能滿足我們的需求,這時候我們可以來自定義視圖以及視圖解析器來完成定製的功能。 主要分為以下三步: 編寫自定義視圖實現類 編寫視圖解析器 在配置文件中將自定義的視圖解析器註入ioc容器中 1、編寫自定義視圖實現類: 2、編寫視圖解析器: 3、在配置文 ...
  • flask的基本使用 一 創建flask項目(避免與其他環境衝突) 1 創建虛擬環境 2 創建flask項目 + 在pycharm中創建Pure Python新項目 + 選擇創建的虛擬環境作為開發環境(使用虛擬環境後which python能看到環境目錄) + 新建.py文件 3 hello wor ...
  • 以下為整理的自己秋招遇到的面試題;主要是Java和大數據相關題型;根據印象整理了下,有些記不起來了。死鎖、樂觀鎖、悲觀鎖synchronized底層原理及膨脹機制ReetrantLock底層原理,源碼是如何實現公平和非公平的synchronized和lock的區別volitale理解?volital ...
  • 最近,我讀了一篇有趣的文章,文中介紹了一些未充分使用的Python特性的。在文章中,作者提到,從Python 3.2開始,標準庫附帶了一個內置的裝飾器functools.lru_cache。我發現這個裝飾器很令人興奮,有了它,我們有可能輕鬆地為許多應用程式加速。 你可能在想,這很好,但這個裝飾器究竟 ...
  • 0. 前言 這是一個新的系列,來源於工作中的一個需求,領導準備新開一個項目線路,要求使用Java,項目符合現有主流技術,並要求對併發量有一定的承受能力 ,支持擴展。我和公司的幾個小伙伴一起溝通了一下,這不就是標準的Spring Cloud微服務的系統架構嗎。 之前讀過小高之前發的文章的小伙伴也清楚我 ...
  • 直接進入主題 爬蟲功能:此項目和QQ空間爬蟲類似,主要爬取新浪微博用戶的個人信息、微博信息、粉絲和關註(詳細見此)。還要註意:不管你是為了Python就業還是興趣愛好,記住:項目開發經驗永遠是核心,如果你沒有2020最新python入門到高級實戰視頻教程,可以去小編的Python交流.裙 :七衣衣九 ...
  • 會話控制是指網站與用戶之間跨頁面數據交互的一種解決方案,主要有cookie和session兩種。 COOKIE 使用本地文件處理跨頁面傳值,用戶的基本信息加密後保存到本地 安全性相對不高 用戶可以禁止cookie 存儲數據有大小限制(<4k) 不同瀏覽器存儲不同的cookie SESSION 在服務 ...
  • 需求如下: 1、在一個頁面中顯示兩張圖片 2、進入頁面可以使用滑鼠拖動各自的圖片,相互不受影響 3、進入頁面後可以使用滑鼠滾輪放大或縮小圖片,相互不受影響,即滑鼠移動到圖片A上,可對圖片A進行放大或縮小,圖片B不受影響,反之亦然 4、拖動需求同3 實現代碼: 參考網址: 1.https://matp ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...