1、DispatcherServlet
DispatcherServlet 是 Spring MVC 中的一个关键组件,用于处理 Web 请求并将其分发给相应的处理器(Controller)进行处理。它是一个 Servlet,作为前端控制器(Front Controller)的核心,负责协调整个 Spring MVC 框架的请求处理过程。
其主要作用是将请求进行分发和转发,使得每个请求能够被正确的处理器处理,并将处理结果返回给客户端。它的配置通常在 Spring MVC 的配置文件中进行,可以配置拦截器、异常处理器、视图解析器等,以定制请求处理流程。
DispatcherServlet 的工作流程:
- 接收请求:客户端发送请求到 DispatcherServlet。
- 请求处理:DispatcherServlet 根据请求的 URL 找到对应的 HandlerMapping(处理器映射器),然后将请求分发给相应的 Controller(处理器)。
- 处理请求:Controller 处理请求,并返回一个 ModelAndView 对象,其中包含了视图名和模型数据。
- 视图解析:DispatcherServlet 根据返回的 ModelAndView 对象,通过 ViewResolver(视图解析器)找到对应的视图。
- 渲染视图:视图解析器将视图渲染成最终的 HTML 输出。
- 返回响应:DispatcherServlet 将渲染好的视图作为响应返回给客户端。
1.1、DispatcherServlet的初始化时机
通过一个案例来说明:
创建一个配置类:
@Configuration
@ComponentScan
public class Config {
/**
* 注册内嵌web容器工厂 tomcat容器
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(){
return new TomcatServletWebServerFactory();
}
/**
* 创建DispatcherServlet
* @return
*/
@Bean
public DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
/**
* 注册DispatcherServlet springmvc入口
* @param dispatcherServlet
* @return
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet){
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
return registrationBean;
}
}
通常是在DispatcherServlet首次使用时,才被tomcat容器初始化:
tomcat启动时DispatcherServlet并未初始化
在浏览器访问8080时,DispatcherServlet才被tomcat容器初始化。
可以在dispatcherServletRegistrationBean()方法中设置tomcat容器启动时即进行DispatcherServlet初始化:
/**
* 注册DispatcherServlet springmvc入口
* @param dispatcherServlet
* @return
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean
(DispatcherServlet dispatcherServlet){
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
//设置tomcat容器启动时即进行DispatcherServlet初始化
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
在设置tomcat容器启动时即进行DispatcherServlet初始化的.setLoadOnStartup();方法中,采用的是硬编码的方式。可以改进成从配置文件中读取对应的值:
在类上加入@PropertySource、@EnableConfigurationProperties注解:
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
- @PropertySource: 用于指定一个或多个属性源文件的位置,Spring 会在启动时加载指定的属性源文件,并将其中的属性值加载到 Spring 的环境(Environment)中。
- @EnableConfigurationProperties: 将@ConfigurationProperties注解标注的类注册为Spring Bean,并将其配置属性注入到这些Bean中。
WebMvcProperties和ServerProperties都是被@ConfigurationProperties 注解标注的类:
Spring 在启动时会加载 application.properties文件,并将其中的属性值注入到 WebMvcProperties和 ServerProperties类的实例中,使得它们可以被其他组件注入和使用:
/**
* 注册DispatcherServlet springmvc入口
* @param dispatcherServlet
* @return
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean
(DispatcherServlet dispatcherServlet,WebMvcProperties webMvcProperties){
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
//设置tomcat容器启动时即进行DispatcherServlet初始化
registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
补充:从配置文件读取值的优点
灵活性和可维护性:将应用程序的配置信息放在外部配置文件中,可以使配置信息与应用程序的代码分离。这样做使得修改配置信息变得更加方便,不需要修改源代码,只需要修改配置文件即可,提高了应用程序的灵活性和可维护性。
安全性:敏感信息(如数据库连接信息、密码等)通常不应该硬编码在代码中,而是应该放在配置文件中,并且限制访问这些配置文件的权限,以提高应用程序的安全性。
跨环境适应性:不同的环境(如开发环境、测试环境、生产环境)可能需要不同的配置信息。通过使用配置文件,可以很容易地在不同的环境中部署应用程序,只需要修改相应的配置文件即可。
便于集中管理:将所有配置信息放在一个或少数几个配置文件中,有助于集中管理配置,降低了维护成本。
便于扩展:当应用程序需要添加新的功能或模块时,通常需要修改或添加一些配置信息。通过使用配置文件,可以轻松地扩展应用程序的功能,而无需修改源代码。
1.2、DispatcherServlet的初始化执行的操作
在初始化时,执行了onRefresh(ApplicationContext context) 方法:
方法的内部又调用了initStrategies(context); 在该方法中,又初始化了不同的组件:
- initMultipartResolver(context): 用于处理文件上传请求。
- initLocaleResolver(context); 用于解析客户端的区域信息,以确定合适的本地化。
- initThemeResolver(context);用于解析请求中的主题信息,以确定所使用的页面主题。
- initHandlerMappings(context);用于将请求映射到相应的处理器(Controller)上。(重点)
- initHandlerAdapters(context);用于将请求分派给相应的处理器方法。
- initHandlerExceptionResolvers(context);用于处理请求过程中发生的异常。
- initRequestToViewNameTranslator(context);用于将请求映射到视图名称上。
- initViewResolvers(context);用于将逻辑视图名称解析为具体的视图实现。
- initFlashMapManager(context);用于处理重定向时的 Flash 属性传递。
1.2.1、HandlerMappings
initHandlerMappings(context); 方法的源码:大致的意思是,首先会在自己的容器中找有误HandlerMappings,如果没有就回去父容器去找:
父子容器中都没有,就会去初始化一个默认的HandlerMappings(在DispatcherServlet.properties中):
如果需要演示initHandlerMappings具体的执行过程,需要手动将RequestMappingHandlerMapping作为bean放入到容器中,因为之前提到,如果父子容器中都没有HandlerMapping,就会使用默认的RequestMappingHandlerMapping。
而默认的RequestMappingHandlerMapping 无法达到演示的效果,在配置类中加入:
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(){
return new RequestMappingHandlerMapping();
}
在主类中获取RequestMappingHandlerMapping,并且通过.getHandlerMethods(); 方法获取映射结果(RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map):
public class A18 {
public static void main(String[] args) throws Exception {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(Config.class);
//解析@RequestMapping 和其派生注解 生成路径与控制器的派生关系 在控制器初始化时生成
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
//获取映射结果
//RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map
//k:请求方式 路径{ /test4} v 方法信息com.itbaima.a18.Controller1#test4()
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k,v)->{
System.out.println( k + "=" + v);
});
}
}
Controller中有三个方法:
@Controller
public class Controller1 {
private static final Logger log = LoggerFactory.getLogger(Controller1.class);
@GetMapping("/test1")
public ModelAndView test1() throws Exception {
log.debug("test1()");
return null;
}
@PostMapping("/test2")
public ModelAndView test2(@RequestParam("name") String name) {
log.debug("test2({})", name);
return null;
}
@PutMapping("/test3")
public ModelAndView test3(String token) {
log.debug("test3({})", token);
return null;
}
}
控制台打印的结果:
{PUT /test3}=com.itbaima.a18.Controller1#test3(String)
{POST /test2}=com.itbaima.a18.Controller1#test2(String)
{GET /test1}=com.itbaima.a18.Controller1#test1()
当浏览器发送请求时,DispatcherServlet会调用.getHandler() 方法,根据请求路径Key 获取RequestMappingHandlerMapping 封装的 Map 对应的Value(HandlerMethod)
//发送请求了
//获取的结果会包装在拦截器链中
//HandlerExecutionChain with [com.itbaima.a18.Controller1#test1()] and 0 interceptors
HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
System.out.println(chain);
HandlerExecutionChain with [com.itbaima.a18.Controller1#test1()] and 0 interceptors
1.2.2、HandlerAdapters
为了演示,同样需要将RequestMappingHandlerAdapter注册成Bean。但是其核心invokeHandlerMethod() 方法的修饰符是protect,所以采用子类继承的方式,将子类注册成为Bean:
/**
* 继承RequestMappingHandlerAdapter 重写invokeHandlerMethod 的修饰符为public
*/
public class RequestMappingHandlerAdapterSub extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
/**
* 向容器中放入RequestMappingHandlerAdapterSub 替换掉默认的
* @return RequestMappingHandlerAdapterSub
*/
@Bean
public RequestMappingHandlerAdapterSub requestMappingHandlerAdapterSub(){
RequestMappingHandlerAdapterSub handlerAdapterSub = new RequestMappingHandlerAdapterSub();
return handlerAdapterSub;
}
上面提到,当浏览器发送请求时,DispatcherServlet会调用.getHandler() 方法,根据请求路径Key 获取RequestMappingHandlerMapping 封装的 Map 对应的Value(HandlerMethod)。
此时已经得到了HandlerMethod,HandlerAdapters负责将请求分派给相应的HandlerMethod方法。
handlerAdapterSub.invokeHandlerMethod(request,response, ((HandlerMethod) chain.getHandler()));
在HandlerAdapters中,有很多自带的解析器,可以分为两类:
- 参数解析器:
//handlerAdapterSub的参数解析器,用于解析@RequestParam等注解信息
System.out.println("<<<<<<<<<<<<<参数解析器<<<<<<<<<<<<<");
for (HandlerMethodArgumentResolver argumentResolver : handlerAdapterSub.getArgumentResolvers()) {
System.out.println(argumentResolver);
}
其中第一个就是解析参数中@RequestParam注解的
- 返回值解析器:
System.out.println("<<<<<<<<<<<<<返回值解析器<<<<<<<<<<<<<");
for (HandlerMethodReturnValueHandler returnValueHandler : handlerAdapterSub.getReturnValueHandlers()) {
System.out.println(returnValueHandler);
}
其中第一个是解析ModelAndView返回值类型的
我们还可以自定义参数解析器和返回值解析器:
现在有一个方法test3,参数上加了自定义的@Token注解
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.info("test3({})", token);
return null;
}
显然通过HandlerAdapters自带的参数解析器是无法解析的,需要自定义参数解析器:
在自定义参数解析器中我们进行了两步操作:
- 判断参数上是否加上了自定义类型的注解
- 对加上了自定义注解的方法进行操作
/**
* 自定义@Token注解的参数解析器
*/
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 判断是否加了@Token注解 只对有@Token注解的方法生效
* @param methodParameter
* @return
*/
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Token token = methodParameter.getParameterAnnotation(Token.class);
return token!=null;
}
/**
* 对加了@Token注解的方法执行操作
* parameter – 要解析的方法参数。。
* mavContainer – 当前请求的 ModelAndViewContainer
* webRequest – 当前请求
* binderFactory – 用于创建 WebDataBinder 实例的工厂
*/
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
return nativeWebRequest.getHeader("token");
}
}
还需要在注册RequestMappingHandlerAdapter Bean的时候设置自定义的参数解析器:
//将自定义的TokenArgumentResolver加入RequestMappingHandlerAdapterSub
handlerAdapterSub.setCustomArgumentResolvers(Collections.singletonList(new TokenArgumentResolver()));
在controller中还有一个方法,在方法上加入了自定义的@Yml注解,是希望将返回的结果转成yml格式(和将加上了@ResponseBody注解的方法的返回值转成JSON返回给前端是一个道理)
@RequestMapping("/test4")
@Yml
public User test4() {
log.debug("test4");
return new User("张三", 18);
}
需要自定义返回值处理器:
在返回值处理器中,进行的两步操作和参数处理器中的操作类似。
/**
* 自定义@YML返回值解析器
*/
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Yml yml = returnType.getMethodAnnotation(Yml.class);
return yml!= null;
}
/**
* returnValue – 处理程序方法返回的值
* returnType – 返回值的类型。
* mavContainer – 当前请求的 ModelAndViewContainer
* webRequest – 当前请求
*/
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
String str = new Yaml().dump(returnValue);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().print(str);
//设置请求已响应完成
mavContainer.setRequestHandled(true);
}
}
还需要在注册RequestMappingHandlerAdapter Bean的时候设置自定义的返回值解析器:
//将自定义的YmlReturnValueHandler加入RequestMappingHandlerAdapterSub
handlerAdapterSub.setCustomReturnValueHandlers(Collections.singletonList(new YmlReturnValueHandler()));
下面通过debug的方式演示一下自定义参数解析器的执行过程,加深一下印象:
启动程序,首先执行到发送模拟请求:
将请求分派到了HandlerAdapters:
进入自定义参数解析器,判断参数中是否加入了@Token注解:
对加了@Token注解的方法执行操作:
执行test3()方法:
下一篇会重点介绍参数解析器和返回值处理器