前言
由@InitBinder
注解修饰的方法用于初始化WebDataBinder
对象,能够实现:从request获取到handler方法中由@RequestParam
注解或@PathVariable
注解修饰的参数后,假如获取到的参数类型与handler方法上的参数类型不匹配,此时可以使用初始化好的WebDataBinder
对获取到的参数进行类型处理。一个经典的例子就是handler方法上的参数类型为Date
,而从request中获取到的参数类型是字符串,SpringMVC在默认情况下无法实现字符串转Date
,此时可以在由@InitBinder
注解修饰的方法中为WebDataBinder
对象注册CustomDateEditor
,从而使得WebDataBinder
能将从request中获取到的字符串再转换为Date
对象。
通常,如果在@ControllerAdvice
注解修饰的类中使用@InitBinder
注解,此时@InitBinder
注解修饰的方法所做的事情全局生效(前提是@ControllerAdvice
注解没有设置basePackages字段);如果在@Controller
注解修饰的类中使用@InitBinder
注解,此时@InitBinder
注解修饰的方法所做的事情仅对当前Controller
生效。本篇文章将结合简单例子,对@InitBinder
注解的使用,原理进行学习。
SpringBoot版本:2.4.1
正文
一. @InitBinder注解使用说明
以前言中提到的字符串转Date
为例,对@InitBinder
的使用进行说明。
@RestController
public class LoginController {
private static final String DATE_STRING = "20200620";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
private final Student student;
public LoginController() {
student = new Student();
student.setName("Lee");
student.setAge(20);
student.setSex("male");
try {
student.setDate(dateFormat.parse(DATE_STRING));
} catch (ParseException e) {
System.out.println(e.getMessage());
}
}
@RequestMapping(value = "/api/v1/student/date", method = RequestMethod.GET)
public ResponseEntity<Object> getStudentByDate(@RequestParam(name = "date") Date date) {
if (student.getDate().equals(date)) {
return new ResponseEntity<>(student, HttpStatus.OK);
} else {
return new ResponseEntity<>(String.format("get student failed by date: %s", date.toString()), HttpStatus.BAD_REQUEST);
}
}
}
@Data
public class Student {
private String name;
private int age;
private String sex;
private Date date;
}
上面写好了一个简单的Controller
,其中有一个Student
成员变量,用于客户端获取,getStudentByDate()
接口实现从请求中获取日期并与Controller
中的Student
对象的日期进行对比,如果一致,则向客户端返回Student
对象。
然后在单元测试中使用TestRestTemplate
模拟客户端向服务端发起请求。程序如下。
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles
class LoginControllerTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");
@Autowired
private TestRestTemplate restTemplate;
@Test
void givenDateStringAndConvertedByWebDataBinder_whenGetStudentByDate_thenGetStudentSuccess() throws Exception {
String dateString = "20200620";
String url = "/api/v1/student/date?date=" + dateString;
ResponseEntity<Student> response = restTemplate.getForEntity(url, Student.class);
assertThat(response.getBody() != null, is(true));
assertThat(response.getBody().getName(), is("Lee"));
assertThat(response.getBody().getAge(), is(20));
assertThat(response.getBody().getSex(), is("male"));
assertThat(response.getBody().getDate(), is(DATE_FORMAT.parse(dateString)));
}
}
由于此时并没有使用@InitBinder
注解修饰的方法向WebDataBinder
注册CustomDateEditor
对象,运行测试程序时断言会无法通过,报错如下所示。
Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Date'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.util.Date] for value '20200620'; nested exception is java.lang.IllegalArgumentException]
由于无法将字符串转换为Date
,导致了参数类型不匹配的异常。下面使用@ControllerAdvice
注解和@InitBinder
注解为WebDataBinder
添加CustomDateEditor
对象,使SpringMVC框架为我们实现字符串转Date
。
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void setDateEditor(WebDataBinder binder) {
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyyMMdd"), false));
}
}
此时再执行测试程序,所有断言通过。
小节:由@InitBinder
注解修饰的方法返回值类型必须为void,入参必须为WebDataBinder
对象实例。如果在@Controller
注解修饰的类中使用@InitBinder
注解则配置仅对当前类生效,如果在@ControllerAdvice
注解修饰的类中使用@InitBinder
注解则配置全局生效。
二. 实现自定义Editor
当需要将Json
字符串转换为自定义的DTO对象且SpringMVC框架并没有提供类似于CustomDateEditor
这样的Editor
时,可以通过继承PropertyEditorSupport
类来实现自定义Editor
。首先看如下的一个Controller
。
@RestController
public class LoginController {
private static final String DATE_STRING = "20200620";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
private final Student student;
public LoginController() {
student = new Student();
student.setName("Lee");
student.setAge(20);
student.setSex("male");
try {
student.setDate(dateFormat.parse(DATE_STRING));
} catch (ParseException e) {
System.out.println(e.getMessage());
}
}
@RequestMapping(value = "/api/v1/student/student", method = RequestMethod.GET)
public ResponseEntity<Object> getStudentByStudent(@RequestParam(name = "student") Student student) {
if (student != null && this.student.getName().equals(student.getName())) {
return new ResponseEntity<>(this.student, HttpStatus.OK);
} else {
return new ResponseEntity<>(String.format("get student failed by student: %s", student), HttpStatus.BAD_REQUEST);
}
}
}
同样的在单元测试中使用TestRestTemplate
模拟客户端向服务端发起请求。
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles
class LoginControllerTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");
private static final ObjectMapper MAPPER = new ObjectMapper();
@Autowired
private TestRestTemplate restTemplate;
@Test
void givenStudentJsonAndConvertedByWebDataBinder_whenGetStudentByStudent_thenGetStudentSuccess() throws Exception {
Student student = new Student();
student.setName("Lee");
String studentJson = MAPPER.writeValueAsString(student);
String url = "/api/v1/student/student?student={student}";
Map<String, String> params = new HashMap<>();
params.put("student", studentJson);
ResponseEntity<Student> response = restTemplate.getForEntity(url, Student.class, params);
assertThat(response.getBody() != null, is(true));
assertThat(response.getBody().getName(), is("Lee"));
assertThat(response.getBody().getAge(), is(20));
assertThat(response.getBody().getSex(), is("male"));
assertThat(response.getBody().getDate(), is(DATE_FORMAT.parse("20200620")));
}
}
此时直接执行测试程序断言会不通过,会报错类型转换异常。现在实现一个自定义的Editor
。
public class CustomDtoEditor<T> extends PropertyEditorSupport {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final Class<T> clazz;
public CustomDtoEditor(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (text == null) {
throw new IllegalArgumentException("could not convert null string");
}
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
T result;
try {
result = MAPPER.readValue(text, clazz);
setValue(result);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("convert " + text + " to " + clazz + " failed");
}
}
}
CustomDtoEditor
是自定义的Editor
,最简单的情况下,通过继承PropertyEditorSupport
并重写setAsText()
方法可以实现一个自定义Editor
。通常,自定义的转换逻辑在setAsText()
方法中实现,并将转换后的值通过调用父类PropertyEditorSupport
的setValue()
方法完成设置。
同样的,使用@ControllerAdvice
注解和@InitBinder
注解为WebDataBinder
添加CustomDtoEditor
对象。
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void setDtoEditor(WebDataBinder binder) {
binder.registerCustomEditor(Student.class,
new CustomDtoEditor(Student.class));
}
}
此时再执行测试程序,断言全部通过。
小节:通过继承PropertyEditorSupport
类并重写setAsText()
方法可以实现一个自定义Editor
。
三. WebDataBinder初始化原理解析
已经知道,由@InitBinder
注解修饰的方法用于初始化WebDataBinder
,并且在SpringMVC-RequestMappingHandlerAdapter这篇文章中提到:从request获取到handler方法中由@RequestParam
注解或@PathVariable
注解修饰的参数后,便会使用WebDataBinderFactory
工厂完成对WebDataBinder
的初始化。下面看一下具体的实现。
AbstractNamedValueMethodArgumentResolver#resolveArgument()
部分源码如下所示。
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
......
// 获取到参数
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
......
if (binderFactory != null) {
// 初始化WebDataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
}
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
实际上,上面方法中的binderFactory是ServletRequestDataBinderFactory
工厂类,该类的类图如下所示。
createBinder()
是由接口WebDataBinderFactory
声明的方法,ServletRequestDataBinderFactory
的父类DefaultDataBinderFactory
对其进行了实现,实现如下。
public final WebDataBinder createBinder(
NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
// 创建WebDataBinder实例
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
if (this.initializer != null) {
// 调用WebBindingInitializer对WebDataBinder进行初始化
this.initializer.initBinder(dataBinder, webRequest);
}
// 调用由@InitBinder注解修饰的方法对WebDataBinder进行初始化
initBinder(dataBinder, webRequest);
return dataBinder;
}
initBinder()
是DefaultDataBinderFactory
的一个模板方法,InitBinderDataBinderFactory
对其进行了重写,如下所示。
public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
for (InvocableHandlerMethod binderMethod : this.binderMethods) {
if (isBinderMethodApplicable(binderMethod, dataBinder)) {
// 执行由@InitBinder注解修饰的方法,完成对WebDataBinder的初始化
Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);
if (returnValue != null) {
throw new IllegalStateException(
"@InitBinder methods must not return a value (should be void): " + binderMethod);
}
}
}
}
如上,initBinder()
方法中会遍历加载的所有由@InitBinder
注解修饰的方法并执行,从而完成对WebDataBinder
的初始化。
小节:WebDataBinder
的初始化是由WebDataBinderFactory
先创建WebDataBinder
实例,然后遍历WebDataBinderFactory
加载好的由@InitBinder
注解修饰的方法并执行,以完成WebDataBinder
的初始化。
四. @InitBinder注解修饰的方法的加载
由第三小节可知,WebDataBinder
的初始化是由WebDataBinderFactory
先创建WebDataBinder
实例,然后遍历WebDataBinderFactory
加载好的由@InitBinder
注解修饰的方法并执行,以完成WebDataBinder
的初始化。本小节将学习WebDataBinderFactory
如何加载由@InitBinder
注解修饰的方法。
WebDataBinderFactory
的获取是发生在RequestMappingHandlerAdapter
的invokeHandlerMethod()
方法中,在该方法中是通过调用getDataBinderFactory()
方法获取WebDataBinderFactory
。下面看一下其实现。
RequestMappingHandlerAdapter#getDataBinderFactory()
源码如下所示。
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
// 获取handler的Class对象
Class<?> handlerType = handlerMethod.getBeanType();
// 从initBinderCache中根据handler的Class对象获取缓存的initBinder方法集合
Set<Method> methods = this.initBinderCache.get(handlerType);
// 从initBinderCache没有获取到initBinder方法集合,则执行MethodIntrospector.selectMethods()方法获取handler的initBinder方法集合,并缓存到initBinderCache中
if (methods == null) {
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
// initBinderMethods是WebDataBinderFactory需要加载的initBinder方法集合
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
// initBinderAdviceCache中存储的是全局生效的initBinder方法
this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
// 如果ControllerAdviceBean有限制生效范围,则判断其是否对当前handler生效
if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
Object bean = controllerAdviceBean.resolveBean();
// 如果对当前handler生效,则ControllerAdviceBean的所有initBinder方法均需要添加到initBinderMethods中
for (Method method : methodSet) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
// 将handler的所有initBinder方法添加到initBinderMethods中
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
// 创建WebDataBinderFactory,并同时加载initBinderMethods中的所有initBinder方法
return createDataBinderFactory(initBinderMethods);
}
上面的方法中使用到了两个缓存,initBinderCache
和initBinderAdviceCache
,表示如下。
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache = new LinkedHashMap<>();
其中initBinderCache
的key是handler的Class
对象,value是handler的initBinder方法集合,initBinderCache
一开始是没有值的,当需要获取handler对应的initBinder方法集合时,会先从initBinderCache
中获取,如果获取不到才会调用MethodIntrospector.selectMethods()
方法获取,然后再将获取到的handler对应的initBinder方法集合缓存到initBinderCache
中。
initBinderAdviceCache
的key是ControllerAdviceBean
,value是ControllerAdviceBean
的initBinder方法集合,initBinderAdviceCache
的值是在RequestMappingHandlerAdapter
初始化时调用的afterPropertiesSet()
方法中完成加载的,具体的逻辑在SpringMVC-RequestMappingHandlerAdapter有详细说明。
因此WebDataBinderFactory
中的initBinder方法由两部分组成,一部分是写在当前handler中的initBinder方法(这解释了为什么写在handler中的initBinder方法仅对当前handler生效),另外一部分是写在由@ControllerAdvice
注解修饰的类中的initBinder方法,所有的这些initBinder方法均会对WebDataBinderFactory
创建的WebDataBinder
对象进行初始化。
最后,看一下createDataBinderFactory()
的实现。
RequestMappingHandlerAdapter#createDataBinderFactory()
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
ServletRequestDataBinderFactory#ServletRequestDataBinderFactory()
public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
@Nullable WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
InitBinderDataBinderFactory#InitBinderDataBinderFactory()
public InitBinderDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
@Nullable WebBindingInitializer initializer) {
super(initializer);
this.binderMethods = (binderMethods != null ? binderMethods : Collections.emptyList());
}
可以发现,最终创建的WebDataBinderFactory
实际上是ServletRequestDataBinderFactory
,并且在执行ServletRequestDataBinderFactory
的构造函数时,会调用其父类InitBinderDataBinderFactory
的构造函数,在这个构造函数中,会将之前获取到的生效范围内的initBinder方法赋值给InitBinderDataBinderFactory
的binderMethods变量,最终完成了initBinder方法的加载。
小节:由@InitBinder
注解修饰的方法的加载发生在创建WebDataBinderFactory
时,在创建WebDataBinderFactory
之前,会先获取对当前handler生效的initBinder方法集合,然后在创建WebDataBinderFactory
的构造函数中将获取到的initBinder方法集合加载到WebDataBinderFactory
中。
总结
由@InitBinder
注解修饰的方法用于初始化WebDataBinder
,从而实现请求参数的类型转换适配,例如日期字符串转换为日期Date
类型,同时可以通过继承PropertyEditorSupport
类来实现自定义Editor
,从而增加可以转换适配的类型种类。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。