spring高级篇(四)

发布于:2024-04-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

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. 安全性:敏感信息(如数据库连接信息、密码等)通常不应该硬编码在代码中,而是应该放在配置文件中,并且限制访问这些配置文件的权限,以提高应用程序的安全性。

  3. 跨环境适应性:不同的环境(如开发环境、测试环境、生产环境)可能需要不同的配置信息。通过使用配置文件,可以很容易地在不同的环境中部署应用程序,只需要修改相应的配置文件即可。

  4. 便于集中管理:将所有配置信息放在一个或少数几个配置文件中,有助于集中管理配置,降低了维护成本。

  5. 便于扩展:当应用程序需要添加新的功能或模块时,通常需要修改或添加一些配置信息。通过使用配置文件,可以轻松地扩展应用程序的功能,而无需修改源代码。

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()方法:


 下一篇会重点介绍参数解析器和返回值处理器


网站公告

今日签到

点亮在社区的每一天
去签到