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

前言

由@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工厂类,该类的类图如下所示。

 

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,从而增加可以转换适配的类型种类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`@InitBinder` 是 Spring MVC 中的一个注解,它可以用来定制数据绑定过程。在 Spring MVC 中,当客户端提交请求时,Spring MVC 会自动将请求中的参数绑定到控制器方法的参数上,这个过程就是数据绑定。`@InitBinder` 可以用来注册自定义的数据编辑器或属性编辑器,从而控制数据绑定的过程。 具体来说,`@InitBinder` 注解可以用在控制器类中的方法上,它的作用是用来初始化 WebDataBinder 对象,这个对象负责将表单提交的数据绑定到控制器的方法参数上。在 `@InitBinder` 注解标记的方法中,可以使用 WebDataBinder 对象的一些方法来定制数据绑定过程,例如注册自定义的属性编辑器。 举个例子,如果你有一个控制器方法接收一个类型为 `java.util.Date` 的参数,你可以使用 `@InitBinder` 注解来注册一个 `CustomDateEditor` 对象,这个对象可以将字符串类型的日期转换成 `java.util.Date` 类型。具体代码如下: ```java @Controller public class MyController { @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); } @RequestMapping("/test") public String test(Date date) { // do something with date return "success"; } } ``` 在上面的例子中,`initBinder` 方法使用 `SimpleDateFormat` 创建了一个日期格式化对象,并将它注册到 `WebDataBinder` 对象中,然后将 `WebDataBinder` 对象作为参数传递给 `initBinder` 方法。控制器方法 `test` 的参数是一个 `Date` 类型的对象,当客户端提交请求时,Spring MVC 会自动调用 `initBinder` 方法初始化 `WebDataBinder` 对象,然后使用这个对象将字符串类型的日期转换成 `java.util.Date` 类型,最后将 `Date` 对象绑定到 `test` 方法的参数上。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值