<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
由@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
以前言中提到的字串轉Date為例,對@InitBinder的使用進行說明。
@RestController public 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。
@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
註解則設定全域性生效。
現在假如需要將日期字串轉換為LocalDate,但是SpringMVC框架並沒有提供類似於CustomDateEditor這樣的Editor時,可以通過繼承PropertyEditorSupport類來實現自定義Editor。首先看如下的一個Controller。
@RestController public 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物件。
@ControllerAdvice public class GlobalControllerAdvice { @InitBinder public void setLocalDateEditor(WebDataBinder binder) { binder.registerCustomEditor(LocalDate.class, new CustomLocalDateEditor()); } }
此時再執行測試程式,斷言全部通過。
小節:通過繼承PropertyEditorSupport
類並重寫setAsText()
方法可以實現一個自定義Editor
。
已經知道,由@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
的初始化。
由第三小節可知,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,從而增加可以轉換適配的型別種類。
以上就是Spring @InitBinder註解使用及原理詳解的詳細內容,更多關於Spring @InitBinder註解原理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45