Tomcat源码解析:Catalina源码解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_26323323/article/details/84848614

1.Catalina

    对于Tomcat来说,Catalina是其核心组件,所有基于JSP/Servlet的Java Web应用均需要依托Servlet容器运行并对外提供服务。

    4.0版本后,Tomcat完全重新设计了其Servlet容器的架构,新版本的Servlet容器被命名为Catalina。

    Catalina包含了前面讲到的所有容器组件。它通过松耦合的方式继承Coyote,以完成按照请求协议进行数据读写。同时还包括我们的启动入口、Shell程序等

    1)Tomcat分层示意图

    2)Digester

        Catalina使用Digester解析XML配置文件并创建应用服务器

        Digester是一款将XML转换为java对象的事件驱动型工具,是对SAX的高层次封装。通过流读取XML文件,当识别出XML节点后便执行特定的动作,或者创建java对象,或者执行对象的某个方法。

        Digester路径为:tomcat-util-scan.jar包下 org.apache.tomcat.util.digester包路径下

    3)Server

        以下是有关Server的结构

 

2.tomcat8.0.52 有关于Catalina源码解析

    1)%TOMCAT_HOME%/bin/startup.bat

        作为tomcat的启动文件,可以看到,其直接调用了catalina.bat文件

set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start
set SECURITY_POLICY_FILE=
set DEBUG_OPTS=
set JPDA=

    可以看到主类为Bootstrap,执行方法为start

    2)Bootstrap

    反射的绝佳用例

    /**
     * Start the Catalina daemon.
     */
    public void start()
        throws Exception {
        if( catalinaDaemon==null ) init();
        Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
        method.invoke(catalinaDaemon, (Object [])null);
    }

    其主要作用就是启动一个catalinaDaemon,在其init()方法中可以看到

        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();
...
        catalinaDaemon = startupInstance;

    3)Catalina.start()

/**
     * Start a new server instance.
     */
    public void start() {

        if (getServer() == null) {
            load();
        }

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        long t1 = System.nanoTime();

        // Start the new server
        try {
            getServer().start();
...
/**
     * Start a new server instance.
     */
    public void load() {

        if (loaded) {
            return;
        }
        loaded = true;

        long t1 = System.nanoTime();

        initDirs();

        // Before digester - it may be needed
        initNaming();

        // Create and execute our Digester
        Digester digester = createStartDigester();
...
    /**
     * Create and configure the Digester we will be using for startup.
     */
    protected Digester createStartDigester() {
        long t1=System.currentTimeMillis();
        // Initialize the digester
        Digester digester = new Digester();
        digester.setValidating(false);
        digester.setRulesValidation(true);
        HashMap<Class<?>, List<String>> fakeAttributes = new HashMap<>();
        ArrayList<String> attrs = new ArrayList<>();
        attrs.add("className");
        fakeAttributes.put(Object.class, attrs);
        digester.setFakeAttributes(fakeAttributes);
        digester.setUseContextClassLoader(true);

        // Configure the actions we will be using
        digester.addObjectCreate("Server",
                                 "org.apache.catalina.core.StandardServer",
                                 "className");
        digester.addSetProperties("Server");
...

    在初始化的时候根据server.xml配置来加载Server、Service等对象

3.Digester

    Catalina使用Digester来解析XML(server.xml)配置文件并创建应用服务器。

    Digester是一款用于将XML转换为java对象的事件驱动型工具。是对SAX的高层次封装。

    现在Digester已经移到了Apache Commons项目,我们可以通过以下来获取对Digester的使用

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-digester3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-digester3</artifactId>
    <version>3.2</version>
</dependency>

    具体的使用可参考:Apache Commons Digester 一 (基础内容、核心API) 这篇文章

4.Web应用加载

    Web应用加载属于Server启动的核心处理过程。

    Catalina对web应用的加载主要由StandardHost、HostConfig、StandardContext、ContextConfig、StandardWrapper这五个类来完成。时序图如下:

5.StandardServer

    Catalina在解析server.xml时调用Catalina.createStartDigester()方法,具体内容如下:

// 1.关于Server的解析,创建        
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");

// 2.关于Service的解析创建
digester.addObjectCreate("Server/Service",
                         "org.apache.catalina.core.StandardService",
                         "className");
digester.addSetProperties("Server/Service");

// 调用其Server.addService方法,将该service添加到Server中
digester.addSetNext("Server/Service",
                    "addService",
                    "org.apache.catalina.Service");

    默认会加载StandardServer类。

    并且会创建Service实现类,并添加到Server中。

    1)StandardServer.addService()如下所示

public void addService(Service service) {

    service.setServer(this);

    synchronized (servicesLock) {
        Service results[] = new Service[services.length + 1];
        System.arraycopy(services, 0, results, 0, services.length);
        results[services.length] = service;
        services = results;

        if (getState().isAvailable()) {
            try {
                // 启动service
                service.start();
            } catch (LifecycleException e) {
                // Ignore
            }
        }

        // Report this property change to interested listeners
        support.firePropertyChange("service", null, service);
    }
}

    2)StandardService.start()

    默认实现在LifecycleBase类中,每个组件都使用到了这个方法,各自实现其startInternal()方法即可

6.StandardService

    Catalina在解析server.xml时调用Catalina.createStartDigester()方法,有关于Service具体内容如下:

// 1.创建Service
digester.addObjectCreate("Server/Service",
                         "org.apache.catalina.core.StandardService",
                         "className");

// 2.创建Listener 并添加到Service
digester.addObjectCreate("Server/Service/Listener",
                         null, // MUST be specified in the element
                         "className");
digester.addSetProperties("Server/Service/Listener");
digester.addSetNext("Server/Service/Listener",
                    "addLifecycleListener",
                    "org.apache.catalina.LifecycleListener");

// 3.创建Executor,并添加到Service
digester.addObjectCreate("Server/Service/Executor",
                         "org.apache.catalina.core.StandardThreadExecutor",
                         "className");
digester.addSetProperties("Server/Service/Executor");

digester.addSetNext("Server/Service/Executor",
                    "addExecutor",
                    "org.apache.catalina.Executor");

// 4.创建Connector并添加到Service
digester.addRule("Server/Service/Connector",
                 new ConnectorCreateRule());
digester.addRule("Server/Service/Connector",
                 new SetAllPropertiesRule(new String[]{"executor", "sslImplementationName"}));
digester.addSetNext("Server/Service/Connector",
                    "addConnector",
                    "org.apache.catalina.connector.Connector");

    1)StandardService.startInternal()

protected void startInternal() throws LifecycleException {

    if(log.isInfoEnabled())
        log.info(sm.getString("standardService.start.name", this.name));
    setState(LifecycleState.STARTING);

    // 1.启动engine
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }

    // 2.启动executor
    synchronized (executors) {
        for (Executor executor: executors) {
            executor.start();
        }
    }

    mapperListener.start();

    // 3.启动connector
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            try {
                // If it has already failed, don't try and start it
                if (connector.getState() != LifecycleState.FAILED) {
                    connector.start();
                }
            } catch (Exception e) {
                log.error(sm.getString(
                    "standardService.connector.startFailed",
                    connector), e);
            }
        }
    }
}

7.StandardEngine

    同理,我们通过分析以上的方式来分析StandardEngine

8.StandardHost

    StandardHost加载web应用(即StandardContext)的入口有两个:

    * Catalina在构造Server实例时,如果在server.xml中Host元素存在Context子元素,那么Context元素就会作为Host容器的子容器添加到Host实例中

    * HostConfig自动扫描部署目录,创建Context实例并启动。这是大多数Web应用的加载方式

    1)StandardHost.startInternal()启动虚拟主机

protected synchronized void startInternal() throws LifecycleException {

    // Set error report valve
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        try {
            boolean found = false;
            Valve[] valves = getPipeline().getValves();
            for (Valve valve : valves) {
                if (errorValve.equals(valve.getClass().getName())) {
                    found = true;
                    break;
                }
            }
            if(!found) {
                Valve valve =
                    (Valve) Class.forName(errorValve).newInstance();
                getPipeline().addValve(valve);
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                "standardHost.invalidErrorReportValveClass",
                errorValve), t);
        }
    }
    super.startInternal();
}

9.HostConfig

    HostConfig类注释如下:

/**
 * Startup event listener for a <b>Host</b> that configures the properties
 * of that Host, and the associated defined contexts.
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class HostConfig implements LifecycleListener {

    主要用于监听Host的事件,实现了lifecycleEvent方法,具体内容如下:

public void lifecycleEvent(LifecycleEvent event) {

    // Identify the host we are associated with
    try {
        host = (Host) event.getLifecycle();
        if (host instanceof StandardHost) {
            setCopyXML(((StandardHost) host).isCopyXML());
            setDeployXML(((StandardHost) host).isDeployXML());
            setUnpackWARs(((StandardHost) host).isUnpackWARs());
            setContextClass(((StandardHost) host).getContextClass());
        }
    } catch (ClassCastException e) {
        log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
        return;
    }

    // 定时扫描Web应用的变更,并进行重新加载
    if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
        check();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
        
    // 该事件在Host启动时触发,host启动后扫描web应用包进行部署
    } else if (event.getType().equals(Lifecycle.START_EVENT)) {
        start();
    } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
        stop();
    }
}

    既然说是Host的监听器,那么我们就从源码的角度来分析一下HostConfig是如何监控的

    1)寻找lifecycleEvent方法的被调用场景

    发现只有LifecycleBase.fireLifecycleEvent有调用

    2)寻找LifecycleBase.fireLifecycleEvent方法被调用场景

        分析:既然是Host的监听器,那就应该是StandardHost的某个方法调用的,我们来看下StandardHost的类结构图

        通过上面两个图结合来看,可以确定在以下两个方法中

        而backgroundProcess方法中监听的是Lifecycle.PERIODIC_EVENT事件,所以我们最后就确定LifecycleBase.setStateInternal方法

        LifecycleBase.setState方法有关于调用LifecycleBase.setStateInternal()

        

    3)寻找LifecycleBase.setState方法被调用场景

        我们直接去其子类ContainerBase中看setState被调用的场景

        发现有两个方法startInternal、stopInternal调用了setState方法,这两个方法就很熟悉了,startInternal是抽象类LifecycleBase的抽象方法,需要子类实现的

        startInternal方法在StandardHost中也有实现,内容如下:

protected synchronized void startInternal() throws LifecycleException {

    // Set error report valve
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        try {
            boolean found = false;
            Valve[] valves = getPipeline().getValves();
            for (Valve valve : valves) {
                if (errorValve.equals(valve.getClass().getName())) {
                    found = true;
                    break;
                }
            }
            if(!found) {
                Valve valve =
                    (Valve) Class.forName(errorValve).newInstance();
                getPipeline().addValve(valve);
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                "standardHost.invalidErrorReportValveClass",
                errorValve), t);
        }
    }
    // 调用ContainerBase.startInternal()
    super.startInternal();
}

    

    4)那么HostConfig的监听器是什么时候关联到StandardHost的呢?

    Catalina.createStartDigester()方法是解析server.xml时被调用的,里面有一句

digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));

// addRuleSet方法
public void addRuleSet(RuleSet ruleSet) {

    String oldNamespaceURI = getRuleNamespaceURI();
    String newNamespaceURI = ruleSet.getNamespaceURI();
    if (log.isDebugEnabled()) {
        if (newNamespaceURI == null) {
            log.debug("addRuleSet() with no namespace URI");
        } else {
            log.debug("addRuleSet() with namespace URI " + newNamespaceURI);
        }
    }
    setRuleNamespaceURI(newNamespaceURI);
    // 主要是这句,会调用HostRuleSet.addRuleInstances方法
    ruleSet.addRuleInstances(this);
    setRuleNamespaceURI(oldNamespaceURI);
}

// 我们来看下这个HostRuleSet.addRuleInstances方法
public void addRuleInstances(Digester digester) {

    // 1.创建StandardHost
    digester.addObjectCreate(prefix + "Host",
                             "org.apache.catalina.core.StandardHost",
                             "className");
    digester.addSetProperties(prefix + "Host");
    digester.addRule(prefix + "Host",
                     new CopyParentClassLoaderRule());
    // 2.创建并添加HostConfig
    digester.addRule(prefix + "Host",
                     new LifecycleListenerRule
                     ("org.apache.catalina.startup.HostConfig",
                      "hostConfigClass"));
    digester.addSetNext(prefix + "Host",
                        "addChild",
                        "org.apache.catalina.Container");

    digester.addCallMethod(prefix + "Host/Alias",
                           "addAlias", 0);

    //Cluster configuration start
    digester.addObjectCreate(prefix + "Host/Cluster",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties(prefix + "Host/Cluster");
    digester.addSetNext(prefix + "Host/Cluster",
                        "setCluster",
                        "org.apache.catalina.Cluster");
    //Cluster configuration end

    digester.addObjectCreate(prefix + "Host/Listener",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties(prefix + "Host/Listener");
    digester.addSetNext(prefix + "Host/Listener",
                        "addLifecycleListener",
                        "org.apache.catalina.LifecycleListener");

    digester.addRuleSet(new RealmRuleSet(prefix + "Host/"));

    digester.addObjectCreate(prefix + "Host/Valve",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties(prefix + "Host/Valve");
    digester.addSetNext(prefix + "Host/Valve",
                        "addValve",
                        "org.apache.catalina.Valve");
}

    所以:HostConfig是在使用Digester解析server.xml时就关联到StandardHost的

    至此,我们就看到一条完整的调用链:

    StandardHost.startInternal() -> ContainerBase.startInternal() -> LifecycleBase.setState() -> LifecycleBase.setStateInternal() -> LifecycleBase.fireLifecycleEvent() -> HostConfig.lifecycleEvent()

    

10.StandardContext    

    StandardContext包含了具体的web应用初始化及启动工作,该部分工作由组件Context完成

    Tomcat提供的ServletContext实现类为ApplicationContext,该类仅为Tomcat服务器使用;Web应用使用的是其门面类ApplicationContextFacade。

    

    1)StandardContext的启动过程(被StandardHost调用start方法)

        实际调用为LifecycleBase.start(),真正StandardContext的实现方法为startInternal(),

        具体内容有点长,可具体参考源码,博客参考:https://blog.csdn.net/w1992wishes/article/details/79499867 

        重点过程包括:

        * 初始化当前Context使用的WebResourceRoot并启动,WebResourceRoot维护了web应用所有的资源集合(Class文件、jar包以及其他资源),主要用于类加载和按照路径查找资源文件

// Add missing components as necessary
if (getResources() == null) {   // (1) Required by Loader
    if (log.isDebugEnabled())
        log.debug("Configuring default Resources");

    try {
        setResources(new StandardRoot(this));
    } catch (IllegalArgumentException e) {
        log.error(sm.getString("standardContext.resourcesInit"), e);
        ok = false;
    }
}
if (ok) {
    resourcesStart();
}

        * 创建web应用类加载器(WebappLoader),并启动

        * 创建会话管理器

        * 创建实例管理器(InstanceManager),用于创建对象实例,如Servlet、Filter等

        * 实例化应用监听器,分为事件监听器(ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener)和生命周期监听器(HttPSessionListener、ServletContextListener)

// Configure and call application event listeners
if (ok) {
    if (!listenerStart()) {
        log.error(sm.getString("standardContext.listenerFail"));
        ok = false;
    }
}

        * 对于loadOnStartup>=0的Wrapper,调用wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init进行初始化

        注意:这个只是针对于启动就加载的Servlet

// Load and initialize all "load on startup" servlets
if (ok) {
    if (!loadOnStartup(findChildren())){
        log.error(sm.getString("standardContext.servletFail"));
        ok = false;
    }
}

11.ContextConfig

    上述StandardContext的启动过程,并不包含web.xml中Servlet、请求映射、Filter等相关配置

    这部分的工作是由ContextConfig负责的。

    至于ContextConfig是如何关联StandardContext的,可以参考上面 那么HostConfig的监听器是什么时候关联到StandardHost的呢?

    1)ContextConfig.lifecycleEvent(用于监听各种事件)

public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }

    // 创建Wrapper(重要阶段)
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();
        
    // 在Context启动之前触发,用于更新Context的docBase属性和解决Web目录锁的问题
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
     
    // Context初始化阶段
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }

}

    2)详细介绍CONFIGURE_START_EVENT事件触发的configureStart()方法

    ContextConfig.configureStart()方法根据配置创建Wrapper(Servlet)、Filter、ServletContextListener,具体步骤如下:

protected synchronized void configureStart() {
    ...
    // 重要方法   
    webConfig();
    ...
}

// webConfig()
/**
     * Scan the web.xml files that apply to the web application and merge them
     * using the rules defined in the spec. For the global web.xml files,
     * where there is duplicate configuration, the most specific level wins. ie
     * an application's web.xml takes precedence over the host level or global
     * web.xml file.
     */
protected void webConfig() {
    ...
    // Step 8. Convert explicitly mentioned jsps to servlets
    if (ok) {
        convertJsps(webXml);
    }

    // Step 9. Apply merged web.xml to Context
    if (ok) {
        configureContext(webXml);
    }
}

// configureContext()解析web.xml并加载Servlet/Filter
private void configureContext(WebXml webxml) {
    // 加载Servlet
    for (ServletDef servlet : webxml.getServlets().values()) {
        // 创建Wrapper,具体在3)中继续分析
        Wrapper wrapper = context.createWrapper();

        if (servlet.getLoadOnStartup() != null) {
            wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
        }
        if (servlet.getEnabled() != null) {
            wrapper.setEnabled(servlet.getEnabled().booleanValue());
        }
        wrapper.setName(servlet.getServletName());
        Map<String,String> params = servlet.getParameterMap();
        for (Entry<String, String> entry : params.entrySet()) {
            wrapper.addInitParameter(entry.getKey(), entry.getValue());
        }
        wrapper.setRunAs(servlet.getRunAs());
        ...
        wrapper.setOverridable(servlet.isOverridable());
        context.addChild(wrapper);
    }
    
    // 加载servletMapping
    for (Entry<String, String> entry :
         webxml.getServletMappings().entrySet()) {
        context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }
    ...
}

    总结:完成2)步骤的时候,ServletWrapper与Context的关系就建立起来了

    3)论Wrapper与Servlet的关系

// StandardContext.createWrapper()
public Wrapper createWrapper() {

    Wrapper wrapper = null;
    if (wrapperClass != null) {
        try {
            wrapper = (Wrapper) wrapperClass.newInstance();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error("createWrapper", t);
            return (null);
        }
    } else {
        wrapper = new StandardWrapper();
    }
    ...
}

12.StandardWrapper

    StandardWrapper具体维护了Servlet实例

    StandardWrapper与Servlet的关系,下面来看下StandardWrapper源码:

    1)StandardWrapper是Servlet的包装类,下面是一些主要成员变量

public class StandardWrapper extends ContainerBase
    implements ServletConfig, Wrapper, NotificationEmitter {
    /**
     * The (single) possibly uninitialized instance of this servlet.
     */
    protected volatile Servlet instance = null;


    /**
     * Flag that indicates if this instance has been initialized
     */
    protected volatile boolean instanceInitialized = false;


    /**
     * The load-on-startup order value (negative value means load on
     * first call) for this servlet.
     */
    protected int loadOnStartup = -1;


    /**
     * Mappings associated with the wrapper.
     */
    protected final ArrayList<String> mappings = new ArrayList<>();


    /**
     * The initialization parameters for this servlet, keyed by
     * parameter name.
     */
    protected HashMap<String, String> parameters = new HashMap<>();


    /**
     * The security role references for this servlet, keyed by role name
     * used in the servlet.  The corresponding value is the role name of
     * the web application itself.
     */
    protected HashMap<String, String> references = new HashMap<>();

    2)StandardWrapper.load()

    如果该servlet配置load-on-startup>=0,则需要调用其load方法,完成Servlet的加载(如果没有配置,则等到用户首次调用Servlet的时候,才会加载)

    源码如下:

public synchronized void load() throws ServletException {
    // 加载Servlet,创建instance实例,并调用Servlet.init方法
    instance = loadServlet();

    if (!instanceInitialized) {
        initServlet(instance);
    }

    // jsp处理
    if (isJspServlet) {
        StringBuilder oname = new StringBuilder(getDomain());

        oname.append(":type=JspMonitor");

        oname.append(getWebModuleKeyProperties());

        oname.append(",name=");
        oname.append(getName());

        oname.append(getJ2EEKeyProperties());

        try {
            jspMonitorON = new ObjectName(oname.toString());
            Registry.getRegistry(null, null)
                .registerComponent(instance, jspMonitorON, null);
        } catch( Exception ex ) {
            log.info("Error registering JSP monitoring with jmx " +
                     instance);
        }
    }
}

// loadServlet()
public synchronized Servlet loadServlet() throws ServletException {

    // Nothing to do if we already have an instance or an instance pool
    if (!singleThreadModel && (instance != null))
        return instance;
    ...

    Servlet servlet;
    try {
        ...
        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
            // 1.创建对应的servletClass对象
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (ClassCastException e) {
           ...
        } 
        ...
        // 2.调用servlet.init方法
        initServlet(servlet);

        // 3.触发load监听
        fireContainerEvent("load", this);

        loadTime=System.currentTimeMillis() -t1;
    } finally {
        ...
    }
    return servlet;

}

参考:Tomcat架构解析(刘光瑞)

猜你喜欢

转载自blog.csdn.net/qq_26323323/article/details/84848614