Spring MVC
中所有组件集中注册中心组件集中注册中心是什么意思?说白了就是如果使用纯注解的方式启动项目,那注册在xml中的组件就转移到WebMvcConfigurer
类的实现上了。
下面结合源码和实例一一解释,所有的组件的配置参见MvcConfig.java,详细源码见
当前目录下spring-mvc
模块
PathMatchConfigurer
ContentNegotiationStrategy
配置PathMatchConfigurer
这里我贴出了PathMatchConfigurer
配置代码
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// 不支持后置模糊匹配,/abc匹配正常,但/abc.*返回404
configurer.setUseSuffixPatternMatch(true);
// 不支持后面的正斜线,如/abc匹配正常,/abc/返回404
configurer.setUseTrailingSlashMatch(false);
AntPathMatcher pathMatcher = new AntPathMatcher();
// Spring MVC默认是对URL大小写敏感的,这里设置为不敏感
pathMatcher.setCaseSensitive(false);
configurer.setPathMatcher(pathMatcher);
}
如果不满足于spring默认对URL解析方式,如/abc/==/abc,/ABC!=/abc,这里我们可以通过配置PathMatchConfigurer
来完成,当然这里要解释的
是如果这里我们不配置suffixPattern
/trailingSlash
等,spring会有一套自己的默认配置。如spring会在WebMvcConfigurationSupport
中
获取PathMatchConfigurer
中配置的PathMatcher
/URLPathHelper
,如果没有配置,那它会new一个新的对象并存入spring容器中。
其它选项(suffixPattern
/trailingSlash
)默认值配置你会发现PathMatchConfigurer
中并没有设置,他们默认值设置参考RequestMappingHandlerMapping
.
ContentNegotiationStrategy
配置什么是ContentNegotiationStrategy
?它在Spring MVC中起到什么作用呢?
为了更好的看到spring报错信息,这里引入log4j框架。注意:由于spring使用的日志框架是commons-logging
,这里我用的是slf4j
,所以这里
需要导入如下三个包,对这块不了解可以参考java日志组件介绍(common-logging,log4j,slf4j,logback ):
org.slf4j
slf4j-api
org.slf4j
jcl-over-slf4j
org.slf4j
slf4j-log4j12
同时记着把这些包放到tomcat classpath
下,Idea
为artifact
中。
在IndexController中增加一个返回json
格式数据的接口.
在前台页面中增加一个Ajax
请求接口,用于自定义请求。
正常Ajax
请求如下,URL:http://localhost:8080/mvc/grb:
$("#get-random-json").click(function () {
$.ajax({
url: "${pageContext.servletContext.contextPath}/grb",
type: "GET",
success: function (data) {
var name = data.name;
var age = data.age;
$("#random-json-text").text("name:" + name + ", age:" + age);
}
})
})
返回结果如下图:
注意:上面请求的URL没有带任何尾缀。
URL:http://localhost:8080/mvc/grb.xml。
返回结果如下图:
可以看出,spring这个时候已经报错了,那这个URL是如何解析的呢?这里是spring mvc请求流程堆栈:
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:184) // 使用MessageConverter将返回值转换为Accept格式
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:174)
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81) //处理RequestMapping注解对应HandlerMethod返回值
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:113)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:94)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:504)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:502)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1132)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:684)
at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2521)
at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2510)
- locked <0x1710> (a org.apache.tomcat.util.net.AprEndpoint$AprSocketWrapper)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
上述栈顶就是spring mvc对请求结果处理逻辑,下面是处理RequestMapping返回值解析核心代码:
protected 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;
valueType = getReturnValueType(outputValue, returnType);
declaredType = getGenericType(returnType);
}
HttpServletRequest request = inputMessage.getServletRequest();
// 获取HttpServletRequest中请求头中Accept或requestParameter中接收的返回值类型
List requestedMediaTypes = getAcceptableMediaTypes(request);
// 获取当前spring mvc环境中可以解析的所有数据类型
List producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
Set compatibleMediaTypes = new LinkedHashSet();
// 如果请求的Accept数据类型为"application/xml"而spring mvc只能解析"application/json",返回的compatibleMediaTypes就为空
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
// 如果没有注册合适的解析器就会抛出HttpMediaTypeNotAcceptableException异常
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
...
}
上面代码中getAcceptableMediaTypes(request)
实现逻辑如下:
private List getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
// 使用contentNegotiationManager解析传过来的请求
List mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}
上面contentNegotiationManager
调用ContentNegotiationStrategy
来解析http请求,从HttpServletRequest
中获取它想要返回的数据格式,
spring mvc默认的解析策略有如下两个:
ServletPathExtensionContentNegotiationStrategy
:根据尾缀如:.xml或请求中携带format参数指定使用什么格式如:?format=xmlHeaderContentNegotiationStrategy
:根据请求头中的Accept
参数来判断接着往下讲,那返回的数据格式又是怎么定的呢?看getProducibleMediaTypes
方法调用过程:
protected List getProducibleMediaTypes(HttpServletRequest request, Class> valueClass, Type declaredType) {
Set mediaTypes = (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List result = new ArrayList();
// 获取到spring mvc中注册的所有message converter
for (HttpMessageConverter> converter : this.messageConverters) {
// GenericHttpMessageConverter将请求结果转化为目标对象并写入到http response中
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
那这里的核心问题是spring mvc默认的message converter
有哪些呢?由于我们在应用中并没有配置任何转换器,spring mvc怎么知道转换请求需要的数据格式,
这个时候可以看spring mvc默认converter注册地方:WebMvcConfigurationSupport#addDefaultHttpMessageConverters
方法,代码如下:
protected final void addDefaultHttpMessageConverters(List> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter
综上所述:tomcat 406错误体现就是spring mvc中没有xml对应转换器,换句话说没有jackson-dataformat-xml
依赖,这个时候将该依赖添加到pom.xml中,
并添加到tomcat classpath中,redeploy后会发现,接口又返回成功了,前面Ajax
返回结果如下:
那ContentNegotiationConfigurer
能做哪些自定义配置呢?这里我写了一个自定义消息转换器DEMO,源码及解释参考两一篇文章:
Spring MVC实现yaml格式交换