2010年7月26日 星期一

JSP & Servlet JSP基礎概念

在JavaEE裡最常來作業面呈現的還是JSP了
Servlet雖然也可以靠著 writer 物件來作出頁面呈現
但程式碼的部分不比JSP美觀及直觀
接下來就作些JSP的簡單介紹

JSP檔案的部署位置在Web Application的根目錄底下即可
在讀取過程中,JSP會先被container轉譯成Servlet檔(.java),接著再被編譯成類別檔(.class)
所以在執行時還是與Servlet一樣

首先來個簡單的JSP範例:
test.jsp==============

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8" import="tw.vencs.storage"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>For Test</title>
</head>
<body>
<%!
  storage sto = new storage();
  int temp = 5;
%>
<%
  out.println("We get " + sto.wordGetter());  //out是JSP的隱含物件
%>
<hr />
<p>Now we will show a number: <%=temp %> </p>
</body>
</html>


storage.java============

package tw.vencs;

public class storage {
  private String[] innerWord = {"one", "two", "three"};
 
  public String wordGetter(){
  int temp = (int) (Math.random()*3);
  return innerWord[temp];
  }
}

我們在範例中的頁面裡放入了Scriptlet來作出動態效果
Scriptlet指的就是簡單的Java程式碼
正如範例所示,Java的程式碼必須放在 <% %> 裡
也因為是Java程式碼,所以結尾要加上 ; 

<%@ page....  這行程式碼代表指令
<%後有個@符號,讓它跟放入一般Java程式碼的Scriptlet不一樣
指令作用就像是header一樣,能在轉譯期間將特殊指示傳給container
指令共有三種: page、include、taglib
雖然只有三種,但卻可以搭配屬性而作到很多的效果
page 指令用來定義頁面的特性,總共有13種屬性
include 指令定義在轉譯期要被增加到當前頁面的文字或程式碼,用來建立可重複使用的區塊
taglib 定義供JSP使用的標籤函式庫,這個東西會在以後的筆記提到

page的屬性有:
import -
匯入套件用的指令

isTreadSafe -
定義轉譯出的Servlet是否實作STM介面。STM是失敗的東西,這裡用預設的 true 就好

contentType -
為JSP回應定義MIME Type

isELIgnored -
定義在JSP轉譯時,是否要忽略EL表達式。後續筆記才會開始討論EL

isErrorPage -
定義此頁面是否為另一個JSP頁面的錯誤頁面,預設值是false
若設定成 true ,此頁面就能存取exception 隱含物件(參照到被引發的例外或錯誤)

errorPage -
定義一個當錯誤或例外發生時會被送往的URL;與 isErrorPage 設定的網址搭配

language -
定義目前使用的語言,目前只接受唯一的預設值 java
這個屬性的設計是為了增加JSP的擴充性

extends -
定義在JSP轉Servlet時要繼承的父類別,一般不需要也不會用到這個選項

session -
定義頁面是否有 session 隱含物件,預設值 true

buffer -
定義 out 隱含物件如何使用buffering

autoFlush -
定義緩衝區的資料是否自動被送出,預設值 true

info -
轉譯後的Servlet可以用 getServletInfo method 來取得此項資訊

pageEncoding
為JSP定義字元編碼,預設值是 "ISO-8859-1"


運算式是另外一種表現方式,程式碼中的 <%=temp %> 就是這樣的例子
<% 後面接 = 就是運算式的寫法
運算式裡不用加 ; 作結尾,它提供了我們作簡短的輸出
它被container轉譯後,會被當成一般Scriptlet裡的 out.print() 的參數輸出
也因為是 out.print() 的參數,若運算式裡的程式碼無法輸出結果的話,會回傳錯誤
boolean值會被輸出成為顯示 true 或 false 的字串
物件類別需要有 toString() method才得以被輸出

<%! 則是宣告用的程式碼,它會在轉譯出來的Servlet裡宣告成員
主要用在靜態及實例變數或方法的宣告,甚至連內部類別也可以
在這裡宣告的變數才會是全域變數,否則都會變成區域變數
這跟JSP轉Servlet時的編譯方式有關
container會將在一般Scriptlet裡的程式碼都放置在一個叫做 _jspService() 方法裡
每次進行服務時都會呼叫這個method,當然裡面的變數也只有區域變數的效力

JSP的註解寫法是 <%-- 註解  --%>,很類似HTML的註解寫法( <!-- 註解 --> )
JSP註解在頁面轉譯過程中就會被拿掉,不像HTML註解會顯示在 Client 端
所以想隱藏註解於JSP頁面的話選擇JSP註解寫法即可

隱含物件在轉譯的過程中會對映到Servlet/JSP API裡的某些東西,如:
out -> JspWriter      //不屬於PrintWriter的類別階層,但擁有與其相似的大多數方法
request -> HttpServletRequest
response -> HttpServletResponse
session -> HttpSession
application -> ServletContext
config -> ServletConfig
exception -> Throwable    //只有在error page才會看到的隱含物件
pageContext -> pageContext  //封裝了其他隱含物件,能夠利用它的參考取得其他隱含物件
page -> Object
能讓我們藉此使用Servlet的一些特性
隱含物件裡和指令裡都可以看到page的字眼,注意兩者完全不相干

被轉譯出來的Servlet會實作HttpJspPage介面
實作的三個主要 method 是 jspInit()、jspDestroy()、_jspService()
_jspService() 為container自動處理的方法,關係到頁面的轉譯,是不能被覆寫的方法
另外兩個方法則可以覆寫來進行處理
jspInit() 會被Servlet的 init() 呼叫,jspDestroy() 則會被Servlet的 destroy() 呼叫
所以接下來先示範用 jspInit() 在JSP中進行Servlet的初始化工作:
web.xml=========

<web-app ....>
....
    <servlet>
    <servlet-name>initTest</servlet-name>
    <jsp-file>/test.jsp</jsp-file>
    <init-param>
      <param-name>val</param-name>
      <param-value>1</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>initTest</servlet-name>
    <url-pattern>/test.jsp</url-pattern>
  </servlet-mapping>
........
</web-app>

與一般Servlet作組態的差別在於,必須增加<jsp-file> tag
接著透過宣告覆寫 jspInit() :
test.jsp============

<%!
public void jspInit(){
    ServletConfig scf = getServletConfig();
    String getAns = scf.getInitParameter("val");
    ServletContext scx = getServletContext();
    scx.setAttribute("Answer", getAns);
}
%>

因為是以Servlet的 init() 所呼叫的,所以到此方法執行時有ServletConfig和ServletContext可用
範例中取出初始參數後,將它放入設定成應用程式作用領域的屬性

與範例不大相同的是,設定及取得屬性一般使用JSP的4個隱含物件來做
1.Application
   Application.setAttribute() = Servlet裡的 getServletContext().setAttribute()

2.Request
   request.setAttribute()  ,Servlet中也是一樣的語法

3.Session
   session.setAttribute() = Servlet裡的 request.getSession().setAttribute()

4.Page
    pageContext.setAttribute(),pageContext是JSP裡獨特的物件

利用指向PageContext的參考可以取得任何作用域的屬性
設定Page作用域的屬性:
<% pageContext.setAttribute("val", 1); %>

取得Page作用域的屬性:
<%= pageContext.getAttribute("val") %>

使用pageContext設定Session作用域的屬性:
<% pageContext.setAttribute("val", 1, PageContext.SESSION_SCOPE); %>

使用pageContext取得Session作用域的屬性:
<%= pageContext.getAttribute("val", PageContext.SESSION_SCOPE) %>
(等同於 <%= session.getAttribute("val") %> )

如同 pageContext 用在session的範例那般,可以設定的作用領域有:
APPLICATION_SCOPE、PAGE_SCOPE、REQUEST_SCOPE、SESSION_SCOPE
如果不知道屬性所在的作用域,也可以利用 pageContext來尋找
語法是 <%= pageContext.findAttribute("val") %>
搜尋範圍會先檢查Page作用域,接著按照 Request -> Session -> Application
如果作用域裡的屬性名稱有相同者,會回傳先找到的那個

最後提到編譯的實際情況
頁面的轉譯及編譯只會發生一次,之後執行就與一般Servlet無異
然而實際執行時,最好先將JSP事先轉譯及編譯過
第一次實質的請求進來時再做,會耗費客戶端太多的時間
可以參考container廠商所提供的預先編譯的方法

2010年7月23日 星期五

JSP & Servlet Session、Cookie處理

Session幫助我們記錄與 Client 端溝通過程的一切,並將之存放在記憶體中
因為HTTP連線協定並不是持續的存在,所以他無法紀錄請求的資訊
對container來說,每個請求都來自新的 Client 端
所以我們會建立具有獨特身分的session來辨識客戶及其需求

溝通的過程中,交換辨識身分用的資訊是必要的
最簡單而且最常見的方式是透過Cookie來交換這項資訊
所以 session 的實作有很大的機會與Cookie相關

正常情況下,也就是 Client 端沒關閉Cookie的功能時
container會自動產生辨識此 Client 端身分用的ID
Client 端會建立一個叫做 jsessionid 的Cookie來儲存此 ID
Server 端會根據這個Cookie裡的資料來對應產生的session
撰寫的程式不應該自行請求 jsessionid 參數( getParameter("jsessionid") )
也不應該在 header 裡增加 jsessionid 屬性,以免Cookie被覆寫
可以由container代為處理的部分千萬不要自己動手
Cookie相關的處理由container代勞,我們只需要關注簡短的session處理即可
程式碼範例:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
       
        HttpSession session = request.getSession();
       
        if(session.isNew()){
            out.println("A new session");
        }else{
            out.println("Welcome back ! ");
        }
    } 

request.getSession()這行程式碼會回傳session
尚未有session的情況下,則會自動產生session ID並以它建立session後回傳
getSession() 裡接受boolean型別參數,能做出篩選已存在session的效果
getSession(true) 跟 getSession()作用相同
getSession(false) 表示希望得到已經存在的session,找不到時會回傳null值
session.isNew() 則如同它字面上的意思般易懂,客戶端還沒用這個session回覆時會回傳true
這邊再提醒一次,一般來說container會自動產生辨識此 Client 端的session
所以這個函式通常只是回傳container自動建立的session
實際運用上,用 session.isNew() 判斷session的存在會比較省事

關閉Cookie會讓 Client 端沒辦法加入session
session.isNew()會一直回傳 true,也不會有警告訊息提醒無法將session關連到 Client 端
解決關閉Cookie的方法是我們在URL上面附加辨識身分的資訊來協助對應session
範例程式碼:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        //to get session if Cookie function is open
        HttpSession session = request.getSession();
       
        out.println("<html><body>");
        out.println("<a href=\"" + response.encodeURL("/destination")
                    + "\">click me</a>");
        out.println("</body></html>");
    }

URL的 rewrite 只有在container嘗試Cookie失敗,和有呼叫回應物件的 encodeURL() 後才發生
container看到 getSession() 後,會同時使用Cookie和附加資訊在URL後
接著由客戶端傳來下一個請求時,getSession()會讓 container 從請求中讀取session ID
若可以藉由Cookie取得,container會忽略 encodeURL() 的呼叫,也省下重寫URL的麻煩
需要使用重導( Redirect )的場合,處理方法也很類似
以response.encodeRedirectURL("/destination")來重寫URL
URL重寫的處理方式由container的廠商提供,這就不在我們觀察的重點裡了
另外,產生URL重寫的條件之一是必須由動態網頁處理
所以不能使用靜態html網頁

session物件儲存在Server的記憶體裡
閒置的session會浪費系統資源,所以必須有方法來清除閒置的session
HttpSession 除了 getAttribute()、setAttribute()、removeAttribute()等針對屬性的method外
還有幾個method可以使用:
1.getCreationTime() 傳回session第一次被建立的時間
   此method可以計算session到目前為止的存活時間,進而做到限制session存活的時間長度

2.getLastAccessedTime() 回傳container最後一次收到與此session ID相關的請求的時間
   可以判斷客戶是否離開很長的時間,並提醒客戶或是直接用 invalidate() 砍掉session
 
3.setMaxInactiveInterval() 指定針對此session的不同請求的最長時間間隔
   客戶若沒有在指定時間內做出請求,會讓此session失效

4.getMaxInactiveInterval() 取得針對此session的不同請求的最長時間間隔
   可以用來判斷一個不活動的客戶端的session還有多少的存活時間
 
5.invalidate() 結束此session並釋放資源
   砍掉不要的session並清除其所擁有的屬性。當session被清除後再去呼叫會回傳例外

除了使用上述方法主動砍掉session外,container也提供了機制來清除session
上面提到的 setMaxInactiveInterval() ,會讓沒有在指定的秒數內作出回應的session被清除
另外也可以在DD裡組態設定讓container主動回收沒有在指定時間內做儲回應的session
範例:
web.xml================

............
  </servlet>

  <session-config>
      <session-timeout>20</session-timeout>
  </session-config>
............

不過需要注意的是,setMaxInactiveInterval()與DD兩方法的輸入參數的時間單位不同
setMaxInactiveInterval()裡需填入存活秒數,DD組態所設定的是存活的分鐘數

JavaEE的Cookie原始設計是用來協助session,但也可以利用它來做一些事
Server將Cookie傳給 Client 端,Client 端則會在產生另一個請求時將它送回
Java的Cookie預設的存活時間跟session一樣,在瀏覽器關閉時即消失
想讓它保持用長久的影響力可以手動設定其存活秒數
範例如下:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
        response.setContentType("text/html");
        String name = request.getParameter("username");

        Cookie cookie = new Cookie("username",name);
        cookie.setMaxAge(30*60);         //set Life of Cookie to 30 minutes
        response.addCookie(cookie);
    }

瀏覽器協助建立Cookie時,會依照網站別建立不同的儲存資料夾
所以不用擔心有相同命名的Cookie的值會被洗掉

另外取得Cookie的方法也需要注意一下
必須用陣列迴圈找出來,沒有用Cookie的header名稱去找的method
如果存活時間設定成 0,Cookie就會立即失效
設定成負值則是在瀏覽器關閉後失效
取得要求物件的方法也可以像範例那般自行傳入
使用方式就必須在JSP頁面中寫出如 <%=xxx.isCookieSet(request) %> 一般的程式碼
前面的 xxx 是在這個頁面中使用的 bean 名稱,request 是JSP的隱含物件
下一篇筆記會談到較詳細的JSP使用方式
範例:
    public boolean isCookieSet(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
       
        if(cookies != null){
            for(int i = 0; i < cookies.length; i++){
                Cookie cookie = cookies[i];
                .....
            }
        }
      ....
    }

HttpSession物件裡還有一個重要的部分沒有提過
那就是針對session搬遷的處理
session被搬移的實例在分散式系統中比較容易見到
分散式系統會為了達到平衡負載的目的,而可能將請求分別送往不同的JVM中
因此 Client 端對同一個Servlet所提出的複數個請求很有可能會在不同的JVM上
如何處理session也會是一個課題

HttpSession物件的處理要遵守其原則
應用程式裡的每個session ID都只會對應到一個HttpSession物件
在同個瀏覽器裡的請求一定會有發生的先後順序
所以HttpSession物件會依照請求順序搬移到該請求所在的JVM上
而偵聽器篇筆記裡提過的HttpSessionActivationListener就是為了應付搬遷要求而產生的
它能讓HttpSession物件在搬遷前作些準備,以及在搬遷後重新活化

實務運用上,只要屬性是可序列化(Serializable)的,它也可以跟著session一起被搬遷
所以HttpSessionActivationListener這個偵聽器是很有可能不會被使用到的
實作Serializable介面的物件可以選擇實作 writeObject() 和 readObject() 兩個method
writeObject()可以在序列化期間將不可序列化的欄位變成 null,並在 readObject()時將之恢復
然而,這些方法不一定會在session搬遷期間被呼叫
拿捏自身需求及時機掌握來選擇要如何實作

此外,HttpSessionBindingEvent 這個與屬性相關的事件裡有一些方法需要注意
getName() 會以String型別回傳觸發事件的屬性的名稱
getValue() 回傳觸發事件的屬性的值,但注意觸發時間點是在被修改前,也就是傳回原本的值

2010年7月21日 星期三

JSP & Servlet 多執行緒澄清概念

每個屬性都是一個物件,被設定在三種Servlet API(Context、Request、Session)上
其組成可以簡單的分成String型別的名稱與Object類別的值,並被存放在Map類別實例變數上
這篇筆記會接續上篇,討論屬性與多執行緒產生的問題

Context的作用領域能讓應用程式的每個部分存取
但這也造成了一個缺點,就是沒有辦法達到執行緒安全(thread-safe)
假使將doGet() 等method增加 synchronized 宣告
卻會使Servlet一次只能服務一個客戶,而失去了並行性這個最大的好處
更糟的是這個方法沒有效果,因為只能限制同一個Servlet無法重複讀取Context屬性
其他的JSP或是Servlet依然可以存取,造成屬性的值的變動並不如我們預期

該被鎖定的不是Servlet,而是Context!
程式撰寫如下:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        synchronized (getServletContext()) {
            getServletContext().setAttribute("a", 1);
            out.println(getServletContext().getAttribute("a"));
        }
        ........
    } 

synchronized鎖住的範圍正是Context相關處理的部分
注意這樣雖然已經是最好的解決之道,但一定會降低處理效率
撰寫時放在synchronized裡的程式碼越短越好

執行緒安全的問題也會發生在session處理上
session的應用在於保存客戶端的對話的狀態
一般來說,session只針對單一客戶做處理,所以執行緒安全問題比較容易被忽視
有問題的原因在於若同一個用戶同時開啟多個瀏覽器,在container的判斷上會是多個 Instance
雖然這種情形發生機會較少,但一樣不可忽視
處理方式類似上面範例程式碼:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                          throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        HttpSession session = request.getSession();
        synchronized (session) {
            session.setAttribute("a", 1);
            out.println(session.getAttribute("a"));
        }
    .........
    }

真正達到執行緒安全的是Request屬性及區域變數
這也提示了撰寫Servlet時最好以區域變數取代實例變數
request本身的setAttribute()、getAttribute()、removeAttribute()
能有傳遞變數的作用

如果應用程式需要多個Servlet來完成請求
程式範例:
            request.setAttribute("a", 1);
           
            RequestDispatcher view = request.getRequestDispatcher("test.jsp");
            view.forward(request, response);

取得RequestDispatcher時也可以附加查詢字串
RequestDispatcher只有兩個method - include() & forward()
include() 一般並不常在Servlet中被使用,但是JSP常用的<jsp:include>即是使用這個方法
forward()則是回應client端的同時也轉發給另一個Servlet
RequestDispatcher的取得方法有兩種,從 ServletRequest 或是 ServletContext 取得
但不論是哪一種都必須指明所要轉交的路徑
跟之前筆記提過的的sendRedirect()可以做個比較
sendRedirect()裡輸入的路徑若以 "/" 開頭,container會建立一個相對於container本身的URL
getRequestDispatcher()則是相對於本Web Application的URL
不加 "/" 開頭的URL則都一樣是相對當前目錄位置,當前應用程式範圍內為限
注意,轉交必須在回應被送出以前完成才會被建立

2010年7月20日 星期二

JSP & Servlet Listener 簡介

偵聽器(Listener)的設置能讓我們在 Web Application 有任何事件被觸發時,得以進行處理
本篇筆記提供一個利用Lsitener做到資源共享的範例
並使用DD來避免將參數寫死在程式中
藉此說明偵聽器的使用方法

DD的用途在筆記(1)寫過不只拿來描述Servlet的配置
首先就來個程式範例:
web.xml================

..............
<servlet>
    <servlet-name>demo</servlet-name>
    <servlet-class>tw.vencs.demo</servlet-class>

    <init-param>
      <param-name>test</param-name>
      <param-value>testString</param-value>
    </init-param>
</servlet>
..............


 demo.java===============

..........
    PrintWriter out = response.getWriter();
    out.println(getServletConfig().getInitParameter("testString"));
.......

demo.java正是這一次測試的Servlet檔案
每個Servlet都會繼承到getServletConfig(),可以利用它來獲得String類別的初始參數
然而呼叫的時機尚須注意,不能在建構式中使用這個method
container從DD取得該Servlet的初始參數,並將參數交給屬於該Servlet的ServletConfig物件
直到執行Servlet的 init() 時才把ServletConfig物件傳給這個Servlet
init(ServletConfig) 會呼叫 init() ,需要改寫時覆寫 init()即可

初始參數只會在 container 初始化Servlet的時候會被讀取
修改完後必須重新啟動Tomcat才能變更
所以如果需要使用經常變動的參數還是使用資料庫或是從檔案讀取的方式較為合宜
初始參數也沒有辦法像一般參數那樣使用 setAttribute() 改變值
因為初始參數本身的設置目的就是提供預設性質的參數以供使用

一個 Servlet會有一個ServletConfig物件,其初始參數也僅能供自身使用
接下來談到的是用Context來創造整個 Web Application 都能利用的初始參數
好處是整個應用程式裡的Servlet和JSP都可以使用
JSP能夠存取的原因是因為JSP最後也會被轉譯成Servlet 
因此也不需要在DD為每個Servlet作一樣的設定
範例:
web.xml================

..............
<servlet>............</servlet>

  <context-param>
    <param-name>email</param-name>
    <param-value>a@gmail.com</param-value>
  </context-param>
  
..............

context-param須放置於<web-app>宣告內,但不能放置於Servlet宣告內
呼叫時使用 getServletContext().getInitParameter("email")
呼叫變數的方法兩者都是getInitParameter()
ServletContext在整個Web Application裡只會有一個
當然前提是這支程式只在單一個JVM執行
如果是分散式系統還需要可量如果每個JVM裡的ServletContext不同所造成的後果

Servlet的ServletConfig 物件有指向ServletContext的參考
所以也可以寫出 getServletConfig().getServletContext().getInitParameter()來取得
這個寫法也等同於 this.getServletContext().getInitParameter()
當然這種寫法很少見的理由是,唯一需要透過ServletConfig取得ServletContext的情況很少見
只有在Servlet並非繼承HttpServlet時才必須這樣寫,所以幾乎沒有必要這樣寫就是了

注意一下,上面提到的初始參數都是String的名值配對
假使需要的是物件、又或許是資料庫連結的作法會有稍許不同
接下來我們會用到偵聽器(Listener)來偵聽Context初始化事件
並在程式開始進行服務前,取得Context參數來執行一些程式碼
程式範例:
demoListener.java======
package tw.vencs;

import javax.servlet.*;

public class demoListener implements ServletContextListener {

    public void contextInitialized(ServletContextEvent event) {
        ServletContext sc = event.getServletContext();
        String doBreed = sc.getInitParameter("kind");
        dog newDog = new dog(doBreed);
        sc.setAttribute("dog", newDog);
    }

    public void contextDestroyed(ServletContextEvent event) {
        //Context結束時,表示整個應用程式都結束
    }
}

web.xml==============

............
  <context-param>
    <param-name>kind</param-name>
    <param-value>yuki</param-value>
  </context-param>

  <listener>
    <listener-class>tw.vencs.demoListener</listener-class>
  </listener>
..........

dog只是普通的測試用類別,就不描述了
Listener只是個單獨運作的Java應用程式,功用就只是初始化類別物件
實作時需要實作 ServletContextListener 介面
contextInitialized() 當然就是在context 建立時被呼叫的method
在開始和結束時都會傳入ServletContextEvent
demoListener只是簡單的從Context裡獲得Context初始參數
並以它建立物件後,使用 setAttribute() 將物件以屬性 "dog" 的方式儲存在Context裡
寫完Listener後,接著在DD裡設定  <listener>
部署方式與<context-param>相同

之後只要用Context取得屬性即可取得物件
如下程式碼:
dog Dog = (dog)getServletContext().getAttribute("dog");

從 getAttribute() 取得的物件會是 Object 類別,所以取出後要記得轉型

這次的偵聽器類別使用的是ServletContextListener,偵聽器總計有 8 種
整理如下:
1.ServletContextAttributeListener
功能:偵聽Context裡的屬性是否有被新增、移除、取代
事件類別:ServletContextAttributeEvent

2.HttpSessionListener
功能:檢查正在活動的 session,可以藉此得知有多少人在線上
事件類別:HttpSessionEvent

3.ServletRequestListener
功能:當有請求進來時都會發出通知事件
事件類別:ServletRequestEvent

4.ServletRequestAttributeListener
功能:偵聽Request裡的屬性是否有被新增、移除、取代
事件類別:ServletRequestAttributeEvent

5.HttpSessionBindingListener
功能:偵聽屬性類別與 session 之間的聯繫關係(被新增或移除至session)
事件類別:HttpSessionBindingEvent

6.HttpSessionAttributeListener
功能:偵聽 session 裡的屬性是否有被新增、移除、取代
事件類別:HttpSessionBindingEvent

7.ServletContextListener
功能:偵聽Context是否被建立或銷毀
事件類別:ServletContextEvent

8.HttpSessionActivationListener
功能:以物件角度偵聽所聯繫的 session 被移到另外一個JVM,或者是移過來時發出通知
事件類別:HttpSessionEvent

HttpSessionBindingListener 與 HttpSessionAttributeListener 的不同點在被偵聽的對象不同
前者是偵聽屬性物件"被"聯繫到 session 裡,後者則是偵聽 session 的狀態
前者可以做到的事,舉例來說:購物車
連接了顧客的session,當顧客購買後,以被連結的物件來對資料庫作更新

HttpSessionAttributeListe 和 HttpSessionActivationListene不需要在DD裡組態
其應用方式會在session篇筆記說明

2010年7月10日 星期六

XML符號對應

有一些字元,如"<"和"&"在XML資料裡嚴格來說是不合法的。
其他像">"則是合法的,但習慣上最好將這些字元置換成XML的實體對應。
以下列示出XML裡事先定義好的實體對應:

原先字元     實體對映          
<                   &lt;           
>                   &gt;
&                  &amp;
'                    &apos;
"                   &quot;

PHP跨資料庫應用淺談

寫一下目前查資料之後的結果
當然實際的驗證及比較要先等一段時間後,有空再去把這些都學完

目前較常拿來做比較的跨資料庫抽象層應用大致上有MDB2、ADOdb、PDO三種

MDB2的優點在於pear的套件取得上較為容易,開發較不會有客戶端缺少開發環境的問題
當然套件的相依性會是比較麻煩的議題
資料庫處理速度不錯,算是各方面都有中上表現的選擇
詳情可以看MDB2筆記

ADOdb以微軟的ADO為基底所製作,很多習慣使用ASP的使用者會比較喜歡ADOdb
它有蠻多強大的功能,卻也因為過於龐大讓資料庫處理速度慢的缺點浮現
官方網站有提供Lite版本,大小大約是原本的1/6
不過因為缺少許多強大的ADOdb功能,如果原先有使用那些功能的話還要改變程式的寫法
官方網站:http://adodb.sourceforge.net/
熱心人士翻譯的手冊:http://www.php5.idv.tw/documents/ADODB/

PDO是PHP5以後新增加的功能,也會是以後PHP6預設的資料庫處理方式
資料庫處理的速度會是目前PHP選擇中最快的
因為PHP5.3後物件導向方面的變化不小,學習及使用上要更加注意

2010年7月8日 星期四

MDB2使用筆記(2)

上篇介紹了如何建立資料庫的連線
接著來說說MDB2的query部分,總共有query()和exec()兩種
成功會回傳結果,失敗則都會回 傳MDB2_Error訊息
差別在於query()是執行sql指令並取得值
exec()則是單純的執行指令,適用於 INSERT、UPDATE或DELETE

query()範例:
// Proceed with a query
$res =  $mdb2->query('SELECT * FROM clients');

// Always check that result is not an error
if (PEAR::isError($res)) {
    die($res->getMessage());
}

exec() 範例:
$sql  = "INSERT INTO clients (name, address) VALUES ($name, $address)";

$affected = $mdb2->exec($sql);

// Always check that result is not an error
if (PEAR::isError($affected)) {
    die($affected->getMessage());
}

另 外在查詢時一樣可以設定查詢的範圍及起點
$sql = "SELECT * FROM clients";
$mdb2->setLimit(20, 10);                 //回傳20筆資料,從第10筆資料後開始算起
$affected =& $mdb2->exec($sql);

$sql = "DELETE FROM clients";
if ($mdb2->supports('limit_queries') === 'emulated') {
    echo 'offset will likely be ignored'
}
// only delete 10 rows
$mdb2->setLimit(10);
$affected =& $mdb2->exec($sql);

查詢時可以使用quote()提高資料存取的安全性, 例如:
$query = 'INSERT INTO sometable (textfield1, boolfield2, datefield3) VALUES ('
    .$mdb2->quote($val1, "text", true).', '
    .$mdb2->quote($val2, "boolean", false).', '
    .$mdb2->quote($val3, "date", true).')';

quote() 函式只有第一個參數-值是需要的,後面的三個參數則是視需求使用
第二個參數是值的類別,可以排除設定類別外的值
允許使用的有text, boolean, integer, decimal, float, time stamp, date, time, clob, blob
第三個參數是是否使用quote(),即用引號包住值,false的話則只是單純的將值輸出
第四個參數則是使否要脫離萬用字元
善用quote()可以增加安全性

針對query出來的結果也可以調整表現的方式,例如:
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
setFetchMode() 有三種屬性
MDB2_FETCHMODE_ORDERED(例:$result[0])
MDB2_FETCHMODE_ASSOC(例:$result['name'])
MDB2_FETCHMODE_OBJECT(例:$result->name)

取得結果之後,接著就是要用函式來取出值
fetchOne(), fetchRow(), fetchCol() and fetchAll()這四個函式看起來就很容易懂
分別是取一個,取一行,取一列,取所有
此外 numRows(), numCols(), rowCount()等函式也可以獲取結果的其他資訊

除了以上處理過程外,也有函式可以幫助簡化撰寫,直接取得結果
就是queryOne(), queryRow(), queryCol() and queryAll()這四個對應的函式

以範例作比較,使用query():
$res = $mdb2->query("select ID,name,birthday from member");
while ($data = $res->fetchRow()) {
  echo $data['ID'].','.$data['name'].','.$data['birthday'];
}

queryOne():
$res = $mdb2->queryOne("select count(*) from member");
if (!is_null($res)) echo $res;

queryRow():
$res = $mdb2->queryRow("select name,birthday from member where ID = 1");
if (!is_null($res)) echo $res['name'].','.$dbquery['birthday'];

依此類推,依需求選擇使用的函式

MDB2尚有預編譯及批量處理Prepare & Execute、moudle等好用功能沒提到
等之後有用到再去查技術手冊了

MDB2使用筆記(1)

來自官方的說明:

PEAR MDB2 is a merge of the PEAR DB and Metabase php database abstraction layers.

It provides a common API for all supported RDBMS. The main difference to most  other DB abstraction packages is that MDB2 goes much further to ensure  portability.


MDB2整合了過去的 PEAR:db函式的新類別庫,為目前PEAR官方所支持
其有許多選擇性的功能,可以增加對各種資料庫系統間資料的可攜性
很適合作為跨資料庫的選擇

MDB2的安裝可以使用套件管理員來進行,詳情可以參考PEAR安裝教學那篇
或者是到PEAR套件官網下載,下載時要連同所連結的資料庫的驅動一起下載
以下簡介使用方式,詳細解說可以到官方說明手冊

首先來個修改自官網的範例:
 <?php
require_once 'MDB2.php';

$dsn = array(
    'phptype'  => 'mysqli',             //輸入所要連結的資料庫類別
    'username' => 'user',
    'password' => 'pw',
    'hostspec' => 'localhost',
    'database' => 'DBname',
    'charset' => 'utf8',
);

$options = array(
    'debug'       => 2,                                                  //numeric debug level
    'portability' => MDB2_PORTABILITY_ALL,
);

$mdb2 =& MDB2::connect($dsn, $options);
if (PEAR::isError($mdb2)) {
    die($mdb2->getMessage(). ', ' . $mdb2->getDebugInfo() );
}

$mdb2->disconnect();
?>

首先要使用PEAR的套件都必須先將使用的檔案include
dsn是Data Source Name,用來記錄資料庫資訊
另外,也可以寫成  $dsn = 'mysqli://user:pw@localhost/DBname';
options當然就是額外的附屬要求
需要一提的是portability選項
除非設置成了MDB2_PORTABILITY_NONE,否則query出來的結果都是小寫
例如結果不可能會有  echo $array['A'];
官方的預設是小寫,目的是要達到各種資料庫的相容性,當然這也是MDB2的原則

資料庫的連結方式有三種
&MDB2::connect 建立MDB2物件並連線資料庫
&MDB2::factory 建立MDB2物件,但等到要進行資料庫操作時才連線
&MDB2::singleton 同factory,但它保證只有一個MDB2物件連線到資料庫
想用哪個就用哪個,一般使用下應是factory效率較高
最後用disconnect()來結束連線

連線與結束連線可以以global variable的方式寫成function來增加使用的方便性
可以參考這篇文章

官網那邊尚有使用SSL來建立連線的範例,如下:
<?php
require_once 'MDB2.php';

$dsn = array(
    'phptype'  => 'mysqli',
    'username' => 'someuser',
    'password' => 'apasswd',
    'hostspec' => 'localhost',
    'database' => 'thedb',
    'key'      => 'client-key.pem',
    'cert'     => 'client-cert.pem',
    'ca'       => 'cacert.pem',
    'capath'   => '/path/to/ca/dir',
    'cipher'   => 'AES',
);

$options = array(
    'ssl' => true,
);

// gets an existing instance with the same DSN
// otherwise create a new instance using MDB2::factory()
$mdb2 =& MDB2::singleton($dsn, $options);
if (PEAR::isError($mdb2)) {
    die($mdb2->getMessage());
}
?>

PHP特殊符號

PHP除了一般運算時常見的符號以外,另外還有些符號較特殊

$         變數宣告符號
&        變數指標(加在變數前)
@      不顯示錯誤訊息(加在函數前)
->       物件的方法或者屬性
=>      陣列的元素值
? :      三元運算子

2010年7月5日 星期一

Flex調整物件重疊順序

Flex中的元件重疊問題很少被提到
因為Flex的運作方式是後來增加的物件會被排列在已有物件的上層
所以要調整物件重疊的先後順序就必須調整物件的加入順序
範例:
private function mouseOverHandler(event:MouseEvent):void{
  this.setChildIndex(event.currentTarget as UIComponent,this.numChildren-1);
}

這個function對讓所點擊的物件的加入順序做改變

Flex使用addChild加入flash.display類別元件

在Flex中使用addChild來加入Sprite、MovieClip等flash.display類別物件須經過處理
直接使用時會回傳強制轉換型別失敗
這是因為Application的 addChild() 並非完全繼承自DisplayObjectContainer

Application→LayoutContainer→Container→UIComponent→FlexSprite→Sprite
 →DisplayObjectContainer

而 addChild() 在Container那裡被覆寫了︰
public override function addChild(child:DisplayObject):DisplayObject

雖然參數child的類型是DisplayObject
但它必須實作IUIComponent介面後才能加入,就如同其他所有Flex元件都必須實作這個介面
如果要在Application裡添加Sprite,可以先把它裝進UIComponent裡
或者用UIMOVIECLIP,然後再添加這個UIComponent︰

import mx.core.UIComponent;

private function init():void {   
  var sp:Sprite = new Sprite();
  var uc:UIComponent = new UIComponent();   
  uc.addChild(sp);   
  addChild(uc);
}