星期五, 12月 11, 2009

Struts2 + Tiles2 架構整合

我們知道 Struts2 提供了 plug-in 可以與 Tiles2 整合, 但此種整合方式是在 struts2 上面暴露 tiles2 的使用方法~如:
<package name="default" extends="tiles-default">  
  <action name="IndexAction" class="tutoial.IndexAction">  
    <result name="success" type="tiles">base.definition</result>   
  </action>  
</package>
由此可看到開發時, 需搭配 tiles 樣版對映, 真正要處理的頁面還需參考到 tiles2 的設定檔.

而此篇文章的目的則是想把 layout 方面的控制定位為架構處理. 讓一般的 struts2 程式感覺不到 tiles2 的存在.
因為在一般的應用系統中, layout 通常不多, 而交易程式佔了絕大部份~

不過在進行之前, 還是要先參考如何將官方版的 struts2 + tiles2 環境建立起來~因為我們仍需要 tiles2 這些 jar 檔與設定, 而是改良其在 struts.xml 中的使用方式, 在此就不說明了~
可以參考官方網站所提供的教學連結:
http://www.vaannila.com/struts-2/struts-2-example/struts-2-tiles-example-1.html

以下我們先來看看想要達成的目的:
<package name="main" extends="struts-default">
  <result-types>
    <result-type name="layout1" class="tutoial.MyTilesTemplateResult" />
  </result-types>
  <action name="index" class="tutoial.IndexAction">
    <result name="success" type="layout1">/home.jsp</result>
  </action>
</package>

在這裡我們可以看到, IndexAction 處理完之後轉到 success, 將會透過 layout1 的 result-type, 也就是 tutoial.MyTilesTemplateResult 進行處理, 而此 result-type 將會把 /home.jsp 自動引入到 tiles 宣告的 definition 裡.

如此, 寫 IndexAction 時, 就不必考慮 layout 的處理, 只需要處理好與本身程式相關的結果頁面即可. 不會感覺到 tiles 的運作.

所以我們這一段的重點將是此 ResultType 的實作.而後續會再討論到如何透過 struts2 提供的 package 彈性讓寫法更加簡單.
先看看 tiles 的定義檔

<tiles-definitions>
    <definition name="default" template="/layout.jsp" />
</tiles-definitions>

簡單起見, 我們只宣告了一個 default 的定義檔, 而 template 檔為 layout.jsp
接著看 layout.jsp
<%@page contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<html>
<body>
<table>
  <tr><td> This is TOP area.</td></tr>
  <tr>
    <td><tiles:insertAttribute name="body"/></td>
  </tr>
  <tr><td> This is FOOTER area.</td></tr>
</table>
</body>
</html>

可以看到此layout只包含了TOP與FOOTER,而將頁面放在中間~(屬性為body).
最後來看看ResultType的實作

package tutoial;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.StrutsResultSupport;
import org.apache.tiles.Attribute;
import org.apache.tiles.AttributeContext;
import org.apache.tiles.TilesContainer;
import org.apache.tiles.access.TilesAccess;

public class MyTilesTemplateResult extends StrutsResultSupport {
  
  public MyTilesTemplateResult() {
    super();
  }
  
  public MyTilesTemplateResult(String location) {
    super(location);
  }
  
  @Override
  public void doExecute(String location, ActionInvocation ai) throws Exception {
    setLocation(location);

    String definitionName = "default";
    String attributeName = "body";
    ServletContext servletContext = ServletActionContext.getServletContext();
    HttpServletRequest request = ServletActionContext.getRequest();
    HttpServletResponse response = ServletActionContext.getResponse();
        
    TilesContainer container = TilesAccess.getContainer(servletContext);

    Attribute attribute = new Attribute(location);
    AttributeContext attributeContext = container.startContext(request, response);
    attributeContext.putAttribute(attributeName, attribute);
    container.render(definitionName, request, response);
    container.endContext(request, response);    
  }
}

由以上程式可以得知此 ResultType 將會把定義為 "default" 之定義檔, 將其 attribute 為 "body" 帶入struts.xml上定義的jsp後, 讓 TilesContaier 進行處理.也就達到了我們的目的.


上述程式僅是說明關鍵流程, 讓讀者先了解背後原理.
而我們可以利用 struts2 的 package 機制讓使用上更好用.
首先我們先宣告一個 package, 將該 result-type 設為預設值.如下:

<package name="layout1-default" extends="struts-default">
  <result-types>
    <result-type name="layout" default="true" class="tutoial.MyTilesTemplateResult" />
  </result-types>
</package>
<package name="default" extends="layout1-default">
  <action name="index" class="tutoial.IndexAction">
    <result name="success">/home.jsp</result>
  </action>
</package>

由此可以看到在 default package 裡面的寫法就像是一般的 struts2 的寫法, 寫程式時更感覺不到 tiles 的運作.是不是更清楚方便了.

當然, 許多的 webapp 有兩種以上的 layout, 此時, 我們可以擴充 MyTilesTemplateResult, 可以帶入參數(definitionName與attributeName).
而 package 可以依多個 layout 來宣告不同的 ResultType 之參數, 再讓每個交易之 package 繼承相對應的 package.
最後達到的結果, 就是讓交易程式不用考慮到 layout 的處理, 切開 layout 與交易處理頁面之關係.

jboss中如何使用JAX-WS建構Web Service

由於 JBoss4.2.0GA 已經內建 jbossws-1.2.1.GA, 且 jbossws1.2.1GA 有支援 JAX-WS
現在來說明如何撰寫一個 JAX-WS web service並deploy到jboss4.2.0GA.
首先, 先撰寫 Service class First:
  package ws;
  import javax.jws.WebMethod;
  import javax.jws.WebService;

  @WebService
  public class FirstWS {
    private String message = new String("Hello, ");
    public void Hello() {}
    @WebMethod
    public String sayHello(String name) {
      return message + name + ".";
    }
  }

在 web.xml 中宣告成 servlet:
  <servlet>
    <servlet-name>WebService</servlet-name>
    <servlet-class>ws.FirstWS</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>WebService</servlet-name>
    <url-pattern>/ws</url-pattern>
  </servlet-mapping>


打包成 test.war 並丟到 deploy 目錄中並啟動 jboss.
在瀏覽器中輸入:

http://localhost:8080/test/ws?wsdl

即可看到此 web service 之 wsdl 宣告.
此篇文章重點在於如何撰寫 JAX-WS 並 deploy 到 jboss.
所以用到的 @WebService 與 @WebMethod 均未指定參數. 所以產生的 wsdl 都會是預設名稱, 這並不符合使用慣例, 至少應該宣告 namespace 資訊.
這方面的資訊不難找到, 就不多說了.

對於 Jboss 採用將其宣告為 Servlet 的方式, 雖然非常便利, 但並非標準, 會失去移植性.
且有些 IDE 會 check web.xml 裡面的 class 是否正確, 也可能會誤判.
不過相對於 RI 的部署方式, 此作法真的讓寫一個 web service 簡單太多了..
若是將 web.war 以目錄存在的方式來部署, 可以看到 jboss 會將該 Servlet 宣告轉換成另一種模式, 如下:

<servlet>
  <servlet-name>WebService</servlet-name>
  <servlet-class>org.jboss.ws.integration.jboss42.JBossServiceEndpointServlet</servlet-class>
  <init-param>
    <param-name>ServiceEndpointImpl</param-name>
    <param-value>ws.FirstWS</param-value>
  </init-param>
</servlet>


但是採用目錄的方式 deploy, 只會在第一次啟動 jboss 時可以正確運作.
第二次啟動 jboss 後, 就會失敗.
我想原因在於啟動時, jboss parse web.xml, 發現該 servlet 是一個 JAX-WS class 之後, 除了會更改掉 web.xml 之外, 還會額外產生 jboss 所需要的 classes.
但是第二次啟動, 由於讀取的是新的 web.xml, 就不會產生這些 classes, 於是就發生錯誤.
但包裝成 test.war 檔案的方式, 由於 web.xml 不會被更改掉, 於是每次啟動都不會有問題.
或許有其他方式可以避免掉這個問題(例如使用某種方式預先產生classes), 等研究出來再來補充

下次再來說如何使用 JAX-WS RI 來實作 web service...

JBoss4.2.0 binding address

最近將 JBoss 更新到 4.0.2GA 版, 卻發現到, JBoss的 binding address 變成是 127.0.0.1
也就是說, 除了本機可以連線之外, 外部機器無法連線到本機之 JBoss.

由於觀察 JBoss 的啟動訊息會dump出所有的 System property, 其中有 jboss.bind.address 的設定. 所以一開始的想法是透過設定 System property 的方式來改變為0.0.0.0, 卻無效.

最後追蹤Jboss的source code, 他會先設定 jboss.bind.address 為 127.0.0.1 (也是唯一有先預設的屬性), 最後再依使用者設定的值來改掉.

那要如何設定 jboss.bind.address呢? 跟設定給 JVM 的方式一樣, 只是並不是傳給 JVM, 而是傳給 JBoss 的啟動程式, 如下:

run -c default -Djboss.bind.address=0.0.0.0

經過追蹤 source code, 才發現原來 run -h 可以 show 出 help message.
用了 jboss 這麼久都不知道...(汗)...
算是額外收穫吧..^^

Tomcat 之 HttpServletRequest之getRequestURL()

由 Tomcat5.0 換到 Tomcat 5.5...
發現到有一些程式不能跑了
最後發現 Tomcat 在處理 HttpServletRequest.getRequestURL() 的做法跟以前不一樣了..

之前 Tomcat 會回傳最剛開始跟 Server 要的網址, 不管後面的 Servlet 如何 forward...

其實若是參考 API 的說明:

Reconstructs the URL the client used to make the request. The returned URL contains a protocol, server name, port number, and server path, but it does not include query string parameters.


看起來並無問題...

不過現在 Tomcat 5.5 並不是這樣處理了...
若 client 要求 /a.do 而, a.do 會 forward 到 b.jsp.
則 b.jsp 若呼叫 HttpServletRequest.getRequestURL(), 將會得到 http://.../b.jsp

而非預期之 http://.../a.do

最後查了 Tomcat 的 Bugzilla 中, 如下: http://issues.apache.org/bugzilla/show_bug.cgi?id=28222

結論是, 現在 Tomcat 5.5 的實作才是正確的~~

找了好久才找到..最後發現..Tomcat 5.5 的 change list 中就有說明到這一點了...
以後應該要多加注意 change list的...