参考地址:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc
源码地址(持续更新中):https://gitee.com/qinshizhang/spring-mvc-learn
入参的POJO对象如下:(注:以下内容都是Spring MVC基于注解的Java配置,而非基于xml配置文件开发)
public class RequestInfo implements Serializable {
private String username;
private Long userId;
private int userAge;
private double userHeight; // 身高
private Date birthday;
private BigDecimal balance; // 余额
// 为了不占篇幅,省略了get和set方法
}
Controller层代码如下:
@RestController
@RequestMapping(path = "v1/hello/")
public class HelloController {
@PostMapping(path = "showRequestInfo")
public RequestInfo showRequestInfo(@RequestBody RequestInfo requestInfo) {
return requestInfo;
}
}
请求参数如下:
### Send POST request with json body
POST http://localhost:9090/v1/hello/showRequestInfo
Content-Type: application/json
{
"username" : "java",
"userId" : "123455",
"userAge" : "23",
"userHeight" : "170.5",
"birthday" : "1990-12-31 23:59:59",
"balance" : "20.55"
}
请求响应结果如下:(部分)
HTTP状态 415 - 不支持的媒体类型
类型 状态报告
描述 源服务器拒绝服务请求,因为有效负载的格式在目标资源上此方法不支持。
Apache Tomcat/9.0.41
跟踪代码调试,发现抛出异常的地方在如下方法中:
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type)
抛异常的代码块:
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}
检查其注册的HttpMessageConverter列表(this.messageConverters)结果如下:(没有Json格式的MessageConverter)
0 = {ByteArrayHttpMessageConverter@5536}
1 = {StringHttpMessageConverter@5537}
2 = {ResourceHttpMessageConverter@5538}
3 = {ResourceRegionHttpMessageConverter@5539}
4 = {SourceHttpMessageConverter@5540}
5 = {AllEncompassingFormHttpMessageConverter@5541}
6 = {Jaxb2RootElementHttpMessageConverter@5542}
而源码中this.messageConverters这个列表是如何初始化的?默认会初始化那些MessageConverter的实现类?我们使用Spring MVC时,使用@Configuration和@EnableWebMvc注解开启Spring MVC的使用。例如配置如下:
@Configuration
@ComponentScan(basePackages = {"org.jackson.mvc.web"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {WebFilter.class})})
@EnableWebMvc
public class ServletConfig implements WebMvcConfigurer {
}
这两个注解配置后Spring会初始化WebMvcConfigurationSupport对象,该对象提供MVC Java配置主要类。
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
WebMvcConfigurationSupport对象中其他属性和方法暂且不去关注,我们直接关注如下方法:
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters
/**
* Adds a set of default HttpMessageConverter instances to the given list.
* Subclasses can call this method from {@link #configureMessageConverters}.
* @param messageConverters the list to add the default message converters to
*/
protected final void addDefaultHttpMessageConverters(List> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
try {
messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}
从源码中可以看到默认直接添加了
ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
ResourceRegionHttpMessageConverter
SourceHttpMessageConverter
AllEncompassingFormHttpMessageConverter
其他的添加都是有条件的,查看对应条件的初始化方法:
static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}
org.springframework.util.ClassUtils#isPresent方法就是检查一个Class是否存在。有点类是使用如下代码进行检查,只不过Spring封装得更加完善。
@Test
public void testIsPresent() {
System.out.println(classIsPresent("com.fasterxml.jackson.databind.ObjectMapper"));
}
private boolean classIsPresent(String classname) {
try {
Class.forName(classname);
return true;
} catch (ClassNotFoundException e) {
// ignore
}
return false;
}
可以看到Spring默认有支持jackson、Gson、Jsonb的支持。到此,引起异常根源的原因找到了,那现在解决方法就比较多了。
方式一:是引入jackson的核心依赖
com.fasterxml.jackson.core
jackson-core
2.11.2
com.fasterxml.jackson.core
jackson-annotations
2.11.2
com.fasterxml.jackson.core
jackson-databind
2.11.2
方式二(推荐使用):引入jackson-dataformat-cbor和jackson-dataformat-xml依赖,这两个依赖也会包含方式一的三个jackson的核心依赖
com.fasterxml.jackson.dataformat
jackson-dataformat-cbor
2.11.2
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
2.11.2
方式三:引入Gson
com.google.code.gson
gson
2.8.5
方式四:引入Jsonb
io.quarkus
quarkus-jsonb
1.8.1.Final
以上内容是个人在学习Spring MVC基于Java配置(注解配置)的开发工程中遇到该问题的解决流程和自己的一些对于Spring源码的理解,若有问题或者错误的地方,欢迎指正。