前言
由@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
的应用进行阐明。
@RestControllerpublic 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); } }}@Datapublic 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)@ActiveProfilesclass 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
。
@ControllerAdvicepublic 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
。
@RestControllerpublic 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)@ActiveProfilesclass 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
对象。
@ControllerAdvicepublic 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,从而减少能够转换适配的类型品种。