- 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,同样的,本地的 @InitBinder
的 value
值是 {"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);
}
上面的代码是 RequestMappingHandlerAdapter
的 getDataBinderFactory
方法。可以注意下这个方法的入参 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
的是在参数解析
的阶段被调用的,可以以 RequestResponseBodyMethodProcessor
的 resolveArgument
为例看下
@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
是一个很重要的处理器适配器
,需要深入仔细研究。