【译】Tomcat如何工作<第8章:加载程序>

【译】Tomcat如何工作<第8章:加载程序>

技术杂谈小彩虹2021-07-12 8:15:36160A+A-

Overview(总览)

在前面的章节中,你已经看到了一个简单的加载器实现,该实现用于加载servlet类。本章介绍了Catalina中的标准Web应用程序加载程序,或简称为loader。Servlet容器需要自定义的加载器,不能简单地使用系统的类加载器,因为它不应该信任正在运行的Servlet。如果像我们在前面几章中所做的那样,使用系统的类加载器加载所有的servlet和servlet所需要的其他类,那么一个servlet将能够访问运行中的Java虚拟机(JVM)的CLASSPATH环境变量中所包含的任何类和库,这将是一种安全漏洞。仅允许Servlet从WEB-INF/classes目录及其子目录中以及从部署到WEB-INF/lib目录中的库中加载类。这就是为什么servlet容器需要自己的加载器。Servlet容器中的每个Web应用程序(上下文)都有其自己的加载器。加载程序使用类加载程序,该类加载程序将某些规则应用于加载类。在Catalina中,加载器由org.apache.catalina.Loader接口表示。

Tomcat需要其自己的加载器的另一个原因是,只要修改了WEB-INF/classesWEB-INF/lib目录中的类,就支持自动重新加载。Tomcat加载器实现中的类加载器使用一个单独的线程,该线程不断检查Servlet和支持类文件的时间戳。为了支持自动重新加载,类加载器必须实现org.apache.catalina.loader.Reloader接口。

本章的第一部分简要回顾了Java中的类加载机制。接下来,它介绍了所有加载程序都必须实现的Loader接口,然后是Reloader接口。在查看了加载器和类加载器的实现之后,本章介绍了一个应用程序,该应用程序演示了如何使用Tomcat的加载器。

本章广泛使用两个术语:存储库(repository)和资源(resources)。存储库是一个由类加载器搜索的地方。术语资源是指类加载器中的DirContext对象,该对象的文档库指向上下文的文档库。

Java Class Loader

每次创建Java类的实例时,都必须先将该类加载到内存中。JVM使用类加如果找不到所需的类,则会引发java.lang.ClassNotFoundException。载器加载类。类加载器通常搜索一些核心Java库以及CLASSPATH环境变量中包含的所有目录。如果找不到所需的类,则会引发java.lang.ClassNotFoundException

从J2SE 1.2开始,JVM使用了三个类加载器:引导类加载器、扩展类加载器和系统类加载器。三个类加载器中的每一个都具有父子关系,其中引导类加载器位于层次结构的顶部,而系统类加载器位于底部。

引导类加载器用于引导JVM。只要您调用java.exe程序,它就会开始工作。因此,必须使用本机代码来实现它,因为它用于加载JVM运行所需的类。而且,它负责加载所有核心Java类,例如java.langjava.io包中的类。引导类加载器搜索核心库,例如rt.jari18n.jar等。要搜索哪些库取决于JVM和操作系统的版本。

扩展类加载器负责加载标准扩展目录中的类。这是为了让程序员的生活更轻松,因为他们只需要将jar文件复制到这个扩展目录中,jar文件就会被自动搜索到。各个厂商的扩展库都不一样。Sun的JVM的标准扩展目录是/jdk/jre/lib/ext

系统类加载器是默认的类加载器,它搜索CLASSPATH环境变量中指定的目录和JAR文件。

那么,JVM使用哪个类加载器?答案在于委托模型,出于安全原因而存在。每次需要加载类时,都会首先调用系统类加载器。但是,它不会立即加载该类。而是将任务委托给其父级(扩展类加载器)。扩展类加载器还将其委托给其父级(引导类加载器)。因此,引导类加载器始终被赋予首次加载类的机会。如果引导类加载器找不到所需的类,则扩展类加载器将尝试加载该类。如果扩展类加载器也失败,则系统类加载器将执行任务。如果系统类加载器找不到该类,则抛出java.lang.ClassNotFoundException。为什么要来回跑?

委托模型对于安全性非常重要。如你所知,你可以使用安全管理器来限制对特定目录的访问。现在,有恶意的人可以编写一个名为java.lang.Object的类,该类可用于访问硬盘中的任何目录。因为JVM信任java.lang.Object类,所以它不会在这方面监视其活动。结果,如果允许加载自定义java.lang.Object,则安全管理器将很容易瘫痪。幸运的是,由于委托模型,这种情况不会发生。下面是它的工作原理。

当在程序中的某个位置调用自定义java.lang.Object类时,系统类加载器将请求委托给扩展类加载器,而扩展类加载器则将委托给引导类加载器。引导类加载器搜索其核心库,并找到标准的java.lang.Object并将其实例化。结果,将永远不会加载自定义java.lang.Object

Java中的类加载机制的妙处在于,你可以通过扩展抽象的java.lang.ClassLoader类来编写自己的类加载器。Tomcat需要自定义类加载器的原因包括:

  • 在加载类中指定某些规则。
  • 缓存以前加载的类。
  • 预加载类,以便可以使用它们。

The Loader Interface

在Web应用程序中加载servlet和其它类有规则。例如,应用程序中的servlet可以使用部署到WEB-INF/classes目录及其下的任何子目录的类。但是,即使这些类包含在运行Tomcat的JVM的CLASSPATH中,它也无法访问其它类。此外,Servlet只能访问部署在WEB-INF/lib目录下的库,而不能访问其它目录。

Tomcat加载器代表Web应用程序加载器,而不是类加载器。加载程序必须实现org.apache.catalina.Loader接口。加载程序实现使用由org.apache.catalina.loader.WebappClassLoader类表示的自定义类加载程序。你可以使用Loader接口的getClassLoader方法在Web加载器中获取ClassLoader。

其中,Loader接口定义了与存储库集合一起使用的方法。Web应用程序的WEB-INF/classesWEB-INF/lib是要添加为存储库的目录。Loader接口的addRepository方法用于添加存储库,其findRepositories方法返回所有存储库的数组。

Tomcat加载器实现通常与上下文相关联,并且使用Loader接口的getContainersetContainer方法来建立此关联。如果已修改上下文中的一个或多个类,则加载程序还可以支持重新加载。这样,servlet程序员可以重新编译servlet或支持类,并且将在不重新启动Tomcat的情况下重新装入新类。为了进行重新加载,Loader接口具有修改后的方法。在加载程序实现中,如果修改了其存储库中的一个或多个类,则修改后的方法必须返回true,因此需要重新加载。但是,加载程序不会自行重新加载。而是调用Context接口的reload方法。其它两种方法setReloadablegetReloadable用于确定是否在加载程序中启用了重新加载。默认情况下,在Context的标准实现中(第12章中讨论的org.apache.catalina.core.StandardContext类),不启用重新加载。因此,要启用上下文的重新加载,你需要在server.xml文件中为该上下文添加一个Context元素,例如:

<Context path="/myApp" docBase="myApp" debug="0" reloadable="true"/>

同样,可以告知Loader实现是否委托给父类加载器。为此,Loader接口提供了getDelegatesetDelegate方法。

清单8.1中给出了Loader接口。

清单8.1:Loader接口

package org.apache.catalina;
import java.beans.PropertyChangeListener;
public interface Loader {
    public ClassLoader getClassLoader();
    public Container getContainer();
    public void setContainer(Container container);
    public DefaultContext getDefaultContext();
    public void setDefaultContext(DefaultContext defaultContext);
    public boolean getDelegate();
    public void setDelegate(boolean delegate);
    public String getInfo();
    public boolean getReloadable();
    public void setReloadable(boolean reloadable);
    public void addPropertyChangeListener(PropertyChangeListener listener);
    public void addRepository(String repository);
    public String[] findRepositories();
    public boolean modified();
    public void removePropertyChangeListener(PropertyChangeListener listener);
}

Catalina提供org.apache.catalina.loader.WebappLoader作为Loader接口的实现。对于其类加载器,WebappLoader对象包含org.apache.catalina.loader.WebappClassLoader类的实例,该实例扩展了java.net.URLClassLoader类。

注意,每当与加载器关联的容器需要Servlet类时,即在调用其invoke方法时,容器首先会调用加载器的getClassLoader方法以获得类加载器。然后,容器调用类加载器的loadClass方法来加载servlet类。有关更多详细信息,请参见第11章“StandardWrapper”。

图8.1给出了Loader接口的类图及其实现。

The Reloader Interface

为了支持自动重新加载,类加载器实现必须实现清单8.2中的org.apache.catalina.loader.Reloader接口。

清单8.2:Reloader接口

package org.apache.catalina.loader;
public interface Reloader {
    public void addRepository(String repository);
    public String[] findRepositories ();
    public boolean modified();
}

Reloader接口中最重要的方法是modified,如果Web应用中的servlet或支持类中的一个被修改,则返回trueaddRepository方法用于添加一个仓库,findRepositories方法返回一个String数组,其中包含实现Reloader的类加载器中的所有仓库。

The WebappLoader Class

org.apache.catalina.loader.WebappLoader类是Loader接口的实现,它表示一个Web应用程序加载器,负责为Web应用程序加载类。WebappLoader创建org.apache.catalina.loader.WebappClassLoader类的实例作为其类加载器。与其它Catalina组件一样,WebappLoader实现org.apache.catalina.Lifecycle,并由关联的容器启动和停止。WebappLoader类还实现java.lang.Runnable接口,以便它可以专用于重复调用其类加载器的modified方法的线程。如果modified方法返回true,则WebappLoader实例会通知其关联的容器(在这种情况下为上下文)。类的重新加载本身是由上下文执行的,而不是由WebappLoader执行的。在第12章“StandardContext”中讨论了Context如何做到这一点。

调用WebappLoader类的start方法时,将执行重要任务:

  • 创建一个类加载器
  • 设置存储库
  • 设置类路径
  • 设定权限
  • 启动新线程进行自动重装。

以下小节将讨论这些任务中的每一个。

Creating A Class Loader

对于加载类,WebappLoader实例使用内部类加载器。你可能从对Loader接口的讨论中回想起,该接口提供了getClassLoader方法,但是没有setClassLoader。因此,你无法实例化类加载器并将其传递给WebappLoader。这是否意味着WebappLoader不具有与非默认类加载器一起使用的灵活性?

答案是不。WebappLoader提供了getLoaderClasssetLoaderClass方法来获取和更改其私有变量loaderClass的值。此变量是一个String,代表类加载器的类名称。默认情况下,loaderClass的值为org.apache.catalina.loader.WebappClassLoader。如果愿意,可以创建扩展WebappClassLoader的自己的类加载器,并调用setLoaderClass强制WebappLoader使用自定义类加载器。否则,在启动时,WebappLoader将通过调用其私有的createClassLoader方法来创建WebappClassLoader的实例。清单8.3中给出了此方法。

清单8.3:createClassLoader方法

private WebappClassLoader createClassLoader() throws Exception {
    Class clazz = Class.forName(loaderClass);
    WebappClassLoader classLoader = null;
    if (parentClassLoader == null) {
        // Will cause a ClassCast if the class does not extend
        // WebappClassLoader, but this is on purpose (the exception will be
        // caught and rethrown)
        classLoader = (WebappClassLoader) clazz.newInstance();
        // in Tomcat 5, this if block is replaced by the following:
        // if (parentClassLoader == null) {
        // parentClassLoader =
        // Thread.currentThread().getContextClassLoader();
        // }
    } else {
        Class[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoader) constr.newInstance(args);
    }
    return classLoader;
}

除了WebappClassLoader实例之外,还可以使用其它类加载器。但是请注意,createClassLoader方法返回一个WebappClassLoader。因此,如果你的自定义类加载器未扩展WebappClassLoader,则此方法将引发异常。

Setting Repositories

WebappLoader类的start方法调用setRepositories方法,以将存储库添加到其类加载器。WEB-INF/classes目录传递到类加载器的addRepository方法,而WEB-INF/lib目录传递到类加载器的setJarPath方法。这样,类加载器将能够从WEB-INF/classes目录以及部署到WEB-INF/lib目录的任何库中加载类。

Setting the Class Path

通过start方法调用setClassPath方法来执行此任务。setClassPath方法为servlet上下文中的属性分配一个字符串,该字符串包含Jasper JSP编译器的类路径信息。这里将不讨论。

Setting Permissions

如果在运行Tomcat时使用了安全管理器,则setPermissions方法会将权限添加到类加载器,以访问必要的目录,例如WEB-INF/classesWEB-INF/lib。如果未使用安全管理器,则此方法立即返回。

Starting a New Thread for Auto-Reload

WebappLoader支持自动重新加载。如果重新编译WEB-INF/classesWEB-INF/lib目录中的类,则必须自动重新装入该类,而无需重新启动Tomcat。为此,WebappLoader具有一个线程,该线程每x秒连续检查每个资源的日期戳。此处的x由checkInterval变量的值定义。默认情况下,其值为15,这意味着每15秒执行一次自动重载检查。getCheckIntervalsetCheckInterval方法用于访问此变量。

在Tomcat 4中,WebappLoader实现了java.lang.Runnable接口以支持自动重载。清单8.3给出了WebappLoader中run方法的实现。

清单8.3:run方法

public void run() {
    if (debug >= 1) {
        log("BACKGROUND THREAD Starting");
    }
    // Loop until the termination semaphore is set
    while (!threadDone) {
        // Wait for our check interval
        threadSleep();
        if (!started) {
            break;
        }
        try {
            // Perform our modification check
            if (!classLoader.modified()) {
                continue;
            }
        } catch (Exception e) {
            log(sm.getString("webappLoader.failModifiedCheck"), e);
            continue;
        }
        // Handle a need for reloading
        notifyContext();
        break;
    }
    if (debug >= 1) {
        log("BACKGROUND THREAD Stopping");
    }
}

注意:在Tomcat 5中,检查修改后的类的任务是由org.apache.catalina.core.StandardContext对象的backgroundProcess方法执行的。org.apache.catalina.core.ContainerBase类(StandardContext的父类)中的专用线程会定期调用此方法。检查ContainerBase类的实现Runnable的ContainerBackgroundProcessor内部类。

清单8.3中的run方法在其核心处包含一个while循环,该循环将一直运行,直到将启动变量(用于指示WebappLoader已启动)设置为false为止。while循环执行以下操作:

  • 睡眠时间由checkInterval变量指定。
  • 通过调用WebappLoader实例的类加载器的modified方法,检查它加载的任何类是否被修改。如果没有,继续。
  • 如果一个类被修改了,调用notifyContext私有方法,要求与这个WebappLoader关联的Context重新加载。

清单8.4给出了notifyContext方法。

清单8.4:notifyContext方法

private void notifyContext() {
    WebappContextNotifier notifier = new WebappContextNotifier();
    (new Thread(notifier)).start();
}

notifyContext方法不会直接调用Context接口的reload方法。而是实例化内部类WebappContextNotifier并传递线程对象并调用其start方法。这样,重新加载的执行将由不同的线程执行。清单8.5中给出了WebappContextNotifier类。

清单8.5:WebappContextNotifier内部类

protected class WebappContextNotifier implements Runnable {
    public void run() {
        ((Context) container).reload();
    }
}

当将WebappContextNotifier的实例传递给线程并调用Thread对象的start方法时,将执行WebappContextNotifier实例的run方法。反过来,run方法调用Context接口的reload方法。 你可以在第12章的org.apache.catalina.core.StandardContext类中看到如何实现reload方法。

The WebappClassLoader Class

org.apache.catalina.loader.WebappClassLoader类表示负责加载Web应用程序中使用的类的类加载器。WebappClassLoader扩展了java.net.URLClassLoader类,该类是我们在前几章中用于在应用程序中加载Java类的类。

WebappClassLoader专为优化和安全而设计。例如,它缓存以前加载的类以增强性能。它还会缓存未能找到的类的名称,以便下次请求加载相同的类时,类加载器可以抛出ClassNotFoundException而无需先尝试查找它们。WebappClassLoader在存储库列表以及指定的JAR文件中搜索类。

关于安全性,WebappClassLoader不允许加载某些类。这些类存储在String数组触发器中,当前具有一个成员:

private static final String[] triggers = {
    "javax.servlet.Servlet" // Servlet API
};

另外,在未首先委派给系统类加载器的情况下,不允许加载属于这些包及其子包的类。

private static final String[] packageTriggers = {
    "javax", // Java extensions
    "org.xml.sax", // SAX 1 & 2
    "org.w3c.dom", // DOM 1 & 2
    "org.apache.xerces", // Xerces 1 & 2
    "org.apache.xalan" // Xalan
};

现在让我们在以下小节中了解此类如何执行缓存和类加载。

Caching

为了获得更好的性能,将对加载的类进行缓存,以便下次需要该类时,可以从缓存中获取该类。缓存可以在本地完成,这意味着缓存由WebappClassLoader实例管理。此外,java.lang.ClassLoader会维护一个之前加载的类的Vector,以防止这些类被垃圾回收。在这种情况下,缓存由超类管理。

WebappClassLoader可以加载的每个类(无论是作为WEB-INF/classs下的类文件部署还是从JAR文件部署)都称为资源。资源由org.apache.catalina.loader.ResourceEntry类表示。ResourceEntry实例保存类的字节数组表示形式,上次修改日期,Manifest(如果资源来自JAR文件)等。

清单8.6中给出了ResourceEntry类。

清单8.6:ResourceEntry类

package org.apache.catalina.loader;

import java.net.URL;
import java.security.cert.Certificate;
import java.util.jar.Manifest;

public class ResourceEntry {
    public long lastModifled = -1;
    // Binary content of the resource.
    public byte[] binaryContent = null;
    public Class loadedClass = null;
    // URL source from where the object was loaded.
    public URL source = null;
    // URL of the codebase from where the object was loaded.
    public URL CodeBase = null;
    public Manifest manifest = null;
    public Certificate[] certificates = null;
}

所有缓存的资源都存储在名为resourceEntries的HashMap中。键是资源名称。找不到的所有资源都存储在另一个名为notFoundResources的HashMap中。

Loading Classes

加载类时,WebappClassLoader类将应用以下规则:

  • 所有以前加载的类都被缓存,因此首先检查本地缓存。
  • 如果在本地缓存中没有找到,就在缓存中检查,即调用java.lang.ClassLoader类的findLoadedClass
  • 如果在两个缓存中都没有找到,则使用系统的类加载器来防止Web应用程序覆盖J2EE类。
  • 如果使用了SecurityManager,检查是否允许加载该类。如果不允许加载该类,则抛出ClassNotFoundException。
  • 如果委托标志开启,或者要加载的类属于包触发器中的包名,则使用父类加载器来加载该类。如果父类加载器为空,则使用系统类加载器。
  • 从当前存储库加载类。
  • 如果在当前资源库中没有找到该类,并且委托标志没有打开,则使用父类加载器。如果父类加载器为空,则使用系统类加载器。
  • 如果仍未找到该类,则抛出ClassNotFoundException。

The Application

本章随附的应用程序演示了如何使用与上下文关联的WebappLoader实例。Context的标准实现是org.apache.catalina.core.StandardContext类,因此此应用程序实例化了StandardContext类。但是,有关StandardContext本身的讨论将推迟到第12章。你无需在此阶段了解此类的详细信息。你只需了解StandardContext,它就可以与侦听器一起使用,以侦听其触发的事件,例如START_EVENTSTOP_EVENT。侦听器必须实现org.apache.catalina.lifecycle.LifecycleListener接口,并调用StandardContext类的setConfigured方法。对于此应用程序,侦听器由ex08.pyrmont.core.SimpleContextConfig类表示,如清单8.6所示。

清单8.6:SimpleContextConfig类

package ex08.pyrmont.core;

import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;

public class SimpleContextConfig implements LifecycleListener {
    public void lifecycleEvent(LifecycleEvent event) {
        if (Lifecycle.START_EVENT.equals(event.getType())) {
            Context context = (Context) event.getLifecycle();
            context.setConfigured(true);
        }
    }
}

你所需要做的就是实例化StandardContext和SimpleContextConfig,然后通过调用org.apache.catalina.Lifecycle接口的addLifecycleListener方法在StandardContext中注册后者。在第6章“生命周期”中详细讨论了此接口。

此外,该应用程序保留了上一章中的以下类:SimplePipeline,SimpleWrapper和SimpleWrapperValve。

可以使用PrimitiveServlet和ModernServlet来测试应用程序,但是这次使用StandardContext指示将servlet存储在应用程序目录的WEB-INF/classes下。该应用程序目录称为myApp,应在首次部署可下载的ZIP文件时创建。为了告诉StandardContext实例在哪里可以找到应用程序目录,可以使用user.dir属性的值设置一个名为catalina.base的系统属性,如下所示。

System.setProperty("catalina.base", System.getProperty("user.dir"));

实际上,这是Bootstrap类的main方法的第一行。然后,main方法实例化默认连接器。

Connector connector = new HttpConnector();

然后,它为两个servlet创建两个包装器并对其进行初始化,就像上一章中的应用程序一样。

Wrapper wrapper1 = new SimpleWrapper();
wrapper1.setName("Primitive");
wrapper1.setServletClass("PrimitiveServlet");
Wrapper wrapper2 = new SimpleWrapper();
wrapper2.setName("Modern");
wrapper2.setServletClass("ModernServlet");

然后,它创建StandardContext的实例,并设置路径以及上下文的文档库。

Context context = new StandardContext();
// StandardContext's start method adds a default mapper
context.setPath("/myApp");
context.setDocBase("myApp");

这等效于在server.xml文件中包含以下元素。

<Context path="/myApp" docBase="myApp"/>

然后,将两个包装器添加到上下文中,并为其添加映射,以便上下文可以找到包装器。

context.addChild(wrapper1);
context.addChild(wrapper2);
context.addServletMapping("/Primitive", "Primitive");
context.addServletMapping("/Modern", "Modern");

下一步是实例化侦听器并将其注册到上下文。

LifecycleListener listener = new SimpleContextConfig();
((Lifecycle) context).addLifecycleListener(listener);

接下来,它实例化WebappLoader并将其与上下文关联。

Loader loader = new WebappLoader();
context.setLoader(loader);

然后,将上下文与默认连接器关联,并调用连接器的initializestart方法,然后调用上下文的start方法。这会将Servlet容器投入使用。

connector.setContainer(context);
try {
    connector.initialize();
    ((Lifecycle) connector).start();
    ((Lifecycle) context).start();

接下来的几行仅显示资源的docBase以及类加载器中所有存储库的值。

// now we want to know some details about WebappLoader
WebappClassLoader classLoader = (WebappClassLoader)loader.getClassLoader();
System.out.println("Resources' docBase: " + ((ProxyDirContext)classLoader.getResources ()).getDocBase());
String[] repositories = classLoader.findRepositories();
for (int i=0; i<repositorles.length; i++) {
    System.out.println(" repository: " + repositories[i]);
}

这些行将使docBase和运行应用程序时显示的存储库列表。

Resources' docBase: C:\HowTomcatWorks\myApp repository: /WEB-INF/classes/ 

docBase的值在你的计算机上可能有所不同,具体取决于你在哪里安装应用程序。

最后,该应用程序等待,直到用户在控制台上按Enter停止该应用程序。

// make the application wait until we press a key.
System.in.read();
((Lifecycle) context).stop();

Running the Application

要在Windows中运行该应用程序,请在工作目录中键入以下内容:

java -classpath ./lib/servlet.jar;./lib/commons-collections.jar;./ex08.pyrmont.startup.Bootstrap

在Linux中,使用冒号分隔两个库。

java -classpath ./lib/servlet.jar:./lib/commons-collections.jar:./ex08.pyrmont.startup.Bootstrap

要调用PrimitiveServlet servlet,请在浏览器中使用以下URL。

http://localhost:8080/Primitive

要调用ModernServlet,请使用以下URL。

http://localhost:8080/Modern

Summary(摘要)

Web应用程序加载程序,或简称为加载程序,是Catalina中最重要的组件。加载器负责加载类,因此使用内部类加载器。这个内部类加载器是一个自定义的类,Tomcat使用它来将某些规则应用于应用程序上下文中的类加载。而且,定制的类加载器支持缓存,并且可以检查是否已修改一个或多个类。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们