SSH框架之struts2专题3:Struts2核心

1 在Action中获取Servlet API

  • 为了避免与Servlet API耦合,Struts2对HttpServletRequest、HttpSession、ServletContext进行了封装,构造了三个Map对象来代替这三种对象。当然,也可以获取到真正的这三个Servlet的API。
  • 在Action中获取这三个对象的方式有三种。

    1.1 通过ActionContext获取

  • 在Struts2框架中,通过Action的执行上下文类ActionContext,可以获取到request/session/application对象。
    ActionContext ctx = ActionContext.getContext();
    Map<String,Object> application = ctx.getApplication();
    Map<String,Object> session = ctx.getSession();
  • ActionContext对象本身就是request范围的存储空间(现在暂时这样认为)。所以,对于向request范围中添加属性,直接向ActionContext对象中添加即可。

    1.2 通过ServletContext获取

  • 通过ActionContext获取的request、session、application均为Map对象,并非真正的Servlet API。而通过ServletActionContext,可以获取到真正的Http请求中的Servlet API对象。
    HttpServletRequest req = ServletActionContext.getRequest();
    HttpServletResponse res = ServletActionContext.getResponse();
    ServletContext application = ServletActionContext.getServletContext();
    HttpSession session = ServletActionContext.getRequest().getSession();

    1.3 通过实现特定接口来获取

  • 通过让Action实现特定接口,也可获取request/session/application对象。不过需要注意的是,这种获取方式获取的为Map,而并非是真正的Servlet API。
    //RequestAware 接口:该接口中只有一个方法
    public void setRequest(Map<String,Object> request)
    //SessionAware 接口:该接口中只有一个方法
    public void setSession (Map<String,Object> session)
    //ApplicationAware 接口:该接口中只有一个方法
    public void setApplication (Map<String,Object> application)

    2 OGNL与值栈

    2.1 OGNL

    2.1.1 OGML与Struts2的关系

  • Struts2中很多地方都使用了OGNL。所以在这里我们要学习Struts2中有关OGNL的知识。
  • 对于OGNL,首先要清楚,OGNL是个第三方的开源项目,其本身是与Struts2没有任何关系的。OGNL表达式的计算需要通过一个Map进行,而该Map有一个名称叫做上下文context。这个context的Map中已经存放了若干对象,这些对象分为两类:根对象和非根对象。
  • Struts2中使用了ActionContext作为上下文来计算OGNL,ActionContext中的根对象只有一个,叫做值栈ValueStack。其余对象均为非根对象。

2.1.2 什么是OGNL?

  • OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言,是一个第三方的开源项目。
  • Struts2通过使用OGNL简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,便利整个对象的结构图,实现字段类型转化等功能。
  • Struts2通过jar包ognl-3...jar将OGNL项目引入。
    SSH框架之struts2专题3:Struts2核心

2.1.3 OGNL的特点

  • 相对其他表达式语言,它提供了更加丰富的功能:
  • 1、支持对象方法调用,如xxx.sayHello()。
  • 2、支持类静态方法调用和常量访问,表达式的格式为:@[全限定性类名]@[方法名 | 常量名]。例如,@java.lang.Integer@parseInt("123")或者是@java.lang.Math@PI。不过对于静态方法的访问,需要通过在Struts2的配置文件struts.xml中设置常量struts.ognl.allowStaticMethodAccess的值为true进行开启。
  • 3、可以操作集合对象。
  • 4、可以直接创建对象。

2.1.4 OGNL文档解读

  • 查看Struts2的有关OGNL的帮助文档,Struts2解压目录下的docs/docs/home.html。
    SSH框架之struts2专题3:Struts2核心
  • (Struts2)框架使用了一个“标准命名上下文”来计算OGNL表达式。用于处理OGNL的最顶层对象是一个Map(通常被称之为上下文Map或者是上下文)。在上下文Map中,OGNL有一个根对象的概念。在表达式中,根对象的引用不用使用任何“标记”,而引用其他对象则需要使用#标记。
    SSH框架之struts2专题3:Struts2核心
  • (Struts2)框架将ActionContext设置为OGNL上下文对象,将值栈设置为OGNL根对象(值栈是一个包含多个对象的集合,但是对于OGNL来说,它是作为一个对象出现的)。和值栈一起,框架也放置了其他对象到ActionContext中,其中包含表现为application、session或者request上下文的Map。这些对象将于值栈中的数据共存于ActionContext中。
    SSH框架之struts2专题3:Struts2核心
  • 从以上文档的阅读中可知,OGNL有一个上下文概念,即Context,用于存放数据。OGNL的上下文其实质就是一个Map,其中存放着很多的JavaBean对象。这些对象根据对其操作方式的不同分为两类:根对象和非根对象。对于非根对象,需要使用#来访问,而对于根对象,则可以直接访问。
  • 无论是根对象还是非根对象,在Struts2中均是用于在应用中共享数据的。一般情况下,会在Action方法中存入数据,而在JSP页面中读取数据。

2.2 值栈

  • 对于Sturts2中的值栈的学习,主要是要搞清楚以下几个对象间的关系:
    1、值栈与ActionContext的关系。
    2、值栈与值栈的Root属性的关系。
    3、值栈的Context属性和ActionContext的关系。
  • 它们的关系,通过以下几个知识点的学习可以加深理解。

2.2.1 值栈的创建

  • 在用户提交一个Action请求后,系统会马上创建两个对象:Action实例与值栈对象。Struts2中的值栈ValueStack是个接口,其实现类为OgnlValueStack。
  • 在代码中选中ValueStack接口,Ctrl + T可以查看到该接口的实现类为OgnlValueStack。
    SSH框架之struts2专题3:Struts2核心

2.2.2 值栈的组成

  • 打开OgnlValueStack类的源码可以看到,OgnlValueStack类包含两个成员:root与context。
    SSH框架之struts2专题3:Struts2核心
    a、root的类型
  • 属性root为CompoundRoot类型,打开CompoundRoot源码发现,该类型的本质为ArrayList。
    SSH框架之struts2专题3:Struts2核心
  • 即CompoundRoot为增强的ArrayList,是一个栈:有截取栈元素、获取栈顶元素、弹栈、压栈操作。
    SSH框架之struts2专题3:Struts2核心
  • ArrayList在存放数据时的特点是,ArrayList只能够顺序存放数据,而无法为该数据命名。例如,对于一个Student类对象stu,其在List中的存放可能为list.add(stu);即只是将stu所携带的数据---属性值存放到了List中,但是stu这个对象并没有名称。不像将数据存放到Map中map.put("student", stu);这样,可以为stu这个对象起名为“student”。
  • 进一步分析CompoundRoot源码中的peek()方法发现,其只能获取栈顶元素,而栈顶元素就是一个Student对象。由于该栈为List结构,无法通过名称获取栈顶元素。所以对于这个Student对象,只需也只能够通过属性名称来获取,如name、age等就可以直接读取该栈顶元素的指定属性值。
  • 综上所述,对于root中的对象的访问,即对于根对象的访问,只需通过对象的属性名便可读取。

b、context的创建

  • 值栈的属性context的类型为Map<String, Object>,查看OgnlValueStack类的setRoot()方法源码可以看到,在初始化root的同时创建了context对象。
    SSH框架之struts2专题3:Struts2核心

    2.2.3 值栈的获取比较麻烦

  • 当一个Action请求到来时,不仅会创建一个Action实例,还会创建一个ValueStack对象,用于存放Action运行过程中的相关数据。当该请求结束时,Action实例消失,用于记录其运行期间数据的值栈也就没有了意义。所以,当请求结束时,同时需要将值栈对象销毁,即值栈的生命周期与请求Request的相同。为了保证这一点,就将值栈对象通过setAttribute()方法,将其放入到了Request域属性中,并将该属性的key以常量的形式保存在ServletActionContext中。所以,为了获取值栈对象,首先需要获取到Request对象,然后再获取到其key,这样才能够获取到值栈对象。这个过程比较麻烦。
    SSH框架之struts2专题3:Struts2核心
        //从request中获取ValueStack对象
        String key = ServletActionContext.STRUTS_VALUESTACK_KEY;
        HttpServletRequest request = ServletActionContext.getRequest();
        ValueStack vs = (ValueStack) request.getAttribute(key);

    2.2.4 context属性的别名ActionContext

  • 在Strut2中,值栈的context属性所起的作用是比较大的,且在代码中需要经常性地访问。但是从以上分析中可知,想要获取到context属性,首先要获取到值栈对象,而获取值栈对象本身就是一个比较麻烦的过程,这就导致获取值栈的context属性会更加麻烦。
  • 为了方便获取值栈的context属性,Struts2专门为其又起了个别名---ActionContext,通过ActionContext的getContext()就可以直接获取到值栈的context属性。
    SSH框架之struts2专题3:Struts2核心
  • ActionContext类中查看getContext()方法,有一个静态对象actionContext,查看其定义,可以看到其是一个ThreadLocal对象,而ThreadLocal对象本质就是一个Map<Thread, Object>,即这个actionContext静态对象的本质是一个Map<Thread, ActionContext>。
    SSH框架之struts2专题3:Struts2核心
  • 再查看这个ThreadLocal的get()方法源码可以看到,get()方法底层调用的是Map的get()方法,其key为当前线程。这样就保证了在当前线程中获取到的Context是同一个Context。
    SSH框架之struts2专题3:Struts2核心

    2.2.5 值栈的获取根简单

  • 通过前面的叙述中可知,值栈的获取相当麻烦,但是值栈又很重要,需要经常访问。而现在值栈的context属性,这个Map<String, Object>获取起来又很简单。为了方便对值栈的访问,于是就将值栈对象直接存放到了context这个Map中。通过查看OgnlValueStack的对root的初始化方法setRoot()可以看到。(注意:存放的是值栈对象的内存地址,所以不存在大包小,小包大的问题)
    SSH框架之struts2专题3:Struts2核心
  • 这样就可以通过以下语句直接获取到值栈对象:
    ValueStack vs2 = ActionContext.getContext().getValueStack();
  • getValueStack()方法的本质就是调用Map的get()方法,通过制定值栈的key获取值栈对象。
    SSH框架之struts2专题3:Struts2核心

    2.2.6 值栈的栈操作

  • 查看OgnlValueStack类中的peek()、pop()、push()方法可知,对valueStack对象的栈的操作,本质是对root栈对象的操作。即从宏观上看可以直接说值栈就是根对象。但是其实根对象指的是值栈中的root对象,而非根对象是值栈中的context对象。
    SSH框架之struts2专题3:Struts2核心

2.3 获取值栈对象

2.3.1 从request中获取值栈对象

  • 每个请求都会创建一个Action,而每个Action实例,都会拥有一个ActionContext实例,每个ActionContext中都包含一个值栈。值栈会贯穿Action的整个生命周期,即与请求request的生命周期相同。为了保证这一点,就将值栈创建在request作用域中。即将值栈放入到request的一个属性中,当请求结束,request被销毁时,值栈空间被收回。
  • 值栈的实质是request中的一个属性值,这个属性的名称为:struts.valueStack,保存在ServletActionContext的常量STRUTS_VALUESTACK_KEY中。
    SSH框架之struts2专题3:Struts2核心
  • 从以上的叙述中可知,从HttpServletRequest中获取,即从底层获取值栈对象的方式为:
    HttpServletRequest request = ServletActionContext.getRequest();
    ValueStack myValueStack = (ValueStack) request.getAttribute(
    ServletActionContext.STRUTS_VALUESTACK_KEY);

    2.3.2 从ActionContext中获取值栈对象

  • 从之前对文档的阅读可知,在Struts2中OGNL上下文Context接口中的实现类为ActionContext,即ActionContext为Map结构。该Map中事先已经存放好了一组对象,这组对象中就包含根对象值栈。其余为非根对象。
    SSH框架之struts2专题3:Struts2核心
  • 由此可知,值栈也可从ActionContext中直接获取。
    ValueStack myValueStack2 = ActionContext.getContext().getValueStack();

    2.4 值栈操作

    2.4.1 搭建测试环境

    1、定义实体Student:

    package com.eason.valuestack.entity;
    public class Student {
        private String name;
        private int age;
        public Student(String name, int age) {
            super();
            this.name = name;
            this.age = age;
        }
        public Student() {
            super();
            // TODO Auto-generated constructor stub
        }
        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + "]";
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
    }

    2、定义Ation

        package com.eason.valuestack.entity;
        import java.util.HashMap;
        import java.util.Map;
        import com.opensymphony.xwork2.ActionContext;
        import com.opensymphony.xwork2.util.ValueStack;
        public class SomeAction {
            public String SomeAction() {
                Student student = new Student("张三", 21);
                //获取值栈对象
                ValueStack valueStack = ActionContext.getContext().getValueStack();
                return "success";
            }
        }

    3、注册Action

    <struts>
        <package name="demo" namespace="/test" extends="struts-default">
            <action name="some" class="com.eason.valuestack.entity.SomeAction">
                <result>/show.jsp</result>
            </action>
        </package>
    </struts>

    4、定义视图页面show

    <body>
        <s:debug/>
    </body>

    2.4.2 向root中显式放入数据

  • 向root中显式地放入数据,有五种方式。下面以向root中放入Student(name, age)对象为例。
    1、通过操作值栈来添加无名称对象
    • ValueStack即值栈,其实现类OgnlValueStack的对象即值栈对象。既然其称之为值栈,就应该有栈的相关操作,查看OgnlValueStack的源码,可以看到其确实有栈操作的方法。但,再看看这些方法的实现,其本质是调用了值栈对象的属性根对象root的栈操作,即对值栈的直接操作,本质上是对根对象root的栈操作。
      SSH框架之struts2专题3:Struts2核心
  • 通过对值栈的压栈操作向root中添加数据。由于root的本质是ArrayList,其特点就是向其中添加的对象时无法指定名称的。
        public String execute() {
            Student student = new Student("張三", 21);
            //获取值栈对象
            ValueStack stack = ActionContext.getContext().getValueStack();
            stack.push(student);
            return "success";
        }

2、通过操作root来添加无名称对象

  • 从前面对root的源码分析可知,对值栈的操作,本质上就是对值栈的root属性的栈操作。所以,可以直接向root中进行压栈操作。
        public String execute() {
            Student student = new Student("張三", 21);
            //获取值栈对象
            ValueStack stack = ActionContext.getContext().getValueStack();
            stack.getRoot().push(student);
            return "success";
        }
    <body>
        <s:debug/>
        ------------------root数据显示--------------------<br/>
        name = <s:property value="name"/><br/>
        age = <s:property value="age"/><br/>
        ----------------el数据显示------------------------<br/>
        el_name = ${name }<br/>
        el_age = ${age }<br/>
    </body>

    3、添加map对象

  • 将对象放入map中,再将map添加到root中。Map的特点是可指定向其中添加对象的名称。
    public String execute() {
            Map<String, Student> map =  new HashMap<String, Student>();
            Student student = new Student("張三", 21);
            map.put("studentMap", student); 
            //获取值栈对象
            ValueStack stack = ActionContext.getContext().getValueStack();
            stack.push(map);
            return "success";
        }
    <body>
        <s:debug/>
        ------------------root数据显示--------------------<br/>
        name = <s:property value="studentMap.name"/><br/>
        age = <s:property value="studentMap.age"/><br/>
        ----------------el数据显示------------------------<br/>
        el_name = ${studentMap.name }<br/>
        el_age = ${studentMap.age }<br/>
    </body>

4、直接添加有名称对象

  • OgnlValueStack值栈中有一个方法set(),可向其中的对象指定名称,其底层也是采用map来实现的。
    SSH框架之struts2专题3:Struts2核心
    public String execute() {
        Student student = new Student("張三", 21);
        //获取值栈对象
        ValueStack stack = ActionContext.getContext().getValueStack();
        stack.set("studentMap", student);
        return "success";
    }

    5、将root作为ArrayList来添加对象

  • 从前面对root的源码分析可知,root的本质是ArrayList,所以可以调用其add()方法来添加数据。
    public String execute() {
        Student student = new Student("張三", 21);
        //获取值栈对象
        ValueStack stack = ActionContext.getContext().getValueStack();
        stack.getRoot().add(student);
        return "success";
    }

    但是,需要注意的是,通过add()添加的数据,是将root作为ArrayList来使用,而ArrayList的add()方法会自动将数据放入到list的最后。就本例来说,通过add()添加的数据,会放入到root栈的栈底。而通过前面栈的操作所添加的数据,是将数据放入到root栈顶。
    SSH框架之struts2专题3:Struts2核心

    2.4.3 向root中隐式放入数据

  • 当Struts2接收到一个请求后,会马上创建一个Action对象,然后为该Action对象创建一个ActionContext对象,那么也就创建了值栈对象。
  • 值栈对象创建好之后,首先会将创建好的Aciton对象直接放入到值栈的栈顶。于是,JSP页面对于Action属性的访问,直接写上Action属性名称即可。
    SSH框架之struts2专题3:Struts2核心
    public class SomeAction {
        private String name;
        private int age;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public String execute() {
            ......
            return "success";
        }
    }

    2.4.4 向context中显式放入数据

  • 向context中放入数据,就是向map中放入数据,需要指定key。Struts2中已经定义好一些key,用于完成特定数据功能。访问context中的数据,即为非根数据,需要使用#。
    SSH框架之struts2专题3:Struts2核心
    1、向context中直接放入数据
  • 向context中直接放入数据,相当于在context中添加了用户自定义的key和value,即map对象。
    public String execute() {
        Student student = new Student("張三", 21);
        ActionContext.getContext().put("myStudent", student);
        return "success";
    }
  • 向ActionContext中直接写入数据,即形成如下情况:
    SSH框架之struts2专题3:Struts2核心
  • 其访问方式即为:
    <body>
        <s:debug/>
        ---------------------context数据显示----------------------<br/>
        name = <s:property value="#myStudent.name"/>
        age = <s:property value="#myStudent.age"/>
    </body>
  • 当然,由于context本身是request范围,那么向context中直接添加数据,也即放入request范围中数据。此时,JSP页面可通过系统定义好的名称为request的key来访问。
    <body>
        <s:debug/>
        ---------------------context数据显示----------------------<br/>
        name = <s:property value="#request.myStudent.name"/>
        age = <s:property value="#request.myStudent.age"/>
    </body>

    2、向context的ssession中放入数据

  • 向context的session中放入数据,即将对象放入到session范围。
        public String execute() {
            Student student = new Student("張三", 21);
            ActionContext.getContext().getSession().put("myStudent", student);
            return "success";
        }
    <body>
    <s:debug/>
    ---------------------context数据显示----------------------<br/>
    name = <s:property value="#session.myStudent.name"/>
    age = <s:property value="#session.myStudent.age"/>
    </body>

    3、向context的application中放入数据

  • 向context的application中放入数据,即将对象放入到application范围中。
    public String execute() {
        Student student = new Student("張三", 21);
        ActionContext.getContext().getApplication().put("myStudent", student);
        return "success";
    }
    <body>
    <s:debug/>
    ---------------------context数据显示----------------------<br/>
    name = <s:property value="#application.myStudent.name"/>
    age = <s:property value="#application.myStudent.age"/>
    </body>

    4、JSP页面通过称之为attr的key读取数据

  • 若JSP页面通过名称为attr的key读取数据,系统会依次从page、request、session、application范围来查找指定key的数据。若没有找到,则为其赋值为null。
    <body>
        <s:debug/>
        ---------------------context数据显示----------------------<br/>
        name = <s:property value="#attr.myStudent.name"/>
        age = <s:property value="#attr.myStudent.age"/>
    </body>

    2.4.5 向context中隐式放入数据

  • 有两种数据是在用户不知情的情况下自动放入到context中的。
    1、请求参数
  • 在提交Action请求时所携带的参数,会自动存放到context的parameters属性中。
    SSH框架之struts2专题3:Struts2核心
  • 需要注意的是,提交action请求时所携带的参数,参数会写入到context中的parameters中。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
  • 当然,在struts配置文件中Action重定向时提交的参数,若重定向到Action,但Action中有属性用于接该参数,则是放入到root中的。若重定向到的Action中无属性接收该参数,或者直接重定向到页面,则是将数据放入到context中key为parameters的map中。
    2、Action对象
  • 当Action实例创建好后,不仅会自动将该Action的属性放入到root的栈顶,而且还会将Action实例本身也放入到context的action属性中。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    2.4.6 数据的加载顺序

    1、root中数据的加载顺序

  • 当在Action中仅仅向context放入某数据some后,页面无论是通过#从context中读取数据,还是不使用#直接从root中读取数据,均可得到放入的值。
  • 对于从root中读取数据,其底层调用了值栈的findValue()方法。查看ValueStack的实现类OgnlValueStack源码,方法间的调用关系如下:
    SSH框架之struts2专题3:Struts2核心
  • 从以上源码可知,<s:property /> 会先从root中加载数据,若不存在该数据,则会从context 中加载,而不会直接设值为null。
    2、request中数据的加载顺序
  • 当在Action中向context与root中分别为某数据some赋予不同的值时,页面从context中与root中可获取其相应的值。但若从#request中获取some值,会发现其获取的与root中的值相同。
    SSH框架之struts2专题3:Struts2核心
  • Struts2将HttpServletRequest进行了再次包装,增强了其功能。在JSP页面中通过对表达式<%= request %>的输出,可看到其类型已经不再是HttpServletRequest,而是org.apache.struts2.dispatcher.StrutsRequestWrapper。
  • <s:property/>对于从request中读取数据,其底层调用了StrutsRequestWrapper的getAttribute()方法。查看StrutsRequestWrapper类的源码:
    SSH框架之struts2专题3:Struts2核心
  • 从以上源码可知,若request范围的属性值不空,则直接取该值;若为空,则再调用值栈的findValue()方法。即找到,则取root中的值;root中没有找到,再从context中查找。
  • 从这个分析可知,之前所说的ActionContext.getContext()所获取的为request范围,并不准确,或者说,并不正确。因为现在从#request中获取的值并不是context中的值,而是root中的值。

    2.5 OGNL对于集合的操作

  • Struts2中使用OGNL对于集合的操作主要涉及以下几种情况:
    1、创建List与Map集合
    2、遍历LIst与Map集合
    3、集合元素的判断
    4、集合投影
    5、集合过滤

    2.5.1 创建List与Map集合

  • OGNL表达式中使用如下形式可以创建相应集合:
  • List集合的创建:{元素1, 元素2, ...}
  • Map集合的创建:#{'key1':value1, 'key2':value2, ...}
  • 当然可以借助于Struts2标签<s:set/>创建一个有名称的集合对象:
    SSH框架之struts2专题3:Struts2核心
  • 此时,创建了一个名为userList的List<String>集合,其包含三个字符串元素,"zs","ls","ww"。
    SSH框架之struts2专题3:Struts2核心
  • 此时,创建一个名为userMap的Map<String, Object>集合,其包含两对元素:("name", "zs")与("pwd", 21)。

    2.5.2 遍历List与Map集合

  • <s:set/>标签还有一个属性 scope,用于指定要将此 List 或 Map 存放入哪个范围。其值
    可以为:application、session、request(放入 context 中)、action(放入 root 中)。
  • 若不指定范围,则会将此变量直接放入 ActionContext 中,与值栈、application、session
    等对象并列(如下图),形成非根对象。所以要对它们进行访问,需要加上#。如:#userList 或 #userMap。
    SSH框架之struts2专题3:Struts2核心
    1、对于List的遍历
  • 若要对集合进行遍历,则需要借助<s:iterator/>标签。
    SSH框架之struts2专题3:Struts2核心
  • <s:iterator/>标签有个特点,会将当前迭代的对象临时放入值栈的栈顶。而<s:property/>
    标签也有个特点,就是在不指定 value 属性时,会输出值栈栈顶元素的值。
    2、对Map的遍历
  • 方式一:直接输出栈顶的Map对象。
    SSH框架之struts2专题3:Struts2核心
  • 方式二:当前迭代的对象命名为变量 entry,该对象会被临时放入到值栈栈顶。所以对于该对象
    的显示,无需使用#,直接输出即可。
    SSH框架之struts2专题3:Struts2核心
  • 方式三:Map 的元素为 Map.Entry 对象,而 Map.Entry 有两个属性:key 与 value。当迭代 Map
    对象时,会将当前迭代的元素对象,即 Map.Entry 对象放入栈顶。即将 key 与 value 属性放
    入了栈顶。
    SSH框架之struts2专题3:Struts2核心

    2.5.3 集合元素的判断

  • 对于集合类型,可以使用双目运算符 in 和 not in 组成表达式,来判断指定元素是否为
    指定集合的元素。其两个运算数为 OGNL 表达式:e1 in e2 或 e1 not in e2。
  • 其中,in用来判断e1是否在集合对象e2中;not in判断e1是否不在集合对象e2中。
    其结果为 boolean 类型。如:
    SSH框架之struts2专题3:Struts2核心
  • 其输出结果为true。

    2.5.4 集合投影

  • 所谓集合投影,指对于集合中的所有数据(相当于行),只选择集合的某个属性值(字段)作为一个新的集合。即行数不变,只选择某列。使用:集合.{字段名}投影。例如,定义好了三个Bean:
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    2.5.4 集合查询

  • 集合查询也称之为集合过滤,指的是对于集合中的所有数据,进行条件筛选,形成的结果集。即列数不变,只选择部分行。集合查询需要使用关键字this,表示当前检索的对象。
  • 对于结果集的选择,可以使用以下三个符号:
  • ?:表示结果集的所有内容。
  • ^:结果集的第一条内容。
  • $:结果集的最后一条内容。
    1、?的使用
    SSH框架之struts2专题3:Struts2核心
  • 查询结果:
    SSH框架之struts2专题3:Struts2核心
    2、^的使用
    SSH框架之struts2专题3:Struts2核心
  • 查询结果:
    SSH框架之struts2专题3:Struts2核心

    3 动态调用方法

  • 若Action中存在多个方法,但在配置文件中注册该Action时,并未为每个方法指定一个<action/>,而是只为这一个Action类注册一个<action/>。那么,当用户访问该<action/>时,到底执行哪个方法,则是由用户发出的请求动态决定。即仅从配置文件中是看不出<action/>标签是对应哪个方法的,只有在运行时根据具体的用户请求,才能够决定执行哪个方法。这种情况称之为动态调用方法。动态调用方法有两种实现方式。

    3.1 动态方法调用

  • 动态方法调用是指,在地址栏提交请求时,直接在URL后跟上“!方法名”方式动态定义要执行的方法。
    -不过,动态方法调用默认是关闭的,可以通过改变“动态方法调用”常量struts.enable.DynamicMethodInvocation的值来开启动态方法调用功能。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
  • 举例:dynamicMethodInvoke
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
  • 地址栏中访问:
    SSH框架之struts2专题3:Struts2核心

    3.2 使用通配符定义的Action

  • 使用通配符定义的action是指,在配置文件中定义action时,其name中包含通配符。请求URL中提交的action的具体值,将作为的真实值。而<action/>中的占位符将接收这个真实值。占位符一般出现在method属性中。
    SSH框架之struts2专题3:Struts2核心

    4 接收请求参数

    4.1 属性驱动方式

  • 所谓属性驱动方式是指服务器端接收来自客户端的离散数据的方式。用户提交的数据,Action 原封不动的进行逐个接收。该接收方式要求,在 Action 类中定义与请求参数同名的属性,即,要定义该属性的 set 方法。这样就能够使 Action 自动将请求参数的值赋予同名属性。
  • 举例:receiveProperty
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    4.2 域驱动方式

  • 所谓域驱动方式是指,服务器端以封装好的对象方式接收来自客户端的数据方式。将用
    户提交的多个数据以封装对象的方式进行整体接收。该方式要求,表单提交时,参数以对象属性的方式提交。而 Action 中要将同名的对象定义为属性(为其赋予 getter and setter)。这样请求将会以封装好的对象数据形式提交给 Action。
  • 举例:receiveObject
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
  • 控制台输出情况:
    SSH框架之struts2专题3:Struts2核心
  • 对象类型数据接收的内部执行过程比较复杂:
    1、当将表单提交给服务器后,服务器首先会解析出表单元素,并读取其中的一个元素name值,如读取如下:
    SSH框架之struts2专题3:Struts2核心
    2、执行Action的getStudent()方法以获取Student对象。判断Student对象是否为null,若为null,则服务器会通过反射创建一个Student对象。
    3、由于此时的Student对象为null,所以系统会创建一个并调用setStudent()方法将student初始化。
    4、此时的student已非空,会将刚才解析出的表单元素,通过调用其set方法,这里是setName()方法,将用户输入的值初始化该属性。
    5、再解析下一个表单元素,并读取其name值。
    SSH框架之struts2专题3:Struts2核心
    6、再次执行Action的getStudent()方法以获取Student对象。此时的Student对象已非空。
    7、将刚才解析出的表单元素,通过调用其set方法,这里是setAge()方法,将用户输入的值初始化该属性。

    4.3 集合数据接收

  • 所谓集合数据接收是指,以集合对象方式接收数据。此情况与域驱动数据原理是相同的。注意,集合与数组是不同的。
  • 举例:receiveCollection
    SSH框架之struts2专题3:Struts2核心SSH框架之struts2专题3:Struts2核心
  • 此时用于接收集合数据的属性,不能定义为数组。因为数组长度是固定的,而集合长度是可扩展的。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    4.4 ModelDriven方式

    • ModelDriven接收请求参数运行背后使用了Struts的核心功能ValueStack。Struts2的默认拦截器中存在一个拦截器ModelDrivenInterceptor。当一个请求经过该拦截器时,在这个拦截器中,首先会判断当前要调用的Action对象是否实现了ModelDriven接口。如果实现了这个接口,则调用getModel()方法,并把返回值压入ValueStack栈顶。
      SSH框架之struts2专题3:Struts2核心
    • 举例:receiveModelDriven
      SSH框架之struts2专题3:Struts2核心
      SSH框架之struts2专题3:Struts2核心
      SSH框架之struts2专题3:Struts2核心
      SSH框架之struts2专题3:Struts2核心
      SSH框架之struts2专题3:Struts2核心

      4.5 Action对象是多例的

    • action对象是多例的。对于包含name和age属性的RegisterAction,在Web中多个用户同时进行访问时,系统会为每个用户创建一个RegisterAction实例,接收来自不同用户的name和age。其值是各不相同,各不相干的。所以action也是线程安全的。
    • 对于同一个业务,Web容器只会创建一个Servlet实例。这个实例允许多个请求共享,即允许多个线程共享。例如对于LoginServlet,只要用户登录,无论几个用户,Web容器只会创建一个LoginServlet实例。若此时,将username与password定义为LoginServlet的成员变量,那么不同的用户为其赋值是不同的,后一个用户的值均会覆盖前一个用户的值,将引起并发问题,线程会不安全。所以,对于Servlet的使用,是不能定义成员变量的。
    • 当然,也基于此,可以为Serlvet添加一个自增的成员变量作为该Servlet的访问计数器。

      5 类型转换

      5.1引入

  • 在Struts2中,请求参数还可以是日期类型。如,定义一个请求参数brithday为Date类型,为其赋值为1949-10-01,则brithday接收到的不是字符串“1949-10-01”,而是日期类型 Sat Oct 01 00:00:00 CST 1949。
  • 举例:typeconverter
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    5.2 默认类型转换器

  • Struts2默认情况下可以将表单中输入的文本数据转换为相应的基本数据类型。这个功能的实现,主要是由于Struts2内置了类型转换器,这些转换器在struts-default.xml中可以看到其定义。
    SSH框架之struts2专题3:Struts2核心
  • 常见的类型,基本均可由String转换为相应类型。
  • 如int和Integer,long和Long,float和Float,double和Double,char和Character,boolean和Boolean,Date(可以接收yyyy-MM-dd或者yyyy-MM-dd HH:mm:ss格式字符串),数组(可以将多个同名参数,存放到数组中),集合:可以将数据保存到List、Map中。

    5.3 自定义类型转换器

  • 在上面程序中,若输入非yyyy-MM-dd格式,如输入yyyy/MM/dd格式,在结果页面中还可以正常看到yyyy/MM/dd的日期输出。但是查看控制台,却给出了null值。其实底层已经发生了类型转换失败,抛出了类型转换异常TypeConversionException。若要使得系统可以接收其他格式的日期类型,则需要自定义类型转换器。
  • 查看DateConverter、NumberConverter等系统定义好的类型转换器源码,可以看到它们都是继承自DefaultTypeConverter类。所以,我们要定义自己的类型转换器,也要继承自该类。在使用时,一般需要覆盖其父类的方法convertValue(),用于完成类型转换。
    SSH框架之struts2专题3:Struts2核心
  • 定义convertValue方法时需要注意,其转换一般是定义为双向的。
  • 就本例而言,从浏览器表单请求到服务器端action时,是由String到Date的转换,那么,第一个参数value为String类型的请求参数值,第二个参数toType为要转换为的Date类型。
  • 当类型转换成功,但是服务器端其他部分出现问题后,需要返回原页面。此时用户填写过的数据应重新回显。回显则是由服务器端向浏览器端的运行,需要将转换过的Date类型重新转换为String。那么,此时的value则为Date类型,而toType则为String。
  • 注意第一个参数value,若转换方向为从请求到action,则value为字符串数组。因为请求中是允许携带多个同名参数的,例如下面表单中的兴趣爱好项的name属性为hobby,其值就有可能为多个值。
    SSH框架之struts2专题3:Struts2核心
  • 这时的这个同名参数,其实就是数组。Struts2为了兼顾到这个多个同名参数的情况,就将从请求到action方向转换的value指定为String[],而非String。其底层使用的API为:String[] value = request.getParameterValues(...);。
  • 注意,对于服务器端向浏览器端的转换,需要使用Struts2标签定义的表单才可演示出。演示时,age元素填入非数字字符,birthday填写正确。另外,向<action/>中添加input视图,Action类需要继承ActionSupport类。
  • 之所以要继承自ActionSupport类,是因为ActionSupport类实现了Action接口,而该接口中定义INPUT字符串常量。一旦发生类型转换异常TypeConversionException,系统将自动转向input视图。
  • 示例:
    1、定义index页面:
    SSH框架之struts2专题3:Struts2核心
    2、定义Action
    SSH框架之struts2专题3:Struts2核心
    3、注册Action
    SSH框架之struts2专题3:Struts2核心
    4、定义自定义日期类型转换器
  • 注意,Date在这里自动导入的是java.sql包,而我们需要的是java.util包中的Date。
    SSH框架之struts2专题3:Struts2核心
  • 注意,此时定义的类型转换器,不仅可以完成日期类型的转换,还可以实现以下的回显:age填写错误,birthday填写正确。但,若age填写正确,birthday填写错误的话,则birthday日期格式不正确,则无法回显。因为birthday值为null,此时发生的异常不是类型转换异常,而是格式解析异常ParseException。ParseException的发生不会使得页面跳转到input视图。所以若要使得日期格式不正确时跳转到input视图,则需要让其抛出TypeConversionException异常。
    SSH框架之struts2专题3:Struts2核心
  • 定义好类型转换器类后,需要注册该转换器,用于通知Struts2框架在遇到指定类型变量时,调用类型转换器。
  • 根据注册方式的不同以及应用范围的不同,可以将类型转换器分为两类:局部类型转换器和全局类型转换器。
    1、局部类型转换器
  • 局部类型转换器,仅仅对指定Action的指定属性起作用。若注册方式为,在Action类所在的包下放置名称为如下格式的属性文件:ActionClassName-conversion.properties文件。其中ActionClassName是Action的类名,-conversion.properties是固定写法。
  • 该属性文件的内容应该遵循如下格式:属性名称=类型转换器的全类名。
    SSH框架之struts2专题3:Struts2核心
    2、全局类型转换器
  • 全局类型转换器,会对所有Action的指定类型的属性生效。其注册方式为,在src目录下放置名称为xwork-conversion.properties属性文件。该文件的内容格式为:待转换的类型=类型转换器的全类名。就本例而言,文件中的内容为:
    SSH框架之struts2专题3:Struts2核心
  • 注意,对于服务器端向浏览器端的转换,即数据的回显功能,需要使用Struts2标签定义的表单才可演示出。演示时,age元素填入非数字字符,birthday填写正确。

    5.4 类型转换异常提示信息的修改

  • 类型转换异常提示信息,是系统定义好的内容,若直接显示到用户页面,会使得页面显得不友好。但是,类型转换异常提示信息时可以修改。
  • 类型转换异常提示信息的修改步骤:
    1、Action所在包中添加名称为ActionClassName.properties的属性文件,其中ActionClassName为Action类的类名。
    2、在该文件中写入内容:invalid.fieldvalue.变量名=异常提示信息

    5.5 接收多种日期格式的类型转换器

  • 对于前面定义的MyDateConverter类型转换器,无论是局部的还是全局的,其转换的日期格式只能是在转换器中指定的格式:yyyy/MM/dd。不能是其他格式,原来默认的格式也不行。那么,如何使类型转换器可以处理多种日期格式的转换呢?
  • 在以上项目的基础进行修改
    1、获取日期格式
  • 在类型转换器中定义一个 private 方法,用于判断并获取到日期的格式,若所有指定格
    式均不符合,则直接抛出类型转换异常,跳转到表单页面。
    SSH框架之struts2专题3:Struts2核心
    2、保存日期格式对象
  • 由于从页面到服务端方向是由String到Date类型的转换,value为待转换的数据,所以可以从这个String的数据中获取到日期格式,然后将String转换为 Date。
  • 但若发生数据回显,则需要使用相同格式的日期格式对象,将Date转换为String。此时的value为没有格式的Date 数据,所以需要想办法获取到 String 到 Date 转换时的日期格式对象。也就是说,需要在 String 到 Date 转换时,就将用到的日期格式对象保存下来,以备Date 到 String 转换时使用。
  • 那么,如何在 String 到 Date 转换时,保存日期格式对象呢?可以将其保存到 ServletAPI的域对象中。即保存到 request、session 或 application 中。
  • 当在 String 到 Date 的转换时将日期格式对象保存在了 ServletAPI 的域对象中,在发生数据回显,由 Date 到 String 转换时,直接从相应的 ServletAPI 的域中获取日期格式对象即可。
    SSH框架之struts2专题3:Struts2核心
    3、最终的类型转换器定义
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    6 数据验证

  • 在Web应用程序中,为了防止客户端传来的数据引发程序的异常,常常需要对数据进行验证。输入验证分为客户端验证与服务器端验证。客户端验证主要通过 JavaScript 脚本进行,而服务器端验证则主要是通过 Java 代码进行验证。
  • 为了保证数据的安全性,一般情况下,客户端验证与服务器端验证都是要进行的。我们这里所讲的是 Struts2 如何在服务端对输入数据进行验证的。Struts2 中的输入验证
    有两种实现方式:手工编写代码实现,与基于 XML 配置方式实现。

    6.1 手工编写代码实现数据验证

  • 在Struts2中,验证代码是在Action中完成的。由于数据的使用者是Action方法,所以验证过程需要在Action方法执行之前进行。验证分为两类:对Action中所有方法执行前的验证;对Action中指定方法执行前的验证。
  • 以下程序举例,均根据如下的需求来做:
  • 登录表单中提供用户名与手机号两个输入。要求,用户名和手机号不能为空,并且手机号要符合手机号码的格式:以1开头,后跟3/4/5或者8,最后9位数字。验证就是要验证用户名和手机号。

    6.1.1 对Action中所有方法执行前的验证

  • 首先,需要进行数据验证的Action类要继承自ActionSupport类。然后,重写validate()方法。Action中所有Action方法在执行之前,validate()方法均会被调用,以实现对数据的验证。
  • 当某个数据验证失败时,Struts2会调用addFieldError()方法向系统的fieldErrors集合中添加验证失败信息。如果系统的fieldErrors集合中包含失败信息,struts2会将请求转发到名为input的Result。在input视图中可以通过<s:fielderror/>显示失败信息。
  • ActionSupport类的addFieldError()方法含有两个参数:第一个参数必须为该Action的属性名的字符串,用于指定验证出错的属性名,即数据。第二个参数为字符串,是错误提示信息。
    SSH框架之struts2专题3:Struts2核心
  • 举例:validate1
    1、在index.jsp中使用绝对路径,否则在input转向时会出错:
  • 注意,这里使用基础路径将相对路径自动变为绝对路径,基础路径会自动加在本页面中所有的相对路径之前。将相对路径变为绝对路径。而基础路径的定义,在任何一个新建JSP文件中均有。
    SSH框架之struts2专题3:Struts2核心
    2、定义Action类:
    SSH框架之struts2专题3:Struts2核心
    3、对当前Ation中所有的方法被调用前均执行该验证方法:
    SSH框架之struts2专题3:Struts2核心
    4、如果系统的fieldErrors集合中包含失败信息,将请求转发到名为input的Result:
    SSH框架之struts2专题3:Struts2核心

    6.1.2 对Action中指定方法执行前的验证

  • 通过在Action中定义public void validateXxx()方法来实现。validateXxx()方法只会验证action中方法名为xxx的方法。其中Xxx的第一个字母要大写。
  • 当数据验证失败时,调用addFieldError()方法,也同样会向系统的fieldErrors集合中添加验证失败信息。
  • 举例:validate2
    SSH框架之struts2专题3:Struts2核心

    6.2 基于XML配置方式实现输入数据验证

  • 使用 XML 配置方式实现输入数据的验证,Action 类仍需要继承自 ActionSupport 类。验证仍分为两类:对 Action 中所有方法执行前的验证;对 Action 中指定方法执行前的验证。
  • 该验证方式需要使用 XML 配置文件,该配置文件的约束,即文件头部,在xwork-core-2.3.24.jar 中的根下的 xwork-validatior-1.0.3.dtd 中可以找到。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
  • 验证配置文件的格式、内容如下:
    SSH框架之struts2专题3:Struts2核心
  • 验证器是由系统提供的,系统已经定义好了 16 种验证器。这些验证器的定义可以在
    xwork-core-2.3.24.jar 中的最后一个包 com.opensymphony.xwork2.validator.validators 下的
    default.xml 中查看到。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    6.2.1 对Action中所有方法执行前的验证

  • 在Action类所在的包中放入一个XML配置文件,该文件的取名应遵守ActionClassName-validation.xml规则。其中ActionClassName为Action的简单类名,-validation为固定写法。例如,Action类为com.abc.actions.UserAction,那么该文件的取名应为:UserAction-validation.xml。
  • 举例:validate3
  • 拷贝validate1,将Action中验证方法删除,并在Action所在包中添加验证配置文件。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    6.2.2 对Action中指定方法执行前的验证

  • 由于在struts.xml中,一个<action>标签一般情况下对应一个action方法的执行。所以,若要对Action类中指定方法进行执行前的验证,则需要按如下规则命名配置文件:ActionClassName-ActionName-validation.xml。其中ActionName指的是struts.xml中<action>标签的name属性值。
  • 当然,该配置文件也是放在该Action的同一个包中的。
  • 举例,validate4(拷贝validate3,在其基础上修改)
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    6.2.3 常用验证器用法

    1、required:非空(必填)验证器

    <field-validator type="required">
    <message>性别不能为空!</message>
    </field-validator>

    2、requiredstring:非空字符串验证器

    <field-validator type="requiredstring">
    <param name="trim">true</param>
    <message>用户名不能为空!</message>
    </field-validator>

    3、fieldexpression:字段表达式关系判断

    <field-validator type="fieldexpression">
    <param name=“expression”>pwd == repwd</param>
    <message>确认密码与密码不一致</message>
    </field-validator>
  • 注意,假设pwd与repwd是表单中的两个元素的name属性值,且表达式就是pwd==repwd,而非pwd!=repwd。
    4、stringlength:字符串长度验证器
    <field-validator type="stringlength">
    <param name="minLength">2</param>
    <param name="maxLength">10</param>
    <param name="trim">true</param>
    <message>产品名称应在${minLength}-${maxLength}个字符之间</message>
    </field-validator>

    5、int:整数范围校验器

    <field-validator type="int">
    <param name="min">1</param>
    <param name="max">150</param>
    <message>年龄必须在 1-150 之间</message>
    </field-validator>
  • 注意,int、long、short、double与date校验器均继承自RangeValidatorSupport<T>类,是范围校验器,即不对数据类型进行验证,只对其有效范围进行校验。它们均由两个参数T min 和T max。
    6、email:邮件地址校验器
    <field-validator type="email">
    <message>电子邮件地址无效</message>
    </field-validator>

    7、regex:正则表达式校验器

    <field-validator type="regex">
    <param name="regexExpression"><![CDATA[^1[3458]\d{9}$]]></param>
    <message>手机号格式不正确!</message>
    </field-validator>
  • 注:<![CDATA[……]]>称为 cData 区,用于存放特殊表达式。

    6.2.4 输入验证的执行流程

  • 若以上四种输入验证方式均进行了设置,则其执行顺序如下:
    1、首先执行基于XML的验证。系统按照下面顺序寻找校验文件:ActonClassName-validation.xml --> ActionClassName-ActionName-validation.xml
  • 当系统寻找到第一个校验文件后,会继续搜索后面的校验文件。当搜索到所有校验文件后,会把校验文件中的所有校验规则汇总,然后全部应用于处理方法的校验。如果两个校验文件中指定的校验规则冲突,则使用指定方法的校验文件的校验规则。
    2、执行Action中的validateXxx()方法。
    3、执行Action中的validate()方法。
    4、经过上面的执行,如果系统中的fieldErrors存在异常信息(即存放异常信息的集合的size大于0),系统自动将请求转发至名称为input的视图。如果系统中的fieldErrors没有任何异常信息。系统将执行action中的处理方法。

    6.2.5 Action类的执行原理以及顺序

    1、类型转换:类型转换失败是在Action调用相应属性的set方法之前发生的,类型转换失败,不影响程序的运行。
    2、set方法:无论类型转换是否成功,都将执行该属性的set方法。只不过,类型转换失败,会设置该属性值为null。
    3、数据验证:若对于类型转换失败的数据,程序中存在为null的验证,则会在向fieldErrors集合中加入类型转换异常信息的同时,将该属性为null的验证信息也加入fieldErrors集合。
    4、Action方法:只有当fieldErrors集合的size为0,即没有异常信息时,才会执行Action方法

    7 拦截器

  • 拦截器是Struts2的一个重要特性。因为Struts2的大多数核心功能都是通过拦截器实现的。拦截器之所以称之为“拦截器”,是因为它可以在执行Action方法之前或者之后拦截下用户请求,执行一些操作。即在Action方法执行之前或者之后执行,以增强Action方法的功能。
  • 例如,一般情况下,用户在打开某个页面之前,需要先登录,否则是无法对资源进行访问的。这就是权限拦截器。
  • Struts2内置了很多拦截器,每个拦截器完成相对独立的功能,多个拦截器的组合体称之为拦截器栈。最为重要的拦截器栈是系统默认的拦截器栈DefaultStack。
  • 拦截器定义在 struts2-core-2.3.24.jar!struts-default.xml中。
拦截器功能描述
exception将action抛出的异常映射到结果,这样就通过重定向来自动处理异常,一般情况下,应该为最后一个拦截器
chain允许当前action能够使用上一个被执行action的属性,这个拦截器通常要和“chain”结果类型一起使用(<result type="chain" .../>)
conversionError将转换错误的信息(包括转换的字符串和参数类型等)存放到action的字段
debugging当使用Struts2的开发模式时,此拦截器会提供更多的调试信息
fileUpload此拦截器主要用于文件上传,它负责解析表单中文件域的内容
i18n这是支持国际化的拦截器,它负责把所选的语言、区域放入用户Session中
modelDriven模型驱动拦截器,当某个Action类实现了ModelDriven接口时,它负责把getModel()方法的结果放入ValueStack中
params负责解析HTTP请求中的参数,并将参数值设置成Action对应的属性值
servletConfig如果某个Action需要直接访问ServletAPI,就是通过这个拦截器实现,它提供访问HttpServletRequest和HttpServletResponse的方法,以map的方式访问
validation通过执行在xxxAction-validation.xml中定义的校验器,从而完成数据校验
workflow为action定义默认的工作流,一般跟在validation等其他拦截器后,当验证失败时,不执行action直接重定向到INPUT视图
  • 对workflow拦截器源码分析:
  • 默认返回视图为Action.INPUT,即"input"
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心

    7.1 普通拦截器定义

  • 通常情况下,自定义一个普通的拦截器类需要实现拦截器接口INterceptor。该接口中定义了三个方法:
  • public void init():拦截器实例被创建之前被调用。
  • public void destroy():拦截器实例被销毁之后被调用。
  • public String intercept(ActionInvocation invocation) throws Exception:该方法在Action执行之前被调用,拦截器的附加功能在该方法中实现。执行参数invocation的invoke()方法,就是调用Action方法。
    SSH框架之struts2专题3:Struts2核心

    7.2 拦截器注册

  • 拦截器类在定义好之后,需要在struts.xml配置文件中注册,以通知Struts框架。其注册方式有以下几种,但是无论哪种,都需要引入Struts2默认的拦截器栈defaultStack。

    7.2.1 拦截器栈方式注册

    SSH框架之struts2专题3:Struts2核心

    7.2.2 拦截器方式注册

    SSH框架之struts2专题3:Struts2核心

    7.2.3 默认拦截器栈方式注册

  • 若该<package>中的所有Action均要使用该拦截器,则可指定默认的拦截器栈。不过,每个包只能指定一个默认拦截器。但是,若为该包中的某个action显式地指定了某个拦截器,则默认拦截器将不起作用。
    SSH框架之struts2专题3:Struts2核心

    7.3 权限拦截器举例

  • 只有经过登录的用户才可以访问Action中的方法,否则,将返回“无权访问”提示。
  • 本例的登录,由一个JSP页面完成,即在该页面里将用户信息放入到session中,也就是说,只要访问过该页面,就说明登录了;没有访问过,则为未登录用户。
  • 项目:permission_intercepter

    7.3.1 项目创建

    1、定义login.jsp

    <body>
    <% session.setAttribute("user", "beijing"); %>
    登录成功!
    </body>

    2、定义logout.jsp

    <!-- 模拟用户退出 -->
    <%
        session.removeAttribute("user");
    %>
    用户退出系统!

    3、定义Action

    package com.eason.actions;
    import com.opensymphony.xwork2.ActionContext;
    public class SomeAction {
    public String execute() {
        //能够执行到Action,说明已经通过了拦截器验证,用户身份合法
        ActionContext.getContext().getSession().put("message", "欢迎登录");
        return "success";
    }
    }

    4、定义拦截器

    package com.eason.interceptor;
    import org.eclipse.jdt.internal.compiler.ast.Invocation;
    import com.opensymphony.xwork2.ActionContext;
    import com.opensymphony.xwork2.ActionInvocation;
    import com.opensymphony.xwork2.interceptor.Interceptor;
    
    public class PermissionInterceptor implements Interceptor {
        public void destroy() {
        }
        public void init() {
        }
    
        @Override
        public String intercept(ActionInvocation invocation) throws Exception {
            String user = (String) ActionContext.getContext().getSession().get("user");
            if("beijing".equals(user)) {
                return invocation.invoke();
            }
            ActionContext.getContext().getSession().put("message", "您未登录");
            return "message";
        }
    }

    5、配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
    "http://struts.apache.org/dtds/struts-2.3.dtd">
    <struts>
    <package name="demo" namespace="/test" extends="struts-default">
        <interceptors>
            <interceptor name="permission" class="com.eason.interceptor.PermissionInterceptor"></interceptor>
            <interceptor-stack name="permissionStack">
                <interceptor-ref name="permission"></interceptor-ref>
                <interceptor-ref name="defaultStack"></interceptor-ref>
            </interceptor-stack>
        </interceptors>
    
        <action name="some" class="com.eason.actions.SomeAction">
            <interceptor-ref name="permissionStack"></interceptor-ref>
            <result>/success.jsp</result>
            <result name="message">/message.jsp</result>
        </action>
    </package>
    </struts>

    6、定义success.jsp

    <body>
    进入系统!
    </body>

    7、定义message.jsp

    <body>
    message = ${message }
    </body>

    7.3.2 项目测试

    1、在地址栏上直接提交some.action请求:
    SSH框架之struts2专题3:Struts2核心
    2、访问 login.jsp,进行用户登录:
    SSH框架之struts2专题3:Struts2核心
    3、再次提交 some.action 请求:
    SSH框架之struts2专题3:Struts2核心
    4、访问 logout.jsp,进行用户退出:
    SSH框架之struts2专题3:Struts2核心
    5、第三次提交 some.action 请求:
    SSH框架之struts2专题3:Struts2核心

    7.4 方法过滤拦截器

  • 直接实现Interceptor接口的过滤器存在一个问题:当将拦截器注册为<package/>中默认的拦截器栈<default-interceptor-ref/>时,会对该<package/>中所有<action/>指定的method进行拦截。此时,可使用Interceptor接口的实现类MethodFilterInterceptor--方法过滤拦截器对指定方法进行过滤。
  • 自定义方法过滤器,可继承MethodFilterInterceptor实现类,重写其方法doInterceptor()。workflow拦截器就是继承自该实现类:
    SSH框架之struts2专题3:Struts2核心
  • 项目:permission_intercepter2
    1、定义Action:
    SSH框架之struts2专题3:Struts2核心
    2、定义拦截器类
    SSH框架之struts2专题3:Struts2核心
    3、修改配置文件
    SSH框架之struts2专题3:Struts2核心
  • MethodFilterInterceptor拦截器有两个参数可进行设置,用于指定或者排除要拦截的方法名。这两个属性不能同时进行设置:
    SSH框架之struts2专题3:Struts2核心

    8 国际化

    8.1 国际化基础

    8.1.1 获取系统支持的语言和国家简称

  • 进行国际化编程和调试,需要了解系统支持的语言和国家简称,以及浏览器切换语言的位置。一般都是在浏览器的“工具 -> Internect 选项 ->语言"中可查看到。下面以360浏览器为例查看。
    SSH框架之struts2专题3:Struts2核心
  • 另外,在Java代码中也可以通过java.util.locale类中的静态方法获取Java支持的语言和国家简称。
    SSH框架之struts2专题3:Struts2核心

    8.1.2 Struts2的i18n拦截器

  • Struts2的默认拦截器中包含了i18n拦截器。而i18n拦截器中包含了一个属性request_locale,专门用于设置浏览器的语言。
    SSH框架之struts2专题3:Struts2核心
  • 通过设置i18n拦截器的request_locale属性,可对被拦截Action所要转向页面的浏览器的语言进行设置。
    SSH框架之struts2专题3:Struts2核心

    8.2 全局范围资源文件

  • 所谓全局范围资源文件是指,整个应用中的所有文件均可访问的资源文件。其命名要遵循以下格式:baseName_language_country.properties
  • 其中baseName是资源文件的基本名,我们可以自定义。但是language和country必须是Java支持的语言和国家简称。例如, login_en_US.properties。
  • 对于全局资源文件,需要注意以下几点:
    1、国家简称必须为大写;2、对于同一内容进行解释的资源文件,其基本名必须相同;3、资源文件需要在struts.xml中注册,注册其位置与基本名。

    8.2.1 JSP 中普通文本的国际化

  • 在 JSP 页面中,普通文本使用 Struts2 标签<s:text name=“”/>输出国际化信息。其中 name的值为资源文件中指定的 key。如:<s:text name=“formhead”/>将会在此显示“登录表单”字样。

    8.2.2 JSP 中表单元素的国际化

  • 在 JSP 页面中,使用 Struts2 表单元素标签,通过 key 属性指定属性文件中的 key,如:<s:textfield name=“userName” key=“uname”/>,标签名称将显示“用户名”。<s:submit key=”submit”/>,提交按键上将显示“登录”。
    SSH框架之struts2专题3:Struts2核心

    8.2.3 Action类中文本的国际化

  • Action类中文本的国际化,可以使用ActionSupport类的getText()方法,该方法的参数用于指定属性文件中的key。如,String message = this.getText("message");,此时message的实际值为“登录成功“字符串。所以,Action类要继承ActionSupport类。

    8.2.4 资源文件中的占位符

  • 资源文件的value值中也可以包含动态参数,即在程序运行时才确定资源文件中value的值。此时,资源文件的value值中的动态参数将以占位符的形式出现,如{0}、{1}等。
  • 若在JSP页面中,参数的值可通过为<s:text/>添加<s:param/>来设置。
    SSH框架之struts2专题3:Struts2核心
  • 若在Action类中,则通过ActionSupport类的带两个参数的getText()进行赋值。要求第二个参数为String[]。
    SSH框架之struts2专题3:Struts2核心

    8.3 包范围资源文件

  • 在一个大型应用中,整个应用有大量的内容需要实现国际化。若均放入全局资源文件中,则会使得全局资源文件过于庞大,所以可以针对不同模块、不同的action来组织国际化文件。
  • 在要使用该资源文件的java的包下,放置按如下格式命名的资源文件:package_language_country.properties。
  • 其中,package为固定写法。处于该包以及子包下的所有action都可以访问该资源。当查找指定key的消息时,系统会先从package资源文件查找,当找不到对应的key时,才会从全局资源文件中寻找。即包范围资源文件的优先级高于全局资源文件。
  • 注意,非经Action跳转而至的JSP页面中读取的是全局资源文件中的内容。而经由Action跳转而来的JSP页面,其读取的为跳转而来的Action所在包的资源文件。

    8.4 Action范围资源文件

  • 可以单独为某个action指定资源文件。只需要在Action类所在的包中放置命名格式如下的资源文件:ActionClassName_language_country.properties。
  • 其中ActionClassName为action类的简单名称。当查找指定key的消息时,系统会先从action范围资源文件查找,如果没有找到对应的key,接着会沿当前包往上查基本名为package的资源文件,一直找到最顶层包。如果还没有找到对应的key,最后会从全局资源文件中寻找。

    8.5 JSP中访问指定资源文件

  • 由于JSP页面所访问资源文件不是由JSP页面本身决定看,要么默认访问全局资源文件,要么访问其跳转来的Action中的资源文件。而这将引发项目在进行分工协作时的麻烦。Struts2的<s:i18n/>标签可以让JSP访问指定资源文件。
  • <s:i18n/>具有一个name属性,用于指定所要访问资源文件的路径以及基本名。
    SSH框架之struts2专题3:Struts2核心

    9 文件上传

  • Struts2是通过拦截器实现文件上传的,而默认拦截器中包含了文件上传拦截器,故表单通过Struts2可直接将文件上传。其底层是通过apache的commons-fileupload完成的。
    SSH框架之struts2专题3:Struts2核心
    SSH框架之struts2专题3:Struts2核心
  • 若要实现文件上传功能,表单的enctype属性值与method属性值必须要如下设置:
    SSH框架之struts2专题3:Struts2核心

    9.1 上传单个文件

  • 举例:fileUploadSingle
    1、新建index.jsp:

    <form action="test/upload.action" method="post" enctype="multipart/form-data">
        文件:<input type="file" name="img"/><br/>
        <input type="submit" value="上传"/>
    </form>

    2、新建Action类

    package com.eason.struts.fileupload;
    
    import java.io.File;
    import java.io.IOException;
    import org.apache.commons.io.FileUtils;
    import org.apache.struts2.ServletActionContext;
    
    public class UploadAction {
        private File img;
        private String imgFileName;
    
        public File getImg() {
            return img;
        }
        public void setImg(File img) {
            this.img = img;
        }
        public String getImgFileName() {
            return imgFileName;
        }
        public void setImgFileName(String imgFileName) {
            this.imgFileName = imgFileName;
        }
    
        public String execute() throws IOException {
            if(img != null) {
                String path = ServletActionContext.getServletContext().getRealPath("/images");
                File file = new File(path, imgFileName);
                FileUtils.copyFile(img, file);
                return "success";
            }
            return "fail";
        }
    }

    3、struts.xml中的配置:

    <package name="demo" namespace="/test" extends="struts-default">
        <action name="upload" class="com.eason.struts.fileupload.UploadAction">
            <result>/success.jsp</result>
            <result name="fail">/fail.jsp</result>                        
        </action>
    </package>

    4、定义success.jsp和fail.jsp:

    <body>
    上传成功!
    </body>
    <body>
    上传失败!
    </body>
  • <input type="file" name="img"/>中的name属性值为img,所以Action类中必须定义File img;和String imgFileName;两属性用以接收文件和文件名。
  • 注意,在Action中想通过获取文件大小来控制文件上传是不行的。因为文件上传拦截器是在Action之前执行的,即执行到Action时,文件上传工作已经完成。即使超过限制大小,抛出异常,也是已经抛出异常后才执行到Action的。

    9.2 上传多个文件

  • 与上传单个文件相比较,发生了如下几个变化:
    1、提交表单中出现多个文件上传栏,这多个的name属性名必须完全相同。
    2、Action中文件不再为File类型,而是File类型的数组或者是List。当然,文件名也为相应的数组或者是List。
    3、Action方法需要遍历这些数组来上传这些文件。
  • 举例:fileuploadMultiple
  • 拷贝fileuploadSingle,只需要修改上传页面以及Action即可。
    1、修改上传页面:

    <form action="test/upload.action" method="post" enctype="multipart/form-data">
        文件1:<input type="file" name="img"/><br/>
        文件2:<input type="file" name="img"/><br/>
        文件3:<input type="file" name="img"/><br/>
        <input type="submit" value="上传"/>
    </form>

    2、修改 Action:

    package com.eason.struts.fileupload;
    import java.io.File;
    import java.io.IOException;
    import org.apache.commons.io.FileUtils;
    import org.apache.struts2.ServletActionContext;
    
    public class UploadAction {
        private File[] imgs;
        private String[] imgsFileName;
    
        public File[] getImgs() {
            return imgs;
        }
        public void setImgs(File[] imgs) {
            this.imgs = imgs;
        }
        public String[] getImgsFileName() {
            return imgsFileName;
        }
        public void setImgsFileName(String[] imgsFileName) {
            this.imgsFileName = imgsFileName;
        }
    
        public String execute() throws IOException {
            if(imgs != null) {
                for(int i = 0; i < imgs.length; i++) {
                    String path = ServletActionContext.getServletContext().getRealPath("/images");
                    File file = new File(path, imgsFileName[i]);
                    FileUtils.copyFile(imgs[i], file);
                }
                return "success";
            }
            return "fail";
        }
    }

    9.3 设置上传文件大小的最高限

  • Struts2默认上传文件的大小是不能超过2M的。若要想上传大于2M的内容,则需要Struts.xml中增加对上传最大值的常量设置。这是当前系统的上传文件大小的最高限。
    SSH框架之struts2专题3:Struts2核心

    9.4 限制上传文件的扩展名

  • 查看文件上传拦截器FileUploadInterceptor源码,可以看到其有一个allowedExtensions属性,该属性可用于限制上传文件的扩展名。
    SSH框架之struts2专题3:Struts2核心
  • 查看setAllowedExtendsions()方法的源码,其调用了方法commaDelimitedStringToSet()即逗号分隔字符串到Set集合方法,用于解析出使用逗号分隔的多个扩展名。
    SSH框架之struts2专题3:Struts2核心
  • allowedExtensions属性的用法如下所示:
    SSH框架之struts2专题3:Struts2核心

    10 文件下载

  • 服务端向客户端浏览器发送文件时,如果是浏览器支持的文件类型,一般会默认使用浏览器打开,比如txt,jpg等,会直接在浏览器中显示;如果需要用户以附件的形式保存,则称之为文件下载。
  • 如果需要向浏览器提供文件下载功能,则需要设置HTTP响应头的Content-Disposition属性,即内容配置属性值为attachment(附件)。
  • Action类中需要提供两个属性,一个为文件输入流,用于指定服务器向客户端所提供下载的文件资源;一个为文件名,即用户要下载的资源文件名。配置文件中Action的result类型,即type属性应该设置为stream。
  • 举例:download
    1、在页面提供文件下载链接,即用户在浏览器上提交文件下载请求的位置:

    <body>
    <a href="test/download.action">美图下载</a>
    </body>

    2、定义Aciton类:

    package com.eason.struts2.action;
    import java.io.InputStream;
    import org.apache.struts2.ServletActionContext;
    public class DownloadAction {
        //服务器本地提供下载资源的输入流
        private InputStream is;
        //为用户所提供的下载资源的文件名
        private String fileName;
    
        public void setIs(InputStream is) {
            this.is = is;
        }
        public void setFileName(String fileName) {
            this.fileName = fileName;
        }
        public InputStream getIs() {
            return is;
        }
        public String getFileName() {
            return fileName;
        }
    
        public String execute() {
            //指定要下载的文件名
            fileName = "timg.jpg";
            is = ServletActionContext.getServletContext().getResourceAsStream("/images/" + fileName);
            //在输入流后修改fileName的值,即为用户下载到客户端后的文件名
            fileName = "beauty.jpg";
            return "success";
        }   
    }

    3、设 置 action 的 视 图 类 型 为 stream , 并 为 其 设 置 两 个 参 数 inputName 与
    contentDisposition:

    <package name="demo" namespace="/test" extends="struts-default">
        <action name="download" class="com.eason.struts2.action.DownloadAction">
            <result type="stream">
                <param name="inputName">is</param>
                <param name="contentDisposition">
                    attachment;filename=${fileName}
                </param>
            </result>
        </action>
    </package>
  • 查看struts-default.xml,可以看到Stream返回类型对应的类为 StreamResult。
    SSH框架之struts2专题3:Struts2核心
  • 查看StreamResult源码,相关属性如下:
    SSH框架之struts2专题3:Struts2核心
  • DEFAULT_PARAM:配置文件中<param/>标签name属性的默认值。
  • contentType:文件的MIME类型,如image/jpeg。无论下载的文件是什么类型,客户端看到的均会是指定类型文件的扩展名,如均是jpg。
  • contentLength:对服务器提供的下载文件大小的上限的限制。单位字节。当下载文件大于此大小时,下载仍会继承。但是,下载不全,只要规定的大小。
  • contentDisposition:下载文件的显示形式。默认为“inline”,即在浏览器上直接显示。若要以附件形式展示给客户端,则其值需要设置为attachment,并通过filename指定其下载后的名称。
  • inputName:输入流的名称,默认为inputStream。若Action中的inputStream对象的名字为inputStream,此时,<param name=”inputName”>inputStream</param>省略不写也可。因为DEFAULT_PARAM指定了默认的param为inputName,而默认的InputStream对象又为inputStream。省略不写,则均会使用默认值。
  • bufferSize:提供下载是可以使用的缓存大小。
  • allowCaching:提供下载服务时是否允许使用缓存。
    4、此时程序部署后就可以完成向客户端提供下载功能,但是有个问题,当指定文件下载到客户端的名称为中文时,浏览器下载会出现文件名称的乱码。解决方式如下:
    public String execute() throws Exception {
        //指定要下载的文件名
        fileName = "timg.jpg";
        is = ServletActionContext.getServletContext().getResourceAsStream("/images/" + fileName);
        //在输入流后修改fileName的值,即为用户下载到客户端后的文件名
        fileName = "美女.jpg";
        fileName = new String(fileName.getBytes("utf-8"), "ISO8859-1");
        return "success";
    }
  • 乱码是如何产生的?为什么使用new String(fileName.getBytes("utf-8"), "ISO8859-1")后,就解决乱码问题?
  • 此时的乱码之所以会产生,是因为Http header报头要求其内容必须为ISO8859-1编码,而ISO8859-1编码不支持汉字。
  • 使用以上方式之所以可以解决乱码问题,是因为:
    1、fileName.getBytes("utf-8)的作用是,将fileName按照utf-8进行编码,并将编码结果存放到字节数组中。utf-8编码中文后为3个字节。
    2、new String(fileName.getBytes("utf-8"), "ISO8859-1")的作用是,将编码后的3个字节解码为ISO8859-1串,即类似%3A56%59DC这样的串。而这样的串正是浏览器所需要的,Httpheader报头中数据的编码集为ISO8859-1。当数据传到浏览器端后,浏览器会自动将数据按照浏览器的字符集进行编码,若浏览器的为utf-8,则会正常显示汉字。

    11 防止表单重复提交

  • 在实际应用中,由于网速等原因,用户在提交过表单后,可能很长时间内服务器未给出任何响应。此时,用户就可能会重复对表单进行提交。
  • 或者是表单提交后,服务器也给出了响应,但用户在响应页面不断点击“刷新”按钮进行页面刷新。此时,用户也是在对表单进行重复提交。

    11.1 令牌机制

  • Struts2 中,使用令牌机制防止表单自动提交。
  • 所谓令牌机制是指,当用户提交页面资源请求后,服务器会产生一个随机的字符串,该字符串即为令牌。并为该字符串保留两个副本:一个保留在服务器,一个随着响应返回给客户端浏览器。
  • 当用户在页面中进行第一次提交时,会将副本字符串与服务器中的副本字符串进行比较。其比较结果一定是相同的,因为这是第一次提交。
  • 当比较结果相同后,服务器会将其保留的副本数据修改。但此修改并不通知(影响)浏
    览器端的原副本。
  • 当用户在页面中进行重复提交时,在发出的请求中仍然携带有原副本字符串,服务器仍然会将该副本与自己的令牌字符串进行比较。但服务器端的令牌字符串已经发生改变,所以比较结果一定不同。这就说明不是第一次提交该请求了,是重复提交了。

    11.2 防止表单重复提交

    1、使用<s:token />标签,该标签应放在<form/>表单中,以便在请求提交时,将令牌字符
    串一起提交。
    2、在定义 Action 时,要求 Action 必须继承 ActionSupport 类。以便在重复提交发生时会
    返回”invalide.token”字符串。
    3、Struts2 配置文件的<action/>中需要定义名称为”invalide.token”的视图。
    4、Struts2 配置文件的<action/>中需要使用 token 拦截器。

    11.3 防止重复提交的步骤

  • 举例:token
    1、在表单中加入<s:token/>
    <form action="test/login.action" method="post">
        <s:token/>
        用户:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/><br/>
        <input type="submit" value="登录">
    </form>
  • 当服务器发现用户提交的页面访问请求,所请求的页面中包含<s:token/>标签,则会生
    成令牌字符串,并为令牌创建两个副本。一个放在服务器端,一个在给出的对用户页面请求响应的页面,以隐藏域的方式发送给客户端浏览器。
    SSH框架之struts2专题3:Struts2核心
    2、Action类要继承自ActionSupport类

    package com.eason.struts.action;
    import com.opensymphony.xwork2.ActionSupport;
    public class LoginAction extends ActionSupport{
    private String username;
    private String password;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    
    public String execute() {
        return "success";
    }
    }

    3、在action配置文件中加入token拦截器以及重复提交视图配置。注意,在加入token拦截器之前,不要忘记先要将核心拦截器栈加入,否则,Struts2的核心功能将无法使用。

    <package name="demo" namespace="/test" extends="struts-default">
        <action name="login" class="com.eason.struts.action.LoginAction">
            <interceptor-ref name="token"></interceptor-ref>
            <interceptor-ref name="defaultStack"></interceptor-ref>
            <result>/success.jsp</result>
            <result name="invalid.token">/message.jsp</result>
        </action>
    </package>

    4、定义成功页面和重复提交提示页面。

    <body>
    提交成功!
    </body>
    <body>
    发生了重复提交!
    </body>

转载于:https://blog.51cto.com/12402717/2058726

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值