2009년 8월 13일 목요일

Annotation기반 MultiActionController서 Validator 사용 하기

우리 가 스프링으로 개발 하다 보면 MultiAction 컨트롤러를 사용하면서 Form 컨트롤러 같은 효과를볼수 있는 방법이 없을까 고민을 한번 정도는 해보셨을 겁니다.
이런 이유는 첫번 째 개발자들이 Form 컨트롤러의 사용방식을 이해하는데
learning curve가 걸린다는 것
(처음 스프링을 사용하는 개발자들은 더더욱)
,둘째는 모랄까 귀찮니즘 아무래도 이것저것 잔손이 많이 가죠
(자잘한 공수도 쌓이면 무시 할수 없죠)
MultiAction에서 Form을 가장 부러워 하는건 아무래도 Validator일 겁니다.
MuliAction에서 그러면 Validator를 사용을 못하냐 그렇지 않습니다.
하지만 메서드 레벨이 아닌 클래스 레벨로 Validator를 적용 할 수 있습니다.
어쨌든!!
MultiAction 컨트롤러의 메서드에 Validator 전용
custom annotation을 만들어서 적용 하는 방법에 대해서
논의 하고자 합니다.
참고로 이 내용은 반드시 annotation-based Controller
(@Controller 선언된)에서만실행이 가능 합니다.
이유는 아래에 설명 드리겠습니다.
※ 소스는 "View Source" 버튼을 사용해서 자세히 보시기 바랍니다.

  • custom annotation 만들기
Validator을 위한 전용 어노테이션을 만듭 니다.
@Target( { ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Validator {
public String beanId();

public String successPage();
}


beanId는 Validator 스프링 빈 아이디 입니다. successPage는 Form 값이 validate한 경우 ,즉 Validator를 통과 했을때
리다리엑트될 페이지 입니다.

  • Sample용 Domain 만들기
테스트용 domain 입니다.
public class DummyMember {
private String id;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}


  • Validator 만들기

@Component
public class SampleValidator implements Validator {
public boolean supports(final Class arg0) {
return DummyMember.class.equals(arg0);
}

public void validate(final Object obj, final Errors errors) {
ValidatorsUtils.rejectIfEmpty(errors, "id", "required", "id is Null");
}
}

  • Annotation-Based Controller 만들기



import static org.apache.commons.lang.StringUtils.trimToEmpty;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ValidationUtils;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.support.RequestContextUtils;

import com.beyondweb.framework.commons.samples.DummyMember;
import com.beyondweb.framework.core.web.annotation.Validator;

@Controller
public class SamplesController {

/** The logger. */
private Logger logger = LoggerFactory.getLogger(this.getClass());

@InitBinder
protected final void initBinder(final HttpServletRequest request,
final ServletRequestDataBinder binder) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("initBinder JSON MultiActionController command");
}
request.setAttribute("REQUEST_BINTER", binder);
}

@RequestMapping("/samples/memberForm.htm")
@Validator(beanId = "sampleValidator", successPage = "redirect:/samples/memberList.htm")
public final String hanlderMemberCreate(final HttpServletRequest request,
DummyMember dummyMember, final Map modelMap) throws Exception {

String forwardURL = Utils.getForwardView(request);
Map<String, String> annotationInfoMap = findValidator(request);
modelMap.put("command", dummyMember);

if (Utils.isPost(request)) { // POST 방식일 경우만 실행
if (!validate(request, dummyMember, annotationInfoMap)) {
return forwardURL;
}
forwardURL = annotationInfoMap.get("successPage");
}
return forwardURL;
}

@RequestMapping("/samples/memberList.htm")
public final String hanlderMemberList(final HttpServletRequest request,
DummyMember dummyMember, final Map modelMap) throws Exception {
String forwardURL = Utils.getForwardView(request);
return forwardURL;
}

private Map<String, String> findValidator(final HttpServletRequest request) {
Map<String, String> validatorInfoMap = new HashMap<String, String>();
String validatorId = "";
Method[] methods = this.getClass().getDeclaredMethods();
String requestURL = StringUtils.trimToEmpty(request.getRequestURI());
boolean continues = true;

for (Method method : methods) {
RequestMapping requestMapping = method
.getAnnotation(RequestMapping.class);
if (requestMapping != null) {
String[] annotationURL = requestMapping.value();
if (requestMapping != null) {
for (String url : annotationURL) {
if (StringUtils.trimToEmpty(url).equals(requestURL)) {
Validator validator = method
.getAnnotation(Validator.class);
validatorInfoMap.put("beanId", validator.beanId());
validatorInfoMap.put("successPage", validator
.successPage());
continues = false;
break;
}
}
if (!continues) {
break;
}
}
}
}
return validatorInfoMap;
}

private boolean validate(final HttpServletRequest request, Object command,
Map<String, String> annotationInfoMap) throws Exception {
String successPage = null;
ServletRequestDataBinder binder;
binder = (ServletRequestDataBinder) request
.getAttribute("REQUEST_BINTER");

if (!StringUtils.isEmpty(annotationInfoMap.get("beanId"))) {
WebApplicationContext wac = Utils.getWebApplicationContext(request,
annotationInfoMap.get("beanId"));
org.springframework.validation.Validator validator = (org.springframework.validation.Validator) wac
.getBean(annotationInfoMap.get("beanId"));

if (validator.supports(command.getClass())) {
BindingResult bindingResult = binder.getBindingResult();
ValidationUtils.invokeValidator(validator, command,
bindingResult);
// validation이 성공일 경우
if (bindingResult.hasErrors()) {
return false;
}
}
}
return true;
}

private static class Utils {

private static String getForwardView(final HttpServletRequest request) {
String forward = request.getServletPath();
if (forward.lastIndexOf('.') > -1) {
return trimToEmpty(forward.substring(0, forward
.lastIndexOf('.')));
}
return "";
}

private static boolean isPost(final HttpServletRequest request) {
if ("POST".equals(request.getMethod().toUpperCase())) {
return true;
}
return false;
}

private static WebApplicationContext getWebApplicationContext(
final HttpServletRequest request, final String beanId)
throws Exception {
Object beanObject = null;
ServletContext sc;
HttpSession hs;
WebApplicationContext webApplicationContext = null;

// DispatcherServlet으로 로딩된 context를 가져 온다.
webApplicationContext = RequestContextUtils
.getWebApplicationContext(request);

// 빈을 검색해서 해당 빈 오브젝트를 가져 온다.
if (webApplicationContext.containsBean(beanId)) {
return webApplicationContext;
}

hs = request.getSession();
sc = hs.getServletContext();
// ContextLoaderListener으로 로딩된 context를 가져 온다.
webApplicationContext = WebApplicationContextUtils
.getWebApplicationContext(sc);

if (webApplicationContext.containsBean(beanId)) {
return webApplicationContext;
}

return webApplicationContext;
}
}
}


메서드 실행 순서는 : initBinder -> hanlderMemberCreate -> findValidator -> validator 입니다.
여기서 Utils클래스는 설명 생략 하겠습니다.
  • initBinder 메서드
annotation 기반이 아닌 컨트롤러(simple,multiaction,simpleform)는 개발자들이
커스터마이징 할수 있도록 메서드가 노출되어 있습니다.
상속을 받으면 override를 통해서 입맛에 맞게 수정이 가능 하죠.
반면에 annotation 기반 컨트롤러는 "AnnotationMethodHandlerAdapter"
클래스에서 모든 컨트롤러의 역할을 합니다.
해당 요청이 오면 @Controller 정의된 클래스를 찾아서 Invoke를 합니다.
실제 @Controller로 선언된 클래스들은 상당히 dummy 합니다.
,즉 AnnotationMethodHandlerAdapter는 annotation 전담 컨트롤러 입니다.
그러다 보니 상당히 커스터마이징 하는데 제약이 많습니다.
AnnotationMethodHandlerAdapter 클래스 입장에서
@Controller로 선언된 클래스들에게 "너희는 그냥 아무것도 하지마
하지만 initBinder는 나도 책임 지기가 애매 하니 그건 너희가 알아서해"
라고 말하고 있습니다.
뒤에 Validator을 찾아서 실행을 하기 위해서는 BindingResult 오브젝트가
필요한데 이것을 얻기 위해서는 initBinder 메서드의 파라미터 중에서
ServletRequestDataBinder 오브젝트에서 가져 올수 있수 있습니다.
그리고 가져온 ServletRequestDataBinder 오브젝트를 메서드간에
주고,받기 위해서 HttpServletRequest.setAttribute를 사용 했습니다.

  • hanlderMemberCreate 메서드
@RequestMapping에 정의된 URL 요청이 들어 올 경우 실행 됩니다.
@Validator(beanId = "sampleValidator", successPage = "redirect:/samples/memberList.htm")
beanId는 Validator 선언된 빈 아이디 입니다. 예를 들어서
XXXValidator 클래스는 빈 아이디 명은 "xXXValidator" 가 됩니다.
successPage는 validate가 성공되면 이동할 페이지 정보 입니다.
findValidator 메서드를 통해서 Map 형태의
@Validator 어노테이션의 정보 (beanId,successPage)를 가져 옵니다.
"modelMap.put("command", dummyMember)" 반드시 해야 합니다.
JSP에서 태그를 사용하려면 오브젝트를 넘겨야 합니다.
null을 넘겼을 경우 에러가 발생 합니다. 반드시 POST 일때만
validate를 수행하고
그렇지 않은 경우는 해당 페이지를
리턴 합니다.
validate 메서드는 findValidator의 맵정보를 기반으로 처리 합니다.
리턴값이 true일 경우 successPage 어노테이션에 정의한 곳으로
페이지 리다이렉트를 합니다.

  • findValidator 메서드
현재 클래스에서 요청된 URL 정보와 @RequestMa
pping 태그에 선언된
URL과 일치 하는 메서드를 검색 합니다. 메서드를 배열로 뽑은 이유는
@RequestMapping는 멀티 값을 갖고 있습니다.
예를 들어 @RequestMapping("/xxx/yyyy.htm /xxxx/zzzz.htm")
이렇기 때문에 배열로 뽑아서 매칭 작업을 해야 합니다.
만약 매칭된 메서드가 발견 되면 @Validator 정보의 값을 Map 형태로
리턴 합니다.
  • validate 메서드 작성
핵심 메서드 입니다. 파라미터를 보면 command는 폼에서 넘어와서
바인딩 된 오브젝트 입니다. 실제 validate를 당할 타켓 오브젝이죠.
annotationInfoMap 파라미터는 invoke 시킬 Validator 빈 아이디 정보
입니다.
initBinder메서드에 선언 했던 "ServletRequestDataBinder"
오브젝트를 꺼내 옵니다. 그리고 WebApplicationContext에서
Validator 빈을 검색해서 Validator를 invoke 시킵니다.
부적합한 경우 false를 리턴 합니다.
  • JSP 만들기
Spring Form 태그를 사용 합니다.


<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page session="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form:form commandName="dummyMember"
name="myForm"
method="post">
<form:input path="id"/> <form:errors cssClass="error" path="id" />
<input type="submit" value="전송"/>
</form:form>
</body>
</html>

annotation 기반이 아닌 컨트롤러들은 구현이 훨씬 편합니다.
스프링의 매력은 역시 수동 드라이빙을 할 수있다는 것이 매력적이네요.

댓글 없음:

댓글 쓰기