Spring @InitBinder注解如何使用
这篇文章主要讲解了“Spring @InitBinder注解如何使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Spring @InitBinder注解如何使用”吧!
一. @InitBinder注解使用说明
以前言中提到的字符串转Date为例,对@InitBinder的使用进行说明。
@RestControllerpublic class DateController { private static final String SUCCESS = "success"; private static final String FAILED = "failed"; private final List<Date> dates = new ArrayList<>(); @RequestMapping(value = "/api/v1/date/add", method = RequestMethod.GET) public ResponseEntity<String> addDate(@RequestParam("date") Date date) { ResponseEntity<String> response; try { dates.add(date); response = new ResponseEntity<>(SUCCESS, HttpStatus.OK); } catch (Exception e) { e.printStackTrace(); response = new ResponseEntity<>(FAILED, HttpStatus.INTERNAL_SERVER_ERROR); } return response; }}
上面写好了一个简单的Controller,用于获取Date并存储。然后在单元测试中使用TestRestTemplate模拟客户端向服务端发起请求,程序如下。
@ExtendWith(SpringExtension.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class DateControllerTest { @Autowired private TestRestTemplate restTemplate; @Test void 测试Date字符串转换为Date对象() { ResponseEntity<String> response = restTemplate .getForEntity("/api/v1/date/add?date=20200620", String.class); assertThat(response.getStatusCodeValue(), is(HttpStatus.OK.value())); }}
由于此时并没有使用@InitBinder注解修饰的方法向WebDataBinder注册CustomDateEditor对象,运行测试程序时断言会无法通过,报错会包含如下信息。
Failed to convert value of type 'java.lang.String' to required type 'java.util.Date'
由于无法将字符串转换为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
现在假如需要将日期字符串转换为LocalDate,但是SpringMVC框架并没有提供类似于CustomDateEditor这样的Editor时,可以通过继承PropertyEditorSupport类来实现自定义Editor。首先看如下的一个Controller。
@RestControllerpublic class LocalDateController { private static final String SUCCESS = "success"; private static final String FAILED = "failed"; private final List<LocalDate> localDates = new ArrayList<>(); @RequestMapping(value = "/api/v1/localdate/add", method = RequestMethod.GET) public ResponseEntity<String> addLocalDate(@RequestParam("localdate") LocalDate localDate) { ResponseEntity<String> response; try { localDates.add(localDate); response = new ResponseEntity<>(SUCCESS, HttpStatus.OK); } catch (Exception e) { e.printStackTrace(); response = new ResponseEntity<>(FAILED, HttpStatus.INTERNAL_SERVER_ERROR); } return response; }}
同样的在单元测试中使用TestRestTemplate模拟客户端向服务端发起请求。
@ExtendWith(SpringExtension.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class LocalDateControllerTest { @Autowired private TestRestTemplate restTemplate; @Test void 测试LocalDate字符串转换为LocalDate对象() { ResponseEntity<String> response = restTemplate .getForEntity("/api/v1/localdate/add?localdate=20200620", String.class); assertThat(response.getStatusCodeValue(), is(HttpStatus.OK.value())); }}
此时直接执行测试程序断言会不通过,会报错类型转换异常。现在实现一个自定义的Editor。
public class CustomLocalDateEditor extends PropertyEditorSupport { private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); @Override public void setAsText(String text) throws IllegalArgumentException { if (StringUtils.isEmpty(text)) { throw new IllegalArgumentException("Can not convert null."); } LocalDate result; try { result = LocalDate.from(dateTimeFormatter.parse(text)); setValue(result); } catch (Exception e) { throw new IllegalArgumentException("CustomDtoEditor convert failed.", e); } }}
CustomLocalDateEditor是自定义的Editor,最简单的情况下,通过继承PropertyEditorSupport并重写setAsText() 方法可以实现一个自定义Editor。通常,自定义的转换逻辑在setAsText() 方法中实现,并将转换后的值通过调用父类PropertyEditorSupport的setValue() 方法完成设置。
同样的,使用@ControllerAdvice注解和@InitBinder注解为WebDataBinder添加CustomLocalDateEditor对象。
@ControllerAdvicepublic class GlobalControllerAdvice { @InitBinder public void setLocalDateEditor(WebDataBinder binder) { binder.registerCustomEditor(LocalDate.class, new CustomLocalDateEditor()); }}
此时再执行测试程序,断言全部通过。
小节:通过继承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
中。
感谢各位的阅读,以上就是“Spring @InitBinder注解如何使用”的内容了,经过本文的学习后,相信大家对Spring @InitBinder注解如何使用这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341