2007年8月28日 星期二

簡易的效能測試方式

以下是一支簡易的 method,可以讓您了解程式的運作花了多少資源, 以便調整.

並介紹 Runtime.getRuntime().maxMemory(), Runtime.getRuntime().totalMemory()
Runtime.getRuntime().freeMemory() 是如何被使用的.
public class FreeMemoryTest {
public static void main(String[] args) {
//JVM 能從OS得到最大的記憶體,如果下了 -Xmx 參數,將以該參數為主
long maxMem = Runtime.getRuntime().maxMemory();
//JVM 已經從OS得到可使用的記憶體,會越取越多,直到 maxMemory 的最大容量,超過則會丟出 java.lang.OutOfMemoryError
long totalMem = Runtime.getRuntime().totalMemory();
//目前使用完剩下的記憶體
long startMem = Runtime.getRuntime().freeMemory();
long startTime = System.currentTimeMillis();

for (int i = 0; i <= 10000; i++) {
String str = "foo";
System.out.println(str);
}
long endMem = Runtime.getRuntime().freeMemory();
System.err.println("使用記憶體(Bytes): " + (startMem - endMem));
long endTime = System.currentTimeMillis();
System.err.println("使用時間(毫秒): " + (endTime - startTime));
System.err.println("總共記憶體(Bytes):" + totalMem);
System.err.println("目前最大記憶體(Bytes):" + maxMem);
}
}

詳細可再參考
透過 Java 參數來改善 Java 效能

2007年8月26日 星期日

分頁效能的實作

Displaytag 有一個常見的 issue, 就是在撈出大量資料時, 會出現 Outofmemory 的錯誤.
因為 displaytag 的預設值, 是將所有的資料一次撈到記憶體, 再執行分頁的動作, 然而這樣的作法是相當消耗記憶體的, 也容易引起exception.
在 displaytag 1.1 版本中(若您的版本是1.0, 請記得切換至1.1), 我們只需要幾個簡單的設定, 就可以改善這個狀況.

先看下列的設定:

<display:table id="row" name="RESULT_LIST" partialList="true" requestURI=""
class="list" pagesize="10" size="${RESULT_SIZE}">

基本上跟 display tag1.0 無太大差異, 但是請記得將 partialList="true" 加入, 表示 displaytag 開啟了有限制的分頁機制,
而 pagesize 為每頁的 record 數目, size 則為資料的總筆數. 該機制與以前最大的不同, 就是只抓取 pagesize 所指定的數量
(該例為10筆, 也就是一次只取出十個物件).

為了確切的達成分頁功能, 我們還需要幾個實作,
先看 Controller 的部分:
...
int pageIndex = getPageIndex(request, getPageIndexName("row"));
//利用 displaytag 的屬性 id="row", 取得目前在第幾頁

int resultSize = dao.getAllCount();
//總共的筆數

List list = dao.search(pageIndex, pageSize);
//目前的筆數

....
request.setAttribute("RESULT_SIZE", new Integer(resultSize));
request.setAttribute("RESULT_LIST", list);

在這個例子當中, 要注意的是 pageIndex 利用到兩個 mtehod getPageIndexName(),
getPageIndex(), 其用意即註解所示, 取得當前的頁數, 詳細實作如下:
....

import org.apache.commons.validator.GenericValidator;
import org.displaytag.util.ParamEncoder;
import org.displaytag.tags.TableTagParameters;

public static String getPageIndexName(String encoder) {
String pageIndexName = new ParamEncoder(encoder).
encodeParameterName(TableTagParameters.PARAMETER_PAGE);
return pageIndexName;
}

public static int getPageIndex(HttpServletRequest request, String pageIndexName) {
int pageIndex = GenericValidator.isBlankOrNull(request.getParameter(pageIndexName)) ? 0
:(Integer.parseInt(request.getParameter(pageIndexName)) - 1 );
return pageIndex;
}
....

而在 dao.getAllCount(), dao.search(), 這兩個呼叫當中, 是為了分別取得總筆數及目前的筆數,
當然 DAO 層的實作可能依照您的 persistence, 而有所不同.

若 dao.search() 是利用 Hibernate 實作的, 範例大致如下:
....
public List search(int pageIndex, int pageSize){
List values;

String hql = "from Person";
Query query = getSession().createQuery(hql);

if (pageIndex >= 0) {
query.setFirstResult(pageSize * pageIndex);
}

if (pageSize > 0) {
query.setMaxResults(pageSize);
}

values = query.list();

return values;
}
....

2007年8月21日 星期二

Session Level cache

Hibernate 的 session 物件有一種 cache 機制, 可維護一份您存取資料後的 instance.
因為存取資料庫的連線, 需要不小的開銷. 該機制將存取 persistence object 的結果讀進記憶體(利用一份map物件來維護), 若在 session 結束後, 有相同的SQL操作, 則 cache 機制可以直接將物件回傳, 不用再執行額外的資料庫連線動作; 而這種存活於 session 內的 cache 機制, 又稱為 session Level cache.

用一簡例說明:

....
session.beginTransaction();

Person aPerson = (Person) session.load(Person.class, personId);
aPerson.getAge();

Person aPerson2 = (Person) session.load(Person.class, personId);
aPerson2.getAge();

session.getTransaction().commit();
session.close();
....

您可以觀察 console 內SQL的執行:
Hibernate: select person0_.PERSON_ID as
PERSON1_2_0_, person0_.age as age2_0_, person0_.firstname as
firstname2_0_, person0_.lastname as lastname2_0_ from PERSON person0_
where person0_.PERSON_ID=?
我們可以發現, 雖然 session.load() 呼叫了兩次, 但由於 cache 機制,
會直接回傳記憶體中的物件給 aPerson2 存取, 如此便減少了一次資料庫的連線成本

但有時存取過於頻繁, cache 內有太多的物件有可能會導致 OutOfMemory,
若您想釋放 cache 中的記憶體, 可以使用 session.clear(), session.evict(object),
另外 session.close() 也會結束該段 cache.

以上例而言:
...
Person aPerson = (Person) session.load(Person.class, personId);
aPerson.getAge();

session.evict(aPerson);
//session.clear()

Person aPerson2 = (Person) session.load(Person.class, personId);
aPerson2.getAge();
...
您會發現 console 會執行兩段SQL指令, 因為 session 中的 cache 已被清除了,session.load()
找不到對應的資料, 就會再重起一段資料連線.

Reference:
Hibernate Gossip: 簡介快取(Session Level)
Reference Documentation

2007年8月20日 星期一

JavaScript 開發筆記 - 格式驗證

格式驗證是常見的議題了, 這裡分享幾種簡單的格式驗證:

郵件格式驗證

function verifyMail(mail) {
var filePath1 = document.all(mail);

if (filePath1.value.length == 0) {
window.alert('請輸入信箱');
return false;
}

apos = filePath1.value.indexOf("@")
dotpos = filePath1.value.lastIndexOf(".")

if (apos<1||dotpos-apos<2) {
window.alert('請輸入正確的郵件格式');
return false
}

return true;
}

2007年8月16日 星期四

Implicit objects 的顯示方式

一般我們在 displaytag 顯示 List 物件時, 會使用下列的方式:


< id="row" name="${queryHospital}" requesturi="" class="sample" pagesize="">" >

< titlekey="text.id" sortable="true">
< href="hospitalList.do?METHOD=editHospital&id=< c:out value=">" >
< value="${row.id}">< value="${row.id}">< /a >
< /display:column >
......
< /display:table >


也就是將物件以 EL 的方式接收, 並利用 display:table 中的屬性 "id", 來作轉換,如此就可以很容易的在
<> 標籤裡,利用屬性 id 所設定的值, 將List物件的property一一取出.

但由於各家伺服器的Web Container所支援的方式不盡相同, 導致在 displaytag 的 "id" 屬性失效, 如此一來 id 完全無法帶出 property 中的資料.

若您的需求僅是將資料作動態的連結, displaytag 還是有幾個方便好用的屬性,
可以解決該問題.

引用一下上面這段:

< titlekey="text.id" sortable="true">
< href="hospitalList.do?METHOD=editHospital&id=< c:out value=">" >< value="${row.id}">< /a >
< /display:column >

可以簡化成:

< property="id" href="hospitalList.do?METHOD=editHospital" titlekey="text.id" paramid="id" paramproperty="id" sortable="true">

該屬性很簡單明瞭, 尤其 paramProperty 可以讓我們動態的去呼叫 Property, 而使用 href, paramId, paramProperty 也有某種程度達到簡化的目的.

該方法在 weblogic 9.2 是測試通過的, 但是displaytag 從去年的六月就停止開發了, 若有相關的 bug, 可能需要自己動修改.
若您的頁面需求比較複雜, 也可以考慮別種solution來實作.

#後記:該問題又找到了一個解決方案,將 display:table 內的ID屬性,更新為UID即可.

displaytag - Implicit objects created by table
http://displaytag.sourceforge.net/11/tut_implicitobjects.html
eXtremeComponents
http://code.google.com/p/extremetable/
jmesa
http://code.google.com/p/jmesa/

2007年8月12日 星期日

Date migration

我們在移動(或修改)資料表的時後, 常常會需要保留原本的資料內容,
因此資料整合的步驟就變的很重要.

其步驟大致為:

1. 建立 temp table
2. 從 original table 複製資料到 temp table
3. 刪除 original table, 並建立新的 schema
4. 從 temp table 複製資料至 新建的 table
5. 刪除 temp table

其中比較關鍵的SQL, 就是複製資料表中的資料內容, 這裡以一個 Person 的表格為例
如下所示:

CREATE TABLE temp_person (
PERSON_ID bigint(20) NOT NULL auto_increment,
age int(11) default NULL,
firstname varchar(255) default NULL,
lastname varchar(255) default NULL,
PRIMARY KEY (`PERSON_ID`)
);

insert into temp_person(person_id, age, firstname, lastname)
select person_id, age, firstname, lastname
from person;

2007年8月10日 星期五

Collection Framework 概觀

Collection Framework 是實現資料結構的重要介面, 基本上如下表所示,
其中 Set, List 繼承於 Collection 介面.
Set --> StoredSet
Map --> StoredMap

Interface

Implementation

Historical

Set(禁止重複)

HashSet(未排序)



TreeSet
(
照字母排序)

LinkedHashSet
(按照加入的順序排序,
實現queue的先進先出)


List(可重複)


ArrayList
(
未排序)


LinkedList(特性同上)

Vector
Stack

(thread-safe)

Map(key-value)

HashMap(未排序)


TreeMap

(照字母排序)

LinkedHashMap
(
特性同上)

Hashtable
Properties

(thread-safe)


三個介面的特性已以大致在表中呈現, 其中 StoredSet, StoredMap
其實是利用 TreeSet, TreeMap 介面來實現. 下面舉一個簡例:
Comparator comparator = Collections.reverseOrder();
Set reverseSet = new TreeSet(comparator);
reverseSet.add("Bernadine");
reverseSet.add("Elizabeth");
reverseSet.add("Gene");
reverseSet.add("Clara");
System.out.println(reverseSet);
output: [Gene, Elizabeth, Clara, Bernadine]

該例呼叫 Collections.reverseOrder(), 再傳入一個 Comparator, 使得 TreeSet
具有倒序的效果, 然而 TreeSet 本身的特性是照字母排序的, 會先排序後, 再執行 Comparator
裡面的 method.

在 Linked 開頭的部分(LinkedHashSet, LinkedList, LinkedHashMap)
都實現了 queue 的資料結構, 也就是先進先出, 跟按照字母排序的 Tree..略有不同
一個簡例如下:
Set linkedHashSet = new LinkedHashSet();
linkedHashSet.add("Bernadine");
linkedHashSet.add("Elizabeth");
linkedHashSet.add("Will");
linkedHashSet.add("Gene");
linkedHashSet.add("Elizabeth");
linkedHashSet.add("Clara");
System.out.println(linkedHashSet);
output: [Bernadine, Elizabeth, Will, Gene, Clara]

那麼使用這些資料結構的時機, 若不需要排序的, 可以使用 Hash 的介面. 若有排序的需求,
可以利用 Tree, Linked..的介面, 但是速度會比較慢. 在 Thread-safe 方面, 則可以參考 Vector, Stack, Hashtable, Properties.

在最後補充遍巡(Iterator) Hash 的方式:
HashMap m = new HashMap();
m.put("foo", "foo");

Set entrySet = m.entrySet();
Iterator it = entrySet.iterator();

while(it.hasNext()){
Map.Entry me = (Map.Entry)it.next();
Object key = me.getKey();
Object value = me.getValue();
}
Reference:
Introduction to the Collections Framework

2007年8月5日 星期日

BeanUtils 學習資源分享

最近的專案常常會進行大量的 form 存取,若欄位少時,直接塞值丟到 DAO裡面也還OK,
但是動輒五六十個欄位就吃不消了,因此查詢了一下 BeanUtils 的用法,有兩個網址可以提供參考:

Jini 版的介紹
http://www.softleader.com.tw/epaper030412.htm

Bean Introspection Utilities (Version 1.7)
http://jakarta.apache.org/commons/beanutils/api

而我常用到的 mathod是
1. PropertyUtils.getSimpleProperty(Obj, "fieldname");
2. BeanUtils.copyProperties(distObj , fromObj);

第一個 PropertyUtils.getSimpleProperty 相當好用,只要傳入 Bean 的物件、及欄位名稱,再加以轉型,就可以得到您想要的值。

例:


for (int i = 0; i < list.size(); i++) {
Object clazz = list.get(i);
String seqNo = (String) PropertyUtils.getSimpleProperty(clazz, "seqNo");
}

BeanUtils.copyProperties 則是兩個 Bean 物件的複製,想把 Form 的值丟到 Persistence-Object時,該method發揮的很好。

例:

IftIpdordfbForm infoform = (IftIpdordfbForm) form;
IftIpdordfb iftIpd = new IftIpdordfb();

BeanUtils.copyProperties(iftIpd , infoform);
dao.save(iftIpd);

saveOrUpdate 的使用時機

當我們呼叫 session.saveOrUpdate() 時,何時會使用 save(), 何時會使用 update()
取決於物件的id屬性值, 與 hbm.xml 中的 unsaved-value 設定.

Hibernate 會利用該設定的內容, 來比對物件的主鍵屬性.
若主鍵的屬性與 unsaved-value 的設定內容相符(Hibernate 把認為該物件為 Transient Object), 則 Hibernate 送出 session.save(),進行儲存,反之(Hibernate 把認為該物件為 Persistence Object)則呼叫 session.update().

例如:

...
session.beginTransaction();

Person aPerson = new Person();
aPerson.setFirstname("Foo");

Person aPersonUpd = new Person();
aPersonUpd.setId(new Long(2));
aPersonUpd.setFirstname("Foo");

session.saveOrUpdate(aPerson);
session.saveOrUpdate(aPersonUpd);

...
session.getTransaction().commit();
session.close();
若您的 unsaved-value 設為 "null",
而 aPerson 物件的id屬性也為 null, 此時 Hibernate 會執行 session.save(), 資料庫變新增一筆 Person 的紀錄.

另外 aPersonUpd 已設定id為 "2", Hibernate 經由比對後, 若發現資料庫有該筆紀錄, 則會呼叫 session.update() 予以更新.

以下是幾種unsaved-value設定的說明:

1. unsaved-value="null"
為預設的設定, 當取出的物件主鍵值為 null(表示資料庫無對應的紀錄),
則呼叫 save().

2. unsaved-value="undefined"
不論該物件的id屬性塞入任何型態的資料, 都會進行檢查, 若在資料庫中找不到對應的鍵值,
則進行 session.save(), 反之呼叫 session.update().

3. unsaved-value="any"
不論該物件的ID屬性有沒有值, 一定會呼叫 session.save(), 以儲存資料.

4. unsaved-value="none"
不論該物件的ID屬性有沒有值, 一定會呼叫 session.update(), 以更新資料.

若您的主鍵設定為 "assigned" (generator class="assigned"), Hibernate 在判斷時,
會先執行 select 的SQL, 再進行鍵值的比對, 在這種狀況下, 執行 session.saveOrUpdate()
的效能會比 Hibernate的 id generator 略差, 這是值得注意的.

Hibernate 生命週期初探

了解 Hibernate 的物件生命週期, 可以協助您確實的進行資料庫的 CRUD. 而 Hibernate 的物件狀態可分為三種, 如下所示:

1. Transient
該狀態只是簡單的 Value Object, 跟資料庫的記錄沒有任何關聯, 也就是該物件的主鍵(id)不對應任何一筆資料.
例如:

User user = new User();
user.setName("first name");
2. Persistent
當一般的物件納入 Hibernate 的容器管理時, Transient 狀態的物件會轉成 Persistent Object, 直到 Session 物件過期, 或呼叫 session.close().
舉個簡單的例子:

session.beginTransaction();

Person aPerson = (Person) session.load(Person.class, personId);
aPerson.setFirstname("Foo");

session.getTransaction().commit();
session.close();
物件在執行 session.load()(或 get,save)之後, 資料會封裝成物件,
並交給 Hibernate 的容器管理, 此時的狀態也就是 Persistence Object.

以上例, 雖然沒有執行 session.update(), 但是 aPerson 物件從 session.load() 取出後, 已進入 Hibernate的管理機制, 表示跟資料庫的某資筆料相關連了. 因此修改 aPerson.setFirstname(),
再執行 session.getTransaction().commit() 之後, 會修改對應的那一筆資料內容.

3. Detached
若某個物件 Persistence Object 的 session 範圍已經失效, 就進入 Detached 狀態, 與 Value Object 的不同是, Detached 狀態可以再次的關連到 session 的實體.
例如:
Person aPerson = new Person();
aPerson.setFirstname("Foo");
...
session.beginTransaction();
session.save(aPerson);
//Persistence.
...
session.getTransaction().commit();
session.close();
//aPerson物件已變成 Detached 狀態
若此時重起一個 session 實例, 再將 aPersion 封裝進Hibernate的管理容器,
則該物件又會進入 Persistence 的狀態.