SpringMVC中HttpRequestMethodNotSupportedException时返回中文乱码分析解决
2017-12-05 15:48
746 查看
版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com
最近写服务器接口时遇到一个令我辗转不眠的问题,为了统一解决
其实
注:以上几个注解的解释仅限于此用法中,比如
注:在
代码解释:
为了读者方便阅读本文,我们约定一下,抛出
关于
所以我很确定我返回的字符串是
定义注解:
在接口上添加注解:
拦截器拦截:
理论上这段代码是没有问题的,一般情况时,我们拦截登录时就可以这样做,是完全没毛病的。
But,很快我就被啪啪打脸了,O接口抛错前并没有走拦截器,也就是说
介于此我不得不看一下源码来一探究竟了。
很惭愧,要是不看源码我之前确实不知道这个方法,这算是其中一个解决方案,但这不是我的根本目的。
根据上面的分析,我要先找到时哪里修改我在
这里省略了一部分代码,剩下的是主要的代码,也是可能出现问题的代码。为了方便读者结合博客阅读代码,我给代码加上了伪行号。
我先在A处代码(获取
于是我猜测是不是这里设置的
果不其然的印证了我的猜想,客户端果然乱码了,也就是说我返回的数据是
特别声明:在
还是刚才的A处代码(获取
而B接口抛错却没有这个属性,我猜想在抛出
跟着代码来到了
果然是这里添加了接口的属性到
然后跟着
这下就完全印证了我的猜想,望文生义,
结论:
在进入
在扑捉到
给
版权声明:转载必须注明本文转自严振杰的博客: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
相关文章推荐
- org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported解决!
- org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PUT' not supported
- org.springframework.web.HttpRequestMethodNotSupportedException: Request method
- springMVC:org.springframework.web.servlet.PageNotFound.handleHttpRequestMethodNotSupported Request method 'POST' not supported
- JQuery中Ajax的Post提交中文乱码、windows.location.href 中文汉字乱码和springmvc使用原生态HttpServletRequest接收参数中文乱码解决办法
- XMLHttpRequest读取中文网页时返回乱码的解决办法
- springmvc解决 405 request method post not supported(Request method 'POST' not supported)
- 解决MVC 中HttpStatusCodeResult((int)HttpStatusCode.BadRequest, des) 通过StatusDescription 返回中文乱码
- HttpRequestMethodNotSupportedException
- 完美解决由NSURLConnection或者ASIHttpRequest返回的中文乱码问题。
- org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PUT' not supported
- XMLHttpRequest读取中文网页时返回乱码的解决办法
- org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supporte
- HttpRequestMethodNotSupportedException
- org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PUT' not supported
- 解决c# httpClient进行PostAsync()后返回数据中文乱码问题
- XMLHttpRequest对象解决中文乱码问题
- [环境搭建]-Web Api搭建到IIS服务器后PUT请求返回HTTP Error 405.0 - Method Not Allowed 解决方法 转摘:http://blog.csdn.net/qiujuer/article/details/23827531
- 解决SpringMVC的@ResponseBody返回中文乱码
- HTTP method GET is not supported by this URL 问题解决