【Dubbo源碼閱讀系列】之 Dubbo XML 配置載入

来源:https://www.cnblogs.com/cfyrwang/archive/2019/01/03/10213085.html
-Advertisement-
Play Games

今天我們來談談 Dubbo XML 配置相關內容。關於這部分內容我打算分為以下幾個部分進行介紹: Dubbo XML Spring 自定義 XML 標簽解析 Dubbo 自定義 XML 標簽解析 DubboBeanDefinitionParser.parse() End Dubbo XML 在本小節 ...


今天我們來談談 Dubbo XML 配置相關內容。關於這部分內容我打算分為以下幾個部分進行介紹:

  • Dubbo XML
  • Spring 自定義 XML 標簽解析
  • Dubbo 自定義 XML 標簽解析
  • DubboBeanDefinitionParser.parse()
  • End

    Dubbo XML

    在本小節開始前我們先來看下 Dubbo XML 配置文件示例:
dubbo-demo-provider.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- provider's application name, used for tracing dependency relationship -->
    <dubbo:application name="demo-provider"/>

    <!-- use multicast registry center to export service -->
    <!--<dubbo:registry address="multicast://224.5.6.7:1234"/>-->
    <dubbo:registry address="zookeeper://10.14.22.68:2181"/>

    <!-- use dubbo protocol to export service on port 20880 -->
    <dubbo:protocol name="dubbo" port="20880"/>

    <!-- service implementation, as same as regular local bean -->
    <bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>

    <!-- declare the service interface to be exported -->
    <dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService"/>
</beans>

在這段配置文件中有一些以 dubbo 開頭的 xml 標簽,直覺告訴我們這種標簽和 dubbo 密切相關。那麼這些標簽的用途是什麼?又是如何被識別的呢?
我們結合 Spring 自定義 xml 標簽實現相關內容來聊聊 Dubbo 是如何定義並載入這些自定義標簽的。

Spring 自定義 XML 標簽解析

Dubbo 中的自定義 XML 標簽實際上是依賴於 Spring 解析自定義標簽的功能實現的。網上關於 Spring 解析自定義 XML 標簽的文章也比較多,這裡我們僅介紹下實現相關功能需要的文件,給大家一個直觀的印象,不去深入的對 Spring 自定義標簽實現作詳細分析。

  1. 定義 xsd 文件
    XSD(XML Schemas Definition) 即 XML 結構定義。我們通過 XSD 文件不僅可以定義新的元素和屬性,同時也使用它對我們的 XML 文件規範進行約束。
    在 Dubbo 項目中可以找類似實現:dubbo.xsd
  2. spring.schemas
    該配置文件約定了自定義命名空間和 xsd 文件之間的映射關係,用於 spring 容器感知我們自定義的 xsd 文件位置。
http\://dubbo.apache.org/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd
http\://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/compat/dubbo.xsd
  1. spring.handlers
    該配置文件約定了自定義命名空間和 NamespaceHandler 類之間的映射關係。 NamespaceHandler 類用於註冊自定義標簽解析器。
http\://dubbo.apache.org/schema/dubbo=org.apache.dubbo.config.spring.schema.DubboNamespaceHandler
http\://code.alibabatech.com/schema/dubbo=org.apache.dubbo.config.spring.schema.DubboNamespaceHandler
  1. 命名空間處理器
    命名空間處理器主要用來註冊 BeanDefinitionParser 解析器。對應上面 spring.handlers 文件中的 DubboNamespaceHandler
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        // 省略...
        registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
    }
}
  1. BeanDefinitionParser 解析器
    實現 BeanDefinitionParser 介面中的 parse 方法,用於自定義標簽的解析。Dubbo 中對應 DubboBeanDefinitionParser 類。

Dubbo 解析自定義 XML 標簽

終於進入到本文的重頭戲環節了。在介紹 Dubbo 自定義 XML 標簽解析前,先放一張圖幫助大家理解以下 Spring 是如何從 XML 文件中解析並載入 Bean 的。


上圖言盡於 handler.parse() 方法,如果你仔細看了上文,對 parse() 應該是有印象的。
沒錯,在前一小結的第五點我們介紹了 DubboBeanDefinitionParser 類。該類有個方法就叫 parse()。那麼這個 parse() 方法有什麼用? Spring 是如何感知到我就要調用 DubboBeanDefinitionParser 類中的 parse() 方法的呢?我們帶著這兩個問題接著往下看。

BeanDefinitionParserDelegate

上面圖的流程比較長,我們先著重看下 BeanDefinitionParserDelegate 類中的幾個關鍵方法。

BeanDefinitionParserDelegate.java
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    // 獲取當前 element 的 namespaceURI
    // 比如 dubbo.xsd 中的為 http://dubbo.apache.org/schema/dubbo
    String namespaceUri = this.getNamespaceURI(ele);
    // 根據 URI 獲取對應的 NamespaceHandler
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    if (handler == null) {
        this.error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
        return null;
    } else {
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }
}

這個方法幹了三件事

  1. 獲取 element 元素的 namespaceURI,並據此獲取對應的 NamespaceHandler 對象。Dubbo 自定義標簽(比如 Dubbo:provider) namespaceUri 的值為 http://dubbo.apache.org/schema/dubbo;
  2. 根據 step1 獲取到的 namespaceUri ,獲取對應的 NamespaceHandler 對象。這裡會調用 DefaultNamespaceHandlerResolver 類的 resolve() 方法,我們下麵會分析;
  3. 調用 handler 的 parse 方法,我們自定以的 handler 會繼承 NamespaceHandlerSupport 類,所以這裡調用的其實是 NamespaceHandlerSupport 類的 parse() 方法,後文分析;

一圖勝千言
在詳細分析 step2 和 step3 中涉及的 resolver()parse() 方法前,先放一張時序圖讓大家有個基本概念:

DefaultNamespaceHandlerResolver.java
public NamespaceHandler resolve(String namespaceUri) {
    Map<String, Object> handlerMappings = this.getHandlerMappings();
    // 以 namespaceUri 為 Key 獲取對應的 handlerOrClassName
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    if (handlerOrClassName == null) {
        return null;
    } else if (handlerOrClassName instanceof NamespaceHandler) {
        return (NamespaceHandler)handlerOrClassName;
    } else {
        // 如果不為空且不為 NamespaceHandler 的實例,轉換為 String 類型
        // DubboNamespaceHandler 執行的便是這段邏輯
        String className = (String)handlerOrClassName;

        try {
            Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
            // handlerClass 是否為 NamespaceHandler 的實現類,若不是則拋出異常
            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
            } else {
                // 初始化 handlerClass
                NamespaceHandler namespaceHandler = (NamespaceHandler)BeanUtils.instantiateClass(handlerClass);
                // 執行 handlerClass類的 init() 方法
                namespaceHandler.init();
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
        } catch (ClassNotFoundException var7) {
            throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "] not found", var7);
        } catch (LinkageError var8) {
            throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]: problem with handler class file or dependent class", var8);
        }
    }
}

resolve() 方法用途是根據方法參數中的 namespaceUri 獲取對應的 NamespaceHandler 對象。這裡會先嘗試以 namespaceUri 為 key 去 handlerMappings 集合中取對象。
如果 handlerOrClassName 不為 null 且不為 NamespaceHandler 的實例。那麼嘗試將 handlerOrClassName 作為 className 並調用 BeanUtils.instantiateClass() 方法初始化一個
NamespaceHandler 實例。初始化後,調用其 init() 方法。這個 init() 方法比較重要,我們接著往下看。

DubboNamespaceHandler
public void init() {
    registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
    registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
    registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
    registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
    registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
    registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
    registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
    registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
    registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
    registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
}

NamespaceHandlerSupport
private final Map<String, BeanDefinitionParser> parsers = new HashMap();
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    this.parsers.put(elementName, parser);
}

DubboNamespaceHandler 類中的 init() 方法乾的事情特別簡單,就是新建 DubboBeanDefinitionParser 對象並將其放入 NamespaceHandlerSupport 類的 parsers 集合中。我們再回顧一下 parseCustomElement() 方法。

BeanDefinitionParserDelegate.java
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    // 省略...
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    // 省略...
}

這裡會調用 NamespaceHandlerSupport 類的 parse() 方法。我們繼續跟蹤一下。

public BeanDefinition parse(Element element, ParserContext parserContext) {
    return this.findParserForElement(element, parserContext).parse(element, parserContext);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    String localName = parserContext.getDelegate().getLocalName(element);
    BeanDefinitionParser parser = (BeanDefinitionParser)this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal("Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }

    return parser;
}

看到這裡大家有沒有一絲豁然開朗的感覺?之前的 resolve() 方法實際上就是根據當前 element 的 namespaceURI 獲取對應的 NamespaceHandler 對象(對於 Dubbo 來說是 DubboNamespaceHandler),
然後調用 DubboNamespaceHandler 中的 init() 方法新建 DubboBeanDefinitionParser 對象並註冊到 NamespaceHandlerSupport 類的 parsers 集合中。
然後 parser 方法會根據當前 element 對象從 parsers 集合中獲取合適的 BeanDefinitionParser 對象。對於 Dubbo 元素來說,實際上最後執行的是 DubboBeanDefinitionParser 的 parse() 方法。

DubboBeanDefinitionParser.parse()

最後我們再來看看 Dubbo 解析 XML 文件的詳細實現吧。如果對具體實現沒有興趣可直接直接跳過。

private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClass(beanClass);
        beanDefinition.setLazyInit(false);
        String id = element.getAttribute("id");
        // DubboBeanDefinitionParser 構造方法中有對 required 值進行初始化;
        // DubboNamespaceHandler 類中的 init 方法會創建並註冊 DubboBeanDefinitionParser 類
        if ((id == null || id.length() == 0) && required) {
            String generatedBeanName = element.getAttribute("name");
            if (generatedBeanName == null || generatedBeanName.length() == 0) {
                if (ProtocolConfig.class.equals(beanClass)) {
                    generatedBeanName = "dubbo";
                } else {
                    // name 屬性為空且不為 ProtocolConfig 類型,取 interface 值
                    generatedBeanName = element.getAttribute("interface");
                }
            }
            if (generatedBeanName == null || generatedBeanName.length() == 0) {
                // 獲取 beanClass 的全限定類名
                generatedBeanName = beanClass.getName();
            }
            id = generatedBeanName;
            int counter = 2;
            while (parserContext.getRegistry().containsBeanDefinition(id)) {
                id = generatedBeanName + (counter++);
            }
        }
        if (id != null && id.length() > 0) {
            if (parserContext.getRegistry().containsBeanDefinition(id)) {
                throw new IllegalStateException("Duplicate spring bean id " + id);
            }
            // 註冊 beanDefinition
            parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
            // 為 beanDefinition 添加 id 屬性
            beanDefinition.getPropertyValues().addPropertyValue("id", id);
        }
        
        // 如果當前 beanClass 類型為 ProtocolConfig
        // 遍歷已經註冊過的 bean 對象,如果 bean 對象含有 protocol 屬性
        // protocol 屬性值為 ProtocolConfig 實例且 name 和當前 id 值一致,為當前 beanClass 對象添加 protocl 屬性
        if (ProtocolConfig.class.equals(beanClass)) {
            for (String name : parserContext.getRegistry().getBeanDefinitionNames()) {
                BeanDefinition definition = parserContext.getRegistry().getBeanDefinition(name);
                PropertyValue property = definition.getPropertyValues().getPropertyValue("protocol");
                if (property != null) {
                    Object value = property.getValue();
                    if (value instanceof ProtocolConfig && id.equals(((ProtocolConfig) value).getName())) {
                        definition.getPropertyValues().addPropertyValue("protocol", new RuntimeBeanReference(id));
                    }
                }
            }
        } else if (ServiceBean.class.equals(beanClass)) {
            // 如果當前元素包含 class 屬性,調用 ReflectUtils.forName() 方法載入類對象
            // 調用 parseProperties 解析其他屬性設置到 classDefinition 對象中
            // 最後設置 beanDefinition 的 ref 屬性為 BeanDefinitionHolder 包裝類
            String className = element.getAttribute("class");
            if (className != null && className.length() > 0) {
                RootBeanDefinition classDefinition = new RootBeanDefinition();
                classDefinition.setBeanClass(ReflectUtils.forName(className));
                classDefinition.setLazyInit(false);
                parseProperties(element.getChildNodes(), classDefinition);
                beanDefinition.getPropertyValues().addPropertyValue("ref", new BeanDefinitionHolder(classDefinition, id + "Impl"));
            }
        } else if (ProviderConfig.class.equals(beanClass)) {
            parseNested(element, parserContext, ServiceBean.class, true, "service", "provider", id, beanDefinition);
        } else if (ConsumerConfig.class.equals(beanClass)) {
            parseNested(element, parserContext, ReferenceBean.class, false, "reference", "consumer", id, beanDefinition);
        }
        Set<String> props = new HashSet<String>();
        ManagedMap parameters = null;
        for (Method setter : beanClass.getMethods()) {
            String name = setter.getName();
            if (name.length() > 3 && name.startsWith("set")
                    && Modifier.isPublic(setter.getModifiers())
                    && setter.getParameterTypes().length == 1) {
                Class<?> type = setter.getParameterTypes()[0];
                String propertyName = name.substring(3, 4).toLowerCase() + name.substring(4);
                String property = StringUtils.camelToSplitName(propertyName, "-");
                props.add(property);
                Method getter = null;
                try {
                    getter = beanClass.getMethod("get" + name.substring(3), new Class<?>[0]);
                } catch (NoSuchMethodException e) {
                    try {
                        getter = beanClass.getMethod("is" + name.substring(3), new Class<?>[0]);
                    } catch (NoSuchMethodException e2) {
                    }
                }
                if (getter == null
                        || !Modifier.isPublic(getter.getModifiers())
                        || !type.equals(getter.getReturnType())) {
                    continue;
                }
                if ("parameters".equals(property)) {
                    parameters = parseParameters(element.getChildNodes(), beanDefinition);
                } else if ("methods".equals(property)) {
                    parseMethods(id, element.getChildNodes(), beanDefinition, parserContext);
                } else if ("arguments".equals(property)) {
                    parseArguments(id, element.getChildNodes(), beanDefinition, parserContext);
                } else {
                    String value = element.getAttribute(property);
                    if (value != null) {
                        value = value.trim();
                        if (value.length() > 0) {
                        // 如果屬性為 registry,且 registry 屬性的值為"N/A",標識不會註冊到任何註冊中心
                        // 新建 RegistryConfig 並將其設置為 beanDefinition 的 registry 屬性
                            if ("registry".equals(property) && RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(value)) {
                                RegistryConfig registryConfig = new RegistryConfig();
                                registryConfig.setAddress(RegistryConfig.NO_AVAILABLE);
                                beanDefinition.getPropertyValues().addPropertyValue(property, registryConfig);
                            } else if ("registry".equals(property) && value.indexOf(',') != -1) {
                                // 多註冊中心解析
                                parseMultiRef("registries", value, beanDefinition, parserContext);
                            } else if ("provider".equals(property) && value.indexOf(',') != -1) {
                                parseMultiRef("providers", value, beanDefinition, parserContext);
                            } else if ("protocol".equals(property) && value.indexOf(',') != -1) {
                                // 多協議
                                parseMultiRef("protocols", value, beanDefinition, parserContext);
                            } else {
                                Object reference;
                                if (isPrimitive(type)) {
                                    // type 為方法參數,type 類型是否為基本類型
                                    if ("async".equals(property) && "false".equals(value)
                                            || "timeout".equals(property) && "0".equals(value)
                                            || "delay".equals(property) && "0".equals(value)
                                            || "version".equals(property) && "0.0.0".equals(value)
                                            || "stat".equals(property) && "-1".equals(value)
                                            || "reliable".equals(property) && "false".equals(value)) {
                                        // 新老版本 xsd 相容性處理
                                        // backward compatibility for the default value in old version's xsd
                                        value = null;
                                    }
                                    reference = value;
                                } else if ("protocol".equals(property)
                                        && ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(value)
                                        && (!parserContext.getRegistry().containsBeanDefinition(value)
                                        || !ProtocolConfig.class.getName().equals(parserContext.getRegistry().getBeanDefinition(value).getBeanClassName()))) {
                                    // 如果 protocol 屬性值有對應的擴展實現,而且沒有被註冊到 spring 註冊表中
                                    // 或者 spring 註冊表中對應的 bean 的類型不為 ProtocolConfig.class
                                    if ("dubbo:provider".equals(element.getTagName())) {
                                        logger.warn("Recommended replace <dubbo:provider protocol=\"" + value + "\" ... /> to <dubbo:protocol name=\"" + value + "\" ... />");
                                    }
                                    // backward compatibility
                                    ProtocolConfig protocol = new ProtocolConfig();
                                    protocol.setName(value);
                                    reference = protocol;
                                } else if ("onreturn".equals(property)) {
                                    int index = value.lastIndexOf(".");
                                    String returnRef = value.substring(0, index);
                                    String returnMethod = value.substring(index + 1);
                                    reference = new RuntimeBeanReference(returnRef);
                                    beanDefinition.getPropertyValues().addPropertyValue("onreturnMethod", returnMethod);
                                } else if ("onthrow".equals(property)) {
                                    int index = value.lastIndexOf(".");
                                    String throwRef = value.substring(0, index);
                                    String throwMethod = value.substring(index + 1);
                                    reference = new RuntimeBeanReference(throwRef);
                                    beanDefinition.getPropertyValues().addPropertyValue("onthrowMethod", throwMethod);
                                } else if ("oninvoke".equals(property)) {
                                    int index = value.lastIndexOf(".");
                                    String invokeRef = value.substring(0, index);
                                    String invokeRefMethod = value.substring(index + 1);
                                    reference = new RuntimeBeanReference(invokeRef);
                                    beanDefinition.getPropertyValues().addPropertyValue("oninvokeMethod", invokeRefMethod);
                                } else {
                                    // 如果 ref 屬性值已經被註冊到 spring 註冊表中
                                    if ("ref".equals(property) && parserContext.getRegistry().containsBeanDefinition(value)) {
                                        BeanDefinition refBean = parserContext.getRegistry().getBeanDefinition(value);
                                        // 非單例拋出異常
                                        if (!refBean.isSingleton()) {
                                            throw new IllegalStateException("The exported service ref " + value + " must be singleton! Please set the " + value + " bean scope to singleton, eg: <bean id=\"" + value + "\" scope=\"singleton\" ...>");
                                        }
                                    }
                                    reference = new RuntimeBeanReference(value);
                                }
                                beanDefinition.getPropertyValues().addPropertyValue(propertyName, reference);
                            }
                        }
                    }
                }
            }
        }
        NamedNodeMap attributes = element.getAttributes();
        int len = attributes.getLength();
        for (int i = 0; i < len; i++) {
            Node node = attributes.item(i);
            String name = node.getLocalName();
            if (!props.contains(name)) {
                if (parameters == null) {
                    parameters = new ManagedMap();
                }
                String value = node.getNodeValue();
                parameters.put(name, new TypedStringValue(value, String.class));
            }
        }
        if (parameters != null) {
            beanDefinition.getPropertyValues().addPropertyValue("parameters", parameters);
        }
        return beanDefinition;
    }

上面這一大段關於配置的解析的代碼需要大家自己結合實際的代碼進行調試才能更好的理解。我在理解 Dubbo XML 解析的時候,也是耐著性子一遍一遍的來。
關於 ProtocolConfig 和 protocol 載入先後順序的問題最後再集合一個小例子總結下吧:

    dubbo-demo-provider.xml
    <dubbo:protocol name="dubbo" port="20880"/>
  1. 當我們先解析了 ProtocolConfig 元素時,我們會遍歷所有已經註冊 spring 註冊表中 bean。如果 bean 對象存在 protocol 屬性且與 name 和當前 ProtolConfig id 匹配,則會新建 RuntimeBeanReference 對象覆蓋 protocol 屬性。對於上面這行配置,最後會新建一個擁有 name 和 port 的 beanDefinition 對象。
  2. 先解析了 protocol 元素,ProtocolConfig 未被解析。此時我們在 spring 註冊表中找不到對應的 ProtocolConfig bean。此時我們將需要新建一個 ProtocolConfig 並將其 name 屬性
    設置為當前屬性值。最後將其設置為 beanDefinition 對象的 protocol 屬性。後面載入到了 ProtocolConfig 元素時,會替換 protocol 的值。

End

Dubbo 對於自定義 XML 標簽的定義和解析實際上藉助了 Spring 框架對自定義 XML 標簽的支持。本篇水文雖然又臭又長,但是對於理解 Dubbo 的初始化過程還是很重要的。後面我們會介紹關於 Dubbo 服務暴露相關內容。

本BLOG上原創文章未經本人許可,不得用於商業用途及傳統媒體。網路媒體轉載請註明出處,否則屬於侵權行為。https://juejin.im/post/5c1753b65188250850604ebe


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

-Advertisement-
Play Games
更多相關文章
  • 所有基礎課程鏈接: 1.JavaScript基礎視頻教程總結(001-010章) 2.JavaScript基礎視頻教程總結(011-020章) 3. JavaScript基礎視頻教程總結(021-030章) 4. JavaScript基礎視頻教程總結(031-040章) 5. JavaScript基 ...
  • 用 JavaScript 操作 DOM 時出現如下錯誤: 例如: 運行時出現如下錯誤: 問題出在 JS 運行的時候你的頁面還沒有載入完成,所以你的 JS 代碼找不到你的頁面元素,就會拋出這個問題。解決辦法就是把 JavaScript 代碼放在 body 的最後,例如: ...
  • https://www.cnblogs.com/shuaifing/p/8185311.html 侵刪 ...
  • 上述按鈕CSS規則 上述按鈕CSS顏色對應樣式依次為 未完待續...... ...
  • 本文主要從8個章節詳解vue技術揭秘,小編覺得挺有用的,分享給大家。 為了把 Vue.js 的源碼講明白,課程設計成由淺入深,分為核心、編譯、擴展、生態四個方面去講,並拆成了八個章節,如下: ...
  • 你們有沒有遇到過這樣的情況,ES6看過了,Promise的文字概念都懂,但是我要怎麼在項目中去寫一個Promise呢? 那天我就是帶著這樣的疑問去網上搜了下。最後成功地在項目中應用了Promise,只有實際成功使用一次,才能明白它的前因後果,明白它的用途。 1.這是一個vue的電商項目-商品詳情頁 ...
  • 這個案例網上是沒有的,屬於無忌獨創,當時在幫孩子輔導作業,小學科學,裡面有一點內容是關於人的牙齒,說牙齒分為:門齒、犬齒、臼齒,問閨女,為什麼這麼分呢?閨女說牙齒雖然都是用來咬食物,但是食物種類很多,咬碎需要的工具也不一樣,門齒用來切割食物,如:蘋果、梨;犬齒用來撕碎食物,如肉類;臼齒用來磨碎食物,... ...
  • 公眾號:SAP Technical 本文作者:matinal 原文出處:http://www.cnblogs.com/SAPmatinal/ 原文鏈接:【MM系列】SAP MM 非限制/可用庫存 前言部分 今天簡單討論一下MM的非限制使用庫存和可用庫存,如果文章有不正確的地方,請及時留言指出,我會第 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...