[[357484]]
创新互联是一家专业提供大安市企业网站建设,专注与成都做网站、成都网站设计、H5高端网站建设、小程序制作等业务。10年已为大安市众多企业、政府机构等服务。创新互联专业的建站公司优惠进行中。
SPI是什么
SPI是一种简称,全名叫 Service Provider Interface,Java本身提供了一套SPI机制,SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样可以在运行时,动态为接口替换实现类,这也是很多框架组件实现扩展功能的一种手段。
而今天要说的Dubbo SPI机制和Java SPI还是有一点区别的,Dubbo 并未使用 Java 原生的 SPI 机制,而是对他进行了改进增强,进而可以很容易地对Dubbo进行功能上的扩展。
学东西得带着问题去学,我们先提几个问题,再接着看
1.什么是SPI(开头已经解释了)
2.Dubbo SPI和Java原生的有什么区别
3.两种实现应该如何写出来
Java SPI是如何实现的
先定义一个接口:
public interface Car { void startUp(); }
然后创建两个类,都实现这个Car接口
public class Truck implements Car{ @Override public void startUp() { System.out.println("The truck started"); } } public class Train implements Car{ @Override public void startUp() { System.out.println("The train started"); } }
然后在项目META-INF/services文件夹下创建一个名称为接口的全限定名,com.example.demo.spi.Car。
文件内容写上实现类的全限定名,如下:
com.example.demo.spi.Train com.example.demo.spi.Truck
最后写一个测试代码:
public class JavaSPITest { @Test public void testCar() { ServiceLoader serviceLoader = ServiceLoader.load(Car.class); serviceLoader.forEach(Car::startUp); } }
执行完的输出结果:
The train started The truck started
Dubbo SPI是如何实现的
Dubbo 使用的SPI并不是Java原生的,而是重新实现了一套,其主要逻辑都在ExtensionLoader类中,逻辑也不难,后面会稍带讲一下
看看使用,和Java的差不了太多,基于前面的例子来看下,接口类需要加上@SPI注解:
@SPI public interface Car { void startUp(); }
实现类不需要改动
配置文件需要放在META-INF/dubbo下面,配置写法有些区别,直接看代码:
train = com.example.demo.spi.Train truck = com.example.demo.spi.Truck
最后就是测试类了,先看代码:
public class JavaSPITest { @Test public void testCar() { ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Car.class); Car car = extensionLoader.getExtension("train"); car.startUp(); } }
执行结果:
The train started
Dubbo SPI中常用的注解
总结一下两者区别:
前面的3个问题是不是已经能回答出来了?是不是非常简单
Dubbo SPI源码分析
Dubbo SPI使用上是通过ExtensionLoader的getExtensionLoader方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,如果没有缓存,则创建一个新的实例,直接上代码:
public T getExtension(String name) { if (name == null || name.length() == 0) { throw new IllegalArgumentException("Extension name == null"); } if ("true".equals(name)) { // 获取默认的拓展实现类 return getDefaultExtension(); } // 用于持有目标对象 Holder holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder()); holder = cachedInstances.get(name); } Object instance = holder.get(); // DCL if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 创建扩展实例 instance = createExtension(name); // 设置实例到 holder 中 holder.set(instance); } } } return (T) instance; } 上面这一段代码主要做的事情就是先检查缓存,缓存不存在创建扩展对象接下来我们看看创建的过程: private T createExtension(String name) { // 从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表 Class clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 反射创建实例 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 向实例中注入依赖 injectExtension(instance); Set> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { // 循环创建 Wrapper 实例 for (Class wrapperClass : wrapperClasses) { // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。 // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量 instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t); } } 这段代码看着繁琐,其实也不难,一共只做了4件事情:1.通过getExtensionClasses获取所有配置扩展类2.反射创建对象3.给扩展类注入依赖4.将扩展类对象包裹在对应的Wrapper对象里面我们在通过名称获取扩展类之前,首先需要根据配置文件解析出扩展类名称到扩展类的映射关系表,之后再根据扩展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下: private Map> getExtensionClasses() { // 从缓存中获取已加载的拓展类 Map> classes = cachedClasses.get(); // DCL if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { // 加载扩展类 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; } 这里也是先检查缓存,若缓存没有,则通过一次双重锁检查缓存,判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面是 loadExtensionClasses 方法的代码 private Map> loadExtensionClasses() { // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的 final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { // 对 SPI 注解内容进行切分 String[] names = NAME_SEPARATOR.split(value); // 检测 SPI 注解内容是否合法,不合法则抛出异常 if (names.length > 1) { throw new IllegalStateException("more than 1 default extension name on extension..."); } // 设置默认名称,参考 getDefaultExtension 方法 if (names.length == 1) { cachedDefaultName = names[0]; } } } Map> extensionClasses = new HashMap>(); // 加载指定文件夹下的配置文件 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadDirectory(extensionClasses, DUBBO_DIRECTORY); loadDirectory(extensionClasses, SERVICES_DIRECTORY); return extensionClasses; } loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情 private void loadDirectory(Map> extensionClasses, String dir) { // fileName = 文件夹路径 + type 全限定名 String fileName = dir + type.getName(); try { Enumeration urls; ClassLoader classLoader = findClassLoader(); // 根据文件名加载所有的同名文件 if (classLoader != null) { urls = classLoader.getResources(fileName); } else { urls = ClassLoader.getSystemResources(fileName); } if (urls != null) { while (urls.hasMoreElements()) { java.net.URL resourceURL = urls.nextElement(); // 加载资源 loadResource(extensionClasses, classLoader, resourceURL); } } } catch (Throwable t) { logger.error("Exception occurred when loading extension class (interface: " + type + ", description file: " + fileName + ").", t); } } loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现 private void loadResource(Map> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { try { BufferedReader reader = new BufferedReader( new InputStreamReader(resourceURL.openStream(), "utf-8")); try { String line; // 按行读取配置内容 while ((line = reader.readLine()) != null) { // 定位 # 字符 final int ci = line.indexOf('#'); if (ci >= 0) { // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略 line = line.substring(0, ci); } line = line.trim(); if (line.length() > 0) { try { String name = null; int i = line.indexOf('='); if (i > 0) { // 以等于号 = 为界,截取键与值 name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0) { // 加载类,并通过 loadClass 方法对类进行缓存 loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { IllegalStateException e = new IllegalStateException("Failed to load extension class..."); } } } } finally { reader.close(); } } catch (Throwable t) { logger.error("Exception when load extension class..."); } } loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下: private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException("..."); } // 检测目标类上是否有 Adaptive 注解 if (clazz.isAnnotationPresent(Adaptive.class)) { if (cachedAdaptiveClass == null) { // 设置 cachedAdaptiveClass缓存 cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { throw new IllegalStateException("..."); } // 检测 clazz 是否是 Wrapper 类型 } else if (isWrapperClass(clazz)) { Set> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet>(); wrappers = cachedWrapperClasses; } // 存储 clazz 到 cachedWrapperClasses 缓存中 wrappers.add(clazz); // 程序进入此分支,表明 clazz 是一个普通的拓展类 } else { // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常 clazz.getConstructor(); if (name == null || name.length() == 0) { // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException("..."); } } // 切分 name String[] names = NAME_SEPARATOR.split(name); if (names != null && names.length > 0) { Activate activate = clazz.getAnnotation(Activate.class); if (activate != null) { // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键, // 存储 name 到 Activate 注解对象的映射关系 cachedActivates.put(names[0], activate); } for (String n : names) { if (!cachedNames.containsKey(clazz)) { // 存储 Class 到名称的映射关系 cachedNames.put(clazz, n); } Class c = extensionClasses.get(n); if (c == null) { // 存储名称到 Class 的映射关系 extensionClasses.put(n, clazz); } else if (c != clazz) { throw new IllegalStateException("..."); } } } } } 综上,loadClass方法操作了不同的缓存,比如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等等到这里基本上关于缓存类加载的过程就分析完了,其他逻辑不难,认真地读下来加上Debug一下都能看懂的。总结从设计思想上来看的话,SPI是对迪米特法则和开闭原则的一种实现。开闭原则:对修改关闭对扩展开放。这个原则在众多开源框架中都非常常见,Spring的IOC容器也是大量使用。迪米特法则:也叫最小知识原则,可以解释为,不该直接依赖关系的类之间,不要依赖;有依赖关系的类之间,尽量只依赖必要的接口。那Dubbo的SPI为什么不直接使用Spring的呢,这一点从众多开源框架中也许都能窥探一点端倪出来,因为本身作为开源框架是要融入其他框架或者一起运行的,不能作为依赖被依赖对象存在。再者对于Dubbo来说,直接用Spring IOC AOP的话有一些架构臃肿,完全没必要,所以自己实现一套轻量级反而是最优解 本文转载自微信公众号「 架构技术专栏」,可以通过以下二维码关注。转载本文请联系 架构技术专栏公众号。 新闻标题:面试重点:来说说DubboSPI机制 本文URL:http://www.shufengxianlan.com/qtweb/news0/37750.html 网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等 广告 声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联 猜你还喜欢下面的内容 如何访问html页面元素 电脑在线安装win7系统的步骤教程 国内服务器国外访问速度慢怎么办?(国外访问企业的服务器速度慢如何变快呢) 生产环境下高并发程序崩溃?麻州大学让多线程软件故障分析自动化 Linux系统中rename命令如何使用 数据库操作常用SQL语句 如何查找数据库的sid? (查找数据库的sid) 文件Linux系统如何重命名文件(linux如何重命名) 如何使用VPS创建VPN,让你的网络更安全?(vps创建vpn) 自适应网站知识 知名端口和公认端口是什么?知名端口可以提供服务吗 开机忘记密码怎么办 Linux下探讨硬件错误信号的解决方法 (linux硬件错误信号) 云服务器不能通过公网ip访问怎么解决 Redis淘汰旧缓存优化性能及体验(redis淘汰缓存机制) 跨域有什么安全隐患 河北省广电网络路由器如何设置 阿里云主机和云服务器有什么区别 开发你的互联网金矿:探索数字时代的机遇与挑战 苏州树间跳动探险乐园门票优惠?苏州双线服务器托管 FabricComputing是云计算的未来? 了解如何使用Terraform管理多云? 为什么三击屏幕会变大 企业上云迎来“黄金时代” JavaI/O大揭秘:BIO、NIO、AIO到底有何区别? 同城分类信息 OPP胶袋 玻璃隔断 不锈钢雕塑 小搅拌车 发电机维修 PVC花箱 木托盘 石凉亭 护栏打桩机 集装箱 自拌料搅拌车 茶艺设计 混凝土搅拌罐车 iso认证 石雕 地磅秤 雅安电信机房 乐山企业网站设计 特丽尔硅藻泥 帛金家居 免费收录网站 达州电信机房 腾讯云香港免备案空间 盐亭网站制作公司 四川瑞杰 网站推广 双流做网站 绵阳主机托管 营销型网站建设 营销型网站建设 护栏打桩机 高端网站建设 免备案虚拟主机 西云机房 雅安主机托管 名片印刷厂
上面这一段代码主要做的事情就是先检查缓存,缓存不存在创建扩展对象
接下来我们看看创建的过程:
private T createExtension(String name) { // 从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表 Class clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 反射创建实例 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 向实例中注入依赖 injectExtension(instance); Set> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { // 循环创建 Wrapper 实例 for (Class wrapperClass : wrapperClasses) { // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。 // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量 instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t); } }
这段代码看着繁琐,其实也不难,一共只做了4件事情:
1.通过getExtensionClasses获取所有配置扩展类
2.反射创建对象
3.给扩展类注入依赖
4.将扩展类对象包裹在对应的Wrapper对象里面
我们在通过名称获取扩展类之前,首先需要根据配置文件解析出扩展类名称到扩展类的映射关系表,之后再根据扩展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下:
private Map> getExtensionClasses() { // 从缓存中获取已加载的拓展类 Map> classes = cachedClasses.get(); // DCL if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { // 加载扩展类 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; }
这里也是先检查缓存,若缓存没有,则通过一次双重锁检查缓存,判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面是 loadExtensionClasses 方法的代码
private Map> loadExtensionClasses() { // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的 final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { // 对 SPI 注解内容进行切分 String[] names = NAME_SEPARATOR.split(value); // 检测 SPI 注解内容是否合法,不合法则抛出异常 if (names.length > 1) { throw new IllegalStateException("more than 1 default extension name on extension..."); } // 设置默认名称,参考 getDefaultExtension 方法 if (names.length == 1) { cachedDefaultName = names[0]; } } } Map> extensionClasses = new HashMap>(); // 加载指定文件夹下的配置文件 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadDirectory(extensionClasses, DUBBO_DIRECTORY); loadDirectory(extensionClasses, SERVICES_DIRECTORY); return extensionClasses; }
loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情
private void loadDirectory(Map> extensionClasses, String dir) { // fileName = 文件夹路径 + type 全限定名 String fileName = dir + type.getName(); try { Enumeration urls; ClassLoader classLoader = findClassLoader(); // 根据文件名加载所有的同名文件 if (classLoader != null) { urls = classLoader.getResources(fileName); } else { urls = ClassLoader.getSystemResources(fileName); } if (urls != null) { while (urls.hasMoreElements()) { java.net.URL resourceURL = urls.nextElement(); // 加载资源 loadResource(extensionClasses, classLoader, resourceURL); } } } catch (Throwable t) { logger.error("Exception occurred when loading extension class (interface: " + type + ", description file: " + fileName + ").", t); } }
loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现
private void loadResource(Map> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { try { BufferedReader reader = new BufferedReader( new InputStreamReader(resourceURL.openStream(), "utf-8")); try { String line; // 按行读取配置内容 while ((line = reader.readLine()) != null) { // 定位 # 字符 final int ci = line.indexOf('#'); if (ci >= 0) { // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略 line = line.substring(0, ci); } line = line.trim(); if (line.length() > 0) { try { String name = null; int i = line.indexOf('='); if (i > 0) { // 以等于号 = 为界,截取键与值 name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0) { // 加载类,并通过 loadClass 方法对类进行缓存 loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { IllegalStateException e = new IllegalStateException("Failed to load extension class..."); } } } } finally { reader.close(); } } catch (Throwable t) { logger.error("Exception when load extension class..."); } }
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:
private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException("..."); } // 检测目标类上是否有 Adaptive 注解 if (clazz.isAnnotationPresent(Adaptive.class)) { if (cachedAdaptiveClass == null) { // 设置 cachedAdaptiveClass缓存 cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { throw new IllegalStateException("..."); } // 检测 clazz 是否是 Wrapper 类型 } else if (isWrapperClass(clazz)) { Set> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet>(); wrappers = cachedWrapperClasses; } // 存储 clazz 到 cachedWrapperClasses 缓存中 wrappers.add(clazz); // 程序进入此分支,表明 clazz 是一个普通的拓展类 } else { // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常 clazz.getConstructor(); if (name == null || name.length() == 0) { // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException("..."); } } // 切分 name String[] names = NAME_SEPARATOR.split(name); if (names != null && names.length > 0) { Activate activate = clazz.getAnnotation(Activate.class); if (activate != null) { // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键, // 存储 name 到 Activate 注解对象的映射关系 cachedActivates.put(names[0], activate); } for (String n : names) { if (!cachedNames.containsKey(clazz)) { // 存储 Class 到名称的映射关系 cachedNames.put(clazz, n); } Class c = extensionClasses.get(n); if (c == null) { // 存储名称到 Class 的映射关系 extensionClasses.put(n, clazz); } else if (c != clazz) { throw new IllegalStateException("..."); } } } } }
综上,loadClass方法操作了不同的缓存,比如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等等
到这里基本上关于缓存类加载的过程就分析完了,其他逻辑不难,认真地读下来加上Debug一下都能看懂的。
总结
从设计思想上来看的话,SPI是对迪米特法则和开闭原则的一种实现。
开闭原则:对修改关闭对扩展开放。这个原则在众多开源框架中都非常常见,Spring的IOC容器也是大量使用。
迪米特法则:也叫最小知识原则,可以解释为,不该直接依赖关系的类之间,不要依赖;有依赖关系的类之间,尽量只依赖必要的接口。
那Dubbo的SPI为什么不直接使用Spring的呢,这一点从众多开源框架中也许都能窥探一点端倪出来,因为本身作为开源框架是要融入其他框架或者一起运行的,不能作为依赖被依赖对象存在。
再者对于Dubbo来说,直接用Spring IOC AOP的话有一些架构臃肿,完全没必要,所以自己实现一套轻量级反而是最优解
本文转载自微信公众号「 架构技术专栏」,可以通过以下二维码关注。转载本文请联系 架构技术专栏公众号。
新闻标题:面试重点:来说说DubboSPI机制 本文URL:http://www.shufengxianlan.com/qtweb/news0/37750.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
广告
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联
猜你还喜欢下面的内容
自适应网站知识
同城分类信息
在线客服
电话咨询
7*24小时客服热线
建站咨询
微信咨询