ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2022-07-22 17:00:22  阅读:191  来源: 互联网

标签:HandlerMapping SpringMVC 视图 Bean 源码 context DispatcherServlet servlet public


什么是特殊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注册。

标签:HandlerMapping,SpringMVC,视图,Bean,源码,context,DispatcherServlet,servlet,public
来源: https://www.cnblogs.com/lilpig/p/16506341.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有