文章目录
- 1. REST简介
- 2. 创建一个REST端点
- 2.1 使用HTTP信息转换器
- 2.2 在响应体中返回资源状态
- 2.3 在请求体中接收资源状态
- 2.4 为控制器默认设置消息转换
- 3. 提供资源之外的其它内容
- 3.1 发送错误信息到客户端
- 3.1.1 使用ResponseEntity
- 3.1.2 处理错误异常
- 3.2 在响应中设置头部信息
- 4. 编写REST客户端
- 4.1 GET资源
- 4.1.1 getForObject()方法
- 4.1.2 getForEntity()方法
- 4.2 PUT资源
- 4.3 DELETE资源
- 4.4 POST资源数据
- 4.4.1 在POST请求中获取响应对象
- 4.4.2 在POST请求后获取资源位置
- 4.3 交换资源
1. REST简介
在REST中,资源通过URL进行识别和定位,关注的核心是事物而不是行为。
例如,URL为/{username}表示对这个用户的信息操作,至于是查看信息还是更新、删除信息,应该通过HTTP方法来定义,URL不应该出现动词。
HTTP方法通常会匹配CURD动作:
- Create :POST
- Read:GET
- Update:PUT或PATCH
- Delete:DELETE
通俗的来说,就是看Url就知道要什么;看http method就知道干什么;看http status code就知道结果如何;
在Spring 4.0中,支持以下方式来创建REST资源:
- 控制器可以处理所有的HTTP方法,GET、POST、PUT、PATCH、DELETE
- 借助@PathVariable注解,控制器能够处理参数化的URL
- 借助Spring的视图和视图解析器,资源能够以多种方式来进行表述。包括将模型数据渲染成XML、JSON、Atom的View实现
- 可以使用ContentNegotiatingViewResolver来选择最适合客户端的表述
- 借助@ResponseBody注解和各种HttpMethodConverter实现,能够替换基于视图的渲染方式
- @RequestBody注解以及HttpMethodConcerter实现可以将传入的HTTP数据转化为传入控制器处理方法的Java对象
- 借助RestTemplate,Spring应用能够方便地使用REST资源
2. 创建一个REST端点
一个RESTful的SpringMVC控制器,后面会对它进行改进:
@Controller
@RequestMapping("/spittles")
public class SpittleController{
private static final String MAX_LONG_AS_STRING = "9223372036854775807";
@Autowired
private SpittleRepository spittleRepository;
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
@RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
@RequestParam(value="count", defaultValue="20") int count){
return spittleRepository.findSpittles(max, count);
}
}
2.1 使用HTTP信息转换器
消息转换能够将控制器产生的数据转换为服务于客户端的表述形式。使用消息转换功能时,DispatcherServlet不需要再将模型数据传送到视图中。实际上,这里没有具体的视图,只提供前端(客户端)所需要的数据,并将数据按照指定的表述形式发送过去。
例如,假如客户端通过请求的Accept头信息表明它接受“application/json”,并且Jackson JSON在类路径下,那么处理方法返回的对象将交给MappingJacksonHttpMessageConverter,并由它转换为JSON形式。如果是“text/xml”,那么Jaxb2RootElementHttpMessageConverter将会产生XML响应。
使用这些信息转换器不需要进行配置,但是要添加一些库到类路径中。
2.2 在响应体中返回资源状态
正常情况下,处理方法返回Java对象时,这个对象会放在模型中并在视图中渲染使用。但是,使用消息转换功能,我们需要告诉Spring跳过正常的模型/视图流程,并使用消息转换器。最简单的方法是添加@ResponseBody注解:
@RequestMapping(method=RequestMethod.GET, produces="application/json")
//produces属性表明这个方法只接受预期输出为 JSON的请求,
//也就是请求的 Accept头信息需要表明它接受"aplication/json"
//其它类型的请求,即使是路径名相同的GET请求,也不会被这个处理器处理。没有合适的处理器会返回406(Not Acceptable)
public @ResponseBody List<Spittle> spittles(
@RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
@RequestParam(value="count", defaultValue="20") int count){
return spittleRepository.findSpittles(max, count);
}
@ResponseBody注解会告知Spring,要将返回的对象作为资源发送给客户端,并将其转化为客户端可接受的表述。在这个例子中,消息转化器会将控制器返回的Spittle列表转化为JSON文档,并将其写入响应体。响应大概如下所示:
[
{
"id" : 01,
"name" : "name_1",
"message" : "Hello World"
},
{
"id" : 02,
"name" : "name_2",
"message" : "Test"
}
]
2.3 在请求体中接收资源状态
REST并不是只读的,它也可以接受来自客户端的表述。@RequestBody能告诉Spring查找一个消息转换器,将来自客户端的资源表述转换为对象。
例如,将客户端提交的新Spittle保存起来:
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
//只处理/spittles的POST请求
//consumes属性表明只接受Context-type头信息为"application/json"的请求
public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle){
return spittleRepository.save(spittle);
}
Spittle参数上加了@RequestBody注解,Spirng将会查看请求中的Context-type头信息,查找能够将请求体转换为Spittle的消息转换器。
本例中,如果Context-type的头信息是"application/json",DispatcherServlet会查找能够将JSON转换为Java对象的消息转换器。
@RequestMapping注解的consumes属性关注请求的Context-type头信息;produces属性关注请求的Accept头信息。
2.4 为控制器默认设置消息转换
Spring 4.0引入了@RestController注解,可以用来替代@Controller注解,Spring会为该控制器的所有方法应用消息转换功能,这样就不必每个方法都添加@ResponseBody注解了。(如果是接受资源的化,还是需要参数添加@RequestBody)
3. 提供资源之外的其它内容
除了将资源表述发给客户端之外,还应该能够给客户端提供额外的一些信息,帮助客户端理解资源或者在请求中出现了什么情况。
3.1 发送错误信息到客户端
例如,按照Id查询Spittle:
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public @ResponseBody Spittle spittleById(@PathVariable long id){
return spittleRepository.findOne(id);
}
消息处理器会产生客户端所需的资源表述,但是如果没有查询到id对应的spittle对象,findOne()方法返回值为null,那么响应体就会为空,同时响应中默认的HTTP状态码为200(OK)。
Spring提供了多种方式来处理这种情况:
- 使用@ResponseStatus注解可以指定状态码
- 控制器方法可以返回ResponseEntity对象,该对象能够包含更多响应相关的元数据
- 异常处理器能够应对错误场景,这样处理器方法就能关注于正常的状况
3.1.1 使用ResponseEntity
ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码)以及要转换成资源表述的对象。
当无法找到Spittle的时候可以返回HTTP 404 错误:
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id){
Spittle spittle = spittleRepository.findOne(id);
//如果spittle为null状态码设置404,如果不为null状态码设置200
HttpStatus status = spittle == null ? HttpStatus.NOT_FOUND : HttpStatus.OK;
return new ResponseEntity<Spittle>(spittle, status);
}
ResponseEntity包含了@ResponseBody的语义,所以没有必要添加@ResponseBody注解。
3.1.2 处理错误异常
可以使用异常处理器来处理错误,让常规处理器只关心正常的逻辑处理。
首先定义SpittleNotFoundException异常:
public class SpittleNotFoundException extends RuntimeException{
private Long spittleId;
public SpittleNotFoundException(Long id){
this.spittleId = id;
}
public long getSpittleId(){
return spittleId;
}
}
定义处理SpittleNotFoundException的异常处理器:
@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异常,就会调用spittleNotFound()方法来处理异常。
控制器可以更改为:
@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);
}
代码还能更加精简,因为设定了异常处理器,spittleById()方法返回的HTTP状态码始终会是200,那么就可以不再使用ResponseEntity,将其替换为@ResponseBody。如果控制器类上使用的是@RestController注解,那么也不需要@ResponseBody。
@RestController
@RequestMapping("/spittle")
public class Controller{
@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;
}
}
同样,对于异常处理器,因为返回值始终是404,也可以进行类似的清理:
@ExceptionHandler(SpittleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
publiic Error spittleNotFound(SpittleNotFoundException e){ //假定已经添加了@RestController注解,那么就不必写@ResponseBody注解
long spittleId = e.getSpittleId();
return new Error(4, "Spittle [" + spittleId + "] not found");
}
以上对代码的清理可以避免使用ResponseEntity,但是当需要设置响应头部信息时,ResponseEntity能够很好的完成,注解和异常处理器却做不到。
3.2 在响应中设置头部信息
在saveSpittle()方法,处理POST请求的过程中创建了一个新的Spittle资源。在方法处理完请求后,服务器在响应体中包含了Spittle的表述以及状态码200(OK),将其返回客户端。但是这并不是完全准确。
创建了新的内容,HTTP 201(Created)表述更为准确,而且应该将新资源的URL放在响应的Location头信息中返回给客户端。这时候ResponseEntity可以用来填充响应头部信息。
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)//返回状态码为201
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb){
Spittle spit = spittleRepository.save(spittle);
HttpHeaders headers = new HttpHeaders();
URI locationUri = ucb.path("/spittles") //拼接URL地址
.path(String.valueOf(spit.getId()))
.build()
.toUri();
headers.setLocation(locationUri);//设置location头信息
return new ResponseEntity<Spittle>(spittle,headers); //返回资源和头信息
}
UriComponentsBuilder会获取host、端口以及Servlet内容,基于这些信息,代码通过设置路径的方式构建UriComponents其余的部分。
4. 编写REST客户端
Spring提供了RestTemplate实现RESTful交互的客户端。
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头信息 |
optionsForAllow() | 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息 |
postForEntity() | POST数据到一个URL,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得到的 |
postForLocation() | POST数据到一个URL,返回新创建资源的URL |
postForObject() | POST数据到一个URL,返回根据响应体匹配形成的对象 |
put() | PUT资源到特定的URL |
大多数操作以三种方法的形式进行重载:
- 使用java.net.URI作为URL格式,不支持参数化URL;
- 使用String作为URL格式,并使用Map指明URL参数;
- 使用String作为URL格式,并使用可变参数列表指明URL参数;
4.1 GET资源
getForEntity()方法和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;
<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;`在这里插入代码片`
除了返回类型,getForEntity就是getForObject的镜像。它们都根据URL检索资源的GET请求。都将资源根据responseType参数匹配一定的类型。区别在于getForObject只返回请求的对象,getForEntity返回请求的对象和响应相关的信息。
4.1.1 getForObject()方法
请求一个资源并按照所选择的Java类型接收该资源(通过HTTP信息转换器):
public Profile fetchFacebookProfile(String id){
RestTemplate rest = new RestTemplate();
return rest.getForObject("http://graph.facebook.com/{spitter}", Profile.class, id);//要求结果是Profile对象
//URL中的占位符会使用id参数填充,每个参数按出现顺序插入到URL的占位符中
}
还可以将id放入map中:
public Profile fetchFacebookProfile(String id){
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("id", id);
RestTemplate rest = new RestTemplate();
return rest.getForObject("http://graph.facebook.com/{spitter}", Profile.class, urlVariables);
}
4.1.2 getForEntity()方法
假设除了获取资源,还想获取资源的最后修改时间。服务端在LastModified头部信息中提供了该信息:
public Spittle fetchSpittle(long id){
RestTemplate rest = new RestTemplate();
ResponseEntity<Spittle> response = rest.getForEntity("http://localhost:8080/spittles/{id}", Spittle.class, id);
Date lastModified = new Date(response.getHeaders().getLastModified());//getHeaders方法返回HttpHeaders对象,可以查询响应头信息
if(response.getStatusCode() == HttpStatus.NOT_MODIFIED){
throw new NotModifiedException();//服务器响应304状态,意味着从上一次请求后再也没有修改
}
return response.getBody();//返回资源对象
}
4.2 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;
例如,更新服务器上的spittle资源:
public void updateSpittle(Spitle spittle) throws SpittleException{
RestTemplate rest = new RestTemplate();
rest.put("http://localhost:8080/spittles/{id}", spittle, spittle.getId());
}
RestTemplate将使用某个HTTP消息转换器将Spittle对象转换为一种表述,并在请求体中发送给服务器端。如果类路径下包含的是Jackson 2库,那么MappingJacksonHttpMessageConverter信息转换器将以application/json格式将Spittle写入到请求中。
4.3 DELETE资源
delete()方法通过URI删除资源,它也有三个版本:
void delete(String url, Object... urlVariables) throws RestClientException;
void delete(String url, Map<String, ?> uriVariables) throws RestClientException;
void delete(URI url) throws RestClientException;
例如,删除指定ID的Spittle:
public void deleteSpittle(long id){
RestTemplate rest = new RestTemplate();
rest.delete("http://localhost:8080/spttles/{id}", id);
}
4.4 POST资源数据
POST有postForObject()、postForEntity()、postForLocation()三个方法,每个方法均有三种重载方式。
4.4.1 在POST请求中获取响应对象
例如,POST一个新的Spittle到服务器端,但此时服务器端并不知道它,它还没有URL。
一种方法是使用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;
第一个参数是资源要PUT到的URL,第二个参数是要发送的对象,第三个参数是预期返回的Java类型。第四个参数是指定的URL变量(可变参数列表或Map)。
调用时:
public Spittle postSpittleForObject(Spittle spittle){
RestTemplate rest = new RestTemplate();
return rest.put("http://localhost:8080/spittles", spittle, Spittle.class);
}
在响应中,它接收一个Spittle对象并将其返回给调用者。如果想得到请求带回来的元数据,可以使用postForEntity()方法,这个方法与postForObject()方法的签名几乎相同。
<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;
例如,除了要获取返回的Spittle资源,还要查看响应的Location头信息:
public ResponseEntity<Spittle> postSpittleForEntity(Spittle spittle){
RestTemplate rest = new RestTemplate();
ResponseEntity<Spittle> response = rest.postForEntity("http://localhost:8080/spittles", spittle, Spittle.class);
Spittle spittle = response.getBody();//得到资源对象
URI url = response.getHeaders().getLocation();//通过ResponseEntity获取一个HttpHeader,再获取头信息的数据
return response;
}
4.4.2 在POST请求后获取资源位置
如果只需要Location头信息的值,而不需要将资源发送回来,那么可以使用postForLocation()方法。
与其它两个post方法不同的是,postForLocation()响应是新创建资源的位置:
URI postForLocation(String url, Object request, Object... uriVariables) throws RestClientException;
URI postForLocation(String url, Object request, Map<String, ?> uriVariables) throws RestClientException;
URI postForLocation(String url, Object request) throws RestClientException;
示例:
public String postSpittle(Spittle spittle){
RestTemplate rest = new RestTemplate();
return rest.postForLocation("http://localhost:8080/spittles", spittle).toString();
}
4.3 交换资源
如果想在发送给服务器端的请求中设置头信息的话,可以使用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参数来表明要使用的HTTP动作。根据这个参数的值,exchange()能够执行其它RestTemplate方法一样的工作。
//例如,使用exchange()代替getForEntity()
public ResponseEnrity<Spittle> spittle(long id){
RestTemplate rest = new RestTemplate();
return rest.exchange("http://localhost:8080/spittles/{id}", HttpMethod.GET, null, Spittle.class, id);//将响应转换为spittle对象
}
第三个参数是用于在请求中发送资源的,因为这个GET请求,所以可以为null。
不指明头信息,exchange()对Spittle的GET请求会带有如下头信息:
GET /Spitter/spittles/12 HTTP/1.1
Accept: application/xml, text/xml, application/*+xml, application/json
Content-Length: 0
User-Agent: Java/1.6.0_20
Host: localhost:8080
Connection: keep-alive
exchange()方法可以传入带有头信息的HttpEntity,例如,将“application/json”设置为Accept的唯一值:
MultiVaueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Accept", "application/json");
HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers);
//之后调用 RestTemplate的 exchange时再将 requestEntity作为第三个参数传入即可