Tomcat启动解析web.xml源码分析

tomcat启动整体时序图


fireLifecycleEvent时序图

        从“tomcat启动整体时序图”可以看出,web.xml解析发生在StandardContext startInternal()的fireLifecycleEvent环节,具体时序图如下:


其中ContextConfig监听器是tomcat启动解析conf/server.xml时加到StandardContext的。

相关源码

[java]  view plain  copy
  1. /** StandardContext.java */  
  2. // StandardContext启动  
  3. protected synchronized void startInternal() throws LifecycleException {  
  4.   
  5.     if(log.isDebugEnabled())  
  6.         log.debug("Starting " + getBaseName());  
  7.   
  8.     // Send j2ee.state.starting notification   
  9.     if (this.getObjectName() != null) {  
  10.         Notification notification = new Notification("j2ee.state.starting",  
  11.                 this.getObjectName(), sequenceNumber.getAndIncrement());  
  12.         broadcaster.sendNotification(notification);  
  13.     }  
  14.   
  15.     setConfigured(false);  
  16.     boolean ok = true;  
  17.   
  18.     // Currently this is effectively a NO-OP but needs to be called to  
  19.     // ensure the NamingResources follows the correct lifecycle  
  20.     if (namingResources != null) {  
  21.         namingResources.start();  
  22.     }  
  23.       
  24.     // Add missing components as necessary  
  25.     if (webappResources == null) {   // (1) Required by Loader  
  26.         if (log.isDebugEnabled())  
  27.             log.debug("Configuring default Resources");  
  28.         try {  
  29.             String docBase = getDocBase();  
  30.             if (docBase == null) {  
  31.                 setResources(new EmptyDirContext());  
  32.             } else if (docBase.endsWith(".war")  
  33.                     && !(new File(getBasePath())).isDirectory()) {  
  34.                 setResources(new WARDirContext());  
  35.             } else {  
  36.                 setResources(new FileDirContext());  
  37.             }  
  38.         } catch (IllegalArgumentException e) {  
  39.             log.error(sm.getString("standardContext.resourcesInit"), e);  
  40.             ok = false;  
  41.         }  
  42.     }  
  43.     if (ok) {  
  44.         if (!resourcesStart()) {  
  45.             throw new LifecycleException("Error in resourceStart()");  
  46.         }  
  47.     }  
  48.   
  49.     if (getLoader() == null) {  
  50.         WebappLoader webappLoader = new WebappLoader(getParentClassLoader());  
  51.         webappLoader.setDelegate(getDelegate());  
  52.         setLoader(webappLoader);  
  53.     }  
  54.   
  55.     // Initialize character set mapper  
  56.     getCharsetMapper();  
  57.   
  58.     // Post work directory  
  59.     postWorkDirectory();  
  60.   
  61.     // Validate required extensions  
  62.     boolean dependencyCheck = true;  
  63.     try {  
  64.         dependencyCheck = ExtensionValidator.validateApplication  
  65.             (getResources(), this);  
  66.     } catch (IOException ioe) {  
  67.         log.error(sm.getString("standardContext.extensionValidationError"), ioe);  
  68.         dependencyCheck = false;  
  69.     }  
  70.   
  71.     if (!dependencyCheck) {  
  72.         // do not make application available if depency check fails  
  73.         ok = false;  
  74.     }  
  75.   
  76.     // Reading the "catalina.useNaming" environment variable  
  77.     String useNamingProperty = System.getProperty("catalina.useNaming");  
  78.     if ((useNamingProperty != null)  
  79.         && (useNamingProperty.equals("false"))) {  
  80.         useNaming = false;  
  81.     }  
  82.   
  83.     if (ok && isUseNaming()) {  
  84.         if (getNamingContextListener() == null) {  
  85.             NamingContextListener ncl = new NamingContextListener();  
  86.             ncl.setName(getNamingContextName());  
  87.             ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite());  
  88.             addLifecycleListener(ncl);  
  89.             setNamingContextListener(ncl);  
  90.         }  
  91.     }  
  92.       
  93.     // Standard container startup  
  94.     if (log.isDebugEnabled())  
  95.         log.debug("Processing standard container startup");  
  96.   
  97.       
  98.     // Binding thread  
  99.     ClassLoader oldCCL = bindThread();  
  100.   
  101.     try {  
  102.   
  103.         if (ok) {  
  104.               
  105.             // Start our subordinate components, if any  
  106.             if ((loader != null) && (loader instanceof Lifecycle))  
  107.                 ((Lifecycle) loader).start();  
  108.   
  109.             // since the loader just started, the webapp classloader is now  
  110.             // created.  
  111.             // By calling unbindThread and bindThread in a row, we setup the  
  112.             // current Thread CCL to be the webapp classloader  
  113.             unbindThread(oldCCL);  
  114.             oldCCL = bindThread();  
  115.   
  116.             // Initialize logger again. Other components might have used it  
  117.             // too early, so it should be reset.  
  118.             logger = null;  
  119.             getLogger();  
  120.               
  121.             if ((cluster != null) && (cluster instanceof Lifecycle))  
  122.                 ((Lifecycle) cluster).start();  
  123.             Realm realm = getRealmInternal();  
  124.             if ((realm != null) && (realm instanceof Lifecycle))  
  125.                 ((Lifecycle) realm).start();  
  126.             if ((resources != null) && (resources instanceof Lifecycle))  
  127.                 ((Lifecycle) resources).start();  
  128.   
  129.             // 触发CONFIGURE_START_EVENT事件,加载web.xml  
  130.             fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);  
  131.               
  132.             // Start our child containers, if not already started  
  133.             for (Container child : findChildren()) {  
  134.                 if (!child.getState().isAvailable()) {  
  135.                     child.start();  
  136.                 }  
  137.             }  
  138.   
  139.             // Start the Valves in our pipeline (including the basic),  
  140.             // if any  
  141.             if (pipeline instanceof Lifecycle) {  
  142.                 ((Lifecycle) pipeline).start();  
  143.             }  
  144.               
  145.             // Acquire clustered manager  
  146.             Manager contextManager = null;  
  147.             if (manager == null) {  
  148.                 if (log.isDebugEnabled()) {  
  149.                     log.debug(sm.getString("standardContext.cluster.noManager",  
  150.                             Boolean.valueOf((getCluster() != null)),  
  151.                             Boolean.valueOf(distributable)));  
  152.                 }  
  153.                 if ( (getCluster() != null) && distributable) {  
  154.                     try {  
  155.                         contextManager = getCluster().createManager(getName());  
  156.                     } catch (Exception ex) {  
  157.                         log.error("standardContext.clusterFail", ex);  
  158.                         ok = false;  
  159.                     }  
  160.                 } else {  
  161.                     contextManager = new StandardManager();  
  162.                 }  
  163.             }   
  164.               
  165.             // Configure default manager if none was specified  
  166.             if (contextManager != null) {  
  167.                 if (log.isDebugEnabled()) {  
  168.                     log.debug(sm.getString("standardContext.manager",  
  169.                             contextManager.getClass().getName()));  
  170.                 }  
  171.                 setManager(contextManager);  
  172.             }  
  173.   
  174.             if (manager!=null && (getCluster() != null) && distributable) {  
  175.                 //let the cluster know that there is a context that is distributable  
  176.                 //and that it has its own manager  
  177.                 getCluster().registerManager(manager);  
  178.             }  
  179.         }  
  180.   
  181.     } finally {  
  182.         // Unbinding thread  
  183.         unbindThread(oldCCL);  
  184.     }  
  185.   
  186.     if (!getConfigured()) {  
  187.         log.error(sm.getString("standardContext.configurationFail"));  
  188.         ok = false;  
  189.     }  
  190.   
  191.     // We put the resources into the servlet context  
  192.     if (ok)  
  193.         getServletContext().setAttribute  
  194.             (Globals.RESOURCES_ATTR, getResources());  
  195.   
  196.     // Initialize associated mapper  
  197.     mapper.setContext(getPath(), welcomeFiles, resources);  
  198.   
  199.     // Binding thread  
  200.     oldCCL = bindThread();  
  201.   
  202.     if (ok ) {  
  203.         if (getInstanceManager() == null) {  
  204.             javax.naming.Context context = null;  
  205.             if (isUseNaming() && getNamingContextListener() != null) {  
  206.                 context = getNamingContextListener().getEnvContext();  
  207.             }  
  208.             Map<String, Map<String, String>> injectionMap = buildInjectionMap(  
  209.                     getIgnoreAnnotations() ? new NamingResources(): getNamingResources());  
  210.             setInstanceManager(new DefaultInstanceManager(context,  
  211.                     injectionMap, thisthis.getClass().getClassLoader()));  
  212.             getServletContext().setAttribute(  
  213.                     InstanceManager.class.getName(), getInstanceManager());  
  214.         }  
  215.     }  
  216.   
  217.     try {  
  218.         // Create context attributes that will be required  
  219.         if (ok) {  
  220.             getServletContext().setAttribute(  
  221.                     JarScanner.class.getName(), getJarScanner());  
  222.         }  
  223.   
  224.         // Set up the context init params  
  225.         mergeParameters();  
  226.   
  227.         // Call ServletContainerInitializers  
  228.         for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :  
  229.             initializers.entrySet()) {  
  230.             try {  
  231.                 entry.getKey().onStartup(entry.getValue(),  
  232.                         getServletContext());  
  233.             } catch (ServletException e) {  
  234.                 log.error(sm.getString("standardContext.sciFail"), e);  
  235.                 ok = false;  
  236.                 break;  
  237.             }  
  238.         }  
  239.   
  240.         // 实例化listeners及其初始化  
  241.         if (ok) {  
  242.             if (!listenerStart()) {  
  243.                 log.error(sm.getString("standardContext.listenerFail"));  
  244.                 ok = false;  
  245.             }  
  246.         }  
  247.           
  248.         try {  
  249.             // Start manager  
  250.             if ((manager != null) && (manager instanceof Lifecycle)) {  
  251.                 ((Lifecycle) getManager()).start();  
  252.             }  
  253.         } catch(Exception e) {  
  254.             log.error(sm.getString("standardContext.managerFail"), e);  
  255.             ok = false;  
  256.         }  
  257.   
  258.         // 配置和初始化filters  
  259.         if (ok) {  
  260.             if (!filterStart()) {  
  261.                 log.error(sm.getString("standardContext.filterFail"));  
  262.                 ok = false;  
  263.             }  
  264.         }  
  265.           
  266.         // 实例化及初始化所有带<load-on-startup>配置的servlets  
  267.         if (ok) {  
  268.             if (!loadOnStartup(findChildren())){  
  269.                 log.error(sm.getString("standardContext.servletFail"));  
  270.                 ok = false;  
  271.             }  
  272.         }  
  273.           
  274.         // Start ContainerBackgroundProcessor thread  
  275.         super.threadStart();  
  276.     } finally {  
  277.         // Unbinding thread  
  278.         unbindThread(oldCCL);  
  279.     }  
  280.   
  281.     // Set available status depending upon startup success  
  282.     if (ok) {  
  283.         if (log.isDebugEnabled())  
  284.             log.debug("Starting completed");  
  285.     } else {  
  286.         log.error(sm.getString("standardContext.startFailed", getName()));  
  287.     }  
  288.   
  289.     startTime=System.currentTimeMillis();  
  290.       
  291.     // Send j2ee.state.running notification   
  292.     if (ok && (this.getObjectName() != null)) {  
  293.         Notification notification =   
  294.             new Notification("j2ee.state.running"this.getObjectName(),  
  295.                              sequenceNumber.getAndIncrement());  
  296.         broadcaster.sendNotification(notification);  
  297.     }  
  298.   
  299.     // Close all JARs right away to avoid always opening a peak number   
  300.     // of files on startup  
  301.     if (getLoader() instanceof WebappLoader) {  
  302.         ((WebappLoader) getLoader()).closeJARs(true);  
  303.     }  
  304.   
  305.     // Reinitializing if something went wrong  
  306.     if (!ok) {  
  307.         setState(LifecycleState.FAILED);  
  308.     } else {  
  309.         setState(LifecycleState.STARTING);  
  310.     }  
  311. }  
  312.   
  313. /** LifecycleBase.java */  
  314. protected void fireLifecycleEvent(String type, Object data) {  
  315.     lifecycle.fireLifecycleEvent(type, data);  
  316. }     
  317.   
  318. /** LifecycleSupport.java */  
  319. public void fireLifecycleEvent(String type, Object data) {  
  320.   
  321.     LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);  
  322.     LifecycleListener interested[] = listeners;  
  323.     for (int i = 0; i < interested.length; i++)  
  324.         interested[i].lifecycleEvent(event);  
  325.   
  326. }  
  327.   
  328. /** ContextConfig.java */  
  329. public void lifecycleEvent(LifecycleEvent event) {  
  330.   
  331.     // Identify the context we are associated with  
  332.     try {  
  333.         context = (Context) event.getLifecycle();  
  334.     } catch (ClassCastException e) {  
  335.         log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);  
  336.         return;  
  337.     }  
  338.   
  339.     // Process the event that has occurred  
  340.     if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {  
  341.         configureStart(); // 响应CONFIGURE_START_EVENT事件  
  342.     } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {  
  343.         beforeStart();  
  344.     } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {  
  345.         // Restore docBase for management tools  
  346.         if (originalDocBase != null) {  
  347.             context.setDocBase(originalDocBase);  
  348.         }  
  349.     } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {  
  350.         configureStop();  
  351.     } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {  
  352.         init();  
  353.     } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {  
  354.         destroy();  
  355.     }  
  356.   
  357. }  
  358.   
  359. /** 
  360.  * Process a "contextConfig" event for this Context. 
  361.  */  
  362. protected synchronized void configureStart() {  
  363.     // Called from StandardContext.start()  
  364.   
  365.     if (log.isDebugEnabled())  
  366.         log.debug(sm.getString("contextConfig.start"));  
  367.   
  368.     if (log.isDebugEnabled()) {  
  369.         log.debug(sm.getString("contextConfig.xmlSettings",  
  370.                 context.getName(),  
  371.                 Boolean.valueOf(context.getXmlValidation()),  
  372.                 Boolean.valueOf(context.getXmlNamespaceAware())));  
  373.     }  
  374.   
  375.     // 解析tomcat/conf/web.xml、tomcat\conf\Catalina\web.xml.default、tomcat\webapps\Context名称\WEB-INF\web.xml  
  376.     // 及tomcat/lib下的所有jar中的web-fragment.xml,将所有这些web.xml配置整合到WebXml中,进而配置给StandardContext  
  377.     webConfig();  
  378.   
  379.     if (!context.getIgnoreAnnotations()) {  
  380.         applicationAnnotationsConfig();  
  381.     }  
  382.     if (ok) {  
  383.         validateSecurityRoles();  
  384.     }  
  385.   
  386.     // Configure an authenticator if we need one  
  387.     if (ok)  
  388.         authenticatorConfig();  
  389.   
  390.     // Dump the contents of this pipeline if requested  
  391.     if ((log.isDebugEnabled()) && (context instanceof ContainerBase)) {  
  392.         log.debug("Pipeline Configuration:");  
  393.         Pipeline pipeline = ((ContainerBase) context).getPipeline();  
  394.         Valve valves[] = null;  
  395.         if (pipeline != null)  
  396.             valves = pipeline.getValves();  
  397.         if (valves != null) {  
  398.             for (int i = 0; i < valves.length; i++) {  
  399.                 log.debug("  " + valves[i].getInfo());  
  400.             }  
  401.         }  
  402.         log.debug("======================");  
  403.     }  
  404.   
  405.     // Make our application available if no problems were encountered  
  406.     if (ok)  
  407.         context.setConfigured(true);  
  408.     else {  
  409.         log.error(sm.getString("contextConfig.unavailable"));  
  410.         context.setConfigured(false);  
  411.     }  
  412.   
  413. }  
  414.   
  415. protected void webConfig() {  
  416.     /* 
  417.      * Anything and everything can override the global and host defaults. 
  418.      * This is implemented in two parts 
  419.      * - Handle as a web fragment that gets added after everything else so 
  420.      *   everything else takes priority 
  421.      * - Mark Servlets as overridable so SCI configuration can replace 
  422.      *   configuration from the defaults 
  423.      */  
  424.   
  425.     /* 
  426.      * The rules for annotation scanning are not as clear-cut as one might 
  427.      * think. Tomcat implements the following process: 
  428.      * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of 
  429.      *   which Servlet spec version is declared in web.xml. The EG has 
  430.      *   confirmed this is the expected behaviour. 
  431.      * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main 
  432.      *   web.xml is marked as metadata-complete, JARs are still processed 
  433.      *   for SCIs. 
  434.      * - If metadata-complete=true and an absolute ordering is specified, 
  435.      *   JARs excluded from the ordering are also excluded from the SCI 
  436.      *   processing. 
  437.      * - If an SCI has a @HandlesType annotation then all classes (except 
  438.      *   those in JARs excluded from an absolute ordering) need to be 
  439.      *   scanned to check if they match. 
  440.      */  
  441.     Set<WebXml> defaults = new HashSet<WebXml>();  
  442.     // 解析tomcat/conf/web.xml、tomcat\conf\Catalina\web.xml.default  
  443.     defaults.add(getDefaultWebXmlFragment());  
  444.   
  445.     WebXml webXml = createWebXml();  
  446.   
  447.     // 解析tomcat\webapps\Context名称\WEB-INF\web.xml  
  448.     InputSource contextWebXml = getContextWebXmlSource();  
  449.     parseWebXml(contextWebXml, webXml, false);  
  450.   
  451.     ServletContext sContext = context.getServletContext();  
  452.   
  453.     // Ordering is important here  
  454.   
  455.     // Step 1. Identify all the JARs packaged with the application  
  456.     // If the JARs have a web-fragment.xml it will be parsed at this  
  457.     // point.  
  458.     // 解析tomcat/lib下的所有jar中的web-fragment.xml  
  459.     Map<String,WebXml> fragments = processJarsForWebFragments(webXml);  
  460.   
  461.     // Step 2. Order the fragments.  
  462.     Set<WebXml> orderedFragments = null;  
  463.     orderedFragments =  
  464.             WebXml.orderWebFragments(webXml, fragments, sContext);  
  465.   
  466.     // Step 3. Look for ServletContainerInitializer implementations  
  467.     if (ok) {  
  468.         processServletContainerInitializers();  
  469.     }  
  470.   
  471.     if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {  
  472.         // Step 4. Process /WEB-INF/classes for annotations  
  473.         if (ok) {  
  474.             // Hack required by Eclipse's "serve modules without  
  475.             // publishing" feature since this backs WEB-INF/classes by  
  476.             // multiple locations rather than one.  
  477.             NamingEnumeration<Binding> listBindings = null;  
  478.             try {  
  479.                 try {  
  480.                     listBindings = context.getResources().listBindings(  
  481.                             "/WEB-INF/classes");  
  482.                 } catch (NameNotFoundException ignore) {  
  483.                     // Safe to ignore  
  484.                 }  
  485.                 while (listBindings != null &&  
  486.                         listBindings.hasMoreElements()) {  
  487.                     Binding binding = listBindings.nextElement();  
  488.                     if (binding.getObject() instanceof FileDirContext) {  
  489.                         File webInfClassDir = new File(  
  490.                                 ((FileDirContext) binding.getObject()).getDocBase());  
  491.                         processAnnotationsFile(webInfClassDir, webXml,  
  492.                                 webXml.isMetadataComplete());  
  493.                     } else {  
  494.                         String resource =  
  495.                                 "/WEB-INF/classes/" + binding.getName();  
  496.                         try {  
  497.                             URL url = sContext.getResource(resource);  
  498.                             processAnnotationsUrl(url, webXml,  
  499.                                     webXml.isMetadataComplete());  
  500.                         } catch (MalformedURLException e) {  
  501.                             log.error(sm.getString(  
  502.                                     "contextConfig.webinfClassesUrl",  
  503.                                     resource), e);  
  504.                         }  
  505.                     }  
  506.                 }  
  507.             } catch (NamingException e) {  
  508.                 log.error(sm.getString(  
  509.                         "contextConfig.webinfClassesUrl",  
  510.                         "/WEB-INF/classes"), e);  
  511.             }  
  512.         }  
  513.   
  514.         // Step 5. Process JARs for annotations - only need to process  
  515.         // those fragments we are going to use  
  516.         if (ok) {  
  517.             processAnnotations(  
  518.                     orderedFragments, webXml.isMetadataComplete());  
  519.         }  
  520.   
  521.         // Cache, if used, is no longer required so clear it  
  522.         javaClassCache.clear();  
  523.     }  
  524.   
  525.     if (!webXml.isMetadataComplete()) {  
  526.         // Step 6. Merge web-fragment.xml files into the main web.xml  
  527.         // file.  
  528.         if (ok) {  
  529.             // 整合web-fragment.xml配置  
  530.             ok = webXml.merge(orderedFragments);  
  531.         }  
  532.   
  533.         // Step 7. Apply global defaults  
  534.         // Have to merge defaults before JSP conversion since defaults  
  535.         // provide JSP servlet definition.  
  536.         // 整合tomcat/conf/web.xml、tomcat\conf\Catalina\web.xml.default  
  537.         webXml.merge(defaults);  
  538.   
  539.         // Step 8. Convert explicitly mentioned jsps to servlets  
  540.         if (ok) {  
  541.             convertJsps(webXml);  
  542.         }  
  543.   
  544.         // Step 9. Apply merged web.xml to Context  
  545.         // WebXml配置给StandardContext  
  546.         if (ok) {  
  547.             webXml.configureContext(context);  
  548.         }  
  549.     } else {  
  550.         webXml.merge(defaults);  
  551.         convertJsps(webXml);  
  552.         webXml.configureContext(context);  
  553.     }  
  554.   
  555.     // Step 9a. Make the merged web.xml available to other  
  556.     // components, specifically Jasper, to save those components  
  557.     // from having to re-generate it.  
  558.     // TODO Use a ServletContainerInitializer for Jasper  
  559.     String mergedWebXml = webXml.toXml();  
  560.     sContext.setAttribute(  
  561.            org.apache.tomcat.util.scan.Constants.MERGED_WEB_XML,  
  562.            mergedWebXml);  
  563.     if (context.getLogEffectiveWebXml()) {  
  564.         log.info("web.xml:\n" + mergedWebXml);  
  565.     }  
  566.   
  567.     // Always need to look for static resources  
  568.     // Step 10. Look for static resources packaged in JARs  
  569.     if (ok) {  
  570.         // Spec does not define an order.  
  571.         // Use ordered JARs followed by remaining JARs  
  572.         Set<WebXml> resourceJars = new LinkedHashSet<WebXml>();  
  573.         for (WebXml fragment : orderedFragments) {  
  574.             resourceJars.add(fragment);  
  575.         }  
  576.         for (WebXml fragment : fragments.values()) {  
  577.             if (!resourceJars.contains(fragment)) {  
  578.                 resourceJars.add(fragment);  
  579.             }  
  580.         }  
  581.         processResourceJARs(resourceJars);  
  582.         // See also StandardContext.resourcesStart() for  
  583.         // WEB-INF/classes/META-INF/resources configuration  
  584.     }  
  585.   
  586.     // Step 11. Apply the ServletContainerInitializer config to the  
  587.     // context  
  588.     if (ok) {  
  589.         for (Map.Entry<ServletContainerInitializer,  
  590.                 Set<Class<?>>> entry :  
  591.                     initializerClassMap.entrySet()) {  
  592.             if (entry.getValue().isEmpty()) {  
  593.                 context.addServletContainerInitializer(  
  594.                         entry.getKey(), null);  
  595.             } else {  
  596.                 context.addServletContainerInitializer(  
  597.                         entry.getKey(), entry.getValue());  
  598.             }  
  599.         }  
  600.     }  
  601. }  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值