springmvc的学习分析

 

springmvc是一个servlet,controller中单独写处理head请求的方法,此方法可以用来检查服务器的状态,因为不返回body所以比get请求更节省网络资源。

 

springmvc的servlet图

152332_lJlV_1866807.png

 

XXXAware在spring中表示对xxx可以感知:如果在某个类中想要使用spring的一些东西,可以实现xxxAware接口告诉spring,spring可以给你送过来,接收的方法实现接口唯一的方法setxxx,比如有个类要用applicationContext,我们只需让它实现applicationContextAware接口,然后实现接口中唯一的方法void setApplicationContext(ApplicationContext applicationContext)就可以了。

 

EnvironmentCapable接口就是具有提供Enviroment的能力,Enviroment getEnvironment();

 

 

ApplicationContext

 

Environment

 

 

HttpServletBean中Enviroment使用的是Standrad_Servlet_Environment封装了ServletContext,ServletConfig,JndiProperty,系统环境变量和系统属性,这些都封装到了其propertySources属性下。

ServletContextPropertySource封装的就是ServletConfig,保存的是ServletContext

152423_4a8g_1866807.png

 

mandatory

英 [ˈmændətəri]

美 [ˈmændətɔ:ri]

adj.

强制的; 命令的; 受委托的;

    

 

n.

受托者;

[例句]Attendance is mandatory.

务必参加。

 

 

@Override

         public final void init() throws ServletException {

                   if (logger.isDebugEnabled()) {

                            logger.debug("Initializing servlet '" + getServletName() + "'");

                   }

 

                   // Set bean properties from init parameters.

                   try {

                            PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);

                            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);

                            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());

                            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));

                            initBeanWrapper(bw);

                            bw.setPropertyValues(pvs, true);

                   }

                   catch (BeansException ex) {

                            logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);

                            throw ex;

                   }

 

                   // Let subclasses do whatever initialization they like.

                   initServletBean();

 

                   if (logger.isDebugEnabled()) {

                            logger.debug("Servlet '" + getServletName() + "' configured successfully");

                   }

         }

httpServletBean的init中,首先将servlet中配置参数使用BeanWrapper设置到DispatcherServlet的相关属性,然后调用模板方法initServletBean,子类就通过这个方法初始化。

BeanWrapper是什么,怎么用?

是spring提供的操作havaBean属性的工具,使用它可以直接修改一个对象的属性。

FrameworkServlet

从httpServletBean中可知,FrameworkServlet的初始化入口方法应该是initServletBean

package org.springframework.web.servlet;

protected final void initServletBean() throws ServletException {

                   getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");

                   if (this.logger.isInfoEnabled()) {

                            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");

                   }

                   long startTime = System.currentTimeMillis();

 

                   try {

                            this.webApplicationContext = initWebApplicationContext();

                            initFrameworkServlet();

                   }

                   catch (ServletException ex) {

                            this.logger.error("Context initialization failed", ex);

                            throw ex;

                   }

                   catch (RuntimeException ex) {

                            this.logger.error("Context initialization failed", ex);

                            throw ex;

                   }

 

                   if (this.logger.isInfoEnabled()) {

                            long elapsedTime = System.currentTimeMillis() - startTime;

                            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +

                                               elapsedTime + " ms");

                   }

         }

 

FrameworkServlet在构建过程中的主要作用就是初始化webApplicationContext;

protected WebApplicationContext initWebApplicationContext() {

                   WebApplicationContext rootContext =

                                     WebApplicationContextUtils.getWebApplicationContext(getServletContext());

                   WebApplicationContext wac = null;

 

                   if (this.webApplicationContext != null) {

                            // A context instance was injected at construction time -> use it

                            wac = this.webApplicationContext;

                            if (wac instanceof ConfigurableWebApplicationContext) {

                                     ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;

                                     if (!cwac.isActive()) {

                                               // The context has not yet been refreshed -> provide services such as

                                               // setting the parent context, setting the application context id, etc

                                               if (cwac.getParent() == null) {

                                                        // The context instance was injected without an explicit parent -> set

                                                        // the root application context (if any; may be null) as the parent

                                                        cwac.setParent(rootContext);

                                               }

                                               configureAndRefreshWebApplicationContext(cwac);

                                     }

                            }

                   }

                   if (wac == null) {

                            // No context instance was injected at construction time -> see if one

                            // has been registered in the servlet context. If one exists, it is assumed

                            // that the parent context (if any) has already been set and that the

                            // user has performed any initialization such as setting the context id

                            wac = findWebApplicationContext();

                   }

                   if (wac == null) {

                            // No context instance is defined for this servlet -> create a local one

                            wac = createWebApplicationContext(rootContext);

                   }

 

                   if (!this.refreshEventReceived) {

                            // Either the context is not a ConfigurableApplicationContext with refresh

                            // support or the context injected at construction time had already been

                            // refreshed -> trigger initial onRefresh manually here.

                            onRefresh(wac);

                   }

 

                   if (this.publishContext) {

                            // Publish the context as a servlet context attribute.

                            String attrName = getServletContextAttributeName();

                            getServletContext().setAttribute(attrName, wac);

                            if (this.logger.isDebugEnabled()) {

                                     this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +

                                                        "' as ServletContext attribute with name [" + attrName + "]");

                            }

                   }

 

                   return wac;

         }

initWebApplicationContext方法做了三件事

获取spring的根容器rootContext,

设置webApplicationContext,并根据情况调用onRefresh方法

将webApplicationContext设置到ServletContext的attribute中

获取spring的根容器rootContext

默认情况下spring会将自己的容器设置成ServletContext的属性,默认根容器的key为 org.springframework.web.context.WebApplicationContext.ROOT,定义在org.springframework.web.context.WebApplicationContext中;

获取根容器只需要调用ServletContext的getAtttribute:

ServletContext#getAttribute(“org.springframework.web.context.WebApplicationContext.ROOT”)

 

设置webApplicationContext并根据情况调用onrefresh方法

有三种方法设置webapplicationcontext:

第一种在构造方法中已经传递了webApplicatioonContext参数,这时只需要一些设置即可。

第二种方法webapplicationContext已经在servletContext中了,配置servlet的时候将servletContext中的webapplicationContext的name配置到contextAttribute属性就可以了。

152645_MkMg_1866807.png

第三种方法是在前两种方式都无效的情况下自己创建一个,正常情况下就是使用这种方式。在createWebApplicationcontext方法中,内部又调用了configureAndRefreshWebApplicationContext方法。

 

 

         protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {

                   if (ObjectUtils.identityToString(wac).equals(wac.getId())) {

                            // The application context id is still set to its original default value

                            // -> assign a more useful id based on available information

                            if (this.contextId != null) {

                                     wac.setId(this.contextId);

                            }

                            else {

                                     // Generate default id...

                                     wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +

                                                        ObjectUtils.getDisplayString(getServletContext().getContextPath()) + "/" + getServletName());

                            }

                   }

 

                   wac.setServletContext(getServletContext());

                   wac.setServletConfig(getServletConfig());

                   wac.setNamespace(getNamespace());

                   wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

 

                   // The wac environment's #initPropertySources will be called in any case when the context

                   // is refreshed; do it eagerly here to ensure servlet property sources are in place for

                   // use in any post-processing or initialization that occurs below prior to #refresh

                   ConfigurableEnvironment env = wac.getEnvironment();

                   if (env instanceof ConfigurableWebEnvironment) {

                            ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());

                   }

 

                   postProcessWebApplicationContext(wac);

                   applyInitializers(wac);

                   wac.refresh();

         }

 

         private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

 

                   @Override

                   public void onApplicationEvent(ContextRefreshedEvent event) {

                            FrameworkServlet.this.onApplicationEvent(event);

                   }

         }

 

         public void onApplicationEvent(ContextRefreshedEvent event) {

                   this.refreshEventReceived = true;

                   onRefresh(event.getApplicationContext());

         }

在接收到事件后调用一次onRefresh,并将refreshEventReceived标志置为true,表示已经refresh过。

  上面三种方法不管哪种,最终都会调用一次refresh方法,并且DispatcherServlet正是通过重写这个模板来实现初始化的。

  前面介绍了配置servlet时可以设置的一些初始化参数,总结如下:

contextAttribute:在servletContext的属性中,要做WebApplicationContext的属性名称

contextClass:创建webApplicationContext的类型

contextConfigLocation:Springmvc配置文件的位置

publishContext:是否将webApplicationContext设置到ServletContext的属性。

 

 

DispatcherServlet

 

   onRefresh方法是DispatcherServlet的入口方法,onRefresh方法调用了initStrategies,其中调用了9个初始化的方法:

package org.springframework.web.servlet

protected void onRefresh(ApplicationContext context) {

                   initStrategies(context);

         }

初始化servlet使用的九个组件

         /**

          * Initialize the strategy objects that this servlet uses.

          * <p>May be overridden in subclasses in order to initialize further strategy objects.

          */

         protected void initStrategies(ApplicationContext context) {

                   initMultipartResolver(context);

                   initLocaleResolver(context);

                   initThemeResolver(context);

                   initHandlerMappings(context);

                   initHandlerAdapters(context);

                   initHandlerExceptionResolvers(context);

                   initRequestToViewNameTranslator(context);

                   initViewResolvers(context);

                   initFlashMapManager(context);

         }

为什么不把initStrategies直接写到onRefresh中去呢?这是功能分层的原因,onRefresh是用来刷新的,这样写可以使逻辑更清晰一些。

 

 

private void initLocaleResolver(ApplicationContext context) {

                   try {

                            this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);

                            if (logger.isDebugEnabled()) {

                                     logger.debug("Using LocaleResolver [" + this.localeResolver + "]");

                            }

                   }

                   catch (NoSuchBeanDefinitionException ex) {

                            // We need to use the default.

                            this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);

                            if (logger.isDebugEnabled()) {

                                     logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME +

                                                        "': using default [" + this.localeResolver + "]");

                            }

                   }

         }

 

初始化分为两步,首先用context.getBean在容器里面按照注册的名称或者类型(localeResolver名称或者localeResolver.class类型)进行查找,所以在spring mvc的配置文件中只需要配置相应类型的组件,容器就可以找到。如果找不到,就按照getDefaultStrategy按照类型获得默认的组件。这里的context指的是frameworkServlet中创建的webApplicationContext,而不是servletContext,

protected <T> T getDefaultStrategy(ApplicationContext context, Class<T> strategyInterface) {

                   List<T> strategies = getDefaultStrategies(context, strategyInterface);

                   if (strategies.size() != 1) {

                            throw new BeanInitializationException(

                                               "DispatcherServlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]");

                   }

                   return strategies.get(0);

         }

         @SuppressWarnings("unchecked")

         protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {

                   String key = strategyInterface.getName();

                   //获得需要的策略类型

                   String value = defaultStrategies.getProperty(key);

                   if (value != null) {

                            String[] classNames = StringUtils.commaDelimitedListToStringArray(value);

                            List<T> strategies = new ArrayList<T>(classNames.length);

                            for (String className : classNames) {

                                     try {

                                               Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());

                                               Object strategy = createDefaultStrategy(context, clazz);

                                               strategies.add((T) strategy);

                                     }

                                     catch (ClassNotFoundException ex) {

                                               throw new BeanInitializationException(

                                                                 "Could not find DispatcherServlet's default strategy class [" + className +

                                                                                    "] for interface [" + key + "]", ex);

                                     }

                                     catch (LinkageError err) {

                                               throw new BeanInitializationException(

                                                                 "Error loading DispatcherServlet's default strategy class [" + className +

                                                                                    "] for interface [" + key + "]: problem with class file or dependent class", err);

                                     }

                            }

                            return strategies;

                   }

                   else {

                            return new LinkedList<T>();

                   }

         }

 

classname来自classNames,classnames来自value,value来自defaultStrategy

 

private static final Properties defaultStrategies;

 

         static {

                   // Load default strategy implementations from properties file.

                   // This is currently strictly internal and not meant to be customized

                   // by application developers.

                   try {

                            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);

                            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);

                   }

                   catch (IOException ex) {

                            throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());

                   }

         }

 

DEFAULT_STRATEGIES_PATH的值是DispatcherServlet.properties中定义的

在同目录下的properties文件

# Default implementation classes for DispatcherServlet's strategy interfaces.

# Used as fallback when no matching beans are found in the DispatcherServlet context.

# Not meant to be customized by application developers.

 

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

 

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

 

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\

         org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

 

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\

         org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\

         org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter

 

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\

         org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\

         org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

 

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

 

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

 

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

定义了八个组件,multipartResolver是没有默认配置的,并不是每个应用都需要上传功能,所以multioartResolver不需要默认配置。

都做了默认配置,也不是spring的推荐配置,只是在没有配置的时候有个默认值,不至于空着,默认配置是相应类型没有配置的时候才会使用,如当使用<mvc:annotation-driven>后,并不会使用默认配置,因为它配置了handlerMapping.handlerAdapter和handler-ExceptionResolver,而且还做了很多别的工作。DispatcherServlet创建过程主要对9大组件进行初始化。

 

在spring中xml文件中通过命名空间配置的标签时怎么解析的

spring xml中可以有很多命名空间配置的信息,这些命名空间配置是怎么解析的?对于具体的一个命名空间。spring是如何找到解析他们的类的?

解析标签的类都放在相应的META-INF目录下的springhandlers文件中。如mvc命名空间解析设置在spring-webmvc-xxx.jar下META-INF下spring.handlers文件中,内容是

http\://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler

如内部将mvc:annotation-drivern的解析交给AnnotationDrivenBeanDefinitionParser

153211_h0VU_1866807.png

public interface NamespaceHandler {

         void init();

         BeanDefinition parse(Element element, ParserContext parserContext);

         BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext);

 

}

接口Namespacehandler的实现类主要有三个:NamespaceHandlerSupport,SimpleConstructorNamespaceHandler,SimplePropertyNamespaceHandler;

NamespaceHandlerSupport是NamespaceHandler的默认实现类,一般的NamespaceHandler都继承自这个类;特殊情况springSecurity的SecurityNamespaceHandler是直接实现的NamespaceHandler接口;SimpleConstructorNamespaceHandler用于统一对C:配置的构造方法进行解析,SimplePropertyNamespaceHandler统一对通过p:配置的参数进行解析。

 

public class MvcNamespaceHandler extends NamespaceHandlerSupport{}

 

 

NamespaceHandlerSupport

public abstract class NamespaceHandlerSupport implements NamespaceHandler {

 

         /**

          * Stores the {@link BeanDefinitionParser} implementations keyed by the

          * local name of the {@link Element Elements} they handle.

          */

         private final Map<String, BeanDefinitionParser> parsers =

                            new HashMap<String, BeanDefinitionParser>();

 

         /**

          * Stores the {@link BeanDefinitionDecorator} implementations keyed by the

          * local name of the {@link Element Elements} they handle.

          */

         private final Map<String, BeanDefinitionDecorator> decorators =

                            new HashMap<String, BeanDefinitionDecorator>();

 

         /**

          * Stores the {@link BeanDefinitionDecorator} implementations keyed by the local

          * name of the {@link Attr Attrs} they handle.

          */

         private final Map<String, BeanDefinitionDecorator> attributeDecorators =

                            new HashMap<String, BeanDefinitionDecorator>();

 

 

         /**

          * Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is

          * registered for that {@link Element}.

          */

         @Override

         public BeanDefinition parse(Element element, ParserContext parserContext) {

                   return findParserForElement(element, parserContext).parse(element, parserContext);

         }

 

         /**

          * Locates the {@link BeanDefinitionParser} from the register implementations using

          * the local name of the supplied {@link Element}.

          */

         private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {

                   String localName = parserContext.getDelegate().getLocalName(element);

                   BeanDefinitionParser parser = this.parsers.get(localName);

                   if (parser == null) {

                            parserContext.getReaderContext().fatal(

                                               "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);

                   }

                   return parser;

         }

 

         /**

          * Decorates the supplied {@link Node} by delegating to the {@link BeanDefinitionDecorator} that

          * is registered to handle that {@link Node}.

          */

         @Override

         public BeanDefinitionHolder decorate(

                            Node node, BeanDefinitionHolder definition, ParserContext parserContext) {

 

                   return findDecoratorForNode(node, parserContext).decorate(node, definition, parserContext);

         }

 

         /**

          * Locates the {@link BeanDefinitionParser} from the register implementations using

          * the local name of the supplied {@link Node}. Supports both {@link Element Elements}

          * and {@link Attr Attrs}.

          */

         private BeanDefinitionDecorator findDecoratorForNode(Node node, ParserContext parserContext) {

                   BeanDefinitionDecorator decorator = null;

                   String localName = parserContext.getDelegate().getLocalName(node);

                   if (node instanceof Element) {

                            decorator = this.decorators.get(localName);

                   }

                   else if (node instanceof Attr) {

                            decorator = this.attributeDecorators.get(localName);

                   }

                   else {

                            parserContext.getReaderContext().fatal(

                                               "Cannot decorate based on Nodes of type [" + node.getClass().getName() + "]", node);

                   }

                   if (decorator == null) {

                            parserContext.getReaderContext().fatal("Cannot locate BeanDefinitionDecorator for " +

                                               (node instanceof Element ? "element" : "attribute") + " [" + localName + "]", node);

                   }

                   return decorator;

         }

 

 

         /**

          * Subclasses can call this to register the supplied {@link BeanDefinitionParser} to

          * handle the specified element. The element name is the local (non-namespace qualified)

          * name.

          */

         protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {

                   this.parsers.put(elementName, parser);

         }

 

         /**

          * Subclasses can call this to register the supplied {@link BeanDefinitionDecorator} to

          * handle the specified element. The element name is the local (non-namespace qualified)

          * name.

          */

         protected final void registerBeanDefinitionDecorator(String elementName, BeanDefinitionDecorator dec) {

                   this.decorators.put(elementName, dec);

         }

 

         /**

          * Subclasses can call this to register the supplied {@link BeanDefinitionDecorator} to

          * handle the specified attribute. The attribute name is the local (non-namespace qualified)

          * name.

          */

         protected final void registerBeanDefinitionDecoratorForAttribute(String attrName, BeanDefinitionDecorator dec) {

                   this.attributeDecorators.put(attrName, dec);

         }

 

}

NamespaaceHandlerSupport定义了三个处理器parsers,decorators.attributeDecorators分别用于处理解析工作,处理标签类型,处理属性类型的装饰。具体的处理器由子类实现,然后注册到NamespaceHandlerSupport上面,所以要定义一个命名空间的解析器,只需要在init中定义相应的parsers,decorators.attributeDecorators,并注册到NamespacehandlerSupport上面。

mvc命名空间的mvcNamespaceHandler的代码:

public class MvcNamespaceHandler extends NamespaceHandlerSupport {

 

         @Override

         public void init() {

                   registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());

                   registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser());

                   registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());

                   registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());

                   registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());

                   registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser());

                   registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser());

                   registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser());

                   registerBeanDefinitionParser("tiles-configurer", new TilesConfigurerBeanDefinitionParser());

                   registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser());

                   registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser());

                   registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());

         }

 

}

 

 

从上面代码可以看出解析annotation-driven的是子类解析器AnnotationDrivenBeanDefinitionParser,注册到了NamespaceHandlerSupport的parsers上

 

 

上面主要解析了springmvc创建servlet的三个层次HttpServletBean ,FrameworkServlet,DispatcherServlet;HttpServletBean直接继承了java中的HttpServlet,作用是将相应的servlet的配置参数设置到对应的属性上,FrameworkServlet初始化了WebApplicationContext,DispatcherServlet初始化自身的9个组件。

spring的特点:结构简单,实现复杂,结构简单是顶层设计好,实现复杂是实现的功能比较多,可配置的地方 也非常多。

 

 

 

 

 

 

 

 

 

转载于:https://my.oschina.net/iioschina/blog/750210

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值