SSM框架原理畅谈之SpringMVC

一、Java SE Servlet标准

Servlet 标准是 Java Web 应用程序开发的基础。

Servlet 是一种 Java 程序,它可以在 Web 服务器(Tomcat、WebLogic等)上运行,用于处理 Http 请求和响应。Servlet 标准定义了 Servlet API,这是一组 Java 类和接口,用于开发 Web 应用程序。

Servlet API 包括 javax.servlet 和 javax.servlet.http 两个包。

  • javax.servlet:定义了 Servlet 的基本接口和类。如常见的Filter、Servlet、ServletConfig、ServletContext、ServletRequest、ServletResponse等。
  • javax.servlet.http:扩展了 javax.servlet 包,提供了处理 Http 请求和响应的类和接口,便捷了开发。如常见的HttpServlet、HttpFilter、HttpServletRequest、HttpServletResponse等。

Servlet 标准的核心是 Servlet 容器。Servlet 容器(Tomcat、Jetty、WebLogic Server等)是 Web 服务器的一部分,它负责加载、初始化和管理 Servlet。Servlet 容器还提供了 Servlet 的生命周期管理、线程安全和请求分发等功能。

Servlet 标准还提供了一些高级功能,如过滤器、监听器和会话管理。

  • 过滤器Filter:拦截请求和响应,可以对齐进行处理或修改。
  • 监听器:监听 Servlet 容器中的事件,主要监听对象有Request、Session、ServletContext。
监听对象监听接口监听事件
ServletRequestServletRquestListener
ServletRquestAttributeListener
ServletRequestEvent
ServletRquestAttributeEvent
HttpSessionHttpSessionListener
HttpSessionActivationListener
HttpSessionAttributeListener
HttpSessionBindingListener
HttpSessionEvent

HttpSessionBingEvent
ServletContextServletContextListener
ServletContextAttributeListener
ServletContextEvent
ServletContextAttributeEvent
  • 会话管理:跟踪用户会话状态,以便在多个请求之间共享数据。主要涉及类有Cookie、Session。

以下是一些Java SE Servlet规范中的关键概念、接口和类:

1.1 Servlet 接口

所有 Servlet 都必须实现的接口,它定义了 Servlet 的生命周期方法,包括初始化、服务处理和销毁。

public interface Servlet {
	// 初始化执行
    void init(ServletConfig servletConfig) throws ServletException;

    ServletConfig getServletConfig();
	// 处理请求:任何请求初始都会进入 service 方法,由 service 方法进行请求的分发和处理
    void service(ServletRequest request, ServletResponse response) throws ServletException, IOException;

    String getServletInfo();
	// 销毁时执行
    void destroy();
}

**HttpServlet类:**抽象类,扩展自GenericServlet类,简化了处理HTTP请求和响应的操作。开发人员通常继承HttpServlet类来实现自定义的Servlet。

在这里插入图片描述
创建一个自定义 HttpServlet:

public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 执行GET请求
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 执行POST请求
        super.doPost(req, resp);
    }
}

添加 HttpServlet 配置:

<servlet>
	<!--名称-->
	<servlet-name>myServlet</servlet-name>
	<!--类全路径-->
	<servlet-class>com.lzq.web.servlet.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
	<!--名称-->
	<servlet-name>myServlet</servlet-name>
	<!--Servlet与URL模式之间的映射关系-->
	<url-pattern>/**</url-pattern>
</servlet-mapping>

1.2 HttpServletRequest 接口

HttpServletRequest 接口是 Java Servlet 规范中定义的接口,用于表示服务器接收到的 HTTP 请求。它提供了一组方法,用于获取请求的各种属性和内容。
在这里插入图片描述
主要涉及方法如下:

  • 获取请求信息:
    • String getMethod(): 获取HTTP请求方法,如GET、POST等。
    • String getRequestURI(): 获取请求的URI(统一资源标识符)。
    • String getQueryString(): 获取请求的查询字符串部分。
    • String getHeader(String name): 获取指定名称的请求头的值。
    • Enumeration<String> getHeaderNames(): 获取所有请求头的名称。
  • 获取请求参数:
    • String getParameter(String name): 获取指定名称的请求参数的值。
    • Map<String, String[]> getParameterMap(): 获取所有请求参数的映射,键为参数名,值为参数值数组。
    • Enumeration<String> getParameterNames(): 获取所有请求参数的名称。
    • String[] getParameterValues(String name): 获取指定名称的请求参数的值数组。
  • 获取请求内容:
    • BufferedReader getReader(): 获取用于读取请求内容的BufferedReader对象。
    • InputStream getInputStream(): 获取用于读取请求内容的InputStream对象。
  • 获取客户端信息:
    • String getRemoteAddr(): 获取客户端的IP地址。
    • String getRemoteHost(): 获取客户端的主机名。
    • String getRemoteUser(): 获取经过身份验证的客户端用户。
  • 获取会话相关信息:
    • HttpSession getSession(): 获取与请求关联的会话对象。
    • HttpSession getSession(boolean create): 获取与请求关联的会话对象,如果不存在则根据参数决定是否创建新的会话。

1.3 HttpServletResponse 接口

HttpServletResponse 接口是 Java Servlet 规范中的一个子接口,继承自 ServletResponse 接口,它提供了更多与 HTTP 协议相关的方法和功能,用于处理HTTP请求的响应。
在这里插入图片描述
主要涉及方法如下:

  • setStatus() / getStatus():设置 / 获取响应的状态码,如200、404、500等。
  • setHeader() / getHandler() / getHandlers:设置 / 获取 http 响应头信息。
  • setContentType() / getContentType():设置 / 获取响应内容类型。
  • setCharacterEncoding():设置响应的字符编码。
  • sendRedirect():发送一个重定向响应到指定的URL。
  • ServletOutputStream getOutputStream():获取输出流,用于写入二进制数据到客户端。
  • PrintWriter getWriter():获取输出流,用于写入字符数据到客户端。
  • void addCookie(Cookie cookie):添加 Cookie。

1.4 Cookie 对象

javax.servlet.http.Cookie 类是 Java Servlet API 中的一部分,提供了对 HTTP Cookie 的封装和操作。它用于创建、设置和解析 Cookie,并与HTTP请求和响应一起使用。

主要涉及方法如下:

  • new Cookie(String name, String value):创建一个 Cookie,并指定name和value。
  • getMaxAge() / setMaxAge(int expiry):获取 / 设置 Cookie 的有效期(单位:秒,为-1时,表示永久)。
  • setPath(String uri) / getPath():设置 / 获取 Cookie 路径。
  • setSecure(boolean flag):设置是否仅通过安全协议传输Cookie。
  • setHttpOnly(boolean httpOnly) / isHttpOnly():设置 / 判断是否仅通过HTTP协议访问Cookie。

1.5 Filter 接口

javax.servlet.Filter 接口是 Java Servlet API 中的一部分,用于实现过滤器(Filter)组件。过滤器是在 Servlet 容器中对 HTTP 请求和响应进行预处理和后处理的组件,用于对请求和响应进行修改、验证、日志记录等操作。

过滤器在Servlet容器中按照配置的顺序对请求进行过滤,并可以修改请求或响应的内容、头信息或状态。过滤器可以用于实现一些通用的逻辑,如身份验证、日志记录、字符编码转换等。

创建一个过滤器:

public class MyFilter implements Filter {
    @Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
	        throws IOException, ServletException {
	    // 对请求进行预处理
	    // 进行逻辑处理,如验证、日志记录等
	    // 调用FilterChain的doFilter()方法继续处理链中的下一个过滤器或Servlet
	    chain.doFilter(request, response);
	    // 对响应进行后处理
	    // 进行逻辑处理,如修改响应内容、头信息等
	}
}

添加过滤器:

<filter>
	<!--过滤器名称-->
    <filter-name>myFilter</filter-name>
	<!--过滤器类名-->
    <filter-class>com.example.MyFilter</filter-class>
</filter>
<filter-mapping>
	<!--过滤器名称-->
    <filter-name>myFilter</filter-name>
	<!--过滤器与URL模式之间的映射关系-->
    <url-pattern>/*</url-pattern>
</filter-mapping>

1.6 HttpSession 接口

HttpSession 是 Java Servlet API 中的一个接口,用于在 Web 应用程序中跟踪用户的会话状态。它提供了一种在多个 HTTP 请求之间存储和检索用户特定信息的机制。

HttpSession的特点是它在服务器端存储会话数据,而客户端只是通过会话ID来标识会话。会话数据可以是任意类型的Java对象,并可以在多个请求之间共享。

HttpSession是一种有状态的机制,用于跟踪用户的会话状态,提供了在Web应用程序中保持用户状态和数据的便捷方式。通过使用HttpSession,开发人员可以存储和检索用户特定的数据,以实现登录状态管理等功能。
在这里插入图片描述

主要方法如下:

  • HttpSession session = request.getSession();:获取当前请求的 HttpSession 对象。如果不存在会话,则创建一个新的会话。
  • HttpSession session = request.getSession(false);:获取当前请求的 HttpSession 对象。如果不存在会话,则返回null,不创建新的会话。
  • setAttribute() / getAttribute() / getAttributeNames:设置 / 获取会话属性:
  • getMaxInactiveInterval() / setMaxInactiveInterval(int interval): 获取 / 设置会话的最大非活动时间间隔(单位:秒)。
  • session.invalidate():使当前会话无效。
  • session.getId():获取会话的唯一标识符(ID)。
  • session.isNew():检查会话是否是新创建的。

二、SpringMVC

Spring MVC 是基于 Java 的 Web 应用程序开发框架,是 Spring Framework 的一部分。它提供了一种模型-视图-控制器(Model-View-Controller,MVC)的架构模式来开发灵活、可维护和可扩展的Web应用程序。

Spring MVC的主要特点和功能如下:

  • MVC架构模式:Spring MVC 采用经典的 MVC 架构模式,将应用程序分为模型(Model)、视图(View)和控制器(Controller)三层,以实现松耦合、可测试和可维护的应用程序。
  • 注解驱动:Spring MVC 提供了注解驱动的方式来定义和处理请求映射、表单验证、数据绑定等,使开发人员可以使用简单的注解来声明请求处理方法和参数绑定。
  • 强大的请求映射:Spring MVC 通过 @RequestMapping 注解提供了灵活而强大的请求映射机制,可以根据URL、请求方法、请求参数等条件来映射到相应的处理方法。
  • 数据绑定和表单处理:Spring MVC 支持将请求参数绑定到方法参数、JavaBean或命令对象中,还提供了表单验证和错误处理的机制。
  • 视图解析和渲染:Spring MVC 支持多种视图技术,如 JSP、Thymeleaf、Freemarker 等,并提供了视图解析器来根据逻辑视图名解析为具体的视图模板。
  • 拦截器和过滤器:Spring MVC 提供了拦截器(Interceptor)和过滤器(Filter)的机制,可以在请求处理前后进行预处理和后处理操作。
  • 国际化和本地化:Spring MVC 提供了国际化和本地化的支持,可以方便地实现多语言和地区的Web应用程序。
  • RESTful风格:Spring MVC 对于构建 RESTful 风格的 Web 服务提供了良好的支持。

Spring MVC 是一个成熟且广泛使用的Web应用程序框架,它结合了 Spring Framework 的依赖注入和面向切面编程的特性,为开发人员提供了强大而灵活的工具和功能来构建现代化的Web应用程序。通过使用 Spring MVC,开发人员可以采用清晰的架构模式,将应用程序的关注点分离开,提高代码的可读性、可测试性和可维护性。

2.1 Spring MVC核心概念

模型(Model):业务逻辑实体类,如POJO等。

视图(View):将模型的数据渲染成用户可见的输出。它可以是 JSP、Thymeleaf、Freemarker 等模板引擎,也可以是 JSON、XML 等格式的数据。

控制器(Controller):控制器负责处理用户请求并返回响应数据给客户端。通过处理HTTP请求,执行业务逻辑处理,最后通过视图进行渲染和返回。

处理器映射器(Handler Mapping):负责将 Http 请求映射到相应的控制器处理方法。

视图解析器(View Resolver):负责将逻辑视图名称解析为具体的视图实现。

中央调度器(DispatcherServlet):DispatcherServlet 是 Spring MVC 的核心组件,它接收所有的HTTP请求,并将请求分发给适当的处理器映射器和控制器进行处理,还负责管理整个请求-响应周期的生命周期和流程

2.2 DispatcherServlet

DispatcherServlet 是 Spring MVC 的核心组件,它接收所有的HTTP请求,并将请求分发给适当的处理器映射器和控制器进行处理,还负责管理整个请求-响应周期的生命周期和流程。类结构图如下:

类结构图

由类结构图可以知道信息如下:

  • EnvironmentAware:获取系统环境属性对象Environment。
  • ApplicationContextAware:获取Spring应用上下文对象ApplicationContext。
  • HttpServlet:Servlet API标准,处理所有Http请求。

DispatcherServlet 默认是懒加载的,系统会在接收到客户端第一个请求后,进行加载,执行Java SE Servlet 标准的 init() 方法进行初始化。

2.3 DispatcherServlet.init()

由 DispatcherServlet 的类结构图进行分析,在其父类 HttpServletBean 中找到了方法 javax.servlet.http.HttpServlet#init() 的具体实现,如下:

public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
	public final void init() throws ServletException {

		// Set bean properties from init parameters.
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
		if (!pvs.isEmpty()) {
			try {
				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) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		// 初始化ServletBean
		initServletBean();
	}
}

initServletBean() 方法为一个抽象方法,最终会调用子类 FrameworkServlet#initServletBean() 中,如下:

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
@Override
	protected final void initServletBean() throws ServletException {
		// 省略非核心方法.....
		try {
			// 重要方法:初始化一个Web应用上下文对象
			this.webApplicationContext = initWebApplicationContext();
			// 初始化FrameworkServlet对象:当前方法为一个空实现
			initFrameworkServlet();
		}
		catch (ServletException | RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			throw ex;
		}
		// 省略非核心方法.....
	}

	protected void initFrameworkServlet() throws ServletException {
	}

	protected WebApplicationContext initWebApplicationContext() {
		// 根据Servlet上下文对象,获取一个root Web应用上下文
		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;
			/**
			 * 判断是否为一个可配置的Web应用上下文对象:如
			 * 				XmlWebApplicationContext / XmlServletWebApplicationContext
			 * 				AnnocationConfigWebApplicationContext  / AnnocationConfigServletWebApplicationContext
			 */
			if (wac instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
				// 判断当前上下文是否活跃,在ssm框架中,会先初始化Spring上下文,active=true
				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);
					}
					// 配置和刷新Web应用程序上下文
					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) {
			// 加锁,保证只有一个线程执行
			synchronized (this.onRefreshMonitor) {
				// 抽象方法,由子类实现
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}
}

在 DispatcherServlet 进行初始化时,执行顺序如下:

HttpServletBean#init() -> FrameworkServlet#initServletBean() -> FrameworkServlet#initWebApplicationContext() -> DispatcherServlet#onRefresh()

最终会调用到 DispatcherServlet 的 onRefresh() 方法中,执行刷新动作。

2.4 DispatcherServlet.onRefresh()

刷新上下文,在 DispatcherServlet 初始化时,或监听到 Spring ContextRefreshedEvent 事件后,都会执行当前方法DispatcherServlet.onRefresh(),刷新 DispatcherServlet。

public class DispatcherServlet extends FrameworkServlet {
	@Override
	protected void onRefresh(ApplicationContext context) {
		// 初始化SpringMVC的9大核心对象
		initStrategies(context);
	}

	/**
	 * 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);
	}
}

SpringMVC 9大核心对象:

  1. MultipartResolver:用于处理文件上传
  2. LocaleResolver:用于处理国际化配置(SpringMVC中有具体的拦截器LocaleChangeInterceptor)
  3. ThemeResolver:用于设置主题Theme,类似于我们手机更换主题,不同的 UI,css 等
  4. HandlerMappers:映射器,用来将 Http 请求与 Controller 进行映射
  5. HandlerAdapters:处理适配器,主要包含 Http 请求处理器适配器,简单控制器处理器适配器,注解方法处理器适配器
  6. HandlerExceptionResolvers:异常处理器,基于 HandlerExceptionResolver 接口的异常处理
  7. RequestToViewNameTranslator:当 Controller 处理器方法没有返回一个 View 对象或逻辑视图名称,并且在该方法中没有直接往 Response 的输出流里面写数据的时候,Spring 将会采用约定好的方式提供一个逻辑视图名称
  8. ViewResolvers:视图处理器,根据 ModelAndView 选择对应的视图进行渲染
  9. FlashMapManager:用于管理FlashMap,FlashMap用于在redirect重定向中传递参数

自此,SpringMVC 完成了初始化动作,当客户端发起请求后,DispatcherServlet 对其进行处理,调用 javax.servlet.http.HttpServlet#service()

2.5 DispatcherServlet.service()

根据 Java SE Servlet 标准,客户端 Http 请求最初会进入 javax.servlet.http.HttServlet#service() 放,改方法被子类 FrameworkServlet 重写,如下:

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
		if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
			processRequest(request, response);
		}
		else {
			super.service(request, response);
		}
	}
}

经分析,不管是 POST 请求,还是 GET 请求,最终调用执行 DispatcherServlet#doDispatch() 方法,处理并响应客户端。

2.6 DispatcherServlet.doDispatch()

所有 HTTP 方法都由这个方法进行调用处理,并返回结果给客户端。

public class DispatcherServlet extends FrameworkServlet {
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				// 检查是否为文件上传请求
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				// 获取当前Request对应的处理器链
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					// 找不到,直接返回客户端
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				// 获取当前请求对应的适配器
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				// 执行对应方法,返回一个 ModeAndView
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			} catch (Exception ex) {
				dispatchException = ex;
			} catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			// 处理结果,返回客户端
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		} catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		} catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		} finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			} else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}
}

在方法 DispatcherServlet.doDispatch() 中,存在几个重要步骤:

  1. mappedHandler = getHandler(processedRequest);:获取此请求的HandlerExecutionChain。
  2. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());:返回此处理程序对象的HandlerAdapter。
  3. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());:执行当前请求对应的方法,返回一个 ModelAndView。
  4. processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);:处理程序调用结果(异常也会被解析为一个ModelAndView)。

2.7 一个请求执行流程

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值