【Dubbo源码阅读系列】之 Dubbo XML 配置加载

栏目: 后端 · 发布时间: 5年前

内容简介:今天我们来谈谈 Dubbo XML 配置相关内容。关于这部分内容我打算分为以下几个部分进行介绍:在本小节开始前我们先来看下 Dubbo XML 配置文件示例:在这段配置文件中有一些以 dubbo 开头的 xml 标签,直觉告诉我们这种标签和 dubbo 密切相关。那么这些标签的用途是什么?又是如何被识别的呢? 我们结合 Spring 自定义 xml 标签实现相关内容来聊聊 Dubbo 是如何定义并加载这些自定义标签的。

今天我们来谈谈 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 的。

【Dubbo源码阅读系列】之 Dubbo XML 配置加载

上图言尽于 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() 方法前,先放一张时序图让大家有个基本概念:

【Dubbo源码阅读系列】之 Dubbo XML 配置加载
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 的值。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Visual C#2005从入门到精通

Visual C#2005从入门到精通

夏普 / 周靖 / 清华大学出版社 / 2006-6 / 49.00元

《Visual C#2005从入门到精通/微软技术丛书》:微软技术丛书系列之一,建议一读! Microsoft Visual C#功能强大、使用简单。本书全面介绍了如何利用Visual Studio 2005和.NET Framework来进行C#编程。作者将C#的各种特性娓娓道来,以范例导航的方式,通过大量的练习引导读者逐步构建Windows窗体应用程序,访问Microsoft SQL Serv......一起来看看 《Visual C#2005从入门到精通》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具