2015/01/27

Mac OS X 裡安裝 Docker

最近 Docker 成了當紅炸子雞,公司內部也將開發的測試環境,開始逐漸移轉到 Docker 上,在此介紹一下 Mac OS X 環境安裝 Docker 的程序

Step 1.
首先安裝 Vitual box https://www.virtualbox.org/wiki/Downloads
在下載的網頁裡面可以選擇下載 OS X hosts ,目前是 4.3.20 版
下載後請直接安裝。

Step 2.
用Homebrew 來安裝 docker

$ brew install docker

==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/docker-1.4.1.yosemite.bottle.tar.gz
Already downloaded: /Library/Caches/Homebrew/docker-1.4.1.yosemite.bottle.tar.gz
==> Pouring docker-1.4.1.yosemite.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completion has been installed to:
  /usr/local/share/zsh/site-functions


Step 3.
安裝這次的重點 boot2docker,boot2docker 會用 Virtualbox 建立 Linux VM,在 Linux VM 上開啟 docker daemon 後,就可以直接對 VM 裡的 docker 下控制命令。
我們來繼續安裝 boot2docker

$ brew install boot2docker

==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/boot2docker-1.4.1.yosemite.bottle.tar.gz
######################################################################## 100.0%
==> Pouring boot2docker-1.4.1.yosemite.bottle.tar.gz
==> Caveats
To have launchd start boot2docker at login:
    ln -sfv /usr/local/opt/boot2docker/*.plist ~/Library/LaunchAgents
Then to load boot2docker now:
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.boot2docker.plist

Step 4.
使用 boot2docker init 指令,建立起 VM 的環境
    
$ boot2docker init

Latest release for boot2docker/boot2docker is v1.4.1
Downloading boot2docker ISO image...
Success: downloaded https://github.com/boot2docker/boot2docker/releases/download/v1.4.1/boot2docker.iso
    to /Users/martin/.boot2docker/boot2docker.iso
Generating public/private rsa key pair.
Your identification has been saved in /Users/martin/.ssh/id_boot2docker.
Your public key has been saved in /Users/martin/.ssh/id_boot2docker.pub.
The key fingerprint is:
4e:1d:89:54:78:95:e7:f2:10:8c:11:90:a0:02:f1:a6 martin@Martin-MacBook-Pro-2.local
The key's randomart image is:
+--[ RSA 2048]----+
|o.    ..o=+*..   |
| o   . .o.o.+ .  |
|  + .   ..o  +   |
| o .     . .o .  |
|E       S .  +   |
|       o      .  |
|        .        |
|                 |
|                 |
+-----------------+

Step 5.
啟動 boot2docker

$ boot2docker start

Waiting for VM and Docker daemon to start...
..........................................oooooooooooooooooooooooooooooooooo
Started.
Writing /Users/martin/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/martin/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/martin/.boot2docker/certs/boot2docker-vm/key.pem

To connect the Docker client to the Docker daemon, please set:
    export DOCKER_HOST=tcp://192.168.59.103:2376
    export DOCKER_CERT_PATH=/Users/martin/.boot2docker/certs/boot2docker-vm
    export DOCKER_TLS_VERIFY=1

Step 6.
記得一一使用 export 來將這些環境變數設定在Mac OS X 裡
PS: 這裡的環境變數請參考 Step 5 啟動 boot2docker 後的顯示說明

$export DOCKER_HOST=tcp://192.168.59.103:2376
$export DOCKER_CERT_PATH=/Users/martin/.boot2docker/certs/boot2docker-vm
$export DOCKER_TLS_VERIFY=1

Step 7.
使用 docker version 查看一下是否已經安裝成功並且成功啟用,如果有出現以下訊息,那就確認安裝沒問題。
    
$ docker version

Client version: 1.4.1
Client API version: 1.16
Go version (client): go1.4
Git commit (client): 5bc2ff8
OS/Arch (client): darwin/amd64
Server version: 1.4.1
Server API version: 1.16
Go version (server): go1.3.3
Git commit (server): 5bc2ff8

您現在已經可以開始使用 docker 指令,下載 Image ,操作docker container 。

讓iOS應用程式支援64-bit處理器的二三事

前言

近年來,新款行動裝置的處理器也逐漸轉為64-bit架構;在iOS設備方面,iPhone 5S, iPhone 6, iPhone 6 Plus, iPad Air, iPad Air2, iPad mini2, iPad mini3皆已採用arm64處理器[1]。

因此,Apple官方也宣布:2015年2月1日之後,新上架的App必須要支援64-bit處理器[2];而2015年6月1日之後,舊的App若要進行更新的話,也必須要支援64-bit[3]。

一般而言,撰寫高階語言時,通常應該心力專注於商務邏輯,程式可讀性與可重複使用性...等,而不太需要關注在與硬體架構的相關的事情;因為與硬體架構相關的事情應由編譯器與開發工具來處理。使用XCode也不外乎如此,理論上只要做以下簡單的設定即可支援arm64(事實上,在XCode 6預設即是以下設定):



然而實際上,使用不同的程式語言、編譯器與開發工具, 在不同的作業系統與硬體設備上開發應用程式,都有若干不同的細節需要注意;因次,程式設計師才必須對計算機結構也有所涉略。

而關於iOS應用程式開發的部分,Apple官方也寫了一份文件給iOS應用程式開發者: 64-Bit Transition Guide for Cocoa Touch

以下則是我在網路上比較常看到被提出來討論的:

(1) NSInteger vs. int

新手iOS程式設計師很常犯下混用NSInteger與int的錯誤:
NSNutableArray* myArray;
//...
unsigned int count = [myArray count];
因為在標頭檔裡,NSInteger與NSUInteger的確是這樣被定義的:
typedef int NSInteger;
typedef unsigned int NSUInteger;
因此,在只需撰寫32-bit app的情況時,這樣的確是不會有什麼問題。
但是,在撰寫64-bit app的情況時,可就不一樣了,編譯器會告訴你:
Implicit conversion loses integer precision: 'NSUInteger' (aka 'unsigned long') to 'unsigned int' 
原因很簡單,因為在標頭檔裡,完整的定義是這樣的:
#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
說穿了,也真不是什麼高深的學問,不過就是:
NSInteger在32-bit環境下是int, 在64-bit環境下是long
如此而已。
只是在Apple強迫大家撰寫支援64-bit app之前,許多iOS程式設計師可能都不會去注意這個細節。
此外,在將NSInteger/NSUInteger格式化為字串時,以往在只有32-bit的環境下,可能會這樣寫:
NSInteger num = 1;
NSLog(@"num: %i", num);
但由於在64-bit的環境下, NSInteger/NSUInteger為long型別, 因此編譯器會提出警告;簡單的解決方式是手動將NSInteger轉型成long
NSInteger num = 1;
NSLog(@"num: %ld", (long) num);
或是使用z與t修飾子:
NSInteger num = 1;
NSLog(@"num: %zd", num);

NSUInteger num2 = 1;
NSLog(@"num2: %tu", num2);

(2) BOOL vs. bool

先看看下面這段簡單的例子,雖然正常情況下你不應該寫出這樣的程式:
BOOL b = 2;
if(b == YES) {
    NSLog(@"YES");
}
if(b == NO) {
    NSLog(@"NO");
}
你可以試著分別編譯出32-bit與64-bit的版本來執行, 結果可能出乎你意料:
32-bit版本,一個字都不會印出來;而64-bit版本, 則會印出YES
原因是32-bit環境下的BOOL是signed char,而非真正的bool型別。 熟悉C語言的朋友都知道,signed char允許存放-128~127。 而64-bit版本的BOOL是真正的bool型別,將非0值賦值給bool型別的變數,都將轉為1(true值)。
#if !defined(OBJC_HIDE_64) && TARGET_OS_IPHONE && __LP64__
typedef bool BOOL;
#else
typedef signed char BOOL; 
#endif
#if __has_feature(objc_bool)
#define YES __objc_yes
#define NO  __objc_no
#else
#define YES ((BOOL)1)
#define NO  ((BOOL)0)
#endif
簡單來說,BOOL型別的變數,應避免賦予YES與NO以外的值; 因此,對於正常使用BOOL型別的變數的程式設計師而言,其實不需要太擔心; 但若是因為知道BOOL是signed char而刻意作為他用,賦予了YES與NO以外的值的人,此時可要特別注意了;因為64-bit版本的BOOL是真正的bool型別,賦予了YES與NO以外的值,都將被視為YES

結論

以上簡單的例子,通常大多數人都不至於會犯;
但背後所隱藏的細節,卻很有可能被我們忽略,而在其他情況時犯下類似的錯誤;而這些錯誤可能難以被找出來。
因此,儘管是撰寫高階語言,對於編譯器、開發工具及硬體架構也應稍微有所了解。

Reference


Android - 如何呼叫手機中已安裝的 APP 開啟任意檔案


隨著 3G 網路與智慧型手機的普及,已經越來越多人使用網路將資料傳送到手機查看。
但到底要如何在手機中開啟像 PDF, word 等檔案呢 ?
最簡單的方式... 就是交給系統判斷!由系統判斷這個檔案可以由那些 APP 開啟。
下面就來看看如何請系統幫我們列出可開啟檔案的 APP

實作

  1. 使用 endsWith() 取得檔案副檔名
  2. 設定 intent type ( type 請參照 MIME Type 列表 )
  3. startActivity 切換到開啟檔案的 app
    ※ 注意 ! 如果手機沒有可開啟檔案的程式,則會出現 ActivityNotFoundException

try {
 String filePath = vSDCard.getCanonicalPath() + File.separator + "Download" + File.separator + vlowerFileName;
 Intent intent = new Intent( Intent.ACTION_VIEW );
 File f1 = new File(filePath);
 if(f1.exists()){
  if(vlowerFileName.endsWith("mpg") || vlowerFileName.endsWith("mp4")){
      // 影片
      intent.setDataAndType(Uri.fromFile(f1), "video/*");
  }else if( vlowerFileName.endsWith("mp3") ){
      // 音樂
      intent.setDataAndType(Uri.fromFile(f1), "audio/*");
  }else if( vlowerFileName.endsWith("bmp") || vlowerFileName.endsWith("gif") || vlowerFileName.endsWith("jpg") || vlowerFileName.endsWith("png")){
      // 影像
      intent.setDataAndType(Uri.fromFile(f1), "image/*");
  }else if(vlowerFileName.endsWith("txt")){
      // 文字檔
      intent.setDataAndType (Uri.fromFile(f1), "text/plain");
  }else if( vlowerFileName.endsWith("apk") ){
      // Android APK
      intent.setDataAndType(Uri.fromFile(f1), "application/vnd.android.package-archive");
  }else if(vlowerFileName.endsWith("pdf")){
      //pdf
      intent.setDataAndType (Uri.fromFile(f1), "application/pdf");
  }else if(vlowerFileName.endsWith("docx") || vlowerFileName.endsWith("doc")){
      //doc
      intent.setDataAndType(Uri.fromFile(f1), "application/msword");
  }else if(vlowerFileName.endsWith("xlsx") || vlowerFileName.endsWith("xls")){
      //xls
      intent.setDataAndType (Uri.fromFile(f1), "application/vnd.ms-excel");
  }else if(vlowerFileName.endsWith("pptx") || vlowerFileName.endsWith("ppt")){
      //ppt
      intent.setDataAndType (Uri.fromFile(f1), "application/vnd.ms-powerpoint");
  }else{
      // 其他
      intent.setDataAndType( Uri.fromFile(f1), "application/*" );
  }
  //切換到開啟檔案的app,注意!如果手機沒有可開啟檔案的程式,則會出現ActivityNotFoundException
  startActivity(intent);
 }else{
  Log.i("ruby", f1.getCanonicalPath() + " file isn't exists");
 }
} catch (IOException e) {
 Toast.makeText(MainActivity.this, "找不到檔案", Toast.LENGTH_SHORT).show();
} catch (ActivityNotFoundException e) {
 //手機上沒有任何應用程式可開啟此檔
 Toast.makeText(MainActivity.this, "沒有任何APP可以開啟此檔案", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
 e.printStackTrace();
 Toast.makeText(MainActivity.this, "error", Toast.LENGTH_SHORT).show();
}

References

2015/01/26

如何設定 MariaDB Cluster

MariaDB 建置備援環境有兩個選擇,Replication 或是 Cluster,以下將設定 cluster 的步驟記錄下來。

測試環境

DB1: 192.168.1.10
DB2: 192.168.1.20

目標是要建置這兩個 DB 的 cluster 環境

安裝 MariaDB galera

在兩個 DB 上分別安裝 MariaDB galera,將 MariaDB 的 respository 加到 yum 中。

vi /etc/yum.repos.d/MariaDB.repo
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/5.5/centos6-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

安裝 MariaDB cluster 版本

yum -y install MariaDB-Galera-server galera MariaDB-client MariaDB-devel

建立給 cluster 使用的 DB 帳號

mysql -u root -p

GRANT ALL PRIVILEGES ON *.* TO galerauser@localhost IDENTIFIED BY 'dbpassword';
GRANT ALL PRIVILEGES ON *.* TO 'galerauser'@'192.168.1.%' IDENTIFIED BY 'dbpassword';
flush privileges;
\q

設定 galera

將設定的 sample 複製到 MariaDB 的設定資料夾中

cp /usr/share/mysql/wsrep.cnf /etc/my.cnf.d/

修改 wsrep.cnf,調整下面列出來的設定項目,其他的設定值不需要修改。

vi /etc/my.cnf.d/wsrep.cnf

wsrep_provider=/usr/lib64/galera/libgalera_smm.so
wsrep_cluster_address="gcomm://"
wsrep_cluster_name="dbcluster"
wsrep_node_address='192.168.1.20'
wsrep_node_name='db1'
wsrep_sst_auth=galerauser:dbpassword
wsrep_sst_method=rsync

首先要注意的是 wsrep_cluster_address 這個設定值,因為我們是初始化 cluster,所以必須先填為 gcomm:// ,意思就是這個初始節點不需要依賴其他DB機器的資料,建立一個 cluster 初始節點。

利用以下指令初始化 cluster。

service mysql bootstrap

利用以下指令檢查 cluster 狀態,可注意 wsrep_ready 是否為 ON,另外就是查閱 wsrep_incoming_addresses, wsrep_cluster_conf_id, wsrep_cluster_size 這幾個欄位的資料,wsrep_cluster_size 目前應該為 1 。

mysql -e "SHOW STATUS LIKE 'wsrep_%'; " -p

檢查 mysql daemon 使用的網路 port,galera 是使用 TCP Port 4567。

netstat -anlp | grep -e 4567 -e 3306

設定第二台 DB 的 galera

依照上面的步驟編輯 wsrep.cnf,要注意的是 wsrep_cluster_address 必須為 gcomm://192.168.1.10,因為這個 DB 節點是 192.168.1.10 的備援節點。

wsrep_cluster_name 必須跟 DB1 的設定一樣,wsrep_node_name 是這個 DB 的識別,要填為 db2。

vi /etc/my.cnf.d/wsrep.cnf

wsrep_provider=/usr/lib64/galera/libgalera_smm.so
wsrep_cluster_address="gcomm://192.168.1.10"
wsrep_cluster_name="dbcluster"
wsrep_node_address='192.168.1.20'
wsrep_node_name='db2'
wsrep_sst_auth=galerauser:dbpassword
wsrep_sst_method=rsync

啟動 MariaDB

service mysql start

在兩台機器上檢查 cluster 的狀態,會發現 wsrep_cluster_size 變成 2。

mysql -e "SHOW STATUS LIKE 'wsrep_%'; " -p

回頭修改 DB1 的 wsrep_cluster_address

一開始初始化 cluster 時,我們是使用這樣的設定。

wsrep_cluster_address="gcomm://"

但為了讓 DB1 離線之後,也能從 DB2 將資料同步過來,我們必須調整設定為

wsrep_cluster_address="gcomm://192.168.1.20"

調整後將 MariaDB 重新啟動

service mysql restart

auto_increment 欄位在 cluster 環境要注意的狀況

通常我們會在 table 裡面使用 auto_increment 欄位,作為 DB table 的識別欄位。

create table mytable
(
   mytableseq   int(12) not null auto_increment comment '流水序號',
   primary key (mytableseq)
);

在 DB 單機的狀況下,我們預期看到 mytableseq 欄位的資料,會是 1, 2, 3, 4 ... 這樣按照順序,一個 record 增加 1 的資料。

而當 DB 設定為 cluster 時,如果這個 cluster 環境裡面只有一台機器,或是兩個 DB 節點中,有一個因故障而離線的時候,也就是 wsrep_cluster_size 為 1 的時候,mytableseq 還是一樣會以一個 record 增加 1 的狀況產生出來。

而當 DB 設定為 cluster,而且 wsrep_cluster_size 為 2 的時候,DB1 的 mytableseq 就會變成 1, 3, 5, 7 這樣,而 DB2 會是 2, 4, 6, 8。

以一個實例來看結果,如果先在 DB1 insert 兩筆資料,然後到 DB2 insert 兩筆資料,再回到 DB1 insert 兩筆資料,這時候,我們在 table 裡面看到的結果會是

1    (DB1)
3    (DB1)
4    (DB2)
6    (DB2)
7    (DB1)
9    (DB1)

參考資料
Auto increments in Galera

DB cluster 裡面有三個節點

在初始化 DB cluster 之後,分別在 DB1, DB2, DB3 給予不同的 wsrep_cluster_address 設定,每一台 DB 可根據另外兩個 DB 的資料進行資料同步作業,這時候當然 auto_increment 欄位就會變成,一次增加 3。

DB1(192.168.1.10)

wsrep_cluster_address="gcomm://192.168.1.20,192.168.1.30"


DB2(192.168.1.20)

wsrep_cluster_address="gcomm://192.168.1.10,192.168.1.30"


DB3(192.168.1.30)
wsrep_cluster_address="gcomm://192.168.1.10,192.168.1.20"

2015/01/19

如何在 Java 中計算出 2 + 1 = 4

My favourite Java puzzler 2 + 1 = 4 提供了一個很短的 Java 程式,可以在做整數計算 2+1 的時候,得到結果為 4 而不是 3。

文章是用問答題的方式問的,但我們資質駑鈍,偷懶直接看答案:

import java.lang.reflect.*;

public class Test {
    public static void main(String... args) throws Exception {
        Integer a = 2;
        Field valField = a.getClass().getDeclaredField("value");
        valField.setAccessible(true);
        valField.setInt(a, 3);

        Integer b = 2;
        Integer c = 1;

        System.out.println("b+c : " + (b + c)); // b+c : 4
    }
}

在程式的後半段,我們可看到 b + c 應該是 2 + 1 ,可是執行的結果卻得到 4 ,很明顯是前面那一半的部份,造成這樣的結果。

a.getClass().getDeclaredField

這一行是利用 Java 的 Reflection API 來取得 Integer 這個 class 的一個 private 欄位 value。

Field valField = a.getClass().getDeclaredField("value");

getField 跟 getDeclaredField 都是取得 class 裡面定義的 member 資料的 API,但 getField 只能取得 public fields,而 getDeclaredField 則不管 accessibility 是 public 或是 private,可以取得所有 fields。因此 a.getClass().getDeclaredField("value") 就是要取得 Integer 為 2 這個物件的 value 這個 field。

valField.setAccessible(true);
valField.setInt(a, 3);

接下來這兩行,則是將該 value 的欄位設定為可以修改的,並用 setInt 把 value 的值設定為 3。

新的 Integer 物件 2

我們在前面把物件 a 的 value 改成了 3,但是後半段卻是產生了一個新的 Interger 物件 2,就 Java 來說,只要是new 新的物件,那應該會不一樣才對啊!

但執行結果告訴我們,雖然有了一個新物件 b (Integer b = 2;),但很明顯地,在計算 b+c 的結果時,b 的數值被剛剛修改物件 a 的數值為 3 的時候,影響到了結果,也就是 b 物件就跟 a 物件是一樣的。

原因就在 Integer 的 source code 裡面

JDK 裡面的 Integer.java

JDK 裡面有 Integer 的 source code,我們節錄重要的部份:

public final class Integer extends Number implements Comparable<Integer> {

....

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    private final int value;

    public Integer(int value) {
        this.value = value;
    }
...
}

Interger Class 裡面放了一個靜態的 Inner Class: IntegerCache,這裡面 cache 了 -128 到 127 這麼多整數的物件。

Integer b=2;

這樣的語法是 auto boxing 的功能,就是 compiler 可以將一些 syntax sugar 自動 unboxing。像上面這個整數物件賦值的語法,compiler 會自動翻譯成以下這樣的語法。

Integer b = Integer.valueOf(2);

另外再看看 valueOf 這個 method,在 IntegerCache 負責的數值區間中,其實是直接使用 IntegerCache 而不是產生一個新的 Integer 物件。

換句話說,當我們使用 -128 到 127 的 Interger 物件時,其實都是參考到同一個物件。

為什麼要有 IntegerCache

cache 的用途基本上唯一的目的就是為了效能考量,而記憶體通常都是 cache 的第一選擇,在實做 Integer 的工程師認定一般使用者最常用到 -128 到 127 這些整數的物件,所以就預先產生出來,放置到記憶體中。

另外因為 IntergerCache 是 JVM 中靜態的物件,這樣子處理,也會因為 -128 ~ 127 使用率的增加,而達到記憶體空間節省的效果,表面上多花了一些記憶體,存放著 256 個整數物件,實際上當程式運作時間越久,產生出來的這些 IntergerCache 就會發生作用。

Integers caching in Java

Python - 使用 FoldPanelBar 實作可摺疊 ListView


之前為了一些需求,需要使用到類似 line 通訊錄的那種可摺疊的畫面。在 android 可用 BaseExpandableListAdapter 實作;ios 則可用 TableView 的 group 來實作,那 python 呢?
上網爬文研究了幾天,看到有人說 python 官方 demo 裡面有可以做到摺疊的畫面,於是自己加工了一下,做出類似 BaseExpandableListAdapter 的東西...
成品如下圖:
以下就簡單說明一下是如何實作,最後面有範例提供給大家取用

架構



實作步驟

  1. 建立一個 ScrollView
  2. 建立一個 FoldPanelBar
  3. 加入 FoldPanel ( group view )
  4. 加入 child view ( 可自訂任何畫面,主要還是以 panel 為最底層 )

#建立一個 FoldPanelBar
self.panel = fpb.FoldPanelBar(self.parentView, 2, wx.DefaultPosition, self.parentView.GetSize(),
                           agwStyle=fpb.FPB_VERTICAL)
#設定 group 展開/收合的圖片
Images = wx.ImageList(27,27)  #寬高一定要跟圖片一樣,不然不能載入
Images.Add(wx.Bitmap( u"image/news_open.png", wx.BITMAP_TYPE_ANY ))
Images.Add(wx.Bitmap( u"image/news_close.png", wx.BITMAP_TYPE_ANY ))

#設定 group 的顏色等 style
cs = foldPanel.CaptionBarStyle()
cs.SetCaptionStyle(foldPanel.CAPTIONBAR_FILLED_RECTANGLE)
cs.SetFirstColour(wx.Colour(85,147,177,0))
cs.SetCaptionColour(wx.WHITE)

titleDir = {
          0: "myProfile",
          1: "Favorites",
          2: "group",
          3: "application",
          4: "supplier",
          5: "company"
}

#list 是空時
if len(self.coll)==0:
    for i in range(0, MAX_TITLE_COUNT+1, +1):
 #增加一個 group
        item = self.panel.AddFoldPanel(titleDir[i], collapsed=False, foldIcons=Images, cbstyle=cs)
 #增加一條分隔線
        self.panel.AddFoldPanelSeparator(item, spacing=0)
 #綁定 action
        item.Bind(fpb.EVT_CAPTIONBAR, functools.partial(self.OnPressCaption, groupPosition=i))
        self.pitemList.append(item)
else:
    for i in range(0, len(self.coll), +1):
 #增加一個 group
        item = self.panel.AddFoldPanel(titleDir[self.coll[i][0]], collapsed=False, foldIcons=Images, cbstyle=cs)
        childItemList = []

        for j in range(0, len(self.coll[i][1]), +1):
            isGroupTitle = False
            bitmapPaht = u"image/name_icon1.png"
            name = "test"
            positionType = self.coll[i][0]
            vo = self.coll[i][1][j]
            if positionType == LOGIN_USER:
                print "LOGIN_USER"
                name = vo
            elif positionType == FAV_USER:
                print "FAV_USER"
                name = vo
            elif positionType == CHAT_GROUP:
                print "CHAT_GROUP"
                bitmapPaht = u"image/name_icon2.png"
                name = vo
            elif positionType == APP_USER:
                bitmapPaht = u"image/name_icon3.png"
                print "APP_USER"
                name = vo
            elif positionType == SUPPLIER:
                print "SUPPLIER"
                name = vo
            elif positionType == COMPANY:
                print "COMPANY"
                name = vo

            temp = wx.Bitmap( bitmapPaht, wx.BITMAP_TYPE_ANY )
            if isGroupTitle:
                isGroupTitle = False
  #==================================
  #增加一個 child view
                panel = wx.Panel(item, j)
                childItemList.append(panel)
                panel.BackgroundColour = wx.WHITE
                bSizer_gorupTitlePanel = wx.BoxSizer( wx.HORIZONTAL )

                #最上層group
                print "name:"+name
                print "fullname:"+vo.fullname
                print "groupname:"+vo.groupname
                st_groupPath = wx.StaticText( panel, wx.ID_ANY, name, (3, 12), wx.DefaultSize, 0 )
                st_groupPath.Wrap( -1 )

                panel.SetClientSize(wx.Size( -1, temp.GetHeight()))
  #==================================

            else:
  #==================================
  #增加一個 child view
                panel = wx.Panel(item, j)
                childItemList.append(panel)
                panel.BackgroundColour = wx.WHITE

                bSizer2 = wx.BoxSizer( wx.HORIZONTAL )
                size = temp.GetWidth(), temp.GetHeight()

                imgbtn_icon = wx.BitmapButton( panel, j, temp, (5, 5), size, 0 )
                bSizer2.Add( imgbtn_icon, 0, wx.ALL|wx.EXPAND, 5 )

                st_name = wx.StaticText( panel, j, name, (temp.GetWidth()+10, temp.GetHeight()/2), wx.DefaultSize, 0 )
                st_name.Wrap( -1 )
                bSizer2.Add( st_name, 1, wx.ALL|wx.EXPAND, 0 )

                panel.SetClientSize(wx.Size( -1, temp.GetHeight()+10))
  #==================================
  #綁定 action
                panel.Bind(wx.EVT_LEFT_DCLICK, functools.partial(self.doubleClick, groupPosition=i, vo=vo), id=j)
                panel.Bind(wx.EVT_LEFT_DOWN, functools.partial(self.OnItemDown, groupPosition=i, obj="test msg"), id=j)
                panel.Bind(wx.EVT_ENTER_WINDOW, functools.partial(self.onMouseOver, groupPosition=i), id=j)
                panel.Bind(wx.EVT_LEAVE_WINDOW, functools.partial(self.onMouseLeave, groupPosition=i), id=j)
                st_name.Bind(wx.EVT_LEFT_DCLICK, functools.partial(self.doubleClick, groupPosition=i, vo=vo), id=j)
                imgbtn_icon.Bind(wx.EVT_BUTTON, functools.partial(self.clickIcon, groupPosition=i, obj="test msg"), id=j)
  #==================================

     #將 child view add 到 FoldPanel 中
            self.panel.AddFoldPanelWindow(item, panel, fpb.FPB_ALIGN_WIDTH, 0)
            #==================================
            #增加一條分隔線
            self.panel.AddFoldPanelSeparator(item, spacing=0)
            #==================================
 #綁定 action
        item.Bind(fpb.EVT_CAPTIONBAR, functools.partial(self.OnPressCaption, groupPosition=i))

        self.itemList.append(childItemList)
        self.pitemList.append(item)

References


2015/01/12

JVM 日期格式該使用 yyyy 或是 YYYY

去年年底有則 twitter 談到因為誤用了日期格式化,使用了 YYYY 來格式化日期,在 2014/12/29 造成了日期錯誤,而把日期變成了 2015 年。

SimpleDateFormat 日期格式化

要在 Java 程式中格式化日期,最正確的格式化字串為

yyyy/MM/dd HH:mm:ss

第一次使用時,通常會遇到的問題是,MM 跟 mm 差別在哪裡,MM 代表是月份,而 mm 代表分鐘,這個部份的錯誤如果寫錯了,很容易就可以發現問題。

再來可能會遇到的問題,是把 HH 打成了 hh,HH 代表 24 小時制的小時,而 hh 是 Hour in am/pm (1-12) ,如果是在早上寫程式,那就不會發現自己寫錯了。

最糟糕的,就是年份遇到的問題,當日期落在 12/28~12/31 區間的時候,就會發生年份增加一年的錯誤。

測試程式

import java.util.Date;
import java.text.SimpleDateFormat;

class Test{
    public static void main(String[] args){

        System.out.println(
            new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));

    // 小時 錯誤
    System.out.println(
            new SimpleDateFormat("yyyy/MM/dd hh:mm:ss").format(new Date()));

    // 月份 錯誤
    System.out.println(
            new SimpleDateFormat("yyyy/mm/dd HH:mm:ss").format(new Date()));

    // 年份 錯誤
    System.out.println(
            new SimpleDateFormat("YYYY/MM/dd HH:mm:ss").format(new Date()));

    }
}

我們直接用 date 指令修改日期,當機器日期在 12/27 的時候,年份是沒有問題的。

> date 122720292014
六 12月 27 20:29:00 CST 2014
> java Test
2014/12/27 20:29:02
2014/12/27 08:29:02        -> 小時錯誤
2014/29/27 20:29:02        -> 月份錯誤
2014/12/27 20:29:02

當機器日期在 12/28~12/31 區間的時候,測試結果如下

> date 122920292014
一 12月 29 20:29:02 CST 2014
> java Test
2014/12/29 20:29:05
2014/12/29 08:29:05        -> 小時錯誤
2014/29/29 20:29:05        -> 月份錯誤
2015/12/29 20:29:05        -> 年份錯誤

Gregorian Calendar vs ISO 8601

一般我們使用的日曆系統為 Gregorian Calendar,他是以400年為一個週期,在這個週期中,一共有97個閏日,閏日儘可能均勻地分佈在各個年份中,所以一年的長度可能有 365天或366天。

而另一個時間規格標準 ISO 8601 卻是用不同的方式處理閏日,他是用潤週的概念,因此一年的長度是364或371天。而且 ISO 8601 規定一年中第一個週四所在的那個星期,作為一年的第一個星期。

今年2015年剛好1月1日就是週四,所以 12/28~12/31 算在 2015年第一週,如果用 ISO 8601 的規則,12/28 是 2015 年。

在 Java SimpleDateFormat 裡面,代表 ISO 8601的年份格式符號是 YYYY,Gregorian Calendar 的符號是 yyyy。

如果使用了 YYYY 作為 DateFormat 的格式符號,就會發生日期瞬間移動一年的問題。

結語

發生 HH 與 hh 的問題,頂多只是 12 小時的錯誤,如果發生了 YYYY 與 yyyy 的問題,就會有一年的落差。

不管如何,在處理日期格式時,必須將 文件 看清楚,在寫程式時,多花一點時間 review 一下,用直覺跟印象寫程式,就很容易出錯。

2015/01/05

Concurrency in Java

關於 Java Concurrency 議題,Java 並發編程的藝術 將所有相關的文章集結成了一本迷你書。在實作 java server side 程式時,一定會面臨到的問題就是 thread 之間的記憶體資料共享,另外還有在面對耗時的工作時,必須要同時執行的工作,也就是非同步呼叫。

synchronized vs volatile

volatile 是一種輕量級的同步機制,使用這種變數,就可以保證不會發生 context switching 或是 thread 切換的動作,但 volatile 的限制是,當變數的新值是依據舊值產生的時候,就不能使用 volatile。volatile 保證了共享變數的 visibility,當一個 thread 修改此共享變數時,另一個 thread 就能夠讀到這個修改後的值,簡單地說,就是 JVM 保證所有 thread 看到這個變數的值,都是一致的。

java 的 atomic operations 如下

  1. all assignments of primitive data types except for long and double
  2. all assignments of references
  3. all operations of java.concurrent.Atomic* classes
  4. all assignments of volatile longs and doubles

long foo = 87654321L;
並不是 thread-safe 的 operation,因為 java 會分成兩個步驟,先寫入 32bits 再寫入後 32bits,所以要改成

volatile long foo = 87654321L;

volatile 並不能完全取代 synchronied,甚至我們要注意,不能誤用 volatile,要不然就很容易出錯。一般基本判斷要不要使用 volatile 的條件,就是對一個變數讀取的次數遠高於寫入的次數。

可以使用 volatile 的情境如下:

狀態標籤

在另一個 thread 中呼叫 shutdown 用以中止 doWork 的無窮迴圈,這樣的寫法可保障 doWork 不回被惡意強制中斷。

volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

one-time safe publication

建立 singleton 物件,可使用 volatile 搭配 Double-checked locking 處理。

public class SingletonVolatile {

    private static volatile SingletonVolatile _instance;

    public static SingletonVolatile getInstance() {
        if (_instance == null) {
            synchronized (SingletonVolatile.class) {
                if (_instance == null)
                    _instance = new SingletonVolatile();
            }
        }

        return _instance;
    }

    public static void main(String[] args) {
        System.out.println(SingletonVolatile.getInstance());
        System.out.println(SingletonVolatile.getInstance());
        System.out.println(SingletonVolatile.getInstance());
    }
}

independent observation

定期 「發佈」 觀察結果供程序內部使用,一個後台thread可能會每隔幾秒讀取一次感應器,並更新包含當前文檔的 volatile 變量。然後,其他thread可以讀取這個變量,從而隨時能夠看到最新的溫度值。

以下是身份驗證機制記憶最近一次登錄的用戶的名字的範例。

public class UserManager {
    public volatile String lastUser;

    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

volatile bean

JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯,對於物件引用的成員,引用的對象必須是有效不可變的。

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }

    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }

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

開銷較低的讀-寫鎖策略

如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

CAS(compare and swap)

獨佔鎖是一種悲觀鎖,synchronized 是一種獨佔鎖,它只有在確保其它線程不會造成干擾的情況下執行。樂觀鎖假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

實現 lock-fre 的 non-blocking 演算法,最有名的方式就是使用 CAS (compare and swap),CAS是個樂觀鎖技術,當有多個threads嘗試同時更新同一個變數時,只有其中一個 thread 能成功更新變數的值,而其它threads都會被告知更新失敗,然後再重試一次,失敗的threads並不會被暫停執行的程序。

CAS內部有3個內部變數,(1) 記憶體實際值V (2) 目標變數的舊預期值A (3) 目標變數要被修改的新值B,當 A==V時,才能將 V 修改為 B,否則什麼都不做。

ConcurrentHashMap

ConcurrentHashMap 是 HashMap 的 multithread 版本,只要會有多個 thread 會同時存取的資料結構,就必須使用 ConcurrentHashMap。至於早期 Java 版本使用的 HashTable,由於內部是以 synchronized 的方式來實作,再面對大量儲存資料的狀況下,其使用效率就會遠低於比 ConcurrentHashMap。

ConcurrentHashMap 是以 Segment 與 HashEntry 組成,Segment 就類似於 HashMap,因此我們可以同時存取對多個 Segment,但到了 Segment 這一層,就會是 synchronzied 的呼叫,所以只要 hash 過後的資料,落入了不同的 Segment,這樣才能保障 ConcurrentHashMap 可以發揮它的功能。

ConcurrentHashMap 的 get method 把所有分享的變數都設定為 volatile,在處理過程中不需要加鎖,除非讀到的值是空的才會加鎖重讀,volatile 可保證只能被單一 thread 修改,且修改後也不會讀到舊的資料,重要的是可讓多個 thread 同時讀取。

put method 為了 thread-safe 必須加鎖處理。size method 裡面是以累加 Segment 裡面的 volatile 變數 count 實作的,由於 count 的變化機率很小,累加過程多半沒有發生 count 的變化,size 會先嘗試兩次不加鎖累加。但如果發生了,就會再進行加鎖累加。

count 的變化判斷是以 modCount 變數進行,在 put, remove, clean 之前,都會先將 modCount 加 1。

ThreadPool

通常在使用 J2EE Server 連結 DB 時,會利用 DB Connection Pool 當作中間層,這可以減少 DB Connection 建立與銷毀所消耗掉的資源。

至於 JVM 的 thread 也一樣,可以使用 ThreadPool 來進行 Thread 的 reuse 與 管理,不僅可減少建立與銷毀 thread 所消耗掉的資源,也可以縮短任務處理的時間。

thread 跟 db connection 都屬於 JVM 的外部資源,這些資源都有使用的上限,以 pool 的方式進行使用管理,而不是無限制地建立與使用,可以保護 server 並提高穩定度。

建立 ThreadPool

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

相關的參數

  1. corePoolSize(pool的基本大小)
    ThreadPool 會持續建立 thread,直到 thread 的數量達到 corePoolSize。如果呼叫 prestartAllCoreThreads method,ThreadPool 就會提前建立所有基本線程。

  2. runnableTaskQueue
    儲存等待執行的任務的blocking queue

    2.1 ArrayBlockingQueue:以 Array 實作,FIFO(先進先出)

    2.2 LinkedBlockingQueue:以 LinkedList 實作,FIFO (先進先出),throughput比ArrayBlockingQueue好。Executors.newFixedThreadPool() 就是使用這個queue。

    2.3 SynchronousQueue:enqueue必須等到另一個線程呼叫移除才能執行,否則enqueue就會一直處於阻塞狀態,throughput比LinkedBlockingQueue高,Executors.newCachedThreadPool使用了這個queue。

    2.4 PriorityBlockingQueue

  3. maximumPoolSize(pool最大大小)
    ThreadPool允許建立的最大線程數。

  4. ThreadFactory
    用於建立thread的factory

  5. RejectedExecutionHandler(飽和策略)
    當 Queue 與 ThreadPool 都滿了,必須採取一種策略處理提交的新任務。以下是JDK1.5提供的四種策略。
    5.1 AbortPolicy:預設值,直接拋出異常。
    5.2 CallerRunsPolicy:使用呼叫端所在線程來執行任務
    5.3 DiscardOldestPolicy:丟棄 queue 裡最近的一個任務,並執行當前任務
    5.4 DiscardPolicy:不處理,直接丟棄
    5.5 自訂 RejectedExecutionHandler

  6. keepAliveTime
    thread 空閒後,保持存活的時間。如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。

  7. TimeUnit
    天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)

提交處理任務的方式有兩種

  1. execute方法沒有返回值,所以無法判斷任務是否被線程池執行成功

    threadsPool.execute(new Runnable() {
             @Override
             public void run() {
             }
         });
    
  2. 使用 submit 來提交任務,它會返回一個future,我們可以通過這個future來判斷任務是否執行成功。future.get()會 blocking 住,直到任務完成,如果使用 get(long timeout, TimeUnit unit) 則會阻塞一段時間後立即返回。

    Future<Object> future = executor.submit(harReturnValuetask);
    try {
      Object s = future.get();
    } catch (InterruptedException e) {
     // 處理中斷異常
    } catch (ExecutionException e) {
     // 處理無法執行任務異常
    } finally {
     // 關閉線程池
     executor.shutdown();
    }
    

當提交一個新任務到 ThreadPool 處理流程如下:

  1. 判斷基本線程池是否已滿?沒滿,建立一個工作線程來執行任務。滿了,則進入下個流程。
  2. 判斷工作隊列是否已滿?沒滿,則將新提交的任務儲存在工作隊列裡。滿了,則進入下個流程。
  3. 判斷整個線程池是否已滿?沒滿,則建立一個新的工作線程來執行任務,滿了,則交給 RejectedExecutionHandler 飽和策略來處理這個任務。

使用 ThreadPool 之前必須先分析任務特性,可以從以下幾個角度來進行分析:

  1. 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
    CPU密集型任務,儘可能小的線程,如配置N(cpu)+1個線程的線程池
    IO密集型任務則由於線程並不是一直在執行任務,則配置儘可能多的線程,如2*N(cpu)
    混合型的任務,如果可以拆分,則將其拆成一個CPU密集型任務和一個IO密集型任務
    Runtime.getRuntime().availableProcessors() 可取得當前的CPU個數
  2. 任務的優先級:高,中和低。
    優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先得到執行,但如果一直有優先級高的任務,那麼優先級低的任務可能永遠會不能被執行。
  3. 任務的執行時間:長,中和短。
    執行時間不同的任務可以交給不同規模的線程池來處理,也可以使用優先級隊列,讓執行時間短的任務先執行。
  4. 任務的依賴性:是否依賴其他系統資源,如DB連接。
    因為發送SQL後需要等待DB返回結果,如果等待的時間越長CPU空閒時間就越長,因此thread 數量應該設置越大,這樣才能更好的利用CPU。

監控 ThreadPool

有一些屬性在監控 ThreadPool 的時候可以使用

  1. taskCount:需要執行的任務數量
  2. completedTaskCount:在執行過程中已完成的任務數量
  3. largestPoolSize:曾經建立過的最大線程數量
  4. getPoolSize:Thread數量如果線程池不銷毀的話,池裡的線程不會自動銷毀,所以這個大小只增不+ getActiveCount:獲取活動的線程數。

可以繼承 ThreadPool 並覆寫 beforeExecute,afterExecute 和 terminated方法,可以在任務執行前,執行後和線程池關閉前,監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裡是空的方法。

protected void beforeExecute(Thread t, Runnable r) { }

Reference

聊聊並發系列文章

非阻塞同步算法與CAS(Compare and Swap)無鎖算法

JDK, Jetty, Tomcat 線程池的實現算法分析
Java 理論與實踐: 正確使用 Volatile 變量

2015/01/04

初探Polymer


Polymer是由Google開發的JavaScript Library, 網頁開發者可以使用Polymer建立自訂的HTML元件, 並將CSS及JavaScript封裝於其中, 實現網頁開發的模組化。

由上例可見到自訂的my-ele元件,在網頁瀏覽器中有欄框, 圓角, 陰影...等樣式,但是在網頁瀏覽器中只有簡單的兩行資料。
Polymer支援大部分常用的瀏覽器,要注意的是IE必須使用10以上的版本才支援。請參考: https://www.polymer-project.org/resources/compatibility.html
以下將一步一步介紹如何使用Polymer。

1. 安裝Polymer:

由zip檔安裝Polyer是最簡潔的方式。
下載並解壓縮後,可以看到其中有個bower_components資料夾,裡面即包含了所有需要用到的Polymer程式碼。

2. 建立專案資料夾

建立ㄧ個專案資料夾,放置Polymer以及範例程式。 該資料夾內容如下圖:

bower_components為方才下載的Polymer程式碼。
elements則用來放置自訂的HTML元件。
index.html則為網頁首頁,使用自訂好的HTML元件來呈現網頁。

3. 建立自訂HTML元件

於elements資料夾中建立my-ele.html, 範例如下:
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="my-ele" noscript>
<template>
<style>
  :host { 
    float:left;
    padding:10px;
    margin:10px;
    box-shadow:2px 2px 2px inset;
    border:5px lightblue solid;
    background-color: lightyellow;
    border-radius:10px;
  }
  ::content name { font-weight:bold; font-size:1.5em; }
  ::content desc { font-style:italic; }
</style>   
<content select="name"></content>
<hr />
<content select="desc"></content>   
</template>
</polymer-element>
首先, 使用link標籤引入Polymer程式碼。接著, 使用polymer-element標籤的name屬性, 定義此自訂的HTML元件的名稱為my-ele (Polymer官方文件提及,自訂的HTML元件名稱必須有"-"符號1)。
template標籤則用以定義自訂元件的內容及樣式。以下簡單介紹其中的content標籤與style標籤。

content標籤

當其他HTML頁面使用了my-ele標籤時,my-ele元件可以透過content標籤取得其內容,並將資料填入指定的位置,舉例來說:
    <!-- index.html中部分內容 -->
    ...
    <my-ele>                            
        <name>MyNameA</name>        
        <desc>MyDescAAA</desc>      
    </my-ele>
    ...

    <!-- my-ele中部分內容 -->
    ...
    <content select="name"></content> <!-- 內容將填入:MyNameA -->
    <content select="desc"></content> <!-- 內容將填入:MyDescAAA -->
    ...
透過content標籤,可以自訂my-ele真正顯示的內容。

style標籤

style標籤用以定義元件CSS樣式。此處CSS寫法與一般寫法無異,但只作用於my-ele.html中。此外,還多了幾個選擇器:

::host選擇器

::host選擇器的樣式,將套用至my-ele元件本身。
    <!-- index.html中部分內容 -->
    <my-ele>        <!-- my-ele將套用::host樣式 -->
        <name>...</name>        
        <desc>description...</desc>     
    </my-ele>

::content選擇器

::content選擇器的樣式,將套用至content標籤中。
    <!-- index.html中部分內容 -->
    <my-ele>                            
        <name>...</name>    <!--name標籤將套用::content name樣式 -->
        <desc>description...</desc>     
    </my-ele>

4.使用自訂元件

建立index.html, 並使用my-ele元件,範例如下:
<!DOCTYPE html>
<html>
<head>
    <!-- 載入Polymer -->
    <script src="bower_components/webcomponentsjs/webcomponents.js"></script>
    <!-- 載入自訂的HTML元件 -->
    <link rel="import" href="elements/my-ele.html">
</head>
<body>
    <my-ele>  <!-- 自訂的HTML元件 -->
        <name>Myles</name>
        <desc>test test test</desc>
    </my-ele>

    <my-ele>  <!-- 自訂的HTML元件 -->
        <name>Michael</name>
        <desc>123456 123456</desc>
    </my-ele>
</body>
</html>
接著,再以瀏覽器打開index.html即可看到結果。

結語

使用自訂元件將樣式, 內容等細節隱藏封裝,便是Polymer的核心概念。事實上,除了樣式與內容外,Polymer也允許在自訂元件中撰寫script,讓網頁的開發更有彈性。此外,Polymer也提供helper method像是import或是mixin幫助網頁的開發。在眾多的JavaScript Framework中不為失一個好選擇。惟Polymer本質上仍大量使用HTML, CSS與JavaScript,在不熟悉的HTML, CSS, JavaScrip的情況下,使用Polymer開發並無太大助益。