Spring实战(第四版)- part4(Spring集成)

Spring实战(第四版)笔记 - part4 Spring集成

第15章 使用远程服务

第16章 使用Spring MVC创建REST API

  • 编写处理REST资源的控制器
  • 以XML、JSON及其他格式来表述资源
  • 使用REST资源数据为王

16.1 了解REST

16.1.1 REST基础知识
  1. REST就是将资源一最适合客户端或服务端的形式从服务器端转移到客户端(或反过来)。
  2. REST通过URL进行资源定位。他关注的是资源(事物、数据)而非行为(对应SOAP)。
  3. HTTP 中的方法映射了数据操作中的CRUD(但不是绝对的例如POST可以用来创建,也可以用来更新)。
16.1.2 Spring是如何支持REST的
  1. 控制器可以处理所有的HTTP方法;
  2. 通过@PathVariable可以处理参数化URL;
  3. 借助视图和视图解析器,资源可以被渲染成XML、JSON等多种实现;
  4. 可以使用ContentNegotiatingViewResolver 来选择最适合客户端的表述;
  5. 借助@ResponseBody注解以及HttpMethodConverter能够替换基于视图的渲染方式;
  6. 使用@RequestBody 注解以及 HttpMethodConverter 实现可以将传入的 HTTP 数据转化为传入控制器处理方法的 Java 对象;

16.2 创建第一个REST端点

  1. 表述是关于客户端和服务端针对某一资源如何通信的。
  2. 控制器不关心资源如何表述(资源在其中都是Java对象)。Spring有两种方法将资源的Java表述转换为发给客户端的表述:
    1. 内容协商:选择一个视图,将模型渲染为呈现给客户端的表述
    2. 消息转换器:通过一个消息转换器将控制器所返回的对象转换为呈现给客户端的表述。
16.2.1 协商资源表述
  1. 当视图返回给人类时,多数是渲染成HTML。根据视图名称匹配到视图即可。

  2. 当考虑到资源表述时,还需要考虑格式是否适合客户端。

  3. Spring 的 ContentNegotiatingViewResolver 是一个特殊的视图解析器,它考虑到了客户端所需要的内容类型

    @Bean
    public ViewResolver cnViewResolver() {
        return new ContentNegotingResolver();
    }
    
  4. 内容协商的步骤:1. 确定媒体类型 2. 找到适合请求媒体类型的最佳视图。

  5. 确定媒体类型的方式

    1. 看URL的文件扩展名
    2. 看Accept请求头信息
    3. 都没找到的话,会使用默认类型:“/” 让客户端强制接收。
  6. 视图解析: ContentNegotingResolver将视图解析委托给合适的viewResolver

    他会将所有的视图类型都解析放进一个列表,然后最后选择合适的。

  7. ContentNegotiationManager可以被设置来更改默认媒体类型选择的行为。

  8. ContentNegotiatingViewResolver在Spring MVC之上构建REST资源表述层,资源代码无需修改。当面向人类和非人类的接口有很多重合时,他的作用就比较大。

  9. 他的一个限制是他没办法觉得客户端发送来的媒体资源的格式类型。

16.2.2 使用HTTP信息转换器
  1. 直接将得到的数据转换为需要的格式。
  2. 不再像之前一样存入model,然后通过视图渲染。直接添加@ResponseBody,将返回转换为响应体。
  3. 类似的可以通过@RequestBody来搜寻适合的来自客户端的资源表述转换为对象。
  4. @RestController实现了默认的信息转换,适用于类中多个方法都需要配置的时候。

16.3 提供资源之外的其他内容

提供元数据给客户端,让客户端明白发生了什么。

16.3.1 发送错误信息到客户端
  1. Spring提供了多种方式处理错误响应

    • 使用@ResponseStatus可以制定状态码
    • 控制器方法可以返回ResponseEntity对象,该对象能够包含更多相应相关的元数据。
    • 异常处理器
  2. 使用ResponseEntity

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public Spittle spittleById(@PathViriable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        HttpStatus httpStatus = spittle != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
        return new ResponseEntity<Spittle>(spittle, status);
    }
    

    此时响应体在错误时候依然为空,可以自定义一个:

    public 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 int getMessage() {
        return message;
      }
    }
    

    将spittleById方法修改

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public Spittle spittleById(@PathViriable 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, status);
    }
    
  3. 代码看起来更复杂了。。。 使用错误处理器重构下

    @ExceptionHandler(SpittleNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public @ResponseBody Error spittleNotFound(SpittleNotFoundException e) {
      long spittleId = e.getSpittleId();
      return new Error(4, "Spittle [" + spittleId + "] not found");
    }
    

    如果在控制器的任意处理方法中抛出 SpittleNotFoundException 异常,就会调 用 spittleNotFound() 方法来处理异常。16.3.2 在响应中设置头部信息

    SpittleNotFoundException

    package spittr.data;
    
    public class SpittleNotFoundException extends RuntimeException {
    
      private static final long serialVersionUID = 1L;
      
      private long spittleId;
    
      public SpittleNotFoundException(long spittleId) {
        this.spittleId = spittleId;
      }
      
      public long getSpittleId() {
        return spittleId;
      }
      
    }
    

    spittleById() 方法

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public @ResponseBody Spittle spittleById(@PathVariable long id) {
      Spittle spittle = spittleRepository.findOne(id);
      if (spittle == null) { throw new SpittleNotFoundException(id); }
      return spittle;
    }
    

    spittle正常返回的话一定有值,所以就不需要使用ResponseEntity了。

16.3.2 在响应中设置头部信息
  1. 使用@ResponseStatus单单可以设置响应码

  2. 使用@ResponseEntity可以设置更多的元数据

  3. 比如返回新建的资源信息。在处理器方法所得到的 UriComponentsBuilder 中,会预先配置已知的信息如 host、端口以及 Servlet 内容。它会从处理器方法所对应的请求中获取这些基础信息。基于这些信息,代码会通过设置路径的方式构建 UriComponents 其余的部分。

    @RequestMapping(method=RequestMethod.POST, consumes="application/json")
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
      Spittle saved = spittleRepository.save(spittle);
        
      HttpHeaders headers = new HttpHeaders();
      URI locationUri = ucb.path("/spittles/")
          .path(String.valueOf(saved.getId()))
          .build()
          .toUri();
      headers.setLocation(locationUri);
        
      ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(saved, headers, HttpStatus.CREATED);
      return responseEntity;
    }
    

16.4 编写REST客户端

16.4.1 了解RestTemplate的操作
方法 描述
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

16.5 小结

第17章 Spring消息

  • 异步消息简介
  • 基于 JMS 的消息功能
  • 使用 Spring 和 AMQP 发送消息
  • 消息驱动的 POJO

17.1 异步消息简介

  1. 如果通信是同步的,客户端必须等待服务端的响应.
  2. 异步就是客户端发送消息之后不需要等待,继续执行其他任务。
17.1.1 发送消息
  1. 间接性是异步消息的关键。两个端异步通信并非直接通信,而是将消息交由中间方。

  2. 关键概念:消息代理目的地

    1. 消息代理:即中间方,用来接收异步消息/
    2. 目的地:此处目的地不是要发到哪里,而是发送方的地址。他将消息发到消息代理就不管了。后续会有其他人取走消息(他们通过目的地获取消息是谁发来的)。
  3. 点对点消息模型:当消息代理得到消息时,它将消息放入一个队列中。当接收者请求队列中的下一条消息时,消息会从队列中取出,并投递给接收者。因为消息投递后会从队列中删除,这样就可以保证消息只能投递给一个接收者。

    img
  4. 但是并没有约束只能有一个接受者,可以有多个接收者轮流获取消息,各自处理得到的那条消息。e.g. 此模型比较像银行处理业务窗口,(多个)业务员通过(一个)取号机叫号,每人每次接待一位顾客(消息)。

  5. 发布—订阅消息模型: 在发布—订阅消息模型中,消息会发送给一个主题。与队列类似,多个接收者都可以监听一个主题。但是,与队列不同的是,消息不再是只投递给一个接收者,而是主题的所有订阅者都会接收到此消息的副本。

    img
  6. 发布—订阅消息模型与杂志发行商和杂志订阅者很相似。杂志(消息)出版后,发送给邮局,然后所有的订阅者都会收到杂志的副本

17.2.2 异步消息的优点
  1. 同步通信的局限
    1. 等待。
    2. 服务接口和远程服务耦合。
    3. 客户端与远程服务的位置耦合。
    4. 客户端与服务的可用性相耦合。
  2. 异步消息解决了这些局限。
    1. 客户端发送完消息无需等待。
    2. 面向数据,而非远程方法
    3. 同步 RPC 服务通常需要网络地址来定位。这意味着客户端无法灵活地适应网络拓扑的改变。如果服务的 IP 地址改变了,或者服务被配置为监听其他端口,客户端必须进行相应的调整,否则无法访问服务。与之相反,消息客户端不必知道谁会处理它们的消息,或者服务的位置在哪里。客户端只需要了解需要通过哪个队列或主题来发送消息。
    4. 发送异步消息时,客户端完全可以相信消息会被投递。即使在消息发送时,服务无法使用,消息也会被存储起来,直到服务重新可以使用为止。

17.2 使用JMS发送消息

17.3 使用AMQP实现消息功能

17.4 小结

第18章 使用WebSocket和STOMP实现消息功能

第19章 使用Spring发送Email

第20章 使用JXM管理SpringBean

第21章 借助Spring Boot简化Spring开发

REFERENCE

https://potoyang.gitbook.io/spring-in-action-v4/untitled-10/untitled-2/untitled