【探索Spring底层】13.深入DispatcherServlet
创始人
2024-04-27 11:09:08
0

文章目录

  • 1. DispatcherServlet概述
  • 2. DispatcherServlet的初始化时机
  • 3. DispatcherServlet初始化执行的操作
  • 4. RequestMappingHandlerMapping 基本用途
  • 5. RequestMappingHandlerAdapter 基本用途
  • 6. 参数和返回值解析器

1. DispatcherServlet概述

DispatcherServlet是SpringMVC的核心——前端控制器,但是其本质其实也就是一个HttpServlet。它主要用来拦截符合要求的外部请求,并把请求分发到不同的控制器去处理,根据控制器处理后的结果,生成相应的响应发送到客户端。

DispatcherServlet作为统一访问点,主要进行全局的流程控制。

img


2. DispatcherServlet的初始化时机

DispatcherServlet什么时候初始化呢?

下面我们通过实践来得出结果

首先准备一个内嵌tomcat的容器

public class A20 {private static final Logger log = LoggerFactory.getLogger(A20.class);public static void main(String[] args) throws Exception {//AnnotationConfig:支持java配置类的形式来构建容器//ServletWebServer:支持内嵌容器AnnotationConfigServletWebServerApplicationContext context =new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);}
}

AnnotationConfigServletWebServerApplicationContext这个类是可以基于Java配置类的形式构造容器,同时也支持内嵌容器。

详情可以查看:【探索Spring底层】2.容器的实现_起名方面没有灵感的博客-CSDN博客

接下来编写配置类

@Configuration
@ComponentScan
public class WebConfig {// ⬅️内嵌 web 容器工厂@Beanpublic TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {return new TomcatServletWebServerFactory(serverProperties.getPort());}// ⬅️创建 DispatcherServlet@Beanpublic DispatcherServlet dispatcherServlet() {return new DispatcherServlet();}// ⬅️注册 DispatcherServlet, Spring MVC 的入口@Beanpublic DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) {DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");return registrationBean;}
}

完成这些之后启动A20main方法

image-20221220153620153

可以发现tomcat启动之后,DispatcherServlet并无初始化。

通过日志我们可以发现启动的服务的端口为8080,这时候我们在浏览器访问localhost:8080

当在浏览器发起请求的一瞬间,观察控制台可以发现DispatcherServlet开始初始化了。

image-20221220153901600

因此不难看出,在首次使用到DispatcherServlet的时候,才会由tomcat服务器来初始化

在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化

那么有没有可不可以修改DispatcherServlet初始化的时机呢,让它在tomcat启动的时候就初始化

答案是可以的

@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) {DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");//在tomcat启动的时候就初始化DispatcherServletregistrationBean.setLoadOnStartup(1);return registrationBean;
}

3. DispatcherServlet初始化执行的操作

DispatcherServlet的初始化操作,离不开这个类中的onRefresh方法

@Override
protected void onRefresh(ApplicationContext context) {initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {//文件上传initMultipartResolver(context);//本地化信息initLocaleResolver(context);initThemeResolver(context);//做路径映射initHandlerMappings(context);//适配不同形式的控制器方法initHandlerAdapters(context);//解析异常initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);
}

initStrategies方法中分别调用了9个方法,这些方法都类似,9个方法其实也就是初始化9类组件

就下面的initHandlerMappings来说

private void initHandlerMappings(ApplicationContext context) {this.handlerMappings = null;//判断是否要到父容器中寻找if (this.detectAllHandlerMappings) {// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.Map 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");}}for (HandlerMapping mapping : this.handlerMappings) {if (mapping.usesPathPatterns()) {this.parseRequestPath = true;break;}}
}

不难看出,这里九大组件都是使用默认的。image-20221220160026077


4. RequestMappingHandlerMapping 基本用途

RequestMappingHandlerMapping的作用是在容器启动后将系统中所有控制器方法的请求条件(RequestMappingInfo)和控制器方法(HandlerMethod)的对应关系注册到RequestMappingHandlerMapping Bean的内存中,待接口请求系统的时候根据请求条件和内存中存储的系统接口信息比对,再执行对应的控制器方法。

其工作流程如下:先到当前容器下找到所有的控制类,然后进一步查看这个控制类有哪一些方法,如果方法上加了如@GetMapping等,便会将其记录下来,记录它们的路径、对应的控制器方法

// 如果用 DispatcherServlet 初始化时默认添加的组件, 并不会作为 bean, 给测试带来困扰
// ⬅️1. 加入RequestMappingHandlerMapping
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {return new RequestMappingHandlerMapping();
}

前面说到九大组件会先在容器中寻找看看有没有对应的Bean,如果没有就会使用默认的,但是使用默认的话,创建出来的RequestMappingHandlerMapping会直接称为DispatcherServlet的其中一个成员变量,在容器中无法获取,对我们的测试有很大影响,因此需要我们自己将RequestMappingHandlerMapping放到容器中。

public class A20 {private static final Logger log = LoggerFactory.getLogger(A20.class);public static void main(String[] args) throws Exception {//AnnotationConfig:支持java配置类的形式来构建容器//ServletWebServer:支持内嵌容器AnnotationConfigServletWebServerApplicationContext context =new AnnotationConfigServletWebServerApplicationContext(WebConfig.class); 作用 解析 @RequestMapping 以及派生注解,生成路径与控制器方法的映射关系, 在初始化时就生成RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);// 获取映射结果Map handlerMethods = handlerMapping.getHandlerMethods();handlerMethods.forEach((k, v) -> {System.out.println(k + "=" + v);});}
}

通过getHandlerMethods就能得到映射结果,运行可发现控制类中的所有请求的路径和对应的方法名都获取成功了

  • key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
  • value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
  • 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet

image-20221220161141153

准备好之后进行一下模拟请求

 请求来了,获取控制器方法  返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test1");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);

image-20221220161614148

在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet


5. RequestMappingHandlerAdapter 基本用途

RequestMappingHandlerAdapter翻译过来叫处理器适配器,其作用就是去调用被@RequestMapping标注的控制器方法

同样的,先将其放在容器中

RequestMappingHandlerAdapter 里面有个非常重要的方法protected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod),但是这个是保护方法,可以稍微做处理方法作用域

public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {@Overridepublic ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {return super.invokeHandlerMethod(request, response, handlerMethod);}
}

放在容器中

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();return handlerAdapter;
}

发送请求

 请求来了,获取控制器方法  返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test1");MockHttpServletResponse response = new MockHttpServletResponse();
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);
//
System.out.println(">>>>>>>>>>>>>>>>>>>>>");
 HandlerAdapter 作用: 调用控制器方法
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, (HandlerMethod) chain.getHandler());

image-20221220162807858

test1方法确实被调用了!!!


6. 参数和返回值解析器

通过这两个方法就可以的出当前所有的参数解析器和返回值解析器了

System.out.println(">>>>>>>>>>>>>>>>>>>>> 所有参数解析器");
for (HandlerMethodArgumentResolver resolver : handlerAdapter.getArgumentResolvers()) {System.out.println(resolver);
}System.out.println(">>>>>>>>>>>>>>>>>>>>> 所有返回值解析器");
for (HandlerMethodReturnValueHandler handler : handlerAdapter.getReturnValueHandlers()) {System.out.println(handler);
}

image-20221220163119653

但是要想进一步了解其是怎么实现的,最好的方法就是模拟实现

@PutMapping("/test3")
public ModelAndView test3(@Token String token) {log.debug("test3({})", token);return null;
}

这里准备一个映射地址/test3,里面的@Token是为了获取请求头的Token,怎么实现呢?

首先先来实现一下参数解析器

自定义一个TokenArgumentResolver实现HandlerMethodArgumentResolver接口

public class TokenArgumentResolver implements HandlerMethodArgumentResolver {@Override// 是否支持某个参数public boolean supportsParameter(MethodParameter parameter) {Token token = parameter.getParameterAnnotation(Token.class);return token != null;}@Override// 解析参数public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {return webRequest.getHeader("token");}
}

这里是先执行supportsParameter方法,判断是否有@Token注解,如果有才会执行resolveArgument方法

这样

虽然这个参数解析器完成了,我们还需要配置一下,因此MyRequestMappingHandlerAdapter并不知有新的参数解析器

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));return handlerAdapter;
}

这样一个参数解析器就搞好了

接下来实现一下返回值解析器

返回值处理器可以根据返回值不同而作出不同的选择。

通过也可以根据方法上是否有某一注解做出一些不同的处理。

@RequestMapping("/test4")
@Yml
public User test4() {log.debug("test4");return new User("张三", 18);
}
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {@Override//判断是否有注解public boolean supportsReturnType(MethodParameter returnType) {Yml yml = returnType.getMethodAnnotation(Yml.class);return yml != null;}@Override                   //  返回值public void handleReturnValue(Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {// 1. 转换返回结果为 yaml 字符串String str = new Yaml().dump(returnValue);// 2. 将 yaml 字符串写入响应HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);response.setContentType("text/plain;charset=utf-8");response.getWriter().print(str);// 3. 设置请求已经处理完毕mavContainer.setRequestHandled(true);}
}
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));handlerAdapter.setCustomReturnValueHandlers(List.of(ymlReturnValueHandler));return handlerAdapter;
}

具体操作和参数解析器类似。


相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...