本文基于《Spring實戰(zhàn)(第4版)》所寫目溉。
Rest含義:
- 表述性(Representational):REST資源實際上可以用各種形式來進(jìn)行表述,包括XML、JSON(JavaScript Object Notation)甚至HTML—最適合資源使用者的任意形式;
- 狀態(tài)(State):當(dāng)使用REST的時候永部,我們更關(guān)注資源的狀態(tài)而不是對資源采取的行為;
- 轉(zhuǎn)移(Transfer):REST涉及到轉(zhuǎn)移資源數(shù)據(jù)呐矾,它以某種表述性形式從一個應(yīng)用轉(zhuǎn)移到另一個應(yīng)用苔埋。
REST的動作(HTTP的方法)以及匹配的CRUD動作:
- Create: POST
- Read: GET
- Update: PUT或PATCH
- DELETE: DELETE
Spring支持以下方式來創(chuàng)建REST資源:
- 控制器可以處理所有的HTTP方法,包含四個主要的REST方法:GET蜒犯、PUT组橄、DELETE以及POST。Spring 3.2及以上版本還支持PATCH方法罚随;
- 借助@PathVariable注解玉工,控制器能夠處理參數(shù)化的URL(將變量輸入作為URL的一部分);
- 借助Spring的視圖和視圖解析器淘菩,資源能夠以多種方式進(jìn)行表述遵班,包括將模型數(shù)據(jù)渲染為XML、JSON潮改、Atom以及RSS的View實現(xiàn)狭郑;
- 可以使用ContentNegotiatingViewResolver來選擇最適合客戶端的表述;
- 借助@ResponseBody注解和和各種HttpMethodConverter實現(xiàn)汇在,能夠替換基于視圖的渲染方式翰萨;
- 類似地,@RequestBody注解以及HttpMethodConverter實現(xiàn)可以將傳入的HTTP數(shù)據(jù)轉(zhuǎn)化為傳入控制器處理方法的Java對象糕殉;
- 借助RestTemplate亩鬼,Spring應(yīng)用能夠方便地使用Rest資源。
Spring提供了兩種方法將資源的Java表述轉(zhuǎn)換為發(fā)送給客戶端的表述形式:
- 內(nèi)容協(xié)商(Content negotiation):選擇一個視圖阿蝶,它能夠?qū)⒛P弯秩緸槌尸F(xiàn)給客戶端表述形式雳锋。不過由于它只能決定資源該如何渲染到客戶端,并沒有涉及到客戶端要發(fā)送什么樣的表述給控制器使用羡洁,比如客戶端發(fā)送JSON或XML玷过,它就無法提供幫助了。而且還有其他限制,不建議使用冶匹。
- 消息轉(zhuǎn)換器(Message conversion):通過一個消息轉(zhuǎn)換器將控制器所返回的對象轉(zhuǎn)換為呈現(xiàn)給客戶端的表述形式习劫。
使用HTTP信息轉(zhuǎn)換器
當(dāng)使用消息轉(zhuǎn)換功能時咆瘟,DispatcherServlet不再將模型數(shù)據(jù)傳送到視圖中嚼隘。實際上,根本就沒有模型袒餐,也沒有視圖飞蛹,只有控制器產(chǎn)生的數(shù)據(jù),以及消息轉(zhuǎn)換器轉(zhuǎn)換數(shù)據(jù)之后所產(chǎn)生的資源表述灸眼。
Spring自帶了各種各樣的轉(zhuǎn)換器卧檐,比如客戶端通過請求的Accept頭信息表明它能接受“application/json”,并且Jackson JSON在類路徑下焰宣,那么處理方法返回的對象將交給MappingJacksonHttpMessageConverter,并由它轉(zhuǎn)換為返回客戶端的JSON表述形式霉囚。大部分轉(zhuǎn)換器都是自動注冊的,不需要Spring配置匕积。但是為了支持它們盈罐,需要添加一些庫到應(yīng)用程序的類路徑下。
如果使用了消息轉(zhuǎn)換功能的話闪唆,我們需要告訴Spring跳過正常的模型/視圖流程盅粪,并使用消息轉(zhuǎn)換器。最簡單的方式是為控制器方法添加@ResponseBody注解悄蕾。例如票顾,如下程序:
@RequestMapping(method=RequestMethod.GET, produces="application/json")
public @ResponseBody List<Spittle> spittles (
@RequestParam(value="max",defaultValue=MAX_LONG_AS_SPRING)) long max,
@RequestParam(value="count",defaultValue="20") int count) {
return spittleRepository.findSpittles(max, count);
}
@ResponseBody注解會告知Spring,我們要將返回的對象作為資源發(fā)送給客戶端帆调,并將其轉(zhuǎn)換為客戶端可接受的表述形式奠骄。更具體地講,DispatcherServlet將會考慮到請求中Accept頭部信息番刊,并查找能夠為客戶端提供所需表述形式的消息轉(zhuǎn)換器(根據(jù)類路徑下實現(xiàn)庫)戚揭。
需要注意的是,默認(rèn)情況下撵枢,Jackson JSON庫在將返回的對象轉(zhuǎn)換為JSON資源表述時民晒,會使用反射。如果重構(gòu)了Java類型锄禽,比如添加潜必、移除或重命名屬性,那么產(chǎn)生的JSON也將會發(fā)生變化沃但。但是磁滚,我們可以在Java類型上使用Jackson的映射注解,改變產(chǎn)生JSON的行為。
談及Accept頭部信息垂攘,在@RequestMapping注解中维雇,我們使用了produces屬性表明這個方法只處理預(yù)期輸出為JSON的請求,其他任何類型的請求晒他,都不會被這個方法處理吱型。這樣的請求會被其他的方法來進(jìn)行處理,或者返回客戶端HTTP 406響應(yīng)陨仅。
與@ResponseBody類似津滞,@RequestBody也能告訴Spring查找一個消息轉(zhuǎn)換器,將來自客戶端的資源表述為對象灼伤。例如:
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle) {
return spittleRepository.save(spittle);
}
通過使用注解触徐,@RequestMapping表明它只能處理“/spittles”(在類級別的@RequestMapping中進(jìn)行了聲明)的POST請求。POST請求體中預(yù)期要包含一個Spittle的資源表述狐赡。因為Spittle參數(shù)上使用了@RequestBody撞鹉,所以Spring將會查看請求中的Content-Type頭部信息,并查找能夠?qū)⒄埱筠D(zhuǎn)換為Spittle的消息轉(zhuǎn)換器颖侄。
例如鸟雏,如果客戶端發(fā)送的Spittle數(shù)據(jù)是JSON表述形式,那么Content-Type頭部信息可能就會是“application/json”发皿。在這種情況下崔慧,DispatcherServlet會查找能夠?qū)SON轉(zhuǎn)換為Java對象的消息轉(zhuǎn)換器。
注意穴墅,@RequestMapping有一個consumes屬性惶室,我們將其設(shè)置為“application/json”。consumes屬性的工作方式類似于produces玄货,不過它會關(guān)注請求的Content-Type頭部信息皇钞。它會告訴Spring這個方法只會處理對“/spittles”的POST請求,并且要求請求的Content-Type頭部信息為“application/json”松捉。如果無法滿足這些條件的話夹界,會有其他方法來處理請求。
Spring 4.0引入了@RestController注解隘世。如果在控制器類上使用@RestController來代替@Controller的話可柿,Spring將會為該控制器的所有處理方法應(yīng)用消息轉(zhuǎn)換功能。我們不必為每個方法都添加@ResponseBody了丙者。添加@RestController注解复斥,此類中所有處理器方法都不需要使用@ResponseBody注解了,因為控制器使用了@RestController械媒,所有它的方法所返回的對象將會通過消息轉(zhuǎn)換機(jī)制目锭,產(chǎn)生客戶端所需的資源表述评汰。
發(fā)送錯誤信息到客戶端
如果一個處理器方法本應(yīng)返回一個對象,但由于查找不到相應(yīng)的對象而返回null痢虹。我們考慮一下在這種場景下應(yīng)該發(fā)生什么被去。至少,狀態(tài)碼不應(yīng)是200奖唯,而應(yīng)該是404惨缆,告訴客戶端它們所要求的內(nèi)容沒有找到。如果響應(yīng)體中能夠包含錯誤信息而不是空的話就更好了臭埋。
Spring提供了多種方式來處理這樣的場景:
- 使用@ResponseStatus注解可以指定狀態(tài)碼踪央;
- 控制器方法可以返回ResponseEntity對象臀玄,該對象能夠包含更多響應(yīng)相關(guān)的元數(shù)據(jù)瓢阴;
- 異常處理器能夠應(yīng)對錯誤場景,這樣處理器方法就能關(guān)注于正常的狀況健无。
使用ResponseEntity
作為@ResponseBody的替代方案荣恐,控制器方法可以返回一個ResponseEntity對象。ResponseEntity中可以包含響應(yīng)相關(guān)的元數(shù)據(jù)(如頭部信息和狀態(tài)碼)以及要轉(zhuǎn)換成資源表述的對象累贤。
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
Spittle spittle = spittleRepository.findOne(id);
HttpStatus status = spittle != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
return new RepositoryEntity<Spittle>(spittle, status);
}
注意叠穆,如果返回ResponseEntity的話,那就沒有必要在方法上使用@ResponseBody注解了臼膏。
如果我們希望在響應(yīng)體中包含一些錯誤信息硼被。我們需要定義一個包含錯誤信息的Error對象:
public class Error {
private int code;
private String message;
public Error(int code ,String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
然后,我們可以修改spittleById()渗磅,讓它返回Error:
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<?> spittleById(@PathVariable long id ) {
Spittle spittle = spittleRepository.findOne(id);
if (spittle == null) {
Error error = new Error(4, "Spittle [" + id + "] not found");
return new ResponseEntity<Error> (error, HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
}
處理錯誤
我們重構(gòu)一下代碼來使用錯誤處理器嚷硫。首先,定義能夠?qū)ο骃pittleNotFoundException的錯誤處理器:
@ExceptionHandler(SpittleNotFoundException.class)
public ResponseEntity<Error> spittleNotFound(SpittleNotFoundException e) {
long spittleId = e.getSpittleId();
Error error = new Error(4, "Spittle [" + spittleId + "] not found");
return new ResponseEntity<Error> (error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler注解能夠用到控制器方法中始鱼,用來處理特定的異常仔掸。至于SpittleNotFoundException,它是一個很簡單異常類:
public class SpittleNotFoundException extends RuntimeException {
private long spittleId;
public SpittleNotFoundException(long spittleId) {
this.spittleId = spittleId;
}
public long getSpittleId() {
return spittleId;
}
}
現(xiàn)在医清,我們可以移除掉spittleById() 方法中大多數(shù)的錯誤代碼:
@RequestMapping(value="/{id}" , method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
Spittle spittle = spittleRepository.findOne(id);
if (spittle == null) { throw new SpittleNotFoundException(id); }
return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
}
更簡潔的版本是(控制器類上使用@RestController)
@RequestMapping(value="/{id}" , method=RequestMethod.GET)
public Spittle spittleById(@PathVariable long id) {
Spittle spittle = spittleRepository.findOne(id);
if (spittle == null) { throw new SpittleNotFoundException(id); }
return spittle;
}
鑒于錯誤處理器的方法會始終返回Error起暮,并且HTTP狀態(tài)碼為404,那么現(xiàn)在我們可以對spittleNotFound() 方法進(jìn)行類似的清理(控制器類上使用@RestController):
@ExceptionHandler(SpittleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Error spittleNotFound(SpittleNotFoundException e) {
long spittleId = e.getSpittleId();
return Error error = new Error(4, "Spittle [" + spittleId + "] not found");
}
在響應(yīng)中設(shè)置頭部信息
如果我們需要在POST請求后会烙,返回201且把資源的URL返回給客戶端负懦,可以用@ResponseEntity實現(xiàn)
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
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;
}
其實我們沒有必要手動構(gòu)建URL,Spring 提供了UriComponentsBuilder柏腻。它是一個構(gòu)建類纸厉,通過逐步指定URL中的各種組成部分(如host、端口葫盼、路徑以及查詢)残腌,我們能夠使用它來構(gòu)建UriComponents實例。
為了使用UriComponentsBuilder,我們需要做的就是在處理器方法中將其作為一個參數(shù)抛猫,如下面的程序清單所示蟆盹。
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
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;
}
在處理器方法所得到的UriComponentsBuilder中,會預(yù)先配置已知的信息如host闺金、斷端口以及Servlet內(nèi)容逾滥。
注意,路徑的構(gòu)建分為兩步败匹。第一步調(diào)用path()方法寨昙,將其設(shè)置“/spittles/”,也就是這個控制器所能處理的基礎(chǔ)路徑掀亩。然后舔哪,在第二次調(diào)用path()的時候,使用了已使用Spittle的ID槽棍。在路徑設(shè)置完成之后捉蚤,調(diào)用build()方法來構(gòu)建UriComponents對象,根據(jù)這個對象調(diào)用toUri()就能得到新創(chuàng)建Spittle的URI炼七。
了解RestTemplate的操作
RestTemplate可以減少我們使用HttpClient創(chuàng)建客戶端所帶來的樣板式代碼缆巧。它定義了36個(只有11個獨立方法,其他都是重載這些方法)與REST資源交互的方法豌拙,其中的大多數(shù)都對應(yīng)于HTTP的方法陕悬。下表展示了這11個獨立方法
方法 | 描述 |
---|---|
delete() | 在特定的URL上對資源執(zhí)行HTTP DELETE操作 |
exchange() | 在URL上執(zhí)行特定的HTTP方法,返回包含對象的ResponseEntity按傅,這個對象是從響應(yīng)體中映射得到的 |
execute() | 在URL上執(zhí)行特定的HTTP方法捉超,返回一個從響應(yīng)體映射得到的對象 |
getForEntity() | 發(fā)送一個HTTP GET請求,返回的ResponseEntity包含了響應(yīng)體所映射成的對象 |
getForObject() | 發(fā)送一個HTTP GET請求逞敷,返回的請求體將映射為一個對象 |
headForHeaders() | 發(fā)送HTTP HEAD請求狂秦,返回包含特定資源URL的HTTP頭 |
optionsForAllow() | 發(fā)送HTTP OPTIONS請求,返回對特定的URL的Allow頭信息 |
postForEntity() | POST數(shù)據(jù)到一個URL推捐,返回包含一個對象的ResponseEntity裂问,這個對象是從響應(yīng)體中映射得到的 |
postForLocation() | POST數(shù)據(jù)到一個URL,返回新創(chuàng)建資源的URL |
postForObject() | POST數(shù)據(jù)到一個URL牛柒,返回根據(jù)響應(yīng)體匹配形成的對象 |
put() | PUT資源到特定的URL |
GET資源
getForObject()都有三種形式的重載
<T> T getForObject(URI url, Class<T> responseType)
throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
throws RestClientException;
<T> T getForObject(String url, Class<T> responseType,
Map<String,?> uriVariables) throws RestClientException;
檢索資源
public Profile fetchFacebookProfile(String id) {
RestTemplate rest = new RestTemplate();
return rest.getForObject("http://graph.facebook.com/{spritter}",Profile.class, id);
}
另一種方案
public Profile fetchFacebookProfile(String id) {
Map<String, String> urlVariables = new HashMap<>();
urlVariables.put("id", id);
RestTemplate rest = new RestTemplate();
return rest.getForObject("http://graph.facebook.com/{spritter}",
Profile.class, urlVariables);
}
getForEntity()都有三種形式的重載
<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)
throws RestClientException;
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType,
Object... uriVariables) throws RestClientException;
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType,
Map<String,?> uriVariables) throws RestClientException;
抽取響應(yīng)的元數(shù)據(jù)
public Spittle fetchSpittle(long id) {
RestTemplate rest = new RestTemplate();
ResponseEntity<Spittle> response = rest.getForEntity(
"http://localhost:8080/spittr-api/spittles/{id}",
Spittle.class, id);
if(response.getStatusCode() == HttpStatus.NOT_MODIFIED) {
throw new NotModifiedException();
}
return response.getBody();
}
PUT資源
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) throws SpitterException {
RestTemplate rest = new RestTemplate();
String url = "http://localhost:8080/spittr-api/spittles/" + spittle.getId();
rest.put(URI.create(url), spittle);
}
public void updateSpittle( Spittle spittle) throws SpitterException {
RestTemplate rest = new RestTemplate();
String url = "http://localhost:8080/spittr-api/spittles/{id}";
rest.put(url, spittle, spittle.getId());
}
DLELTE資源
delete()方法有三個版本
void delete(String url ,Object... uriVariables) throws RestClientException;
void delete(String url ,Map<String, ?> uriVariables) throws RestClientException;
void delete(URI url) throws RestClientException;
POST資源數(shù)據(jù)
postForObject() 方法的三個變種簽名如下:
<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;
在所有情況下堪簿,第一個參數(shù)都是資源要POST的URL,第二個參數(shù)是要發(fā)送的對象皮壁,而第三個參數(shù)是預(yù)期返回的Java類型椭更。在將URL作為String類型的兩個版本中,第四個參數(shù)指定了URL變量(要么是可變參數(shù)列表蛾魄,要么是一個Map)虑瀑。
例如
public Spitter postSpitterForObject(Spitter spitter) {
RestTemplate rest = new RestTemplate();
return rest.postForObject("http://localhost:8080/spittr-api/spitters",
spitter, Spitter.class);
}
postForEntity() 方法的三個變種簽名如下:
<T> ResponseEntity<T> postForEntity(URI url, Object request,
Class<T> responseType) throws RestClientException;
<T> ResponseEntity<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/spittr-api/spitters",
spitter, Spitter.class);
Spitter spitter = response.getBody();
URI url = response.getHeaders().getLocation();
如果只是需要的是Location頭信息的值湿滓,那么使用RestTemplate的postForLocation()方法會更簡單。以下是postForLocation()的三個方法簽名:
URI postForLocation(String url, Object request, Object... 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/spittr-api/spitters",
spitter).toString();
}
交換資源
如果想在發(fā)送給服務(wù)端的請求中設(shè)置頭信息的話舌狗,那就是RestTemplate的exchange()的用武之地了叽奥。
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) throws RestClientException;
<T> ResponseEntity<T> exchange(String url, HttpMethod method,
HttpEntity<?> requestEntity, Class<T> responseType,
Map<String,?> uriVariables) throws RestClientException;
exchange() 方法使用HttpMethod參數(shù)來表明要使用的HTTP動作。根據(jù)這個參數(shù)的值痛侍,exchange()能夠執(zhí)行與其他RestTemplate方法一樣的工作朝氓。
例如,從服務(wù)器端獲取Spitter資源的一種方式是使用RestTemplate的getForEntity()方法主届,如下所示:
ResponseEntity<Spitter> response = rest.getForEntity(
"http://localhost:8080/spittr-api/spitters/{spitter}",
Spitter.class, spitterId);
Spitter spitter = response.getBody();
在下面的代碼片段中赵哲,可以看到exchange() 也可以完成這項任務(wù):
ResponseEntity<Spitter> response = rest.exchange(
"http://localhost:8080/spittr-api/spitters/{spitter}",
HttpMethod.GET, null ,Spitter.class, spitterId);
Spitter spitter = response.getBody();
如果不指明頭信息,exchange() 對Spitter的GET請求會帶有如下的頭信息:
GET /Spitter/spitters/habuma HTTP/1.1
Accept: application/xml, test/xml, application/*+xml, application/json
Content-Length: 0
User-Agent: Java/1.6.0_20
Host: location:8080
Connection: keep-alive
如果我們需要將“application/json”設(shè)置為Accept頭信息的唯一值君丁。
設(shè)置請求頭信息是很簡單的枫夺,只需要構(gòu)造發(fā)送給exchange()方法的 HttpEntity對象即可,HttpEntity中包含承載頭信息的MultiValueMap:
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Accept", "application/json");
HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers);
如果這是一個PUT或POST請求谈截,我們需要為HttpEntity設(shè)置在請求體中發(fā)送的對象—對于GET請求來說筷屡,這是沒有必要的涧偷。
現(xiàn)在我們可以傳入HttpEntity來調(diào)用exchange();
ResponseEntity<Spitter> response = rest.exchange(
"http://localhost:8080/spittr-api/spitters/{spitter}",
HttpMethod.GET, headers ,Spitter.class, spitterId);
Spitter spitter = response.getBody();