天天看點

使用Spring MVC 搭建Rest服務

使用Spring MVC 搭建Rest服務

本文由大關總結整理所得,不保證内容的正确性,轉載請标明出處!

    Rest(Representational State Transfer,表述狀态轉移),是一種基于Http協定的,能夠快速開發網絡服務程式,并且提高網絡服務系統伸展性的設計和開發方式。Rest的兩端可以是不同構的程式設計和程式運作環境,Rest通過Http協定将通信的兩端進行連接配接,在服務兩端通過對Http協定的使用,最終完成資料的翻譯和轉換。

    Rest有幾個重要的特點,使得Rest能夠在服務搭建上占有一定的優勢。首先,Rest以一切皆資源的方式來看待所有的web提供的服務(包括web服務本身,以及web服務中某個具體的服務應用),所有的資源都采用URI進行定位。其次,Rest通過對Http中相應字段屬性設定,确定用戶端和服務端的緩存政策,在一定程度上,可以緩解伺服器和用戶端的壓力(Rest服務被認為是無狀态的web服務),最後,Rest通過對Http消息體中(MIME)Content-Type的内容不同,可以采用不同處理政策對消息分解,擷取需要的資訊,此外Rest還有很多優勢,具體可以參考:Architectural Styles and the Design of Network-based Software Architectures這篇論文,文中較長的描述了Rest服務的推導過程。

    要想使用Rest必須要了解Http服務中定義的幾個關鍵的方法。GET,用于從服務端擷取資訊,但并不更改服務端的任何狀态;POST,用于送出建立的資訊,一般更改服務端的資料狀态,并且傳回新建立對象的ID(一般來說,為了保證ID的唯一性,ID的值需要由服務端來确定,是以在建立結束後會将新配置設定的ID回報給用戶端);PUT,用于更改服務端某個對象的狀态(一般被認為是Update),服務端狀态受到影響,不需要傳回影響後的結果;DELETE,删除服務端的對象,服務端的狀态發生改變,可以傳回删除後的對象,也可以不傳回。(Http還有其他的方法,但在Rest中不是很常用,可以檢視RFC2616)。這些方法僅是意義上這樣說,但是并不是一定要這用,具體的方法對應的動作完全是服務兩端确定的。

    在JAVA中搭建Rest服務有很多種,其中JAX-RS是JAVA定義的Rest服務的API,可以使用Apache實作的jersy搭建Rest服務。在Spring MVC中也提供了搭建Rest服務的方法,下文主要探讨如何使用Spring MVC搭建Rest服務。

    下面的内容需要您對如下内容有所了解:

(1)  會使用Spring架構,了解依賴注入原理,了解Spring的基本配置。

(2)  使用過Java相關的Web容器(例如,Tomcat或Apache等,這裡使用的是Jetty,原理相同)

(3)  會使用一款OXM架構(例如:Castor或JAXB,這裡使用的是JAXB2)

(4)  對什麼是Http服務和什麼是Rest服務略有了解

如果您完全滿足上面的條件,那麼了解下面的内容就根本不成問題,如果您對有些内容不是很清楚,可以先看整個服務的流程,具體細節可以略看。

1.  Rest功能說明

下面我們将開始使用Spring MVC的方法搭建一個Rest服務,在這個服務中,伺服器端維護了一個關于Student的集合,用戶端可以通過連接配接到服務端,向服務端添加、删除、查找和更新學生資訊等操作。

2.  學生資料結構(bean)

通過對Rest功能的分析,可以看出,服務端有兩個關鍵的資料結構,一個是用來表示學生資訊的Student類,另外一個是用來表示學生集合的StudentList類。下面我們通過schema定義這兩個資料結構。

StudentLists.xsd

<?xml version="1.0" encoding="UTF-8"?>

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <xsd:complexType name="student">

       <xsd:sequence>

              <xsd:element name="name" type="xsd:string" minOccurs="1"/>

              <xsd:element name="age" type="xsd:int"/>

           </xsd:sequence>

           <xsd:attribute name="id" type="xsd:string" use="required"/>

    </xsd:complexType>

    <xsd:element name="studentList">

       <xsd:complexType>

           <xsd:sequence>

              <xsd:element name="students" type="student"  maxOccurs="unbounded"/>

           </xsd:sequence>

       </xsd:complexType>

    </xsd:element>

</xsd:schema>

可以看出,schema中定義了兩個結構,一個是學生類型,一個是學生集合類型。學生類型包括兩個元素(分别是name和age)和一個屬性(id)。學生集合類型中僅包含一個元素(students,是一個學生類型的集合)。

通過使用xjc生成java類,xjc指令如下:

I:\programs\eclipse\SpringMVCRestTest\src>xjc -p com.upc.upcgrid.guan.springMvcR

estTest.bean.student StudentLists.xsd

xjc(xml to java compiler),可以将schema轉換成java類,-p後面的兩個參數分别是轉換後的包名,以及需要轉換的schema檔案名字。此時,你重新整理你的服務,可以看到xjc已經生成了Student類和StudentLists類,此外還生成了一個ObjectFactory類,這裡我們不需要ObjectFactory類,是以直接将這個類删除,之後在Student類的聲明前增加一條語句,使得Student類聲明部分如下:

@XmlType(name = "student", propOrder = {

    "name",

    "age"

})

@XmlRootElement(name="student")

public class Student {

3.  學生管理類

學生的基本結構已經搭建完成,現在需要提供一個管理類,用于來管理學生集合(注意,正常情況下,應當将學生資料存入資料庫,這裡為了簡化,僅将這些資訊出入一個Map中進行管理)。學生管理類維護這學生集合,以及基于集合之上的操作。

StudentManager.java

@Component

public class StudentManager {

    Map<String, Student> students = new ConcurrentHashMap<String, Student>();

    public synchronized void addStudent(Student student){

       if(students.containsKey(student.getId()))

           return;

       students.put(student.getId(), student);

    }

    public synchronized void deleteStudent(String id){

       if(students.containsKey(id))

           students.remove(id);

    }

    public synchronized void updateStudent(String id,Student student){

       deleteStudent(id);

       if(student.getId()==null || student.getId().equals(""))

           student.setId(id);

       addStudent(student);

    }

    public synchronized Student getStudent(String id){

       return students.get(id);

    }

    public synchronized StudentList getStudent() {

       StudentList sl = new StudentList();

       for(String key : students.keySet())

       {

           sl.getStudents().add(students.get(key));

       }

       return sl;

    }

}

可以看出,我們将StudentManager标記成component,以便Spring能夠将這個類的執行個體注入到使用它的類中(注意,這個類必須是一個單例模式,因為這個類中維護着學生的集合)。此外,StudentManager中的所有方法都标記成了同步方法,并且Map集合也是一個同步集合。StudentManager提供了對學生集合Map的基本操作。

4.  Rest服務實作

下面提供了使用Spring MVC實作Rest服務的方法。

@Controller

@RequestMapping(value="/students")

public class StudentServer {

    private StudentManager manager;

    @RequestMapping(method=RequestMethod.POST)

    @ResponseBody

    public String createStudent(@RequestBody Student student){

       manager.addStudent(student);

       return student.getId();

    }

    @RequestMapping(value="/{id}",method=RequestMethod.GET)

    @ResponseBody

    public Student getStudent(@PathVariable String id){

       return manager.getStudent(id);

    }

    @RequestMapping(method=RequestMethod.GET)

    @ResponseBody

    public StudentList getStudent(){

       return manager.getStudent();

    }

    @RequestMapping(value="/{id}",method=RequestMethod.PUT)

    @ResponseBody

    public void updateStudent(@RequestBody Student student,@PathVariable String id){

       manager.updateStudent(id, student);

    }

    @RequestMapping(value="/{id}",method=RequestMethod.DELETE)

    @ResponseBody

    public void deleteStudent(@PathVariable String id){

       manager.deleteStudent(id);

    }

    @Autowired

    public void setManager(StudentManager manager) {

       this.manager = manager;

    }

}

用Control标記,Spring會認為這是一個Web服務,使用RequestMapping指明了服務映射的URI内容和映射的Http請求的方法。使用PathVariable可以從URI中擷取參數,使用RequestBody,Spring會将Http消息體内部的資料使用配置政策轉換成一個對象(參考5. Spring的配置),使用ResponseBody,Spring會将傳回值轉換成消息體需要的格式(參考5.Spring的配置)。

5.  Spring的配置

現在,我們需要對Spring進行基本的配置,以便Spring能夠使用正确的方式對接收到(發送出)的Http請求(相應)使用正确的方法處理(生成)消息體。我們的基本政策是,如果接收到的是XML文檔(即Content-type是***/xml類型),則使用JAXB2将消息體轉換成對象,否則将消息體作為普通文本進行處理;如果要發送的消息是複雜對象(不是簡單類型,例如:String、Integer、void等),則将消息使用JAXB2進行編組,否則僅生成文本消息。

Spring的配置如下:

SpringConfig.java

@Configuration

public class SpringConfig {

    private Jaxb2Marshaller marshaller;

    public @Bean Jaxb2Marshaller jaxb2Marshaller()//配置JAXB2Context

    {  

       Jaxb2Marshaller marshaller = new Jaxb2Marshaller();//建立JAXB上下文環境

       marshaller.setClassesToBeBound(Student.class,StudentList.class);//映射的xml類放入JAXB環境中    

       this.marshaller = marshaller;

       return marshaller;

    }

    public @Bean AnnotationMethodHandlerAdapter annotationMethodHandlerAdapter()

    {

       AnnotationMethodHandlerAdapter adapter = new AnnotationMethodHandlerAdapter();//建立消息體轉換器

       HttpMessageConverter<?>[] converters = new HttpMessageConverter<?>[2];//建立轉換數組

       StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();//建立字元轉換器

       MarshallingHttpMessageConverter marshallerConverter = new MarshallingHttpMessageConverter();//建立xom轉換器

       marshallerConverter.setMarshaller(marshaller);//設定marshaller

       marshallerConverter.setUnmarshaller(marshaller);//設定unmarshaller

       //将兩個轉換器放入清單

       converters[0] = (stringConverter);

       converters[1] = (marshallerConverter);

       //将轉換器清單指派給消息體轉換器

       adapter.setMessageConverters(converters);

       return adapter;   //傳回消息體轉換器  

    }

}

SpringConfig中先配置了JAXB2,在JAXB2中需要指定在2學生資料結構中生成的兩個類,是以JAXB環境會知道如何将Student和StudentList編組和解組。

SpringConfig中還配置了一個Adapter,這個轉換器是Spring用來轉換消息體時使用的,在這裡,我們為Adapter配置了兩個Converter,一個使用來處理XML的Marshaller的Converter,一個是用來處理普通類型String的Converter。是以Spring會根據情況,選擇正确的Converter處理消息體。(注意,隻要你提供了一個Adapter,Spring就會使用你提供的這個Adapter處理消息體,如果你沒有提供,Spring将會使用預設的Adapter處理消息體,由于對于XML類型(或者JSON類型)的資料必須手動提供Converter,是以這裡不能使用預設的Converter)。

之後是Spring的配置檔案:

rest-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context"

       xmlns:tx="http://www.springframework.org/schema/tx"

       xmlns:aop="http://www.springframework.org/schema/aop"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

           http://www.springframework.org/schema/aop

           http://www.springframework.org/schema/aop/spring-aop-2.5.xsd

           http://www.springframework.org/schema/tx

           http://www.springframework.org/schema/tx/spring-tx-2.5.xsd

           http://www.springframework.org/schema/context

           http://www.springframework.org/schema/context/spring-context-2.5.xsd">

           <context:annotation-config/>

           <context:component-scan base-package="com.upc.upcgrid.guan"/>                

</beans>

Spring的配置文檔中,僅讓Spring環境掃描标有标記類的包。

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5"

    xmlns="http://java.sun.com/xml/ns/javaee"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <servlet>

       <servlet-name>rest</servlet-name>

       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

       <init-param>

           <param-name>contextConfigLocation</param-name>

           <param-value>/conf/rest-servlet.xml</param-value>

       </init-param>

       <load-on-startup>1</load-on-startup>

    </servlet>

    <servlet-mapping>

       <servlet-name>rest</servlet-name>

       <url-pattern>/rest/*</url-pattern>

    </servlet-mapping>  

</web-app>

Spring使用DispatcherServlet完成URI任務的比對和分發,在建立DispatcherServlet的時候,提供了Spring配置檔案的路徑。

6.  Jetty伺服器的配置和啟動

我們這裡使用較輕量級的Jetty伺服器,以嵌入式的方式啟動Spring服務。Jetty服務要比Tomcat小的多,并且無需安裝,可以嵌入到程式中執行。下面給出Jetty的啟動過程。

JettyServerStart.java

public class JettyServerStart {

    public static void main(String[] args) {

        Server server= new Server();//建立jetty web容器(這與tomcat容器類似)

       server.setStopAtShutdown(true);//在退出程式是關閉服務

       //建立連接配接器,每個連接配接器都是由IP位址和端口号組成,連接配接到連接配接器的連接配接将會被jetty處理

       //第一個連接配接器的連接配接方式為http://202.194.158.128:8586

       Connector connector = new SelectChannelConnector();//建立一個連接配接器

       connector.setPort(8586);//連接配接的端口号,(tomcat下)一般是8080,這裡根據服務需求進行設定

       connector.setHost("202.194.158.128");//ip位址

       server.addConnector(connector);//添加連接配接

       //建立本地連接配接器,連接配接方式為http://localhost:8585

       Connector connectorLocal = new SelectChannelConnector();

       connectorLocal.setPort(8585);

       connectorLocal.setHost("localhost");

       server.addConnector(connectorLocal);

       //配置rest服務

       WebAppContext context = new WebAppContext();//建立服務上下文

       context.setContextPath("/SpringMVCRestTest");//通路服務路徑 http://{ip}:8568/SpringMVCRestTest

       context.setConfigurationDiscovered(true);

    context.setDescriptor(System.getProperty("user.dir")+File.separator+"conf"+File.separator+"web.xml");//指明服務描述檔案,就是web.xml

       context.setResourceBase(System.getProperty("user.dir"));//指定服務的資源根路徑,配置檔案的相對路徑與服務根路徑有關

       server.setHandler(context);//添加處理

       try {

           server.start();//開啟服務

           server.join();

       } catch (Exception e) {

           e.printStackTrace();

           System.exit(1);

       }//開啟服務

    }

現在,我們執行main方法,會的到如下輸出:

2011-06-24 21:22:07.609:INFO::jetty-7.3.0.v20110203

2011-06-24 21:22:07.796:INFO::NO JSP Support for /SpringMVCRestTest, did not find org.apache.jasper.servlet.JspServlet

2011-06-24 21:22:07.921:INFO::started o.e.j.w.WebAppContext{/SpringMVCRestTest,file:/I:/programs/eclipse/SpringMVCRestTest/}

2011-06-24 21:22:08.093:INFO:/SpringMVCRestTest:Initializing Spring FrameworkServlet 'rest'

2011-6-24 21:22:08 org.springframework.web.servlet.FrameworkServlet initServletBean

資訊: FrameworkServlet 'rest': initialization started

2011-6-24 21:22:08 org.springframework.context.support.AbstractApplicationContext prepareRefresh

資訊: Refreshing WebApplicationContext for namespace 'rest-servlet': startup date [Fri Jun 24 21:22:08 CST 2011]; root of context hierarchy

2011-6-24 21:22:08 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions

資訊: Loading XML bean definitions from ServletContext resource [/conf/rest-servlet.xml]

2011-6-24 21:22:08 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons

資訊: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFacto[email protected]: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,springConfig,studentManager,studentServer,jaxb2Marshaller,annotationMethodHandlerAdapter]; root of factory hierarchy

2011-6-24 21:22:08 org.springframework.oxm.jaxb.Jaxb2Marshaller createJaxbContextFromClasses

資訊: Creating JAXBContext with classes to be bound [class com.upc.upcgrid.guan.springMvcRestTest.bean.student.Student,class com.upc.upcgrid.guan.springMvcRestTest.bean.student.StudentList]

2011-6-24 21:22:09 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler

資訊: Mapped URL path [/students/{id}] onto handler [com[email protected]]

2011-6-24 21:22:09 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler

資訊: Mapped URL path [/students/{id}.*] onto handler [com[email protected]]

2011-6-24 21:22:09 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler

資訊: Mapped URL path [/students/{id}/] onto handler [com[email protected]]

2011-6-24 21:22:09 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler

資訊: Mapped URL path [/students] onto handler [com[email protected]]

2011-6-24 21:22:09 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler

資訊: Mapped URL path [/students.*] onto handler [com[email protected]]

2011-6-24 21:22:09 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler

資訊: Mapped URL path [/students/] onto handler [com[email protected]]

2011-6-24 21:22:09 org.springframework.web.servlet.FrameworkServlet initServletBean

資訊: FrameworkServlet 'rest': initialization completed in 969 ms

2011-06-24 21:22:09.125:INFO::Started [email protected]:8586

2011-06-24 21:22:09.125:INFO::Started [email protected]:8585

從輸出的内容可以看出,Spring服務已經正常啟動,并且Jetty将會在本地的8586和8585兩個端口進行監聽。

7.  編寫用戶端測試程式

Rest的用戶端可以使用Spring的RestTemplate進行編寫。為了給使用這屏蔽掉我們背景與伺服器進行通信的複雜操作,我們編寫了一個RestUtility,這個類主要完成與伺服器的通信。

RestUtility.java

public class RestUtility {

    private static String url = "http://202.194.158.128:8586/SpringMVCRestTest/rest/students/";   

    private static RestTemplate restTemplate;

    static {

       createRestTemplate();

    }

    public static RestTemplate getRestTemplate(){   

       return restTemplate;

    }

    private static void createRestTemplate() {

       restTemplate = new RestTemplate();

       List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();//消息體轉換器清單

       MarshallingHttpMessageConverter marshalConverter = new MarshallingHttpMessageConverter();//xom類型的消息體轉換器

       Jaxb2Marshaller marshaller = new Jaxb2Marshaller();//建立JAXB2類型的xom環境

       marshaller.setClassesToBeBound(Student.class,StudentList.class);//将類綁定到JAXB2

       marshalConverter.setMarshaller(marshaller);//設定編組器

       marshalConverter.setUnmarshaller(marshaller);//設定解組器

       converters.add(marshalConverter);//将xom消息體轉換器添加到清單 

       converters.add(new StringHttpMessageConverter());

       restTemplate.setMessageConverters(converters);//将轉換器清單放入RestTemplate

    }

    public static void addStudent(Student student) throws RestClientException, URISyntaxException{

       System.out.println(restTemplate.postForObject(new URI(url), student,String.class));

    }

    public static Student getStudent(String id){

       return restTemplate.getForObject(url+"{id}", Student.class, id);

    }

    public static void updateStudent(String id,Student student){

       restTemplate.put(url+"{id}", student, id);

    }

    public static void deleteStudent(String id){

       restTemplate.delete(url+"{id}", id);

    }

    public static StudentList getAllStudents() throws RestClientException, URISyntaxException{

       return restTemplate.getForObject(new URI(url),StudentList.class);

    }

}

這個類主要使用RestTemplate對伺服器進行通信的。

最後給出一個測試程式,并給出結果.

public class RestClient {

    public static void main(String[] args) throws RestClientException, URISyntaxException {

       Student student = new Student();

       student.setAge(20);

       student.setName("Mary");

       student.setId("05080416"); 

       RestUtility.addStudent(student);      

       student.setAge(21);

       student.setName("Lucy");

       student.setId("05080411");

       RestUtility.addStudent(student);

       student = RestUtility.getStudent("05080416");

       System.err.println(student.getName());

       StudentList sl = RestUtility.getAllStudents();

       for(Student s : sl.getStudents())

       {

           System.err.println(s.getName());

       }

}

在測試程式中,我們先建立了兩個學生,并将兩個學生添加到遠端,之後從遠端查詢了一個學生,并輸出資訊,然後查詢了所有學生,輸出資訊。整個過程沒有對錯誤進行處理。

執行輸出:

2011-6-24 21:36:55 org.springframework.oxm.jaxb.Jaxb2Marshaller createJaxbContextFromClasses

資訊: Creating JAXBContext with classes to be bound [class com.upc.upcgrid.guan.springMvcRestTest.bean.student.Student,class com.upc.upcgrid.guan.springMvcRestTest.bean.student.StudentList]

05080416

05080411

Mary

Lucy

Mary

8.  程式結構

使用Spring MVC 搭建Rest服務

Jar包:

使用Spring MVC 搭建Rest服務

參考:

    Spring3.0官方文檔

    Spring MVC與JAX-RS對比:http://www.infoq.com/articles/springmvc_jsx-rs

    教材:Restful Java with JAX-RS

    論文:Architectural Styles and the Design of Network-based Software Architectures

    Jetty:http://blog.sina.com.cn/s/blog_616e189f0100r1fs.html

    JAXB:http://blog.sina.com.cn/s/blog_616e189f0100slij.html