近几年来,以信息为中心的表述性状态转移(Representational Statae Transfer,REST)已成为替换传统的SOAP Web服务的流行方案。SOAP一般会关注行为和处理,而REST关注的是要处理的数据。
首先,REST与RPC几乎没有任何关系。RPC是面向服务的,并关注于行为和动作;而REST是面向资源的,强调描述应用程序的事物和名词。
将其首字母缩写拆分为不同的构成部分来理解:
简洁地讲,REST就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来)。
在REST中,资源通过URL进行识别和定位。至于RESTful URL的结构并没有严格的规则,但是URL应该能够识别资源,而不是简单的发一条命令到服务器上。再次强调,关注的核心是事物,而不是行为。
REST中会有行为,它们通过HTTP方法来定义。这些HTTP方法通常会匹配为如下的CRUD动作:
当前的4.0版本中,Spring支持以下方式来创建REST资源:
表述是REST中很重要的一个方面。它是关于客户端和服务端针对某一资源是如何通信的。
控制器本身通常并不关心资源如何表述。控制器以Java对象的方式来处理资源。控制器完成了它的工作之后,资源才会被转化成最适合客户端的形式。
Spring提供了两种方法将资源的Java表述形式转换为发送给客户端的表述形式:
Spring的ContentNegotiatingViewResolver是一个特殊的视图解析器,他考虑到了客户端所需要的内容类型。
要理解ContentNegotiatingViewResolver是如何工作的,这涉及内容协商的两个步骤:
确定请求的媒体类型
ContentNegotiatingViewResolver将会考虑到请求的Accept头部信息并使用它所请求的媒体类型,但是它会首先查看URL的文件扩展名。如果URL在结尾处有文件扩展名的话,ContentNegotiatingViewResolver将会基于该扩展名确定所需的类型。如果扩展名是“.json”的话,那么所需的内容类型必须是“application/json”。如果扩展名是“.xml”,那么哭护短请求的就是“application/xml”。当然,“.html”扩展名表明客户端所需的资源表述为HTML(text/html)。
如果根据文件扩展名不能得到任何媒体类型的话,那就会考虑请求中的Accept头部信息。在这种情况下,Accept头部信息中的值就表明了客户端想要的MIME类型,没有必要再去查找了。
最后,如果没有Accept头部信息,并且扩展名也无法提供帮助的话,ContentNegotiatingViewResolver将会使用“/”作为默认的内容类型,这就意味着客户端必须要接受服务器发送的任何形式的表述。
一旦内容类型确定之后,ContentNegotiatingViewResolver就该将逻辑视图名解析为渲染模型的View。与Spring的其他视图解析器不同,ContentNegotiatingViewResolver本身不会解析视图。而是委托给其他的视图解析器,让他们来解析视图。
ContentNegotiatingViewResolver要求其他的视图解析器将逻辑视图名解析为视图。解析得到的每个视图都会放到一个列表中。这个列表装配完成后,ContentNegotiatingViewResolver会虚幻客户端请求的所有媒体类型,在候选的视图中查找能够产生对应内容类型的视图。第一个匹配的视图会用来渲染模型。
影响媒体类型的选择
上面,我们说到了确定所请求媒体类型的默认策略。但是通过为其设置一个ContentNegotiationManager,我们能够改变它的行为。借助ContentNegotiationManager我们所能做到的事情如下所示:
有三种配置ContentNegotiationManager的方法:
第一种方法有一些复杂,除非有充分的原因,否则我们不会愿意这样做。后两种方案能够让创建ContentNegotiationManager更加简单。
通过XML使用ContentNegotiationManagerFactoryBean来创建和配置ContentNegotiationManager bean使用“application/json”作为默认的内容类型。
id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"
p:defaultContentType="applicatin/json">
ContentNegotiationManagerFactoryBean是FactoryBean的实现,所以他会创建一个ContentNegotiationManager bean。
使用Java配置,重写WebMvcConfigurerAdapter的configureContentNegotiation():
public class WebConfig extends WebMvcConfigurerAdapter{
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
现在我们已经有了ContentNegotiationManager bean接下来把它注入到ContentNegotiatingViewResolver中即可。
@Bean
@Autowired
public ViewResolver cnViewResolver(ContentNegotiationManager contentNegotiationManager){
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
viewResolver.setContentNegotiationManager(contentNegotiationManager);
return viewResolver;
}
下面是一个配置样例,当我们使用ContentNegotiatingViewResolver 的时候,通常会采用这种做法:默认会使用HTML视图,但是对特定的视图名称将会渲染为JSON输出。
public class WebConfig extends WebMvcConfigurerAdapter{
@Bean
@Autowired
public ViewResolver cnViewResolver(ContentNegotiationManager cnm){
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
viewResolver.setContentNegotiationManager(cnm);
return viewResolver;
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.TEXT_HTML);//默认为HTML
}
@Bean
public ViewResolver beanNameViewResolver(){
return new BeanNameViewResolver();//以bean的形式查找视图
}
@Bean
public View restTest(){
return new MappingJackson2JsonView();//将“restTest”定义为JSON视图
}
}
ContentNegotiatingViewResolver 最大的优势在于,它在Spring MVC之上构建了REST资源表述层,控制器代码无需修改。
但ContentNegotiatingViewResolver 有一些弊端,通常我们更加倾向于使用Spring的消息转换功能来生成资源表述。
消息转换(message conversion)提供了一种更为直接的方式,它能够将控制器产生的数据转换为服务于客户端的表述形式。
Spring提供了多个HTTP信息转换器,用于实现资源表述与各种Java类型之间的互相转换
信息转换器 | 描述 |
---|---|
AtomFeedHttpMessageConverter | Rome Feed对象和Atom feed(媒体类型application/atom+xml)之间的互相转换。入股Rome包在类路径下将会进行注册 |
BufferedImageHttpMessageConverter | BufferedImages与图片二进制数据之间互相转换 |
ByteArrayHttpMessageConverter | 读取/写入字节数组。从所有媒体类型(*/* )中读取,并以application/octet-stream格式写入 |
FormHttpMessageConverter | 将application/x-www-form-urlencoded内容读入到MultiValueMap 中,也会将MultiValueMap 写入到application/x-www-form-urlencoded中或将MultiValueMap 写入到multipart/form-data中 |
Jaxb2RootElementHttpMessageConverter | 在XML(text/xml或application/xml)和使用JAXB2注解的对象间互相读取和写入。如果JAXB v2库在类路径下,将进行注册 |
MappingJackson2HttpMessageConverter | 在JSON和类型化的对象或非类型化的HashMap间互相读取和写入。如果Jackson 2 JSON库字类路径下,将进行注册 |
MarshallingHttpMessageConverter | 使用注入的编排器和解排器(marshaller和unmarshaller)来读取和写入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的扩展,使用SourceHttpMessageConverter来支持基于XML的部分 |
表中的HTTP信息转换器除了其中的五个以外都是自动注册的,所以要使用它们的话,不需要Spring配置。但是为了支持它们,需要添加一些库到应用程序的类路径下。
为了支持消息转换,我们需要对Spring MVC的编程模型进行一些小调整。
在响应体中返回资源状态
正常情况下,当处理方法返回Java对象(除String外或View的实现以外)时,这个对象会放在模型中并在视图中渲染使用。但是,如果使用了消息转换功能的话,我们需要高数Spring跳过正常额模型/视图流程,并使用消息转换器。最简单的方法是为控制器方法添加@ResponseBody注解:
@Controller
@RequestMapping("myREST")
public class RESTController {
@Autowired
private RESTService rESTService;
@RequestMapping(method=RequestMethod.GET,
produces="application/json")//produces属性表明这个方法只处理预期输出为JSON的请求,也就是说只处理Accept头部信息包含“application/json”的请求
public @ResponseBody List list(){
return rESTService.listUser();
}
}
@ResponseBody 注解会告知Spring,我们要将返回的对象作为资源发送给客户端,并将其转换为客户端可接受的表述形式。更具体地讲,DispatcherServlet将会考虑到请求中Accept头部信息,并查找能够为客户端提供所需表述形式的消息转换器。
例如,如果客户端的Accept头部信息表明它接受“application/json”,并且Jackson JSON或Jackson 2 JSON库位于应用的类路径下,那么将会选择MappingJacksonHttpMessageConverter或MappingJackson2HttpMessageConverter。消息转换器会将控制器中返回的User列表转换为JSON文档,并将其写入到响应体中。
在请求体重接受资源状态
@RequestBody告诉Spring查找一个消息转换器,将来自客户端的资源表述转换为对象。
@RequestMapping(method=RequestMethod.POST,
consumes="application/json")//consumes属性表明这个方法只处理Content-Type头信息为“application/json”的请求
public @ResponseBody
User saveUser(@RequestBody User user){
return rESTService.saveUser(user);
}
上面,在User参数上使用了@RequestBody,所以Spring将会查看请求中的Content-Type头部信息,并查找能够将请求体转换为User的消息转换器。
例如,如果客户端发送的User数据是JSON表述形式,那么Content-Type头部信息可能就会是“application/json”。在这种情况下,DispatcherServlet会查找能够将JSON转换为Java对象的消息转换器。如果Jackson 2 库在类路径中,那么MappingJackson2HttpMessageConverter将会担此重任,将JSON表述转换为User,然后传递到saveUser()方法中。
为控制器默认设置消息转换
Spring4.0 引入了@RestController注解。如果在控制器上使用@RestController来代替@Controller注解的话,Spring将会为该控制器的所有处理方法应用消息转换功能,我们就不必为每个方法都添加@ResponseBody了。
@RestController
@RequestMapping("myREST")
public class RESTController {
@Autowired
private RESTService rESTService;
@RequestMapping(method=RequestMethod.GET,
produces="application/json")
public List list(){
return rESTService.listUser();
}
@RequestMapping(method=RequestMethod.POST,
consumes="application/json")
public User saveUser(@RequestBody User user){
return rESTService.saveUser(user);
}
}
当我们在业务处理的过程中,出现错误的时候,我们希望能把这个错误返回给客户端。比如,查找一个用户,当没有找到这个用户的时候,我们需要告知客户端,返回的状态也应该是404.
使用ResponseEntity
作为@ResponseBody的替代方案,控制器方法可以返回一个ResponseEntity对象。ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码)以及要转换成资源表述的对象。
示例:
@RequestMapping(value="myREST/{id}",
method=RequestMethod.GET,
consumes="application/json")
public ResponseEntity userById(@PathVariable long id){
User user = rESTService.findone(id);
HttpStatus status = user != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
return new ResponseEntity(user, status);
}
除了包含响应头信息、状态码以及负载以外,ResponseEntity还包含了@ResponseBody的语义,因此负载部分将会渲染到响应体中,就像之前在方法上使用@ResponseBody注解一样。
我们还可以返回一个专门的Error给客户端:
Error类:
public class Error {
private int code;
private String message;
public Error(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
@RequestMapping(value="myREST/{id}",
method=RequestMethod.GET,
consumes="application/json")
public ResponseEntity> userById(@PathVariable long id){
User user = rESTService.findone(id);
if(user == null){
Error error = new Error(4, "User ["+id+"] not found");
return new ResponseEntity(error, HttpStatus.NOT_FOUND);
}
return new ResponseEntity(user, HttpStatus.OK);
}
处理错误
ResponseEntity所使用的泛型为它解析或者出现错误留下了太多的空间。下面我们借助错误处理器来完善上面的代码。
首先,定义一个对应UserNotFoundException的错误处理器:
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public @ResponseBody Error userNotFound(UserNotFoundException e){
long userId = e.getUserId();
return new Error(4, "User ["+userId+"] not found");
}
UserNotFoundException是一个很简单的异常类:
public class UserNotFoundException extends RuntimeException{
private static final long serialVersionUID = 7420163893317582570L;
private long userId;
public UserNotFoundException(long userId) {
this.userId = userId;
}
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
}
做好这些以后,我们的userById()方法只要像下面这样就可以了:
@RequestMapping(value="myREST/{id}",
method=RequestMethod.GET,
consumes="application/json")
public @ResponseBody User userById(@PathVariable long id){
User user = rESTService.findone(id);
if(user == null){
throw new UserNotFoundException(id);
}
return user;
}
很简洁,有木有^_^
哦,还有一点,用@ResponseBody或者ResponseEntity的时候,记得添加convert类,比如用来对JSON和对象之间进行转化,那么你的WebConfig里面重写的configureMessageConverters方法(WebMvcConfigurerAdapter类的):
@Override
public void configureMessageConverters(List> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
super.configureMessageConverters(converters);
}
然后,使用MappingJackson2HttpMessageConverter的话,你需要在类路径下添加Jackson 2 JSON库,这里我用的maven:
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-coreartifactId>
<version>2.8.8version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.8.8version>
dependency>
OK 进入下一环节。
假设我们有这样一个需求,当我们保存一个用户的时候,如果成功,返回201更能表达创建了一个资源,而且,我们可以把新创建的资源的URL放在响应的Location头部信息中,并返回个客户端,这是一种很好的方式。
为了完成这个任务,我们可以这样做:
@RequestMapping(
value="save",
method=RequestMethod.POST,
consumes="application/json")
public ResponseEntity saveUser(
@RequestBody User user){
User u = rESTService.saveUser(user);
HttpHeaders headers = new HttpHeaders();
URI locationUri = URI.create("http://localhost:8080/myREST/"+u.getName());
headers.setLocation(locationUri);
ResponseEntity responseEntity = new ResponseEntity(user, headers, HttpStatus.CREATED);
return responseEntity;
}
HttpHeaders 类是MultiValueMap
的特殊实现,他有一些便利的setter方法,用来设置常见的HTTP头部信息。
但是,上面的URL是通过字符串的形式指定的,这种硬编码使得应用的范围非常局限。
我们可以借助UriComponentsBuilder来构建URL。它是一个构建类,通过指定URL中的各种组成部分(如host,port,路径以及查询),我们能够使用它来构建UriComponents实例,借助这个实例,我们就能获得设置给Location的URL:
@RequestMapping(
value="save",
method=RequestMethod.POST,
consumes="application/json")
public ResponseEntity saveUser(
@RequestBody User user,UriComponentsBuilder ucb){
User u = rESTService.saveUser(user);
HttpHeaders headers = new HttpHeaders();
URI locationUri = ucb.path("/myREST/")
.path(u.getName())
.build()
.toUri();
headers.setLocation(locationUri);
ResponseEntity responseEntity = new ResponseEntity(user, headers, HttpStatus.CREATED);
return responseEntity;
}
在处理器方法所得到的UriComponentsBuilder 中,会预先配置已知的信息如host、端口以及Servlet内容。它会从处理器方法所对应的请求中获取这些基础信息。基于这些信息,代码会通过设置路径的方式构建UriComponents实例。每次调用path()都会基于上次调用的结果。
RestTemplate定义了36个与REST资源交互的方法,其中的大多数都对应于HTTP的方法。其中,这里面只有11个独立的方法,其中有十个有三种重载形式,而第十一个则重载了六次。下表中的大多数方法都以三种方法的形式进行了重载:
方法 | 描述 |
---|---|
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 |
本章Demo:http://download.csdn.net/download/csdn_xuexiaoqiang/9869714