SpringMVC中HttpRequestMethodNotSupportedException时返回中文乱码分析解决
Posted 严振杰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringMVC中HttpRequestMethodNotSupportedException时返回中文乱码分析解决相关的知识,希望对你有一定的参考价值。
版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com
最近写服务器接口时遇到一个令我辗转不眠的问题,为了统一解决RestController
的全局异常,包括自定义异常和SpringMVC
底层抛出来的异常,我用了@ControllerAdvice
来拦截异常,出现的问题是:拦截到自定义异常和MissingServletRequestParameterException
返回数据没有问题,但是拦截HttpRequestMethodNotSupportedException
时返回带中文的JSON
时,客户端接受到时乱码。
其实SpringMVC
中处理我的需求的方案不少,在我的方案中遇到的这个问题很怪异,当然也可以秒秒钟用很多种办法解决,但是了解我的人就知道,在技术上这种情况我时绝对不能忍的,我本着打破砂锅问到底的精神,还是跟踪了一下源码。
我的代码
轻描淡写的解释一下,在一个类上加上@ControllerAdvice
注解表示增强控制器,再加上@RestController
注解表示方法返回的内容是ReponseBody
,在这个类内部的方法上加上@ExceptionHandler
注解表示扑捉什么异常。
注:以上几个注解的解释仅限于此用法中,比如
@RestController
在控制器中还有别的作用和释义,@ControllerAdvice
注解的类内部还可以用@InitBinder
和@ModelAttribute
做其他事情等。
@ControllerAdvice
@RestController
public class AppControlAdvice
@ExceptionHandler(BaseException.class)
public String missingParamHandle(request, response, BaseException e)
return handleException(request, response, e);
@ExceptionHandler(MissingServletRequestParameterException.class)
public String missingParamHandle(request, response, MissingServletRequestParameterException e)
return handleException(request, response, new ClientException("缺少必要的参数"));
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public String methodNotSupportHandle(request, response, HttpRequestMethodNotSupportedException e)
return handleException(request, response, new ClientException("不支持的请求方法"));
public String handleException(request, response, Throwable ex)
response.setHeader("Content-Type", "application/json;charset=UTF-8");
...; // 组织数据并返回统一的JSON。
注:在
@ExceptionHandler
中给Response
设置在@RequestMapping
中指定的属性是无效的,不要问为什么,看完文章你就知道了。
代码解释:
BaseException
:自定义异常基类。MissingServletRequestParameterException
:Query中缺少必要的参数。HttpRequestMethodNotSupportedException
:客户端使用的请求方法不被该接口支持。handleException()
:统一处理错误,并根据错误类型返回JSON字符串。
为了读者方便阅读本文,我们约定一下,抛出MissingServletRequestParameterException
异常时称为X接口抛错,抛出HttpRequestMethodNotSupportedException
时称为O接口抛错。
关于@ControllerAdvice
和@ExceptionHandler
的用法不再赘述,Google可以搜出来一大票详解的文章,这里出现的问题是扑捉到BaseException
和X接口抛错时返回JSON
后中文不会乱码,O接口抛错时返回的中文居然乱码了,上面可以清晰的看到我给Response
设置了Content-Type
为utf-8
,在web.xml
中也指定了编码过滤器使用utf-8
:
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
所以我很确定我返回的字符串是utf-8
编码的,于是我用浏览器打开了这个接口,但是我看到浏览器接受到的Content-Type
却是text/html;charset=iso-8859-1
:
问题分析
既然我们设置了响应头的Content-Type
为application/json;charset=utf-8
,但是返回给客户端时居然变了,很显然这是SpringMVC
内部帮我们改了,于是我想到在拦截器内拦截所有接口的请求方法,判断客户端的请求方法是否被这个接口支持,如果不支持我直接抛出一个自定义异常,这样不就解决了吗?
定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SupportMethod
RequestMethod[] value();
在接口上添加注解:
@RestController
@RequestMapping("/xxx")
public class XXXController
@SupportMethod(RequestMethod.POST);
@RequestMapping(
value = "/add",
method = RequestMethod.POST)
public String add()
...;
拦截器拦截:
public class AppInterceptor implements HandlerInterceptor
@Override
public boolean preHandle(request, response, Object handler) throws Exception
if (handler != null && handler instanceof HandlerMethod)
HandlerMethod handlerMethod = (HandlerMethod) handler;
SupportMethod supportMethod = handlerMethod.getMethodAnnotation(SupportMethod.class);
if(method != null)
// 拿到客户端请求方法:
String inMethod = RequestMethodrequest.getMethod();
RequestMethod clientMethod = RequestMethod.valueOf(inMethod);
// 接口指定的请求方法:
List<RequestMethod> methodList = Arrays.asList(supportMethod.value());
// 判断是否支持:
if(!methodList.contains(clientMethod))
throw new ClientException("不支持的请求方法");
return true;
理论上这段代码是没有问题的,一般情况时,我们拦截登录时就可以这样做,是完全没毛病的。
But,很快我就被啪啪打脸了,O接口抛错前并没有走拦截器,也就是说DispatcherServlet
是先判断的请求方法,然后才走到拦截器,于是我放弃这个方案。
介于此我不得不看一下源码来一探究竟了。
排查原因
打开DispatcherServlet
后发现内部提供了一个方法,只要我们重写这个方法就可以统一处理异常:
protected ModelAndView processHandlerException(request, response, Object handler, Exception ex);
很惭愧,要是不看源码我之前确实不知道这个方法,这算是其中一个解决方案,但这不是我的根本目的。
根据上面的分析,我要先找到时哪里修改我在Response
中设置的Content-Type
请求,从DispatcherServlet
分发请求的方法开始debug
走起:
protected void doDispatch(request, response) throws Exception
try
Exception exception = null;
...;
A. mappedHandler = getHandler(request);
...;
B. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
C. mv = ha.handle(request, response, mappedHandler.getHandler());
...;
D. applyDefaultViewName(request, mv);
E. mappedHandler.applyPostHandle(request, response, mv);
catch (Exception ex)
exception = ex;
F. processDispatchResult(..., exception);
这里省略了一部分代码,剩下的是主要的代码,也是可能出现问题的代码。为了方便读者结合博客阅读代码,我给代码加上了伪行号。
我先在A处代码(获取Request
的Handler
)打上了断点,分别扑捉X接口抛错和O接口抛错并观察两个异常的时候,代码都是如何走的。实验结果发现:X接口抛错时在执行C(执行Handler
)处代码时抛出异常,O接口抛错时在执行A处代码(获取Request
的Handler
)时抛出异常,那么唯一的区别就是X接口抛错时执行了B处代码(获取Handler
的属性)处代码,这里望文生义,getHandlerAdapter
的意思不就是拿到Handler
的适配器,也就是Controller
中方法的@RequestMapping
等相关属性了。
于是我猜测是不是这里设置的Response
的Content-Type
是无效的,还是会被Handler
的@RequestMapping
覆盖了。然后我又做了个测试,把A接口抛错时返回数据的Conent-Type
中的编码改成iso-8859-1
看看效果:
@RequestMapping(
value = "/xxx",
method = RequestMethod.GET,
produces = "application/json;charset=iso-8859-1")
果不其然的印证了我的猜想,客户端果然乱码了,也就是说我返回的数据是utf-8
编码的,但是我却告诉客户端我的数据是ios-8859-1
的,也就时说在文章开头的这个方法中设置的Content-Type
是无效的:
public String handleException(request, response, Throwable ex)
response.setHeader("Content-Type", "application/json;charset=UTF-8");
...; // 组织数据并返回统一的JSON。
特别声明:在@ExceptionHandler
中给Response
设置在@RequestMapping
中指定的属性是无效的。
原因印证
如果上面的测试不够具有说服力,那么下面我带读者们来跟踪下代码。
还是刚才的A处代码(获取Request
的Handler
)断点,分别扑捉A接口抛错和B接口抛错并观察两个异常的时候,各个对象有什么不同,因为必定是某个对象的某个属性影响了结果。实验结果发现,A接口抛错在走完A处代码(获取Request
的Handler
)后,Request
的Attribute
多了如下的属性:
org.springframework.web.servlet.HandlerMapping.producibleMediaTypes: application/json;charset=utf-8
而B接口抛错却没有这个属性,我猜想在抛出HttpRequestMethodNotSupportedException
异常时Request
少了上述属性,这个属性就是为了保存接口@RequestMapping
注解的produces
属性。那我们一步步跟踪一下我们猜的对不对:
第一步,哪里添加的属性
先从DispatcherServlet#getHandler()
下手(大概是940行代码处),:
mappedHandler = getHandler(processedRequest);
跟着代码来到了RequestMappingInfoHandlerMapping#handleMatch()
(大概是117行左右),发现了玄机:
if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty())
Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
果然是这里添加了接口的属性到Request
的属性中,但是这还是不能够证明它影响了返回结果啊。
第二步,哪里验证了属性
这里要麻烦读者朋友回到上面标有伪行号ABCDE
那里看看代码,其中有一行F:processDispatchResult(..., exception);
,这里是处理了当前请求的结果,无论异常还是正常。
然后跟着processDispatchResult()
来到了AbstractMessageConverterMethodProcessor#getProducibleMediaTypes()
(大概是304行):
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes))
return new ArrayList<MediaType>(mediaTypes);
这下就完全印证了我的猜想,望文生义,getProducibleMediaTypes()
就是获取返回ResponseBody
的produces
。
结论:@ControllerAdvice
和@ExceptionHandler
配合使用时,SpringMvc
覆盖了我们设置的Response
的Content-Type
。
解决方案
- 给SpringMVC提交PR,优化这个问题。
- 在进入
Controller
的方法之前SpringMVC
抛出的HttpRequestMethodNotSupportedException
异常处理返回数据时不要使用中文。 - 在扑捉到
HttpRequestMethodNotSupportedException
异常时自己给Request
添加HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
属性,代码如下:
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public String methodNotSupportHandle(request, response, HttpRequestMethodNotSupportedException e)
Set<MediaType> mediaTypeSet = new HashSet<>();
MediaType mediaType = new MediaType("application", "json", Charset.forName("utf-8"));
mediaTypeSet.add(mediaType);
request.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypeSet);
return handleException(request, response, new ClientException("不支持的请求方法"));
给SpringMVC
提交PR是我后面要做的事,现在工作生活都比较忙,没时间看的更深入更理解,所以不敢擅自提交PR。
版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com
以上是关于SpringMVC中HttpRequestMethodNotSupportedException时返回中文乱码分析解决的主要内容,如果未能解决你的问题,请参考以下文章
SpringMVC在SpringMVC中`/`和`/*`的区别