背景
为什么会有这次的分析?
项目中数据分页的查询实现方式使用的是ktanx-jdbc,
在做分页列表查询时:
PageList<UserInfo> users = jdbcDao.queryPageList(user);
返回的数据列表对象是PageList
,这是继承了ArrayList
的分页类型扩展,主要代码如下:
public class PageList<T> extends ArrayList<T> {
private Pager pager;
...
}
在 ArrayList 的基础上添加了Pager
分页信息,这在Java代码中使用毫无问题,还能带来很多的便利性,
但是当你要在系统间进行数据的转换传输时,问题就来了。
问题
从上面的结构我们可以假想一下,如果要转换成当前流行的json格式,应该是怎样的?
List转换后的结果:
[
{
"userId": 0,
"username": "0name",
"password": "1230"
},
{
"userId": 1,
"username": "1name",
"password": "1231"
},
{
"userId": 2,
"username": "2name",
"password": "1232"
}
]
可以看到是一个json数组,如果要加上 PageList 中的 Pager 分页信息要加在哪里呢?发现没地方放,无法在数组中间夹杂一个别的数据。
所以正确的格式应该是像下面这样:
{
"list": [
{
"userId": 0,
"username": "0name",
"password": "1230"
},
{
"userId": 1,
"username": "1name",
"password": "1231"
},
{
"userId": 2,
"username": "2name",
"password": "1232"
}
],
"pager": {
"pageNum": 1,
"totalItems": 2147,
"pageSize": 20
}
}
list数据一个属性,pager分页信息一个属性,但是这和PageList的结构就对不上了,PageList本身就是一个ArrayList又怎么会有list属性呢!
所以想要在Spring MVC返回数据做json转换时自动给它加一层包装,以达到上面的要求。
分析
我们想要达到的目标是在不修改任何已实现代码的情况下达到自动转换的效果,最好的方式肯定是对Spring MVC进行自定义的扩展。
这里我们定义一个包装对象,只需要将PageList对象转换成该对象再转成json即可:
public class Page {
private List<?> list;
private Pager pager;
//getter setter...
}
扩展方案一
对返回值进行处理,首先想到的当然是HttpMessageConverter
,扩展起来也方便。
Spring MVC默认使用的实现是MappingJackson2HttpMessageConverter
,内部主要转换代码在抽象父类AbstractJackson2HttpMessageConverter
的writeInternal
方法中:
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
....
}
参数object就是具体的数据,这里我们只需要判断object是否是PageList类型,如果是将它转成上面的Page对象替换object再进行下面转换业务逻辑即可。
if (object instanceof PageList){
PageList pagelist = (PageList) object;
Page page = new Page();
page.setList(pageList);
page.setPager(pagelist.getPager());
object = page;
}
super.writeInternal(object, type, outputMessage);
最后通过重写WebMvcConfigurerAdapter
的configureMessageConverters
方法添加这个自定义的转换器:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(pageListMappingJackson2HttpMessageConverter());
}
顺便来了解一下Spring中对于HttpMessageConverter
的处理逻辑。
WebMvcConfigurationSupport
类中添加HttpMessageConverter
的代码:
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
可以看到主要调用了三个方法:configureMessageConverters
、addDefaultHttpMessageConverters
、extendMessageConverters
。
addDefaultHttpMessageConverters
方法中添加了默认的 HttpMessageConverter:
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter<Source>());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
messageConverters.add(new MappingJackson2HttpMessageConverter(
Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build()));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
}
因为我们启用了json所以包含了MappingJackson2HttpMessageConverter
。
再来看一下使用HttpMessageConverter
时的匹配逻辑,在AbstractMessageConverterMethodProcessor
类的writeWithMessageConverters
方法中,主要代码:
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
...
}
}
}
可以看到是顺序匹配,如果有多个HttpMessageConverter
同时支持当前的处理转换,那么在前面的会先被匹配到,后面的就没什么事了。这就要保证你想要优先处理的 HttpMessageConverter 在添加时必须排在前面。
以本示例为例,我们自定义的PageListMappingJackson2HttpMessageConverter
和默认添加的MappingJackson2HttpMessageConverter
在匹配条件上都是一样的,都支持json的转换,但是我们想要使用 PageListMappingJackson2HttpMessageConverter 来处理,这就要保证初始化时 PageListMappingJackson2HttpMessageConverter 在 MappingJackson2HttpMessageConverter 之前被添加。
再回头看一下上面初始化时调用的三个方法,configureMessageConverters
方法在默认的addDefaultHttpMessageConverters
方法前面,所以只要在该方法里面完成对 PageListMappingJackson2HttpMessageConverter 的添加那么就能覆盖默认的 MappingJackson2HttpMessageConverter。
如果从源码一路看下去,就会发现该方法最终调用达到的结果就是收集Spring提供的扩展配置类WebMvcConfigurerAdapter
实例的 configureMessageConverters 方法添加的 HttpMessageConverter,所以上面我们只需要在该方法中添加即可。
当然里面也能找到extendMessageConverters
方法,但是该方法添加的HttpMessageConverter
优先级在默认的之后,并不适用本次情况。
扩展方案二
上面的扩展方案一是最简单的方式,但是通过分析源码发现还有其它的扩展方式也就记录一下。
Spring MVC对于返回结果的处理都是通过HandlerMethodReturnValueHandler
来完成的,对应json(ResponseBody,RestController本质上是一样的)的具体实现是RequestResponseBodyMethodProcessor
。
处理的业务逻辑都在handleReturnValue
方法中:
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
只要重写 RequestResponseBodyMethodProcessor 替换returnValue
达到的效果也是一样的。
同样的在 WebMvcConfigurerAdapter 中提供了addReturnValueHandlers
方法,这里就不细讲了。
扩展方案三
分析过Spring MVC的源码就会发现对于Controller的调用都是通过ServletInvocableHandlerMethod
来完成的:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
......
}
同样的我们可以重写 ServletInvocableHandlerMethod 对 returnValue
进行处理,如果是PageList类型就加一层包装。
但是重写之后我们要怎么进行切入呢?Spring MVC并没有提供 ServletInvocableHandlerMethod 的切入方法。
通过源码分析我们发现 ServletInvocableHandlerMethod 的创建都是在RequestMappingHandlerAdapter
中完成的:
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
......
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
......
}
protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
return new ServletInvocableHandlerMethod(handlerMethod);
}
所以我们只需要覆盖它的创建方法就行。
但是这个 RequestMappingHandlerAdapter 可不是我们常用的Spring提供配置扩展的Adapter
,不能直接继承覆盖方法然后加@Configuration
来使用,这样会导致意想不到的结果。
从Spring Boot 1.4.0版本开始,提供了WebMvcRegistrationsAdapter
类来加强web项目的可自定义程度,其中包括RequestMappingHandlerAdapter
也就是我们上面创建使用 ServletInvocableHandlerMethod 的类,有了这个一切就又都容易了。
重写 RequestMappingHandlerAdapter 创建 ServletInvocableHandlerMethod 的方法:
public class MvcRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
return new PagelistServletInvocableHandlerMethod(handlerMethod);
}
}
然后通过 WebMvcRegistrationsAdapter 注册使用我们重写后的MvcRequestMappingHandlerAdapter
:
@Configuration
public class MvcWebMvcRegistrationsAdapter extends WebMvcRegistrationsAdapter {
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new MvcRequestMappingHandlerAdapter();
}
}
这同样达到了我们的要求。
最后
项目使用了Spring Boot、Spring cloud。
可以看到至少有三种方式可以达到我们的要求,推荐使用第一种。
在返回时做了处理转换,在接收时同样要做处理转换,十分的麻烦,这里就不讲了。
当然,在系统间交互时直接返回PageList这种数据结构是不推荐的,这将无法处理错误信息及错误码等,但是不同的团队不同的开发人员之间总会有不按套路出牌的人,所以才有了本次分析。
同时也发现PageList这种类型在本地使用方便,但在交互时真是个鸡肋的结构,后期要改!