Spring实战(十六)-使用Spring MVC创建REST API

本文基于《Spring实战(第4版)》所写。

Rest含义:

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

REST的动作(HTTP的方法)以及匹配的CRUD动作:

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

Spring支持以下方式来创建REST资源:

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

Spring提供了两种方法将资源的Java表述转换为发送给客户端的表述形式:

  • 内容协商(Content negotiation):选择一个视图,它能够将模型渲染为呈现给客户端表述形式。不过由于它只能决定资源该如何渲染到客户端,并没有涉及到客户端要发送什么样的表述给控制器使用,比如客户端发送JSON或XML,它就无法提供帮助了。而且还有其他限制,不建议使用。
  • 消息转换器(Message conversion):通过一个消息转换器将控制器所返回的对象转换为呈现给客户端的表述形式。

使用HTTP信息转换器

当使用消息转换功能时,DispatcherServlet不再将模型数据传送到视图中。实际上,根本就没有模型,也没有视图,只有控制器产生的数据,以及消息转换器转换数据之后所产生的资源表述。

Spring自带了各种各样的转换器,比如客户端通过请求的Accept头信息表明它能接受“application/json”,并且Jackson JSON在类路径下,那么处理方法返回的对象将交给MappingJacksonHttpMessageConverter,并由它转换为返回客户端的JSON表述形式。大部分转换器都是自动注册的,不需要Spring配置。但是为了支持它们,需要添加一些库到应用程序的类路径下。

如果使用了消息转换功能的话,我们需要告诉Spring跳过正常的模型/视图流程,并使用消息转换器。最简单的方式是为控制器方法添加@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,我们要将返回的对象作为资源发送给客户端,并将其转换为客户端可接受的表述形式。更具体地讲,DispatcherServlet将会考虑到请求中Accept头部信息,并查找能够为客户端提供所需表述形式的消息转换器(根据类路径下实现库)。

需要注意的是,默认情况下,Jackson JSON库在将返回的对象转换为JSON资源表述时,会使用反射。如果重构了Java类型,比如添加、移除或重命名属性,那么产生的JSON也将会发生变化。但是,我们可以在Java类型上使用Jackson的映射注解,改变产生JSON的行为。

谈及Accept头部信息,在@RequestMapping注解中,我们使用了produces属性表明这个方法只处理预期输出为JSON的请求,其他任何类型的请求,都不会被这个方法处理。这样的请求会被其他的方法来进行处理,或者返回客户端HTTP 406响应。

与@ResponseBody类似,@RequestBody也能告诉Spring查找一个消息转换器,将来自客户端的资源表述为对象。例如:

@RequestMapping(method=RequestMethod.POST, consumes="application/json")
public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle) {
    return spittleRepository.save(spittle);
}

通过使用注解,@RequestMapping表明它只能处理“/spittles”(在类级别的@RequestMapping中进行了声明)的POST请求。POST请求体中预期要包含一个Spittle的资源表述。因为Spittle参数上使用了@RequestBody,所以Spring将会查看请求中的Content-Type头部信息,并查找能够将请求转换为Spittle的消息转换器。

例如,如果客户端发送的Spittle数据是JSON表述形式,那么Content-Type头部信息可能就会是“application/json”。在这种情况下,DispatcherServlet会查找能够将JSON转换为Java对象的消息转换器。

注意,@RequestMapping有一个consumes属性,我们将其设置为“application/json”。consumes属性的工作方式类似于produces,不过它会关注请求的Content-Type头部信息。它会告诉Spring这个方法只会处理对“/spittles”的POST请求,并且要求请求的Content-Type头部信息为“application/json”。如果无法满足这些条件的话,会有其他方法来处理请求。

Spring 4.0引入了@RestController注解。如果在控制器类上使用@RestController来代替@Controller的话,Spring将会为该控制器的所有处理方法应用消息转换功能。我们不必为每个方法都添加@ResponseBody了。添加@RestController注解,此类中所有处理器方法都不需要使用@ResponseBody注解了,因为控制器使用了@RestController,所有它的方法所返回的对象将会通过消息转换机制,产生客户端所需的资源表述。

发送错误信息到客户端

如果一个处理器方法本应返回一个对象,但由于查找不到相应的对象而返回null。我们考虑一下在这种场景下应该发生什么。至少,状态码不应是200,而应该是404,告诉客户端它们所要求的内容没有找到。如果响应体中能够包含错误信息而不是空的话就更好了。

Spring提供了多种方式来处理这样的场景:

  • 使用@ResponseStatus注解可以指定状态码;
  • 控制器方法可以返回ResponseEntity对象,该对象能够包含更多响应相关的元数据;
  • 异常处理器能够应对错误场景,这样处理器方法就能关注于正常的状况。

使用ResponseEntity

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

@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注解了。

如果我们希望在响应体中包含一些错误信息。我们需要定义一个包含错误信息的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);
}

处理错误

我们重构一下代码来使用错误处理器。首先,定义能够对象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,它是一个很简单异常类:

public class SpittleNotFoundException extends RuntimeException {
    private long spittleId;
    public SpittleNotFoundException(long spittleId) {
        this.spittleId = spittleId;
    }

    public long getSpittleId() {
        return spittleId;
    }
}

现在,我们可以移除掉spittleById() 方法中大多数的错误代码:

@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状态码为404,那么现在我们可以对spittleNotFound() 方法进行类似的清理(控制器类上使用@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");
}

在响应中设置头部信息

如果我们需要在POST请求后,返回201且把资源的URL返回给客户端,可以用@ResponseEntity实现

@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;
}

其实我们没有必要手动构建URL,Spring 提供了UriComponentsBuilder。它是一个构建类,通过逐步指定URL中的各种组成部分(如host、端口、路径以及查询),我们能够使用它来构建UriComponents实例。

为了使用UriComponentsBuilder,我们需要做的就是在处理器方法中将其作为一个参数,如下面的程序清单所示。

@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中,会预先配置已知的信息如host、断端口以及Servlet内容。

注意,路径的构建分为两步。第一步调用path()方法,将其设置“/spittles/”,也就是这个控制器所能处理的基础路径。然后,在第二次调用path()的时候,使用了已使用Spittle的ID。在路径设置完成之后,调用build()方法来构建UriComponents对象,根据这个对象调用toUri()就能得到新创建Spittle的URI。

了解RestTemplate的操作

RestTemplate可以减少我们使用HttpClient创建客户端所带来的样板式代码。它定义了36个(只有11个独立方法,其他都是重载这些方法)与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

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;   

抽取响应的元数据

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资源数据

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;

在所有情况下,第一个参数都是资源要POST的URL,第二个参数是要发送的对象,而第三个参数是预期返回的Java类型。在将URL作为String类型的两个版本中,第四个参数指定了URL变量(要么是可变参数列表,要么是一个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();
}

交换资源

如果想在发送给服务端的请求中设置头信息的话,那就是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参数来表明要使用的HTTP动作。根据这个参数的值,exchange()能够执行与其他RestTemplate方法一样的工作。

例如,从服务器端获取Spitter资源的一种方式是使用RestTemplate的getForEntity()方法,如下所示:

ResponseEntity<Spitter> response = rest.getForEntity(
      "http://localhost:8080/spittr-api/spitters/{spitter}",
      Spitter.class, spitterId);
Spitter spitter = response.getBody();

在下面的代码片段中,可以看到exchange() 也可以完成这项任务:

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”设置为Accept头信息的唯一值。

设置请求头信息是很简单的,只需要构造发送给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设置在请求体中发送的对象—对于GET请求来说,这是没有必要的。

现在我们可以传入HttpEntity来调用exchange();

ResponseEntity<Spitter> response = rest.exchange(
      "http://localhost:8080/spittr-api/spitters/{spitter}",
      HttpMethod.GET, headers ,Spitter.class, spitterId);
Spitter spitter = response.getBody();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容