RPC是面向服務的,並關注與行為和動作;而REST是面向資源的,強調描述應用程序的事務的名詞。REST將資源的狀態以最適合客戶端或服務端的形式從服務器端轉移到客戶端。
REST
Representational(表述性):REST資源實際上可以用各種形式來進行表述,包括XML,JSON深圳HTML
State(狀態):當使用REST的時候,我們更關注資源的狀態而不是對資源采取的行為
Transfer(轉移):REST涉及到轉移資源數據,它以某種表屬性形式從一個應用轉移到另一個應用
在REST中,資源通過URL進行識別和定位。REST中的行為是通過HTTP方法定義的。這些HTTP方法通常會匹配如下CRUD動作:
Create:POST
Read:GET
Update:PUT或PATCH
Delete:DELETE
Spring支持一下方式來創建REST資源:
控制器可以出了力所有的HTTP方法,包含四個主要的REST方法:GET、PUT、DELETE和POST。Spring3.2及以上版本還支持PATCH方法
借助@PathVariable注解,控制其能夠處理參數化URL
借助Spring的視圖和視圖解析器,資源能夠以多種方式進行表述,包括將模型數據渲染為XML、JSON、Atom以及RSS的View實現
可以使用ContentNegotitaingViewResolver來選擇最合適客戶端的表述
借助@ResponseBody注解和各種HttpMethodConvertor實現,能夠替換基於視圖的渲染方式
@RequestBody注解以及HttpMethodConvertor實現可以將傳入的HTTP數據轉化為傳入控制器處理方法的Java對象
借助RestTemplate,Spring應用能夠方便的使用REST資源
Spring提供了兩種方法將資源的Java表述形式轉換為發送給客戶端的表述形式
內容協商(Content negotiation):選擇一個視圖,它能夠將模型渲染為呈現給客戶端的表述形式
消息轉換器(Message conversion):通過一個消息轉換器將控制器所返回的對象轉換為呈現給客戶端的表述形式
Spring的ContentNegotiatingViewResolver是一個特殊的視圖解析器,它考慮到了客戶端所需要的內容類型。按照其最簡單的形式,ContentNegitiatingViewResolver可以按照如下配置,它可以完成兩個功能:確定請求的媒體類型和找到合適請求媒體類型的最佳視圖
@Bean public ViewResolver cnViewResolver(){ return new ContentNegotiatingViewResolver(); }
確定請求的媒體類型
ContentNegotitaingViewResolver會考慮Accept頭部信息並使用它所請求的媒體類型,但是它會首先查看URL的文件擴展名。如果URL在結尾處有文件擴展名,ContentNegotiatingViewResolver將會給予該擴展名確定所需的類型,如果擴展名是.json,那么所需的內容類型必須是"application/json"。如果擴展名是.xml,那么客戶端請求的是"application/xml"。如果根據文件擴展名不能得到任何媒體類型的話,那么就會考慮Accept頭部信息。此時,Accept頭部信息中的值就表明了客戶端想要的MIME類型。如果沒有Accept頭部信息,並且擴展名也無法提供任何幫助的話,ContentNegotiatingViewResolver將會使用/作為默認的內容,這意味着客戶端必須要接受服務器發送的任何形式的表述。一旦內容類型確定之后,ContentNegotiatingViewResolver就會將邏輯視圖名解析為渲染模型的View,ContentNegotiatingViewResolver本身不會解析視圖,而是委托給其他的視圖解析器,讓它們來解析視圖。ContentNegotiatingViewResolver要求其他的視圖解析器將邏輯視圖名解析為視圖。解析得到的每個視圖都會放到一個列表中,這個列表裝配完成后,ContentNegotiatingViewResolver會循環客戶端請求的所有媒體類型,在候選的收中查找能夠產生對應內容類型的視圖。第一個匹配的視圖會用來渲染模型。
影響媒體類型的選擇
通過設置ContentNegotiationManager可以改變選擇媒體類型的策略,ContentNegotiationManager可以做到
指定默認的內容類型,如果根據請求無法得到內容類型的話,會使用默認值
通過請求參數指定內容類型
忽視請求的Accept頭部信息
將請求的擴展名映射為特定的媒體類型
將JAF(Java Activation Framework)作為根據擴展名名查找媒體類型的備用方案
有三種配置ContentNegotiationManager方法
直接聲明一個ContentNegotiationManager類型的bean
通過ContentNegotiationManagerFactoryBean間接創建bean
重載WebMvcConfigurerAdapater的configureContentNegotiation()方法。
使用Java配置,獲得ContentNegotiationManager的最簡便方法就是擴展WebMvcConfigurerAdapter並重新configureContentNegotiation()方法。
@Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer){ configurer.defaultContentType(MediaType.APPLICATION_JSON); }
ContentNegotiatingViewResolver最大的優勢在於它在Spring MVC之上構建了REST資源表述層,控制器代碼無需修改。相同一套控制器方法能夠產生HTML,也可以產生JSON或XML。ContentNegotiatingViewResolver作為ViewResolver的實現,它只能決定資源該如何渲染到客戶端,並沒有涉及到客戶端要發送什么樣的表述給控制器,如果客戶端發送JSON或XML的話,ContentNegotiatingViewResolver就無法識別。
使用HTTP信息轉換器
消息轉換(message conversion)提供了一種更為直接的方式,它能將控制器產生的數據轉換為服務於客戶端的表述形式。當使用消息轉換功能時,DispatcherServlet不再需要那么麻煩地將模型數據傳送到視圖中。
Spring自帶了各種各樣的轉換器,用於實現資源表述與各種Java類型之間的互相轉換
AtomFeedHttpMessageConverter Rome Feed對象和Atom feed之間相互轉換(如果Rome包在類路徑下將會進行注冊)
BufferedImageHttpMessageConverter BufferedImages與圖片二進制數據之間相互轉換
ByteArrayHttpMessageConverter 讀取/寫入字節數組。從所有媒體類型中讀取,並以application/octet-stream格式寫入
FormHttpMessageConverter 將application/x-www-form-urlencode內容讀入到MultiValueMap<String, String>中,也會將MultiValueMap<String, String>寫入到application/x-www-form-urlencode中或將MultiValueMap<String, Object>寫入到multipart/form-data中。
Jaxb2RootElementHttpMessageConverter 在XML(text/xml或application/xml)和使用JAXB2注解的對象間互相讀取和寫入。如果JAXB v2庫在類路徑下,將進行注冊
MappingJacksonHttpMessageConverter 在JSON和類型化的對象或非類型化的HashMap間相互讀取和寫入,如果Jackson JSON庫在類路徑下,將進行注冊
MappingJackson2HttpMessageConverter 在JSON和類型化的對象或非類型化的HashMap間互相讀取和寫入,如果Jackson 2 JSON庫在類路徑下,將進行注冊
MarshallingHttpMessageConverter 使用注入的編排器和解排器來讀入和寫入XML。支持的編排器和解排器保羅Castor,JAXB2,JIBX,XMLBeans以及Xstream。
ResourceHttpMessageConverter 讀取或寫入Resource
RssChannelHttpMessageConverter 在RSS feed和Rome Channel對象間互相讀取和寫入。若Rome庫在類路徑下,將進行注冊
SourceHttpMessageConverter 在XML和javax.xml.transform.Source對象間互相讀取和寫入
StringHttpMessageConverter 將所有媒體類型讀取為String,將String寫入為text/plain
XmlAwareFormHttpMessageConverter FormHttpMessageConverter的擴展,使用SourceHttp MessageConverter來支持基於XML部分
正常情況下,當處理方法返回Java對象時,這個對象會放在模型中並在視圖中渲染使用。但使用了消息轉換功能的話,需要告訴Spring跳過正常的模型/視圖流程,並使用消息轉換器。最簡單的方法是在Controller上添加@ResponseBody注解
默認情況下,Jackson JSON庫將返回的對象轉換為JSON資源表述時,會使用反射。但如果重構Java時,產生的JSON也可能會發生變化。因此我們可以在Java類型行使用Jackson的映射注解,從而改變產生JSON的行為。
當處理請求時,@ResponseBody和@RequestBody是啟用消息轉換的一種簡潔和強大方式。若每個方法都需要信息轉換功能,則這些注解會帶來一定的重復性。Spring4.0引入了RestController注解,用@RestController代替@Controller,Spring將會為該控制器的所有處理方法應用消息轉換功能。
Spring提供了三種方式來處理業務邏輯異常
使用@ResponseStatus注解可以指定狀態碼
控制器方法可以返回ResponseEntity對象,改對象能夠包含更多響應相關的元數據
異常處理器能夠應對錯誤場景,這樣處理器方法就能夠關注與正常的情況
使用@ResponseEntity作為@ResponseBody的替代方案,控制器方法可以返回一個ResponseEntity對象。ResponseEntity中可以包含響應相關的元數據以及要轉換成資源表述的對象。
@RequestMapping(value="/{id}") public ResponseEntity<Order> getOrderById(@PathVariable String id){ Order order = orderRepository.findOne(id); HttpStatus status == order != null? HttpStatus.OK : HttpStatus.NOT_FOUND; return new ResponseEntity<Order>(order, status); }
使用@ExceptionHandler處理異常
public class Error{ private String id; private String message; public Error(String id, String message){ this.id = id; this.message = message; } public void setId(String id){ this.id = id; } public void getId(){ return id; } public void setMessage(String message){ this.message = message; } public String getMessage(){ return message; } } public class ModelNotFoundException extends RuntimeException{ private String id; public ModelNotFoundException(String id){ this.id = id; } public String getId(){ return id; } } // controller @RequestMapping("/getException") public Order getException(String id){ throw new ModelNotFoundException("id"); } @ExceptionHandler(ModelNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Error modelNotFound(ModelNotFoundException e){ return new Error(4, e.getId + " not found"); }
當創建新資源的時候,將資源的URL放在響應的Location頭部信息中,並返回給客戶端是一種方式。HttpHeaders用來存放在響應中包含的頭部信息值。HttpHeaders是MultiValueMap<String, String>的特殊實現。
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle){ Spittle spittle = spittleRepository.save(spittle); HttpHeaders headers = new HttpHeaders(); URI locationUri = URI.create("http://localhost:8080/spittr/spittles/" + spittle.getId()); headers.setLocation(locationUri); ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(spittle, headers, HttpStatus.CREATED); return responseEntity; }
Spring提供了UriComponentsBuilder,它是一個構建類,通過逐步指定URL中的各個組成部分(host,端口,路徑以及查詢)。
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb){ Spittle spittle = spittleRepository.save(spittle); HttpHeaders headers = new HttpHeaders(); URI locationUri = ucb.path("/spittles/").path(String.valueOf(spittle.getId())).build().toUri(); headers.setLocation(locationUri); ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(spittle, headers, HttpStatus.CREATED); return responseEntity; }
RestTemplate定義了36個與REST資源交互的方法,其中大多數對應與HTTP的方法。除了TRACE外,RestTemplate涵蓋了所有Http動作,execute()和exchange()提供了較低層次的通用方法來使用任意的HTTP方法。
大多數操作以三種方法的形式進行了重載:
一個使用java.net.URI作為URL格式,不支持參數化URL
一個使用String作為URL格式,並使用Map指明URL參數
一個使用String作為URL格式,並使用可變參數列表指明URL參數
RestTemplate定義了11個獨立的操作,每一個都有重載,一共36個方法
delete() 在特定的URL上對資源執行HTTP DELETE操作
exchange() 在URL上執行特定的HTTP方法,返回包含對象的ResponseEntity,這個對象是從響應體重映射得到的
execute() 在URL上執行特定的HTTP方法,返回一個從響應體映射得到的對象
getForEntity() 發送一個HTTP GET請求,返回的ResponseEntity包含了響應體所映射成的對象
getForObject() 發送一個HTTP GET遷就,返回的請求體映射為一個對象
headForHeaders() 發送HTTP HEAD請求,返回包含特定資源URL的HTTP頭
optionsForAlllow() 發送HTTP OPTIONS請求,返回對特定URL的Allow頭信息
postForEntity() POST數據到一個URL,返回包含一個對象的ResponseEntity,這個對象時從響應體中映射得到的
postForLocation() POST一個數據到URL,返回新創建資源的URL
postForObject() POST數據到一個URL,返回根據響應體匹配形成的對象
put() PUT資源到特定的URL
public Profile fetchFacebookProfile(String id){ RestTemplate rest = new RestTemplate(); return new rest.getForObject("http://graph.facebook.com/{spittler}", Profile.class, id); } public Spittle[] fetchFacebookProfile(String id){ Map<String, String> urlVariables = new HashMap<String, String>(); urlVariables.put("id", id); RestTemplate rest = new RestTemplate(); return rest.getForObject("http://graph.facebbook.com/{spitter}", Profile.class, urlVariables); }
getForEntity()會在ResponseEntity中返回相同的對象,而且ResponseEntity會帶有額外的信息。
getHeaders()方法返回一個HttpHeaders對象,該對象提供了多個便利的方法:public List<MediaType> getAccept();public List<Charset> getAcceptCharset();public Set<HttpMethod> getAllow();public String getCacheControl();public List<String> getConnection();public long getContentLength();public MediaType getContentType();public long getDate();public String getETag();public long getExpires();public long getIfNotModifiedSince();public List<String> getIfNoneMatche();public long getLastModified();public URI getLocation();public String getOrigin();public String getPragma();public String getUpgrade();
PUT資源
RestTemplate提供了三個簡單的put()方法。
void put(URI url, Object request) throws RestClientException;
void put(String url, Object request, Object... uriVariables) throws RestClientException;
void put(String url, Object request, Map<String, ?> uriVariables) throws RestClientException;
public void updateSpittle(Spittle spittle){ RestTemplate rest = new RestTemplate(); String url = "http://localhost:8080/spittles/" + spittle.getId(); rest.put(URI.create(url), spittle); }
public void updateSpittle(Spittle spittle){
RestTemplate rest = new RestTemplate();
rest.put("http://localhost:8080/spittles/{id}", spittle, spittle.getId());
}
public void updateSpittle(Spittle spittle){
RestTemplate rest = new RestTemplate();
Map<String, String> params = new HashMap<String, String>();
params.put("id", spittle.getId());
rest.put("http://localhost:8080/spittles/{id}", spittle, params);
}
在所有版本的put()中,第二個參數都是表示資源的Java對象,它將按照指定的URI發送到服務器端。對象將被轉換成什么樣的內容很大程度上取決於傳遞給put()方法的類型。如果給定一個String值,將會用StringHttpMessageConverter,這個值直接被寫到請求中,內容類型設置為text/plain。如果給定一個MultiValueMap<String, String>,那么這個Map中的值會被FormHttpMessageConverter以"application/x-www-form-urlencode"的格式寫到請求體中。以為傳進來的是一個java bean,所以需要一個能夠處理任意對象的信息轉換器。若類路徑下包含Jackson2庫,那么MappingJacksonHttpMessageConverter將以application/json格式將java bean寫到請求中。
DELETE資源
當不需要在服務端保留某個資源時,可以調用RestTemplate的delete()方法。
void delete(String url, Object... uriVariables) throws RestClientException;
void delete(String url, Map<String, ?> uriVariables) throws RestClientException;
void delete(URI url) throws RestClientException;
public void deleteSpittle(long id){ RestTemplate rest = new RestTemplate(); rest.delete(URI.create("http://localhost:8080/spittles/" + id)); } public void deleteSpittle(long id){ RestTemplate rest = new RestTemplate(); rest.delete("http://localhost:8080/spittles/{id}", id); }
POST資源數據
POST資源到服務端的一種方式是使用RestTemplate的postForObject()方法。第一個參數都是資源要POST的URL,第二個參數似乎要發送的對象,第三個參數是預期返回的Java類型。第四個參數指定了URL變量
<T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
public Spitter postSpitterForObject(Spitter spitter){ RestTemplate rest = new RestTemplate(); return rest.postForObject("http://localhost:8080/spitters", spitter, Spitter.class); }
postForEntity()
<T> ResponseEntity<T> postForEntity(URI url, Object request, Class<T> responseType) throws RestClientException;
<T> ResponsesEntity<T> postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
RestTemplate rest = new RestTemplate(); ResponseEntity<Spitter> response = rest.postForEntity("http://localhost:8080/spitters", spitter, Spitter.class); Spitter spitter = response.getBody(); URI url = response.getHeaders().getLocation();
在不需要將資源發送回來但需要是Location頭信息的值時,可以使用RestTemplate的postForLocation()替代postForEntity()
URI postForLocation(String url, Object request, Oject... uriVariables) throws RestClientException;
URI postForLocation(String url, Object request, Map<String, ?> uriVariables) throws RestClientException;
URI postForLocation(URI url, Object request) throws RestClientException;
public String postSpitter(Spitter spitter){ RestTemplate rest = new RestTemplate(); return rest.postForLocation("http://localhost:8080/spitters", spitter).toString(); }
交換資源
RestTemplate的exchange可以在發送給服務端的請求中設置頭信息。
<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) throws RestClientException;
<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throwns RestClientException;
<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throwns RestClientException;
ResponseEntity<Spitter> response = rest.exchange("http://localhost:8080/spitters/{spitter}", HttpMethod.GET, null, Spitter.class, spitterId); Spitter spitter = response.getBody();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Accept", "application/json");
HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers);
ResponseEntity<Spitter> response = rest.exchange("http://localhost:8080/spitters/{spiiter}", HttpMethod.GET, requestEntity, Spitter.class, spitterId);
Spitter spitter = response.getBody();