2007年4月24日 星期二

Synchronized造成效能低落嗎? 試試樂觀同步化吧!!

前陣子調效一個Web Application的效能,在貢獻了許多咖啡因到腦袋後,我找到了原因-- Synchronized

是的,在某支Servlet中的某函式使用了Synchronized關鍵字,造成系統在此排隊執行,因而效能低落,於是立刻招開小組會議,但幾經測試及討論後,卻發現此區塊一定得同步化,否則發生Race Condition時,會造成資料錯誤…

我效能調效的工作遇到了瓶頸…Orz

...
...


幾經思量,我決定將此函式改造成使用樂觀同步化技術!!

什麼是樂觀同步化?
使用Java寫作的程式,通常在可能會發生同時存取共用資料的情形時,使用Synchronized關鍵字,以確保同一時間只會有一個Thread執行這段程式,但這種作法就是所謂的悲觀同步化,問題解決了,但代價可能是會有多個Thread在排隊等候,造成效能低落。

樂觀同步化則樂觀的認為同時存取的情形很少發生,萬一發生的話,再補救就好了。

說明看起來很簡單,但使用較早期版本的JDK要實作可不簡單,幸運的是,JDK從1.5之後引進了Atomic類別庫,讓你可以方便的實作樂觀同步化。

Race Condition
在開始之前,先來看看什麼是「同時存取共用資料」,這個問題學名叫「Race Condition」,就是指共用的資料,幾乎同時間被許多不同的Thread存取,造成資料不一致的問題。

讓我們來看個例子:
有個定貨系統,使用了三個執行緒在處理同一個產品的訂單,訂單在產生之前必須先更新產品的資料。

產品物件的定義如下:




public class MyProduct {
private String productId;
private double price;
private String description;

public MyProduct(String productId, double price, String description) {
this.productId = productId;
this.price = price;
this.description = description;
}

public String toString() {
return productId +
"(NT$ " + price +
", " + description + ")";
}

public boolean equals(Object anObject) {
MyProduct newProduct = (MyProduct) anObject;
return productId.equals(newProduct.getProductId()) &&
price == newProduct.getPrice() &&
description.equals(newProduct.getDescription());
}

public void createOrderTO() {
// 用一段sleep來模擬產生訂單需要執行的時間
try {
long rnd = (long)(java.lang.Math.random() * 100);
Thread.sleep(rnd);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}

public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
}



處理訂單的執行緒程式如下:

public class ProductProcessor implements Runnable {

public ProductProcessor(int id) {
processorId = id;
}

private int processorId;

public int getProcessorId() {
return processorId;
}

private MyProduct product;
public void setProduct(MyProduct product) {
this.product = product;
}

/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
// 每個thread對此產品產生5個訂單
for (int i = 0; i < 5; i++) {
// 更新產品資料
product.setPrice(processorId * 10);
product.setDescription(
"modified by processor: " + processorId
);

// 跟據product產生訂單物件(Order Transfer Object)
product.createOrderTO();

// 列印訂單
System.out.println("Processor " + processorId +
" Create Order for: " + product.toString());
}

}

public static void main(String args[]) {
// 產生一個product
MyProduct prod = new MyProduct("my_product_01", 9999999, "this is my product!");

ProductProcessor p1 = new ProductProcessor(1);
ProductProcessor p2 = new ProductProcessor(2);
ProductProcessor p3 = new ProductProcessor(3);

// 讓三個thread都一起存取同一個Product
p1.setProduct(prod);
p2.setProduct(prod);
p3.setProduct(prod);

Thread t1 = new Thread(p1);
Thread t2 = new Thread(p2);
Thread t3 = new Thread(p3);

t1.start();
t2.start();
t3.start();
}


}


執行結果如下:


Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 2 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 3 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 1 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 3 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 2 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 2 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)



從結果可以發現許多衝突,例如第2行Processor 2產生的訂單,其說明卻註記說是Processor 3更新的。
這種衝突的情現就叫Race Condition。

悲觀同步化
要如何解決Race condition呢? 最簡單的方法就是使用Synchronized區塊了:
在有可能發生Race Condition的地方,使用Synchronize {}把這些程式碼都包起來,就可以解決問題,像下面這樣:

// use synchronized block to solve race condition
synchronized (product) {
// 更新產品資料
product.setPrice(processorId * 10);
product.setDescription(
"modified by processor: " + processorId
);

// 跟據product產生訂單物件(Order Transfer Object)
product.createOrderTO();

// 列印訂單
System.out.println("Processor " + processorId +
" Create Order for: " + product.toString());
}


再執行一次,我們可以得到完美的結果:


Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
...
...



這個技巧就是悲觀同步化,Java保證synchronized區塊同時只會有一個執行緒進入存取product物件,因此Race Condition不會再發生,但有時這種解法會造成效能低落。


樂觀同步化
在大多數情況下,其實Race Condition是很少發生的;當衝突很少發生,卻使用悲觀管控,就好像是「交通部長要求交通警察限制同一時間只能有一台車上高速公路一樣」,可以想見交流道口會塞的又臭又長。

所以樂觀同步化就派上用場了,基本原理很簡單:「如果發生衝突,就重做」!

JDK 1.5之後提供了AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference…等類別讓你可以不使用lock卻能解決Race Condition。

這些類別的特色是裡面的方法都是Atomic的,也就是所有的方法都可以視作單一的一種操作,不會受其它Thread的影響。例如:

AtomicInteger ival = new AtomicInteger(1);
ival.set(5); // 這個操作是Atomic的

// 如果ival==5的話就把ival的值改成10 (這個操作也是Atomic的)
ival.compareAndSet(5, 10);


(注:你可能覺得ival.set(5)和宣告成一般的int然後執行ival=5;有什麼不同,但其實用一般的int,其操作還是沒有Atomic,這遷涉到byte code的內容和register的載入方式,以後有機會再聊聊。)


使用AtomicReference
思考一下前述發生問題的程式,如果以下三個步驟:

// 更新產品資料
...
// 跟據product產生訂單物件(Order Transfer Object)
...
// 列印訂單
...


可以結合成一個Atomic的動作,那不就不需要再使用synchronized關鍵字了嗎?


問題是,要怎麼做呢? 我們可以使用AtomicReference將MyProduct包裝一下,像這樣:


import java.util.concurrent.atomic.AtomicReference;

public class MyAtomicProduct{
private AtomicReference product;

public MyAtomicProduct(String productId, double price, String description) {
product = new AtomicReference
( new MyProduct(productId, price, description) );
}

public void updateProductAndMakeOrder(int processorId,
double price,
String description) {
MyProduct origVal, newVal;
String origProductId;

// 這是一個無窮迴圈,會一直重做到沒有發生Race Condition為止
while (true) {
origVal = product.get();
origProductId = origVal.getProductId();

// 記住使用AtomicReference在設定值的時候一定要指派一個新的
newVal = new MyProduct(origProductId, price, description);

// 跟據product產生訂單物件(Order Transfer Object)
newVal.createOrderTO();

// compareAndSet會確保product的值和原來(origVal)一樣時
// ,才會將product的值更新,否則就不做事且傳回false
// (如果不一樣表示過程中有其他的thread進來,也就是Race Condition
// 當發生Race Condition時,迴圈就會重做)
if (product.compareAndSet(origVal, newVal)) {
System.out.println("Processor " + processorId +
" Create Order for: " + newVal.toString());
break; // 產生的訂單是正確的,所以結束迴圈
} else {
// Debug用,在此特別將發生Race Condition而重做的情形印出來
System.out.println("** [Redo] because of Race Condition! **");
}

}
}
}



然後把void main中產生product的那行改成如下

MyAtomicProduct prod = new MyAtomicProduct("my_product_01", 9999999, "this is my product!");



執行結果如下:


Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 3 Create Order for: my_product_01(NT$ 30.0, modified by processor: 3)
** [Redo] because of Race Condition! **
** [Redo] because of Race Condition! **
Processor 1 Create Order for: my_product_01(NT$ 10.0, modified by processor: 1)
** [Redo] because of Race Condition! **
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)
Processor 2 Create Order for: my_product_01(NT$ 20.0, modified by processor: 2)



可以發現當Race Condition發生時,迴圈會重做以確保資料正確,在衝突不常發生的情形下,使用樂觀同步化是有可能解決效能低落的狀況的,不過如果Race Condition常常發生,則值不值得就見人見智了,更何況實作樂觀同步化會使你的系統更複雜了。

2007年4月21日 星期六

使用MetaProgramming 動態定義類別

使用MetaProgramming寫程式的確和以往有很多不一樣的地方,例如,我可以在程式中讀取資料庫中的表格名稱和欄位名稱,然後根據表格的Schema動態產生一個類別定義,並且利用此類別產生Instance,像下面這樣:(注:為求簡化範例,資料庫存取的部份用模擬的)


class MetaProgrammingTest

# class method for creating a new class
def self.create_class(class_name, attr_names)
# use class name to create a class
klass = Object.const_set(class_name,Class.new)

klass.class_eval do
attr_accessor *attr_names

define_method(:initialize) do |*values|
attr_names.each_with_index do |name,i|
instance_variable_set("@"+name, values[i])
end
end

define_method(:to_s) do
str = "[#{self.class}:"
attr_names.each {|name| str << " #{name}=#{self.send(name)}" }
str + "]"
end

end

# return the class
klass
end

end


class TestDBSchemaReader
def self.getTableNameFromDB
# 下列程式可以改成從資料庫取得Table Name
return "Person"
end

def self.getAttributeNamesFromDB(table_name)
# 下列程式可以改成從資料庫取得Table的所有欄位名稱
return ["name", "age"]
end
end

table_name = TestDBSchemaReader.getTableNameFromDB

attr_names = TestDBSchemaReader.getAttributeNamesFromDB(table_name)

# 動態產生一個新的類別
myCls = MetaProgrammingTest.create_class(table_name, attr_names)

# 動態產生一個新的類別的Instance
vo = myCls.new("richard", 15)

puts "class name=" + vo.class.name
puts "instance: vo.name = " + vo.name + ", vo.age=" + vo.age.to_s



執行結果像這樣

>ruby meta_programming_test.rb
class name=Person
instance: vo.name = richard, vo.age=15
>Exit code: 0


類別"Person"被動態的定義了,而且還使用此類別建立了Instance,蠻容易的,不是嗎?

更厲害的是,動態定義的類別還可以被繼承,像下列這段,我在程式後方又加上了一個GoodPerson類別,繼承自動態定義出來的Person類別:

class MetaProgrammingTest

# class method for creating a new class
def self.create_class(class_name, attr_names)




#更神奇的是,動態定義的類別還可以被繼承
class GoodPerson < Person
def hello
print "我是#{@name},我今年#{@age},我被發好人卡了!\n"
end
end

gp = GoodPerson.new("steve", 31)


執行結果

>ruby meta_programming_test.rb
class name=Person
instance: vo.name = richard, vo.age=15
我是steve,我今年31,我被發好人卡了!
>Exit code: 0


更進一步的,我還可以在這個GoodPerson類別中,動態加入新method:

...
...

procBlock = proc { |x| puts "the " + x + "is a nerd!" }

GoodPerson.class_eval do
define_method(:showNerd, procBlock)
end

gp.showNerd "jonny"



執行結果

>ruby meta_programming_test.rb
class name=Person
instance: vo.name = richard, vo.age=15
我是steve,我今年31,我被發好人卡了!
the jonnyis a nerd!
>Exit code: 0

2007年4月19日 星期四

結合Meta Programming的OR Mapping - Active Record

像Ruby這種動態語言有個一般靜態語言較難達成的功能--Meta Programming。

它並不是什麼新技術,早在70年代的SmallTalk就有內建這玩意了,不過它的好處倒是在Ruby on Rails出現後,才紅了起來。

什麼是Meta Programming?
簡單說就是類別的定義可在執行期自由自在的修改…
可以在執行期對動態現存的類別新增、修改成員函式,也可以新增、修改成員變數...
舉例來說:
Date是一個Ruby提供的類別,而你需要在程式中判斷所指定的日期是不是假日,在一般靜態語言中你可能會新增一個全域函式,或是在你自己的Helper類別中加入一個函式,如下:



boolean isHoliday(Date theDay);



然後使用時是像這樣:

Date someDay = ...;
...
...
if (isHoliday(someDay)) {
....
}
...


嗯…較不直覺,不是嗎?
看看下列程式,如果可以這樣用呢?

if (someDay.isHoliday?()) {
...
}


是不是方便多了!!

如果你還不太了解的話,可以參考一下我之前寫的這篇Meta Programming簡介


當然Meta Programming的作用不止於此,它可以做出許多令人驚艷的應用,在Ruby on Rails中的Active Record就是使用此技術的殺手級應用之一。

Active Record是結合Meta Programming的OR Mapping框架,就像Java中的Hibernate一樣,但比它更好,在如此肯定的說他好之前,我們來回想一下Hibernate:

在資料庫中有一個Table: User,內容是使用者的資料,Schema如下:

create table products (
id int not null,
name varchar(100) not null,
age number,
primary key (id)
);


我希望使用物件的方式存取使用者的資料,使用Java+Hibernate時,我有以下幾個步驟:
1. 建立User.hbm.xml,做為OR Mapping的定義:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

<class name="richard.User" table="userinfo">

<id name="id" column="id" type="java.lang.Integer">
<generator class="assigned"/>
</id>

<property name="name" column="name" type="java.lang.String"/>

<property name="age" column="age" type="java.lang.Integer"/>

</class>

</hibernate-mapping>


2. 為物件中的欄位建立相對應的Getter和Setter:

package richard;

public class User {
private Integer id;
private String name;
private Integer age;

// 必須要有一個預設的建構方法
// 以使得Hibernate可以使用Constructor.newInstance()建立物件
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}



3. 在Client端程式中如下列方法使用:

// Configuration 負責管理 Hibernate 配置訊息
Configuration config = new Configuration().configure();
// 根據 config 建立 SessionFactory
// SessionFactory 將用於建立 Session
SessionFactory sessionFactory = config.buildSessionFactory();

// 新增持久化的物件
User user = new User();
user.setId(new Integer(4));
user.setName("caterpillar");
user.setAge(new Integer(30));

// 開啟Session,相當於開啟JDBC的Connection
Session session = sessionFactory.openSession();
// Transaction表示一組會話操作
Transaction tx= session.beginTransaction();
// 將物件映射至資料庫表格中儲存
session.save(user);

// delete
User user2 = new User();
session.load(user2, 2);
session.delete(user2);

tx.commit();

session.close();
sessionFactory.close();




嗯!Hibernate還是不失為在Java上一個相當優良的OR Mapping工具,使用起來是很簡單易懂的,但還是有一些不方便的地方:

1. 必須定義XML檔,當Table很多,欄位也愈來愈多時會很難維護。
2. 必須為物件依照要操作的欄位定義Getter和Setter方法,同樣的,資料愈多愈難管。
3. 當物件之間有關聯時,會更難維護。
4. 每次設計有變更時,一連串要更新的地方都讓我想要離職。

這些都是目前OR Mapping難解的部分,很幸運的,如果你使用Ruby,現在就有個Active Record解決了這些問題:

Active Record是怎麼做的?

1. 你需要一個User類別,長這樣:

class User < ActiveRecord::Base
end


2. 使用方法如下:

# 可以用Active Record 新增一筆資料
user1 = User.new
user1.name="Richard"
user1.age=18
user1.save

# 也可以用Active Record來找一筆資料
user2 = User.find 1
print user2.name


我並沒有在User類別中定義屬性name, age,但就是直接可以用,我也沒有在User類別中定義類別方法(static method),但User.find()這個方法是work的。

很神奇嗎?
Active Record透過Meta Programming幫你把這些都加上去了,ORMapping變輕鬆了!!

更好用的AES加密,4096 bit 安全性

最近在翻箱倒櫃整理以前寫過的一些程式,突然找到以前用Java寫的AES對稱式加密實作,而且我還把它寫成可以支援到4096 bit的加密強度,記得之前自己寫AES加密實作時,Java還沒有內建AES實作,所以這個Library用起來還是挺Happy的。(好吧…我承認我在自爽....Orz)

後來Java在1.4的時候已經提供了AES的實作,不過因為美國規定的加密技術輸出規則中所述,有些國家是不允許輸出高強度的加密技術的,所以Java預設只支援128 bit的加密強度,如果要用到192 bit或是256 bit時,還得去Sun原廠網站下載「JCE Unlimited Strength Jurisdiction Policy Files」。

不過沒有內含在JDK中用起來就是不方便,增加了Deploy的困擾,所以如果你有需要的話,可以到下列網址去下載Policy File:
http://java.sun.com/products/jce/index-14.html#UnlimitedDownload
注:到了JDK6預設仍然只有128bit的AES喔

什麼是AES?
寫到這突然發現我忘了介紹一下AES了,Advanced Encryption Standard (AES)-二十一世紀的一種加密演算法,美國國家技術標準局(NIST)在1998年所招攬的DES演算法繼承人,NIST希望在二十世紀結束之前,能夠預備好一個新的加密標準,要求加密區塊的明文長度為128位元,可接受的密鑰長度為128位元、192位元和256位元(含以上)。判定的標準為安全性、效率與適應性三方面。

於是在1998年8月20日第一次AES候選者的研討會中,出現了十五個候選的演算法。
列示如下:
CAST-256, CRYPTON, DEAL, DFC, E2, FROG, Hasty Pudding Cipher(HPC) , LOKI97, MARS, Magenta, RC6, RIJNDAEL, SAFER+, SERPENT, TWOFISH。

1999/3/22-23 NIST在義大利羅馬的奎爾諾飯店召開第二次AES候選者的研討會,NIST發出問卷調查,由參加者選出五種較有可能的演算法。

最後留下來參加決賽的有: MARS、RC6、Rijndael、Serpent、Twofish。

1999/4/15 Rijndael最後勝出,就是大家目前所使用的AES演算法,Rijndael是演算法作者的名字,盡管它很難唸…盡管大家都只知道AES而沒聽過Rijndael,我們還是別忘了他是作者。

Rijndael的理論基礎是將每個byte看成一個多項式:
例如:
有一個Byte的資料是01001010
則可將此Byte寫成多項式 0*X^7 + 1*X^6 + 0*X^5 + 0*X^4 + 1*X^3 + 0*X^0 + 1*X^1 + 0*X^0

然後利用迦羅瓦域定理經過一段運算而得,我並不打算在此說明理論細節,如果你有興趣的話,可以參考FIPS PUB 197,這篇Paper是AES演算法的詳細內容(在看之前惡補一下離散數學裡的迦羅瓦域定理有助於了解原理)

如何使用
我的AES Class很簡單使用,你可以參考以下範例:



// 指定加密強度,最高可到4096 bit
AESAlgorithm alg = new AESAlgorithm(AESAlgorithm.KEY_SIZE_4096);

// 產生Key
byte[] bytesKey = alg.createKey();
int[] wordsKeyExpansion = alg.createKeyExpansion(bytesKey);

String strMessage = "This is just an AES範例!!";

byte[] bytesMessage = strMessage.getBytes(); // 將字串轉成Byte Array
byte[] bytesEncrypted = alg.cipher(bytesMessage, wordsKeyExpansion); // 加密
byte[] bytesDecrypted = alg.invCipher(bytesEncrypted, wordsKeyExpansion); // 解密

System.out.println("decrypted= " + new String(bytesDecrypted) );


那裡可以下載?
最後,我的AES Library原始碼在這,供您參考!

2007年4月14日 星期六

純種Java與JavaScript的結合

JavaScript是個廣泛應用的描述性語言,除了製作動態網頁外,常常可以看到各種工具使用各種Script來讓使用者有更彈性的設定,例如:


  • 報表工具(如JasperReport)可以使用用Script自定欄位的顯示格式。

  • 各種Workflow、BPM的工具也可以使用Script訂定各站過站時的邏輯。

  • 在ERP中可以使用Script來設定成本計算公式。

  • 在MES中可以使用Script來設定良率計算公式。




將Script整合進應用程式所能達到的彈性空間的確十分強大,如果能在我們開發的系統也提供Scripting的能力,除了強化擴充性之外,整個系統的質感也是提升不少…爽度up!up!

但以往要達成這目標,有個難處,就是解釋並執行Script用的Interpreting Engine那裡來,自己寫顯然是太累了…。

Java 6這回帶來了好消息:Scripting API,在Java系統中結合Scripting再也不難了,你只需要import javax.script.*;

不可免俗的,還是讓我們來Hello一下吧:


import javax.script.*;

class HelloScript {
public static void main(String args[])
{
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
}
}


是的…我在Java程式中使用JavaScript印出了Hello, world!
只需要幾行而已,取得ScriptEngine,然後將要執行的Script傳入eval()中就好了。

不過,通常要讓Script在系統中派上用場,你需要更完全的整合:在Script中要能使用Java物件、在Script中要能使用Java API、Script的執行結果也要可以回報給Java、在Java中也要可以呼叫Script的method。

接著,讓我們來一一擊破…

1. 在Script中使用Java物件

// 先在Java中產生一個ArrayList
List namesList = new ArrayList();
namesList.add("Jill");
namesList.add("Bob");
namesList.add("Laureen");
namesList.add("Ed");

// 取得Script Engine
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");

// 把ArrayList物件透過Engine傳進去,取名叫namesListFromJava
jsEngine.put("namesListFromJava", namesList);
System.out.println("==== Executing in script environment... ====");
try {
// 執行Script,注意,此時已經可以在Script中使用namesListFromJava這個
// 由Java傳入的物件了
jsEngine.eval("var x;" +
"var names = namesListFromJava.toArray();" +
"for(x in names) {" +
" println(names[x]);" +
"}" +
"namesListFromJava.add(\"Dana\");");
} catch (ScriptException ex) {
ex.printStackTrace();
}



程式執行結果如下

==== Executing in script environment... ====
Jill
Bob
Laureen
Ed



2. 使用Java呼叫JavaScript Method,並取得其回傳值

// getting engine
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");

// 必須將Engine轉型成Invocable
Invocable invocableEngine = (Invocable)jsEngine;

try {
jsEngine.eval("function sumTwoValue(var1,var2) {" +
" return var1 + var2;" +
"}");


// 在Java中呼叫Script Method並印出回傳值
System.out.println("return value from javascript is : " +
invocableEngine.invokeFunction("sumTwoValue", 3, 4));

} catch (ScriptException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
}




執行結果如下

return value from javascript is : 7.0


3. 在Script中使用Java API

// getting engine
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");

try {
jsEngine.eval(
"importPackage(javax.swing);" +
"var optionPane = " +
" JOptionPane.showMessageDialog(null, 'Hello, world!');");
} catch (ScriptException ex) {
ex.printStackTrace();
}



程式執行結果如下:


可以發現,我輕鬆的在JavaScript中叫出了一個Swing對話盒,向各位說「Hello, world!」。

JDK6 野馬與Script的整合,的確讓牠更狂野奔放了:D

2007年4月12日 星期四

爪哇人看Ruby(4) -- Duck Typing

以前上心理學時有聽過「刻版印象」一詞,它的定義是這樣的:

刻版印象為一種心理的機制,與類別的形成有關,這種機制協助人們運作由所處環境所獲得的資料;刻版印象如同「腦海中的圖畫」(pictures in our heads)(Lippmann, 1922)。

嗯…講白一點就是「貼標籤」,例如:

看到外籍新娘就覺得是花錢買親,但他們不可以是自由戀愛結婚的嗎?
看到開雙B的年輕人就覺得他是家裡有錢,但搞不好人家是年輕有為呢?

....

所以,我得承認,我也有刻版印象--聽到Scripting Language 就覺得不是物件導向、型別檢查不嚴謹。

但事實上,Ruby是很純的物件導向語言,並且,它的Typing也不是Weak Typing,而是Strong Typing,來看看以下的程式片段執行結果:


ivalue = 10
puts ivalue.class # ivalue的型別是Fixnum

puts "hello " + ivalue
# 此行會發生Error
# TypeError: can't convert Fixnum into String
# from (irb):3:in `+'
# from (irb):3


看看最後列印 「"hello " + ivalue」這段,因為ivalue的型別不是字串,所以發生錯誤,和一般的Scripting Language是不一樣的。

再看看下列這段:

puts "hello " + ivalue2
# 發生錯誤
# NameError: undefined local variable or method `ivalue2' for main:Object
# from (irb):2

我們可以發現,未經宣告的變數也是不能直接使用的。


講精確一點,Ruby是Dynamic Typing且Strong Typing的語言,而Java是Static Typing且Strong Typing的語言。

所以,在Ruby中,直譯器幫你在變數宣告時,依據初始值的型態來決定變數的型態,並且在你使用變數時,是「依據實際上Instance有沒有該功能決定能不能用」。

這個說法很玄,但這就是Duck Typing的意函 -- 「如果它走起路來像鴨子,叫起來也像鴨子,那麼它一定是鴨子」。

Ruby在意的不是物件的身分是什麼,而在於他能不能做什麼

來看個例子

在Ruby中,我有Order和People兩個類別,而我需要這兩種物件都能轉成xml,所以我在這兩個類別中都加入了to_xml()方法


然後看一下程式的實作,當類別定義完成後,我使用一個陣列,裡頭存了二個Order物件和一個People物件,接著使用一個迴圈將整個陣列裡的物件都轉成XML

class Order
def initialize(orderNO, description)
@orderNO = orderNO
@description = description
end

def to_xml
return "" + @orderNO +
"
" + @description + "
"
end
end

class People
def initialize(name, age)
@name = name
@age = age
end

def to_xml
return "" + @name +
"
" + @age.to_s + "
"
end
end

objs = [ Order.new("ord_0001", "test order 1"),
People.new("Richard", 30),
Order.new("ord_0002", "test order 2") ]

# 將各物件轉成xml
objs.each do |o|
puts o.to_xml
end



而類似的事情若要在Java中實作,則必須先定義好一個抽象類別或Interface,將to_xml()函式先定義好,再讓Order及People兩個class繼承並實作之,如下圖:


程式碼如下:

in XMLDoc.java

public abstract class XMLDoc {
public abstract String to_xml();
}


in Order.java
public class Order extends XMLDoc{
private String orderNO;
private String description;

public Order(String orderNO, String description) {
this.orderNO = orderNO;
this.description = description;
}

public String to_xml() {
return "" + this.orderNO +
"
" + this.description + "
";
}
}


in People.java
public class People extends XMLDoc{
private String name;
private int age;

public People(String name, int age) {
this.name = name;
this.age = age;
}

public String to_xml() {
return "" + this.name +
"
" + this.age + "
";
}
}


in XMLWriter.java
import java.util.*;

class XMLWriter {
public static void main(String args[])
{
List objs = new ArrayList();
objs.add(new Order("ord_0001", "Test Order 1"));
objs.add(new People("Richard", 30));
objs.add(new Order("ord_0002", "Test Order 2"));

for (int i = 0; i < objs.size(); i++) {
XMLDoc o = (XMLDoc)objs.get(i);
System.out.println(o.to_xml() + "\n");
}
}
}


有人覺得Java的做法比較嚴謹,有人覺得Ruby的做法更簡潔有彈性,不管如何,這是語言本身的特色。

2007年4月10日 星期二

寫Java Script的好工具--Aptana

JavaScript一直是網頁開發者又愛又恨的玩意,你能用它做出令人驚艷的網頁效果,但JavaScript過多的網頁卻又複雜而難以維護,這個痛苦現在可以減緩,只需要一個好用的工具。

Aptana是一套以Eclipse為核心的IDE,開發對像是Html和JavaScript,並且內建許多AJAX的Library,並且讓你能以更直覺的方式對JavaScript進行Debug,當然,設Break Point和Watch是最基礎的了,如下圖:
按下小蟲圖示開始Debug



Debug過程中可以自在的設中斷點和監看變數值




而且還支援Auto Completion,例如下例程式碼,我寫了一個自定函式test2(),則test2這個函式名稱不但可以打到一半自動完成,還可以再輸入到左括號時自動顯示出該傳入那些參數。



它和Eclipse緊密整合,所以若你本來就是使用Eclipse來開發你的Java Web Application的話,不用再害怕網頁中的JavaScript難以偵錯及維護了。

注:有需要的同好可以自行到Aptana 官網下載。

2007年4月4日 星期三

爪哇人的Ruby(3) -- Meta Programming簡介

Meta Programming是什麼呢?讓我用白話一點的方式來說明:
就是可以動態更改Class的設定,可以新增成員變數,也可以新增方法,看一下以下這段程式:



str1 = "hello meta programming"

class String
  def to_html
    return "<p><b>" + self + "</b></p>\n"
  end
end

str2 = "author: richard"

# 輸出 <p><b>hello meta programming</b></p>
print str1.to_html

# 輸出 <p><b>author: richard</b></p>
print str2.to_html



有發現什麼神奇之處嗎?
是的,我在這簡短的程式中,加了一個新的to_html()的新方法到String類別定義中,所以之後所有的字串都能使用to_html()這個成員方法了。

這個功能除了可以讓你輕鬆的完成所謂的AOP之外,還可以做到一些Java做不到的事,回想一下使用Hibernate、JPA或是Entity Bean的步驟吧!
1. 在資料庫中建好Table及欄位
2. 在XML檔中設定物件的Getter、Setter和實際欄位的對應…(現在可以改用annotation,但還是不夠好)
3. 寫JavaBean,必須包含各欄位的Getter和Setter

一開始辛苦一次還可以,但如果每次的Schema更動都要把各種檔案掃過一次呢?那可就累了,不是嗎?

如果Java中的Hibernate或是JPA,可以在程式讀取資料庫表格Schema後,自動將Getter和Setter都產生,然後不再需要XML設定你複雜的O-R Mapping,那會是多麼美好呀!

這樣好用的工具已經有了,可惜是出現在Ruby上,希望以後Java也會有。

2007年4月3日 星期二

爪哇人的Ruby(2)

Ruby的OO成分比Java更純,怎講呢?

Java的世界中,所有事物都是物件,只差了一點:Primitive Types(原生型態)

看看以下的程式吧:


Integer money = new Integer(10);
int moreMoney = 99999;

List listOfSaving = new ArrayList();

listOfSaving.add(money); // 成功
listOfSaving.add(moreMoney); // 失敗,因為moreMoney是Primitive Type不是物件


其實我個人也很習慣使用Primitive Type,原因有二:1. 符合C++使用者的習慣。 2. 增加一點點效能。

不過也付出一些代價:1. 程式碼變的比較醜,常常要使用到轉型。 2. JDK 5為解決程式碼醜的問題,引入了Auto Boxing機制,卻也帶來了更多麻煩(以後有空再來聊聊)。

Ruby中沒有原生型態這回事,任何在Ruby中出現的事物都是物件,事情變的比較單純了。

Ruby還提供了兩種在Java中沒有的OO機制:Meta Programming和Mix-In,這也是Ruby具有高生產力的密訣之一,這個就容我之後再講了。