【springboot 】web.xml去哪了 1 (SpringServletContainerInitializer接口、WebApplicationInitializer接口)


相关文章:
Spring MVC的web.xml配置详解(ContextLoaderListener创建容器监听器、DispatcherServlet) web.xml配置

系列文章:
【springboot 】web.xml去哪了 1 (SpringServletContainerInitializer接口、WebApplicationInitializer接口)
【springboot 】web.xml去哪了2 (ServletWebServerApplicationContext容器)

前言

Servlet3.0之后,可以通过编码方式替换web.xml

  • 普通的servlet工程 ,通过用户自己去实现ServletContainerInitializer,在onStartup()方法中 创建servlet和filter。当然,需要依赖SPI机制进行注入

  • springmvc工程(对普通servlet进行了封装,本质也是servlet)

    springmvc框架已经内置SpringServletContainerInitializer,它也是ServletContainerInitializer的子类,在onStartup()方法中他负责读取WebApplicationInitializer接口,因此,用户需要自己去实现 WebApplicationInitializer接口接口即可

1:web.xml是怎么没的?

1.1:Servlet3.0之前

在servlet3.0以前,编写web程序的流程大概如下:

1:编写servlet,并通过<servlet>标签注册到web.xml文件中
2:编写filter,并通过<filter>标签注册到web.xml文件中
3:编写lisner,并通过<listener>标签注册到web.xml文件中

参见 Spring MVC的web.xml配置详解(ContextLoaderListener创建容器监听器、DispatcherServlet)

用的比较多,这里就不再提供具体实例了。

web.xml文件是web容器和我们应用程序的纽带,相关的信息都需要注册在这里。此时这些信息的注册是通过web容器读取web.xml来完成的。但是这个过程比较繁琐,作为开发的我们,如果能通过编程的方式来完成这个过程的话就能实现不依赖于web.xml了。所幸,我们最终迎来了servlet3.0时代,让我们这个需求能够得到满足。

1.2:Servlet3.0

既然是通过编程的方式来完成servlet,filter,listener等web容器组件的注册,那么肯定就要提供对应的方式了,目前有两种方式,第一种是使用@WebServlet,@WebFilter等注解,第二种方式使用使用javax.servlet.ServletContext(这是web容器封装servlet,filter,listener等组件的上下文对象),主要API如下:

  • 添加servlet相关API

    public ServletRegistration.Dynamic addServlet(String servletName,
                Class<? extends Servlet> servletClass);
    public ServletRegistration.Dynamic addServlet(String servletName, String className);
    public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet);
    
    

    调用以上方法,相当于在web.xml中配置<servlet>标签。

  • 添加filter相关API

    public FilterRegistration.Dynamic addFilter(String filterName,
                Class<? extends Filter> filterClass);
    public FilterRegistration.Dynamic addFilter(String filterName, Filter filter);
    public FilterRegistration.Dynamic addFilter(String filterName, String className);
    
    

    调用以上方法相当于在web.xml中配置标签。

  • 添加listener相关API

    public void addListener(String className);
    public <T extends EventListener> void addListener(T t);
    public void addListener(Class<? extends EventListener> listenerClass);
    
    

    调用以上方法相当于在web.xml中配置<listener>标签。

现在,向web容器注册各种组件的API有了,想要完成注册,该怎么做呢?此时,就需要接口来提供规范了,这个接口是javax.servlet.ServletContainerInitializer,源码如下:

// 通过在META-INF/services/文件夹下创建javax.servlet.ServletContainerInitializer文件
// 然后将自己提供的实现类按照一行一个的格式配置到文件中,web容器在启动的时候会通过SPI来加载,并执行
// 对应的onStartup方法,执行自定义的注册逻辑,完成相关组件的注册
public interface ServletContainerInitializer {
	// 参数c:期望处理的class类型集合
	// 参数ctx:servlet上线文对象,我们通过该对象中来完成各种web组件注册工作
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

ServletContainerInitializer 接口内含有一个onStartup(),顾名思义,启动的时候触发,那么我们可以在这个回调函数中注入我们自定义的servletfilter

2. 项目实战

新建一个servlet项目,当然也可以直接下载源码(参见原文)。来测试一下。
servlet

public class MyServletContainerInitializerServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("hello from servlet register by ServerContainerInitializer!!!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // post也走get
        this.doGet(req, resp);
    }
}

filter:

public class MyServletContainerInitializerFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyServletContainerInitializerFilter.init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("MyServletContainerInitializerFilter.doFilter");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("MyServletContainerInitializerFilter.destroy");
    }
}

ServletContainerInitializer实现类:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    private final static String JAR_HELLO_URL = "/hello";

 //启动启动时会触发,spi机制
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        System.out.println("注册servlet到web容器开始!");
        //注入Servlet
        ServletRegistration.Dynamic dynamicServlet = servletContext.addServlet(MyServletContainerInitializerServlet.class.getSimpleName(), MyServletContainerInitializerServlet.class);
        // 添加映射
        dynamicServlet.addMapping(JAR_HELLO_URL);
        System.out.println("注册servlet到web容器结束!");

        System.out.println("注册filter到web容器开始!");
         //注入filter
        FilterRegistration.Dynamic dynamicFilter = servletContext.addFilter(MyServletContainerInitializerFilter.class.getSimpleName(), MyServletContainerInitializerFilter.class);
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
        dispatcherTypes.add(DispatcherType.REQUEST);
        dispatcherTypes.add(DispatcherType.FORWARD);
        dynamicFilter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
        System.out.println("注册filter到web容器结束!");
    }
}

现在准备工作都完成了,但是,web容器此时还无法识别到我们的ServletContainerInitializer,根据规范,还需要配置SPI,因此我们在META-INF/services下创建文件javax.servlet.ServletContainerInitializer,并填入实现类内容,如下:

在这里插入图片描述

接下来我们启动查看日志:

...snip...
注册servlet到web容器开始!
注册servlet到web容器结束!
注册filter到web容器开始!
注册filter到web容器结束!
...snip...

访问测试:

C:\Users\Administrator>curl http://192.168.10.119:10080/test_servlet3_without_webxml/hello
hello from servlet register by ServerContainerInitializer!!!

3. springMVC如何集成servlet3.0

我们自己开发程序来集成servlet3.0是通过提供ServletContainerInitializer的实现类,并通过SPI来配置,供web服务器读取,springMVC也是如此SPI机制。

先从servlet3.0之后的启动规则说起

servlet3.0规则:

1)、服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例:

2)、ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名

spring-web-xxx.RELEASE.jar已经内置一个ServletContainerInitializer的子类,子类类型为SpringServletContainerInitializer :
在这里插入图片描述
其提供的实现类是org.springframework.web.SpringServletContainerInitializer(也实现了ServletContainerInitializer),源码如下详细看注释!!!

org.springframework.web.SpringServletContainerInitializer
// 该类设计的目的是让开发人员基于编码的方式来支持servlet容器,看到@HandlesTypes(WebApplicationInitializer.class),代表该类设置web容器在回调时,将classpath下的WebApplicationInitializer的实现类作为参数传递到方法onStartup的参数webAppInitializerClasses
// 中,这种是和web.xml对立的方式(也可能和web.xml方式混合使用)
// 操作机制:当支持servlet3的web容器启动的时候,会通过jar servicec API(ServiceLoader.load(xxx))从classpath下的spring-web.jar包中读取META-INF/services/javax.servlet.ServletContainerInitializer文件,在该文件中配置的实现类正是该类,
// 然后就会调用该类的onStartup方法,并将classpath下WebApplicationInitializer的实现类作为参数传递到webAppInitializerClasses参数中
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	// 参数webAppInitializerClasses:classpath下WebApplicationInitializer的实现类
	// 参数servletContext:web容器servlet上下文对象
	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();
		// web容器传进来的webAppInitializerClasses不为空
		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// 因为一些web容器会将@HandlesTypes指定的类型外的一些类传进来,所以再进一步做个判断,可以说是因为web容器的bug而不得不写的代码
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						// 添加到initializers集合中
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}
		// initializers为空,即在classpath下没有WebApplicationInitializer的子类,简单的给出日志提示,并return
		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}
		// 日志记录在classpath下发现了多少个WebApplicationInitializer的子类
		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		// 排序
		AnnotationAwareOrderComparator.sort(initializers);
		//**** 循环调用onoStartup方法,注册web组件到ServletContext中
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

上面代码的onStartup()干了什么?只作一件事情,就是提取WebApplicationInitializer的实现类,然后调用他们的onStartup()(参见最后代码****处的for循环)。也就是说上面代码的onStartup()是个入口,实际上是通过用户自定义的WebApplicationInitializer实现类间接实现注入的。

到此处,我们就知道,想要在无web.xml文件的情况下使用springMVC,只需要提供一个org.springframework.web.WebApplicationInitializer的实现类,然后在其onStartup方法中注册web容器相关组件就可以了

3.1 WebApplicationInitializer接口源码

WebApplicationInitializer接口源码如下,而且注释中包含了示例,教你如何注入一个自定义的spring mvc 元素!!!:

// 该接口用来在servlet3.0环境中通过编程的方式配置ServletContext(注册Servlet,Filter,Listener等),与之对应的是基于web.xml的配置方式(二者有时候也可以混用)。具体的实现类会通过SpringServletContainerInitializer(web容器的构子类)类加载并调用。
// 一般开发人员会通过web.xml方式来注册DispatcherServlet(当然还有其他组件)到web容器的。可能如下:
/*
<servlet>
   <servlet-name>dispatcher</servlet-name>
   <servlet-class>
     org.springframework.web.servlet.DispatcherServlet
   </servlet-class>
   <init-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
   </init-param>
   <load-on-startup>1</load-on-startup>
 </servlet>

 <servlet-mapping>
   <servlet-name>dispatcher</servlet-name>
   <url-pattern>/</url-pattern>
 </servlet-mapping>
*/
// 如果是使用WebApplicationInitializer的等价编程配置方式的话,可能如下:
/*
 public class MyWebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
      XmlWebApplicationContext appContext = new XmlWebApplicationContext();
      appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

      ServletRegistration.Dynamic dispatcher =
        container.addServlet("dispatcher", new DispatcherServlet(appContext));
      dispatcher.setLoadOnStartup(1);
      dispatcher.addMapping("/");
    }

 }
*/
// 例子中是直接实现WebApplicationInitializer接口,实际中可以通过继承AbstractDispatcherServletInitializer类来完成操作,
// 该类已经完成了注册DispatcherServlet等基础工作,我们只需要实现其抽象方法提供IOC容器就可以了
// 程序中“appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");”还是使用了xml文件,我们可以使用基于注解的spring IOC容器来改造代码,实现零xml配置,代码可能如下:
/*
 public class MyWebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
      // Create the 'root' Spring application context
      AnnotationConfigWebApplicationContext rootContext =
        new AnnotationConfigWebApplicationContext();
      rootContext.register(AppConfig.class);

      // Manage the lifecycle of the root application context
      container.addListener(new ContextLoaderListener(rootContext));

      // Create the dispatcher servlet's Spring application context
      AnnotationConfigWebApplicationContext dispatcherContext =
        new AnnotationConfigWebApplicationContext();
      dispatcherContext.register(DispatcherConfig.class);

      // Register and map the dispatcher servlet
      ServletRegistration.Dynamic dispatcher =
        container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
      dispatcher.setLoadOnStartup(1);
      dispatcher.addMapping("/");
    }
 }
*/
public interface WebApplicationInitializer {

	// 在该方法中可以对servletContext进行servlets,fitlers,listeners,context-params以及属性信息的配置。
	void onStartup(ServletContext servletContext) throws ServletException;
}

3.2 WebApplicationInitializer接口实战

既然是springMVC程序,首先当然得定义一个处理请求的handler了,如下:

@Controller
@RequestMapping("/testinterceptor")
public class HelloController {

    @PostConstruct
    public void xxx() {
        System.out.println("HelloController.xxx");
    }

    @RequestMapping("/hi")
    @ResponseBody
    public String sayHi(HttpServletResponse response) {
        System.out.println("HelloController.sayHi");
        String msg = "testinterceptor hi";
        System.out.println(msg);
        return msg;
    }
}

然后我们定义WebApplicationInitializer的子类,如下:

public class MyWebXml implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        System.out.println("MyWebXml 加载开始!");
        // new springmvc的容器对象
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        // 注册控制器
        ctx.register(HelloController.class);
        // 设置servlet上线文对象
        ctx.setServletContext(servletContext);
        String dispatcherServletName = DispatcherServlet.class.getSimpleName();
        // 注册springmvc分发请求的DispatcherServlet
        /*
        相当于在web.xml中配置代码:
          <servlet>
            <servlet-name>DispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
          </servlet>
          <servlet-mapping>
            <servlet-name>DispatcherServlet</servlet-name>
            <url-pattern>/</url-pattern>
          </servlet-mapping>
         */
        ServletRegistration.Dynamic dynamicDispatcherServlet = servletContext.addServlet(dispatcherServletName, new DispatcherServlet(ctx));
        dynamicDispatcherServlet.addMapping("/");
        dynamicDispatcherServlet.setLoadOnStartup(1);
        System.out.println("MyWebXml 加载结束!");
    }
}

此时我们就可以启动程序来访问我们的接口了,如下:

C:\Users\Administrator>curl http://localhost:10080/springmvc_without_webxml_war_exploded/testinterceptor/hi
testinterceptor hi

当然我们也可以通过实现WebApplicationInitializer的抽象子类AbstractDispatcherServletInitializer,该类已经完成了DispatcherServlet的注册工作,可以修改MyWebXml如下:

public class MyWebXml extends AbstractDispatcherServletInitializer {
    private static AnnotationConfigWebApplicationContext servletAc = new AnnotationConfigWebApplicationContext();
    private static AnnotationConfigWebApplicationContext rootAc = new AnnotationConfigWebApplicationContext();

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        // 注册handler
        servletAc.register(HelloController.class);
    }

    /*
    创建 dispatcherservlet的Spring IOC容器
    */
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        return servletAc;
    }

    /*
    返回DispatcherServlet的servletmapping信息
     */
    @Override
    protected String[] getServletMappings() {
        List<String> servletMappingList = new ArrayList<>();
        servletMappingList.add("/");
        return StringUtils.toStringArray(servletMappingList);
    }

    // 创建root 的spring IOC容器
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return rootAc;
    }
}

效果是完全一样的。测试源码从这里下载(参见原文)

4. springboot如何集成servlet3.0

注意,这里是集成原生的servlet,而不是springmvc中的controller

4.1:通过servlet3注解+@ServletComponentScan

通过servlet3.0中定义的@WebXxx相关注解,然后通过@ServletComponentScan配置要扫描的包路径,如下测试:

springboot如何集成spring的? @ComponentScan,参见 【spring】JavaConfig、@Configuration、@ComponentScan入门例子

  • 定义servlet ,通过@WebServlet声明一个servlet:

    @WebServlet("/myservlet")
    public class MyServlet extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().write("res from spring boot servlet!!!");
        }
    }
    
    
  • 启动类,通过@ServletComponentScan,定义了扫描的路径,如果不填,则默认取@SpringBootApplication类所在的包作为路径

    @SpringBootApplication
    @ServletComponentScan(basePackages = { "com.example.springbootintegarationspringmvc" })
    public class SpringbootIntegrationSpringmvcApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootIntegrationSpringmvcApplication.class, args);
        }
    
    }
    
    

启动测试

C:\Users\Administrator>curl http://localhost:8080/myservlet
res from spring boot servlet!!!

4.2:通过RegistrationBean

rg.springframework.boot.web.servlet.RegistrationBean是springboot提供的抽象类,实现了ServletContextInitializer接口,负责将servlet,filter,listener等注册到spring IOC容器中。其源码如下:

RegistrationBean 是个内置的间接类,实现了ServletContextInitializer接口,我们可以扩展它,不用使用最原始的ServletContextInitializer接口

// 用于在servlet3.0+环境中程序化方式配置servletContext的接口。与实现了WebApplicationInitializer接口
// 的类不同,不会被SpringServletContainerInitializer自动获取,因此不会被web容器自动加载。
// 但是其扮演的角色和ServletContainerInitializer类似,不同之处在于通过spring来完成生命周期
// 的管理,而非像ServletContainerInitializer是通过web容器管理生命周期。
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {

	private static final Log logger = LogFactory.getLog(RegistrationBean.class);

	private int order = Ordered.LOWEST_PRECEDENCE;

	private boolean enabled = true;
	
	// 这里实现具体的逻辑,完成web组件,如servlet,filter,listener等向servletcontext中
	// 注册的工作,但是注意这个过程是spring在启动web容器的过程中完成的,因为此时web容器只是
	// springboot的一个组件而已。
	// 启动过程是"springboot main->启动web容器->调用该方法配置servletContext"
	@Override
	public final void onStartup(ServletContext servletContext) throws ServletException {
		String description = getDescription();
		if (!isEnabled()) {
			logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
			return;
		}
		register(description, servletContext);
	}

	protected abstract String getDescription();

	protected abstract void register(String description, ServletContext servletContext);

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public boolean isEnabled() {
		return this.enabled;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	@Override
	public int getOrder() {
		return this.order;
	}

}

4.2.1 RegistrationBean实战

首先来定义一个servlet,注意,没有@WebServlet修饰:

public class MyServlet1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("hello from my servlet 1");
    }
}

定义Java config类,注意,在 @Bean中注入一个ServletRegistrationBean 实例,对应一个Servlet:

@Configuration
public class WebComponentConfiguration {

    @Bean
    public ServletRegistrationBean myServlet1() {
    //使用ServletRegistrationBean ,映射一个Servlet
        ServletRegistrationBean myServlet1 = new ServletRegistrationBean();
        myServlet1.addUrlMappings("/myservlet1");
        myServlet1.setServlet(new MyServlet1());
        return myServlet1;
    }
}

启动类:

@SpringBootApplication
@ServletComponentScan(basePackages = { "com.example.springbootintegarationspringmvc" })
public class SpringbootIntegrationSpringmvcApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootIntegrationSpringmvcApplication.class, args);
    }

}

启动测试:

$ curl.exe --silent http://192.168.10.119:8080/myservlet1
hello from my servlet 1

接下来,我们看下springboot是如何最终通过调用我们的RegistrationBean来完成ServletContext的设置的。

4.3:调用过程分析

基于springboot V1.5.4RELEASE版本分析。

参考

springboot之IOC容器ServletWebServerApplicationContext分析

回答: Springboot是一个快速开发框架,它脱离了传统的web.xml配置文件来进行配置。在Springboot中,没有web.xml文件,而是使用ServletContainerInitializer接口来进行配置。这个接口的实现类通过java spi机制被servlet容器发现并执行onstartup方法,起到了和web.xml文件一样的作用。这样的设计使得Springboot可以快速搭建项目,避免了繁琐的xml配置。在使用Spring Boot进行Web开发时,官方推荐使用内嵌的Servlet容器来部署应用,也可以使用传统的war包来部署,只需要让Main Class继承org.springframework.boot.web.servlet.support.SpringBootServletInitializer即可。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Springboot中的web.xml去哪了?](https://blog.csdn.net/weixin_44159662/article/details/110092047)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [SpringBootweb.xml文件](https://blog.csdn.net/qq_24313635/article/details/114002819)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值