@月黑风高食肉虎 噗噗虎的技术博客

Spring的后缀匹配问题


很多同学用Spring boot做微服务,然后就遇到了很奇怪的后缀匹配问题,如果你去百度, 可以看到很多很神奇的解决方案,包括在Controller上用正则表达式去匹配,甚至有人重写了PathMatcher, 简直神奇。所以我说,国内百度和CSDN简直就是害人。要找技术文档还是Google。所以,某些人密谋把Google屏蔽了,那就是阻碍国内技术交流发展,违背时代潮流,倒行逆施的行为,这样的恶行总是要还的。

举个栗子,小张同学用Spring boot做了一个登陆API,该API的URL为http://localhost:8080/login, 但是如果你用http://localhost:8080/login.json, http://localhost:8080/login.do ,http://localhost:8080/login.action, http://localhost:8080/login.xml, 甚至http://localhost:8080/login/去访问,发现竟然都可以正常访问, 返回结果根据具体情况不同分为两种,一种返回200说是正常登录成功,另一种则返回406,抛出HttpMediaTypeNotAcceptableException例外。

测试同学无意中发现了这个问题,然后汇报给产品经理,傻逼产品经理出于“对用户负责”的借口要求小张同学修复, 于是小张同学抓耳挠腮,百度了许久,加班到深夜,终于在“百度代码合集”的帮助下,历经千辛万苦修复了这个问题。 但其实他自己也不知道是怎么修复的,反正能work了。

在彻底了解这个问题之前,你首先要了解另外一个概念:Content Negotiation

简单来说,就是Spring会根据用户发过来的accept header所指定的media type来选择能返回相应media type的mapping。 而负责这个选择的,叫做ContentNegotiationManager。然而,这个聪明的manager不仅仅会解析用户端发过来的accept header, 还会解析url上的后缀所表示的类型,并且url上的后缀有更高的优先级。(官方文档)

Spring框架为了方便大家,就算你不指定url的后缀,它也会自动把url上的.*后缀统统mapping到你的controller上。

所以,拿小张的例子来说,当一个用户访问http://localhost:8080/login.xml,框架会去寻找同路径下path=/login, produce=application/xml的Controller, 小张虽然没有给他的controller指定produce,但框架发现他的controller没法返回xml类型的结果,就会抛出HttpMediaTypeNotAcceptableException例外, 并且经由DefaultHandlerExceptionResolver处理为406返回。

另外有趣的是,因为这个例外是在controller之前抛出的,所以controller adivsor虽然可以处理这个例外,但是因为response已经被 DefaultHandlerExceptionResolver处理为406返回,所以advisor并不能改变返回的结果。

后缀匹配可以通过配置关闭,配置方法非常简单,不再赘述,详情请参考官方文档。 我要说的是,就算你关闭了后缀匹配,Content Negotiation仍然会去匹配路径上的后缀,因为这是两个完全不同的方面,如果你不想要406的返回,可以通过关闭favorPathExtension选项实现。

你可能没听懂,我再举个例子:

如果你有一个controller mapping到/person,Spring会自动把这个mapping暗搓搓地mapping到/person.*, 类似/person.xml/persion.json/erson.csv等都会被转发到这个controller的mapping上。至于这个controller能不能返回xml、csv,这就要看你具体实现了。 如果关闭了后缀匹配,那么类似/person.xml/persion.json/erson.csv等就不会被转发到这个controller的mapping上了。理论上你会收到404的返回,除非你另外做了mapping。 但这个时候(关闭了后缀匹配后),如果你有一个controller的mapping是/person.xml,但你却让他返回了json类型(譬如通过produce指定),用户虽然没有发送accept的头, 但这个时候你仍然可能就会收到406的返回。

同样的,如果你有一个controller mapping到/person,并且没有关闭后缀匹配,但是你关闭了favorPathExtension。那么类似/person.xml/persion.json/erson.csv等 仍然会被转发到这个controller的mapping上,并且能够正确返回结果,在用户没有给accept的头的前提下,不考虑返回的media type是啥。

调整这些选项的方法都很简单,就不在此赘述了,详情还是希望同学们花时间仔细阅读官方文档, 如果有兴趣的话,还可以仔细尝试各种配置,或者阅读源代码,而不是去百度或看CSDN的东西,以免被误导。

个人建议,如果你用spring boot做微服务,最好把content negotiation都关了,只用一个FixedContentNegotiationStrategy,并且把suffixPatternMatchtrailingSlashMatch都关闭了。 这样你的API会看起来比较舒爽一点。当然这只是个人建议,具体情况还需要具体讨论。