Struts2_攔截器棧&標簽庫
一、攔截器棧
1. 攔截器
Java里的攔截器是動態攔截Action調用的對象。它提供了一種機制可以使開發者可以定義在一個action執行的前后執行的代碼,也可以在一個action執行前阻止其執行,同時也提供了一種可以提取action中可重用部分的方式。在AOP(Aspect-Oriented Programming)中攔截器用于在某個方法或字段被訪問之前,進行攔截然后在之前或之后加入某些操作。
幾個關鍵字:
- 攔截Action:Struts2 攔截器在訪問某個 Action 方法之前或之后實施攔截,(在action之前調用的稱之為前置攔截器,之后也稱之為后置攔截)
- 阻止執行:由于攔截器可以在Action執行之前執行,那么如果不想讓某個action執行,可以阻斷其執行。
- 可重用部分:重復的代碼抽取出來形成攔截器,攔截器是可插拔的。
- AOP(Aspect-Oriented Programming):一種編程思想,該思想的簡單理解就是:在不改變原來代碼的情況下,對原來的代碼功能進行增強(增加或減少)。它的通常是采用代理的機制實現(代理對目標代碼進行控制和增強 )
問題: 攔截器和過濾器的區別?
過濾器(filter)是javaweb階段的知識點,攔截服務器端所有資源的訪問 (靜態、 動態)。在web.xml配置。
攔截器(Interceptor),在struts2框架內部,只對Action訪問進行攔截 (默認攔截器 ,無法攔截靜態web資源,
如果要攔截靜態資源,比如html、jsp,可以將靜態web資源放入WEB-INF\xxx, 通過Action間接訪問)
2. 攔截器棧
攔截器棧(Interceptor Stack):是將攔截器按照一定的順序連接成一條鏈后的一個稱呼。
在訪問被攔截的方法時,攔截器鏈的中攔截器就會按照其之前定義的順序被一次調用。
Struts2 將攔截器定義攔截器棧,作用于目標Action,攔截器棧的名字為defaultStack
defaultStack
是Struts2
默認執行的攔截器棧。
2.1 Struts2運行原理的底層分析(了解)
1. 當web.xml被加載后,經過Struts2的前端控制器,會進入StrutsPrepareAndExecuteFilter,它會調用init
方法進行初始化,準備Struts2的相關環境,加載相應的配置文件,(6個,包括struts.xml)--它會將所有action的name
都加載到Struts2的環境中。
2. 當有請求訪問時,前端控制器攔截訪問doFilter方法,ActionMapping mapping = prepare.findActionMapping(request, response, true);
會在查找要訪問的action的name是否有配置,如果沒有配置,直接過濾攔截,忽略后面所有的攔截器與action的執行,
并告知action映射不存在,不往下運行。
如果存在,execute.executeAction(request, response, mapping);準備執行action
3. 準備執行action :
ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
namespace, name, method, extraContext, true, false);
生成action的代理對象---增強---使用過濾器進行增強
繼續向下走 : proxy.execute();
invocation.invoke();
ActionInvocation增強器里面的invoke方法,判斷if (interceptors.hasNext())-配置的那些攔截器有沒有
執行完,如果沒有執行完,就執行:
resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this)
這個具體的攔截器,執行之后 return invocation.invoke();,返回到原來的調用對象,原來的
調用對象又會自動調用invoke方法。--(鏈式遞歸)遞歸調用。
4. 當攔截器都執行完成之后(增強完成之后),resultCode = invokeActionOnly();讓它去執行具體的Action:
invokeAction(getAction(), proxy.getConfig());返回結果集視圖。
3. 自定義攔截器
![Upload img22.png failed. Please try again.]
![Upload img23.png failed. Please try again.]
- 程序中每個攔截器 都必須實現 Interceptor 接口
- 也可以繼承 AbstractInterceptor 只需要覆蓋 intercept 方法
- 也可以繼承 MethodFilterInterceptor ,只需要覆蓋 doIntercept 方法
可以設置哪些方法 不進行過濾攔截(功能最強,推薦)
3.1 實現自定義攔截器
編寫測試的Action(被攔截的Action):
@Override
//通過攔截器增強這個Action
public String execute() throws Exception {
System.out.println("TestAction執行了................");
return NONE;
}
編寫一個自定義攔截器:
編寫一個類,繼承MethodFilterInterceptor,實現doFilter方法。
在doFilter方法內部編寫要對其增強的代碼。
public class MyInterceptor extends MethodFilterInterceptor {
@Override
//目標 :使用攔截器對Action進行增強
protected String doIntercept(ActionInvocation invocation) throws Exception {
System.out.println("攔截器執行了,增強Action方法.................");
//增強后,放行將執行權交給下個攔截器或Action
return invocation.invoke();
}
}
注冊攔截器
定義全局攔截器:
寫在package內,action前
<interceptors>
<!--注冊自定義攔截器 -->
<interceptor name="myInterceptor" class="com.itdream.struts2.interceptor.MyInterceptor" />
<!-- 注冊自定義攔截器棧 -->
<interceptor-stack name="myStack">
<!-- 先執行自己的攔截器(順序看需求) -->
<interceptor-ref name="myInterceptor" />
<!-- 執行默認的攔截器棧 -->
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<!-- 自定義攔截器棧覆蓋Struts2的默認攔截棧,使其生效 -->
<default-interceptor-ref name="myStack" />
-------------------------------------------------------------------------
繼承MethodFilterInterceptor有一個強大的功能,可以設置哪些Action不進行攔截。
如何使用 :
<interceptor-ref name="myInterceptor">
<!-- 排除哪些方法,不攔截它們,多個方法間用逗號隔開 -->
<param name="excludeMethods">execute</param>
<!-- 包含哪些方法,只有這些方法才被攔截,多個方法間用逗號隔開.與上面的排除配置互斥 -->
<!-- <param name="includeMethods">execute</param> -->
</interceptor-ref>
定義局部攔截器 :
首先也要在package內,action注冊自攔截器:
<interceptors>
<!--注冊自定義攔截器 -->
<interceptor name="myInterceptor" class="com.itdream.struts2.interceptor.MyInterceptor" />
</interceptors>
然后在指定action內局部使用該攔截器:
<!--
局部攔截器:寫在指定action內部,只對這個action有效
局部攔截器會覆蓋全局配置
-->
<interceptor-ref name="myInterceptor">
<interceptor-ref name="defaultStack"/>
結論:我們在自定義攔截器后,需要將其進行注冊,并且使用它.使用時都不會拋棄Struts2的攔截器棧。
實際上,我們自定義攔截器后基本上都是注冊全局攔截器讓其對所有的Action生效。
二、 自定義攔截器,攔截未登陸用戶訪問
攔截器只能攔截訪問Action請求,不能攔截靜態Web資源(jsp,html等),如果要想攔截它們,使用Action間接訪問這些資源即可。
目標:用戶在未登錄的情況下,不允許訪問系統的功能,讓其跳轉到用戶登錄頁面。
前提:所有的頁面請求都經過Action,自定義攔截器進行Action請求攔截,在執行Action之前進行判斷操作。
1. 修改新增客戶,讓其通過Action訪問add.jsp
menu.jsp :
href="${pageContext.request.contextPath }/customer_showAdd.action"
CustomerAction動作類:
//跳轉添加客戶頁面
public String showAdd() {
return "addjsp";
}
struts.xml配置結果集視圖跳轉頁面:
<!-- 跳轉添加客戶頁面 -->
<result name="addjsp" type="redirect">/jsp/customer/add.jsp</result>
2. 完成用戶登錄功能
- 創建用戶的數據庫表user
- 默認給定用戶名和密碼
- 創建用戶持久化類User
- 完成用戶持久化類與數據庫表user的映射文件mapping
- 用戶在頁面輸入用戶名密碼,數據庫校驗是否存在
- 登陸成功跳轉首頁,登陸失敗,跳轉回登陸頁面,告知提示信息
2.1 ORM關系創建(省略)
2.2 修改登錄頁面login.jsp
<FORM id=form1 name=form1 method="post" action="${pageContext.request.contextPath }/user_login.action">
表單中name屬性修改與模型類的屬性一致,用于Struts2框架的攔截器封裝數據。(省略)
struts.xml配置訪問的Action:
<action name="user_*" class="com.itdream.crm.web.action.UserAction" method="{1}"></action>
2.3 Action處理請求,查詢數據庫
Action動作類處理請求:
public class UserAction extends ActionSupport implements ModelDriven<User> {
// 創建一個模型對象用于封裝參數
private User user = new User();
@Override
// 提供getter方法Struts2框架獲取model對象
public User getModel() {
return user;
}
// 用戶登錄
public String login() {
// 獲取參數(模型驅動)
// 調用業務層查詢是否有符合賬號密碼的User存在
UserService service = new UserServiceImpl();
User loginUser = service.findUserByUsernameAndPassword(user.getUsername(), user.getPassword());
if (loginUser != null) { // 登陸成功
// 將User對象存入Session域中
ServletActionContext.getRequest().getSession().setAttribute("user", loginUser);
// 跳轉首頁
return "loginSuccess";
}
// 登陸失敗
addActionError("用戶名或密碼不正確,請重新輸入");
return LOGIN;
}
}
---------------------------------------------------------------------------------
Service層:
public class UserServiceImpl implements UserService {
@Override
//根據賬號密碼查詢用戶
public User findUserByUsernameAndPassword(String username, String password) {
//獲取Session對象
Session session = HibernateUtils.getCurrentSession();
//開啟事務
Transaction transaction = session.beginTransaction();
User user = null;
try {
//業務邏輯
//調用dao層
UserDAO dao = new UserDAOImpl();
user = dao.findUserByUsernameAndPassword(username,password);
} catch (Exception e) {
//回滾事務
transaction.rollback();
e.printStackTrace();
}finally {
//提交事務
transaction.commit();
}
return user;
}
}
--------------------------------------------------------------------------
dao層:
public class UserDAOImpl implements UserDAO {
@Override
//根據用戶名和密碼查詢User
public User findUserByUsernameAndPassword(String username, String password) {
//獲取Session對象
Session session = HibernateUtils.getCurrentSession();
//使用Criteria查詢(面向對象的無語句查詢)
Criteria criteria = session.createCriteria(User.class);
System.out.println("username:"+username);
System.out.println("password:"+password);
//添加限制條件
criteria.add(Restrictions.eq("username", username));
criteria.add(Restrictions.eq("password", password));
//執行查詢
User user = (User) criteria.uniqueResult();
return user;
}
}
2.4 自定義登陸攔截器
用戶未登錄的情況下訪問服務器的資源時,跳轉到登陸頁面,并提示用戶登錄。
1. 繼承MethodFilterInterceptor完成自定義攔截器。
2. 將需要寫回的信息放入ActionError集合中在jsp頁面回顯。
3. 最后需要放行,invocation.invoke()。
自定義攔截器,攔截Action請求:
public class LoginInterceptor extends MethodFilterInterceptor {
@Override
//攔截未登錄用戶
protected String doIntercept(ActionInvocation invocation) throws Exception {
//獲取Session域中的user是否存在來判斷是否登陸
User user = (User) ServletActionContext.getRequest().getSession().getAttribute("user");
if (user == null) { //如果未登陸,執行攔截,跳轉到登錄頁面
//使用Struts2的大管家獲取攔截的Action,寫回友好信息
ActionSupport action = (ActionSupport) invocation.getAction();
action.addActionError("對不起,您還沒有登陸");
//跳轉到登陸頁面
return action.LOGIN;
}
//否則就放行,將執行權交到下一個攔截器或者Action
return invocation.invoke();
}
}
頁面回顯提示:
先引入Struts2的標簽庫。
<%@ taglib prefix="s" uri="/struts-tags" %>
友好提示:
<s:actionerror/>
2.5 struts2.xml
最終版本
1. Action的結果集視圖跳轉對應頁面。
1. 這里配置了一個全局結果集"login",未登陸用戶訪問服務器資源/登陸失敗,讓其全跳回登陸頁
2. 注冊自定義攔截器,并使用自定義攔截器棧覆蓋默認攔截器棧
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<!-- 簡單樣式 -->
<constant name="struts.ui.theme" value="simple" />
<package name="default" namespace="/" extends="struts-default">
<!-- 自定義攔截器 -->
<interceptors>
<!-- 注冊自定義攔截器 -->
<interceptor name="loginInterceptor" class="com.itdream.crm.web.interceptor.LoginInterceptor"/>
<!-- 注冊自定義攔截器棧 -->
<interceptor-stack name="myStack">
<!-- 先執行登陸攔截,驗證是否登陸需要攔截 -->
<interceptor-ref name="loginInterceptor">
<!-- 如果是執行登錄操作的方法,就不攔截.標簽體內填寫要排除的方法名 -->
<param name="excludeMethods">login</param>
</interceptor-ref>
<!-- 再執行Struts2默認攔截棧 -->
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<!-- 使用自定義攔截器棧(自定義攔截器棧覆蓋默認棧) -->
<default-interceptor-ref name="myStack"/>
<!-- 全局結果集,未登錄用戶訪問任何頁面都跳轉login.jsp -->
<global-results>
<result name="login">/login.jsp</result>
</global-results>
<action name="customer_*" class="com.itdream.crm.web.action.CustomerAction"
method="{1}">
<!-- 跳轉添加客戶頁面 -->
<result name="addjsp" type="redirect">/jsp/customer/add.jsp</result>
<result name="flushListCustomer" type="redirectAction">customer_list.action
</result>
<result name="listCustomer">/jsp/customer/list.jsp</result>
<!-- 默認轉發,傳遞customer數據 -->
<result name="editjsp">/jsp/customer/edit.jsp</result>
</action>
<!-- 與User有關的action請求 -->
<action name="user_*" class="com.itdream.crm.web.action.UserAction"
method="{1}">
<!-- 登陸成功 -->
<result name="loginSuccess">/index.jsp</result>
</action>
</package>
</struts>
二、標簽庫
測試Struts2的標簽庫:
Struts.xml配置:
<!-- 標簽庫測試 -->
<action name="tag_*" class="com.itdream.struts2.taglib.TagAction" method="{1}">
<result name="result">/result.jsp</result>
</action>
1. 通用(Generic)標簽
1.1. <s:property/>
標簽
作用:將OGNL表達式的內容輸出到頁面
value
:屬性。接收OGNL表達式
default
:屬性, 如果OGNL表達式,取不到值,default設置顯示默認值。默認為""
escapeHtml
:屬性, 是否對HTML標簽轉義輸出 (默認是轉義,可以關閉)
Action類:
//測試property標簽
public String property() {
//模擬業務層返回的數據壓入值棧,轉發jsp頁面接收
// 獲取值棧
ValueStack valueStack = ActionContext.getContext().getValueStack();
//壓入root棧,匿名
valueStack.push("push壓入root棧");
//壓入root棧,有名字
valueStack.set("msg", "set壓入root棧");
//壓入map棧,匿名
ActionContext.getContext().put("msg", "put壓入map棧");
//跳轉頁面(值棧和request的生命周期一樣,采用轉發方式)
return "result";
}
result.jsp :
<%@ taglib prefix="s" uri="/struts-tags" %>
<h3>------------測試Property標簽-------------------</h3>
<!-- 從值棧中取值 -->
<!-- 取壓入root棧的匿名對象 -->
<s:property value="[1].top"/><br/>
<!-- 取壓入root棧的有名字對象
1. 值棧的默認查找機制
2. 索引獲取值棧的對象,再通過key取值
3. 神奇的request(el表達式,Struts2增強了request.getAttribute方法)。
先到request域中查找,再使用值棧的默認查找機制到值棧中查找
-->
<s:property value="msg"/>|<s:property value="[0].top.msg"/>|${msg}<br/>
<!-- 取存入map棧的值
1.可以使用值棧的默認查找機制取map棧的值,但如果root棧中有重名的key就取不到map棧的值
因為值棧的默認查找機制默認先找root棧,root棧沒有才查找map棧.
這里因為root棧已經有msg了,就不能使用這種方法了
2. 神奇的request,先找request,再走值棧默認查找
3.使用#+key指定查找map棧的key,取出map棧的值
-->
<s:property value="#msg"/><br/>
<hr/>
-----------------------------------------------------------------------------
1.2. <s:iterator/>
標簽
作用:遍歷集合對象(可以是List、set和數組等),顯示集合對象的數據。
遍歷的過程:
每次遍歷時,將遍歷的值壓入棧頂(匿名),并且在map棧中放入一個副本,key是var的變量名,map的value就是要遍歷的值。
每次遍歷完一個值,就將它從棧頂彈出,并刪除map棧key為var變量名的鍵值對。
由于這種原理,因此取遍歷的值有幾種方式:
1. 直接使用棧頂取值.[0].top
2. 使用值棧默認查找機制取map棧的值,key為var的變量名
3. 指定取map棧的值,#key
4. el表達式使用神奇的request(先找request,request沒有就走值棧默認查找機制)
value
:迭代的集合。支持OGNL表達式,如果沒有設置該屬性,則默認使用值棧棧頂的集合來迭代。(類似于jstl中c:foreach
標簽的items
屬性)
var
:引用變量的名稱,該變量是集合迭代時的子元素。
begin
: 開始的數字
end
: 結束的數字
status
:引用迭代時的狀態對象IteraterStatus實例(類似于varstatus
),其有如下幾個方法:
1. int getCount(),返回當前迭代了幾個元素;
2. int getIndex(),返回當前迭代元素的索引;
3. Boolean isEvent(),偶數
4. boolean isOdd(),奇數
5. boolean isFirst(),第一個
6. boolean isLast(),最后一個
Action 類:
// 測試iterator標簽遍歷集合
List<User> users = new ArrayList<>();
// username和password
User user1 = new User("tom", "123");
User user2 = new User("jerry", "123");
User user3 = new User("tony", "123");
User user4 = new User("lucy", "123");
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 將集合壓入值棧
valueStack.push(users);
valueStack.set("users", users);
ActionContext.getContext().put("users", users);
// 跳轉頁面(值棧和request的生命周期一樣,采用轉發方式)
return "result";
jsp頁面遍歷集合:
<!-- 遍歷action轉發的集合,根據壓入棧選擇,這里我壓入了三種,匿名的被有名字的壓下去了,所以可以使用:
索引獲取集合:[1].top,取root棧:users,取map棧:#users
-->
<s:iterator value="#users" var="user" status="status">
<!-- 1.因為每次遍歷的對象都在棧頂,可以直接獲取棧頂對象的屬性.(推薦)
2. 神奇的request.通過el表達式默認直接獲取到站定對象的屬性值
3. var的變量名是存入map棧的副本的key. 通過默認查找機制找,通過#直接取。通過request的el表達式取
-->
1<s:property value="[0].top.username"/>|2<s:property value="username"/>|3${username }|4<s:property value="#user.username"/>|5${user.username }<br/>
</s:iterator>
遇到的問題:
- 可以直接獲取棧頂對象的屬性
-
<s:property/>標簽
會將value
屬性內的字符串當成一個整體去執行值棧的默認查找機制。即不能使用<s:property value="user.username" />
企圖通過默認查找機制獲取map棧中的user對象,再通過getUsername
獲取屬性值。(例外:[index].top.屬性名
不會當成一個整體字符串,即:<s:property value="[0].top.username" />
它會先找到到Root棧的對象,再獲取他的value值)
擴展了解:
1. 遍歷集合時配合begin與end可以控制遍歷一定數量的元素
2.<s:property/>如果沒有value值,默認獲取棧頂的對象
#####1.3. ```<s:if> <s:elseif> <s:else>```標簽
**支持OGNL表達式**
//模擬代表用戶狀態的標識存入值棧
valueStack.set("userRole", 1);
// 跳轉頁面(值棧和request的生命周期一樣,采用轉發方式)
return "result";
<s:if test="userRole==0">管理員</s:if><br/>
<s:elseif test="userRole==1">普通用戶</s:elseif><br/>
<s:else>游客</s:else>
#####1.4. ```<s:a>```標簽
作用:生成a標簽鏈接
<!-- Html超鏈接 -->
<a href="${pageContext.request.contextPath }/product_find.action?name=水果">我是超鏈接</a>
<br/>
<!-- action是action的名字 -->
<s:a action="product_find" namespace="/">
<!-- name:是參數名,value:參數值
原因:value:是個ognl表達式
-->
<s:param name="name" value="'蘋果'"/>
我是新的超鏈接
</s:a>

#####1.5 其他一些用到的標簽
<s:fielderror/>
<s:actionerror/>
<s:actionmessage/>
<s:i18n>
<s:param>
####2. 用戶界面(UI)標簽
用戶界面標簽主要是包括表單類標簽和其他類標簽
#####2.1 ```<s:form>```標簽
作用:生成form標簽。
屬性:
* action屬性,對應 struts.xml <action>元素name屬性;
* namespace屬性,對象 struts.xml <package>元素 namespace屬性
Html表單:
<form action="${pageContext.request.contextPath }/form.action" method="post"></form>
Struts2表單:
<s:form action="form" namespace="/" method="post"></form>
#####2.2 ```<s:textfield>, <s:password>, <s:hidden>, <s:textarea>```標簽
<s:textfield> 文本域 ,相當于 <input type=”text” >
<s:password> 密碼域 ,相當于<input type=”password” >
showpassword:表單回顯時是否顯示密碼
<s:hidden> 隱藏域 , 相當于 <input type=”hidden” >
<s:textarea> 文本框 , 相當于 <textarea></textarea>
例:
Html:
<input type="hidden" name="name" value="jack"/>
用戶名:<input type="text" name="username"/>
密碼:<input type="password" name="password">
備注:<textarea rows="3" cols="20" name="memo"></textarea>
Struts2:
<s:hidden name="name" value="jack"/><br/>
用戶名:<s:textfield name="username"/>
密碼:<s:password name="password"/>
備注:<s:textarea name="memo" rows="3" cols="20"/>
#####2.3 ```<s:radio>、<s:checkboxlist>、<s:select>```標簽
* ```<s:radio>``` 接收list或者map 生成一組單選按鈕
* ```<s:select>``` 接收list或者map ,生成一組下拉列表
* ```<s:checkboxlist>``` 接收list或者map ,生成一組復選框
單選項 radio:
Html:
性別:<input type="radio" name="sex" value="male"/>男
<input type="radio" name="sex" value="female"/>女<br/>
Struts2:
性別:<s:radio name="sex" list="{'男','女'}" value="{'male','female'}"/>
//顯示的內容與value的值相同時可以省略value(不推薦)
-----------------------------------------------------------------------------
下拉框 select:
Html:
城市:<select name="city">
<option value="">--請選擇城市--</option>
<option value="bj">北京</option>
<option value="sz">深圳</option>
</select><br/>
Struts2:
<!-- 構建map集合 -->
使用#{key:value構建map集合,key為html中value的值,這里的value就是顯示的內容。以逗號隔開
城市:<s:select name="city" list="#{'bj':'北京','sz':'深圳'}" headerKey="" headerValue="--請選擇城市--"/><br/>
----------------------------------------------------------------------------
復選框 checkbox:
Html:
愛好:<input type="checkbox" name="hobby" value="football"/>足球
<input type="checkbox" name="hobby" value="basketball"/>籃球
<input type="checkbox" name="hobby" value="pinpong"/>乒乓球<br/>
Struts2:
<!-- 構建map集合 -->
使用#{key:value構建map集合,key為html中value的值,這里的value就是顯示的內容。以逗號隔開
愛好:<s:checkboxlist name="hobby" list="#{'football':'足球','basketball':'籃球','pingpong':'乒乓球' }"/>
#####2.4 ```<s:file>、<s:submit>、<s:reset>```標簽
* ```<s:file>對應html中input標簽的file```
* ```<s:submit>、<s:reset>```分別對應html中的提交和重置
文件上傳 file:
Html:
頭像:<input type="file" name="icon"/>
Struts2:
頭像:<s:file name="icon"/>
-------------------------------------------------------------------------
提交表單 submit:
Html:
<input type="submit" value="提交"/>
Struts2:
<s:submit value="提交"/>
-------------------------------------------------------------------------
重置表單 reset:
Html:
<input type="reset" value="重置"/>
Struts2:
<s:reset value="重置"/>
-------------------------------------------------------------------------
#####2.5 主題樣式
經過上面表單標簽的編寫,我們會發現用```struts2標簽庫```編寫完的表單頁面不好看。這是因為Struts2提供了不同的主題。
Struts2 模板文件,支持兩種Freemarker生成 (.ftl模板文件) , Velocity生成 (.vm 模板文件)
struts2默認采用 Freemarker 。
提供四種主題 :
Simple 沒有任何修飾效果,最簡單主題
Xhtml 通過 布局表格 自動排版 (默認主題 )
css_xhtml 通過CSS進行排版布局
ajax 以Xhtml模板為基礎,增加ajax功能
問題: 如何修改主題

開發中,在struts.xml 配置常量,修改默認主題樣式,對所有form生效
<!-- 簡單主題 -->
<constant name="struts.ui.theme" value="simple"/>
將主題修改為簡單樣式后,效果如下圖:

#####2.6 小結
Struts2的標簽有兩種:通用標簽、表單界面標簽。
注意:這兩種標簽屬性支持ognl表達式的屬性名字是不一樣。
**表單標簽:name是支持ognl表達式,value不支持,直接顯示值。**
**但,除此之外,其他的所有通用標簽,value是支持ognl表達式的。**