Сводка Springboot по форматированию даты и времени

Spring Boot

В проекте в качестве типа данных времени в DTO используется ряд LocalDateTime, но SpringMVC всегда сообщает об ошибке после получения параметров.Для настройки глобального преобразования типа времени пробуются следующие методы обработки.

Примечание: эта статья основана на тесте Springboot2.x. Если он не работает, это может быть вызвано более ранней версией Spring. PS: Если параметры типа LocalDate в вашем Controller не аннотированы (RequestParam, PathVariable и т.д.), будет ошибка, т.к. по умолчанию при разборе таких параметров используетсяModelAttributeMethodProcessorProcessing, и этот процессор должен создать экземпляр объекта посредством отражения, а затем преобразовать параметры в объекте, но класс LocalDate не имеет конструктора, поэтому он сообщит об ошибке, потому что он не может быть создан путем отражения! ! !

для достижения целей

  • Входным параметром запроса является строка (указанный формат) для даты, поддержки получения, публикации (тип содержимого = приложение/json)
  • Возвращаемые данные представляют собой тип даты, преобразованный в указанные символы формата даты и времени.
  • Поддерживает API даты Java8, такие как:LocalTime,localDateиLocalDateTime

Преобразование формата строки даты и времени в форме запроса GET и POST

К этой ситуации следует относиться иначе, чем к случаю, когда время используется в виде строки Json, потому что от внешнего json к внутреннему pojo используется инструмент сериализации Json Jackson (HttpMessgeConverter); и когда строка времени передается как обычный параметр запроса, преобразование используетConverter, есть разница в том, как они обрабатываются.

Использование пользовательского конвертера параметров (Converter)

Реализуйте org.springframework.core.convert.converter.Converter, преобразователь пользовательских параметров, следующим образом:

@Configuration
public class DateConverterConfig {
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
      	return new Converter<String, LocalDate>() {
            @Override
            public LocalDate convert(String source) {
                return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            }
        };
    }

    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<String, LocalDateTime>() {
            @Override
            public LocalDateTime convert(String source) {
                return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            }
        };
    }
}

Комментарий. Вышеуказанные два bean-компонента будут введены в анализатор параметров Spring MVC (кажется, он называетсяParameterConversionService), когда входящая строка должна быть преобразована в класс LocalDateTime, Spring вызовет Converter для преобразования этого входного параметра.

Уведомление:关于自定义的参数转换器 Converter,这里我遇到了一个坑,我再这里详细记录下, изначально моей идеей было упростить код, упростить запись анонимного внутреннего класса выше в лямбда-выражение:

    @Bean
    @ConditionalOnBean(name = "requestMappingHandlerAdapter")
    public Converter<String, LocalDate> localDateConverter() {
        return source -> LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
    }

Когда я снова запускаю проект, я получаю исключение:

Caused by: java.lang.IllegalArgumentException: Unable to determine source type <S> and target type <T> for your Converter [com.example.demo126.config.MappingConverterAdapter$$Lambda$522/817994751]; does the class parameterize those types?

Я не мог этого понять, и после ознакомления с информацией я узнал одно или два:

веб-проект начать регистрациюrequestMappingHandlerAdapterбудет инициализирован, когдаWebBindingInitializer

adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());

иConfigurableWebBindingInitializerнеобходимостьFormattingConversionService, иFormattingConversionServiceбудет всеConverterДобавьте, вам нужно получить общую информацию при добавлении:

@Override
public void addFormatters(FormatterRegistry registry) {
    for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
      	registry.addConverter(converter);
    }
    for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
      	registry.addConverter(converter);
    }
    for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
      	registry.addFormatter(formatter);
    }
}

Добавление Converter.class обычно получает конкретные типы двух универсальных типов через интерфейс.

public ResolvableType as(Class<?> type) {
    if (this == NONE) {
      return NONE;
    }
    Class<?> resolved = resolve();
    if (resolved == null || resolved == type) {
      return this;
    }
    for (ResolvableType interfaceType : getInterfaces()) {
      ResolvableType interfaceAsType = interfaceType.as(type);
      if (interfaceAsType != NONE) {
        return interfaceAsType;
      }
    }
    return getSuperType().as(type);
}

Интерфейс лямбда-выраженияConverter, и не могу получить конкретный тип. После просмотра исходного кода SpringMVC я узнал, что это так. Поскольку указана причина, решение:

  • Самый простой способ - не использовать лямбда-выражения или честно использовать анонимные внутренние классы, чтобы не возникало вышеуказанных проблем.

  • или просто подождиrequestMappingHandlerAdapterПосле завершения регистрации бина добавьте свой собственныйconverterне будет регистрироваться наFormattingConversionServiceсередина

    @Bean
    @ConditionalOnBean(name = "requestMappingHandlerAdapter")
    public Converter<String, LocalDateTime> localDateTimeConverter() {
      return source -> LocalDateTime.parse(source, DateTimeUtils.DEFAULT_FORMATTER);
    }
    

Вы также можете выполнять регулярное сопоставление строки, переданной внешним интерфейсом, например гггг-ММ-дд ЧЧ:мм:сс, гггг-ММ-дд, ЧЧ:мм:сс и т. д., для сопоставления. для различных сценариев.

@Component
public class DateConverter implements Converter<String, Date> {
    @Override
    public Date convert(String value) {
        /**
         * 可对value进行正则匹配,支持日期、时间等多种类型转换
         * 这里我偷个懒,在匹配Date日期格式时直接使用了 hutool 为我们已经写好的解析工具类,这里就不重复造轮子了
         * cn.hutool.core.date.DateUtil
         * @param value
         * @return
         */
        return DateUtil.parse(value.trim());
    }
}

**Примечание.** Здесь я ленив. При сопоставлении формата даты даты я напрямую использую класс инструмента синтаксического анализа hutool, который мы написали для нас. Мы не будем повторять здесь колесо. Следующий метод также использует этот класс инструмента. Также очень просто использовать этот класс инструментов в собственном проекте.Достаточно ввести зависимость hutool в pom-файле проекта следующим образом:

<!--hu tool 工具类-->
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.1.3</version>
</dependency>

Использование аннотаций Spring

Используйте собственную аннотацию Spring @DateTimeFormat(pattern = "yyyy-MM-dd") следующим образом:

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date startDate;

Если используется пользовательский преобразователь параметров, Spring будет предпочтительно использовать этот метод для обработки, то есть аннотации Spring не будут действовать.

Используйте ControllerAdvice с initBinder

@ControllerAdvice
public class GlobalExceptionHandler {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            }
        });
        binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }
        });
        binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern("HH:mm:ss")));
            }
        });
    }
}

Как видно из названия, это вырезанный в контроллере цикл (может быть и глобальный захват исключений), а параметры перед входом в обработчик конвертируются, конвертируются в соответствующий нам объект.

Глобальная обработка входных параметров JSON и возвращаемых значений

Тип запроса: post, content-type=application/json, используется в фоновом режиме.@RequestBodyReceive, формат получаемого и возвращаемого значения по умолчанию:yyyy-MM-dd HH:mm:ss

Изменить файл application.yml

Добавьте в файл application.propertities следующее:

spring:
	jackson:
		date-format: yyyy-MM-dd HH:mm:ss
		time-zone: GMT+8
  • Поддержка (content-type=application/json) в формате запроса естьyyyy-MM-dd HH:mm:ssстрока, используемая в качестве фона@RequestBodyПолучите, и дата возврата конвертируется вyyyy-MM-dd HH:mm:ssстрока формата;
  • Не поддерживается (content-type=application/json), что строки типа yyyy-MM-dd в запросе преобразуются в дату;
  • API даты java8 не поддерживается;

Использование сериализации и десериализации JSON Джексона

@Configuration
public class JacksonConfig {

    /** 默认日期时间格式 */
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /** 默认日期格式 */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /** 默认时间格式 */
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();

        // 忽略json字符串中不识别的属性
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 忽略无法转换的对象 
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // PrettyPrinter 格式化输出
        objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        // NULL不参与序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        // 指定时区
        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
        // 日期类型字符串处理
        objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT));

        // java8日期日期处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        objectMapper.registerModule(javaTimeModule);

        converter.setObjectMapper(objectMapper);
        return converter;
    }
}

Суммировать:

  • Поддержка (content-type=application/json) в формате запроса естьyyyy-MM-dd HH:mm:ssстрока, используемая в качестве фона@RequestBodyПолучить, а возвращаемое значение Дата преобразуется вyyyy-MM-dd HH:mm:ssстрока формата;
  • Поддержка API даты Java8;
  • Не поддерживается (тип контента=application/json) в запросеyyyy-MM-ddи другие типы строк преобразуются в дату;

Вышеуказанные два метода — это глобальная обработка входных параметров JSON, второй метод рекомендуется, особенно для больших проектов, которые нужно задать глобально в базовом пакете.

Входной параметр JSON и возвращаемое значение частичной дифференциальной обработки

Сценарий: если глобальный формат обработки даты и времени:yyyy-MM-dd HH:mm:ss, но для поля требуется дата для получения или возвратаyyyy-MM-dd.

метод первый

Используйте аннотации, поставляемые с Springboot@JsonFormat(pattern = "yyyy-MM-dd"),Следующее:

@JsonFormat(pattern = "yyyy-MM-dd", timezone="GMT+8")
private Date releaseDate;

Комментарии:Springboot предоставляется по умолчанию и имеет мощные функции, которые могут удовлетворить потребности распространенных сценариев и могут указать часовой пояс.

Способ 2

Настройте сериализацию и десериализацию даты следующим образом:

/**
 * 日期序列化
 */
public class DateJsonSerializer extends JsonSerializer<Date> {
    @Override
    public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        jsonGenerator.writeString(dateFormat.format(date));
    }
}

/**
 * 日期反序列化
 */
public class DateJsonDeserializer extends JsonDeserializer<Date> {
    @Override
    public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            return dateFormat.parse(jsonParser.getText());
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}

/**
 * 使用方式
 */
@JsonSerialize(using = DateJsonSerializer.class)
@JsonDeserialize(using = DateJsonDeserializer.class)
private Date releaseDate;

Полная настройка форматирования даты и времени

@Configuration
public class DateHandlerConfig {

    /** 默认日期时间格式 */
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /** 默认日期格式 */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /** 默认时间格式 */
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    /**
     * LocalDate转换器,用于转换RequestParam和PathVariable参数
     * `@ConditionalOnBean(name = "requestMappingHandlerAdapter")`: 等requestMappingHandlerAdapter bean注册完成之后
     * 再添加自己的`converter`就不会注册到`FormattingConversionService`中
     */
    @Bean
    @ConditionalOnBean(name = "requestMappingHandlerAdapter")
    public Converter<String, LocalDate> localDateConverter() {
        return source -> LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
    }

    /**
     * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    @ConditionalOnBean(name = "requestMappingHandlerAdapter")
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return source -> LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT));
    }

    /**
     * LocalTime转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    @ConditionalOnBean(name = "requestMappingHandlerAdapter")
    public Converter<String, LocalTime> localTimeConverter() {
        return source -> LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
    }

    /**
     * Date转换器,用于转换RequestParam和PathVariable参数
     * 这里关于解析各种格式的日期格式采用了 hutool 的日期解析工具类
     */
    @Bean
    public Converter<String, Date> dateConverter() {
        return new Converter<String, Date>() {
            @Override
            public Date convert(String source) {
                return DateUtil.parse(source.trim());
            }
        };
    }

    /**
     * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
     */
    @Bean
    public ObjectMapper objectMapper(){
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

        //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));


        //Date序列化和反序列化
        javaTimeModule.addSerializer(Date.class, new JsonSerializer<>() {
            @Override
            public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
                String formattedDate = formatter.format(date);
                jsonGenerator.writeString(formattedDate);
            }
        });
        javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<>() {
            @Override
            public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
                SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
                String date = jsonParser.getText();
                try {
                    return format.parse(date);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }
}

Расширение исходного кода: углубленное изучение процесса привязки данных SpringMVC

Затем войдите в режим отладки, чтобы увидеть, как mvc связывает параметры в нашем запросе с параметрами нашего метода слоя контроллера;

Напишите простой контроллер, нажмите точку останова, чтобы увидеть стек вызова метода:

    @GetMapping("/getDate")
    public LocalDateTime getDate(@RequestParam LocalDate date,
                                 @RequestParam LocalDateTime dateTime,
                                 @RequestParam Date originalDate) {
        System.out.println(date);
        System.out.println(dateTime);
        System.out.println(originalDate);
        return LocalDateTime.now();
    }

После вызова интерфейса давайте посмотрим на некоторые ключевые методы в стеке вызовов методов:

//进入DispatcherServlet
doService:942, DispatcherServlet
//处理请求
doDispatch:1038, DispatcherServlet
//生成调用链(前处理、实际调用方法、后处理)
handle:87, AbstractHandlerMethodAdapter
//反射获取到实际调用方法,准备开始调用
invokeHandlerMethod:895, RequestMappingHandlerAdapter
invokeAndHandle:102, ServletInvocableHandlerMethod
//这里是关键,参数从这里开始获取到
invokeForRequest:142, InvocableHandlerMethod
doInvoke:215, InvocableHandlerMethod
//这个是Java reflect调用,因此一定是在这之前获取到的参数
invoke:566, Method

Согласно вышеприведенному анализу было установлено, чтоinvokeForRequest:142, InvocableHandlerMethodВот код для получения фактических параметров:

    @Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {
        //这个方法是获取参数的,在这里下个断
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        if (logger.isTraceEnabled()) {
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        //这里开始调用方法
        return doInvoke(args);
    }

Введите этот метод, чтобы увидеть, что он делает:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    //获取方法参数数组,包含了入参信息,比如类型、泛型等等
    MethodParameter[] parameters = getMethodParameters();
    //这个用来存放一会从request parameter转换的参数
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
      MethodParameter parameter = parameters[i];
      parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
      //这里看起来没啥卵用(providedArgs为空)
      args[i] = resolveProvidedArgument(parameter, providedArgs);
      //这里开始获取到方法实际调用的参数,步进
      if (this.argumentResolvers.supportsParameter(parameter)) {
        //从名字就看出来:参数解析器解析参数
        args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        continue;
      }
    }
    return args;
}

Введите resolveArgument, чтобы увидеть:

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                              NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  //根据方法入参,获取对应的解析器
  HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  //开始解析参数(把请求中的parameter转为方法的入参)
  return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

Здесь соответствующий синтаксический анализатор параметра получается в соответствии с параметрами, чтобы увидеть, как он получается внутри:

//遍历,调用supportParameter方法,跟进看看
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
  if (methodArgumentResolver.supportsParameter(parameter)) {
    result = methodArgumentResolver;
    this.argumentResolverCache.put(parameter, result);
    break;
  }
}

Здесь пройдите парсер параметров, чтобы найти подходящий парсер! Итак, какие есть парсеры параметров (26, когда я тестировал)? ? ? Я перечисляю несколько важных, на которые стоит обратить внимание, это знакомо? ! !

{RequestParamMethodArgumentResolver@7686} 
{PathVariableMethodArgumentResolver@8359} 
{RequestResponseBodyMethodProcessor@8366} 
{RequestPartMethodArgumentResolver@8367} 

Мы вошли в наиболее часто используемый парсер и посмотрели его метод supportsParameter, и обнаружили, что соответствующий парсер получается через аннотацию параметра.

    public boolean supportsParameter(MethodParameter parameter) {
        //如果参数拥有注解@RequestParam,则走这个分支(知道为什么上文要对RequestParam和Json两种数据区别对待了把)
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            //这个似乎是对Optional类型的参数进行处理的
            if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
                return (requestParam != null && StringUtils.hasText(requestParam.name()));
            }
            else {
                return true;
            }
        }
        //......
    }

То есть для@RequestParamи@RequestBodyа также@PathVariableАннотированные параметры, SpringMVC будет использовать разные преобразователи параметров для привязки данных! Итак, какие конвертеры используют эти три парсера для анализа параметров? Давайте рассмотрим три парсера по отдельности: Первый взглядRequestParamMethodArgumentResolverВыясняется, что WebDataBinder используется внутри для привязки данных, а нижний слой — это ConversionService (то есть там, где внедряется наш Converter)

WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
//通过DataBinder进行数据绑定的
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
//跟进convertIfNecessary()
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
                                @Nullable MethodParameter methodParam) throws TypeMismatchException {

  return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}
//继续跟进,看到了把
ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
  TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
  if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
    try {
      return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
    }
    catch (ConversionFailedException ex) {
      // fallback to default conversion logic below
      conversionAttemptEx = ex;
    }
  }
}

тогда посмотри наRequestResponseBodyMethodProcessorУстановлено, что используемый преобразователь имеет тип HttpMessageConverter:

//resolveArgument方法内部调用下面进行参数解析
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

//step into readWithMessageConverters(),我们看到这里的Converter是HttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
  Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
  GenericHttpMessageConverter<?> genericConverter =
    (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
  if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
      (targetClass != null && converter.canRead(targetClass, contentType))) {
    if (message.hasBody()) {
      HttpInputMessage msgToUse =
        getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
      body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
              ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
      body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    }
    else {
      body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
    }
    break;
  }
}

посмотри наконецPathVariableMethodArgumentResolverОбнаружено, что путь выполнения, выбранный RequestParam, одинаков (оба унаследованы от синтаксического анализатора AbstractNamedValueMethodArgumentResolver), поэтому код не будет опубликован.

Суммировать

Если вы хотите преобразовать параметры из запроса в указанные нами типы, вам нужно различать их по входным параметрам:

  • Если это RequestBody, то настроив ObjectMapper (эта штука будет внедрена в HttpMessagConverter Джексона, а именноMappingJackson2HttpMessageConverter) реализовать сериализацию и десериализацию данных формата Json;
  • Если это параметр типа RequestParam или PathVariable, преобразование параметров осуществляется путем настройки конвертера (эти конвертеры будут внедрены в ConversionService).