顯示具有 Struts2 標籤的文章。 顯示所有文章
顯示具有 Struts2 標籤的文章。 顯示所有文章

2013年2月4日 星期一

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());
    }

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

Struts2 Internationalization (i18n) & Localization (l10n)

細節可參考:Localization
但這裡想要強調的是Resource Bundle Search Order,官方文件說明如下:
Resource bundles are searched in the following order:
  1. ActionClass.properties
  2. Interface.properties (every interface and sub-interface)
  3. BaseClass.properties (all the way to Object.properties)
  4. ModelDriven's model (if implements ModelDriven), for the model object repeat from 1
  5. package.properties (of the directory where class is located and every parent directory all the way to the root directory)
  6. search up the i18n message key hierarchy itself
  7. global resource properties
在程式開發的時候,一開始為了避免同時修改properties可能會盡量從ActionClass.properties做起,但隨著系統開發過程應該將公用的部分逐漸往search order下方移,又或是在SA / SD階段就整理出已確定會公用的部分,直接一開始就寫在package.properties甚至是global resource properties裡面,但不管如何,這些properties的整理雖然很繁瑣,但卻不可忽視。

Why Struts2

Choosing the Right Java Web Development Framework一文中有一些評比的因子可以參考,其中針對Struts2的部分我要做一些補充:
  1. Convention over Configuration:Convention Plugin
  2. AJAX:jQuery Plugin
  3. Navigation Paths:Java based with Convention Plugin、Config Browser Plugin
所以我個人的評比,Struts2還是位居第一名的。

2013年1月31日 星期四

Unit Test to Struts2 Action with Spring

我將展示使用JUnit3和JUnit4兩種測試Struts2 Action的寫法,兩種都會enable Spring以及所有Struts2 plugins。

JUnit3:
TestActionJUnit3Test
import static org.apache.commons.lang.StringUtils.replace;
import lombok.SneakyThrows;
import org.apache.struts2.StrutsSpringTestCase;
import com.opensymphony.xwork2.ActionProxy;

public class TestActionJUnit3Test extends StrutsSpringTestCase {

    @SneakyThrows
    public void test() {
        ActionProxy proxy = getActionProxy("/test/test.action");
        String result = proxy.execute();
        assertEquals("success", result);
        assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());

        executeAction("/test/test");
        assertFalse(((TestAction) findValueAfterExecute("action")).hasFieldErrors());
        assertEquals("Test by Linus", findValueAfterExecute("message"));
    }

    @Override
    protected String[] getContextLocations() {
        return new String[] { "classpath:" + replace(getClass().getName(), ".", "/") + "-context.xml" };
    }

}
上面我將預設讀取的Spring XML路徑改成和@ContextConfiguration一致。

JUnit4:
TestActionJUnit4Test
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.apache.struts2.StrutsSpringJUnit4TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.opensymphony.xwork2.ActionProxy;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TestActionJUnit4Test extends StrutsSpringJUnit4TestCase<TestAction> {

    @Override
    protected String getConfigPath() {
        return "struts-plugin.xml";
    }

    @Test
    public void testExecute() throws Exception {
        ActionProxy proxy = getActionProxy("/test/test.action");
        String result = proxy.execute();
        assertEquals("success", result);
        assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());

        executeAction("/test/test");
        assertFalse(getAction().hasFieldErrors());
        assertEquals("Test by Linus", findValueAfterExecute("message"));
    }

}
上面getConfigPath()可以回傳其它Struts2的XML檔,利用這個method可以啟動其它plugins,但JUnit3的寫法不需要這個動作。

2013年1月23日 星期三

Interview Struts2 Static Content

為了方便將javascript and css file包在jar裡面,Struts2提供了Static Content的機制,但Struts2怎麼處理browser cache的問題呢?答案就在org.apache.struts2.dispatcher.DefaultStaticContentLoader中。

從程式碼我們可以發現Struts2預設會將static content的cache設為一天,也可以設定
Struts XML
<constant name="struts.serve.static.browserCache" value="false" />
來強迫不要cache。

當然我們也可以自己implement org.apache.struts2.dispatcher.StaticContentLoader取代原本的DefaultStaticContentLoader,只要設定
Struts XML
<bean type="org.apache.struts2.dispatcher.StaticContentLoader" class="MyStaticContentLoader" name="myLoader" />
<constant name="struts.staticContentLoader" value="myLoader" />
就可以了。

2013年1月17日 星期四

Struts2 Breadcrumb Bar

麵包屑這功能在Struts2上面我選用了struts2-arianna-plugin,它有以下幾個特點:
  1. 它使用annotation可以快速設定需要記錄到麵包屑的Action。
  2. 它可以選擇麵包屑需不需要回溯,也就是說它會維持麵包屑路徑的相依性。
  3. 它可以讓我們選擇在Action執行前就記錄或者是Action執行完畢後記錄。
  4. 它可以設定麵包屑的記錄筆數。
使用上將它的interceptor掛上來就可以了,但我個人的建議是最好放在整個interceptor stack的最後一個。

2013年1月16日 星期三

How to Obtain Result in Action -- Applying Struts2 PreResultListener

最近為了個資法上路,各家資訊廠商都做了因應,其中最常見的需求就是把系統曾經被使用者匯出去的機敏性資料留下記錄,但是因為資料是會隨時間一直變動的,最保險的方式就是將當下匯出去的資料保存下來,那麼在Struts2裡面該怎樣做到這件事呢?答案是:PreResultListener

設計概念如下:
  1. 利用interceptor判斷Action是否需要Result的內容,如果需要就註冊PreResultListener。
  2. PreResultListener執行時,判斷Result若是可以記錄的型別則替換mock response物件,並且執行Result execute method。
  3. 從mock response物件取得content和content type,並且將原始的response物件還原。
  4. 呼叫callback method將content和content type傳回Action,讓Action自己implement想要處理的動作。
程式如下:
ObtainResultInterceptor
package com.gss.gmo.cao.struts2.interceptor;

import com.gss.gmo.cao.struts2.interceptor.obtainresult.ObtainResultAware;
import com.gss.gmo.cao.struts2.interceptor.obtainresult.ObtainResultListener;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

/**
 * @author linus_chien
 *
 */
public class ObtainResultInterceptor extends AbstractInterceptor {

    private static final long serialVersionUID = 1L;

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        if (invocation.getAction() instanceof ObtainResultAware) {
            invocation.addPreResultListener(new ObtainResultListener());
        }
        return invocation.invoke();
    }

}
ObtainResultListener
package com.gss.gmo.cao.struts2.interceptor.obtainresult;

import static org.apache.commons.lang3.ArrayUtils.contains;

import javax.servlet.http.HttpServletResponse;

import lombok.SneakyThrows;

import org.apache.struts2.StrutsStatics;
import org.apache.struts2.dispatcher.StreamResult;
import org.apache.struts2.views.jasperreports.JasperReportsResult;
import org.springframework.mock.web.MockHttpServletResponse;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.DefaultActionInvocation;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.mock.MockActionInvocation;

/**
 * @author linus_chien
 *
 */
public class ObtainResultListener implements PreResultListener, StrutsStatics {

    private static final Class<?>[] RESULT_TYPES = { StreamResult.class, JasperReportsResult.class };

    @Override
    @SneakyThrows
    public void beforeResult(ActionInvocation invocation, String resultCode) {
        Object action = invocation.getAction();
        if (action instanceof ObtainResultAware) {
            ObtainResultAware obtainResultAction = (ObtainResultAware) action;

            Result result = getResult(invocation);

            if (isNotValidResultType(result)) {
                return;
            }

            MockHttpServletResponse mockResponse = new MockHttpServletResponse();
            HttpServletResponse realResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);
            invocation.getInvocationContext().put(HTTP_RESPONSE, mockResponse);

            try {
                result.execute(invocation);
                byte[] content = mockResponse.getContentAsByteArray();
                String contentType = mockResponse.getContentType();
                obtainResultAction.handleResult(content, contentType);
            } finally {
                invocation.getInvocationContext().put(HTTP_RESPONSE, realResponse);
            }
        }
    }

    private boolean isNotValidResultType(Result result) {
        return result == null || !contains(RESULT_TYPES, result.getClass());
    }

    @SneakyThrows
    private Result getResult(ActionInvocation invocation) {
        Result result;
        if (!isMock(invocation)) {
            result = ((DefaultActionInvocation) invocation).createResult();
        } else {
            result = invocation.getResult();
        }
        return result;
    }

    private boolean isMock(ActionInvocation invocation) {
        return invocation instanceof MockActionInvocation;
    }

}
ObtainResultAware
package com.gss.gmo.cao.struts2.interceptor.obtainresult;

/**
 * @author linus_chien
 *
 */
public interface ObtainResultAware {

    void handleResult(byte[] content, String contentType);

}

2013年1月15日 星期二

Integrate Bean Validation (JSR303) with Struts2 -- SpringI18nInterceptor

Integrate Bean Validation (JSR303) with Struts2 -- Outline
JSR303本身就有提供i18n的功能,但是因為我們要和Struts2整合,所以i18n的部分必須和Struts2同步,這支interceptor的功能就是用來取得Struts2目前的locale,然後傳給Spring,須注意這支interceptor的位置必須在Struts2 i18n interceptor之後。
com.gss.gmo.cao.struts2.interceptor.SpringI18nInterceptor
package com.gss.gmo.cao.struts2.interceptor;

import java.util.Locale;

import org.springframework.context.i18n.LocaleContextHolder;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

/**
 * Populate
 * org.springframework.context.i18n.LocaleContextHolder.setLocale(Locale) by the
 * result from com.opensymphony.xwork2.interceptor.I18nInterceptor.
 * 
 * @author linus_chien
 * 
 */
public class SpringI18nInterceptor extends AbstractInterceptor {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        Locale locale = invocation.getInvocationContext().getLocale();
        LocaleContextHolder.setLocale(locale);
        String result = invocation.invoke();
        return result;
    }

}

Integrate Bean Validation (JSR303) with Struts2 -- Annotation & ValidateMessageFormatter

Integrate Bean Validation (JSR303) with Struts2 -- Outline
我提供了兩個annotation和一個interface,功能如下:
  1. ValidateFieldConfig:若沒設定,預設是驗證整個Action,用此annotation可以只驗證想要驗證的field。
  2. ValidateGroupConfig:搭配JSR303的group使用。
  3. ValidateMessageFormatter:當Action有implement此interface時,進行callback,讓Action自己決定error message format。
com.gss.gmo.cao.struts2.interceptor.validation.ValidateFieldConfig
package com.gss.gmo.cao.struts2.interceptor.validation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * Used by SpringValidationInterceptor. The intercepter will validate entire
 * action if ValidateFieldConfig not presented, or all fields one by one
 * configured in ValidateFieldConfig.
 *
 * @author linus_chien
 *
 */
@Documented
@Target(value = METHOD)
@Retention(value = RUNTIME)
public @interface ValidateFieldConfig {

    /**
     * Field names in action to validate.
     *
     * @return
     */
    String[] fields() default {};

}
com.gss.gmo.cao.struts2.interceptor.validation.ValidateGroupConfig
package com.gss.gmo.cao.struts2.interceptor.validation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.groups.Default;

/**
 * Applying Bean Validation Groups.
 *
 * @author linus_chien
 *
 */
@Documented
@Target(value = METHOD)
@Retention(value = RUNTIME)
public @interface ValidateGroupConfig {

    /**
     * @return
     */
    Class<?>[] groups() default { Default.class };

}
com.gss.gmo.cao.struts2.interceptor.validation.ValidateMessageFormatter
package com.gss.gmo.cao.struts2.interceptor.validation;

public interface ValidateMessageFormatter {

    String format(String fieldName, String errorMessage);

}

Integrate Bean Validation (JSR303) with Struts2 -- Configuration

Integrate Bean Validation (JSR303) with Struts2 -- Outline
有三個主要的設定,一是設定Validator instance,implementation可以使用Hibernate Validator;另一個是設定SpringValidationInterceptor和SpringI18nInterceptor,三者我們都交給Spring控管;最後在struts.xml中設定interceptor,並套用到Action就可以了。
Spring XML
<bean id="gmo.cao.validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
Spring Configuration
package com.gss.gmo.cao.struts2.spring;

import javax.validation.Validator;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.gss.gmo.cao.struts2.interceptor.SpringI18nInterceptor;
import com.gss.gmo.cao.struts2.interceptor.SpringValidationInterceptor;
import com.opensymphony.xwork2.interceptor.Interceptor;

@Configuration
public class InterceptorConfiguration {

    final static String EXCLUDE_METHODS = "input, back, cancel, browse";

    @Bean(name = "gmo.cao.springI18n")
    Interceptor getSpringI18nInterceptor() {
        return new SpringI18nInterceptor();
    }

    @Bean(name = "gmo.cao.springValidation")
    @Autowired
    Interceptor createSpringValidationInterceptor(@Qualifier("gmo.cao.validator") Validator validator) {
        SpringValidationInterceptor interceptor = new SpringValidationInterceptor();
        interceptor.setExcludeMethods(EXCLUDE_METHODS);
        interceptor.setValidator(validator);
        return interceptor;
    }

}
struts.xml
<interceptor name="springI18n" class="gmo.cao.springI18n" />
<interceptor name="springValidation" class="gmo.cao.springValidation" />

Integrate Bean Validation (JSR303) with Struts2 -- Struts2 Action Sample

Integrate Bean Validation (JSR303) with Struts2 -- Outline
最後是一個套用JSR303的Action sample code。
代碼:
package com.gss.gmo.cao.struts2.interceptor;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import lombok.Setter;

import com.gss.gmo.cao.struts2.interceptor.validation.ValidateFieldConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateGroupConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateMessageFormatter;
import com.opensymphony.xwork2.ActionSupport;

public class UserAction extends ActionSupport implements ValidateMessageFormatter {

    private static final long serialVersionUID = 1L;

    @Setter
    @NotNull
    private String username;

    @Setter
    @Min(1)
    @Max(99)
    private int age;

    @Setter
    private UserAction user;

    @ValidateGroupConfig
    public String execute() {
        return SUCCESS;
    }

    @ValidateFieldConfig(fields = "user")
    public String validateFieldConfig() {
        return SUCCESS;
    }

    @Override
    public String format(String fieldName, String errorMessage) {
        return fieldName + " - " + errorMessage;
    }

}

Integrate Bean Validation (JSR303) with Struts2 -- SpringValidationInterceptor

Integrate Bean Validation (JSR303) with Struts2 -- Outline
整個整合的核心就在這支interceptor裡面,將整個Action傳給Validator進行驗證,若有錯誤就將錯誤塞到FieldError裡面,對熟悉Struts2的人來說應該不難。須額外注意的是如果Action是CGLIB proxy的話,需要轉換成target object進行驗證,因為Spring AOP生成的proxy object裡面field並不會有值。
com.gss.gmo.cao.struts2.interceptor.SpringValidationInterceptor
package com.gss.gmo.cao.struts2.interceptor;

import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.springframework.aop.support.AopUtils.getTargetClass;
import static org.springframework.aop.support.AopUtils.isCglibProxy;
import static org.springframework.util.ReflectionUtils.findField;
import static org.springframework.util.ReflectionUtils.getField;
import static org.springframework.util.ReflectionUtils.makeAccessible;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import lombok.Setter;

import org.apache.struts2.interceptor.validation.SkipValidation;

import com.gss.gmo.cao.struts2.interceptor.validation.ValidateFieldConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateGroupConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateMessageFormatter;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.util.reflection.ReflectionProvider;

/**
 * Use Spring and JSR 303 to validate action data.
 *
 * @author linus_chien
 *
 */
public class SpringValidationInterceptor extends MethodFilterInterceptor {

    private static final long serialVersionUID = 1L;

    @Setter
    private Validator validator;

    private ReflectionProvider reflectionProvider;

    @Inject
    public void setReflectionProvider(ReflectionProvider prov) {
        this.reflectionProvider = prov;
    }

    @Override
    protected String doIntercept(ActionInvocation invocation) throws Exception {
        Object originalAction = invocation.getAction();
        if (!(originalAction instanceof ActionSupport)) {
            return invocation.invoke();
        }

        Object targetAction = originalAction;
        if (isCglibProxy(originalAction)) {
            targetAction = getTargetClass(originalAction).newInstance();
            reflectionProvider.copy(originalAction, targetAction, invocation.getInvocationContext().getContextMap(), null, null);
        }

        Method method = targetAction.getClass().getDeclaredMethod(invocation.getProxy().getMethod(), new Class[] {});
        if (method.isAnnotationPresent(SkipValidation.class)) {
            return invocation.invoke();
        }

        ActionSupport originalActionSupport = (ActionSupport) originalAction;
        ActionSupport targetActionSupport = (ActionSupport) targetAction;

        ValidateGroupConfig groupConfig = method.getAnnotation(ValidateGroupConfig.class);
        Class<?>[] groups = {};
        if (groupConfig != null) {
            groups = groupConfig.groups();
        }

        ValidateFieldConfig fieldConfig = method.getAnnotation(ValidateFieldConfig.class);
        if (fieldConfig != null) {
            for (String field : fieldConfig.fields()) {
                if (isNotBlank(field)) {
                    validate(originalActionSupport, targetActionSupport, field, groups);
                }
            }
        } else {
            validate(originalActionSupport, targetActionSupport, "", groups);
        }

        return invocation.invoke();
    }

    /**
     * @param originalActionSupport
     * @param targetActionSupport
     * @param propertyPathPrefix
     * @param groups
     */
    private void validate(ActionSupport originalActionSupport, ActionSupport targetActionSupport, String propertyPathPrefix, Class<?>... groups) {
        Object target;
        if (isBlank(propertyPathPrefix)) {
            target = targetActionSupport;
        } else {
            Field targetField = findField(targetActionSupport.getClass(), propertyPathPrefix);
            if (targetField == null) {
                return;
            }
            makeAccessible(targetField);
            target = getField(targetField, targetActionSupport);
            if (target == null) {
                return;
            }
        }

        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(target, groups);

        for (ConstraintViolation<Object> constraintViolation : constraintViolations) {
            String propertyPath = constraintViolation.getPropertyPath().toString();
            if (isNotBlank(propertyPathPrefix)) {
                propertyPath = propertyPathPrefix + "." + propertyPath;
            }
            String errorMessage = constraintViolation.getMessage();
            if (originalActionSupport instanceof ValidateMessageFormatter) {
                errorMessage = ((ValidateMessageFormatter) originalActionSupport).format(originalActionSupport.getText(propertyPath, ""), errorMessage);
            }
            originalActionSupport.addFieldError(propertyPath, errorMessage);
        }
    }

}

Integrate Bean Validation (JSR303) with Struts2 -- Outline

許多MVC Framework都已經將JSR303納為default form validation的方式,但是Struts2卻遲遲未做,所以我得自己來整合這一段,步驟頗繁瑣,所以分為幾篇來說明,這裡只列出大綱。

Integrate Spring AOP with Struts2 Action

若是要使用Spring AOP來攔截Struts2 Action,首先當然要讓Spring管理所有的Action object,細節可以參考:Struts2 Convention Plugin with Spring Plugin

接下來可能會發生兩個問題:
  1. java.lang.NoSuchMethodException:原因是一般Action method不太會特別製作interface,預設Spring AOP會以interfaces為準,此時AOP proxy就不會有Action method,那麼就會發生此問題。
  2. Missing parameters:若Action scope被設定為prototype就會有此問題,因為每個method call都會讓Spring AOP proxy object重新new Action object,這樣之前呼叫過setter的parameter就會遺失。
解決方法:
  1. 修改@Scope參數
  2. 代碼:
    import org.springframework.context.annotation.Scope;
    import org.springframework.context.annotation.ScopedProxyMode;
    import org.springframework.web.context.WebApplicationContext;

    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
  3. 在web.xml中添加org.springframework.web.context.request.RequestContextListener
  4. web.xml
    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>
讓Action object的scope定在request,並且強制Spring使用CGLIB產生proxy,這樣就能順利在Struts2 Action上使用Spring AOP。

2013年1月14日 星期一

Struts2 ParamsPrepareParamsStack & Partial Update

在struts-default.xml裡面有一組名為paramsPrepareParamsStack的interceptor-stack,如下:
struts-default.xml
<interceptor-stack name="paramsPrepareParamsStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="alias"/>
    <interceptor-ref name="i18n"/>
    <interceptor-ref name="checkbox"/>
    <interceptor-ref name="multiselect"/>
    <interceptor-ref name="params">
        <param name="excludeParams">dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,parameters\...*</param>
    </interceptor-ref>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="chain"/>
    <interceptor-ref name="modelDriven"/>
    <interceptor-ref name="fileUpload"/>
    <interceptor-ref name="staticParams"/>
    <interceptor-ref name="actionMappingParams"/>
    <interceptor-ref name="params">
        <param name="excludeParams">dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,parameters\...*</param>
    </interceptor-ref>
    <interceptor-ref name="conversionError"/>
    <interceptor-ref name="validation">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
    <interceptor-ref name="workflow">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
</interceptor-stack>
官方說明
An example of the paramsPrepareParams trick. This stack is exactly the same as the defaultStack, except that it includes one extra interceptor before the prepare interceptor: the params interceptor.

This is useful for when you wish to apply parameters directly to an object that you wish to load externally (such as a DAO or database or service layer), but can't load that object until at least the ID parameter has been loaded. By loading the parameters twice, you can retrieve the object in the prepare() method, allowing the second params interceptor to apply the values on the object.
意思就是如果想要利用Struts2幫忙將partial parameters直接塞到model裡面的話,可以先利用第一次的ParametersInterceptor拿到id值,然後用PrepareInterceptor取得完整的model,再利用第二次的ParametersInterceptor讓Struts2將user要變更的資料塞到完整的model之中,這樣就可以處理頁面上user只做partial update的操作行為,好處就是在Action裡面可以直接將model往後端logic / data tier傳就可以了。

2013年1月9日 星期三

Struts2 Convention Plugin with Spring Plugin

Struts2 Spring Plugin 詳解裡面我們提到StrutsSpringObjectFactory的運作方式,那麼如果我們使用了Struts2 Convention Plugin的話,又該怎麼設定讓它和Spring一起運作呢?

以下是以一個Action為範例,
代碼:
import org.apache.struts2.convention.annotation.Action;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.opensymphony.xwork2.ActionSupport;

@Component("login-action")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class LoginAction extends ActionSupport {
    @Action(className = "login-action")
    public String execute() {
        return SUCCESS;
    }
}
重點:
  1. @Component("login-action")讓Spring管理LoginAction instance。
  2. @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)避開thread safe問題。
  3. @Action(className = "login-action")用來以Spring bean name覆蓋預設的full class name。
如此就可以在Convention Plugin的開發模式中運用Spring Plugin的特性。

Struts2 Spring Plugin 詳解

打開struts2-spring-plugin-2.3.8.jar的struts-plugin.xml可以看到有以下兩行重要的設定:
struts-plugin.xml
<bean type="com.opensymphony.xwork2.ObjectFactory" name="spring" class="org.apache.struts2.spring.StrutsSpringObjectFactory" />

<!--  Make the Spring object factory the automatic default -->
<constant name="struts.objectFactory" value="spring" />
這設定就讓struts2-spring-plugin-2.3.8.jar放入你的/WEB-INF/lib時自動將預設的object factory改用StrutsSpringObjectFactory。

再仔細看看StrutsSpringObjectFactory可以發現它繼承了com.opensymphony.xwork2.spring.SpringObjectFactory,這個class最重要的是它override了兩個名為buildBean的method。

首先先看以下這個method,
代碼:
@Override
public Object buildBean(String beanName, Map<String, Object> extraContext, boolean injectInternal) throws Exception {
    Object o;
  
    if (appContext.containsBean(beanName)) {
        o = appContext.getBean(beanName);
    } else {
        Class beanClazz = getClassInstance(beanName);
        o = buildBean(beanClazz, extraContext);
    }
    if (injectInternal) {
        injectInternalBeans(o);
    }
    return o;
}
這個method就是Struts2主要會用來create Action、Interceptor、Result等bean instance的入口method,從程式我們可以發現SpringObjectFactory會先判斷Spring是否有這個bean,如果有就直接使用;如果沒有,才抓出class name,並且呼叫另外一個buildBean method。

接下來看另外一個method,
代碼:
@Override
public Object buildBean(Class clazz, Map<String, Object> extraContext) throws Exception {
    Object bean;

    try {
        // Decide to follow autowire strategy or use the legacy approach which mixes injection strategies
        if (alwaysRespectAutowireStrategy) {
            // Leave the creation up to Spring
            bean = autoWiringFactory.createBean(clazz, autowireStrategy, false);
            injectApplicationContext(bean);
            return injectInternalBeans(bean);
        } else {
            bean = autoWiringFactory.autowire(clazz, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
            bean = autoWiringFactory.applyBeanPostProcessorsBeforeInitialization(bean, bean.getClass().getName());
            // We don't need to call the init-method since one won't be registered.
            bean = autoWiringFactory.applyBeanPostProcessorsAfterInitialization(bean, bean.getClass().getName());
            return autoWireBean(bean, autoWiringFactory);
        }
    } catch (UnsatisfiedDependencyException e) {
        if (LOG.isErrorEnabled())
            LOG.error("Error building bean", e);
        // Fall back
        return autoWireBean(super.buildBean(clazz, extraContext), autoWiringFactory);
    }
}
這個method稍微複雜一點,但不難發現它是用來inject Spring beans到新生成的Struts2 bean之中。

從上面兩個method可以知道,Struts2 Spring Plugin會先嘗試從Spring拿到所需要的bean,如果拿不到,才會自己生成bean,然後再inject所需要的Spring beans,這就是為什麼在struts.xml中將Action class name設定為Spring bean name可以運作的原因,即使在Action依舊設定 full class name,也會執行injection的動作,讓Action可以拿到Spring beans。

最後要提醒的是,只要是Struts2的bean都會這樣生成,無論是Action、Interceptor、Result等,將其轉給Spring控管後,須小心thread safe的問題,因為Spring預設scope是singleton。

2013年1月8日 星期二

Struts2 JSONResult 參數調校

JSONResult在它的struts-plugin.xml裡面的設定如下:
代碼:
<result-types>
    <result-type name="json" class="org.apache.struts2.json.JSONResult"/>
</result-types>
預設並沒有任何調整,這樣的狀況下會有兩個問題:
  1. 若model有繼承其它class,那些parent class attribute並不會被serialize。
  2. 若attribute為null,也會被serialize出去,造成無謂的網路傳輸量。
所以我們必須多加兩個設定:
  1. set ignoreHierarchy = false, default true.
  2. set excludeNullProperties = true, default false.
最終設定如下:
代碼:
<result-types>
    <result-type name="json" class="org.apache.struts2.json.JSONResult">
        <param name="ignoreHierarchy">false</param>
        <param name="excludeNullProperties">true</param>
    </result-type>
</result-types>
將它放到自己的struts.xml就可以了。

Struts2 Config Browser Plugin -- 管理你的Struts2設定

不管你是用傳統struts.xml或是用Convention Plugin開發系統,都一定會需要知道目前有哪些Action,這些Action又mapping到哪些URL,又或者是目前有哪些interceptor會生效等資訊,那麼Config Browser Plugin就是你需要的。

它的安裝很簡單,只要把jar檔放到WEB-INF/lib底下就可以了。然後在瀏覽器上輸入類似http://localhost:8080/starter/config-browser/index.action的網址就可以看到以下畫面。
螢幕截圖: