Struts2

2017/04/17 JavaEE

Struts2是在Struts1和webwork基础之上发展的全新的基于MVC模式的web层框架

优点

  • 拥有MVC框架的优点
  • 应用可以不依赖servlet api,非侵入式设计
  • 提供了拦截器,可以进行AOP编程
  • 提供了类型转换器
  • 支持多种表现层技术
  • 支持输入校验
  • 提供了全局范围、包范围、Action范围的国际化资源文件管理实现

配置

web.xml

<!-- Struts2 filter -->
<filter>
    <filter-name>struts2</filter-name>
    <!-- 新版本中没有ng包名, -->
    <!-- <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> -->
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
    <!-- 2.1.3版本前老版本中已经过时 -->
    <!-- <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class> -->
    <init-param>
        <param-name>struts.i18n.encoding</param-name> 
        <param-value>utf-8</param-value>
    </init-param>
</filter>

struts.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
        "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
    <constant name="struts.devMode" value="true"/>
    <package name="basicStruts2" extends="struts-default">
        <action name="index">
            <result>/index.html</result>
        </action>
        <action name="hello" class="com.xpress.hello.action.HelloWorldAction" method="execute">
            <result name="success">/index.jsp</result>
        </action>
    </package>
</struts>

action

public class HelloWorldAction extends ActionSupport {
    private static final long serialVersionUID = 1L;
    public String execute() throws Exception {
        return SUCCESS;
    }
}

struts.xml配置文件

include标签

<include file="struts-login.xml"/>

constant标签

常量配置,可配置default.properties内容,另可在web.xml中的filter init参数中配置,也可在struts.properties中配置

常用常量

<!-- 打印更多的错误信息 -->
<constant name="struts.devMode" value="false"/>
<!-- 重新加载xml -->
<constant name="struts.configuration.xml.reload" value="false"/>
<!-- 作用于request的setCharacterEncoding()方法和freemarker、velocity的输出 -->
<constant name="struts.i18n.encoding" value="UTF-8"/>
<constant name="struts.action.extension" value="do,action,,"/>
<!-- 浏览器是否缓存静态内容 -->
<constant name="struts.serve.static.browserCache" value="true"/>
<!-- 表单标签默认主题 -->
<constant name="struts.ui.theme" value="simple"/>
<!-- 创建action的工厂类 -->
<constant name="struts.objectFactory" value="spring"/>
<!-- 文件上传大小限制 -->
<constant name="struts.multipart.maxSize" value="16777216" />
<!-- 允许访问静态方法 -->
<constant name="struts.ognl.allowStaticMethodAccess" value="false"/>

ps:常量配置可以配置在constant标签中,也可以配置在struts.properties等文件中

常量加载顺序
  1. struts-default.xml
  2. struts-plugin.xml
  3. struts.xml(推荐)
  4. struts.properties
  5. web.xml

ps:多个配置中相同常量配置后加载覆盖前面加载的配置

package标签

  • name 包名
  • extends 继承关系
    • 默认struts-default,struts-default中定义了很多拦截器和Result
  • namespace package访问路径,默认”“,当前url对应的命名空间找不到会交给默认命名空间下对应的action处理
  • abstract=”true” 抽象包,不能包含action
Action查找顺序
  1. 当前命名空间下查找
  2. 找不到action,会递归向上级继续查找
  3. 当前或上级包不存在会到默认命名空间(namespace不指定或指定为”“)下查找
  4. 都找不到则报错
默认action配置
<default-action-ref name="index"/>

action标签

动作

  • name action访问路径
  • class action执行的class全路径
  • method action执行的方法名称
action标签的默认值
  • 如果没有指定class,默认是ActionSupport
  • method属性默认值是execute()
  • name默认值”success”
method属性

方法访问配置

  • action标签的method属性
  • 使用通配符(同时匹配时按精确程度选择)
  • 动态访问
<!-- method属性方式 -->
<action name="addHelloAction" class="com.xpress.action.HelloAction" method="add">
</action>
<action name="deleteHelloAction" class="com.xpress.action.HelloAction" method="delete">
</action>
<!-- 通配符方式 -->
<action name="*Action" class="com.xpress.action.{1}Action" method="{1}">
    <result name="success">/{1}.jsp</result>
</action>
<!-- 动态访问方式 -->
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<action name="helloAction" class="com.xpress.action.HelloAction">
</action>
<!-- http://localhost:8080/helloAction!add.action -->
<!-- http://localhost:8080/helloAction!delete.action -->

result标签

返回结果

  • name 同方法返回值
  • type 结果处理方式(转发或重定向,默认转发)
result配置
  • 全局结果页面
    • action返回的结果和页面都是相同的可以使用
    • 使用包继承将全局结果定义在父package可以使全局结果页面所有package共享
  • 局部结果页面

ps:注意,result结果页面配置时不写”/”前缀默认会根据当前package的namespace来寻找jsp

<!-- 全局结果页面配置 -->
 <package name="default" extends="struts-default" namespace="/">
    <!-- 作用范围为package -->
    <global-results>
        <result name="success">index.jsp</result>
    </global-results>

    <action name="addHelloAction" class="com.xpress.action.HelloAction" method="add">
        <!-- 使用全局结果页面 -->
        <!-- <result name="success">index.jsp</result> -->
    </action>
    <action name="deleteHelloAction" class="com.xpress.action.HelloAction" method="delete">
        <!-- 使用局部结果页面,全局结果页面则不生效 -->
        <result name="success">index.jsp</result>
    </action>
</package>

type属性值

  • dispatcher 转发到页面(默认值)
  • redirect 重定向
  • chain 转发到actin(一般不用,缓存问题)
  • redirectAction 重定向到action
  • plainText 返回页面的html,Content-type=”text/plain”
  • freemarker
  • httpheader
  • stream
  • velocity
  • xslt
  • tiles
  • 参数传递
    • 使用ognl表达式${propertyName}访问action中属性
      • <result name="login">${page}?userame=${userame}</result>
<!-- result配置参数,参数参看type对应result-type类的成员遍历 -->
<result type="redirectAction" name="redirect">
    <param name="namespace">/other</param>
    <param name="actionName">list</param>
</result>
<result type="plainText">
    <param name="location">index.html</param>
    <param name="charSet">UTF-8</param>
</result>

param标签

  • 在action中配置,为action的filed注入值
  • 在result中使用,为result-type对应的class注入filed参数
<action name="test"  class="com.xpress.action.HelloAction">
    <param name="propertyName">value</param>
    <result type="plainText">
        <param name="location">index.html</param>
        <param name="charSet">UTF-8</param>
    </result>
    <result type="redirectAction" name="redirect">
        <param name="namespace">/other</param>
        <param name="actionName">list</param>
    </result>
</action>

Action

三种实现方式

  • 普通类
  • 实现Action接口
  • 继承自ActionSupport

method无返回值

  • 使用void返回值类型
  • return NONE;

获取表单数据

获取表单数据

  • 使用ActionContext类(推荐,完全解耦合)
  • 使用ServletActionContext类(常见)
  • 使用接口注入
public class HelloAction extends ActionSupport implements ServletRequestAware {
    private HttpServletRequest httpServletRequest;

    @Override
    public void setServletRequest(HttpServletRequest request) {
        httpServletRequest = request;
    }

    public String getParameter() {
        Map parameters;
        // 使用ActionContext
        ActionContext actionContext = ActionContext.getContext();
        parameters = actionContext.getParameters();
        // 使用ServletActionContext
        HttpServletRequest request = ServletActionContext.getRequest();
        parameters = request.getParameterMap();
        // 实现ServletRequestAware接口注入request
        parameters = httpServletRequest.getParameterMap();
        // 原始数据封装
        // 属性封装
        // 模型驱动封装
        return NONE;
    }
}

封装数据到实体

封装方法
  • 原始数据封装
    • 获取request后获得parameter
    • 设置到实体中
  • 属性封装
    • 使用action成员变量
    • 表单项和成员变量名一致并生成get、set方法
    • 获取属性值并设置到实体中
  • 模型驱动封装
    • 实现ModelDriven接口
    • 创建需要转换的实体对象,并new一个实例
    • 实现T getModel()方法返回需要转换的实体
  • 表达式封装
    • 属性封装的属性为实体对象并生成get、set方法(会使用get方法查找对象)
    • 表单中使用表达式名称(name=”bean.property”)
    • 如果实体时Map或者List也可以封装(name=”list[0].property”,name=”map[‘key’].property”)

ps;不能同时使用属性封装和模型驱动封装获取同一表单数据,同时使用只执行模型驱动封装
ps;表达式封装需要给定bean的无参构造,否则会出错

public class ModelDrivenAction extends ActionSupport implements ModelDriven<User> {
    private User user;
    public User getUser() {
        return user;
    }
    @Override
    public User getModel() {
        return user;
    }
模型驱动封装和表达式封装比较
  • 都可以封装表单数据到实体
  • 使用模型驱动只能把数据封装到一个实体,表达式封装可以封装到多个实体中
类型转换器

struts2支持类如”2010-01-01”的日期格式自动转换,但如”20100101”格式无法自动转换

实现转换器类,继承自DefaultTypeConverter

public class DateAction extends ActionSupport {
    private Date birthday;

    @Override
    public String execute() {
        System.out.println(birthday);
        return SUCCESS;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}

public class DateConverter extends DefaultTypeConverter {
    /**
     * 转换方法,双向转换,同时只是String->Date和Date->String
     *
     * @param context ognl context
     * @param value   转换值
     * @param toType  转换类型
     * @return
     */
    @Override
    public Object convertValue(Map<String, Object> context, Object value, Class toType) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        if (toType == Date.class) {
            try {
                // 页面参数为数组形式传递
                String[] param = (String[]) value;
                return simpleDateFormat.parse(param[0]);
            } catch (ParseException e) {
                e.printStackTrace();
                return null;
            }
        } else if (toType == String.class) {
            return simpleDateFormat.format(value);
        } else {
            // 当前不能处理交给父类处理
            return super.convertValue(context, value, toType);
        }
    }
}

继承struts的类型转换器

public abstract class StrutsTypeConverter extends DefaultTypeConverter {
    public abstract Object convertFromString(Map context, String[] values, Class toClass);
    public abstract String convertToString(Map context, Object o);
}

注册局部转换器,在Action类所在包下建立ActionName-conversion.properties

DateAction-conversion.properties

birthday=com.xpress.converter.DateConverter

注册全局转换器,在WEB-INF\classes下建立xwork-conversion.properties

java.util.Date=com.xpress.converter.DateConverter
文件上传
<form enctype="multipart/form-data" method="post" action="<s:url action="Fileupload_upload"/>">
    <input type="file" name="cardPhoto"/>
    <input type="submit"/>
</form>
public class FileuploadAction extends ActionSupport {
    private File cardPhoto;// 得到上传文件
    private String cardPhotoContentType;// 得到文件类型
    private String cardPhotoFileName;// 得到文件名称
    private static final String UPLOAD_DIR = "/upload/";

    public String upload() {
        String dir = ServletActionContext.getServletContext().getRealPath(UPLOAD_DIR);
        File dirs = new File(dir);
        dirs.mkdirs();
        try {
            File destFile = new File(dir + File.separator + cardPhotoFileName);
            FileUtils.copyFile(cardPhoto, destFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return SUCCESS;
    }

    public void setCardPhoto(File cardPhoto) {
        this.cardPhoto = cardPhoto;
    }

    public void setCardPhotoContentType(String cardPhotoContentType) {
        this.cardPhotoContentType = cardPhotoContentType;
    }

    public void setCardPhotoFileName(String cardPhotoFileName) {
        this.cardPhotoFileName = cardPhotoFileName;
    }
}

ps:Unable to find ‘struts.multipart.saveDir’ property setting. Defaulting to javax.servlet.context.tempdir;配置临时目录否则使用tomcat临时目录

ps:多文件上传要修改成员变量为数组,form表单中name值相同,然后根据索引遍历得到所上传的文件

操作域对象

通过ActionContext操作

ActionContext中也提供了域对象操作的方法,但是存在问题

// 这里是操作ActinContext中的map对象,struts会处理它们和域对象的联系,也就导致了下面的问题
ActionContext actionContext = ActionContext.getContext();
actionContext.put("key", "value");
actionContext.getSession().put("key", "value");
actionContext.getApplication().put("key", "value");

HttpServletRequest httpServletRequest = ServletActionContext.getRequest();
httpServletRequest.setAttribute("newKey", "value");
// 放入request中的值不能立即在actionContext中get得到
System.out.println(actionContext.get("newKey"));// null

获取域对象操作

HttpServletRequest httpServletRequest = ServletActionContext.getRequest();
HttpServletResponse httpServletResponse = ServletActionContext.getResponse();
HttpSession httpSession = httpServletRequest.getSession();
ServletContext servletContext = ServletActionContext.getServletContext();

通过实现接口操作

servletConfig拦截器对对象进行了注入

// 这里注入的是域对象
public class AwareAction extends ActionSupport implements ServletRequestAware, ServletResponseAware, ServletContextAware {
    private HttpServletRequest request;
    private HttpServletResponse response;
    private ServletContext context;

    @Override
    public void setServletRequest(HttpServletRequest request) {
        this.request = request;
    }
    @Override
    public void setServletResponse(HttpServletResponse response) {
        this.response = response;
    }
    @Override
    public void setServletContext(ServletContext context) {
        this.context = context;
    }
}
// 这里注入的是map对象
public class MapAware extends ActionSupport implements RequestAware, SessionAware, ApplicationAware {
    private Map<String, Object> application;
    private Map<String, Object> request;
    private Map<String, Object> session;

    @Override
    public void setApplication(Map<String, Object> application) {
        this.application = application;
    }

    @Override
    public void setRequest(Map<String, Object> request) {
        this.request = request;
    }

    @Override
    public void setSession(Map<String, Object> session) {
        this.session = session;
    }
}

输入校验

实现方式

  • 重写方法实现
    • 对action中所有方法
    • 对action中指定方法
  • XML配置实现
    • 对action中所有方法
    • 对action中指定方法
校验结果
  • 验证失败后,请求转发至input视图
  • 页面中使用<s:fielderror/>显示失败信息
<!-- 这种方式会带有ul标签,指定theme="simple"无效 -->
<s:fielderror/>
<!-- 通过值栈获取错误信息 -->
<%--值栈中取到errors,是一个根据字段名称存储错误信息的map--%>
<s:iterator value="errors">
    <%--遍历每个字段的错误信息--%>
    <s:iterator value="value">
        <s:property/><br/>
    </s:iterator>
</s:iterator>
<!-- 直接根据key获取 -->
<s:property value="errors.name[0]"/>

ps:值栈中取到的errors包含fieldErrors和actionErrors,可以分开获取

重写方法实现
<form method="post" action="/my/myAction_login.do">
    <%--显示错误信息--%>
    <s:fielderror/>
    <input type="text" name="username"/>
    <input type="submit"/>
</form>
public class MyAction extends ActionSupport {
    private String username;
    public String operate() {
        return SUCCESS;
    }
    public String login() {
        return SUCCESS;
    }
    // 重写validate方法,校验action中全部方法
    @Override
    public void validate() {
        if (StringUtils.isBlank(username)) {
            // 放入错误信息,检测到错误struts会跳转到INPUT页面
            // 错误可以有多条,都会显示
            addFieldError("username", "用户名不能为空");
        }
    }
    // 会对Operate方法进行校验
    public void validateOperate() {
        if (StringUtils.isBlank(username)) {
            addFieldError("username", "用户名不能为空");
        }
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
}

ps:同时使用指定方法校验和全部方法校验,先执行指定方法校验

XML配置实现

使用基于XML配置方式实现输入校验时,Action也需要继承ActionSupport,并且提供校验文件,校验文件和action类放在同一个包下

  • 校验actino中所有方法:文件的取名格式为:ActionClassName-validation.xml
  • 校验actino中指定方法:文件的取名格式为:ActionClassName-ActionName-validation.xml

ps:注意指定方法文件名称中间为ActionName,为访问action时url上面的名字

<action name=”user_*” …
UserAction-user_add-validation.xml

ps:系统寻找到第一个校验文件时还会继续搜索后面的校验文件,当搜索到所有校验文件时,会把校验文件里的所有校验规则汇总,然后全部应用于action方法的校验。如果两个校验文件中指定的校验规则冲突,则只使用后面文件中的校验规则。

加载顺序(后加载覆盖先加载):父类所有方法校验文件 > 父类指定方法校验文件 > 子类所有方法校验文件 > 子类指定方法校验文件

<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.3//EN"
        "http://struts.apache.org/dtds/xwork-validator-1.0.3.dtd">
<validators>
    <validator type="requiredstring">
        <param name="fieldName">user.username</param>
        <message>username is required.</message>
    </validator>
    <field-validator type="regex">
     <param name="user.phone"><![CDATA[^1[358]\d{9}$]]></param>
     <message>手机号格式不正确!</message>
    </field-validator>
</validators>
public class XMLValidateAction extends ActionSupport {
    private User user;
    @Override
    public String execute() throws Exception {
        return SUCCESS;
    }
    public User getUser() {
        return user;
    }
    public void setUser(User user) {
        this.user = user;
    }
}
XML校验校验器

校验器的定义可以在xwork-2.x.jar中的com.opensymphony.xwork2.validator.validators下的default.xml中找到

系统提供的校验器如下:

validator type comment
required 必填校验器,要求field的值不能为null
requiredstring 必填字符串校验器,要求field的值不能为null,并且长度大于0,默认情况下会对字符串去前后空格
stringlength 字符串长度校验器,要求field的值必须在指定的范围内,否则校验失败,minLength参数指定最小长度,maxLength参数指定最大长度,trim参数指定校验field之前是否去除字符串前后的空格
regex 正则表达式校验器,检查被校验的field是否匹配一个正则表达式.expression参数指定正则表达式,caseSensitive参数指定进行正则表达式匹配时,是否区分大小写,默认值为true
int 整数校验器,要求field的整数值必须在指定范围内,min指定最小值,max指定最大值
double 双精度浮点数校验器,要求field的双精度浮点数必须在指定范围内,min指定最小值,max指定最大值
fieldexpression 字段OGNL表达式校验器,要求field满足一个ognl表达式,expression参数指定ognl表达式,该逻辑表达式基于ValueStack进行求值,返回true时校验通过,否则不通过
email 邮件地址校验器,要求如果field的值非空,则必须是合法的邮件地址
url 网址校验器,要求如果field的值非空,则必须是合法的url地址
date 日期校验器,要求field的日期值必须在指定范围内,min指定最小值,max指定最大值
conversion 转换校验器,指定在类型转换失败时,提示的错误信息
visitor 用于校验action中的复合属性,它指定一个校验文件用于校验复合属性中的属性
expression OGNL表达式校验器,expression参数指定ognl表达式,该逻辑表达式基于ValueStack进行求值,返回true时校验通过,否则不通过,该校验器不可用在字段校验器风格的配置中

校验流程

  1. 类型转换器对请求参数进行转换,并赋值给action中的属性
  2. 如果出现异常,conversionError拦截器将异常信息封装到fieldErrors里,继续进行第三步
  3. 系统通过反射技术调用action中validateMethodName()方法
  4. 在调用action中的validate()方法
  5. 经过上面四步,如果系统的fieldErrors存在错误信息,系统自动转发请求到input视图,如果没有错误信息,将调用action中的方法

Servlet和Action的区别

  • Servlet:默认第一次访问的时候创建,创建一次
  • Action:访问时候创建,每次访问时候都会创建一个对象,创建多次,是线程安全的(区别于struts1,struts1放入缓存重用)

拦截器

struts的功能封装在默认拦截器中,配置在strtus-default.xml中

执行时机

action对象创建之后,action的方法执行之前

拦截器原理

  • aop思想
  • 责任链模式

自定义拦截器

  • 继承AbstractInterceptor或实现Interceptor接口
  • 继承MethodFilterInterceptor实现可配置的拦截和不拦截某些方法(建议)

待拦截action

public class MyAction extends ActionSupport {
    public String operate() {
        return SUCCESS;
    }
    public String login() {
        return SUCCESS;
    }
    public String logout() {
        return SUCCESS;
    }
}

实现拦截器

// 继承自MethodFilterInterceptor可以配置排除方法
public class CustomerInterceptor extends MethodFilterInterceptor {
    @Override
    protected String doIntercept(ActionInvocation invocation) throws Exception {
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpSession session = request.getSession();
        if (session.getAttribute("userInfo") == null) {
            return "login";// 返回登录
        } else {
            return invocation.invoke();// 放行
        }
    }
}

注册拦截器

<package name="interceptorDemo" extends="struts-default">
    <interceptors>
        <!--声明自定义拦截器-->
        <interceptor name="customerInterceptor" class="com.xpress.interceptor.CustomerInterceptor">
            <param name="excludeMethods">login,logout</param>
        </interceptor>
        <!--声明自定义拦截器栈-->
        <interceptor-stack name="myStack">
            <interceptor-ref name="defaultStack"/>
            <interceptor-ref name="customerInterceptor"/>
        </interceptor-stack>
    </interceptors>
    <!--指定包默认拦截器,也可以只用包继承拦截所有-->
    <default-interceptor-ref name="myStack"/>
    <action name="myAction" class="com.xpress.action.MyAction">
        <!--注册默认拦截器栈,否则不会自动注册-->
        <interceptor-ref name="defaultStack"/>
        <!--注册自定义拦截器-->
        <interceptor-ref name="customerInterceptor"/>
        <!--直接注册拦截器栈-->
        <!--<interceptor-ref name="myStack"/>-->
        <result name="success">main.jsp</result>
        <result name="login">login.jsp</result>
    </action>
</package>

过滤器和拦截器的区别

  • 过滤器过滤所有内容,html,jsp,servlet,图片路径等
  • 拦截器只拦截action

声明式异常处理

使用

<!-- 定义全局结果页面 -->
<global-results>
    <result name="success">user.jsp</result>
    <result name="index">index.jsp</result>

    <result name="error">/error.jsp</result>
    <result name="invalid.token">/tokenError.jsp</result>
</global-results>
<global-exception-mappings>
    <!-- 全局异常处理 -->
    <exception-mapping exception="java.lang.Exception" result="error"/>
</global-exception-mappings>

<action name="hello" class="com.xpress.action.HelloAction">
    <!-- 局部异常处理 -->
    <exception-mapping exception="java.lang.Exception" result="error"/>
    <!-- 局部异常结果页面,会优先被使用 -->
    <result name="error">/error.jsp</result>
</action>

原理

exception拦截器catch异常后根据异常结果类型修改了页面返回结果

try {
    result = invocation.invoke();
} catch (Exception e) {
    if (isLogEnabled()) {
        handleLogging(e);
    }
    List<ExceptionMappingConfig> exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings();
    ExceptionMappingConfig mappingConfig = this.findMappingFromExceptions(exceptionMappings, e);
    if (mappingConfig != null && mappingConfig.getResult()!=null) {
        Map parameterMap = mappingConfig.getParams();
        // create a mutable HashMap since some interceptors will remove parameters, and parameterMap is immutable
        invocation.getInvocationContext().setParameters(new HashMap<String, Object>(parameterMap));
        result = mappingConfig.getResult();
        publishException(invocation, new ExceptionHolder(e));
    } else {
        throw e;
    }
}

值栈

每个Action对象都会有一个值栈对象

获取值栈对象方法

ValueStack valueStack = ActionContext.getContext().getValueStack();

值栈结构

值栈分为两部分:root和context

  • root:Value Stack Contents,对象是CompoundRoot,结构是list结合(一般操作root中数据)
  • context:Stack Context,对象是OgnlContext,结构是map集合

使用标签查看值栈结构和存储值

<s:debug/>

root

向值栈中放数据

  • ActionContext.getContext().getValueStack().push(new Object()); 存入值栈的map中
  • ActionContext.getContext().getValueStack().set(“key”,new Object()); 存入值栈中
  • 在action中创建成员变量并提供get方法并给对象赋值 存入值栈的action引用的对象中(常用)

context

context常用存储对象

key value
request request对象引用
session HttpSession对象引用
application ServletContext对象引用
parameters 传递的相关参数
attr 获取域对象中域范围最小的域中的值

OGNL和Struts标签库

Struts标签库

常用标签

set标签
  • scope:指定变量被放置的范围,application、session、request、 page或action。如果没有设置该属性,则默认放置在OGNL Context中
  • value:赋给变量的值.如果没有设置该属性,则将ValueStack栈顶的值赋给变量。
  • var:变量名
  • name:deprecated
  • id:deprecated
<!-- 赋值 -->
<s:set var="list" value="{'zhangming','xiaoi','liming'}"/>
bean标签

定义bean对象并放到context中

<s:bean var="dog" name="com.xpress.model.Dog">
    <!-- 成员变量赋值 -->
    <s:param name="name" value="puppy"/>
</s:bean>
property标签
  • default:可选属性,如果需要输出的属性值为null,则显示该属性指定的值
  • escape:可选属性,指定是否格式化HTML代码。
  • value:可选属性,指定需要输出的属性值,如果没有指定该属性,则默认输出ValueStack栈顶的值。
  • id:可选属性,指定该元素的标识

ps:取不到值会显示空

<!-- 取值栈中值 -->
<s:property value="username" default="admin"/>
<!-- 输出字符串 -->
<s:property value="'string'"/>
<!-- 取context对象加#,root对象不加#,不指定value默认输出栈顶的值 -->
<s:property value="#request.key"/>
<!-- 调用方法 -->
<s:property value="password.length()"/>
<!-- 调用静态方法和常量,需要开启struts.ognl.allowStaticMethodAccess -->
<s:property value="@com.xpress.constants.Constants@CHAR_C"/>
<s:property value="@com.xpress.constants.Constants@toLowerCase('abc')"/>
<!-- 访问java.lang.Math类静态方法 -->
<s:property value="@@max(2,3)"/>
<!-- 全限定名new普通类 -->
<s:property value="new java.lang.String('str')"/>
<!-- 访问array或list -->
<s:property value="userList"/>
<s:property value="userList[0]"/>
<!-- 访问list内实体的所有username集合 -->
<s:property value="userList.{username}"/>
<s:property value="userList.{username}[0]"/>
<!-- 访问map -->
<s:property value="userMap"/>
<s:property value="userMap['key']"/>
<s:property value="userMap.key"/>
<s:property value="userMap[\"key\"]"/>
<s:property value="userMap.keys"/>
<s:property value="userMap.values"/>
<s:property value="userMap.size()"/>
<!-- 不使用括号也可以 -->
<s:property value="userMap.size"/>
<!-- 值栈访问,值栈从下标位置开始取,一直到最后的对象集合 -->
<s:property value="[0]"/>
<!-- top访问第一个元素 -->
<s:property value="[1].top"/>
iterator标签
  • var:指定存储当前元素的name,如果指定该值,将存储当前元素到context
  • value:可选属性,指定被迭代的集合,如果没有设置该属性,则使用ValueStack栈顶的集合。
  • id:可选属性,指定集合里元素的id。
  • status:可选属性,该属性指定迭代时的IteratorStatus实例。
    • int getCount(),返回当前迭代了几个元素。
    • int getIndex(),返回当前迭代元素的索引。
    • boolean isEven(),返回当前被迭代元素的索引是否是偶数
    • boolean isOdd(),返回当前被迭代元素的索引是否是奇数
    • boolean isFirst(),返回当前被迭代元素是否是第一个元素。
    • boolean isLast(),返回当前被迭代元素是否是最后一个元素。
<!-- 遍历,迭代时,会把元素放到栈顶,直接取值。指定var后放到context使用#取值 -->
<s:iterator value="stringList" var="str">
    <!-- 取context值 -->
    <s:property value="#str"/>
    <!-- 直接取栈顶值 -->
    <s:property />
</s:iterator>
<s:iterator value="#list" status="st">
    <font color=<s:if test="#st.odd">red</s:if><s:else>blue</s:else>>
        <s:property/></font><br>
</s:iterator>
if/elseif/else标签
<!-- 判断 -->
<s:if test="propertyName==null">
    is null
</s:if>
<s:elseif test="propertyName.equals('')">
    ""
</s:elseif>
<s:else>
    <s:property value="propertyName"/>
</s:else>
in/not in条件
<!-- in -->
<s:if test="'foo' in {'foo','bar'}"></s:if>
<s:else>
   不在
</s:else>
<!-- not in -->
<s:if test="'foo' not in {'foo','bar'}">
   不在
</s:if>
<s:else></s:else>
投影
<!-- 投影:使用某个规则获得集合对象的子集 -->
<!-- 
?:获得所有符合逻辑的元素。
^:获得符合逻辑的第一个元素。
$:获得符合逻辑的最后一个元素。
 -->
 <!-- this代表当前迭代的元素 -->
<s:iterator value="books.{?#this.price > 35}">
      <s:property value="title" /> - $<s:property value="price" /><br>
</s:iterator>
<s:iterator value="#map">
      <s:property value="key" /> - $<s:property value="value" /><br>
</s:iterator>
subset标签

截取集合

<s:subset source="list" start="10" count="3">
    <s:iterator>
        <s:property/>
    </s:iterator>
</s:subset>
debug标签
<!-- 查看值栈结构和数据 -->
<s:debug/>
a标签
<!-- 链接 -->
<s:a value="index.jsp">index jsp</s:a>
<s:a action="index">index action</s:a>
<s:a href="index">index link</s:a>

表单标签

<!--theme:simple,xhtml,xhtml会生成table,simple是干净的标签 -->
<s:form action="addAction" method="POST" theme="simple">
<!--文本 -->
<s:textfield name="username" value="john" label="username"/>
<!--密码 -->
<s:password name="password" value="" label="password"/>
<!--单选,list集合显示值和value相同,map结合key为value值,value为显示值,value指定勾选的值 -->
<s:radio name="sex" label="sex" list="{'male','female'}" value="{'male'}"/>
<s:radio name="sex1" label="sex" list="#{'M':'male','F':'female'}" listKey="key" listValue="value"  value="{'F'}"/>
<s:radio name="beans" list="#request.persons" listKey="persionId" listValue="personName"  value="{'1'}"/>
<!--多选,list集合显示值和value相同,map结合key为value值,value为显示值,value指定勾选的值 -->
<s:checkboxlist name="hobby" label="hobby" list="{'basketball','ping-pong'}" value="{'ping-pong'}"/>
<s:checkboxlist name="hobby1" label="hobby" list="#{'B':'basketball','P':'ping-pong'}" listKey="key" listValue="value" value="{'B'}"/>
<s:checkboxlist name="beans" list="#request.persons" listKey="persionId" listValue="personName"  value="{'1'}"/>
<!--下拉框 -->
<s:select name="hobby" label="hobby" list="{'basketball','ping-pong'}" value="{'ping-pong'}"/>
<s:select name="hobby1" label="hobby" list="#{'B':'basketball','P':'ping-pong'}" listKey="key" listValue="value" value="{'B'}"/>
<s:select name="beans" list="#request.persons" listKey="persionId" listValue="personName"  value="{'1'}"/>
<!--文件上传 -->
<s:file label="upload"/>
<!--hidden -->
<s:hidden name="userid" value="userid"/>
<!--submit -->
<s:submit value="submit"/>
<!--reset -->
<s:reset value="reset"/>
<!--textarea -->
<s:textarea name="description" value="description" label="description" cols="60" rows="5"/>
</s:form>
<!-- url,自动补充后缀和contextpath,拼装参数 -->
<s:url value="%{#url}">
    <s:param name="personid" value="23"/>
</s:url>
<!-- 链接 -->
<s:a value="index.jsp">index jsp</s:a>
<s:a action="index">index action</s:a>
<s:a href="index">index link</s:a>
<s:a href="%{#url}">index link</s:a>

防止表单重复提交

在表单中加入token标签

<s:form action="submitAction" method="post">
    <input type="text" name="username"/>
    <input type="submit"/>
    <s:token/>
    <s:submit/>
</s:form>

加入拦截器

<action name="submitAction" class="cn.xpress.action.SubmitAction" method="execute">
    <interceptor-ref name="defaultStack"/>
    <interceptor-ref name="token"/>
    <result name="invalid.token">/WEB-INF/page/message.jsp</result>
    <result>/WEB-INF/page/success.jsp</result>
</action>

OGNL

OGNL是一个独立的项目,Struts2的默认表达式语言,主要用域操作Struts值栈数据,Struts2中,OGNL表达式需要配合Struts标签才可以使用。

  • 支持对象方法调用
  • 支持类静态方法调用和值访问
  • 支持赋值操作和表达式串联
  • 访问OGNL context和ActionContext
  • 操作集合对象

ONGL context

Struts 2中的OGNL Context实现者为ActionContext,当Struts2接受一个请求时,会迅速创建ActionContext,ValueStack,action 。然后把action存放进ValueStack,所以action的实例变量可以被OGNL访问。它结构示意图如下:

另外OGNL会设定一个根对象(root对象),在Struts2中根对象就是ValueStack(值栈) 。如果要访问根对象(即ValueStack)中对象的属性,则可以省略#命名空间,直接访问该对象的属性即可。

OGNL-context.PNG

OGNL-context.PNG

获取值栈中的数据

OGNL表达式搜索顺序

在root变量中处于第一位的对象叫栈顶对象。通常我们在OGNL表达式里直接写上属性的名称即可访问root变量里对象的属性,搜索顺序是从栈顶对象开始寻找,如果栈顶对象不存在该属性,就会从第二个对象寻找,如果没有找到就从第三个对象寻找,依次往下访问,直到找到为止

private String str;
private User user;
private List<User> userList;
public String getStr() {
    return str;
}
public User getUser() {
    return user;
}
public List<User> getUserList() {
    return userList;
}
<%@taglib prefix="s" uri="/struts-tags" %>
<!-- 获取字符串对象 -->
<s:property value="str"/>
<!-- 获取对象值 -->
<s:property value="user.username"/>
<!-- 获取list值 -->
<s:property value="userList[0].username"/>
<!-- 不指定var直接遍历 -->
<s:iterator value="userList">
    <s:property value="username"/>
    <s:property value="password"/>
    <s:property value="nickname"/>
</s:iterator>
<!-- 指定var后,ognl把每次遍历的对象user放到context对象里面,获取context内对象要加#号,否则从root中取值直接写username不需要#ava -->
<s:iterator value="userList" var="user">
    <s:property value="#user.username"/>
    <s:property value="#user.password"/>
    <s:property value="#user.nickname"/>
</s:iterator>
ActionContext.getContext().getValueStack().set("key","value");
ActionContext.getContext().getValueStack().push("value");
<%@taglib prefix="s" uri="/struts-tags" %>
<!-- 获取set对象 -->
<s:property value="key"/>
<!-- push方法设置值,没有名称 -->
<!-- 想值栈中取数据,[0]拿到整个值栈,取top -->
<s:property value="[0].top"/>
EL表达式为什么可以获取值栈数据
  • EL表达式使用getAttribute()方法获取域对象中的值
  • struts底层增强了request对象里的getAttribute()方法
    • 首先获取request域中的值,如果获取到直接返回
    • 如果从request域中没有获取到值,则去值栈中获取并将值放到域对象里

ps:鉴于上面过程,使用el表达式遍历值栈对象效率很低,推荐使用struts标签配合ognl表达式操作数据

ps:EL表达式获取action属性时依赖的时get方法,属性名判定也依赖get方法名,而不是成员变量名

使用“#”获取Context数据

如果访问其他Context中的对象,由于他们不是根对象,所以在访问时,需要添加#前缀。

#context的key名称.域对象的key

<s:property value="#request.key"/>
<s:property value="#session.key"/>
<s:property value="#application.key"/>
<!-- 参见下面list获取值例子 -->
对象 作用
application 访问ServletContext,例如#application.userName或者#application[‘userName’],相当于调用ServletContext的getAttribute(“username”)。
session 访问HttpSession,例如#session.userName或者#session[‘userName’],相当于调用session.getAttribute(“userName”)。
request 访问HttpServletRequest属性(attribute)的Map,例如#request.userName或者#request[‘userName’],相当于调用request.getAttribute(“userName”)。
parameters 访问HTTP的请求参数,例如#parameters.userName或者#parameters[‘userName’],相当于调用request.getParameter(“username”)。
attr 按page->request->session->application顺序访问其属性。

OGNL表达式创建List/Map集合对象

<s:set name="list" value="{'zhangming','xiaoli','liming'}"/>
<s:set name="foobar" value="#{'foo1':'bar1', 'foo2':'bar2'}"/>

使用“%{}”在表单标签中使用OGNL

在Struts表单标签中使用ognl表达式不被识别,需要%{}后才会识别

<s:textfield name="username" value="%{#request.key}"/>

OGNL符号(#%$)使用总结

符号 用途
# 1. 访问OGNL上下文和Action上下文
2. 用于过滤和投影
3. 构造Map
% 在标志的属性为字符串类型时,计算OGNL表达式的值,主要在struts表单标签中
$ 1. 用于在国际化资源文件中,如validation.xml
2. 在Struts 2配置文件中

国际化

资源包中搜索顺序

  1. ActionClass.properties 包下Action同名properties文件
  2. Interface.properties (every interface and sub-interface) 接口包下接口同名properties文件
  3. BaseClass.properties (all the way to Object.properties) 父类包下父类同名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) 包下package.properties文件
  6. search up the i18n message key hierarchy itself
  7. global resource properties 全局资源文件struts.custom.i18n.resources

全局范围资源文件

  • 建立资源文件

xpress_en_US.properties
xpress_zh_CN.properties xpress.properties

key=welcome {0},last login date:{1};
key=欢迎{0},上次登录日期:{1};
  • 配置全局资源文件
<constant name="struts.custom.i18n.resources" value="xpress" />
  • 获取国际化信息

jsp页面中

<s:property value="getText('key')" />
<!-- name为资源文件中的key -->
<s:text name="welcome">
    <s:param>john</s:param>
    <s:param>2010-01-01</s:param>
</s:text>
<!-- 在表单标签中,可通过key属性指定资源文件中的key -->

action中

<!-- 继承ActionSupport,使用getText()方法得到国际化信息 -->
public class MyAction extends ActionSupport {
    @Override
    public String execute() {
        //获取带占位符的国际化信息
        String message = getText("welcome", new String[]{"john", "2010-01-01"});
        return SUCCESS;
    }
}

输入校验中

<validator type="requiredstring">
    <param name="fieldName">user.username</param>
    <message>
        ${getText("welcome",{#request.user.username})}
    </message>
</validator>

ps:message标签为校验失败后的提示信息,如果需要国际化,可以为message指定key属性,key的值为资源文件中的key。

包范围资源文件

如果我们把国际化的内容都放置在全局资源属性文件中,会导致资源文件变的过于庞大、臃肿,不便于维护,我们可以针对不同模块,使用包范围来组织国际化文件。

在java的包下放置package_language_country.properties资源文件,package为固定写法,处于该包及子包下的action都可以访问该资源。

系统会先从package资源文件查找,当找不到对应的key时,才会从常量struts.custom.i18n.resources指定的资源文件中寻找。

package_en_US.properties
package_zh_CN.properties

Action范围资源文件

在Action类所在的路径,放置ActionClassName_language_country.properties资源文件,ActionClassName为action类的简单名称。

系统会先从ActionClassName_language_country.properties资源文件查找,如果没有找到对应的key,然后沿着当前包往上查找基本名为package 的资源文件,一直找到最顶层包。如果还没有找到对应的key,最后会从常量struts.custom.i18n.resources指定的资源文件中寻找。

MyAction_en_US.properties
MyAction_zh_CN.properties

JSP中直接访问某个资源文件

从某个资源文件中获取国际化数据,而无需任何配置:

<!-- 直接访问某资源文件 -->
<s:i18n name="xpress">
    <s:text name=“welcome”/>
</s:i18n>
<!-- 包资源文件 -->
<s:i18n name="cn/itcast/action/package">
    <s:text name="welcome">
        <s:param>john</s:param>
        <s:param>2010-01-01</s:param>
    </s:text>
</s:i18n>
<!-- action资源文件 -->
<s:i18n name="cn/itcast/action/MyAction">
    <s:text name="welcome">
        <s:param>john</s:param>
        <s:param>2010-01-01</s:param>
    </s:text>
</s:i18n>

国际化切换语言切换

单次请求参数中加入request_locale后,所有请求就进行了国际化切换。

http://localhost:8080/i18n?request_locale=en_US

ps:struts将值栈中locale和session中WW_TRANS_I18N_LOCALE设置成了参数中的值。

prepare.setEncodingAndLocale(request, response);

struts返回json数据

引入struts2-json-plugin插件,继承json-default包并将结果type设置为json

<package name="json" namespace="/" extends="json-default">
    <action name="myAction-*" class="com.xpress.action.MyAction" method="{1}">
        <result name="json" type="json">
            <!-- root指定根元素,不指定默认action为根 -->
            <param name="root">userList</param>
        </result>
    </action>
</package>

整合spring

  • 导入jar

  • struts2.xml

<!-- 由spring创建action -->
<constant name="struts.objectFactory" value="spring"/>

action配置直接引用pring的bean名称,不使用全限定名


<action name="*_*" class="{1}Action" method="{2}">
</action>
  • applicationContext.xml增加包扫描,或者使用bean标签配置,不使用注解
<context:component-scan base-package="com.xpress.service,
                                        com.xpress.action,
                                        com.xpress.dao,
                                        com.xpress.model,
                                        com.xpress.advice"/>
  • action注解形式
@Controller("usersAction")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)// 多例模式
public class UsersAction extends ActionSupport {

源码分析

官方架构图

FilterDispatcher在新版本中为StrutsPrepareAndExecuteFilter

Struts2-Architecture.png

流程图

struts调用流程.jpg

方法调用

  • org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter
    • init()方法加载配置文件包含自己创建的配置和struts自带的配置
      • init_DefaultProperties(); org/apache/struts2/default.properties
      • init_TraditionalXmlConfigurations(); DEFAULT_CONFIGURATION_PATHS:struts-default.xml,struts-plugin.xml,struts.xml
      • init_LegacyStrutsProperties(); stuts.properties, struts.custom.properties
      • init_CustomConfigurationProviders(); 用户自定义的配置加载器
      • init_FilterInitParameters(); filter的init参数配置
      • init_AliasStandardObjects(); 初始化标准类
    • doFilter()
      • prepare.wrapRequest() 增强request的getAttribute()方法,获取值栈对象并放入域对象
      • execute.executeAction()
        • dispatcher.serviceAction(request, response, mapping);
          • valueStackFactory.createValueStack(stack) 创建值栈
          • ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method, extraContext, true, false); 创建代理对象
          • proxy.execute() 执行
            • invocation.invoke()
              • interceptors.hasNext() 便利并调用interceptor
                • interceptor.intercept(DefaultActionInvocation.this);
                • invokeActionOnly() interceptor调用完成后调用action
                • executeResult() 执行结果

以上概念总结于传智播客Struts2课程

Search

    Post Directory