前语

由@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() 办法中完结,并将转化后的值经过调用父类PropertyEditorSupportsetValue() 办法完结设置。

相同的,运用@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;
}

实际上,上面办法中的binderFactoryServletRequestDataBinderFactory工厂类,该类的类图如下所示。

详细分析@InitBinder注解的使用和原理

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的获取是发生在RequestMappingHandlerAdapterinvokeHandlerMethod() 办法中,在该办法中是经过调用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);
}

上面的办法中运用到了两个缓存,initBinderCacheinitBinderAdviceCache,表明如下。

private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache = new LinkedHashMap<>();

其中initBinderCachekeyhandlerClass目标,valuehandlerinitBinder办法调集,initBinderCache一开始是没有值的,当需求获取handler对应的initBinder办法调集时,会先从initBinderCache中获取,假如获取不到才会调用MethodIntrospector#selectMethods办法获取,然后再将获取到的handler对应的initBinder办法调集缓存到initBinderCache中。

initBinderAdviceCachekeyControllerAdviceBeanvalueControllerAdviceBeaninitBinder办法调集,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办法赋值给InitBinderDataBinderFactorybinderMethods变量,最终完结了initBinder办法的加载。

末节:由@InitBinder注解润饰的办法的加载发生在创立WebDataBinderFactory时,在创立WebDataBinderFactory之前,会先获取对当时handler收效的initBinder办法调集,然后在创立WebDataBinderFactory的结构函数中将获取到的initBinder办法调集加载到WebDataBinderFactory

总结

由@InitBinder注解润饰的办法用于初始化WebDataBinder,然后完结恳求参数的类型转化适配,例如日期字符串转化为日期Date类型,一起能够经过承继PropertyEditorSupport类来完结自定义Editor,然后增加能够转化适配的类型种类。