首页 > 代码库 > Struts2龙之总结
Struts2龙之总结
一、Struts2执行流程:
1.客户端初始化一个指向servlet容器(tomcat)的请求;
2.这个请求经过一系列过滤器(Filter);
3.接着StrutsPrepareAndExecuteFilter被调用,StrutsPrepareAndExecuteFilter询问ActionMapper来决定这个请求是否需要调用某个Action;
4.如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy;
5.ActionProxy通过Configuration Manager询问框架的配置文件,找到需要调用的Action类;
6.ActionProxy调用invoke方法创建一个ActionInvocation的实例。
7.递归调用,判断是否还有下一个拦截器,当没有了下一个拦截器,就执行Action的业务方法。
8.一旦Action执行完毕,ActionInvocation负责根据struts.xml中的配置找到对应的返回结果。返回结果通常是(但不总是,也可能是另外的一个Action链;
9.返回结果的时候依旧经过一系列拦截器。
请求 ---- StrutsPrepareAndExecuteFilter 核心控制器 ----- Interceptors 拦截器(实现代码功能,只访问action时执行 ) ----- Action 的execuute --- 结果页面 Result
二、在web.xml 配置struts2 前端控制器 (Filter)
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
三、struts2 常量配置 (键值对 properties)
struts2 默认常量 在 default.properties 中配置
常用常量
<constant name="struts.i18n.encoding" value="http://www.mamicode.com/UTF-8"/> ----- 相当于request.setCharacterEncoding("UTF-8"); 解决post请求乱码
<constant name="struts.action.extension" value="http://www.mamicode.com/action,,"/> --- 访问struts2框架Action访问路径 扩展名 (要求)
* struts.action.extension=action,, 默认以.action结尾扩展名 和 不写扩展名 都会分发给 Action
<constant name="struts.serve.static.browserCache" value="http://www.mamicode.com/false"/> false不缓存,true浏览器会缓存静态内容,产品环境设置true、开发环境设置false
<constant name="struts.devMode" value="http://www.mamicode.com/true" /> 提供详细报错页面,修改struts.xml后不需要重启服务器 (要求)
<action>的name 和 <package>的namespace属性 共同决定 Action的访问路径 !!!!!!!!
例如 :
<package name="usermanager" namespace="/user" extends="struts-default">
<action name="hello" class="cn.itcast.struts2.demo1.HelloAction">
访问路径 /user/hello.action
3) <action> 元素配置默认值
<package> 的namespace 默认值 /
<action> 的class 默认值 ActionSupport 类
<result> 的 name 默认值 success
<action> 的method默认值execute
4、 struts2 配置文件分离
通过 <include file="struts-part1.xml"/> 将struts2 配置文件 拆分
分离配置文件的原则:按模块进行分离,再统一组装到struts.xml (<include>标签)
五、 Action的访问
HTTP请求 提交 Struts2 StrutsPrepareAndExecuteFilter 核心控制器 ------ 请求分发给不同Action
1、 让请求能够访问Action ----- Action书写方式 三种
第一种 Action可以是 POJO ((PlainOldJavaObjects)简单的Java对象) ---- 不需要继承任何父类,实现任何接口
* struts2框架 读取struts.xml 获得 完整Action类名
* obj = Class.forName("完整类名").newInstance();
* Method m = Class.forName("完整类名").getMethod("execute"); m.invoke(obj); 通过反射 执行 execute方法
第二种 编写Action 实现Action接口
Action接口中,定义默认五种 逻辑视图名称
public static final String SUCCESS = "success"; // 数据处理成功 (成功页面)
public static final String NONE = "none"; // 页面不跳转 return null; 效果一样
public static final String ERROR = "error"; // 数据处理发送错误 (错误页面)
public static final String INPUT = "input"; // 用户输入数据有误,通常用于表单数据校验 (输入页面)
public static final String LOGIN = "login"; // 主要权限认证 (登陆页面)
* 五种逻辑视图,解决Action处理数据后,跳转页面
第三种 编写Action 继承ActionSupport (推荐)
在Action中使用 表单校验、错误信息设置、读取国际化信息 三个功能
2、 Action中方法调用
1) 在配置 <action> 元素时,没有指定method属性, 默认执行 Action类中 execute方法
<action name="request1" class="baoming.demo3.RequestAction1" />
2) 在<action> 元素内部 添加 method属性,指定执行Action中哪个方法
<action name="regist" class="baoming.demo4.RegistAction" method="regist"/> 执行 RegistAction 的regist方法
3) 使用通配符* ,简化struts.xml配置
<a href="http://www.mamicode.com/${pageContext.request.contextPath }/user/customer_add.action">添加客户</a>
<a href="http://www.mamicode.com/${pageContext.request.contextPath }/user/customer_del.action">删除客户</a>
struts.xml
<action name="customer_*" class="baoming.struts2.demo4.CustomerAction" method="{1}"></action> --- {1}就是第一个* 匹配内容
Action 构造方法调用几次?是否为单例
Action的生命周期?
因为每次请求都会调用构造方法,说明它是多实例的,不存在线程并发访问时的冲突问题
Struts1会不会存在线程安全问题?Struts1是单例的,会存在线程并发访问时的安全问题
3、 动态方法调用 (零配置路线)
访问Action中指定方法,不进行配置
1) 在工程中使用 动态方法调用 ,必须保证 struts.enable.DynamicMethodInvocation = true 常量值为true
2) 在action的访问路径 中 使用 "!方法名"
页面
<a href="http://www.mamicode.com/${pageContext.request.contextPath }/user/product!add.action">添加商品</a>
配置
<action name="product" class="bao.struts2.demo4.ProductAction"></action>
执行 ProductAction 中的 add方法
六、在Action中使用Servlet API
1、 在Action 中解耦合方式 间接访问 Servlet API --------- 使用 ActionContext 对象
在struts2 中 Action API 已经与 Servlet API 解耦合 (没有依赖关系 )
* Servlet API 常见操作 : 表单提交请求参数获取,向request、session、application三个范围存取数据
actionContext = ActionContext.getContext();
actionContext.getParameters(); 获得所有请求参数Map集合
actionContext.put("company", "数据"); / actionContext.get("company") 对request范围存取数据
3) actionContext.getSession(); 获得session数据Map,对Session范围存取数据
actionContext.getSession().put("key","value");
4) actionContext.getApplication(); 获得ServletContext数据Map,对应用访问存取数据
actionContext.getApplication().put("key","value");
*本质分析是否存在线程安全问题? 不会有 答案原码:ActionContext (使用ThreadLocal存储相关的值)
3、 在Action中直接通过 ServletActionContext 获得Servlet API
ServletActionContext.getRequest() : 获得request对象 (session)
ServletActionContext.getResponse() : 获得response 对象
ServletActionContext.getServletContext() : 获得ServletContext对象
* 静态方法没有线程问题,ThreadLocal
七、 结果页面的配置
Action处理请求后, 返回字符串(逻辑视图名), 需要在struts.xml 提供 <result>元素定义结果页面
1、 局部结果页面 和 全局结果页面
<action name="result" class="bao.struts2.demo6.ResultAction">
<!-- 局部结果 当前Action使用 -->
<result name="success">/demo6/result.jsp</result>
</action>
<global-results>
<!-- 全局结果 当前包中 所有Action都可以用-->
<result name="success">/demo6/result.jsp</result>
</global-results>
2、 结果页面跳转类型
* 在struts-default.xml 定义了 一些结果页面类型
* 使用默认type 是 dispatcher 转发 (request.getRequestDispatcher.forward)
1) dispatcher :Action 转发给 JSP
2) chain :Action调用另一个Action (同一次请求)
<result name="success" type="chain">hello</result> hello是一个Action的name
3) redirect : Action重定向到 JSP
<result name="success" type="redirect">hello.action</result> hello是一个Action的name
4) redirectAction :Action重定向到另一个Action
<result name="success" type="redirectAction">hello</result>
Action : 可以作为模型,也可以是控制器
属性驱动 和 模型驱动
第一种 :Action 本身作为model对象,通过成员setter封装 (属性驱动 )
页面:
用户名 <input type="text" name="username" /> <br/>
Action :
public class RegistAction1 extends ActionSupport {
private String username;
public void setUsername(String username) {
this.username = username;
}
}
* struts2 Action 是多实例
struts1 Action 是单例的
第二种 :创建独立model对象,页面通过ognl表达式封装 (属性驱动)
页面:
用户名 <input type="text" name="user.username" /> <br/> ----- 基于OGNL表达式的写法
Action:
public class RegistAction2 extends ActionSupport {
private User user;
public void setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
问题: 谁来完成的参数封装
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor"/>
第三种 :使用ModelDriven接口,对请求数据进行封装 (模型驱动 ) ----- 主流
页面:
用户名 <input type="text" name="username" /> <br/>
Action :
public class RegistAction3 extends ActionSupport implements ModelDriven<User> {
private User user = new User(); // 必须手动实例化
public User getModel() {
return user;
}
}
* struts2 有很多围绕模型驱动的特性
* <interceptor name="modelDriven" class="com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor"/> 为模型驱动提供了更多特性
对比第二种、第三种 : 第三种只能在Action中指定一个model对象,第二种可以在Action中定义多个model对象
<input type="text" name="user.username" />
<input type="text" name="product.info" />
属性驱动和模型驱动都是框架的拦截器来帮助实现的。
数据类型转换问题
1、 struts2 内部提供大量类型转换器,用来完成数据类型转换问题
boolean 和 Boolean
char和 Character
int 和 Integer
long 和 Long
float 和 Float
double 和 Double
Date 可以接收 yyyy-MM-dd格式字符串 (yyyy/MM/dd不能转)
数组 可以将多个同名参数,转换到数组中
集合 支持将数据保存到 List 或者 Map 集合
案例: 输入合法年龄和生日可以自动转换
当输入abc 转换为 int类型 age时
Caused by: java.lang.NoSuchMethodException: bao.struts2.demo3.CustomerAction.setAge([Ljava.lang.String;
分析: 输入20 ,转换 int类型20 --- setAge(int)
输入abc,转换int 出错 ---- setAge(String) ----- 报错方法不存在异常
2、 自定义类型转换器 (了解)
1) 自定义类型转换器
第一种 实现TypeConverter接口
convertValue(java.util.Map<java.lang.String,java.lang.Object> context, java.lang.Object target, java.lang.reflect.Member member, java.lang.String propertyName, java.lang.Object value, java.lang.Class toType)
第二种 继承 DefaultTypeConverter
convertValue(java.util.Map<java.lang.String,java.lang.Object> context, java.lang.Object value, java.lang.Class toType)
第三种 继承 StrutsTypeConverter
convertFromString(java.util.Map context, java.lang.String[] values, java.lang.Class toClass) --- 请求封装
convertToString(java.util.Map context, java.lang.Object o) --- 数据回显
类型转换器 一直都是双向转换
页面提交请求参数,封装到model --- 需要转换
model数据 需要在页面 回显 ---- 需要转换
2) 以 1990/10/10 为例,自定义日期转换器,完成转换 第二种方法
public Object convertValue(Map<String, Object> context, Object value,
Class toType) {
// 根据toType判断 是请求封装 还是 数据回显
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
if (toType == Date.class) {
// 请求参数封装 (value是字符串)
String[] params = (String[]) value;
String strVal = params[0]; // 转换为 日期类型
try {
return dateFormat.parse(strVal);
} catch (ParseException e) {
e.printStackTrace();
}
} else {
// 回显(value是 Date)
Date date = (Date) value;
return dateFormat.format(date);
}
return null;
}
3) 注册类型转换器
局部注册 : 只对当前Action有效 (针对属性)
全局注册 : 针对所有Action的日期类型有效 (针对类型 )
局部注册 : 在Action类所在包 创建 Action类名-conversion.properties , 格式 : 属性名称=类型转换器的全类名
全局注册 : 在src下创建 xwork-conversion.properties ,格式 : 待转换的类型=类型转换器的全类名
3、 类型转换中错误处理
通过分析拦截器作用,得知当类型转换出错时,自动跳转input视图 ,在input视图页面中 <s:fieldError/> 显示错误信息
* 在Action所在包中,创建 ActionName.properties,在局部资源文件中配置提示信息 : invalid.fieldvalue.属性名= 错误信息
校验器
1、 校验的分类 : 客户端数据校验 和 服务器端数据校验
客户端数据校验 ,通过JavaScript 完成校验 (改善用户体验,使用户减少出错 )
服务器数据校验 ,使用框架内置校验功能 (struts2 内置校验功能 ) ----- 必须的
2、 struts2 支持校验方式
代码校验 :在服务器端通过编写java代码,完成数据校验
配置校验 :XML配置校验(主流) 和 注解配置校验
4、 XML配置方式 数据校验 (企业主流校验)
代码校验 不适用于大型项目, 流程数据复杂时,开发量和维护量 都会很大
xml配置校验原理 : 将很多校验规则代码已经写好,只需要在xml中定义数据所使用校验规则就可以了
步骤一 :编写jsp
步骤二 :编写Action 继承ActionSupport 或者 实现 Validateable 接口
步骤三 :封装请求参数
* 使用xml校验 必须提供get方法
步骤四 :编写校验规则xml文件
在Action所在包 编写 Action类名-validation.xml 对Action所有业务方法进行校验
引入DTD
------ xwork-core-2.3.7.jar 中 xwork-validator-1.0.3.dtd
<!DOCTYPE validators PUBLIC
"-//Apache Struts//XWork Validator 1.0.3//EN"
"http://struts.apache.org/dtds/xwork-validator-1.0.3.dtd">
内置校验器定义文件
xwork-core-2.3.7.jar 中 /com/opensymphony/xwork2/validator/validators/default.xml
内建校验器
* required (必填校验器,要求被校验的属性值不能为null)
* requiredstring (必填字符串校验器,要求被校验的属性值不能为null,并且长度大于0,默认情况下会对字符串去前后空格)
* stringlength (字符串长度校验器,要求被校验的属性值必须在指定的范围内,否则校验失败,minLength参数指定最小长度,maxLength参数指定最大长度,trim参数指定校验field之前是否去除字符串前后的空格)
* regex (正则表达式校验器,检查被校验的属性值是否匹配一个正则表达式,expression参数指定正则表达式,caseSensitive参数指定进行正则表达式匹配时,是否区分大小写,默认值为true)
* int(整数校验器,要求field的整数值必须在指定范围内,min指定最小值,max指定最大值)
* double(双精度浮点数校验器,要求field的双精度浮点数必须在指定范围内,min指定最小值,max指定最大值)
* fieldexpression (字段OGNL表达式校验器,要求field满足一个ognl表达式,expression参数指定ognl表达式,该逻辑表达式基于ValueStack进行求值,返回true时校验通过,否则不通过)
* email(邮件地址校验器,要求如果被校验的属性值非空,则必须是合法的邮件地址)
* url(网址校验器,要求如果被校验的属性值非空,则必须是合法的url地址)
* date(日期校验器,要求field的日期值必须在指定范围内,min指定最小值,max指定最大值)
案例
required 必填校验器
<field-validator type="required">
<message>性别不能为空!</message>
</field-validator>
requiredstring 必填字符串校验器
<field-validator type="requiredstring">
<param name="trim">true</param>
<message>用户名不能为空!</message>
</field-validator>
stringlength:字符串长度校验器
<field-validator type="stringlength">
<param name="maxLength">10</param>
<param name="minLength">2</param>
<param name="trim">true</param>
<message><![CDATA[产品名称应在2-10个字符之间]]></message>
</field-validator>
int:整数校验器
<field-validator type="int">
<param name="min">1</param>
<param name="max">150</param>
<message>年龄必须在1-150之间</message>
</field-validator>
date: 日期校验器
<field-validator type="date">
<param name="min">1900-01-01</param>
<param name="max">2050-02-21</param>
<!-- OGNL表达式在配置文件中的写法-->
<message>生日必须在${min}到${max}之间</message>
</field-validator>
url: 网络路径校验器
<field-validator type="url">
<message>数据的主页地址必须是一个有效网址</message>
</field-validator>
email:邮件地址校验器
<field-validator type="email">
<message>电子邮件地址无效</message>
</field-validator>
regex:正则表达式校验器
<field-validator type="regex">
<param name="expression"><![CDATA[^13\d{9}$]]></param>
<message>手机号格式不正确!</message>
</field-validator>
fieldexpression : 字段表达式校验
<field-validator type="fieldexpression">
<param name="expression"><![CDATA[(password==repassword)]]></param>
<message>两次密码输入不一致</message>
</field-validator>
如何对指定的方法校验 ??? 格式 Action类名-ActionName(<action>元素name属性)-validation.xml
例如 : 校验AddCustomerAction中execute方法 配置 <action name="addcustomer" ...method="regist"/>
校验文件名字: AddCusotmerAction-addcustomer-validation.xml
国际化信息显示
1、 国际化原理 ? 什么是国际化 ?
同一款软件 可以为不同用户,提供不同语言界面 ---- 国际化软件 internationalization --i18n 本地化localization l10n
需要一个语言资源包(很多properties文件,每个properties文件 针对一个国家或者语言 ,通过java程序根据来访者国家语言,自动读取不同properties文件 )
2、 资源包编写
properties文件命名 : 基本名称_语言(小写)_国家(大写).properties
例如 :
messages_zh_CN.properties 中文中国
messages_en_US.properties 英文美国
4、 struts2 框架国际化配置
第一种 全局国际化信息文件 (所有Action都可以使用 ) ------- 最常用
* properties文件可以在任何包中
* 需要在struts.xml 中配置全局信息文件位置
struts.xml
<constant name="struts.custom.i18n.resources" value="http://www.mamicode.com/messages"></constant> messages.properties 在src根目录
<constant name="struts.custom.i18n.resources" value="http://www.mamicode.com/bao.resources.messages"></constant> messages.properties 在 bao.resources 包
国际化信息
在Action中使用 : this.getText("msg");
在jsp中使用 :<s:text name="msg" />
在配置文件中(校验xml) : <message key="agemsg"></message>
自定义拦截器
拦截器 的使用 ,源自Spring AOP(面向切面编程)思想
拦截器 采用 责任链 模式
* 在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。
* 责任链每一个节点,都可以继续调用下一个节点,也可以阻止流程继续执行
在struts2 中可以定义很多个拦截器,将多个拦截器按照特定顺序 组成拦截器栈 (顺序调用 栈中的每一个拦截器 )
拦截器与拦截器栈的区别?
拦截器:<interceptor> 拦截器栈:<interceptor-stack>
从功能上看,拦截器栈本身就是一个功能更加强大的拦截器
从结构上看,拦截器栈是由多个拦截器有序组织在一起
配置一个拦截器:<interceptor>
拦截器栈:<interceptor-stack>
在栈中引入一个定义好的拦截器<interceptor-ref name=""/>
定义默认的拦截器栈:<default-interceptor-ref name=""/>
类的写法
1、 struts2 所有拦截器 都必须实现 Interceptor 接口
2、 AbstractInterceptor 类实现了 Interceptor 接口. 并为 init, destroy 提供了一个空白的实现
所有实际开发中,自定义拦截器 只需要 继承 AbstractInterceptor类, 提供 intercept 方法实现
3、 常用struts2 拦截器
<interceptor-ref name="modelDriven"/> 模型驱动
<interceptor-ref name="fileUpload"/> 文件上传
<interceptor-ref name="params"> 参数解析封装
<interceptor-ref name="conversionError"/> 类型转换错误
<interceptor-ref name="validation"> 请求参数校验
<interceptor-ref name="workflow"> 拦截跳转 input 视图
Oracle和MySQL 作为应用数据库区别
mysql存在数据库概念,在企业开发中,针对一个项目创建一个单独数据库,创建单独用户, 为用户授予数据库权限 ,
oracle 一个数据库就是一个服务,在这个库中可以存在很多用户,每个用户有单独表空间 ,针对一个项目,只需要创建一个用户
Struts2 框架 上传与下载
文件上传
1) 企业常用文件上传技术 : jspSmartUpload(主要应用 JSP model1 时代) 、 fileupload (Apache commons项目中一个组件)、 Servlet3.0 集成文件上传 Part类
2) 文件上传 enctype="multipart/form-data" 是 MIME协议定义多部分请求体 (消息体)
3) 上传页面编写
存在 <input type="file" name="upload"/> 上传项,必须提供name属性
表单提交方式 必须 post 提交
表单编码类型 enctype="multipart/form-data"
4) Struts2 对文件上传的支持
提供 FileUpload 拦截器,用于解析 multipart/form-data 编码格式请求,解析上传文件的内容
fileUpload拦截器 默认在 defaultStack 栈中, 默认会执行的
在Action需要对上传文件内容进行接收
页面:
<input type="file" name="upload" />
Action :
public class UploadAction extends ActionSupport {
// 接收上传内容
// <input type="file" name="upload" />
private File upload; // 这里变量名 和 页面表单元素 name 属性一致
private String uploadContentType;//MIME类型 image/jpeg text/html
private String uploadFileName;
}
* 格式 : 上传表单项name属性 + ContentType 、 上传表单项name属性 + FileName
* 为三个对象 提供 setter 方法
通过FileUtils 提供 copyFile 进行文件复制,将上传文件 保存到服务器端
5) Struts2 上传文件过程中错误处理
配置 input 视图 ,作为上传出错后 跳转页面
在文件上传时,如果发生错误 ,fileUpload拦截器 会设置错误信息,workflow拦截器 跳转到 input 视图
通过 struts.multipart.maxSize 常量设置文件上传总大小限制
* struts.multipart.maxSize=2097152 默认上传文件总大小 2MB
* 超过文件总大小,跳转input 视图, 通过 <s:actionError /> 回显错误信息
在struts.xml 设置上传总大小
<constant name="struts.multipart.maxSize" value="http://www.mamicode.com/20000000"></constant>
设置上传文件总大小,对所有上传form有效,只想对当前form进行设置,可以设置fileUpload拦截器属性
FileUpload 拦截器有 3 个属性可以设置.
* maximumSize: 上传文件的最大长度(以字节为单位), 默认值为 2 MB
* allowedTypes: 允许上传文件的类型, 各类型之间以逗号分隔 image/jpeg,text/html
* allowedExtensions: 允许上传文件扩展名, 各扩展名之间以逗号分隔
如果针对fileUpload 进行参数设置,当出错时,在页面通过 <s:fieldError /> 回显错误信息
文件下载
文件下载原理:
1.一个流 输出流
2.两个头 response.setHeader("Content-Type","application/octet-stream");
response.setHeader("Content-Disposition","attachment;filename=1.jpg");
1) struts2 完成文件下载,通过 结果集类型 (Result Type) stream 来完成的
struts-default.xml 定义 <result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/>
2) 使用Stream结果集 完成文件下载
文件下载原理: 服务器读取下载文件内容,通过Response响应流写回, 设置 ContentType、 ContentDisposition 头信息
public class StreamResult extends StrutsResultSupport {
protected String contentType = "text/plain"; // contentType头信息 (下载文件对应 MIME协议规定类型 )
* html --- text/html . txt--- text/plain
protected String contentDisposition = "inline"; // ContentDisposition头信息 (下载文件打开方式 inline浏览器内部打开, attachment 以附件形式打开)
protected String inputName = "inputStream"; // 需要Action中 提供 getInputStream 方法 返回 InputStream 提供下载文件 内容
}
Action 提供 InputStream 返回值 getInputStream 方法 ------- 指定下载文件流
配置 stream 结果集 参数 <param name="contentType">${contentType}</param> ---- 在Action 中提供 getContentType
* ServletActionContext.getServletContext().getMimeType(filename);
配置 stream 结果集 参数 <param name="contentDisposition">attachment;filename=${filename}</param> ---- 在Action 提供 getFilename
* 下载附件名乱码问题 , IE和火狐 解决不同
public String encodeDownloadFilename(String filename, String agent)
throws IOException {
if (agent.contains("Firefox")) { // 火狐浏览器
filename = "=?UTF-8?B?"
+ new BASE64Encoder().encode(filename.getBytes("utf-8"))
+ "?=";
} else { // IE及其他浏览器
filename = URLEncoder.encode(filename, "utf-8");
}
return filename;
}
Struts2龙之总结