Ya no use la fecha como parámetro de entrada en Spring Boot, ¡LocalDateTime y LocalDate son realmente fragantes!

Autor: TinyThing Enlace: https://www.jianshu.com/p/b52db905f020

fondo 0x0

El proyecto utiliza la serie LocalDateTime como tipo de hora en dto, pero Spring siempre informa un error después de recibir los parámetros. Para configurar la conversión de tipo de hora globalmente, se prueban los siguientes tres métodos.

Nota: Este artículo se basa en la prueba Springboot2.0. Si no tiene efecto, puede deberse a una versión inferior de Spring. PD: Si no agrega ninguna anotación (RequestParam, PathVariable, etc.) a los parámetros del tipo LocalDate en su Controlador, también se producirán errores, porque de forma predeterminada, el análisis de dichos parámetros se utiliza para el procesamiento y este procesador debe procesarse a través ModelAttributeMethodProcessorde instancias de reflexión Cree un objeto y luego convierta cada parámetro en el objeto, pero la clase LocalDate no tiene constructor y no se puede reflejar ni crear instancias, por lo que se informará un error. ! !

0x1 Cuando se utiliza LocalDateTime como RequestParam o PathVariable

Esta situación debe tratarse de manera diferente a cuando se usa el tiempo como una cadena Json, porque la capa inferior del json front-end al pojo back-end usa la herramienta Jackson de serialización Json (HttpMessgeConverter); cuando la cadena de tiempo se pasa como un Parámetro de solicitud normal, la conversión utiliza Converter, existe una diferencia entre los dos. En este caso, existen varias opciones:

Recomiende un proyecto práctico Spring Boot gratuito y de código abierto:

https://github.com/javastacks/spring-boot-best-practice

1. Utilice el convertidor

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.converter.HttpMessageConverter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Configuration
public class DateConfig {

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

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

}

Los dos beans anteriores se inyectarán en el analizador de parámetros de Spring MVC (parece llamarse ParameterConversionService). Cuando la cadena entrante se va a convertir a la clase LocalDateTime, Spring llamará al convertidor para convertir el parámetro de entrada.

2. Utilice ControllerAdvice con 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")));
            }
        });
    }
}

Como puede verse por el nombre, este es un corte de anillo en el controlador (también hay una captura de excepción global), y el parámetro se convierte antes de ingresar al controlador; se convierte a nuestro objeto correspondiente.

0x2 Cuando LocalDateTime se pasa como Json

En este caso, como se describe anteriormente, utilice la serialización y deserialización json de Jackson:

@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 ObjectMapper objectMapper(){
        ObjectMapper objectMapper = new ObjectMapper();
//            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
//            objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
        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).registerModule(new ParameterNamesModule());
        return objectMapper;
    }

}

0x3 Llega a una configuración completa

No presentaré los conceptos básicos de Spring Boot, se recomienda ver este proyecto práctico:

https://github.com/javastacks/spring-boot-best-practice

package com.fly.hi.common.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

@Configuration
public class DateConfig {

    /** 默认日期时间格式 */
    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参数
     */
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        return new Converter<>() {
            @Override
            public LocalDate convert(String source) {
                return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
            }
        };
    }

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

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

    /**
     * Date转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, Date> dateConverter() {
        return new Converter<>() {
            @Override
            public Date convert(String source) {
                SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
                try {
                    return format.parse(source);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }


    /**
     * 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;
    }


}

0x4 Estudio en profundidad del proceso de enlace de datos SpringMVC

A continuación, ingrese al modo de depuración para ver cómo MVC vincula los parámetros de nuestra solicitud a los parámetros de entrada de nuestro método de capa de controlador:

Escriba un controlador simple y observe la pila de llamadas al método en el siguiente punto de interrupción:

@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();
}

Después de detenernos, veamos algunos métodos clave en la pila de llamadas a métodos:

//进入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

Según el análisis anterior, se encuentra que invokeForRequest: 142. El código aquí en InvocableHandlerMethod se usa para obtener los parámetros reales:

@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);
}

Ingrese a este método para ver qué hace:

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;
}

Ingrese resolveArgument para ver:

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);
}

Aquí, el analizador de parámetros correspondiente se obtiene de acuerdo con los parámetros para ver cómo se obtiene internamente:

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

Aquí, recorra los analizadores de parámetros para descubrir si hay un analizador adecuado. Entonces, ¿qué analizadores de parámetros existen (26 cuando probé)? ? ? ¡Enumero algunos importantes para ver si te resultan familiares! ! !

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

Ingresamos a uno de los analizadores más utilizados para observar su método supportParameter y descubrimos que el analizador correspondiente se obtuvo mediante anotaciones de parámetros.

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;
        }
    }
    //......
}

Es decir, para los parámetros de @RequestParamy anotaciones, SpringMVC utilizará diferentes analizadores de parámetros para el enlace de datos @RequestBody. @PathVariable Entonces, ¿qué convertidor utilizan estos tres analizadores para analizar los parámetros? Vayamos a los tres analizadores y echemos un vistazo: primero, RequestParamMethodArgumentResolverencontramos que WebDataBinder se usa internamente para el enlace de datos y la capa inferior usa ConversionService (es decir, donde se inyecta nuestro convertidor).

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;
        }
    }
}

Luego mira y RequestResponseBodyMethodProcessordescubre que el convertidor utilizado es del tipo 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;
    }
}

Finalmente, PathVariableMethodArgumentResolverdescubrí que la ruta de ejecución es la misma que la de RequestParam (ambos se heredan del analizador AbstractNamedValueMethodArgumentResolver), por lo que el código no se publicará.

0xFFResumen

Si desea convertir los parámetros pasados ​​por la solicitud al tipo que especificamos, debe distinguirlos según las anotaciones de entrada:

  1. MappingJackson2HttpMessageConverterSi es RequestBody, serialice y deserialice los datos en formato Json configurando ObjectMapper (esto se inyectará en HttpMessagConverter de Jackson, es decir, en el medio);
  2. Si es un parámetro de tipo RequestParam o PathVariable, la conversión de parámetros se realiza configurando Converter (estos convertidores se inyectarán en ConversionService).

Recomendación de artículo reciente:

1. Más de 1000 preguntas y respuestas de entrevistas en Java (última versión de 2022)

2. ¡Brillante! Las corrutinas de Java están por llegar. . .

3. Tutorial de Spring Boot 2.x, ¡demasiado completo!

4. No llenes la pantalla con explosiones y explosiones, prueba el modo decorador, ¡esta es la forma elegante! !

5. Descargue rápidamente la última versión del "Manual de desarrollo de Java (edición Songshan)".

¡Siéntete bien, no olvides darle me gusta + adelante!

Supongo que te gusta

Origin blog.csdn.net/youanyyou/article/details/132554803
Recomendado
Clasificación