跳至主要內容

RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter

mozzie大约 9 分钟Spring MVCSpring MVC

DispatcherServlet 的初始化

选择支持内嵌 Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:

public static void main(String[] args) {
    AnnotationConfigServletWebServerApplicationContext context =
        new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServlet 和 DispatcherServlet 注册对象。

@Configuration
@ComponentScan
public class WebConfig {
    /**
     * 内嵌 Web 容器工厂
     */
    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }

    /**
     * 创建 DispatcherServlet
     */
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    /**
     * 注册 DispatcherServlet,Spring MVC 的入口
     */
    @Bean
    public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }
}

运行 main() 方法,控制台打印出:

Tomcat 容器初始化成功,Spring 容器初始化成功,但 DispatcherServlet 还未被初始化。

当 Tomcat 服务器 首次 使用到 DispatcherServlet 时,才会由 Tomcat 服务器初始化 DispatcherServlet。

清空控制台信息,使用浏览器访问 localhost:8080,控制台打印出:

信息: Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO ] Initializing Servlet 'dispatcherServlet' 
[TRACE] No MultipartResolver 'multipartResolver' declared 
[TRACE] No LocaleResolver 'localeResolver': using default [AcceptHeaderLocaleResolver] 
[TRACE] No ThemeResolver 'themeResolver': using default [FixedThemeResolver] 
[TRACE] No HandlerMappings declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No HandlerAdapters declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No HandlerExceptionResolvers declared in servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No RequestToViewNameTranslator 'viewNameTranslator': using default [DefaultRequestToViewNameTranslator] 
[TRACE] No ViewResolvers declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No FlashMapManager 'flashMapManager': using default [SessionFlashMapManager] 
[INFO] Completed initialization in 482 ms 

完成 DispatcherServlet 的初始化。

使用 DEBUG 查看 DispatcherServlet 的初始化时机

断点 DispatcherServlet 的 onRefresh() 方法中 this.initStrategies(context); 的所在行:

protected void onRefresh(ApplicationContext context) {
    this.initStrategies(context);
}

以 DEBUG 方式重启程序,此时程序尚未执行到断点处。

再次在浏览器中访问 localhost:8080,程序执行到断点处。

查看调用栈可知,是从 GenericServlet 的 init() 方法执行到 onRefresh() 方法的:

public void init(ServletConfig config) throws ServletException {
    this.config = config;
    this.init();
}

因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。

使 DispatcherServlet 在 Tomcat 服务器启动时被初始化

修改添加到 Spring 容器的 DispatcherServlet 注册 Bean:

@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
    DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    registrationBean.setLoadOnStartup(1);
    return registrationBean;
}

设置其 loadOnStartup 为一个正数。

当存在多个 DispatcherServlet 需要被注册时,设置的 loadOnStartup 越大,优先级越小,初始化顺序越靠后。

在源码中loadOnStartup默认为-1

再次重启程序,根据控制台输出的内容可知,不仅完成 Tomcat 和 Spring 容器的初始化,DispatcherServlet 也初始化成功。

抽取配置信息到配置文件中

使用 @PropertySource 注解设置配置类需要读取的配置文件,以便后续读取配置文件中的内容。

要读取配置文件中的内容,可以使用 @Value 注解,但该注解一次仅仅能够读取一个值,现实是往往需要从配置文件中读取多个值。

可以使用 @EnableConfigurationProperties 注解完成配置文件信息与对象的绑定,后续使用时作为 @Bean 注解标记的方法的参数直接在方法中使用即可:

server.port=8000
spring.mvc.servlet.load-on-startup=1

Spring提供了一些默认的配置类:WebMvcProperties、ServerProperties

@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
    /**
     * 内嵌 Web 容器工厂
     */
    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {
        return new TomcatServletWebServerFactory(serverProperties.getPort());
    }

    /**
     * 创建 DispatcherServlet
     */
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    /**
     * 注册 DispatcherServlet,Spring MVC 的入口
     */
    @Bean
    public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet,
                                                                               WebMvcProperties webMvcProperties) {
        DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
        registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
        return registrationBean;
    }
}

再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 8000,DispatcherServlet 也在 Tomcat 启动时被初始化。

DispatcherServlet 初始化时执行的操作

回到 DispatcherServlet 的 onRefresh() 方法,它又调用了 initStrategies() 方法:

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context); // 文件上传的一种格式解析器
    initLocaleResolver(context); // 本地化信息解析器i18n
    initThemeResolver(context); // 解析和管理Web应用程序中的主题
    initHandlerMappings(context); // 路径映射
    initHandlerAdapters(context); // 处理适配器,调用具体的方法对用户发来的请求来进行处理
    initHandlerExceptionResolvers(context); // 处理器异常解析器
    initRequestToViewNameTranslator(context); // 请求信息(如HTTP请求的方法、路径等)转换为视图名称
    initViewResolvers(context); // 初始化视图解析器
    initFlashMapManager(context); // 初始化FlashMapManager,处理闪存的组件
}

重点:initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers

在所有的初始化方法中都有一个相似的逻辑,首先使用一个布尔值判断是否检测 所有 目标组件。

Spring 支持父子容器嵌套,如果判断的布尔值为 true,那么 Spring 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找。

以 initHandlerMappings() 为例:

private void initHandlerMappings(ApplicationContext context) {
    // 成员变量, 存储所有的映射处理器
    this.handlerMappings = null;
    // 是否检测所有的,到父子容器查找
    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerMapping> 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类型找, 存储到上面的集合中
            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.
        }
    }

    // 上面没找到提供一个默认的
    if (this.handlerMappings == null) {
        // 在DispatcherServlet.properties这个文件里面
        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;
        }
    }
}

没有的时候都用默认的,这些默认的是不会添加到Spring容器中的,只是放在Dispathcher的成员变量中

RequestMappingHandlerMapping

HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。

RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。

当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。

注册一个RequestMappingHandlerMapping

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(){
    return new RequestMappingHandlerMapping();
}

控制器

@Slf4j
@Controller
public class Controller1 {
    @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;
    }

    @RequestMapping("/test4")
    public User test4() {
        log.debug("test4");
        return new User("张三", 18);
    }

    @Data
    @AllArgsConstructor
    public static class User {
        private String name;
        private int age;
    }
}

编写 main() 方法,从 Spring 容器中获取 RequestMappingHandlerMapping,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:

public class Start {
    public static void main(String[] args) throws Exception {
        // 使用注解来注册web容器
        AnnotationConfigServletWebServerApplicationContext context =
                new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

        // 解析 @RequestMapping 以及派生注解,在初始化时生成路径与控制器方法的映射关系
        RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);

        // 将解析的注解存到一个map中, key是请求的信息(例: GET请求, 请求路径/test1), value是对应的方法信息(属于哪个类, 哪个方法)
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        handlerMethods.forEach((k, v) -> System.out.println(k + " = " + v));

        // 请求来了, 根据请求的信息, 获取控制器方法 返回执行链对象(包含拦截器)
        HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
        System.out.println(chain);
    }
}

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。

同样需要在配置类中将 RequestMappingHandlerAdapter 添加到 Spring 容器,但该类中需要测试的方法被 protected 修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:

public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

    @Override
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,
                                               HandlerMethod handlerMethod) throws Exception {
        return super.invokeHandlerMethod(request, response, handlerMethod);
    }
}

将我们自定义的adapter注册到容器中

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

main方法

    public static void main(String[] args) throws Exception {
        // 使用注解来注册web容器
        AnnotationConfigServletWebServerApplicationContext context =
                new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

        // 解析 @RequestMapping 以及派生注解,在初始化时生成路径与控制器方法的映射关系
        RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);

        // 将解析的注解存到一个map中, key是请求的信息(例: GET请求, 请求路径/test1), value是对应的方法信息(属于哪个类, 哪个方法)
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();

        // 无参get方法
        MockHttpServletRequest getNotArgumentReq = new MockHttpServletRequest("GET", "/test1");
        MockHttpServletResponse response = new MockHttpServletResponse();

        // 请求来了, 根据请求的信息, 获取控制器方法 返回执行链对象(包含拦截器)
        HandlerExecutionChain chain = handlerMapping.getHandler(getNotArgumentReq);

        // 使用requestMappingHandlerAdapter 来执行方法
        MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
        handlerAdapter.invokeHandlerMethod(getNotArgumentReq, response, (HandlerMethod) chain.getHandler());

        // 执行带参数的post方法
        MockHttpServletRequest postArgumentReq = new MockHttpServletRequest("POST", "/test2");
        postArgumentReq.setParameter("name", "du");
        HandlerExecutionChain chain1 = handlerMapping.getHandler(postArgumentReq);
        handlerAdapter.invokeHandlerMethod(postArgumentReq, response, (HandlerMethod) chain1.getHandler());
    }

实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?

显然是需要解析 @RequestParam 注解。

Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdapter 的 getArgumentResolvers() 方法获取所有参数解析器。

Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdapter 的 getReturnValueHandlers() 方法获取所有返回值处理器。

System.out.println("------------>所有的参数解析器");
Objects.requireNonNull(handlerAdapter.getArgumentResolvers()).forEach(System.out::println);
System.out.println("------------>所有的返回值解析器");
Objects.requireNonNull(handlerAdapter.getReturnValueHandlers()).forEach(System.out::println);

自定义参数解析器

创建一个自定义的注解

// 例如经常需要用到请求头中的 token 信息, 用下面注解来标注由哪个参数来获取它
// token=令牌
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
    log.debug("test3({})", token);
    return null;
}

自定义token注解的参数解析器

public class TokenArgumentResolvers implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Token.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return webRequest.getHeader("token");
    }
}

将自定义的注解解析器添加到RequestMappingHandlerAdapter中去

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter(){
    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
    handlerAdapter.setCustomArgumentResolvers(Collections.singletonList(new TokenArgumentResolvers()));
    return handlerAdapter;
}

测试

// 解析自定义的token
MockHttpServletRequest tokenRequest = new MockHttpServletRequest("PUT", "/test3");
tokenRequest.addHeader("token", "令牌");
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerExecutionChain tokenChain = handlerMapping.getHandler(tokenRequest);
handlerAdapter.invokeHandlerMethod(tokenRequest, response, (HandlerMethod) tokenChain.getHandler());

自定义返回值解析器

创建一个自定义的解析器

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Yaml {
}
@RequestMapping("/test4")
@Yaml
public User test4() {
    log.debug("test4");
    return new User("张三", 18);
}

自定义Yaml注解的返回值解析器

public class YamlReturnValueResolvers implements HandlerMethodReturnValueHandler {
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.hasMethodAnnotation(Yaml.class);
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        HttpServletResponse response = (HttpServletResponse) webRequest.getNativeResponse();
        // 1.返回值转换为Yaml格式
        String result = new org.yaml.snakeyaml.Yaml().dump(returnValue);
        // 2.写入response
        response.setContentType("text/plain;charset=utf-8");
        response.getWriter().print(result);

        // 3.设置请求已经处理完毕
        mavContainer.setRequestHandled(true);
    }
}

将自定义的注解解析器添加到RequestMappingHandlerAdapter中去

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter(){
    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
    // 添加返回值解析器
    handlerAdapter.setCustomReturnValueHandlers(Collections.singletonList(new YamlReturnValueResolvers()));
    return handlerAdapter;
}

测试

MockHttpServletRequest yamlRequest = new MockHttpServletRequest("", "/test4");
HandlerExecutionChain yamlChain = handlerMapping.getHandler(yamlRequest);
handlerAdapter.invokeHandlerMethod(tokenRequest, response, (HandlerMethod) yamlChain.getHandler());
System.out.println(response.getContentAsString());

处理流程总结

  1. RequestMappingHandlerMapping将请求映射为HandlerMethod
  2. RequestMappingHandlerAdapter的ArgumentResolver解析请求参数
  3. RequestMappingHandlerAdapter执行invokeHandlerMethod方法
  4. RequestMappingHandlerAdapter的ReturnValueResolver处理返回值
贡献者: du,mozzie