使用Spring MVC创建REST API

来源:互联网 发布:支付宝钱包软件下载 编辑:程序博客网 时间:2024/06/10 03:21

近几年来,以信息为中心的表述性状态转移(Representational Statae Transfer,REST)已成为替换传统的SOAP Web服务的流行方案。SOAP一般会关注行为和处理,而REST关注的是要处理的数据。

1.了解REST

1.1 REST的基础知识

首先,REST与RPC几乎没有任何关系。RPC是面向服务的,并关注于行为和动作;而REST是面向资源的,强调描述应用程序的事物和名词。
将其首字母缩写拆分为不同的构成部分来理解:

  • 表述性(Representational):REST资源实际上可以用各种形式来进行表述,包括XML,JSON甚至HTML——最适合资源使用者的任意形式;
  • 状态(State):当使用REST的时候,我们更专注资源的状态而不是对资源采取的行为;
  • 转移(Transfer):REST涉及到转移资源数据,它以某种表述性形式从一个应用转移到另一个应用。

简洁地讲,REST就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来)。
在REST中,资源通过URL进行识别和定位。至于RESTful URL的结构并没有严格的规则,但是URL应该能够识别资源,而不是简单的发一条命令到服务器上。再次强调,关注的核心是事物,而不是行为。
REST中会有行为,它们通过HTTP方法来定义。这些HTTP方法通常会匹配为如下的CRUD动作:

  • Create: POST
  • Read: GET
  • Update: PUT或PATCH
  • Delete:DELETE

1.2 Spring是如何支持REST

当前的4.0版本中,Spring支持以下方式来创建REST资源:

  • 控制器可以处理所有的HTTP方法,包含四个主要的REST方法:GET、PUT、DELETE以及POST。Spring3.2及以上版本还支持PATCH方法;
  • 借助@PathVariable注解,控制器能够处理参数化的URL(将变量输入作为URL的一部分)
  • 借助Spring的视图和视图解析器,资源能够以多种形式进行表述,包括将模型数据渲染为XML、JSON、Atom以及RSS的View实现;
  • 可以使用ContentNegotiatingViewResolver来选择最适合客户端的表述;
  • 借助@ResposeBody注解和各种HttpMethodConverter实现,能够替换基于视图的渲染方式;
  • 类似的,@RequestBody注解以及HttpMethodConverter实现可以将传入的HTTP数据转化为传入控制器处理方法的Java对象;
  • 借助RestTemplate,Spring应用能够方便地使用REST资源。

- 创建第一个REST端点

表述是REST中很重要的一个方面。它是关于客户端和服务端针对某一资源是如何通信的。
控制器本身通常并不关心资源如何表述。控制器以Java对象的方式来处理资源。控制器完成了它的工作之后,资源才会被转化成最适合客户端的形式。
Spring提供了两种方法将资源的Java表述形式转换为发送给客户端的表述形式:

  • 内容协商(Content negotiation):选择一个视图,它能够将模型渲染为呈现给客户端的表述形式;
  • 消息转换器(Message conversion):通过一个消息转换器将控制器所返回的对象转换为呈现给客户端的表述形式。

2.1 协商资源表述

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我们所能做到的事情如下所示:

  • 指定默认的内容类型,如果根据请求无法得到内容类型的话,将会使用默认值
  • 通过请求参数指定内容类型
  • 忽视请求的Accept头部信息
  • 将请求的扩展名映射为特定的媒体类型
  • 将JAF(Java Activation Framework)作为根据扩展名查找媒体类型的备用方案

有三种配置ContentNegotiationManager的方法:

  • 直接声明一个ContentNegotiationManager类型的bean
  • 通过ContentNegotiationManagerFactoryBean间接创建bean
  • 重载WebMvcConfigurerAdapter的configureContentNegotiation()方法

第一种方法有一些复杂,除非有充分的原因,否则我们不会愿意这样做。后两种方案能够让创建ContentNegotiationManager更加简单。

通过XML使用ContentNegotiationManagerFactoryBean来创建和配置ContentNegotiationManager bean使用“application/json”作为默认的内容类型。

<bean 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的消息转换功能来生成资源表述。

2.2 使用HTTP信息转换器

消息转换(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<String,String>中,也会将MultiValueMap<String,String>写入到application/x-www-form-urlencoded中或将MultiValueMap<String,Object>写入到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<User> 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<User> list(){        return rESTService.listUser();    }    @RequestMapping(method=RequestMethod.POST,            consumes="application/json")    public User saveUser(@RequestBody User user){        return rESTService.saveUser(user);    }}

3.提供资源之外的其他内容

3.1 发送错误消息到客户端

当我们在业务处理的过程中,出现错误的时候,我们希望能把这个错误返回给客户端。比如,查找一个用户,当没有找到这个用户的时候,我们需要告知客户端,返回的状态也应该是404.

使用ResponseEntity
作为@ResponseBody的替代方案,控制器方法可以返回一个ResponseEntity对象。ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码)以及要转换成资源表述的对象。
示例:

@RequestMapping(value="myREST/{id}",            method=RequestMethod.GET,            consumes="application/json")    public ResponseEntity<User> userById(@PathVariable long id){        User user = rESTService.findone(id);        HttpStatus status = user != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;        return new ResponseEntity<User>(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>(error, HttpStatus.NOT_FOUND);        }        return new ResponseEntity<User>(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<HttpMessageConverter<?>> converters) {        converters.add(new MappingJackson2HttpMessageConverter());        super.configureMessageConverters(converters);    }

然后,使用MappingJackson2HttpMessageConverter的话,你需要在类路径下添加Jackson 2 JSON库,这里我用的maven:

    <dependency>        <groupId>com.fasterxml.jackson.core</groupId>        <artifactId>jackson-core</artifactId>        <version>2.8.8</version>    </dependency>    <dependency>          <groupId>com.fasterxml.jackson.core</groupId>          <artifactId>jackson-databind</artifactId>          <version>2.8.8</version>      </dependency>

OK 进入下一环节。

3.2 在响应中设置头部信息

假设我们有这样一个需求,当我们保存一个用户的时候,如果成功,返回201更能表达创建了一个资源,而且,我们可以把新创建的资源的URL放在响应的Location头部信息中,并返回个客户端,这是一种很好的方式。
为了完成这个任务,我们可以这样做:

    @RequestMapping(            value="save",            method=RequestMethod.POST,            consumes="application/json")    public ResponseEntity<User> 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<User> responseEntity = new ResponseEntity<User>(user, headers, HttpStatus.CREATED);        return responseEntity;    }

HttpHeaders 类是MultiValueMap<String, String>的特殊实现,他有一些便利的setter方法,用来设置常见的HTTP头部信息。
但是,上面的URL是通过字符串的形式指定的,这种硬编码使得应用的范围非常局限。
我们可以借助UriComponentsBuilder来构建URL。它是一个构建类,通过指定URL中的各种组成部分(如host,port,路径以及查询),我们能够使用它来构建UriComponents实例,借助这个实例,我们就能获得设置给Location的URL:

@RequestMapping(            value="save",            method=RequestMethod.POST,            consumes="application/json")    public ResponseEntity<User> 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<User> responseEntity = new ResponseEntity<User>(user, headers, HttpStatus.CREATED);        return responseEntity;    }

在处理器方法所得到的UriComponentsBuilder 中,会预先配置已知的信息如host、端口以及Servlet内容。它会从处理器方法所对应的请求中获取这些基础信息。基于这些信息,代码会通过设置路径的方式构建UriComponents实例。每次调用path()都会基于上次调用的结果。

下面就是我们接收到的响应头:
响应头信息

4. 编写REST客户端

了解RestTemplate的操作

RestTemplate定义了36个与REST资源交互的方法,其中的大多数都对应于HTTP的方法。其中,这里面只有11个独立的方法,其中有十个有三种重载形式,而第十一个则重载了六次。下表中的大多数方法都以三种方法的形式进行了重载:

  • 一个使用java.net.URI作为URL格式,不支持参数化URL
  • 一个使用String作为URL格式,并使用Map指明URL参数
  • 一个使用String作为URL格式,并使用可变参数列表指明URL参数
方法 描述 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

原创粉丝点击