問題背景:調用http的post接口返回一個String類型的字符串時中文出現亂碼,定位出問題后在@RequestMapping里加produces注解produces = "application/json;charset=utf-8",再次請求http報406,代碼發現spring拋出異常:org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation。
問題代碼附上:
/** * 執行登陸行為 * * @author wulinfeng * @param request * @param user * @return * @throws ServletException * @throws IOException */ @RequestMapping(value = "/loginAction.html", method = RequestMethod.POST, produces = "application/json;charset=utf-8") public @ResponseBody String loginAction(HttpServletRequest request, HttpServletResponse response, @RequestBody UserBean user) throws ServletException, IOException { // 驗證碼校驗 String validateCode = (String)request.getSession().getAttribute("randomString"); if (StringUtils.isEmpty(validateCode) || !validateCode.equals(user.getValidate().toUpperCase())) { return PropertiesConfigUtil.getProperty("verify_code_error"); } // 用戶名密碼校驗 String result = testPillingService.login(user.getUsername(), user.getPassword()); // 校驗通過,創建token並放入session中;校驗失敗,返回錯誤描述 if ("success".equals(result)) { String tokenId = UUID.randomUUID().toString(); // 登陸成功后是使用cookie還是session來存放tokenId if (IS_COOKIE.equals("1")) { Cookie cookie = new Cookie("tokenId", tokenId); cookie.setMaxAge(3 * 24 * 60 * 60); // 3天過期 response.addCookie(cookie); } else { request.getSession(true).setAttribute("tokenId", tokenId); } if (user.getUsername().toUpperCase().equals("ADMIN")) { return "register"; } } return result; }
問題定位:spring源碼逆向跟蹤,我們從異常拋出的地方回溯到問題發生的地方。
異常所在地:RequestMappingInfoHandlerMapping類235行,標紅;producibleMediaTypes實例化處,218行,標紅
if (patternAndMethodMatches.isEmpty()) { consumableMediaTypes = getConsumableMediaTypes(request, patternMatches); producibleMediaTypes = getProducibleMediaTypes(request, patternMatches); paramConditions = getRequestParams(request, patternMatches); } else { consumableMediaTypes = getConsumableMediaTypes(request, patternAndMethodMatches); producibleMediaTypes = getProducibleMediaTypes(request, patternAndMethodMatches); paramConditions = getRequestParams(request, patternAndMethodMatches); } if (!consumableMediaTypes.isEmpty()) { MediaType contentType = null; if (StringUtils.hasLength(request.getContentType())) { try { contentType = MediaType.parseMediaType(request.getContentType()); } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } } throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes)); } else if (!producibleMediaTypes.isEmpty()) { throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes)); } else if (!CollectionUtils.isEmpty(paramConditions)) { throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap()); } else { return null; }
判斷請求是否能匹配注解produces配置的Content-Type(即“application/json;charset=utf-8”):類258行
Set<MediaType> result = new HashSet<MediaType>();
for (RequestMappingInfo partialMatch : partialMatches) {
if (partialMatch.getProducesCondition().getMatchingCondition(request) == null) {
result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes());
}
}
return result;
}
匹配邏輯:ProducesRequestCondition類185行
public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) { if (isEmpty()) { return this; } Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>(expressions); for (Iterator<ProduceMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) { ProduceMediaTypeExpression expression = iterator.next(); if (!expression.match(request)) { iterator.remove(); } } return (result.isEmpty()) ? null : new ProducesRequestCondition(result, this.contentNegotiationManager); }
匹配請求的Content-Type:AbstractMediaTypeExpression類75行
public final boolean match(HttpServletRequest request) { try { boolean match = matchMediaType(request); return (!this.isNegated ? match : !match); } catch (HttpMediaTypeException ex) { return false; } }
獲取請求匹配的Content-Type:ProducesRequestCondition類300行、236行
protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request); for (MediaType acceptedMediaType : acceptedMediaTypes) { if (getMediaType().isCompatibleWith(acceptedMediaType)) { return true; } } return false; }
private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; }
解析請求Content-Type:ContentNegotiationManager類109行
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { for (ContentNegotiationStrategy strategy : this.strategies) { List<MediaType> mediaTypes = strategy.resolveMediaTypes(request); if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) { continue; } return mediaTypes; } return Collections.emptyList(); }
好了,到底了,最終解析Content-Type的地方在這里,AbstractMappingContentNegotiationStrategy類
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest)); } /** * An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts * an already extracted key. * @since 3.2.16 */ public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, String key) throws HttpMediaTypeNotAcceptableException { if (StringUtils.hasText(key)) { MediaType mediaType = lookupMediaType(key); if (mediaType != null) { handleMatch(key, mediaType); return Collections.singletonList(mediaType); } mediaType = handleNoMatch(webRequest, key); if (mediaType != null) { addMapping(key, mediaType); return Collections.singletonList(mediaType); } } return Collections.emptyList(); }
怎么取到html這個后綴的呢?AbstractMappingContentNegotiationStrategy的子類PathExtensionContentNegotiationStrategy類114行
protected String getMediaTypeKey(NativeWebRequest webRequest) { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request == null) { logger.warn("An HttpServletRequest is required to determine the media type key"); return null; } String path = this.urlPathHelper.getLookupPathForRequest(request); String filename = WebUtils.extractFullFilenameFromUrlPath(path); String extension = StringUtils.getFilenameExtension(filename); return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null; }
回到最頂端,我的@RequestMapping匹配的url是“/loginAction.html”,getMediaTypeKey方法就是在取url后綴,拿到html后作為上面resolveMediaTypeKey方法的里key,然后去調用lookupMediaType方法
protected MediaType lookupMediaType(String extension) { return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH)); }
而這里mediaTypes對象是什么東西呢?它是啟動時加載的,我這里取出來是這樣子的:{xml=application/xml, html=text/html, json=application/json},所以最終解析出來我的請求竟然是text/html,而實際上我從ajax調用http時是設置了Content-Type為application/json;charset=UTF-8的。
看到這里,問題已經出來了,url以html結尾,導致請求頭設置的Content-Type被覆蓋了。那么解決方式相對就簡單了,不以html結尾即可,我這里是直接把/loginAction.html改為/loginAction,重新試一下,406沒有了,中文也出來了。