Loading

SpringMVC文档、源码阅读——DispatcherServlet特殊Bean加载

什么是特殊Bean

DispatcherServlet作为一个Servlet,它要一方面要接受用户的请求,一方面又要利用各种组件来处理这个请求。举个例子,当它接收到请求,它会交给Controller来处理,Controller返回一个字符串,它又调用ViewResolver来将这个字符串解析成视图。

所以无疑,DispatcherServlet想要工作,就要有这些组件的支持。在Spring中,你需要通过向WebApplicationContext中注册这些组件为Bean,而这些Bean就被称作特殊Bean。

如下是DispatcherServlet所要检查的特殊Bean表格:

Bean类型 解释
HandlerMapping 将一个请求映射到一个Handler(处理器)和一系列对请求做前置后置处理的Interceptor上
HandlerAdapter 帮助DispatcherServlet调用一个Handler而无需知道Handler实际如何被调用
HandlerExceptionResolver 解析异常的策略,可能将异常映射到一个Handler、一个HTML的错误页面或其它地方
ViewResolver 视图解析器,解析一个Handler返回的基于字符串的逻辑视图名到一个实际的视图上
LocaleResolver 解析客户端正在使用的地区以及可能的时区,以便提供国际化的视图
ThemeResolver 解析你的Web应用的主题
MultipartResolver 在一些multipart解析库的帮助下解析multipart请求
FlashMapManager 存储和获取输入输出的FlashMap,可以用于将属性从一个请求传递到另一个请求

这表格中的大部分东西我们都见过,少部分我没有使用过。

DispatcherServlet的源码中也定义了一些静态常量,指定了这些特殊Bean应该有的Bean名。

public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";

public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";

public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver";

public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";

public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter";

public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver";

public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator";

public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver";

public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager";

特殊Bean的加载

onRefresh

DispatcherServletonRefresh方法会被父类在WebApplicationContext初始化并刷新完毕后回调,此时,所有Bean都已经注册到context中。

onRefresh方法中,它调用了initStrategies来初始化某种策略:

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

initStrategies中就是加载特殊Bean的一些调用

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

我们会对其中的HandlerMappingHandlerAdapterViewResolver的加载进行分析。

HandlerMapping

HandlerMapping是用于将一个请求映射到一个Handler和一系列Interceptor上的组件。Handler是用于处理一个请求的组件,所以,在我们常见的SpringMVC开发方式中,Handler就是Controller中的方法。

下面是initHandlerMappings方法的代码:

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

    // 是否检测所有HandlerMapping
    if (this.detectAllHandlerMappings) {
        // 检测ApplicationContext中所有HandlerMapping,包括祖先context
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            // 将返回的所有HandlerMapping设置给本地变量`handlerMappings`
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // 保证HandlerMappings的顺序
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    // 否则,只通过约定的HandlerMapping BeanName来获取
    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.
        }
    }

    // 如果没找到任何handlerMapping,那么注册一个默认的以确保我们有至少一个HandlerMapping
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
    }
}
  1. 如果类中的detectAllHandlerMappings开关打开,代表允许检查context中所有HandlerMapping类型的Bean并注册
  2. 否则只通过约定的名字来到context中获取HandlerMapping
  3. 如果一个都没找到,使用getDefaultStrategies来获取默认HandlerMapping

下面是我们提供的唯一ServletConfig配置类:

@Configuration
@ComponentScan(basePackages = "top.yudoge.controller")
public class AppConfig { }

所以这种情况下,必然是通过默认策略获取默认的handlerMappings,我们来看看获取默认策略这个功能是如何实现的:

getDefaultStrategies

下面是这个方法的代码,看起来注释的内容很难理解并且代码也很难读懂。

/**
* 为给定的策略接口创建一个默认策略对象的列表
*
* @params context 当前WebApplicationContext
* @params strategyInterface 策略接口对象
*/
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<>();
    }
}

实际上很简单,该方法就是获取一个指定类型的对象的List以在用户没有显式指定该类型对象时作为默认的对象List,比如刚刚的handlerMappings。那为啥这里要反复强调Strategies这个单词呢?设计模式没学好吧!

对于DispatcherServlet来说,它调度一些组件来完成请求的处理,返回相应的视图,但是它负责的只有调度,请求如何被处理,视图如何被解析渲染,这些都不是它的任务,它不关心这些。取而代之的是,它调用这些组件的接口来完成功能,具体如何完成的是这些组件接口的实现来定义的。这些组件接口(比如HandlerMapping)就是策略接口(Strategie Interfaces),这些接口的实现类就是具体的策略(Concrete Strategie),所以,这不就是策略设计模式的一个较为庞大的应用嘛。

举个例子,DispatcherServlet只需要知道HandlerMapping是一个可以将请求映射到一个Handler和一批Interceptor上的策略接口即可,它并不需要知道具体是如何映射的,具体的映射规则由实际的策略来实现。比如RequestMappingHandlerMapping将请求映射到一个标注有@RequestMapping的方法上。

好了,该方法的第一行代码获取了策略接口的全限定名,然后试图以全限定名为key调用defaultStrategies.getProperty来获得一个值,然后它把这个值分割成了一批具体的策略类名,加载并实例化这些策略类,添加到策略列表中。

所以defaultStrategies中保存有每个策略接口的多个默认实现类。它是这样被初始化的:

private static final Properties defaultStrategies;
static {
    try {
        ClassPathResource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {}
}

它的初始化就是读取类路径下的名为DispatcherServlet.properties的配置文件,然后加载成Properties对象而已。我们看看SpringMVC的包底下有没有这个文件:

img

果然在这里有这个文件,它的内容如下:

# 省略除了HandlerMapping策略接口以外的配置项
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

所以,默认情况下有三个HandlerMapping被加载:

  1. BeanNameUrlHandlerMapping:从URL到BeanName的映射,使用Bean作为Handler
  2. RequestMappingHandlerMapping:从URL到带有@RequestMapping方法的映射,使用方法作为Handler
  3. RouterFunctionMapping:不知道干啥的

打个断点验证一下:

img

自己配置HandlerMapping

@Configuration
@ComponentScan(basePackages = "top.yudoge.controller")
public class AppConfig {
    
    class MyHandler {
        public String handle() {
            return "helloPage";
        }
    }
    
    @Bean
    public HandlerMapping handlerMapping() {
        return new HandlerMapping() {
            @Override
            public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
                HandlerExecutionChain executionChain = new HandlerExecutionChain(new MyHandler());
                return executionChain;
            }
        };
    }
}

可以看到这次走到了这个分支中,并且只有一个this.handlerMappings中只有一个HandlerMapping,就是我们在AppConfig中定义的内部类MyHandler:

img

这个代码目前显然还没什么意义,因为我们还没有对应的HandlerAdapter以及ViewResovler,现在通过浏览器访问,你会发现产生如下报错:

img

上面说的需要一个能够支持AppConfig$MyHandler这个处理器的HandlerAdapter。

顺便提一嘴,HandlerMapping的getHandler方法需要根据传入的HttpServletRequest来判断自己能否处理这个请求,如果能,就返回对应的HandlerExecutionChain(包含一个用于处理请求的Handler和若干Interceptor),否则返回null。我们的代码没有判断直接返回了一个HandlerExecutionChain,这代表它能处理所有请求。

HandlerMappingHandlerAdapterHandler比较陌生的可以看这篇文章

HandlerAdapter

万事开头难,有了上面的分析打基础,后面的分析都会变得简单,就比如initHandlerAdapters方法的分析:

private void initHandlerAdapters(ApplicationContext context) {
    this.handlerAdapters = null;

    if (this.detectAllHandlerAdapters) {
        Map<String, HandlerAdapter> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerAdapters = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerAdapters);
        }
    }
    else {
        try {
            HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
            this.handlerAdapters = Collections.singletonList(ha);
        }
        catch (NoSuchBeanDefinitionException ex) {
        }
    }

    if (this.handlerAdapters == null) {
        this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

?区别???

没有区别,我们直接来查看DispatcherServlet.properties文件,看看默认情况下有哪些HandlerAdapter为我们服务

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
  1. HttpRequestHandlerAdapter:用于调用HttpRequestHandler
  2. SimpleControllerHandlerAdapter:用于调用实现了org.springframework.web.servlet.mvc.Controller接口的类,这种情况下Controller实现类就是Handler。(它对标的应该是BeanNameHandlerMapping
  3. RequestMappingHandlerAdapter:用于调用@RequestMapping标注的方法,它和RequestMappingHandlerMapping一起工作
  4. HandlerFunctionAdapter:用于调用HandlerFunction这种Handler(它对标的应该是RouterFunctionMapping

现在,我们为AppConfig定义一个HandlerAdapter:

@Bean
public HandlerAdapter handlerAdapter() {
    return new HandlerAdapter() {
        @Override
        public boolean supports(Object handler) {
            return handler instanceof MyHandler;
        }

        @Override
        public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String viewName = ((MyHandler) handler).handle();
            return new ModelAndView(viewName);
        }

        @Override
        public long getLastModified(HttpServletRequest request, Object handler) {
            return -1;
        }
    };
}

在它的supports方法中,我们判断了Handler是否是MyHandler的实例,只有当它是MyHandler的实例时才返回支持。

handle方法中,我们调用了MyHandler.handle方法,并把这个方法返回的字符串(helloPage)当作视图名,构建一个ModelAndView并返回。

getLastModified方法中,返回了-1代表不支持此功能。

现在运行,还是报错:

img

这里的错误看起来是一个循环引用,我们先往下深入。

ViewResovler

initViewResovlers的代码也一样,我就不看了,只看DispatcherServlet.properties中定义了哪些默认视图解析器吧

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

只定义了一个InternalResourceViewResolver,这个视图解析器使用内部资源来进行视图解析,它会将视图名加上前缀和后缀,然后以内部资源表示处理后的视图名。说白了就是将请求转发到这个加上前缀后缀的视图名上。如果以后有机会会阅读ViewResovler的源码,不过我感觉我的时间快不够了哈哈哈哈。

所以,MyHandler这个逼拦截一切请求,它必然也会拦截视图解析器的转发,所以,这个转发又被转到MyHandler中进行处理,而如果任由MyHandler处理的化,视图解析器会再次转发,这样就陷入了循环,所以上面报了循环解析异常。

利用所学,解决上面的问题

造成上面的问题的根本原因就是——MyHandler把视图解析器的转发到helloPage的请求给拦截了。我们现在打算最终让MyHandler中的返回值helloPage/helloPage.jsp这个jsp文件服务。

想把字符串helloPage变成内部资源URL/helloPage.jsp,需要提供一个内部资源视图解析器,并提供前缀后缀:

@Bean
public ViewResolver viewResolver() {
    return new InternalResourceViewResolver("/", ".jsp");
}

其次,我们想让MyHandler不拦截jsp文件的请求,我们可以这样写:

@Bean
public HandlerMapping handlerMapping() {
    return new HandlerMapping() {
        @Override
        public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
            if (request.getServletPath().endsWith(".jsp")) {
                return null;
            }
            HandlerExecutionChain executionChain = new HandlerExecutionChain(new MyHandler());
            return executionChain;
        }
    };
}

但实际上,我们并用不到写这个判断,因为本来所有.jsp结尾的url也不会被MyHandler拦截,这个我们稍后分析下原因。

反正现在,页面显示出来了:

img

为什么我们的DispatcherServlet不拦截jsp结尾的文件

首先,我们所定义的DispatcherServlet的匹配规则如下:

@Override
protected String[] getServletMappings() {
    return new String[]{"/"};
}

也就是匹配/这个路径,官方对这种路径是这样描述的:

A string containing only the ’/’ character indicates the "default" servlet of the application.

一个仅仅包含字符“/”的字符串,代表着应用程序的默认Servlet。

还有描述匹配规则优先级中的下面这一段:

If neither of the previous three rules result in a servlet match, the container will attempt to serve content appropriate for the resource requested. If a "default" servlet is defined for the application, it will be used. Many containers provide an implicit default servlet for serving content.

如果上面的三个规则都没有导致一个servlet被匹配,容器将尝试提供适合所请求资源的内容。如果一个“默认”servlet在应用程序中被定义,那么它将被使用。很多容器提供一个隐式的默认servlet来提供内容。

所以,只有当没有Servlet能够提供请求的响应时,我们的DispatcherServlet才会被使用。这代表着肯定有某个Servlet匹配了对jsp文件的访问。

我们不妨在jsp页面上输出一下当前系统中所有的Servlet、它们匹配的路径以及它们的类名:

<h1>HelloPage</h1>
<ul>
    <%
        Map<String, ServletRegistration> registrationMap = (Map<String, ServletRegistration>) application.getServletRegistrations();
        for (ServletRegistration sr : registrationMap.values()) {
    %>
        <li><%=sr.getName()%> , <%=sr.getMappings()%>, <%=sr.getClassName()%></li>
    <%
        }
    %>
</ul>

可以看到,有一个默认的,啥也不匹配的servlet,有一个匹配*.jspx*.jsp的servlet,最后一个就是我们的DispatcherServlet,它匹配/,是官方定义中的默认servlet,它的优先级不如上面的jsp

img

org.apache.catalina/org.apache.jsper这个两个包,可以推断出前两个Servlet来自tomcat内部,由tomcat注册。

posted @ 2022-07-22 16:54  yudoge  阅读(40)  评论(0编辑  收藏  举报