Spring 3.0 의 DefaultAnnotationHandlerMapping 과 AnnotationMethodHandlerAdapter가 Spring 3.1 에선 @Deprecated 되고, 대신 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter 로 바뀌었다.
이유인즉슨, Controller 의 요청이 메소드 단위로 세분화 되면서 , DefaultAnnotationHandlerMapping 이 AnnotationMethodHandlerAdapter로 handler 를 전달해줄 때, 문제가 생겼기 때문이다 .
문제에 대해 아~~주 간략히 설명하자면 DefaultAnnotationHandlerMapping 은 AnnotationMethodHandlerAdapter에 메소드 단위를 전달해줄 방법이 없었기때문에 handler Object 를 통째로 전달해줬고, AnnotationMethodHandlerAdapter 에선 본래 자신이 할 일이 아닌 넘어온 handler Object에서 실행해야 할 메소드를 찾아야 하는 단일책임원칙 을 위반한 구조였다.
원칙을 위반하였기 때문에 당연히 확장성에서도 문제가 발생하였다.. HandlerIntercepter 가 바로 대표적 사례이다.
그리하여, RequestMappingHandlerMapping 에선 HandlerMethod 를 생성하여 메소드 요청 정보를 담아 RequestMappingHandlerAdapter 에 전달해주는 방식으로 이 문제를 해결하였다.
그런데 오늘 RequestMappingHandlerMapping 의 defaultHandler 를 등록하던 중 문제가 발생하였다.
defaultHandler를 등록하였을 경우, 웹요청을 처리할 핸들러(Controller) 를 찾지 못했을 경우,
default로 사용할 핸들러(컨트롤러)를 등록함으로써 사용자에게 404 에러 페이지 대신, 친절한 안내 페이지를 보여주게 된다.
그런데 에러가 발생하였다 ..
위와 같은 오류가 발생하여, 디버깅을 해보았다..
AbstractHandlerMapping 의
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception
에 브레이크 포인트를 걸고, 확인해보니 디폴트로 등록된 HandlerMapping 전략
(BeanNameHandlerMapping, SimpleUrlHandlerMapping, RequestMappingHandlerMapping)을 차례로 돌아가며 실행하였고,
if (handler == null) {
handler = getDefaultHandler();
}
와 같이 디폴트 핸들러를 가져오도록 돼었다.
RequestMappingHandlerMapping 시점에서 등록된 defaultHandler 를 발견하고,
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception
메소드를 호출하였고,
메소드 내부에서
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
로 HandlerMethod 를 가져왔다 ... lookupHandlerMethod(lookupPath, request) 를 따라가보니 미리 추가된 urlMap
(
private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();
Set<String> patterns = getMappingPathPatterns(mapping);
for (String pattern : patterns) {
if (!getPathMatcher().isPattern(pattern)) {
this.urlMap.add(pattern, mapping);
}
}
위와 같은 방법으로 urlMap 에 값을 add한다.
값은 {/callable/view=[{[/callable/view],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /defaultcon=[{[/defaultcon],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}], /fileupload=[{[/fileupload],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}], /home=[{[/home],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /filelist=[{[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[application/xml || application/json],custom=[]}], /fileupload_old=[{[/fileupload_old],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload_old],methods=[POST],params=[],headers=[],consumes=[multipart/form-data],produces=[],custom=[]}], /testview=[{[/testview],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /myrequest=[{[/myrequest],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /jsontest=[{[/jsontest],methods=[],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}], /filedownload/{fileId}=[{[/filedownload/{fileId}],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /advicetest=[{[/advicetest],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}], /userinfo=[{[/userinfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/userinfo],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}]}
이런식으로 들어가 있으며, @requestMapping에 해당하는 요청정보를 add 한 것이다. )
에서 요청 url 에 해당하는 키값으로 value 를 가져와서 List<Match> matches = new ArrayList<Match>(); 에 add하도록 돼어있으나, 난 defaultHandler 작동여부를 확인하기 위해
존재하지 않는 url을 호출하였기에 값이 있을리 없었다. 값이 존재 하지 않아 matches 가 텅텅 비어 있으면
if (matches.isEmpty()) {
// No choice but to go through all mappings
addMatchingMappings(this.handlerMethods.keySet(), matches, request);
}
를 실행한다
addMatchingMappings를 호출 할 때 넘겨준 this.handlerMethods.keySet() 는
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
Map이고, 데이터는 아래와 같이 들어 있다. 여기엔 단순 url 정보뿐만 아니라 컨트롤러와 메소드, 파라미터 정보까지 들어있다.
{{[/callable/view],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.util.concurrent.Callable<java.lang.String> me.anna.springmvc.CallableController.callableWithView(org.springframework.ui.Model), {[/defaultcon],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.DefaultController.home(), {[/fileupload],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public void me.anna.springmvc.FileUploadController.fileUploadForm(), {[/fileupload],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}=public void me.anna.springmvc.FileUploadController.processUpload(org.springframework.web.multipart.MultipartFile,org.springframework.ui.Model) throws java.io.IOException, {[/home],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.home(me.anna.domain.FileDomain,org.springframework.ui.Model), {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.fileList(org.springframework.ui.Model), {[/fileupload_old],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.fileupload(org.springframework.ui.Model), {[/testview],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.testView(org.springframework.ui.Model), {[/myrequest],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.myWelcome(), {[/jsontest],methods=[],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}=public me.anna.domain.Result me.anna.springmvc.HomeController.jsonTest(), {[/filedownload/{fileId}],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.fileDownload(int,org.springframework.ui.Model,org.springframework.web.servlet.mvc.support.RedirectAttributes), {[/advicetest],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.adviceTest(org.springframework.ui.Model) throws java.io.IOException, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[application/xml || application/json],custom=[]}=public java.util.List<me.anna.domain.FileDomain> me.anna.springmvc.HomeController.listWithMarshalling(), {[/fileupload_old],methods=[POST],params=[],headers=[],consumes=[multipart/form-data],produces=[],custom=[]}=public java.util.concurrent.Callable<org.springframework.http.ResponseEntity<me.anna.domain.FileDomain>> me.anna.springmvc.HomeController.handleFormUpload(me.anna.domain.FileDomain,org.springframework.web.multipart.MultipartFile), {[/userinfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.UserEditController.getUser(org.springframework.ui.Model), {[/userinfo],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.UserEditController.updateUser(me.anna.domain.User,org.springframework.web.bind.support.SessionStatus,org.springframework.web.servlet.mvc.support.RedirectAttributes)}
위 handlerMethod의 키셋 만 넘겨주니 최종적으로 파라미터로 넘겨준 값은 아래와 같다.
[{[/callable/view],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/home],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload_old],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/testview],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/myrequest],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/jsontest],methods=[],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}, {[/filedownload/{fileId}],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/advicetest],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[application/xml || application/json],custom=[]}, {[/fileupload_old],methods=[POST],params=[],headers=[],consumes=[multipart/form-data],produces=[],custom=[]}, {[/userinfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/userinfo],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}]
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
for (T mapping : mappings) {
T match = getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new Match(match, handlerMethods.get(mapping)));
}
}
}
따라와 보니 루프를 돌면서 getMatchingMapping 을 호출하고 있다. 키값 을 하나 씩 넘기면서..
getMatchingMapping 를 따라가보니,
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
와 같은 형태다 . addMatchingMappings 을 호출할 때 넘겨준 파라미터 this.handlerMethods.keySet() 은
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>(); 형태고
이 Map 의 키에 해당하는 T가 바로 getMatchingMapping 의 파라미터 인 RequestMappingInfo 타입인것이다.
그런데 이 루프를 다 돌아도 여전히 match 는 null이다 ... 왜냐하면 요청 url 에 해당하는 키값을 찾을수가 없는것이다 ...
최종적으로 DispatcherServlet 의
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
for (HandlerAdapter ha : this.handlerAdapters) {
if (logger.isTraceEnabled()) {
logger.trace("Testing handler adapter [" + ha + "]");
}
if (ha.supports(handler)) {
return ha;
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
메소드를 호출하고
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
익셉션이 발생한다.
내용이 너무 방대하여 관련내용을 모두 작성하지는 못했지만, 스프링 3.0 의 DefaultAnnotationHandlerMapping 과 AnnotationMethodHandlerAdapter 에선 defaultHandler 를 등록하면 잘 작동한다..
하지만 3.2에선 작동하지 않는다.
이는 3.1 이상에서 생긴 HandlerMethod 를 생성하지 못했기에, HandlerAdapter 전략에서 support 하지 못하여 익셉션이 발생하는것으로 보인다.