当前位置: 首页 > article >正文

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 是一个很重要的处理器适配器,需要深入仔细研究。


http://www.kler.cn/a/501951.html

相关文章:

  • SQL 详解数据库
  • 计算机网络(五)——传输层
  • WPF 如何添加系统托盘
  • 设计一篇利用python爬虫获取1688详情API接口的长篇软文
  • Oracle重启后业务连接大量library cache lock
  • linux-28 文本管理(一)文本查看,cat,tac,more,less,head,tail
  • EF Core一对一和多对多
  • AI的崛起:它将如何改变IT行业的职业景象?
  • 多模态论文笔记——CLIP
  • C#上位机通过CAN总线发送bin文件
  • 高阶C语言|探索指针的根源之目(进阶指针)
  • 云原生周刊:Prometheus 3.0 正式发布
  • 检测模型安全的更高级的方法
  • 例子:WeTextProcessing,如何查看现在已安装的这个模块的版本号呢?查看虚拟环境中模块的版本
  • 征服Windows版nginx(2)
  • 今日总结 2025-01-13
  • 【Apache Paimon】-- 14 -- Spark 集成 Paimon 之 Filesystem Catalog 与 Hive Catalog 实践
  • matlab的绘图的标题中(title)添加标量以及格式化输出
  • 青少年编程与数学 02-006 前端开发框架VUE 17课题、组件深入
  • CClink IEF Basic设备数据 转 EtherCAT项目案例
  • 基于React的两种方式使用React-pdf
  • 开关不一定是开关灯用 - 命令模式(Command Pattern)
  • HarMonyOS使用Tab构建页签
  • Megatron:深度学习中的高性能模型架构
  • LeetCode 977 题:有序数组的平方
  • Python AI教程之十八:监督学习之决策树(9) 决策树模型中的过度拟合