woniper

Spring-MVC 읽기 #7. HandlerMapping 본문

Spring

Spring-MVC 읽기 #7. HandlerMapping

woniper1 2019. 5. 22. 08:45

DispatcherServlet 클래스를 읽어보며 여러 가지 Resolver가 존재한다는 것을 알았다. Resolver는 뜻 그대로 무언가를 해결한다는 의미다. ViewResolver는 View를 어떻게 만들 것인지, LocaleResolver는 어떻게 지역화(언어)를 선택할 것인지 해결한다. 어렵지 않은 코드라 생각해서 (사실은 귀찮아서) 여러 Resolver 코드 읽기는 하지 않겠다.

이번 글은 HandlerMapping이 무엇인지 알아보고 코드를 살펴보자.

HandlerMapping의 역할

코드를 살펴보기 전에 HandlerMapping이 어떤 역할을 하는지 알아야겠다. HandlerMapping 문서에는 아래와 같이 설명한다.

Request와 Handler 객체 간의 매핑을 정의한다. 프레임워크에 기본 HandlerMapping은 BeanNameUrlHandlerMappingRequestMappingHandlerMapping 클래스다. Handler는 항상 HandlerExecutionChain 인스턴스에 포함되어 실행된다.

HandlerMapping 구현체

  • HandlerMapping은 DispatcherServlet에 의해 초기화된다.
  • HandlerMapping은 항상 HandlerExecutionChain을 통해 실행된다.
  • HandlerExecutionChain은 AbstractHandlerMapping 클래스에 의해 생성된다.
  • 기본 HandlerMapping 구현체인 BeanNameUrlHandlerMapping은 AbstractUrlHandlerMapping을 상속한다.
  • 기본 HandlerMapping 구현체인 RequestMappingHandlerMapping은 AbstractHandlerMethodMapping을 상속한다.

HandlerMapping의 초기화

DispatcherServlet은 HandlerMapping을 이용해 request를 어떤 Handler로 mapping 할지 결정한다. 그렇다면 HandlerMapping은 어떻게 초기화될까?

DispatcherServlet#onRefresh

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}
  1. DispatcherServlet#onRefresh 메소드는 DispatcherServlet#initStrategies 메소드를 호출한다.
  2. DispatcherServlet#initStrategies 메소드는 dispatch에 필요한 여러 객체를 초기화한다.
    • HandlerMapping을 초기화하는 initHandlerMappings 메소드를 호출한다.

DispatcherServlet#initHandlerMappings

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerMapping later.
        }
    }

    // Ensure we have at least one HandlerMapping, by registering
    // a default HandlerMapping if no other mappings are found.
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

모든 HandlerMapping을 조회하는 경우 (if 블록)

  1. detectAllHandlerMappings가 true면 모든 HandlerMapping을 초기화한다.
    • detectAllHandlerMappings 멤버 변수의 기본값은 true 다.
  2. 모든 HandlerMapping bean을 가져온다. (BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false) 메소드)
  3. HandlerMapping 초기화 후 정렬한다. (AnnotationAwareOrderComparator.sort(this.handlerMappings))
    • HandlerMapping이 2개 이상인 경우 우선순위가 존재하기 때문에 정렬한다.
    • org.springframework.core.Ordered를 기준으로 정렬한다.

하나의 HandlerMapping만 존재하는 경우 (else 블록)

  1. handlerMapping의 이름을 갖은 bean을 가져온다. (context.getBean(HANDLERMAPPINGBEAN_NAME, HandlerMapping.class) 메소드)
    • public static final String HANDLERMAPPINGBEAN_NAME = "handlerMapping";
  2. 초기화한다. (Collections.singletonList(hm) 메소드)
  3. 이 때도 handlerMappings가 없다면, default로 초기화한다. (getDefaultStrategies 메소드)

HandlerExecutionChain 실행 및 생성

DispatcherServlet#getHandler (실행)

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}
  1. DispatcherServlet#getHandler 메소드는 DispatcherServlet#doDispatch 메소드에서 실행된다.
  2. HandlerMapping list에서 HandlerExecutionChain을 얻는다. (mapping.getHandler(request) 메소드)
    • getHandler 메소드는 AbstractHandlerMapping 클래스에 구현되어 있다.
    • HandlerMapping 구현체에서 설명했듯, HandlerExecutionChain은 AbstractHandler 클래스에서 생성한다.

AbstractHandlerMapping#getHandler (생성)

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    // Bean name or resolved handler?
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = obtainApplicationContext().getBean(handlerName);
    }

    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

    if (logger.isTraceEnabled()) {
        logger.trace("Mapped to " + handler);
    }
    else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
        logger.debug("Mapped to " + executionChain.getHandler());
    }

    if (CorsUtils.isCorsRequest(request)) {
        CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }

    return executionChain;
}
  1. handler를 얻는다. (getHandlerInternal 메소드)

    • getHandlerInternal 메소드는 추상 메소드다.
    @Nullable
    protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
  2. handler가 null이면, 기본 handler를 얻는다. (getDefaultHandler 메소드)

    • 다음 설명 참고.
  3. handler가 null이면, null을 반환한다.

  4. handler type이 String 이면, ApplicationContext에서 Handler bean을 얻는다. (obtainApplicationContext().getBean(handlerName) 메소드)

  5. HandlerExecutionChain을 생성한다. (getHandlerExecutionChain 메소드)

  6. 현재 요청이 CORS 요청이면, Cors HandlerExecutionChain을 생성한 후 반환한다. (getCorsHandlerExecutionChain(request, executionChain, config) 메소드)

AbstractHandlerMethodMapping#getHandlerInternal

위 코드에서 AbstractHandlerMapping#getHandlerInternal 메소드는 추상 메소드라 설명했다. AbstractHandlerMapping의 하위 클래스인 AbstractHandlerMethodMapping 클래스와 AbstractUrlHandlerMapping 클래스가 있다. AbstractHandlerMethodMapping를 예제로 코드를 살펴보자.

@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    this.mappingRegistry.acquireReadLock();
    try {
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}
  1. request url을 구한다. (getUrlPathHelper().getLookupPathForRequest(request) 메소드)
  2. HandlerMethod를 구한다. (lookupHandlerMethod(lookupPath, request))
  3. HandlerMethod를 반환한다.

AbstractHandlerMethodMapping#lookupHandlerMethod

실제 HandlerMethod가 생성되는 lookupHandlerMethod 메소드를 보자.

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
    if (directPathMatches != null) {
        addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
        // No choice but to go through all mappings...
        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
    }

    if (!matches.isEmpty()) {
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        matches.sort(comparator);
        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
            if (logger.isTraceEnabled()) {
                logger.trace(matches.size() + " matching mappings: " + matches);
            }
            if (CorsUtils.isPreFlightRequest(request)) {
                return PREFLIGHT_AMBIGUOUS_MATCH;
            }
            Match secondBestMatch = matches.get(1);
            if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                Method m1 = bestMatch.handlerMethod.getMethod();
                Method m2 = secondBestMatch.handlerMethod.getMethod();
                String uri = request.getRequestURI();
                throw new IllegalStateException(
                        "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
            }
        }
        request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
        handleMatch(bestMatch.mapping, lookupPath, request);
        return bestMatch.handlerMethod;
    }
    else {
        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
    }
}
  1. MappingRegistry를 통해 현재 request 정보로 HandlerMethod를 조회한다.
    • MappingRegistry는 Bean 라이프사이클 초기화에 의해 이미 request 정보와 그에 해당하는 HandlerMethod를 저장하고 있다.
    • MappingRegistry의 초기화 코드는 AbstractHandlerMethodMapping에 구현되어 있다. (이 글에서 자세한 설명은 제외한다.)
  2. request 정보와 일치하는 Match 리스트 중 0번 index 클래스를 얻는다.
    • Match 클래스는 request와 HandlerMethod를 담고 있는 private 클래스다.
    • Match 리스트는 우선순위에 의해 정렬되어 있으므로, 0번 index 클래스가 request와 가장 일치하는 클래스다.
  3. HandlerMethod를 반환한다. (return bestMatch.handlerMethod)

다시 AbstractHandlerMapping#getHandler 메소드로 돌아가서.

getHandler 메소드는 크게 2가지 일을 한다.

  1. getHandlerInternal 메소드를 호출해 request 정보로 HandlerMethod를 얻는다.
  2. getHandlerExecutionChain 메소드를 호출해 HandlerExecutionChain을 생성한다.

AbstractHandlerMapping#getHandlerExecutionChain

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
    HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
            (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

    String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
    for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
        if (interceptor instanceof MappedInterceptor) {
            MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
            if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
                chain.addInterceptor(mappedInterceptor.getInterceptor());
            }
        }
        else {
            chain.addInterceptor(interceptor);
        }
    }
    return chain;
}
  1. HandlerExecutionChain을 생성한다.
  2. request 정보로 path를 찾는다.
  3. path에 해당하는 Interceptor를 찾아 HandlerExecutionChain에 추가(addInterceptor 메소드)한다.
  4. HandlerExecutionChain 반환한다.

HandlerMethod

HandlerMapping은 requst 정보를 통해 HandlerMethod를 얻어냈다. 이 글에서 HandlerMapping의 역할에 대해서 설명했지만, 더 중요한 HandlerMethod의 역할이 설명되지 않았다. HandlerMethod는 우리가 정의한 Contoller 클래스의 메소드다. 즉, @RequestMapping이 선언된 메소드다. HTTP 요청 시 메소드가 실행되기 위해 필요한 정보를 캡슐화한 클래스가 바로 HandlerMethod다.

마무리

HandlerMapping을 설명하기 위해서는 HandlerAdapter 설명 생략할 순 없다. 둘은 연관되어 실행된다. 때문에 다음 글은 HandlerAdapter를 설명하려 한다.

Comments