Struts2学习总结

一、概述

1、是什么?

       Struts2轻量级的MVC框架,主要解决了请求分发的问题,重心在控制层和表现层。低侵入性,与业务代码的耦合度很低。Struts2实现了MVC,并提供了一系列API,采用模式化方式简化业务开发过程。

    1、运行在web层.负责处理请求的.
    2、struts2已经帮我们封装了很多web中常用的功能(拦截器)
    3、struts2 与 struts1 没什么关系. struts2是基于webwork框架

2、与Servlet对比

    优点:业务代码解耦,提高开发效率。提供了对MVC的一个清晰的实现,这一实现包含了很多参与对所以请求进行处理的关键组件,如:拦截器、OGNL表达式语言、堆栈。

    缺点:执行效率偏低,需要使用反射、解析XML等技术手段,结构复杂。

3、不同框架实现MVC的方式

    Servlet:

Spring:

Struts2:

二、工作原理

Suruts2的工作原理可以用下面这张图来描述,下面我们分步骤介绍一下每一步的核心内容

  一个请求在Struts2框架中的处理大概分为以下几个步骤 

    1、客户端初始化一个指向Servlet容器(例如Tomcat)的请求

    2、这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做ActionContextCleanUp的可选过滤器,这个过滤器对于Struts2和其他框架的集成很有帮助,例如:SiteMesh Plugin) 

    3、接着FilterDispatcher被调用,FilterDispatcher询问ActionMapper来决定这个请是否需要调用某个Action 

         FilterDispatcher是控制器的核心,就是mvc中c控制层的核心。下面粗略的分析下我理解的FilterDispatcher工作流程和原理:FilterDispatcher进行初始化并启用核心doFilter
 

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException ...{
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        ServletContext servletContext = filterConfig.getServletContext();
        // 在这里处理了HttpServletRequest和HttpServletResponse。
        DispatcherUtils du = DispatcherUtils.getInstance();
        du.prepare(request, response);//正如这个方法名字一样进行locale、encoding以及特殊request parameters设置
        try ...{
            request = du.wrapRequest(request, servletContext);//对request进行包装
        } catch (IOException e) ...{
            String message = "Could not wrap servlet request with MultipartRequestWrapper!";
            LOG.error(message, e);
            throw new ServletException(message, e);
        }
                ActionMapperIF mapper = ActionMapperFactory.getMapper();//得到action的mapper
        ActionMapping mapping = mapper.getMapping(request);// 得到action 的 mapping
        if (mapping == null) ...{
            // there is no action in this request, should we look for a static resource?
            String resourcePath = RequestUtils.getServletPath(request);
            if ("".equals(resourcePath) && null != request.getPathInfo()) ...{
                resourcePath = request.getPathInfo();
            }
            if ("true".equals(Configuration.get(WebWorkConstants.WEBWORK_SERVE_STATIC_CONTENT)) 
                    && resourcePath.startsWith("/webwork")) ...{
                String name = resourcePath.substring("/webwork".length());
                findStaticResource(name, response);
            } else ...{
                // this is a normal request, let it pass through
                chain.doFilter(request, response);
            }
            // WW did its job here
            return;
        }
        Object o = null;
        try ...{
            //setupContainer(request);
            o = beforeActionInvocation(request, servletContext);
//整个框架最最核心的方法,下面分析
            du.serviceAction(request, response, servletContext, mapping);
        } finally ...{
            afterActionInvocation(request, servletContext, o);
            ActionContext.setContext(null);
        }
    }
du.serviceAction(request, response, servletContext, mapping);
//这个方法询问ActionMapper是否需要调用某个Action来处理这个(request)请求,如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy
 
public void serviceAction(HttpServletRequest request, HttpServletResponse response, String namespace, String actionName, Map requestMap, Map parameterMap, Map sessionMap, Map applicationMap) ...{ 
        HashMap extraContext = createContextMap(requestMap, parameterMap, sessionMap, applicationMap, request, response, getServletConfig());  //实例化Map请求 ,询问ActionMapper是否需要调用某个Action来处理这个(request)请求
        extraContext.put(SERVLET_DISPATCHER, this); 
        OgnlValueStack stack = (OgnlValueStack) request.getAttribute(ServletActionContext.WEBWORK_VALUESTACK_KEY); 
        if (stack != null) ...{ 
            extraContext.put(ActionContext.VALUE_STACK,new OgnlValueStack(stack)); 
        } 
        try ...{ 
            ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, extraContext); 
//这里actionName是通过两道getActionName解析出来的, FilterDispatcher把请求的处理交给ActionProxy,下面是ServletDispatcher的 TODO: 
            request.setAttribute(ServletActionContext.WEBWORK_VALUESTACK_KEY, proxy.getInvocation().getStack()); 
            proxy.execute(); 
         //通过代理模式执行ActionProxy
            if (stack != null)...{ 
                request.setAttribute(ServletActionContext.WEBWORK_VALUESTACK_KEY,stack); 
            } 
        } catch (ConfigurationException e) ...{ 
            log.error("Could not find action", e); 
            sendError(request, response, HttpServletResponse.SC_NOT_FOUND, e); 
        } catch (Exception e) ...{ 
            log.error("Could not execute action", e); 
            sendError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); 
        } 
} 

    4、如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy 

    5、ActionProxy通过ConfigurationManager询问框架的配置文件,找到需要调用的Action类 ,这里,我们一般是从struts.xml配置中读取。

    6、ActionProxy创建一个ActionInvocation的实例。

    7、ActionInvocation实例使用命名模式来调用,在调用Action的过程前后,涉及到相关拦截器(Intercepter)的调用。

下面我们来看看ActionInvocation是如何工作的:

    ActionInvocation是Xworks 中Action 调度的核心。而对Interceptor 的调度,也正是由ActionInvocation负责。ActionInvocation 是一个接口,而DefaultActionInvocation 则是Webwork 对ActionInvocation的默认实现。

    Interceptor的调度流程大致如下:

    1.ActionInvocation初始化时,根据配置,加载Action相关的所有Interceptor。

    2. 通过ActionInvocation.invoke方法调用Action实现时,执行Interceptor。

    Interceptor将很多功能从我们的Action中独立出来,大量减少了我们Action的代码,独立出来的行为具有很好的重用性。XWork、WebWork的许多功能都是有Interceptor实现,可以在配置文件中组装Action用到的Interceptor,它会按照你指定的顺序,在Action执行前后运行。

    这里,我们简单的介绍一下Interceptor

    在struts2中自带了很多拦截器,在struts2-core-2.1.6.jar这个包下的struts-default.xml中我们可以发现对于sturts2自带的拦截器,使用起来就相对比较方便了,我们只需要在struts.xml的action标签中加入<interceptor-ref name=" logger " />并且struts.xml扩展struts-default,就可以使用,如果是要自定义拦截器,首先需要写一个拦截器的类:

package ceshi;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
 
publicclassAuthorizationInterceptor extends AbstractInterceptor {
 
    @Override
    public Stringintercept(ActionInvocation ai)throws Exception {
       
           System.out.println("abc");
            return ai.invoke();
           
    }
 
}

并且在struts.xml中进行配置:

<!DOCTYPEstruts PUBLIC
"-//Apache SoftwareFoundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
 
 
<struts>
    <package name="test"extends="struts-default">
     <interceptors>
      <interceptor name="abc"class ="ceshi.AuthorizationInterceptor"/>
    </interceptors>
        <action name="TestLogger"class="vaannila.TestLoggerAction">
           <interceptor-refname="abc"/>
           <result name="success">/success.jsp</result>
           </action>
    </package>
</struts>

8、一旦Action执行完毕,ActionInvocation负责根据struts.xml中的配置找到对应的返回结果。返回结果通常是(但不总是,也可能是另外的一个Action链)一个需要被表示的JSP或者FreeMarker的模版。在表示的过程中可以使用Struts2 框架中继承的标签。在这个过程中需要涉及到ActionMapper,在上述过程中所有的对象(Action,Results,Interceptors,等)都是通过ObjectFactory来创建的。
三、Struts2使用

1、导入Struts2核心jar包

2、在web.xml配置前端控制器filter

<filter>
    <filter-name>Struts2</fileter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>Struts2</filter-name>
    <url-pattern>/*</url-pattern>
<filter-mapping>

3、创建struts.xml(格式可以参考核心包根路径下的DTD文件,struts-default.xml)

4、编写控制器Action

     - 方法是public的

     - 返回值为String类型(返回值与struts.xml->action->result的name属性匹配,即根据此返回值找到对应result)

     - 参数列表为空

5、创建JSP页面

6、配置struts.xml

<struts>
    <!--
        package:包,用于对Action进行封装
        name:包名,根元素下可以有多个包,彼此不能重名
        extends:继承,用于指定继承的包,相当于将继承包下的配置信息复制到当前包
        namespace:命名空间,用于规定Action的访问路径,必须“/”开头    -->
    <package name="test01" namespace="/test01" extends="struts-default">
        <!--action:业务控制器,用于注册业务控制器组件
            name:action名称,用于规定Action的访问路径
            class:业务控制器组件,用于指定业务控制器对应的类
            method:方法,用于指定访问当前action时要调用的方法
            *请求URL:http://ip:port/projectName/namespace/ActionName.action
        -->
        <action name="hello" class="test01.konrad.action.HelloAction" method="execute">
            <!--result:输出组件,用于转发、重定向、直接输出
                name:名称,一个action下可以有多个result,彼此不能重名
                默认值转发,元素内设置转发的页面
            -->
            <result name="success">
                /hello.jsp
            </result>
        </action>
    </package>
</struts>

三、Action

        在struts 2中,action是其核心功能,使用struts 2框架,主要的开发都是围绕action进行的,我们编写的action通常需要实现com.opensymphony.xwork2.Action接口,需要实现的方法是execute方法,但是在实际的开发中,编写的action也可以不必实现action接口,而是直接创建一个普通Java类,并添加execute方法就可以public String execute(){return "success";}。还有一种方式是集成ActionSupport类,该类位于com.opensymphony.xwork2下,其实现了Action接口的execute方法。以上方式后面两种方式是最常用的。

       围绕action,分为以下内容:

action属性 
动态方法调用 
默认action配置 
通配符映射

1、action属性

action中有一个映射框架,主要是讲url映射到对应的action类,action的配置主要在struts.xml文件中编写,所有action的属性如下:

       需要注意的是,action的name属性一般不允许出现.或者/或者-的。但是下划线是可以的。另外,如果在配置文件中没有为action配置class完整类名,那么框架会调用ActionSupport类中execute方法,该方法的实现仅仅是返回一个SUCCESS,相当于是转发了,所以这点实际上与struts 1中的ActionForward的作用是一致的。所以我们可以这个特性,实现仅仅需要完成转发功能的action,这点比使用ActionForward方便多了。

       下面重点对method属性进行说明,通常action的作用是完成一个功能点,但是例如CRUD这样的操作使用四个action类显然不划算,在struts 2中可以将这四个功能映射到一个action中进行处理,这里就需要使用method属性了。具体的做法是:在struts.xml配置文件中为一个action使用method属性和name属性指定不同别名,就可以实现CRUD映射到同一个action了。

       比如有一个用户管理模块,需要对用户进行增加、修改、删除和查询,我们首先创建UserAction类,如下:

package action;

import com.opensymphony.xwork2.ActionSupport;

public class UserAction extends ActionSupport{

    private static final long serialVersionUID = 1L;

    //查询所有用户
    public String list() throws Exception {
        return SUCCESS;
    }

    //修改用户信息
    public String update(){
        return SUCCESS;
    }

    //删除用户信息
    public String delete(){
        return SUCCESS;
    }

    //添加用户
    public String add(){
        return SUCCESS;
    }
}

        要注意的是,在action中并非一定需要execute方法,也可以指定自己需要的方法,action编写完毕后,就需要在struts.xml文件中编写配置文件了,如下:

<package name="user" namespace="/user" extends="struts-default">
        <action name="list" class="action.UserAction" method="list">
            <result>/list.jsp</result>
        </action>
        <action name="add" class="action.UserAction" method="add">
            <result>/add.jsp</result>
        </action>
        <action name="delete" class="action.UserAction" method="delete">
            <result>/delete.jsp</result>
        </action>
        <action name="update" class="action.UserAction" method="update">
            <result>/update.jsp</result>
        </action>
    </package>

        这里,通过使用别名的方式把多个业务功能映射到一个action中的不同方法,这是方式虽然有点笨拙,但是逻辑清晰,一目了然,但是在框架中国还提供了不同编写配置文件也能映射到action中的不同方法,这就是下面要讲的动态方法调用(Dynamic Method Invocation ,简称DMI)。
2、动态方法调用

     所谓动态方法调用是指在action的名字中使用感叹号!标识需要调用的方法名,调用格式是actionName!actionMethod.action,通过这种方式就可以实方法的动态调用了,下面对这一结论做一个测试,首先把配置文件做一点小的修改:

<package name="user" namespace="/user" extends="struts-default">
        <action name="userAction" class="action.UserAction">
            <result name="success">/list.jsp</result>
            <result name="update">/update.jsp</result>
            <result name="add">/add.jsp</result>
        </action>
        <!-- <action name="add" class="action.UserAction" method="add">
            <result>/add.jsp</result>
        </action>
        <action name="delete" class="action.UserAction" method="delete">
            <result>/delete.jsp</result>
        </action>
        <action name="update" class="action.UserAction" method="update">
            <result>/update.jsp</result>
        </action> -->
    </package>

        然后对UserAction修改如下:

//查询所有用户
    public String list() throws Exception {
        return SUCCESS;
    }

    //修改用户信息
    public String update(){
        return "update";
    }

    //删除用户信息
    public String delete(){
        return null;
    }

    //添加用户
    public String add(){
        return "add";
    }

       需要注意的是,尽管DMI给开发带来了诸多遍历,但是存在安全隐患,由于可以通过url直接放完action中任意方法,所以很容易受到恶意攻击。在这种情况下需要使用安全控制机制。

       关于method属性与DMI应该使用哪种方式的问题,可以简要总结如下:如果一个action中的不同方法使用相同的配置(相同的result和拦截器配置),那么可以使用DMI;否则使用method属性在struts.xml文件中进行配置。
3、默认的action

     默认的action是在访问一个不存在的action的时候访问的action,配置默认action,只需要在package中添加如下配置:

<package name="default" namespace="/" extends="struts-default">
        <default-action-ref name="hello"/>
        <!-- HelloWorld演示程序 -->
        <action name="hello" class="example.HelloWorld">
            <result>/index.jsp</result>
        </action>
    </package>

        这里的action配置在package下面,需要注意的是:每个package可以有一个默认的action,但是每个namespace应该只有一个默认的action,因为使用多个的话,框架不知道访问默认的action。

4、通配符映射

     使用通配符映射的方式可以大大减少action的数量,所谓通配符就是使用*,用于匹配0个或多个字符。在action的配置中,可以为name属性使用*来匹配任意的字符。比如下面的配置:

<action name="edit*" class="action.Edit{1}Action">
            <result>/{1}.jsp</result>
        </action>

        {1}会被name属性中的*的内容填充,比如在浏览器中访问/editUser,会映射到action.EditUserAction类,返回到User.jsp页面中。其中的{1}实际上就是作为占位符的,大括号的值可以是0-9,其中{0}代表整个请求URL。比如下面的配置:

<action name="*_*" class="action.{1}Action" method="{2}">
            <result>/{1}_{2}.jsp</result>
        </action>

     当访问User_list的时候,会映射到UserAction类,访问UserAction中的list方法,返回的结果页面是User_list.jsp。

四、OGNL

      1.概念:Object Graph Navigation Language,是一门功能强大的表达式语言,类似于EL。Strut2默认采用OGNL表达式访问Action的数据,实际上是通过ValueStack对象来访问Action。

  2.用法:在Struts2中,OGNL表达式要结合Struts2标签来访问数据

  EL:${user.userName} <==> OGNL:<s:property value="user.userName">

    *a)访问基本属性  <s:property value="属性名"/>

    *b)访问实体对象  <s:property value="对象名.属性名"/>

    c)访问数组/集合  <s:property value="someArray[1]"/> | <s:property value="someList[1]"/>

    d)访问Map  <s:property value="someMap.key" />

    e)运算  <s:property value="'My name is' + name" />

    f)调用方法  <s:property value="name.toUpperCase()" />

    g)创建集合  <s:property value="{'a','b','c'}" /> ArrayList

    h)创建Map  <s:property value="#{'mm':'MM','nn':'NN'}" /> LinkedHashMap

五、ValueStack

  1.概念:是Struts2中,Action向页面传递数据的媒介,封装了Action的数据,并允许JSP通过OGNL来对其访问

  2.原理

3.访问ValueStack

    a)通过<s:debug>观察其结构

    b)输出栈顶:<s:property />

    c)访问Context对象:

      - OGNL表达式以"#"开头

      - 以key来访问context对象的值,即"#key"得到context中某属性值

    d)迭代集合  

 e)按数字迭代

   4.ValueStack栈顶的变化

    - 默认情况下栈顶为Action

    - 循环过程中,栈顶为循环变量(集合迭代时,循环变量是集合中的对象,即栈顶为实体对象,可以以实体对象为root来写OGNL表达式;数字迭代时,循环变量是数字,不能以数字为实体对象,需要通过var声明变量名,以"#变量名"来引用,此情况下,是从context对象中取出值)

    - 循环结束后,栈顶变回Action

 5.EL表达式访问ValueStack

    a)EL也是从ValueStack中取的值

    b)EL默认的取值范围是page,request,session,application

    c)Struts2重写的request的getAttribute方法,先试图从原始request中取值,如果没取到再从ValueStack中取值

六、Result

1、介绍:用于做输出的组件,用于向页面输出一些内容,转发、重定向可以理解为特殊方式的输出。每一个Result实际上是一个类,这些类都实现了共同的接口Result。Struts2预置了10种类型的Result,定义在strtus-default.xml

2、Result类型

    a)dispatcher:用于转发的result,可以将请求转发给JSP,这种类型的Result对应的类为ServletDispacherResult,通过default="true"指定该Result为Struts2默认的Result类型。

    b)stream:用于向页面输出二进制数据,此种类型的Result可以将二进制数据输出到请求发起端,对应类为StreamResult。

<result name="success" type="stream">
  <!--codeStream 为定义在Action的输入流InputStream -->
    <param name="inputName">codeStream</param>
</result>

              c)redirectAction:用于将请求重定向给另外一个Action,对应类为ServletActionRedirectResult。

<result name="login" type="redirectAction">
    <!--若重定向的Action与当前Action在同一个namespace下,可以省略namespace-->
    <param name="namespace">
    /命名空间
    </param>
    <param name="actionName">
    action名
    </param>
</result>

    d)json:用于向页面输出json格式的数据,可以将json字符串输出到请求发起端。对应类为JSONResult。

<result name="success" type="json">
    <!--输出一个Action属性
    指定属性为基本类型,则直接返回该属性值
    如果指定属性为实体对象,则返回格式{"code":"as1","name":"hk"}
    -->
    <param name="root">属性名</param>
    <!--输出多个Action属性-->
    <param name="includeProperties">属性名1,属性名2...</param>
    <!--输出所有属性,不需要param标签-->
   
</result>

    json需要导包,修改package继承关系为json-default。

七、、拦截器

1、用途:拦截器适合封装一些通用处理,便于重复利用。例如请求参数传递给Action属性,日志的记录,权限检查,事务处理等。拦截器是通过配置方式调用,因此使用方法比较灵活,便于维护和扩展。

2、使用步骤

    (1)创建拦截器组件(创建一个类,实现Interceptor接口,并实现intercept方法;也可以继承MethodFilterInterceptor,这种方式可以使action中某个方法不进行拦截)

public String intercept(ActionInvocation invocation){
   //拦截器--前部分处理
   invocation.invoke();
   //拦截器--后续处理      
}

              (2)注册拦截器

<package>
    <interceptors>
        <interceptor name="别名" class="实现类"/>
        <!--其他拦截器-->
    </interceptors>
</package>

               (3)引用拦截器(哪个Action希望被拦截器扩展,需要在此action配置下,引用拦截器)

<action>
   <!--手动的使用一次系统默认的拦截器-->
   <interceptor-ref name="defaultStack"/>
    <interceptor-ref name="拦截器别名"/>
    <!--可以写多个-->
   <!--可以使用excludeMethods参数属性,设置不过滤的方法-->
</action>

 3、拦截器栈

<interceptor-stack name="myStack">
    <interceptor-ref name="拦截器别名1"/>
    <interceptor-ref name="拦截器别名2"/>
</interceptor-stack>

4、FileUpload拦截器

    a)原理:首先FileUpload拦截器将表单中提交的文件,以临时文件的形式保存到服务器临时路径下。之后FileUpload拦截器将该临时文件对象注入给Action,Action自主处理该临时文件。最后FileUpload拦截器删除临时文件。

    b)使用步骤:

      导包 commons-io.jar

      Action:定义File类型属性(如some),接受拦截器注入的临时文件对象。若想要获取原始文件名,要定义String类型属性,属性名为File类型属性+FileName(如someFileName)

      表单设置:method="post", enctype="multipart/form-data"

    c)设置限制(Struts2文件上传默认最大值为2097152B,即2M)

      在struts.xml中重置默认限制值  <constant name="struts.multipart.maxSize" value="5000000" />

 八、Struts2优缺点

1、优点:

        (1)  实现了MVC模式,层次结构清晰,使程序员只需关注业务逻辑的实现。

        (2)  丰富的标签库,大大提高了开发的效率。

        (3) Struts2提供丰富的拦截器实现。

        (4) 通过配置文件,就可以掌握整个系统各个部分之间的关系。

        (5) 异常处理机制,只需在配置文件中配置异常的映射,即可对异常做相应的处理。
        (6) Struts2的可扩展性高。Struts2的核心jar包中由一个struts-default.xml文件,在该文件中设置了一些默认的bean,resultType类型,默认拦截器栈等,所有这些默认设置,用户都可以利用配置文件更改,可以更改为自己开发的bean,resulttype等。因此用户开发了插件的话只要很简单的配置就可以很容易的和Struts2框架融合,这实现了框架对插件的可插拔的特性。

        (7) 面向切面编程的思想在Strut2中也有了很好的体现。最重要的体现就是拦截器的使用,拦截器就是一个一个的小功能单位,用户可以将这些拦截器合并成一个大的拦截器,这个合成的拦截器就像单独的拦截器一样,只要将它配置到一个、Action中就可以。

2、缺点:

        (1) Struts2中Action中取得从jsp中传过来的参数时还是有点麻烦。可以为Struts2的Action中的属性配置上Getter和Setter方法,通过默认拦截器,就可以将请求参数设置到这些属性中。如果用这种方式,当请求参数很多时,Action类就会被这些表单属性弄的很臃肿,让人感觉会很乱。还有Action中的属性不但可以用来获得请求参数还可以输出到Jsp中,这样就会更乱。假设从JSP1中获得了参数money=100000,但是这个Action还要输出到JSP2中,但是输出的格式却不同,money=100,000,这样这个Action中的money中的值就变了。

       (2) 校验还是感觉比较繁琐,感觉太烦乱,也太细化了,如果校验出错的只能给用户提示一些信息。如果有多个字段,每个字段出错时返回到不同的画面,这个功能在Strut2框架下借助框架提供的校验逻辑就不容易实现。
       (3) 安全性有待提高。Struts2曝出2个高危安全漏洞,一个是使用缩写的导航参数前缀时的远程代码执行漏洞,另一个是使用缩写的重定向参数前缀时的开放式重定向漏洞。这些漏洞可使黑客取得网站服务器的“最高权限”,从而使企业服务器变成黑客手中的“肉鸡”。
 

       

 

 

  • 11
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值