涉及的知识:
a) MVC和Struts2
b) Struts2的部署和Struts的配置文件(struts.xml)
c) Action的生命周期
d) Action中访问web对象
e) VauleStack和OGNL
f) Struts2拦截器、数据转换器和国际化资源
g) Struts2标签库和模版技术
1、struts2 in Action
(1)到struts2官网下载struts-2.1.8.1-all.zip。解压缩得到:
src目录:struts2的源代码。
lib目录:struts2的jar包和所依赖的jar包。
docs目录:struts2的官方文档。
apps目录: 是官方示例,可以运行的web应用。
具体设置参考blank.war中的应用。注意:要删除没用的插件(如xxx-plugin-2.1.8.1.jar)
(2)struts.xml
struts2的默认属性位于struts2-core-2.1.8.1jar包org/apache/struts2下面的default.properties里。用户可以在项目的/WEB-INF/classes下添加struts.properties覆盖默认的配置。
struts2的配置文件主要是struts.xml,struts.properties文件的内容基本上都可以在struts.xml文件中配置。
struts-default包是配置的包的父包,此包定义在struts2-core-2.1.8.1.jar包中的根目录下的文件struts-default.xml中。
struts2使用filter作为分发器。若有多个filter,要把struts2的分发器filter作为最后一个filter。
struts2的FilterDispatcher能截获所有URI。若URI以.action结尾,struts2会查找对应的action或jsp。
struts2.xml中:
<bean>配置常用的POJO类。
<default-action-ref name=""></default-action-ref> 用于当输入的action在struts.xml中找不到时,就访问默认的action name;
<constant name="struts.i18n.encoding" value="utf-8">设置程序运行时用的编码方式。相当于调用request.setCharacterEncoding("uft-8");
(3)<pacakge>元素
用来把逻辑上相关的一组action、result、intercepter等元素封装起来形成一个独立模块。
namespace用来防止不同的package中的action重名冲突,配置了namespace后,访问action时需要添加namespace作为action的前缀。
abstract用来定义抽象包的,即不能包含Action定义。抽象包用来被其它包继承。这样,子package里的action能使用父package里的资源。
自定义的package一般继承自struts-default。这里面包含了struts2默认的拦截器,如从request中获取数据并设置到action等工作都是由拦截器完成的。
(4)调用非execute方法
a、在<action>元素中配置method属性。缺点是需要为每个方法都定义一种action名称(即action name属性)。
b、在请求的URL中直接指定,如action名!方法名。或action名!方法名.action。
(且需在struts.xml文件中配置<constant name="struts.enable.DynamicMethodInvocation" value="true" /> )
(5)通配符
struts2支持class属性和method属性使用来自name属性的通配符。
如:<action name="*_*" class="org.cendy.action.{1}Action" method="{2}">
<result name="success">/hello.jsp</result>
</action>
*表示长度不为0的任意字符串。
若页面使用Hello_create.action作为访问的Action名称,此时struts.xml中Action名称为Hello_create,第一个通配符匹配Hello,第二个通配符匹配create。即由org.cendy.action.HelloAction的create方法来响应。
注:struts2首先会找到并使用有精确匹配的<action>元素!若没有精确匹配的<action>元素,则struts2会找到第一个匹配的使用通配符的<action>元素来使用。
又如:<action name="*_*_*_*" class="org.cendy.action.{1}Action" method="{2}">
<result name="{3}">/{4}.jsp</result>
</action>
若页面使用Hello_login_ok_welcome.action作为访问的action名称,此时等价于:
<action name="Hello_login_ok_welcome" class="org.cendy.action.HelloAction" method="login">
<result name="ok">/welcome.jsp</result>
</action>
在实际项目中,通常使用如下配置,如:
<action name="service_*_*" class="{1}ServiceAction" method="{2}">
<result name="{1}_{2}">/{1}/{1}_{2}.jsp</result>
</action>
(6)ResultType(在struts2中,同一个web应用,可同时使用多种不同的视图技术,如jsp、freemarker、velocity、jfreechart等)
ResultType分为预定义和自定义两种情况,用来打造自己的视图技术。
在struts2中,预定义了很多ResultType,实质就是定义了很多展示结果的技术。struts2把内置的<result-type>都放在struts-default包中。每一个<result-type>元素都是一种视图技术或跳转方式的封装。
对于dispatcher,除了可配置jsp,也可配置servlet;如:<result name="aaa">/login</ result>。但不能访问其他web应用中的web资源。
对于redirect,可以配置Jsp,也可配置servlet;(可以传参,如:<result name="aa" type="redirect">/hello.jsp?aaa=${name}</result> 其中${name}是使用OGNL来引用参数,即action中的属性)。可以访问其他web应用中的web资源。
对于chain,用来将action执行完后链接到另一个action中继续执行,新的action使用上一个action的上下文actionContext,数据也会被传递。chain不能在result配置时传递参数,因为这里要求配置的是要链接的action的name,不能传递参数。
对于freemarker,用来处理结果页面为使用FreeMarker制作的页面。配置ftl文件。 注:FreeMarker对应的FTL文件,不是放在WebContent下,而是放在classes路径下。在eclipse开发时,放在src下,eclipse会自动将其编译到classes路径中。 ftl页面可以使用形如${account}的EL表达式。
对于velocity,用来处理velocity模板,将velocity模板转化为数据流的形式输出。
对于xslt,用来处理XML/XLST模板,将结果转换为XML输出。
对于stream,用来向浏览器进行流式输出。
设置FreeMarker类型的Result为默认的Result。只需要在包中覆盖对FreeMarker类型的声明,设置default属性为true即可。如:
<package name="hello" extends="struts-default">
<result-types>
<result-type name="freemarker" class="org.apache.Struts2.views.freemarker.FreemarkerResult" default="true" />
</result-types>
</package>
开发自定义的Result
a、写一个类实现com.opensymphony.xwork2.Result接口。如MyResult。若想获取更多要展示的值,可通过ActionInvocation获取AcitonContext,在ActionContext里封装着所有需要的值。
b、在struts.xml中配置使用MyResult。如:
<package name="hello" extends="struts-default">
<result-types>
<result-type name="MyResult" class="org.cendy.action.MyResult" default="false" />
</result-types>
<action name="hello" class="org.cendy.action.HelloAction">
<result name="aaa" type="MyResult">/hello.jsp</result>
</action>
</package>
困难的是如何展示数据。
(7)Result
局部result:<result>元素作为<action>的子元素出现。
全局result:<result>元素作为<global-results>的子元素出现,让多个action共享。
搜寻result的顺序:
a、先找自己的<action>元素内的<result>元素是否有匹配的。
b、再找自己的<action>所在的包的全局result,看是否有匹配的。
c、再递归的寻找自己的包的父包、祖父包的全局result是否有匹配的。
d、若都没有匹配,则抛出Exception。
动态结果:
用action的一个属性来保存一个result的内容(即要显示的那个页面);这个属性的值自己确定;在struts.xml里通过${}将此属性的值取出来,用于显示是哪个页面;
${}表示:专门用在struts.xml配置文件里去读Value Stack里面的内容。
传递参数:
客户端跳转才需要传递参数,其它跳转基本用不上;
${}表达式,这种表达式只用在struts.xml配置文件中从value stack里取值;
一次request只有一个value stack; 采用action forward,两个action共享一个value stack; 两次request是不共享一个value stack的。
(8)Action
ActionSupport中实现了其他的方法,如数据校验等,继承ActionSupport的好处是可以直接使用数据校验等struts2集成的方法。
struts2的action不一定要实现Action接口。只要action具有public String execute()方法就行。若struts2发现action没有实现Action接口,会通过反射调用execute()方法。
struts2还可以执行Action的其它方法,只要这些方法没有参数,并返回String类型。这些方法可以有throws声明,也可以没有。
(9)Action的数据(action的生命周期:struts2为每个请求都重新初始化一个Action实例)
在action中,接收用户请求的数据时,可以不用HttpServletRequest,直接通过属性来获取。
在向下一个页面传值时,可以不用HttpServletRequest。在下一个页面,直接通过OGNL表达式从属性获取所需要的值。
在struts2中,页面的数据和action有两种基本对应方式:属性驱动和模型驱动。
属性驱动分为两种情况:
a、一种是基本数据类型的属性对应,称为属性驱动;
b、一种是JavaBean风格的属性对应,称为直接使用域对象。即将属性和对应的setter和getter方法从action中移出,单独做成一个域对象,这个对象用来封装这些数据,然后在action中直接使用这个对象即可。此时action中需要自己去new域对象,且需要写getter/setter方法,且在页面中name属性的值需要添加一个域对象的前缀,指明这个值对应到哪个域对象里面去。(推荐使用) 注:struts2不需要显式new一个域对象,若没有对象,会在运行时通过反射实例化一个对象。
(user.xxx 只有传,才会构造,且必须要有一个没有参数的构造方法,因为自动构造的时候,不知道调用哪个构造方法;想初始化domain model,可以自己new,也可以传参数值,如user.age=8,这样会调用user的构造方法,但此时需要保持参数为空的user构造方法。)
模型驱动:基本实现方式是让action实现一个ModelDriven接口,这个接口需要我们实现一个getModel方法,此方法返回的就是action所使用的数据模型对象,此时action中不需要有getter/setter方法,页面也不需要添加域对象的前缀。缺点是限制了一个Action对应多个数据模型的能力。(当属性和模型都能同时匹配同一个值时,模型驱动优先!)
如:private User user;
public Object getModel() {
return user;
}
在模型驱动模式下,将请求参数和处理结果放在模型中封装,而不是在Action的属性中!
若action中属性采用基本类型,如int,当用户在页面没有填写值时会报错。若改为Integer类型就不会报错。
页面传入一组数目不确定的字符串,如:
<input type="checkbox" name="habits" value="sports">运动,
<input type="checkbox" name="habits" value="reading">读书,
<input type="checkbox" name="habits" value="sleep">睡觉,
action中可以写private String[] habits;或private List<String> habits;
页面传入一组数目不确定的域对象,如:
<input type="text" name="users.account" >,
<input type="text" name="users.password" >,
<input type="text" name="users.account" >,
action中可以写private List<UserModel> users;action接收到的是4个对象,分别拥有一个属性的值。
(10)自动类型转换
从request里接收的值都是String类型的,而Action的属性可以是各种类型的。因此需要Struts2的类型转换机制来支持。
使用自定义的类型转换器:页面上传入的String类型的值,action的属性可以是各种类型的变量来接收(类型转换器接收String类型的值后注入给action的属性)。用struts2标签输出action的属性值时(如<s:property value="rectangle"/>),会再次调用转换器变成String类型值输出。
自定义类型转换器:
a、写一个类,继承org.apache.Struts2.util.StrutsTypeConverter抽象类。实现两个方法:
convertFromString(),实现字符串向对象的转换。其中context表示转换上下文,可以在里面引用各种对象,如通过context.get(ValueStack.VALUE_STACK)来引用值栈。values表示用户输入的字符串。toClass表示将要被转换成的对象类型。
convertToString(),实现对象向字符串的转换。其中context表示转换上下文,o表示需要被转换的对象。
b、注册此自定义的类型转换器(为action配置转换器)。
在src下建立一个xwork-conversion.properties文件,这个文件中用“全类名=这个类对应的类型转换器全类名”,来建立类和类型转换器的关系。
如:org.cendy.Rectangle=org.cendy.convert.RectangleConverter
c、action不需要做任何的处理,即使用自定义类型转换器时,对action没有影响。
action中:如private Rectangle rectangle;
全局级类型转换器引用:
在src下建立一个xwork-conversion.properties文件,这个文件中用“全类名=这个类对应的类型转换器全类名”,来建立类和类型转换器的关系。这样注册后,整个项目的Rectangle类都会使用RectangleConverter自定义类型转换器来处理。
类级类型转换器引用:(在action类上注册它的某个属性使用哪个类型转换器)
删掉xwork-conversion.properties文件以保证只有类级类型转换器起作用。
在使用了RectangleConverter的action同包下建立一个action名-conversion.properties文件,里面用“属性名=属性引用的类型转换器”的形式,来指定属性和它引用的类型转换器。
如在ConvertAction-conversion.properties文件中加入 :
rectangle=org.cendy.convert.RectangleConverter
使用类级类型转换器引用时,只对这个文件名指定的action有效,对其它action无效。
类级类型转换器的引用会覆盖全局级类型转换器的引用。
(11)ActionContext、值栈和OGNL
struts2在每个action刚开始运行时,都会单独为它建立一个ActionContext,把所有能访问的数据,包括请求参数(request的parameter)、请求的属性(request的attribute)、会话(session)信息等,都放到ActionContext中。ActionContext被认为是每个action拥有的一个独立的内存数据中心。
OGNL(对象图导航语言),是一种强大的表达式语言(EL)。可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。
值栈可用来容纳多个对象,主要用来存放一些临时对象。 struts2的标签是到值栈里取值。
当有请求到达时,struts2会为每个请求创建一个新的值栈。不同的请求,值栈也不一样。值栈封装了一次请求所有需要操作的相关的数据。可供action、result、interceptor等struts2的其它部分使用。struts2把action的所有属性都放在value stack里。value stack本身是放在request里的。
值栈能够线程安全地为每个请求提供公共的数据存取服务。
狭义值栈,指的是实现com.opensymphony.xwork2.util.ValueStack接口的对象,即OgnlValueStack对象。主要用来存取动态EL运算需要的值和结果。OgnllValueStack对象主要用来支持OGNL运算的。
狭义值栈中存放着一些OGNL可以存取访问的数据,如:
a、action实例,这样可以通过OGNL来访问action实例中的属性值。
b、OGNL表达式运算的值,可以设置到值栈中,可以主动访问值栈对象,强行设置。
c、OGNL表达式产生的中间变量,如struts2用循环标签,会有循环变量,这些存放在值栈中。
广义值栈指的是ActionContext对象。ActionContext是一个容器,是线程安全的,是action运行的上下文。
ActionContext里存放很多值,如:
a、request的parameters:请求中的参数。这里数据的变化不会改变请求对象里面的参数值的。
b、request的attribute:请求中的属性,这里是个Map。
c、session的attribute:会话中的属性,这里是个Map。
d、application的attribute:应用中的属性,这里是个Map。
e、value stack:也就是狭义的值栈。ActionContext以及value stack作为被OGNL访问的根。OGNL在没有特别指明情况下,访问的就是value stack里面的数据。
f、attr:在所有的属性范围中获取值,依次搜索page、request、session和application。
获取ActionContext有两个基本方法:
a、在不能获取到ActionInvocation的地方,直接使用ActionContext一个静态的getContext方法;
ActionContext ctx = ActionContext.getContext();
b、在能获取到ActionInvocation的地方,如在拦截器里、自定义的Result里等,都可以通过ActionInvocation获取到ActionContext。
ActionContext ctx = actionInvocation.getInvocationContext();
Value Stack(通常向valuestack里压入值是由struts2完成,访问valuestack多是通过标签中的OGNL表达式,直接使用valuestack并不多)
由ActionContext对象的getValueStack()方法获取。
peek():获取value stack中的顶层对象,不修改valuestack对象。
pop():获取value stack中的顶层对象,并把这个对象从valuestack中移走。
OGNL的基本使用(OGNL有一个特点:当没有值或取不到值,就会不显示,但不会报错;)
a、使用OGNL访问value stack
在OGNL中,没有前缀代表了访问当前值栈。如<s:property value="account"/>,其中value属性的值使用的ognl,它没有任何前缀,表示直接访问值栈。
第一个页面中,name使用account;
action中,有一个属性为account;
b、常量与操作符
如:<s:property value="7+8">,输出是15。 string常量用单引号或双引号括起来。如<s:proerty value="'aa'">,其中aa是个字符串,因为是‘aa’。
c、使用OGNL访问ActionContext
在OGNL中,通过符号#来访问ActionContext中除了值栈外的各种值。
如:<s:proerty value="#parameters.name">,当前请求中的参数,对应request.getParameterValues("name"),一般使用#parameters.name[0];
<s:proerty value="#request.name">,请求作用域中的属性,对应request.getAttribute("name");
<s:proerty value="#session.name">,会话作用域中的属性,对应session.getAttribute("name");
<s:proerty value="#application.name">,应用程序作用域的属性。
<s:proerty value="#attr.name">,按照(页面page、)request、session和application的顺序,返回第一个符合条件的属性。
d、访问静态方法和静态属性
在OGNL中,通过@访问任意类中的静态方法和静态属性。
格式为:<s:property value="@org.cendy.action.ActionTest@name">,<s:property value="@org.cendy.action.ActionTest@getName()">
在高版本的struts2中,如2.1.8版本,默认是关闭了访问类的静态方法的。要想访问类的静态方法,需要开启设置。
<constant name="struts.ognl.allowStaticMethodAccess" value="true">
e、访问域对象
<s:property value="user.name">
第一个页面中:name属性为user.name;
action中:有一个属性为User类的user对象;
f、访问Lits或数组
<s:property value="userlist[0].name">
也可以访问数组的length属性,或访问集合的方法。
直接在OGNL中构建集合,如OGNL表达式为:{1,2,3}[0],表示构建一个包含3个值的集合,然后获取索引为0的值,即1。
第一个页面中:name属性为userlist[0].name;
action中:private List<User> userlist = new AarryList<User>();
g、访问Map
格式为:Map名称['键的名称']来访问Map中的值。或者Map名称.键的名称来访问Map中的值。
如:<s:proerty value="userMap['u1'].name">
也可以Map的方法。
直接在OGNL中构建Map,如OGNL表达式为:#{'one':'aa','two':'bb'}['one'],表示构建一个包含两组值的Map,然后获取key为one的值,即aa。
第一个页面中:name属性为userMap['u1'].name;
action中:private Map<String, User> userMap = new HashMap<String, User>();
总结:(在action中可以不用自己去new,要有getter、setter方法!)
JavaBean:用.来访问自己的属性。
数组或list:用[索引]来访问自己的第几个元素。
Map:用['key']来访问自己的键为key的元素。
(12)struts2的Taglib(URI指定taglib在什么地方)
jsp页面需引入:<%@ taglib uri="/struts-tags" prefix="s"%>
标签是在jsp页面初始化时被调用,然后生成相应的HTML代码。
struts2标签库主要使用OGNL语言。从struts 2.0.11起,struts2标签库不再支持EL表达式,即用${}获取各种Jsp资源。
struts2的标签都统一包含在struts-tags.tld文件中(位于struts2-core-2.1.8.1.jar的META-INF文件夹下),都使用统一的前缀。标签库“/struts-tags”位于struts2的struts2-core-2.1.8.1.jar包中,不需要复制到/WEB-INF/下,直接使用即可。
struts2标签大致分为四类:
a、数据标签。用来从值栈上取值或向值栈赋值。
property标签:用来输出OGNL表达式的值。也用于输出一个变量或变量的属性,也可输出当前的action的属性。 转义:就是把HTML中的一些特殊字符用实体进行替换。
set标签:定义一个变量,并赋值。 如:<s:set var="i" value="1"/> <s:property value="#i"/>(使用变量需要在前面加上#号)
date标签:用来格式化输出一个日期数据。如:<s:date name="#request.d" format="yyyy-MM-dd HH:mm:ss"/>,前提:request.setAttribute("d",new java.util.Date());注: s:date标签使用时,一定要是Date类型
debug标签:可帮助进行调试,它在页面上生成一个链接,单击这个链接可查看ActionContext和值栈中所有能访问的值。如:<s:debug/>
b、控制标签。控制程序的运行流程。
<s:if test="">
</s:if>
<s:elseif test="">
</s:elseif>
<s:else>
</s:else>
if标签和elseif标签都只有一个test属性,它本身就是一个OGNL表达式,运算结果为一个boolean值,表示是否符合条件。如:
<s:if test="libBean.searchMode == 'warn'">
</s:if>
iterator标签用来处理循环,可以用它遍历数组、set和list等集合对象。
<s:iterator value="userlist" id="u">
<s:property value="#u.name"/>
</s:iterator>
或者:
<s:iterator value="userlist" >
<s:property value="name"/>
</s:iterator> (iterator标签在循环时,会把当前正在循环的对象放到值栈的栈顶)
或者:
<s:iterator value="userlist" id="u">
${u.name}
</s:iterator>
value:用来指明到底循环的是谁,这个属性的值是OGNL表达式,用来访问ActionContext和值栈中需要被循环的对象;
id:变量名称,用来引用存放到值栈的被循环的对象。
status属性:
<s:iterator value="userlist" status="state">
索引=<s:property value="#state.index"/>
</s:iterator>
<s:if test="#state.even">
...
</s:if>
<s:else>
.......
</s:else>
c、UI标签。用来显示UI页面的标签,多会生成HTML。
d、其它标签。如生成URL和输出国际化文本等。
struts2标签支持主题,标签会根据设置的主题自动布局页面。使用Ajax主题,该主题会自己加载ajax功能所需的js类库(也就是struts2内置的DOJO库)。
ognl相比el,有如下优点:
a、能访问对象的方法;
b、 能访问静态方法和静态属性;
c、操作集合类对象;
d、访问ognl上下文和actonContext(所有的servlet资源)
ognl主要有三个符号:#、$、%。
#声明ognl表达式,主要有三种用途:
a、访问ognl上下文和actonContext(所有的servlet资源),相当于ActionContext.getContext(),如#request、#session、#application、#parameters、#attr;
b、用于过滤或筛选集合,如books.{?#this.price<20},表示所有的price<20的书。
c、构造map。如#f{'foo1':'bar1','foo2':'bar2'}
%{}显式声明ognl表达式:%用于表示某字符串为一个ongl表达式。某些标签中既能接受字符串,又能接受ognl表达式。此时,标有%的被当做ognl表达式并被执行,没有标%的被当做普通字符串。
${}在资源文件中引用ognl表达式:
$主要用于在资源国际化文件中或者struts.xml文件中引用ognl表达式。如在国际化文件中:aaa=${getText(name)} is aaa.
(13)ActionContext和ServletActionContext
ThreadLocal,也称为线程局部变量,它为每一个使用该变量的线程都提供一个变量值副本,使每一个线程都可以独立改变自己的副本,而不会和其它线程的副本冲突。
ServletActionContext利用ThreadLocal维护不同线程的Servlet对象,如HttpServletRequest、HttpServletResponse、Session、Application。
通过DI的方式获取相应的Map对象(实现三个接口):
使用SessionAware接口来获取包装会话对象的attribute中的值的Map;
使用RequestAware接口来获取包装请求对象的attribute中的值的Map;
使用ApplicationAware接口来获取包装ServletContext对象的attribute中的值的Map;
使用ParameterAware来获取包装请求对象的参数中的值的Map;
使用CookieAware来获取包装cookie中的数据的Map;
在Action中要获取到包装了session、servletContext等的Map,需要用到ActionContext。 通过其静态方法,可取得Map类型的session、servletContext对象。
在Action中要能获取到Servlet相关的API,需要用ServletActionContext(主要负责获取servlet对象)。通过其静态方法,它可以取得的对象有:HttpServletRequest、HttpServletResponse、ServletContext、PageContext。通过HttpServletRequest对象可以得到HttpSession对象。
通过DI的方式获取相应的servlet对象:
通过ServletRequestAware接口获取HttpServletRequest对象,
通过ServletResponseAware接口获取HttpServletResponse对象。
通过ServletContextAware接口获取ServletContext对象。
在Action中,优先使用ActionContext,只有ActionContext不能满足功能要求时,才使用ServletActionContext。尽量让Action与web无关。
在使用ActionContext时,不要在action的构造函数里使用ActionContext.getContext(),此时ActionContext里的一些值也许还没有设置,此时通过ActionContext取得的值是Null.
struts2框架将与web相关的很多对象重新进行了包装,如将HttpSession对象重新包装成了一个Map对象,里面放着session中的数据,提供这个Map给action使用,让action可以和web层解耦。
(14)拦截器(拦截器在服务器启动时,会调用其init()方法进行初始化)
每个struts2工程一定使用了拦截器。可以实现多种功能:
a、帮用户把request参数设置到action的属性中。
b、实现上传文件。
c、防止重复提交。
d、实现验证框架。
e、通用错误处理。
f、程序国际化。
Interceptor比filter具有更强大的功能,如拦截器与servlet的API无关,拦截器可以访问到值栈。
struts2的预定义拦截器都定义在struts-default.xml文件的struts-default包内。
<interceptor>元素用来定义一个拦截器。仅仅是定义,还没有任何一个action引用它。
<interceptor-stack>元素定义了一个拦截器栈,这个栈中可以引用其它已定义好的拦截器。拦截器栈简化了action在引用拦截器时的操作。
使用预定义拦截器:
a、在struts.xml的action配置中,引用需要使用的拦截器。(<interceptor-ref>)
b、在<package>中引用包内所有的action都使用的拦截器。(<default-interceptor-ref>)
拦截器的调用顺序:
a、看<action>元素有没有<interceptor-ref>子元素。<interceptor-ref>元素中的name属性不仅可以写一个拦截器名字,还可以出现一个更小的拦截器栈的名字!
b、看<action>所在的包有没有声明默认的拦截器引用,即<package>元素的<default-interceptor-ref>子元素。
c、递归寻找这个包的父包,看有没有声明默认的拦截器引用。
注:这3个位置的定义是覆盖的关系。
可以在<package>元素中定义一个自定义的拦截器栈。然后用<default-interceptor-ref>来引用此拦截器栈。
自定义拦截器:用户自己定义并实现的拦截器,不是由struts2定义好的拦截器。
a、写一个实现com.opensymphony.xwork2.interceptor.Interceptor接口的类MyInterceptor。 其中init()用于初始化相关资源,destroy()用于释放资源,intercept()是拦截器执行的处理方法,用户要实现的功能主要写在此方法中。
对于intercept()方法,要注意:
首先,在intercept()中写“invocation.invoke();”,这句话的意思是继续运行拦截器后续的处理,若此拦截器后面还有拦截器,那么会继续执行,一直到运行action,然后执行result。
其次,若intercept()中没有写“invocation.invoke();”,表示对请求的运行处理到此为止,不再继续向后运行了。即后续的拦截器和action就不再执行了。而是在这里返回result字符串,直接去进行result处理了。
在“invocation.invoke();”,这句话之前写的功能,会在action运行之前执行。
在“invocation.invoke();”,这句话之后写的功能,会在result运行之后执行。
interceptor方法的返回值就是最终要返回的result字符串,这个只是在前面没有执行result时才有效,也就是前面没有“invocation.invoke();”这句话的时候,这个返回值就相当于最终要返回的result字符串,然后才执行相应的result处理。
b、在struts.xml里配置拦截器的声明和引用。
在<package>元素中声明:
<interceptors>
<interceptor name="myInterceptor" class="org.cendy.action.MyInterceptor" />
</interceptors>
在<action>中引用:
<action name="hello" class="org.cendy.action.HelloAction">
<result >/hello.jsp</result>
<interceptor-ref name="myInterceptor"/>
<interceptor-ref name="defaultStack"/>
</action>
向拦截器传参:
当同一个拦截器为不同的action服务时,需要根据传入的参数进行处理上的变化。
a、在引用拦截器时设置参数。
如:
<interceptor-ref name="myInterceptor">
<param name="dbOrFile">db</param>
</interceptor-ref >
在这里,通过<param>子元素来为拦截器注入参数,名称要与拦截器里的属性匹配上。
b、在定义拦截器时设置参数。
<interceptors>
<interceptor name="myInterceptor" class="org.cendy.action.MyInterceptor" >
<param name="dbOrFile">db</param>
</interceptor>
</interceptors>
(15)验证框架
做一个成熟、稳定的web应用,无论如何服务器端验证不可缺少。不要完全相信客户端传递过来的数据,在调用业务逻辑前要对数据进行校验。
(16)分模块配置
一个项目会根据业务的不同来划分出不同的模块。在划分配置文件时,可以按照模块来划分。
一个<package>元素代表一个业务模块。
根据不同的模块编写不同的struts-XXX.xml文件,最后通过struts.xml文件引入,如<include file="struts-XXX.xml"></include>。
(17)struts2的异常映射
局部异常映射:在<action>元素中设置<exception-mapping>元素,可指定当action中的方法出现异常时,跳转到哪个指定的页面。若找不到符合条件的<exception-mapping>元素,会把异常抛给web容器。
全局异常映射:在<global-exception-mappings>元素中设置<exception-mapping>元素。配置了<global-exception-mappings>自然要配置<global-results>,且<global-results>必须在<global-exception-mappings>之前。
局部异常映射和全局异常映射的查找顺序与搜寻result的顺序类似。
当action中的方法抛出错误时,若其<action>元素配置了<exception-mapping>子元素,则会按顺序寻找,找到第一个符合条件的<exception-mapping>元素,跳转到其指定的Result。
使用了异常机制后,action中的方法不需要自己去try-catch,原来对应的多个catch块,实际上都变成了<exception-mapping>的配置。
在页面输出异常信息:
<s:property value="exception"/>:简单打印出exception对象的例外信息。
<s:property value="exceptionStack"/>:打印出exception的堆栈信息。
(18) PreResultListener
监听的事件是Action执行完毕,马上要开始执行result了这么一个事件。
需要一个实现PreResultListener的类,在类中实现相应的事件回调方法。需要在事件触发前注册监听器。
a、写一个类实现PreResultListener;
b、在action的方法中注册这个监听器对象。如:
PreResultListener pr = new MyPreResult();
ActionContext.getContext().getActionInvocation().addPreResultListener(pr);
(19)国际化
可以通过native2ASCII工具把中文转换成unicode编码。
向国际化信息传入参数:首先从properties文件中取得相应的信息,然后通过MessageFormat.format()方法进行传参。properties文件中用{0}、{1}....来进行占位。
如:Locale l = Locale.SIMPLIFIED_CHINESE;
ResourceBundle bundle = ResourceBundle.getBundle("message",l);
String message = bundle.getString("name");
String[] params = new String[2];
params[0] = "aaa";
params[1] = "bbb";
String info = MessageFormat.format(message, params);
使用标签<s:text>访问国际化信息,name属性为key。可以使用子标签<s:param>为国际化信息传参。
可以使用<s:i18n name="">来为<s:text>标签指定国际化信息的来源。全局级国际化信息资源,直接用文件前缀名,如message;包级国际化信息资源,用这个包的全限定名,如org.cendy;类级国际化信息资源,用这个类的全限定名,如org.cendy.TestAction。
设置struts2引用国际化信息资源文件,只要在struts.xml中设置一个常量:<constant name="struts.custom.i18n.resources" value="message">,其中message是文件名的前缀。即message.properties是默认语言文件,message_zh_CN.properties是中文语言文件,message_en_US.properties是英文语言文件。
<contant name="struts.locale" value="zh_CN"/> 此时,无论怎么修改浏览器的语言设置,Struts2都会去访问中文信息。
由用户选择语言:用户只要在提交请求时加上reqeust_locale参数,并提供对应的值就可以自由选择语言了。因为i18n拦截器在action运行前会检查请求中是否包含了reqeust_locale的参数,若存在此参数,就会用它的值建立一个Locale对象,并用这个Locale对象去覆盖struts2的常量设置和浏览器的语言设置。只需传入一次request_locale参数,session就会记住用户的选择,整个网站都变成用户选择的语言了。
用户指定参数>struts.xml中配置常量>浏览器设置。
全局级资源文件:只需要在struts.xml中设置常量值struts.custom.i18n.resources即可。如::<constant name="struts.custom.i18n.resources" value="message">,其中message是一组国际化资源文件名的前缀。
包级资源文件:如package.properties,package_zh_CN.properties,package_en_US.properties。必须放在当前访问的action的同包下。
类级资源文件:如action类名.properties,action类名_zh_CN.properties,action类名_en_US.properties。
资源文件的覆盖顺序:类级(action类名.properties,和类放在一起)>包级(package.properties,放在action这个类的同包下)>全局级(message.properties,放在src下)。
在action中访问国际化信息(此action要继承ActionSupport类):
a、getText(),访问默认的国际化信息;
b、getText(),访问默认的国际化信息并传入参数值;
c、getTexts()得到ResourceBundle,然后通过此bundle.getString("key"),访问指定国际化信息来源中的信息;
struts2默认加载WEB-INF/classes下的资源文件。
程序国际化的主要思想:
程序界面中需要输出国际化信息的地方,不要在页面中直接输出信息,而是输出一个key值。该key值在不同语言环境下对应不同的字符串。当程序需要显示时,程序将根据不同的语言环境,加载该key对应该语言环境下的字符串。
(20)
要想测试完整的struts2的运行流程,应该在测试中获取到ActionProxy对象,通过它来获取ActionInvocation对象,然后运行ActionInvocation对象就可以依次调用拦截器、action、result等组件。
做单元测试,所需jar包为blank.war中lib的所有jar包以及struts2.1.8.1/lib下的4个jar包:struts2-junit-plugin-2.1.8.1.jar、spring-core-2.5.6.jar、spring-test-2.5.6.jar、commons-logging-1.0.4.jar。
测试用例中:
a、继承的父类不同,继承来自Struts2-junit-plugin-2.1.8.1.jar包中的StrutsTestCase;
b、不新建action对象,而是通过父类的getActionProxy("url")方法获得ActionProxy对象。其中url为在struts.xml中配置的,如"/testAction"。
c、传入参数方式不同。以前直接在action对象上赋值就行。现在要新建一个map,设置action所需要的参数,然后把这个map和ActionContext的parameters关联上。
(21)ActionMapper会判断请求是否应该被struts2处理。
ActionInvocation对象描述了action运行的整个过程。
ActionInvocation对象执行的时候:
a、首先按照拦截器的引用顺序依次执行各个拦截器的前置部分。
b、然后执行action的方法。
c、根据action的方法返回的结果,即result,在struts.xml中匹配选择下一个页面。
d、找到页面后,(现在的页面是模板页面),在页面上,通过struts2自带的标签库访问需要的数据,并生成最终页面。(jsp也是一种模板页面技术)
e、最后ActionInvocation对象按照拦截器的引用顺序的倒序依次执行各个拦截器的后置部分。
ActionInvocation对象执行完毕后,实际已经得到响应对象了,即HttpServletResponse对象。最后,按与过滤器配置定义相反的顺序依次经过过滤器,向用户展示响应的结果。
2、整合Struts2和spring
Action的生命周期由spring管理,即当struts2需要action时,不是自己创建,而是向spring请求获取实例。
(1)先复制spring需要的jar包,然后复制struts2需要的jar包。其中struts2-spring-plugin-2.1.8.1.jar(struts2的spring插件)是必须的。
(2)编写spring的配置文件applicationContext.xml文件。加入相应Action的<bean />。
如:<bean id=”LoginAction” class=”yaso.struts.action.LoginAction” scope="prototype">
<property name=”loginDao” ref=”LoginDao”/>
</bean>
(3)在web.xml中引用spring配置文件。
其中listener实现了当这个web工程启动时,就去读取spring配置文件。这个类由spring提供。
上下文参数的配置里,contextConfigLocation的值classpath*:applicationContext.xml,表示所有出现在classpath路径下的applicationContext.xml文件,都是listener要读取的spring配置文件。若存在多个Spring配置文件,则在<param-value>中依次列出,之间以逗号隔开。
如:
<!--设置上下文本地配置文件的路径-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/classes/applicationContext.xml
</param-value>
</context-param>
<!-- 装载Spring上下文 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
(4)修改struts.xml。
a、添加常量struts.objectFactory,其值为spring。表示struts2使用action时不是自己去创建,而是向spring请求获取action实例。
如:<constant name="struts.objectFactory" value="srping"/>
b、<action>元素的class属性,不需要输Action类的全类名,而是填一个在spring配置文件中配置的Action的Bean的名字,即<bean>元素的id属性。
如:<action name=”LoginAction” class=”LoginAction”>
<result name=”success”>/index.jsp</result>
</action>
还有一种方法:
业务类在Spring配置文件中配置,业务逻辑控制器类不需要配置,Struts2的Action像没有整合Spring之前一样配置,<action>的class属性指定业务逻辑控制器类的全限定名。
业务逻辑控制器类中引用的业务类不需要自己去装配,Struts2的Spring插件会使用bean的自动装配将业务类注入进来,其实业务逻辑控制器也不是Struts2创建的,而是Struts2的Spring插件创建的。默认情况下,插件使用by name的方式装配。
可以通过增加Struts2常量来修改匹配方式:设置方式为:struts.objectFactory.spring.autoWire = typeName,
可选的装配参数如下:
a) name:等价于Spring配置中的autowire=”byName”,这是缺省值。
b) type:等价于Spring配置中的autowire=”byType”。
c) auto:等价于Spring配置中的autowire=”autodetect”。
d) constructor:等价于Spring配置中的autowire=” constructor”。
如果原先在Struts2中使用了多个object factory,则需要通过Struts2常量显式指定object factory,方式如下:struts.objectFactory = spring;如果没有使用多个object factory,这一步可以省略。
3、TIPS
(1)使用DTD来获得XML帮助
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
其中-//Apache Software Foundation//DTD Struts Configuration 2.0//EN 可认为是struts的DTD对应的ID,包括组织名、项目名、版本、语种。
http://struts.apache.org/dtds/struts-2.0.dtd是这个DTD在网上发布的地址。
若eclipse当前处于网络可用,则eclipse会自动下载这个DTD,存在工作空间内,这样编写XML时,eclipse会有代码提示和帮助。
若网络不可用,
a、首先在struts 2-core-2.1.8.1.jar中找到struts-2.0.dtd。
b、eclipse中,window——preference——web and xml——xml catalog中,点击add,location选择此dtd的实际位置,key type选择public ID,将DTD声明中的ID复制到key文本框中(即-//Apache Software Foundation//DTD Struts Configuration 2.0//EN)。
(2)对AJAX的支持
流行的AJAX框架如Dojo、DWR。struts2可通过插件形式使用JSON,也可使用struts2提供的Ajax JSP tags。若Ajax JSP tags的功能不能满足需要,可以直接使用原始的Ajax技术,也可以直接使用JQuery等ajax框架。
使用名为Stream的ResultType可以实现ajax的功能:
在action中定义流类型的属性,如:private InputStream inputStream; 在action的方法中,可以为inputStream赋值,这样后台的数据就能输出到前台,由ajax来接收和处理这些数据。如:inputStream = new ByteArrayInputStream("输入正确".getBytes("utf-8"));
struts2直接提供一个能直接向客户端返回一个stream的result,直接把后台处理后的数据输出到前台,然后由ajax来接收和处理这些数据。其中
<result type="stream">
<param name="contentType">text/html</param>
<param name="inputName'>inputStream</param>
</result>
配置的参数contentType是指返回的数据类型,而inputName配置的是要返回的流,这个值和action中定义的流类型的属性相对应。
要使用struts2.1的Ajax Tags(非重点,略):
a、把dojo插件,即struts 2-dojo-plugin-2.1.8.1.jar文件,复制到WEB-INF/lib文件夹下;
b、在页面上增加taglib的引用,<%@taglib prefix="sx" uri="/struts-dojo-tags"%>
c、在每个页面的顶部包含head标签。
使用JSON插件:(服务器将action中的属性封装为json对象,然后返回给客户端)
要使用JSON插件,只需把struts 2-json-plugin-2.1.8.1.jar文件复制到WEB-INF/lib里面即可。这个插件提供了名为json的ResultType,若设置某个result类型为json,那么json插件会把这个action对象序列化为一个json格式的字符串,然后向客户端返回这个字符串。
struts.xml配置有所不同:首先不再继承struts-default,而是改为继承json-default,其次result的type要设置为json。 其中<param name="root">user.name</param>,表示根据ognl表达式取出用户需要输出的结果的根对象。
页面上,获取action的返回值后,需要通过eval方法把json字符串变为一个js对象。
json字符串格式:如{"name":'aa',"id":2}
var dd = {}; 其中dd表示是一个对象,可以为对象赋值,即dd["gender"]='male'。
(3)struts2实现文件上传(Struts2直接将上传的文件封装为File对象)
需要使用struts2预定义的fileUpload拦截器。
若在一个表单中,包含了文件上载的表单域,一定要把整个表单enctype属性设置为multipart/form-data,method属性设置为post。且表单中的文件域的name名称要与action中的File类型的属性名相同。
如:表单中包含一个name属性为xxx的文件域,在action中,使用File xxxx,封装文件域对应的文件内容;使用String xxxFileName接收上传文件的文件名;使用String xxxContentType接收上传文件的文件类型。
可以将上传的文件存储在web服务器上,甚至数据库中。
在struts.xml文件中引用fileUpload拦截器时,可以指定参数,用来限制文件的大小、类型及上传的文件的扩展名。若上传的文件不满足以上参数指定的条件,会跳转到一个叫input的<result>上。如:
<interceptor-ref name="fileUpload">
<param name="allowedTypes">image/bmp,image/gif,image/x-png</param>
<param name="maximumSize">20480</param>
</interceptor-ref >
<constant name="struts.multipart.maxSize" value="10000000"/>,设置上传文件的最大值约为10MB。(默认上传文件的大小是不能超过2097152字节的)
在一个表单中上传多个文件:只要在提交页面上添加同名的多个文件输入域,然后在action中对应使用File类型数组和String类型数组去接收这些参数即可。
(4)struts2实现文件下载
会用到struts2的stream类型的result。这种result最终会返回一个InputStream,只需要让这个InputStream能读到用户想要下载的文件即可。
使用stream类型作为Result的action和普通的action有很大的不同,它不需要execute方法,而是action中需要一个公有的、返回InputStream的getInputStream方法,用来返回文件的内容。如:
public InputStream getInputStream() throws Exception {
File file = new File("e:/temp/aa.doc");
return new FileInputStream(file);
}
在struts.xml中配置,<result type="stream"><param name="contentDisposition"">attachment;filename="aa.doc"</param></result>。表示在访问这个result时,会弹出一个下载框,其中默认文件名为aa.doc。
让下载显示的文件名称不是写死的两种方法:
a、在action中提供一个返回文件名称的方法,然后在配置文件中引用这个变量; 如:方法名为getDownloadFileName(),在配置文件中引用${downloadFileName}。
b、直接在action中提供一个getContentDisposition的方法,不需要在struts.xml中配置关于contentDisposition了。如:
public String getContentDisposition() {
return "attachment;filename=\"test.doc\"";
}
处理中文文件名:在action里动态设置文件名称时,对其按照“ISO8859-1”进行编码。即
public String getContentDisposition() {
String s = new String("测试".getBytes(),"ISO8859-1");
return "attachment;filename=\""+s+".doc\"";
}
(5)struts2中使用token防止重复提交
在form表单中加入<s:token/>标签。当此页面初始化时,会向session中写入一个值作为记录。
然后,必须为action手动引用名为token的拦截器。<inteceptor-ref name="token">。当表单提交时,拦截器会比较请求中的token值和session中的token值是否相同,一致则移除session中的token值,然后执行execute()方法。不一致,则重定向到name为invalid.token的result。
让重复提交看起来好像没有发生过,浏览器最终跳转到正常提交指定的result:
把token拦截器变为tokenSession拦截器即可。此时不需要配置名为invalid.token的result。
tokenSession拦截器判断某个请求为重复请求后,不是立即重定向到名为invalid.token的result,而是先阻塞这个重复请求,直到浏览器响应最初的正常请求,然后就可以跳转到处理正常请求后的result了。
提交完成后,单击浏览器的“刷新”按钮,浏览器会弹出对话框,询问是否重新提交数据。单击“是”,浏览器会重新提交数据。
(6)在action中可直接使用枚举类型,其对应的参数值只需要对应枚举定义时的定义名即可。
如:
action中:private ColorEnum color;
枚举定义为:public enum ColorEnum{
red,blue,green;
}
访问的URL为:http://localhost/helloAction.action?color=red。
此时在action中可以看到,color == ColorEnum.red,为true。
(7)execAndWait 执行等待拦截器
若某个action执行时间较长,浏览器会因为长时间等待服务器响应而长时间显示空白。execAndWait拦截器接收请求后,能判断上一个请求是否处理完,若已经处理完毕,则会显示结果页面,否则,显示等待页面。在struts.xml中,需配置result的name为wait的等待页面。
(8)JFreeChart使用
JFreeChart是一个实现图形化报表的开源框架。它封装了各种图形化的报表模型,使用户不用关心如何画图形报表,只需要专注显示什么样的数据,直接把数据对象交给JFreeChart,由它来生成图形化的报表。
到JFreeChar官网下载jfreechart-1.0.13.zip。
解压缩后,可看到两个文件夹:
lib:放了JFreeChart的jar包和它所依赖的jar包。
tests:放了官方以junit方式实现的单元测试的用例。可以将其看成官方示例。
使用JFreeChart,要引入jfreechart-1.0.13.jar和jcommon-1.0.16.jar。
struts2结合JFreeChart:
只需引入struts2-jfreechart-plugin-2.1.8.1.jar即可。
a、实现action
JFreeChart作为一种结果类型,可以作为action的Result。action不需要execute方法,而是一个getChart方法。如:
public JFreeChart getChart() {
return chart;
}
action中的getDataset方法提供要显示的数据,getChart方法用来设置图的相关信息,如图的字体,标题的字体等。
b、配置struts.xml
<package name="hello" extends="jfreechart-default">
<action name="chartAction" class="org.cendy.chart.ChartAction" >
<result type="chart">
<param name="width">400</param>
<param name="height">300</param>
</result>
</package>
给<result>元素设置<param>子元素来限定图形化报表的宽和高。
(8)struts2采用热部署的方式注册插件,即如果向struts2中添加插件,直接把jar文件添加到lib中即可,不需要配置任何文件。
在启动web容器时,struts2有一个运行时配置,会按照以下顺序加载配置文件 :
a、struts-default.xml文件,此文件在struts2-core-版本号.jar中。
b、struts-plugin.xml文件,在所有的插件包中,即struts2-XXX-plugin-版本号.jar。
c、struts.xml文件,在web应用的classpath根目录中。
因此我们可以像引用struts-default包一样来引用struts-plugin.xml文件中的其它包。
(9)使用java注解来代替struts.xml进行配置
所有可以引用的注解都在org.apache.Struts 2.convention.annotation这个包中。
注意:注解不能完整代替struts.xml,而是代替了其中action部分的配置信息。
(7) 在servlet的request对象里,有如下数据:
a、参数区(parameter),就是用户在页面上填写并提交的数据;
b、Head区,由浏览器在发出请求时,自动加入到请求包得数据;
c、属性区(attribute),由开发人员存储在属性区的值。
d、cookie区,由浏览器在发出请求时,自动把相关的cookie数据通过request传递到服务端。
(9) 更改jsp默认编码ISO Latin-1为chinese National Standard,方法是:window——preferences——jsp。
(10)读源码的方法:
先运行起来;顺线读,可以运用debug跟踪;这条线一直到尾读下来,就能够知道整个系统的架构。
从页面开始,后台找!
(11)要考虑的第一个问题是:界面原型、设计数据库、要用什么样的架构(平台)、到底用什么样的约定(编码、配置时要规定一个约定,用文字表述) 。
(12)朴素编程原理——写程序是从一个小程序开始,一点点加,一边加一边调试一边确定结果;写项目也是这样,先做原型(整个程序的一个小小的原型,没有那么多功能,要尽早写出来),然后在此原型的基础之上不断的添加新东西;——迭代式开发,更能应付需求的变化;
瀑布式模型,不自然:开发一个项目,先把所有细节都规定好,直接开发到最后才出现结果;
迭代式的做实验; 做实验,要重新写;做一点,加一点;加一点,做一点;
(13)jsp的include:一种是<%@include> 静态包含;另一种是<jsp:include>动态包含,即将jsp的执行结果包含在本页面内;
(14)学习新知识,阅读文档和提供的源代码; 懂概念和原理。
(15)The Struts dispatcher cannot be found. This is usually caused by using Struts tags without the associated filter. Struts tags are only usable when the request has passed through its servlet filter, which initializes the Struts dispatcher needed for this tag.
解决方法:
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注意<url-pattern>这里一定要是 /*
(16)action中的属性,可以在struts.xml文件中的<action>标签中使用<param>元素赋值!
(17) ServletContext.getResourceAsStream();
ServletContext.getRealPath();
(18)Action类的成员属性,也可能是封装了Action需要传入下一个JSP页面中显示的属性的值,这些值被封装在ValueStack对象中。
ValueStack vs = (ValueStack)request.getAtrribute("struts.valueStack");
属性值=vs.findValue("属性名");
(19)Action类的validate()方法会在执行execute()方法之前执行。若执行该方法后,Action类的fieldErrors中已包含了错误,请求将被转发到input逻辑视图处。
addFieldError("u", getText("user.required"));其中getText()来自于ActionSupport基类的方法。
(20)webwork中创建控制器代理的方式,是AOP编程方式。
http请求——>webwork的核心控制器——>Action代理——>拦截器——>Action——>Result——>JSP——>拦截器——>http响应。
(21)拦截器的类定义在struts-default.xml文件中。若继承了struts-default默认的包名就可使用了,否则必须在自己的包中定义拦截器,在<interceptors>中进行定义。
转自:http://www.jishuziyuan.com/archive/cenedy_69576750/8022058.html