您的位置:首页 > 理论基础 > 计算机网络

SpringMVC中HttpRequestMethodNotSupportedException时返回中文乱码分析解决

2017-12-05 15:48 746 查看
版权声明:转载必须注明本文转自严振杰的博客: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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐