spring mvc3 + fastjson 转换 REST 参数以及输出

80357518 2014-03-26

spring 3可以支持Rest风格参数,其内置了jackson框架作为REST的json参数转换成javabean对象,以及bean对象转换成json参数。

下文以spring 3.1.1 + fastjson为例(低于这个版本的不知道能不能行),说明如何使用springmvc构造resuful参数及输出。

不说废话了,直接上代码:Controller类:

@Controller
public class TestCon
{
    @ResponseBody
    @RequestMapping("/test")
    public Object test(@RequestBody
    final TBean tBean)
    {
        System.out.println(tBean);
        final List<Object> lists = new ArrayList<Object>();
        lists.add("3434");
        lists.add(tBean);
        lists.add(new String[] { "a", "b" });

        return lists;
    }
}

其中:@RequestBody可以使请求中的REST风格的json字符串直接转换成我们想要的参数TBean(当然,低层是使用fastjson等框架转换).

此方法的返回值为一个List对象,经过转换成json输出后,是一个js数组格式的内容。

@RequestBody不可少,表示将方法的返回值作为响应内容。

数据bean对象。

public class TBean
{

    private String id;

    //....
}

好了,最关键的一步:在springmvc 的配置文件中(XXX-servlet.xml)配置json格式转换方式:

<!-- 方式一 -->
<!--

     <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" />
        </mvc:message-converters>
    </mvc:annotation-driven>
 -->
 
<!-- 方式二 -->
 
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"></bean>
            </list>
        </property>
    </bean>

需要注意的是:方式一和方式二,选择一种实现即可。其它方式一是使用了命名空间的引入,需要引入mvc的命名空间,sprng3.0 不支持这种写法。这两种写法不能同时存在,如果同时存在,功能将异常。

核心代码就是这样的。当然不可缺少的要把fastjson框架的jar包引入到工程中。

模拟发起请求:

spring mvc3 + fastjson 转换 REST 参数以及输出

其中:{id:'34'}为REST风格的字符串,将被fastjson转换成TBean对象,直接扔给控制器的方法调用。

Content-Type: application/json; charset=utf-8
Accept: application/json
为请求头信息,以上的代码只支持Context-Type为application/json格式的头,如果没有此头或头内容不是application/json,spring将抛出415的http返回码,导致失败:

spring mvc3 + fastjson 转换 REST 参数以及输出

如果配置了log4j打印spring debug级别的日志,

可以看到日志中出现如下信息:

************************************************************未设置Content-Type信息的错误日志********************************************************************************

DEBUG DefaultAnnotationHandlerMapping - Mapping [/test.do] to HandlerExecutionChain with handler [test.controllers.TestCon@c81672] and 1 interceptor
DEBUG AnnotationMethodHandlerExceptionResolver - Resolving exception from handler [test.controllers.TestCon@c81672]: org.springframework.web.HttpMediaTypeNotSupportedException: Cannot extract parameter (TBean tBean): no Content-Type found
DEBUG ResponseStatusExceptionResolver - Resolving exception from handler [test.controllers.TestCon@c81672]: org.springframework.web.HttpMediaTypeNotSupportedException: Cannot extract parameter (TBean tBean): no Content-Type found
DEBUG DefaultHandlerExceptionResolver - Resolving exception from handler [test.controllers.TestCon@c81672]: org.springframework.web.HttpMediaTypeNotSupportedException: Cannot extract parameter (TBean tBean): no Content-Type found
DEBUG DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'dispatcher': assuming HandlerAdapter completed request handling

************************************************************设置为非application/json的错误日志*********************************************************************************

DEBUG DefaultAnnotationHandlerMapping - Mapping [/test.do] to HandlerExecutionChain with handler [test.controllers.TestCon@c81672] and 1 interceptor
DEBUG AnnotationMethodHandlerExceptionResolver - Resolving exception from handler [test.controllers.TestCon@c81672]: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/pan' not supported
DEBUG ResponseStatusExceptionResolver - Resolving exception from handler [test.controllers.TestCon@c81672]: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/pan' not supported
DEBUG DefaultHandlerExceptionResolver - Resolving exception from handler [test.controllers.TestCon@c81672]: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/pan' not supported
DEBUG DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'dispatcher': assuming HandlerAdapter completed request handling

如果content-type输入正确的话,将会看到正确的输出结果:

spring mvc3 + fastjson 转换 REST 参数以及输出

*******************************************分隔线*****************************************************************************************************************************************

如果你不需要要求输入Context-Type即可解析的话,还有另一种方式实现,参考:

自定义Spring MVC3的参数映射和返回值映射 + fastjson

首先说一下场景:在一些富客户端Web应用程序中我们会有比较多的Ajax调用,并且希望与服务器交互的数据需要是复杂的JSON对象。 fastjon是一个非常高效的JSON序列化和反序列化库,我希望我们输入的JSON串能通过fastjson直接反序列化为一个复杂的JavaBean对象,同时我的返回值能够能通过fastjson序列化为JSON串。

所谓复杂的JavaBean就是,不仅仅只有一层属性,而是属性也是JavaBean的情况, 例如:

public class FooBean { 
 private String name; 
 private Long id; 
 private Date birthday; 
 private List<Address> addresses; 
 public String getName() { 
 return name; 
 } 
 public void setName(String name) { 
 this.name = name; 
 } 
 public Long getId() { 
 return id; 
 } 
 public void setId(Long id) { 
 this.id = id; 
 } 
 public Date getBirthday() { 
 return birthday; 
 } 
 public void setBirthday(Date birthday) { 
 this.birthday = birthday; 
 } 
 public List<Address> getAddresses() { 
 return addresses; 
 } 
 public void setAddresses(List<Address> addresses) { 
 this.addresses = addresses; 
 } 
 @Override 
 public String toString() { 
 return "FooBean{" + 
 "name='" + name + '\'' + 
 ", id=" + id + 
 ", birthday=" + birthday + 
 ", addresses=" + addresses + 
 '}'; 
 } 
}
public class Address { 
 private String street; 
 private int number; 
 public String getStreet() { 
 return street; 
 } 
 public void setStreet(String street) { 
 this.street = street; 
 } 
 public int getNumber() { 
 return number; 
 } 
 public void setNumber(int number) { 
 this.number = number; 
 } 
 @Override 
 public String toString() { 
 return "Address{" + 
 "street='" + street + '\'' + 
 ", number=" + number + 
 '}'; 
 } 
}




当然,结构还可以再复杂,Adress对象里还可以又复杂JavaBean的属性。

在SpringMVC3中我们可以把输入简单的映射为某个Action方法的参数, 例如:

@RequestMapping(value="/someAction", method=RequestMethod.POST) 
public String processSubmit(FooBean fooBean, Model model) { 
// 利用fooBean 
 return “views/some_page”; 
}



用Spring MVC3, 我们可以把Form里的字段轻松的映射到JavaBean的属性。 Spring MVC3 提供了丰富的参数映射机制, 详细信息可以参见这里

同时对于Spring MVC3默认的提供的映射机制不能涵盖的对象,我们可以通过扩展HandlerMethodArgumentResolver和HttpMessageConverter的机制来实现。
HandlerMethodArgumentResolver对应输入, HttpMessageConverter对应输出

假设对于上面的FooBean, 我们有这样一个JSON对象和它对应:

@RequestMapping(value = "/fastjson", method = RequestMethod.POST) 
public @ResponseBody FooBean fastjson2(@FastJson FooBean foo) { 
 System.out.println(foo); 
 return foo; 
}



首先这里有个@FastJson的标注,这是我们为了让自己的HandlerMethodArgumentResolver能够识别这个参数是需要自己来处理而定义的一个Annotation

@Target(ElementType.PARAMETER) 
@Retention(RetentionPolicy.RUNTIME) 
@Documented 
public @interface FastJson { 
}



然后就是定义一个FastJsonArgumentResolver,来对HttpServletRequest的body进行解析:

public class FastJsonArgumentResolver implements HandlerMethodArgumentResolver { 
 @Override 
 public boolean supportsParameter(MethodParameter parameter) { 
 return parameter.getParameterAnnotation(FastJson.class) != null; 
 } 
 @Override 
 public Object resolveArgument(MethodParameter parameter, 
 ModelAndViewContainer mavContainer, 
 NativeWebRequest webRequest, 
 WebDataBinderFactory binderFactory) throws Exception { 
 HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); 
 // content-type不是json的不处理 
 if (!request.getContentType().contains("application/json")) { 
 return null; 
 } 
 // 把reqeust的body读取到StringBuilder 
 BufferedReader reader = request.getReader(); 
 StringBuilder sb = new StringBuilder(); 
 char[] buf = new char[1024]; 
 int rd; 
 while((rd = reader.read(buf)) != -1){ 
 sb.append(buf, 0, rd); 
 } 
 // 利用fastjson转换为对应的类型 
 if(JSONObjectWrapper.class.isAssignableFrom(parameter.getParameterType())){ 
 return new JSONObjectWrapper(JSON.parseObject(sb.toString())); 
 } else { 
 return JSON.parseObject(sb.toString(), parameter.getParameterType()); 
 } 
 } 
}



在这里,我们只针对content-type是application/json的对象做处理,最后通过JSON.parseObject方法简单的把JSON串反序列化为指定的类型。

这里有一个JSONObjectWrapper对象需要解释一下。 原本我是想如果Action方法的参数的类型是JSONObject这样的原始类型的话就直接利用JSON.parseObject(sb.toString())映射过去。 但是由于JSONObject实现了Map结果,所以Spring MVC3的默认处理器MapMethodProcessor会先起作用,这样就不能正常的映射成JSONObject对象了。 没有办法做了一个简单的JSONObject包装类,以使MapMethodProcessor不能对其进行处理。

public class JSONObjectWrapper { 
 private JSONObject jsonObject; 
 public JSONObjectWrapper(JSONObject jsonObject) { 
 this.jsonObject = jsonObject; 
 } 
 public JSONObject getJSONObject() { 
 return jsonObject; 
 } 
}



这里顺便提一下,Spring MVC自己的HandlerMethodArgumentResolver有哪些,并且会以什么样的顺序执行呢?
其实定义在RequestMappingHandlerAdapter里:

/** 
 * Return the list of argument resolvers to use including built-in resolvers 
 * and custom resolvers provided via {@link #setCustomArgumentResolvers}. 
 */ 
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { 
 List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); 
 // Annotation-based argument resolution 
 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); 
 resolvers.add(new RequestParamMapMethodArgumentResolver()); 
 resolvers.add(new PathVariableMethodArgumentResolver()); 
 resolvers.add(new PathVariableMapMethodArgumentResolver()); 
 resolvers.add(new MatrixVariableMethodArgumentResolver()); 
 resolvers.add(new MatrixVariableMapMethodArgumentResolver()); 
 resolvers.add(new ServletModelAttributeMethodProcessor(false)); 
 resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters())); 
 resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters())); 
 resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); 
 resolvers.add(new RequestHeaderMapMethodArgumentResolver()); 
 resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); 
 resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); 
 // Type-based argument resolution 
 resolvers.add(new ServletRequestMethodArgumentResolver()); 
 resolvers.add(new ServletResponseMethodArgumentResolver()); 
 resolvers.add(new HttpEntityMethodProcessor(getMessageConverters())); 
 resolvers.add(new RedirectAttributesMethodArgumentResolver()); 
 resolvers.add(new ModelMethodProcessor()); 
 resolvers.add(new MapMethodProcessor()); 
 resolvers.add(new ErrorsMethodArgumentResolver()); 
 resolvers.add(new SessionStatusMethodArgumentResolver()); 
 resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); 
 // Custom arguments 
 if (getCustomArgumentResolvers() != null) { 
 resolvers.addAll(getCustomArgumentResolvers()); 
 } 
 // Catch-all 
 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); 
 resolvers.add(new ServletModelAttributeMethodProcessor(true)); 
 return resolvers; 
}



在这里我们可以看到:

  • Spring MVC本身提供了非常丰富的HandlerMethodArgumentResolver实现。
  • HandlerMethodArgumentResolver是按顺序执行,当然为了性能Spring本身是有Cache的,一旦确定了某一个参数可以应用的HandlerMethodArgumentResolver,下次就不会再遍历这个List了。
  • 自定的HandlerMethodArgumentResolver会晚于Spring自己的被执行,这也是上面提到的JSONObject会被MapMethodProcessor先处理的原因。
  • Spring自己的JSON映射机制是通过RequestResponseBodyMethodProcessor + AllEncompassingFormHttpMessageConverter来实现的
  • 很不幸这是一个private方法, 你没有办法简单的改变Spring MVC的默认行为,除非你重写RequestMappingHandlerAdapter



好了,有了FastJsonArgumentResolver, 接下来我们要让它生效:

@Override 
protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException, 
 HttpMessageNotWritableException { 
 OutputStream out = outputMessage.getBody(); 
 String text = JSON.toJSONString(obj, features); 
 byte[] bytes = text.getBytes(charset); 
 out.write(bytes); 
}



实现很简单, 就不详细说了。

最后来看看如何通过Ajax调用上面的Action方法:

var data = { 
 name : "matianyi", 
 id : 12345, 
 birthday : "1983-07-01 01:12:12", 
 addresses : [ 
 { 
 street : "street1", 
 number : 1 
 }, 
 { 
 street : "street2", 
 number : 2 
 } 
 ] 
}; 
var link = $(this); 
$.ajax({ 
 url:"/spring-sample/fastjson1", 
 dataType:"json", 
 type:"POST", 
 contentType: "application/json", 
 data : JSON.stringify(data), 
 success : function(obj){ 
 console.log(obj); 
 } 
});



两点需要注意:

  • contentType: "application/json"
  • data : JSON.stringify(data)


这样JavaScript的对象会被转换为JSON串,并且最为HttpRequest的BODY传给服务器。

相关推荐