自学内容网 自学内容网

spring mvc源码学习笔记之十一

  • pom.xml 内容如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qs.demo</groupId>
    <artifactId>test-011</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.3.30.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring4</artifactId>
            <version>3.0.11.RELEASE</version>
        </dependency>
    </dependencies>

</project>
  • src/main/webapp/WEB-INF/web.xml 内容如下
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <!-- / 表示除了 xxx.jsp 之外的所有请求 -->
        <!-- /* 表示所有请求 -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
  • src/main/webapp/WEB-INF/app-servlet.xml 内容如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 这个文件的名字是有讲究的 -->
    <!-- springmvc 的配置 -->

    <mvc:annotation-driven/>

    <!-- 开启组件扫描 -->
    <context:component-scan base-package="com.qs.demo"/>

    <!-- 配置视图解析器 -->
    <bean id="thymeleafViewResolver" class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
        <property name="characterEncoding" value="UTF-8"/>
        <property name="order" value="1"/>
        <property name="templateEngine">
            <bean class="org.thymeleaf.spring4.SpringTemplateEngine">
                <property name="templateResolver">
                    <bean class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
                        <property name="prefix" value="/WEB-INF/templates/"/>
                        <property name="suffix" value=".html"/>
                        <property name="templateMode" value="HTML"/>
                        <property name="characterEncoding" value="UTF-8"/>
                    </bean>
                </property>
            </bean>
        </property>
    </bean>

</beans>
  • src/main/webapp/WEB-INF/templates/t01.html 内容如下
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>t01</title>
</head>
<body>
<a th:href="@{/t02}">hello</a>
</body>
</html>
  • src/main/webapp/WEB-INF/templates/t02.html 内容如下
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>t01</title>
</head>
<body>
   <h1>Peter</h1>
</body>
</html>
  • com.qs.demo.A_ControllerAdvice 内容如下
package com.qs.demo;

import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

import java.beans.PropertyEditorSupport;
import java.util.Date;

/**
 * @author qs
 * @date 2025/01/10
 */
@ControllerAdvice(basePackages = "com.qs.demo.controller")
public class A_ControllerAdvice {

  @InitBinder({"time"})
  public void a(WebDataBinder webDataBinder) {
    webDataBinder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
      @Override
      public void setAsText(String text) throws IllegalArgumentException {
        setValue(new Date(Long.parseLong(text) / 1000));
      }
    });
  }

}
  • com.qs.demo.controller.FirstController 内容如下
package com.qs.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.beans.PropertyEditorSupport;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author qs
 * @date 2024/12/20
 */
@Controller
public class FirstController {

  @InitBinder({"date"})
  public void a(WebDataBinder webDataBinder) {
    webDataBinder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
      @Override
      public void setAsText(String text) throws IllegalArgumentException {
        setValue(new Date(Long.parseLong(text)));
      }
    });
  }

  @RequestMapping("/t01")
  public String t01() {
    return "t01";
  }

  @RequestMapping("/t02")
  public String t02() {
    return "t02";
  }

  @RequestMapping("/t03")
  @ResponseBody
  public String t03(@RequestParam Date date) {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
  }

  @RequestMapping("/t04")
  @ResponseBody
  public String t04(@RequestParam Date time) {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time);
  }

}

以上就是全部代码。需要注意的是代码里边有跟本文无关的内容。本文只需要关注 /t03 和 /t04 这2个接口即可。

写这个例子,主要是为了讲解 @InitBinder 注解。
在例子中,我们展示了 @InitBinder 的两种用法:一种局部的(或者说本地的,此处的本地指的是controller内部)和一种全局的。
不管是本地的还是全局的,代码所做的事情都是我们编码中常见的将前端传过来的 long 格式的日期转换为 Java 中的Date。无非是这里为了区分,全局的 @InitBinder 对前端传过来的 long 格式的时间先除以1000然后转为 Date,而本地的 @InitBinder 则直接将前端传来的 long 格式的时间转换为 Date
当然,还有一个点需要注意,在全局的 @InitBinder 上,我们将它的 value 值设置为 {"time"} 意思是只转换名字为 time 的参数,对应的就是接口 /t04,同样的,本地的 @InitBindervalue 值是 {"date"} 意思是只转换名字为 date 的参数,对应的就是接口 /t03。

测试 /t03 接口

http://localhost:8080/test_011/t03?date=1736501024000

结果是

2025-01-10 17:23:44

测试 /t04 接口

http://localhost:8080/test_011/t04?time=1736501024000

结果是

1970-01-21 10:21:41

以上是对代码的解释,下面开始分析 @InitBinder 的源码

  • 第一点就是看 @InitBinder 这个注解是在哪里被解析的

@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
        System.out.println("这个方法仔细看");
        System.out.println("这个方法仔细看");
        System.out.println("这个方法仔细看");

initControllerAdviceCache();

if (this.argumentResolvers == null) {

          System.out.println("初始化默认的参数解析器");
          System.out.println("初始化默认的参数解析器");
          System.out.println("初始化默认的参数解析器");

List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
    
          System.out.println("初始化默认的返回值处理器");
          System.out.println("初始化默认的返回值处理器");
          System.out.println("初始化默认的返回值处理器");

List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}

上面的代码是 RequestMappingHandlerAdapter 中的 afterPropertiesSet 方法。我们只需要关注第一行的 initControllerAdviceCache();

private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}

        System.out.println("寻找 @ControllerAdvice 注解。");

if (logger.isInfoEnabled()) {
logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
}

List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);

List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @ModelAttribute methods in " + adviceBean);
}
}
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);

            System.out.println("@InitBinder 方法有 " + binderMethods.size() + " 个");
            
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
if (logger.isInfoEnabled()) {
logger.info("在 ControllerAdviceBean ---> " + adviceBean
            + " 中检测到了 @InitBinder 方法。 Detected @InitBinder methods in " + adviceBean);
}
}
if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
if (logger.isInfoEnabled()) {
logger.info("Detected RequestBodyAdvice bean in " + adviceBean);
}
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
if (logger.isInfoEnabled()) {
logger.info("Detected ResponseBodyAdvice bean in " + adviceBean);
}
}
}

if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
}

这里边我们只需要关注跟 @InitBinder 相关的部分。它所做的事情就是先找到被 @ControllerAdvice 注解标记的 bean,然后遍历这些 bean,找到被 @InitBinder 注解标记的方法,把这些方法缓存到成员变量 initBinderAdviceCache 里边。这其实就是全局的 @InitBinder 被解析的过程。

  • 接下来看本地的 InitBinder 是在哪里被解析的
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {

        System.out.println("获取数据绑定工厂 WebDataBinderFactory ");

Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {

          System.out.println("找标记了 InitBinder 注解的方法");
          System.out.println("找标记了 InitBinder 注解的方法");
          System.out.println("找标记了 InitBinder 注解的方法");
      
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);

this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
// Global methods first
    
        System.out.println("全局的 @InitBinder 方法优先");
        System.out.println("全局的 @InitBinder 方法优先");
        System.out.println("全局的 @InitBinder 方法优先");
    
this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
if (clazz.isApplicableToBeanType(handlerType)) {
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
return createDataBinderFactory(initBinderMethods);
}

上面的代码是 RequestMappingHandlerAdaptergetDataBinderFactory 方法。可以注意下这个方法的入参 HandlerMethod,也就是我们写的 controller 里边处理请求的方案。这段代码的逻辑就是先获取 HandlerMethod 所在的类,也就是我们的 controller 类,然后找 controller 类中的 @InitBinder 方法,把这些方法解析完了以后放到成员变量 initBinderCache 里边。

到这里,我们就知道不管是全局的 @InitBinder 还是本地的 @InitBinder ,最终都是在被解析后放到了 RequestMappingHandlerAdapter 的成员变量里边。

  • 第二点就是看 InitBinder 方法是在哪里被调用的
/**
 * Initialize a WebDataBinder with {@code @InitBinder} methods.
 * If the {@code @InitBinder} annotation specifies attributes names, it is
 * invoked only if the names include the target object name.
 * @throws Exception if one of the invoked @{@link InitBinder} methods fail.
 */
@Override
public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {

        System.out.println("循环调用所有 @InitBinder 方法");
        System.out.println("循环调用所有 @InitBinder 方法");

for (InvocableHandlerMethod binderMethod : this.binderMethods) {

if (isBinderMethodApplicable(binderMethod, binder)) {

Object returnValue = binderMethod.invokeForRequest(request, null, binder);

                System.out.println("@InitBinder methods should return void ----> @InitBinder 方法应该返回 void");
                System.out.println("@InitBinder methods should return void ----> @InitBinder 方法应该返回 void");

if (returnValue != null) {
throw new IllegalStateException(
"@InitBinder methods should return void: " + binderMethod);
}
}
}
}

上面这段代码是 InitBinderDataBinderFactory 中的。它的逻辑就是遍历成员变量 binderMethods 并依次调用。关于这个成员变量是在哪里被赋值的,其实是在构造方法中

public InitBinderDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
@Nullable WebBindingInitializer initializer) {

super(initializer);
this.binderMethods = (binderMethods != null ? binderMethods : Collections.emptyList());
}

而这个构造方法又被子类 ServletRequestDataBinderFactory 的构造方法调用了

public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
@Nullable WebBindingInitializer initializer) {

super(binderMethods, initializer);
}

子类 ServletRequestDataBinderFactory 的构造方法又被 RequestMappingHandlerAdapter 调用了

protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {

return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}

调用的地方恰恰就是上面讲的 getDataBinderFactory 方法。

到这里,不要迷糊,只是追踪了 InitBinderDataBinderFactory 的成员变量 binderMethods 的赋值链路,并没有追踪 binderMethods 的调用链路。
直接说结论,binderMethods的是在参数解析的阶段被调用的,可以以 RequestResponseBodyMethodProcessorresolveArgument 为例看下

@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    parameter = parameter.nestedIfOptional();

    System.out.println("HttpMessageConverter 是在这里用的 ------");
    System.out.println("HttpMessageConverter 是在这里用的 ------");
    System.out.println("HttpMessageConverter 是在这里用的 ------");

    Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

    System.out.println("获取参数对应的变量名");
    System.out.println("获取参数对应的变量名");
    String name = Conventions.getVariableNameForParameter(parameter);
    System.out.println("获取参数对应的变量名,得到的值是 " + name);

    if (binderFactory != null) {

        System.out.println("创建 WebDataBinder -----> 开始");
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        System.out.println("创建 WebDataBinder -----> 结束");


        if (arg != null) {
            // 参数校验是在这里做的
            // 参数校验是在这里做的
            // 参数校验是在这里做的
            System.out.println("----- 参数校验是在这里做的 ---- 开始");
            System.out.println("----- 参数校验是在这里做的 ---- 开始");
            System.out.println("----- 参数校验是在这里做的 ---- 开始");

            validateIfApplicable(binder, parameter);

            System.out.println("----- 参数校验是在这里做的 ---- 结束");

            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {

                System.out.println("参数校验失败的话就抛出 MethodArgumentNotValidException 。");

                throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
            }
        }
        if (mavContainer != null) {
            mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
    }

    return adaptArgumentIfNecessary(arg, parameter);
}

@InitBinder 方法就是在如下这段代码中被调用的

WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);

多说一嘴,方法的调用链很长,可以根据本文的线索自己追踪。还有一点,RequestMappingHandlerAdapter 是一个很重要的处理器适配器,需要深入仔细研究。


原文地址:https://blog.csdn.net/qq_17586821/article/details/145122238

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!