文章目录

  • 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作为第三个参数传入即可