DispatcherServlet初始化机制
1. DispatcherServlet初始化时机
首先编写一段代码,手动使用内嵌tomcat服务器:
WebConfig.java
@Configuration
@ComponentScan
public class WebConfig {
// 1. 内嵌web容器工厂
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(){
return new TomcatServletWebServerFactory();
}
// 2. 创建DispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
// 3. 注册DispatcherServlet到tomcat容器中
// 凡是请求路径为"/"的请求都会交给dispatcherServlet来处理
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet){
return new DispatcherServletRegistrationBean(dispatcherServlet,"/");
}
}
编写一段测试类:
public class A20 {
private static final Logger logger = LoggerFactory.getLogger(A20.class);
public static void main(String[] args) {
// 准备一个支持内嵌tomcat容器的一个Spring容器的实现, 并加载配置类
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}
运行后的控制台如图所示:
20:58:34.577 [main] INFO org.springframework.boot.web.embedded.tomcat.TomcatWebServer -- Tomcat initialized with port 8080 (http)
七月 05, 2024 8:58:34 下午 org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
七月 05, 2024 8:58:34 下午 org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
七月 05, 2024 8:58:34 下午 org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/10.1.25]
七月 05, 2024 8:58:34 下午 org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
20:58:34.837 [main] INFO org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext -- Root WebApplicationContext: initialization completed in 1112 ms
七月 05, 2024 8:58:34 下午 org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
20:58:34.969 [main] INFO org.springframework.boot.web.embedded.tomcat.TomcatWebServer -- Tomcat started on port 8080 (http) with context path '/'
我们从上面的日志发现,tomact和IOC容器已经初始化成功了。但是DispatcherServlet一开始并没有初始化。
DispatcherServlet对象是由IOC容器创建的,但是该对象是由TomcatServletWebServerFactory
在首次使用到Dispatcher对象时初始化的。
我们首先访问一下"/"下面的路由看一下控制台日志:
七月 05, 2024 9:06:14 下午 org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring DispatcherServlet 'dispatcherServletRegistrationBean'
21:06:14.583 [http-nio-8080-exec-1] INFO org.springframework.web.servlet.DispatcherServlet -- Initializing Servlet 'dispatcherServletRegistrationBean'
21:06:14.909 [http-nio-8080-exec-1] INFO org.springframework.web.servlet.DispatcherServlet -- Completed initialization in 326 ms
21:06:14.923 [http-nio-8080-exec-1] WARN org.springframework.web.servlet.PageNotFound -- No mapping for GET /hello
21:06:14.932 [http-nio-8080-exec-1] WARN org.springframework.web.servlet.PageNotFound -- No endpoint GET /hello.
我们可以从日志中看到,DispatcherServlet对象会在第一个请求以"/"为路由时进行初始化,紧接着会进行不同组件的初始化步骤。
那我们不想让DispatcherServlet对象在第一次请求发送过来的时候进行初始化,而是在ioc容器创建好DispatcherServlet对象后立刻进行初始化,我们可以在注册DispatcherServlet对象时设置DispatcherServlet对象启动优先级为最高。
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet){
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
bean.setLoadOnStartup(1);
return bean;
}
重新启动观察控制台日志:
21:14:54.420 [main] INFO org.springframework.boot.web.embedded.tomcat.TomcatWebServer -- Tomcat initialized with port 8080 (http)
七月 05, 2024 9:14:54 下午 org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
七月 05, 2024 9:14:54 下午 org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
七月 05, 2024 9:14:54 下午 org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/10.1.25]
七月 05, 2024 9:14:54 下午 org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
21:14:54.675 [main] INFO org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext -- Root WebApplicationContext: initialization completed in 1097 ms
七月 05, 2024 9:14:54 下午 org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
七月 05, 2024 9:14:54 下午 org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring DispatcherServlet 'dispatcherServletRegistrationBean'
21:14:54.816 [main] INFO org.springframework.web.servlet.DispatcherServlet -- Initializing Servlet 'dispatcherServletRegistrationBean' // DispatcherServlet 对象初始化成功!!!
21:14:55.234 [main] INFO org.springframework.web.servlet.DispatcherServlet -- Completed initialization in 418 ms
21:14:55.235 [main] INFO org.springframework.boot.web.embedded.tomcat.TomcatWebServer -- Tomcat started on port 8080 (http) with context path '/'
2 DispatherServlet 初始化做了什么
Dispatcher对象初始化的时机是在调用DispatcherServlet类中的onRefresh()方法,在该方法中又有如下方法:
protected void initStrategies(ApplicationContext context) {
// 初始化文件上传方面的解析器
this.initMultipartResolver(context);
// 初始化本地化方面的解析器
this.initLocaleResolver(context);
this.initThemeResolver(context);
// 初始化处理器(路径)映射方面的解析器,如RequestMapping, GetMapping等
this.initHandlerMappings(context); ---> 重要
// 初始化处理器(路径)适配 -- 用于适配不同形式的控制器方法,然后调用控制器
this.initHandlerAdapters(context); ---> 重要
// 初始化处理器异常解析器 -- 出现异常时解析异常
this.initHandlerExceptionResolvers(context); ---> 重要
this.initRequestToViewNameTranslator(context);
// 初始化视图方面的解析
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
3. RequestMappingHandlerMapping 基本用途
RequestMappingHandlerMapping又叫处理器映射器
,是用来建立请求路径与处理器之间的映射关系。
处理器映射器会在DispatcherServlet对象初始化时会在当前ioc容器中找到控制器类@Controller注解标注的类
,找到控制器以后会进一步查看控制器中有哪些方法,如果方法上加上了类似于@Getpamming, @PostMapping, @RequetsMapping
等注解,处理器映射器就会筛选出来,然后记录出它们的路径以及对应的控制器方法,并存储在RequestMappingHandlerMapping中。
首先在配置文件中手动创建一个RequestMappingHandlerMapping
对象:
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(){
return new RequestMappingHandlerMapping();
}
接着从ioc容器中获取 RequestMappingHandlerMapping 对象用于解析@RequestMapping以及派生注解,生成路径与方法之间的映射关系。并获取映射结果:其中key代表的是路径信息, value代表的是控制器方法信息
// 解析 @RequestMapping注解及其派生注解,生成路径与控制器方法之间的映射关系(在初始化时生成)
RequestMappingHandlerMapping handlerMapping =
context.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k,v)->{
System.out.println(k +":"+v);
});
最后写一个控制器并写几个请求方法,启动主程序查看上面Map打印的结果:
@Controller
public class Controller1 {
private static final Logger logger = LoggerFactory.getLogger(Controller1.class);
@GetMapping("/test1")
public ModelAndView test1(){
logger.debug("test1()");
return null;
}
@GetMapping("/test2")
public ModelAndView test2(@RequestParam("name")String name){
logger.debug("test1({})",name);
return null;
}
}
运行结果如下:
INFO: Starting ProtocolHandler ["http-nio-8080"]
七月 06, 2024 12:08:23 上午 org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring DispatcherServlet 'dispatcherServletRegistrationBean'
00:08:23.882 [main] INFO org.springframework.web.servlet.DispatcherServlet -- Initializing Servlet 'dispatcherServletRegistrationBean'
00:08:24.692 [main] INFO org.springframework.web.servlet.DispatcherServlet -- Completed initialization in 809 ms
00:08:24.695 [main] INFO org.springframework.boot.web.embedded.tomcat.TomcatWebServer -- Tomcat started on port 8080 (http) with context path '/'
{GET [/test1]}:com.cherry.springanalyse.a20.Controller1#test1()
{GET [/test2]}:com.cherry.springanalyse.a20.Controller1#test2(String)
我们发现,处理器映射器会在在请求来之前就已经初始化好了。
但是当请求来的时候,处理器映射器又是怎么根据请求路由选择对应的方法以及获取控制器方法呢?
这是因为处理器映射器有一个getHandler()
方法可以通过request请求的路由信息获取到对应的方法信息:
HandlerExecutionChain chanin = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
System.out.println(chanin);
getHandler()
方法会返回一个处理器执行链对象,这个处理器链对象不光包括对应的方法信息,好多了一些拦截器,我们可以查看一下该拦截器对象:
HandlerExecutionChain with [com.cherry.springanalyse.a20.Controller1#test1()] and 0 interceptors
同时我们查看 HandlerExecutionChain
的源码可以发现,该类有两个属性成员,一个是 url 上对应的处理器,还有一个是用于存储多个拦截器的List集合。
public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
private final Object handler;
private final List<HandlerInterceptor> interceptorList = new ArrayList<>();
private int interceptorIndex = -1;
......
}
4. RequestMappingHandlerAdapter基本用途
RequestMappingHandlerAdapter又叫处理器适配器,作用就是根据路由去调用对应的控制器方法。
首先在配置文件中创建一个RequestMappingHandlerAdapter对象:
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(){
return new RequestMappingHandlerAdapter();
}
在ioc容器中获取RequestMappingHandlerAdapter
对象 。但是RequestMappingHandlerAdapter中的invokeHandlerMethod()
方法是受保护的,不能直接调用,因此我们可以创建RequestMappingHandlerAdapter的子类,并将该方法访问修饰设置为public
:
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
同样也需要在配置类中进行修改
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter(){
return new MyRequestMappingHandlerAdapter();
}
// 调用路由上对应的控制器方法
System.out.println(myRequestMappingHandlerAdapter.invokeHandlerMethod(request, response, (HandlerMethod) chanin.getHandler()));MockHttpServletRequest request = new MockHttpServletRequest("GET","/test2");
request.setParameter("name","syh");
MockHttpServletResponse response = new MockHttpServletResponse() ;
MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter =
context.getBean(MyRequestMappingHandlerAdapter.class);
HandlerExecutionChain chanin = handlerMapping.getHandler(request);
System.out.println(chanin);
System.out.println(">>>>>>>>>>>>>>>>>");
// 调用路由上对应的控制器方法
System.out.println(myRequestMappingHandlerAdapter.invokeHandlerMethod(request, response, (HandlerMethod) chanin.getHandler()));
运行结果如下:
com.cherry.web.controller.Controller - test2(syh)
我们可以看到适配器找到了对应的控制器方法。
HandlerAdapter
作用就是调用控制器方法,对于一些请求可能会比较复杂,例如会带一些解析器,那么HandlerAdapter是如何获取路由上的参数呢?这就涉及到了参数解析器
。我们可以试着打印这些解析器:
List<HandlerMethodArgumentResolver> handlerMethodArgumentResolverList = handlerAdapter.getArgumentResolvers();
handlerMethodArgumentResolverList.forEach(e->{
System.out.println(e);
});
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@245a060f
org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver@6edaa77a
org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver@1e63d216
org.springframework.web.servlet.mvc.method.annotation.PathVariableMapMethodArgumentResolver@62ddd21b
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMethodArgumentResolver@16c3ca31
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMapMethodArgumentResolver@2d195ee4
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@2d6aca33
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@21ab988f
org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver@29314cc9
org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver@4e38d975
org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver@35f8a9d3
org.springframework.web.servlet.mvc.method.annotation.ServletCookieValueMethodArgumentResolver@48ea2003
org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver@6b1e7ad3
org.springframework.web.servlet.mvc.method.annotation.SessionAttributeMethodArgumentResolver@63e5e5b4
org.springframework.web.servlet.mvc.method.annotation.RequestAttributeMethodArgumentResolver@13a37e2a
org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver@a50ae65
org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver@1280851e
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@5e840abf
org.springframework.web.servlet.mvc.method.annotation.RedirectAttributesMethodArgumentResolver@56de6d6b
org.springframework.web.method.annotation.ModelMethodProcessor@5972d253
org.springframework.web.method.annotation.MapMethodProcessor@4fcc0416
org.springframework.web.method.annotation.ErrorsMethodArgumentResolver@31e32ea2
org.springframework.web.method.annotation.SessionStatusMethodArgumentResolver@1473b8c0
org.springframework.web.servlet.mvc.method.annotation.UriComponentsBuilderMethodArgumentResolver@5b5c0057
org.springframework.web.servlet.mvc.method.annotation.PrincipalMethodArgumentResolver@749f539e
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@5ca1f591
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@551de37d
我们发现Spring的处理器适配器提供了很多参数解析器,例如解析Request Param参数的参数解析器,解析restful风格路由的参数解析器等等;除了参数解析器,还有一些返回值解析器,专门用于处理对返回对象的解析等
5. 自定义参数解析器
我们如何自己编写一个自定义参数解析器,例如@Token
注解:
// 自定义参数注解,用户获取请求头中的 token信息, 用下面注解来标注哪个参数来获取它
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}
接下来我们为该注解编写一个参数解析器 TokenArgumentResolver
package com.cherry.springanalyse.a20;
import org.springframework.core.MethodParameter;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 该解析器是否支持某个参数
* 如果该方法参数上有@Token注解,则会返回true, 否则返回false(不需要执行后面的解析)
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 根据方法参数获取参数上的注解
Token token = parameter.getParameterAnnotation(Token.class);
// 如果返回的参数上有@Token注解则返回true,否则返回false
return token != null;
}
// 解析参数
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getHeader("token");
}
}
紧接着在处理器适配器
中加入对该注解解析器的支持:
// 加入自定义参数解析器
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter(){
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
adapter.setArgumentResolvers(List.of(tokenArgumentResolver));
return new MyRequestMappingHandlerAdapter();
}
编写一个测试controller
@PutMapping("/test3")
public ModelAndView test3(@Token String token){
logger.debug("test3({})",token);
return null;
}
MockHttpServletRequest request = new MockHttpServletRequest("PUT","/test3");
request.setParameter("name","syh");
// 设置请求头
request.addHeader("token","1234567890");
MockHttpServletResponse response = new MockHttpServletResponse() ;
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);
MyRequestMappingHandlerAdapter handlerAdapter =
context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request,response, (HandlerMethod) chain.getHandler());
测试结果如下:
[DEBUG] 20:43:32:523 [main] com/cherry/springanalyse/a20/Controller1 --test3(123456)
6. 自定义返回值处理器
返回值处理器可以根据控制器返回的类型不同而做出不同的选择,例如ModelAndView
,String
等等。同时还会根据控制器上是不是加了某些注解,例如@ResponseBody
而做出一些不同的处理。
这里我们自定义一个注解@Yml
用来感受一下,将返回值对象转换为JSON
:
首先编写一个返回值处理器YmlReturenHandler
package com.cherry.springanalyse.a20;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.yaml.snakeyaml.Yaml;
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
// 该方法主要用于判断方法上有没有包含@Yml注解,如果有,则放行
// 如果没有,则不放行
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 判断干方法上是否有@Yml注解
Yml ymlAnnotation = returnType.getMethodAnnotation(Yml.class);
return ymlAnnotation==null;
}
/**
*
* @param returnValue: 返回值对象(值)
* @param returnType
* @param mavContainer
* @param webRequest
* @throws Exception
*/
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 将返回结果转换为yaml
String str = new Yaml().dump(returnValue);
// 将转换后的yaml字符串交给response
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
// 设置内容类型
response.setContentType("text/html;charset=utf-8");
response.getWriter().print(str);
// 由于这里已经直接将结果输出给response了,因此不再需要spring mvc来处理了,
// 可以让spring mvc知道此次请求已经处理完毕了
// 设置请求已经处理完毕
mavContainer.setRequestHandled(true);
}
}
同样的,在配置类里面将自己定义的返回处理器加入到处理器适配器中 。
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter(){
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
adapter.setArgumentResolvers(List.of(tokenArgumentResolver));
adapter.setReturnValueHandlers(List.of(ymlReturnValueHandler));
return new MyRequestMappingHandlerAdapter();
}
编写一个测试类:
@GetMapping("/test4")
@Yml
public User test4(){
return new User("lily",18);
}
class User{
private String name;
private int age;
// 此处省略getter,setter,构造方法
}
MockHttpServletRequest request = new MockHttpServletRequest("GET","/test4");
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>");
MyRequestMappingHandlerAdapter adapter = context.getBean(MyRequestMappingHandlerAdapter.class);
adapter.invokeHandlerMethod(request,response, (HandlerMethod) chain.getHandler());
// 检查响应
byte[] byteArray = response.getContentAsByteArray();
System.out.println(new String(byteArray, StandardCharsets.UTF_8));
测试结果如下:
{name:lily,age:18}
标签:初始化,web,springframework,org,时机,DispatcherServlet,servlet,method
From: https://www.cnblogs.com/lilyflower/p/18292562