Spring MVC @RequestParam中Date参数的问题

文摘   科技   2022-09-30 17:18   中国香港  

这是个不使用feign的人应该不会遭遇的问题,但追究起来却挺有意思。

起因

我们系统A有一个API foo,接收两个Date参数,查询返回一个List<FooResult>。
使用feign方式定义。
public class API { @GetMapping("/foo") List<FooResult> foo(@RequestParam Date beginTime, @RequestParam Date endTime);}
在另一个系统B中,如下调用feign API。
就像调用本地方法一样,简洁又优雅。
List<FooResult> list = api.foo(new Date(), new Date());
CI/CD同样非常顺滑,
不费什么功夫就顺利通过了测试。
看起来一切都很美好,
不久这一套代码如预期一样顺利上线到了生产环境。
接下来,立马灾难来了,
从B发出的所有请求都400了。

排查
经抓包分析,发现同样的一个请求,在测试环境和生产上的http请求分别是
GET /foo?beginTime=Thu%20Jan%2001%2008%3A00%3A00%20CST%201970&endTime=Fri%20Sep%2002%2019%3A01%3A51%20CST%202022和GET /foo?beginTime=Thu%20Jan%2001%2007%3A00%3A00%20WIB%201970&endTime=Fri%20Sep%2002%2018%3A01%3A51%20WIB%202022

两者之间唯一的区别在于时区,测试是CST,生产是WIB。
回头检查docker container的配置,
分别是Asia/Shanghai和Asia/Jakarta,所以看起来也没什么问题。
仔细分析spring mvc的代码,
RequestParamMethodArgumentResolver对于没有添加@DateTimeFormat注解的Date类型最终会交给org.springframework.core.convert.support.ObjectToObjectConverter.convert方法处理,并且最终调用的是java.util.Date的Date(String)的构造方法,这个方法早已经@Deprecated
@Deprecatedpublic Date(String s) { this(parse(s));}
而里面的parse方法代码非常复杂难懂。大致就是对Date().toString()生成的如 Sun Sep 04 00:49:51 CST 2022 的字符串进行解析,其中会对星期、月份和时区和预定的一个array做对比。匹配不上就会出exception。
private final static String wtb[] = { "am", "pm", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", "gmt", "ut", "utc", "est", "edt", "cst", "cdt", "mst", "mdt", "pst", "pdt"};
可以看出,这里对时区的支持是十分有限的。公认的,java早期版本Date相关类的实现非常糟糕,所以大部分方法都被deprecated了。但是RequestParamMethodArgumentResolver依然没有对Date类型做特殊处理,而是简单地用反射方式统一使用Constructor.newInstance(String)。
而feign这边,则是简单地调用了toString()方法。然后这段代码不出所料的,在timezone是Asia/Shanghai时,因为刚好timezone在这个列表中,所以运行从不报错。到了Asia/Jakarta,就不出意料400了。

总结
  • 如果有的选,避免调用@Deprecated的API,积极拥抱最新的实现。

  • 请保持测试环境与生产环境同样的时区设置,否则出了时区问题会非常不好排查。

  • java.util.Date里有太多糟糕的实现,我们可以看到大部分方法都被@Deprecated 了,尤其是 Date.toString()和new Date(String),他们都不是使用的ISO格式的日期字符串,更坑的是他们之间不是一对可逆方法。个人偏好在request parameter中使用long类型而不是Date类型。如果要使用Date类型,请一定一定要加上@DateTimeFormat 注解,并且是DateTimeFormat.ISO.DATE_TIME。

  • 如果有系统是因为设置的timezone是Asia/Shanghai而侥幸没有报错的,也不要高兴,因为查询的结果很有可能是有问题的。具体可以试着运行下面的代码,你会发现第4行和第5行的结果不一样了。原因可以自己找一下,反正是Date类的又一个大坑。

Date d0 = new Date();Date d1 = new Date(d0.toString());SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());System.out.println(sdf.format(d0));System.out.println(sdf.format(d1));
另外:领创集团知乎官方账号已开通!
每周一个技术干货分享!
期待你的关注




关于领创集团

(Advance Intelligence Group)
领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。
领创集团Advance Group
领创集团是亚太地区AI技术驱动的科技集团。
 最新文章