验证码: 看不清楚,换一张 查询 注册会员,免验证
  • {{ basic.site_slogan }}
  • 打开微信扫一扫,
    您还可以在这里找到我们哟

    关注我们

Spring @InitBinder注解如何使用

阅读:361 来源:乙速云 作者:代码code

Spring @InitBinder注解如何使用

      一. @InitBinder注解使用说明

      以前言中提到的字符串转Date为例,对@InitBinder的使用进行说明。

      @RestController
      public class DateController {
          private static final String SUCCESS = "success";
          private static final String FAILED = "failed";
          private final List dates = new ArrayList<>();
          @RequestMapping(value = "/api/v1/date/add", method = RequestMethod.GET)
          public ResponseEntity addDate(@RequestParam("date") Date date) {
              ResponseEntity 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 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 localDates = new ArrayList<>();
          @RequestMapping(value = "/api/v1/localdate/add", method = RequestMethod.GET)
          public ResponseEntity addLocalDate(@RequestParam("localdate") LocalDate localDate) {
              ResponseEntity 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 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工厂类,该类的类图如下所示。

      Spring @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的获取是发生在RequestMappingHandlerAdapter的invokeHandlerMethod() 方法中,在该方法中是通过调用getDataBinderFactory() 方法获取WebDataBinderFactory。下面看一下其实现。

      RequestMappingHandlerAdapter#getDataBinderFactory源码如下所示。

      private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
          // 获取handler的Class对象
          Class handlerType = handlerMethod.getBeanType();
          // 从initBinderCache中根据handler的Class对象获取缓存的initBinder方法集合
          Set 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 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, Set> initBinderCache = new ConcurrentHashMap<>(64);
      private final Map> 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 binderMethods)
              throws Exception {
          return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
      }

      ServletRequestDataBinderFactory#ServletRequestDataBinderFactory

      public ServletRequestDataBinderFactory(@Nullable List binderMethods,
              @Nullable WebBindingInitializer initializer) {
          super(binderMethods, initializer);
      }

      InitBinderDataBinderFactory#InitBinderDataBinderFactory

      public InitBinderDataBinderFactory(@Nullable List 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中。

    分享到:
    *特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: hlamps#outlook.com (#换成@)。
    相关文章
    {{ v.title }}
    {{ v.description||(cleanHtml(v.content)).substr(0,100)+'···' }}
    你可能感兴趣
    推荐阅读 更多>