Spring

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

2018-07-18  本文已影响129人  阳光的技术小栈

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

Rest含义:

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

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

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

使用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提供了多种方式来处理这样的场景:

使用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();
上一篇下一篇

猜你喜欢

热点阅读