【手撕Spring源码】深度理解SpringMVC【上】

这篇具有很好参考价值的文章主要介绍了【手撕Spring源码】深度理解SpringMVC【上】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

DispatcherServlet

既然我们讨论SpringMVC那么就必然绕不开一个东西叫做DispatcherServlet。

DispatcherServlet是SpringMVC的核心Servlet,也叫做前端控制器。它的主要作用是调度请求并将请求分发给相应的处理器。

我们要注意:
DispatcherServlet由Servlet容器创建,并且它的生命周期也是Servlet那套体系由Servlet容器进行控制

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

【手撕Spring源码】深度理解SpringMVC【上】

那么DispatcherServlet 在初始化的时候都做了什么呢?

DispatcherServlet 初始化的时候会调用onRefresh方法,而这个方法中又会调用initStrategies方法:
【手撕Spring源码】深度理解SpringMVC【上】

DispatcherServlet的initStrategies()方法用于初始化DispatcherServlet所需的各种策略(strategy):
【手撕Spring源码】深度理解SpringMVC【上】

主要包括:

  1. HandlerMapping:路径映射器,用于根据请求URL找到对应的Handler(Controller的方法)
  2. HandlerAdapter:处理器配置器,用于执行Handler,将请求参数绑定到Handler入参,创建返回的ModelAndView。
  3. ViewResolver:用于根据逻辑视图名解析成真正的视图View。
  4. LocaleResolver:本地化信息解析器,用于获取客户端的地域信息,国际化用。
  5. ThemeResolver:用于提供主题信息,一般用不太多。
  6. MultipartResolver:文件上传解析器,用于上传文件用,当有文件上传需求时使用。
  7. HandlerExceptionResolvers:控制器异常解析器

默认的DispatcherServlet会对这些策略进行自动检测和设置。我们也可以自定义这些策略。

其中最常自定义的就是HandlerMapping、ViewResolver和MultipartResolver。

所以这个方法主要是初始化一些DispatchServlet执行请求所需要的策略和组件。这些组件大多来自Spring容器,所以DispatcherServlet在初始化阶段首先要创建Spring容器,然后再从容器中获取这些策略的实现。有了这些策略和组件的支持,DispatchServlet才有能力完成从接收请求到产生响应的整个流程

RequestMappingHandlerMapping

RequestMappingHandlerMapping是一个HandlerMapping实现,它的作用是根据RequestMapping注解将请求映射到对应的Handler(Controller的方法)。
它会解析类及方法上的@RequestMapping注解,并根据注解中的信息注册Handler。当请求过来时,会根据URL查找对应的Handler进行执行。
主要功能如下:

  1. 解析@RequestMapping注解,获取URL、method等信息。

  2. 根据URL、method等条件查找对应的Handler。支持ANT风格的URL。

  3. 支持组合注解。即一个类或方法上有多个@RequestMapping注解的情况。会将这些信息组合起来一并解析。

  4. 支持派生注解。如@GetMapping、@PostMapping等也可以使用。

  5. 支持定制的HandlerMapping通过实现HandlerMapping接口。

主要的解析规则是:

  • 方法上的@RequestMapping优先级最高
  • 然后是类上的@RequestMapping
  • URL可以使用ANT风格的通配符
  • 当一个Handler同时匹配类和方法的@RequestMapping时,方法的映射规则优先

RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中

  • key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
  • value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
  • 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet

接下来我们使用代码模拟一下过程:

配置类:
【手撕Spring源码】深度理解SpringMVC【上】

这个地方我们如果不主动注入,DispatcherServlet 初始化时默认会添加RequestMappingHandlerMapping组件,但是并不会作为 bean,而是会当作DispatcherServlet 的属性。

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);

        // 获取映射结果
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        handlerMethods.forEach((k, v) -> {
            System.out.println(k + "=" + v);
        });

        // 请求来了,获取控制器方法  返回处理器执行链对象
        MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test4");
        HandlerExecutionChain chain = handlerMapping.getHandler(request);
        System.out.println(chain);
    }
}

这里的MockHttpServletRequest 用来简单的模拟请求而不用实现接口中繁杂的方法,所属依赖:
【手撕Spring源码】深度理解SpringMVC【上】

结果:
【手撕Spring源码】深度理解SpringMVC【上】

注意:

  • 路径与控制器方法的映射关系, 在初始化时就生成了
  • HandlerExecutionChain是一个HandlerMethod执行链,它包含一个HandlerMethod和多个HandlerInterceptor。当一个请求匹配到一个HandlerMethod时,会创建一个HandlerExecutionChain,然后顺序执行链中的所有拦截器和最后一个HandlerMethod。
    • 它的主要属性有:
      • HandlerMethod:要执行的HandlerMethod
      • HandlerInterceptorList:要执行的拦截器列表

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter是一个HandlerAdapter实现,它支持处理基于注解的Controller,即使用@RequestMapping映射请求的Controller。

它主要功能是:

  1. 绑定请求参数到Controller方法的参数上。支持@RequestParam、@RequestBody等注解。
  2. 执行HandlerMethod,为方法提供一个绑定了请求参数的可执行的方法参数数组。
  3. 处理返回值并设置到ModelAndViewContainer中,包括:
    • 返回String则当成逻辑视图名,交给视图解析器解析。
    • 返回void则当作逻辑视图名为空。
    • 返回ModelAndView对象则直接使用。
    • 返回其他对象则当作模型数据添加到Model中。

简单来说,HandlerAdapter就是为HandlerMethod的执行做好准备工作并执行,然后处理返回值

RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件(这两个组件都是RequestMappingHandlerAdapter的属性),如:

  • HandlerMethodArgumentResolver 解析控制器方法参数
  • HandlerMethodReturnValueHandler 处理控制器方法返回值

我们使用代码模拟一下:

【手撕Spring源码】深度理解SpringMVC【上】

这里我们使用的MyRequestMappingHandlerAdapter是因为invokeHandlerMethod方法是Protected的,我们继承一下才能用:
【手撕Spring源码】深度理解SpringMVC【上】

自定义参数解析器

例如我们经常需要用到请求头中的 token 信息, 用下面注解来标注由哪个参数来获取它:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}

Controller:

    @PutMapping("/test3")
    public ModelAndView test3(@Token String token) {
        log.debug("test3({})", token);
        return null;
    }

然后我们自定义一个参数处理器:

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");
    }
}

注意:

HandlerMethodArgumentResolver接口中定义了两个方法:

  • supportsParameter():该方法用于判断当前的参数解析器是否支持解析该方法参数。如果返回true,则会调用resolveArgument()进行实际解析。
  • resolveArgument():该方法会根据请求信息(webRequest)解析请求参数,并将解析后的参数绑定到方法入参上。
  • 这两个方法需要结合使用,DispatcherServlet会先调用supportsParameter判断当前解析器是否支持该参数,如果支持再调用resolveArgument()进行实际解析。
  • resolveArgument方法中的WebDataBinderFactory参数非常重要,它可以用来创建DataBinder,从而达到类型转换以及对象绑定的功能

我们自定义完一个参数解析器之后,还要讲我们的参数解析器加入到我们的RequestMappingHandlerAdapter 中去:

    // ⬅️2. 继续加入RequestMappingHandlerAdapter, 会替换掉 DispatcherServlet 默认的 4 个 HandlerAdapter
    @Bean
    public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
        MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
        handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
        return handlerAdapter;
    }

这里使用setCustomArgumentResolvers()就可以添加我们自定义的参数解析器了。

自定义返回值处理器

与前面的自定义参数处理器差不多:

HandlerMethodReturnValueHandler接口用于处理HandlerMethod的返回值。它定义了两个方法:

  • supportsReturnType():该方法用于判断当前的返回值处理器是否支持处理该返回值类型。如果返回true,则会调用handleReturnValue()方法进行实际处理。
  • handleReturnValue():该方法会对返回值进行处理,主要做了以下工作:
    1. 根据返回值添加模型数据到ModelAndViewContainer中。
    2. 设置逻辑视图名到ModelAndViewContainer。
    3. 对特殊的返回值类型(如ResponseEntity)进行处理。
    4. 处理异常,添加到ModelAndViewContainer中。

接下来我们看一个例子:

我们自定义了一个注解@Yml,接下来我们通过识别这个注解来达到返回值处理的效果:

【手撕Spring源码】深度理解SpringMVC【上】

自定义返回值处理器:

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. 设置请求已经处理完毕
        mavContainer.setRequestHandled(true);
    }
}

这里我们直接通过响应流的方式输出文字,不用走视图解析的流程,所以我们通过mavContainer.setRequestHandled(true)的方式佛告诉不需要使用视图解析

最后将自定义的返回值处理器加入到RequestMappingHandlerAdapter 中去:

【手撕Spring源码】深度理解SpringMVC【上】

参数解析器

绑定请求参数到Controller方法的参数上,这一重要的步骤在SpringMVC框架中就是参数解析器帮助我们完成的。

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

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

在RequestMappingHandlerAdapter 中自带了以下几种HandlerMethodArgumentResolver(参数解析器):

【手撕Spring源码】深度理解SpringMVC【上】

这里我们看几个常用的参数解析器,测试代码如下:

    static class Controller {
        public void test(
                @RequestParam("name1") String name1, // name1=张三
                String name2,                        // name2=李四
                @RequestParam("age") int age,        // age=18
                @RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
                @RequestParam("file") MultipartFile file, // 上传文件
                @PathVariable("id") int id,               //  /test/124   /test/{id}
                @RequestHeader("Content-Type") String header,
                @CookieValue("token") String token,
                @Value("${JAVA_HOME}") String home2, // spring 获取数据  ${} #{}
                HttpServletRequest request,          // request, response, session ...
                @ModelAttribute("abc") User user1,          // name=zhang&age=18
                User user2,                          // name=zhang&age=18
                @RequestBody User user3              // json
        ) {
        }
    }

这里我们首先来看RequestParamMethodArgumentResolver参数解析器,它对应的注解就是@RequestParam。也就对应了我们测试代码中这五个示例:
【手撕Spring源码】深度理解SpringMVC【上】

都一种是标准使用方式,对比第一种:

  • 第二种测试不显示使用@RequestParam的情况
  • 第三种测试涉及类型转换的情况
  • 第四种测试从环境变量中获取值的情况
  • 第五种测试获取文件的情况

测试代码如下:

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
        DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
        // 准备测试 Request对象,这里使用了mock进行模拟,可以让我们脱离web环境进行测试
        HttpServletRequest request = mockRequest();

        // 要点1. 控制器方法被封装为 HandlerMethod
        HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test", String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class, String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));

        // 要点2. 准备对象绑定与类型转换
        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

        // 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果
        ModelAndViewContainer container = new ModelAndViewContainer();

        // 要点4. 解析每个参数值
        for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
           
            //false 表示必须有 @RequestParam
            RequestParamMethodArgumentResolver resolver =   new RequestParamMethodArgumentResolver(beanFactory, false),                                    
            
            String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
            String str = annotations.length() > 0 ? " @" + annotations + " " : " ";
            parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

            if (composite.supportsParameter(parameter)) {
                // 支持此参数
                Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
//                System.out.println(v.getClass());
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + "->" + v);
                System.out.println("模型数据为:" + container.getModel());
            } else {
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName());
            }
        }


    }

    private static HttpServletRequest mockRequest() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("name1", "zhangsan");
        request.setParameter("name2", "lisi");
        request.addPart(new MockPart("file", "abc", "hello".getBytes(StandardCharsets.UTF_8)));
        Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
        System.out.println(map);
        request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
        request.setContentType("application/json");
        request.setCookies(new Cookie("token", "123456"));
        request.setParameter("name", "张三");
        request.setParameter("age", "18");
        return new StandardServletMultipartResolver().resolveMultipart(request);
    }

结果:
【手撕Spring源码】深度理解SpringMVC【上】

注意:

  • 测试代码主体分为以下几部分

    • 控制器方法被封装为 HandlerMethod:这一部分的工作一般是由HandlerMapping来做的,他们在做路径映射的时候就会把方法封装成HandlerMethod,这里我们没有使用。这里我们没有使用路径映射,所以要自己单独封装
    • 准备对象绑定与类型转换
    • 准备 ModelAndViewContainer 用来存储中间 Model 结果
    • 解析每个参数值
  • RequestParamMethodArgumentResolver有两种工作模式,对应着构造器中的第二个参数,true表示可以没有@RequestParam也能解析,false表示必须有@RequestParam才能解析

  • ServletRequestDataBinderFactory用于对象的绑定与类型转换,如果没有它那么我们这个时候解析出来的age其实是string类型的,而我们想要的是int类型。对象的绑定就是说方法接收到的是一个对象,该组件就可以将对象的属性与方法的参数进行绑定

  • 如果涉及到${ } #{ }的解析,前文我们提到过ApplicationContext 容器继承了EnvironmentCapable接口具有读取环境变量、配置文件的能力,也就是说这方面的功能我们是交给容器去实现的。这也就是说我们为什么在构造RequestParamMethodArgumentResolver的时候要传入一个容器对象

可以看到我们解析到第五个之后就报错了,这是因为此时我们只有1个解析器,我们可以多添加几种解析器,一个解析失败就换另一个尝试。Spring在底层使用了一种组合模式:

【手撕Spring源码】深度理解SpringMVC【上】
我们也使用这种组合模式:

【手撕Spring源码】深度理解SpringMVC【上】

这里的HandlerMethodArgumentResolverComposite就是一个参数解析器的复合,有了它之后我们可以更加方便。以后我们只需要调用复合解析器的supportsParameter、resolveArgument方法,而不需要关心其中包含有什么解析器。

然后我们加入PathVariableMethodArgumentResolver,这个解析器用来处理@PathVariable。

其原理如下:

  • HandlerMapping会将路径中的一组对应关系放在map集合里,存在request域中
    • 举个例子:请求路径:/test/124 ,注解中的值:/test/{id}
    • 于是map中就会把id --> 124 进行对应
  • 当执行到此解析器的时候,他就会根据@PathVariable中的name到map集合中去找
  • 找到之后就和我们方法的参数值进行绑定

@Value对应的解析器是ExpressionValueMethodArgumentResolver

而我们的第九种:
【手撕Spring源码】深度理解SpringMVC【上】

它使用的解析器是ServletRequestMethodArgumentResolver,它是根据参数的类型进行解析,事实上这个解析器他不光可以解析HttpServletRequest这一个类型,还有一些其他的类型,我们看看它的supportsParameter 方法:

【手撕Spring源码】深度理解SpringMVC【上】

再来说说@ModelAttribute,它由ServletModelAttributeMethodProcessor进行解析,它可以将名字等于值的参数与我们的java对象进行绑定,参数名对应着java对象中的属性。并且它还会把我们参数解析器处理得到的结果作为模型数据存储到ModelAndViewContainer中。
【手撕Spring源码】深度理解SpringMVC【上】

在Spring底层添加了两次ServletModelAttributeMethodProcessor:
【手撕Spring源码】深度理解SpringMVC【上】

分别处理@ModelAttribute标识和省略@ModelAttribute的情况。

我们还有一个注意点:我们参数解析器的顺序也是有讲究的:
【手撕Spring源码】深度理解SpringMVC【上】
我们省略的情况要统一放到最后判断,加入这里我将倒数第二和倒数第三换个位置,就会出现下面的结果:
【手撕Spring源码】深度理解SpringMVC【上】
我们使用@RequestBody解析的参数,会被误认为省略@ModelAttribute的情况而被ServletModelAttributeMethodProcessor先解析。

最后还要注意:

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

获取参数名

首先我们要知道我们的java文件在编译的时候,是不会保留参数名的,例如:
Java文件:
【手撕Spring源码】深度理解SpringMVC【上】
编译之后:
【手撕Spring源码】深度理解SpringMVC【上】

所以说这个参数名的获取也不是我们想象中的那么简单。

获取参数名的方法如下:

  1. 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名(通过ASM拿不到)
    【手撕Spring源码】深度理解SpringMVC【上】

  2. 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
    【手撕Spring源码】深度理解SpringMVC【上】

    • 普通类, 会包含局部变量表, 用 asm 可以拿到参数名(局部变量表使用反射是拿不到的)
    • 接口, 不会包含局部变量表, 无法获得参数名
      • 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
  • spring boot 在编译时会加 -parameters
  • 大部分 IDE 编译时都会加 -g

在Spring中,其底层使用了LocalVariableTableParameterNameDiscoverer来通过ASM去获得参数名

还记得我们在前面写过的代码吗?

我们在获得参数名称的时候直接使用MethodParameter.getParameterName()得到的参数名是null,我们还需要一句话:

 parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

而这个DefaultParameterNameDiscoverer中我们就可以发现LocalVariableTableParameterNameDiscoverer:

【手撕Spring源码】深度理解SpringMVC【上】

上面那个StandardReflectionParameterNameDiscoverer就是用来解决第一种情况。也就是说Spring对两种情况都进行了处理并进行了封装。

类型转换体系

Spring中的类型转换体系非常的复杂,涉及到:

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

接下来我们一个个来看:

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

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

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

这一套由JDK提供,并不是Spring中的

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

问:为什么Spring要同时使用这两套类型转换接口?
答:历史遗留问题,最早的Spring直至JDK的这套转换接口,随着发展这套转换接口的功能不够全面,例如:不支持任意两个类型的转换。为了拓展于是Spring引入了一套新的转换体系。不抛弃旧的是因为涉及到版本兼容原因。所以呈现出了两套转换接口并存的情况。

高层转换接口与实现

  • TypeConverter 有四个常见的实现类,在涉及到类型转换时(这四个实现类有的不止有转换功能,有的还有绑定功能、赋值功能等),会用到 TypeConverter Delegate 委派底层的两套转换接口ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)

    • 处理逻辑:
    • 首先看PropertyEditorRegistry中是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
    • 再看有没有 ConversionService 转换
    • 再利用默认的 PropertyEditor 转换
    • 最后有一些特殊处理
  • SimpleTypeConverter 仅做类型转换

  • BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property(也就是通过getter/setter方法)

  • DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field(直接通过反射拿到属性)

  • ServletRequestDataBinder 为 bean 的属性执行绑定,绑定时类型不一致就需要做类型转换也就是走底层那两套转换接口,属性赋值会根据 directFieldAccess 选择走 Property(BeanWrapperImpl ) 还是 Field(DirectFieldAccessor ),同时还具备校验与获取校验结果功能

接下来我们详细的说说这四个组件,他帮我们实现了基本的类型转换与数据绑定:

  • SimpleTypeConverter
  • BeanWrapperImpl
  • DirectFieldAccessor
  • ServletRequestDataBinder

SimpleTypeConverter

public class TestSimpleConverter {
    public static void main(String[] args) {
        // 仅有类型转换的功能
        SimpleTypeConverter typeConverter = new SimpleTypeConverter();
        Integer number = typeConverter.convertIfNecessary("13", int.class);
        Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class);
        System.out.println(number);
        System.out.println(date);
    }
}

BeanWrapperImpl

BeanWrapperImpl是Spring框架中一个重要的类,它的主要作用是:
将JavaBean属性的读取、写入操作统一进行处理。它可以设置和获取JavaBean的属性(本质是通过getter/setter方法),并通过PropertyEditor支持数据类型转换。

它简化了我们直接操作Bean的复杂度,提供了一套完备的机制用于属性的访问。在Spring框架中有广泛的使用,如:

  • DataBinder使用BeanWrapper进行数据绑定
  • BeanFactory使用BeanWrapper进行Bean属性的依赖注入
  • Expression解析器使用BeanWrapper获取或设置值
  • 等等
public class TestBeanWrapper {
    public static void main(String[] args) {
        // 利用反射原理, 为 bean 的属性赋值
        MyBean target = new MyBean();
        BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
        wrapper.setPropertyValue("a", "10");
        wrapper.setPropertyValue("b", "hello");
        wrapper.setPropertyValue("c", "1999/03/04");
        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 +
                   '}';
        }
    }
}

DirectFieldAccessor

与传统的通过getter/setter方法来读写属性相比,DirectFieldAccessor的优点是:

  1. 性能更高。直接访问field避免了方法调用的开销。
  2. 可以访问private字段。

与传统的BeanWrapperImpl相比,DirectFieldAccessor的优点在于性能更高,可以访问private字段。但是由于直接访问Field的方式破坏了封装性,也带来了一定隐患:

  1. 子类继承的字段也会变成可访问,这可能不是设计意图。
  2. 修改字段值会直接作用在原始对象上,违反JavaBean的设计模式。这可能会引起既有代码的问题。
public class TestFieldAccessor {
    public static void main(String[] args) {
        // 利用反射原理, 为 bean 的属性赋值
        MyBean target = new MyBean();
        DirectFieldAccessor accessor = new DirectFieldAccessor(target);
        accessor.setPropertyValue("a", "10");
        accessor.setPropertyValue("b", "hello");
        accessor.setPropertyValue("c", "1999/03/04");
        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 +
                   '}';
        }
    }
}

DataBinder

public class TestDataBinder {

    public static void main(String[] args) {
        // 执行数据绑定
        MyBean target = new MyBean();
        DataBinder dataBinder = new DataBinder(target);
        //根据 directFieldAccess 选择走 Property 还是 Field
        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 +
                   '}';
        }
    }
}

web环境下对应着其子类ServletRequestDataBinder:

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));

        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 +
                   '}';
        }
    }
}

我们前面所说的ServletModelAttributeMethodProcessor参数解析器中的功能就是它提供的。

ServletRequestParameterPropertyValues是Spring框架中一个实现了PropertyValues接口的类。
它的主要作用是:将ServletRequest中的参数转为PropertyValues,以便用于数据绑定。
【手撕Spring源码】深度理解SpringMVC【上】
当我们要将ServletRequest的参数绑定到JavaBean时,需要先将这些参数转为PropertyValues格式,才可以使用DataBinder进行绑定。
ServletRequestParameterPropertyValues常与WebDataBinder、RequestMappingHandlerAdapter等配合使用,完成高效的请求参数到Bean的绑定,

自定义转换器

接下来我们将上面的代码稍作修改:

【手撕Spring源码】深度理解SpringMVC【上】

可以看到这两个绑定上面这一种肯定是不能成功的,日期准换不支持这种格式,而下面这一种可以成功:
【手撕Spring源码】深度理解SpringMVC【上】
如果我们非要这样绑定呢?所以这里我们就引出一个新的问题:添加自定义转换器

这里我们有两种思路:

  • ConversionService + Formatter
  • PropertyEditorRegistry + PropertyEditor

也就是对应着我们前面说的底层两套转换接口。接下来我们开始实现:

PropertyEditorRegistry + PropertyEditor

这里我们就不自己创建ServletRequestDataBinder,而是改用ServletRequestDataBinderFactory帮我们创建ServletRequestDataBinder。因为使用这个工厂创建时可以添加各种选项,比如说想基于JDK转换接口进行拓展还是Spring转换接口进行拓展自己的转换器。

当然直接使用工厂还是没有转换功能的,这里我们可以借助@InitBinder注解帮助我们进行转换器的拓展。

@InitBinder是SpringMVC中一个非常重要的注解,它的主要作用是:
初始化WebDataBinder,为Web请求参数到JavaBean的绑定提供定制化的功能。

这里的WebDataBinder是DataBinder和ServletRequestDataBinder的中间类型:
【手撕Spring源码】深度理解SpringMVC【上】

例如,我们可以通过@InitBinder在控制器中添加:

  1. Custom editors 自定义属性编辑器
  2. Custom converters 自定义类型转换器
  3. Custom validators 自定义校验器

来增强WebDataBinder的功能,以完成更加丰富的数据绑定。

使用方式:

在@Controller的方法上标注@InitBinder,传入WebDataBinder类型的参数:

@Controller
public class MyController {
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // 添加编辑器、转换器、校验器
        binder.addCustomEditor(...);
        binder.addConverter(...); 
        binder.addValidator(...);
    }  
} 

这样,为@InitBinder方法传入的WebDataBinder对象添加的定制化功能会应用到该Controller的所有@RequestMapping方法中。

接下来我们就开始修改我们的代码:

public class TestServletDataBinderFactory {
    public static void main(String[] args) throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("birthday", "1999|01|02");
        request.setParameter("address.name", "西安");

        User target = new User();
        //将控制器中的方法包装成回调方法
        InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
        //创建工厂的时候将回调方法的传入,在创建DataBinder的时候会进行回调
        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);
               

        WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
        dataBinder.bind(new ServletRequestParameterPropertyValues(request));
        System.out.println(target);
    }


    static class MyController {
        @InitBinder
        public void aaa(WebDataBinder dataBinder) {
            // 扩展 dataBinder 的转换器
            dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
        }
    }

自定义转换器:

【手撕Spring源码】深度理解SpringMVC【上】

这个地方要注意我们使用InvocableHandlerMethod将@InitBinder标注的方法包了一层。

InvocableHandlerMethod是Spring MVC中一个重要的类,它的主要作用是:
代表一个可调用的控制器方法,通过它我们可以调用指定的控制器方法。

在创建ServletRequestDataBinderFactory的时候,我们传入了一个列表里面就装着这些控制器回调方法,如此该工厂在创建WebDataBinder就可以回调这些方法,对WebDataBinder进行类型转换的拓展。

我们点开addCustomFormatter方法,发现它使用的就是PropertyEditorRegistry + PropertyEditor:
【手撕Spring源码】深度理解SpringMVC【上】

ConversionService + Formatter

public class TestServletDataBinderFactory {
    public static void main(String[] args) throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("birthday", "1999|01|02");
        request.setParameter("address.name", "西安");

        User target = new User();
        //创建类型转换服务
        FormattingConversionService service = new FormattingConversionService();
        //将自定义的转化器添加到服务中
        service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
        //创建一个WebDataBinder的初始化器
        ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
        //设置类型转换服务
        initializer.setConversionService(service);
        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
               

        WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
        dataBinder.bind(new ServletRequestParameterPropertyValues(request));
        System.out.println(target);
    }

这里我们使用到了ConfigurableWebBindingInitializer,其主要作用就是初始化WebDataBinder。这次我们在创建ServletRequestDataBinderFactory时候没有传入回调函数的列表而是传入了这个WebDataBinder的初始化器。

如果我们同时使用这两种方法:

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), initializer);

@InitBinder的优先级更高

还有一种方法:使用默认 ConversionService 转换
【手撕Spring源码】深度理解SpringMVC【上】
【手撕Spring源码】深度理解SpringMVC【上】
要配合@DateTimeFormat注解进行使用。
也就是说:
@DateTimeFormat注解是DefaultFormattingConversionService负责解析的

最后我们总结一下:

ServletRequestDataBinderFactory 的用法和扩展点

  1. 可以解析控制器的 @InitBinder 标注方法作为扩展点,添加自定义转换器
    • 控制器私有范围
  2. 可以通过 ConfigurableWebBindingInitializer 配置 ConversionService 作为扩展点,添加自定义转换器
    • 公共范围
  3. 同时加了 @InitBinder 和 ConversionService 的转换优先级
    1. 优先采用 @InitBinder 的转换器
    2. 其次使用 ConversionService 的转换器
    3. 使用默认转换器
    4. 特殊处理(例如有参构造)

参数绑定过程

我们可以将参数解析看作下面过程:

  • 获取值
  • 类型转换
  • 参数绑定

参数绑定是属于参数解析里面的,而参数解析的逻辑都在HandlerMethodArgumentResolver 接口中的resolveArgument方法中,所以自然参数绑定的核心逻辑我们也可以在这个方法中找到。

简单参数绑定

@Controller
@RequestMapping("/ParameterBind")
public class ParameterBindTestController {
    @ResponseBody
    @RequestMapping("/test1")
    public String test1(int id){
        System.out.println(id);
        return "test1";
    }
}

我们完整的来一遍过程梳理:

  • 请求进入DispatcherServlet的doDispatch后,获取HandlerMethod。然后根据HandlerMethod来确认HandlerApapter,确认后执行HandlerAdapter的handle方法。这里确认HandlerApater为RequestMappingHandlerAdapter,在执行handlerMethod之前,需要处理参数的绑定。然后看看详细的参数绑定过程
  • 执行HandlerAdapter的handler方法后,进入RequestMappingHandlerAdapter的invokeHandleMethod方法
private ModelAndView invokeHandleMethod(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);

    WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
    ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
    //根据handlerMethod和binderFactory创建一个ServletInvocableHandlerMethod。后续把请求直接交给ServletInvocableHandlerMethod执行。
    //createRequestMappingMethod方法比较简单,把之前RequestMappingHandlerAdapter初始化的argumentResolvers和returnValueHandlers添加至ServletInvocableHandlerMethod中
    ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);

    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
    modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
    mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

    AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
    asyncWebRequest.setTimeout(this.asyncRequestTimeout);

    final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.setTaskExecutor(this.taskExecutor);
    asyncManager.setAsyncWebRequest(asyncWebRequest);
    asyncManager.registerCallableInterceptors(this.callableInterceptors);
    asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

    if (asyncManager.hasConcurrentResult()) {
        Object result = asyncManager.getConcurrentResult();
        mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
        asyncManager.clearConcurrentResult();

        if (logger.isDebugEnabled()) {
            logger.debug("Found concurrent result value [" + result + "]");
        }
        requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result);
    }

    requestMappingMethod.invokeAndHandle(webRequest, mavContainer);

    if (asyncManager.isConcurrentHandlingStarted()) {
        return null;
    }

    return getModelAndView(mavContainer, modelFactory, webRequest);
}

然后进入invokeAndHanldle方法,然后进入invokeForRequest方法,这个方法的职责是从request中解析出HandlerMethod方法所需要的参数,然后通过反射调用HandlerMethod中的method。代码如下:

public final Object invokeForRequest(NativeWebRequest request,
                                        ModelAndViewContainer mavContainer,
                                        Object... providedArgs) throws Exception {
        //这里对应的就是参数解析的过程(参数解析包括参数绑定)
        //从request中解析出HandlerMethod方法所需要的参数,并返回Object[]
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

        if (logger.isTraceEnabled()) {
            StringBuilder builder = new StringBuilder("Invoking [");
            builder.append(this.getMethod().getName()).append("] method with arguments ");
            builder.append(Arrays.asList(args));
            logger.trace(builder.toString());
        }
        //通过反射执行HandleMethod中的method,方法参数为args。并返回方法执行的返回值
        Object returnValue = invoke(args);

        if (logger.isTraceEnabled()) {
            logger.trace("Method [" + this.getMethod().getName() + "] returned [" + returnValue + "]");
        }

        return returnValue;
    }

直接进入getMethodArgumentValues方法看看其过程,代码如下:

/**
* 获取当前请求的方法参数值。
*/
private Object[] getMethodArgumentValues(
        NativeWebRequest request, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    //获取方法参数数组
    MethodParameter[] parameters = getMethodParameters();
    //创建一个参数数组,保存从request解析出的方法参数
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(parameterNameDiscoverer);
        GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());

        args[i] = resolveProvidedArgument(parameter, providedArgs);
        if (args[i] != null) {
            continue;
        }
        //判断之前RequestMappingHandlerAdapter初始化的那24个HandlerMethodArgumentResolver(参数解析器),是否存在支持该参数解析的解析器
        if (argumentResolvers.supportsParameter(parameter)) {
            try {
                args[i] = argumentResolvers.resolveArgument(parameter, mavContainer, request, dataBinderFactory);
                continue;
            } catch (Exception ex) {
                if (logger.isTraceEnabled()) {
                    logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
                }
                throw ex;
            }
        }

        if (args[i] == null) {
            String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
            throw new IllegalStateException(msg);
        }
    }
    return args;
}

进入HandlerMethodArgumentResolverComposite的resolveArgument方法:

public Object resolveArgument(
            MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
            throws Exception {
        //首先获取参数解析器,这里获取的逻辑是首先从argumentResolverCache缓存中获取该MethodParameter匹配的HandlerMethodArgumentResolver。如果为空,遍历初始化定义的那24个。查找匹配的HandlerMethodArgumentResolver,然后添加至argumentResolverCache缓存中
        HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");
        //解析参数
        return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }

然后进入HandlerMethodArgumentResolver的resolverArgument方法:

public final Object resolveArgument(
            MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
            throws Exception {
        //获取int的Class对象
        Class<?> paramType = parameter.getParameterType();
        //根据参数定义创建一个NamedValueInfo对象
        NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
        //根据参数名解析出对象的值
        Object arg = resolveName(namedValueInfo.name, parameter, webRequest);
        if (arg == null) {
            if (namedValueInfo.defaultValue != null) {
                arg = resolveDefaultValue(namedValueInfo.defaultValue);
            }
            else if (namedValueInfo.required) {
                handleMissingValue(namedValueInfo.name, parameter);
            }
            arg = handleNullValue(namedValueInfo.name, arg, paramType);
        }
        else if ("".equals(arg) && (namedValueInfo.defaultValue != null)) {
            arg = resolveDefaultValue(namedValueInfo.defaultValue);
        }
        //上面步骤获取的args是String类型,然后转换为方法参数所需要的类型(int)
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
            arg = binder.convertIfNecessary(arg, paramType, parameter);
        }

        handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

        return arg;
    }

这个方法的职责是,首先获取paramType。也就是int对应的Class对象。然后根据parameter对象创建一个NamedValueInfo对象。这个对象存放的就是参数名、是否必须、参数默认值3个成员变量。然后进入resolverName方法解析参数,里面的逻辑其实很简单,就是根据方法的参数名来获取request中的参数。关键代码如下

String[] paramValues = webRequest.getParameterValues(name);
if (paramValues != null) {
    arg = paramValues.length == 1 ? paramValues[0] : paramValues;
}

所以这里返回的值就是9999,这里返回的值还是String类型的。而需要的参数是int类型的。然后通过binder.coverIfNecessary方法把String转换为int类型返回。这个coverIfNecessary方法其底层就是使用的我们上面所说的类型转换体系TypeConverter,委派底层真正的两套类型转换接口进行处理。

对象绑定

@ResponseBody
@RequestMapping("/test2")
public String test2(User u){
    System.out.println(u.toString());
    return "test1";
}

我们直接看其参数解析器ServletModelAttributeMethodProcessor里面的处理逻辑:

/**
* 解析model中的参数,如果从ModelAndViewContainer未找到,直接通过反射实例化一个对象。具体实例化是通过父类的createAttribute方法,通过调用BeanUtils.instantiateClass方法来实例化的。这个对象便是后续传给test2(User u)方法的对象,但是此时创建的对象里面的值都还为空,注入值是通过bindRequestParameters方法来实现的。
*/
public final Object resolveArgument(
            MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest request, WebDataBinderFactory binderFactory)
            throws Exception {

        String name = ModelFactory.getNameForParameter(parameter);
        Object attribute = (mavContainer.containsAttribute(name)) ?
                mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);

        WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
        if (binder.getTarget() != null) {
            //将请求绑定至目标binder的target对象,也就是刚刚创建的attribute对象。
            bindRequestParameters(binder, request);
            //如果有验证,则验证参数
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors()) {
                if (isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }
        }

        // Add resolved attribute and BindingResult at the end of the model

        Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);

        return binder.getTarget();
    }

  • 该方法的职责是实例化一个parameterType的对象,然后根据request和attribute、name创建一个WebDataBinder对象,其中。然后进入bindRequestParameters方法绑定,根据reqeust中的参数创建一个MutablePropertyValues对象。MutablePropertyValues里面存放了一个或多个PropertyValue,其中PropertyValue用于保存单个bean属性的相关信息,比如参数名、参数值。这里需要注意的是PropertyValue并不是保存request对象的所有参数属性信息。而是一个参数属性对应一个PropertyValue。比如这里的reqeust对象,携带了两个参数,name和age,便会分别创建两个PropertyValue对象。
  • 创建MutablePropertyValues对象化后,进入DataBinder.applyPropertyValues(DataBinder.java line737)。会根据刚刚创建的User对象。创建一个BeanWrapperImpl对象,BeanWrapperImpl实现了PropertyAccessor(属性访问器)接口。这是spring-bean下的一个类,在Sping中,对Bean属性的存取都是通过BeanWrapperImpl类来实现的。BeanWarapperImpl在这里作用就是通过PropertyValue中的属性相关描述,注入到BeanWarapperImpl对应的java对象的属性中去。具体注入的方法是setPropertyValues,这个方法略复杂。它的职责简单总结起来就是根据属性名调用对应的set…方法。比如注入User对象的name属性时,通过反射获取setName方法。如果有该方法便调用。这也是为什么在定义SpringMVC model 对象需要set…方法。如果没有set方法,参数注入便会失败。

@ControllerAdvice 之 @InitBinder

@ControllerAdvice是一个注解,它的主要作用是:对一组Controller进行全局配置,比如:

  • 异常处理
  • 数据绑定
  • 拦截器绑定
  • 等等

使用@ControllerAdvice,我们可以将一些共享的代码抽取出来,应用到一组Controller上。比如:

// 定义一个统一异常处理类
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = Exception.class)
    public String exception(Exception e) {
        // 统一异常处理逻辑
    }
}

然后,GlobalExceptionHandler中的方法会应用到所有@Controller上,实现全局异常处理。

我们也可以将@ControllerAdvice的属性使用更加具体:

@ControllerAdvice(assignableTypes = {Controller1.class, Controller2.class})
public class ExampleAdvice {} 

上述@ControllerAdvice只应用于Controller1和Controller2类型的Controller,实现更加细粒度的配置。

@ControllerAdvice还支持像@ModelAttribute这样的注解,我们可以整合多个Controller的@ModelAttribute方法:

@ControllerAdvice
public class ExampleAdvice {
    @ModelAttribute("commonAttribute")
    public void commonAttribute() { ... }
}

这样,所有的Controller在其@RequestMapping方法被调用前,首先会执行@ModelAttribute(“commonAttribute”)方法。

除此之外,@ControllerAdvice还支持:

  • @InitBinder: 实现多个Controller的参数绑定定制
  • @Resource和@Autowired: 向一组Controller提供共享的bean
  • Interceptor: 向一组Controller添加拦截器
  • 等等

@ControllerAdvice注解是由Spring MVC的处理器映射器(HandlerMapping)解析的。
Spring MVC中的处理器映射器有以下几种:

  • RequestMappingHandlerMapping:处理@RequestMapping注解的映射
  • HandlerMethodMapping:处理映射到handler method的请求
  • ControllerAdviceBean:处理@ControllerAdvice注解的映射

其中,ControllerAdviceBean处理器映射器专门负责解析@ControllerAdvice注解。它会扫描ApplicationContext中所有标注@ControllerAdvice的Bean,解析其中定义的异常处理方法、数据绑定方法等,生成ControllerAdviceBean对象。后期,一些处理器会在内置初始化时(afterPropertiesSet)对相应的ControllerAdviceBean进行收集,然后适时调用。

接下来我们详细的说说@InitBinder:
【手撕Spring源码】深度理解SpringMVC【上】

它是由RequestMappingHandlerAdapter进行解析的。其解析后的结果存放在RequestMappingHandlerAdapter的两个属性值中:

【手撕Spring源码】深度理解SpringMVC【上】

  • initBinderCache:用来存放每个控制器类的initBinder,map中的key是控制器类型,值是@InitBinder标注的方法
  • initBinderAdviceCache:用来存储全局的initBinder

接下来我们看看这两种@InitBinder解析的时机:

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

  • RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
  • HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
  • HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers

重点💡:

  1. RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
  2. RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
  3. 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析
  4. 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂

附:说说MVC

MVC可以说是三层架构的一种实现类

MVC框架是一种设计模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。MVC框架通过这三个组件的协同工作,实现了应用程序的解耦、可维护性和可扩展性。

具体来说,MVC框架中的:

  • 模型(Model)表示应用程序的数据和业务逻辑
  • 视图(View)表示数据的展示方式
  • 控制器(Controller)则负责协调模型和视图之间的交互。

当用户请求一个页面时,控制器接收到请求后会调用模型来获取数据,然后将数据传递给视图进行展示。用户可以通过视图来操作数据,控制器则负责将这些操作反映到模型中。

我们举个例子:

假设我们正在开发一个简单的图书管理系统,其中包含以下功能:

  • 用户可以浏览图书列表,并查看每本书的详细信息;
  • 用户可以添加、编辑和删除图书。

在这个系统中,我们可以将:

  • 图书作为模型(Model),它包含图书的各种属性(如书名、作者、出版社、出版日期等)以及对图书的各种操作(如添加、编辑和删除等)

  • 视图(View)则是用来展示图书信息的界面,它可以是一个HTML页面、一个JSP页面或者一个JSON数据格式等。例如,我们可以创建一个图书列表页面,用来展示所有图书的基本信息,以及一个图书详情页面,用来展示某本书的详细信息。

  • 控制器(Controller)则负责协调模型和视图之间的交互。例如,当用户请求图书列表页面时,控制器会调用模型来获取所有图书的信息,然后将这些信息传递给视图进行展示。当用户点击某本书的链接时,控制器会根据图书的ID来调用模型,获取该书的详细信息,然后将这些信息传递给视图展示。

ModelAndView

ModelAndView是SpringMVC中重要的对象之一,它封装了模型数据和视图信息,用来从Controller向视图传递 数据和视图信息

它包含两个部分:

  • Model:模型数据,用来携带Controller处理后需要显示给用户的数据。可以是任意的POJO对象。
  • View:视图信息,指定了视图的名称或实例,告诉Spring要使用哪个视图渲染模型数据。可以是一个逻辑名称,也可以是View实例。

ModelAndView的创建方式有三种:

  1. 指定模型数据和逻辑视图名:

    ModelAndView mv = new ModelAndView("viewName", "modelAttr", modelObj);
    
  2. 指定模型数据和View实例:

    View view = ... 
    ModelAndView mv = new ModelAndView(view, "modelAttr", modelObj);
    
  3. 不指定任何参数,后续再添加模型数据和视图信息:

    ModelAndView mv = new ModelAndView();
    mv.addObject("modelAttr", modelObj); 
    mv.setViewName("viewName");
    

使用方式:

Controller方法可以返回ModelAndView,并在方法中创建并返回:

@Controller
public class MyController {
    @RequestMapping("/someUrl")
    public ModelAndView handleRequest() {
        ModelAndView mv = new ModelAndView("viewName");
        mv.addObject("modelAttr", modelObj);
        return mv;
    }
}

这样DispatcherServlet在调用完Controller后会获取到返回的ModelAndView,并使用其中的视图信息进行视图解析,得到View。然后利用ModelAndView中的模型数据渲染View,得到最终响应。

所以ModelAndView的主要作用是在Controller和View之间传递数据和视图信息。Controller将需要显示的数据和视图放入ModelAndView,然后DispatcherServlet使用这些信息进行视图解析和渲染,生成最终响应。

综上,ModelAndView是SpringMVC中非常重要且常用的对象,它承载了Controller处理请求后需要显示给用户的模型数据和指定的视图,起到了在Controller和View之间传递信息的作用。虽然现在有更加灵活的返回值支持,但是ModelAndView仍然是比较经典和简单的选择。

ModelAndViewContainer

ModelAndViewContainer用来存储 HandlerMethod的处理结果,它包含模型数据、逻辑视图名、异常信息等

它主要有以下作用:

  1. 存储从HandlerMethod中返回的模型数据。无论HandlerMethod的返回值是Model、ModelMap、Map等,都可以存储为模型数据。
  2. 存储HandlerMethod返回值中的逻辑视图名。
  3. 存储在HandlerMethod执行过程中出现的异常信息。
  4. 此外,也可以存储一些标志位,如是否跳转至登录页等信息。

那么ModelAndViewContainer与ModelAndView有什么联系吗?

简单来说ModelAndView就是通过ModelAndViewContainer得来的,我们来看看源码:

DispatcherServlet使用RequestMappingHandlerAdapter组件获得ModelAndView,涉及到handle方法,这是一个接口方法,对应的在RequestMappingHandlerAdapter中的实现方法就是handleInternal方法。
【手撕Spring源码】深度理解SpringMVC【上】

这个handleInternal方法在内部又通过invokeHandlerMethod也就是通过执行控制器方法来获得ModelAndView:
【手撕Spring源码】深度理解SpringMVC【上】

我们继续进入invokeHandlerMethod方法,发现这个方法中是通过getModelAndView获得的ModelAndView,而这个方法传入了ModelAndViewContainer,其内部原理就是创建一个ModelAndView,然后将ModelAndViewContainer内部的视图名,Model等东西赋值给ModelAndView最后返回:

【手撕Spring源码】深度理解SpringMVC【上】

所以ModelAndViewContainer是连接HandlerAdapter和DispatcherServlet的重要载体,它承载处理HandlerMethod的结果,然后创建ModelAndView传递给DispatcherServlet使用。它简化了HandlerMethod的返回值,方法可以返回各种类型,而不需要局限于ModelAndView。因为无论返回什么类型,都可以通过HandlerMethodReturnValueHandler处理后存储到ModelAndViewContainer中,然后再转化为ModelAndView交给DispatcherServlet使用

同时这也从侧面反映了我们前面在RequestMappingHandlerAdapter中所说的返回值处理器的重要性,这个返回值处理器一个很重要的方面就是把方法的返回值与ModelAndViewContainer相联系起来,当然前提是你走的是视图解析的流程,如果你只是想返回一个json数据那么就不需要走视图解析的流程。文章来源地址https://www.toymoban.com/news/detail-469371.html

到了这里,关于【手撕Spring源码】深度理解SpringMVC【上】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 从源码层面深度剖析Spring循环依赖

    **以下举例皆针对单例模式讨论** 图解参考 https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce 对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。 Spring 在创建 Bean 过程中,使用到了三级缓存,即 DefaultSingletonBeanRegistry.java 中定义的: 以 com.gyh.general 包下的 OneBe

    2024年02月14日
    浏览(38)
  • 从源码层面深度剖析Spring循环依赖 | 京东云技术团队

    以下举例皆针对单例模式讨论 图解参考 https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce 对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。 Spring 在创建 Bean 过程中,使用到了三级缓存,即 DefaultSingletonBeanRegistry.java 中定义的: 以 com.gyh.general 包下的 OneBean 为

    2024年02月13日
    浏览(36)
  • Java项目:ssm框架基于spring+springmvc+mybatis框架的民宿预订管理系统设计与实现(ssm+B/S架构+源码+数据库+毕业论文)

    本项目是一套ssm827基于SSM框架的民宿预订管理系统设计与实现,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含:项目源码、数据库脚本等,该项目附带全部源码可作为毕设使用。 项目都经过严格调试,eclipse 确保可以运行! 该系统功能

    2024年01月22日
    浏览(84)
  • 【C++】手撕vector类(从会用到理解)

    一、标准库中的vector类 1.1 vector类介绍 1.2 vector的常用接口 1.2.1 常用的构造函数 1.2.2 容量操作接口 (1)size (2)capacity (3)empty (4)resize (5)reserve 1.2.3 访问和遍历 (1)operator[] (2)迭代器 (3)at (4)back (5)front 1.2.4 vector的增删查改 (1)push_back (2)pop_back (3)

    2024年03月26日
    浏览(37)
  • 深度学习HashMap之手撕HashMap

    HashMap其实是数据结构中的哈希表在Java里的实现。 哈希表也叫散列表,我们先来看看哈希表的定义: 哈希表是根据关键码的值而直接进行访问的数据结构。 简单说来说 ,哈希表由两个要素构成: 桶数组 和 散列函数 。 我们可能知道,有一类基础的数据结构线性表,而线性

    2024年02月09日
    浏览(52)
  • 手撕深度学习中的优化器

    深度学习中的优化算法采用的原理是梯度下降法,选取适当的初值 params ,不断迭代,进行目标函数的极小化,直到收敛。由于负梯度方向时使函数值下降最快的方向,在迭代的每一步,以负梯度方向更新 params 的值,从而达到减少函数值的目的。 Gradient descent in deep learning

    2023年04月09日
    浏览(36)
  • 【初中生讲机器学习】14. 手撕公式,一篇带你理解逻辑回归!

    创建时间:2024-03-03 最后编辑时间:2024-03-10 作者:Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏,很高兴遇见你~ 我是 Geeker_LStar,一名初三学生,热爱计算机和数学,我们一起加油~! ⭐(●’◡’●) ⭐ 那就让我们开始吧! 嘿嘿,好几篇前,好像是在线性回归那篇

    2024年04月10日
    浏览(48)
  • 【C++高阶(四)】红黑树深度剖析--手撕红黑树!

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C++从入门到精通⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习C++   🔝🔝 如果说发明AVL树的人是天才,那么 发明红黑树的人可以称为天才中的 精英!为什么AVL树这么强大但是没啥 人用呢?就是因为红黑树比你还好

    2024年02月05日
    浏览(45)
  • 【手撕Spring - 深入篇】Spring 的设计理念和整体架构

    👉 博主介绍 : 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主 ⛪️ 个人社区:个人社区 💞 个人主页:个人主页 🙉 专栏地址: ✅ 带你手撕 Spring 🙉八股文专题:剑指大厂,

    2024年02月14日
    浏览(43)
  • springMVC理解

    springMVC是一种思想,将软件划分为,模型Model,视图View,控制器Controller。 MVC的工作原理:用户通过前端视图页面,发送请求到服务器,在服务器中请求被Controller接收,Controller调用相应的Model层处理请求,处理完毕返回到Controller,再根据请求处理的结果找到对应的视图,经过

    2024年04月16日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包