SpringMVC

20) RequestMappingHandlerMapping 与 RequestMapping- HandlerAdapter

RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter 俩是一对,分别用来

  • 处理 @RequestMapping 映射

  • 调用控制器方法、并处理方法参数与方法返回值

首先创建了 Servlet 容器(TomcatServletWebServer),其次创建了 Servlet 的前端控制器(DispatcherServlet),最后将控制器注册到容器中(Register)。

收获💡

  1. DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化 registrationBean.setLoadOnStartup()

  2. 在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化。每个具体的init的方法中都会去收集自定义的 Resolver 如果没有则会加载默认的配置 

    DispatcherServlet.properties

     可查看 Tomcat 启动日志查看

  3.  RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中 key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息  value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象。有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet  主要方法:org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

  4. RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件,如:HandlerMethodArgumentResolver 解析控制器方法参数;HandlerMethod-ReturnValueHandler 处理控制器方法返回值 主要方法:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

    /**
     *  为了方便调用 RequestMappingHandlerAdapter 的 invokeHandlerMethod 方法
     *  自定义的 HandlerAdapter
     */
    public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
        @Override
        public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
            return super.invokeHandlerMethod(request, response, handlerMethod);
        }
    }
    
    
    
    /**
     *  自定义参数解析器
     */
    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");
        }
    }
    
    
    
    /**
     *  自定义返回值解析器
     */
    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. 设置请求已经处理完毕(不让SpringMVC走视图解析的步骤)
            mavContainer.setRequestHandled(true);
        }
    }
    
    
    
    /**
     *  为了方便调用 RequestMappingHandlerAdapter 的 invokeHandlerMethod 方法
     *  自定义的 HandlerAdapter
     */
    public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
        @Override
        public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
            return super.invokeHandlerMethod(request, response, handlerMethod);
        }
    }
    
    
    
    
    // config 类省略部分代码
        @Bean
        public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
            TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
            YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
            // RequestMappingHandlerAdapter的 invokeHandlerMethod 范访问范围是私有的  创建子类 重写方法
            // 为 HandlerAdapter 添加自定义的 ArgumentResolver(参数解析器)与自定义的 ReturnValueHandler(返回值解析器)
            MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
            handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
            handlerAdapter.setCustomReturnValueHandlers(List.of(ymlReturnValueHandler));
            return handlerAdapter;
        }
    
    
    
    // controller 省略部分代码
        @PutMapping("/test3")
        public ModelAndView test3(@Token String token) {
            log.debug("test3({})", token);
            return null;
        }
    
        @RequestMapping("/test4")
    //    @ResponseBody
        @Yml
        public User test4() {
            log.debug("test4");
            return new User("张三", 18);
        }
    
    
    
    // 测试类
    public class A20 {
        private static final Logger log = LoggerFactory.getLogger(A20.class);
    
        public static void main(String[] args) throws Exception {
            AnnotationConfigServletWebServerApplicationContext context =
                    new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
    
            // 作用 解析 @RequestMapping 以及派生注解,生成路径与控制器方法的映射关系, 在初始化时就生成
            RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
    
    
            // 请求来了,获取控制器方法  返回处理器执行链对象
            MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/test3");
            request.setParameter("name", "张三");
            request.addHeader("token", "某个令牌");
            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());
    
            // 检查自定义返回值适配器的响应
            byte[] content = response.getContentAsByteArray();
            System.out.println(new String(content, StandardCharsets.UTF_8));
    
    
            /*
            学到了什么
                a. DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化
                b. 在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等
    
            本章介绍两个最为重要的组件
                a. RequestMappingHandlerAdapter, 以 @RequestMapping 作为映射路径
                b. RequestMappingHandlerAdapter, 调用 handler
                c. 控制器的具体方法会被当作 handler
                    - handler 的参数和返回值多种多样
                    - 需要解析方法参数, 由 HandlerMethodArgumentResolver 来做
                    - 需要处理方法返回值, 由 HandlerMethodReturnValueHandler 来做
            */
        }
    }
    

    执行效果

>>>>>>>>>>>>>>>>>>>>>
[DEBUG] 23:13:32.542 [main] com.itheima.a20.Controller1         - test3(某个令牌) 

Spring MVC学习教程之RequestMappingHandlerAdapter详解_java_脚本之家 (jb51.net)

21) 参数解析器

演示 - 常见参数解析器

代码参考

com.itheima.a21

         从handlerMapping 中获取到 HandlerMethod。遍历所有参数(准备好参数解析器组合,依次放入参数解析器,暴露出形参的名字-使用的DefaultParameterNameDiscoverer 既结合了标准的基于反射的参数又结合了本地变量表的方式。调用HandlerMethodArgumentResolver 的两个方法进行解析,参数解析器使用到了 WebDataBinder-用于对象绑定与数据转换以及ModelAndView容器,对中间存在的model进行存储)。使用到了组合模式组合模式 | 菜鸟教程 (runoob.com)。添加参数解析器的顺序也有讲究。因为@Parameter 与@ModelAttribute 都可以省略。 所以参数解析器分别配置了两个。并将能解析省略注解的参数解析器放置在最后用于兜底处理。

收获💡

  1. 初步了解 RequestMappingHandlerAdapter 的调用过程

    1. 控制器方法被封装为 HandlerMethod

    2. 准备对象绑定与类型转换

    3. 准备 ModelAndViewContainer 用来存储中间 Model 结果

    4. 解析每个参数值

  2. 解析参数依赖的就是各种参数解析器,它们都有两个重要方法

    • supportsParameter 判断是否支持方法参数

    • resolveArgument 解析方法参数

  3. 常见参数的解析

    • @RequestParam

    • 省略 @RequestParam

    • @RequestParam(defaultValue)

    • MultipartFile

    • @PathVariable

    • @RequestHeader

    • @CookieValue

    • @Value

    • HttpServletRequest 等

    • @ModelAttribute

    • 省略 @ModelAttribute

    • @RequestBody

  4. 组合模式在 Spring 中的体现

  5. @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取

参数名解析

演示 - 两种方法获取参数名

// 防止IDE自动编译,该包不能放在src 下,而是作为修改项目结构->依赖->添加 JAR或目录
package com.itheima.a22;

public class Bean2 {
    public void foo(String name, int age) {

    }
}



public class A22 {

    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException {
        // 1. 反射获取参数名(编译时使用 javac -parameters)
        Method foo = Bean2.class.getMethod("foo", String.class, int.class);
        /*for (Parameter parameter : foo.getParameters()) {
            System.out.println(parameter.getName());
        }*/

        // 2. 基于 LocalVariableTable 本地变量表(编译时使用 -g 参数名会维护到本地变量表中,可通过反编译class查看 javap -c -v)
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] parameterNames = discoverer.getParameterNames(foo);
        System.out.println(Arrays.toString(parameterNames));

        /*
            学到了什么
                a. 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名
                b. 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
                    1. 普通类, 会包含局部变量表, 用 asm 可以拿到参数名
                    2. 接口, 不会包含局部变量表, 无法获得参数名 (这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名)
         */
    }

}

代码参考

com.itheima.a22.A22

收获💡

  1. 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名

  2. 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况

    • 普通类, 会包含局部变量表, 用 asm 可以拿到参数名

    • 接口, 不会包含局部变量表, 无法获得参数名

      • 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名

23) 对象绑定与类型转换

底层第一套转换接口与实现

        Printer 把其它类型转为 String;Parser 把 String 转为其它类型;Formatter 综合 Printer 与 Parser 功能;Converter 把类型 S 转为类型 T;Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合;FormattingConversionService 利用其它们实现转换

底层第二套转换接口

         PropertyEditor 把 String 与其它类型相互转换;PropertyEditorRegistry 可以注册多个 PropertyEditor 对象;与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配

高层接口与实现

它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)

  • 首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)

  • 再看有没有 ConversionService 转换

  • 再利用默认的 PropertyEditor 转换

  • 最后有一些特殊处理

        SimpleTypeConverter 仅做类型转换;BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property;DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field;ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能.

DataBinder

public class TestDataBinder {

    public static void main(String[] args) {
        // 执行数据绑定
        MyBean target = new MyBean();
        DataBinder dataBinder = new DataBinder(target);
        dataBinder.initDirectFieldAccess();
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.add("a", "10");
        pvs.add("b", "hello");
        pvs.add("c", "1999/03/04");
        dataBinder.bind(pvs);
        System.out.println(target);
    }

    static class MyBean {
        private int a;
        private String b;
        private Date c;

        @Override
        public String toString() {
            return "MyBean{" +
                   "a=" + a +
                   ", b='" + b + '\'' +
                   ", c=" + c +
                   '}';
        }
    }
}

ServletRequestDataBinder

import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;

import java.util.Date;

public class TestServletDataBinder {

    public static void main(String[] args) {
        // web 环境下数据绑定
        MyBean target = new MyBean();
        ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("a", "10");
        request.setParameter("b", "hello");
        request.setParameter("c", "1999/03/04");

//        dataBinder.bind(new ServletRequestParameterPropertyValues(request));
        dataBinder.bind(request);

        System.out.println(target);
    }

    static class MyBean {
        private int a;
        private String b;
        private Date c;

        public int getA() {
            return a;
        }

        public void setA(int a) {
            this.a = a;
        }

        public String getB() {
            return b;
        }

        public void setB(String b) {
            this.b = b;
        }

        public Date getC() {
            return c;
        }

        public void setC(Date c) {
            this.c = c;
        }

        @Override
        public String toString() {
            return "MyBean{" +
                   "a=" + a +
                   ", b='" + b + '\'' +
                   ", c=" + c +
                   '}';
        }
    }
}

获取泛型参数

class Teacher {
}

class BaseDao<T> {
    T findOne() {
        return null;
    }
}

class TeacherDao extends BaseDao<Teacher> {
}
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.ResolvableType;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class TestGenericType {
    public static void main(String[] args) {
        // 小技巧
        // 1. java api
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
        Type type = TeacherDao.class.getGenericSuperclass();
        System.out.println(type);

        if (type instanceof ParameterizedType ) {
            System.out.println(((ParameterizedType) type).getActualTypeArguments()[0]);
        }

        // 2. spring api 1
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
        Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class);
        System.out.println(t);

        // 3. spring api 2
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(ResolvableType.forClass(TeacherDao.class).getSuperType().getGeneric().resolve());
    }

}

24) @ControllerAdvice 之 @InitBinder

准备 @InitBinder 在整个 HandlerAdapter 调用过程中所处的位置

    @ControllerAdvice
    static class MyControllerAdvice {
        @InitBinder
        public void binder3(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
        }
    }

    @Controller
    static class Controller1 {
        @InitBinder
        public void binder1(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
        }
        @RequestMapping("xxxxx")
        public void foo() {

        }
    }

         @ControllerAdvice 类下配置的是全局的类型转换器的 @InitBinder 方法一般设置无返回值,形参可为WebRequest、WebDataBinder。@Controller 及其衍生子类 仅能在局域生效

收获💡

  1. RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法

  2. RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法

  3. 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析

  4. 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂

25) 控制器方法执行流程

26) @ControllerAdvice 之 @ModelAttribute

        标注在控制器方法参数上的 @ModelAttribute 使用的是控制器参数解析器Servlet -ModelAttribute -MethodProcessor;用于解析url请求头中 `?arreibute0=abc&arreibute1=123`;

标注在控制器方法上的@ModelAttribute,使用的是HandlerAdpater。

27) 返回值处理器

        常见的返回值处理器与返回值

常见的返回值处理器
        org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@4c9e38
        org.springframework.web.method.annotation.ModelMethodProcessor@5d1e09bc
        org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@4bdc8b5d
        org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler@3bcd426c
        org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler@5f14a673
        org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@726a17c4
        org.springframework.web.servlet.mvc.method.annotation.HttpHeadersReturnValueHandler@5dc3fcb7
        org.springframework.web.servlet.mvc.method.annotation.CallableMethodReturnValueHandler@c4c0b41
        org.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler@76911385
        org.springframework.web.servlet.mvc.method.annotation.AsyncTaskMethodReturnValueHandler@5467eea4
        org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@160396db
        org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@7a799159
        org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler@40ab8a8
        org.springframework.web.method.annotation.MapMethodProcessor@6ff37443
        org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@65cc8228
    static class Controller {
        private static final Logger log = LoggerFactory.getLogger(Controller.class);
        
        // 1. 测试返回值类型为 ModelAndView
        public ModelAndView test1() {
            log.debug("test1()");
            ModelAndView mav = new ModelAndView("view1");
            mav.addObject("name", "张三");
            return mav;
        }

        // 2. 测试返回值类型为 String 时, 把它当做视图名
        public String test2() {
            log.debug("test 2()");
            return "view2";
        }
        // 3. 测试返回值添加了 @ModelAttribute 注解时, 此时需找到默认视图名
        // 如果找不到则会使用路径的名字 “test3”
        @ModelAttribute
//        @RequestMapping("/test3")
        public User test3() {
            log.debug("test3()");
            return new User("李四", 20);
        }
        
        // 4. 测试返回值不加 @ModelAttribute 注解且返回非简单类型时, 此时需找到默认视图名
        public User test4() {
            log.debug("test4()");
            return new User("王五", 30);
        }

        // 5. 测试返回值类型为 ResponseEntity 时, 此时不走视图流程
        public HttpEntity<User> test5() {
            log.debug("test5()");
            return new HttpEntity<>(new User("赵六", 40));
        }

        // 6. 测试返回值类型为 HttpHeaders 时, 此时不走视图流程
        public HttpHeaders test6() {
            log.debug("test6()");
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "text/html");
            return headers;
        }

        // 7. 测试返回值添加了 @ResponseBody 注解时, 此时不走视图流程
        @ResponseBody
        public User test7() {
            log.debug("test7()");
            return new User("钱七", 50);
        }
    }

        后三种返回值类型,不走视图解析流程是因为  `mavContainer.setRequestHandled(true);`

28) MessageConverter

         @Controller 标注的类上 @RequestBody 与 @ResponseBody 都使用了 MessageConverter;

        getSupportedMediaTypes 可通过 检查响应头(优先级高,统一设置 或者 @RequestMapping (produces = "application/json") ) 或者请求头确定使用哪种类型转换

    private static void test4() throws IOException, HttpMediaTypeNotAcceptableException, NoSuchMethodException {
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        ServletWebRequest webRequest = new ServletWebRequest(request, response);

        request.addHeader("Accept", "application/xml");
        response.setContentType("application/json");

        RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
                List.of(
                        new MappingJackson2HttpMessageConverter(), new MappingJackson2XmlHttpMessageConverter()
                ));
        processor.handleReturnValue(
                new User("张三", 18),
                new MethodParameter(A28.class.getMethod("user"), -1),
                new ModelAndViewContainer(),
                webRequest
        );
        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
    }

    
    @ResponseBody
    @RequestMapping(produces = "application/json")
    public User user() {
        return null;
    }

29) @ControllerAdvice 之 ResponseBodyAdvice 返回响应体前包装

        ResponseBodyAdvice 返回响应体前包装; ResponseBodyAdvice#supports用来检查Mapping上是否标注了 指定的注解(@ResponseBody),ResponseBodyAdvice#beforeBodyWrite 是具体的逻辑。我们所有的程序返回值大部分是Object类型的。

    @ControllerAdvice
    static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
        // 满足条件才转换
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
                AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
//                returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) {
                return true;
            }
            return false;
        }

        // 将 User 或其它类型统一为 Result 类型
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            // 如果已经是Result 类型的数据 则不用再转换了
            if (body instanceof Result) {
                return body;
            }
            return Result.ok(body);
        }



    @RestController
    public static class MyController {
        public User user() {
            return new User("王五", 18);
        }
    }

         ResponseBodyAdvice 失效的场景

// 参数 HttpServletResponse  使得mapping 无法被增强
@ResponseBody
@RequestMapping("test")
public void test(HttpServletRequest request, HttpServletResponse response){
    // 业务逻辑处理
}

 跟踪断点排查到了 mavContainer.isRequestHandled(); 那么何时设置的 

mavContainer.setRequestHandled(true);

我们搜索源码可得

 满足了 mavContainer != null;所以没有走到 `this.returnValueHandlers.handleReturnValue`。

SpringMVC系列第18篇:强大的RequestBodyAdvice解密_路人甲Java的博客-CSDN博客

30) 异常解析器

 org.springframework.web.servlet.DispatcherServlet#doDispatch 首先获取处理器,然后执行handle 方法。

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

返回 mv是null 的情况 返回值为 HttpEntity、HttpHeader、或者标注了@ResponseBody;

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

是对正常返回视图 或者是执行期间遇到的异常对异常的处理

 

package com.itheima.a30;

import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

public class A30 {
    public static void main(String[] args) throws NoSuchMethodException {
        ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
        resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
        resolver.afterPropertiesSet();

        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        // 1.测试 json
//        HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
//        Exception e = new ArithmeticException("被零除");
//        resolver.resolveException(request, response, handlerMethod, e);
//        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        // 2.测试 mav
//        HandlerMethod handlerMethod = new HandlerMethod(new Controller2(), Controller2.class.getMethod("foo"));
//        Exception e = new ArithmeticException("被零除");
//        ModelAndView mav = resolver.resolveException(request, response, handlerMethod, e);
//        System.out.println(mav.getModel());
//        System.out.println(mav.getViewName());
        // 3.测试嵌套异常
//        HandlerMethod handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
//        Exception e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
//        resolver.resolveException(request, response, handlerMethod, e);
//        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));

        // 4.测试异常处理方法参数解析
        HandlerMethod handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
        Exception e = new Exception("e1");
        resolver.resolveException(request, response, handlerMethod, e);
        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        /*
            学到了什么
                a. ExceptionHandlerExceptionResolver 能够重用参数解析器、返回值处理器,实现组件重用
                b. 能够支持嵌套异常
         */
    }

    static class Controller1 {
        public void foo() {

        }
        @ExceptionHandler
        @ResponseBody
        public Map<String, Object> handle(ArithmeticException e) {
            return Map.of("error", e.getMessage());
        }
    }

    static class Controller2 {
        public void foo() {

        }
        @ExceptionHandler
        public ModelAndView handle(ArithmeticException e) {
            return new ModelAndView("test2", Map.of("error", e.getMessage()));
        }
    }

    static class Controller3 {
        public void foo() {

        }
        @ExceptionHandler
        @ResponseBody
        public Map<String, Object> handle(IOException e3) {
            return Map.of("error", e3.getMessage());
        }
    }

    static class Controller4 {
        public void foo() {}
        @ExceptionHandler
        @ResponseBody
        public Map<String, Object> handler(Exception e, HttpServletRequest request) {
            System.out.println(request);
            return Map.of("error", e.getMessage());
        }
    }
}

         ExceptionHandlerExceptionResolver 能够解析 标注了@RequestMapping 的返回值 与响应值 是因为 在该Bean初始化的时候添加了一批参数解析器与返回值处理器。能够处理嵌套异常并返回最原始的异常是应为递归遍历了异常。

Spring Boot  自定义ErrorPage

 方式一

@Configuration
public class ErrorPageConfig {

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer) {
                ErrorPage errorPage400 = new ErrorPage(HttpStatus.BAD_REQUEST,"/error-400.html");
                ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND,"/error-404.html");
                ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR,"/error-500.html");

            configurableEmbeddedServletContainer.addErrorPages(errorPage400,errorPage404,errorPage500);
            }

        };

    }

}

方式二 

    @Bean
    public BasicErrorController basicErrorController() {
        ErrorProperties errorProperties = new ErrorProperties();
        errorProperties.setIncludeException(true);
        return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
    }
    
    /**
     *  添加视图名为error 的视图
     * @return
     */
    @Bean
    public View error() {
        return new View() {
            @Override
            public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
                System.out.println(model);
                response.setContentType("text/html;charset=utf-8");
                response.getWriter().print(/*"""
                        <h3>服务器内部错误</h3>
                        "*/"");
            }
        };
    }

    /**
     *  供 DisPatcherService 使用
     * @return
     */
    @Bean
    public ViewResolver viewResolver() {
        return new BeanNameViewResolver();
    }

         BasicErrorController 提供两种实现 一种是ModelAndView 另外一种是Json 格式的数据。BasicErrorController 可以传递配置属性 ErrorProperties。 通过读取配置文件动态加载。

server.error.include-exception=true
server.error.include-message=always
server.error.exception=false
server.error.whitelabel.enabled=true
server.error.whitelabel.message=always
server.error.whitelabel.exception=false

33) BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter

 添加HandlerMethod

    
// TomcatServletWebServerFactory、DispatcherServlet Tomcat DispatcherServletRegistrationBean 省略

// 添加三个Bean 都实现了Controller 接口 并且Bean的名字以'/'开头
    Component("/c1")
    public static class Controller1 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("this is c1");
            return null;
        }
    }

    @Component("/c2")
    public static class Controller2 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("this is c2");
            return null;
        }
    }

    @Bean("/c3")
    public Controller controller3() {
        return (request, response) -> {
            response.getWriter().print("this is c3");
            return null;
        };
    }

 添加 HandlerMapping 与 HandlerAdapter

    @Bean
    public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
        return new BeanNameUrlHandlerMapping();
    }

    @Bean
    public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
        return new SimpleControllerHandlerAdapter();
    }

// 模拟实现  HandlerMapping HandlerAdapter


    @Component
    static class MyHandlerMapping implements HandlerMapping {
        @Override
        public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
            String key = request.getRequestURI();
            Controller controller = collect.get(key);
            if (controller == null) {
                return null;
            }
            return new HandlerExecutionChain(controller);
        }
        // 依赖注入 ApplicationContext
        @Autowired
        private ApplicationContext context;
        // 存储 url-HandlerMethod 的 Map 信息
        private Map<String, Controller> collect;
        // 初始化的时候需要url 与 HandlerMethod
        @PostConstruct
        public void init() {
            // 获取 ApplicationContext 类型是 Controller的map信息,将名字以 '/' 开头的维护在一个Map 中
            collect = context.getBeansOfType(Controller.class).entrySet()
                    .stream().filter(e -> e.getKey().startsWith("/"))
                    .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
            System.out.println(collect);
        }
    }

    @Component
    static class MyHandlerAdapter implements HandlerAdapter {
        // HandlerAdapter是否要处理此 handlerMethod,如果是 Controller 类型的则处理
        @Override
        public boolean supports(Object handler) {
            return handler instanceof Controller;
        }

        @Override
        public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            if (handler instanceof Controller controller) {
                // 处理请求并返回
                controller.handleRequest(request, response);
            }
            // 此处省略返回 ModelAndView 的细节
            return null;
        }
        
        // 过时的方法无需关注
        @Override
        public long getLastModified(HttpServletRequest request, Object handler) {
            return -1;
        }
    }


 mvc 处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:

  1. 服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术

    • 路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器

      • jsp 不会匹配到 DispatcherServlet

      • 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet

    • 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean

       

    • 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量

      • HandlerMapping,初始化时记录映射关系

      • HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器

      • HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器

      • ViewResolver

  1. DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法

    • 例如根据 /hello 路径找到 @RequestMapping("/hello") 对应的控制器方法

    • 控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet

    • HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象

  2. DispatcherServlet 接下来会:

    1. 调用拦截器的 preHandle 方法

    2. RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod

      
      org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
      					org.springframework.web.servlet.DispatcherServlet#doDispatch
      // 应用注册拦截器的preHandle方法
      if (!mappedHandler.applyPreHandle(processedRequest, response)) {
      					return;
      }
      
      //使用给定的处理程序来处理此请求 主要是HandlerMethod
      mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
      
      ServletInvocableHandlerMethod
      	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#createInvocableHandlerMethod
      		org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
      			org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
      				org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
      					org.springframework.web.servlet.DispatcherServlet#doDispatch

       RequestMappingHandlerAdapter

       

      • @ControllerAdvice 全局增强点1️⃣:补充模型数据

      • @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器

      • 使用 HandlerMethodArgumentResolver 准备参数

        • @ControllerAdvice 全局增强点3️⃣:RequestBody 增强

      • 调用 ServletInvocableHandlerMethod

      • 使用 HandlerMethodReturnValueHandler 处理返回值

        • @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强

      • 根据 ModelAndViewContainer 获取 ModelAndView

        • 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程

          • 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null

        • 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程

    3. 调用拦截器的 postHandle 方法

      org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
      					org.springframework.web.servlet.DispatcherServlet#doDispatch
      
      // 应用注册拦截器的postHandle方法
      mappedHandler.applyPostHandle(processedRequest, response, mv);

    4. 处理异常或视图渲染

      • 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程

        • @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理

      • 正常,走视图解析及渲染流程

    5. 调用拦截器的 afterCompletion 方法

org.springframework.web.servlet.DispatcherServlet#doDispatch
	
        catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
            // 此处执行了 HandlerMethodInterceptor 的afterCompletion
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
        
    // 所有的catch 语句块捕获异常使用 

 ExceptionHandlerMethodResolver

 

1:首先调用的就是处理业务逻辑方法 service

org.springframework.web.servlet.FrameworkServlet#service,发现走的是super.service(request, response);

2:调用父类的service方法,里面根据不同的请求类型调用不同的方法,下面以get为例

javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse),

3:调用处理请求的方法

org.springframework.web.servlet.FrameworkServlet#doGet

4:调用处理请求接口

org.springframework.web.servlet.FrameworkServlet#processRequest

5:调用doService处理方法,来真正处理请求,service->doService 

org.springframework.web.servlet.FrameworkServlet#doService

!!!发现doService是个抽象方法,并没有具体的实现

6:这时候才真正的进入到doService,在子类中实现,校验一系列的参数

org.springframework.web.servlet.DispatcherServlet#doService

7:放入分发器中去处理

org.springframework.web.servlet.DispatcherServlet#doDispatch

7.1:是否是文件类型

org.springframework.web.servlet.DispatcherServlet#checkMultipart

7.2:查找处理器映射器

org.springframework.web.servlet.DispatcherServlet#getHandler

7.3:查找处理器适配器

org.springframework.web.servlet.DispatcherServlet#getHandlerAdapter

7.4:查找视图

org.springframework.web.servlet.HandlerAdapter#handle

7.5:填充视图 -- 后置处理器

org.springframework.web.servlet.HandlerExecutionChain#applyPostHandle

7.6:处理分发结果

org.springframework.web.servlet.DispatcherServlet#processDispatchResult

总结
其实springmvc的执行流程相对来说还是比较简单的, 大致的流程是

service->doGet(doPost)->doService->doDispatch->校验一些参数->寻找对应的处理器
————————————————
版权声明:本文为CSDN博主「奔跑的小猪zc」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45191798/article/details/101085042

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值