顯示具有 軟體研發 標籤的文章。 顯示所有文章
顯示具有 軟體研發 標籤的文章。 顯示所有文章

2015年5月26日 星期二

Elasticsearch Class Diagram for Shard Routing and Preference


RotationShardShuffler is initialized with a random seed, then the seed will be increased by one (round-robin) on every certain action.

2015年5月5日 星期二

Interview with LZ4 (Extremely Fast Compression algorithm)

Why is LZ4 so fast?
  1. Fast scan strategy
    • xxHash (Extremely fast non-cryptographic hash algorithm)
  2. Multi-threading
  3. Reduced memory usage fits into Intel x86 L1 cache
    • #define LZ4_MEMORY_USAGE 14 (default 16 KB, see lz4.h)
Reference:
  1. LZ4 - Extremely fast compression
  2. LZ4 Java
    1. JNI (fastest)
    2. Pure Java
    3. Java uses sun.misc.Unsafe API

2015年4月23日 星期四

Performance tuning for Elasticsearch

Some important environment variables about performance tuning for Elasticsearch.
  1. Linux
    1. max_file_descriptors: 65536
    2. vm.max_map_count: 262144 (per process)
  2. elasticsearch.yml
    1. bootstrap.mlockall: true
    2. discovery.zen.minimum_master_nodes
    3. gateway.recover_after_nodes
    4. gateway.expected_nodes
    5. gateway.recover_after_time
    6. index.number_of_shards
    7. index.number_of_replicas
    8. index.refresh_interval
    9. indices.fielddata.cache.size: 50%
    10. indices.breaker.fielddata.limit: 50%
  3. Create index
    1. index.number_of_shards
    2. index.number_of_replicas
    3. index.refresh_interval: 30s
    4. #index.merge.policy.type: tiered
    5. #index.translog.flush_threshold_size
    6. #index.search.slowlog.threshold.query
    7. #index.search.slowlog.threshold.fetch
    8. #index.routing.allocation.include.box_type
    9. #indices.memory.index_buffer_size
    10. #indices.memory.min_index_buffer_size
    11. #indices.memory.min_shard_index_buffer_size
    12. #indices.ttl.interval
  4. Search time tips
    1. _optimize?max_num_segments=1 (less segments more efficiency)
    2. Index per Time Frame
    3. Faking Index per User with Aliases
    4. shard_size & size, by default, shard_size = size * shards
    5. Index Warmer (suffer refresh time)
    6. collect_mode: breadth_first
  5. Fielddata
    1. enable doc_values, it will use mmapfs by default
    2. Fielddata Filtering
    3. Eagerly Loading Fielddata (suffer merge time)
    4. Global ordinals
    5. Eager Global ordinals (suffer refresh time)
You could google above terms for more information.

2014年12月19日 星期五

JVM Monitoring using SNMP in Cacti

Cacti 是個監控工具,可以透過 SNMP 監控主機的運行狀態,預設可以監控 OS 的 CPU、Memory、Disk、Network usage,這裡我們說明透過 JVM-MANAGEMENT-MIB 讓 JVM 也能被 Cacti 監控。

步驟:
  1. 在被監控端安裝 snmpd,參考:Monitoring Performance with Net-SNMP
  2. 在監控端安裝 Cacti,CentOS / Fedora 用戶直接執行 yum install cacti。
  3. 啟動 JVM SNMP 功能,在執行 java command 加上:
    1. -Dcom.sun.management.snmp.port=1610
    2. -Dcom.sun.management.snmp.acl.file=snmp.acl
  4. snmp.acl 從 $JAVA_HOME/jre/lib/management/snmp.acl.template 複製,將最後的 acl、trap 區塊 uncomment。
  5. 修改 snmpd.conf,加上 proxy -v 2c -c public localhost:1610 .1.3.6.1.4.1.42。
  6. 複製 jvm_mem_pool.xmljvm_gc.xml 到 Cacti 的 snmp_queries 目錄下。
  7. 到 Cacti 的管理介面匯入 cacti_host_template_jvm_snmp_host.xml
  8. 成功匯入後會看到 Host Templates 多了一個名為 JVM SNMP Host 的 template,其下有:
    1. Associated Graph Templates
      1. JVM - Classes Count
      2. JVM - Heap Usage
      3. JVM - NonHeap Usage
      4. JVM - Thread Usage
    2. Associated Data Queries
      1. JVM - Garbage Collection Query
      2. JVM - Memory Pool Query
      3. SNMP - Get Mounted Partitions
      4. SNMP - Get Processor Information
      5. SNMP - Interface Statistics
  9. 在 Cacti 新增 Device 時,Host Template 選取 JVM SNMP Host。
  10. 相關檔案可在 https://github.com/linuschien/Cacti 取得。
  11. 範例 JVM Heap Memory Usage 圖:

2014年11月27日 星期四

自訂 Docker image 範例

這個範例從CentOS 6 image上安裝sshd、supervisord、JDK 1.7、Tomcat 7、MySQL,並且預設啟動supervisord,再由supervisord自動帶起sshd、MySQL。
Dockerfile
FROM centos:centos6

MAINTAINER Linus Chien <linus_chien@mail.gss.com.tw>

RUN yum install -y openssh-server java-1.7.0-openjdk wget tar python-setuptools mysql-server

# setup root's password
RUN echo 'root:12345678' | chpasswd

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

# init SSH keys
RUN /sbin/service sshd start && /sbin/service sshd stop

# init MySQL database
RUN /sbin/service mysqld start && /sbin/service mysqld stop

ENV TOMCAT_SITE https://archive.apache.org/dist/tomcat
ENV TOMCAT_MAJOR_VERSION 7
ENV TOMCAT_MINOR_VERSION 7.0.57

RUN wget -q ${TOMCAT_SITE}/tomcat-${TOMCAT_MAJOR_VERSION}/v${TOMCAT_MINOR_VERSION}/bin/apache-tomcat-${TOMCAT_MINOR_VERSION}.tar.gz && \
    wget -qO- ${TOMCAT_SITE}/tomcat-${TOMCAT_MAJOR_VERSION}/v${TOMCAT_MINOR_VERSION}/bin/apache-tomcat-${TOMCAT_MINOR_VERSION}.tar.gz.md5 | md5sum -c - && \
    tar zxf apache-tomcat-*.tar.gz && \
    rm apache-tomcat-*.tar.gz && \
    mv apache-tomcat* /opt/

RUN easy_install supervisor

ADD supervisord.conf /etc/supervisord.conf

EXPOSE 22 3306 8080

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
supervisord.conf
[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=10MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true                ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)

;[program:apache2]
;command = /usr/sbin/httpd -D FOREGROUND
;priority = 30

;[program:tomcat7]
;command = /opt/apache-tomcat-7.0.57/bin/catalina.sh run
;priority = 20

[program:mysql5]
command = /usr/bin/pidproxy /var/run/mysqld/mysqld.pid /usr/bin/mysqld_safe
priority = 10

[program:sshd]
command = /usr/sbin/sshd -D
priority = 40
步驟:
  1. 安裝docker,並啟動,細節請自行參閱官方文件。
  2. 準備好上述兩個檔案:Dockerfile、supervisord.conf
  3. 執行
  • docker build -t cao:tomcat7 . # use your own tag name, don't miss the dot
  • docker run -d --name="tomcat" cao:tomcat7 # assign a unique name to container for convenience
  • docker inspect tomcat # you can find container's IP address
  • ssh {IP} # default root's password setup in Dockerfile

Docker 常用指令

  1. docker search {keyword} # 搜尋雲端images
  2. docker pull {image} # 將雲端image拉回主機
  3. docker build -t {tag} {path} # 使用Dockerfile建立image
  4. docker run {options} {image} {command} # 從image執行指令(建立container)
  5. docker run -i -t {image} /bin/bash # 進入container shell,(ctrl+p) + (ctrl+q)可丟背景離開shell
  6. docker inspect --format='{{.NetworkSettings.IPAddress}}' {container} # 查看container IP
  7. docker images # 查看local所有的images
  8. docker ps -a # 查看container包含已結束的
  9. docker rm {container} # 移除已結束的container
  10. docker rm $(docker ps -a -q) # 移除所有container
  11. docker rmi {image} # 移除image
  12. docker commit {comtainer} {image} # 將container異動過的內容寫入image
  13. docker start {container} # 重新啟動已結束的container
  14. docker attach {container} # 重新接入正在執行container
  15. docker save {image} > {tarball} # 備份image
  16. docker load < {tarball} # 還原image

2014年1月22日 星期三

Install Tomcat 7 as Linux Service

基本上在Tomcat 7裡面已經自帶了安裝成service的必要source code和shell script,但安裝到Linux上,例如:Fedora、CentOS還是需要動一點手腳。

如何compile daemon出來請參考:http://tomcat.apache.org/tomcat-7.0-doc/setup.html#Unix_daemon

只要做完第一個步驟,將compile出來的jsvc執行檔複製到$CATALINA_HOME/bin底下即可,此時在bin目錄下除了jsvc之外,還有一個重要的daemon.sh。

編輯daemon.sh增加兩行comment,如下方所示紅色部分:
daemon.sh
#!/bin/sh
#
# chkconfig: 35 90 10
# description: Start/Stop the Tomcat daemon.
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
35代表level 3和5會啟動這個service,90代表啟動順序,10代表關閉順序。

接下來切換到root身分,到/etc/init.d底下執行:
  1. ln -s $CATALINA_HOME/bin/daemon.sh tomcat
  2. chkconfig -add tomcat
這樣就已經完成service安裝,可以執行以下指令試驗看看開關Tomcat:
  1. /sbin/service tomcat start
  2. /sbin/service tomcat stop
預設service會用tomcat這個user啟動,如果沒有這個user可以先建一個,或是在$CATALINA_HOME/bin/setenv.sh裡面加入TOMCAT_USER=user name就可以了。

2013年4月23日 星期二

Send mail by Spring Integration gateway through Rabbit MQ

Maven dependency
<dependency>
  <groupId>org.springframework.integration</groupId>
  <artifactId>spring-integration-mail</artifactId>
  <version>2.2.3.RELEASE</version>
</dependency>

<dependency>
  <groupId>javax.mail</groupId>
  <artifactId>mail</artifactId>
  <version>1.4.6</version>
</dependency>

<dependency>
  <groupId>org.springframework.integration</groupId>
  <artifactId>spring-integration-amqp</artifactId>
  <version>2.2.3.RELEASE</version>
</dependency>
Gateway interface
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.Header;
import org.springframework.integration.annotation.Payload;
import org.springframework.integration.mail.MailHeaders;

public interface SendMail {

  @Gateway(requestChannel = "input")
  void send(@Header(MailHeaders.FROM) String from, @Header(MailHeaders.TO) String to, @Header(MailHeaders.SUBJECT) String subject, @Payload String message);

}
Spring configuration
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-mail="http://www.springframework.org/schema/integration/mail"
  xmlns:int-amqp="http://www.springframework.org/schema/integration/amqp" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
  xsi:schemaLocation="http://www.springframework.org/schema/integration/amqp http://www.springframework.org/schema/integration/amqp/spring-integration-amqp.xsd
    http://www.springframework.org/schema/integration/mail http://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd
    http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
    http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <rabbit:connection-factory id="rabbitConnectionFactory" host="xxx.xxx.xxx.xxx" virtual-host="/linus" username="linus_chien" password="xxx" />

  <int-amqp:channel id="input" connection-factory="rabbitConnectionFactory" queue-name="linus.test" />

  <int-mail:outbound-channel-adapter id="mailAdaptor" channel="input" host="xxx.xxx.xxx.xxx" />

  <int:gateway id="sendMail" service-interface="SendMail" />

</beans>
Test Case
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SpringIntegrationTest {

  @Autowired
  private SendMail sendMail;

  @Test
  public void test() {
    sendMail.send("linus_chien@xxx.xxx.xxx.xxx", "linus_chien@xxx.xxx.xxx.xxx", "Test", "This is a gateway annotation test.");
  }

}
就這樣沒寫什麼程式就可以發信了。

2013年2月20日 星期三

Online Picture Resizer

http://www.picresize.com/線上提供了轉換圖檔的功能,支援的格式有:
  1. JPG
  2. PNG
  3. GIF
  4. BMP
可以截圖、旋轉、變更大小以及加上特殊效果,從自己的電腦上傳或直接給一個URL也行。

Online escape tools

HtmlEscape.net線上提供了以下幾個功能,還蠻方便的,可以試試看:
  1. Html escape tool
  2. Html un-escape tool
  3. Java/.NET String escaping
  4. JavaScript String escaping

2013年2月7日 星期四

Android + Maven + M2Eclipse All Together

若是想要在Eclipse裡面使用Maven順利開發Android程式,還是需要動一點手腳的,必要工具有以下三項:
  1. android-maven-plugin
  2. m2e-android
  3. maven android archetypes
建議先裝好Spring Tool Suite會比較方便,以下是安裝與使用步驟:
  1. 安裝m2e-android,從Eclipse Marketplace裡面搜尋Android可以找到。

  2. 如果預先裝好STS,那麼只要選第一項就可以了,如果ADT還沒裝,可以在這裡勾選。

  3. 裝好plugin之後重新啟動Eclipse,然後新增一個Maven project,依照以下畫面新增archetype,然後下一步。

  4. 依照自己的需要輸入必要欄位,platform的API就看ADT裡面安裝了哪些版本,預設是16。

  5. 新專案建立之後會有點小問題,一是src/test/java目錄沒開,二是JDK compile版本是1.5,我改成1.6。

  6. 直接Run As Android Application就會看到執行畫面了。

  7. 如果要deploy apk到device上,則需要下maven指令install android:deploy以及設定環境變數。

  8. 設定環境變數ANDROID_HOME,以及在%PATH%中加入%ANDROID_HOME%\tools%ANDROID_HOME%\platform-tools
以上就是全部的設定步驟。

2013年2月5日 星期二

Speedup Android Testing

在開發Android App時,若是使用官方的emulator會非常慢,原因是程式會經過兩次指令的轉換,一次是Java class轉換成dex,接著再把手機CPU的RISC指令轉成一般x86的CISC指令,這就是為什麼會這麼慢的原因。

為了加速開發,有人會直接用真的手機做測試,這裡是介紹AndroVM做為開發的模擬環境,AndroVM是直接用x86重新compile一次Android,執行時不需要做RISC轉CISC指令的動作,所以速度上快非常多,以下說明安裝步驟。
  1. 安裝Oracle VM VirtualBox
  2. 下載AndroVM OVA檔,p代表phone,t代表tablet,我下載了androVM_vbox86p_4.1.1_r4-20121119-gapps-houdini-flash.ova。
  3. 直接double click ova檔案之後開始匯入VM image,會看到以下畫面。

  4. 匯入完畢後修改網路介面卡設定,第一張網路卡的設定如下圖。

  5. 第二張網路卡是用來模擬WiFi,設定如下圖。

  6. 設定完畢後啟動VM,經過幾個步驟設定完之後會看到下面的畫面。

  7. 打開AndroVM Configuration會看到第一張網卡的IP Address,這是用來讓Eclipse連線用的。

  8. 記住IP之後打開命令提示字元,執行adb connect <IP>指令,成功會看到以下畫面。

  9. 回到Eclipse執行Android專案,就會看到App的執行結果直接打在VM的畫面上,這樣就可以進行測試了。

  10. 在程式集的畫面也會看到開發的App出現在桌面上。
AndroVM官方還提供了AndroVM player,可以自由調整手機解析度,不過在這個時間點似乎還有點問題,待官方修正後我再介紹。

Why RabbitMQ

選擇RabbitMQ有以下幾點理由:
  1. Advanced Message Queuing Protocol (AMQP) implementation
  2. Open source and commercially supported
  3. Supports a huge number of developer platforms
  4. Spring AMQP的支援,同時有Java和.NET的版本
  5. Spring Integration的支援
  6. 為Cloud Foundry內建的messaging service

2013年2月4日 星期一

QUnit: A JavaScript Unit Testing framework.

以下是一個使用QUnit撰寫的test case,延續ISO8601.js的測試,主要有5個檔案:
  1. qunit-1.11.0.js
  2. qunit-1.11.0.css
  3. ISO8601.js
  4. testcases.js
  5. qunit.html
前兩項從QUnit官方網站下載。
ISO8601.js
Date.prototype.setISO8601 = function(string) {
    var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})"
            + "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?"
            + "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
    var d = string.match(new RegExp(regexp));

    var offset = 0;
    var date = new Date(d[1], 0, 1);

    if (d[3]) {
        date.setMonth(d[3] - 1);
    }
    if (d[5]) {
        date.setDate(d[5]);
    }
    if (d[7]) {
        date.setHours(d[7]);
    }
    if (d[8]) {
        date.setMinutes(d[8]);
    }
    if (d[10]) {
        date.setSeconds(d[10]);
    }
    if (d[12]) {
        date.setMilliseconds(Number("0." + d[12]) * 1000);
    }
    if (d[14]) {
        offset = (Number(d[16]) * 60) + Number(d[17]);
        offset *= ((d[15] == '-') ? 1 : -1);
    }

    offset -= date.getTimezoneOffset();
    time = (Number(date) + (offset * 60 * 1000));
    this.setTime(Number(time));
};

Date.prototype.getISO8601 = function() {
    return this.getFullYear() + '-' + (this.getMonth() + 1).format(2) + '-'
            + this.getDate().format(2) + 'T' + this.getHours().format(2) + ':'
            + this.getMinutes().format(2) + ':' + this.getSeconds().format(2)
            + '.' + this.getMilliseconds().format(3) + this.getTimezone();
};

Date.prototype.getTimezone = function() {
    var offset = -this.getTimezoneOffset();
    if (offset == 0) {
        return "Z";
    }
    var hour = parseInt(Math.abs(offset / 60)).format(2);
    var minute = Math.abs(offset % 60).format(2)
    var sign = offset > 0 ? "+" : "-";
    return sign + hour + ":" + minute;
};

Number.prototype.format = function(length) {
    var str = "" + this;
    while (str.length < length) {
        str = '0' + str;
    }
    return str;
};
testcases.js
test( "iso8601 test1", function() {
  var date = new Date();
  date.setISO8601('2012-01-30T16:07:03.007Z');
  equal(date.getISO8601(), '2012-01-31T00:07:03.007+08:00');
});

test( "iso8601 test2", function() {
  var date = new Date();
  date.setISO8601('2012-01-30T16:07:03.012+08:00');
  equal(date.getISO8601(), '2012-01-30T16:07:03.012+08:00');
});

test( "iso8601 test3", function() {
  var today = new Date();
  var milliseconds = today.valueOf();
  today.setISO8601(today.getISO8601());
  equal(today.valueOf() == milliseconds, true);
});
qunit.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>QUnit Example</title>
  <link rel="stylesheet" href="qunit-1.11.0.css">
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture"></div>
  <script src="ISO8601.js"></script>
  <script src="qunit-1.11.0.js"></script>
  <script src="testcases.js"></script>
</body>
</html>
Test Result

Enterprise Organization Data Model

這個data model主要是想要能夠應付千變萬化的組織型態,並且能夠符合BPM的應用,主要構思我參考了:
  1. The Data Model Resource Book, Vol. 1: A Library of Universal Data Models for All Enterprises
  2. A Universal Person and Organization Data Model
過程中對於BPM的Unit Role是不是Party Role這件事有過深切的考慮,最後以矛盾法證明出Unit Role並不是Party Role而決定將Party Role從data model中拿掉,證明過程如下:
  1. 假設Unit Role是Party Role而不是Party。
  2. 那麼必須有Party Role Type來記錄哪些Unit Role是屬於同一種Party Role。
  3. 假設Unit Role的Party Role Type為Unit Role Type。
  4. 但是Unit Role Type必須和Unit有relation,所以Unit Role Type其實是一種Party,與第1點假設產生矛盾。
  5. 故Unit Role是一種Party。
最後我們的data model比較像〝A Universal Person and Organization Data Model〞一文中最後一張圖所示:

Struts2 Type Conversion

在Struts1使用Commons BeanUtils做為type conversion的工具,在Struts2則改用OGNL,細節可參考:Type Conversion

不管用哪種技術,它們的目的都是為了在model中直接宣告資料正確的型別,然後透過type conversion在data binding時直接處理資料的型別轉換,如果型別轉換失敗,錯誤訊息會透過ConversionErrorInterceptor塞入field error裡面,不過程式開發的原則應該是盡量避開conversion error的產生才對。

以下我們自訂一個PercentFormatConverter,專門處理具有%符號的數字轉換。
NumberFormatConverter
package com.gss.gmo.cao.struts2.conversion;

import static java.text.NumberFormat.getNumberInstance;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang3.ArrayUtils.isEmpty;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Map;

import org.apache.struts2.util.StrutsTypeConverter;

public class NumberFormatConverter extends StrutsTypeConverter {

    private NumberFormat numberFormat = getNumberInstance();

    public void setNumberFormat(NumberFormat numberFormat) {
        this.numberFormat = numberFormat;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Object convertFromString(Map context, String[] values, Class toClass) {
        if (isEmpty(values)) {
            return null;
        }

        if (!toClass.isArray() && values.length == 1) {
            return convertFromString(context, values[0], toClass);
        }

        String[] numbers = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            try {
                numbers[i] = numberFormat.parse(values[i]).toString();
            } catch (ParseException e) {
                numbers[i] = null;
            }
        }
        return performFallbackConversion(context, numbers, toClass);

    }

    @SuppressWarnings("rawtypes")
    private Object convertFromString(Map context, String value, Class toClass) {
        if (isBlank(value)) {
            return null;
        }

        String number;
        try {
            number = numberFormat.parse(value).toString();
        } catch (ParseException e) {
            number = null;
        }
        return performFallbackConversion(context, number, toClass);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public String convertToString(Map context, Object object) {
        if (object == null) {
            return null;
        }

        if (object instanceof String) {
            return (String) object;
        }

        return numberFormat.format(object);
    }

}
PercentFormatConverter
package com.gss.gmo.cao.struts2.conversion;

import static java.text.NumberFormat.getPercentInstance;

public class PercentFormatConverter extends NumberFormatConverter {

    public PercentFormatConverter() {
        setNumberFormat(getPercentInstance());
    }

}
設定方式就詳閱官方文件,這裡不多做贅述。