스프링 MVC 동작 원리(9) : DispatcherServlet 3부 - 커스텀 ViewResolver

de_sj_awa·2021년 6월 27일
0

@Controller 클래스에서 @GetMapping 애노테이션이 붙은 메소드가 반환하는 문자열을 실제 뷰로 변환하는 과정은 어떻게 동작하는가?

DispatcherServlet 초기화

  • 다음의 특별한 타입의 빈들을 찾거나, 기본 전략에 해당하는 빈들을 등록한다.
  • HandlerMapping: 핸들러를 찾아주는 인터페이스
  • HandlerAdapter: 핸들러를 실행하는 인터페이스
  • HandlerExceptionResolver
  • ViewResolver
  • ...

DispatcherServlet 동작 순서

  1. 요청을 분석한다. (로케일, 테마, 멀티파트 등)
  2. (핸들러 맵핑에게 위임하여) 요청을 처리할 핸들러를 찾는다.
  3. (등록되어 있는 핸들러 어댑터 중에) 해당 핸들러를 실행할 수 있는 “핸들러 어댑터”를 찾는다.
  4. 찾아낸 “핸들러 어댑터”를 사용해서 핸들러의 응답을 처리한다.
    • 핸들러의 리턴값을 보고 어떻게 처리할지 판단한다.
    • 뷰 이름에 해당하는 뷰를 찾아서 모델 데이터를 랜더링한다.
    • @ResponseBody가 있다면 Converter를 사용해서 응답 본문을 만들고.
  5. (부가적으로) 예외가 발생했다면, 예외 처리 핸들러에 요청 처리를 위임한다. 6. 최종적으로 응답을 보낸다.

여기서는 ViewResolver를 살펴본다.

9. DispatcherServlet 3부

일단 먼저 DispatcherServlet이 사용하는 기본 전략(HandlerMapping, HandlerAdapter, ViewResolver 등)은 어떻게 초기화되는가?

DispatcherServlet.class

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

또한 이 과정은 DispatcherServlet이 초기화될 때 한번만 수행된다.

서블릿 컨테이너가 서블릿 인스턴스의 init() 메소드를 호출하여 초기화 한다.

  • 최초 요청을 받았을 때 한번 초기화 하고 나면 그 다음 요청부터는 이 과정을 생략한다.

이중에서 initViewResolver(context)만 살펴보자.

private void initViewResolvers(ApplicationContext context) {
    this.viewResolvers = null;
    if (this.detectAllViewResolvers) {
        // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.viewResolvers = new ArrayList<>(matchingBeans.values());
            // We keep ViewResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.viewResolvers);
        }
    }
    else {
        try {
            ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
            this.viewResolvers = Collections.singletonList(vr);
        }catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default ViewResolver later.
	}
    }

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

detectAllViewResolvers 플래그 값이 true인 경우 모든 해당 타입의 빈을 가져오고 플래그 값이 false인 경우에는 이름이 정확히 일치하는 빈 하나만 가져온다. 그리고 기본적으로 플래그값은 true이다.

if (this.viewResolvers == null) {
this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);

이 부분을 보면 viewResolvers가 없다면 기본 전략을 가져온다는 것을 알 수 있다.

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
    String key = strategyInterface.getName();
    String value = defaultStrategies.getProperty(key);
    if (value != null) {
        String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
        List<T> strategies = new ArrayList<>(classNames.length);
        for (String className : classNames) {
            try {
                Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                Object strategy = createDefaultStrategy(context, clazz);
                strategies.add((T) strategy);
            }catch (ClassNotFoundException ex) {
                throw new BeanInitializationException(
                    "Could not find DispatcherServlet's default strategy class [" + className +
		    "] for interface [" + key + "]", ex);
            }catch (LinkageError err) {
		throw new BeanInitializationException(
		    "Unresolvable class definition for DispatcherServlet's default strategy class [" +
		     className + "] for interface [" + key + "]", err);
	    }
        }
	return strategies;
    }else {
	return new LinkedList<>();
    }
}

String value = defaultStrategies.getProperty(key);

를 따라가보면

private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";

/**
* Common prefix that DispatcherServlet's default strategy attributes start with.
*/
private static final String DEFAULT_STRATEGIES_PREFIX = "org.springframework.web.servlet";

/** Additional logger to use when no mapped handler is found for a request. */
protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);

private static final Properties defaultStrategies;

private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";를 볼 수 있다.

DispatcherServlet.properties

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

그렇다면 커스텀한 기본 전략 객체를 만들어보자.

@Configuration
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
  
    @Bean
    public ViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}
@org.springframework.stereotype.Controller("/simple")
public class SimpleController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //return new ModelAndView("/WEB-INF/simple.jsp");
        return new ModelAndView("simple");
    }
}
public class HelloController {

    @Autowired
    HelloService helloService;

    @GetMapping("/hello")
    public String hello() {
        return "Hello, " + helloService.getName();
    }

    @GetMapping("/sample")
    public String sample(){
        //return "/WEB-INF/sample.jsp";
        return "sample";
    }
}

참고

  • 인프런 : 스프링 웹 MVC(백기선)
profile
이것저것 관심많은 개발자.

0개의 댓글