Struts2运行过程以及StrutsPrepareAndExecuteFilter源码阅读

Struts2运行过程

1.Struts2启动过程

Created with Raphaël 2.1.0 Struts2启动过程 Tomcat Tomcat StrutsPrepareAndExcuteFilter StrutsPrepareAndExcuteFilter InitOperations InitOperations Dispatcher Dispatcher init() initDispatcher() init()

在Tomcat启动的时候对Struts2 访问的过程:
1.先访问StrutsPrepareAndExcuteFilter类中的init()方法
2.在init方法中调用InitOperations的initDispatcher()方法
3.调用Dispatcher中的init()方法

最终在init()方法中加载
1.default.properties 默认的常量配置文件
2.struts-default.xml 内部的xml
3.struts-plugin.xml 插件机制的xml文件
4.struts.xml 自己写的xml文件

注:后面三个xml文件的did约束是一样的,如果出现相同的项目后者覆盖前者

2.action请求时的过程

当我们在浏览器输入一个url进行action的访问时,tomcat会访问web.xml配置文件,又因为我们在web.xml这样定义拦截器:
<filter>
    <filter-name>struts2</filter-name>
    <filterclass>org.apache.struts2.dispatcher.ng.
    filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter> 

所以Struts2将进行StrutsPrepareAndExecuteFilter类的初始化

public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter

可以看到StrutsPrepareAndExecuteFilter实现了StrutsStatics 以及 Filter接口
下面看看StrutsStatics以及Filter接口声明了什么

public interface StrutsStatics {

    /**
     * Constant for the HTTP request object.
     */
    public static final String HTTP_REQUEST = "com.opensymphony.xwork2.dispatcher.HttpServletRequest";

    /**
     * Constant for the HTTP response object.
     */
    public static final String HTTP_RESPONSE = "com.opensymphony.xwork2.dispatcher.HttpServletResponse";

    /**
     * Constant for an HTTP {@link javax.servlet.RequestDispatcher request dispatcher}.
     */
    public static final String SERVLET_DISPATCHER = "com.opensymphony.xwork2.dispatcher.ServletDispatcher";

    /**
     * Constant for the {@link javax.servlet.ServletContext servlet context} object.
     */
    public static final String SERVLET_CONTEXT = "com.opensymphony.xwork2.dispatcher.ServletContext";

    /**
     * Constant for the JSP {@link javax.servlet.jsp.PageContext page context}.
     */
    public static final String PAGE_CONTEXT = "com.opensymphony.xwork2.dispatcher.PageContext";

    /** Constant for the PortletContext object */
    public static final String STRUTS_PORTLET_CONTEXT = "struts.portlet.context";

    /**
     * Set as an attribute in the request to let other parts of the framework know that the invocation is happening inside an
     * action tag
     */
    public static final String STRUTS_ACTION_TAG_INVOCATION= "struts.actiontag.invocation";
}

从源码中我们可以看出在StrutsStatics 定义了一堆常量
从JavaDoc中我们可以了解到

/**
 * Constants used by Struts. The constants can be used to get or set objects
 * out of the action context or other collections.
 *
 * <p/>
 *
 * Example:
 * <ul><code>ActionContext.getContext().put(HTTP_REQUEST, request);</code></ul>
 * <p/>
 * or
 * <p/>
 * <ul><code>
 * ActionContext context = ActionContext.getContext();<br>
 * HttpServletRequest request = (HttpServletRequest)context.get(HTTP_REQUEST);</code></ul>
 */

这些常量是用来对对象进行静态注入。

在StrutsStatics 中我们看出是一个常量接口,那么在Filter接口中又有什么方法呢?

public interface Filter {
    //初始化
    public void init(FilterConfig filterConfig) throws ServletException;
    //执行过滤
    public void doFilter ( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException;
    //销毁
    public void destroy();
}

通过Filter接口我们可以了解到filter的生命周期init-doFilter-destroy
下面来分析StrutsPrepareAndExecuteFilter类中的init方法

1.init()

public void init(FilterConfig filterConfig) throws ServletException {
        InitOperations init = new InitOperations();
        Dispatcher dispatcher = null;
        try {
 //封装filterConfig,其中有个主要方法getInitParameterNames将参数名字以String格式存储在List中 
            FilterHostConfig config = new FilterHostConfig(filterConfig);
            // 初始化struts内部日志 
            init.initLogging(config);
   //创建dispatcher ,并初始化,这部分下面我们重点分析,初始化时加载那些资源
            dispatcher = init.initDispatcher(config);
            init.initStaticContentLoader(config, dispatcher);
   //初始化类属性:prepare 、execute  
            prepare = new PrepareOperations(dispatcher);
            execute = new ExecuteOperations(dispatcher);
            this.excludedPatterns = init.buildExcludedPatternsList(dispatcher);
   //回调空的postInit方法   
            postInit(dispatcher, filterConfig);
        } finally {
            if (dispatcher != null) {
                dispatcher.cleanUpAfterInit();
            }
            init.cleanup();
        }
    }

下面着重研究dispatcher = init.initDispatcher(config);

public Dispatcher initDispatcher( HostConfig filterConfig ) {
        Dispatcher dispatcher = createDispatcher(filterConfig);
        dispatcher.init();
        return dispatcher;
    }

private Dispatcher createDispatcher( HostConfig filterConfig ) {
        Map<String, String> params = new HashMap<String, String>();
        for ( Iterator e = filterConfig.getInitParameterNames(); e.hasNext(); ) {
            String name = (String) e.next();
            String value = filterConfig.getInitParameter(name);
            params.put(name, value);
        }
        return new Dispatcher(filterConfig.getServletContext(), params);
    }

在创建dispatcher的时候会调用了一个HostCofig,那么HostCofig是干嘛的呢

public interface HostConfig {

    /**
     * @param key The parameter key
     * @return The parameter value
     */
    String getInitParameter(String key);

    /**
     * @return A list of parameter names
     */
    Iterator<String> getInitParameterNames();

    /**
     * @return The servlet context
     */
    ServletContext getServletContext();
}

三个方法分别是:
1.通过键获得值
2.获取属性名称的迭代器
3.获得servlet上下文
通过快捷键crlt+t来查看实现HostConfig接口的类

/**
 * Host configuration that wraps FilterConfig
 * FilterConfig的包装类 
 */
public class FilterHostConfig implements HostConfig {

    private FilterConfig config;
    //构造方法
    public FilterHostConfig(FilterConfig config) {
        this.config = config;
    }
    //根据init-param配置的param-name获取param-value的值 
    public String getInitParameter(String key) {
        return config.getInitParameter(key);
    }
    //返回初始化参数名的List的迭代器
    public Iterator<String> getInitParameterNames() {
        return MakeIterator.convert(config.getInitParameterNames());
    }
    //返回ServletContext
    public ServletContext getServletContext() {
        return config.getServletContext();
    }
}
/**
 * Host configuration that just holds a ServletContext
 * 只存在ServletContext的配置类
 */
public class ListenerHostConfig implements HostConfig {
    private ServletContext servletContext;
    //构造函数
    public ListenerHostConfig(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    public String getInitParameter(String key) {
        return null;
    }

    public Iterator<String> getInitParameterNames() {
        return Collections.<String>emptyList().iterator();
    }
    //返回ServletContext
    public ServletContext getServletContext() {
        return servletContext;  
    }
}
/**
 * Host configuration that wraps a ServletConfig
 * ServletConfig包装类
 */
public class ServletHostConfig implements HostConfig {
    private ServletConfig config;
    //构造函数
    public ServletHostConfig(ServletConfig config) {
        this.config = config;
    }
    //根据init-param配置的param-name获取param-value的值 
    public String getInitParameter(String key) {
        return config.getInitParameter(key);
    }
    //返回初始化参数名的List的迭代器
    public Iterator<String> getInitParameterNames() {
        return MakeIterator.convert(config.getInitParameterNames());
    }
    //返回ServletContext
    public ServletContext getServletContext() {
        return config.getServletContext();
    }
}

在getInitParameterNames对配置文件中的属性名进行封装,并包装成迭代器进行返回

以上三个方法都是对配置文件进行解析,从而进行创建dispatch那么createDispatcher(filterConfig)是做了什么

private Dispatcher createDispatcher( HostConfig filterConfig ) {
        Map<String, String> params = new HashMap<String, String>();
        //根据迭代器中的值来对属性进行赋值封装成为一个Map,然后根据servlet上下文和参数Map构造Dispatcher 
        for ( Iterator e = filterConfig.getInitParameterNames(); e.hasNext(); ) {
            String name = (String) e.next();
            String value = filterConfig.getInitParameter(name);
            params.put(name, value);
        }
        return new Dispatcher(filterConfig.getServletContext(), params);
    }

创建完了Dispatcher接着就要进行 dispatcher.init();初始化方法
而 Dispatcher初始化的过程,加载struts2的相关配置文件,将按照顺序逐一加载:default.properties,struts-default.xml,struts-plugin.xml,struts.xml,……

/**
     * Load configurations, including both XML and zero-configuration strategies,
     * and update optional settings, including whether to reload configurations and resource files.
     * 加载配置文件包括如下...
     */
    public void init() {
        //如果configurationManager 不存在则创建configurationManager 
        if (configurationManager == null) {
            configurationManager = createConfigurationManager(DefaultBeanSelectionProvider.DEFAULT_BEAN_NAME);
        }

        try {
            //初始化文件管理器,用于加载配置文件
            init_FileManager();
            //加载org/apache/struts2/default.properties
            init_DefaultProperties(); // [1]
            init_TraditionalXmlConfigurations(); // [2]
            init_LegacyStrutsProperties(); // [3]
            //用户自己实现的ConfigurationProviders类 
            init_CustomConfigurationProviders(); // [5]
            //Filter的初始化参数  
            init_FilterInitParameters() ; // [6]
            //别名的类的加载
            init_AliasStandardObjects() ; // [7]

            Container container = init_PreloadConfiguration();
            container.inject(this);
            init_CheckWebLogicWorkaround(container);

            if (!dispatcherListeners.isEmpty()) {
                for (DispatcherListener l : dispatcherListeners) {
                    l.dispatcherInitialized(this);
                }
            }
            errorHandler.init(servletContext);

        } catch (Exception ex) {
            if (LOG.isErrorEnabled())
                LOG.error("Dispatcher initialization failed", ex);
            throw new StrutsException(ex);
        }
    }
 private void init_DefaultProperties() {
        configurationManager.addContainerProvider(new DefaultPropertiesProvider());
    }

addContainerProvider调用xwork包中的方法

 public void addContainerProvider(ContainerProvider provider) {
        if (!containerProviders.contains(provider)) {
            containerProviders.add(provider);
            providersChanged = true;
        }
    }

想providers容器添加一个provider那么DefaultPropertiesProvider中又有什么呢

/**
 * Loads the default properties, separate from the usual struts.properties loading
 */
public class DefaultPropertiesProvider extends PropertiesConfigurationProvider {

    public void destroy() {
    }

    public void init(Configuration configuration) throws ConfigurationException {
    }

    public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
        try {
            PropertiesSettings defaultSettings = new PropertiesSettings("org/apache/struts2/default");
            loadSettings(props, defaultSettings);
        } catch (Exception e) {
            throw new ConfigurationException("Could not find or error in org/apache/struts2/default.properties", e);
        }
    }

通过javaDoc和方法体可以知道DefaultPropertiesProvider 是对各种配置文件进行加载
init主要的方法就是这些,具体就不详解了。

2.doFilter()

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    //父类向子类转:强转为http请求、响应   
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
            //如果prepare检测出request的uri与excludedPatterns中的pattern匹配则转发到链的下一个filter
                chain.doFilter(request, response);
            } else {
                //设置编码格式:utf-8,值来自于org.apach.struts 包中的默认default.properties
                prepare.setEncodingAndLocale(request, response);
                //创建Action上下文 重点
                prepare.createActionContext(request, response);
                prepare.assignDispatcherToThread();
                request = prepare.wrapRequest(request);
                ActionMapping mapping = prepare.findActionMapping(request, response, true);
                if (mapping == null) {
                    boolean handled = execute.executeStaticResourceRequest(request, response);
                    if (!handled) {
                        chain.doFilter(request, response);
                    }
                } else {
                    execute.executeAction(request, response, mapping);
                }
            }
        } finally {
            prepare.cleanupRequest(request);
        }
    }

其中createActionContext是重点内容我们观察下其中的方法:

 /**
     * Creates the action context and initializes the thread local
     * 创建action上下文并且初始化thread local 
     */
    public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
        ActionContext ctx;
        Integer counter = 1;
        Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
        if (oldCounter != null) {
            counter = oldCounter + 1;
        }
        //注意此处是从ThreadLocal中获取此ActionContext变量   
        ActionContext oldContext = ActionContext.getContext();
        if (oldContext != null) {
            // detected existing context, so we are probably in a forward
            ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap()));
        } else {
            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
            stack.getContext().putAll(dispatcher.createContextMap(request, response, null));
            ctx = new ActionContext(stack.getContext());
        }
        request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
        ActionContext.setContext(ctx);
        return ctx;
    }

其中 dispatcher.createContextMap():

 public Map<String,Object> createContextMap(HttpServletRequest request, HttpServletResponse response,
            ActionMapping mapping) {

        // request map wrapping the http request objects
        Map requestMap = new RequestMap(request);

        // parameters map wrapping the http parameters.  ActionMapping parameters are now handled and applied separately
        Map params = new HashMap(request.getParameterMap());

        // session map wrapping the http session
        Map session = new SessionMap(request);

        // application map wrapping the ServletContext
        Map application = new ApplicationMap(servletContext);

        Map<String,Object> extraContext = createContextMap(requestMap, params, session, application, request, response);

        if (mapping != null) {
            extraContext.put(ServletActionContext.ACTION_MAPPING, mapping);
        }
        return extraContext;
    }

可以看出:
通过struts2的容器获取到valueStack对象:OnglValueStack
ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
1、把request,session,application等封装成一些map,再把这些map放入到大map中
stack.getContext().putAll(dispatcher.createContextMap(request, response, null));
2.把大map的引用指向了ActionContext中的Map content;
ctx = new ActionContext(stack.getContext());
把ActionContext放入到了当前线程中ActionContext.setContext(ctx);

说明:因为把ActionContext放入到了当前线程中,所以valueStack也在线程中,这样数据就可以保证安全了并且在一个线程范围内可以共享数据

接下来的进行的方法request = prepare.wrapRequest(request);
对request进行了包装:返回的对象是:
1. StrutsRequestWrapper
2. MultPartRequestWrapper 文件上传 继承了 StrutsRequestWrapper

接下来进行的就是
ActionMapping mapping = prepare.findActionMapping(request, response, true);
那么ActionMapping是干什么的?

public class ActionMapping {

    private String name;
    private String namespace;
    private String method;
    private String extension;
    private Map<String, Object> params;
    private Result result;

发没发现跟sturts2.xml格式很想

<package name="users" namespace="/users" extends="default">
        <action name="*_*" class="action.{1}Action" method="{2}">
            <result name="login_success">/users/Users_login_success.jsp</result>
            <result name="login_false">/users/Users_login.jsp</result>
            <result name="logout_success">/users/Users_login.jsp</result>
            <result name="input">/users/Users_login.jsp</result>
        </action>

可见ActionMapping就是对struts2的配置文件的包装
同时也就可以明白findActionMapping就是对struts2.xml的解析

public ActionMapping findActionMapping(HttpServletRequest request, HttpServletResponse response, boolean forceLookup) {
        ActionMapping mapping = (ActionMapping) request.getAttribute(STRUTS_ACTION_MAPPING_KEY);
        if (mapping == null || forceLookup) {
            try {
                mapping = dispatcher.getContainer().getInstance(ActionMapper.class).getMapping(request, dispatcher.getConfigurationManager());
                if (mapping != null) {
                    request.setAttribute(STRUTS_ACTION_MAPPING_KEY, mapping);
                }
            } catch (Exception ex) {
                dispatcher.sendError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
            }
        }

        return mapping;
    }

接下来就是进行execute.executeAction(request, response, mapping);

/**
     * Executes an action
     * @throws ServletException
     */
    public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException {
        dispatcher.serviceAction(request, response, mapping);
    }

从javadoc看出是执行一个action,继续看dispatcher.serviceAction(request, response, mapping);是怎么执行的

 public void serviceAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping)
            throws ServletException {

        Map<String, Object> extraContext = createContextMap(request, response, mapping);

        // If there was a previous value stack, then create a new copy and pass it in to be used by the new Action
        //根据request获取值栈
        ValueStack stack = (ValueStack) request.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);
        boolean nullStack = stack == null;
        if (nullStack) {
            ActionContext ctx = ActionContext.getContext();
            if (ctx != null) {
                stack = ctx.getValueStack();
            }
        }
        if (stack != null) {
        //当值栈不为空时,将值栈的内容拷贝到extraContext中
            extraContext.put(ActionContext.VALUE_STACK, valueStackFactory.createValueStack(stack));
        }

        String timerKey = "Handling request from Dispatcher";
        try {
            UtilTimerStack.push(timerKey);
            String namespace = mapping.getNamespace();
            String name = mapping.getName();
            String method = mapping.getMethod();
        //重点
            ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
                    namespace, name, method, extraContext, true, false);

            request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());

            // if the ActionMapping says to go straight to a result, do it!
            if (mapping.getResult() != null) {
                Result result = mapping.getResult();
                result.execute(proxy.getInvocation());
            } else {
            //重点
                proxy.execute();
            }

            // If there was a previous value stack then set it back onto the request
            if (!nullStack) {
                request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
            }
        } catch (ConfigurationException e) {
            logConfigurationException(request, e);
            sendError(request, response, HttpServletResponse.SC_NOT_FOUND, e);
        } catch (Exception e) {
            if (handleException || devMode) {
                sendError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
            } else {
                throw new ServletException(e);
            }
        } finally {
            UtilTimerStack.pop(timerKey);
        }
    }

在ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
namespace, name, method, extraContext, true, false);中

其中ActionProxyFactory.class是在struts-default.xml声明的

  <bean type="com.opensymphony.xwork2.ActionProxyFactory" name="prefix" class="org.apache.struts2.impl.StrutsActionProxyFactory"/>

同时点开createActionProxy的实现方法

 public ActionProxy createActionProxy(String namespace, String actionName, String methodName, Map<String, Object> extraContext, boolean executeResult, boolean cleanupContext) {
        ActionInvocation inv = createActionInvocation(extraContext, true);
        container.inject(inv);
        return createActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
}

继续点到createActionProxy

public class StrutsActionProxyFactory extends DefaultActionProxyFactory {

    @Override
    public ActionProxy createActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
         StrutsActionProxy proxy = new StrutsActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
        container.inject(proxy);
        proxy.prepare();
        return proxy;
    }
}

继续 proxy.prepare();

protected void prepare() {
        super.prepare();
    }

继续

protected void prepare() {
        String profileKey = "create DefaultActionProxy: ";
        try {
            UtilTimerStack.push(profileKey);
            config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);

            if (config == null && unknownHandlerManager.hasUnknownHandlers()) {
                config = unknownHandlerManager.handleUnknownAction(namespace, actionName);
            }
            if (config == null) {
                throw new ConfigurationException(getErrorMessage());
            }

            resolveMethod();

            if (!config.isAllowedMethod(method)) {
                throw new ConfigurationException("Invalid method: " + method + " for action " + actionName);
            }

            invocation.init(this);

        } finally {
            UtilTimerStack.pop(profileKey);
        }
    }

忽略中间的解析配置过程直接看到 invocation.init(this);

public void init(ActionProxy proxy) {
        this.proxy = proxy;
        Map<String, Object> contextMap = createContextMap();

        // Setting this so that other classes, like object factories, can use the ActionProxy and other
        // contextual information to operate
        ActionContext actionContext = ActionContext.getContext();

        if (actionContext != null) {
            actionContext.setActionInvocation(this);
        }
        //创建action
        createAction(contextMap);

        if (pushAction) {
            stack.push(action);
            contextMap.put("action", action);
        }

        invocationContext = new ActionContext(contextMap);
        invocationContext.setName(proxy.getActionName());

        createInterceptors(proxy);
    }

最终目标

protected void createAction(Map<String, Object> contextMap) {
        // load action
        String timerKey = "actionCreate: " + proxy.getActionName();
        try {
            UtilTimerStack.push(timerKey);
            //创建action
            action = objectFactory.buildAction(proxy.getActionName(), proxy.getNamespace(), proxy.getConfig(), contextMap);
        } catch (InstantiationException e) {
            throw new XWorkException("Unable to intantiate Action!", e, proxy.getConfig());
        } catch (IllegalAccessException e) {
            throw new XWorkException("Illegal access to constructor, is it public?", e, proxy.getConfig());
        } catch (Exception e) {
            String gripe;

            if (proxy == null) {
                gripe = "Whoa!  No ActionProxy instance found in current ActionInvocation.  This is bad ... very bad";
            } else if (proxy.getConfig() == null) {
                gripe = "Sheesh.  Where'd that ActionProxy get to?  I can't find it in the current ActionInvocation!?";
            } else if (proxy.getConfig().getClassName() == null) {
                gripe = "No Action defined for '" + proxy.getActionName() + "' in namespace '" + proxy.getNamespace() + "'";
            } else {
                gripe = "Unable to instantiate Action, " + proxy.getConfig().getClassName() + ",  defined for '" + proxy.getActionName() + "' in namespace '" + proxy.getNamespace() + "'";
            }

            gripe += (((" -- " + e.getMessage()) != null) ? e.getMessage() : " [no message in exception]");
            throw new XWorkException(gripe, e, proxy.getConfig());
        } finally {
            UtilTimerStack.pop(timerKey);
        }

        if (actionEventListener != null) {
            action = actionEventListener.prepare(action, stack);
        }
    }

DefaultActionInvocation中的init方法
createAction(contextMap); 创建action
objectFactory.buildAction
stack.push(action); 把action放入到栈顶
contextMap.put(“action”, action);
把action放入到map中
List interceptorList = new ArrayList(proxy.getConfig().getInterceptors());
interceptors = interceptorList.iterator();
获取这次请求所有的拦截器,并且返回拦截器的迭代器的形式
最后通过proxy.execute();
调用到
DefaultActionInvocation中的invoke方法
1、按照顺序的方式执行所有的拦截器
2、执行action中的方法
3、执行结果集
4、按照倒叙的方式执行拦截器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值