因爲業務須要,今天公司的JDK
升級到1.8
,容器要求Spring
也須要同時升級到4.0+
,解決完依賴的問題以後,代碼啓動成功,頁面展現正常,可是遇到Ajax
請求的地方就炸了,錯誤碼406
,致使請求失敗,內容沒法正常返回,Debug
發現業務代碼處理邏輯執行正常,懷疑在Spring
對結果的渲染出錯,F12
分析請求能夠發現返回頭的內容內容並非application/json
而是text\html
,不符合@ResponseBody
註解的目的。html
首先進入
DispatcherServlet
類的doDispatch
核心處理web
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
中查看spring
@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)
這個方法,這個也就是咱們須要跟蹤的方法json
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);
是主要的處理邏輯這裏邊包含了請求的處理,和返回值的裝飾spring-mvc
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中bash
因爲咱們是有註解
@ResponseBody
,咱們的處理器就是RequestResponseBodyMethodProcessor
session
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經歷了內容協商器
這個處理器,這個處理器就是 ``mvc
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
來判斷的,app
ServletPathExtensionContentNegotiationStrategy
這個處理器幹掉text/html
方式的返回值處理器第一種方法:async
<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>
複製代碼