spring mvc加了@produces注解后報406


  問題背景:調用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行

 private Set<MediaType> getProducibleMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
    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沒有了,中文也出來了。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM