首頁 > 軟體

Spring @InitBinder註解使用及原理詳解

2023-09-06 14:00:58

前言

由@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 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註解則設定全域性生效。

二. 實現自定義Editor

現在假如需要將日期字串轉換為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

三. 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,從而增加可以轉換適配的型別種類。

以上就是Spring @InitBinder註解使用及原理詳解的詳細內容,更多關於Spring @InitBinder註解原理的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com