由于业务需要,今天公司的 JDK
升级到 1.8
,容器要求 Spring
也需要同时升级到 4.0+
,解决完依赖的问题之后,代码启动成功,页面展示正常,但是遇到 Ajax
请求的地方就炸了,错误码 406
,导致请求失败,内容无法正常返回, Debug
发现业务代码处理逻辑执行正常,怀疑在 Spring
对结果的渲染出错, F12
分析请求可以发现返回头的内容内容并不是 application/json
而是 text/html
,不符合 @ResponseBody
注解的目的。
首先进入 DispatcherServlet
类的 doDispatch
核心处理
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ..... // 处理请求和修饰结果的方法 /** * ha 变量是类 RequestMappingHandlerAdapter 的实例 * 其继承自AbstractHandlerMethodAdapter,ha.handle方法执行的所在类 * mappedHandler.getHandler() 根据请求地址查询出对应的类.方法 / mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ..... } 复制代码
AbstractHandlerMethodAdapter.handle
方法调用抽象方法 handleInternal
,我们回到子类 RequestMappingHandlerAdapter
中查看
@Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No HttpSession available -> no mutex necessary mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No synchronization on session demanded at all... mav = invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); } else { prepareResponse(response); } } return mav; } 复制代码
可以发现不管怎样都需要走 invokeHandlerMethod(request, response, handlerMethod)
这个方法,这个也就是我们需要跟踪的方法
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); // 这边主要为接下来的处理放入一些参数处理和返回值处理的处理器 ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); ........... ........... if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); if (logger.isDebugEnabled()) { logger.debug("Found concurrent result value [" + result + "]"); } invocableMethod = invocableMethod.wrapConcurrentResult(result); } // 这边是我们的主要的处理方法 invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } 复制代码
invocableMethod.invokeAndHandle(webRequest, mavContainer);
是主要的处理逻辑这里边包含了请求的处理,和返回值的装饰
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 这里边包含了请求参数转换为方法参数,并且反射调用相应的方法也就是我们的 // 业务代码来处理请求,并获取返回值,returnValue就是方法的返回值 // 这次主要是分析对返回值的处理就不做分析了 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); setResponseStatus(webRequest); if (returnValue == null) { if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) { mavContainer.setRequestHandled(true); return; } } else if (StringUtils.hasText(this.responseReason)) { mavContainer.setRequestHandled(true); return; } mavContainer.setRequestHandled(false); try { // 这边是对返回值的处理,返回json还是渲染页面都是这边的,看名字也能看出来 // getReturnValueType(returnValue)方法是分析返回值的包装下 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); } throw ex; } } 复制代码
@Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } 复制代码
从前边注册的返回值处理器中选择正确的处理器并处理请求,debug发现注册的处理器有15中
由于我们是有注解 @ResponseBody
,我们的处理器就是 RequestResponseBodyMethodProcessor
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException { mavContainer.setRequestHandled(true); if (returnValue != null) { // 这边走 writeWithMessageConverters(returnValue, returnType, webRequest); } } 复制代码
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object outputValue; Class<?> valueType; Type declaredType; if (value instanceof CharSequence) { outputValue = value.toString(); valueType = String.class; declaredType = String.class; } else { outputValue = value; // 返回值得类型 我这边是ArrayList valueType = getReturnValueType(outputValue, returnType); declaredType = getGenericType(returnType); } HttpServletRequest request = inputMessage.getServletRequest(); // 请求要求的内容类型,这边3.0和4.0的有较大的区别, //也是导致升级后不可用的原因 List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request); // 可处理返回值类型的处理器可以接受的返回值类型 List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); if (outputValue != null && producibleMediaTypes.isEmpty()) { throw new IllegalArgumentException("No converter found for return value of type: " + valueType); } Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); for (MediaType requestedType : requestedMediaTypes) { for (MediaType producibleType : producibleMediaTypes) { if (requestedType.isCompatibleWith(producibleType)) { compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); } } } // 匹配不到就抛出异常 也是我们的异常的产生源 if (compatibleMediaTypes.isEmpty()) { if (outputValue != null) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } return; } ................... ................... } 复制代码
getAcceptableMediaTypes()
这个获取请求的的 content-type
类型3.0和4.0存在较大的区别,3.0是直接通过请求头来获取的,而4.0经历了 内容协商器
这个处理器,这个处理器就是 ``
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } 复制代码
@Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { /** * strategies 注册了两个处理器 * ServletPathExtensionContentNegotiationStrategy即为内容协商器处理器 * HeaderContentNegotiationStrategy */ 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
类型,比如默认的 json->application/json;xml->application/xml
等等,按理来说均无法匹配,但是它后边有个调用容器 this.servletContext.getMimeType("file." + extension)
方法(extension为htm),竟然返回了 text/html
,然后他就把这个当成自己的常用匹配并且把 htm->text/html
加入了默认的集合,这也是网上一些人说spring会根据后缀名猜返回值类型的出错,其实是 servletContext.getMimeType
的问题 由于对象的处理的 jackson
也就是 MappingJackson2HttpMessageConverter
,他返回支持的类型是 application/json
,这就造成了请求的类型为 text/html
,可处理的类型为 application/json
无法匹配,报错 但是可以发现 HeaderContentNegotiationStrategy
处理类还是根据请求头的 accept
来判断的,
ServletPathExtensionContentNegotiationStrategy text/html
第一种方法:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" /> <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"> <property name="useJaf" value="false"/> <!--干掉路径扩展 也就是ServletPathExtensionContentNegotiationStrategy--> <property name="favorPathExtension" value="false"/> </bean> 复制代码
所有的自定义标签均是 AnnotationDrivenBeanDefinitionParser
类解析,进入 spring-mvc
包的 AnnotationDrivenBeanDefinitionParser
类 进入 parse
方法
@Override public BeanDefinition parse(Element element, ParserContext parserContext) { ... // 构造内容协商 RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext); ... } 复制代码
private RuntimeBeanReference getContentNegotiationManager(Element element, Object source, ParserContext parserContext) { RuntimeBeanReference beanRef; if (element.hasAttribute("content-negotiation-manager")) { String name = element.getAttribute("content-negotiation-manager"); beanRef = new RuntimeBeanReference(name); } else { RootBeanDefinition factoryBeanDef = new RootBeanDefinition(ContentNegotiationManagerFactoryBean.class); factoryBeanDef.setSource(source); factoryBeanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); factoryBeanDef.getPropertyValues().add("mediaTypes", getDefaultMediaTypes()); String name = CONTENT_NEGOTIATION_MANAGER_BEAN_NAME; parserContext.getReaderContext().getRegistry().registerBeanDefinition(name , factoryBeanDef); parserContext.registerComponent(new BeanComponentDefinition(factoryBeanDef, name)); beanRef = new RuntimeBeanReference(name); } return beanRef; } 复制代码
可以发现如果不制定 content-negotiation-manager
那么就会以 ContentNegotiationManagerFactoryBean
类默认属性来构造
Override public void afterPropertiesSet() { List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>(); if (this.favorPathExtension) { PathExtensionContentNegotiationStrategy strategy; if (this.servletContext != null && !isUseJafTurnedOff()) { strategy = new ServletPathExtensionContentNegotiationStrategy( this.servletContext, this.mediaTypes); } else { strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes); } strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions); if (this.useJaf != null) { strategy.setUseJaf(this.useJaf); } strategies.add(strategy); } if (this.favorParameter) { ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes); strategy.setParameterName(this.parameterName); strategies.add(strategy); } if (!this.ignoreAcceptHeader) { strategies.add(new HeaderContentNegotiationStrategy()); } if (this.defaultNegotiationStrategy != null) { strategies.add(this.defaultNegotiationStrategy); } this.contentNegotiationManager = new ContentNegotiationManager(strategies); } 复制代码
在 ContentNegotiationManagerFactoryBean
类的 afterPropertiesSet()
方法可以看到 如果 favorPathExtension
属性为 true
(默认为true)时就会根据是否使用 Jaf
来判断是否构造 ServletPathExtensionContentNegotiationStrategy
或者 PathExtensionContentNegotiationStrategy
(和文件有关),所以我们主动声明 favorPathExtension
为 false
可以禁止注册此处理器
关于内容协商有个很好的文章: blog.csdn.net/u012410733/…
第二种方法:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean> </list> </property> </bean> 复制代码