Spring?@InitBinder注解使用及原理詳解
前言
由@InitBinder注解修飾的方法用于初始化WebDataBinder對(duì)象,能夠?qū)崿F(xiàn):從request獲取到handler方法中由@RequestParam注解或@PathVariable注解修飾的參數(shù)后,假如獲取到的參數(shù)類型與handler方法上的參數(shù)類型不匹配,此時(shí)可以使用初始化好的WebDataBinder對(duì)獲取到的參數(shù)進(jìn)行類型處理。
一個(gè)經(jīng)典的例子就是handler方法上的參數(shù)類型為Date,而從request中獲取到的參數(shù)類型是字符串,SpringMVC在默認(rèn)情況下無法實(shí)現(xiàn)字符串轉(zhuǎn)Date,此時(shí)可以在由@InitBinder注解修飾的方法中為WebDataBinder對(duì)象注冊(cè)CustomDateEditor,從而使得WebDataBinder能將從request中獲取到的字符串再轉(zhuǎn)換為Date對(duì)象。
通常,如果在@ControllerAdvice注解修飾的類中使用@InitBinder注解,此時(shí)@InitBinder注解修飾的方法所做的事情全局生效(前提是@ControllerAdvice注解沒有設(shè)置basePackages字段);如果在@Controller注解修飾的類中使用@InitBinder注解,此時(shí)@InitBinder注解修飾的方法所做的事情僅對(duì)當(dāng)前Controller生效。本篇文章將結(jié)合簡單例子,對(duì)@InitBinder注解的使用,原理進(jìn)行學(xué)習(xí)。
SpringBoot版本:2.4.1
一. @InitBinder注解使用說明
以前言中提到的字符串轉(zhuǎn)Date為例,對(duì)@InitBinder的使用進(jìn)行說明。
@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;
}
}
上面寫好了一個(gè)簡單的Controller,用于獲取Date并存儲(chǔ)。然后在單元測(cè)試中使用TestRestTemplate模擬客戶端向服務(wù)端發(fā)起請(qǐng)求,程序如下。
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DateControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void 測(cè)試Date字符串轉(zhuǎn)換為Date對(duì)象() {
ResponseEntity<String> response = restTemplate
.getForEntity("/api/v1/date/add?date=20200620", String.class);
assertThat(response.getStatusCodeValue(), is(HttpStatus.OK.value()));
}
}
由于此時(shí)并沒有使用@InitBinder注解修飾的方法向WebDataBinder注冊(cè)CustomDateEditor對(duì)象,運(yùn)行測(cè)試程序時(shí)斷言會(huì)無法通過,報(bào)錯(cuò)會(huì)包含如下信息。
Failed to convert value of type 'java.lang.String' to required type 'java.util.Date'
由于無法將字符串轉(zhuǎn)換為Date,導(dǎo)致了參數(shù)類型不匹配的異常。
下面使用@ControllerAdvice注解和@InitBinder注解為WebDataBinder添加CustomDateEditor對(duì)象,使SpringMVC框架為我們實(shí)現(xiàn)字符串轉(zhuǎn)Date。
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void setDateEditor(WebDataBinder binder) {
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyyMMdd"), false));
}
}
此時(shí)再執(zhí)行測(cè)試程序,斷言通過。
小節(jié):由@InitBinder注解修飾的方法返回值類型必須為void,入?yún)⒈仨殲?code>WebDataBinder對(duì)象實(shí)例。如果在@Controller注解修飾的類中使用@InitBinder注解則配置僅對(duì)當(dāng)前類生效,如果在@ControllerAdvice注解修飾的類中使用@InitBinder注解則配置全局生效。
二. 實(shí)現(xiàn)自定義Editor
現(xiàn)在假如需要將日期字符串轉(zhuǎn)換為LocalDate,但是SpringMVC框架并沒有提供類似于CustomDateEditor這樣的Editor時(shí),可以通過繼承PropertyEditorSupport類來實(shí)現(xiàn)自定義Editor。首先看如下的一個(gè)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;
}
}
同樣的在單元測(cè)試中使用TestRestTemplate模擬客戶端向服務(wù)端發(fā)起請(qǐng)求。
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class LocalDateControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void 測(cè)試LocalDate字符串轉(zhuǎn)換為LocalDate對(duì)象() {
ResponseEntity<String> response = restTemplate
.getForEntity("/api/v1/localdate/add?localdate=20200620", String.class);
assertThat(response.getStatusCodeValue(), is(HttpStatus.OK.value()));
}
}
此時(shí)直接執(zhí)行測(cè)試程序斷言會(huì)不通過,會(huì)報(bào)錯(cuò)類型轉(zhuǎn)換異?!,F(xiàn)在實(shí)現(xiàn)一個(gè)自定義的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() 方法可以實(shí)現(xiàn)一個(gè)自定義Editor。通常,自定義的轉(zhuǎn)換邏輯在setAsText() 方法中實(shí)現(xiàn),并將轉(zhuǎn)換后的值通過調(diào)用父類PropertyEditorSupport的setValue() 方法完成設(shè)置。
同樣的,使用@ControllerAdvice注解和@InitBinder注解為WebDataBinder添加CustomLocalDateEditor對(duì)象。
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void setLocalDateEditor(WebDataBinder binder) {
binder.registerCustomEditor(LocalDate.class,
new CustomLocalDateEditor());
}
}
此時(shí)再執(zhí)行測(cè)試程序,斷言全部通過。
小節(jié):通過繼承PropertyEditorSupport類并重寫setAsText()方法可以實(shí)現(xiàn)一個(gè)自定義Editor。
三. WebDataBinder初始化原理解析
已經(jīng)知道,由@InitBinder注解修飾的方法用于初始化WebDataBinder,并且在詳解SpringMVC-RequestMappingHandlerAdapter這篇文章中提到:從request獲取到handler方法中由@RequestParam注解或@PathVariable注解修飾的參數(shù)后,便會(huì)使用WebDataBinderFactory工廠完成對(duì)WebDataBinder的初始化。下面看一下具體的實(shí)現(xiàn)。
AbstractNamedValueMethodArgumentResolver#resolveArgument部分源碼如下所示。
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// ...
// 獲取到參數(shù)
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;
}
實(shí)際上,上面方法中的binderFactory是ServletRequestDataBinderFactory工廠類,該類的類圖如下所示。

createBinder() 是由接口WebDataBinderFactory聲明的方法,ServletRequestDataBinderFactory的父類DefaultDataBinderFactory對(duì)其進(jìn)行了實(shí)現(xiàn),實(shí)現(xiàn)如下。
public final WebDataBinder createBinder(
NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
// 創(chuàng)建WebDataBinder實(shí)例
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
if (this.initializer != null) {
// 調(diào)用WebBindingInitializer對(duì)WebDataBinder進(jìn)行初始化
this.initializer.initBinder(dataBinder, webRequest);
}
// 調(diào)用由@InitBinder注解修飾的方法對(duì)WebDataBinder進(jìn)行初始化
initBinder(dataBinder, webRequest);
return dataBinder;
}
initBinder() 是DefaultDataBinderFactory的一個(gè)模板方法,InitBinderDataBinderFactory對(duì)其進(jìn)行了重寫,如下所示。
public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
for (InvocableHandlerMethod binderMethod : this.binderMethods) {
if (isBinderMethodApplicable(binderMethod, dataBinder)) {
// 執(zhí)行由@InitBinder注解修飾的方法,完成對(duì)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() 方法中會(huì)遍歷加載的所有由@InitBinder注解修飾的方法并執(zhí)行,從而完成對(duì)WebDataBinder的初始化。
小節(jié):WebDataBinder的初始化是由WebDataBinderFactory先創(chuàng)建WebDataBinder實(shí)例,然后遍歷WebDataBinderFactory加載好的由@InitBinder注解修飾的方法并執(zhí)行,以完成WebDataBinder的初始化。
四. @InitBinder注解修飾的方法的加載
由第三小節(jié)可知,WebDataBinder的初始化是由WebDataBinderFactory先創(chuàng)建WebDataBinder實(shí)例,然后遍歷WebDataBinderFactory加載好的由@InitBinder注解修飾的方法并執(zhí)行,以完成WebDataBinder的初始化。本小節(jié)將學(xué)習(xí)WebDataBinderFactory如何加載由@InitBinder注解修飾的方法。
WebDataBinderFactory的獲取是發(fā)生在RequestMappingHandlerAdapter的invokeHandlerMethod() 方法中,在該方法中是通過調(diào)用getDataBinderFactory() 方法獲取WebDataBinderFactory。下面看一下其實(shí)現(xiàn)。
RequestMappingHandlerAdapter#getDataBinderFactory源碼如下所示。
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
// 獲取handler的Class對(duì)象
Class<?> handlerType = handlerMethod.getBeanType();
// 從initBinderCache中根據(jù)handler的Class對(duì)象獲取緩存的initBinder方法集合
Set<Method> methods = this.initBinderCache.get(handlerType);
// 從initBinderCache沒有獲取到initBinder方法集合,則執(zhí)行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中存儲(chǔ)的是全局生效的initBinder方法
this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
// 如果ControllerAdviceBean有限制生效范圍,則判斷其是否對(duì)當(dāng)前handler生效
if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
Object bean = controllerAdviceBean.resolveBean();
// 如果對(duì)當(dāng)前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));
}
// 創(chuàng)建WebDataBinderFactory,并同時(shí)加載initBinderMethods中的所有initBinder方法
return createDataBinderFactory(initBinderMethods);
}
上面的方法中使用到了兩個(gè)緩存,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對(duì)象,value是handler的initBinder方法集合,initBinderCache一開始是沒有值的,當(dāng)需要獲取handler對(duì)應(yīng)的initBinder方法集合時(shí),會(huì)先從initBinderCache中獲取,如果獲取不到才會(huì)調(diào)用MethodIntrospector#selectMethods方法獲取,然后再將獲取到的handler對(duì)應(yīng)的initBinder方法集合緩存到initBinderCache中。
initBinderAdviceCache的key是ControllerAdviceBean,value是ControllerAdviceBean的initBinder方法集合,initBinderAdviceCache的值是在RequestMappingHandlerAdapter初始化時(shí)調(diào)用的afterPropertiesSet() 方法中完成加載的,具體的邏輯在詳解SpringMVC-RequestMappingHandlerAdapter有詳細(xì)說明。
因此WebDataBinderFactory中的initBinder方法由兩部分組成,一部分是寫在當(dāng)前handler中的initBinder方法(這解釋了為什么寫在handler中的initBinder方法僅對(duì)當(dāng)前handler生效),另外一部分是寫在由@ControllerAdvice注解修飾的類中的initBinder方法,所有的這些initBinder方法均會(huì)對(duì)WebDataBinderFactory創(chuàng)建的WebDataBinder對(duì)象進(jìn)行初始化。
最后,看一下createDataBinderFactory() 的實(shí)現(xiàn)。
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());
}
可以發(fā)現(xiàn),最終創(chuàng)建的WebDataBinderFactory實(shí)際上是ServletRequestDataBinderFactory,并且在執(zhí)行ServletRequestDataBinderFactory的構(gòu)造函數(shù)時(shí),會(huì)調(diào)用其父類InitBinderDataBinderFactory的構(gòu)造函數(shù),在這個(gè)構(gòu)造函數(shù)中,會(huì)將之前獲取到的生效范圍內(nèi)的initBinder方法賦值給InitBinderDataBinderFactory的binderMethods變量,最終完成了initBinder方法的加載。
小節(jié):由@InitBinder注解修飾的方法的加載發(fā)生在創(chuàng)建WebDataBinderFactory時(shí),在創(chuàng)建WebDataBinderFactory之前,會(huì)先獲取對(duì)當(dāng)前handler生效的initBinder方法集合,然后在創(chuàng)建WebDataBinderFactory的構(gòu)造函數(shù)中將獲取到的initBinder方法集合加載到WebDataBinderFactory中。
總結(jié)
由@InitBinder注解修飾的方法用于初始化WebDataBinder,從而實(shí)現(xiàn)請(qǐng)求參數(shù)的類型轉(zhuǎn)換適配,例如日期字符串轉(zhuǎn)換為日期Date類型,同時(shí)可以通過繼承PropertyEditorSupport類來實(shí)現(xiàn)自定義Editor,從而增加可以轉(zhuǎn)換適配的類型種類。
以上就是Spring @InitBinder注解使用及原理詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring @InitBinder注解原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中Collection與Collections的區(qū)別詳解
這篇文章主要為大家詳細(xì)介紹了Java中Collection與Collections的區(qū)別,文中有詳細(xì)的代碼示例,具有一定的參考價(jià)值,感興趣的同學(xué)可以參考一下2023-06-06
Spring中DAO被循環(huán)調(diào)用的時(shí)候數(shù)據(jù)不實(shí)時(shí)更新的解決方法
這篇文章主要介紹了Spring中DAO被循環(huán)調(diào)用的時(shí)候數(shù)據(jù)不實(shí)時(shí)更新的解決方法,需要的朋友可以參考下2014-08-08
Spring中的@Autowired注解深入解析與實(shí)戰(zhàn)指南
本文介紹了Spring框架中的@Autowired注解,詳細(xì)講解了其基本用法、高級(jí)用法以及實(shí)際應(yīng)用場(chǎng)景,通過@Autowired注解,Spring容器可以自動(dòng)將依賴的Bean注入到目標(biāo)Bean中,從而簡化代碼并提高可維護(hù)性,需要的朋友可以參考下2024-11-11
玩轉(zhuǎn)SpringBoot中的那些連接池(小結(jié))
這篇文章主要介紹了玩轉(zhuǎn)SpringBoot中的那些連接池(小結(jié)),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
Java中new關(guān)鍵字和newInstance方法的區(qū)別分享
在初始化一個(gè)類,生成一個(gè)實(shí)例的時(shí)候,newInstance()方法和new關(guān)鍵字除了一個(gè)是方法一個(gè)是關(guān)鍵字外,最主要的區(qū)別是創(chuàng)建對(duì)象的方式不同2013-07-07
Java實(shí)現(xiàn)定時(shí)器的四種方式
這篇文章主要介紹了Java實(shí)現(xiàn)定時(shí)器的四種方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07

