Spring 4.2.4.RELEASE MVC 学习笔记 - 8 - fasterxml jackson
本小结学习一些spring mvc框架整合fasterxml jackson工具,实现Restfull模式api实现对json、xml的支持。
PS:学习本小节之前请大家先看一下Spring 4.2.4.RELEASE MVC 学习笔记 - 8.1小结,需要对项目做一下改动,以免对下面的学习有混淆视听。
URL后缀 | 请求Mediatype映射 | 学习目标 |
---|---|---|
*.json | application/json; charset=UTF-8 | Controller处理方法返回对象解析成JSON |
*.xml | application/json; charset=UTF-8 | Controller处理方法返回对象解析成XML文件 |
application/pdf; charset=UTF-8 | Controller处理方法返回对象解析成pdf文件 | |
*.xls | application/xls; charset=UTF-8 | Controller处理方法返回对象解析成xls文件 |
*.xlsx | application/xlsx; charset=UTF-8 | Controller处理方法返回对象解析成xlsx文件 |
查看HttpMessageConverter接口以及派生类
在Spring mvc中对Controller方法返回结果进行集中解析是通过最上层HttpMessageConverter接口来实现的,也就是我们需要看一下Spring mvc体系中一共有多少个类实现了HttpMessageConverter接口,然后从中可以确认Spring mvc框架中都支持了那些数据解析器。
在本次学戏中我们只关注返回数据json、xml、pdf、xls和xlsx支持的解析。
如下图,我列出了Spring mvc 4.2.4版本中的所有的派生类(呵呵,为了截图特意换了个大屏幕显示器。其中大家有没有注意到一个特殊的GSONHttpMessageConverte,这就是之前我们自定义的解析器)。
- GenericHttpMessageConverter
- GsonHttpMessageConverter
- MappingJackson2HttpMessageConverter
- MappingJackson2XmlHttpMessageConverter
- AbstractHttpMessageConverter
- StringHttpMessageConverter
- GSONHttpMessageConverte(自定义的)
- AbstractJackson2HttpMessageConverter
- MappingJackson2HttpMessageConverter
- MappingJackson2XmlHttpMessageConverter
- AbstractWireFeedHttpMessageConverter
- AtomFeedHttpMessageConverter
- RssChannelHttpMessageConverter
如此之多的都是spring mvc在启动的时候需要实例化逐一加载的,在spring mvc源码中是通过AbstractMessageConverterMethodProcessor类来管理处理这些解析器的。下面我只截取部分该类的代码简单的说明一下,感兴趣的同学自己渗入研究吧。
准备工作
修改Log4j日志配置文件
修改Log4j日志配置文件,将spring框架的日志级别调整为Debug级别,以便我们观察日志信息。
File:/framework_spring/src/main/resources/log4j.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<!-- 默认打印所有debug级别的日志 -->
<root>
<priority value="DEBUG" />
<appender-ref ref="stdout" />
<appender-ref ref="framework_spring" />
</root>
<!-- 配置一个文件日志,框架以及第三方jar的日志集中打印到此日志文件中=。 -->
<!-- 指定以org.springframework包开头的类中打印DEBUG级别的日志。 -->
<logger name="org.springframework" additivity="false">
<priority value ="DEBUG"/>
<appender-ref ref="framework_spring_console" />
</logger>
<!-- 配置一个控制台appender stdout,将日志信息输入到控制台中。 -->
<appender name="stdout" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="@%-5p|%d|%l|%t %n Info:%m %n%n" />
</layout>
<!--过滤器设置输出的级别-->
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="levelMin" value="DEBUG" />
<param name="AcceptOnMatch" value="true" />
</filter>
</appender>
<!-- 配置一个文件日志,框架以及第三方jar的日志集中打印到此日志文件中=。 -->
<appender name="framework_spring" class="org.apache.log4j.RollingFileAppender">
<!-- 设置日志输出文件名 -->
<param name="File" value="/logs/framework_spring.log" />
<!-- 设置是否在重新启动服务时,在原有日志的基础添加新日志 -->
<param name="Append" value="false" />
<!--
MaxBackupIndex备份日志文件的个数(默认是1个)
MaxFileSize表示日志文件允许的最大字节数(默认是10M)
<param name="MaxBackupIndex" value="1" />
<param name="MaxFileSize" value="1048576" />
-->
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="@%-5p|%d|%l|%t %n Info:%m %n%n" />
</layout>
</appender>
<!-- 配置一个文件日志,框架以及第三方jar的日志集中打印到此日志文件中=。 -->
<appender name="framework_spring_console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="@%-5p|%d|%l|%t %n Info:%m %n%n" />
</layout>
<!--过滤器设置输出的级别-->
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="levelMin" value="DEBUG" />
<param name="AcceptOnMatch" value="true" />
</filter>
</appender>
</log4j:configuration>
创建RestFullController控制器
Class:cn.vfire.framework.spring.mvc.controller.RestFullController
package cn.vfire.framework.spring.mvc.controller;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import cn.vfire.framework.spring.mvc.mode.BookMode;
import cn.vfire.framework.spring.mvc.view.Result;
import cn.vfire.framework.spring.mvc.view.XModelAndView;
@RequestMapping("/restfull")
@RestController
public class RestFullController {
@RequestMapping(path = "/book", method = RequestMethod.GET)
@ResponseBody
public Result book(String username) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2016-02-01");
XModelAndView modeView = new XModelAndView();
BookMode book = new BookMode();
{
book.setName("迪士尼双语经典电影故事:疯狂动物城");
book.setPublisher("中央广播电视大学出版社");
book.setNumberOfPages(96);
book.setOpeningtime(date);
book.setRevision(1);
}
modeView.addObject("book", book);
return modeView.toResult();
}
}
创建BookMode模型
别问我@Getter @Setter怎么来的,前面的章节讲过了。
Class:cn.vfire.framework.spring.mvc.mode.BookMode
package cn.vfire.framework.spring.mvc.mode;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
public class BookMode {
/** 书名 */
@Getter
@Setter
private String name;
/** 出版社 */
@Getter
@Setter
private String publisher;
/** 印刷次数 */
@Getter
@Setter
private int revision;
/** 出版时间 */
@Getter
@Setter
private Date openingtime;
/** 页数 */
@Getter
@Setter
private int numberOfPages;
}
测试请求
URL后缀 | 请求Mediatype映射 | 响应Mediatype |
---|---|---|
*.json | application/json; charset=UTF-8 | application/json; charset=UTF-8 |
*.xml | application/json; charset=UTF-8 | application/json; charset=UTF-8 |
application/pdf; charset=UTF-8 | application/pdf; charset=UTF-8 | |
*.xls | application/xls; charset=UTF-8 | application/xls; charset=UTF-8 |
*.xlsx | application/xlsx; charset=UTF-8 | application/xlsx; charset=UTF-8 |
测试URL观察日志
下面我们就逐个的测试URL地址,然后去观察每一个的后台打印日志的情况,主要看spring的日志。
/restfull/book
测试地址 |
---|
http://127.0.0.1:8000/restfull/book |
@DEBUG|2016-03-10 11:23:25,592|org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:861)|http-bio-8000-exec-5
Info:DispatcherServlet with name 'spring' processing GET request for [/restfull/book]
"**Spring MVC 识别出一个Get方式的请求,请求URI为/restfull/book。**"
@DEBUG|2016-03-10 11:23:25,592|org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:318)|http-bio-8000-exec-5
Info:Looking up handler method for path /restfull/book
"**Spring MVC 解析URI映射/restfull/book** "
@DEBUG|2016-03-10 11:23:25,592|org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:325)|http-bio-8000-exec-5
Info:Returning handler method [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]
"**Spring MVC通过映射解析出此请求转交给RestFullController.book(java.lang.String)方法处理**"
@DEBUG|2016-03-10 11:23:25,593|org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:251)|http-bio-8000-exec-5
Info:Returning cached instance of singleton bean 'restFullController'
"**Spring MVC从BeanFactory中获取RestFullController的Bean对象restFullController**"
@DEBUG|2016-03-10 11:23:25,593|org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:947)|http-bio-8000-exec-5
Info:Last-Modified value for [/restfull/book] is: -1
"**Spring MVC解析RestFullController.book(String)方法返回对象的映射视图但是没有找到**"
@DEBUG|2016-03-10 11:23:25,601|org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:225)|http-bio-8000-exec-5
Info:Written [cn.vfire.framework.spring.mvc.view.Result@364ba02d] as "application/xhtml+xml" using [org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@30bc2a56]
"**Spring MVC遍历所有数据解析器解析返回对象,触发了MappingJackson2XmlHttpMessageConverter解析对象,并将返回结果MediaType处理成application/xhtml+xml**"
@DEBUG|2016-03-10 11:23:25,602|org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1034)|http-bio-8000-exec-5
Info:Null ModelAndView returned to DispatcherServlet with name 'spring': assuming HandlerAdapter completed request handling
@DEBUG|2016-03-10 11:23:25,602|org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:999)|http-bio-8000-exec-5
Info:Successfully completed request
"**Spring MVC处理请求到应答成功**"
/restfull/book.json
测试地址 |
---|
http://127.0.0.1:8000/restfull/book.json |
@DEBUG|2016-03-10 11:52:58,861|org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:861)|http-bio-8000-exec-10
Info:DispatcherServlet with name 'spring' processing GET request for [/restfull/book.json]
"**Spring MVC 识别出一个Get方式的请求,请求URI为/restfull/book.json。**"
@DEBUG|2016-03-10 11:52:58,861|org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:318)|http-bio-8000-exec-10
Info:Looking up handler method for path /restfull/book.json
"**Spring MVC 解析URI映射/restfull/book.json** "
@DEBUG|2016-03-10 11:52:58,862|org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:325)|http-bio-8000-exec-10
Info:Returning handler method [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]
"**Spring MVC通过映射解析出此请求转交给Result RestFullController.book(java.lang.String)方法处理**"
@DEBUG|2016-03-10 11:52:58,862|org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:251)|http-bio-8000-exec-10
Info:Returning cached instance of singleton bean 'restFullController'
"**Spring MVC从BeanFactory中获取RestFullController的Bean对象restFullController**"
@DEBUG|2016-03-10 11:52:58,862|org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:947)|http-bio-8000-exec-10
Info:Last-Modified value for [/restfull/book.json] is: -1
"**Spring MVC解析RestFullController.book(String)方法返回对象的映射视图但是没有找到**"
@DEBUG|2016-03-10 11:52:58,868|org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:225)|http-bio-8000-exec-10
Info:Written [cn.vfire.framework.spring.mvc.view.Result@2f8b20d5] as "application/json;charset=UTF-8" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@54319b14]
"**Spring MVC在数据解析器调度方法AbstractMessageConverterMethodProcessor.writeWithMessageConverters中解析出该请求MediaType类型为application/json;charset=UTF-8,所以默认调度MappingJackson2HttpMessageConverter解析器处理返回数据对象。**"
@DEBUG|2016-03-10 11:52:58,869|org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1034)|http-bio-8000-exec-10
Info:Null ModelAndView returned to DispatcherServlet with name 'spring': assuming HandlerAdapter completed request handling
@DEBUG|2016-03-10 11:52:58,869|org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:999)|http-bio-8000-exec-10
Info:Successfully completed request
"**Spring MVC处理请求到应答成功**"
/restfull/book.xml
测试地址 |
---|
http://127.0.0.1:8000/restfull/book.xml |
"**以上的日志意思基本雷同,不再列出。**"
@DEBUG|2016-03-10 12:01:43,884|org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:225)|http-bio-8000-exec-6
Info:Written [cn.vfire.framework.spring.mvc.view.Result@6bd330ff] as "application/xml;charset=UTF-8" using [org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@30bc2a56]
"**Spring MVC在数据解析器调度方法AbstractMessageConverterMethodProcessor.writeWithMessageConverters中解析出该请求MediaType类型为'application/xml;charset=UTF-8',所以默认调度MappingJackson2XmlHttpMessageConverter解析器处理返回数据对象。**"
/restfull/book.pdf
测试地址 |
---|
http://127.0.0.1:8000/restfull/book.pdf |
"**以上的日志意思基本雷同,不再列出。**"
@DEBUG|2016-03-10 12:04:56,915|org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:133)|http-bio-8000-exec-2
Info:Resolving exception from handler [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
"**Spring MVC在请求和应答结果任何地方都没有识别出该请求映射MediaType,所以找不到任何数据解析器来处理返回数据对象,只能抛出一个HttpMediaTypeNotAcceptableException异常。**"
"**第一种方式去解析,无结果。**"
@DEBUG|2016-03-10 12:04:56,916|org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:133)|http-bio-8000-exec-2
Info:Resolving exception from handler [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
"**第二种方式去解析,无结果。**"
@DEBUG|2016-03-10 12:04:56,916|org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:133)|http-bio-8000-exec-2
Info:Resolving exception from handler [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
"**第三种方式去解析,无结果。**"
@DEBUG|2016-03-10 12:04:56,916|org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1034)|http-bio-8000-exec-2
Info:Null ModelAndView returned to DispatcherServlet with name 'spring': assuming HandlerAdapter completed request handling
@DEBUG|2016-03-10 12:04:56,916|org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:999)|http-bio-8000-exec-2
Info:Successfully completed request
"**Spring MVC处理请求到应答成功**"
"**但是由于Spring MVC在处理返回结果时,既没有找到对应的视图,也没有找到对应的数据解析器,所以页面显示返回406错误。**"
/restfull/book.xls
测试地址 |
---|
http://127.0.0.1:8000/restfull/book.xls |
http://127.0.0.1:8000/restfull/book.xlsx |
"**与/restfull/book.pdf日志解读雷同**"
@DEBUG|2016-03-10 12:13:09,819|org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:133)|http-bio-8000-exec-4
Info:Resolving exception from handler [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
@DEBUG|2016-03-10 12:13:09,819|org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:133)|http-bio-8000-exec-4
Info:Resolving exception from handler [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
@DEBUG|2016-03-10 12:13:09,820|org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:133)|http-bio-8000-exec-4
Info:Resolving exception from handler [public cn.vfire.framework.spring.mvc.view.Result cn.vfire.framework.spring.mvc.controller.RestFullController.book(java.lang.String) throws java.text.ParseException]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
@DEBUG|2016-03-10 12:13:09,820|org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1034)|http-bio-8000-exec-4
Info:Null ModelAndView returned to DispatcherServlet with name 'spring': assuming HandlerAdapter completed request handling
@DEBUG|2016-03-10 12:13:09,821|org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:999)|http-bio-8000-exec-4
Info:Successfully completed request
Spring对控制器方法返回结果的处理
通过上面的测试和日志分析,Spring MVC在处理控制器方法返回结果数据对象的时候,优先检索是否有对应的视图,如果有则直接交给视图处理,并返回视图页面,请求结束,如果没有则检索是否存在对应的数据解析器,有过有则调用数据解析器处理控制方法的返回结果数据对象,如果没有则直接返回HTTP406错误。
测试地址 | 结果 |
---|---|
http://127.0.0.1:8000/restfull/book | 将Result数据对象处理成xml报文返回 |
http://127.0.0.1:8000/restfull/book.json | 将Result数据对象处理成json格式报文返回 |
http://127.0.0.1:8000/restfull/book.xml | 将Result数据对象处理成xml报文返回 |
http://127.0.0.1:8000/restfull/book.pdf | 请求相应正常,但没数据解析器处理结果,返回406错误 |
http://127.0.0.1:8000/restfull/book.xls | 请求相应正常,但没数据解析器处理结果,返回406错误 |
http://127.0.0.1:8000/restfull/book.xlsx | 请求相应正常,但没数据解析器处理结果,返回406错误 |
在上文的日志分析中,我们看到比较关键的一个日志记录下面两个方法。
方法 | 触发 | 说明 |
---|---|---|
类:AbstractMessageConverterMethodProcessor 方法:writeWithMessageConverters() | restfull/book /restfull/book.json /restfull/book.xml | 当Spring MVC可以从request、response中解析出MediaType的类型时候,会调用该方法,在该方法内部通过MediaType来调度对应的数据解析器解析返回数据对象后,将其结果返回请求端。 |
类:AbstractHandlerExceptionResolver 方法:resolveException() | /restfull/book.pdf /restfull/book.xls /restfull/book.xlsx | 当Spring MVC从request、response中没有解析出MediaType时,则会调用该方法,抛出HttpMediaTypeNotAcceptableException异常。 |
下面是Spring MVC 4.2.4的源代码,为什么要列出来呢,起始此处的渗入学习,若想从spring源码上了解的话,这两个方法是研究一个spring请求到应答的全过程的非常好的切入点。感兴趣的同学,大家可以把Debug模式的断点打在这两个方法的内部,然后一步一步跟踪spring的处理流程。不过这里我肯定不会这样做了,又不是直播。
AbstractMessageConverterMethodProcessor.writeWithMessageConverters(….)
/**
* Writes the given return value to the given web request. Delegates to
* {@link #writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)}
*/
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
AbstractHandlerExceptionResolver.resolveException(…)
/**
* Check whether this resolver is supposed to apply (i.e. if the supplied handler
* matches any of the configured {@linkplain #setMappedHandlers handlers} or
* {@linkplain #setMappedHandlerClasses handler classes}), and then delegate
* to the {@link #doResolveException} template method.
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
if (shouldApplyTo(request, handler)) {
// Log exception, both at debug log level and at warn level, if desired.
if (this.logger.isDebugEnabled()) {
this.logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
}
logException(ex, request);
prepareResponse(ex, response);
return doResolveException(request, response, handler, ex);
}
else {
return null;
}
}
AbstractMessageConverterMethodProcessor.writeWithMessageConverters(….)
因为该类是一个抽象类,我习惯性的看了一下该方法是否有被重写过,如果有阅读起来会相对麻烦点。还好该方法没有被任何子类重写,至少spring mvc里面是没有的。
下面我们重点看一下writeWithMessageConverters()方法的源码。
getAcceptableMediaTypes()方法,从请求中解析出请求的MediaType类型,Spring支持三种方式,分别为Request Headers中解析,支持从URI的后缀中解析(/restfull/book.json解析为application/json),支持URL入参指定的方式解析(/restfull/book?format=json解析为application/json),这三种方式的优先级我这里不做特殊说明了,大家自己测试理解。
所以在我们测试几个URL中,带有后缀的通过此方法可以顺利的解析出请求的MediaType类型。
URL后缀 | 请求Mediatype映射 |
---|---|
*.json | application/json; charset=UTF-8 |
*.xml | application/json; charset=UTF-8 |
application/pdf; charset=UTF-8 | |
*.xls | application/xls; charset=UTF-8 |
*.xlsx | application/xlsx; charset=UTF-8 |
getProducibleMediaTypes()方法,自然是从响应的层级上解析返回数据MediaType,因为Controller的方法是接收请求、处理请求、响应结果,所以在Controller的方法中我们处理玩请求后根据返回结果的需求回特殊指定返回数据MediaType类型(也就是指定Response Headers的Content-Type属性值)。
这是我们回头看看该请求的Controller处理方法。
cn.vfire.framework.spring.mvc.controller.RestFullController
@RequestMapping(path = "/book", method = RequestMethod.GET)
@ResponseBody
public Result book(String username) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2016-02-01");
XModelAndView modeView = new XModelAndView();
BookMode book = new BookMode();
{
book.setName("迪士尼双语经典电影故事:疯狂动物城");
book.setPublisher("中央广播电视大学出版社");
book.setNumberOfPages(96);
book.setOpeningtime(date);
book.setRevision(1);
}
modeView.addObject("book", book);
return modeView.toResult();
}
cn.vfire.framework.spring.mvc.controller.UserController
@RequestMapping(path = "/userAdd.api", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
@ResponseBody
public String userAdd(UserMode user) {
XModelAndView modeView = new XModelAndView();
modeView.addObject("user", user);
System.out.println(String.format("添加用户%s完成", user.getName()));
return modeView.toJson();
}
在上面两个Controller的方法中,我们看一下@RequestMapping注解里面有一个属性produces,这个属性值起始就是设置Response Headers的Content-Type属性,也就是如果想主动设置返回数据对象MediaType类型是可以通过此注解来完成,这样在前面的getProducibleMediaTypes()方法中可以获取到该请求指定的MediaType了。
但是在测试用的RestFullController.book(…)方法上我并没有主动设置produces的值,这个时候getProducibleMediaTypes()方法会遍历所有实现了HttpMessageConverter接口的Bean对象,从满足条件的Bean中提取MediaType返回。
我们看一下getProducibleMediaTypes()代码实现。
/**
* Returns the media types that can be produced:
* <ul>
* <li>The producible media types specified in the request mappings, or
* <li>Media types of configured converters that can write the specific return value, or
* <li>{@link MediaType#ALL}
* </ul>
* @since 4.2
*/
@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass, Type returnValueType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
//此处往下是关键代码
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
//this.messageConverters是一个容器对象,里面存放着所有的实现了HttpMessageConverter接口的Bean对象
if (converter instanceof GenericHttpMessageConverter && returnValueType != null) {
//条件满足判断,Bean是否实现了GenericHttpMessageConverter接口,实现该接口的好像是不需要额外注册的。
if (((GenericHttpMessageConverter<?>) converter).canWrite(returnValueType, returnValueClass, null)) {
//条件满足判断,调用canWrite()方法,该方法在HttpMessageConverter接口方法,是需要具体的Bean来实现内部逻辑,简单说明,是否满足返回数据的条件。
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(returnValueClass, null)) {
//条件满足,当Bean为注册类的时候,直接调用canWrite()判断是否能满足返回数据条件。
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
//都不满足的满足的时候,返回所有的spring mvc内部支持的MediaType
return Collections.singletonList(MediaType.ALL);
}
}
读完getProducibleMediaTypes(…)代码后,我们回到writeWithMessageConverters(…)代码中,在此方法内部会对请求MediaType信息与响应的MediaType信息做匹配,看下面代码。
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
... 省略 ...
//获取请求中的MediaType信息
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
//获取指定的响应的MediaType信息
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass, returnValueType);
... 省略 ...
//连个For循环逐一匹配,找到匹配符合条件的,记录到compatibleMediaTypes属性中。
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
//符合条件,,记录到compatibleMediaTypes属性中。
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
//当compatibleMediaTypes没有任何MediaType的时候,Spring mvc会抛出HttpMediaTypeNotAcceptableException异常了,这个异常也就是我们在之前分析日志的时候看到异常。所以如果若控制该异常的出现,就需要让请求的MediaType与响应的MediaType信息有匹配。回想一下之前自定义的Gson解析器。(text/gson和自定义GSONHttpMessageConverte解析器)
if (compatibleMediaTypes.isEmpty()) {
if (returnValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
... 省略 ...
}
对writeWithMessageConverters(…)方法的代码逻辑简单总结的描述,从请求中提取MediaType信息(Request头、URI后缀、GET方式传参三种方式中提取),再从Controller方法的处理响应中提取MediaType信息(Controller方法的@RequestMapping注解、所有实现了HttpMessageConverter接口实例Bean中),然后找到两者一一匹配的MediaType信息提取出来,根据提出取来的MediaType信息获取到对应的实现了HttpMessageConverter接口的实例Bean,该Bean也就是数据解析器,来对Controller方法返回的数据对象进行解析,并将解析结果返回给最终的请求端。
需要注意的是,如果使用@RequestMapping注解produces=”text/mydata; charset=UTF-8”描述一个自定义的MediaType信息,Spring会优先检测该MediaType是否对应的该类型的数据解析器即HttpMessageConverter实例Bean,如果没有则直接抛出HttpMediaTypeNotAcceptableException异常。
实际操作
之前我们做的都是准备工作,讲了如何分析日志,根据日志如何阅读Spring代码(好累),好了下面就不废话直接进入实际操作,来实现最初的设定的学习目标。
URL后缀 | 请求Mediatype映射 | 学习目标 |
---|---|---|
*.json | application/json; charset=UTF-8 | Controller处理方法返回对象解析成JSON |
*.xml | application/json; charset=UTF-8 | Controller处理方法返回对象解析成XML文件 |
application/pdf; charset=UTF-8 | Controller处理方法返回对象解析成pdf文件 | |
*.xls | application/xls; charset=UTF-8 | Controller处理方法返回对象解析成xls文件 |
*.xlsx | application/xlsx; charset=UTF-8 | Controller处理方法返回对象解析成xlsx文件 |
测试地址 | 结果 |
---|---|
http://127.0.0.1:8000/restfull/book | 将Result数据对象处理成xml报文返回 |
http://127.0.0.1:8000/restfull/book.json | 将Result数据对象处理成json格式报文返回 |
http://127.0.0.1:8000/restfull/book.xml | 将Result数据对象处理成xml报文返回 |
http://127.0.0.1:8000/restfull/book.pdf | 将Result数据对象处理成pdf文件字节流返回 |
http://127.0.0.1:8000/restfull/book.xls | 将Result数据对象处理成xls文件字节流返回 |
http://127.0.0.1:8000/restfull/book.xlsx | 将Result数据对象处理成xlsx文件字节流返回 |
/restfull/book
浏览器中发送http://127.0.0.1:8000/restfull/book请求,Spring容器会从Request Header头里面获取Accept属性值作为MediaType信息。如下图:
我使用的Google浏览器,所以放访问URL时,浏览器默认在Request Header中Accept属性追加很多MediaType信息进去。也就是只要Spring mvc中所有实现了HttpMessageConverter接口Bean中只要有一个匹配上就可以正常解析出来。
从执行结果来看,很显然该请求返回的结果为xml报文。回头看看开篇我列出的Spring中所有的HttpMessageConverter派生类列表,在回头看看后台日志,可以该请求的响应数据对象是交给了MedaiType=application/xhtml+xml对应的MappingJackson2XmlHttpMessageConverter数据解析器解析的。感兴趣的同学可以去看一下这个类的源代码,里面必须有MedaiType=application/xhtml+xml的属性值。
由于MappingJackson2XmlHttpMessageConverter类中使用com.fasterxml.jackson包中的类,所以项目需要引入com.fasterxml.jackson的相关jar包(pom.xml)。
Class:org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter
File:/framework_spring/pom.xml
/restfull/book.json
分析过程同上,这里不做过多讲解,只列出截图。
这里需要注意的是,该URI中是带有后缀”.json”,所以Spring mvc优先从URI的后缀解析出MediaType信息MediaType=application/json;charset=UTF-8这一点在后台日志中也是可以看到的。
在Spring MVC中也找到了MediaType=application/json;charset=UTF-8的对应数据解析器MappingJackson2HttpMessageConverter类。
Class:org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
/restfull/book.xml
分析过程同上,这里不做过多讲解,只列出截图。
请求URI中分析MediaType=application/xml;charset=UTF-8,Spring中对应的数据解析器为MappingJackson2XmlHttpMessageConverter。
Class:org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter
/restfull/book.pdf
请看下一个章节
/restfull/book.xls和/restfull/book.xlsx
请看下一个章节