Как элегантно использовать Date и LocalDateTime в приложениях Spring Boot

Spring Boot

Java 8 существует уже много лет, но многие до сих пор придерживаются ее при разработке.Dateа такжеSimpleDateFormatДелайте временные операции.SimpleDateFormatне является потокобезопасным, в то время какDateВремя обработки является громоздким, поэтому Java8 обеспечиваетLocalDateTime,LocalDateа такжеLocalTimeДождитесь появления нового API управления временем. Будь тоDateвсе ещеLocalDate, при разработке приложений Spring Boot часто необходимо добавить поле даты каждого класса сущностей@DateTimeFormatАннотация для получения внешних значений, привязанных к полю даты, плюс@JsonFormatАннотация, чтобы поле даты, возвращаемое во внешний интерфейс, было отформатировано в нужном нам формате времени. Частота использования типов времени и даты в разработке очень высока.Добавлять эти две аннотации к каждому полю очень громоздко.Есть ли способ справиться с глобальными настройками? Позвольте мне представить его вам сегодня.

Примечание. Эта статья основана на версии Springboot 2.3.0.

Различные конфигурации должны быть выполнены в соответствии с различными методами запроса.Следующее разделено на два случая: передача параметров режима JSON и запрос GET и передача параметров формы POST.

Передача параметров в JSON

Этот случай относится к типу POST, а Content-Type — это запрос в режиме application/json. Для этого типа запроса контроллеру необходимо добавить@RequestBodyАннотация для обозначения локальной переменной, которую мы используем для получения параметров запроса, код выглядит следующим образом:

@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDateLearningApplication.class, args);
    }
    
     /**
     * DateTime格式化字符串
     */
    private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    /**
     * Date格式化字符串
     */
    private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";

    /**
     * Time格式化字符串
     */
    private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";

    public static class DateEntity {
        private LocalDate date;

        private LocalDateTime dateTime;

        private Date originalDate;

        public LocalDate getDate() {
            return date;
        }

        public void setDate(LocalDate date) {
            this.date = date;
        }

        public LocalDateTime getDateTime() {
            return dateTime;
        }

        public void setDateTime(LocalDateTime dateTime) {
            this.dateTime = dateTime;
        }

        public Date getOriginalDate() {
            return originalDate;
        }

        public void setOriginalDate(Date originalDate) {
            this.originalDate = originalDate;
        }

    }

    @RequestMapping("/date")
    public DateEntity getDate(@RequestBody DateEntity dateEntity) {
        return dateEntity;
    }
}    

Предположим, что формат по умолчанию для получения и возврата значенийyyyy-MM-dd HH:mm:ss, доступны следующие параметры.

Настройте файл application.yml

Настройте в файле application.yml следующее:

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

резюме:

  • Поддерживает запросы POST, Content-Type которых — application/json. Строка параметра запроса и возвращаемый формат обаyyyy-MM-dd HH:mm:ssЕсли параметры запроса имеют другие форматы, напримерyyyy-MM-ddСтрока сообщит об исключении 400 Bad Request.
  • API-интерфейсы даты Java8, такие как LocalDate, не поддерживаются.

Добавить конфигурацию Джексона

/**
  * Jackson序列化和反序列化转换器,用于转换Post请求体中的json以及将对象序列化为返回响应的json
  */
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    return builder -> builder
            .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
            .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
            .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
            .serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
            .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
            .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
            .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
            .deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN))
            ;
}

резюме:

  • Поддерживает запросы POST, Content-Type которых — application/json. Строка параметра запроса и возвращаемый формат обаyyyy-MM-dd HH:mm:ssЕсли параметры запроса имеют другие форматы, напримерyyyy-MM-ddСтрока сообщит об исключении 400 Bad Request.
  • Поддержка API даты Java8, такого как LocalDate.

PS: Вышеупомянутый способ - настроитьJackson2ObjectMapperBuilderCustomizerBean завершен, в дополнение к этому вы также можете настроитьMappingJackson2HttpMessageConverterреализовать.

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    ObjectMapper objectMapper = new ObjectMapper();
    // 指定时区
    objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
    // 日期类型字符串处理
    objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));

    // Java8日期日期处理
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
    javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
    javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
    javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
    javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
    javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
    objectMapper.registerModule(javaTimeModule);

    converter.setObjectMapper(objectMapper);
    return converter;
}

Все вышеперечисленные методы могут реализовывать глобальную настройку при передаче параметров в JSON, более рекомендуется добавить компоненты конфигурации к последним двум кодам, которые могут поддерживать обаDateа такжеLocalDate.

GET-запрос и POST-форма для передачи параметров

Этот метод полностью отличается от описанного выше метода JSON, который обрабатывается в Spring Boot. Последний способ передать параметры в JSON — этоHttpMessgeConverterот ДжексонаObjectMapperПреобразуйте тело http-запроса в объект параметра, который мы написали в контроллере, и этот метод используетConverterИнтерфейсы (определенные в spring-core) используются для преобразования исходных типов (обычноString) в интерфейс целевого типа), между ними есть существенная разница.

Конвертер пользовательских параметров (Конвертер)

Настройте преобразователь параметров для реализации вышеупомянутогоorg.springframework.core.convert.converter.ConverterИнтерфейс, настройте следующие компоненты в классе конфигурации, примеры следующие:

@Bean
public Converter<String, Date> dateConverter() {
    return new Converter<>() {
        @Override
        public Date convert(String source) {
            SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
            try {
                return formatter.parse(source);
            } catch (Exception e) {
                throw new RuntimeException(String.format("Error parsing %s to Date", source));
            }
        }
    };
}

@Bean
public Converter<String, LocalDate> localDateConverter() {
    return new Converter<>() {
        @Override
        public LocalDate convert(String source) {
            return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN));
        }
    };
}

@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
    return new Converter<>() {
        @Override
        public LocalDateTime convert(String source) {
            return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
        }
    };
}

В то же время, добавляя некоторые параметры в интерфейс контроллера, можно обнаружить, что он может нормально преобразовываться в интерфейсе только с получением переменной.

@RequestMapping("/date")
public DateEntity getDate(
        LocalDate date,
        LocalDateTime dateTime,
        Date originalDate,
        DateEntity dateEntity) {
    System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate);
    return dateEntity;
}

резюме:

  • Запрос GET и запрос формы POST.
  • Поддержка API даты Java8, такого как LocalDate.

использовать@DateTimeFormatаннотация

Как упоминалось ранее, также можно использовать методы запроса GET и формы POST.@DateTimeFormatДля обработки его можно использовать отдельно в параметрах интерфейса контроллера или атрибутах класса сущностей, таких как@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate. Уведомление,Если используется пользовательский преобразователь параметров (Converter), Spring будет предпочтительно использовать этот метод для обработки, то есть@DateTimeFormatАннотация не вступает в силу, и эти два метода несовместимы.

Затем, если мы используем пользовательский преобразователь параметров, но все еще хотим быть совместимыми сyyyy-MM-ddФорма принята? Мы можем поставить предыдущийdateConverterТакже хорошим решением является использование обычного метода сопоставления, пример следующий.

/**
 * 日期正则表达式
 */
private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";

/**
 * 时间正则表达式
 */
private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";

/**
 * 日期和时间正则表达式
 */
private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;

/**
 * 13位时间戳正则表达式
 */
private static final String TIME_STAMP_REGEX = "1\\d{12}";

/**
 * 年和月正则表达式
 */
private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";

/**
 * 年和月格式
 */
private static final String YEAR_MONTH_PATTERN = "yyyy-MM";

@Bean
public Converter<String, Date> dateConverter() {
    return new Converter<String, Date>() {
        @SuppressWarnings("NullableProblems")
        @Override
        public Date convert(String source) {
            if (StrUtil.isEmpty(source)) {
                return null;
            }
            if (source.matches(TIME_STAMP_REGEX)) {
                return new Date(Long.parseLong(source));
            }
            DateFormat format;
            if (source.matches(DATE_TIME_REGEX)) {
                format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
            } else if (source.matches(DATE_REGEX)) {
                format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
            } else if (source.matches(YEAR_MONTH_REGEX)) {
                format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
            } else {
                throw new IllegalArgumentException();
            }
            try {
                return format.parse(source);
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }
        }
    };
}

резюме:

  • Запрос GET и запрос формы POST, но их необходимо добавлять в каждом месте, где они используются.@DateTimeFormatаннотация.
  • Не совместим с преобразователями пользовательских параметров.
  • Поддержка API даты Java8, такого как LocalDate.

использовать@ControllerAdviceСотрудничать@initBinder

/*
 * 在类上加上@ControllerAdvice
 */
@ControllerAdvice
@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {
    ...
    @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(DEFAULT_DATE_PATTERN)));
            }
        });
        binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
            }
        });
        binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
            }
        });
        binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
                try {
                    setValue(formatter.parse(text));
                } catch (Exception e) {
                    throw new RuntimeException(String.format("Error parsing %s to Date", text));
                }
            }
        });
    }   
    ...
}    

В практических приложениях мы можем поместить приведенный выше код в родительский класс, и все интерфейсы наследуют этот родительский класс для достижения эффекта глобальной обработки. Принцип аналогичен АОП, мы используем определение, которое мы определили при преобразовании параметров перед входом в обработчик.PropertyEditorSupportобрабатывать.

резюме:

  • Запрос GET и запрос формы POST.
  • Поддержка API даты Java8, такого как LocalDate.

местная дифференциация

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

использовать@DateTimeFormatа также@JsonFormatаннотация

@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
private Date originalDate;

Как показано выше, можно добавить в поле@DateTimeFormatа также@JsonFormatОбратите внимание, что форматы полученной и возвращенной даты для этого поля можно указать отдельно.

PS:@JsonFormatа также@DateTimeFormatАннотации не предоставляются Spring Boot и могут также использоваться в приложениях Spring.

Напомни еще раз,Если используется пользовательский преобразователь параметров (Converter), Spring будет предпочтительно использовать этот метод для обработки, то есть@DateTimeFormatАннотация не вступает в силу.

Пользовательские сериализаторы и десериализаторы

/**
 * {@link Date} 序列化器
 */
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));
    }
}

/**
 * {@link 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 IOException(e);
        }
    }
}

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

Как показано выше, можно использовать на полях@JsonSerializeа также@JsonDeserializeАннотация для указания использования нашего пользовательского сериализатора и десериализатора при сериализации и десериализации.

Наконец, давайте получим полную конфигурацию, совместимую с режимом JSON, запросом GET и режимом формы POST.

@Configuration
public class GlobalDateTimeConfig {

    /**
     * 日期正则表达式
     */
    private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";

    /**
     * 时间正则表达式
     */
    private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";

    /**
     * 日期和时间正则表达式
     */
    private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;

    /**
     * 13位时间戳正则表达式
     */
    private static final String TIME_STAMP_REGEX = "1\\d{12}";

    /**
     * 年和月正则表达式
     */
    private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";

    /**
     * 年和月格式
     */
    private static final String YEAR_MONTH_PATTERN = "yyyy-MM";

    /**
     * DateTime格式化字符串
     */
    private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    /**
     * Date格式化字符串
     */
    private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";

    /**
     * Time格式化字符串
     */
    private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    /**
     * LocalDate转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        return new Converter<String, LocalDate>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public LocalDate convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
            }
        };
    }

    /**
     * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<String, LocalDateTime>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public LocalDateTime convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
            }
        };
    }

    /**
     * LocalDate转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalTime> localTimeConverter() {
        return new Converter<String, LocalTime>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public LocalTime convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
            }
        };
    }

    /**
     * Date转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, Date> dateConverter() {
        return new Converter<String, Date>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public Date convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                if (source.matches(TIME_STAMP_REGEX)) {
                    return new Date(Long.parseLong(source));
                }
                DateFormat format;
                if (source.matches(DATE_TIME_REGEX)) {
                    format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
                } else if (source.matches(DATE_REGEX)) {
                    format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
                } else if (source.matches(YEAR_MONTH_REGEX)) {
                    format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
                } else {
                    throw new IllegalArgumentException();
                }
                try {
                    return format.parse(source);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    /**
     * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
                .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .serializerByType(Long.class, ToStringSerializer.instance)
                .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
                .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
    }

}

Анализ исходного кода

Поняв, как делать глобальные настройки, давайте проанализируем, как Spring MVC выполняет привязку параметров через исходный код отладки.

Тем не менее, возьмите приведенный выше контроллер в качестве примера для отладки.

@RequestMapping("/date")
public DateEntity getDate(
        LocalDate date,
        LocalDateTime dateTime,
        Date originalDate,
        DateEntity dateEntity) {
    System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate);
    return dateEntity;
}

Вот некоторые ключевые методы стека вызовов методов после получения запроса:

// DispatcherServlet处理请求
doService:943, DispatcherServlet
// 处理请求
doDispatch:1040, DispatcherServlet
// 生成调用链(前处理、实际调用方法、后处理)
handle:87, AbstractHandlerMethodAdapter
handleInternal:793, RequestMappingHandlerAdapter
// 反射获取到实际调用方法,准备开始调用
invokeHandlerMethod:879, RequestMappingHandlerAdapter
invokeAndHandle:105, ServletInvocableHandlerMethod 
// 关键步骤,从这里开始处理请求参数
invokeForRequest:134, InvocableHandlerMethod
getMethodArgumentValues:167, InvocableHandlerMethod
resolveArgument:121, HandlerMethodArgumentResolverComposite

Ниже начинаем с ключаinvokeForRequest:134, InvocableHandlerMethodЗапустите анализ, исходный код выглядит следующим образом

// InvocableHandlerMethod.java
@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 {
 // 获取当前handler method的方法参数数组,封装了入参信息,比如类型、泛型等
    MethodParameter[] parameters = getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;
    }
 // 该数组用来存放从MethodParameter转换后的结果
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        args[i] = findProvidedArgument(parameter, providedArgs);
        if (args[i] != null) {
            continue;
        }
        // resolvers是定义的成员变量,HandlerMethodArgumentResolverComposite类型,是各式各样的HandlerMethodArgumentResolver的集合。这里来判断一下是否存在支持当前方法参数的参数处理器
        if (!this.resolvers.supportsParameter(parameter)) {
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
            // 调用HandlerMethodArgumentResolverComposite来处理参数,下面会重点看一下内部的逻辑
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        }
        catch (Exception ex) {
   ......
        }
    }
    return args;
}

Необходимо ввести нижеHandlerMethodArgumentResolverComposite#resolveArgumentисходный код метода.

// HandlerMethodArgumentResolverComposite.java
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
 // 这里来获取匹配当前方法参数的参数解析器
    HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
    if (resolver == null) {
        throw new IllegalArgumentException("Unsupported parameter type [" +
                parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
    }
    // 调用真正的参数解析器来处理参数并返回
    return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// 获取匹配当前方法参数的参数解析器
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    // 首先从缓存中查询是否有适配当前方法参数的参数解析器,首次进入是没有的
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            // 逐个遍历argumentResolvers这个list里的参数解析器来判断是否支持
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}

argumentResolversВсего существует 26 парсеров параметров, и наиболее распространенные из них перечислены ниже.

this.argumentResolvers = {LinkedList@6072}  size = 26
 0 = {RequestParamMethodArgumentResolver@6098} 
 1 = {RequestParamMapMethodArgumentResolver@6104} 
 2 = {PathVariableMethodArgumentResolver@6111} 
 3 = {PathVariableMapMethodArgumentResolver@6112} 
 ......
 7 = {RequestResponseBodyMethodProcessor@6116} 
 8 = {RequestPartMethodArgumentResolver@6117} 
 9 = {RequestHeaderMethodArgumentResolver@6118} 
 10 = {RequestHeaderMapMethodArgumentResolver@6119} 
 ......
 14 = {RequestAttributeMethodArgumentResolver@6123} 
 15 = {ServletRequestMethodArgumentResolver@6124} 
 ......
 24 = {RequestParamMethodArgumentResolver@6107} 
 25 = {ServletModelAttributeMethodProcessor@6133} 

Все парсеры параметров реализованыHandlerMethodArgumentResolverинтерфейс.

public interface HandlerMethodArgumentResolver {

 // 上面用到用来判断当前参数解析器是否支持给定的方法参数
 boolean supportsParameter(MethodParameter parameter);

    // 解析给定的方法参数并返回
 @Nullable
 Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

Вот и разберемся с идеями.Анализ параметров метода заключается в поиске подходящего путем обхода по одному.HandlerMethodArgumentResolverзавершить. Например, если параметр помечен@RequestParamили@RequestBodyили@PathVariableАннотация, SpringMVC будет использовать разные преобразователи параметров для разрешения. Выберите один из наиболее часто используемыхRequestParamMethodArgumentResolverДавайте подробно проанализируем процесс детального анализа.

RequestParamMethodArgumentResolverунаследовано отAbstractNamedValueMethodArgumentResolver,AbstractNamedValueMethodArgumentResolverДостигнутоHandlerMethodArgumentResolverинтерфейсresolveArgumentметод.

// AbstractNamedValueMethodArgumentResolver.java
@Override
@Nullable
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) {
        // 创建 DataBinder 
        WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
        try {
            // 通过DataBinder进行参数绑定,参数列表:原始值,目标类型,方法参数
            arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
        }
  ......
    }

    handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

    return arg;
}

// DataBinder.java
@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
        @Nullable MethodParameter methodParam) throws TypeMismatchException {
 // 调用子类的convertIfNecessary方法,这里的具体实现是TypeConverterSupport
    return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}

// TypeConverterSupport.java
@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
        @Nullable MethodParameter methodParam) throws TypeMismatchException {
 // 调用重载的convertIfNecessary方法,通过MethodParameter构造了类型描述符TypeDescriptor
    return convertIfNecessary(value, requiredType,
            (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
}
// convertIfNecessary方法
@Nullable
@Override
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
        @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {

    Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
    try {
        // 调用TypeConverterDelegate的convertIfNecessary方法
        return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor);
    }
 ......
}

Введите следующийTypeConverterDelegateисходный код.

// TypeConverterDelegate.java
@Nullable
public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
        @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {

    // 查找是否有适合需求类型的自定义的PropertyEditor。还记得上面的 使用@ControllerAdvice配合@initBinder 那一节吗,如果有按那样配置,这里就会找到
    PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

    ConversionFailedException conversionAttemptEx = null;

    // 查找到类型转换服务 ConversionService
    ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
    // 关键判断,如果没有PropertyEditor 就使用ConversionService
    if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
        TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
        if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
            try {
                // #1,类型转换服务转换完成后就返回,下面会详细解释
                return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
            }
            catch (ConversionFailedException ex) {
                // fallback to default conversion logic below
                conversionAttemptEx = ex;
            }
        }
    }

    Object convertedValue = newValue;

    // 关键判断,如果有PropertyEditor就使用PropertyEditor
    if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
     ......
        // 由editor完成转换    
        convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
    }

    boolean standardConversion = false;

    if (requiredType != null) {
        // Try to apply some standard type conversion rules if appropriate.

        if (convertedValue != null) {
            if (Object.class == requiredType) {
                return (T) convertedValue;
            }
            // 下面是数组、集合类型属性的处理,这里会遍历集合元素,递归调用convertIfNecessary转化,再收集处理结果
            else if (requiredType.isArray()) {
                // Array required -> apply appropriate conversion of elements.
                if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {
                    convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
                }
                return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());
            }
            else if (convertedValue instanceof Collection) {
                // Convert elements to target type, if determined.
                convertedValue = convertToTypedCollection(
                        (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
                standardConversion = true;
            }
            else if (convertedValue instanceof Map) {
                // Convert keys and values to respective target type, if determined.
                convertedValue = convertToTypedMap(
                        (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor);
                standardConversion = true;
            }
            if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {
                convertedValue = Array.get(convertedValue, 0);
                standardConversion = true;
            }
            if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
                // We can stringify any primitive value...
                return (T) convertedValue.toString();
            }
            else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {
    ......
            }
            else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {
                convertedValue = NumberUtils.convertNumberToTargetClass(
                        (Number) convertedValue, (Class<Number>) requiredType);
                standardConversion = true;
            }
        }
        else {
            // convertedValue == null,空值处理
            if (requiredType == Optional.class) {
                convertedValue = Optional.empty();
            }
        }

  ......
    }
 // 异常处理
    if (conversionAttemptEx != null) {
        if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
            throw conversionAttemptEx;
        }
        logger.debug("Original ConversionService attempt failed - ignored since " +
                "PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
    }

    return (T) convertedValue;
}

Если мы настроим пользовательскийConverter, войдет в ветку №1, поConversionServiceприведение к своему подклассуGenericConversionServiceНапример.

// GenericConversionService.java
@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
 ......
    // 从缓存中找到匹配类型的conveter,以LocalDateTime为例,会找到我们自定义的localDateTimeConverter
    GenericConverter converter = getConverter(sourceType, targetType);
    if (converter != null) {
        // 通过工具方法调用真正的converter完成类型转换。至此,完成了源类型到目标类型的转换
        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
        return handleResult(sourceType, targetType, result);
    }
    return handleConverterNotFound(source, sourceType, targetType);
}

Выше приведена аннотация обработки@RequestParamАннотированные параметрыRequestParamMethodArgumentResolverПроцесс разбора.

Давайте посмотрим на обработку аннотаций@RequestBodyАннотированные параметрыRequestResponseBodyMethodProcessorПроцесс разбора все еще сresolveArgumentзапись метода.

// RequestResponseBodyMethodProcessor.java
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    parameter = parameter.nestedIfOptional();
    // 在这里完成参数的解析
    Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
 ......
    return adaptArgumentIfNecessary(arg, parameter);
}

@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
        Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

    HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
    Assert.state(servletRequest != null, "No HttpServletRequest");
    ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
 // 调用父类AbstractMessageConverterMethodArgumentResolver完成参数解析
    Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
    if (arg == null && checkRequired(parameter)) {
        throw new HttpMessageNotReadableException("Required request body is missing: " +
                parameter.getExecutable().toGenericString(), inputMessage);
    }
    return arg;
}

Введите родительский класс нижеAbstractMessageConverterMethodArgumentResolverисходный код.

// AbstractMessageConverterMethodArgumentResolver.java
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
        Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    ......
    EmptyBodyCheckingHttpInputMessage message;
    try {
        message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
  // 遍历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);
                    // 实际由MappingJackson2HttpMessageConverter调用父类AbstractJackson2HttpMessageConverter的read方法完成解析,
                    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;
            }
        }
    }
 ......
    return body;
}

// AbstractJackson2HttpMessageConverter.java
@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {
 // 获得要转换的目标参数Java类型,如LocalDateTime等
    JavaType javaType = getJavaType(type, contextClass);
    // 调用本类的readJavaType方法
    return readJavaType(javaType, inputMessage);
}

// AbstractJackson2HttpMessageConverter.java
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
    try {
        if (inputMessage instanceof MappingJacksonInputMessage) {
            Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
            if (deserializationView != null) {
                return this.objectMapper.readerWithView(deserializationView).forType(javaType).
                        readValue(inputMessage.getBody());
            }
        }
        // 调用jackson类库,将HTTP的json请求信息解析为需要的参数类型。至此,将json请求转换成目标Java类型
        return this.objectMapper.readValue(inputMessage.getBody(), javaType);
    }
 ......
}

Суммировать

Параметры метода контроллера передаются через разныеHandlerMethodArgumentResolverзавершен разбор. Если параметр отмечен@RequestBodyАннотация, фактически черезMappingJackson2HttpMessageConverterизObjectMapperДесериализуйте и проанализируйте входящие данные формата json в целевой тип. Если отмечено@RequestParamАннотации вводятся в приложение во время инициализации.ConversionServiceпо одномуConverterбыть реализованным. другиеHandlerMethodArgumentResolverКаждый из них имеет свое собственное применение. Вы можете еще раз просмотреть соответствующий код, чтобы углубить свое понимание.