笔记内容说明
Struts2(李翊老师主讲,占笔记内容100%);
目 录
1.10案例:修改1.6、1.9案例使用户不能绕过前端控制器 6
2.10案例:修改1.12案例(使用Struts2标签和OGNL表达式) 18
3.3在Action中访问Session和Application 19
3.6案例:重构NetCTOSS资费列表分页显示(使用属性注入) 23
4.2 Result组件利用<result>元素的type属性指定result类型 25
4.5 NetCTOSS项目:基于StreamResult生成验证码 27
4.6 NetCTOSS项目:基于JSONResult进行验证码检验 29
4.7 NetCTOSS项目:添加资费模块中的验证资费名是否重复 31
6.9案例:使用fileUpload拦截器实现文件上传 45
一、Struts2概述
1.1为什么要用Struts
1)JSP用HTML与Java代码混用的方式开发,把表现与业务逻辑代码混合在一起给前期开发与后期维护带来巨大的复杂度。
2)解决办法:把业务逻辑代码从表现层中清晰的分离出来。
3)2000年,Craig McClanahan采用了MVC的设计模式开发Struts主流的开发技术,大多数公司在使用。
1.2什么是MVC
1)M-Model 模型
模型(Model)的职责是负责业务逻辑。包含两部分:业务数据和业务处理逻辑。比如实体类、DAO、Service都属于模型层。
2)V-View 视图
视图(View)的职责是显示界面和用户交互(收集用户信息)。属于视图的类是不包含业务逻辑和控制逻辑的JSP。
3)C-Controller 控制器
控制器是模型层M和视图层V之间的桥梁,用于控制流程。比如我们之前项目中写的ActionServlet。
1.3 JSP Model 1和JSP Model 2
1)JSP Model 1
最早的JSP/Servlet模式被我们称之为JSP Model 1(JSP设计模式1),在其中有模型层M,但是视图层V的JSP中包含了业务逻辑或控制逻辑,JSP和Servlet紧密耦合。即:JSP(数据的展现和业务流程的控制)+ JavaBean(对数据的访问和运算,Model)。
2)JSP Model 2
JSP Model 2(JSP设计模式2)和JSP Model1不同之处再与将JSP中的业务逻辑和控制逻辑全部剔除,并全部放入控制层C中,JSP仅仅具有显示页面和用户交互的功能。
编程理念:高内聚低耦合。即:
M:Model,用JavaBean来做(对数据的访问和运算)
V:View,用JSP来作(数据的展现)
C:Controller,用Servlet来作(业务流程的控制)
1.4 Struts2发展史
1)最早出现的Struts1是一个非常著名的框架,它实现了MVC模式。Struts1简单小巧,其中最成熟的版本是Struts1.2。
2)之后出现了WebWork框架,其实现技术比Struts1先进,但影响力不如Struts1。
3)在框架技术不断发展的过程中,有人在WebWork的核心XWork的基础上包装了Struts1(算是两种框架的整合),由此,结合了Struts1的影响力和WebWork的先进技术,Struts2诞生了。
4)Struts2不是Struts1的升级,它更像是WebWork的升级版本。
1.5衡量一个框架的标准
1)健壮性:Struts2.0不健壮(带0的就是实验品),Struts2.1.8是健壮的(2.1.6也不健壮,该版本BUG较多),3颗星。
2)易用性:易用性越好,原理则越复杂,4颗星。
3)扩展性:Struts2,5颗星。
4)侵入性:即对写代码的要求(如:必须继承某个类才能怎么怎么样……),越低越好。Struts2低但也有侵入性,4颗星。
1.6 Struts2使用步骤
step1:创建一个JavaWeb Project,命名为struts01(Struts2只能用在Web项目里)
step2:拷贝Struts2的核心Jar包到WEB-INF/lib/下,基本功能核心jar包 5个:
1)xwork-core-2.1.6.jar:Struts2核心包,是WebWork内核。
2)struts-core-2.1.8.jar:Struts2核心包,是Struts框架的“外衣”。
3)ognl-2.7.3.jar:用来支持OGNL表达式的,类似于EL表达式,功能比EL表达式强大的多。
4)freemarker-2.3.15.jar:freemarker是比JSP更简单好用,功能更加强大的表现层技术,用来替代JSP的。在Struts2中提倡使用freemarker模板,但实际项目中使用JSP也很多。
5)commons-fileupload.jar:用于实现文件上传功能的jar包。
step3:在web.xml中配置Struts2的前端控制器,Struts2是用Filter(过滤器)实现的前端控制器,它只负责根据请求调用不同的Action
u 注意事项:原来的项目是用Servlet的方式作为控制器。
web.xml内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- 上面的内容是规定必须写的,复制粘贴即可 -->
<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>
</web-app>
step4:在WebRoot下新建jsp文件夹,并在其中创建nameform.jsp和welcome.jsp
1)nameform.jsp
<form action="welcome.action" method="post">
<input name="name" type="text" /><input value="提交" type="submit" />
<span style="color:red;">${error }</span>
</form>
2)Welcome.jsp
<h1>Welcome,${name}</h1>
step5:在com.tarena.action包下创建WelcomeAction类
private String name; private String error; ……各自的get/set方法
public String execute() { System.out.println("WelcomeAction.execute()...");
if(name==null||"".equals(name)){ error="用户名不能为空"; return "fail"; }
System.out.println("name: " + name);//用于测试
if ("test".equalsIgnoreCase(name)) { error="不能使用名字text登录";
return "fail";
}else{ return "success"; } }
step6:写Struts2所需要的控制器配置文件struts.xml,非常重要!该文件写请求与各个Action(Java类)的对应关系,前端控制器会根据配置信息调用对应的Action(Java类)去处理请求
u 注意事项:
v 在编写时struts.xml放在src目录中,而部署后该文件位于WEB-INF/classes/下!
v 同样的,.properties文件也放src下,部署后也在WEB-INF/classes/下!
v 程序一启动,struts.xml就会被加载到内存,并不是现读的。
struts.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN" "http://struts.apache.org/dtds/struts-2.1.dtd">
<!-- 以上内容是规定必须写的,复制粘贴即可 -->
<struts><!-- struts的根标签,也是规定,各个标签详解见1.7节 -->
<package name="helloworld" namespace="/day01" extends="struts-default" >
<action name="welcome" class="com.tarena.action.WelcomeAction">
<result name="success">/jsp/welcome.jsp</result>
<result name="fail">/jsp/nameform.jsp</result>
</action>
</package>
</struts>
u 注意事项:xml文件内容严格按照.dtd格式要求,如:<?xml...?>不在第一行可能会报错,标签中随意换行也会引起404!
step7:部署,地址栏输入:http://localhost:8080/struts01/day01/welcome.action进行测试,发现一进入页面,则错误提示信息已经显示!为了解决该问题,需要再定义一个<action>用于读取页面,而不是发请求后立即执行WelcomeAction类进行处理,处理应当在点击提交后才执行。
step8:在struts.xml中添加新的<action>仅仅用于读取页面
<action name="welcomeform"><!--不指明class,Struts2会自动创建一个类,详见1.7节-->
<result name="success">/jsp/nameform.jsp</result>
</action>
step9:重新部署,地址栏输入:http://localhost:8080/struts01/day01/welcomeform.action进行测试,发现问题已经解决,可正常执行
1.7 struts.xml内容详解
1)<package>:作用是为<action>分组,<struts>标签下可有多个<package>,而<package>标签有如下属性:
①name="helloworld":唯一的标识,表示包名为helloworld。
u 注意事项:在相同包里的不能重复,不同包的可以重复!
②namespace="/day01":用来设置该包中的action的地址的命名空间。
u 注意事项:
v 命名空间中的“/”要写!
v 也可以这么写:namespace="/",表示根命名空间,此时Action的访问路径为:http://localhost:8080/appName/actionName.action 即:http://localhost:8080/struts01/welcome.action
v 一般写法为:namespace="/day01",此时Action的访问路径为:http://localhost:8080/appName/namespace/actionName.action 即:http://localhost:8080/struts01/day01/welcome.action
③extends="struts-default":继承的包名,一般继承Struts2默认提供的struts-default包,该包中定义了很多Struts2应用必须的组件(比如:拦截器);该package声明的位置在struts2-core-2.1.8.jar/struts-default.xml文件中。
u 注意事项:
v appName是部署时项目的名字!
v 包只能继承包。不能说包继承某个类!
2)<action>:作用是指明每个请求对应的Action类之间的对应关系(一个<action>对应一个请求 ),<package>下可有多个<action>。而<action>标签有如下属性:
①name="welcome":表示请求的名字为welcome(即welcome.action)
②class="com.tarena.action.WelcomeAction":格式为“包名.类名”。指定了对应的Action类(Java类)来处理请求。class属性可以不写,不写则Struts2会默认为该<action>添加一个class(自动创建一个类,该类中的execute()方法只有一个return "success";语句),作用是转发到对应的<result name="success">中指定的页面。
③method="xxx":用于指定在对应的Action类中要执行的方法名,该方法的返回值必须是String类型(这是规定!!)
例如:public String xxx(){...return string;}//返回的string是<result>标签中某个name属性的值(结果的名字)!
若没有method属性或method="",则默认调用方法名为execute的方法,即:public String execute(){...return string}。
3)<result>:作用是指明执行相应的Action类之后,显示哪种结果页。而<result>标签有如下属性:
①name="success":是该result的名称,Action返回哪一个result的name值,意味着要转发到哪一个result 所对应的JSP地址,<result>不写name属性,则默认为success。
②type="":默认dispatcher(转发),还可写json、stream、……等10种
1.8 Struts2提供的方便之处
1)数据的自动封装(输入属性)
根据页面组件的name属性,自动封装到Action中对应的name属性中,即自动调用某属性的set方法。
例如:在JSP页面<input name="name" type="text" />,而在Action中则会自动给属性private String name赋值。
u 注意事项:必须有name属性对应的get和set方法。
2)数据的自动传递(输出属性)
Action中的属性在JSP页面可以直接用EL表达式拿到,即自动调用某属性的get方法。
例如:Action中的属性private String name;在JSP页面上可以直接${name}得到对应的值。
1.9案例:简单登录(使用Strust2)
step1:新建Web工程struts01,导入Struts2的五个基本核心包,并在web.xml中配置Struts2的前端控制器,web.xml代码,见1.6中step3
step2:在WebRoot下创建jsp文件夹,并放入loginform.jsp和welcome.jsp
1)loginform.jsp
<form action="/struts01/day01/login.action" method="post"><!--使用的是绝对路径-->
<table><tr><td>用户名:</td><td><input name="name" type="text" /></td></tr>
<tr><td>密码:</td><td><input name="pwd" type="password" /></td></tr></table>
<input value="提交" type="submit" />
<span style="color:red;">${errorMsg }</span><!-- 接收错误信息 -->
</form>
u 注意事项:
v 应用名前的“/”要写。
v <form>标签中的action属性只写action="login.action"也可!如果写成action="day01/login.action",则会出现叠加问题!可参考Spring笔记12.2节。
2)welcome.jsp
<h1>Welcome,${name}</h1>
step3:在包com.tarena.action下新建LoginAction类
private String name; private String pwd; private String errorMsg;
……各自的get/set方法
public String execute(){
if("chang".equals(name)&&"123".equals(pwd)){//模拟登录成功
return "success";// 与struts.xml某一个<result>对应,用来生产视图
}else{ errorMsg="用户名或密码错误"; return "fail"; } }
step4:在struts.xml中配置<action>
<struts> <!-- 包只能继承包 -->
<package name="one" namespace="/day01" extends="struts-default">
<!--请求地址:http://localhost:8080/appName/namespace/actionName.action -->
<action name="login" class="com.tarena.action.LoginAction"><!--包名.类名-->
<result name="success">/jsp/welcome.jsp</result>
<result name="fail">/jsp/loginform.jsp</result>
</action>
</package>
</struts>
step5:部署,地址栏输入:http://localhost:8080/struts01/day01/login.action进行测试,发现一进入页面,则错误提示信息已经显示!为了解决该问题,需要再定义一个<action>用于读取页面,而不是发请求后立即执行LoginAction类进行处理,处理应当在点击登录后才执行。
step6:在struts.xml中添加新的<action>仅仅用于读取页面
<action name="loginform" class="com.tarena.action.LoginFormAction">
<result name="success">/jsp/loginform.jsp</result>
</action>
u 注意事项:当前配置指明了class,则需要自己再写一个LoginFormAction类
step7:在包com.tarena.action下新建LoginFormAction类
public class LoginFormAction { public String execute(){ return "success"; } }
step8:重新部署,地址栏输入:http://localhost:8080/struts01/day01/loginform.action进行测试,发现问题已经解决,可正常执行
step9:优化:step6也可这么写,此时也不用写step8中的LoginFormAction类了
<action name="loginform" ><!--不指明class,则Struts2会自动创建一个类,详见1.7节-->
<result name="success">/jsp/loginform.jsp</result>
</action><!--result中的name属性也可以不写-->
1.10案例:修改1.6、1.9案例使用户不能绕过前端控制器
1)问题描述:1.6和1.9案例都存在一个问题,用户可以绕过前端控制器,直接访问JSP页面,如地址栏直接输入:http://localhost:8080/struts01/jsp/welcome.jsp,可显示页面,只是获取的名字为空。
2)如何解决:
step1:将jsp文件夹放入WEB-INF目录下。因为WEB-INF目录中的内容是受保护的,不能直接访问!但能转发过来。
step2:修改1.9案例struts.xml中result的转发路径,1.6也同理
<action name="loginform"><!-- result的type属性不写,默认为转发 -->
<result name="success">/WEB-INF/jsp/loginform.jsp</result>
</action>
<action name="login" class="com.tarena.action.LoginAction">
<result name="success">/WEB-INF/jsp/welcome.jsp</result><!-- 修改路径 -->
<result name="fail">/WEB-INF/jsp/loginform.jsp</result><!-- 修改路径 -->
</action>
step3:测试
http://localhost:8080/struts01/WEB-INF/jsp/welcome.jsp不能直接访问
http://localhost:8080/struts01/WEB-INF/jsp/loginform.jsp也不能直接访问
http://localhost:8080/struts01/day01/loginform.action只能根据请求,访问登录页面
1.11 NetCTOSS项目:显示资费列表
step1:新建Web工程:NetCTOSS,导入需要的包:Struts2核心包、Oracle数据库驱动包、JSTL包(若为J2EE5.0则不需要导这个包了,若为J2EE1.4则需要导入!)
step2:创建COST_CHANG表,并插入数据,别忘记commit
create table cost_chang(
id number(4) constraint cost_chang_id_pk primary key,
name varchar2(50) not null,
base_duration number(11),
base_cost number(7,2),
unit_cost number(7,4),
status char(1) constraint cost_chang_status_ck check(status in (0,1)),
descr varchar2(100),
creatime date default sysdate,
startime date,
cost_type char(1) );
insert into cost_chang values (1,'5.9元套餐',20,5.9,0.4,0,'5.9元20小时/月,超出部分0.4元/时',default,null,'2');
insert into cost_chang values (2,'6.9元套餐',40,6.9,0.3,0,'6.9元40小时/月,超出部分0.3元/时',default,null,'2');
insert into cost_chang values (3,'8.5元套餐',100,8.5,0.2,0,'8.5元100小时/月,超出部分0.2元/时',default,null,'2');
insert into cost_chang values (4,'10.5元套餐',200,10.5,0.1,0,'10.5元200小时/月,超出部分0.1元/时',default,null,'2');
insert into cost_chang values (5,'计时收费',null,null,0.5,0,'0.5元/时,不使用不收费',default,null,'3');
insert into cost_chang values (6,'包月',null,20,null,0,'每月20元,不限制使用时间',default,null,'1');
u 注意事项:在Oracle数据库中,语句大小写都可,但最终在数据库中显示的是大写形式。
step3:新建db.properties到src目录下,将username和password修改为自己的数据库的用户名和密码
user=system
password=chang
url=jdbc:oracle:thin:@localhost:1521:dbchang
driver=oracle.jdbc.driver.OracleDriver
step4:在com.tarena.netctoss.util包中创建DBUtils工具类,用于打开连接接和关闭连接
public class DBUtils {
private static String driver; private static String url; private static String user;
private static String password;
static {
try { Properties props = new Properties();
rops.load(DBUtils.class.getClassLoader().getResourceAsStream("db.properties"));
driver = props.getProperty("driver"); url = props.getProperty("url");
user = props.getProperty("user"); password = props.getProperty("password");
Class.forName(driver);
} catch (Exception e) { throw new RuntimeException("数据库驱动加载错误",e);}
}
public static Connection openConnection() throws SQLException {
Connection con = DriverManager.getConnection(url, user, password);
return con; }
public static void closeConnection(Connection con) {
if (con != null) { try { con.close(); } catch (SQLException e) { } } }
public static void main(String[] args) throws Exception {
Connection con = openConnection();//简单测试下
System.out.println(con); } }
step5:在com.tarena.netctoss.entity包中创建实体类Cost
private Integer id; //资费ID private String name; //资费名称 NAME
private Integer baseDuration; //包在线时长 BASE_DURATION
private Float baseCost; //月固定费 BASE_COST
private Float unitCost; //单位费用 UNIT_COST
private String status; //0:开通;1:暂停;STATUS
private String descr; //资费信息说明 DESCR
private Date startTime; //启用日期 STARTTIME
private Date creaTime;//创建时间 CREATIME
private String costType;//资费类型 COST_TYPE
……各自的get/set方法……
step6:在com.tarena.netctoss.dao包中新建DAOException类,并继承Exception
public class DAOException extends Exception {
public DAOException(String message, Throwable cause) {
super(message, cause); } }
step7:在com.tarena.netctoss.dao包中新建CostDAO接口
public interface CostDAO { public List<Cost> findAll() throws DAOException; }
step8:在com.tarena.netctoss.dao.impl包中新建CostDAOImpl类,并实现CostDAO
public class CostDAOImpl implements CostDAO {
private static String findAll = "select ID, NAME, BASE_DURATION, BASE_COST, UNIT_COST, CREATIME, STARTIME, STATUS, DESCR, COST_TYPE from COST_CHANG";
public List<Cost> findAll() throws DAOException {
Connection con = null;
try { con = DBUtils.openConnection();
PreparedStatement stmt = con.prepareStatement(findAll);
ResultSet rs = stmt.executeQuery();
List<Cost> costList = new ArrayList<Cost>();
while (rs.next()) { costList.add(toCost(rs)); }
return costList;
} catch (SQLException e) { e.printStackTrace();
throw new DAOException("访问异常", e);//抛自己定义的DAOException
} finally { DBUtils.closeConnection(con); } }
/** 将结果集中的每一条数据转换成每一个Cost对象 */
private Cost toCost(ResultSet rs) throws SQLException {
Cost cost = new Cost(); cost.setId(rs.getInt("ID"));
cost.setName(rs.getString("NAME"));
cost.setBaseDuration(rs.getInt("BASE_DURATION"));
cost.setBaseCost(rs.getFloat("BASE_COST"));
cost.setUnitCost(rs.getFloat("UNIT_COST"));
cost.setStartTime(rs.getDate("STARTIME"));
cost.setStatus(rs.getString("STATUS"));
cost.setCreaTime(rs.getDate("CREATIME"));
cost.setDescr(rs.getString("DESCR"));
cost.setCostType(rs.getString("COST_TYPE")); return cost; } }
step9:在com.tarena.netctoss.dao包中新建DAOFactory工厂类
private static CostDAO costDAO = new CostDAOImpl();
public static CostDAO getCostDAO() { return costDAO; }
step10:在com.tarena.netctoss.test包中新建TestCostDAO类,用于测试DAO。用JUnit或main方法测试,此处先用main方法测
public static void main(String[] args) throws Exception {
CostDAO costDAO = DAOFactory.getCostDAO();
List<Cost> costList = costDAO.findAll();
for (Cost cost : costList) { System.out.println(cost.getId() + "," + cost.getName());} }
step11:在com.tarena.netctoss.action.cost包中新建CostListAction,用于接收请求并处理
private List<Cost> costList;//输出属性
private CostDAO costDAO = DAOFactory.getCostDAO();//DAO属性不用设置get/set方法
……costList属性的get/set方法……
public String execute() throws Exception {
costList = costDAO.findAll(); return "success"; }
step12:在web.xml中配置前端控制器
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"><!--以上都为固定格式-->
<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>
</web-app>
step13:拷贝NetCTOSS静态页面、图片、样式到工程中,结构如下:
step14:将静态页面“.html”后缀改为动态页面“.jsp”后缀
1)首先在页面顶部添加页面指令:<%@page pageEncoding="utf-8"%>
2)然后保存页面,最后再去将后缀改为“.jsp”,如:cost_list.jsp
step15:在cost_list.jsp页面中保留标题行,使用JSTL标签和EL表达式修改数据行
<c:forEach items="${costList}" var="cost"><!--使用JSTL中forEach标签循环显示-->
<tr><td>${cost.id}</td> <!--使用EL--> <td><a href="#">${cost.name}</a></td>
<td>${cost.baseDuration }小时</td> <td>${cost.baseCost }元</td>
<td>${cost.unitCost }元/小时</td> <td>${cost.creaTime }</td>
<td>${cost.startTime }</td>
<td> <c:if test="${cost.status == 0}">开通</c:if>
<c:if test="${cost.status == 1}">暂停</c:if>
<c:if test="${cost.status == 2}">删除</c:if></td>
<td><c:if test="${cost.status == 0}">
<input type="button" value="暂停" class="btn_pause" /></c:if>
<c:if test="${cost.status == 1}">
<input type="button" value="启用" class="btn_start" /></c:if>
<input type="button" value="修改" class="btn_modify" />
<input type="button" value="删除" class="btn_delete" /></td>
</tr></c:forEach>
u 注意事项:
v 当学习了Struts2的标签和OGNL表达式后,上面的JSTL标签和EL表达式可都被替换掉。
v JSP页面记得引入:<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
step16:【项目经验】在src目录下分别创建struts.xml和struts-cost.xml配置文件
1)struts.xml,相当于主配置,分别引入其他配置文件。为何要用这种形式?因为当配置文件太大时,采用该方式便于管理
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.1//EN"
"http://struts.apache.org/dtds/struts-2.1.dtd"><!--以上为固定格式-->
<struts>
<include file="struts-cost.xml" /><!-- 当配置文件太大时,采用该方式 -->
</struts>
2)struts-cost.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.1//EN"
"http://struts.apache.org/dtds/struts-2.1.dtd">
<struts>
<package name="cost" namespace="/cost" extends="struts-default">
<action name="list" class="com.tarena.netctoss.action.cost.ListCostAction">
<result name="success">/WEB-INF/jsp/cost/cost_list.jsp</result>
</action>
</package>
</struts>
step17:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/cost/list.action,效果如下:
u 注意事项:为了看效果,此处在数据库中修改了几个资费状态值和开通时间。
1.12 NetCTOSS项目:资费列表分页显示
step1:此例是在1.11案例基础之上做修改。也可以复制1.11案例的工程再修改,如果采取复制,则步骤如下:
1)点击NetCTOSS工程,右键点击“Copy”,然后右键点击“Paste”,起个工程名,如:NetCTOSS2
2)但是这样复制的工程,在MyEclipse中工程名虽然为NetCTOSS2,但是当要部署到Tomcat下时,发现无法部署,因为已经有同名的应用被部署了!所以,此时需要修改工程名为NetCTOSS2的工程,在部署时的应用名!
3)修改部署时的应用名:右键工程-->选择“Properties”-->选择“MyEclipse”-->选择“Web”-->在Web Context-root文本框中,为当前工程部署时的应用名,将它修改为“/NetCTOSS2”,点击“OK”即可,记得要写“/”。此时,工程名和应用名一致。
u 注意事项:要注意区分工程名和应用名的问题!
step2:在CostDAO接口中添加方法定义
public List<Cost> findAll(int page,int rowsPerPage) throws DAOException;//分页查询
public int getTotalPages(int rowsPerPage) throws DAOException;//获得总页数
step3:在CostDAOImpl中实现方法
private static String findAllByPage="select ID, NAME, BASE_DURATION, BASE_COST,
UNIT_COST, CREATIME, STARTIME, STATUS, DESCR,COST_TYPE from (select
ID, NAME, BASE_DURATION, BASE_COST, UNIT_COST, CREATIME, STARTIME,
STATUS,DESCR,COST_TYPE,rownum n from COST_CHANG where rownum<?) where n>=?";
public List<Cost> findAll(int page, int rowsPerPage) throws DAOException {//分页查询
Connection con = null;
try { con = DBUtils.openConnection();
PreparedStatement stmt = con.prepareStatement(findAllByPage);
int start=(page-1)*rowsPerPage +1; int end=start+rowsPerPage;
stmt.setInt(1, end); stmt.setInt(2, start); ResultSet rs = stmt.executeQuery();
List<Cost> costList = new ArrayList<Cost>();
while (rs.next()) { costList.add(toCost(rs)); } return costList;
} catch (SQLException e) {
e.printStackTrace(); throw new DAOException("访问异常", e);
} finally { DBUtils.closeConnection(con); } }
public int getTotalPages(int rowsPerPage) throws DAOException {
Connection con = null;
try { con = DBUtils.openConnection();
PreparedStatement stmt = con.prepareStatement(
"select count(*) from COST_CHANG");
ResultSet rs = stmt.executeQuery();
List<Cost> costList = new ArrayList<Cost>();
rs.next(); int totalRows=rs.getInt(1);
if(totalRows%rowsPerPage==0){ return totalRows/rowsPerPage;
}else { return (totalRows/rowsPerPage)+1; }
} catch (SQLException e) {
e.printStackTrace(); throw new DAOException("访问异常", e);
} finally { DBUtils.closeConnection(con); } }
step4:修改CostListAction
private int page=1;//添加当前页属性,input、output双重属性
private int totalPages;//添加总页数属性,output
……各自get/set方法……
public String execute() throws Exception { costList = costDAO.findAll(page,2);//每页2条
totalPages=costDAO.getTotalPages(2);//每页2条数据,计算出的总页数
return "success"; }
step5:修改cost_list.jsp中的分页条
<div id="pages">
<a href="list.action?page=1">首页</a>
<c:choose><c:when test="${page > 1}">
<a href="list.action?page=${page-1}">上一页</a>
</c:when>
<c:otherwise><a href="#">上一页</a></c:otherwise>
</c:choose>
<c:forEach begin="1" end="${totalPages}" var="p"><!--forEach用法见JSP笔记3.3-->
<c:choose><c:when test="${page == p}">
<a href="list.action?page=${p}" class="current_page">${p }</a>
</c:when>
<c:otherwise><a href="list.action?page=${p}">${p }</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:choose> <c:when test="${page < totalPages}">
<a href="list.action?page=${page+1}">下一页</a>
</c:when>
<c:otherwise><a href="#">下一页</a></c:otherwise>
</c:choose>
<a href="list.action?page=${totalPages }">末页</a>
</div>
u 注意事项:
v JSP页面记得引入:<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
v 如果切换上下页,出现错位的情况,在JSP页面按下Ctrl+Shift+F,格式化一下,然后保存即可。
step6:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/cost/list.action,效果如下:
二、
OGNL技术
2.1什么是OGNL
OGNL是Object-Graph Navigation Language(对象图形导航语言)的缩写,它是一种功能强大的表达式语言。
OGNL可以让我们用非常简单的表达式访问对象层,它用一个独立的lib形式出现(封装于ognl.jar中),方便我们使用或构建自己的框架。
2.2 OGNL基本语法
OGNL引擎访问对象的格式:Ognl.getValue("OGNL表达式",root对象);//root对象是Ognl要操作的对象。
1)访问基本类型属性:(见案例1)
System.out.println(Ognl.getValue("id", foo));//100
System.out.println(Ognl.getValue("name", foo));//Java
2)访问数组属性:
System.out.println(Ognl.getValue("arry[0]", foo));//one
3)访问集合List属性:
System.out.println(Ognl.getValue("list[1]", foo));//B
4)访问集合Map属性:
System.out.println(Ognl.getValue("map.one", foo));//Java
或System.out.println(Ognl.getValue("map['two']", foo));//JavaJava
5)基本运算:
System.out.println(Ognl.getValue("id+100", foo));//200
System.out.println(Ognl.getValue("\"What is \"+name", foo));//What is Java
System.out.println(Ognl.getValue("\"name: \" + name + \" id: \" +id",foo));
System.out.println(Ognl.getValue("id > 150", foo));//false //name: Java id:100
6)调用方法:
System.out.println(Ognl.getValue("name.toUpperCase()", foo));//JAVA
System.out.println(Ognl.getValue("list.size()", foo));//3
u 注意事项:方法的参数也可以使用属性
System.out.println(Ognl.getValue("map['three'].lastIndexOf(name)", foo));//8
7)调用静态方法:调用静态方法的格式:@类名@方法名
System.out.println(Ognl.getValue("@java.util.Arrays@toString(arry)",foo));//[one,two,three]
8)创建的List对象:
Object obj = Ognl.getValue("{1,2,3,4,5}", null);//这种方法更方便地临时创建一个List对象
System.out.println(obj.getClass().getName());//java.util.ArrayList
System.out.println(obj);//[1,2,3,4,5]
9)创建的Map对象:
obj = Ognl.getValue("#{1:'java',2:'javajava',3:'javajavajava'}", null);//注意:“#”号不能丢
System.out.println(obj.getClass().getName());//java.util.LinkedHashMap
System.out.println(obj);//[1=java,2=javajava,3=javajavajava]
u 注意事项:OGNL中只能创建List和Map对象。
10)案例1:step1:新建工程,导入Struts2核心包,配置前端控制器
step2:创建Foo类,属性如下
private Integer id; private String name; private String[] arry;
private List<String> list; private Map<String, String> map; …各自的get/set方法
step3:创建TestOgnl类,在main方法中先进行赋值,然后执行各类型测试
Foo foo = new Foo(); foo.setId(100); foo.setName("Java");
foo.setArry(new String[] { "one", "two", "three" });
foo.setList(Arrays.asList("A","B","C"));//返回一个受指定数组支持的固定大小的集合。
HashMap<String, String> map = new HashMap<String, String>();
map.put("one", "Java"); map.put("two", "JavaJava");
map.put("three", "JavaJavaJava"); foo.setMap(map);
11)访问对象中的属性:(见案例2)
System.out.println(Ognl.getValue("name", emp1));//chang1
System.out.println(Ognl.getValue("dept.name", emp2));//R&D
System.out.println(Ognl.getValue("emps[2].salary", dept));//3000.0
System.out.println(Ognl.getValue("emps[0].salary + 1000", dept));//2000.0
System.out.println(Ognl.getValue("emps[1].salary > 10000", dept));//false
System.out.println(Ognl.getValue("emps[2].name.toUpperCase()", dept));//CHANG3
12)案例2:step1:创建Emp员工类
private String name; private double salary; private Dept dept;//又是一个对象类型
public Emp(String name,double salary) { this.name=name; this.salary=salary; }//构造器
……各自的get/set方法
step2:创建Dept部门类
private String name; private List<Emp> emps=new ArrayList<Emp>();
public Dept(String name){ this.name=name; }//构造器
……各自的get/set方法
public void addEmp(Emp emp){ emps.add(emp);//将员工加入集合
emp.setDept(this);//将当前Dept对象的名称给emp对象的dept属性赋值 }
step3:在main方法中先赋值,然后执行访问对象属性的测试
Dept dept=new Dept("R&D"); Emp emp1=new Emp("chang1",1000);
Emp emp2=new Emp("chang2",2000); Emp emp3=new Emp("chang3",3000);
dept.addEmp(emp1); dept.addEmp(emp2); dept.addEmp(emp3);
2.3 OGNL表达式中加“#”和不加“#”的区别
step1:在2.2节的工程中,新建Bar类,添加name属性,并设置get/set方法
step2:创建TestOgnl2类,在main方法中进行测试
//自定义context对象(见2.4-2.5节OGNL体系结构),如果不写系统会自动加一个
Map<String, Object> ctx = new HashMap<String, Object>(); ctx.put("num", 10);
Bar root = new Bar();//创建root对象 root.setName("bar");
//不加"#",表示从业务对象root中取数据
System.out.println(Ognl.getValue("name", ctx, root));//bar
//加"#",表示从公共对象context中取数据
System.out.println(Ognl.getValue("#num", ctx, root));//10
2.4 OGNL体系结构
2.5 XWord框架对OGNL进行了改造
2.6 ValueStack对象结构
2.7 ValueStack结构演示
step1:新建工程struts02,导入Struts2核心包,配置前端控制器
step2:新建DebugAction类
private String name; private int id; private String[] arry;
public String execute() { name = "java"; id = 100;
arry = new String[] {"struts","hibernate","spring" }; return "success"; }
……各自get/set方法
step3:配置struts.xml
<package name="valuestack" namespace="/day02" extends="struts-default" >
<action name="debug" class="action.DebugAction">
<result name="success">/WEB-INF/jsp/debug.jsp</result>
</action>
</package>
step4:在WEB-INF/jsp下新建debug.jsp
<%@page pageEncoding="utf-8"%>
<%@page import="java.util.*" %>
<% Enumeration en = request.getAttributeNames();//获取request对象中所有属性
while(en.hasMoreElements()){//循环打印出所有属性
out.println(en.nextElement() + "<br/>"); }
Object obj = request.getAttribute("struts.valueStack");//打印出struts.valueStack
out.println("<hr/>struts.valueStack对象:<br/>" + obj); %>
step5:部署,地址栏输入:http://localhost:8080/struts02/day02/debug.action,测试结果如下:
step6:Struts2提供了专门的标记库,用于查看ValueStack中的内容,一般用于调试程序,在debug.jsp尾部添加标签
<s:debug />
step7:地址栏输入:http://localhost:8080/struts02/day02/debug.action,点击[debug],显示如下:
2.8 Struts2标签的使用
Struts2的很多标记就是通过访问ValueStack获得数据的。
使用前要在页面中要引入:<%@taglib uri="/struts-tags" prefix="s"%>,prefix:表示前缀
1)通过OGNL从ValueStack取数据,并且显示。
<s:property value ="ognl表达式"/>
2)省略value,取出ValueStack的栈顶(默认从栈顶取值)。
<s:property />
3)通过OGNL从ValueStack取出集合,依次将集合中的对象置于栈顶,在循环中,ValueStack栈顶即为要显示的数据。
<s:iterator value="ognl表达式指定的集合名">
<s:property value="name" /><!--将每个对象中name属性的值输出-->
</s:iterator>
将OGNL指定的集合属性循环迭代,在迭代中,会将当前元素压入root栈顶位置,因此使用“属性”格式的OGNL,此时定位的是元素属性,而不是Action属性。Iterator循环结束后,Action对象又恢复成栈顶。与JSTL标签中的forEach类型,也有相同的属性。其中var、status属性指定的变量存储在context区域,需要使用“#变量”访问。
4)if-else判断标签
<s:if test=“ognl判断表达式”>满足条件时</s:if> <s:else>不满足条件时</s:else>
5)可在value属性中做字符串拼接
<s:property value="name + 'java'" /><!--接2.7节,结果为:java java-->
6)可调用方法
<s:property value="arry[1].toUpperCase()" /><!--接2.7节,结果为:HIBERNATE -->
7)其他详细用法,可参考2.2节或第五章,只不过我们在Struts2标签中直接写OGNL表达式(字符串)即可!
2.9 Struts2对EL表达式的支持
EL表达式默认是访问page、request、session、application范围的数据。
在Struts2中也可以使用EL表达式访问Action对象的属性。访问顺序是page、request、root栈(action)、session、application。当EL从request中获取不到信息后,会去ValueStack对象的root栈获取,原因是:Struts2重写了HttpServletRequest的方法。
2.10案例:修改1.12案例(使用Struts2标签和OGNL表达式)
<div id="pages"> <a href="list.action?page=1">首页</a>
<s:if test="page > 1"><a href="list.action?page=${page-1}">上一页</a></s:if>
<s:else><a href="#">上一页</a></s:else>
<s:iterator begin="1" end="totalPages" var="p">
<s:if test="top==page"><a href="#" class="current_page"><s:property /></a></s:if>
<s:else><a href="list.action?page=${top }"><s:property /></a></s:else>
<!-- Struts2标签写在属性里也行,但不符合习惯,一般写EL表达式-->
</s:iterator>
<s:if test="page < totalPages"><a href="list.action?page=${page+1}">下一页</a></s:if>
<s:else><a href="#">下一页</a></s:else>
<a href="list.action?page=${totalPages }">末页</a> </div>
三、
Action
3.1 Struts2的核心组件
1)FC:前端控制器 2)VS:ValueStack 3)Action:动作
4)Result:结果 5)Interceptor:拦截器 6)Tags:标记库
3.2 Struts2的工作流程
1)所有请求提交给FC。
2)根据配置信息确定要调用的Action。
3)创建一个ValueStack对象(每个请求都有一个独立的VS对象)。
4)创建Action对象,把Action对象放到VS的栈顶,将VS对象存入到request中,存储的key为“struts.valueStack”。
5)控制器调用Action对象接收请求参数,并在方法中根据输入属性算输出属性。
6)在调用Action之前或之后调用一系列Interceptor。
7)根据Action返回的字符串确定Result(10种类型)。
8)调用Result对象,将VS中的数据按照特定的格式输出。
9)很多情况下,Result将转发到JSP,JSP页面用Tags取出数据并显示。
10)请求处理完后,将ValueStack对象和Action对象销毁。
3.3在Action中访问Session和Application
1)利用ActionContext,返回Map类型
ActionContext context=ActionContext.getContext();
Map<String, Object> session=context.getSession();
session.put("username", "chang");
Map<String, Object> request=(Map)context.get("request");
Map<String, Object> app=context.getApplication();
u 注意事项:不推荐使用ActionContext访问Session的方式,因为这种方式的“侵入性”较强。ActionContext是Struts2的API,如果使用其他框架代替目前的Struts2框架,而我们实际项目中的Action的数量非常庞大,每个类都修改的话,会非常麻烦。推荐使用实现SessionAware接口的方式。
2)ServletActionContext返回的是Servlet使用类型
HttpServletRequest httpreq=ServletActionContext.getRequest();
HttpSession httpsession=httpreq.getSession();
ServletContext httpapp=ServletActionContext.getServletContext();
3)在Action中实现Aware接口,由框架底层将对象注入给Action变量
RequestAware:Map类型的request
SessionAware:Map类型的session
ApplicationAware:Map类型的application
ServletRequestAware:http类型的request
ServletResponseAware:http类型的response
ServletContextAware:http类型的application
3.4 NetCTOSS项目:用户登录
在原项目的基础上,添加登录模块。
step1:创建ADMIN_CHANG表,并插入数据,别忘记commit
create table admin_chang(
id number(11) constraint admin_info_id_pk_chang primary key,
admin_code varchar2(30) unique not null,
password varchar2(30) not null,
name varchar2(30) not null,
telephone varchar2(30),
email varchar2(50),
enrolldate date not null );
insert into admin_chang values(1001,'admin','123','管理员','13688997766','admin@sin.com'
,to_date('2013-08-05 15:30:30', 'yyyy-mm-dd hh24:mi:ss'));
insert into admin_chang values(1002,'user','123','用户','13688997766','shiyl@sin.com'
,to_date('2013-08-22 08:30:23','yyyy-mm-dd hh24:mi:ss'));
step2:在com.tarena.netctoss.entity包下,新建Admin.实体类
private Integer id; // ID private String adminCode; // ADMIN_CODE
private String password; // PASSWORD private String name; // NAME
private String telephone; // TELEPHONE private String email; //EMAIL
private Date enrollDate; // ENROLLDATE ……各自的get/set方法
step3:在com.tarena.netctoss.dao包下,新建AdminDAO接口
public interface AdminDAO {
public Admin findByCodeAndPwd(String code, String pwd) throws DAOException; }
step4:在com.tarena.netctoss.dao.impl包下,新建AdminDAOImpl类,并实现AdminDAO
public class AdminDAOImpl implements AdminDAO {
private static final String findByCodeAndPwd ="select ID, ADMIN_CODE,
PASSWORD, NAME, TELEPHONE, EMAIL, ENROLLDATE from ADMIN_CHANG where ADMIN_CODE=? and PASSWORD=?";
public Admin findByCodeAndPwd(String code, String pwd) throws DAOException {
Connection con = null; Admin admin=null;
try { con = DBUtils.openConnection();
PreparedStatement stmt = con.prepareStatement(findByCodeAndPwd);
stmt.setString(1, code); stmt.setString(2, pwd);
ResultSet rs = stmt.executeQuery();
if(rs.next()){ admin = toAdmin(rs); }
return admin;
} catch (Exception e) { e.printStackTrace();
throw new DAOException("访问异常", e);//抛自己定义的DAOException }finally{ DBUtils.closeConnection(con); } }
/** 将结果集中的每一条数据转换成每一个Admin对象 */
private Admin toAdmin(ResultSet rs) throws SQLException{
Admin admin = new Admin(); admin.setId(rs.getInt("ID"));
admin.setAdminCode(rs.getString("ADMIN_CODE"));
admin.setPassword(rs.getString("PASSWORD"));
admin.setName(rs.getString("NAME"));
admin.setTelephone(rs.getString("TELEPHONE"));
admin.setEmail(rs.getString("EMAIL"));
admin.setEnrollDate(rs.getDate("ENROLLDATE")); return admin; } }
step5:在DAOFactory工厂类中添加Admin实例
private static AdminDAO adminDAO = new AdminDAOImpl();
public static AdminDAO getAdminDAO() { return adminDAO; }
step6:在com.tarena.netctoss.test包中新建TestAdminDAO类,用于测试DAO。此处用JUnit
@Test
public void testFindAllByPage() throws Exception {
AdminDAO adminDAO=DAOFactory.getAdminDAO();
Admin admin=adminDAO.findByCodeAndPwd("admin", "123");
System.out.println(admin.getId()); System.out.println(admin.getName()); }
step7:在com.tarena.netctoss.action包中新建LoginAction,用于接收客户端的请求并处理
private Admin admin; private String errorMsg="用户名或密码错误";
private AdminDAO adminDAO = DAOFactory.getAdminDAO();//DAO不用设置get/set
……各自的get/set方法……
public String execute() throws Exception {
admin = adminDAO.findByCodeAndPwd(admin.getAdminCode(), admin.getPassword());
if (admin != null) { session.put(ADMIN_KEY, admin);
return "success"; // 使用Session,需要实现SessionAware接口
} else { return "fail"; } }
step8:在com.tarena.netctoss.action包中新建BaseAction,BaseAction实现SessionAware,然后step7中的LoginAction继承BaseAction
1)BaseAction
public class BaseAction implements SessionAware {
protected Map<String, Object> session;
public void setSession(Map<String, Object> session) { this.session = session; } }
u 注意事项:
v 该类实现了SessionAware接口,表明Struts2在启动BaseAction时,首先,创建出该Action的对象,放入“栈顶”然后,会调用BaseAction的setSession方法,把session传入给BaseAction对象(注意:如果是普通的Action,Struts2在启动时仅创建出该Action的对象,然后放入“栈顶”)由此,我们定义了属性session,以便在之后其它的方法中使用。为了让子类也能使用,所以访问控制符为protected。
v 按照这样的机制,我们可以让所有的Action(如LoginAction)继承实现了SessionAware接口的BaseAction,当需要更换Struts2框架为其他框架时,只需要修改BaseAction即可(另外的 框架只要提供一个类似SessionAware接口的接口,由BaseAction继承)
2)LoginAction
public class LoginAction extends BaseAction { ... }
step9:为了方便统一管理KEY,所以在com.tarena.netctoss包下新建Constants接口,用于定义常量KEY
public interface Constants {
public static final String ADMIN_KEY = "com.tarena.netctoss.admin.key"; }
step10:所以BaseAction要再实现Constants
public class BaseAction implements SessionAware,Constants { ... }
step11:拷贝登录页面、主页的静态页面片工程中,并修改后缀为“.jsp”,结构如下:
step12:修改login_form.jsp,添加<form>标签、帐号和密码添加name属性,用于提交给服务器、页面引入Struts2标签:<%@taglib uri="/struts-tags" prefix="s"%>
<form id="loginForm" action="login.action" method="post">
<table> ……<input name="admin.adminCode" type="text" class="width150" />
……<input name="admin.password" type="password" class="width150" />
……<td class="login_button" colspan="2"><!--loginForm为form的id值-->
<a href="javascript:;" onclick="loginForm.submit();">
<img src="../images/login_btn.png" /></a></td><!--登录按钮图片-->
<td><span class="required"><s:property value="errorMsg"/></span></td>
……</form>
u 注意事项:
v 在<a>标签中,href="javascript:;"一般和onclick一起使用,主要用于触发点击事件,单独用相当于空连接:href="#"。
v 由于放置位置的原因,login_form.jsp中的样式、图片链接,都要加“../”,如:href="../styles/global.css"
step13:在src目录下新建struts-login.xml
<package name="login" namespace="/login" extends="struts-default">
<action name="loginform"><!--仅用于读取页面-->
<result name="success">/WEB-INF/jsp/login_form.jsp</result>
</action>
<action name="login" class="com.tarena.netctoss.action.LoginAction">
<result name="success">/WEB-INF/jsp/index.jsp</result>
<result name="fail">/WEB-INF/jsp/login_form.jsp</result>
</action>
</package>
step14:在struts.xml中引入
<include file="struts-login.xml" />
u 注意事项:在xml文件中,有时多个空行都会报错!
step15:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/login/loginform.action,输入正确则进入主页面,输入错误才出现提示信息:
3.5 Action属性注入
在<action>配置中,为Action组件的属性指定一个初始值,该值在创建Action对象时注入,可以将一些变化的参数值,利用该方法指定。例如:pageSize、password密码、dir存储路径等。例如:
public class ParamAction{ private String param1; private int param2; …各自get/set}
<action name="param" class="com.tarena.ParamAction">
<param name="param1">ABC</param>
<param name="param2">10</param>
</action>
3.6案例:重构NetCTOSS资费列表分页显示(使用属性注入)
一般情况下,为该Action定义一些属性,有些是用于输入的(比如表单提交用户名、密码),有些是用于输出的(比如员工列表、当前页数)此外,还有一些属性是Action自用的参数,比如1.12节分页案例中,每页显示多少行的行数,我们是写死在程序里的。
例如1.12案例step4:
costList = costDAO.findAll(page,2);//每页2条
totalPages=costDAO.getTotalPages(2);//每页2条数据,计算出的总页数
但是,我们可以把它声明为一个属性,方便更改。pageSize就是参数,而参数注入就是说,像pageSize这样的参数,我们可以写在配置文件中。
step1:修改1.12案例step4中的ListCostAction
private int pageSize =2;//添加pageSize属性,以及它的get/set方法
costList = costDAO.findAll(page,pageSize);//修改方法中的每页条数
totalPages=costDAO.getTotalPages(pageSize);
step2:修改struts-cost.xml
<action name="list" class="com.tarena.netctoss.action.cost.ListCostAction">
<param name="pageSize">3</param><!--会把原来定义的每页2条修改为3条-->
<result name="success">/WEB-INF/jsp/cost/cost_list.jsp</result>
</action>
3.7使用通配符配置Action
1)*_*:代表名称中有“_”下划线的所有Action。
2)例如:
<action name="*_*_*" class="com.tarena.{1}Action" method="{2}">
<result name="success">/WEB-INF/jsp/{3}.jsp</result>
</action>
①{1}Action:表示name属性中第一个“*”星号匹配的字符串。
②method="{2}":表示方法名为name属性中第二个“*”星号匹配的字符串。
③{3}.jsp:表示显示的页面为name属性中第三个“*”星号匹配的字符串。
3.8案例:通配符配置(资费增、改、查)
step1:新建项目struts03,导入Struts2核心包,配置前端控制器
step2:在WebRoot目录下新建cost.jsp
<a href="Cost_list.action">资费查看</a>
<a href="Cost_update.action">资费更新</a>
<a href="Cost_add.action">资费添加</a>
step3:在action包中新建CostAction,并定义三个方法
public String add(){ System.out.println("资费添加操作"); return "success"; }
public String list(){ System.out.println("资费查看操作"); return "success"; }
public String update(){ System.out.println("资费更新操作"); return "success"; }
step4:新建struts.xml
<package name="cost" extends="struts-default">
<action name="*_*" class="action.{1}Action" method="{2}">
<result name="success">cost.jsp</result>
</action>
</package>
step5:部署,测试,地址栏输入:http://localhost:8080/struts03/cost.jsp,效果如下:
step6:点击三个连接,则分别执行了各自的方法:
3.9 Struts2中Action的设计经验
1)配置文件的拆分。
2)控制好Action类的粒度(不能太多或就1个)。
3)Action和Servlet API的耦合度要低,如若非要用底层API,实现XXXAware接口,可以封装拦截器,如:sessionAware。
4)适当用通配符。
5)根据输入算输出。
四、
Result
Result组件是一个类,职责是生成视图。该类必须实现Result接口,并且将约定的execute方法实现,在该方法中编写了生成响应输出的逻辑代码。
视图可以是多种多样的(比如JSP、JSON、报表、freeMarker等),这些视图都可以由Result负责。
4.1 Result注册配置
<package>
<result-types>
<result-type name="类型名" class="实现类"
//...其它类型的result
</result-type>
</package>
4.2 Result组件利用<result>元素的type属性指定result类型
<action>
<result type="result类型">
//...result参数指定
</result>
</action>
4.3常见的Result组件类型
1)dispatcher(默认):以请求转发方式调用一个JSP,生成响应视图。
2)redirect:以重定向方式调用一个JSP,生成响应视图。
3)redirectAction:以重定向方式调用一个Action。
参数:①actionName:要定向到的Action的name值。
②namespace:要定向到的Action所在包的命名空间的名字。
4)chain:以请求转发方式调用一个Action。
5)stream:以字节流方式响应,将Action中指定的一个InputStream类型的属性以字节流方式输出。
参数:①inputName:OGNL表达式,表示要输出的一个输入流对象(要输出数据的来源)。 ②contentType:用于设置响应中contentType。
6)json:以json字符串响应,将Action中指定的属性,拼成一个json字符串输出。
参数:①root:OGNL表达式,要做成JSON字符串的对象
4.4 NetCTOSS项目:资费删除
step1:在CostDAO接口中添加删除
public void delete(int id) throws DAOException;
step2:在CostDAOImpl类中实现该方法
private static String delete="delete from COST_CHANG where ID=?";//SQL删除语句
public void delete(int id) throws DAOException {
Connection con = null;
try { con = DBUtils.openConnection();
PreparedStatement stmt = con.prepareStatement(delete);
stmt.setInt(1, id); stmt.executeUpdate();
} catch (Exception e) { e.printStackTrace();
throw new DAOException("访问异常", e);
} finally { DBUtils.closeConnection(con); } }
step3:在com.tarena.netctoss.action.cost包下,新建DeleteCostAction
private int id; private CostDAO costDAO=DAOFactory.getCostDAO();//不需要get/set
……id属性的get/set方法
public String execute() throws DAOException{ costDAO.delete(id); return "success"; }
step4:在struts-cost.xml中添加删除配置
<action name="delete" class="com.tarena.netctoss.action.cost.DeleteCostAction">
<result name="success" type="redirectAction">list</result>
</action>
u 注意事项:上面代码中的type="redirectAction"表示重定向到一个action,重定向到一个jsp可以使用type="redirect"。转发到一个action使用type="chain",转发到一个jsp使用type="dispatcher"。如果result没有定义type属性,那么默认值就是"dispatcher"。
step5:为cost_list.jsp中的删除按钮添加onclick事件
<input type="button" value="删除" onclick="location.href='delete.action?id=${cost.id }'"... />
u 注意事项:由于此时还是用的JSTL中的forEach标签,所以必须要有var属性绑定变量,而之前var绑定的是cost,即var="cost",所以EL表达式中要写成${cost.id }。如果用的是Struts2标签进行循环,则EL表达式写${id }即可。
step6:将cost_list.jsp中的循环改为Struts2标签和OGNL表达式:(记得导入Struts2标签)
<s:iterator value="costList">
<tr><td><s:property value="id" /></td>
<td><a href="#"><s:property value="name" /></a></td>
<td><s:property value="baseDuration" />小时</td>
<td><s:property value="baseCost" />元</td>
<td><s:property value="unitCost" />元/小时</td>
<td><s:date name="creaTime" format="yyyy-MM-dd hh:mm:ss"/></td>
<td><s:property value="startTime" /></td>
<td><s:property value="statusOptions[status]" /></td>
<!--<s:if test="status==1"暂停</s:if><s:else>开通</s:else>
这些状态也是数据,写这不好,所以采取定义到Action或DAO中-->
<td><input type="button" value="启用" class="btn_start" />
<input type="button" value="修改" class="btn_modify" />
<input type="button" value="删除" class="btn_delete"
onclick="location='delete.action?id=${id }';" /></td>
</tr>
</s:iterator>
step7:将状态数据定义到DAO中,所以在CostDAO接口中添加定义
public Map<String,String> getCostStatusOptions();
step8:在CostDAOImpl中实现该Map方法
public Map<String, String> getCostStatusOptions() {
Map<String, String> statusOptions;
statusOptions=new LinkedHashMap<String, String>(); statusOptions.put("0","开通");
statusOptions.put("1","暂停"); statusOptions.put("2","删除");
return statusOptions; }
step9:在ListCostAction中添加statusOptions属性,并计算
private Map<String, String> statusOptions; ……get/set方法
public String execute() throws Exception { ……
statusOptions=costDAO.getCostStatusOptions(); …… }
step10:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/cost/list.action,点击删除,执行成功并重定向到list.action
4.5 NetCTOSS项目:基于StreamResult生成验证码
struts-default.xml中:
<result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/>
step1:在com.tarena.netctoss.util包下,新建VerifyCodeUtils类,用于生成验证码
private String code; private byte[] codeArr;
private static char[] seq = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I','J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R'
, 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8','9' };
public String getCode() { return code; }
public byte[] getCodeArr() { return codeArr; }
public void generate(int width, int height, int num) throws Exception {
Random r = new Random();
// 图片的内存映像
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
// 获得画笔对象
Graphics g = image.getGraphics();
g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
g.fillRect(0, 0, width, height); g.setColor(new Color(0, 0, 0));
// 用于存储随即生成的验证码
StringBuffer number = new StringBuffer();
// 绘制验证码
for (int i = 0; i < num; i++) {
g.setColor(new Color(r.nextInt(255), r.nextInt(255),r.nextInt(255)));
int h = (int) ((height * 60 / 100) * r.nextDouble() + (height * 30 / 100));
g.setFont(new Font(null, Font.BOLD | Font.ITALIC, h));
String ch = String.valueOf(seq[r.nextInt(seq.length)]);
number.append(ch); g.drawString(ch, i * width / num * 90 / 100, h); }
// 绘制干扰线
for (int i = 0; i <= 12; i++) {
g.setColor(new Color(r.nextInt(255), r.nextInt(255),r.nextInt(255)));
g.drawLine(r.nextInt(width), r.nextInt(height), r.nextInt(width),r.nextInt(height)); }
code = number.toString();
// 字节数组输出流,向字节数组中输出信息
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos);//压缩成JPEG
// 图片的二进制信息输出到了内存中
encoder.encode(image); codeArr = baos.toByteArray(); }
step2:在com.tarena.netctoss.action包下,创建VerifyCodeAction类,由于要使用session,所以继承BaseAction
public class VerifyCodeAction extends BaseAction {
private InputStream codeInputStream;//StreamResult只能输出InputStream类型属性值
private String code;//输入用户提交的验证码
private boolean ok=false;//输出验证的结果
/** 注意:boolean类型的变量自动生成get/set方法后,get方法名叫isXXX(),而不是getXXX()!如此例中的ok,则是isOk(),不是getOk()。如果是Boolean类型(字母b大写),则自动生成的get方法为getXXX() */
public String code() throws Exception {//生成验证码
VerifyCodeUtils verifyCodeUtils = new VerifyCodeUtils();
verifyCodeUtils.generate(80, 30, 4);
String code = verifyCodeUtils.getCode();
byte[] codeArr = verifyCodeUtils.getCodeArr();
session.put(VERIFYCODE_KEY, code); System.out.println(code);
codeInputStream = new ByteArrayInputStream(codeArr);
return "success"; } }
step3:在Constants接口中,添加KEY常量
public static final String VERIFYCODE_KEY = "com.tarena.netctoss.verifycode.key";
step4:在struts-login.xml中添加获取验证码的配置
<action name="code" class="com.tarena.netctoss.action.VerifyCodeAction" method="code">
<result name="success" type="stream"><!-- StreamResult负责输出二进制信息 -->
<!-- 给StreamResult对象的inputName属性赋值。inputName是一个OGNL表达式,该表达式可以从VS中获取一个InputStream对象。而StreamResult就是从这个inputStream对象中读取出要输出的二进制信息。codeInputStream与VerifyCodeAction中的属性相对应 -->
<param name="inputName">codeInputStream</param>
<param name="contentType">image/jpg</param><!--输出类型为图片-->
</result>
<result name="fail">/WEB-INF/jsp/login_form.jsp</result>
</action>
step5:修改login_form.jsp中的验证码
<tr><td class="login_info">验证码:</td>
…… <input id="verifycode" name="code" type="text" class="width70" /></td>
<td><!-- 放在img标签中,火狐浏览器就知道这里是一个图片,而不是下载 -->
<img src="code.action" onclick="this.src='code.action?#'+(new Date()).getTime();" alt="验证
码" title="点击更换" /></td><!--这么写的效果为:点击图片更换验证码-->
<td><span id="msg_verifycode" class="required"></span></td></tr>
u 注意事项:onclick="this.src='code.action?#'+(new Date()).getTime();",中间的“#”号要加,否则点击图片更换验证码时,会报异常:java.lang.NumberFormatException。
step6:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/login/loginform.action,验证码可正常显示,点击图片也会更换
4.6 NetCTOSS项目:基于JSONResult进行验证码检验
Struts2的struts-default.xml中没有提供json类型的Result,所以要扩展Result。
step1:导入struts2-json-plugin-2.1.8.jar …… 等一系列包到工程中
step2:该jar包中有个struts-plugin.xml,里面添加了json的配置,并且继承了struts-default.xml,所以以后的配置文件只要继承json-default包,也就有了struts-default中的配置
<package name="json-default" extends="struts-default">
<result-types>
<result-type name="json" class="org.apache.struts2.json.JSONResult"/>
</result-types>
<interceptors>
<interceptor name="json" class="org.apache.struts2.json.JSONInterceptor"/>
</interceptors>
</package>
step3:修改struts-login.xml,使包继承json-default
<package name="login" namespace="/login" extends="json-default">
step4:在com.tarena.netctoss.action包下的VerifyCodeAction中,添加验证方法
public String verify(){//进行验证
String c=(String)session.get(VERIFYCODE_KEY);
if(c!=null){ ok=c.equalsIgnoreCase(code.trim()); } return "success";
step5:在struts-login.xml中配置验证
<action name="verifycode" class="com.tarena.netctoss.action.VerifyCodeAction"
method="verify"><!--获取、验证都定义在一个Action中,但调用不同的方法-->
<!-- JSONResult默认将会把VS的栈顶(当前的Action)做成JSON字符串返回 -->
<result name="success" type="json">
<!-- JSONResult对象的root属性是一个OGNL表达式,通过该表达式可以从VS中获取一个对象,JSONResult将把这个对象做成JSON字符串并返回 -->
<param name="root">ok</param><!--配置参数后只把该对象中的ok属性输出-->
</result>
</action>
u 注意事项:json的三种使用方式:
v 将一个属性以json格式返回
<result type="json">
<param name="root">属性名</param>
</result>
v 将所有属性以json格式返回
<result type="json"></result>
v 将一部分属性以json格式返回
<result type="json">
<param name="includeProperties">属性1,属性2,...</param>
</result>
step6:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/login/code.action,先产生一个验证码,然后地址栏再输入:http://localhost:8080/NetCTOSS/login/verifycode.action?code
=1234,1234是随便写的,此时页面返回false。
step7:在WebRoot目录下新建js文件夹,将jquery-1.4.1.min.js(jquery框架)放入。同时新建validation.js文件,在其中添加自定义验证方法,可参考jQuery笔记5.12案例
$.fn.required=function(errorMsg,errorCtn){//必须填写
var value=this.val();
if(value!=null&&value.length>0){ $(errorCtn).text(""); return true;
}else{ $(errorCtn).text(errorMsg); return false; } }
$.fn.remote=function(url,errorMsg,errorCtn){//与服务器间的数据进行验证
var b=false; var value=this.val(); var name=this.attr("name");
$.ajax( { url:url+"?"+name+"="+value, dataType:"json", async:false,
success:function(data){
if(data){ $(errorCtn).text(""); b=true;
}else{ $(errorCtn).text(errorMsg); } }
});
return b; }
step8:在login_form.jsp中引入这两个js文件,并编写验证代码
<script language="javascript" src="../js/jquery-1.4.1.min.js"></script>
<script language="javascript" src="../js/validation.js"></script>
<script language="javascript" type="text/javascript">
function validateForm(){//普通方法
var b1=$("#admincode").required("帐号必须填写","#msg_admincode");
/* 这么写也行,传进去的是字符串 */
var b2=$("#password").required("密码必须填写",$("#msg_password"));
/* 这种方式,传进去的是对象,虽然validation.js中的方法也写了$,但这样写也可以,因为设计者无法确定传进去的是什么,所以加个$,更灵活 */
var b3=$("#verifycode").remote("verifycode.action","验证码错误~",
$("#msg_verifycode"));
return b1&&b2&&b3; }
$(function(){//一加载页面就执行的方法
$("#submit").click(function(){ var b = validateForm();
if(b) { $("#loginForm").submit(); } }); }); </script>
step9:在login_form.jsp中添加id属性,修改登录按钮使之采用脚本验证后再提交
<form id="loginForm" action="login.action" method="post">
<input id="admincode" name="admin.adminCode" ……
<span id="msg_admincode" class="required"></span>
<input id="password" name="admin.password" ……
<span id="msg_password" class="required"></span>
<input id="verifycode" name="code" type="text" ……
<span id="msg_verifycode" class="required"> ……
<a id="submit" href="javascript:;" ><img src="../images/login_btn.png" /></a> ……
</form>
step10:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/login/loginform.action,点击登录按钮,出现如下提示,只有都填写,且正确才可进入主页
step11:进入主页后发现地址栏还是:http://localhost:8080/NetCTOSS/login/login.action,且点F5刷新,会弹出“确认重新提交表单”的框,这不符合实际业务需求,因此要修改struts-login.xml中的配置
step12:修改3.4案例step13中的struts-login.xml,使用redirectAction类型的Result
<action name="login" class="com.tarena.netctoss.action.LoginAction">
<result name="success" type="redirectAction">
<param name="namespace">/main</param>
<param name="actionName">main</param>
</result>
<result name="fail">/WEB-INF/jsp/login_form.jsp</result>
</action>
step13:添加struts-main.xml
<package name="main" namespace="/main" extends="struts-default">
<action name="main">
<result name="success">/WEB-INF/jsp/index.jsp</result>
</action>
</package>
step14:在struts.xml中引入struts-main.xml
<include file="struts-main.xml" />
step15:部署,测试,当正确填写用户名、密码和验证码后,进入主页。此时,地址栏已经变为http://localhost:8080/NetCTOSS/main/main.action,并且按F5刷新,不会出现弹框。
4.7 NetCTOSS项目:添加资费模块中的验证资费名是否重复
实现方式也是利用json。
step1:在CostDAO接口中定义findByName
public Cost findByName(String name) throws DAOException;
step2:在CostDAOImpl中实现该方法
private static String findByName= "select ID, NAME, BASE_DURATION, BASE_COST,
UNIT_COST, CREATIME, STARTIME, STATUS, DESCR,COST_TYPE from COST_CHANG
where NAME=?";
public Cost findByName(String name) throws DAOException {
Connection con = null;
try { con = DBUtils.openConnection();
PreparedStatement stmt = con.prepareStatement(findByName);
stmt.setString(1, name); ResultSet rs = stmt.executeQuery();
Cost cost=null; if (rs.next()) { cost=toCost(rs); } return cost;
} catch (Exception e) { e.printStackTrace();
throw new DAOException("访问异常", e);
}finally{ DBUtils.closeConnection(con); } }
step3:测试findByName,在TestCostDAO中添加方法
@Test
public void testFindByName() throws Exception {
CostDAO costDAO=DAOFactory.getCostDAO();
Cost cost=costDAO.findByName("包月");
System.out.println(cost.getId()+" "+cost.getName()); }
step4:在com.tarena.netctoss.action.cost包中,新建ValidateCostNameAction
//private String name;//不用这个了,否则后面的JS报错!
private Cost cost=new Cost(); private boolean ok=false; ……各自get/set方法
private CostDAO costDAO=DAOFactory.getCostDAO();
public String execute() throws DAOException{
System.out.println(cost.getName()); Cost cost1=costDAO.findByName(cost.getName());
if(cost1==null){ ok=true; } return "success"; }
u 注意事项:报错的原因是因为页面中,name属性的值为“cost.name”,如果action中的属性用name接收,则无法接收。报错信息如下:
Error setting expression 'cost.name' with value '[Ljava.lang.String;@54996c' ognl.OgnlException: target is null for setProperty(null, "name" ....
step5:在struts-cost.xml中添加配置,并让cost包继承json-default
<action name="validateName"
class="com.tarena.netctoss.action.cost.ValidateCostNameAction">
<result name="success" type="json">
<param name="root">ok</param>
</result>
</action>
step6:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/cost/validateName.action?
cost.name=包月,页面显示true,且控制台输出“????”,这是由于中文乱码产生的问题!如果有个资费名称不包含中文的,则页面显示false。对于中文乱码问题,后面的JS脚本会解决
step7:将静态页面资费添加拷贝到cost文件夹中,改为cost_add.jsp
<script language="javascript" src="../js/jquery-1.4.1.min.js"></script>
<script language="javascript" src="../js/validation.js"></script>
<script language="javascript" type="text/javascript">
function validateCostForm(){
b=$('#costName').remote("validateName.action","资费名称已被占用",
$('#msg_costName')); }
</script>
<input id="costName" name="cost.name" onblur="validateCostForm();" type="text" ……
<div id="msg_costName" class="validate_msg_short"> ……
step8:修改validation.js中的remote函数,用于解决中文乱码问题,可与4.6案例step7比较
$.fn.remote=function(url,errorMsg,errorCtn){//与服务器间的数据进行验证
var b=false; var value=this.val(); var name=this.attr("name");
var params =name+"="+value;
$.ajax( { url:url, data:params, dataType:"json", type:"post", async:false,
success:function(data){
if(data){ $(errorCtn).text(""); b=true;
}else{ $(errorCtn).text(errorMsg); } }
});
return b; }
u 注意事项:用post方式不用考虑中文乱码,因为$.ajax()函数默认使用utf-8,而且Struts2框架也默认使用utf-8。如果url:url+"?"+name+"="+value,则还是乱码。
step9:在struts-cost.xml中添加配置
<action name="addform">
<result name="success">/WEB-INF/jsp/cost/cost_add.jsp</result>
</action>
step10:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/cost/addform.action,输入“包月”,则显示资费名已被占用
4.8自定义一个Result
此例模拟json Result。
step1:新建工程struts_result,导入一系列包,并在result包中,新建SomeResult
public class SomeResult implements Result {
private String root; ……get/set方法
public void execute(ActionInvocation arg0) throws Exception {
ValueStack vs=arg0.getStack();//获得VS Object obj;
if(root==null){ obj=vs.findValue("top");//从栈顶获得
}else{ obj=vs.findValue(root); }
JSONObject json=JSONObject.fromObject(obj);//json的原理
String jsonstr=json.toString();
HttpServletResponse response = ServletActionContext.getResponse();
PrintWriter out=response.getWriter(); out.println(jsonstr); } }
step2:在entity包中,新建Emp
private String name; private double salary; ……get/set方法
public Emp(String name, double salary) { this.name = name; this.salary = salary; }
step3:在action包中,新建TestAction
private Emp emp; private String name; ……get/set方法
public String execute() throws Exception{
emp=new Emp("chang",10000); name="java"; return "success"; }
step4:新建struts.xml,进行配置
<package name="test" namespace="/" extends="struts-default">
<result-types><!--声明自定义的result-->
<result-type name="some" class="result.SomeResult"></result-type>
</result-types>
<action name="test" class="action.TestAction">
<result name="success" type="some"><!-- 模拟json -->
<param name="root">emp</param>
<!-- 去掉上面的参数,会返回json字符串:
{"name":"java","emp":{"name":"chang","salary":10000}}
加上只返回emp对象的字符串:
{"name":"chang","salary":10000} -->
</result>
</action>
</package>
step5:部署,测试,地址栏输入:http://localhost:8080/struts_result/test.action,显示结果为:{"name":"chang","salary":10000},如果把配置中的参数去掉,则显示为:
{"name":"java","emp":{"name":"chang","salary":10000}}
五、
Struts2标签
5.1 A开头的标签
<s:a xhref=""></s:a>:超链接,类似于html 里的<a></a>
<s:action name=""></s:action>:执行一个view里面的一个action
<s:actionerror/>:如果action的errors有值那么显示出来
<s:actionmessage/>:如果action的message 有值那么显示出来
<s:append></s:append>:添加一个值到list,类似于list.add();
<s:autocompleter></s:autocompleter>:自动完成<s:combobox>标签的内容,这个是ajax
5.2 B开头的标签
<s:bean name=""></s:bean>:类似于struts1.x中的,JavaBean的值
5.3 C开头的标签
<s:checkbox></s:checkbox>:复选框
<s:checkboxlist list=""></s:checkboxlist>:多选框
<s:combobox list=""></s:combobox>:下拉框
<s:component></s:component>:图像符号
5.4 D开头的标签
<s:date/>:获取日期格式
<s:datetimepicker></s:datetimepicker>:日期输入框
<s:debug></s:debug>:显示错误信息
<s:div></s:div>:表示一个块,类似于html的<div></div>
<s:doubleselect list="" doubleName="" doubleList=""></s:doubleselect>:双下拉框
5.5 E开头的标签
<s:if test="判断条件">满足条件时执行此处</s:if>
<s:elseif test="判断条件">满足条件时执行此处</s:elseif>
<s:else></s:else>:这3个标签可一起使用,表示条件判断
5.6 F开头的标签
<s:fielderror></s:fielderror>:显示文件错误信息
<s:file></s:file>:文件上传
<s:form action="" theme="" method="" namespace=""></s:form>:类似于html中的<form>
u 注意事项:
v <s:form>标签中主题theme默认为xhtml(以表格布局,可查看源代码,自动添加了table表格),如果不喜欢该主题,就设置theme="simple",此时就可以使用自定义的样式了。
v Struts2的<s:form>标签可以自动将Action的属性填写到页面form表单中,但密码除外!
5.7 G开头的标签
<s:generator separator="" val=""></s:generator>:和<s:iterator>标签一起使用
5.8 H开头的标签
<s:head/>:在<head></head>里使用,表示头文件结束
<s:hidden></s:hidden>:隐藏值
5.9 I开头的标签
<s:i18n name=""></s:i18n>:加载资源包到值堆栈
<s:include value=""></s:include>:包含一个输出,servlet或jsp页面
<s:inputtransferselect list=""></s:inputtransferselect>:获取form的一个输入
<s:iterator></s:iterator>:用于遍历集合
5.10 L开头的标签
<s:label></s:label>:只读的标签
5.11 M开头的标签
<s:merge></s:merge>:合并遍历集合出来的值
5.12 O开头的标签
<s:optgroup></s:optgroup>:获取标签组
<s:optiontransferselect doubleList="" list="" doubleName=""></s:optiontransferselect>:左右选择框
5.13 P开头的标签
<s:param></s:param>:为其他标签提供参数
<s:password size="" maxlength="" readonly=""></s:password>:密码输入框
<s:property/>:得到VS栈顶的元素
<s:push value=""></s:push>:value的值push到栈中,从而使property标签的能够获取value 的属性
u 注意事项:size属性规定输入字段的宽度(即是文本框只显示size个字符大小的宽度),由于 size属性是一个可视化的设计属性,所以我们可以使用CSS中的width来代替它。
5.14 R开头的标签
<s:radio list="OGNL需要迭代的集合" listValue="用作于每一个选项的提示"
listKey="用作于每一个要提交的值"></s:radio>:单选按钮
<s:reset></s:reset>:重置按钮
5.15 S开头的标签
<s:select list="OGNL需要迭代的集合" listValue="用作于每一个选项的提示"
listKey="用作于每一个要提交的值"></s:select>:单选框
<s:set name=""></s:set>:赋予变量一个特定范围内的值
<s:sort comparator=""></s:sort>:通过属性给list分类
<s:submit></s:submit>:提交按钮
<s:subset></s:subset>:为遍历集合输出子集
5.16 T开头的标签
<s:tabbedPanel id=""></s:tabbedPanel>:表格框
<s:table></s:table>:表格
<s:text name=""></s:text>:I18n文本信息
<s:textarea rows="" cols=""></s:textarea>:文本域输入框
<s:textfield size="" maxlength="" readonly=""></s:textfield>:文本输入框
<s:token></s:token>:拦截器
<s:tree></s:tree>:树
<s:treenode label=""></s:treenode>:树的结构
5.17 U开头的标签
<s:updownselect list=""></s:updownselect>:多选择框
<s:url></s:url>:创建url
u 注意事项:
v 以上标记,有的可以用单标记,有的也可用双标记。
v 以上标记,有的属性并未写全!在页面中的标签内,使用“Alt + /”可显示该标签所有的属性。
5.18所有标签都具备的属性
1)label:input类型的按钮,不能用label设置按钮上的文本,只能用value。在主题theme为simple下,label、tooltip不起作用。
2)labelposition 3)required 4)tooltip
5)tooltipIconPath 6)cssClass:html中的class
7)cssStyle:html中的style 8)value:用于获取值
9)name:在Struts2标签中,用于提交值或获取值;在普通HTML标签中用于提交值
u 注意事项:
v Struts2中的标签name属性有双重作用,获取值,提交值,前提是获取和提交值的名字一致,如果不一致则需要分别写value属性和name属性。
v HTML标签中如若有回显效果,也要提交值,则必须写value和name属性。
5.19案例:常用标签
step1:新建工程struts04,导入Struts2核心包,配置前端控制器
step2:在com.tarena.action包下,新建BaseAction,并继承RequestAware
public class BaseAction implements RequestAware{
protected Map<String,Object> request;
public void setRequest(Map<String, Object> request) {
this.request = request; } }
step3:在com.tarena.entity包下,新建Favor实体类
private int id; private String name; ……各自get/set方法
public Favor(int id, String name) { this.id = id; this.name = name; }
step4:在com.tarena.dao包下,新建FavorDAO类,用于模拟访问数据库
public List<Favor> findAll(){
List<Favor> list = new ArrayList<Favor>();
list.add(new Favor(1,"睡觉")); list.add(new Favor(2,"上网"));
list.add(new Favor(3,"吃喝")); list.add(new Favor(4,"编程")); return list; }
step5:在com.tarena.action包下,新建FormAction
private String name;//output private String password; private int age;
private String sex; private String desc;
private boolean marry;//单个checkbox选中
private int[] favorsKey;//多个checkbox选中
private int favor;// select选中
public String execute() {//模拟从数据取出原有数据
name = "常"; password = "123"; age = 24; sex = "M";
desc = "我是一个好人"; marry = true;
FavorDAO favorDao = new FavorDAO();//获取数据
List<Favor> list = favorDao.findAll(); request.put("favors", list);//放入request中
favorsKey = new int[] { 1, 4 };//设置哪些checkbox选中
favor = 4;//设置select选中 return "success"; }
step6:在WEB-INF中创建jsp文件夹,并将form.jsp放入
<s:form action="form" theme="simple">
姓名:<s:textfield name="name" maxlength="8" size="30"></s:textfield><br/>
密码:<s:password name="password"></s:password><br/>
年龄:<s:textfield name="age"></s:textfield><br/>
性别:<s:radio list='#{"M":"男","F":"女"}' name="sex"></s:radio><br/>
婚姻状况:<s:checkbox name="marry"></s:checkbox><br/>
兴趣爱好:<s:checkboxlist list="#request.favors" listKey="id"
listValue="name" name="favorsKey"></s:checkboxlist><br/>
select:<s:select list="#request.favors" listKey="id" listValue="name" name="favor">
</s:select><br/>
简介:<s:textarea name="desc" cols="10" rows="3"></s:textarea><br/>
</s:form>
step7:创建struts.xml文件,并进行配置
<struts>
<package name="uitag" extends="struts-default">
<action name="form" class="com.tarena.action.FormAction">
<result>/WEB-INF/jsp/form.jsp</result>
</action>
</package>
</struts>
step8:部署,测试,地址栏输入:http://localhost:8080/struts04/form.action,其中password标签没有回显效果:
step9:不用Struts2标签,则以前的方式为:
<%@taglib uri="/struts-tags" prefix="s"%>
<form><!-- 以前的方式 -->
Name:<input type="text" name="" value="${emp.name}" /><br />
Salary:<input type="text" name="" value="${emp.salary}" /><br />
Sex:<s:if test="emp.sex.equals('m')">
<input type="radio" name="sex" value="m" checked="checked">Male
<input type="radio" name="sex" value="f">Female
</s:if>
<s:else>
<input type="radio" name="sex" value="m">Male
<input type="radio" name="sex" value="f" checked="checked">Female
</s:else>
</form>
六、
拦截器
6.1 Struts2详细流程图
6.2拦截器的作用
拦截器适合封装一些共通处理,便于重复利用。例如:请求参数给Action属性,日志的记录,权限检查,事务处理等。拦截器是通过配置方式调用,因为使用方法比较灵活,便于维护和扩展。
6.3拦截器的常用方法
1)如果不调用下面的两种方法,那么就不会调用Action,也不会调用后面的Interceptor,拦截器中的return的String觉定了最后的Result(极端情况下)。
2)arg0.invoke();调用Action,也包括Result,拦截器中的return的内容无效。
3)arg0.invokeActionOnly();调用Action,不包括后面的Interceptor和Result,拦截器中的return的String决定了最后的Result。
4)在拦截器中,如何调用底层API:
①可调用ActionInvocation对象的getStack方法获取VS。
②可调用ServletActionContext的静态方法获取Servlet API,如getResponse获取response。
6.4自定义拦截器步骤
step1:编写拦截器组件,组件类实现Interceptor接口,实现约定的interceptor方法。在该方法中添加自定义的共通处理
pubic class DemoInterceptor implments Interceptor{
public String interceptor(ActionInvocation ai){
//拦截器前部分处理
ai.invoke();//执行action和result,或ai.invokeActionOnly()
//拦截器后续处理
}
}
step2:将拦截器注册给Struts2框架,在struts.xml中注册(添加该拦截器的声明)
<package>
<interceptors>
<interceptor name="名称" class="实现类" />
//...其他interceptor
</interceptors>
</package>
step3:使用拦截器组件(添加该拦截器的引用)
1)方式一:定义默认拦截器
<default-interceptor-ref name="拦截器名" />
u 注意事项:默认(不写)情况下,一个Action在执行时,会默认调用defaultStack拦截器栈。
2)方式二:显式指定调用哪个拦截器
<action>
<interceptor-ref name="拦截器名"/>
//...可以写多个
</action>
u 注意事项:当指定Action调用的拦截器后,默认的defaultStack将不再执行!
6.5 Struts2内置的拦截器
在struts-default.xml中可查看。
<interceptors>
<interceptor name="cookie" class="org.apache.struts2.interceptor.CookieInterceptor"/>
<interceptor name="i18n" class="com.opensymphony.xwork2.interceptor.I18nInterceptor"/>
<interceptor name="token" class="org.apache.struts2.interceptor.TokenInterceptor"/>
<interceptor name="store" class="org.apache.struts2.interceptor.MessageStoreInterceptor" />
<interceptor name="checkbox" class="org.apache.struts2.interceptor.CheckboxInterceptor" />
…… …… ……
<interceptor-stack name="defaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="servletConfig"/>
<interceptor-ref name="chain"/>
…… ……
</interceptor-stack>
…… …… ……
</interceptors>
…… …… ……
<default-interceptor-ref name="defaultStack"/>
6.6案例:拦截器入门
step1:新建工程struts05,导入Struts2核心包,配置前端控制器
step2:在com.tarena.intercept包下,新建SomeInterceptor类,并实现Interceptor
public class SomeInterceptor implements Interceptor {
private String name;//设置拦截器参数 ……get/set方法
public void destroy() { System.out.println("SomeInterceptor.destroy()..."); }
public void init() { System.out.println("SomeInterceptor.init()..."); }
public String intercept(ActionInvocation arg0) throws Exception {
/** 自定义拦截器,一般会用到底层的request、response、session、servletContext,但是在Action里不允许出现底层的API,就是写纯业务代码! */
/** ActionContext ctx=arg0.getInvocationContext();
ValueStack stack=ctx.getValueStack();//拿到VS
//stack.setValue("ognl", obj);//放
//Object obj=stack.findValue("ognl");//取
stack.setValue("name", "chang");
HttpServletRequest request = ServletActionContext.getRequest();//获得request对象
System.out.println(request);
HttpServletResponse response = ServletActionContext.getResponse();//获得response
System.out.println(response);
ServletContext application = ServletActionContext.getServletContext();
System.out.println(application); */
System.out.println("SomeInterceptor:"+name);
//ActionInvocation封装了Action和Result的整个调用流程
System.out.println("SomeInterceptor.intercept()...before");//在调用Action之前的操作
arg0.invoke(); //String result=arg0.invokeActionOnly(); System.out.println(result);
System.out.println("SomeInterceptor.intercept()...after");//调用Action之后的操作
//return "success";//若不调用invoke和invokeActionOnly方法,则当前是极端情况,则跟Action没关系了,由拦截器决定返回哪个页面
return "error";//当调用invoke()方式时,return什么都行 } }
u 注意事项:
v invoke():调用Action,同时已经决定了Result,所以Result不能再换了,由Action决定Result。同时继续调用后面的拦截器。
v invokeActionOnly():调用Action,但是还没有决定Result,则Result由拦截器说了算,同时后面的拦截器也不调用了,直接掉Action。
step3:在com.tarena.action包下,新建SomeAction类
private String name; ……get/set方法
public String execute(){ System.out.println("Action:"+name);
System.out.println("SomeAction.execute()..."); return "success"; }
step4:新建struts.xml文件,将拦截器注册给Struts2框架,指定拦截器作用的Action
<package name="day05" namespace="/" extends="struts-default">
<interceptors><!-- 声明拦截器,注意和action的先后顺序 -->
<!-- 单个拦截器 -->
<interceptor name="some" class="com.tarena.intercept.SomeInterceptor"/>
</interceptors>
<action name="some" class="com.tarena.action.SomeAction">
<interceptor-ref name="basicStack"/><!--引用拦截器,必须写,见注意事项-->
<!--<interceptor-ref name="some"/> 引用拦截器,不加参数这么写 -->
<!--前面的拦截器若调用invokeActionOnly则后面的拦截器都失效,见step10-->
<interceptor-ref name="some"><!-- 引用拦截器,加参数这么写 -->
<param name="name">ABC</param>
</interceptor-ref><!-- 自己定义的拦截器放里面,贴身(离Result近)的 -->
<result name="success">/WEB-INF/jsp/some.jsp</result>
<result name="error">/WEB-INF/jsp/error.jsp</result>
</action>
</package>
u 注意事项:
v 上面代码中,如果不写拦截器,则默认是defaultStack拦截器栈,但自己写上拦截器后,系统不会再添加默认拦截器!所以自己写项目时,最少也要写basicStack,也可写defaultStack。一般情况下,自己定义的拦截器放里面,贴身(离Result近)的。
v 自己写拦截器的目的是扩展拦截器,增加Struts2的功能,是通用性的,不要把业务代码也写里面,所以程序员自己写拦截器的情况很少!
step5:在WEB-INF/jsp下,新建some.jsp
<h1>Some jsp</h1>
step6:部署,测试,地址栏输入:http://localhost:8080/struts05/some.action?name=chang,页面显示“Some jsp”,而控制台输出如下:
SomeInterceptor:ABC
SomeInterceptor.intercept()...before
Action:chang
SomeAction.execute()...
SomeInterceptor.intercept()...after
step7:在com.tarena.intercept包下,再创建一个拦截器OtherInterceptor
public class OtherInterceptor implements Interceptor{
public void destroy() { System.out.println("OtherInterceptor.destroy()..."); }
public void init() { System.out.println("OtherInterceptor.init()..."); }
public String intercept(ActionInvocation arg0) throws Exception {
System.out.println("OtherInterceptor.intercept()...before");
arg0.invoke();
System.out.println("OtherInterceptor.intercept()...after");
return "success";// 因为采取invoke(),所以return null也可以 } }
step8:在struts.xml中添加该拦截器的声明和引用
<interceptors>
<interceptor name="some" class="com.tarena.intercept.SomeInterceptor"/>
<interceptor name="other" class="com.tarena.intercept.OtherInterceptor"/>
</interceptors>
<action name="some" class="com.tarena.action.SomeAction">
<interceptor-ref name="basicStack"/>
<!-- 前面的拦截器若调用invokeActionOnly,则后面的拦截器都失效了,见step10 -->
<interceptor-ref name="some">
<param name="name">ABC</param>
</interceptor-ref>
<interceptor-ref name="other"/><!-- other离Result最近,可调用invokeActionOnly,对前面的拦截器没有影响 -->
<result name="success">/WEB-INF/jsp/some.jsp</result>
<result name="error">/WEB-INF/jsp/error.jsp</result>
</action>
step9:部署,测试,地址栏输入:http://localhost:8080/struts05/some.action?name=chang,页面显示“Some jsp”,而控制台输出如下:
SomeInterceptor:ABC
SomeInterceptor.intercept()...before
OtherInterceptor.intercept()...before
Action:chang
SomeAction.execute()...
OtherInterceptor.intercept()...after
SomeInterceptor.intercept()...after
step10:测试“前面的拦截器若调用invokeActionOnly,则后面的拦截器都失效了”这句话。把SomeInterceptor拦截器中的arg0.invoke();改为arg0.invokeActionOnly();//结果由拦截器决定
step11:由于SomeInterceptor拦截器的返回结果为:return "error";所以在WEB-INF/jsp下,新建error.jsp
<h1>Error jsp</h1>
step12:部署,测试,地址栏输入:http://localhost:8080/struts05/some.action?name=chang,页面显示“Error jsp”,而控制台输出如下:即OtherInterceptor拦截器没被调用、失效
SomeInterceptor:ABC
SomeInterceptor.intercept()...before
Action:chang
SomeAction.execute()...
SomeInterceptor.intercept()...after
step13:把SomeInterceptor改回arg0.invoke();,把OtherInterceptor改成arg0.invokeActionOnly();看结果有何变化
step14:部署,测试,地址栏输入:http://localhost:8080/struts05/some.action?name=chang,页面显示“Some jsp”,而控制台输出如下:即修改OtherInterceptor拦截器为invokeActionOnly(),则对前面的拦截器没有影响
SomeInterceptor:ABC
SomeInterceptor.intercept()...before
OtherInterceptor.intercept()...before
Action:chang
SomeAction.execute()...
OtherInterceptor.intercept()...after
SomeInterceptor.intercept()...after
6.7拦截器栈
拦截器栈相当于一组拦截器的组合。在引用时,就把它当成一个拦截器。
例如,6.5案例中的struts.xml也可这么写:
<package name="day05" namespace="/" extends="struts-default">
<interceptors><!-- 注意和action的先后顺序,假设存在拦截器ThreeInterceptor -->
<interceptor name="some" class="com.tarena.intercept.SomeInterceptor"/>
<interceptor name="other" class="com.tarena.intercept.OtherInterceptor"/>
<interceptor name="three" class="com.tarena.intercept.ThreeInterceptor"/>
<interceptor-stack name="someother">
<interceptor-ref name="some" />
<interceptor-ref name="other" />
</interceptor-stack>
<interceptor-stack name="all">
<interceptor-ref name="someother" />
<interceptor-ref name="three" />
</interceptor-stack> -->
</interceptors>
<action name="some" class="com.tarena.action.SomeAction">
<interceptor-ref name="basicStack"/>
<interceptor-ref name="all"/>
<result name="success">/WEB-INF/jsp/some.jsp</result>
…… ……
6.8 fileUpload拦截器原理
该拦截器首先会调用commons-file-upload.jar组件,将客户端上传的文件保存到服务器临时目录下,之后将临时目录下的文件对象给Action属性赋值。当Action和Result调用完毕之后,清除临时目录下的文件。因此在Action业务方法中,需要做文件复制,将临时文件转移到目标目录中。
6.9案例:使用fileUpload拦截器实现文件上传
step1:在原有5个核心包的基础上添加commons-io-1.3.2.jar到struts05工程的lib中
step2:在WEB-INF/jsp下,新建upload.jsp
<h1>文件上传</h1>
<form action="upload.action" method="post" enctype="multipart/form-data">
<input type="file" name="some">
<input type="submit" value="提交" />
</form>
u 注意事项:method必须为post!enctype必须为multipart/form-data!
step3:在com.tarena.action包下,新建BaseAction,并实现ServletContextAware,用于获取application对象,在后面会使用
public class BaseAction implements ServletContextAware {
protected ServletContext application;
public void setServletContext(ServletContext context) { this.application = context; }
public String toRealPath(String path) {// 该方法功能:得到绝对路径
return application.getRealPath(path); } }
step4:根据fileUpload的实现原理,在com.tarena.util包下,新建FileUtil类,用于复制上传的文件
public static boolean copy(File src, File dest) {
BufferedInputStream bis = null; BufferedOutputStream bos = null;
try { bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dest));
byte[] bts = new byte[1024];
int sum = -1;
while ((sum = bis.read(bts)) != -1) { bos.write(bts, 0, sum); }
return true;
} catch (Exception e) { e.printStackTrace(); return false;
} finally { if (bis != null) { try { bis.close(); } catch (IOException e) {
e.printStackTrace(); } }
if (bos != null) { try { bos.close(); } catch (IOException e) {
e.printStackTrace(); } } }
step5:在com.tarena.action包下,新建UploadAction,并继承BaseAction
public class UploadAction extends BaseAction{
private File some;//临时文件对象,属性名和表单提交写的名要相同!
private String someFileName; //原文件名,XXXFileName是固定写法,自动获得上传时的文件名
private String someContentType; //原文件类型
private String filePath;//存放文件的路径
……各自get/set方法
public String execute(){//测试文件大小超出范围,这的输出要注释掉,否则空指针
System.out.println(some); System.out.println(some.length());
System.out.println(someFileName); System.out.println(someContentType);
if(some == null){ return "error"; }
String fileName = "file_" + System.currentTimeMillis()
+ someFileName.substring(someFileName.lastIndexOf("."));
System.out.println( "fileName:" + fileName);//随机生成上传文件名
filePath = "upload/" + fileName;//上传文件的相对路径,页面的链接使用
//上传文件的绝对路径,写文件时使用,toRealPath()方法定义于BaseAction中
String realFilePath = toRealPath(filePath);
System.out.println("realFilePath :" + realFilePath);
FileUtil.copy(some, new File(realFilePath));//从缓存中读取图片文件
//IOUtils.copy(bis, bos);//commons.io.IOUtils提供的复制
return "success"; } }
step6:在WEB-INF/jsp中新建ok.jsp和error.jsp
1)ok.jsp
<h1>文件上传成功</h1>
<img src="${filePath}" />
2)error.jsp
<h1>文件上传失败,文件太大了。请<a href="fileform.action">重试</a></h1>
step7:在struts.xml中添加配置
<action name="fileform">
<result>/WEB-INF/jsp/upload.jsp</result>
</action>
<action name="upload" class="com.tarena.action.UploadAction">
<interceptor-ref name="fileUpload"><!--Struts2带的拦截器,注意和basicStack的顺序-->
<param name="maximumSize">30000</param><!--单位字节-->
</interceptor-ref>
<interceptor-ref name="basicStack" /><!-- 它不包含fileUpload拦截器 -->
<result name="success">/WEB-INF/jsp/ok.jsp</result>
<result name="error">/WEB-INF/jsp/error.jsp</result>
</action>
step8:部署,测试,地址栏输入:http://localhost:8080/struts05/fileform.action,上传图片大小小于30KB的,则显示如下:
step9:如果上传图片太大,则显示如下(记得把UploadAction中的输出语句注释掉,否则报空指针异常):
6.10 NetCTOSS项目:登录检查拦截器
step1:在com.tarena.netctoss.interceptor包下,新建SessionValidateInterceptor拦截器类,该拦截器的作用是:从session中取KEY为“com.tarena.netctoss.admin.key”的value,如果取到的value是null,说明该用户没有登录,返回到登录页面。
public class SessionValidateInterceptor implements Interceptor {
private String key; private String errorResult;//设置这两个参数,是为了通用性
……各自get/set方法
public void destroy() { }
public void init() { }
public String intercept(ActionInvocation arg0) throws Exception {
if(key==null){//拦截器有通信性,当用户没有配置key时,应该是拦截器失效,而不是空指针等,灾难性错误(key的值会在xml文件中配置)。
arg0.invoke(); return null; }
HttpSession session = ServletActionContext.getRequest().getSession();
Object value=session.getAttribute(key);
if(value == null){ return errorResult;//errorResult的值也会在xml文件中配置
}else{ arg0.invoke(); return null; } } }
step2:在struts-main.xml中添加拦截器配置,加一个新的<package>
<package name="netctoss-default" abstract="true" extends="json-default"><!--继承json包-->
<!-- abstract="true"表此包可以被继承 -->
<interceptors>
<interceptor name="sessionValidate"
class="com.tarena.netctoss.interceptor.SessionValidateInterceptor">
</interceptor>
<interceptor-stack name="netctossStack">
<interceptor-ref name="defaultStack" />
<interceptor-ref name="sessionValidate">
<param name="key">com.tarena.netctoss.admin.key</param>
<param name="errorResult">loginForm</param><!--下面有定义-->
</interceptor-ref>
</interceptor-stack><!-- 注意:个别多余的空行也会报错! -->
</interceptors>
<default-interceptor-ref name="netctossStack" /><!--所有Action默认用该拦截器栈-->
<global-results><!-- 包里所有的Action都能用的Result -->
<result name="loginForm" type="redirectAction"><!-- 这里用重定向好些 -->
<param name="namespace">/login</param><!-- 跨包了,所以这么写 -->
<param name="actionName">loginform</param>
</result>
</global-results><!-- 注意和default-interceptor-ref的顺序 -->
</package>
step3:修改struts-cost.xml,使该包继承netctoss-default包
<package name="cost" namespace="/cost" extends="netctoss-default">
step4:修改struts-main.xml,使main包继承netctoss-default包
<package name="main" namespace="/main" extends="netctoss-default">
step5:部署,测试,地址栏输入:http://localhost:8080/NetCTOSS/cost/list.action,则会跳转到登录页面;地址栏输入:http://localhost:8080/NetCTOSS/main/main.action,也会跳转到登录页面;只有登录后才可正常访问
七、
Struts2中如何处理异常
7.1异常一般出现在何处
MVC模式,一般M出现异常!
M要做状态恢复、连接关闭、资源关闭、数据库回滚,然后抛出异常。
7.2如何配置异常
根据抛出不同的异常,转到不同的异常页面。
step1:新建工程struts06,导入Struts2核心包,配置前端控制器
step2:在exception包下,新建DAOException,并继承Exception
public class DAOException extends Exception { //无内容的 }
step3:在dao包下,新建FooDAO,用于模拟DAO层
public class FooDAO {
public void foo() throws DAOException{ throw new DAOException(); } }
step4:在action包下,新建TestAction,用于模拟控制层
public class TestAction {
public String execute() throws Exception{ System.out.println("TestAction.execute()...");
FooDAO fooDAO=new FooDAO();//模拟自定义异常
//FooDAO fooDAO=null;//模拟运行时异常
fooDAO.foo(); return "success"; } }
step5:在WEB-INF/jsp下新建dao_error.jsp和rt_error.jsp
1)dao_error.jsp
<h1 style="color:red;">DAO异常</h1>
2)rt_error.jsp
<h1 style="color:red;">运行时异常</h1>
step6:新建struts.xml文件,进行异常的配置
<package name="test" namespace="/" extends="struts-default">
<action name="test" class="action.TestAction"><!--注意元素间的顺序,自定义异常-->
<exception-mapping result="daoError" exception="exception.DAOException" />
<exception-mapping result="rtError" exception="java.lang.RuntimeException" />
<result name="daoError">/WEB-INF/jsp/dao_error.jsp</result>
<result name="rtError">/WEB-INF/jsp/rt_error.jsp</result>
</action>
</package>
step7:部署,测试,地址栏输入:http://localhost:8080/struts06/test.action,若模拟DAO异常,则页面显示DAO异常;若模拟运行时异常,则页面显示运行时异常
step8:也可定义全局异常,注意:在<action>前定义
<global-results><result name="rtError">/WEB-INF/jsp/rt_error.jsp</result></global-results>
<global-exception-mappings>
<exception-mapping result="rtError" exception="java.lang.RuntimeException" />
</global-exception-mappings>
u 注意事项:若全局和局部同名,听局部的。
八、
Struts2中如何实现国际化
8.1 i18n
把页面中的文字全部写在属性文件“.properties”中(也可叫资源文件)。由于该文件不能写中文,所以对于中文,要写对应的Unicode编码 \uxxxx\uxxxx\uxxxx。
8.2如何获得中文的Unicode编码
step1:Windows系统:从开始菜单,进入命令控制行,或搜索栏直接输入cmd
step2:输入jdk命令:源文件 目标文件
native2ascii 1.properties 2.properties
8.3浏览器如何决定用哪个资源文件
资源文件的命名有规范!命名正确则自动切换,由浏览器发送的请求来确定是哪个国家哪个语言,放Action包中。
zh:中文的国际编码 CN:中国的国际编码
en:英文的国际编码 US:美国的国际编码
8.4资源文件的命名
例如:TestAction_zh_CN.properties 和 TestAction_en_US.properties
但是这么写的话,只能是TextAction用,若Action有共同的部分,则把前面的TestAction替换为package,例如:package_zh_CN.properties,此时该属性文件被所在包中的所有Action都可用。
8.5资源文件的分类
1)类级:TestAction_zh_CN.properties。
2)包级:package_zh_CN.properties,类级和包级资源文件放Action包中。
3)全局:放src下,message_zh_CN.properties,前面的名字(即message)可随便起,但必须在struts.xml中配置:
<constant name="struts.custom.i18n.resources" value="message"></constant>
u 注意事项:
v 全局的配置,在<package>标签外配置。
v 资源信息先去类级里找,然后包级,最后全局。
v 若局部和全局的命名相同,则听局部的。
8.6实现国际化的步骤
step1:新建工程struts07,导入Struts2核心包,配置前端控制器,准备好资源文件
1)TestAction_zh_CN.properties,放Action中
jsp.welcome=\u6B22\u8FCE\u4F60
2)TestAction_en_US.properties,放Action中
jsp.welcome=Welcome!
jsp.name=chang
3)package_en_US.properties,放Action中
jsp.name=hello
4)message_en_US.properties,放src目录下
jsp.logo=Struts2
step2:在action包中,新建TestAction,且必须继承ActionSupport类
public class TestAction extends ActionSupport {
private String name; ……get/set方法
public String execute() throws Exception {
System.out.println("TestAction.execute(..)...");
name = getText("jsp.name"); return "success"; } }
u 注意事项:Action中的错误提示信息,也可以放入资源文件中,用key表示。由于继承了ActionSupport,所以有了getText("键的名字");方法,可以把这个返回值赋给Action中的错误提示信息属性。
step3:在WEB-INF/jsp文件夹中,新建main.jsp,并在页面中使用标签:<s:text name="文件中共同的地方,键" />
<%@taglib uri="/struts-tags" prefix="s"%>
<s:text name="jsp.welcome"/>
<s:text name="jsp.name"/>
<s:text name="jsp.logo"/>
<s:property value="name"/>
step4:在struts.xml中配置全局资源文件
<struts>
<constant name="struts.custom.i18n.resources" value="message" />
<package name="test" namespace="/" extends="struts-default">
<action name="i18n" class="action.TestAction">
<result name="success">/WEB-INF/jsp/main.jsp</result>
</action>
</package>
</struts>
step5:部署,测试,地址栏输入:http://localhost:8080/struts07/i18n.action,由于当前为中文操作系统,所以显示结果为:欢迎你 jsp.name jsp.logo jsp.name
step6:如何模拟英文操作系统?可以采取修改浏览器首选语言的方式,修改后重新发请求,则显示结果为:Welcome! chang Struts2 chang
u 注意事项:服务器安装的操作系统是哪个国家的语言,则是默认的语言资源文件。如:服务器上安的是中文的系统,则中文资源文件是默认的,那么第三方其他国家的用户看得则是中文。
九、
NetCTOSS项目
9.1 DAO优化、重构、封装!【重要】
step1:在com.tarena.netctoss.entity包中,新建Entity抽象类,且内容为空,目的是为了封装后,返回的对象是父类型
public abstract class Entity { //内容为空 }
step2:让每个实体类继承Entity类
public class Cost extends Entity{ ... }
public class Admin extends Entity{ ... }
step3:在com.tarena.netctoss.dao.impl包中,新建BaseDAO抽象类
public abstract class BaseDAO {//该封装可以和之前写的方法进行比较~
/** 去除List的泛型,加final修饰,为的是防止不小心重写,其中的一个参数为对象数组,对象数组可以放任何类型的参数。该方法的封装只可进行查询操作 */
protected final List query(String sql,Object[] params) throws DAOException{
Connection conn=null;
try { conn = DBUtils.openConnection();//根据指定的SQL进行查询
PreparedStatement stmt=conn.prepareStatement(sql);
if(params!=null){
for(int i=0;i<params.length;i++){
stmt.setObject(i+1, params[i]);//第一个问号?是从1开始的
/** 如果需要任意参数类型转换,使用setObject方法,该方法将
给定的参数转换为相应的SQL类型。 */
}
}
ResultSet rs=stmt.executeQuery();//执行查询操作
List entityList=new ArrayList();//返回entityList集合
while(rs.next()){//根据子类分别重写的toEntity方法,进行转换
entityList.add(toEntity(rs)); }
return entityList;
} catch (SQLException e) { e.printStackTrace();
throw new DAOException("访问异常", e);
} finally{ DBUtils.closeConnection(conn); } }
/** 定义抽象方法toEntity(),由子类去实现:将结果集中的每一条数据转换成对应的每一个实体对象 */
public abstract Entity toEntity(ResultSet rs) throws SQLException;
/** 加final修饰,为的是防止不小心重写。该方法的封装可进行增、删、改操作 */
protected final int update(String sql,Object[] params) throws DAOException {
Connection conn=null;
try { conn = DBUtils.openConnection();//根据指定的SQL进行查询
PreparedStatement stmt=conn.prepareStatement(sql);
if(params!=null){
for(int i=0;i<params.length;i++){
stmt.setObject(i+1, params[i]);//第一个问号?是从1开始的
}
}
return stmt.executeUpdate();//执行更新操作,会返回影响的记录数
} catch (SQLException e) { e.printStackTrace();
throw new DAOException("访问异常", e);
} finally{ DBUtils.closeConnection(conn); } } }
step4:让每个实现类都继承BaseDAO
public class CostDAOImpl extends BaseDAO implements CostDAO { ... }
public class AdminDAOImpl extends BaseDAO implements AdminDAO { ... }
step5:重构CostDAOImpl中的方法,并实现toEntity()方法
public List<Cost> findAll() throws DAOException {//重构后
return query(findAll, null); }
public List<Cost> findAll(int page, int rowsPerPage) throws DAOException {//重构后
int start=(page-1)*rowsPerPage +1; int end=start+rowsPerPage;
return query(findAllByPage,new Object[]{end,start}); }
public int getTotalPages(int rowsPerPage) throws DAOException {//方法不变 ... }
public void delete(int id) throws DAOException {//重构后
update(delete, new Object[]{id}); }
public Cost findByName(String name) throws DAOException {//重构后
List<Cost> costList=query(findByName, new Object[]{name});
if(costList!=null&&costList.size()>0){ return costList.get(0);
}else { return null; } }
……以后其他的方法能用当前的封装就用,不能用则单独写……
/** 在Cost实体类中实现toEntity方法,之前的toCost(ResultSet rs)方法删除掉 */
public Entity toEntity(ResultSet rs) throws SQLException {
Cost cost = new Cost(); cost.setId(rs.getInt("ID"));
cost.setName(rs.getString("NAME"));
cost.setBaseDuration(rs.getInt("BASE_DURATION"));
cost.setBaseCost(rs.getFloat("BASE_COST"));
cost.setUnitCost(rs.getFloat("UNIT_COST"));
cost.setStartTime(rs.getDate("STARTIME"));
cost.setStatus(rs.getString("STATUS"));
cost.setCreaTime(rs.getDate("CREATIME"));
cost.setDescr(rs.getString("DESCR"));
cost.setCostType(rs.getString("COST_TYPE"));
return cost;//此处Cost类要继承Entiy类,才能返回Cost类型 }
step6:重构AdminDAOImpl中的方法,并实现toEntity()方法
public Admin findByCodeAndPwd(String code, String pwd) throws DAOException {//重构后
List<Admin> adminList = query(findByCodeAndPwd, new Object[]{code,pwd});
if (adminList != null && adminList.size() > 0) { return adminList.get(0);
} else { return null; } }
……以后其他的方法能用当前的封装就用,不能用则单独写……
/** 在Admin实体类中实现toEntity方法,之前的toAdmin(ResultSet rs)方法删除掉 */
public Entity toEntity(ResultSet rs) throws SQLException {
Admin admin = new Admin(); admin.setId(rs.getInt("ID"));
admin.setAdminCode(rs.getString("ADMIN_CODE"));
admin.setPassword(rs.getString("PASSWORD"));
admin.setName(rs.getString("NAME"));
admin.setTelephone(rs.getString("TELEPHONE"));
admin.setEnrollDate(rs.getDate("ENROLLDATE"));
admin.setEmail(rs.getString("EMAIL"));
return admin;//此处Admin类要继承Entiy类,才能返回Admin类型 }
step7:部署,测试,一切正常
9.2资费更新
step1:在DAO优化、重构、封装的基础上进行
step2:在CostDAO接口中添加方法定义
public Cost findById(Integer id) throws DAOException;
public void update(Cost cost) throws DAOException;
step3:在CostDAOImpl中实现方法
private static String findById= "select ID, NAME, BASE_DURATION, BASE_COST,
UNIT_COST, CREATIME, STARTIME, STATUS, DESCR,COST_TYPE from COST_CHANG
where ID=?";
private static String update="update COST_CHANG set NAME=?,BASE_DURATION=?,
BASE_COST=?,UNIT_COST=?,DESCR=?,COST_TYPE=? where ID=?";
public Cost findById(Integer id) throws DAOException {
List<Cost> costList=query(findById, new Object[]{id});
if(costList!=null&&costList.size()>0){ return costList.get(0);
}else { return null; } }
public void update(Cost cost) throws DAOException {//实现接口的方法
Object[] params={cost.getName(),cost.getBaseDuration(),cost.getBaseCost(),
cost.getUnitCost(),cost.getDescr(), cost.getCostType(),cost.getId()};
update(update,params);//封装类中的方法 }
step4:在BaseAction中,实现RequestAware接口,并添加set方法
public class BaseAction implements SessionAware,Constants,RequestAware {
protected Map<String, Object> session;
protected Map<String, Object> request;
public void setSession(Map<String, Object> session) { this.session = session; }
public void setRequest(Map<String, Object> request) { this.request = request; }
public String execute() throws Exception { return "success"; } }
step5:在com.tarena.netctoss.action.cost包中,新建UpdateCostAction和UpdateCostFormAction
1)UpdateCostAction,并继承BaseACtion
private Cost cost; private int page;//页面的form把page传入 ……各自get/set方法
private CostDAO costDAO=DAOFactory.getCostDAO();
public String execute()throws DAOException{
try{ costDAO.update(cost); request.put("ok",0);
}catch (Exception e) { e.printStackTrace(); request.put("ok",1); }
return "success"; }
2)UpdateCostFormAction
private Cost cost; private int id; private int page;//用于保存请求时的页数
private CostDAO costDAO=DAOFactory.getCostDAO();//不需要get/set方法
……各自get/set方法
public String execute() throws DAOException{
cost=costDAO.findById(id); return "success"; }
u 注意事项:上面两个Action可以合成一个,使用不同的方法名区分,然后在配置文件指定各自的方法名即可。
step6:在struts-cost.xml中添加配置
<action name="updateForm"
class="com.tarena.netctoss.action.cost.UpdateCostFormAction">
<result name="success">/WEB-INF/jsp/cost/cost_modi.jsp</result>
</action>
<action name="update" class="com.tarena.netctoss.action.cost.UpdateCostAction">
<result name="success">/WEB-INF/jsp/cost/cost_modi.jsp</result>
</action>
step7:修改cost_list.jsp,增加修改连接
<input type="button" value="修改" class="btn_modify" onclick="location='updateForm.action?id=${id }&page=${page }'"/>
step8:复制资费静态页面到工程中,并修改为cost_modi.jsp,记得导入各种资源
<%@taglib uri="/struts-tags" prefix="s"%>
<script language="javascript" src="../js/jquery-1.4.1.min.js"></script>
<script language="javascript" type="text/javascript">
$(function(){ $('#save').click(function(){ $('#costForm').submit(); }); })
//body定义onload事件,页面刷新时执行,根据request中ok的值显示不同的提示。
function showMsg(){
feeTypeChange(${cost.costType});//读取页面判断资费类型从而显示不同效果
var flag="${ok}";//若ok值没有,则var flag="";如果不写引号,则var flag= ;,有些浏览器不认识
var msg_div=document.getElementById("save_result_info")
if(flag=="0"){ msg_div.innerHTML="修改成功!"; showResult();
}else if(flag=="1"){//直接写else则初始状态为空则也显示失败!
msg_div.innerHTML="修改失败!"; showResult(); } }
//注意:把原来的自费类型切换函数改一改:.className="width148",.readOnly为大写的字母“o”,否则无效
</script>
<body onload="showMsg();"><!--页面一刷新则调用--><!--请求中加page是为了返回用-->
<form id="costForm" action="update.action?page=${page}" method="post" ……
<input name="cost.id" type="text" class="readonly" readonly
value="<s:property value='cost.id'/>" /> ……
<input name="cost.name" id="costName" type="text"
value="<s:property value='cost.name'/>" /> ……
<s:radio list='#{"1":"包月","2":"套餐","3":"计时"}' name="cost.costType"
onclick="feeTypeChange(this.value);"/> ……
<input name="cost.baseDuration" type="text"
value="<s:property value='cost.baseDuration'/>" /> ……
<input name="cost.baseCost" type="text"
value="<s:property value='cost.baseCost'/>" /> ……
<input name="cost.unitCost" type="text"
value="<s:property value='cost.unitCost'/>" /> ……
<s:textarea name="cost.descr" cssClass="width300 height70"/>
<input type="button" value="取消" class="btn_save"
onclick="location.href='list.action?page=${page }';" /> ……
u 注意事项:使用HTML中的<textarea>标签,会在描述信息的前、后,莫名其妙的多出空格。
step9:部署,测试,一切正常
9.3导航条
step1:为了方便使用,把页面中共同的部分:导航条提取出来。这样处理后,我们只要修改一次连接地址,则所有页面都可生效
step2:在WEB-INF/jsp文件夹中,新建head.jsp,该jsp页面只有如下内容,没有其他HTML标签
<%@page pageEncoding="utf-8"%>
<script language="javascript" type="text/javascript">
$(function(){ <% String uri=request.getRequestURI();
System.out.println("请求的uri为:"+uri); %>
var uri="<%=request.getRequestURI() %>";//获得uri
$('#menu a').each(function(){//相当于foreach 得到a链接的整个元素,去挨个比较
if(uri.indexOf($(this).attr("id"))>0){//找到后,更改样式
$(this).removeClass($(this).attr("id")+"off");
$(this).addClass($(this).attr("id")+"on");
}else if(uri.indexOf("cost_")>0){//样式中没有cost_所以单独处理。或改页面名字
$("#cost_").removeClass("fee_off");
$("#cost_").addClass("fee_on");
}else if(uri.indexOf("index")>0){//主页也特殊,要单独处理。或者改主页的名字
$("#index").removeClass("index_off");
$("#index").addClass("index_on"); } });
});
</script>
<ul id="menu">
<li><a id="index" href="/NetCTOSS/main/main.action" class="index_off"></a></li>
<li><a id="role_" href="" class="role_off"></a></li>
<li><a id="admin_" href="" class="admin_off"></a></li>
<li><a id="cost_" href="/NetCTOSS/cost/list.action" class="fee_off"></a></li>
<li><a id="account_" href="" class="account_off"></a></li>
<li><a id="service_" href="" class="service_off"></a></li>
<li><a id="bill_" href="" class="bill_off"></a></li>
<li><a id="report_" href="" class="report_off"></a></li>
<li><a id="information_" href="" class="information_off"></a></li>
<li><a id="password_" href="" class="password_off"></a></li>
</ul>
step3:将所有页面中<ul>...</ul>部分删除,<div id="navi"></div>要保留
step4:在<div id="navi"></div>中添加Struts2的include标签
<div id="navi"><s:include value="../head.jsp" /></div>
step5:主页index.jsp特殊一些
<div id="index_navi"><s:include value="../jsp/head.jsp" /></div>
u 注意事项:今后所有页面都引入该head.jsp,而连接只要修改一次即可所有页面生效。
step6:所有页面添加jQuery框架
<script language="javascript" src="../js/jquery-1.4.1.min.js"></script>
u 注意事项:所有页面记得导入Struts2标签:<%@taglib uri="/struts-tags" prefix="s"%>
step7:部署,测试,连接正常!样式修改也正常!
十、
项目经验
10.1主键用int还是Integer
主键用int、Integer都可以,但在Hibernate框架中,习惯用Integer。而非主键最好用封装类Integer,这样可以允许有空值,否则至少有个0。
10.2 “../”表示的意思
表示向上跳一级目录,看的是浏览器地址!如:localhost:8080/应用名/命名空间/xx.action,用“../”则跳到应用名,而“命名空间/xx.action”整体为一级,共同决定响应后显示的页面。
10.3导入静态页面,样式、JS失效问题
项目结构和发布后的结构不一样的,发布后结构为webapps/应用名/然后是WebRoot中那一推东西,即WEB-INF文件夹啊、images文件夹啊、js文件夹啊等。
所以,样式和js的导入路径为:
<link type="text/css" rel="stylesheet" media="all" href="../styles/global_color.css" />
<script language="javascript" src="../js/jquery-1.4.1.min.js"></script>
即从当前请求地址跳到应用名,然后找到js文件夹,然后找到jquert。
10.4 <s:hidden>和<s:textarea>标签
<s:hidden value="">中value属性不是OGNL表达式,使用value="%{ognl}"可使里面的字符串强制转成ognl表达式。
<s:textarea value="">中的value属性也不是OGNL表达式,也使用value="%{ognl}"强转。
10.5四种情形下的绝对路径写法
绝对路径:前面一定有个“/”,且从应用名开始写。以下为4中情形的绝对路径写法:
1)链接:/应用名/... 2)提交:/应用名/.. 3)重定向:/应用名/..
4)转发:/应用名后开始写
u 注意事项:例如在cost_list.jsp页面,添加add页面的连接时,可以只写add.action(相对路径),因为它们是同包的,或者写绝对路径,但不能写成cost/add.action,会出现叠加问题,可参考Spring笔记12.2节。
10.6 URL和URI
http://ip:port/NetCTOSS/......
|<-- uri -->|
|<-- url -->|
10.7 util.Date和sql.Date
当要把日期插入数据库时,要注意程序导入的是util.Date还是sql.Date。
如:new Date(System.currentTimeMillis());
它们之间的转换:
Date date=new Date();//util.Date
java.sql.Date date1=new java.sql.Date(date.getTime());