如何在Spring Boot應用中優(yōu)雅的使用Date和LocalDateTime

Java8已經發(fā)布很多年了顶别,但是很多人在開發(fā)時仍然堅持使用著DateSimpleDateFormat進行時間操作猜煮。SimpleDateFormat不是線程安全的拗胜,而Date處理時間很麻煩灵汪,所以Java8提供了LocalDateTime檀训、LocalDateLocalTime等全新的時間操作API。無論是Date還是LocalDate享言,在開發(fā)Spring Boot應用時經常需要在每個實體類的日期字段上加上@DateTimeFormat注解來接收前端傳值與日期字段綁定峻凫,加上@JsonFormat注解來讓返回前端的日期字段格式化成我們想要的時間格式。時間和日期類型在開發(fā)中使用的頻率是非常高的览露,如果每個字段都加上這兩個注解的話是非常繁瑣的荧琼,有沒有一種全局設置的處理方式呢?今天就來向大家介紹一下差牛。

注:本文基于Springboot2.3.0版本命锄。

根據不同的請求方式需要做不同的配置,下文中分為了JSON方式傳參和GET請求及POST表單方式傳參兩種情況多糠。

JSON方式傳參

這種情況指的是類型POST累舷,Content-Type 是application/json 方式的請求。對于這類請求夹孔,controller中需要加上@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

小結:

  • 支持Content-Type 是application/json的POST請求只怎,請求參數字符串和返回的格式都是yyyy-MM-dd HH:mm:ss如果請求參數是其他格式袜瞬,如yyyy-MM-dd字符串則報400 Bad Request異常。
  • 不支持LocalDate等Java8日期API身堡。

增加Jackson配置

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

小結:

  • 支持Content-Type 是application/json的POST請求,請求參數字符串和返回的格式都是yyyy-MM-dd HH:mm:ss如果請求參數是其他格式贴谎,如yyyy-MM-dd字符串則報400 Bad Request異常汞扎。
  • 支持LocalDate等Java8日期API。

PS:上面的方式是通過配置一個Jackson2ObjectMapperBuilderCustomizerBean完成的擅这,除了這種澈魄,也可以通過自定義一個MappingJackson2HttpMessageConverter來實現。

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    ObjectMapper objectMapper = new ObjectMapper();
    // 指定時區(qū)
    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傳參時的全局化配置仲翎,更推薦后兩種代碼中增加配置bean的方式痹扇,可以同時支持DateLocalDate

GET請求及POST表單方式傳參

這種方式和上面的JSON方式溯香,在Spring Boot處理的方式是完全不同的鲫构。上一種JSON方式傳參是在HttpMessgeConverter中通過jackson的ObjectMapper將http請求體轉換成我們寫在controller中的參數對象的,而這種方式用的是Converter接口(spring-core中定義的用于將源類型(一般是String)轉成目標類型的接口)玫坛,兩者是有本質區(qū)別的结笨。

自定義參數轉換器(Converter)

自定義一個參數轉換器,實現上面提到的org.springframework.core.convert.converter.Converter接口湿镀,在配置類里配置上以下幾個bean禀梳,示例如下:

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

同時把controller接口增加一些參數,可以發(fā)現在接口里單獨用變量接收也是可以正常轉換的肠骆。

@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表單方式請求算途。
  • 支持LocalDate等Java8日期API。

使用@DateTimeFormat注解

和前面提到的一樣蚀腿,GET請求及POST表單方式也是可以用@DateTimeFormat來處理的嘴瓤,單獨在controller接口參數或者實體類屬性中都可以使用,比如@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate莉钙。注意廓脆,如果使用了自定義參數轉化器(Converter),Spring會優(yōu)先使用該方式進行處理磁玉,即@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注解。
  • 與自定義參數轉化器(Converter)不兼容癞揉。
  • 支持LocalDate等Java8日期API纸肉。

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

在實際應用中,我們可以把上面代碼放到父類中喊熟,所有接口繼承這個父類柏肪,達到全局處理的效果。原理就是與AOP類似芥牌,在參數進入handler之前進行轉換時使用我們定義的PropertyEditorSupport來處理预吆。

小結:

  • GET請求及POST表單方式請求。
  • 支持LocalDate等Java8日期API胳泉。

局部差異化處理

假設按照前面的全局日期格式設置的是: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會優(yōu)先使用該方式進行處理返吻,即@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)));
    }

}

源碼剖析

在了解完怎么樣進行全局設置后磁携,接下來我們通過debug源碼來深入剖析一下Spring MVC是如何進行參數綁定的。

仍然是以上面的controller為例進行debug良风。

@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));
    }
    // 反射調用篙议,真正開始執(zhí)行方法
    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) {
        // 創(chuàng)建 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 那一節(jié)嗎,如果有按那樣配置酒唉,這里就會找到
    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);
    }
    ......
}

總結

controller方法的參數是通過不同的HandlerMethodArgumentResolver完成解析的脓诡。如果參數標注了@RequestBody注解无午,實際上是通過MappingJackson2HttpMessageConverterObjectMapper將傳入json格式數據反序列化解析成目標類型的。如果標注了@RequestParam注解祝谚,是通過在應用初始化時注入到ConversionService的一個個Converter來實現的宪迟。其他的HandlerMethodArgumentResolver也是各有各的用處,大家可以再看看相關代碼交惯,以便加深理解次泽。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市席爽,隨后出現的幾起案子意荤,更是在濱河造成了極大的恐慌,老刑警劉巖只锻,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玖像,死亡現場離奇詭異,居然都是意外死亡齐饮,警方通過查閱死者的電腦和手機捐寥,發(fā)現死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門笤昨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人握恳,你說我怎么就攤上這事瞒窒。” “怎么了乡洼?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵崇裁,是天一觀的道長。 經常有香客問我就珠,道長寇壳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任妻怎,我火速辦了婚禮壳炎,結果婚禮上,老公的妹妹穿的比我還像新娘逼侦。我一直安慰自己匿辩,他們只是感情好,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布榛丢。 她就那樣靜靜地躺著铲球,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晰赞。 梳的紋絲不亂的頭發(fā)上稼病,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音掖鱼,去河邊找鬼然走。 笑死,一個胖子當著我的面吹牛戏挡,可吹牛的內容都是我干的芍瑞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼褐墅,長吁一口氣:“原來是場噩夢啊……” “哼拆檬!你這毒婦竟也來了?” 一聲冷哼從身側響起妥凳,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤竟贯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后逝钥,有當地人在樹林里發(fā)現了一具尸體澄耍,經...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了齐莲。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痢站。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖选酗,靈堂內的尸體忽然破棺而出阵难,到底是詐尸還是另有隱情,我是刑警寧澤芒填,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布呜叫,位于F島的核電站,受9級特大地震影響殿衰,放射性物質發(fā)生泄漏朱庆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一闷祥、第九天 我趴在偏房一處隱蔽的房頂上張望娱颊。 院中可真熱鬧,春花似錦凯砍、人聲如沸箱硕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剧罩。三九已至,卻和暖如春座泳,著一層夾襖步出監(jiān)牢的瞬間惠昔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工挑势, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镇防,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓薛耻,卻偏偏與公主長得像营罢,于是被迫代替她去往敵國和親赏陵。 傳聞我的和親對象是個殘疾皇子饼齿,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359