2014年1月23日

BeanUtilsBean 的小小應用 2

在利用 BeanUtilsBean 的一點小技巧來解決部份 property 不做 copyProperties() 的需要之後。
如果處理 BigDecimal 的 property 賦值, 而其值為 null 時, 會出現...
Exception in thread "main" org.apache.commons.beanutils.ConversionException: No value specified
at org.apache.commons.beanutils.converters.BigDecimalConverter.convert(BigDecimalConverter.java:...)
...


解決問題前, 先從 BeanUtils 取得 BeanUtilsBean.getInstance() 來處理 copyProperties() 的 source 中看到:

    private static final ContextClassLoaderLocal beansByClassLoader = new ContextClassLoaderLocal() {

        protected Object initialValue() {
            return new BeanUtilsBean();
        }
    };

實際是使用了預設的 BeanUtilsBean() constructor:

    public BeanUtilsBean() {
        this(new ConvertUtilsBean(), new PropertyUtilsBean());
    }

    public BeanUtilsBean(ConvertUtilsBean convertUtilsBean, PropertyUtilsBean propertyUtilsBean) {
        this.log = LogFactory.getLog(BeanUtils.class);
        this.convertUtilsBean = convertUtilsBean;
        this.propertyUtilsBean = propertyUtilsBean;
    }
明顯的看到它使用了預設的 ConvertUtilsBean() constructor, 並在 deregister() method 中建構各類型的 converter 以便爾後的 properties 賦值處理:

    public ConvertUtilsBean() {
        this.converters.setFast(false);
        deregister();
        this.converters.setFast(true);
    }
    ...
    public void deregister() {
        ...
        register(BigDecimal.class, new BigDecimalConverter());
        ...
    }
    ...
但, 當中 BigDecimalConverter() constructor 是不預設賦值的:
    public BigDecimalConverter() {
        this.defaultValue = null;
        this.useDefault = false;
    }
另一 constructor 則是:
    public BigDecimalConverter(Object defaultValue) {
        this.defaultValue = defaultValue;
        this.useDefault = true;
    }
也就是說, 可以使用另一組 BigDecimalConverter() constructor 來避免上述的 exception 發生:
    BeanUtilsBean defaultNullValueBeanUtils = new BeanUtilsBean(new ConvertUtilsBean() {

        @Override
        public void deregister() {
            super.deregister();
            // 這裡還是預設了 null 為其值
            register(new org.apache.commons.beanutils.converters.BigDecimalConverter(null), BigDecimal.class);
            ...
        }
    }, new PropertyUtilsBean());
    ...
    defaultNullValueBeanUtils.copyProperties(dest, orig);
    ...
done!

2014年1月17日

BeanUtilsBean 的小小應用

Apache Commons BeanUtils 發展有十多年, 累積了很多很棒的功能; 它在當時推動 JavaBean 風潮的時空背景下, 算是主要的推手之一。主要的精神在於讓 class 像 data type 一般, 借由 naming convention 慣例, 使用 attributes(或稱 properties) 的 setter/getter methods 來存取 information, 保有了物件導向理論中封裝特性的彈性與好處。雖說另有一派(人)不同意此一看法,但仍有其存在的必要。

最常見的是, 兩個物件有幾乎相仿的 setter/getter methods 情形下進行 copy;在很早的 COBOL、FoxBase ... 年代, 這些 language 在宣告上或語法上已有支援, 而 4GL 年代也有相同、類似的支援方式:
        LET recordA. * = recordB.*
唯獨當年剛從 internet 中展露頭角的 Java 還沒有"方便"的方式, 尤其是當兩個物件有幾十、甚至上百個 attributes 時, programmer 一定會恨死如:
    ...
    beanA.setAttr1(beanB.getAttr1());
    beanA.setAttr2(beanB.getAttr2());
    beanA.setAttr3(beanB.getAttr3());
    ...
    beanA.setAttr99(beanB.getAttr99());
    ...
這種佔篇幅、又容易打(或複製)錯字而不易察覺的笨挫寫法。

此時, programmer 一定會很感激 BeanUtils 的存在; 它的其中一個最基本的功能:
BeanUtils.copyProperties(destBean, origBean);
它會取得 origBean 中所有 public getXXX() 之類的 methods 回傳值, 找到 destBean 中相對應的 setXXX() methods, 接著叫(調)用並賦值給它。(當然,  origBean 的 getter 回傳型別與對應 destBean 的 setter 參數型別要相容)

當有十幾、廿個 attributes 要賦值, 但僅少數幾個則否; 那要怎樣進行呢?

可從 BeanUtils 的 source code 看來, 它主要是由 BeanUtilsBean class 的 method 進行 copy 功能:
public void copyProperties(Object dest, Object orig)
        throws IllegalAccessException, InvocationTargetException {
        // ...
        copyProperty(dest, name, value);
        // ...
}
所以, 只要製訂這 class 新的 subclass, 並控制 copyProperty() 的處理, 如下所示:
@Override
public void copyProperty(Object bean, String name, Object value)
        throws IllegalAccessException, InvocationTargetException {

        if (!excludeNames.contains(name)) {
                super.copyProperty(bean, name, value);
        }
}
而在其中, 只要先紀錄 excludeNames 即可。

但, 如果沒有重用(re-use)的情形, 定義新的 class 實在會顯得 Java 語言的繁瑣; 索性, 可利用 anonymous inner class 寫法來做就好:
        // ...
        new BeanUtilsBean() {
                private List〈string〉 excludeNames = new ArrayList〈string〉(0);

                BeanUtilsBean setExcludeNames(String[] excludeNames) {
                        if (excludeNames != null && excludeNames.length > 0) {
                                this.excludeNames = Arrays.asList(excludeNames);
                        }
                        return this;
                }

                @Override
                public void copyProperty(Object bean, String name, Object value)
                        throws IllegalAccessException, InvocationTargetException {
                        if (!excludeNames.contains(name)) {
                                super.copyProperty(bean, name, value);
                        }
                }
        }.setExcludeNames(new String[] {"someAttr"})
        .copyProperties(destBean, origBean);
        // ...
如此直接叫(調)用即可。
而其中 setExcludeNames() 回傳 this 的方法, 可以免除自定 class 及其新的 constructor 來紀錄 excludeNames 的麻煩。

2014年1月12日

Grails Scaffolding

Grails 的 Scaffolding 功能自開天闢地以來就有了, 也是介紹入門時的介紹重點之一。

上篇 GORM custom id 另解 就是由 scaffolding plugin 產生出來後所進行的調整; 如果多數 controller 變更情形如同上篇, 那多半會考慮變更 template files, 在產生(generate-all)時就大致已調整完成, 而稍做修改即可。

變更修改前, 須先產生預設的 template files 在專案目錄 src/templates(/scaffolding) 下:
grails> install-templates
接著再以 editor 直修改它, 存檔後會在下次產生另一組 controller/views 時生效。

其實這小撇步早在多年前另一篇 GORM 物件於 update method 後立即顯示資料 也用過。

當時 Grails 的版本及 IDE 工具比較沒有整合的很好, 多半是 console 下打指令來進行開發 (而 IDE 則用來 debug)。目前的版本 v2.3 早已提供 interactive mode, 可利 TAB 鍵進行快速提示與執行; 另外 GGTS IDE 也提供了很好的整合, 以 generate-all 指令為例, 有兩種方式:
(1) 原先提供的 context menu :
  • 在 project 上按右鍵, 選擇 [Grails Tools]
  • 選擇適合的功能 (多半以 [Open Grails Command Prompt])
  • 當然, 最快的方式是按下快捷鍵 [Cmd]+[Opt]+[Shift]+G

(2) [New] function:
  • 直接在 domain class 上按右鍵, 選 [New]
  • 選擇 [Generate Controller and Views], 並輸入 domain class 即可
以上