본문 바로가기
java

[Struts] ActionForm, ActionErrors, ExceptionHandler Struts Framework 에서의 사용자 입력 처리(ActionForm의 일생)

by 새로운 도전을 위한 한걸음 2015. 5. 10.

1 Struts Framework 에서의 사용자 입력 처리(ActionForm의 일생)
1.1 ActionForm 의 설정
1.1.1 ActionForm 클래스 작성
org.apache.struts.action.ActionForm 을 상속받은 ActionForm 클래스를 작성한다.

 
public class LoginForm extends ActionForm{
    /**
     * 유저 아이디
     */
    private String id;
    /**
     * 비밀번호
     */
    private String password;  
   
    /**
     * 유저 로그인시 폼 밸리데이션
     */
    public ActionErrors validate(
ActionMapping mapping,
HttpServletRequest request){
        ActionErrors errors = new ActionErrors();
        if(id==null || "".equals(id)){
            errors.add("error.require.id",new ActionError("error.require.id"));
        }
        if(password==null || "".equals(password)){
            errors.add("error.require.password",
new ActionError("error.require.password"));
        }
        return errors;
    }
   
    public String getId() {
        return id;
    }

    public String getPassword() {
        return password;
    }

    public void setId(String string) {
        id = string;
    }

public void setPassword(String string) {
        password = string;
    }

1.1.2 struts-config.xml 설정
ActionForm 을 사용하기 위해 struts-config.xml 을 설정한다.

 
<form-beans>
<form-bean
  name=”loginForm”
  type=”com.oreilly.struts.order.LoginForm”/>
</form-bean>
</form-beans>
<action-mappings>
<action
path=”/signin”
type=”com.oreilly.struts.storefront.security.LoginAction”
scope=”request”
name=”loginForm”
validate=”true”
input=”/security/signin.jsp”>
<forward name=”Success” path=”/index.jsp” redirect=”true”/>
<forward name=”Failure” path=”/security/signin.jsp” redirect=”true”/>
</action>
</action-mappings> 

1.2 ActionForm 의 처리과정
1.2.1 RequestProcessor 의 process() 내에서 ActionForm과 관련된 프로세스는  다음과 같은 3단계로 처리된다.
ActionMapping 에 설정된 ActionForm 의 instance 생성
Request 내의 parameter를 ActionForm의 각 field에 세팅
ActionForm의 validate 수행

 
RequestProcessor.java
public void process(HttpServletRequest request,
                        HttpServletResponse response)
        throws IOException, ServletException {
… 전략 …
// ActionMapping에 설정된 ActionForm의 instance를 얻어옴
ActionForm form = processActionForm(request, response, mapping);
// request의 값을 ActionForm에 설정
        processPopulate(request, response, form, mapping);
        // form validation 처리
        if (!processValidate(request, response, form, mapping)) {
            return;
        }
… 후략 …

ActionForm의 LifeCycle
 
 

1.2.2 ActionForm의 instance를 얻는 과정.
ActionMapping에 ActionForm 에 관한 설정이 있는지 확인한다. 설정이 없으면 null 반환
request 또는 session에서 ActionMapping의 attribute 를 key값으로 갖는 ActionForm의 instance를 가져온다.
얻어온 instance가 null 이 아니고, form-bean에 설정된 form의 type에 해당하는 클래스의 instance이면 얻어온 instance를 반환한다.
그렇지 않으면, 새로운 instance를 생성하고 ActionServlet을 instance에 세팅하여 반환한다.
얻어온 instance를 request 또는 session에 저장한다.

 
RequestProcessor.java
protected ActionForm processActionForm(HttpServletRequest request,
                                           HttpServletResponse response,
                                           ActionMapping mapping) {

        // Create (if necessary a form bean to use
        ActionForm instance = RequestUtils.createActionForm
            (request, mapping, moduleConfig, servlet);
        if (instance == null) {
            return (null);
        }
        if ("request".equals(mapping.getScope())) {
            request.setAttribute(mapping.getAttribute(), instance);
        } else {
            HttpSession session = request.getSession();
            session.setAttribute(mapping.getAttribute(), instance);
        }
        return (instance);
    } 
 
RequestUtils.java
public static ActionForm createActionForm( HttpServletRequest request,
                                           ActionMapping mapping,
                                           ModuleConfig moduleConfig,
                                          ActionServlet servlet) {
    // Is there a form bean associated with this mapping?
    String attribute = mapping.getAttribute();
    if (attribute == null) {
        return (null);
    }
    // Look up the form bean configuration information to use
    String name = mapping.getName();
    FormBeanConfig config = moduleConfig.findFormBeanConfig(name);
    if (config == null) {
        return (null);
    }
    ActionForm instance = null;
    HttpSession session = null;
    if ("request".equals(mapping.getScope())) {
        instance = (ActionForm) request.getAttribute(attribute);
    } else {
        session = request.getSession();
        instance = (ActionForm) session.getAttribute(attribute);
    }
    // Can we recycle the existing form bean instance (if there is one)?
    if (instance != null) {
        if (config.getDynamic()) {
            String className = ((DynaBean) instance).getDynaClass().getName();
            if (className.equals(config.getName())) {
                return (instance);
            }
        } else {
            try {
                Class configClass = applicationClass(config.getType());
                if (configClass.isAssignableFrom(instance.getClass())) {
                    return (instance);
                }
            } catch (Throwable t) {
   return (null);
            }
        }
    }
    // Create and return a new form bean instance
    if (config.getDynamic()) {
        try {
            DynaActionFormClass dynaClass =
                DynaActionFormClass.createDynaActionFormClass(config);
            instance = (ActionForm) dynaClass.newInstance();
            ((DynaActionForm) instance).initialize(mapping);
        } catch (Throwable t) {
            return (null);
        }
    } else {
        try {
            instance = (ActionForm) applicationInstance(config.getType());
        } catch (Throwable t) {
            return (null);
        }
    }
    instance.setServlet(servlet);
    return (instance);

1.2.3 ActionForm에 값을 세팅하는 과정.
form에 ActionServlet 세팅
form의 reset()을 호출하여 값 초기화
multipart/form-data 일 경우 form-bean에 multipartRequestHandler 세팅
multipart/form-data 일 경우 multipartRequestHandler 의 handleRequest를 호출하여 multipart data를 처리
multipart/form-data 일 경우 데이터가 최대값을 넘어가면 return;
request 내의 parameter들의 key값을 Enumeration으로 얻어옴
key값과 action 태그에 설정된 prefix, suffix 값을 이용하여(없으면 무시)  HashMap에 request의 parameter 값들을 저장
org.apache.commons.beanutils.BeanUtils 를 이용하여 HashMap의 값을 ActionForm의 instance에 저장

 
RequestProcessor.java
protected void processPopulate(HttpServletRequest request,
                               HttpServletResponse response,
                               ActionForm form,
                               ActionMapping mapping)
                               throws ServletException {
    if (form == null) {
        return;
    }
    // Populate the bean properties of this ActionForm instance
    form.setServlet(this.servlet);
    form.reset(mapping, request);
    if (mapping.getMultipartClass() != null) {
        request.setAttribute(Globals.MULTIPART_KEY, mapping.getMultipartClass());
    }
    RequestUtils.populate(form, mapping.getPrefix(), mapping.getSuffix(), request);
    // Set the cancellation request attribute if appropriate
    if ((request.getParameter(Constants.CANCEL_PROPERTY) != null) ||
        (request.getParameter(Constants.CANCEL_PROPERTY_X) != null)) {
        request.setAttribute(Globals.CANCEL_KEY, Boolean.TRUE);
    }

 
RequestUtils.java
public static void populate( Object bean,
                          String prefix,
                          String suffix,
                          HttpServletRequest request)
                          throws ServletException {
    // Build a list of relevant request parameters from this request
    HashMap properties = new HashMap();
    // Iterator of parameter names
    Enumeration names = null;
    // Map for multipart parameters
    Map multipartParameters = null;

    String contentType = request.getContentType();
    String method = request.getMethod();
    boolean isMultipart = false;
    if ((contentType != null)
        && (contentType.startsWith("multipart/form-data"))
        && (method.equalsIgnoreCase("POST"))) {
        // Get the ActionServletWrapper from the form bean
        ActionServletWrapper servlet;
        if (bean instanceof ActionForm) {
            servlet = ((ActionForm) bean).getServletWrapper();
        } else {
            throw new ServletException(
                "bean that's supposed to be "
                    + "populated from a multipart request is not of type "
                    + "\"org.apache.struts.action.ActionForm\", but type "
                    + "\""
                    + bean.getClass().getName()
                    + "\"");
        }
        // Obtain a MultipartRequestHandler
        MultipartRequestHandler multipartHandler = getMultipartHandler(request);
        // Set the multipart request handler for our ActionForm.
        // If the bean isn't an ActionForm, an exception would have been
        // thrown earlier, so it's safe to assume that our bean is
        // in fact an ActionForm.
        ((ActionForm) bean).setMultipartRequestHandler(multipartHandler);
        if (multipartHandler != null) {
            isMultipart = true;
            // Set servlet and mapping info
            servlet.setServletFor(multipartHandler);
            multipartHandler.setMapping(
                (ActionMapping) request.getAttribute(Globals.MAPPING_KEY));
            // Initialize multipart request class handler
            multipartHandler.handleRequest(request);
            //stop here if the maximum length has been exceeded
            Boolean maxLengthExceeded =
                (Boolean) request.getAttribute(
                    MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED);
            if ((maxLengthExceeded != null) && (maxLengthExceeded.booleanValue())) {
                return;
            }
            //retrive form values and put into properties
            multipartParameters = getAllParametersForMultipartRequest(
                        request, multipartHandler);
            names = Collections.enumeration(multipartParameters.keySet());
        }
    }
    if (!isMultipart) {
        names = request.getParameterNames();
    }
    while (names.hasMoreElements()) {
        String name = (String) names.nextElement();
        String stripped = name;
        if (prefix != null) {
            if (!stripped.startsWith(prefix)) {
                continue;
            }
            stripped = stripped.substring(prefix.length());
        }
        if (suffix != null) {
            if (!stripped.endsWith(suffix)) {
                continue;
            }
            stripped = stripped.substring(0, stripped.length() - suffix.length());
        }
        if (isMultipart) {
            properties.put(stripped, multipartParameters.get(name));
        } else {
            properties.put(stripped, request.getParameterValues(name));
        }
    }
    // Set the corresponding properties of our bean
    try {
        BeanUtils.populate(bean, properties);
    } catch (Exception e) {
        throw new ServletException("BeanUtils.populate", e);
    }

1.2.4 ActionForm의 validate 처리 과정.
Action의 validate 값이 true이면 무조건 true를 반환
Form.validate()를 수행하여 ActionErrors를 가져온다.
Form에 multipartRequestHandler가 있을 경우 multipartRequestHandler의 처리를 rollback한다.
Action 의 input값을 가져와 null이면 error 페이지로 보낸다.
Request 에 Globals.ERROR_KEY 키값으로  ActionErrors 를 저장한다.
Controller의 inputForward 값에 따라 적당한 input 페이지로 포워드 시킨다.

 
RequestProcessor.java
protected boolean processValidate(HttpServletRequest request,
                                  HttpServletResponse response,
                                  ActionForm form,
                                  ActionMapping mapping)
                                  throws IOException, ServletException {
    if (form == null) {
        return (true);
    }
    // Was this request cancelled?
    if (request.getAttribute(Globals.CANCEL_KEY) != null) {
return (true);
    }
    // Has validation been turned off for this mapping?
    if (!mapping.getValidate()) {
        return (true);
    }
    // Call the form bean's validation method
    ActionErrors errors = form.validate(mapping, request);
    if ((errors == null) || errors.isEmpty()) {
return (true);
    }
    // Special handling for multipart request
    if (form.getMultipartRequestHandler() != null) {
form.getMultipartRequestHandler().rollback();
    }
    // Has an input form been specified for this mapping?
    String input = mapping.getInput();
    if (input == null) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                getInternal().getMessage("noInput", mapping.getPath()));
        return (false);
    }
    // Save our error messages and return to the input form if possible
    request.setAttribute(Globals.ERROR_KEY, errors);
    if (moduleConfig.getControllerConfig().getInputForward()) {
        ForwardConfig forward = mapping.findForward(input);
        processForwardConfig( request, response, forward);
    } else {
        internalModuleRelativeForward(input, request, response);
    }
    return (false);

1.3 ActionErrors 의 사용
1.3.1 ActionErrors의 처리
Struts에서는 Request 수행시 발생하는 에러들을 사용자에게 보여주기 위한 방법으로서 ActionErrors를 제공하고 있다. ActionErrors는 다음과 같이 처리된다.
ActionForm 의 validate() 또는 Action의 execute() 내에서 ActionErrors를 생성하여 request 에 Globals.ERROR_KEY 를 key값으로 갖는 attribute로 저장한다.
<html:errors/> 커스텀 태그를 이용하여 jsp 에서 ActionErrors의 내용을 사용자에게 보여준다.

1.3.2 ActionErrors의 생성
1.3.2.1 ActionForm의 validate() 에서의 생성
ActionForm의 validate()는 ActionErrors를 반환하도록 되어 있다. 반환된 ActionErrors는 RequestProcessor의 processValidate() 내에서 request 에 저장하도록 되어 있으므로, 여기서는 단순히 ActionError들을 생성하여 ActionErrors에 add하기만 하면 된다.

 
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request){
    ActionErrors errors = new ActionErrors();
    if(id==null || "".equals(id)){
        errors.add("id",new ActionError("error.require.id"));
    }
    if(password==null || "".equals(password)){
        errors.add("password",new ActionError("error.require.password"));
    }
    return errors;

1.3.2.2 Action의 execute() 내에서의 ActionErrors 생성
비즈니스 처리가 예외를 던질 경우(ID를 찾지 못한다거나, password가 적합하지 않는다거나 하는 등)에 대하여 ActionForm의 validate()에 그 처리를 넣는 것은 적합하지 못하다. 이러한 경우에는 Action 의 execute() 내에서 적합한 절차를 수행해야 한다.

 
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
 throws Exception {
    LoginForm loginForm = (LoginForm)form;
    UserService userService = UserService.getInstance();
    UserEntity user = null;
    ActionForward forward = mapping.findForward("main");
    ActionErrors errors = new ActionErrors();
    try{
        user = userService.login(loginForm.getId(), loginForm.getPassword());
        request.getSession().setAttribute("user", user);
    }catch(IdNotFoundException e){
        errors.add("id",new ActionError("error.login.idNotFound"));
        forward = mapping.getInputForward();
    }catch(PasswordInvalidException e){
        errors.add("password",new ActionError("error.login.passwordInvalid"));
        forward = mapping.getInputForward();          
    }finally{
        saveErrors(request, errors);
    }          
    return forward;

1.3.2.3 ActionError의 생성자 및 ActionErrors.add() 에 관한 설명은 “자카르타 스트럿츠 프로그래밍1.3.2.4 ” p.256~257 및 API 문서를 참고하면 된다.

2 Struts Framework 에서 제공하는 Exception 처리
2.1 Struts Framework에서 제공하는 Exception 처리
Struts에서는 코드와 독립적으로 struts-config.xml에 예외처리 정책을 표현하여 구현하는 선언적 예외처리 방식을 제공한다. 이것은 중요한 코드의 재 컴파일 없이 예외처리 로직을 수정하기 쉽게 만들어준다.

2.2 struts-config.xml 설정

 
exception 요소의 속성   
className 표준 설정빈인 org.apache.struts.config.ExceptionConfig를 상속받아 새로운 설정빈을 만들어 사용할 경우, 사용할 클래스명을 설정한다.   
handler 예외 처리를 담당할 예외처리 클래스의 전체 이름. 기본값은 org.apache.struts.action.ExceptionHandler   
key 하위 애플리케이션의 리소스 번들에 지정된 키 값. ActionError 클래스의 인스턴스에서 이 속성의 값을 사용한다.   
path 예외 상황이 발생할 경우 전송해야 할 리소스의 상대 경로. 값을 지정하지 않으면 ActinMapping에서 지정하는 input속성의 값을 사용한다.   
scope ActionError 클래스의 인스턴스를 저장할 scope. “request” 또는 “session” 이어야 한다.   
type 예외 처리 클래스의 전체이름. 반드시 지정해야 한다.   
bundle 현재 exception에서 지정한 key 속성이 위치할 리소스 번들을 구별하는 ServleyContext속성 
 
<global-exceptions>
<exception
  key=”global.error.invalidation”
  path=”/security/signin.jsp”
  scope=”request”
  type=”com.oreilly.struts.framework.exceptions.InvalidLoginException”/>
</global-exception> 

2.3 Exception의 처리과정
Action 의 execute() 처리과정에서 Exception 발생시 RequestProcessor의 processException() 수행
Exception Class에 해당하는 ExceptionConfig 얻음. 해당 ExceptionConfig가 존재하지 않을경우
n Exception Class가 IOException 의 instance 이면 Exception을 IOException으로 캐스팅하여 던짐
n Exception Class가 ServletException 의 instance 이면 Exception을 ServletException으로 캐스팅하여 던짐
n 그렇n 지 않으면 Exception을 ServletException으로 chaining하여 던짐
ExceptionConfig에 설정되어 있는 ExceptionHandler를 얻어와 execute() 를 실행
기본 ExceptionHandler 의 경우
n ExceptionConfig에서 path를 가져옴. null이면 ActionMapping의 input을 사용함
n Request에 Globals.EXCEPTION_KEY를 key로 갖도록 Exception 을 Attribute로 저장
n Exception 에서 ActionError 를 얻어오거나(Exception이 ModuleException의 instance인 경우), 생성하여 ActionErrors에 add 한 후 Globals.ERROR_KEY 를 key값으로 갖도록 ActionErrors를 ExceptionConfig에 설정된 scope에 저장한다.

 
RequestProcessor.java
protected ActionForward processActionPerform(HttpServletRequest request,
                                               HttpServletResponse response,
                                               Action action,
                                               ActionForm form,
                                               ActionMapping mapping)
                                              throws IOException, ServletException {
    try {
        return (action.execute(mapping, form, request, response));
    } catch (Exception e) {
        return (processException(request, response, e, form, mapping));
    }

 
RequestProcessor.java
protected ActionForward processException(HttpServletRequest request,
                                           HttpServletResponse response,
                                           Exception exception,
                                           ActionForm form,
                                           ActionMapping mapping)
                                           throws IOException, ServletException {
    // Is there a defined handler for this exception?
    ExceptionConfig config = mapping.findException(exception.getClass());
    if (config == null) {
        log.warn(getInternal().getMessage("unhandledException", exception.getClass()));
        if (exception instanceof IOException) {
            throw (IOException) exception;
        } else if (exception instanceof ServletException) {
            throw (ServletException) exception;
        } else {
            throw new ServletException(exception);
        }
    }
    // Use the configured exception handling
    try {
        ExceptionHandler handler = (ExceptionHandler)
        RequestUtils.applicationInstance(config.getHandler());
        return (handler.execute(exception, config, mapping, form, request, response));
    } catch (Exception e) {
        throw new ServletException(e);
    }

  
 
ExceptionHandler.java
public ActionForward execute(Exception ex,
                              ExceptionConfig ae,
                              ActionMapping mapping,
                              ActionForm formInstance,
                              HttpServletRequest request,
                              HttpServletResponse response)
                              throws ServletException {
    ActionForward forward = null;
    ActionError error = null;
    String property = null;
    // Build the forward from the exception mapping if it exists
    // or from the form input
    if (ae.getPath() != null) {
        forward = new ActionForward(ae.getPath());
    } else {
        forward = mapping.getInputForward();
    }
    // Figure out the error
    if (ex instanceof ModuleException) {
        error = ((ModuleException) ex).getError();
        property = ((ModuleException) ex).getProperty();
    } else {
        error = new ActionError(ae.getKey(), ex.getMessage());
        property = error.getKey();
    }
    // Store the exception
    request.setAttribute(Globals.EXCEPTION_KEY, ex);
    storeException(request, property, error, forward, ae.getScope());
    return forward;
}

protected void storeException(HttpServletRequest request,
                              String property,
                              ActionError error,
                              ActionForward forward,
                              String scope) {                           
    ActionErrors errors = new ActionErrors();
    errors.add(property, error);
    if ("request".equals(scope)){
        request.setAttribute(Globals.ERROR_KEY, errors);
    } else {
        request.getSession().setAttribute(Globals.ERROR_KEY, errors);
    }