Struts是Apache Group的一個優秀的MVC的前台架構,目前也使用的非常廣泛。
但僅僅停留在使用的層次上是沒有挑戰性的,研究其源碼才是真正樂趣所在。
本文以一個很小的例子入手,將這個例子中牽涉到的Struts源碼做了一次剖析,
豁然發現就在這個不起眼的例子中,也有許多值得學習的東西,故將其整理成文,目的旨在拋磚引玉,希望有興趣者同樂。
本文的例子就來自Struts內建的一個Example(struts-example.war)。在這個例子中的第一個JSP(index.jsp),一開始就有這樣的一段代碼:
==============================================
-
Code: Select all
-
<logic:notPresent name="database" scope="application">
<font color="red">
ERROR: User database not loaded -- check servlet container logs
for error messages.
</font>
<hr>
</logic:notPresent>
==============================================
從文檔上的描述來看,這段代碼主要有如下的作用:
1、由於本sample的資料庫是一個database.xml檔案,所以在JSP的一開頭就用這段代碼來驗證database.xml檔案是否已被裝載成功
2、如果裝載不成功,那麼將列印中間那一段出錯資訊
OK,由以上的描述,很自然的會想到,這個標籤大概做的事情可能就是在/WEB-INF/目錄下尋找database.xml,成功就返回成功,錯誤就
返回失敗,如此如此,順理成章
可是Struts的原始碼是否就是按照這樣的邏輯寫的呢?答案顯然是否
下面就開始層層剖析Struts的源碼
首先當然是從這個標籤類入手,開啟org.apache.struts.taglib.logic.NotPresentTag類,發現如下代碼:
==============================================
-
Code: Select all
-
public class NotPresentTag extends PresentTag { // ------------------------------------------------------ Protected Methods
/**
* Evaluate the condition that is being tested by this particular tag,
* and return <code>true</code> if the nested body content of this tag
* should be evaluated, or <code>false</code> if it should be skipped.
* This method must be implemented by concrete subclasses.
*
* @exception JspException if a JSP exception occurs
*/
protected boolean condition() throws JspException {
return (condition(false));
}
}
==============================================
看來封裝的不錯,初步估計應該是這樣:
1、NotPresentTag繼承自PresentTag類,但Tag標籤類必須有doStartTag這樣的方法啊,那自然就是在PresentTag類裡面有了
2、有一個condition方法,看來是PresentTag類的doStartTag方法調用了condition方法,正好這裡利用物件導向的“多態”,調用到了這個方法,而不是
PresentTag中的condition方法
繼續,開啟PresentTag類,看到如下代碼:
==============================================
-
Code: Select all
-
public class PresentTag extends ConditionalTagBase {//。。。中間有省略
/**
* Evaluate the condition that is being tested by this particular tag,
* and return <code>true</code> if the nested body content of this tag
* should be evaluated, or <code>false</code> if it should be skipped.
* This method must be implemented by concrete subclasses.
*
* @exception JspException if a JSP exception occurs
*/
protected boolean condition() throws JspException {
return (condition(true));
}
/**
* Evaluate the condition that is being tested by this particular tag,
* and return <code>true</code> if the nested body content of this tag
* should be evaluated, or <code>false</code> if it should be skipped.
* This method must be implemented by concrete subclasses.
*
* @param desired Desired outcome for a true result
*
* @exception JspException if a JSP exception occurs
*/
protected boolean condition(boolean desired) throws JspException {
// Evaluate the presence of the specified value
boolean present = false;
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
if (cookie != null) {
present = this.isCookiePresent(request);
} else if (header != null) {
String value = request.getHeader(header);
present = (value != null);
} else if (name != null) {
present = this.isBeanPresent();
} else if (parameter != null) {
String value = request.getParameter(parameter);
present = (value != null);
} else if (role != null) {
StringTokenizer st = new StringTokenizer(role, ROLE_DELIMITER, false);
while (!present && st.hasMoreTokens()) {
present = request.isUserInRole(st.nextToken());
}
} else if (user != null) {
Principal principal = request.getUserPrincipal();
present = (principal != null) && user.equals(principal.getName());
} else {
JspException e = new JspException
(messages.getMessage("logic.selector"));
TagUtils.getInstance().saveException(pageContext, e);
throw e;
}
return (present == desired);
}
/**
* Returns true if the bean given in the <code>name</code> attribute is found.
* @since Struts 1.2
*/
protected boolean isBeanPresent() {
Object value = null;
try {
if (this.property != null) {
value = TagUtils.getInstance().lookup(pageContext, name, this.property, scope);
} else {
value = TagUtils.getInstance().lookup(pageContext, name, scope);
}
} catch (JspException e) {
value = null;
}
return (value != null);
}
/**
* Returns true if the cookie is present in the request.
* @since Struts 1.2
*/
protected boolean isCookiePresent(HttpServletRequest request) {
Cookie cookies[] = request.getCookies();
if (cookies == null) {
return false;
}
for (int i = 0; i < cookies.length; i++) {
if (this.cookie.equals(cookies.getName())) {
return true;
}
}
return false;
}
==============================================
通過這段代碼,又看到了如下的東西:
1、PresentTag又繼承自ConditionalTagBase,抽象的比較精彩
2、代碼中的一大堆變數如name, cookie, role等等,這些正好是logic:notPresent元素的屬性
3、這些變數沒有在PresengTag中定義,看來是定義在了ConditionalTagBase裡面了
4、留心看name這個屬性的處理,因為我們研究的那段代碼(第一段代碼),屬性中最關鍵的也就是那個name="database"了
看到她調用了isBeanPresent這個方法,而這個方法又調用了TagUtils.getInstance().lookup這個方法
OK,到此,可以看到,研究方向轉移到了TagUtils.getInstance().lookup這個方法,在開始看這個方法之前,還是把ConditionalTagBase
這個類也看一看,說不定還有意外的收穫,如下:
==============================================
public abstract class ConditionalTagBase extends TagSupport {
//。。。中間省略了那些屬性(name, role, scope, user等等元素屬性)的get/set方法
//這些get/set方法將被自動調用,所以第一段代碼中的name="database"中,database這個值將被自動賦給name變數
-
Code: Select all
-
/**
* Perform the test required for this particular tag, and either evaluate
* or skip the body of this tag.
*
* @exception JspException if a JSP exception occurs
*/
public int doStartTag() throws JspException { if (condition())
return (EVAL_BODY_INCLUDE);
else
return (SKIP_BODY);
}
/**
* Evaluate the remainder of the current page normally.
*
* @exception JspException if a JSP exception occurs
*/
public int doEndTag() throws JspException {
return (EVAL_PAGE);
}
/**
* Release all allocated resources.
*/
public void release() {
super.release();
cookie = null;
header = null;
name = null;
parameter = null;
property = null;
role = null;
scope = null;
user = null;
}
// ------------------------------------------------------ Protected Methods
/**
* Evaluate the condition that is being tested by this particular tag,
* and return <code>true</code> if the nested body content of this tag
* should be evaluated, or <code>false</code> if it should be skipped.
* This method must be implemented by concrete subclasses.
*
* @exception JspException if a JSP exception occurs
*/
protected abstract boolean condition() throws JspException;
==============================================
由以上的代碼,雖然沒有意外收穫,但是確認了不少之前的想法,如下:
1、condition這個方法是一個抽象方法,具體實現在PresentTag和NotPresentTag類中
2、doStartTag方法中調用了condition方法,導致了PresentTag和NotPresentTag類中condition方法的被調用
猜測已被確認,接下來繼續看TagUtils.getInstance().lookup這個方法,找到原始碼如下:
==============================================
-
Code: Select all
-
/**
* Locate and return the specified bean, from an optionally specified
* scope, in the specified page context. If no such bean is found,
* return <code>null</code> instead. If an exception is thrown, it will
* have already been saved via a call to <code>saveException()</code>.
*
* @param pageContext Page context to be searched
* @param name Name of the bean to be retrieved
* @param scopeName Scope to be searched (page, request, session, application)
* or <code>null</code> to use <code>findAttribute()</code> instead
* @return JavaBean in the specified page context
* @exception JspException if an invalid scope name
* is requested
*/
public Object lookup(PageContext pageContext, String name, String scopeName)
throws JspException { if (scopeName == null) {
return pageContext.findAttribute(name);
}
try {
return pageContext.getAttribute(name, instance.getScope(scopeName));
} catch (JspException e) {
saveException(pageContext, e);
throw e;
}
}
/**
* Locate and return the specified property of the specified bean, from
* an optionally specified scope, in the specified page context. If an
* exception is thrown, it will have already been saved via a call to
* <code>saveException()</code>.
*
* @param pageContext Page context to be searched
* @param name Name of the bean to be retrieved
* @param property Name of the property to be retrieved, or
* <code>null</code> to retrieve the bean itself
* @param scope Scope to be searched (page, request, session, application)
* or <code>null</code> to use <code>findAttribute()</code> instead
* @return property of specified JavaBean
*
* @exception JspException if an invalid scope name
* is requested
* @exception JspException if the specified bean is not found
* @exception JspException if accessing this property causes an
* IllegalAccessException, IllegalArgumentException,
* InvocationTargetException, or NoSuchMethodException
*/
public Object lookup(
PageContext pageContext,
String name,
String property,
String scope)
throws JspException {
// Look up the requested bean, and return if requested
Object bean = lookup(pageContext, name, scope);
if (bean == null) {
JspException e = null;
if (scope == null) {
e = new JspException(messages.getMessage("lookup.bean.any", name));
} else {
e =
new JspException(
messages.getMessage("lookup.bean", name, scope));
}
saveException(pageContext, e);
throw e;
}
if (property == null) {
return bean;
}
// Locate and return the specified property
try {
return PropertyUtils.getProperty(bean, property);
} catch (IllegalAccessException e) {
saveException(pageContext, e);
throw new JspException(
messages.getMessage("lookup.access", property, name));
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
if (t == null) {
t = e;
}
saveException(pageContext, t);
throw new JspException(
messages.getMessage("lookup.target", property, name));
} catch (NoSuchMethodException e) {
saveException(pageContext, e);
throw new JspException(
messages.getMessage("lookup.method", property, name));
}
}
==============================================
由上,可以看到:
1、TagUtils是一個Singleton的類
2、代碼Object bean = lookup(pageContext, name, scope);的用意顯然是在尋找name變數綁定的對象
3、2步驟尋找的過程是首先查看scope變數是否為空白,如果為空白,將調用pageContext.findAttribute方法
4、findAttribute方法將依次在page, request, session, application四個範圍內尋找name變數綁定的對象(來自Servlet API文檔的解釋)
5、如果scope變數不為空白(在我們的例子中,scope="Application"),那麼直接調用pageContext.getAttribute方法,在scope裡面尋找
name變數綁定的對象
6、如果這個對象找到了,並且property屬性沒有被賦值(在我們的例子中確實沒有賦值),那麼直接將對象返回
7、如果對象不為空白的返回到了PresentTag的condition(boolean desire)方法,那麼最終結果就是存在,notPresent這個標籤裡面的出錯資訊
就不會被顯示
通過以上的分析,可以看到,現在的關鍵問題已經轉移到看Application範圍內是否有一個對象綁定一個key叫做"database"(看步驟5的描述)
但是這個事情Struts的這個Example程式又是在什麼時候做的呢?
通過觀察和分析,發現在struts-config配置中,將這項工作作為了一個PlugIn進行了配置,如下:
==============================================
-
Code: Select all
-
<plug-in className="org.apache.struts.webapp.example.memory.MemoryDatabasePlugIn">
<set-property property="pathname" value="/WEB-INF/database.xml"/>
</plug-in>
==============================================
事到如今,終於離目標越來越近了,MemoryDatabasePlugIn這個類在Tomcat啟動的時候啟動,讀取了database.xml檔案,
將其中的值讀取出來並構建出了一個對象,然後將這個對象綁定到database這個key,放在了Application範圍內的一個容器中
這樣的解釋,終於可以和前面研究的結果相銜接起來了,不是嗎?:)
迫不及待的開啟MemoryDatabasePlugIn類,看到了如下代碼:
==============================================
-
Code: Select all
-
/**
* Initialize and load our initial database from persistent storage.
*
* @param servlet The ActionServlet for this web application
* @param config The ApplicationConfig for our owning module
*
* @exception ServletException if we cannot configure ourselves correctly
*/
public void init(ActionServlet servlet, ModuleConfig config)
throws ServletException { log.info("Initializing memory database plug in from '" +
pathname + "'");
// Remember our associated configuration and servlet
this.config = config;
this.servlet = servlet;
// Construct a new database and make it available
database = new MemoryUserDatabase();
try {
String path = calculatePath();
if (log.isDebugEnabled()) {
log.debug(" Loading database from '" + path + "'");
}
database.setPathname(path);
database.open();
} catch (Exception e) {
log.error("Opening memory database", e);
throw new ServletException("Cannot load database from '" +
pathname + "'", e);
}
// Make the initialized database available
servlet.getServletContext().setAttribute(Constants.DATABASE_KEY,
database);
// Setup and cache other required data
setupCache(servlet, config);
}
==============================================
果然不出所料,這段代碼做了如下的事情:
1、產生了MemoryUserDatabase這個對象,並調用其setPathname和open方法,讀取database.xml的值並填充到成員變數中
2、將這個對象綁定到了ServletContext中,Constants.DATABASE_KEY這個常量就是"database"
3、ServletContext是Servlet的一個上下文環境,也是ServletConfig的一個提供者。ServletConfig中定義了Servlet和Servlet容器
溝通時的一些資訊(如Servlet的方法定義,參數等等)。這個上下文在一個JVM、一個Web應用中一般只會存在一份,所以顯然是
Application範圍內的東西
至此,終於搞清楚了Struts中這個logic:notPresent標籤的整個運行過程,和我們的想像不完全相同的是,Struts是將database
的配置在啟動的時候就讀取出來然後綁定到一個對象;然後這個標籤中的代碼去嘗試尋找這個對象,如果找到,表明database裝載
成功,否則就出錯。
另外,有一個收穫是,標籤的書寫中,scope="Application"可以不寫,因為如果不寫scope屬性值,Struts會依次在page, request, session,
application四個範圍內進行尋找,一樣可以找到這個對象,但有些地方還是有點不妥的,下面將提到。
依次尋找這個動作是通過pageContext類的一個抽象方法findAttribute方法來完成的,由於是抽象方法,所以這個方法的具體實現應該在Servlet
容器的原始碼中,也就是說,這個方法的實現是和容器相關的。另外,依次尋找這個動作肯定沒有指定scope尋找來得快,所以效能上也會有影響
我想這就是Struts的這個example中將scope手動指定的緣故吧。
OK,讓我們刨根問底,再來看findAttribute方法的實現,這裡摘錄的是Tomcat4.1的原始碼,這個方法的實現在Tomcat的
org.apache.jasper.runtime.PageContextImpl這個類中,代碼如下:
==============================================
-
Code: Select all
-
public Object findAttribute(String name) {
Object o = attributes.get(name);
if (o != null)
return o; o = request.getAttribute(name);
if (o != null)
return o;
if (session != null) {
o = session.getAttribute(name);
if (o != null)
return o;
}
//這裡的context就是ServletContext的一個執行個體
return context.getAttribute(name);
}
==============================================
不用再解釋了,再清楚不過了,的確是一個一個的找了過來
至此,我們已經從logic:notPresent這個標籤開始,做了一次全程曆險,而且最後還到Tomcat家裡去小坐了一下
總結來說,logic:notPresent這個標籤是在指定scope中尋找name變數綁定的一個對象,可以用來定位一些資源是否被裝載
但這個標籤沒有強大到自動去找指定的檔案或其他資源的地步,需要我們在這個標籤被調用之前,將資源裝載並綁定到
page, request, session或Application中
Struts是一個優秀的開源項目,除了一整套Web運行機制外,她還帶了一個大標籤庫。這個標籤庫中有很多比較實用的標籤,
但如果理解不透,很容易發生錯用的現象。所以研讀Struts的源碼無其他目的,只是想好好搞清楚這些標籤的作用而已。