Friday, April 27, 2012
Tuesday, April 17, 2012
成本降就該降售價嗎?
台灣媒體從業人員笨死了,成天在媒體上獵女巫,檢討別人的成本結構;殊不知,(除了原物料必需品外),售價是由供需曲線決定的,而不是由成本所決定的,成本,只決定是否有業者會提供某項產品。
媒體成天講什麼,稅金降所以該降價,成本漲所以物價漲,全民的腦力都浪費在幫別人算成本,而不是幫自己開創新產品。
講多了,全民都變笨了。而且還偏好用名牌,漲價時不會去選用替代品,只會忙著叫龍頭降價,問題是,你越愛用,龍頭業者越有調漲的本錢。
最近年紀到了,開始準備生小孩,就開始頭痛未來教育問題,不知道怎麼幫他規劃如何活在這塊土地上,而不被一些錯誤的觀念所誤導呢?
還是說,我又該離鄉背景工作個幾年,幫小孩弄個外國籍,未來送到台北歐洲學校或日僑學校去(美僑學校已經招生爆炸,只收父母都是美國人的小孩了)
媒體成天講什麼,稅金降所以該降價,成本漲所以物價漲,全民的腦力都浪費在幫別人算成本,而不是幫自己開創新產品。
講多了,全民都變笨了。而且還偏好用名牌,漲價時不會去選用替代品,只會忙著叫龍頭降價,問題是,你越愛用,龍頭業者越有調漲的本錢。
最近年紀到了,開始準備生小孩,就開始頭痛未來教育問題,不知道怎麼幫他規劃如何活在這塊土地上,而不被一些錯誤的觀念所誤導呢?
還是說,我又該離鄉背景工作個幾年,幫小孩弄個外國籍,未來送到台北歐洲學校或日僑學校去(美僑學校已經招生爆炸,只收父母都是美國人的小孩了)
Wednesday, April 11, 2012
Lesson Learned form Developing Locadz SDK
這是我 4/25 號要在 GTUG 發表的東西,先把講稿寫在 blog 上。
市面上大部份講 Android 的書,多數都是在講 API 要怎麼樣使用,但是,許多的書都沒有提到一個重要的問題,就是什麼是 UI Thread (有時被稱做Main Thread) ,為什麼要有 UI Thread 以及為什麼不可以在 UI Thread 中執行耗時間的運算。
在本文中,我講談一談我們在開發Locadz SDK所使用到的一些技巧。
UI Thread是Android(or other GUI library)用來處理 Event 的 thread ,這些 Event 可能是使用者處發的,如Touch Event,或者是其它程式或者是系統底層的事件,如 Intent 或 Location Updates
在 UI Thread 處理完一個事件後, UI Thread 會重畫整個 Application ,而前面這句話解釋了處理 ANR 的兩個原則
有許多的文章談到如何用 AsyncTask 或 ResultReceiver 來把需要大量運算的程式,放在另一個 Thread 來做處理。
然而,這些範例都有著一個常備忽略的問題,那就是,這些 AsyncTask or ResultReceiver 常會把前景的Activity包進來,如果說你的程式只有一個 Activity 的話,這或許不是什麼問題,但是若是你的 App 有多個畫面的話,這些在背景執行的程式可能就會讓你的程式有 memory leak 的問題;如某家廣告商的 SDK 就有此問題。
一個可能的發生狀況是,Activity A透過 AsyncTask 去遠端下載一個圖片來顯示在Activity A之上,但因為某些因素(如忘了設 connection timeout),這個下載的程緒卡住了,既使 User 以從Activity A切換到Activity B之上,這個Activity A還是不會被 GC 回收掉。
要避開因為有個 Strong Reference 造成 Activity 無法被回收的問題,我們在把有可能被回收的物件傳到另一個 Thread 中被延後執行時,必需用 WeakReference包住這個物件;如此一來,當Garbage Collector碰到一個已經不在前景的 Activity 時,Garbage Collector會把這物件處理掉,如此一來,就不會有 memory leak 的問題。
AsyncTask是 Android 最常被用來處理複雜運算時用的工具,透過AsyncTask,我們可以在背景處裡一些複雜的運算,再把結果放回前景之上。
但據我的經驗,使用 AsyncTask 同時間來擔任 MVC 中的 View & Controller 的工作,最後往往是把程式碼弄成一團麵線。因此,在開發 Locadz SDK 時,我們把一些跟 UI 無關的運算,都切出來變成 IntentService或者是非Inner class的AsyncTask,把所有的運算邏輯從 Activity 中切出來,增加重用的可能。
然後運算的結果,再透過 getHandler().post(...) 更新到 UI 之上.
底下的圖表,是Jeff Dean發表的,在談的是讀取資料的的效率,我們把這幾個數字先記起來,再加一個代表UI設計時人體覺得是即時反應的反應時間上限 100 ms。然後我們再來談 Android UI 的設計。
大家可以看到 Main Memory Reference(0.001ms) 與 Disk Seek(10ms) Disk Read(30ms) 的重大差距,然而,後者的數字在 Mobile Phone 上就不是這樣了。在 Mobile Phone 上,傳統的硬碟扮演的角色,被NAND Flash Memory, External SD Card所取代了。在存取效率上 NAND Flash Memory 雖然不比 RAM 快,但是,也仍是 seek time ~1ms 的狠角色。
這 1ms 的負擔,雖比 0.001ms 的負擔高上百倍,以上,但是在 100ms 這UI 反應需求上,卻又變得很渺小了。
因此,在這邊,我會建議大家,若是有 cache 的需求時,直接往 Internal Storage 塞吧,不要放在Main memory上,或用個SoftReferenceMap包著。
在上面第一個範例中有個錯誤,那就是DownloadImagesTask.onPostExecute()會在呼叫DownloadImagesTask.execute(...)的那個 Thread 上執行,如果說,很不幸的,這個 DownloadImagesTask 並不是從 UI Thread 上來呼叫的話,那麼,imageView.setImageBitmap(result)便有可能不會即時更新到UI之上。
如果你的開發環境會有這種問題,在包在層層呼叫後,無法確保 Method 是否是在 UI Thread 上執行;那麼我會建議你把會更新 UI 的 Method ,標成 protected ,然後開放一個 public async method 出來,範例如下:
Introduction
市面上大部份講 Android 的書,多數都是在講 API 要怎麼樣使用,但是,許多的書都沒有提到一個重要的問題,就是什麼是 UI Thread (有時被稱做Main Thread) ,為什麼要有 UI Thread 以及為什麼不可以在 UI Thread 中執行耗時間的運算。
在本文中,我講談一談我們在開發Locadz SDK所使用到的一些技巧。
UI Thread
UI Thread是Android(or other GUI library)用來處理 Event 的 thread ,這些 Event 可能是使用者處發的,如Touch Event,或者是其它程式或者是系統底層的事件,如 Intent 或 Location Updates
在 UI Thread 處理完一個事件後, UI Thread 會重畫整個 Application ,而前面這句話解釋了處理 ANR 的兩個原則
- UI事件的處理要越快越好,在處理完前,UI不會有反應
- UI的更新,一定要發生在UI Thread,要不然,要等到下次UI Event被處發時才會一併被處理
Lesson I: Use WeakReference to Avoid Strong Reference and Memory Leak
有許多的文章談到如何用 AsyncTask 或 ResultReceiver 來把需要大量運算的程式,放在另一個 Thread 來做處理。
然而,這些範例都有著一個常備忽略的問題,那就是,這些 AsyncTask or ResultReceiver 常會把前景的Activity包進來,如果說你的程式只有一個 Activity 的話,這或許不是什麼問題,但是若是你的 App 有多個畫面的話,這些在背景執行的程式可能就會讓你的程式有 memory leak 的問題;如某家廣告商的 SDK 就有此問題。
一個可能的發生狀況是,Activity A透過 AsyncTask 去遠端下載一個圖片來顯示在Activity A之上,但因為某些因素(如忘了設 connection timeout),這個下載的程緒卡住了,既使 User 以從Activity A切換到Activity B之上,這個Activity A還是不會被 GC 回收掉。
要避開因為有個 Strong Reference 造成 Activity 無法被回收的問題,我們在把有可能被回收的物件傳到另一個 Thread 中被延後執行時,必需用 WeakReference包住這個物件;如此一來,當Garbage Collector碰到一個已經不在前景的 Activity 時,Garbage Collector會把這物件處理掉,如此一來,就不會有 memory leak 的問題。
/**
* AsyncTask to Load Image
*/
public class DownloadImagesTask extends AsyncTask<Uri, Void, Bitmap> {
WeakReference<imageview> imageViewWeakReference = null;
public DownloadImagesTask(ImageView imageView) {
imageViewWeakReference = new WeakReference<Imageview>(imageView);
}
@Override
protected Bitmap doInBackground(Uri... uri) {
return downloadImage(uri);
}
@Override
protected void onPostExecute(Bitmap result) {
ImageView imageView = imageViewWeakReference.get();
if (imageView != null) {
imageView.setImageBitmap(result);
}
}
private Bitmap downloadImage(Uri url) {
...
}
Lesson II: Use IntentService to Run Business Logic
AsyncTask是 Android 最常被用來處理複雜運算時用的工具,透過AsyncTask,我們可以在背景處裡一些複雜的運算,再把結果放回前景之上。
但據我的經驗,使用 AsyncTask 同時間來擔任 MVC 中的 View & Controller 的工作,最後往往是把程式碼弄成一團麵線。因此,在開發 Locadz SDK 時,我們把一些跟 UI 無關的運算,都切出來變成 IntentService或者是非Inner class的AsyncTask,把所有的運算邏輯從 Activity 中切出來,增加重用的可能。
然後運算的結果,再透過 getHandler().post(...) 更新到 UI 之上.
/** Service that retrieve the ad unit allocations from external source and cache locally in SharedPreference. */
public class AdUnitAllocationService extends IntentService {
private static final int CACHE_EXPIRATION_PERIOD = 30 * 60 * 1000; // 30 minutes.
private final static String PREFS_STRING_TIMESTAMP = "timestamp";
private final static String PREFS_STRING_CONFIG = "config";
// response code for possible result.
public static final int RESULT_OK = 1;
public AdUnitAllocationService() {
super(AdUnitAllocationService.class.getCanonicalName());
}
@Override
protected void onHandleIntent(Intent intent) {
AdUnitContext adUnitContext =
(AdUnitContext) intent.getParcelableExtra(IntentConstants.EXTRA_ADUNIT_CONTEXT);
AdUnitAllocation adUnitAllocation = getAdUnitAllocation(adUnitContext);
if (adUnitAllocation != null) {
Ration ration = getRandomRation(adUnitAllocation.getRations());
// send response through ResultReceiver.
ResultReceiver receiver = intent.getParcelableExtra(IntentConstants.EXTRA_RECEIVER);
Bundle resultData = new Bundle();
resultData.putString(IntentConstants.EXTRA_ADUNIT_ID, adUnitContext.getAdUnitId());
resultData.putSerializable(IntentConstants.EXTRA_RATION, ration);
resultData.putSerializable(IntentConstants.EXTRA_EXTRA,
adUnitAllocation.getExtra());
receiver.send(RESULT_OK, resultData);
}
}
/**
* Select a random ration form the provided rations.
* @param rations the candidates.
* @return a random ration from the candidates.
*/
private Ration getRandomRation(List<Ration> rations) {
//...
}
/**
* Get the allocation configuration for the adunit.
* @param adUnitContext the context of the adunit.
* @return the allocation configuration for the adunit.
*/
AdUnitAllocation getAdUnitAllocation(AdUnitContext adUnitContext) {
//...
}
}
Lesson III: Use Disk Cache instead of (Main) Memory Cache
底下的圖表,是Jeff Dean發表的,在談的是讀取資料的的效率,我們把這幾個數字先記起來,再加一個代表UI設計時人體覺得是即時反應的反應時間上限 100 ms。然後我們再來談 Android UI 的設計。
大家可以看到 Main Memory Reference(0.001ms) 與 Disk Seek(10ms) Disk Read(30ms) 的重大差距,然而,後者的數字在 Mobile Phone 上就不是這樣了。在 Mobile Phone 上,傳統的硬碟扮演的角色,被NAND Flash Memory, External SD Card所取代了。在存取效率上 NAND Flash Memory 雖然不比 RAM 快,但是,也仍是 seek time ~1ms 的狠角色。
這 1ms 的負擔,雖比 0.001ms 的負擔高上百倍,以上,但是在 100ms 這UI 反應需求上,卻又變得很渺小了。
因此,在這邊,我會建議大家,若是有 cache 的需求時,直接往 Internal Storage 塞吧,不要放在Main memory上,或用個SoftReferenceMap包著。
Lesson IV: Make All Public Method Async to Avoid UI Update Issue
在上面第一個範例中有個錯誤,那就是DownloadImagesTask.onPostExecute()會在呼叫DownloadImagesTask.execute(...)的那個 Thread 上執行,如果說,很不幸的,這個 DownloadImagesTask 並不是從 UI Thread 上來呼叫的話,那麼,imageView.setImageBitmap(result)便有可能不會即時更新到UI之上。
如果你的開發環境會有這種問題,在包在層層呼叫後,無法確保 Method 是否是在 UI Thread 上執行;那麼我會建議你把會更新 UI 的 Method ,標成 protected ,然後開放一個 public async method 出來,範例如下:
/**
* Remove old ad views and push the new one.
*
* @param subView the adview to push.
*/
protected void pushSubView(ViewGroup subView) {
//....
}
/**
* submit a push view request to Android's handler. This will remove
* old ad view and push a new one to this layout asynchronously.
*
* @param subView the adview to push.
*/
public void submitPushSubViewRequest(ViewGroup subView) {
Log.d(LOG_TAG, String.format("Scheduled pushSubView(%s)", subView));
getHandler().post(new ViewAdRunnable(this, subView));
}
/**
* Runnable runs on the Main Thread that pushes an AdView to the layout.
*/
private static final class ViewAdRunnable implements Runnable {
private final WeakReference<Adunitlayout> locadzLayoutWeakReference;
private ViewGroup subView;
public ViewAdRunnable(AdUnitLayout layout, ViewGroup subView) {
locadzLayoutWeakReference = new WeakReference<Adunitlayout>(layout);
this.subView = subView;
}
@Override
public void run() {
AdUnitLayout locadzLayout = locadzLayoutWeakReference.get();
if (locadzLayout != null) {
locadzLayout.pushSubView(subView);
}
}
}
Tuesday, March 27, 2012
[Service上線前你該知道的事]How to Debian-ize Your Web Application.
在布建一個Web Application時,用.war來佈屬服務,算是最沒有效率的一種方式,因為,這樣無法把web container所需要的設定檔一起佈建出去,同時,也無法板本控製你的應用程式;本文接下來會敘述,如何用maven & jetty & debian來佈屬你的Web Application。
首先,先講講我的需求,我的需求如下
這些需求,我是靠 unix-maven-plugin ,幫我把我的 web application 包成一個 .deb 來做到的,包成 .deb 的好處是,在 debian 上可以簡單的安裝,若是公司有架 debian package repository, 那麼,打個 apt-get install 就可以安裝或昇級到最新版,同時間,可以透過 debian 安裝的機制,來把些 script 掛入 rc.*
由於 unix-maven-plugin 已被原作者遺棄,所以需要從 github下載並安裝後人修改過的版本。
然後依附錄的方式,增修你的設定檔,需要增修的檔案有
將這些檔案設定好後,在 debian 的機器上打 mvn package deploy ,就可以自動生成 .deb 的檔案,並且上傳到你的 maven repository 上去,如果貴公司有架 debian package repository 的話,還可以加一段程式碼把 .deb 傳到 debian package repository 去。
接著,打 dpkg -i xxxx.deb 就可以把你的 web application 當成一個 debian service 安裝到你的 debain machine 上去。
查詢目前安裝的板本則是打 dpkg -l xxxx 即可
把底下的這段,加入你的 pom.xml
然後把下面這塊存成 src/main/unix/resources/default/${service.user}
然後把下面這塊存成 src/main/unix/resources/ect/jetty.conf
然後把下面這塊存成 src/main/unix/resources/etc/start.conf
然後把下面這塊存成 src/main/unix/scripts/${service.user}
首先,先講講我的需求,我的需求如下
- Web Application的版本必需能夠簡單被管理,要能簡單的就知道目前安裝的版本,並且輕鬆的昇級到新版
- Web Application被佈建時,要連Web Container及設定擋包在一起
- Debian Box開關機時,要能自動開關Web Container
這些需求,我是靠 unix-maven-plugin ,幫我把我的 web application 包成一個 .deb 來做到的,包成 .deb 的好處是,在 debian 上可以簡單的安裝,若是公司有架 debian package repository, 那麼,打個 apt-get install
由於 unix-maven-plugin 已被原作者遺棄,所以需要從 github下載並安裝後人修改過的版本。
然後依附錄的方式,增修你的設定檔,需要增修的檔案有
- pom.xml: 加入 unix-maven-plugin 及服務的名稱及PORT
- src/main/unix/resources/default/${service.user} : 給 jetty 用的 default 設定值,在這邊你可以修改 JAVA_OPTIONS 及 NO_START
- src/main/unix/resources/etc/jetty.conf
- src/main/unix/resources/etc/start.conf
- src/main/unix/resources/scripts/${service.user} : 給 debian 用的 init script
- src/main/unix/resources/scripts/post-install post-remove pre-install pre-remove : debian 在安裝及昇級 .deb 時會跑的 script
將這些檔案設定好後,在 debian 的機器上打 mvn package deploy ,就可以自動生成 .deb 的檔案,並且上傳到你的 maven repository 上去,如果貴公司有架 debian package repository 的話,還可以加一段程式碼把 .deb 傳到 debian package repository 去。
接著,打 dpkg -i xxxx.deb 就可以把你的 web application 當成一個 debian service 安裝到你的 debain machine 上去。
查詢目前安裝的板本則是打 dpkg -l xxxx 即可
||/ Name Version Description +++-==============-==============-============================================ ii backend 1.2.0-20120327.143115 backend
附錄
把底下的這段,加入你的 pom.xml
4.0.0 com.locadz backend war backend 1.2.0-SNAPSHOT backend 1.0-alpha-6.1 7.4.5.v20110725 Locadz Backend /var/lib/bluetang/locadz-backend locadz-backend 9080 debian-package unix linux maven-resources-plugin 2.5 copy-resources process-resources copy-resources ${basedir}/target src/main/unix unix **/* true org.mortbay.jetty.toolchain unix-maven-plugin ${version.maven.unix.plugin} true package package-deb-attached true yho@bluetangstudio.com yho@bluetangstudio.com ${service.user} adm ${service.user} adm bluetang extra org.eclipse.jetty:jetty-distribution:zip ${service.home} /jetty-distribution-${version.jetty}(.*) $1 */contexts/** */contexts-available/** */webapps/** */javadoc/** */LICENSES/** */logs/** **/win32/** ${project.build.directory}/${project.build.finalName}.${project.packaging} ${service.home}/webapps/ROOT.war ${basedir}/target/unix/resources/default/${service.user} /etc/default ${basedir}/target/unix/resources/etc /etc/bluetang/${service.user} ${basedir}/target/unix/scripts/${service.user} /etc/init.d 0754 ${service.user} admin org.eclipse.jetty jetty-distribution ${version.jetty} zip * *
然後把下面這塊存成 src/main/unix/resources/default/${service.user}
# Defaults for jetty see /etc/${service.user}/jetty for more
# change to 0 to allow Jetty to start
NO_START=0
# change to 'no' or uncomment to use the default setting in /etc/default/rcS
VERBOSE=yes
# Run Jetty as this user ID (default: jetty)
# Set this to an empty string to prevent Jetty from starting automatically
JETTY_USER=${service.user}
# Listen to connections from this network host
# Use 0.0.0.0 as host to accept all connections.
# Uncomment to restrict access to localhost
#JETTY_HOST=$(uname -n)
JETTY_HOST=0.0.0.0
# The network port used by Jetty
JETTY_PORT=${service.port}
# Timeout in seconds for the shutdown of all webapps
#JETTY_SHUTDOWN=30
# Additional arguments to pass to Jetty
#JETTY_ARGS=
# Extra options to pass to the JVM
JAVA_OPTIONS="-Xmx256m -Djava.awt.headless=true -Duser.timezone=UTC"
# Home of Java installation.
#JAVA_HOME=
# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not
# defined in /etc/default/jetty). Should contain a list of space separated directories.
#JDK_DIRS="/usr/lib/jvm/default-java /usr/lib/jvm/java-6-sun"
# Java compiler to use for translating JavaServer Pages (JSPs). You can use all
# compilers that are accepted by Ant's build.compiler property.
#JSP_COMPILER=jikes
# Jetty uses a directory to store temporary files like unpacked webapps
JETTY_TMP=/var/cache/bluetang/jetty
# Jetty uses a config file to setup its boot classpath
JETTY_START_CONFIG=/etc/bluetang/${service.user}/start.config
# Default for number of days to keep old log files in /var/log/jetty/
#LOGFILE_DAYS=14
然後把下面這塊存成 src/main/unix/resources/ect/jetty.conf
# list of jetty configuration and property files
/var/lib/bluetang/${service.user}/etc/jetty-logging.xml
然後把下面這塊存成 src/main/unix/resources/etc/start.conf
# This file controls what file are to be put on classpath or command line.
#
# Format is as follows:
# Each line contains entry for one JAR file.
# Format of line:
#
# SUBJECT [ [!] CONDITION [AND|OR] ]*
#
# where SUBJECT:
# ends with ".class" is the Main class to run.
# ends with ".xml" is a configuration file for the command line
# ends with "/" is a directory from which to add all jar and zip files.
# ends with "/*" is a directory from which to add all unconsidered jar and zip files.
# ends with "/**" is a directory from which to recursively add all unconsidered jar and zip files.
# Containing = are used to assign system properties.
# all other subjects are treated as files to be added to the classpath.
#
# Subjects may include system properties with $(propertyname) syntax.
#
# Files starting with "/" are considered absolute, all others are relative to
# the home directory.
#
# CONDITION is one of:
# always
# never
# available classname # true if class on classpath
# property name # true of set
# java OPERATOR version # java version compared to literal
# nargs OPERATOR number # number of command line args compared to literal
# OPERATOR := one of "<",">","<=",">=","==","!="
#
# CONDITIONS can be combined with AND OR or !, with AND being the assume
# operator for a list of CONDITIONS.
# Classpath operations are evaluated on the fly, so once a class or jar is
# added to the classpath, subsequent available conditions will see that class.
#
$(jetty.class.path) always
$(jetty.lib)/** exists $(jetty.lib)
jetty.home=/var/lib/bluetang/${service.user} always
# The main class to run
org.mortbay.xml.XmlConfiguration.class
# The default configuration files
$(jetty.home)/etc/jetty.xml nargs == 0
/usr/share/java/servlet-api-2.5.jar
/usr/share/java/slf4j-api.jar
# Optional stuff for libjetty-extra-java
/usr/share/java/gnumail.jar
/usr/share/java/activation.jar
/usr/share/java/ant.jar
# Set the jetty classpath
/usr/share/jetty/lib/**
# Add a resources directory if it is there
$(jetty.home)/resources/
然後把下面這塊存成 src/main/unix/scripts/${service.user}
#!/bin/bash
### BEGIN INIT INFO
# Provides: ${service.user}
# Required-Start:
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start daemon at boot time
# Description: Enable service provided by daemon.
### END INIT INFO
#
# Startup script for jetty under *nix systems (it works under NT/cygwin too).
#
# Configuration files
#
# /etc/default/${service.user}
# If it exists, this is read at the start of script. It may perform any
# sequence of shell commands, like setting relevant environment variables.
#
# /etc/bluetang/${service.user}/jetty.conf
# If found, and no configurations were given on the command line,
# the file will be used as this script's configuration.
# Each line in the file may contain:
# - A comment denoted by the pound (#) sign as first non-blank character.
# - The path to a regular file, which will be passed to jetty as a
# config.xml file.
# - The path to a directory. Each *.xml file in the directory will be
# passed to jetty as a config.xml file.
#
# The files will be checked for existence before being passed to jetty.
#
# $JETTY_HOME/etc/jetty.xml
# If found, used as this script's configuration file, but only if
# /etc/bluetang/${service.user}/jetty.conf was not present. See above.
#
# Configuration variables
#
# JAVA
# Command to invoke Java. If not set, java (from the PATH) will be used.
#
# JAVA_OPTIONS
# Extra options to pass to the JVM
#
# JETTY_HOME
# Where Jetty is installed. If not set, the script will try go
# guess it by first looking at the invocation path for the script,
# and then by looking in standard locations as $HOME/opt/jetty
# and /opt/jetty. The java system property "jetty.home" will be
# set to this value for use by configure.xml files, f.e.:
#
# /webapps/jetty.war
#
# JETTY_PORT
# Override the default port for Jetty servers. If not set then the
# default value in the xml configuration file will be used. The java
# system property "jetty.port" will be set to this value for use in
# configure.xml files. For example, the following idiom is widely
# used in the demo config files to respect this property in Listener
# configuration elements:
#
#
#
# Note: that the config file could ignore this property simply by saying:
#
# 8080
#
# JETTY_RUN
# Where the jetty.pid file should be stored. It defaults to the
# first available of /var/run, /usr/var/run, and /tmp if not set.
#
# JETTY_PID
# The Jetty PID file, defaults to $JETTY_RUN/jetty.pid
#
# JETTY_ARGS
# The default arguments to pass to jetty.
#
# JETTY_USER
# if set, then used as a username to run the server as
#
JETTY_HOME=${service.home}
JETTY_USER=${service.user}
JETTY_PID=$JETTY_RUN/${service.user}.pid
usage()
{
echo "Usage: ${0##*/} [-d] {start|stop|run|restart|check|supervise} [ CONFIGS ... ] "
exit 1
}
[ $# -gt 0 ] || usage
##################################################
# Some utility functions
##################################################
findDirectory()
{
local L OP=$1
shift
for L in "$@"; do
[ "$OP" "$L" ] || continue
printf %s "$L"
break
done
}
running()
{
local PID=$(cat "$1" 2>/dev/null) || return 1
kill -0 "$PID" 2>/dev/null
}
readConfig()
{
(( DEBUG )) && echo "Reading $1.."
source "$1"
}
##################################################
# Get the action & configs
##################################################
CONFIGS=()
NO_START=0
DEBUG=0
while [[ $1 = -* ]]; do
case $1 in
-d) DEBUG=1 ;;
esac
shift
done
ACTION=$1
shift
##################################################
# See if there's a default configuration file
##################################################
if [ -f /etc/default/${service.user} ] ; then
. /etc/default/${service.user}
fi
##################################################
# Set tmp if not already set.
##################################################
TMPDIR=${TMPDIR:-/tmp}
##################################################
# Jetty's hallmark
##################################################
JETTY_INSTALL_TRACE_FILE="etc/jetty.xml"
##################################################
# Try to determine JETTY_HOME if not set
##################################################
if [ -z "$JETTY_HOME" ]
then
JETTY_SH=$0
case "$JETTY_SH" in
/*) ;;
./*) ;;
*) JETTY_SH=./$JETTY_SH ;;
esac
JETTY_HOME=${JETTY_SH%/*/*}
if [ ! -f "${JETTY_SH%/*/*}/$JETTY_INSTALL_TRACE_FILE" ]
then
JETTY_HOME=
fi
fi
##################################################
# if no JETTY_HOME, search likely locations.
##################################################
if [ -z "$JETTY_HOME" ] ; then
STANDARD_LOCATIONS=(
"/usr/share"
"/usr/share/java"
"${HOME}"
"${HOME}/src"
"${HOME}/opt"
"/opt"
"/java"
"/usr/local"
"/usr/local/share"
"/usr/local/share/java"
"/home"
)
JETTY_DIR_NAMES=(
"jetty-7"
"jetty7"
"jetty-7.*"
"jetty"
"Jetty-7"
"Jetty7"
"Jetty-7.*"
"Jetty"
)
for L in "${STANDARD_LOCATIONS[@]}"
do
for N in "${JETTY_DIR_NAMES[@]}"
do
POSSIBLE_JETTY_HOME=("$L/"$N)
if [ ! -d "$POSSIBLE_JETTY_HOME" ]
then
# Not a directory. skip.
unset POSSIBLE_JETTY_HOME
elif [ ! -f "$POSSIBLE_JETTY_HOME/$JETTY_INSTALL_TRACE_FILE" ]
then
# Trace file not found. skip.
unset POSSIBLE_JETTY_HOME
else
# Good hit, Use it
JETTY_HOME=$POSSIBLE_JETTY_HOME
# Break out of JETTY_DIR_NAMES loop
break
fi
done
if [ -n "$POSSIBLE_JETTY_HOME" ]
then
# We have found our JETTY_HOME
# Break out of STANDARD_LOCATIONS loop
break
fi
done
fi
##################################################
# No JETTY_HOME yet? We're out of luck!
##################################################
if [ -z "$JETTY_HOME" ]; then
echo "** ERROR: JETTY_HOME not set, you need to set it or install in a standard location"
exit 1
fi
cd "$JETTY_HOME"
JETTY_HOME=$PWD
#####################################################
# Check that jetty is where we think it is
#####################################################
if [ ! -r "$JETTY_HOME/$JETTY_INSTALL_TRACE_FILE" ]
then
echo "** ERROR: Oops! Jetty doesn't appear to be installed in $JETTY_HOME"
echo "** ERROR: $JETTY_HOME/$JETTY_INSTALL_TRACE_FILE is not readable!"
exit 1
fi
##################################################
# Try to find this script's configuration file,
# but only if no configurations were given on the
# command line.
##################################################
if [ -z "$JETTY_CONF" ]
then
if [ -f /etc/bluetang/${service.user}/jetty.conf ]
then
JETTY_CONF=/etc/bluetang/${service.user}/jetty.conf
elif [ -f "${JETTY_HOME}/etc/jetty.conf" ]
then
JETTY_CONF="${JETTY_HOME}/etc/jetty.conf"
fi
fi
##################################################
# Get the list of config.xml files from jetty.conf
##################################################
if [ -z "$CONFIGS" ] && [ -f "$JETTY_CONF" ] && [ -r "$JETTY_CONF" ]
then
while read -r CONF
do
if expr "$CONF" : '#' >/dev/null ; then
continue
fi
if [ -d "$CONF" ]
then
# assume it's a directory with configure.xml files
# for example: /etc/jetty.d/
# sort the files before adding them to the list of CONFIGS
for file in "$CONF/"*.xml
do
if [ -r "$FILE" ] && [ -f "$FILE" ]
then
CONFIGS+=("$FILE")
else
echo "** WARNING: Cannot read '$FILE' specified in '$JETTY_CONF'"
fi
done
else
# assume it's a command line parameter (let start.jar deal with its validity)
CONFIGS+=("$CONF")
fi
done < "$JETTY_CONF"
fi
#####################################################
# Find a location for the pid file
#####################################################
if [ -z "$JETTY_RUN" ]
then
JETTY_RUN=$(findDirectory -w /var/run /usr/var/run /tmp)
fi
#####################################################
# Find a PID for the pid file
#####################################################
if [ -z "$JETTY_PID" ]
then
JETTY_PID="$JETTY_RUN/jetty.pid"
fi
##################################################
# Setup JAVA if unset
##################################################
if [ -z "$JAVA" ]
then
JAVA=$(which java)
fi
if [ -z "$JAVA" ]
then
echo "Cannot find a Java JDK. Please set either set JAVA or put java (>=1.5) in your PATH." 2>&2
exit 1
fi
#####################################################
# See if JETTY_PORT is defined
#####################################################
if [ "$JETTY_PORT" ]
then
JAVA_OPTIONS+=("-Djetty.port=$JETTY_PORT")
fi
#####################################################
# See if JETTY_LOGS is defined
#####################################################
if [ "$JETTY_LOGS" ]
then
JAVA_OPTIONS+=("-Djetty.logs=$JETTY_LOGS")
fi
#####################################################
# Are we running on Windows? Could be, with Cygwin/NT.
#####################################################
case "`uname`" in
CYGWIN*) PATH_SEPARATOR=";";;
*) PATH_SEPARATOR=":";;
esac
#####################################################
# Add jetty properties to Java VM options.
#####################################################
JAVA_OPTIONS+=("-Djetty.home=$JETTY_HOME" "-Djava.io.tmpdir=$TMPDIR")
[ -f "$JETTY_HOME/etc/start.config" ] && JAVA_OPTIONS=("-DSTART=$JETTY_HOME/etc/start.config" "${JAVA_OPTIONS[@]}")
#####################################################
# This is how the Jetty server will be started
#####################################################
JETTY_START=$JETTY_HOME/start.jar
[ ! -f "$JETTY_START" ] && JETTY_START=$JETTY_HOME/lib/start.jar
START_INI=$(dirname $JETTY_START)/start.ini
[ -r "$START_INI" ] || START_INI=""
RUN_ARGS=(${JAVA_OPTIONS[@]} -jar "$JETTY_START" $JETTY_ARGS "${CONFIGS[@]}")
RUN_CMD=("$JAVA" ${RUN_ARGS[@]})
#####################################################
# Comment these out after you're happy with what
# the script is doing.
#####################################################
if (( DEBUG ))
then
echo "JETTY_HOME = $JETTY_HOME"
echo "JETTY_CONF = $JETTY_CONF"
echo "JETTY_RUN = $JETTY_RUN"
echo "JETTY_PID = $JETTY_PID"
echo "JETTY_ARGS = $JETTY_ARGS"
echo "CONFIGS = ${CONFIGS[*]}"
echo "JAVA_OPTIONS = ${JAVA_OPTIONS[*]}"
echo "JAVA = $JAVA"
echo "RUN_CMD = ${RUN_CMD}"
fi
##################################################
# Do the action
##################################################
case "$ACTION" in
start)
echo -n "Starting ${service.name}: "
if (( NO_START )); then
echo "Not starting ${service.user} - NO_START=1";
exit
fi
if type start-stop-daemon > /dev/null 2>&1
then
unset CH_USER
if [ -n "$JETTY_USER" ]
then
CH_USER="-c$JETTY_USER"
fi
if start-stop-daemon -S -p"$JETTY_PID" $CH_USER -d"$JETTY_HOME" -b -m -a "$JAVA" -- "${RUN_ARGS[@]}" --daemon
then
sleep 1
if running "$JETTY_PID"
then
echo "OK"
else
echo "FAILED"
fi
fi
else
if [ -f "$JETTY_PID" ]
then
if running $JETTY_PID
then
echo "Already Running!"
exit 1
else
# dead pid file - remove
rm -f "$JETTY_PID"
fi
fi
if [ "$JETTY_USER" ]
then
touch "$JETTY_PID"
chown "$JETTY_USER" "$JETTY_PID"
# FIXME: Broken solution: wordsplitting, pathname expansion, arbitrary command execution, etc.
su - "$JETTY_USER" -c "
${RUN_CMD[*]} --daemon &
disown \$!
echo \$! > '$JETTY_PID'"
else
"${RUN_CMD[@]}" &
disown $!
echo $! > "$JETTY_PID"
fi
echo "STARTED ${service.name} `date`"
fi
;;
stop)
echo -n "Stopping ${service.name}: "
if type start-stop-daemon > /dev/null 2>&1; then
start-stop-daemon -K -p"$JETTY_PID" -d"$JETTY_HOME" -a "$JAVA" -s HUP
TIMEOUT=30
while running "$JETTY_PID"; do
if (( TIMEOUT-- == 0 )); then
start-stop-daemon -K -p"$JETTY_PID" -d"$JETTY_HOME" -a "$JAVA" -s KILL
fi
sleep 1
done
rm -f "$JETTY_PID"
echo OK
else
PID=$(cat "$JETTY_PID" 2>/dev/null)
kill "$PID" 2>/dev/null
TIMEOUT=30
while running $JETTY_PID; do
if (( TIMEOUT-- == 0 )); then
kill -KILL "$PID" 2>/dev/null
fi
sleep 1
done
rm -f "$JETTY_PID"
echo OK
fi
;;
restart)
JETTY_SH=$0
if [ ! -f $JETTY_SH ]; then
if [ ! -f $JETTY_HOME/bin/jetty.sh ]; then
echo "$JETTY_HOME/bin/jetty.sh does not exist."
exit 1
fi
JETTY_SH=$JETTY_HOME/bin/jetty.sh
fi
"$JETTY_SH" stop "$@"
"$JETTY_SH" start "$@"
;;
supervise)
#
# Under control of daemontools supervise monitor which
# handles restarts and shutdowns via the svc program.
#
exec "${RUN_CMD[@]}"
;;
run|demo)
echo "Running Jetty: "
if [ -f "$JETTY_PID" ]
then
if running "$JETTY_PID"
then
echo "Already Running!"
exit 1
else
# dead pid file - remove
rm -f "$JETTY_PID"
fi
fi
exec "${RUN_CMD[@]}"
;;
check)
echo "Checking arguments to Jetty: "
echo "JETTY_HOME = $JETTY_HOME"
echo "JETTY_CONF = $JETTY_CONF"
echo "JETTY_RUN = $JETTY_RUN"
echo "JETTY_PID = $JETTY_PID"
echo "JETTY_PORT = $JETTY_PORT"
echo "JETTY_LOGS = $JETTY_LOGS"
echo "START_INI = $START_INI"
echo "CONFIGS = ${CONFIGS[*]}"
echo "JAVA_OPTIONS = ${JAVA_OPTIONS[*]}"
echo "JAVA = $JAVA"
echo "CLASSPATH = $CLASSPATH"
echo "RUN_CMD = ${RUN_CMD[*]}"
echo
if [ -f "$JETTY_RUN/jetty.pid" ]
then
echo "Jetty running pid=$(< "$JETTY_RUN/jetty.pid")"
exit 0
fi
exit 1
;;
*)
usage
;;
esac
exit 0
然後把下面這塊存成 src/main/unix/scripts/post-install,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
set -e
USER=locadz-backend
case "$1" in
configure)
if ! id $USER > /dev/null 2>&1 ; then
adduser --system --home /var/lib/bluetang/$USER --no-create-home \
--group --disabled-password --shell /bin/false \
$USER
fi
if [ ! -e /var/log/bluetang ]
then
mkdir -p /var/log/bluetang
fi
if [ ! -e /var/log/bluetang/$USER/ ]
then
ln -s /var/lib/bluetang/$USER/logs /var/log/bluetang/$USER
fi
if [ ! -e /var/cache/bluetang/$USER ]
then
mkdir -p /var/cache/bluetang/$USER
fi
chown -R $USER:adm /var/cache/bluetang/$USER /var/log/bluetang/$USER /var/lib/bluetang/$USER
chmod 0744 /etc/init.d/$USER
update-rc.d $USER defaults
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "$0 called with unknown argument \`$1'" >&2
exit 1
;;
esac
然後把下面這塊存成 src/main/unix/scripts/post-remove,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
set -e
USER=locadz-backend
#DEBHELPER#
# Remove cached files
rm -rf /var/cache/blutang/$USER/*
case "$1" in
remove)
# Remove ROOT webapp if not modified
RWLOC="/var/lib/bluetang/$USER/webapps/root"
RWFILES="$RWLOC/index.html $RWLOC/jetty_banner.gif"
if [ "`(cat $RWFILES | md5sum -) 2>/dev/null | cut -d ' ' -f 1`" \
= "12471c4b3020defb7ebd30ef84c0f9dd" ] ; then
rm $RWFILES
rmdir --ignore-fail-on-non-empty \
/var/lib/bluetang/$USER/webapps/root \
/var/lib/bluetang/$USER/webapps \
/var/lib/bluetang/$USER || true
fi
if [ -d "/var/cache/bluetang/$USER" ] ; then
rm -rf /var/cache/bluetang/$USER
fi
;;
purge)
# Remove user/group and log files (don't remove everything under
# /var/lib/jetty because there might be user-installed webapps)
deluser jetty || true
rm -rf /var/log/bluetang/$USER
if [ -d "/var/lib/bluetang/$USER" ] ; then
rmdir --ignore-fail-on-non-empty /var/lib/bluetang/$USER || true
fi
rmdir --ignore-fail-on-non-empty /etc/bluetang/$USER/contexts /etc/bluetang/$USER || true
;;
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
# Nothing to do here
;;
*)
echo "$0 called with unknown argument \`$1'" >&2
exit 1
;;
esac
然後把下面這塊存成 src/main/unix/scripts/pre-install,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
#
# This is the preinst script for the Blue Tang Servers
#
# Written by Yung-Lin Ho yho@bluetangstudio.com
set -e
USER=locadz-backend
SCRIPT=/etc/init.d/$USER
case "$1" in
upgrade|remove|purge)
$SCRIPT stop
;;
install|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "preinst called with unknown argument \`$1'" >&2
;;
esac
exit 0
下面這塊存成 src/main/unix/scripts/pre-remove,並修改 USER=locadz-backend 這一行成跟你 ${service.user} 一樣的值
#!/bin/sh
#
# This is the prerm script for the Blue Tang Servers
#
# Written by Yung-Lin Ho yho@bluetangstudio.com
set -e
USER=locadz-backend
SCRIPT=/etc/init.d/$USER
case "$1" in
upgrade)
;;
remove|purge)
$SCRIPT stop
;;
failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "prerm called with unknown argument \`$1'" >&2
;;
esac
exit 0
Bravo to Spotify Apps.
spotify's app is pretty good business model. 之前我抱怨 #spotify 的 editorial material 比不上 #rhapsody. 用 Rhapsody 時,因為 Rhapsody 內有十多位人員負責編排本週推薦的音樂,還有客製化的播放清單及電台,所以訂閱 rhapsody 就像有 DJ 幫你找歌來聽。
但是用 Spotify ,就只能夠聽,你已知的歌手, spotify 沒有一個好用的推薦功能可用。
最近 spotify 的 gui client 開始可以放上第三方App的功能,馬上就把缺的地方補上,而且還更好。
像是串入 last.fm 就有推薦清單可用,soundrop可以當線上DJ,TuneWiki可以看歌詞,歌詞還會照時間來跳到現在唱的句子;這證明了 Open API的威力,你有缺的地方,用 3rd party app 來補上,而且可以做的更好。
其實 Rhapsody 內部早在 2005 已有這種 API ,我在 real.com 時就是做這一塊的,許多第三方的程式及硬體就是呼叫我們的API,如 sonos, sandisk sansa, 我自己也寫了隻程式給我的Nokia N82用,只是可惜不能放出來。
Rhapsody 再不把 API 放出來,恐怕就連美國這塊市場都要開始敗退了...
但是用 Spotify ,就只能夠聽,你已知的歌手, spotify 沒有一個好用的推薦功能可用。
最近 spotify 的 gui client 開始可以放上第三方App的功能,馬上就把缺的地方補上,而且還更好。
像是串入 last.fm 就有推薦清單可用,soundrop可以當線上DJ,TuneWiki可以看歌詞,歌詞還會照時間來跳到現在唱的句子;這證明了 Open API的威力,你有缺的地方,用 3rd party app 來補上,而且可以做的更好。
其實 Rhapsody 內部早在 2005 已有這種 API ,我在 real.com 時就是做這一塊的,許多第三方的程式及硬體就是呼叫我們的API,如 sonos, sandisk sansa, 我自己也寫了隻程式給我的Nokia N82用,只是可惜不能放出來。
Rhapsody 再不把 API 放出來,恐怕就連美國這塊市場都要開始敗退了...
Wednesday, March 7, 2012
[Job]Job Market in US
這篇的草稿躺在我的部絡格兩個月了,看來完成遙遙無期,所以先把屍體放出來好了
要在美國求職,第一要務,就是先了解到,美國的求職市場是怎樣的環境。
大多人在台灣的求職的方式,是上 104 找適合的工作,然後再寄信過去應徵工作;然而,很不幸的是,你把求職市場放大十倍,這種方式就變得不是很有效率了。
首先,你要知道的是,在矽谷,一個工作被登上美國的求職網站,兩週內,可能吸引到2000個應徵者投遞履例表,這數字是什麼意思呢,這是說,當 HR 過濾履例表時,既使只花30秒在一份履例表上,也要花上整整16小時兩個工作天,才能篩檢完畢。我曾經看過同事在過濾履例時,就像在拿電風扇在吹一樣,看到過去工作過的公司有趣留下來,看到經驗有趣的留下來,看到名字念起來有趣的留下來;有時後,會不會被找來面識,真的是要靠一點運氣的。
所以說,要寫出一份好的履例表,讓 HR 一目了然,抓到重點,把你的履例表留下來細看,就是一件很重要的事了。
好的履例表,第一要鍵就是,格式要清楚,格式不清楚,當下就是被放一邊,不會有 HR 去細看的;第二則是,不管你經歷有多少,履例表的內容,一定要是一業滿版,比一頁多,你要刪成只有一頁,比一頁少,你不管怎樣就是要加到一頁滿版。
底下是我2005時,剛畢業時用的履例表;內容的格式有點小問題,不過這個板型也是我後來一直延用下去的格式。
這份履例表的排列是社會新鮮人用的,(未完待續)
Friday, February 17, 2012
Tuesday, February 7, 2012
受夠了台灣的 Costco
真的有點受夠了台灣 Costco 老是在賣些美國沒賣的美國產品了。
https://www.google.com/search?q=vitamin%20goldmax 找下去,找不到一個條目的東西,台灣 Costco 也敢拿出來賣?這些採購人員是心理只有錢是吧?
以前住美國時,我大多的日用產品都是在 costco 買的,那是因為,美國 costco 設定的營業方針是『貨品以近乎進貨成本來賣、獲利全靠會員費』,賺不賺錢,就看選貨能力來吸引會員數。
Costco@USA選貨的標準又剛好跟我的收入很相近,所以,若是要買日用品又懶得做功課時,就去 costco 買;那是我相信 costco 的選貨標準,不用讓我在買日用品時,還要一個個拿起來看產地與成份,逛的很舒服
但是台灣的 Costco 逛起來給我的感覺卻不是如此, 50%的貨品是台灣採購的,讓我逛起來像是在逛市場一樣,要玩訛虞我詐的遊戲,許多商品的外包照片跟內容跟本不符,又有許多假美國商品,逛起來要心驚膽顫,一點都沒有消費的樂趣。
https://www.google.com/search?q=vitamin%20goldmax 找下去,找不到一個條目的東西,台灣 Costco 也敢拿出來賣?這些採購人員是心理只有錢是吧?
以前住美國時,我大多的日用產品都是在 costco 買的,那是因為,美國 costco 設定的營業方針是『貨品以近乎進貨成本來賣、獲利全靠會員費』,賺不賺錢,就看選貨能力來吸引會員數。
Costco@USA選貨的標準又剛好跟我的收入很相近,所以,若是要買日用品又懶得做功課時,就去 costco 買;那是我相信 costco 的選貨標準,不用讓我在買日用品時,還要一個個拿起來看產地與成份,逛的很舒服
但是台灣的 Costco 逛起來給我的感覺卻不是如此, 50%的貨品是台灣採購的,讓我逛起來像是在逛市場一樣,要玩訛虞我詐的遊戲,許多商品的外包照片跟內容跟本不符,又有許多假美國商品,逛起來要心驚膽顫,一點都沒有消費的樂趣。
Tuesday, January 3, 2012
How to Protect A WebService.
最近做了不少Restful WebService的案子,也用了不少外部的WebService API,看到許多的系統,在安全性上,有許多的漏洞,因此想寫篇文章來說說,到底要怎樣保護好你的WebService,必竟,台灣主流網站的安全性已經夠糟了,不用再多幾個有問題的API來把環境弄更糟。
大家用Web API,最常碰到的保護機制是 ApiKey ,但是,請不要以為 ApiKey 有什麼保護做用,HTTP Request的東西都是明碼,你傳什麼,網路上流過的機器都可以看的一清二楚,假設你做的 client 端程式是用 ApiKey 來保護 client <-> server 間的通訊的話,要破的人,只要在 client 所在的區網內,安裝 packet sniffer 就可以拿到 client 內包的 ApiKey 了。
ApiKey通常是WebService Provider拿來統計流量用的,不要以為他有什麼安全機制存在。
再來,有些 Web API 要求你要登入,才可以使用他們的 API ,如 chargify or GMail,這些服務所使用的機制是HTTP Basic Authentication ;但是非常不幸的是,HTTP Basic Authentication只不過變形的ApiKey,Basic Auth的運作方式是把 username:password 用 base64 編碼後,放在 HTTP Request Header 當成密碼傳到 server 端,因此,一樣可以被 sniffer 直接偷出來用。
當然,你會問,我們不是有用 HTTPS 來把封包加密了嗎?然而,除非你把 certificate 預載在 client 端,要不然 client <-> server 第一次的通訊是危險的,會受到 Man in the middle attack ,直接從中間把雙方收送的 certificate 換掉。
較安全的做法是,使用HTTPS Client Authentication,在包 client 就把 client 端該用的 server-signed certificate 包進 client 端裡面;這樣一來,少了透過 internet 交換 certificate 的機制,能夠確保 certificate 不會被偷換掉。
另外,使用 Client Authentication 的好處是,只有 server 端認可的 client 可以使用 API ,縱始是第三方拿到了username, password, apikey,還是沒有辦法使用你的服務。
至於有那些大廠,用了不安全的方式來保護他們的 web service api 呢? unblock-us 這邊有幾個名單
公商服務時間:
小弟的公司,目前有時間接設計 WebService API 的案子,若是有合作機會的,請與我連絡。
大家用Web API,最常碰到的保護機制是 ApiKey ,但是,請不要以為 ApiKey 有什麼保護做用,HTTP Request的東西都是明碼,你傳什麼,網路上流過的機器都可以看的一清二楚,假設你做的 client 端程式是用 ApiKey 來保護 client <-> server 間的通訊的話,要破的人,只要在 client 所在的區網內,安裝 packet sniffer 就可以拿到 client 內包的 ApiKey 了。
ApiKey通常是WebService Provider拿來統計流量用的,不要以為他有什麼安全機制存在。
再來,有些 Web API 要求你要登入,才可以使用他們的 API ,如 chargify or GMail,這些服務所使用的機制是HTTP Basic Authentication ;但是非常不幸的是,HTTP Basic Authentication只不過變形的ApiKey,Basic Auth的運作方式是把 username:password 用 base64 編碼後,放在 HTTP Request Header 當成密碼傳到 server 端,因此,一樣可以被 sniffer 直接偷出來用。
當然,你會問,我們不是有用 HTTPS 來把封包加密了嗎?然而,除非你把 certificate 預載在 client 端,要不然 client <-> server 第一次的通訊是危險的,會受到 Man in the middle attack ,直接從中間把雙方收送的 certificate 換掉。
較安全的做法是,使用HTTPS Client Authentication,在包 client 就把 client 端該用的 server-signed certificate 包進 client 端裡面;這樣一來,少了透過 internet 交換 certificate 的機制,能夠確保 certificate 不會被偷換掉。
另外,使用 Client Authentication 的好處是,只有 server 端認可的 client 可以使用 API ,縱始是第三方拿到了username, password, apikey,還是沒有辦法使用你的服務。
至於有那些大廠,用了不安全的方式來保護他們的 web service api 呢? unblock-us 這邊有幾個名單
unblock-us 是怎麼破解幾大影音網站的 geo-location check 的呢?
一般的解決方案是用 VPN,但是影音串流不適合透過 VPN 來做 proxy。
就我的猜測 unblock-us 等用的解決方案應該是這樣,在 DNS 那層把 server 端的 ip 反解成他們自己的機器,當 client 端送 request 給 http://api.poorguy.com, unblock-us 會先檢查這個 call 的用途,如果是做 geo-location 反解的,就送個假的回應給 client 端,如果是其他的方式,就轉送到正常的機器。
等 client 端把這些處理做完後,要做影音串流的 rstp://stream.poorguy.com 又是傳回正常的 ip ,所以這樣用到的流量最小。
這樣等於是直接的違反人家的授權去 hack API,這種服務可以活的很久嗎??我不清楚
不過從這一點也可以看到,各大廠做 operation 的能力優劣,像我的前雇主 rhapsody就有利用 client authentication 在各式各樣的 client 端上。所以讓我想在台灣用都沒得用 :(
公商服務時間:
小弟的公司,目前有時間接設計 WebService API 的案子,若是有合作機會的,請與我連絡。
Thursday, December 8, 2011
Lucene On Android
嘗試性的把 Lucene 放到 Android 上面來跑,結果不是太理想,但仍是一些心得分享出來,省去後人嘗試的時間。
Lucene 要跑在 Android 上,第一個碰上的問題是,如何把 index files 傳到手機上去,在 Lucene 中,對 index 的讀取,是以目錄為單位的,所以說,無法把 index files 放在 apk 中直接讀取,一定要存放在 device or external storage 上,才能夠使用;或者是自己弄個虛擬目錄出來,不過,這會耗用過多的計憶體空間。
我是選用把 index 放在 'src/res/raw' 底下,讓他變成 apk 的一部份,省去在網路上找個空間來放置 index 的問題,當要更新 index 時,就重編個 apk 叫使用者更新就好。
在放 lucene index 時,如果你想用 compound format 的話,可以用底下的指令,把多個 index files 包裹成單一檔案 .cfs
接著,是把產生的 segment, segments_1, _0.cfs 拷到 src/res/raw 底下,讓這些檔案變成 .apk 的一部份。
接著,是要在第一次執行時,把這些 index 從 apk 中覆製到 SD 卡上或是機子上,這邊,我寫了個小工具來做這件事
最後,是在 *App 上加上這段
這樣一來,就能在 Android 上面跑 lucene-core 了.
Lucene 要跑在 Android 上,第一個碰上的問題是,如何把 index files 傳到手機上去,在 Lucene 中,對 index 的讀取,是以目錄為單位的,所以說,無法把 index files 放在 apk 中直接讀取,一定要存放在 device or external storage 上,才能夠使用;或者是自己弄個虛擬目錄出來,不過,這會耗用過多的計憶體空間。
我是選用把 index 放在 'src/res/raw' 底下,讓他變成 apk 的一部份,省去在網路上找個空間來放置 index 的問題,當要更新 index 時,就重編個 apk 叫使用者更新就好。
在放 lucene index 時,如果你想用 compound format 的話,可以用底下的指令,把多個 index files 包裹成單一檔案 .cfs
// run REPL with 'scala -cp luke-3.4.0_1-all.jar'
import org.apache.lucene.store.FSDirectory
import org.apache.lucene.index._
import org.apache.lucene.analysis.standard._
val source = FSDirectory.open(new java.io.File("source"))
val dest = FSDirectory.open(new java.io.File("dest"))
// open source index
val reader = IndexReader.open(source)
// create writer for compound index.
val analyzer = new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_34)
val writer = new org.apache.lucene.index.IndexWriter(dir, analyzer, IndexWriter.MaxFieldLength.UNLIMITED)
// force writer always use compound index format.
writer.getMergePolicy.asInstanceOf[LogByteSizeMergePolicy].setNoCFSRatio(1.0)
// add source index to dest index.
writer.addIndexes(reader)
writer.optimized
writer.close
reader.close
接著,是把產生的 segment, segments_1, _0.cfs 拷到 src/res/raw 底下,讓這些檔案變成 .apk 的一部份。
接著,是要在第一次執行時,把這些 index 從 apk 中覆製到 SD 卡上或是機子上,這邊,我寫了個小工具來做這件事
import android.content.Context
import android.os.Environment
import android.util.Log
import com.bluetangstudio.android.disastermap.TaipeiDisasterApp.LogTag
import org.apache.commons.io.FileUtils
import org.apache.lucene.store.{FSDirectory, Directory}
import scala.collection.JavaConversions._
import java.io.File
/**
* Helper class that search for lucene index directories on the device. The search order is
* external storage first then local storage. If lucene index does not exist on device, this
* class will copy the index from the apk to the device storage.
*
* @param context the application context
* @param path the root folder name of the index directory to use and to look for.
* @param source the source of index resource to copy if index does not exist on the device.
* format: Seq[(filename, resourceId)]
*/
case class LuceneOpenHelper(context: Context, path: String, source: Seq[Tuple2[String, Int]]) {
/**
* create or open an Directory.
*/
def open(): Option[Directory] = {
val candidates = Seq(externalFolder, internalFolder).flatten
// find the folder with index in it.
val folder = candidates.filter(f => f.exists() && f.list().length > 0).headOption
val withIndex = folder.orElse(
candidates.find(
f => {
// ensure folder is available.
f.exists() || f.mkdirs() match {
// folder is not accessible
case false => false
case _ => {
Log.d(LogTag, "Duplicating index from apk to %s...".format(f))
source.foreach(
s => {
val is = context.getResources.openRawResource(s._2)
try {
FileUtils.copyInputStreamToFile(is, new File(f, s._1))
} finally {
is.close()
}
}
)
true
}
}
}
)
)
return withIndex.map(FSDirectory.open(_))
}
private def externalFolder: Option[File] = {
Environment.getExternalStorageState match {
case Environment.MEDIA_MOUNTED => Option(context.getExternalFilesDir(path))
case _ => None
}
}
private def internalFolder: Option[File] = {
return Option(new File(context.getFilesDir, path))
}
}
最後,是在 *App 上加上這段
object MyApp {
private val INDEX_DIRECTORY = "idx"
private val INDEX_FILES = Seq(
("_0.cfs", R.raw.idx_0),
("segments", R.raw.segments),
("segments_1", R.raw.segments_1)
)
}
class MyApp extends android.app.Application {
import MyApp._
private var _luceneSearcher: Option[IndexSearcher] = None
def luceneSearcher: Option[IndexSearcher] = {
if (_luceneSearcher.isEmpty) {
Log.d(LogTag, "Initializing new IndexSearcher...")
_luceneSearcher = LuceneOpenHelper(this, INDEX_DIRECTORY, INDEX_FILES).open().map(new IndexSearcher(_))
}
_luceneSearcher
}
override def onLowMemory() {
_luceneSearcher.foreach(s => s.close())
_luceneSearcher = None
}
}
這樣一來,就能在 Android 上面跑 lucene-core 了.
Thursday, November 24, 2011
Analytics: Hadoop V.S. DBMS
最近兩周,我開始參加 GTUG 的活動,在活動間,有人問起,為什麼我公司選用DBMS(MySQL)來做資料分析,而不用 Hadoop。在這邊,我把跟公司的DB怪才同事的討論結果,稍為簡單敘述一下。
首先,要先理解到的一點是,在 MapReduce 及 Hadoop 出現之前,Data Mining的工具及技術,已經在DBMS上建構了有30年了,相關的技術也發產的很成熟且有許多專書論述。
ETL(Extract, Transform, Load)是這個領域的關鍵字,用 ETL 去找下去,會找到許多的書籍講怎麼做資料分析
假設,你的原始資料有一整年這麼多,總共有四千萬筆(40M),那麼,前者就是把四千萬筆的資料讀出來,然後過濾加總,而後者,由於資料匯入時已先處理過了,所以要處理的只有24 * 365筆資料;而這兩者間所花費的時間差易,會隨著查尋次數而產生重大差異。
試問,若是每次你老闆跟你要資料時,你要跟他說,要等半小時才可以,還是說等我五分鐘? 至於前面的半小時是怎麼算出來的呢?
由於DB運算,瓶頸多是卡在硬碟存取上,在前面的例子中,40M筆資料,假設一筆是1K bytes,那麼,總資料量是 40GB ,從硬碟循序讀取 40GB 出來,所花的時間是 40GB / 40MB/s(硬碟速度) = 17分鐘
MapReduce是把資料分拆運算,然後再加總的技術;但是,MapReduce的概念對 RDBMS 並不是什麼新奇的事,在這份IBM的文件中,便清楚的敘述到,DBMS會對 SQL 指令做編譯,DBMS內部就會依資料型態、INDEX檔、CPU Core的數量等,去做最佳化,這些,是Hadoop不會幫你做的。
既然DBMS那麼好那麼,為什麼會有 MapReduce 等技術的出現呢,那是因為,DBMS可以幫你scale up,但是最終,還是會碰上硬體的極限,那麼,這時只有 scale out ,才能解決這問題。 但是 scale out 的程式難度,總是比 scale up 困難,而且,一個不好的分析模式,並不會因為跑的快10倍,就變得比較好;因此, ETL 的概念,在使用 MapReduce 時仍是可以應用的,
講到這邊,什麼時候該用 Hadoop ,什麼時候該選用 DBMS ,應該很清楚了
首先,要先理解到的一點是,在 MapReduce 及 Hadoop 出現之前,Data Mining的工具及技術,已經在DBMS上建構了有30年了,相關的技術也發產的很成熟且有許多專書論述。
ETL(Extract, Transform, Load)是這個領域的關鍵字,用 ETL 去找下去,會找到許多的書籍講怎麼做資料分析
- Extract: 如何在雜亂的原始資料中,找出有用的訊號,並把這些訊號歸檔
例如:把 Apache 的 access.log 轉換成一對對的 (timestamp, session) - Transform: 如何把整理好的訊號,轉換成更好用的中介查詢表格
例如:把前面的 (timestamp, session) 轉換成以小時為單位的存取次數 (time-slice, count) - Load: 把整理好的資料,存到資料庫中。
# pseudo code 應該是錯的,不過你知道我在表達什麼就好了 :P SELECT COUNT(UNIQUE session) FROM http_access GROUP BY DATE_FORMAT(timestamp,'%Y %m %d')變成
SELECT SUM(count) FROM access_counts GROUP BY DATE_FORMAT(time_slice,'%Y %m %d')
假設,你的原始資料有一整年這麼多,總共有四千萬筆(40M),那麼,前者就是把四千萬筆的資料讀出來,然後過濾加總,而後者,由於資料匯入時已先處理過了,所以要處理的只有24 * 365筆資料;而這兩者間所花費的時間差易,會隨著查尋次數而產生重大差異。
試問,若是每次你老闆跟你要資料時,你要跟他說,要等半小時才可以,還是說等我五分鐘? 至於前面的半小時是怎麼算出來的呢?
由於DB運算,瓶頸多是卡在硬碟存取上,在前面的例子中,40M筆資料,假設一筆是1K bytes,那麼,總資料量是 40GB ,從硬碟循序讀取 40GB 出來,所花的時間是 40GB / 40MB/s(硬碟速度) = 17分鐘
MapReduce & DBMS
MapReduce是把資料分拆運算,然後再加總的技術;但是,MapReduce的概念對 RDBMS 並不是什麼新奇的事,在這份IBM的文件中,便清楚的敘述到,DBMS會對 SQL 指令做編譯,DBMS內部就會依資料型態、INDEX檔、CPU Core的數量等,去做最佳化,這些,是Hadoop不會幫你做的。
既然DBMS那麼好那麼,為什麼會有 MapReduce 等技術的出現呢,那是因為,DBMS可以幫你scale up,但是最終,還是會碰上硬體的極限,那麼,這時只有 scale out ,才能解決這問題。 但是 scale out 的程式難度,總是比 scale up 困難,而且,一個不好的分析模式,並不會因為跑的快10倍,就變得比較好;因此, ETL 的概念,在使用 MapReduce 時仍是可以應用的,
講到這邊,什麼時候該用 Hadoop ,什麼時候該選用 DBMS ,應該很清楚了
- 首先,就是要先看你要找的分析訊號是什麼,定好分析的步驟及查詢表格
- 再來,看你的資料量會有多大,做些簡單的計算,看看是不是能在合理的時間內跑完
- 最後,看你這些報表倒底是有多常被取用,若是,這是內部的報表,只要一個月能出一次就好,那麼,Hadoop可能就大才小用;若是,你是要做產品,像Google Analytics做到即時大量分析,那麼你可能就需要用到Hadoop
過去我在 Medio 服務時,公司有幫美國幾大電信商分析 App 的購買行為,由於,這些是每個月才需產生一次的報表,而且,資料的訊號有待我們的 data expert 去尋找,因此,我們的資料專家是把資料倒到 MicroStrategy 再去下指令分析的,並沒有用到 Hadoop
Tuesday, November 22, 2011
Source code of my Scala on Android Project.
把前面幾回講的用Scala寫的Android程式,放上 bitbucket 了,有興趣的可以自己去下載來看。
https://bitbucket.org/bluetang/android-taipei-disaster
https://bitbucket.org/bluetang/android-taipei-disaster
Wednesday, November 16, 2011
My experience with Scala on Android
用Scala開發Android,是個滿有趣的經驗,大致上來因為我切入的時間點較晚,所以大部份的問題已經被前人所解決,就用 https://github.com/jberkel/android-plugin 把專案用 sbt 開好後,就可以開始寫 android.
我用的 IDE 是 intellij 11 EAP,用 sbt-idea 把 idea project 設好後,把預設的 asset pa th 從 .idea_module 改成 src/main ,就可以開始開發了。
在開發上,除了前一回碰上的proguard問題外,我還碰上另一個比較嚴重的問題-不能在Scala裡寫 AsyncTask
這問題跟SI-3622 SI-3494有關,看來是在 Scala 2.8解掉的問題,2.9又跑回來了,我這邊看到的狀況是
會被Scala compiler專換成
而底下的code,則是scala compiler會吐出 overides nothing.
不管怎樣,都跟Android要求的
解決方法是在 java 裡寫個 bridge interface ,幫 scala compiler 搞不定的東西,在這個 interface 裡定意好
我用的 IDE 是 intellij 11 EAP,用 sbt-idea 把 idea project 設好後,把預設的 asset pa th 從 .idea_module 改成 src/main ,就可以開始開發了。
在開發上,除了前一回碰上的proguard問題外,我還碰上另一個比較嚴重的問題-不能在Scala裡寫 AsyncTask
這問題跟SI-3622 SI-3494有關,看來是在 Scala 2.8解掉的問題,2.9又跑回來了,我這邊看到的狀況是
override protected def doInBackground(params: Params*): Result
會被Scala compiler專換成
public Result doInBackground(Seq params)
public Result doInBackground(Params[] params)
而底下的code,則是scala compiler會吐出 overides nothing.
override protected def doInBackground(params: Array[Params]): Result
不管怎樣,都跟Android要求的
protected Result doInBackground(Params[] params)不同,所以在runtime時會跑出NoSuchMethodError.解決方法是在 java 裡寫個 bridge interface ,幫 scala compiler 搞不定的東西,在這個 interface 裡定意好
public abstract class SAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
protected abstract Result doInBackground(Seq<Params> params);
@Override
protected Result doInBackground(Params... paramses) {
return doInBackground(JavaConversions.asScalaBuffer(Arrays.asList(paramses)));
}
}
Tuesday, November 15, 2011
Data Modeling With Jackson Json and Scala - Proguard
關於Proguard,官方的網頁是這麼自述的:
在Mobile App開發的時候,多會用proguard把沒有用到的程式碼濾除,並把變數名稱跟函式名稱用更精簡的字串來取代,這樣編出來的程式會更小,以我用Scala開發的Android程式,使用的的函式庫大小超過10MB,但是用proguard編出來的class.min.jar只有1.7MB,8.xMB的scala-runtime.jar一大票沒用到的功能都被移掉了。
這麼好的功具當然也有他的問題存在,proguard是使用靜態分析的方式,去追縱看有那些程式碼會被執行到,有那些程式碼是不會被碰到的可以被移除的;但由於Jackson是使用Reflection的方式去取得物件的屬性及是用reflection的方式去生成物件,因此,這些行為並不會被proguard偵測到,反而是被認為是dead code而被移除掉。
另外proguard會把變數名稱改寫,這也是跟jackson不相容的地方,當getXxxx被改寫成gY,jackson自然無法知道這是Xxxx的getter,因此若是要在proguard的環境下使用jackson,obfuscator是要被關掉的。
底下這邊,是我在台北積水地圖內,用的proguard設定檔
ProGuard is a free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. Finally, it preverifies the processed code for Java 6 or for Java Micro Edition.
在Mobile App開發的時候,多會用proguard把沒有用到的程式碼濾除,並把變數名稱跟函式名稱用更精簡的字串來取代,這樣編出來的程式會更小,以我用Scala開發的Android程式,使用的的函式庫大小超過10MB,但是用proguard編出來的class.min.jar只有1.7MB,8.xMB的scala-runtime.jar一大票沒用到的功能都被移掉了。
這麼好的功具當然也有他的問題存在,proguard是使用靜態分析的方式,去追縱看有那些程式碼會被執行到,有那些程式碼是不會被碰到的可以被移除的;但由於Jackson是使用Reflection的方式去取得物件的屬性及是用reflection的方式去生成物件,因此,這些行為並不會被proguard偵測到,反而是被認為是dead code而被移除掉。
另外proguard會把變數名稱改寫,這也是跟jackson不相容的地方,當getXxxx被改寫成gY,jackson自然無法知道這是Xxxx的getter,因此若是要在proguard的環境下使用jackson,obfuscator是要被關掉的。
底下這邊,是我在台北積水地圖內,用的proguard設定檔
proguardOption in Android :=
("-dontoptimize -dontpreverify -dontobfuscate" // shrinking only
:: "-dontskipnonpubliclibraryclassmembers" // keep Jackson's internal classes
:: "-dontskipnonpubliclibraryclasses" // keep Jackson's internal classes
:: "-keepattributes *Annotation*." // keep Jackson Json Annotations.
:: "-keep class org.codehaus.jackson.**"
:: "-keep class com.bluetangstudio.android.model.**"
:: "-keep class com.bluetangstudio.searchcloud.client.**"
:: """-keep class com.bluetangstudio.searchcloud.client.** {
(...);
public static ** valueOf(...);
}"""
:: """-keep class com.bluetangstudio.** {
void set*(***);
void set*(int, ***);
boolean is*();
boolean is*(int);
*** get*();
*** get*(int);
}"""
:: """-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}"""
:: """-keepclassmembers class * {
** MODULE$;
}"""
:: "-keep public class org.xml.sax.EntityResolver"
:: "-keep public class scala.Either"
:: "-keep public class scala.Function1"
:: "-keep public class scala.Function2"
:: "-keep public class scala.Tuple2"
:: "-keep public class scala.collection.Iterable"
:: "-keep public class scala.PartialFunction"
:: "-keep public class scala.collection.Seq"
:: "-keep public class scala.collection.TraversableOnce"
:: "-keep public class scala.collection.generic.CanBuildFrom"
:: "-keep public class scala.collection.immutable.Map"
:: "-keep public class scala.collection.immutable.List"
:: "-keep public class scala.collection.mutable.StringBuilder"
:: "-keep public class scala.Predef$$less$colon$less"
:: "-keep public class scala.math.Numeric"
:: "-keep public class scala.math.Ordering"
:: "-keep public class scala.reflect.ClassManifest"
:: "-keep public class scala.runtime.IntRef"
:: "-keep public class scala.runtime.BooleanRef"
:: "-keep public class scala.runtime.AbstractFunction1"
:: "-keep public class * extends android.app.Activity"
:: "-keep public class * extends android.app.Application"
:: "-keep public class * extends android.app.Service"
:: "-keep public class * extends android.appwidget.AppWidgetProvider"
:: "-keep public class * extends android.content.BroadcastReceiver"
:: "-keep public class * extends android.content.BroadcastReceiver"
:: "-keep public class * extends android.app.backup.BackupAgentHelper"
:: "-keep public class * extends android.content.ContentProvider"
:: "-keep public class * extends android.view.View"
:: "-keep public class * extends android.preference.Preference"
:: Nil
) mkString " "
Data Modeling With Jackson Json and Scala - lazy val
Scala中有個好用的功能是 lazy val - lazy initialized variable ,然而,Scala在實作這功能時,是會加入一個
若是這欄位沒有被從生成的 json 中替除的話,那麼,讀回來的 scala object 中,所有的lazy val,都會仍是 null ,不會被初始化。
這個問題,只有做好unit-testing可以避免,因為這個欄位是 public 的,所以不需要setter or constructor, Jackson 就可以直接把值設定進去,只用unit-test仔細檢查生成的,確定沒有預料之外的欄位被寫入到 json 中,才可以免去後來在 runtime 發生不可預料的問題。
至於怎麼去除這欄位呢,只要在class上標上
public volatile int bitmap$0;
若是這欄位沒有被從生成的 json 中替除的話,那麼,讀回來的 scala object 中,所有的lazy val,都會仍是 null ,不會被初始化。
這個問題,只有做好unit-testing可以避免,因為這個欄位是 public 的,所以不需要setter or constructor, Jackson 就可以直接把值設定進去,只用unit-test仔細檢查生成的,確定沒有預料之外的欄位被寫入到 json 中,才可以免去後來在 runtime 發生不可預料的問題。
至於怎麼去除這欄位呢,只要在class上標上
@JsonIgnoreProperties({"bitmap$0"})即可
Data Modeling With Jackson Json and Scala - Polymorphic Types
關於 Jackson Json 對多型的支援,原開發者已經有一篇 blog 很清楚的解釋該怎麼做,因此,我只寫下在 scala 上面要注意的眉角。
首先,我先寫下理想中,Jackson在Scala上該怎麼使用,在來談,為什麼不能這麼用
在 scala 2.8.0 上面,我碰上的問題是不能在annotation內使用 java enum 如:
在 scala 2.9.1 上,上面的問題消失了,不過碰上另一個問題,當一個 annotation 上面有兩個 java enum,scala compiler 及 java compiler 都不會吐出錯誤,但是annotations從bytecode中消失了,SI-5165中紀錄了這問題。
不管怎樣,在現在這時間,若是你想用Jackson + Scala來處理多型,那麼,最頂層類別,一定要用Java來寫,才不會碰上上述兩個問題。
首先,我先寫下理想中,Jackson在Scala上該怎麼使用,在來談,為什麼不能這麼用
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(Array(
new JsonSubTypes.Type(name = "flood", value = classOf[Flood]),
new JsonSubTypes.Type(name = "mountainslide", value = classOf[MountainSlide])
))
trait Disaster {}
case class Flood @JsonCreator()(
@BeanProperty @JsonProperty("event") event: String,
@BeanProperty @JsonProperty("address") address: String
) extends Disaster
case class MountainSlide @JsonCreator()(
@BeanProperty @JsonProperty("event") event: String,
@BeanProperty @JsonProperty("landmark") landmark: String
) extends Disaster
在 scala 2.8.0 上面,我碰上的問題是不能在annotation內使用 java enum 如:
use = JsonTypeInfo.Id.NAME 在 scala 2.9.1 上,上面的問題消失了,不過碰上另一個問題,當一個 annotation 上面有兩個 java enum,scala compiler 及 java compiler 都不會吐出錯誤,但是annotations從bytecode中消失了,SI-5165中紀錄了這問題。
不管怎樣,在現在這時間,若是你想用Jackson + Scala來處理多型,那麼,最頂層類別,一定要用Java來寫,才不會碰上上述兩個問題。
Data Modeling With Jackson Json and Scala
我用Scala有一年半左右,第一次看brianhsu介紹 case class ,就對 Scala 的簡潔著迷,當然,經過了一年半後,許多的面紗揭開後,Scala在與Java函式庫整合的小問題就跑出來了,接下來,我會寫寫怎麼用Jackson Json及Scala來設計一個Json WebService API的Model Objects.
Scala的case class在經過Scala compiler轉換後,就會生成一個POJO,例如這樣一個簡單的case class
會被轉換成如下的Java Code
這個POJO跟一般POJO的不同處在於
這在一般使用上,不是問題,反而是優點,然而,當於 Java binding framework 如 Jackson Json 或 JAXB 要做整合時,就變成問題了,原因在於,多數的 java binding frameworks 在將資料轉回Java Object時,多是呼叫沒代參數的default constructor,然後再呼叫setter(s)把物件的屬性傳進去。
另一個問題則是,多數的java binding frameworks在偵側那些屬性是serializable時,要麻是用java annotation一個個屬性去標,要不然就是用 refection 去找 getXxxx()
而好死不死的plain scala case class沒有辦法滿足上面兩個條件,因此,我們要想辦法繞過這兩個問題。第二個問題比較好解,只要在屬性上標上 @scala.reflect.BeanProperty ,Scala compiler就會自動幫你把你的屬性加上 getXxxx() 及 setXxxx(...)。
第一的問題,在Jackson Json的解法是用 @JsonCreator 去指定不代變數default constructor的替代品,然而,在這邊我們須要把每一個屬性的名稱,用 @JsonProperty("xxx") 標在 constructor 的參數上,這是因為,Java compiler在編譯時,會把 parameter 的名稱擦去,如 public Disaster(String event) 在經過編譯後的 bytecode 中只剩下 public Disaster(String p1),因此我們須要用java annotation來強制幫參數取個名稱,這樣,當Jackson在把 Json String 轉成Object時,才知道該把那個json object的屬性對到java object的那個屬性之上。
當我們的Scala case class已經是Jackson Json Serializable後,我們要怎麼樣把scala case class object轉成json string再轉回來呢,請看底下的例子。
這邊我們會碰上Scala較Java語法上比較沒有那麼漂亮的地方,在Java裡,若是函式支援generic且又有個Class的參數,那麼,我們不用把傳兩次進去這個函式,如
因此在Scala的這邊,我會寫個JsonSerializer來把 scala 這邊的語法弄漂亮些
最後,
case class
Scala的case class在經過Scala compiler轉換後,就會生成一個POJO,例如這樣一個簡單的case class
case class Disaster(event: String)
會被轉換成如下的Java Code
public class Disaster
implements ScalaObject, Product, Serializable
{
public String event()
{
return event;
}
public int hashCode()
{
return ScalaRunTime$.MODULE$._hashCode(this);
}
public String toString()
{
return ScalaRunTime$.MODULE$._toString(this);
}
public boolean equals(Object obj)
{
//removed
}
public Disaster(String event)
{
this.event = event;
super();
scala.Product.class.$init$(this);
}
private final String event;
}
這個POJO跟一般POJO的不同處在於
- immutable object,沒有辦法去更改這物件的屬性
- getter的命名不是Java通用的 getXxxx() 而是 xxxx()
這在一般使用上,不是問題,反而是優點,然而,當於 Java binding framework 如 Jackson Json 或 JAXB 要做整合時,就變成問題了,原因在於,多數的 java binding frameworks 在將資料轉回Java Object時,多是呼叫沒代參數的default constructor,然後再呼叫setter(s)把物件的屬性傳進去。
另一個問題則是,多數的java binding frameworks在偵側那些屬性是serializable時,要麻是用java annotation一個個屬性去標,要不然就是用 refection 去找 getXxxx()
而好死不死的plain scala case class沒有辦法滿足上面兩個條件,因此,我們要想辦法繞過這兩個問題。第二個問題比較好解,只要在屬性上標上 @scala.reflect.BeanProperty ,Scala compiler就會自動幫你把你的屬性加上 getXxxx() 及 setXxxx(...)。
case class Disaster(@BeanProperty event: String)
第一的問題,在Jackson Json的解法是用 @JsonCreator 去指定不代變數default constructor的替代品,然而,在這邊我們須要把每一個屬性的名稱,用 @JsonProperty("xxx") 標在 constructor 的參數上,這是因為,Java compiler在編譯時,會把 parameter 的名稱擦去,如 public Disaster(String event) 在經過編譯後的 bytecode 中只剩下 public Disaster(String p1),因此我們須要用java annotation來強制幫參數取個名稱,這樣,當Jackson在把 Json String 轉成Object時,才知道該把那個json object的屬性對到java object的那個屬性之上。
case class Disaster @JsonCreator()(
@BeanProperty @JsonProperty("event") event: String
)
當我們的Scala case class已經是Jackson Json Serializable後,我們要怎麼樣把scala case class object轉成json string再轉回來呢,請看底下的例子。
val disaster = new Disaster("88 typhoon")
val mapper = new org.codehaus.jackson.map.ObjectMapper()
val json = mapper.writeValueAsString(disaster)
assert(json === """{"event": "88typhon"}"""}
val copy = mapper.readValue[Disaster](json, classOf[[Disaster])
assert(copy === disaster)
這邊我們會碰上Scala較Java語法上比較沒有那麼漂亮的地方,在Java裡,若是函式支援generic
Disaster copy = mapper.readValue(json, Disaster.class)
因此在Scala的這邊,我會寫個JsonSerializer來把 scala 這邊的語法弄漂亮些
object JsonSerializer {
private val mapper = new ObjectMapper
def fromJson[T <: AnyRef](jsonString: String)(implicit m: Manifest[T]): T = {
mapper.readValue(jsonString, m.erasure).asInstanceOf[T];
}
}
// readValue the scala way.
val disaster = JsonSerializer.fromJson[Disaster](json)
最後,
@JsonCreator不一定是要標在constructor上面,他也可以標在static method上面,這樣一來,可以讓你做到scala constructor無法做到的一些處理case class Disaster(
@BeanProperty event: String,
location: Option[GeoPoint]) {
/** helper method for jackson json */
def getLocation = location.getOrElse(null)
}
object Disaster {
@JsonCreator
def newInstance(
@Property("event") event: String,
@Property("location") location: GeoPoint): Disaster = {
val locationOption = if (location == null) {
None
} else {
Some(location)
}
return new Disaster(event, locationOption)
}
}
下一回,我們會看,如何讓Jackson Json能夠對多型的物件做處理
台北市政府公開資料(三)- 台北積水地圖
花了三個禮拜,學習怎麼開發Android程式,在沒買開發機的狀況下,在模擬器上完成了我的第一個Android App - 台北積水地圖


資料來源一樣是從台北市政府所來,原始資料檔可從『臺北市歷年積水紀錄圖』下載。我把資料檔的座標系轉換(TW67 -> WSG84)後,就塞到跟前一回一樣的後端去做處理,剩下的就都是前端的工作了。
在前端UI開發上,依以前開發的習慣,先把wireframe(假UI)畫好才動工,找了找一些畫UI的工具,最後買了keynotopia出的工具,這傢伙也是個奇人,開start-up不到三小時就開始獲利;在畫假UI時就發現,傳統上我們用點來代表一個物件,在畫地圖(羞~)時,並不能合理的表現許多地理圖型。
然而,若是我們要把地理圖型直接畫在地圖上讓使用者直接點開,那麼,我們又要處理重疊的狀況,這在淹水地圖上就特別麻煩,因為常淹水的區塊會互相重疊多次,或像納利颱風,一次淹掉了一大塊的區塊,在處理點選時,我們要怎麼知道使用者點選的是那一層的資料?
而在顯示上,要怎麼處理重疊的區塊,讓使用者知道,某個地點曾淹水多次?一個選項是用顏色來做,重疊的部份用加深的顏色來顯式;最後,我們還是回歸傳統的點座標的方式,來表示淹水的區域,等使用者點開後,才用色塊畫出實際淹水的地區。等下次改版時,等我對Android 2D再熟一些時,或許會嘗試用顏色的方式來做呈現。
接下來,我會在有空的時候,再開發一個程式,把台北市的土地分區(住一、住二、商用...)資料,跟土地的地號資料,做成個台北都更地圖,若各位有類似的需求,請與我聯絡
資料來源一樣是從台北市政府所來,原始資料檔可從『臺北市歷年積水紀錄圖』下載。我把資料檔的座標系轉換(TW67 -> WSG84)後,就塞到跟前一回一樣的後端去做處理,剩下的就都是前端的工作了。
在前端UI開發上,依以前開發的習慣,先把wireframe(假UI)畫好才動工,找了找一些畫UI的工具,最後買了keynotopia出的工具,這傢伙也是個奇人,開start-up不到三小時就開始獲利;在畫假UI時就發現,傳統上我們用點來代表一個物件,在畫地圖(羞~)時,並不能合理的表現許多地理圖型。
然而,若是我們要把地理圖型直接畫在地圖上讓使用者直接點開,那麼,我們又要處理重疊的狀況,這在淹水地圖上就特別麻煩,因為常淹水的區塊會互相重疊多次,或像納利颱風,一次淹掉了一大塊的區塊,在處理點選時,我們要怎麼知道使用者點選的是那一層的資料?
而在顯示上,要怎麼處理重疊的區塊,讓使用者知道,某個地點曾淹水多次?一個選項是用顏色來做,重疊的部份用加深的顏色來顯式;最後,我們還是回歸傳統的點座標的方式,來表示淹水的區域,等使用者點開後,才用色塊畫出實際淹水的地區。等下次改版時,等我對Android 2D再熟一些時,或許會嘗試用顏色的方式來做呈現。
接下來,我會在有空的時候,再開發一個程式,把台北市的土地分區(住一、住二、商用...)資料,跟土地的地號資料,做成個台北都更地圖,若各位有類似的需求,請與我聯絡
Monday, October 17, 2011
台北市政府公開資料(二)- 按地理位址搜尋資料
多數的搜尋技術,在處理地理位址時,只處理地點的資料,例如餐廳、百貨公司等小地理區塊等,但是這些地點搜詢技術,並不適合用在處理大區塊範圍的資料,如行政區及學區等。
如多數外國網站用的坐標轉行政區的GeoNames,他在搜尋某個座標目前所屬的行政區時,是計算座標點與附近行政區中心的距離來猜測,但是行政區往往不是正圓型的,行政區的劃分多是依自然環境(如河川)來切割的,所以,既始某個座標離行政區甲較近,但是,在實務上則是被劃分到乙行政區去的;類似的例子還有學區等
另一類無法用點去描述 的,是線型的資料,如登山步道、腳踏車道,當我們登山時,我們不一定要從起點開始爬,而是可以從中間點開始加入,因此,當我們在找附近所有的登山步道時,該是計算所有線型資料與目前座標點的最短距離,而非是算起點與目前座標點的距離。
依據了這套需求,我在我公司的產品上增加了對地理區塊搜尋的功能,讓用戶能對點(Point)、線(LineString),多邊型(Polygon)等資料,做搜尋。
底下的連結,是搜詢台北101大樓所在的行政區,大家可以玩玩看
{"results": [{"id": 302,"properties": {"AREA": 522872.00372,"NEW": 6300200001,"FULL": "臺北市信義區西村里","PERF_ID": 63002,"COUN_ID": "6300200","CPID": "63002","CPTID": "6300200","CPTVID": "6300200001","NPID": 63002,"NPTID": 6300200,"NPTVID": 6300200001,"PNAME": "臺北市","TNAME": "信義區","VNAME": "西村里","PTVNAME": "臺北市信義區西村里","PTNAME": "臺北市信義區","TVNAME": "信義區西村里","TM2_MAX_X": 306528.415,"TM2_MAX_Y": 2770435.21,"TM2_MIN_X": 305428.311,"TM2_MIN_Y": 2769739.57,"MAX_X": 0,"MAX_Y": 0,"MIN_X": 0,"MIN_Y": 0}}],"metadata": {"searchQuery": {"queries": [],"constraints": [{"@type": "location","field": "geometry","location": "25.033611,121.565","radius": 1}],"pagination": {"start": 0,"count": 1}},"start": 0,"count": 1,"total": 1}}
Thursday, October 6, 2011
轉換台北市政府公開資料的格式
在台北市政府公開的資料中,有一大部份是用shapefile的格式開放的,如何把這些資料轉換出來,變成是一個問題,在這邊把我怎麼把TWD67座標的shapefile,轉換成WSG84的 GeoJSON格式。
首先,請參考前文把里介圖在QGIS中打開。
再來按這邊講的,把TWD67的定義加入到 QGIS 中
接著,選Layer Property -> Set Layer CRS -> TWD 67
然後,指定專案用的CRS定意,FIle -> Project Property -> CRS -> WSG 84
最後,是把Layer另存新檔,Layer -> Save As -> File Format: GeoJSON, Encoding: UTF-8
這樣,就可以把shapefile的內容匯出來,變成像下述的格式
{ "type": "Feature", "id": 0, "properties": { "AREA": 16326625.055040, "NEW": 6301200042.000000, "FULL": "臺北市北投區湖田里", "PERF_ID": 63012.000000, "COUN_ID": "6301200", "CPID": "63012", "CPTID": "6301200", "CPTVID": "6301200042", "NPID": 63012.000000, "NPTID": 6301200.000000, "NPTVID": 6301200042.000000, "PNAME": "臺北市", "TNAME": "北投區", "VNAME": "湖田里", "PTVNAME": "臺北市北投區湖田里", "PTNAME": "臺北市北投區", "TVNAME": "北投區湖田里", "TM2_MAX_X": 306973.245000, "TM2_MAX_Y": 2789383.300000, "TM2_MIN_X": 301490.096000, "TM2_MIN_Y": 2783263.350000, "MAX_X": 0.000000, "MAX_Y": 0.000000, "MIN_X": 0.000000, "MIN_Y": 0.000000 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 121.522589, 25.174993 ], [ 121.522587, 25.174995 ], [ 121.522557, 25.175033 ], [ 121.522408, 25.175170 ], [ 121.522317, 25.175324 ], [ 121.522314, 25.175367 ], [ 121.522323, 25.175569 ], [ 121.522315, 25.175789 ], [ 121.522322, 25.175950 ], [ 121.522270, 25.176082 ], [ 121.522158, 25.176302 ], [ 121.522043, 25.176548 ], [ 121.522002, 25.176598 ], [ 121.521997, 25.176648 ], [ 121.522278, 25.177273 ], [ 121.522359, 25.177825 ], [ 121.522384, 25.178200 ], [ 121.522338, 25.178332 ], [ 121.522127, 25.178615 ], [ 121.521821, 25.178986 ], [ 121.521780, 25.179026 ], [ 121.521691, 25.179134 ], [ 121.522349, 25.179671 ], [ 121.522623, 25.179892 ], [ 121.522810, 25.179997 ], [ 121.522949, 25.180089 ], [ 121.523101, 25.180244 ], [ 121.523337, 25.180539 ], [ 121.523425, 25.180655 ], [ 121.523550, 25.180683 ], [ 121.523641, 25.180779 ], [ 121.523652, 25.180779 ], [ 121.523816, 25.1809
首先,請參考前文把里介圖在QGIS中打開。
再來按這邊講的,把TWD67的定義加入到 QGIS 中
接著,選Layer Property -> Set Layer CRS -> TWD 67
然後,指定專案用的CRS定意,FIle -> Project Property -> CRS -> WSG 84
最後,是把Layer另存新檔,Layer -> Save As -> File Format: GeoJSON, Encoding: UTF-8
這樣,就可以把shapefile的內容匯出來,變成像下述的格式
{ "type": "Feature", "id": 0, "properties": { "AREA": 16326625.055040, "NEW": 6301200042.000000, "FULL": "臺北市北投區湖田里", "PERF_ID": 63012.000000, "COUN_ID": "6301200", "CPID": "63012", "CPTID": "6301200", "CPTVID": "6301200042", "NPID": 63012.000000, "NPTID": 6301200.000000, "NPTVID": 6301200042.000000, "PNAME": "臺北市", "TNAME": "北投區", "VNAME": "湖田里", "PTVNAME": "臺北市北投區湖田里", "PTNAME": "臺北市北投區", "TVNAME": "北投區湖田里", "TM2_MAX_X": 306973.245000, "TM2_MAX_Y": 2789383.300000, "TM2_MIN_X": 301490.096000, "TM2_MIN_Y": 2783263.350000, "MAX_X": 0.000000, "MAX_Y": 0.000000, "MIN_X": 0.000000, "MIN_Y": 0.000000 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 121.522589, 25.174993 ], [ 121.522587, 25.174995 ], [ 121.522557, 25.175033 ], [ 121.522408, 25.175170 ], [ 121.522317, 25.175324 ], [ 121.522314, 25.175367 ], [ 121.522323, 25.175569 ], [ 121.522315, 25.175789 ], [ 121.522322, 25.175950 ], [ 121.522270, 25.176082 ], [ 121.522158, 25.176302 ], [ 121.522043, 25.176548 ], [ 121.522002, 25.176598 ], [ 121.521997, 25.176648 ], [ 121.522278, 25.177273 ], [ 121.522359, 25.177825 ], [ 121.522384, 25.178200 ], [ 121.522338, 25.178332 ], [ 121.522127, 25.178615 ], [ 121.521821, 25.178986 ], [ 121.521780, 25.179026 ], [ 121.521691, 25.179134 ], [ 121.522349, 25.179671 ], [ 121.522623, 25.179892 ], [ 121.522810, 25.179997 ], [ 121.522949, 25.180089 ], [ 121.523101, 25.180244 ], [ 121.523337, 25.180539 ], [ 121.523425, 25.180655 ], [ 121.523550, 25.180683 ], [ 121.523641, 25.180779 ], [ 121.523652, 25.180779 ], [ 121.523816, 25.1809
Subscribe to:
Posts (Atom)



