Struts2从上到下的详解


Struts 是一个 MVC 框架,它的核心是 拦截器 和 值栈:

        a:每个拦截器实现单一功能,拦截器的组合实现了整个流程处理。

        b:值栈承载数据,通过值栈统一了数据的处理逻辑,简单而且高效。

创建 struts 项目的大致顺序如下:

    1、创建工程,引入 jar 包

                org.apache.struts:struts2-core:+

    2、配置 web.xml,拦截所有请求

                

                    abc

                   StrutsPrepareAndExecuteFilter

               

             

                  abc

                  /*

             

3、配置 struts.xml, 实现 action,实现 jsp (MVC)

    C:

      

     

          

                /emplist.jsp

          

        

    M:

        public class EmpAction extends ActionSpport {

                private String name;

                private List emps = new ArrayList<>();

                public String emplist() {

                emps = empDAO.findByName(name);

                return SUCCESS;

                 }

            }

       V:

        

                  

                   

  • ${s.index}: ${e.name} | ${e.salary}
  •               

            

4、部署,启动,看效果。

流程


1、请求发送给 StrutsPrepareAndExecuteFilter

2、StrutsPrepareAndExecuteFilter 询问 ActionMapper:请求是否 Action(是的话返回非空的 ActionMapping)

3、如果上一步确定请求时 Action,则把请求交给 ActionProxy 处理。 ActionProxy 是 xwork 和 struts 的连接层。

4、ActionProxy 通过 ConfigurationManager 加载配置文件,确定相关 Action 类和方法。

5、ActionProxy 创建一个 ActionInvocation 实例并初始化。

6、ActionInvocation 负责调用 Action,在调用的前后,需要执行 Interceptor 链(默认是 defaultStack)。在调用完 Action 后要执行 result 的结果。

7、把结果发送到客户端。

defaultStack


exception - 异常处理,包在最外面

servletConfig - 处理 xxxAware 接口

i18n - 处理国际化

prepare - 如果实现了 preparable 接口,则寻找并执行 pepareXxx/repareDoXxx

chain - 如果 type 是 chain 则复制值栈

scopedModelDriven - 在 request/session 范围内查找并初始化 model

modelDriven - 调用 getModel 方法,初始化 model 并压栈

fileUpload - 处理文件上传(MultiPartRequestWrapper 请求)

checkbox - 将隐藏域的 checkbox 赋值为 false

datetime - 格式化 text 域中的时间

multiselect - 为 __multiselect_ 赋值 null

staticParams - 把配置中的静态参数填装到 Action 中

actionMappingParams - 把 actionMapping 里的参数压栈

params - 封装请求参数到值栈

conversionError - 处理转型错误

validation - 进行编程验证

workflow - 处理验证错误,跳转 input 页面

debugging - 处理 devMode 等

StrutsPrepareAndExecuteFilter


如果需要用到其他过滤器,为了不影响 Struts 功能,需要把 StrutsPrepareAndExecuteFilter 拆分,再将自己的过滤器插入中间:

            StrutsPrepareAndExecuteFilter = StrutsPrepareFilter + StrutsExecuteFilter

比如,如果使用 SiteMesh 进行页面装饰:

        

               StrutsPrepareFilter

               /*

        

        

               sitemesh

               /*

        

        

                   StrutsExecuteFilter

                   /*

         

值栈


值栈是 struts 中数据传递处理的核心,它的基础是 OGNL,是一种 EL 表达式。

OGNL


OGNL 是 Struts2 中值栈的基础:

    1、三要素: Expression, Root, Context.

    2、核心: getValue.. setValue..

As:

// Prepare Data

Person p1 = new Person("sharry");

Person p2 = new Person("shatom");

Person p3 = new Person("shahat");

/* Get Value from Single Object */

String name = Ognl.getValue("name", p1);

String name = Ognl.getValue("name", p1, String.class);

/* Multiple Objects, with a Map Container */

Map context = new HashMap<>();

context.put("req", p1);

context.put("ses", p2);

context.put("app", p3);

// expression

Ognl.getValue("name", p2);

Ognl.getValue("name", context, p2);

Ognl.getValue("#app.name", context, p2);

/* Map -> OgnlContext */

OgnlContext context = new OgnlContext();

context.put("req", p1);

context.put("ses", p2);

context.put("app", p3);

context.setRoot(p1);

// params: [expression, context, root]

Ognl.getValue("name.length()", context, context.getRoot());          // default, from root

Ognl.getValue("#app.name.toUpperCase()", context, context.getRoot()); // from #app

Ognl.getValue("@java.lang.Math@E", context, context.getRoot());      // static method invoke.

// with '$()' method, anything can be easier:

public Object $(String exp) { return Ognl.getValue(exp, context, context.getRoot()); }

$("name");

$("#ses.name");

$("#app.name.toUpperCase()");

$("@java.lang.Math@E");

/* Operate on Collection */

// Make list/map

$("{111, 222, 333, 444}");

$("#{aaa: aaa, bbb: bbb}");

// Get Value

$("#tom.address['city']");

// 投影集合:collection.{expression}

$("friends.{name}");

// 过滤集合:collection.{?/^/$ expression}

$("friends.{? #name.length() > 7}");

ActionConext/ValueStack


每次 action 调用都会创建一个运行环境 ActionContext。它保存在 ThreadLocal 中,线程安全。

ActionContext 的主体是一个 Map 结构:

publicclassActionContext{

                privateMapcontext;

}

在预处理过程,=ActionContext#context= 里会被放入 request/session/application/ValueStack/etc,它本质是个以 ValueStack 为 root 的 =OgnlContext=,是 struts 运行过程中的数据中心。

OgnlContext (ActionContext#context)

  +--- attr

  +--- request

  +--- CompoundRoot (ValueStack, ArryList with pop/push)

  +--- session

  +--- others

CompoundRoot(ValueStack) 是个堆栈结构,最先被压入的是 Action 的实例,实例属性将会在后面的 params 拦截器中被赋值。它的物理位置是:

        request.getAttribute("struts.valueStack")

在 jsp 中,可以通过 struts 提供的标签使用值栈中的数据:

        

        ...

        ${salary} // 因为 struts 重写了 Request#getAttribute 方法,所以 ${salary} 会先从 request 里取,取不到再去值栈中取

         // 非 root 内的数据的获取

         // 属性的属性

        

         // 输出第一个拥有 message 属性对象的属性值

          // 从第二个开始搜索

ServletContext


继承自 ActionContext, 扩展了获取处理 Servlet 原生对象的一些方法。

取得HttpSession对象:

           HttpSession session = ServletActionContext. getRequest().getSession();

参数封装


请求参数


client

 

      

      

      

      

      

      

      

      

      

server

// for 1

String ename1;

Long deptno1;

public String empsave() {

        Emp e = new Emp(ename1, new Dept(deptno1));

        empDAO.save(e);

        return SUCCESS;

}

// for 2

Emp emp;

public String empsave() {

        empDAO.save(emp);

        return SUCCESS;

}

ModelDriven:

如果要把请求的参数封装到 Model 类中,最好使用 =ModelDriven=。只需要继承并实现 ModelDriven 接口即可:

public EmpAction implements ModelDriven {

        private Emp emp = new Emp();  // 可被各个 action 复用


        @Override Emp getModel() {

        return emp;

    }

    public String empsave() {

        empDAO.save(emp);

    }

    public String empdel() {

        empDAO.delete(emp.getId());

    }

}

[补充内容]

*比较特殊的是 update 操作*,参数的封装逻辑应该分为两步:

    1、先根据参数中的 id 从数据库中读取实体类

    2、再将其他请求参数覆盖到实体类

使用 ModelDriven 方式,我们需要这样定义 getModel 方法:

private Emp emp;

// 经过 ModelDriven 拦截器时从数据库中加载完整 emp

// 之后经过 Params 拦截器,再将请求参数覆盖其中

@Override Emp getModel() {

        emp = empDAO.findById(emp.getId());

        return emp;

}

可以看到,我们需要在 ModelDriven 拦截器前后分别执行一次 Params 拦截器,一次用于获取 Id,一次用于覆盖数据。 这就必须使用 paramsPrepareParamsStack 拦截器栈。

上述 getModel 定义会作用于所有 Action 请求,但对 save/delete 等请求是没必要的,因为他们不需要从数据库中再加载一次 emp。 所以需要区分,只为特定 action 请求加载 emp。这就需要用到 Prepare 拦截器。 使用 paramsPrepareParamsStack + Prepare 后,整个执行顺序为:        

         -> prepareDo -> prepare -> getModel -> -> action

All in All:

// 需要先配置使用 paramsPrepareParamsStack

// 再让 Action 实现 Prepareable 接口

private Emp emp;

void prepareUpdate() {

    emp = empDAO.findById(emp.getId());

}

Emp getModel() {

    if(emp == null)

        emp = new Emp();

    return emp;

}

当然,有时侯也没必要这么麻烦,为 update 请求多定义几个接收 property,再手动加载,手动赋值。即可。

响应数据


跟请求参数的处理是一致的,都是在 Action 中定义,随着 action 被压入值栈,就可以在 jsp 中使用能从值栈中获取数据的标签去获取并渲染数据了。

当然,也可以将数据放到 request/session 中。

获取 Request/Response 的方式有:

    ActionContext.getContext().getSession();

    ServletActionContext.getRequest();

    implements xxxAware

类型转换

html 提交的数据全都是字符串类型,所以在 server 端要转换为合适的 Java 类型

     1、struts 中,由 Parameters 拦截器负责转换,它是 defaultStack 中的一员

     2、Parameters 拦截器只能对 字符串->基本类型 进行转换。复杂转换需要自定义转换器:

                a:创建转换器,即实现 ognl.TypeConverter 接口。实际上继承 StrutsTypeConverter 即可。

                b:配置使用。基于字段(model/ModelClassName-conversion.properties)或基于类型(src/xwork-conversion.properties),添加:

                            java.util.Date=imfine.convert.DataConverter

   3、 如果转换失败,由 ConversionError 拦截器负责添加出错消息。

    4、如果存在转换或验证错误,由 Workflow 拦截器决定是否转到名为 input 的 result

    5、可添加 ActionName.properties#invalid.filedvalue.fieldName=xxx 定制错误信息。

    6、页面上中,错误信息可以通过下面方式显示:

            ${fieldErrors.age[0] }

              // 默认主题会生成 ul 列表

输入验证


验证是由 ValidationInterceptor 拦截器实现的。

验证分为两种:

    1、声明式验证,需要在action类的包下面创建一个验证使用的 xml 文件,里面定义我们要验证的内容。   

    2、编程式验证,为 Action 类实现 Validatable 接口,然后,实现 validate 方法。

声明式验证


比如,要为 LoginAction 做验证,需要在相同目录下面新建一个 LoginAction-validation.xml,内容类似下面:

    

    

 

 

   

      true

      请填写您的用户名

   

 

 

   

      true

      请填写您的密码

   

 

内建的验证有 15 中,可以参加文档。例:

1、required

2、requiredstring

3、stringlength

4、email

5、url

6、regex

7、int

8、conversion

9、expression/fieldexpression

例如,要验证数字范围:

  20

  20

 

如果要验证两次输入的密码是否不一致

 

  两次输入的密码不一致,请重试。

编程式验证


首先, Action 要实现 Validateable 接口。当然,ActionSupport 类是实现了这个接口的,所以如果我们也可以直接继承 ActionSupport 类。

其次,我们需要实现 validate() 方法。如果针对特定方法进行验证,我们需要实现相关的 validateMethodName() 方法。下面是一个栗子,对登录进行验证。要求

    1、用户名不为空

    2、密码不为空

/**

* 验证登录输入

*/

public void validateLogin() {

    if (username == null || username.isEmpty())

        addFieldError("username", "请填写用户名");

    if (password == null || password.isEmpty())

        addFieldError("password", "请填写密码");

}

最后,我们要为 action 写一个名字为 input 的 result。即如果验证失败后,显示哪个页面。如果不写 input,会抛出异常。

异常处理


声明式异常处理

    

也可以通过 global-exception-mappings 设置全局异常处理。

声明式异常处理由拦截器 ExceptionMappingInterceptor 处理。当出现异常时, ExceptionMappingInterceptor 会向 ValueStack 中添加两个对象:

        exception 表示被捕获异常的 Exception 对象

        exceptionStack 包含着被捕获异常的栈

所以可以通过 来显示异常信息。通过查看 ExceptionMappingInterceptor 源码,一清二楚。

在页面上显示:

        

        

${actionErrors[0]}

可以在 head 标签里使用 生成一些内置的错误样式。

渲染视图


Result type:

dispatcher, 转发到 jsp/html,默认类型

chain,转发到另一个 action

redirect, 重定向到 jsp/html

redirectAction, 重定向到另一个 action

plainText,返回文件内容,text/plain

freemarker/velocity, 转发到 freemarker/velocity 视图

stream, 处理二进制数据,比如上传下载,还可以处理 JSON 返回

json, 将对象序列化为 json 字符串并返回,需要 struts-json-plugin.jar 支持

i18n


处理国际化的是 i18n 拦截器。

使用资源文件的方式有:

    

    

    标签里的 key 属性

    验证文件中的

    Action 中的 getText() 方法

比如:

// xxx 按照原样输出

    // yyy 是值栈中对应名字的数据

          // zzz 表示从系统的资源文件(xxx.properties)加载数据

      // xyz 使用资源文件里的数据

struts 是按照下面顺序判断区域的:

    1、getParameter(“request_locale”)

    2、session.getAttribute(“WW_TRANS_I18N”)

    3、如果以上都没有取到的话,那么从系统中获取 (java.util.Locale.getDefault())

资源文件的搜索顺序:

    1、ActionClass.properties

    2、Interface.properties (every interface and sub-interface)

    3、BaseClass.properties (all the way to Object.properties)

    4、ModelDriven’s model (if implements ModelDriven), for the model object repeat from 1

    5、package.properties (of the directory where class is located and every parent directory all the way to the root directory)

    6、search up the i18n message key hierarchy itself

    7、global resource properties

                

标签


property/date


property 是最基本的标签,用来输出 ValueStack 中属性值,date 用来格式化日期

${#session.date}

url/param


url 用来创建一个 url 字符串,可以自动添加 ContextPath

// name.值栈中的属性

  // 'name'.字符串name

    // 生成的是action请求

使用 s:url 定义的 url

param 用于给它的父标签传递参数

默认会对 value 进行 ognl 求值

如果想使用字面字符串,用单引号括起来

也可以不使用 value 属性,而把值写在标签里面。这样可以传递一个 El 表达式的值。

set/push

set 用来向 page/request/session/application 中压入值

价格: ${requestScope.price}

push 用来临时将某些值压到 ValueStack 顶部,便于操作

    姓名: ${name}   

if/elif/else


高档

中档

低端

iterator/sort

iterator 遍历集合,把可遍历对象的每个元素依次压入弹出值栈

   

${s.index}: ${name}  -  ${age}

sort 对可遍历对象的元素排序

form/textfield/select/checkbox/radio


form 结合其他可以自动排版,自动回显。

radio/select/checkboxlist 等标签需要使用 list 属性提供数据。

   

   

拦截器

实现了 Interceptor 接口的类,叫拦截器。

在 Struts 中,是利用拦截器进行功能实现的,比如值的自动封装,类型的转换,值栈的维护,验证,国际化等方面。

每一个拦截器都实现用来完成单一的功能。多个拦截器,按照顺序放在一个列表中,按照顺序执行,可以达到完成一系列功能的目的。 这多个的拦截器放在一起,像一根链条一样,称为拦截器栈(栈是一种非常基本的数据结构,它遵守先进后出的原则。简单理解,它是有顺序的一个链表)。

比如,在 struts 中,对值进行自动封装的拦截器叫 ParameterFilterInterceptor;对异常处理的拦截器叫 ExceptionMappingInterceptor;FileUploadInterceptor 负责处理文件的上传等。其他功能,在 struts 中都有相应的拦截器实现。

所以一个请求在到达 Action 对象前会经过一系列的拦截器。

        拦截器a -> 拦截器 B -> 拦截器 C -> 拦截器 D ... -> Action.Method -> 后续的一些处理,包括返回显示页面等。

在 struts.xml 中,可以通过 interceptor-ref 为每个 action 设置相应的拦截器链。如果我们不去设置,那么如果我们继承了 struts-default, action 会默认使用 defaultStack 拦截器栈。

defaultStack 拦截器栈定义了一组有序的拦截器,它包含的每个拦截器都是 struts 内置的。我们可以通过在 struts-default.xml 中查看详情。

配置拦截器的方式为,在 action 下面,增加 interceptor-ref 节点:

  /aaa.jsp

 

 

在 struts 处理请求的过程中,会分析你的配置,把你为 action 配置的所有拦截器引用按照先后顺序整理成一个新的链表。然后按照顺序去执行。

当然,你也可以在 package 里面声明新的拦截器和拦截器栈。上面的代码可以改写,并改进为:

 

 

 

 

 

 

   

   

   

   

   

 

  /aaa.jsp

 

当然,如果想自定义拦截器,只需要实现 Interceptor 接口即可。为了方便,也可以直接继承 AbstractInterceptor 类,这样,我们只需要重写 intercept 方法就可以了。

例子:

第一步,实现自己的拦截器。

/**

* 这是一个简单的用来判断登录的拦截器栗子。

*/

public class VertifyInterceptor extends AbstractInterceptor {

    // 日志系统,你们需要了解一下

    Log logger = LogFactory.getLog(VertifyInterceptor.class);

    @Override

    public String intercept(ActionInvocation invocation) throws Exception {

        // 获取 action 的名字

        String actionName = invocation.getProxy().getActionName();

        // 这只是一个小例子,通过这样设置,我们可以让这些请求跳过下面的验证。

        Set excludes = new HashSet<>();

        excludes.add("login");

        excludes.add("index");

        if (excludes.contains(actionName)) {

            // 如果请求在我们的白名单中,将不执行之后的判断逻辑。

            return invocation.invoke();

        }

        // 下面开始进行相关验证。

        HttpSession session = ServletActionContext.getRequest().getSession();


        // 未登录检测。如果session为空,或者 session 没有保存相关状态,则判断,这个人没有登录。那么让他去登录页面。

        if (session == null || session.getAttribute(Globals.USER_KEY) == null) {

            // 记录或打印日志

            logger.info(ServletActionContext.getRequest().getRequestURI() + "  尚未登录,返回首页");

            // 如果直接返回一个字符的话,那么下一步将直接进入这里指定的 index 页面,而不会执行到 action 里面去。

            return "loginPage";

        }

        // 权限控制。防止绕过验证,直接进入管理员的页面。

        // 如果请求的 namespace 是 /admin,但 session 里保存的用户类型不是 1,那么我们可以判断,这是非管理员要访问我们的管理员页面。所以毫无疑问,要禁止他的操作。

        if (invocation.getProxy().getNamespace().equalsIgnoreCase("/admin") && ((User) session.getAttribute(Globals.USER_KEY)).getUsertypeid() != 1) {

            logger.info(ServletActionContext.getRequest().getRequestURI() + "  不具备相应权限,返回登录");

            // 将 session 设置为无效

            session.invalidate();

            // 返回相关警告页面,或者跳转到登录页面

            return "errorPage";

        }

        // 默认情况,继续运行。

        return invocation.invoke();

    }

}

第二步,在 struts.xml 中配置自定义的拦截器


 

 

                 

   

 

  /aaa.jsp

 

就这么简单。

Ajax/Json

在 struts2 中使用 ajax 获取 json 数据主要以下三种方法:

Servlet 原生写法

struts.xml:

 

   

 

action:

public String einfo () throws Exception {

    // Writer

    PrintWriter writer = ServletActionContext.getResponse().getWriter();

    // Data

    String result = "{\"name\": \"Alice\", \"age\": 22}";

    // Output

    writer.write(result);

    writer.flush();

    // Return

    return null;

}

front-page:

$.post("/einfo.action",null,r=>alert(r));

使用 stream 类型

struts.xml:

 

   

     

      text/html; charset=UTF-8

      inputStream

   

 

action:

// Define

private InputStream inputStream;

public String einfo () {

    // Data

    String result = "{\"name\": \"Alice\", \"age\": 22}";

    // Assign

    inputStream = new ByteArrayInputStream(result.getBytes("UTF-8"));


    return "success";

}

front-page:

$.post("/einfo.action",null,r=>console.log(r));

使用 struts-json 插件

json 插件会自动将指定对象序列化为 json 字符串并返回。

首先添加依赖:

compile "org.apache.struts:struts2-json-plugin:+"

struts.xml:

 

   

   


   

   

      #request.emps

   

   

   

      \[\d+\].department, \[\d+\].manager

   

   

   

      #request.emps.{#{"n": name, "s": salary}}

   

 

action:

public String elist () {

    request.put("emps", empDAO.getAll());

    return "r3";

}

front-page:

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = () => {

    if(xhr.readyState === 4) {

        const emps = JSON.parse(xhr.responseText);

        document.querySelector("#xxx").innerHTML = emps.map(e => {

            `${e.n}${e.s}`

        }).join("\n");

    }

};

xhr.open("GET", "/elist.action", true);

xhr.send();

Files


upload


form:

 

 

 

action:

private File aaa;

private String aaaFileName;

private String aaaContentType;

private String describe;

public String upload () {

    // Save with Stream

    FileUtils.copyFile(aaa, new File("d:/xxx/" + aaaFileName));

    return "success";

}

需要注意:

struts2 的文件上传实际上用的是 Commons FileUpload 组件,所以要导入相关 jar 包

处理文件上传的是 FileUpload 拦截器。可通过配置拦截器参数(maximumSize/allowedExtensions)限制上传文件的大小、格式等

在 Action 中定义上述 3 个属性(param+XXX),配合 IO 流完成数据写入。多文件上传则需要将上述属性定义成 List 类型

上面的三个属性可以随意定义,但是相应的 setter 方法一定是 paramXXX 格式

download

超链接的形式是静态文件下载。但如果要动态下载,需要使用 type=stream。

struts.xml:

 

    2048

    attachment;filename=${file.name}

 

action:

// Define

private File file;

private InputStream inputStream;

public String download() {

    // Data

    file = new File("D:/aaa/abc.jpg");

    inputStream = new FileInputstream(file);

    return "success";

}

防止重复提交 


三种情况会引发重复提交:

    1、多次点击

    2、回退,再提交

    3、转发时 F5 刷新

解决方案:使用 token/tokenSession 拦截器

    1、在配置文件中添加 token/tokenSession 拦截器 (它不包含在 defaultStack 中)

    2、在 form 中添加标签  (会在页面生成一个 hidden 域并将值保存在 session 中)

    3、若使用 token 拦截器: 出错后会有页面跳转,所以需要配置一个名为 token.valid 的 result

    4、若使用 tokenSession 拦截器:出错后页面不会发生变化,所以不需要其他配置

你可能感兴趣的:(Struts2从上到下的详解)