0x0. LocalDateTime在SpringBoot中的窘境 问题由来 :在前不久,我们后台就已经抛弃了Date这个类,而改用了java8提供的LocalDateTime,但是正常情况下LocalDateTime的构造函数是私有的,无法像Date那样直接被spring mvc直接处理,所以带给了我很多困扰,甚至有一次项目急迫致使我直接使用String去对时间进行接收,然后再通过DateTimeFormatter在Controller层对参数进行了一层处理,才能将LocalDateTime传给了Service进行处理,直接导致的结果就是方法变得有点丑陋了(逃~
0x1. 初步解决LocalDateTime作为query参数的问题 经过多方面查询得知,对于java8时间格式的问题,其实困扰着很多人,网上也提供了很多的解决方案,比如说下面这种在query参数上加注解的方式:
1 2 3 4 5 6 7 8 9 10 11 @RestController @RequestMapping("/api") public class TestApi { @GetMapping("/time") public RestInfo getTime (@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time) { return RestInfo.ok(time); } }
请求结果如下:
但是这样造成的后果是严重影响了代码的质量,每个使用LocalDateTime的参数的地方都需要使用两个注解。
0x2. 寻找全局解决方式 接着我又继续尝试了这一种在加了@Configuration的类中使用@Bean注入了一个Convert转换器的方式就是一种很常见的方法(PS:LocalDateTimeUtils是我自己写的一个工具类,后面再提):
1 2 3 4 5 6 7 8 9 10 11 12 @Bean public Converter<String, LocalDateTime> localDateTimeConvert () { return new Converter <String, LocalDateTime>() { @Override public LocalDateTime convert (String source) { if (StringUtils.isEmpty(source)) { return null ; } return LocalDateTimeUtils.convert(source.trim()); } }; }
Controller层代码:
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/api") public class TestApi { @GetMapping("/time") public RestInfo getTime (LocalDateTime time) { return RestInfo.ok(time); } }
但是我使用过后却并没有得到我想要的效果,出现了报错,这是为什么? 得到的报错如下java.lang.IllegalStateException: No primary or default constructor found for class java.time.LocalDateTime
,仔细想了一下为什么别人能使用呢,后面才发现自己缺少了一个注解@RequestParam,好的,我这就去加上:
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/api") public class TestApi { @GetMapping("/time") public RestInfo getTime (@RequestParam LocalDateTime time) { return RestInfo.ok(time); } }
请求结果:
nice!全局方式query也生效了,现在我们来尝试一下使用Model去接收参数:
TestModel:
1 2 3 4 5 @Data public class TestModel { private LocalDateTime time; }
Controller层:
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/api") public class TestApi { @GetMapping("/model") public RestInfo getModel (TestModel model) { return RestInfo.ok(model); } }
请求结果:
嗯,现在已经大致满足要求了,但是对于简洁到了极致的自己来说却依旧不能感到满足,为什么LocalDateTime作为一个属性存在于Model中时却不需要使用注解方式去指定呢?而我们不使用@RequestParam时LocalDateTime却会出现报错?
0x3. 深入研究分析参数转换过程 我们先在先在Controller层下一个断点,然后查看调用堆栈,找到了一个可疑的方法:invokeForRequest
我们点进去看一看:
1 2 3 4 5 6 7 8 9 10 @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); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } 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 ; } if (!this .resolvers.supportsParameter(parameter)) { throw new IllegalStateException (formatArgumentError(parameter, "No suitable resolver" )); } try { args[i] = this .resolvers.resolveArgument(parameter, mavContainer, request, this .dataBinderFactory); } catch (Exception ex) { if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @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) { if (resolver.supportsParameter(parameter)) { result = resolver; this .argumentResolverCache.put(parameter, result); break ; } } } return result; }
我们展开参数解析器的列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 this.argumentResolvers = {LinkedList@6957} size = 26 0 = {RequestParamMethodArgumentResolver@6959} 1 = {RequestParamMapMethodArgumentResolver@7055} 2 = {PathVariableMethodArgumentResolver@7056} 3 = {PathVariableMapMethodArgumentResolver@7057} 4 = {MatrixVariableMethodArgumentResolver@7058} 5 = {MatrixVariableMapMethodArgumentResolver@7059} 6 = {ServletModelAttributeMethodProcessor@7060} 7 = {RequestResponseBodyMethodProcessor@7061} 8 = {RequestPartMethodArgumentResolver@7062} 9 = {RequestHeaderMethodArgumentResolver@7063} 10 = {RequestHeaderMapMethodArgumentResolver@7064} 11 = {ServletCookieValueMethodArgumentResolver@7065} 12 = {ExpressionValueMethodArgumentResolver@7066} 13 = {SessionAttributeMethodArgumentResolver@7067} 14 = {RequestAttributeMethodArgumentResolver@7068} 15 = {ServletRequestMethodArgumentResolver@7069} 16 = {ServletResponseMethodArgumentResolver@7070} 17 = {HttpEntityMethodProcessor@7071} 18 = {RedirectAttributesMethodArgumentResolver@7072} 19 = {ModelMethodProcessor@7073} 20 = {MapMethodProcessor@7074} 21 = {ErrorsMethodArgumentResolver@7075} 22 = {SessionStatusMethodArgumentResolver@7076} 23 = {UriComponentsBuilderMethodArgumentResolver@7077} 24 = {RequestParamMethodArgumentResolver@7078} 25 = {ServletModelAttributeMethodProcessor@7079}
是不是看到了几个看着很眼熟的东西?RequestParam
,PathVariable
,RequestBody
,在idea中查找RequestParamMethodArgumentResolver类,进入他的方法看一下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Override public boolean supportsParameter (MethodParameter parameter) { if (parameter.hasParameterAnnotation(RequestParam.class)) { if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); return (requestParam != null && StringUtils.hasText(requestParam.name())); } else { return true ; } } else { if (parameter.hasParameterAnnotation(RequestPart.class)) { return false ; } parameter = parameter.nestedIfOptional(); if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { return true ; } else if (this .useDefaultResolution) { return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); } else { return false ; } } }
这里可以基本确定一件事,在我这测试时默认的参数解析器有26个,每个参数解析器有着各自的用途,比如加了@RequestParam注解的参数会被RequestParamMethodArgumentResolver所解析,然后再查找类型有无相应的Convert转换器,在我们这里使用@Bean注入了一个Convert转换器,所以LocalDateTime能被正确的解析。
那么我们之前不加@RequestParam直接使用LocalDateTime作为参数的时候是被哪个解析器所捕获的呢,方法已经找到了,那么直接下个断点跑一遍查看result返回的是哪个解析器就知道了!
如果没记错ServletModelAttributeMethodProcessor是参数解析器的最后一个,我们进入到这个类看一眼,没有supportsParameter方法,但是他继承了ModelAttributeMethodProcessor,我们再进入他的父类中:
1 2 3 4 5 @Override public boolean supportsParameter (MethodParameter parameter) { return (parameter.hasParameterAnnotation(ModelAttribute.class) || (this .annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); }
对于没有指定参数解析器的参数来说,默认指定的参数解析器是ModelAttributeMethodProcessor,并且由源码得知,如果加了@ModelAttribute注解,或者非简单属性则会被该解析器捕获,所以我们平时所使用的model去接收不需要加注解即可被正确的解析,既然都来了,那么顺便看一下resolveArgument方法是怎么解析参数的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Override @Nullable public final Object resolveArgument (MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { try { attribute = createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException ex) { if (isBindExceptionRequired(parameter)) { throw ex; } if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } bindingResult = ex.getBindingResult(); } } }
继续跟进createAttribute方法,看看他是怎么将值绑定给对象的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 protected Object createAttribute (String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception { MethodParameter nestedParameter = parameter.nestedIfOptional(); Class<?> clazz = nestedParameter.getNestedParameterType(); Constructor<?> ctor = BeanUtils.findPrimaryConstructor(clazz); if (ctor == null ) { Constructor<?>[] ctors = clazz.getConstructors(); if (ctors.length == 1 ) { ctor = ctors[0 ]; } else { try { ctor = clazz.getDeclaredConstructor(); } catch (NoSuchMethodException ex) { throw new IllegalStateException ("No primary or default constructor found for " + clazz, ex); } } } Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest); if (parameter != nestedParameter) { attribute = Optional.of(attribute); } return attribute; }
看到这里是不是发现报错的地方有点熟悉?这就是我们之前LocalDateTime抛出异常的地方了,可以得知对于对象的绑定,先通过BeanUtils获取主要的构造函数,如果获取不到,则使用反射的方式先尝试获取声明为public的无参构造函数,最后才会尝试使用getDeclaredConstructor获取所有的无参构造函数,但是对于LocalDateTime这种使用工厂构造不存在无参构造函数的类来说就会直接抛出NoSuchMethodException异常。那么如果我们不想使用@RequestParam注解加在参数上怎么办呢?
0x4. 创建参数解析器 如果我们不希望在LocalDateTime上增加注解,然后再通过RequestParamMethodArgumentResolver解析,我们可以自己定制一种针对LocalDateTime的参数解析器,通过继承WebMvcConfigurer重写addArgumentResolvers方法,然后add一个自定义的HandlerMethodArgumentResolver即可解决刚才的问题,但是在2.2.0版本上会自动指定LocalDateTime的解析器原因未知(我太蔡了…找不到是怎么做到的,想看看他是怎么获取的,但是发现那个方法再无限调用,完全没有头绪。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addArgumentResolvers (List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new LocalDateArgumentResolverHandler ()); } public static class LocalDateArgumentResolverHandler implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter (MethodParameter parameter) { return parameter.getParameterType().equals(LocalDateTime.class); } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { String param = webRequest.getParameter(Objects.requireNonNull(parameter.getParameterName())); if (StringUtils.isEmpty(param)) { return null ; } return LocalDateTimeUtils.convert(param.trim()); } } }
0x5. 推荐使用注册Convert的方式 相比于注入@Bean的方式创建Convert的方式,我个人更喜欢使用继承WebMvcConfigurer后重写addFormatters方法来注册自定义Convert。这种方式相比于@Bean注入,更容易让人理解,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addFormatters (FormatterRegistry registry) { registry.addConverter(new LocalDateTimeConverter ()); } public static class LocalDateTimeConverter implements Converter <String, LocalDateTime> { @Override public LocalDateTime convert (String source) { if (StringUtils.isEmpty(source)) { return null ; } return LocalDateTimeUtils.convert(source.trim()); } } }
0x6. 小结 对于request参数其实还有initBinder,Formatter等方式进行处理,这里就不一一贴出来了,有兴趣的各位可以在网上查询相关资料。还有jackson相关的序列化,通过继承com.fasterxml.jackson.databind.JsonDeserializer和com.fasterxml.jackson.databind.JsonSerializer生成自定义的Jackson Deserializer和Serializer,然后在@Configuration注解的配置类内注入@Bean将Jackson Module注册为@Bean,SpringBoot会自动注入进ObjectMapper中。另一种方式是自己注入一个自己定制的ObjectMapper为@Bean,然后将Module注入进ObjectMapper中。甚至可以通过定制转换规则,从而使类型支持多种参数。
0xf. 附录 时间字符串转 LocalDateTime 工具类。转换不同时区的时间为本地时间(默认使用 ZoneId.systemDefault()
为本地时区)
支持10位秒时间戳
13位毫秒时间戳
通用时间格式: yyyy-MM-dd HH:mm[:ss][.sss]
ISO-8601 时间格式: yyyy-MM-ddTHH:mm[:ss][.sss][Z|(+|-)HH:mm]
RFC-3339 时间格式: yyyy-MM-dd HH:mm[:ss][.sss][Z|(+|-)HH:mm]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 public abstract class LocalDateTimeUtils { private final static Pattern DATE_TIME_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}.*$" ); private final static ZoneId LOCAL_ZONE_ID = ZoneId.systemDefault(); private final static DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder () .parseCaseInsensitive() .append(ISO_LOCAL_DATE) .optionalStart() .appendLiteral('T' ) .optionalEnd() .optionalStart() .appendLiteral(' ' ) .optionalEnd() .append(ISO_LOCAL_TIME) .toFormatter(); private static boolean isTimestamp (@NonNull String resolver) { for (int i = 0 ; i < resolver.length(); i++) { char ch = resolver.charAt(i); if (!Character.isDigit(ch)) { return false ; } } return resolver.length() == 10 || resolver.length() == 13 ; } private static boolean isOffsetDateTime (@NonNull String resolver) { for (int i = 10 ; i < resolver.length(); i++) { char ch = resolver.charAt(i); if (ch == 'Z' || ch == '+' || ch == '-' ) { return true ; } } return false ; } private static boolean isZonedDateTime (@NonNull String resolver) { return resolver.endsWith("]" ); } @NonNull private static String getISOTimeStr (@NonNull String resolver) { if (resolver.charAt(10 ) == ' ' ) { return resolver.substring(0 , 10 ) + "T" + resolver.substring(11 ); } else { return resolver; } } @Nullable public static LocalDateTime convert (@Nullable String resolver) { if (resolver == null ) { return null ; } if (isTimestamp(resolver)) { Instant instant; if (resolver.length() == 10 ) { instant = Instant.ofEpochSecond(Long.parseLong(resolver)); } else { instant = Instant.ofEpochMilli(Long.parseLong(resolver)); } return LocalDateTime.ofInstant(instant, LOCAL_ZONE_ID); } if (DATE_TIME_PATTERN.matcher(resolver).matches()) { resolver = getISOTimeStr(resolver); boolean isZoned = isZonedDateTime(resolver); boolean isOffset = isOffsetDateTime(resolver); if (isOffset && isZoned) { return ZonedDateTime.parse(resolver, DateTimeFormatter.ISO_ZONED_DATE_TIME) .withZoneSameInstant(LOCAL_ZONE_ID) .toLocalDateTime(); } else if (isOffset) { return ZonedDateTime.parse(resolver, DateTimeFormatter.ISO_OFFSET_DATE_TIME) .withZoneSameInstant(LOCAL_ZONE_ID) .toLocalDateTime(); } else { return LocalDateTime.parse(resolver, LOCAL_DATE_TIME_FORMATTER); } } return null ; } public static void main (String[] args) { System.out.println(convert("2011-12-03T10:15:30+01:00[Europe/Paris]" )); System.out.println(convert("2011-12-03T10:15:30+01:00" )); System.out.println(convert("2011-12-03T10:15:30Z" )); System.out.println(convert("2011-12-03 10:15:30.123Z" )); System.out.println(convert("2011-12-03 10:15:30.123+01:00" )); System.out.println(convert("2011-12-03 10:15:30" )); System.out.println(convert("1322819330" )); System.out.println(convert("1322819330123" )); System.out.println(convert("123" )); } }