**
SpringMVC中使用@RequestBody,@ResponseBody註解實現Java對象和XML/JSON數據自動轉換(上)
**
Spring3.1開始使用新的HandlerMapping 和 HandlerAdapter 來支持@Contoller 和@RequestMapping註解處理:處理器映射RequestMappingHandlerMapping和處理器適配器RequestMappingHandlerAdapter組合來代替Spring2.5 開始的處理器映射DefaultAnnotationHandlerMapping和處理器適配器AnnotationMethodHandlerAdapter。
HandlerMapping:請求到處理器的映射,如果映射成功返回一個HandlerExecutionChain 對象(包含一個Handler處理器(頁面控制器)對象、多個HandlerInterceptor 攔截器)對象;
HandlerAdapter:HandlerAdapter 將會把處理器包裝爲適配器,從而支持多種類型的處理器,即適配器設計模式的應用,從而很容易支持很多類型的處理器。
配合@ResponseBody註解,以及HTTP Request Header中的Accept屬性,Controller返回的Java對象可以自動被轉換成對應的XML或者JSON數據。
先看一個例子,只需要簡單的幾步,就可以返回XML數據。(本文使用Spring版本 4.1.6,並使用maven做項目構建)
1)在配置文件中添加
- <!-- 自動掃描的包名 -->
- <context:component-scan base-package="learning.webapp.controller" />
- <!-- 默認的註解映射的支持 -->
- <mvc:annotation-driven/>
2)添加以下幾個java類
- package learning.webapp.model;
-
- public class Employee {
- private String name;
- private int salary;
-
- public Employee() {
- }
-
- public Employee(String name, int salary) {
- this.name = name;
- this.salary = salary;
- }
-
- public String getName() {
- return name;
- }
-
- public int getSalary() {
- return salary;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public void setSalary(int salary) {
- this.salary = salary;
- }
- }
- package learning.webapp.model;
-
- import javax.xml.bind.annotation.XmlRootElement;
-
- @XmlRootElement
- public class EmployeeX extends Employee {
- public EmployeeX() {
- super();
- }
-
- public EmployeeX(String name, int salary) {
- super(name, salary);
- }
- }
- package learning.webapp.controller;
-
- import learning.webapp.model.Employee;
- import learning.webapp.model.EmployeeX;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.springframework.web.bind.annotation.ResponseBody;
-
- @Controller
- @RequestMapping("/employees")
- public class XmlOrJsonController {
- @RequestMapping(value="/xml/{name}", method=RequestMethod.GET)
- @ResponseBody
- public Employee getEmployeeXml(@PathVariable String name) {
- return new EmployeeX(name, 16000);
- }
- }
3) 在Eclipse中使用Jetty插件啓動Web Server,然後在瀏覽器中訪問:
非常簡單!Spring是怎麼實現這個轉換的呢?我們先了解下Spring的消息轉換機制。
在SpringMVC中,可以使用@RequestBody和@ResponseBody兩個註解,分別完成請求報文到對象和對象到響應報文的轉換,底層這種靈活的消息轉換機制,就是Spring3.x中新引入的HttpMessageConverter即消息轉換器機制。
我們可以用下面的圖,簡單描述一下這個過程。
這裏最關鍵的就是<mvc:annotation-driven/>,加了這句配置,Spring會調用org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser來解析。
在這個類的parse(Element, ParserContext)方法中,分別實例化了RequestMappingHandlerMapping,RequestMappingHandlerAdapter等諸多類。
RequestMappingHandlerAdapter是請求處理的適配器,我們重點關注它的messageConverters屬性。
1)RequestMappingHandlerAdapter在調用handle()的時候,會委託給ServletInvocableHandlerMethod的invokeAndHandle()方法進行處理,這個方法又調用HandlerMethodReturnValueHandlerComposite類進行處理。
HandlerMethodReturnValueHandlerComposite維護了一個HandlerMethodReturnValueHandler列表。
由於我們使用了@ResponseBody註解,getReturnValueHandler就會返回RequestResponseBodyMethodProcessor的實例。
2)之後RequestResponseBodyMethodProcessor.handleReturnValue()方法會被調用。此方法會調用AbstractMessageConverterMethodProcessor.writeWithMessageConverters()。它會根據request header中的Accept屬性來選擇合適的message converter.
3) messageConverters中有如下的6個converter. 它們是從哪裏來的呢?前面提到,AnnotationDrivenBeanDefinitionParser.parse(Element, ParserContext)方法中,分別實例化了RequestMappingHandlerMapping,RequestMappingHandlerAdapter以及messageConverters屬性。
需要關注org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter這個類,就是它實現了返回對象到XML的轉換。
4)看一下getMessageConverters()中的處理。有5個message converter是一定會加進來的。
- if (convertersElement == null || Boolean.valueOf(convertersElement.getAttribute("register-defaults"))) {
- messageConverters.setSource(source);
- messageConverters.add(createConverterDefinition(ByteArrayHttpMessageConverter.class, source));
-
- RootBeanDefinition stringConverterDef = createConverterDefinition(StringHttpMessageConverter.class, source);
- stringConverterDef.getPropertyValues().add("writeAcceptCharset", false);
- messageConverters.add(stringConverterDef);
-
- messageConverters.add(createConverterDefinition(ResourceHttpMessageConverter.class, source));
- messageConverters.add(createConverterDefinition(SourceHttpMessageConverter.class, source));
- messageConverters.add(createConverterDefinition(AllEncompassingFormHttpMessageConverter.class, source));
然後再看,這裏jaxb2Present爲true, 因此Jaxb2RootElementHttpMessageConverter被添加到messageConverters中。
5)看一下jaxb2Present的定義,原來javax.xml.bind.Binder這個類是JDK中包含的類,所以jaxb2Present=true。
6)我們看一下Jaxb2RootElementHttpMessageConverter的canWrite()方法。返回true的條件有兩個
a) 返回對象的類具有XmlRootElement註解;
b) 請求頭中的Accept屬性包含application/xml。
- @Override
- public boolean canWrite(Class<?> clazz, MediaType mediaType) {
- return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
- }
7) 在chrome中打開開發者工具,可以看到請求頭中確實包含了Accept=application/xml
接下來我們看看如果想要返回JSON數據,應該怎麼做?
根據上面的分析,首先我們需要添加一個支持JSON的message converter. 前面分析getMessageConverters()代碼的時候,看到
- if (jackson2Present) {
- RootBeanDefinition jacksonConverterDef = createConverterDefinition(MappingJackson2HttpMessageConverter.class, source);
- GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
- jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
- messageConverters.add(jacksonConverterDef);
- }
- else if (gsonPresent) {
- messageConverters.add(createConverterDefinition(GsonHttpMessageConverter.class, source));
- }
然後再來看看jackson2Present和gsonPresent的定義。
- private static final boolean jackson2Present =
- ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) &&
- ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
- private static final boolean gsonPresent =
- ClassUtils.isPresent("com.google.gson.Gson", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
所以我們只要把Jackson2或者GSON加入工程的class path,Spring就會自動把GsonHttpMessageConverter加進來。
1)我們在POM中添加以下依賴
- <dependency>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-databind</artifactId>
- <version>2.6.1</version>
- </dependency>
或者
- <dependency>
- <groupId>com.google.code.gson</groupId>
- <artifactId>gson</artifactId>
- <version>2.3.1</version>
- </dependency>
2)在XmlOrJsonController.java中添加getEmployeeJson()方法
- @RequestMapping(value="/json/{name}", method=RequestMethod.GET)
- @ResponseBody
- public Employee getEmployeeJson(@PathVariable String name) {
- return new Employee(name, 16000);
- }
和getEmployeeXml()相比,這裏唯一的不同是返回對象變成了Employee,因爲Employee類上沒有@XmlRootElement註解,所以Spring不會選擇Jaxb2RootElementHttpMessageConverter。又因爲Accept屬性中包含了*/*,表示接受任意格式返回數據,所以GsonHttpMessageConverter的canWrite()方法返回true.這樣Spring就會選擇MappingJackson2HttpMessageConverter或者GsonHttpMessageConverter來進行數據轉換。
至此,我們知道請求頭中的Accept屬性是一個很關鍵的東西,我們可以根據這個在Controller中寫一個方法,根據Accept的值自動返回XML或者JSON數據。
- /**
- * 根據request header中的Accept自動選擇返回XML or JSON
- */
- @RequestMapping(value="/{name}", method=RequestMethod.GET)
- @ResponseBody
- public Employee getEmployee(@PathVariable String name) {
- return new EmployeeX(name, 16000);
- }
因爲瀏覽器的Accept值不方便修改,我們自己寫客戶端來調用。
- package learning.webapp;
-
- import java.io.IOException;
- import java.io.InputStream;
- import java.net.URI;
- import java.net.URISyntaxException;
-
- import org.junit.Test;
- import org.springframework.http.HttpEntity;
- import org.springframework.http.HttpHeaders;
- import org.springframework.http.HttpMethod;
- import org.springframework.http.client.ClientHttpRequest;
- import org.springframework.http.client.ClientHttpResponse;
- import org.springframework.http.client.SimpleClientHttpRequestFactory;
- import org.springframework.web.client.RestTemplate;
-
- public class XmlOrJasonControllerTest {
-
- @Test
- public void testJsonResponse() throws IOException, URISyntaxException {
- String url = "http://localhost:8080/employees/Jack";
-
- ClientHttpRequest request = new SimpleClientHttpRequestFactory().createRequest(new URI(url), HttpMethod.GET);
- request.getHeaders().set("Accept", "application/json");
- ClientHttpResponse response = request.execute();
- InputStream is = response.getBody();
- byte bytes[] = new byte[(int) response.getHeaders().getContentLength()];
- is.read(bytes);
-
- String jsonData = new String(bytes);
- System.out.println(jsonData);
- }
-
- @Test
- public void testXmlResponse() throws IOException, URISyntaxException {
- String url = "http://localhost:8080/employees/Jack";
- // response headers 中包含Transfer-Encoding:chunked,沒有content length,
- HttpHeaders requestHeaders = new HttpHeaders();
- requestHeaders.set("Accept", "application/xml");
-
- RestTemplate restTemplate = new RestTemplate();
- HttpEntity<Object> httpEntity = new HttpEntity<Object>(requestHeaders);
- String xmlData = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class).getBody();
-
- System.out.println(xmlData);
- }
- }
[參考資料]
1)http://www.cnblogs.com/fangjian0423/p/springMVC-xml-json-convert.html
2)http://my.oschina.net/lichhao/blog/172562